本篇学习指南
读完你能
  • 按规范命名 SQL 脚本(<序号>_<语义>.sql)并放进镜像分支树的对应目录
  • 写出「跑多少遍结果都一样」的幂等脚本
  • 知道改完数据库结构后,要登记到 CHANGELOG 并走现场流程
阅读时长
约 8 分钟 · 改数据库前必读
前置知识
会写基本 SQL;先读 01 了解版本
打个比方:增量脚本与幂等
增量脚本就像装修——只能「加新墙、装新柜子」,不能拆旧承重墙(不能 DROP 已有列、不能改已有数据语义),否则旧版本代码跑不动。幂等的意思是「这脚本跑 1 遍和跑 10 遍结果一样」——就像装插座,已经装过的地方不会再装第二个。所以脚本开头要先判断「这列是不是已经加过了」,加过就跳过,这样现场误操作重复执行也不会出事。
概念入门
理解 SQL 版本化
幂等
执行多次和执行一次效果相同。比如 INSERT 前先检查是否已存在,就不会重复插入。这是增量脚本的核心要求。
增量 vs 全量
全量建库是从零建整个库;增量脚本只做「加法」(新增列、新增表),不破坏已有数据。
CHANGELOG
变更日志,记录每个脚本的执行情况。现场部署时按顺序执行脚本,并在 CHANGELOG 中登记,保证可追溯。
增量脚本就像搭积木 —— 只往上加,不拆下面的
全量建库(2.1.0 基线)
从零开始建整个库
新现场首次部署时用一次
增量脚本(2.1.1、2.2.0…)
2.1.0 基线
2.1.1 +新增列
2.2.0 +新功能
每次只加新东西,不破坏旧数据
1
SQL 版本化规范
4.1 目录结构:与分支同构

在仓库根目录建 sql/,目录树镜像分支树:分支叫什么路径,SQL 就放同路径目录。SQL 在 feature 分支目录中生长(hotfix 分支禁改库,见 03·规则三),最终汇总进 sql/release/<版本>/;打包发版当天冻结为 sql/tag/<版本>/ 最终版(以下为 EasyPro 实际结构,其他产品同理):

sql/
release/ # 各版本交付 SQL 的汇总终点
2.1.0/ # 首个规范化交付:全量建库 + 增量(历史命名沿用)
ibms-system.sql …(9 个库全量建库)
upgrade_2.1.0.sql / upgrade_2.1.0_bill.sql
2.1.1/ # 2.1.1 起按主题汇总,数字序号即执行顺序
0_common.sql · 1_mqtt.sql · 2_app.sql · 3_modbus.sql
4_bacnet.sql · 5_zhiwen.sql · 6_knx.sql · 7_rcp.sql
feature/
2.1.1/<语义>/ # feature 开发期草稿,合入时汇总进 release
tag/
2.1.0/ # 打包发版当天冻结的最终版(安装包附带),此后不改
CHANGELOG.md # 版本登记表(唯一权威:顺序、现场登记)

两个「不产出」:hotfix 分支不产出 SQL(03·规则三禁改库);定制 SQL 一律放 doc/定制/<客户>/(03·定制 SQL 隔离),禁止混入通用 sql/:通用目录按版本顺序执行到所有现场,混放会导致其他现场误执行定制脚本。

目录即归属:看路径就知道脚本属于哪条分支、处于哪个阶段;托管平台上与分支列表一致,按前缀自动折叠聚拢。

4.2 分支 ↔ 目录映射与生命周期
分支 / 节点SQL 目录定位
release/<版本>sql/release/<版本>/该版本交付 SQL 的汇总终点:feature 的 SQL 最终都汇入这里
feature/<版本>/<语义>sql/feature/<版本>/<语义>/开发期草稿,合入 release 时按主题汇总进对应序号文件
hotfix/<版本>/<语义> 不产出hotfix 禁改库(03·规则三);已发布版本需改库走 custom,SQL 放 doc/定制/
custom/<版本>/<语义> 不入 sql/定制 SQL 一律放 doc/定制/(03·定制 SQL 隔离),禁止混入通用
dev/<人名>sql/dev/<人名>/(可选)个人试验脚本;不合入则随分支一并废弃
产品线四段式前缀后插产品代号段如 sql/feature/eos/1.1.0/<语义>/、sql/line/eos/1.0.0/(子产品用自身版本号)
tag vX.Y.Z(打包)sql/tag/<版本>/打包最终版:打包当天由 release 目录冻结而来,安装包附带

生命周期规则:

  1. 创建:规划命名的分支创建时即建同名占位目录;暂无脚本时放一行 README.md 占位锚定目录,写明对应分支与汇总去向。
  2. 汇总:feature 的 SQL 合入 release 时按主题归入对应序号文件(同主题追加到已有序号文件末尾,新主题新建下一序号文件);hotfix 分支不产出 SQL。
  3. 序号冲突:并行分支出现同序号不同主题时,后合入者改取下一可用序号。
  4. 发版打包:打 tag 当天,把 sql/release/<版本>/ 全量复制为 sql/tag/<版本>/(检查项:该版本的 feature 目录应已全部合入清空)。此后 tag 目录冻结——安装包附带 tag 目录 SQL,现场按序号顺序执行。
  5. 发版后:已打包版本的 tag 目录不回改;修正请改 sql/release/<版本>/ 并走下个版本 / hotfix(不改库)/ 维护性微版本流程,通用 DB 修复经回流进入下个开发中版本的汇总。
  6. 删除:分支合并删除时,其 SQL 已汇总/回流,对应分支目录一并清理,不在 sql/ 留空壳。
  7. 基线:2.1.0 为首个规范化交付版本,目录内含全量建库(ibms-*.sql)+ 增量升级;新现场从 2.1.0 全量起步,老现场按版本逐级升级。

文档侧有一套同构规范:目录同样镜像分支树、生命周期与分支联动、doc/README.md 为唯一入口——但合入策略相反(文档原地保留、不汇总)。见 10·文档目录规范。

4.3 脚本命名规则

2.1.1 起统一按主题分文件、数字序号定执行顺序:

CODE
<序号>_<语义>.sql
  • 序号从 0 起,在同一 sql/release/<版本>/ 内递增,数字即执行顺序(CHANGELOG 登记顺序须与之一致)。
  • 语义用英文小写 + 下划线,禁止中文、空格;一个驱动/模块一个序号文件(如 0_common.sql、3_modbus.sql)。
  • 同一主题的后续变更追加到已有序号文件末尾,不新开文件;新主题才取下一序号。
  • 2.1.0 目录的 ibms-*.sql(全量建库)、upgrade_2.1.0*.sql、example.sql 为历史命名,沿用不追溯。

示例:

  • 0_common.sql、6_knx.sql
  • V2.1.1.1__modbus_write_function_code.sql (V 前缀旧式已弃用,目录名已含版本号)
  • 2.1.1增量.sql (中文、无序号)
  • update.sql (无序号、无语义)
4.4 增量原则(除 2.1.0 全量建库外所有脚本)

核心要求:增量修改,绝不破坏原有数据。

允许 禁止
ALTER TABLE ... ADD COLUMNDROP TABLE
ALTER TABLE ... ADD INDEXDROP COLUMN(确需时走安全流程)
INSERT(带幂等保护)TRUNCATE TABLE
UPDATE(迁移数据)DELETE 全表
CREATE TABLE IF NOT EXISTS直接 DROP 后重建

幂等性要求:每个增量脚本必须可重复执行而不报错、不重复插入。标准做法:

  • 列存在性判断:IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE ...)
  • 数据存在性判断:游标遍历 + IF NOT EXISTS 插入
  • 沿用已验证可靠的「游标遍历 + 幂等保护」成熟范式(现汇总于 sql/release/2.1.1/3_modbus.sql,可直接参考)。
幂等脚本(跑 N 遍都安全)
SQL
-- 加列前先判断是否已存在
IF NOT EXISTS (
  SELECT 1 FROM information_schema.columns
  WHERE table_name='iot_driver'
    AND column_name='protocol_type'
) THEN
  ALTER TABLE iot_driver
    ADD COLUMN protocol_type VARCHAR(32);
END IF;
重复执行时 IF NOT EXISTS 命中,直接跳过,不报错
非幂等脚本(跑第二遍就崩)
SQL
-- 直接加,不管列在不在
ALTER TABLE iot_driver
  ADD COLUMN protocol_type VARCHAR(32);

-- 第二遍执行报错:
-- Duplicate column name 'protocol_type'
现场误重跑、脚本中断后重试,都会踩这个坑

破坏性变更的安全流程(确需删列/改类型):

  1. 先在新列写入数据(兼容期)。
  2. 发版切换读写。
  3. 下一版本再删旧列(此时数据已迁移)。
  4. 删列脚本必须附 doc/规则/ 备案与回滚脚本。

为什么破坏性变更要分好几步,不能一把梭?因为现场不是所有人同时升级。A 客户升到新版、B 客户还在跑旧版——如果你一个脚本里既加新列又删旧列,B 客户的旧代码还在 SELECT 旧列,当场报错崩溃。分步走(先加、再切、后删)保证新旧版本都能正常运行,给现场留出升级窗口期。这是「向前兼容」原则。

4.5 标准模板(每个增量脚本)
SQL
-- =============================================================
-- <版本号> <功能简述>
-- 功能:<详细说明,多个要点用 1.2.3.>
-- 影响库表:<库名>.<表名>
-- 前置条件:<依赖的前置脚本/数据状态>
-- 幂等说明:<如何保证重复执行不报错>
-- 作者:<姓名> 日期:YYYY-MM-DD 关联版本:<release/x.x.x>
-- =============================================================

USE `ibms-iot`;

-- 步骤 1:结构变更(带幂等保护)
ALTER TABLE `iot_driver_attribute`
  MODIFY COLUMN `id` bigint(20) NOT NULL AUTO_INCREMENT FIRST;

-- 步骤 2:数据变更(游标遍历 + 幂等保护)
DELIMITER $$
DROP PROCEDURE IF EXISTS proc_upgrade_xxx$$
CREATE PROCEDURE proc_upgrade_xxx()
BEGIN
    -- 游标定义、遍历、IF NOT EXISTS 插入 ...
END$$
DELIMITER ;

CALL proc_upgrade_xxx();
DROP PROCEDURE IF EXISTS proc_upgrade_xxx;

该模板源自已验证可靠的「游标遍历 + 幂等保护」范式(现汇总于 sql/release/2.1.1/3_modbus.sql)。实际落库示例可参考 sql/release/2.1.1/7_rcp.sql 的头部注释——来源分支、汇总去向、作用与幂等说明一应俱全:

SQL
-- 来源:doc/驱动设计文档/RCP驱动/工程同步机制/RCP驱动_属性升级SQL.sql
--        (feature/2.1.1/Rcp同步工程文件机制 合入 release/2.1.1 时按规范汇总归档,序号 7)
-- 作用:rcp 驱动新增 6 个属性模板……幂等可重复执行
增量脚本的标准结构
头部注释
版本号 · 功能说明 · 影响库表 · 作者 · 日期
↓
步骤 1:结构变更
ALTER TABLE · CREATE TABLE IF NOT EXISTS
幂等
↓
步骤 2:数据变更
游标遍历 + IF NOT EXISTS · INSERT / UPDATE
幂等
↓
步骤 3:清理
DROP PROCEDURE IF EXISTS(删除临时存储过程)
核心原则:只做加法,不破坏旧数据 · 每个步骤必须幂等(可重复执行不报错)
4.6 CHANGELOG.md 登记表

sql/CHANGELOG.md 作为版本登记表,记录每个脚本的执行情况(唯一权威:顺序、现场登记):

字段说明
版本对应 release/x.x.x
脚本文件名(含 sql/ 下目录路径,如 release/2.1.1/3_modbus.sql;打包后在 sql/tag/<版本>/ 存同名冻结副本)
类型结构变更 / 数据变更 / 兼容
影响表<库>.<表>
作者姓名或工号
日期YYYY-MM-DD
现场执行现场名 + 执行日期(多现场分行登记)

现场执行流程:

  1. 现场拿到新版本安装包后,按 sql/tag/<目标版本>/ 目录下的脚本顺序执行;新现场首次部署先执行 sql/release/2.1.0/ 全量建库;老现场跨版本升级,从自己所处版本之后的 tag 目录起逐版本执行到目标版本。
  2. 每执行完一个脚本,在 CHANGELOG.md 对应行的「现场执行」列登记。
  3. 执行顺序严格遵循 CHANGELOG 的登记顺序(结构变更优先于依赖它的数据变更);历史上手工执行过的脚本重复执行无害(幂等),现场登记以首次执行为准。
新手常见错误
SQL 直接作用在客户现场的真库上,出错往往没有回头路
我在 Navicat 里直接改了表结构,没写脚本,有问题吗?
大问题。只有你本地的库变了,其他同事和现场都不会变。数据库变更必须写成版本化脚本(<序号>_<语义>.sql,放进对应分支的镜像目录),提交到仓库,这样所有人 pull 后能同步、现场升级时能执行。直接在工具里改 = 改动丢失,是 SQL 规范的头号禁忌。
我的脚本写成了 ALTER TABLE ADD COLUMN,现场跑了两次,多加了一列?
这就是非幂等——跑两遍就出错。正确写法是先判断列是否存在:IF NOT EXISTS (SELECT ... FROM syscolumns ...) 再 ADD。幂等原则保证脚本重复执行也安全,因为现场运维人员可能误操作、脚本可能因中断被重跑。
我 DROP 了一个没用的旧列,清理一下,这样对吧?
非常危险。旧版本代码可能还在 SELECT 那一列,DROP 后现场如果跑旧代码会直接报错崩溃。增量脚本的原则是「只加不减不改语义」——废弃的列标记弃用、保留不动,等确认所有版本都不再用时才能清理。破坏性变更要单独评估。
我加了脚本但忘了登记 CHANGELOG,会怎样?
现场运维不知道这个版本有哪些 SQL 要执行,可能漏跑,升级后代码找不到对应的表结构,启动报错。每个脚本必须登记到 CHANGELOG.md,写清版本、描述、执行顺序,这是现场升级流程的依据。
之前不是 V 开头命名吗(V2.1.1.1__xxx.sql),为什么现在不让用了?
V 前缀是早期借鉴迁移框架的写法,但本项目并没有用框架自动执行 SQL,版本信息已由目录名(sql/release/2.1.1/)表达,文件名再带一遍属于冗余。2.1.1 起统一为 <序号>_<语义>.sql:序号即执行顺序,语义即主题,一眼可读。旧文件(2.1.0 目录的 ibms-*.sql、upgrade_2.1.0*.sql)沿用不追溯。
我在 hotfix 分支上顺手加了一列,改完才发现规范说 hotfix 禁改库,怎么办?
把这段 DB 变更从 hotfix 分支摘出来。分三种去向:只服务某个现场 → 搬到该客户的 custom/ 定制分支,SQL 放 doc/定制/<客户>/;所有现场都需要的通用变更 → 进下个开发中版本的 sql/release/<版本>/ 对应序号文件;多个现场紧急需要 → 走维护性微版本(03·规则五)。hotfix 本身只改代码,保持「现场快速修复、不跑升级脚本」的节奏。