Community研究与数据分析github.com

scary-production/claude-plugins

PostgreSQL 迁移:配对的 up/down 文件、现代类型与约束、可逆变更、非阻塞索引及迁移器要求。

claude-plugins 是什么?

该技能为 AI 助手(如 Claude Code)提供了一套完整的 PostgreSQL 数据库迁移规范。它要求每次迁移都是配对的 `up.sql` 和 `down.sql` 文件,且 `down` 操作必须完全可逆。技能重点推荐使用 `IDENTITY` 列、带 `CHECK` 约束的 `text` 类型、`timestamptz` 和 `jsonb` 等现代 SQL 类型,并给出了详细的使用场景和模板。 对于生产环境的在线表,该技能详细讲解了如何使用 `CREATE INDEX CONCURRENTLY` 创建无阻塞索引,包括其限制(不能放在事务内)、风险(可能产生 INVALID 索引)以及验证方法。它甚至分析了一种罕见但令人困惑的情况——索引有效但查询计划仍使用 `Seq Scan`,并给出了排查步骤(检查 `indcheckxmin` 和历史事务)。 最后,技能明确列出了对迁移器的技术要求:每个迁移独立事务、并行执行锁、支持无事务模式的特殊标记(如 `-- migrate:no-transaction`),以及离线创建迁移文件的能力。整体而言,这是一份面向高频写入生产数据库的、严谨且可落地的工程实践指南。

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/scary-production/claude-plugins/tree/HEAD/plugins/go-postgres/skills/postgres-migrations

Installed? Explore more 研究与数据分析 skills: affaan-m/uspto-database, affaan-m/scholar-evaluation, affaan-m/literature-review · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

PostgreSQL 迁移

核心原则。 每次迁移都是一对 up/down 文件,down 必须完全撤销 up 的操作。每条迁移在其自身的事务中执行。唯一的例外是:在已有数据且持续写入的表上创建索引 —— 使用 CREATE INDEX CONCURRENTLY 放在单独的无事务迁移中,且每个文件只包含一条命令。

断言检查见 CHECKS.md。

文件格式

migrations/
├── 20250524120000_create_users.up.sql
├── 20250524120000_create_users.down.sql
├── 20250525090000_add_users_phone.up.sql
└── 20250525090000_add_users_phone.down.sql
  • 前缀为 UTC 时间 YYYYMMDDHHmmss,名称为 snake_case 的简短描述。
  • *.up.sql 执行变更,*.down.sql 回滚变更。两个文件都必须存在。
  • 已应用的迁移不要修改 —— 编写新的迁移。

类型与约束

避免使用推荐写法原因
bigserialbigint GENERATED ALWAYS AS IDENTITYSQL 标准;如果需要插入自定义 id(如导入),使用 BY DEFAULT
varchar(n)text NOT NULL CHECK (length(x) <= n)长度是数据规则,更容易修改
timestamptimestamptz存储时间点,而非“墙上时钟”
integer 标志位boolean
jsonjsonb二进制格式,支持索引和运算符
来自扩展的 uuiduuid DEFAULT gen_random_uuid()PostgreSQL 13 起内置

除非是预期长度很长的文本(如描述、评论),否则始终用 CHECK 约束限制文本列的长度。如果最大长度不明确,请询问用户。

数据约束
电子邮件CHECK (length(email) <= 320)
姓名CHECK (length(name) <= 255)
电话CHECK (length(phone) <= 50)
slug、代码CHECK (length(code) <= 100)
URLCHECK (length(url) <= 2048)
描述无限制,或 CHECK (length(bio) <= 10000)

表模板:

CREATE TABLE users (
    id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name       text        NOT NULL CHECK (length(name) <= 255),
    email      text        NOT NULL CHECK (length(email) <= 320),
    is_active  boolean     NOT NULL DEFAULT true,
    metadata   jsonb,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now()
);

CREATE UNIQUE INDEX ix_users_email ON users (email);

回滚

updown
CREATE TABLE tDROP TABLE IF EXISTS t
ALTER TABLE t ADD COLUMN cALTER TABLE t DROP COLUMN IF EXISTS c
CREATE INDEX ixDROP INDEX IF EXISTS ix
CREATE INDEX CONCURRENTLY ixDROP INDEX CONCURRENTLY IF EXISTS ix — 同样无事务
CREATE TYPE t AS ENUM (...)DROP TYPE IF EXISTS t
ALTER TYPE t ADD VALUE 'v'PostgreSQL 不支持 DROP VALUE —— 重建类型或保留空 down
ALTER TABLE t ALTER COLUMN c SET NOT NULLALTER TABLE t ALTER COLUMN c DROP NOT NULL
ALTER TABLE t ADD CONSTRAINT cALTER TABLE t DROP CONSTRAINT IF EXISTS c

down 中使用 IF EXISTS 是为了保证回滚的幂等性。

常见变更

带默认值的列。 使用常量 DEFAULT 时,PostgreSQL 11+ 不会重写整个表,命令几乎是瞬间完成。如果使用 volatile 默认值(如 gen_random_uuid()、clock_timestamp()),表会在排他锁下整体重写 —— 对于大型在线表,先添加无默认值的列,再分批填充。

ALTER TABLE users ADD COLUMN role text NOT NULL DEFAULT 'user' CHECK (length(role) <= 50);

外键 — 立即在引用列上创建索引:PostgreSQL 不会自动创建,没有索引时删除父记录和 join 操作会进行全表扫描。

CREATE TABLE orders (
    id      bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    user_id bigint NOT NULL REFERENCES users (id),
    total   numeric NOT NULL
);
CREATE INDEX ix_orders_user_id ON orders (user_id);

枚举值新增。 ALTER TYPE ... ADD VALUE 可以在事务中工作,但新值在提交之前无法使用(错误代码 55P04 unsafe use of new value)。如果迁移器在事务中执行迁移,请将新增值和使用该值(UPDATE、DEFAULT、CHECK)放在不同的迁移中。

ALTER TYPE payment_method ADD VALUE 'bank_transfer';

down:由于没有 DROP VALUE。如果该值无害——保留空的 down 并加注释;否则——重建类型(新建类型、ALTER COLUMN ... TYPE ... USING、删除旧类型)。

在线表上的索引

对于已有数据且仍在写入的表,索引应通过 CREATE INDEX CONCURRENTLY (CIC) 创建。普通 CREATE INDEX 会在构建期间阻塞写入。如果索引与新表在同一迁移中创建,则使用普通 CREATE INDEX。数据库大小不是借口:挂起的事务会在任何大小下破坏新索引(见下文)。

up:

CREATE INDEX CONCURRENTLY ix_orders_customer_id ON orders (customer_id);

down:

DROP INDEX CONCURRENTLY IF EXISTS ix_orders_customer_id;
  • 单独的无事务迁移,每个文件一条命令。 在事务块中禁止使用 CIC(“cannot run inside a transaction block”)。单次请求发送多条命令时,PostgreSQL 会将其视为隐式事务,导致 CIC 报相同错误。
  • 如果迁移器不支持无事务迁移,不要将 CIC 放在普通迁移中。 告知用户需要这种模式(参见“对迁移器的要求”部分),或者单独执行 CIC 然后手动记录迁移。
  • CIC 会等待所有持有旧快照的事务完成,即使是只读取其他表的事务。迁移可能会持续很长时间。
  • 应用后验证索引。 CIC 不是事务性的:失败时会留下 INVALID 索引,而迁移本身不会显示这一点。
SELECT c.relname, i.indisvalid, i.indcheckxmin
FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid
WHERE c.relname = 'ix_orders_customer_id';
  • indisvalid = false — 执行 DROP INDEX CONCURRENTLY IF EXISTS 并重新创建,或者使用 REINDEX INDEX CONCURRENTLY。
  • 重试失败的 CIC 迁移。 不使用 IF NOT EXISTS 时,在 INVALID 索引上会报“relation already exists”错误。使用 IF NOT EXISTS 更糟:命令会静默跳过 INVALID 索引,导致迁移“通过”。因此不要在 CIC 中使用 IF NOT EXISTS:先移除无效索引,再重试。

索引存在,但计划是 Seq Scan

索引有效,但优化器不使用它——检查 indcheckxmin(使用上面的查询)。true:在索引创建之前启动的事务存活期间,查询不会使用该索引。原因:UPDATE 操作在表中留下了未引用索引的 HOT 版本链。仅 INSERT 不会导致此问题。不要重建索引或查找统计信息问题。查找旧事务:

SELECT pid, xact_start, backend_xmin, state, left(query, 60) AS query
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL
ORDER BY xact_start;

等待它们完成或终止(pg_terminate_backend(pid))——仅在与用户协商后。发布前复现:在一个会话中打开 repeatable read 事务并读取表,在另一个会话中执行 UPDATE 行和 CREATE INDEX;indcheckxmin 将变为 true。

对迁移器的要求

  • 每条迁移在其自身的事务中执行,同时记录到已应用迁移日志:失败的迁移完全回滚,已应用的保持不变。
  • 防止并行执行(例如 advisory lock):同时启动的副本不会重复应用相同的迁移。
  • 对单独迁移支持无事务模式 —— 在文件中标记(例如,第一行 -- migrate:no-transaction)。此文件作为单条命令发送,绕过事务。代价:应用和日志记录不再是原子操作。如果进程在两者之间崩溃,重试会因“already exists”而失败;检查 indisvalid,如果索引有效,手动将迁移记录到日志。
  • 无需连接数据库即可生成配对文件。

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

scary-production/claude-plugins

Работа с PostgreSQL в Go через pgxpool (pgx v5) и avito-tech/go-transaction-manager: пул и устойчивый старт, структуры с тегами db и CollectRows, репозиторий через DefaultTrOrDB, транзакции через Do, propagation и savepoint, повтор транзакции при 40001/40P01/55P03, защита от SQL-инъекций, классификация ошибок и ON CONFLICT, batch и CopyFrom. Конкурентная запись и блокировки — скилл postgres-concurrency. PostgreSQL in Go via pgxpool and go-transaction-manager: pool, repositories, transactions, retries, errors, bulk loads.

scary-production/claude-plugins

Конкурентная запись в PostgreSQL без потерянных обновлений и write skew: условие в UPDATE, SELECT FOR NO KEY UPDATE против FOR UPDATE, exclusion-ограничения против пересечения интервалов, serializable и повтор при 40001/40P01/55P03, очередь задач на SKIP LOCKED, lock_timeout и NOWAIT, advisory-блокировки, разбор зависаний. Примеры — Go (pgx и go-transaction-manager), правила — про сам PostgreSQL. PostgreSQL concurrency: lost updates, write skew, row locks, exclusion constraints, isolation and retries, SKIP LOCKED queues, advisory locks.

scary-production/claude-plugins

Как найти, что тормозит в PostgreSQL, и доказать, что правка помогла. Профиль по pg_stat_statements, сравнение вариантов по числу страниц (BUFFERS), а не по секундам, разбор плана от места, где оценка строк впервые разошлась с фактом. N+1 из Go-цикла сворачиваем в один запрос. Правим условия, FK, статистику и память, а не боремся с планировщиком. Postgres query tuning: profile first, compare by buffers, fix row estimates, remove N+1.

scary-production/claude-plugins

Показывает стили строки состояния на примере или включает один из них (full, compact, bars, minimal) в настройках пользователя или проекта.

相关技能