postgresql
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePostgreSQL
PostgreSQL
Data Type Defaults
数据类型默认值
| Need | Use | Avoid |
|---|---|---|
| Primary key | | |
| Timestamps | | |
| Text | | |
| Money | | |
| Boolean | | nullable booleans |
| JSON | | |
| UUID | | |
| IP addresses | | text |
| Ranges | | pair of columns |
| 需求 | 推荐使用 | 避免使用 |
|---|---|---|
| 主键 | | |
| 时间戳 | | |
| 文本 | | |
| 金额 | | |
| 布尔值 | 带 | 可空布尔值 |
| JSON 数据 | | |
| UUID | | |
| IP 地址 | | 文本格式 |
| 范围类型 | | 用两个列表示范围 |
Schema Rules
模式设计规则
- Every FK column gets an index (PG does NOT auto-create these)
- on every column unless NULL has business meaning
NOT NULL - constraints for domain rules at DB level
CHECK - constraints for range overlaps:
EXCLUDEEXCLUDE USING gist (room WITH =, during WITH &&) - Default
created_at TIMESTAMPTZ NOT NULL DEFAULT now() - Separate with trigger, never trust app layer alone. Gate it with
updated_atso a no-op write neither fires the function nor bumps the timestamp -- onWHEN (OLD.* IS DISTINCT FROM NEW.*)the row image is built before the trigger runs, so the comparison sees the caller's row, not the one the trigger is about to stamp.BEFORE UPDATE - Use PKs -- cheaper JOINs than UUID, better index locality
BIGINT - Safe migrations: , add columns with a non-volatile
CREATE INDEX CONCURRENTLY(instant add). NeverDEFAULTon large tables in-place.ALTER TYPE - A whose expression is
DEFAULTrewrites the entire table underVOLATILE; onlyACCESS EXCLUSIVE/IMMUTABLEdefaults get the metadata-only fast path. Check before shipping the migration:STABLE--SELECT provolatile FROM pg_proc WHERE proname = 'gen_random_uuid';is volatile,v/sare not. SoiandDEFAULT 7are instant,DEFAULT now()is a full rewrite; add the column nullable, backfill in batches, then set the default.DEFAULT gen_random_uuid() - on unique indexes (PG15+) -- treats NULLs as equal for uniqueness
NULLS NOT DISTINCT - Under , a pre-flight duplicate check written with SQL
NULLS NOT DISTINCTmisses NULL/NULL collisions -- the index rejects the second row, but=evaluates to NULL (not true), so a self-join orNULL = NULLprobe silently skips exactly the pairs the index will reject. Write the probe withWHERE a.col = b.colso NULL/NULL compares as equal.IS NOT DISTINCT FROM - Revoke default public schema access:
REVOKE ALL ON SCHEMA public FROM public
- 每个外键(FK)列都需要创建索引(PostgreSQL 不会自动创建)
- 除非 NULL 具有明确业务含义,否则所有列都需设置
NOT NULL - 在数据库层面通过 约束实现领域规则
CHECK - 用 约束防止范围重叠:
EXCLUDEEXCLUDE USING gist (room WITH =, during WITH &&) - 默认添加
created_at TIMESTAMPTZ NOT NULL DEFAULT now() - 单独设置 字段并通过触发器维护,绝不单独依赖应用层。触发器需添加
updated_at条件,确保无变更的写入不会触发函数或更新时间戳——在WHEN (OLD.* IS DISTINCT FROM NEW.*)阶段,行镜像在触发器执行前已构建,因此比较操作会读取调用者提交的行,而非触发器即将标记的行。BEFORE UPDATE - 使用 作为主键——比 UUID 更高效的 JOIN 操作,更好的索引局部性
BIGINT - 安全迁移:使用 ,添加列时使用非易失性的
CREATE INDEX CONCURRENTLY(可即时添加)。绝不在大型表上直接执行DEFAULT。ALTER TYPE - 表达式为 的
VOLATILE会在DEFAULT锁下重写整个表;只有ACCESS EXCLUSIVE/IMMUTABLE类型的默认值才能使用仅修改元数据的快速路径。迁移前需检查:STABLE——SELECT provolatile FROM pg_proc WHERE proname = 'gen_random_uuid';表示易失性,v/s表示非易失性。因此i和DEFAULT 7可即时完成,DEFAULT now()会触发全表重写;应先添加可空列,批量回填数据,再设置默认值。DEFAULT gen_random_uuid() - 在唯一索引上使用 (PG15+)——将 NULL 视为相等值以保证唯一性
NULLS NOT DISTINCT - 在 规则下,用 SQL
NULLS NOT DISTINCT编写的前置重复检查会遗漏 NULL/NULL 的冲突——索引会拒绝第二行,但=结果为 NULL(而非 true),因此自连接或NULL = NULL的查询会跳过索引拒绝的配对。需使用WHERE a.col = b.col编写查询,使 NULL/NULL 被视为相等。IS NOT DISTINCT FROM - 撤销 public 模式的默认访问权限:
REVOKE ALL ON SCHEMA public FROM public
Migration Safety
迁移安全规范
Core rules:
- Every schema change is a migration. No ad-hoc DDL in production.
- Migrations are immutable once deployed -- never edit a migration that has run in any shared environment.
- Schema migrations and data migrations are separate files. Schema changes are fast and transactional; data backfills are slow and may need batching.
- Forward-only in production. Rollback = a new forward migration that reverses the change.
Expand-contract pattern for zero-downtime renames and removals:
- Expand: add the new column/table, backfill data, update writes to populate both old and new
- Migrate: switch reads to the new column/table, verify in production
- Contract: remove the old column/table in a later deploy
Never rename or remove a column in a single migration -- callers reading the old name will break between deploy and code rollout.
Dangerous operations:
- without a
NOT NULLon an existing table locks and rewrites every row. Add the column nullable first, backfill, then add the constraint.DEFAULT - (without
CREATE INDEX) locks writes for the duration. Always useCONCURRENTLY, which cannot run inside a transaction block -- keep it in its own migration.CONCURRENTLY - Large data backfills: batch with to avoid locking the entire table:
FOR UPDATE SKIP LOCKED
sql
UPDATE target SET new_col = compute(old_col)
WHERE id IN (
SELECT id FROM target
WHERE new_col IS NULL
LIMIT 1000
FOR UPDATE SKIP LOCKED
);Run in a loop until zero rows affected.
Full-replace clobber on read-modify-write loops. A migration that loops silently drops concurrent writes that landed between SELECT and UPDATE. Any column written by live traffic is exposed: documents, comma-separated tag fields, denormalized counters, JSON-encoded attribute blobs. Mitigations, in order of preference:
SELECT col → mutate in app → UPDATE SET col = new_full_value WHERE id = ?jsonb- In-place atomic update when the edit is expressible as SQL: , or
UPDATE t SET col = jsonb_set(col, '{path}', :value) WHERE ...— no read-modify-write window.UPDATE t SET tags = array_append(tags, :tag) WHERE ... - Row-level lock during the loop: wrap each iteration in a transaction, , then mutate and write. Cheaper to author, accepts more lock contention.
SELECT ... WHERE id = ? FOR UPDATE - Compare-and-swap retry: include the original snapshot in , check the affected-row count; on 0, re-read and retry. Robust under contention, requires explicit retry-loop handling.
WHERE col = :original_value
Default chunked decode-encode loops are only safe during a maintenance window with writes blocked. ORM "chunkById + load + mutate + save" patterns hit this same trap.
核心规则:
- 所有模式变更都需通过迁移完成,禁止在生产环境执行临时 DDL
- 迁移脚本一旦部署即不可修改——绝不要编辑已在任何共享环境中执行过的迁移脚本
- 模式迁移和数据迁移需分开存储为独立文件。模式变更快速且支持事务;数据回填速度慢,可能需要分批处理
- 生产环境仅支持正向迁移。回滚操作需通过新的正向迁移脚本实现逆向变更
零停机重命名与删除的扩展-收缩模式:
- 扩展:添加新列/表,回填数据,更新写入逻辑以同时填充新旧字段
- 迁移:将读取逻辑切换到新列/表,在生产环境验证
- 收缩:在后续部署中删除旧列/表
绝不要在单个迁移脚本中重命名或删除列——在部署完成到代码更新完成的间隙,调用者读取旧字段会导致报错。
危险操作:
- 在已有表上添加无 的
DEFAULT约束会锁定并重写每一行。应先添加可空列,回填数据,再添加约束。NOT NULL - (不带
CREATE INDEX)会在执行期间锁定写入操作。始终使用CONCURRENTLY,且该命令不能在事务块内执行——需单独放在一个迁移脚本中。CONCURRENTLY - 大型数据回填:使用 分批处理,避免锁定整个表:
FOR UPDATE SKIP LOCKED
sql
UPDATE target SET new_col = compute(old_col)
WHERE id IN (
SELECT id FROM target
WHERE new_col IS NULL
LIMIT 1000
FOR UPDATE SKIP LOCKED
);循环执行直到影响行数为 0。
读-改-写循环中的全量覆盖问题。若迁移脚本采用 的循环逻辑,会静默丢失在 SELECT 和 UPDATE 之间提交的并发写入。任何被实时流量写入的列都会受影响: 文档、逗号分隔的标签字段、反规范化计数器、JSON 编码的属性 blob。缓解方案按优先级排序:
SELECT col → 在应用中修改 → UPDATE SET col = new_full_value WHERE id = ?jsonb- 原地原子更新:若修改逻辑可通过 SQL 表达,直接使用 SQL 更新:,或
UPDATE t SET col = jsonb_set(col, '{path}', :value) WHERE ...——无需读-改-写窗口。UPDATE t SET tags = array_append(tags, :tag) WHERE ... - 循环期间的行级锁:将每次迭代包裹在事务中,先执行 ,再修改并写入。实现成本更低,但会增加锁竞争。
SELECT ... WHERE id = ? FOR UPDATE - 比较-交换重试:在 子句中包含原始快照
WHERE,检查影响行数;若为 0,则重新读取并重试。在高并发场景下更可靠,但需显式处理重试逻辑。WHERE col = :original_value
默认的分块解码-编码循环仅在维护窗口(禁止写入)期间安全。ORM 的“chunkById + 加载 + 修改 + 保存”模式也会遇到同样问题。
Index Strategy
索引策略
| Type | Use When |
|---|---|
| B-tree (default) | Equality, range, sorting, |
| GIN | JSONB ( |
| GiST | Geometry, ranges, full-text (smaller but slower than GIN) |
| BRIN | Large tables with natural ordering (timestamps, serial IDs) |
Index rules:
- Composite: most selective column first, max 3-4 columns
- Partial: -- smaller, faster
WHERE status = 'active' - Covering: -- avoids heap lookup
INCLUDE (col) - Expression: -- for function-based WHERE
ON (lower(email)) - A GIN index on an array column serves the containment operators, not :
=seq-scans even withWHERE 'x' = ANY(col), becauseenable_seqscan = offover an array expands to equality and no GIN operator class implements it. Write the predicate asANYto reach the index.WHERE col @> ARRAY['x'] - on write-heavy tables -- reserves space for HOT updates, reducing index bloat
fillfactor = 70-90 - Drop unused indexes (only after one full business cycle since last restart -- check first, otherwise you may drop a primary key on a freshly restarted DB or read replica):
pg_stat_database.stats_resetSELECT * FROM pg_stat_user_indexes WHERE idx_scan = 0
Detect unindexed foreign keys:
sql
SELECT conrelid::regclass, a.attname
FROM pg_constraint c
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
WHERE c.contype = 'f'
AND NOT EXISTS (
SELECT 1 FROM pg_index i
WHERE i.indrelid = c.conrelid AND a.attnum = ANY(i.indkey)
);| 类型 | 使用场景 |
|---|---|
| B-tree(默认) | 等值查询、范围查询、排序、 |
| GIN | JSONB( |
| GiST | 几何数据、范围类型、全文检索(比 GIN 占用空间小但速度慢) |
| BRIN | 具有自然排序的大型表(时间戳、自增 ID) |
索引规则:
- 复合索引:选择性最高的列放在最前面,最多包含 3-4 列
- 部分索引:——占用空间更小,速度更快
WHERE status = 'active' - 覆盖索引:——避免堆表查询
INCLUDE (col) - 表达式索引:——用于基于函数的 WHERE 查询
ON (lower(email)) - 数组列上的 GIN 索引支持包含操作符,但不支持 :
=即使设置WHERE 'x' = ANY(col)仍会执行顺序扫描,因为数组上的enable_seqscan = off会展开为等值查询,而 GIN 操作符类未实现该逻辑。需将条件改写为ANY以命中索引。WHERE col @> ARRAY['x'] - 写入频繁的表设置 ——为 HOT 更新预留空间,减少索引膨胀
fillfactor = 70-90 - 删除未使用的索引(需在数据库重启后经过一个完整业务周期再操作——先检查 ,否则可能误删刚重启的主库或只读副本上的主键):
pg_stat_database.stats_resetSELECT * FROM pg_stat_user_indexes WHERE idx_scan = 0
检测未索引的外键:
sql
SELECT conrelid::regclass, a.attname
FROM pg_constraint c
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
WHERE c.contype = 'f'
AND NOT EXISTS (
SELECT 1 FROM pg_index i
WHERE i.indrelid = c.conrelid AND a.attnum = ANY(i.indkey)
);JSONB Patterns
JSONB 使用模式
sql
-- GIN index for containment queries
CREATE INDEX ON items USING gin (metadata);
SELECT * FROM items WHERE metadata @> '{"status": "active"}';
-- Expression index for specific key access
CREATE INDEX ON items ((metadata->>'category'));
SELECT * FROM items WHERE metadata->>'category' = 'electronics';Prefer typed columns over JSONB for frequently queried, well-structured data. Use JSONB for truly dynamic/variable attributes.
Use operator class for containment-only () queries -- 2-3x smaller index. Use default when key-existence (, ) is needed.
jsonb_path_ops@>jsonb_ops??|Delete operators:
| Operator | Operand | Behavior | Example |
|---|---|---|---|
| text | remove top-level key from object | |
| text[] | remove multiple top-level keys | |
| integer | remove array element by index | |
| text[] | remove value at nested path | |
Common mistakes:
- treats
col - 'a,b'as a single key name (no-op against a normally-structured document — the comma isn't a path separator).'a,b' - first removes the entire
col - 'a' - 'b'subtree before attemptingaon the result (data loss of- 'b', then a no-op).a.* - sets the value to JSON
jsonb_set(col, '{a,b}', 'null'::jsonb)rather than removing the key — strict "key absent" checks downstream then fail. Worse:nullwith a bare SQLjsonb_set(col, '{a,b}', NULL)makes the STRICT function return SQLNULL, clobbering the entire column on update. To delete the key, useNULL; to set it explicitly to JSON null, use#-(and know that's distinct from absence).'null'::jsonb
For nested deletes, use with a text-array path. Verify with one round-tripped row of the worst-case shape before committing the migration: , then confirm the key is gone (not present-as-null, no sibling data loss).
#-SELECT col #- '{a,b}' FROM t WHERE id = ? LIMIT 1sql
-- 用于包含查询的 GIN 索引
CREATE INDEX ON items USING gin (metadata);
SELECT * FROM items WHERE metadata @> '{"status": "active"}';
-- 用于特定键访问的表达式索引
CREATE INDEX ON items ((metadata->>'category'));
SELECT * FROM items WHERE metadata->>'category' = 'electronics';对于频繁查询、结构固定的数据,优先使用类型化列而非 JSONB。仅当属性真正动态可变时使用 JSONB。
仅需包含查询()时,使用 操作符类——索引大小可缩小 2-3 倍。当需要键存在性检查(、)时,使用默认的 。
@>jsonb_path_ops??|jsonb_ops删除操作符:
| 操作符 | 操作数 | 行为 | 示例 |
|---|---|---|---|
| 文本 | 移除对象的顶层键 | |
| 文本数组 | 移除多个顶层键 | |
| 整数 | 按索引移除数组元素 | |
| 文本数组 | 移除嵌套路径上的值 | |
常见错误:
- 会将
col - 'a,b'视为单个键名(对正常结构的文档无作用——逗号不是路径分隔符)。'a,b' - 会先移除整个
col - 'a' - 'b'子树,再尝试对结果执行a(丢失- 'b'数据,后续操作无作用)。a.* - 会将值设置为 JSON
jsonb_set(col, '{a,b}', 'null'::jsonb),而非删除键——下游的严格“键不存在”检查会失败。更糟的是:null使用纯 SQLjsonb_set(col, '{a,b}', NULL)会使 STRICT 函数返回 SQLNULL,更新时覆盖整个列。要删除键,使用NULL;要显式设置为 JSON null,使用#-(需注意这与键不存在是不同的)。'null'::jsonb
对于嵌套删除,使用 搭配文本数组路径。提交迁移前,用最坏情况的行数据验证:,确认键已被删除(不是以 null 存在,且未丢失同级数据)。
#-SELECT col #- '{a,b}' FROM t WHERE id = ? LIMIT 1Row-Level Security (RLS)
行级安全(RLS)
sql
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY; -- applies to table owner too
-- Set session context (generic, no extensions needed)
SET app.current_user_id = '123';
CREATE POLICY orders_user_policy ON orders
FOR ALL
USING (user_id = current_setting('app.current_user_id')::bigint);Performance: Policy expressions evaluate per row. Wrap function calls in a scalar subquery so PG evaluates once and caches:
sql
-- BAD: called per row
USING (get_current_user() = user_id)
-- GOOD: evaluated once, cached
USING ((SELECT get_current_user()) = user_id)Always index columns referenced in RLS policies. For complex multi-table checks, use helper functions.
SECURITY DEFINERsql
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY; -- 对表所有者也生效
-- 设置会话上下文(通用方式,无需扩展)
SET app.current_user_id = '123';
CREATE POLICY orders_user_policy ON orders
FOR ALL
USING (user_id = current_setting('app.current_user_id')::bigint);性能优化: 策略表达式会逐行计算。将函数调用包裹在标量子查询中,使 PostgreSQL 仅计算一次并缓存:
sql
-- 不好:逐行调用
USING (get_current_user() = user_id)
-- 好:仅计算一次,缓存结果
USING ((SELECT get_current_user()) = user_id)始终为 RLS 策略中引用的列创建索引。对于复杂的多表检查,使用 辅助函数。
SECURITY DEFINERQuery Optimization
查询优化
- Always before optimizing
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) - Use for slow-query detection and
pg_stat_statementsfor bloat (see Detection queries below for the full SQL)pg_stat_user_tables - Sequential scan on large table -> add index or check for function wrapping
WHERE - High -> index doesn't match predicate
rows removed by filter - CTEs are inlined by default; use /
MATERIALIZEDhints to control optimizationNOT MATERIALIZED - Prefer over
EXISTSfor correlated subqueriesIN - Use when subquery needs outer row reference
LATERAL JOIN - Cursor pagination () over
WHERE id > $last ORDER BY id LIMIT $nOFFSET - Approximate row counts: -- avoids full
SELECT reltuples FROM pg_class WHERE relname = 'table'on large tablescount(*) - Materialized views for expensive aggregations: (needs unique index). Schedule refresh, not per-query.
REFRESH MATERIALIZED VIEW CONCURRENTLY
- 优化前始终执行
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) - 使用 检测慢查询,使用
pg_stat_statements检测表膨胀(完整 SQL 见下方检测查询)pg_stat_user_tables - 大型表上的顺序扫描 → 添加索引或检查 WHERE 子句是否包含函数包装
- 高 值 → 索引与查询条件不匹配
rows removed by filter - CTE 默认会被内联;使用 /
MATERIALIZED提示控制优化行为NOT MATERIALIZED - 关联子查询优先使用 而非
EXISTSIN - 当子查询需要引用外部行时,使用
LATERAL JOIN - 使用游标分页()替代
WHERE id > $last ORDER BY id LIMIT $nOFFSET - 近似行数统计:——避免在大型表上执行全表
SELECT reltuples FROM pg_class WHERE relname = 'table'count(*) - 对昂贵的聚合查询使用物化视图:(需要唯一索引)。定时刷新,而非每次查询时刷新。
REFRESH MATERIALIZED VIEW CONCURRENTLY
Concurrency Patterns
并发模式
See concurrency-patterns.md for UPSERT, deadlock prevention, N+1 elimination, batch inserts, and queue processing with SKIP LOCKED.
有关 UPSERT、死锁预防、N+1 查询消除、批量插入和使用 SKIP LOCKED 的队列处理,请参考 concurrency-patterns.md。
Partitioning
分区
Use when table exceeds ~100M rows or needs TTL purge:
- -- time-series (by month/year), most common
RANGE - -- categorical (by region, tenant)
LIST - -- even distribution when no natural key
HASH
Partition key must be in every unique/PK constraint. Create indexes on partitions, not parent.
当表行数超过约 1 亿或需要按 TTL 清理时使用分区:
- ——时间序列数据(按月份/年份),最常用
RANGE - ——分类数据(按地区、租户)
LIST - ——无自然键时的均匀分布
HASH
分区键必须包含在所有唯一/主键约束中。在分区上创建索引,而非父表。
Transactions & Locking
事务与锁
- Keep transactions short -- long txns block vacuum and bloat tables
- Advisory locks for application-level mutual exclusion:
pg_advisory_xact_lock(key) - Non-blocking alternative: -- returns false instead of waiting
pg_try_advisory_lock(key) - Check blocked queries:
SELECT * FROM pg_stat_activity WHERE wait_event_type = 'Lock' - Monitor deadlocks:
SELECT deadlocks FROM pg_stat_database WHERE datname = current_database() - only locks rows that already exist -- it does not prevent a phantom insert of a missing row. Two transactions can both query a key, both see no row, both proceed to insert; the second fails the unique constraint (or both succeed if none existed). For a get-or-create / insert-if-missing race,
SELECT ... FOR UPDATEis the wrong tool -- use a partial unique index +FOR UPDATE, or serialize the key withINSERT ... ON CONFLICT DO NOTHING/UPDATEbefore the existence check.pg_advisory_xact_lock(hashtext(:key)) - A unique-violation (SQLSTATE 23505) caught inside an open transaction can't continue in that same transaction -- once any statement raises, the transaction enters the aborted state and every later statement fails with . Wrap the risky statement in a
current transaction is aborted, commands ignored until end of transaction blockandSAVEPOINTon error, or push the insert-or-update into a singleROLLBACK TO SAVEPOINTstatement that never raises. A bare try/catch around the failing statement is not enough on PostgreSQL.ON CONFLICT - A nested (or framework
BEGINwrapper) becomes atransaction(), not an independent transaction -- only the outermostSAVEPOINTis a real transaction. A per-iteration "transaction" inside an outer one does not commit independently and does not release row locks between iterations (held until the outerBEGIN); an unhandled inner error aborts the whole outer transaction. For a long backfill that needs per-row commit and lock release, run each unit as its own top-level transaction -- don't nest it under an outer one.COMMIT
- 保持事务简短——长事务会阻塞 vacuum 并导致表膨胀
- 使用 advisory 锁实现应用级互斥:
pg_advisory_xact_lock(key) - 非阻塞替代方案:——返回 false 而非等待
pg_try_advisory_lock(key) - 检查被阻塞的查询:
SELECT * FROM pg_stat_activity WHERE wait_event_type = 'Lock' - 监控死锁:
SELECT deadlocks FROM pg_stat_database WHERE datname = current_database() - 仅锁定已存在的行——无法阻止插入不存在的行。两个事务可以同时查询某个键,都未找到行,然后都执行插入;第二个事务会因唯一约束失败(若没有唯一约束则两个都成功)。对于“获取或创建/不存在则插入”的竞争场景,
SELECT ... FOR UPDATE不是正确的工具——使用部分唯一索引 +FOR UPDATE,或在检查存在性前使用INSERT ... ON CONFLICT DO NOTHING/UPDATE序列化键。pg_advisory_xact_lock(hashtext(:key)) - 在未提交的事务中捕获唯一约束冲突(SQLSTATE 23505)后,事务无法继续——一旦有语句抛出异常,事务进入中止状态,后续所有语句都会报错:。需将风险语句包裹在
current transaction is aborted, commands ignored until end of transaction block中,出错时执行SAVEPOINT,或使用单个ROLLBACK TO SAVEPOINT语句实现插入或更新,避免抛出异常。仅在异常周围添加 try/catch 在 PostgreSQL 中是不够的。ON CONFLICT - 嵌套的 (或框架的
BEGIN包装器)会成为transaction(),而非独立事务——只有最外层的SAVEPOINT是真正的事务。外层事务内的每一轮“事务”不会独立提交,也不会在迭代之间释放行锁(直到外层事务BEGIN);未处理的内层错误会中止整个外层事务。对于需要逐行提交和释放锁的长时回填任务,需将每个单元作为独立的顶层事务执行——不要嵌套在外层事务中。COMMIT
Full-Text Search
全文检索
See full-text-search.md for weighted tsvector setup, query syntax, highlighting, and when to use PG full-text vs external search.
有关加权 tsvector 配置、查询语法、高亮显示以及何时使用 PostgreSQL 全文检索 vs 外部检索系统,请参考 full-text-search.md。
Connection Pooling
连接池
Always pool in production. Direct connections cost ~10MB each.
- PgBouncer in mode for most workloads
transaction - mode if no session-level features (prepared statements, temp tables, advisory locks)
statement
Prepared statement caveat: Named prepared statements are bound to a specific connection. In transaction-mode pooling, the next request may hit a different connection. Use unnamed/extended-query-protocol statements (most ORMs default to this), or deallocate immediately after use.
生产环境始终使用连接池。直接连接每个约占用 10MB 内存。
- 大多数工作负载使用 PgBouncer 的 模式
transaction - 若无需会话级特性(预准备语句、临时表、advisory 锁),使用 模式
statement
预准备语句注意事项: 命名预准备语句绑定到特定连接。在事务模式连接池中,下一个请求可能分配到不同连接。使用未命名/扩展查询协议的语句(大多数 ORM 默认使用),或使用后立即释放。
Operations
运维
See operations.md for performance tuning, maintenance/monitoring, WAL, replication, and backup/recovery.
有关性能调优、维护/监控、WAL、复制和备份/恢复,请参考 operations.md。
Vector Search (pgvector)
向量检索(pgvector)
sql
CREATE EXTENSION vector;
ALTER TABLE items ADD COLUMN embedding vector(1536); -- match your model's output dimensions
-- HNSW: better recall, higher memory. Default choice.
CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops);
-- IVFFlat: lower memory for large datasets. Set lists = sqrt(row_count).
CREATE INDEX ON items USING ivfflat (embedding vector_cosine_ops) WITH (lists = 1000);Always filter BEFORE vector search (use partial indexes or CTEs with pre-filtered rows). Distance operators: cosine, L2, inner product.
<=><-><#>sql
CREATE EXTENSION vector;
ALTER TABLE items ADD COLUMN embedding vector(1536); -- 匹配模型输出维度
-- HNSW:召回率更高,内存占用更大。默认选择。
CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops);
-- IVFFlat:大型数据集内存占用更低。设置 lists = sqrt(行数)。
CREATE INDEX ON items USING ivfflat (embedding vector_cosine_ops) WITH (lists = 1000);始终先过滤再执行向量检索(使用部分索引或预过滤行的 CTE)。距离操作符: 余弦距离, L2 距离, 内积。
<=><-><#>Anti-Patterns
反模式
| Anti-Pattern | Fix |
|---|---|
| List needed columns |
| N+1 queries in application loop | Use |
| Cursor pagination: |
| Approximate: |
| Nullable booleans | |
| Missing FK indexes | See detection query in Index Strategy above |
| Use |
Detection queries:
sql
-- Slow queries (requires pg_stat_statements)
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
WHERE mean_exec_time > 100
ORDER BY mean_exec_time DESC LIMIT 20;
-- Table bloat (dead tuples awaiting vacuum)
SELECT relname, n_dead_tup, last_vacuum, last_autovacuum
FROM pg_stat_user_tables
WHERE n_dead_tup > 10000
ORDER BY n_dead_tup DESC;
-- Unused indexes (candidates for removal)
SELECT schemaname, relname, indexrelname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0 AND indexrelname NOT LIKE '%_pkey'
ORDER BY pg_relation_size(indexrelid) DESC;| 反模式 | 修复方案 |
|---|---|
| 列出所需列 |
| 应用循环中的 N+1 查询 | 使用 |
大型表上使用 | 游标分页: |
大型表上执行 | 近似统计: |
| 可空布尔值 | |
| 缺失外键索引 | 见索引策略中的检测查询 |
| 使用 |
检测查询:
sql
-- 慢查询(需要 pg_stat_statements)
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
WHERE mean_exec_time > 100
ORDER BY mean_exec_time DESC LIMIT 20;
-- 表膨胀(等待 vacuum 的死元组)
SELECT relname, n_dead_tup, last_vacuum, last_autovacuum
FROM pg_stat_user_tables
WHERE n_dead_tup > 10000
ORDER BY n_dead_tup DESC;
-- 未使用的索引(可考虑删除)
SELECT schemaname, relname, indexrelname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0 AND indexrelname NOT LIKE '%_pkey'
ORDER BY pg_relation_size(indexrelid) DESC;Verify
验证
Run on changed queries. Confirm no sequential scans on large tables and no unindexed FK columns before declaring done.
EXPLAIN (ANALYZE, BUFFERS)对修改后的查询执行 。确认大型表无顺序扫描、无未索引的外键列后,方可完成优化。
EXPLAIN (ANALYZE, BUFFERS)