写数据库表结构这件事,看起来是所有开发的基本功,但很多线上事故、性能瓶颈、改表痛苦,其实在最初建表那一刻就已经埋下了。我见过太多人随手CREATE TABLE一把梭,字段类型不讲究,约束乱加,等业务跑起来才发现索引建错、数据类型扛不住、删字段要锁表一小时。这篇文章我打算把建表阶段值得注意的细节系统梳理一遍,整理成18个可以落地的小技巧,覆盖字段类型、约束设计、扩展性、工程规范几个层面。不扯理论,全是实践中验证过的经验。
1. 先把字段类型和长度定明白——建表最容易忽略的地基
很多人建表时对字段类型的理解停留在"能存就行"的层面,但实际上类型选错,轻则浪费存储空间,重则让索引失效、计算出错、后期迁移要命。这一节先捋一遍最基础也最关键的几个选择。
1.1 技巧1:整数类型选型,别为了省空间委屈自己
整数几乎是每张表都有的字段类型,常见的有TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT。区别在于占用字节数和取值范围,我用一张表把对应关系列清楚:
| 类型 | 字节数 | 有符号范围 | 常见用途 |
|---|---|---|---|
| TINYINT | 1 | -128 ~ 127 | 状态码、开关值 |
| SMALLINT | 2 | -32768 ~ 32767 | 枚举值、小范围计数 |
| MEDIUMINT | 3 | -8388608 ~ 8388607 | 中等计数 |
| INT | 4 | -2147483648 ~ 2147483647 | 常规业务ID |
| BIGINT | 8 | -9223372036854775808 ~ 9223372036854775807 | 雪花ID、大表主键 |
一个很常见的坑:默认用INT做主键,时间一长数据量涨到几十亿,INT上限到不了,只能改表,而改主键类型在MySQL里是要重建表的,几千万行数据直接锁表几小时。我建议主键字段直接上BIGINT,尤其在分库分表、使用分布式ID方案的场景下,这几乎是必须的。反过来,如果字段只是存性别、状态码、删除标记这类个位数取值的,用TINYINT就够了,不要每个字段都上INT,一张表几十个字段,累积下来空间差异非常明显。
另外注意MySQL 8.0.17开始已经废弃了INT(11)这种显示宽度的写法,以后不要再用INT(11)去限制显示长度了,它不影响存储范围,只影响显示补零,现在写出来反而会触发警告。
1.2 技巧2:金额字段用decimal,别碰float/double
涉及金额、价格、费率、积分这类需要精确计算的字段,一律使用DECIMAL,严禁使用FLOAT或DOUBLE。原因很简单:FLOAT和DOUBLE是浮点数,存储的是近似值,在加减乘除时会出现精度丢失。举个最直观的例子,0.1 + 0.2用浮点数算出来不是0.3,而是0.30000000000000004。金额算错一分钱,财务对不上账,这种事故我见过不止一次。
DECIMAL是定点数,按十进制存储,能够精确表示小数。定义时有两个参数要确定:总位数和小数位数。比如DECIMAL(10,2)表示总长度10位,其中小数2位,整数部分最多8位。对于绝大多数业务,金额用DECIMAL(10,2)够用,如果涉及汇率、利率这种精度要求更高的场景,考虑DECIMAL(18,4)或更长的位数。
还有一点容易被忽略:不同数据库的DECIMAL实现有细微差别,但MYSQL、Oracle、PostgreSQL都支持标准用法。不要试图用BIGINT存"分"来规避精度问题,虽然可行,但查询时到处除以100,代码可读性和维护成本都吃亏。
1.3 技巧3:varchar长度别随手写255
字符串字段的长度规划,是个很低调但影响面很大的细节。先说一个大原则:长度按业务真实约束来定,不要每个字符串字段都写VARCHAR(255)。
原因有两个层面。第一层是存储空间:VARCHAR是变长字符串,虽然超长部分不会直接占满,但当你给它定义255时,MySQL内部要按最大长度去做排序和临时表操作时的内存分配。第二层是索引:在InnoDB中,单列索引长度有限制,早期版本是767字节,新版本是3072字节。在utf8mb4字符集下,一个字符最多占4字节,如果字段定义过长,比如VARCHAR(255),255×4=1020字节,单列索引能撑住,但如果你想在多个大字段上建联合索引,就很容易超出索引长度限制,只能被迫缩短字段或者用前缀索引。
实操建议:手机号用VARCHAR(20),邮箱用VARCHAR(100),用户名根据产品逻辑定,一般VARCHAR(50)就够,地址、备注这类内容可能较长的,用VARCHAR(255)或直接上TEXT,URL用VARCHAR(2048)。核心原则是:先想清楚这个字段真实的最大长度,再往上留20%到50%余量即可。
1.4 技巧4:时间字段别用varchar存,按精度选类型
很多新手会把时间戳直接存成字符串,比如"2024-03-15 14:30:00",甚至存Unix时间戳的字符串形式。这种做法非常不推荐,原因很直接:无法用数据库自带的时间函数做区间统计、排序、格式化,索引效率也比原生时间类型差,还容易因格式不统一产生脏数据。比较不同格式的字符串,比如"2024-3-5"和"2024-03-05"排序结果会是乱的。
时间字段的选择,主要看精度需求:
- 只需要日期,用
DATE,占用3字节,格式是2024-03-15。 - 需要精确到秒,用
DATETIME或TIMESTAMP,占用5到8字节(MySQL 5.6.4之后支持小数秒,占用空间随小数位变化)。 - 需要毫秒/微秒,在
DATETIME后加精度参数,比如DATETIME(3)。
DATETIME和TIMESTAMP的区别值得单独说:TIMESTAMP是4字节,范围只有1970年到2038年,存储时会按当前时区转成UTC,查询时再转回当前时区,适合记录操作时间且有时区转换需求的场景;DATETIME是8字节,范围1000年到9999年,存什么就是什么,不随时区变化。我的经验是,业务系统里统一用DATETIME更省心,避免时区转换带来的隐性bug,只有明确需要跨时区统一换算时才选TIMESTAMP。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 约束、默认值与索引:从源头挡住脏数据
数据质量不是靠应用层代码保证的,而是靠数据库约束从入口拦截。很多人建表时不加约束,觉得"后面在代码里判断就行",结果就是线上数据各种脏:状态字段出现未定义值、必填字段变NULL、重复数据涌入。这一节讲的都是建表时该做好的防线。
2.1 技巧5:主键设计,自增还是业务主键要想清楚
主键是表设计里最核心的一个决策点。常见方案有三种:
- 自增主键:优点是写入性能好,顺序插入利用InnoDB聚簇索引特性,页分裂概率小;缺点是分布到多库多表时可能重复,且业务数据可见,容易被爬虫遍历。
- 雪花ID/分布式ID:解决了全局唯一和不可预测问题,但作为字符串或大整数存储,随机性较强,会导致InnoDB聚簇索引页分裂,写入性能略低于自增主键。
- 自然主键/业务主键:比如用户表用身份证号、订单表用订单号,优点是查询时不用回表,缺点是业务逻辑一变主键就得改,字段本身可能违反"主键应由数据库生成、对业务无意义"的经典设计原则。
我的建议是:单库单表场景,优先自增主键;分布式场景,用雪花ID或号段模式,但主键字段类型一定要用BIGINT,不要为了可读性去用VARCHAR存ID。还有一点,UUID字符串做主键是最差的选择,随机字符串导致索引频繁页分裂且占用空间大,性能几何级下降,千万别用。
2.2 技巧6:非空约束和默认值是双保险
建表时给每个业务字段都考虑两个问题:能不能为NULL?默认值是什么?很多人懒得写,让字段默认允许NULL,结果代码里出现NULL参与计算、NULL出现在唯一索引里,排查半天才发现是数据问题。
非空约束的规则很简单:业务上不允许为空的字段,一律加NOT NULL。比如用户表的昵称、邮箱、手机号,订单表的金额、状态,这些字段如果允许为空,后续写WHERE条件、做聚合运算时都要额外小心,COUNT(字段)会忽略NULL值,SUM遇到NULL会直接返回NULL,这些都是隐藏雷区。
默认值也要尽最大可能设置。比如状态字段默认0表示待处理,创建时间默认CURRENT_TIMESTAMP,删除标记默认0。这样应用层插入数据时即使漏传某个字段,也不会产生无意义的数据。MySQL 8.0.13开始,表达式默认值也被支持了,比如DEFAULT (JSON_ARRAY()),可以用来给JSON字段设置默认值。
2.3 技巧7:唯一约束要想清楚再建
唯一约束是用来保证业务数据唯一性的重要手段,最典型的应用场景是防止重复创建用户(同一手机号/同一邮箱只能注册一次)、防止重复下单、防止订单号重复。但建唯一约束之前需要先想清楚两件事:业务上是否真的唯一?唯一键如何设计?
唯一约束和索引有个容易搞混的点:UNIQUE KEY本身就是一个索引,查询时可以利用它加速,所以不需要额外再建普通索引。如果发现某个字段既建了唯一约束又建了普通索引,那就是冗余,白白浪费写入成本。
还有一个特别常见的坑:在MySQL中,唯一索引对NULL值不生效,也就是说一个字段允许NULL且不加默认值时,多条记录的该字段为NULL,它们不会触发唯一冲突。我之前就遇到过场景,给用户邮箱建了唯一约束,结果用户注册时邮箱未填,NULL值成功插入多条,导致后续用邮箱作为登录凭证时出现多个匹配行。解决办法是:唯一约束字段要么加NOT NULL,要么把空值统一成空字符串,这样空字符串也会触发唯一约束。
2.4 技巧8:外键能不用就不用
关于外键,业界的共识度已经很高了:在互联网高并发、分库分表场景下,尽量不用数据库外键,而是通过应用层代码保证数据一致性。但很多从课本里学数据库的同学一上来就喜欢FOREIGN KEY,这个习惯在真实业务里会带来不少问题。
外键带来的问题主要有几个:第一,每次插入、更新子表数据时,数据库都要去校验外键对应的父表记录,多一次查询开销,高并发场景下放大得很明显;第二,分库分表后外键没法跨实例生效,曾经的约束突然失效,代码反而没做兜底;第三,删除父表数据时要考虑级联删除或限制删除,ON DELETE CASCADE一用,误删一条父数据,子表数据跟着全没了,数据恢复难度极大。
不建外键,不等于是"可以产生脏数据"。正确做法是:在应用层显式校验外键对应的记录存在后再插入;在删除前先查询子表是否存在关联引用。数据一致性由应用逻辑保证,数据库只做存储和基本类型约束,这也是分布式系统里普遍接受的取舍。
2.5 技巧9:检查约束是好东西,但要注意数据库支持情况
CHECK约束用来限制字段的取值范围,比如性别只能为0或1,状态值只能为1、2、3。这个约束在设计层面非常有用,能有效防止写代码时漏校验导致非法值入库。
但这里有个历史坑必须提醒:MySQL 8.0.16之前,CHECK约束只是被解析,并不会真正生效,很多老版本的帖子还在说"MySQL的check约束无效",导致很多人忽视了它。8.0.16之后MySQL才真正实现了CHECK约束的校验逻辑。如果你的生产数据库是8.0.16以下版本,或者用的是某些不支持CHECK的数据库,就不要依赖它,该在应用层做的校验还是要做。
新版MySQL中用法很简单:
sql复制CREATE TABLE user (
id BIGINT PRIMARY KEY,
status TINYINT NOT NULL DEFAULT 0,
CONSTRAINT chk_user_status CHECK (status IN (0, 1))
);
在PostgreSQL、Oracle、SQL Server中CHECK约束都是完整支持的,可以放心使用。需要说明的是,CHECK约束在数据量大的场景下也会带来少量校验开销,但相比它挡住脏数据带来的价值,这个成本基本可以忽略。
2.6 技巧10:创建时间和更新时间字段,建表必备
不管什么业务表,都会有"这条数据什么时候创建的""什么时候修改的"这类审计需求,所以建表时统一加上created_at和updated_at两个字段,能省掉后期很多麻烦。
在MySQL中,这两个字段的定义一般这样写:
sql复制created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
ON UPDATE CURRENT_TIMESTAMP的作用是:当记录中任何一个字段被更新时,这个字段自动刷新为当前时间。这样应用代码里完全不用手动维护updated_at,非常省心。
PostgreSQL里的差异要提一下,它没有ON UPDATE CURRENT_TIMESTAMP这种语法,通常用触发器实现:
sql复制CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_user_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
Oracle则可以用DEFAULT SYSTIMESTAMP配合应用层赋值,或者在触发器中处理。每个数据库语法不同,但思路一样:这个字段必须存在,且尽量由数据库自动维护。
3. 可扩展性设计:别让表结构成为业务瓶颈
表结构设计最难的其实是预见性。今天够用的表结构,明天业务翻倍后可能就是灾难。这一节讲的技巧不是为了炫技,而是让你在动手建表时多留几个心眼,减少将来改表的痛苦。
3.1 技巧11:逻辑删除与唯一约束的冲突,处理方案要提前定
很多表会设计一个deleted字段(0表示未删除,1表示已删除)来做逻辑删除。这样做的好处是数据不真删,误操作可恢复,审计有痕迹。但这套方案和唯一约束会打架——比如用户表给手机号建了唯一约束,用户删除了,再注册同一个手机号,插入时第二个NULL或已删除标记的记录就会触发唯一冲突。
常见解决办法有几种:
- 删除标记为NULL/时间戳方案:删除时把
deleted更新为NULL,未删除为DEFAULT值(比如0)。因为唯一约束对NULL不生效,所以删除后的记录可以直接再插一条相同手机号的数据。 - 删除标记加唯一字段组合:比如把唯一键设计成
(phone, deleted),如果删除标记只取0和1,删除后再插还是冲突,所以一般配合删除时间戳使用,把删除时间(毫秒时间戳)写入deleted字段,这样每个删除记录在唯一键的数据都不同,就可以允许多次删除和重新注册。 - 不用唯一约束,完全靠应用层查询校验唯一性:在逻辑删除场景下最灵活,但存在并发问题,需要额外加锁保证。
我实际项目里最常用的是方案2。比如deleted字段存0表示正常,删除时填当前时间戳,唯一索引建在(phone, deleted)上,就能兼顾逻辑删除和唯一性。
3.2 技巧12:冗余字段可以要,但要算清楚账
数据库设计三范式里强调表结构要精简、字段不可冗余,但实际业务中,冗余字段用得好,对性能提升非常明显。
什么是冗余字段?比如订单列表页要展示用户昵称,而昵称存在用户表里,如果不冗余,每次查询订单都要JOIN用户表。单条JOIN没问题,但列表页一次查几百条订单,还要关联多个维表,查询就变得很重。如果把用户昵称冗余到订单表里,查询时少一次JOIN,性能立刻提升很多。
但冗余不能乱加,要算清楚账。加一个冗余字段的代价是:数据更新时要多维护一个字段,如果用户改了昵称,订单表里的冗余昵称也得同步更新,这就会引入一致性问题。我的经验是:
- 只在读多写少的场景下冗余,比如订单里的商品快照信息,商品改名不影响历史订单展示。
- 冗余字段越稳定越好,比如用户ID、商品名称这种极少变更的字段。
- 高更新频次的字段不要冗余,比如用户积分、余额。
3.3 技巧13:预留字段是典型的坏味道
很多建表的人习惯性预留一些field1、field2、reserve1之类的字段,觉得"以后业务扩展用得上"。这个习惯是我最想劝退的——它几乎没有好处,只有坏处。
坏处有三点:
- 破坏了字段语义。过半年你自己都记不清
field1里存的是手机号还是年龄,新人接手更是一头雾水。 - 占用了不必要的存储空间。就算大多数是NULL,MySQL的变长字段也有额外开销。
- 真正的扩展往往不是加一两个字段能解决的。需求变了通常需要新增实体、新增关联表,而不是往预留字段里塞零散数据。
建表时的正确姿势是:先满足当前明确的业务需求,字段为当前业务设计;以后扩展了,用ALTER TABLE ADD COLUMN新增字段。MySQL 8.0的ADD COLUMN在部分场景下支持INSTANT算法,秒级完成,所以"改表很麻烦"的顾虑在逐渐降低。
3.4 技巧14:分区表、分库分表要提前想还是事后补
表数据量大了之后,查询变慢、写入变慢、备份变慢,这时就会考虑分区表或者分库分表。但很多人的误区是:建表时完全不考虑,等数据量上来才痛苦地做迁移。
我的经验是:建表时就要评估数据量和增长趋势。如果明确知道单表数据会过亿,一开始就可以考虑分区策略。比如日志表、操作流水表,天然可以按时间范围分区;订单表如果有明确的地域来源,可以按地域维度分库。
分区表的坑也不少。MySQL的分区表有个限制:所有分区键必须是表主键的一部分。如果你的主键是id,分区键是created_at,那建表时会直接报错。所以设计分区表时,主键通常要设计成(id, created_at)这种联合主键或让分区键成为主键的一部分。
分库分表则是一个更大的架构决策,通常配合中间件(如ShardingSphere)或分布式数据库来做,建表时需要额外考虑分片键的选择、跨分片查询的代价、全局ID生成方案。不需要每个项目都上分库分表,但如果预判数据量会跨过千万级别,建表时就应该把ID方案、时间字段、分片键这些预先安排好。
3.5 技巧15:大字段溢出,拆表比压在一起更稳
TEXT、BLOB、JSON这类大字段,和常规字段混在一张表里,会带来一个容易忽略的问题:行溢出和主表膨胀。
InnoDB存储引擎中,一页默认16KB,当一条记录中的数据超过页大小时,大字段会被放到溢出页,主表的一行只保留一个20字节的指针。这倒不影响正确性,但如果你经常SELECT *,大字段内容会被大量加载到内存,导致缓冲池命中率下降、查询变慢,尤其是列表页根本不需要查这些大字段内容时。
正确的做法是:把大字段拆到独立的扩展表里,主表只保留业务高频访问的短字段。举个例子,文章表保存文章标题、发布时间、阅读数等短字段,文章正文单独一张表存content,主表和内容表通过article_id关联。列表页只需要主表字段,详情页才去查内容表,性能会好很多。
JSON字段在MySQL里也要谨慎使用。它对灵活扩展友好,但失去了关系型数据库的约束能力,查询JSON内部字段无法走常规索引(需要生成列加索引),而且占用空间大、更新代价高。适合存那些结构不稳定、几乎不需要条件查询的配置类数据。
3.6 技巧16:字符集和排序规则在建表时定好,别指望事后改
字符集的选择直接影响:能存哪些字符、索引能建多长、排序和比较的规则是什么。一个非常常见的线上事故就是:建表用了默认的latin1或utf8,插入中文正常,但插入Emoji表情时直接报错。
这里的背景是:MySQL的utf8其实不是完整版UTF-8,它最多支持3字节,而大部分Emoji表情占用4字节,所以用utf8的库插入Emoji会报Incorrect string value错误。从MySQL 5.5.3开始引入了utf8mb4,这才是完整的UTF-8实现。
建表时字符集和排序规则建议这样定:
sql复制CREATE TABLE user (
id BIGINT PRIMARY KEY,
nickname VARCHAR(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
utf8mb4_0900_ai_ci是MySQL 8.0的默认排序规则,不区分大小写,比较规则比较友好。PostgreSQL和Oracle默认字符集通常是UTF8,一般不会踩这个坑,但还是要确认一下。
排序规则对查询的影响也要提一句:如果字段排序规则对大小写敏感度不符合业务预期,会导致比较结果和预想不一致,比如用户名区分大小写登录时,用_ai_ci(不区分重音和大小写)就可能导致两个不同账号被当成同一个。此时需要为字段单独指定utf8mb4_bin或utf8mb4_0900_as_cs。
4. 工程化与协作:建表不只是写一条CREATE TABLE
建表这件事在国内团队里常常是"谁开发谁建表",一个项目的表结构往往分散在多人手里,没有统一规范,没有版本管理,时间一长,建表和改表变成一场灾难。这一节谈谈建表之外的工程化协作问题。
4.1 技巧17:表和字段注释必须写,这是最低成本的文档
很多开发者觉得注释这种东西可写可不写,写完代码就完了,表结构到时候让人家看字段名猜意思就行。这个习惯非常误事——数据库字段的语义,只有设计者本人最清楚,不写注释等于把记忆负担留给所有后来的人(包括三个月后的自己)。
以MySQL为例,建表时可以通过COMMENT给表和字段添加注释:
sql复制CREATE TABLE `order` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_no` VARCHAR(64) NOT NULL COMMENT '业务订单号,全局唯一',
`user_id` BIGINT NOT NULL COMMENT '下单用户ID,关联user.id',
`total_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '订单总金额,单位元,保留2位小数',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态:0待支付 1已支付 2已发货 3已完成 4已取消',
`pay_time` DATETIME DEFAULT NULL COMMENT '支付时间,未支付时为NULL',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表';
从这段建表语句就能看出注释的价值:字段含义、状态码枚举值、关联关系、时间单位,全都能在注释里说清楚。后期写SQL、做数据报表、排查问题,都不用再去翻代码或问人。我见过不少团队把字段注释规划得比代码注释还细致,这直接提升了协作效率。
4.2 技巧18:命名规范要一致,别让表名和字段名各写各的
命名规范看起来是小事,但表名、字段名不一致带来的心理开销和沟通成本是实打实的。比如同一套系统里,一张表叫order,另一张表叫t_order_info,还有一张表叫orders,你根本分不清它们之间有没有关系。
我建议的命名规范(主流团队比较通用的方案):
- 表名单数小写,下划线分隔,比如
user、order_item,不用t_前缀(除非团队已有统一约定且无法改变)。 - 字段名也用snake_case,比如
nick_name而不是nickName。 - 主键统一叫
id,关联字段统一叫xxx_id,比如user_id、order_id。 - 统一用
created_at表示创建时间,updated_at表示更新时间,不要一张表叫create_time,另一张表叫created_time。 - 布尔类字段用
is_前缀,比如is_deleted、is_enabled,但MySQL 5.7以下版本里布尔类型本质是TINYINT(1),命名上不要造成误解。 - 唯一索引前缀
uk_,普通索引前缀idx_,这样在SHOW INDEX时一眼能区分出约束类型。
命名规范的价值在跨团队协作时尤其凸显:A团队开发的订单表,B团队接手做报表时,不需要额外沟通就可以按规则找到字段、知道含义。
4.3 补充:把建表脚本纳入版本管理,使用数据库迁移工具
很多团队建表靠开发在自己本地执行一遍,然后在群里说"表建好了,你们连一下"。这种方式最大的问题是:表结构变更不可追溯、不可复现、多环境(开发、测试、生产)容易不一致。
我现在参与的项目里都引入了数据库迁移工具:
- MySQL/通用Java项目:
Flyway、Liquibase - Golang项目:
golang-migrate、pressly/goose - Python项目:
Alembic(配合SQLAlchemy)
这些工具的核心思路都是一样的:把数据库结构变更写成版本化的SQL脚本,每次启动服务时自动检查并执行未执行的迁移脚本,保证不同环境的数据库结构保持一致。比如Flyway的脚本命名是V1__create_user_table.sql、V2__add_order_table.sql,每个脚本执行后会在flyway_schema_history表里记录执行状态,别人拉代码到本地时执行flyway migrate就能把库表结构建好。
这个习惯能避免的典型问题:代码合到主干了,但表结构只在某个人本地建过,其他人一跑就报"表不存在";或者生产环境有人手动改过表结构,和脚本不一致,后续迁移直接失败。
4.4 补充:改表操作要了解Online DDL的边界条件
建表做完之后,表结构总会有需要调整的时候。这里要提醒的是:不同数据库、不同版本下ALTER TABLE的在线能力差异很大,搞不清楚边界条件就会在生产环境锁表。
MySQL 8.0的ALTER TABLE支持多种算法:
ALGORITHM=INSTANT:只需修改数据字典,秒级完成,比如ADD COLUMN在多数情况下可以走这个算法。ALGORITHM=INPLACE:不需要拷贝全表,但会阻塞部分写入,比如ADD INDEX。ALGORITHM=COPY:拷贝全表数据,期间锁表,是不可接受的。
实际操作时,大多数情况下你没有显式指定算法,MySQL会自行选择最优策略,但有些操作必然要拷贝数据,比如修改字段类型、修改字符集。面对千万级大表,这类操作只能选在低峰期执行,或者用pt-osc、gh-ost这类在线变更工具来降低影响。
有个基本的经验是:数据量超过500万行的表,尽量不要直接用ALTER TABLE修改字段类型或长度,先用pt-online-schema-change预估变更方案和时间,再决定执行窗口。
最后再分享一个我在实际操作里体会很深的小技巧:建表语句写完后,先EXPLAIN一下即将高频执行的查询语句,确认已经命中了预期索引,再执行建表。这个习惯能帮你把"建表后才发现漏了索引,重新ALTER TABLE ADD INDEX锁一次表"这类尴尬尽可能避免掉。表结构设计这种事,前期多想十分钟,后期能省十个小时。
