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 没用,重跑必挂。

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

相关推荐
代码什么用1 小时前
Spring对IoC的实现
数据库·spring
hz567894 小时前
涉密视频会议设备配置指南:终端、音视频采集与配套设施选型
服务器·网络·数据库·安全·实时音视频·信息与通信·智能硬件
广州浮点FLOATLIC4 小时前
许可证服务器迁移后软件打不开:研发 IT 怎样定位连接问题
linux·服务器·数据库
程序员Sunday4 小时前
MySQL 为什么使用 B+ 树索引?把范围查询、回表和覆盖索引连起来
数据库·mysql
半杯咖啡半行码4 小时前
Qt开发实战:数据库、MV 模式、QProcess与串口通信全攻略
数据库·qt
小米里的大麦4 小时前
16 MySQL 事务
数据库·mysql
西柚小萌新4 小时前
【LLM&&AI应用开发 八股文】--4.3.Agent智能体(下)
java·开发语言·数据库
邓工说电5 小时前
智慧断路器安全吗?数据加密、离线保护与合规认证全解读
大数据·数据库·人工智能·智能断路器·炜晔科技
adinnet20266 小时前
企业微调大模型(Fine-tuning):先吃透 RAG 还是先训自己的模型
大数据·数据库