postgres-best-practices
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePostgres
Postgres
Rules for schema and queries on Postgres. They are language-agnostic; the Go half lives in the
skill (, ).
go-idiomaticreferences/database.mdreferences/patterns.mdPostgres 上 schema 和查询的规则。它们与语言无关;Go 部分位于
skill (, )。
go-idiomaticreferences/database.mdreferences/patterns.mdWhere to Look
查阅位置
| If you're working on… | Load this |
|---|---|
Go code that runs these queries ( | |
| A closed value set mirrored as a Go type | |
| Writing or changing a migration, any tool | |
Converting an existing Postgres | |
| Migrations in a repo that uses goose | |
Only if the repo has | |
| 如果你正在处理…… | 加载此内容 |
|---|---|
使用 | |
| 映射为 Go 类型的封闭值集合 | |
| 编写或修改 migration(使用任何工具) | |
将现有 Postgres | |
| 使用 goose 的 repo 中的 migration | |
仅当 repo 包含 | |
Schema
Schema
- Closed value sets are + a named
text, never a PostgresCHECK. Adding a value to anENUMis a type migration and removing one is not supported; aENUMis oneCHECK. Never mix the two styles in one schema, and never leave a status column unchecked. An existingALTERis a conversion to schedule, not a style to extend.ENUM - Every foreign key column has an index. Postgres does not create one. Without it, deleting
a parent scans the child table, and turns that into one scan per row.
ON DELETE CASCADE - for instants,
timestamptzonly for calendar dates. Never store times asdate.text - for JSON, never
jsonb.text - Name every constraint and index, with one scheme (,
<table>_<cols>_key,_fkey,_check). Names are what errors and migrations refer to.<table>_<cols>_idx - One meaning for absence per column. NULL or , not both. Pick NULL unless the column is
''by design.NOT NULL DEFAULT '' - An append-only table gets a retention policy in the migration that creates it. Outboxes,
run logs and audit trails only grow; decide how long rows live, index the column the cleanup
filters on, and delete expired rows in batches () from a scheduled job. Added later, the first cleanup is one huge delete on a huge table.
DELETE ... WHERE id IN (SELECT id ... LIMIT n)
- 封闭值集合使用 + 具名
text,绝不使用 PostgresCHECK。 向ENUM添加值属于类型 migration,而删除值则不受支持;ENUM只需一次CHECK。 同一 schema 中绝不能混用两种风格,也绝不能让状态列缺少ALTER约束。已有的CHECK应安排转换,而不是把它当作可继续扩展的风格。ENUM - 每个外键列都要有索引。 Postgres 不会自动创建索引。缺少索引时,删除
父记录会扫描子表,而 会将操作变成逐行扫描。
ON DELETE CASCADE - 用于时间点,
timestamptz仅用于日历日期。 绝不要将时间存为date。text - JSON 使用 ,绝不使用
jsonb。text - 为每个约束和索引命名,并采用统一命名规则 (,
<table>_<cols>_key,_fkey,_check)。错误信息和 migration 都通过名称引用它们。<table>_<cols>_idx - 每列的缺失状态只表达一种含义。 NULL 或 ,二者不可并用。除非该列在设计上 明确采用
'',否则选择 NULL。NOT NULL DEFAULT '' - append-only 表必须在创建它的 migration 中同时制定保留策略。 Outbox、run log
和 audit trail 只会增长;要决定记录保留多久,为清理条件所过滤的列建立索引,
并由定时任务分批删除过期记录 ()。 如果之后再补上该策略,第一次清理就会在大型表上执行一次巨型删除。
DELETE ... WHERE id IN (SELECT id ... LIMIT n)
Queries
Queries
- One clock per comparison. is fine for audit columns. Any time the application compares against, or a test asserts on, is passed in as a parameter. Mixing SQL
DEFAULT now()with the app'sNOW()makes leases and expiries untestable and skewed.time.Now() - Explicit column lists. No and no
SELECT *; adding a column must not change what callers receive.RETURNING * - Every list query has a hard . A list without one returns the whole table once the table grows. On growing tables page by keyset (
LIMIT), notWHERE (created_at, id) < ($1, $2) ORDER BY created_at DESC, id DESC LIMIT $3, which reads and discards every skipped row.OFFSET - No N+1 loops. Fetch with , insert with multi-row
= ANY($1::uuid[])orVALUES.COPY - Claiming work uses so concurrent workers take different rows instead of queueing on one. Update the claimed row in the same statement or transaction.
FOR UPDATE SKIP LOCKED - Read-then-write uses in a transaction. A plain read races.
SELECT ... FOR UPDATE - Match unique violations (SQLSTATE ) by constraint name, not by message text or by assuming which constraint fired. This is why constraints are named.
23505 - names its target (
ON CONFLICTorON CONFLICT (col)). A bareON CONSTRAINT namesilently swallows violations of constraints you did not mean.ON CONFLICT DO NOTHING
- 每次比较只使用一个时钟。 审计列可以使用 。应用进行时间比较或 测试进行时间断言时,相关时间都应作为参数传入。混用 SQL
DEFAULT now()与应用的NOW()会使 lease 和过期逻辑无法测试且产生偏差。time.Now() - 显式列出列。 不使用 ,也不使用
SELECT *;新增列不得改变 调用方收到的内容。RETURNING * - 每个列表查询都必须设置硬性 。 没有
LIMIT的列表查询会随着表增长 而返回整张表中的所有数据。对于不断增长的表,使用 keyset 分页 (LIMIT),而不是WHERE (created_at, id) < ($1, $2) ORDER BY created_at DESC, id DESC LIMIT $3;后者会读取并丢弃每一个被跳过的行。OFFSET - 禁止 N+1 循环。 使用 获取数据,使用多行
= ANY($1::uuid[])或VALUES插入数据。COPY - 领取任务时使用 ,让并发 worker 领取不同的行, 而不是都排队等待同一行。在同一条语句或同一个事务中更新已领取的行。
FOR UPDATE SKIP LOCKED - 先读后写操作应在事务中使用 。 普通读取会产生竞态。
SELECT ... FOR UPDATE - 按约束名称匹配唯一性冲突(SQLSTATE ),不要按错误消息文本匹配, 也不要假定触发的是哪个约束。这正是约束需要命名的原因。
23505 - 必须指定目标 (
ON CONFLICT或ON CONFLICT (col))。不带目标的ON CONSTRAINT name会静默吞掉非目标约束的冲突。ON CONFLICT DO NOTHING
Altering tables
修改表
- Add columns nullable or with a constant default, then backfill, then add . Never rewrite a large table inside the migration that adds the column.
NOT NULL - Backfill in batches, keyed on the primary key, committing between batches. One giant
holds locks and bloats the table.
UPDATE - Create indexes on live tables with , outside a transaction.
CREATE INDEX CONCURRENTLY - Add constraints in two steps on big tables: , then
ADD CONSTRAINT ... NOT VALID, which does not block writes.VALIDATE CONSTRAINT - Migrations are forward-only once merged. Fix mistakes with a new migration.
- 添加列时,先以可为空或带常量默认值的方式添加列,然后执行 backfill,最后添加
。绝不要在添加该列的 migration 中重写大型表。
NOT NULL - 分批执行 backfill,以主键为依据,并在批次之间提交。单个巨型
会持有锁并导致表膨胀。
UPDATE - 在生产环境中的表上使用 创建索引,并在事务 之外执行。
CREATE INDEX CONCURRENTLY - 在大型表上分两步添加约束: ,然后
ADD CONSTRAINT ... NOT VALID,后者不会阻塞写入。VALIDATE CONSTRAINT - migration 合并后只能向前执行。 使用新的 migration 修复错误。