跳到主要内容

2 篇博文 含有标签「SQL」

查看所有标签

PostgreSQL 数据库迁移后留下废弃空表?用 information_schema 审计 schema 残留

· 阅读需 7 分钟

在一次广告数据源从日表切换到周表后,我发现 schema 里还躺着一张只在最早期的 migration 里建过、0 行数据、运行时代码从不引用的空表——还带着过时的字段名和没规范化的中文列。

在开发 AI运营 时遇到此问题——基于大语言模型的智能分析,自动洞察市场趋势、用户行为、销售数据,提供精准运营策略。这次数据源切换后,写入端早已迁到新的周表,旧日表的清理 migration 也补了,唯独一张只在 baseline 里 CREATE 过的月表被遗漏——它既没有对应的新写入,也没有 DROP,就那样潜伏在 schema 里,带着早已废弃的字段定义。

TL;DR

废弃表的典型特征:只在早期/baseline migration 里 CREATE、当前代码 0 引用、常带旧字段或未规范化的列名。批量迁移时它们不会被自动处理,需要主动用 information_schema.tables 列出 schema 全表,再与代码引用比对,定位 orphan 表后写一条 DROP migration 清理——而不是手动 psql 删完就了事。

问题现象

一张典型的废弃表长这样:

  • 0 行数据——业务早已不再写入它;
  • 0 运行时引用——代码里 grep 不到任何 SELECT/INSERT,只剩 migration 文件里的 CREATE
  • 旧字段残留——字段名是上一版命名(如 ad_plan_id/product_id),与当前规范不一致;
  • 未规范化的列名——甚至还有中文列名没来得及改。

它不报错、不影响线上运行,所以从「线上没出问题」的视角完全无感。但它的危害是隐性的:误导后来者以为它仍在用、占用 schema 命名空间、在跨表审计时制造噪音,还可能被某个误判的 SELECT * 意外读到脏数据。

根因

数据库迁移有一个普遍的模式:migration 是「加法」的

一次数据源切换通常这样演进:

  1. 早期 baseline migration CREATE 了一批表(日表、月表);
  2. 业务跑通后,写入端开始依赖这些表;
  3. 需求变化,引入新表(周表),写入端逐步迁移过去;
  4. 旧表的写入停了,补一条 migration DROP 旧日表;
  5. 但月表/其他只在 baseline 建过、从未被写入端直接引用的表,没有对应的 DROP

问题出在第 5 步:迁移注意力集中在「现在用到的表」上——哪些表在写入、哪些 SQL 在查。而「曾经存在、但从未进入主链路」的表既不在写入端、也不在查询端,自然不会触发任何 DROP,于是成了 orphan。这类残留和 Airflow 删除 DAG 后元数据残留 是同一类问题:「删了入口、忘了清结构」,是迁移类问题的高发区。

解决方案

核心流程:列全表 → 比对引用 → 确认空表 → 写 migration DROP → 验证

步骤 1:用 information_schema 列出 schema 下所有基础表

-- 列出某 schema 下所有基础表(排除视图)
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'your_schema'
AND table_type = 'BASE TABLE'
ORDER BY table_name;

information_schema.tables 是 SQL 标准目录视图,跨 PostgreSQL/MySQL/SQL Server 通用,字段稳定,非常适合写进审计脚本。

步骤 2:grep 代码库确认运行时引用

对每张候选表,在代码库里搜索引用,排除 migration 文件本身

# 搜索运行时代码引用,排除 migrations 目录
grep -rn "ad_product_monthly_stats" src/ --include="*.py" \
| grep -v "migrations/"
# 0 行输出 → 运行时无引用,进入候选

0 引用是判定 orphan 的关键证据。注意一定要排除 migration 目录——baseline 里的 CREATE 不算「引用」。

步骤 3:确认是空表

SELECT count(*) FROM your_schema.ad_product_monthly_stats;
-- 0 → 确认无数据,可安全清理

对有数据的表要格外谨慎:先确认它真的废弃(而非只是近期没写入),有疑问就先做逻辑备份再处理。

步骤 4:写一条 migration DROP(而非手动删)

-- db-migrations/{project}/027_drop_ad_product_monthly_stats.sql
DROP TABLE IF EXISTS your_schema.ad_product_monthly_stats;

务必走 migration 文件:它会被版本控制、在所有环境(开发/预发/生产)一致重放,留下审计轨迹。手动 psql 删一次,换台机器就又长回来了。

步骤 5:验证已删除

SELECT to_regclass('your_schema.ad_product_monthly_stats');
-- 返回 NULL 表示表已不存在

to_regclass() 是验证表是否存在的标准手段,返回 NULL 即确认删除成功。

批量审计:按前缀一次性排查同类遗漏

单张表清掉后,按前缀把同类表全部列出来逐个核对,避免「清了一张、漏了兄弟」:

-- 列出某前缀下所有表,逐个走 步骤2-5
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'your_schema'
AND table_name LIKE 'ad_%'
ORDER BY table_name;

注意事项

  • DROP 前先备份/快照:生产库删表不可逆。对任何有数据的表,先确认废弃再做逻辑备份(如 CREATE TABLE ... AS SELECT 导出到归档库)。
  • 外键依赖要排查:如果有其他表的外键指向它,DROP TABLE 会失败。确认依赖已解除或有意 CASCADE——但 CASCADE 会连带删除依赖对象,生产环境慎用。
  • 走 migration,不要手动 psql:手动删除只在当前环境生效,迁移文件才能保证多环境一致并留下记录。
  • 用前缀批量审计:一次切换通常涉及一组同前缀的表(如 ad_*),清完一张后用 LIKE 'ad_%' 把兄弟表都过一遍,主动发现同类遗漏。

常见问题

怎么列出 PostgreSQL 数据库中的所有表?

information_schema.tables,过滤 table_schematable_type = 'BASE TABLE',即可列出某 schema 下所有基础表。它比 psql 的 \dt 更适合写进脚本做自动化审计,且是 SQL 标准、跨数据库通用,代码可移植性更好。

PostgreSQL 怎么找出没被使用的废弃表?

information_schema.tables 列出全部表,再与代码库或查询日志的引用做比对,运行时代码 0 引用且无写入的表即为废弃候选;空表可进一步用 SELECT count(*) 确认行数,确认无数据、无外键依赖后再写 migration DROP 清理。

information_schema 和 pg_catalog 有什么区别?

information_schema 是 SQL 标准定义的目录视图,跨 PostgreSQL/MySQL/SQL Server 通用、字段稳定不易变,适合写可移植的审计脚本;pg_catalog 是 PostgreSQL 专有目录,信息更全更细(如精确行数估算、存储细节),但版本间可能调整。做通用 schema 审计优先用 information_schema

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询

PostgreSQL ON CONFLICT 报 there is no unique constraint?改唯一键后 INSERT 必须同步

· 阅读需 6 分钟

在为某张表收紧唯一键(移除一个不再区分数据的列)之后,原本正常的 UPSERT 写入立刻批量报错——there is no unique or exclusion constraint matching the ON CONFLICT specification

在开发 AI运营 时遇到此问题——基于大语言模型的智能分析,自动洞察市场趋势、用户行为、销售数据,提供精准运营策略。

TL;DR

PostgreSQL 的 ON CONFLICT (cols) 要求 cols 精确匹配一个已存在的唯一约束或唯一索引(列与顺序都要一致,否则错误码 42P10)。一旦你 ALTER 了唯一键,所有引用它的 INSERT ... ON CONFLICT 必须同步修改;而且 migration 跑完后写入端要立刻部署,中间窗口会持续报错。

问题现象

唯一键改造一上线,定时导入任务全量失败,写库日志只剩这一条:

ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification
SQL state: 42P10

业务表 0 行写入,但同一张表的其它纯 SELECT 查询完全正常——问题只出在带 ON CONFLICT 的写入路径上。

根因

ON CONFLICT (cols) 里指定的列集叫仲裁器(arbiter)。PostgreSQL 要求它精确匹配表上某个 UNIQUE 约束或唯一索引:

  • 列的集合必须相同;
  • 列的顺序也要相同;
  • 如果是带 WHERE 的部分唯一索引(partial unique index),ON CONFLICT 还要带上相同的 WHERE

找不到匹配项时,PostgreSQL 不知道用哪个索引来判断"冲突",于是抛出 42P10。

典型触发场景是收缩唯一键:原先唯一键含 3 列,你发现其中一列(比如 audience)的 4 个取值对应的指标行 100% 全等、纯属冗余,于是把唯一键降到 2 列。这是对的优化方向,但旧的 INSERT 仍写着 ON CONFLICT (c1, c2, c3),而表上只剩 (c1, c2) 的唯一约束——仲裁器找不到落点,报错。

旧唯一键: UNIQUE (store_id, metric_key, audience)
新唯一键: UNIQUE (store_id, metric_key)

旧 INSERT: ON CONFLICT (store_id, metric_key, audience) ← 找不到匹配

解决方案

下面是最小复现,建表、触发、修复一条龙,可直接在 psql 里跑:

-- 1. 带 3 列唯一键的表
CREATE TABLE daily_metric (
store_id TEXT NOT NULL,
metric_key TEXT NOT NULL,
audience TEXT NOT NULL,
value NUMERIC,
CONSTRAINT daily_metric_unique UNIQUE (store_id, metric_key, audience)
);

-- 2. 旧 UPSERT:ON CONFLICT 含 audience
INSERT INTO daily_metric (store_id, metric_key, audience, value)
VALUES ('s1', 'revenue', 'visitor', 100)
ON CONFLICT (store_id, metric_key, audience)
DO UPDATE SET value = EXCLUDED.value;

-- 3. 收缩唯一键:移除 audience
ALTER TABLE daily_metric
DROP CONSTRAINT daily_metric_unique,
ADD CONSTRAINT daily_metric_unique_new UNIQUE (store_id, metric_key);

-- 4. 再跑第 2 步的 INSERT,立刻报错 ↓
-- ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification

修复就是把 INSERTON CONFLICT 列同步收缩到 2 列;既然 audience 不再区分数据,写入端干脆把它的值固定为字面量,避免按入参凭空拼出多行:

INSERT INTO daily_metric (store_id, metric_key, audience, value)
VALUES ('s1', 'revenue', 'visitor', 100)
ON CONFLICT (store_id, metric_key) -- ← 同步收缩
DO UPDATE SET value = EXCLUDED.value;

真正容易踩的是部署顺序,不是 SQL 本身:

  1. 先发 migration(DROP 旧约束 + ADD 新约束);
  2. 紧接着发布写入端代码(INSERTON CONFLICT 改为 2 列);
  3. 两步之间不要留间隔——旧代码撞新 schema 必报 42P10,新代码撞旧 schema 同样报 42P10(找不到 2 列的唯一约束)。

如果你用 Drizzle 这类 ORM,ON CONFLICT 的列一旦在 sql 模板里写死,改 schema 时极易漏改——schema 与写入端不同步的代价,在另一篇 Drizzle + PostgreSQL 的坑里也领教过。

注意事项

注意事项

  • 列顺序敏感ON CONFLICT (a, b)UNIQUE (b, a) 不算匹配,顺序必须一致。
  • 部分唯一索引要带 WHERE:若仲裁器是 UNIQUE ... WHERE activeINSERT 里要写成 ON CONFLICT (cols) WHERE active DO ...,否则同样报 42P10。
  • 只想"冲突就跳过":用不带列的 ON CONFLICT DO NOTHING,它不指定仲裁器、无需匹配任何具体索引,能捕获所有冲突。
  • 灰度并存:新老版本写入端可能短暂共存,确保两套代码都能匹配当前 schema,或让 migration 与代码同步上线、不留窗口。

常见问题

PostgreSQL ON CONFLICT 必须有唯一约束吗?

只有指定列时才必须。ON CONFLICT (cols)cols 要精确对应一个已存在的 UNIQUE 约束或唯一索引,否则报 42P10。如果你只想"有任何冲突就跳过"、不关心具体哪个约束,用不带列的 ON CONFLICT DO NOTHING,它不需要匹配特定索引。

PostgreSQL ON CONFLICT 可以指定多个唯一约束吗?

不能。单条 INSERTON CONFLICT 只能指定一个仲裁约束(一个列集,或一个索引名)。表上可以有多个唯一键,但一条语句只能选其一做冲突判定。需要按不同唯一键分别处理时,要么拆成多次写入,要么在应用层先查再决定 INSERT 还是 UPDATE

报 there is no unique or exclusion constraint matching the ON CONFLICT specification 怎么办?

这是错误码 42P10,含义是 ON CONFLICT 指定的列集在表上找不到匹配的唯一索引。按顺序排查:确认存在覆盖这些列的 UNIQUE 约束、列与顺序完全一致、最近改过唯一键后 INSERT 已同步更新;如果用的是带 WHERE 的部分唯一索引,ON CONFLICT 还要补上相同的 WHERE 子句。

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询