SQL 脚本的导入顺序

六个 SQL 脚本,语法检查全过,每个都自带 USE,没有一个 DROP TABLE。按文件名 1 到 6 执行,第二个脚本停在第 248 行。

最郁闷的是,验收的每一条都是通过的。

数据库上线,通常不是单个 SQL 文件,而是一串。建表、补表、种子数据、改字段、修链接,各一个脚本,命名从 1 排到 6。

从 1 排到 6 这个命名方式,本身就在暗示你按数字顺序执行。于是绝大多数人就这么做了。

语法检查会全过,因为每个脚本单独看都没问题。可它们之间有依赖,而依赖关系不写在文件名里。

先说环境。数据库是 MySQL 8.4.11,容器镜像 mysql:8.4.11。1Panel 容器化,端口只映射到 127.0.0.1:3306。导入方式是 docker exec -i 容器 mysql < 脚本.sql,走批处理模式。六个脚本里,建表 2 个、种子 1 个、改表 1 个、修数据 1 个、补表 1 个,终态 29 张表。

命令里用变量代替容器名,先在终端跑一次:

bash 复制代码
export MYSQL_CTN="mysql84"
export DB_NAME="<你的库名>"

报错原文、缺失范围、幂等性结论都来自这次真实部署,2026 年 9 月 17 日。脚本依赖检查的输出来自我用同一批脚本做的静态扫描,9 月 19 日复核。

按文件名的数字顺序 1 到 6 执行,跑到第二个脚本,报错原文是这样的:

复制代码
ERROR 1146 (42S02) at line 248: Table '<库名>.v1_demand_i18n' doesn't exist

我当时看到 ERROR 1146 的第一反应,是某个表建漏了。后来才发现,漏的不是一张表。

重点不在那一行报错,在它后面那句没写出来的话。

mysql 客户端在批处理模式下,也就是 mysql < file.sql 这种用法,默认遇错即停,除非你显式加 --force。

所以真实的杀伤范围是,第 248 行之后的每一条语句,都没有执行。

它不只是这条语句失败。从这个位置开始,整个文件剩下的部分全部被跳过。

这就是为什么这条报错比一般的报错危险得多。它断在文件中间。

中断之后,跑一遍常规验收。表数量期望 29,实测 29。库级字符集和排序规则期望 utf8mb4 加 utf8mb4_unicode_ci,实测一致。数据抽查了三张主要表,都有数据。

我当时以为三条全过就算导入成功了。

原因很简单。中断发生在建表全部完成之后、插数据进行到一半。表结构是完整的,被抽查的那三张表恰好都在第 248 行之前插完了。

只有一条路能发现它,按行号精确核对缺失范围。

第 248 行之前已经写进去 11 张表。

服务、服务 i18n、推荐、推荐 i18n、工厂、工厂 i18n、工厂产品、

产品 i18n、工厂资质、资质 i18n、留言。

第 248 行之后缺了 6 张。

v1_demands、v1_demand_i18n、v1_articles,

v1_article_i18n、v1_notifications、v1_notification_i18n。

看一眼这 6 张,它们正好等于 3_create_phase2.sql 建的那 6 张。

依赖是这样长的。1_create_auto.sql 谁都不依赖,被 2 的 11 张表种子、4、5 依赖。3_create_phase2.sql 也不依赖别人,被 2 的 Phase 2 六张表种子依赖,陷阱在这。2_seed.sql 依赖 1 加 3,被 5 依赖。4_alter_messages.sql 依赖 1。5_update_recommends.sql 依赖 2 的数据。6_create_demand_match.sql 谁都不依赖。

2_seed.sql 里含有 Phase 2 那 6 张表的种子数据。而那 6 张表由 3_create_phase2.sql 创建。

于是正确顺序是:

复制代码
1_create_auto.sql → 3_create_phase2.sql → 2_seed.sql → 4_alter_messages.sql → 5_update_recommends.sql → 6_create_demand_match.sql

数字顺序 1 到 3 是错的。文件名上的 2 和 3,业务上是 seed 和 phase2。编号是按写脚本的时间顺序给的,不是按该执行的顺序。

这个坑的通用形态是,脚本按编写顺序编号,而不是按依赖顺序编号时,数字顺序就一定是错的。

当时动手之前,本地核实过两件事。六个脚本都自带 USE 语句,命令行不用指定库名。没有任何 DROP TABLE,不会误删数据。

两条都对。但两条都不够。

漏掉的是第三件,脚本之间的表依赖。语法全对、顺序错,照样中断。

可复用的检查方法,思路很朴素。A 类是每个脚本 CREATE TABLE 建了哪些表。B 类是每个脚本 INSERT INTO、UPDATE、ALTER TABLE 引用了哪些表。判据是把脚本按顺序走一遍,维护一个已建表集合,如果某个脚本引用了此刻还没建的表,就存在顺序依赖。

python 复制代码
import re, io

made = lambda f: set(re.findall(r"CREATE TABLE (?:IF NOT EXISTS )?`([a-z0-9_]+)`", io.open(f, encoding="utf-8").read()))
used = lambda f: set(re.findall(r"(?:INSERT INTO|UPDATE|ALTER TABLE) `([a-z0-9_]+)`", io.open(f, encoding="utf-8").read()))

ORDER = ["1_create_auto.sql", "3_create_phase2.sql", "2_seed.sql",
         "4_alter_messages.sql", "5_update_recommends.sql", "6_create_demand_match.sql"]

built = set()
for f in ORDER:
    gap = used(f) - built - made(f)
    print("%-28s 此刻还没建:%s" % (f, sorted(gap) if gap else "---"))
    built |= made(f)

把两个顺序都跑一遍,对比就出来了(实测输出,2026 年 9 月 19 日):

复制代码
【顺序】1 -> 2 -> 3 -> 4 -> 5 -> 6
   1_create_auto.sql            此刻还没建: ---
   2_seed.sql                   此刻还没建: ['v1_article_i18n', 'v1_articles', 'v1_demand_i18n',
                                              'v1_demands', 'v1_notification_i18n', 'v1_notifications']
   3_create_phase2.sql          此刻还没建: ---
   ...

【顺序】1 -> 3 -> 2 -> 4 -> 5 -> 6
   1_create_auto.sql            此刻还没建: ---
   3_create_phase2.sql          此刻还没建: ---
   2_seed.sql                   此刻还没建: ---
   4_alter_messages.sql         此刻还没建: ---
   5_update_recommends.sql      此刻还没建: ---
   6_create_demand_match.sql    此刻还没建: ---

错误顺序下,它精准点出了那 6 张表,和实际中断后缺失的 6 张完全一致。这个检查在动手前 10 秒就能跑完,本可以省掉一整轮返工。

这段脚本没用任何第三方库,纯标准库。也不需要连数据库,纯文本扫描。

发现缺数据之后,第一反应是全部重跑一遍。这一步会把小问题变成大问题。

1_create_auto.sql 不幂等。重跑会报 ERROR 1050: Table 'v1_admins' already exists 然后停止。22 条全是裸 CREATE TABLE,没有 IF NOT EXISTS。

2_seed.sql 幂等,脚本里先 DELETE 再 INSERT,重跑正常。

3_create_phase2.sql 幂等,6 条都带 CREATE TABLE IF NOT EXISTS。

4_alter_messages.sql 不幂等。重跑报 ERROR 1060: Duplicate column name 'scene_type'。

5_update_recommends.sql 幂等,UPDATE 本身幂等。

6_create_demand_match.sql 幂等,带 CREATE TABLE IF NOT EXISTS。

所以真正需要重跑的只有两个。2 是补数据,5 是因为 2 重插了推荐表数据之后,链接修正需要重新应用。

有一点要记住,那两个报错是预期行为。1 和 4 重跑时必报错,因为它们的产物已经存在了。看到 ERROR 1050 或者 ERROR 1060 就以为又坏了,会把自己带进沟里。

另一点,4 为什么没有 IF NOT EXISTS 可用。因为 MySQL 不支持 ADD COLUMN IF NOT EXISTS。这是 MySQL 相对 PostgreSQL 的一个长期缺口,加列语句天生不幂等,只能靠人工判断该不该跑。

顺带一个设计建议。新建表一律用 CREATE TABLE IF NOT EXISTS,成本极低。本例里 3 和 6 用了,重跑毫无压力;1 没用,重跑必挂。

按修正后的顺序完整重跑一遍,六个脚本全部无报错。

相关推荐
lusklusklusk11 小时前
Python 巡检脚本:进程运行状态、数据库写入时效检查及邮件通知与告警
数据库·python·oracle
GeW11 小时前
产业新优势基础软件的人才底盘,从Linux与数据库开始
linux·数据库
SelectDB11 小时前
Doris Arrow Flight SQL 高速读取协议实战:与 Snowflake 客户端取数链路对照
大数据·数据库·数据分析
曼岛_11 小时前
[图解密码学]第4章 古典密码
java·数据库·密码学
瀚高PG实验室12 小时前
HGDB V9.0.5 .pgpass 免密登录提示 IsEncryptNon-base64 encoding 警告处理
数据库·postgresql·瀚高数据库
龙腾AI白云13 小时前
大模型检索增强生成(RAG)的技术原理与架构设计
数据库·python·机器学习·知识图谱
KaiwuDB13 小时前
KaiwuDB 运维实战06:数据库备份与恢复:从理论到实战
运维·数据库·时序数据库·kaiwudb
数字新视界13 小时前
空调控制器AIRC800的智能化管理与应用实践
大数据·服务器·数据库·动环监控系统·动力与环境监控系统
学IT的周星星14 小时前
LangGraph:用「流程图」的方式编排 Agent
数据库·人工智能·大模型·llm·流程图·agent
吃一碗粉只付一碗钱14 小时前
10-令牌里的用户主键不要明文
服务器·数据库·sql·sqlserver·c#