postgresql

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

PostgreSQL

PostgreSQL

Data Type Defaults

数据类型默认值

NeedUseAvoid
Primary key
BIGINT GENERATED ALWAYS AS IDENTITY
SERIAL
,
BIGSERIAL
Timestamps
TIMESTAMPTZ
TIMESTAMP
(loses timezone)
Text
TEXT
VARCHAR(n)
unless constraint needed
Money
NUMERIC(precision, scale)
MONEY
,
FLOAT
Boolean
BOOLEAN
with
NOT NULL DEFAULT
nullable booleans
JSON
JSONB
JSON
(no indexing), text JSON
UUID
gen_random_uuid()
(PG13+)
uuid-ossp
extension
IP addresses
INET
/
CIDR
text
Ranges
TSTZRANGE
,
INT4RANGE
, etc.
pair of columns
需求推荐使用避免使用
主键
BIGINT GENERATED ALWAYS AS IDENTITY
SERIAL
BIGSERIAL
时间戳
TIMESTAMPTZ
TIMESTAMP
(会丢失时区信息)
文本
TEXT
VARCHAR(n)
(除非有明确长度约束需求)
金额
NUMERIC(precision, scale)
MONEY
FLOAT
布尔值
NOT NULL DEFAULT
BOOLEAN
可空布尔值
JSON 数据
JSONB
JSON
(不支持索引)、文本格式的 JSON
UUID
gen_random_uuid()
(PG13+)
uuid-ossp
扩展
IP 地址
INET
/
CIDR
文本格式
范围类型
TSTZRANGE
INT4RANGE
用两个列表示范围

Schema Rules

模式设计规则

  • Every FK column gets an index (PG does NOT auto-create these)
  • NOT NULL
    on every column unless NULL has business meaning
  • CHECK
    constraints for domain rules at DB level
  • EXCLUDE
    constraints for range overlaps:
    EXCLUDE USING gist (room WITH =, during WITH &&)
  • Default
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
  • Separate
    updated_at
    with trigger, never trust app layer alone. Gate it with
    WHEN (OLD.* IS DISTINCT FROM NEW.*)
    so a no-op write neither fires the function nor bumps the timestamp -- on
    BEFORE UPDATE
    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.
  • Use
    BIGINT
    PKs -- cheaper JOINs than UUID, better index locality
  • Safe migrations:
    CREATE INDEX CONCURRENTLY
    , add columns with a non-volatile
    DEFAULT
    (instant add). Never
    ALTER TYPE
    on large tables in-place.
  • A
    DEFAULT
    whose expression is
    VOLATILE
    rewrites the entire table under
    ACCESS EXCLUSIVE
    ; only
    IMMUTABLE
    /
    STABLE
    defaults get the metadata-only fast path. Check before shipping the migration:
    SELECT provolatile FROM pg_proc WHERE proname = 'gen_random_uuid';
    --
    v
    is volatile,
    s
    /
    i
    are not. So
    DEFAULT 7
    and
    DEFAULT now()
    are instant,
    DEFAULT gen_random_uuid()
    is a full rewrite; add the column nullable, backfill in batches, then set the default.
  • NULLS NOT DISTINCT
    on unique indexes (PG15+) -- treats NULLs as equal for uniqueness
  • Under
    NULLS NOT DISTINCT
    , a pre-flight duplicate check written with SQL
    =
    misses NULL/NULL collisions -- the index rejects the second row, but
    NULL = NULL
    evaluates to NULL (not true), so a self-join or
    WHERE a.col = b.col
    probe silently skips exactly the pairs the index will reject. Write the probe with
    IS NOT DISTINCT FROM
    so NULL/NULL compares as equal.
  • Revoke default public schema access:
    REVOKE ALL ON SCHEMA public FROM public
  • 每个外键(FK)列都需要创建索引(PostgreSQL 不会自动创建)
  • 除非 NULL 具有明确业务含义,否则所有列都需设置
    NOT NULL
  • 在数据库层面通过
    CHECK
    约束实现领域规则
  • EXCLUDE
    约束防止范围重叠:
    EXCLUDE USING gist (room WITH =, during WITH &&)
  • 默认添加
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
  • 单独设置
    updated_at
    字段并通过触发器维护,绝不单独依赖应用层。触发器需添加
    WHEN (OLD.* IS DISTINCT FROM NEW.*)
    条件,确保无变更的写入不会触发函数或更新时间戳——在
    BEFORE UPDATE
    阶段,行镜像在触发器执行前已构建,因此比较操作会读取调用者提交的行,而非触发器即将标记的行。
  • 使用
    BIGINT
    作为主键——比 UUID 更高效的 JOIN 操作,更好的索引局部性
  • 安全迁移:使用
    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()
    会触发全表重写;应先添加可空列,批量回填数据,再设置默认值。
  • 在唯一索引上使用
    NULLS NOT DISTINCT
    (PG15+)——将 NULL 视为相等值以保证唯一性
  • NULLS NOT DISTINCT
    规则下,用 SQL
    =
    编写的前置重复检查会遗漏 NULL/NULL 的冲突——索引会拒绝第二行,但
    NULL = NULL
    结果为 NULL(而非 true),因此自连接或
    WHERE a.col = b.col
    的查询会跳过索引拒绝的配对。需使用
    IS NOT DISTINCT FROM
    编写查询,使 NULL/NULL 被视为相等。
  • 撤销 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:
  1. Expand: add the new column/table, backfill data, update writes to populate both old and new
  2. Migrate: switch reads to the new column/table, verify in production
  3. 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:
  • NOT NULL
    without a
    DEFAULT
    on an existing table locks and rewrites every row. Add the column nullable first, backfill, then add the constraint.
  • CREATE INDEX
    (without
    CONCURRENTLY
    ) locks writes for the duration. Always use
    CONCURRENTLY
    , which cannot run inside a transaction block -- keep it in its own migration.
  • Large data backfills: batch with
    FOR UPDATE SKIP LOCKED
    to avoid locking the entire table:
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
SELECT col → mutate in app → UPDATE SET col = new_full_value WHERE id = ?
silently drops concurrent writes that landed between SELECT and UPDATE. Any column written by live traffic is exposed:
jsonb
documents, comma-separated tag fields, denormalized counters, JSON-encoded attribute blobs. Mitigations, in order of preference:
  • In-place atomic update when the edit is expressible as SQL:
    UPDATE t SET col = jsonb_set(col, '{path}', :value) WHERE ...
    , or
    UPDATE t SET tags = array_append(tags, :tag) WHERE ...
    — no read-modify-write window.
  • Row-level lock during the loop: wrap each iteration in a transaction,
    SELECT ... WHERE id = ? FOR UPDATE
    , then mutate and write. Cheaper to author, accepts more lock contention.
  • Compare-and-swap retry: include the original snapshot in
    WHERE col = :original_value
    , check the affected-row count; on 0, re-read and retry. Robust under contention, requires explicit retry-loop handling.
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
  • 迁移脚本一旦部署即不可修改——绝不要编辑已在任何共享环境中执行过的迁移脚本
  • 模式迁移和数据迁移需分开存储为独立文件。模式变更快速且支持事务;数据回填速度慢,可能需要分批处理
  • 生产环境仅支持正向迁移。回滚操作需通过新的正向迁移脚本实现逆向变更
零停机重命名与删除的扩展-收缩模式:
  1. 扩展:添加新列/表,回填数据,更新写入逻辑以同时填充新旧字段
  2. 迁移:将读取逻辑切换到新列/表,在生产环境验证
  3. 收缩:在后续部署中删除旧列/表
绝不要在单个迁移脚本中重命名或删除列——在部署完成到代码更新完成的间隙,调用者读取旧字段会导致报错。
危险操作:
  • 在已有表上添加无
    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 col → 在应用中修改 → UPDATE SET col = new_full_value WHERE id = ?
的循环逻辑,会静默丢失在 SELECT 和 UPDATE 之间提交的并发写入。任何被实时流量写入的列都会受影响:
jsonb
文档、逗号分隔的标签字段、反规范化计数器、JSON 编码的属性 blob。缓解方案按优先级排序:
  • 原地原子更新:若修改逻辑可通过 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
    子句中包含原始快照
    WHERE col = :original_value
    ,检查影响行数;若为 0,则重新读取并重试。在高并发场景下更可靠,但需显式处理重试逻辑。
默认的分块解码-编码循环仅在维护窗口(禁止写入)期间安全。ORM 的“chunkById + 加载 + 修改 + 保存”模式也会遇到同样问题。

Index Strategy

索引策略

TypeUse When
B-tree (default)Equality, range, sorting,
LIKE 'prefix%'
GINJSONB (
@>
,
?
,
?&
), arrays, full-text (
tsvector
)
GiSTGeometry, ranges, full-text (smaller but slower than GIN)
BRINLarge tables with natural ordering (timestamps, serial IDs)
Index rules:
  • Composite: most selective column first, max 3-4 columns
  • Partial:
    WHERE status = 'active'
    -- smaller, faster
  • Covering:
    INCLUDE (col)
    -- avoids heap lookup
  • Expression:
    ON (lower(email))
    -- for function-based WHERE
  • A GIN index on an array column serves the containment operators, not
    =
    :
    WHERE 'x' = ANY(col)
    seq-scans even with
    enable_seqscan = off
    , because
    ANY
    over an array expands to equality and no GIN operator class implements it. Write the predicate as
    WHERE col @> ARRAY['x']
    to reach the index.
  • fillfactor = 70-90
    on write-heavy tables -- reserves space for HOT updates, reducing index bloat
  • Drop unused indexes (only after one full business cycle since last restart -- check
    pg_stat_database.stats_reset
    first, otherwise you may drop a primary key on a freshly restarted DB or read replica):
    SELECT * 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(默认)等值查询、范围查询、排序、
LIKE 'prefix%'
前缀匹配
GINJSONB(
@>
?
?&
)、数组、全文检索(
tsvector
GiST几何数据、范围类型、全文检索(比 GIN 占用空间小但速度慢)
BRIN具有自然排序的大型表(时间戳、自增 ID)
索引规则:
  • 复合索引:选择性最高的列放在最前面,最多包含 3-4 列
  • 部分索引:
    WHERE status = 'active'
    ——占用空间更小,速度更快
  • 覆盖索引:
    INCLUDE (col)
    ——避免堆表查询
  • 表达式索引:
    ON (lower(email))
    ——用于基于函数的 WHERE 查询
  • 数组列上的 GIN 索引支持包含操作符,但不支持
    =
    WHERE 'x' = ANY(col)
    即使设置
    enable_seqscan = off
    仍会执行顺序扫描,因为数组上的
    ANY
    会展开为等值查询,而 GIN 操作符类未实现该逻辑。需将条件改写为
    WHERE col @> ARRAY['x']
    以命中索引。
  • 写入频繁的表设置
    fillfactor = 70-90
    ——为 HOT 更新预留空间,减少索引膨胀
  • 删除未使用的索引(需在数据库重启后经过一个完整业务周期再操作——先检查
    pg_stat_database.stats_reset
    ,否则可能误删刚重启的主库或只读副本上的主键):
    SELECT * 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
jsonb_path_ops
operator class for containment-only (
@>
) queries -- 2-3x smaller index. Use default
jsonb_ops
when key-existence (
?
,
?|
) is needed.
Delete operators:
OperatorOperandBehaviorExample
-
textremove top-level key from object
'{"a":1,"b":2}'::jsonb - 'a'
{"b":2}
-
text[]remove multiple top-level keys
'{"a":1,"b":2}'::jsonb - ARRAY['a','b']
{}
-
integerremove array element by index
'[1,2,3]'::jsonb - 1
[1,3]
#-
text[]remove value at nested path
'{"a":{"b":1}}'::jsonb #- '{a,b}'
{"a":{}}
Common mistakes:
  • col - 'a,b'
    treats
    'a,b'
    as a single key name (no-op against a normally-structured document — the comma isn't a path separator).
  • col - 'a' - 'b'
    first removes the entire
    a
    subtree before attempting
    - 'b'
    on the result (data loss of
    a.*
    , then a no-op).
  • jsonb_set(col, '{a,b}', 'null'::jsonb)
    sets the value to JSON
    null
    rather than removing the key — strict "key absent" checks downstream then fail. Worse:
    jsonb_set(col, '{a,b}', NULL)
    with a bare SQL
    NULL
    makes the STRICT function return SQL
    NULL
    , clobbering the entire column on update. To delete the key, use
    #-
    ; to set it explicitly to JSON null, use
    'null'::jsonb
    (and know that's distinct from absence).
For nested deletes, use
#-
with a text-array path. Verify with one round-tripped row of the worst-case shape before committing the migration:
SELECT col #- '{a,b}' FROM t WHERE id = ? LIMIT 1
, then confirm the key is gone (not present-as-null, no sibling data loss).
sql
-- 用于包含查询的 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。
仅需包含查询(
@>
)时,使用
jsonb_path_ops
操作符类——索引大小可缩小 2-3 倍。当需要键存在性检查(
?
?|
)时,使用默认的
jsonb_ops
删除操作符:
操作符操作数行为示例
-
文本移除对象的顶层键
'{"a":1,"b":2}'::jsonb - 'a'
{"b":2}
-
文本数组移除多个顶层键
'{"a":1,"b":2}'::jsonb - ARRAY['a','b']
{}
-
整数按索引移除数组元素
'[1,2,3]'::jsonb - 1
[1,3]
#-
文本数组移除嵌套路径上的值
'{"a":{"b":1}}'::jsonb #- '{a,b}'
{"a":{}}
常见错误:
  • col - 'a,b'
    会将
    'a,b'
    视为单个键名(对正常结构的文档无作用——逗号不是路径分隔符)。
  • col - 'a' - 'b'
    会先移除整个
    a
    子树,再尝试对结果执行
    - 'b'
    (丢失
    a.*
    数据,后续操作无作用)。
  • jsonb_set(col, '{a,b}', 'null'::jsonb)
    会将值设置为 JSON
    null
    ,而非删除键——下游的严格“键不存在”检查会失败。更糟的是:
    jsonb_set(col, '{a,b}', NULL)
    使用纯 SQL
    NULL
    会使 STRICT 函数返回 SQL
    NULL
    ,更新时覆盖整个列。要删除键,使用
    #-
    ;要显式设置为 JSON null,使用
    'null'::jsonb
    (需注意这与键不存在是不同的)。
对于嵌套删除,使用
#-
搭配文本数组路径。提交迁移前,用最坏情况的行数据验证:
SELECT col #- '{a,b}' FROM t WHERE id = ? LIMIT 1
,确认键已被删除(不是以 null 存在,且未丢失同级数据)。

Row-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
SECURITY DEFINER
helper functions.
sql
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 DEFINER
辅助函数。

Query Optimization

查询优化

  • Always
    EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
    before optimizing
  • Use
    pg_stat_statements
    for slow-query detection and
    pg_stat_user_tables
    for bloat (see Detection queries below for the full SQL)
  • Sequential scan on large table -> add index or check
    WHERE
    for function wrapping
  • High
    rows removed by filter
    -> index doesn't match predicate
  • CTEs are inlined by default; use
    MATERIALIZED
    /
    NOT MATERIALIZED
    hints to control optimization
  • Prefer
    EXISTS
    over
    IN
    for correlated subqueries
  • Use
    LATERAL JOIN
    when subquery needs outer row reference
  • Cursor pagination (
    WHERE id > $last ORDER BY id LIMIT $n
    ) over
    OFFSET
  • Approximate row counts:
    SELECT reltuples FROM pg_class WHERE relname = 'table'
    -- avoids full
    count(*)
    on large tables
  • Materialized views for expensive aggregations:
    REFRESH MATERIALIZED VIEW CONCURRENTLY
    (needs unique index). Schedule refresh, not per-query.
  • 优化前始终执行
    EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
  • 使用
    pg_stat_statements
    检测慢查询,使用
    pg_stat_user_tables
    检测表膨胀(完整 SQL 见下方检测查询)
  • 大型表上的顺序扫描 → 添加索引或检查 WHERE 子句是否包含函数包装
  • rows removed by filter
    值 → 索引与查询条件不匹配
  • CTE 默认会被内联;使用
    MATERIALIZED
    /
    NOT MATERIALIZED
    提示控制优化行为
  • 关联子查询优先使用
    EXISTS
    而非
    IN
  • 当子查询需要引用外部行时,使用
    LATERAL JOIN
  • 使用游标分页(
    WHERE id > $last ORDER BY id LIMIT $n
    )替代
    OFFSET
  • 近似行数统计:
    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:
  • RANGE
    -- time-series (by month/year), most common
  • LIST
    -- categorical (by region, tenant)
  • HASH
    -- even distribution when no natural key
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:
    pg_try_advisory_lock(key)
    -- returns false instead of waiting
  • 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()
  • SELECT ... FOR UPDATE
    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,
    FOR UPDATE
    is the wrong tool -- use a partial unique index +
    INSERT ... ON CONFLICT DO NOTHING/UPDATE
    , or serialize the key with
    pg_advisory_xact_lock(hashtext(:key))
    before the existence check.
  • 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
    current transaction is aborted, commands ignored until end of transaction block
    . Wrap the risky statement in a
    SAVEPOINT
    and
    ROLLBACK TO SAVEPOINT
    on error, or push the insert-or-update into a single
    ON CONFLICT
    statement that never raises. A bare try/catch around the failing statement is not enough on PostgreSQL.
  • A nested
    BEGIN
    (or framework
    transaction()
    wrapper) becomes a
    SAVEPOINT
    , not an independent transaction
    -- only the outermost
    BEGIN
    is 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 outer
    COMMIT
    ); 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.
  • 保持事务简短——长事务会阻塞 vacuum 并导致表膨胀
  • 使用 advisory 锁实现应用级互斥:
    pg_advisory_xact_lock(key)
  • 非阻塞替代方案:
    pg_try_advisory_lock(key)
    ——返回 false 而非等待
  • 检查被阻塞的查询:
    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
    ,或使用单个
    ON CONFLICT
    语句实现插入或更新,避免抛出异常。仅在异常周围添加 try/catch 在 PostgreSQL 中是不够的。
  • 嵌套的
    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
    transaction
    mode for most workloads
  • statement
    mode if no session-level features (prepared statements, temp tables, advisory locks)
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-PatternFix
SELECT *
List needed columns
N+1 queries in application loopUse
JOIN
,
IN
, or batch fetch
OFFSET
for pagination on large tables
Cursor pagination:
WHERE id > $last ORDER BY id LIMIT $n
count(*)
on large tables
Approximate:
SELECT reltuples FROM pg_class WHERE relname = 'table'
Nullable booleans
NOT NULL DEFAULT false
-- three-valued logic causes subtle bugs
Missing FK indexesSee detection query in Index Strategy above
ORDER BY RANDOM()
Use
TABLESAMPLE
or application-side shuffle
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;
反模式修复方案
SELECT *
列出所需列
应用循环中的 N+1 查询使用
JOIN
IN
或批量获取
大型表上使用
OFFSET
分页
游标分页:
WHERE id > $last ORDER BY id LIMIT $n
大型表上执行
count(*)
近似统计:
SELECT reltuples FROM pg_class WHERE relname = 'table'
可空布尔值
NOT NULL DEFAULT false
——三值逻辑会导致隐蔽 bug
缺失外键索引见索引策略中的检测查询
ORDER BY RANDOM()
使用
TABLESAMPLE
或应用层洗牌
检测查询:
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
EXPLAIN (ANALYZE, BUFFERS)
on changed queries. Confirm no sequential scans on large tables and no unindexed FK columns before declaring done.
对修改后的查询执行
EXPLAIN (ANALYZE, BUFFERS)
。确认大型表无顺序扫描、无未索引的外键列后,方可完成优化。