ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

数据库表结构设计规范:字段类型选型与命名最佳实践

数据库表结构设计规范:字段类型选型与命名最佳实践 数据库表结构设计是每个后端开发都绕不开的基础功。很多人觉得建表就是写几行DDL草草了事但真正等业务上线、数据量上来之后才发现当初随手定的字段类型、命名方式带来了多少麻烦。这篇文章结合我这些年接手的各种项目实际经验把字段设计和命名规范这块一次性讲透从原则到实操到常见坑位全部覆盖到看完可以直接套用到自己的项目里。1. 字段设计规范的核心打好地基再盖楼数据库表设计这件事很多人第一反应是“不就是建几张表吗”。但真正出问题的时候往往恰恰是因为最开始建表时太随性。字段设计是整个数据架构最底层的地基地基歪了后面索引优化、查询调优、业务扩展都是空中楼阁。我在实际项目里最常见的场景是这样的需求评审会上业务方说“这个功能很简单加几个字段就行了”等真正动手时发现要加的字段类型和现有字段对不上或者命名风格完全不同再或者同一个含义的字段在几张表里名字叫法都不一样。这种问题几乎每个项目都会遇到根子就在于建表初期没有一套可落地的规范每个人都按自己的习惯来。1.1 先分清逻辑设计和物理设计的边界做字段设计的第一步不是打开Navicat或者PowerDesigner就开始画表而是先确立逻辑设计和物理设计这两个阶段的关系。逻辑设计解决的是“业务上需要哪些数据、数据之间什么关系”物理设计解决的是“这些数据在数据库里用什么类型、什么长度、什么约束来存储”。很多新手容易把这两个阶段混在一起一边聊业务需求一边拍脑袋定字段类型最后做出来既不像逻辑模型也不像物理模型。正确的做法是先通读需求文档或原型把业务实体梳理清楚。比如做用户信息表思考“用户”这个实体包含哪些属性这些属性是单值的还是多值的哪些属性是可变的哪些属性是不可变的。逻辑模型阶段不关心字段具体用什么类型只关心有哪些数据项。物理设计阶段才针对每个数据项确定存储方案。这样的分离最大的好处是当底层数据库选型从MySQL换到PostgreSQL时逻辑模型可以直接复用物理模型只需按新数据库的规范做类型映射就行。我在一个项目里就经历过从MySQL迁到PostgreSQL的改造因为当初逻辑设计和物理设计分得清整个迁移过程几乎没有改表结构逻辑只是做了一批类型转换脚本。1.2 设计顺序不可逆先定实体再定字段字段设计的实际执行顺序应该严格遵循“实体 - 字段项 - 字段类型 - 约束条件 - 备注说明”这个链条。先明确这个表代表什么业务实体再列出这个实体全部的业务属性然后才是逐字段确定类型、长度、是否可空、默认值这些物理属性。最后一步但最容易偷懒的是备注说明很多开发觉得备不备注无所谓但等三个月后别人接手你写的表时一行好的字段备注比十行代码注释都有用。有一个我强烈推荐的做法在设计阶段就为每个字段准备一个“字段字典”完整列出字段名、类型、长度、允许值、业务含义、来源系统。这个字典不需要一次性建好可以在设计过程中逐步完善但必须在建表时同步完成。后面做数据对账、排查数据问题的时候字段字典的价值怎么强调都不为过。1.3 预留扩展性但别过度设计设计字段时经常面临一个矛盾预留多少扩展空间合适预留太少后面业务一扩展就要改表预留太多又会让表结构臃肿、性能下降。这里的判断标准是可预测的扩展提前预留不可预测的扩展不要预留。可预测的扩展比如用户实名认证状态初期可能只有未认证、已认证两种状态但按行业经验大概率后期会加“审核中”、“认证失败”这些中间状态这种就可以在字段类型选择上直接预留更宽泛的取值范围。不可预测的扩展比如用户可能有哪些你根本想象不到的新属性这类不要去用预留一坨字段的方式瞎猜而是用扩展表或者JSON字段来解决。我在一个电商后台项目里见到过最糟糕的“过度预留”实例有个同事在设计订单表时为了让表“万能”加了30个预留字段叫reserved_1到reserved_30然后业务上线半年后这些字段一个都没用上反而让表变得异常笨重每次查订单都要带着这一大串空字段。这就是典型的过度设计。2. 字段类型选型实战每一种类型都有最适合的场景字段类型选型是整个字段设计规范里最考验基本功的部分。选错类型带来的影响通常不会立刻暴露但随着数据量增长性能问题和数据精度问题会逐渐显现。我见过太多因为早期选错字段类型后面不得不做数据迁移的惨痛案例做一次全面的字段类型调整影响面往往涉及所有关联接口和报表可以说是牵一发动全身。2.1 数值类型不只是整数和小数那么简单MySQL的数值类型看起来简单但实际使用中坑位很多。先看整数类型tinyint、smallint、mediumint、int、bigint的存储范围和占用的字节数都不一样选型时不是一看“整数”就无脑用int而要预估业务上限。最典型的例子是主键对于To C业务用户量百万级以内int完全够用但如果是消息记录、操作日志这种高并发插入且持续累积的表主键必须用bigint。我自己接过一个项目线上交易流水表的主键用的是int业务跑了三年后主键逼近最大值最后不得不花一个周末做表迁移把int改成bigint停服维护加上数据校验整个操作耗时十几个小时还承担了不小的数据风险。这个教训让我之后的主键选型策略变成一句话凡是只增不减、持续累积的表主键一律bigint不差那四个字节。小数类型的坑更大。数据库里的float和double是浮点类型存储的是近似值做金额计算时会出现0.10.2不等于0.3这种精度丢失的问题。业务上涉及金额、费率、积分等精确计算的数据必须用decimal类型。decimal的精度和标度要提前定义好比如decimal(10,2)表示总位数10位、小数位2位能够表示的最大金额是99999999.99是否够用要看业务量级。还有一类常见的坑是“用int存状态用int存百分比”。状态字段用int配合注释这种方式本身没问题但问题在于很多项目里状态值的定义散落在业务代码里数据库没有校验代码库里的定义也各有各的版本。这个问题我放到后面“枚举字段”部分细说。百分比字段用int存储时要考虑清楚是存0-100还是0-10000百分之一精度这个约定如果不在设计阶段定清楚后面做统计报表时各个数据口径不一致会非常折磨。2.2 字符类型选错性能差别是数量级的字符类型最核心的区分是char、varchar、text三者的选型。char是定长字符串varchar是变长字符串。这里有一个常见的误区觉得varchar是变长的所以任何字符串都用varchar最合适。但实际上char的存取效率比varchar高因为char类型存储时不用记录额外的长度前缀。所以对于长度基本固定的场景比如身份证号、手机号、MD5摘要这种用char反而更合理。varchar要特别注意长度的设定。我见过不少表里所有字符串字段一律varchar(255)这种看似省心的做法实则是隐患。varchar存的是字符数而不是字节数varchar(255)意味着最多存255个字符。在utf8mb4编码下每个字符最多占4字节那么这一个字段最长可能需要约1020字节的存储空间。如果是一次性确定数据长度的字段还好但如果是索引字段过长的varchar会导致索引体积急剧膨胀影响写入性能和查询效率。更严重的是当varchar长度超过特定阈值时InnoDB可能将行记录变成溢出页存储进一步拖慢查询。实际设计原则是根据业务真实需要精确计算长度。比如用户名业务上限制了最多20个字符就用varchar(50)——注意要预留一些余量给国际化前缀或异常数据但不要动不动就255。一个几千字的文章内容如果不需要全文索引可以直接用text但要注意如果表自带了包含text字段就不是“紧凑型行”主键索引的效率会受影响。text字段还有个大坑不能有默认值。MySQL里给text字段设置默认值直接报错除非设置explicit_defaults_for_timestamp等特殊场景这个特性意味着你在设计表时要提前想清楚哪些字段可能是大文本如果业务上存在“先插入空内容再更新”的场景text会让插入操作变得更繁琐。2.3 日期时间类型的选择时机决定一切日期时间类型有date、datetime、timestamp三种主要选择。date只存年月日datetime存年月日时分秒timestamp也是存年月日时分秒但两者在存储范围上有区别。datetime的存储范围是1000-01-01 00:00:00到9999-12-31 23:59:59timestamp的存储范围是1970-01-01 00:00:01到2038-01-19 03:14:07这就是著名的2038年问题。timestamp的最大优势是支持时区转换写入时从当前时区转换成UTC存储查询时再从UTC转换成当前时区。datetime不支持时区存进去是什么就是什么。对于需要跨时区的全球化业务timestamp有天然优势。但要注意timestamp的范围上限问题如果业务要处理超过2038年的日期就必须用datetime。另一个容易忽略的点是“只精确到天”的场景比如生日、入职日期、合同生效日期这类只用日期不用时间的直接选date类型不要用datetime。原因很简单date类型占用字节更少而且在做日期比较、日期函数计算时不会因为时分秒字段干扰而产生意外的边界问题。我处理过的一个bug就是从“当天”这个语义上发现的问题用datetime存入职日期凌晨秒级数据会导致统计口径混乱后面改成date类型才彻底解决。2.4 大字段与JSON字段的取舍现代数据库一般都支持JSON类型MySQL从5.7版本开始提供原生的JSON支持。JSON字段确实方便——可以让一张表承载结构不固定的数据但JSON字段的代价是查询无法走传统索引。虽然MySQL提供了JSON索引优化但实际使用中如果业务查询经常需要读取JSON内部的某个键性能依然远不如将那个键提取成独立字段。我的建议是高频查询属性一律独立成字段低频多变的属性用JSON。举个例子用户表里姓名、手机号、状态这种查询频率极高的字段必须独立成普通字段而用户偏好设置、用户自定义属性这种很少用于查询、结构经常调整的用JSON存储最合适。大字段方面text和blob要特别注意不要滥用。如果表的字段数很多且包含多个大字段InnoDB会在存储时采用溢出页方式存放大字段内容这样即使只查主键和几个小字段也可能需要读取额外的数据页性能影响直接被放大。3. 命名规范从表名到字段名的统一艺术命名规范是数据库表设计里争议最多、也最容易被忽视的部分。我在不同的公司体会过完全不同的风格——有的崇尚“表名要长含义要全”有的崇尚“表名要短代码好写”。其实命名规范没有绝对的对错核心是“统一”。一个项目里最怕的不是用了某一种风格而是每一张表都有自己的风格。3.1 表命名的两种主流风格与取舍表命名主要分两种流派一种是单数风格users表叫userorders表叫order另一种是复数风格users表叫usersorders表叫orders。两种风格都有大量项目在使用。我的建议是复数和单数本质上不是最关键的最关键的是和团队的代码风格保持一致。如果一个项目里ORM框架生成的实体类倾向使用单数那表名就用单数这样从表名到实体类的映射没有心理负担。反过来如果项目从最开始就用复数表名就统一保持复数。更重要的规范是表名前缀。在同一个数据库里如果既有业务表又有日志表、临时表通过前缀快速区分表用途能节省大量沟通成本。常见的命名前缀有t_前缀表示业务表如t_user、t_ordertmp_前缀表示临时表如tmp_export_20250101log_前缀表示日志表如log_operation、log_login使用前缀的好处是第一代码review时眼见就知道这张表大概是什么用途第二运维在排查慢查询时能快速定位到“哦这是日志表不需要走索引优化”。但要注意前缀不要搞太多种有个项目里我看到过t_、tb_、tab_、b_、sys_、x_各种前缀五花八门还不如统一用t_一种来得简洁。3.2 字段命名七条铁律字段命名规范我总结了七条铁律都是踩过坑之后提炼出来的第一一律使用小写字母加下划线分隔。userId这个写法是驼峰式在Java代码里很常见但字段名存储到数据库后MySQL在Windows平台大小写不敏感但Linux平台大小写敏感如果代码里用userId查询数据库里存的是user_id就会出问题。统统使用小写蛇形命名彻底规避大小写敏感问题。第二禁止使用数据库保留字作为字段名。比如name、order、group、desc、level这些词看起来人畜无害但order是SQL关键字desc是缩写关键字直接作为字段名称会让SQL语句非常别扭。解决方案无外乎两种要么字段名加前缀如order_status、user_name要么使用反引号包裹但后者是治标不治本。归根结底设计字段名时避开关键字才是正道。第三字段名要能直接反映含义禁止使用无意义缩写。uid、pwd、nm、dsc这种缩写出了项目组根本没人看得懂过一个月自己看也费劲。字段名应该在“长度合适”和“含义清晰”之间找到平衡比如用户名用user_name比uname和username都清晰创建时间用created_at比create_time和gmt_create更贴合惯例。第四同一个含义的词在不同表中必须用同一个英文单词。用户名在t_user里叫user_name在t_admin里叫admin_name这种情况勉强说得过去但如果叫name在t_user表里叫name、在t_student表里叫student_name就会造成跨表查询时的字段命名不一致问题。所以在项目初始化阶段就应建立“命名词典”——同一个中文含义对应固定的英文字段名新表设计时查词典复用不再创造新词。第五不用词义过于宽泛的字段名。比如status这个字段名放在订单表里大家还能猜到是订单状态但如果放在一个综合业务表里就完全不知道这个状态是“审核状态”还是“支付状态”还是“发货状态”。更精确的做法是将状态的核心业务修饰词前置如order_status、pay_status、audit_status。这一条直接关系到字段自解释性——拿到字段名不需要看备注或查代码就知道这个字段管什么。第六布尔字段的命名要有统一的约定。布尔字段有is_enabled、is_deleted、has_xxx这几种常用风格项目里选定一种就全部统一。注意is前缀有个众所周知的坑MyBatis等框架在映射Boolean类型到Java实体时会默认把is_enabled映射成enabled而不是isEnabled容易导致各种踩坑踩到怀疑人生。为此很多团队干脆约定不使用is_前缀改用enabled、deleted、published这类形容词或过去分词做布尔字段名避免框架映射问题。第七日期时间字段的命名要区分出授时语义。created_at/updated_at几乎是行业标配但order_time这种表述到底是下单时间还是支付时间还是发货时间潜意识里就会有歧义。更好的做法是把这个时间对应的业务动作直接放进字段名比如order_created_at、paid_at、shipped_at、completed_at一眼就能看懂。如果需要存“操作人”对应地就是creator_id、operator_id、approver_id按照动作严格对应时间和操作人字段要成对出现。3.3 索引命名和约束命名也别敷衍索引、约束的命名同样值得花点心思。主键约束一般是表名加_pkey这样的后缀唯一索引用uniq_前缀加字段名普通索引用idx_前缀加字段名。从可运维性来看索引命名清晰的意义在于当线上出现慢查询DBA拿到慢日志里的索引名能直接通过索引名猜出索引建立在哪些字段上不用再连上数据库去查表结构。比如idx_user_name这个索引名一眼就知道是用户名字段上的索引。如果索引名是随便生成的idx_1、idx_2排查效率会大打折扣。还有一个细节当需要删除某个索引时如果索引名包含了字段含义就能精准定位避免误删其他索引。索引名称的唯一性在MySQL中是在表级别生效的同一张表里索引名不能重复不同表可以有同名索引所以保存好索引字典建议在表设计文档里把索引清单也一并记录清楚。4. 用户信息表设计实战把规范落到一张具体的表上理论说再多不如动手做一张完整的表。这里就以最常见的用户信息表为案例完整演示一遍从需求分析到字段设计到最终落地DDL的全过程。这也是网上那个热搜“第1关数据库表设计 —— 用户信息表”背后真正想考察的能力。4.1 用户信息表的需求分析先梳理用户信息表需要承载哪些数据。一个常规的业务系统里用户实体必然包含用户的唯一标识即主键用户在业务侧的标识如用户名、昵称联系方式如手机号、邮箱认证信息如密码摘要、盐基础属性如性别、生日、头像账户状态如是否锁定、是否可用审计字段如创建时间、更新时间、创建人、更新人逻辑删除标记这个列表看着全面但核心原则是一张表只存这个实体自身的属性信息其他和用户有关系的业务数据比如用户的订单、用户的地址都应该放到对应的订单表、地址表里去而不是冗余在用户表里。这个判断标准用起来很简单如果一个字段要体现“用户在某个业务动作中产生的数据”就不属于用户表属于那个业务动作的表。比如“用户最近一次下单时间”这个字段初看起来是在描述用户但实质是用户与订单的关系不应该放进用户表而是通过订单表来获取。4.2 全字段明细与类型选型推演基于上面的分析逐步定下用户信息表的字段设计。整个过程我建议在PowerDesigner这类建模工具里画物理模型来做体验比直接写DDL好得多字段之间的相对位置调整非常直观而且能直接生成DDL脚本。先看主键字段idbigint unsigned自增主键。为什么不选int前面已经说过用户表是典型的只增不减的表。哪怕现在只有几万用户直接把bigint定好。主键字段虽然叫id很常见但有些团队喜欢用user_id这个看规范统一这里用id更通用。再看用户名与认证字段user_namevarchar(50)用户登录名是用户唯一性标识建议加唯一索引。注意这里不建用户名字段时用varchar(255)50已经足够覆盖绝大多数业务场景的用户名长度。如果业务有国际化需求再根据实际规则调整长度但不要一上来就非常宽。password_hashvarchar(100)密码哈希值。之前看到很多表用varchar(32)来存MD5的密码摘要但现代密码存储推荐使用bcrypt或者argon2算法这类算法生成的哈希串长度通常在60位以上所以预留到100比较合理。这一行中间那个下划线是一种通用习惯表示存储的是某种摘要/加密后的结果。password_saltvarchar(32)密码盐。盐值通常随机生成用32位的字符串存入比较稳妥。phonechar(11)手机号。这里用char而非varchar因为手机号长度固定为11位用定长char存储效率更高。注意手机号未来可能存在国际化的可能性比如e.164格式带86前缀那长度就要重新评估。如果不确定就varchar(20)同时兼顾后续扩展。emailvarchar(100)邮箱地址。长度给到100主要是考虑不同邮箱服务商和域名长度常规的邮箱50以内就够但偶尔会遇到较长的域名。再看用户基本信息和状态nicknamevarchar(50)昵称允许为空。很多系统里用户可以不设置昵称此时可以回退到user_name来展示。gendertinyint性别0未知、1男、2女。这个问题我经常被问到“用不用枚举类的字符串存性别”。这里用tinyint加代码注释数据库比较轻量业务代码里做一层转换就行但前提是项目里必须有完善的枚举管理机制。后面会专门讲到枚举字段的注意事项这里先按下不表。birthdaydate生日只到日期即可。avatar_urlvarchar(255)头像地址。如果项目里用了对象存储存的是URL。因为对象存储的访问地址长度有时会超过255所以如果需要预留更多可以varchar(500)但这个长度对InnoDB行格式不友好实际多数场景255够用。statustinyint账户状态1正常、2锁定、3禁用。网上很多教程喜欢用is_active来表示激活与否但从业务扩展来看账户状态往往不只是“激活/未激活”二态还可能有“冻结”这种异常态所以直接用status配合注释的方式更灵活。如果想要彻底规避状态歧义可以拆成多个布尔字段比如is_locked、is_disabled选择性很强。这里强烈不推荐用两个布尔字段来表示“锁定且禁用”这类叠加状态——布尔字段只有0和1本质上是存不了多状态的。last_login_atdatetime最近一次登录时间。这个字段常用且经常会被漏掉但设计时要想清楚它代表“最近一次成功登录时间”还是“最近一次登录尝试时间”建议在字段备注里写明。last_login_ipvarchar(45)IP地址。IPv6的地址最长是45个字符所以varchar(45)不要用varchar(20)更不要用int来存IP——因小失大别省那点存储。最后是审计字段created_atdatetime创建时间。updated_atdatetime更新时间每次更新自动修改。created_byvarchar(50)创建人存用户的ID或系统标识。这里如果业务上有明确的“操作人ID”可以使用bigint但常见做法是varchar因为可能是系统触发或者admin可读性更强。这个可以按团队习惯统一。updated_byvarchar(50)更新人。is_deletedtinyint逻辑删除标记0未删除、1已删除。4.3 在PowerDesigner里落地物理模型PowerDesigner是数据库设计领域的老牌工具虽然界面有些年头了但胜在功能完备而且是很多企业规范里明确要求的建模工具。用PowerDesigner设计用户信息表的步骤并不复杂第一步新建Physical Data Model选择数据库类型为MySQL 5.0或对应版本注意版本选择会影响生成DDL的语法和类型映射。第二步新建Table命名为t_user依次填入上面梳理出的所有字段。每个字段要同时设置好数据类型、是否必填、是否主键、默认值和注释。PowerDesigner的字段面板操作很简单关键是手动为每个字段编辑好Comment后续生成DDL时注释会自动带过去这一点对于维护数据字典非常重要。第三步建立索引。在Table的属性窗口切到Indexes页签为user_name建立唯一索引unique_index_user_name为phone建立唯一索引unique_index_phone为status创建普通索引idx_status。这里注意唯一索引的作用是防止业务代码并发插入相同用户名数据库层面做一个兜底这是很常见且必要的操作。第四步生成SQL脚本。在PowerDesigner菜单里选择Database - Generate Database勾选生成创建表的脚本一个全新的用户信息表物理模型就落地了。生成的DDL还可以导出为sql文件提交到Git仓库作为数据库变更记录。PowerDesigner设计一个单独的数据库表E-R示例本身就是很多教材里的经典练习核心目的就是让大家通过实际动手把上面的规范和操作串起来。做完一张表后对你的理解提升是看十篇文档都换不来的。4.4 用户信息表完整DDL参考前面用PowerDesigner设计的物理模型生成的DDL最终效果如下可以直接跑在你的MySQL 5.7及以上版本里CREATE TABLE t_user ( id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 主键, user_name varchar(50) NOT NULL COMMENT 用户登录名, password_hash varchar(100) NOT NULL COMMENT 密码哈希值, password_salt varchar(32) NOT NULL COMMENT 密码盐, phone char(11) NOT NULL COMMENT 手机号, email varchar(100) DEFAULT NULL COMMENT 邮箱, nickname varchar(50) DEFAULT NULL COMMENT 昵称, gender tinyint NOT NULL DEFAULT 0 COMMENT 性别0未知1男2女, birthday date DEFAULT NULL COMMENT 生日, avatar_url varchar(255) DEFAULT NULL COMMENT 头像地址, status tinyint NOT NULL DEFAULT 1 COMMENT 账户状态1正常2锁定3禁用, last_login_at datetime DEFAULT NULL COMMENT 最近一次登录时间, last_login_ip varchar(45) DEFAULT NULL COMMENT 最近一次登录IP, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, created_by varchar(50) DEFAULT NULL COMMENT 创建人, updated_by varchar(50) DEFAULT NULL COMMENT 更新人, is_deleted tinyint NOT NULL DEFAULT 0 COMMENT 逻辑删除0未删除1已删除, PRIMARY KEY (id), UNIQUE KEY uniq_phone (phone), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_bin COMMENT用户信息表;这段DDL里有两个细节值得展开说一下。第一是排序规则utf8mb4_bin。utf8mb4_bin是二进制排序规则和默认的utf8mb4_general_ci / utf8mb4_0900_ai_ci相比它的比较是区分大小写的而且按二进制值排序更加确定。对于用户名这种字段使用_bin排序规则可以避免MySQL在比较时对大小写不敏感导致的用户唯一性冲突。举个例子如果使用utf8mb4_general_ciTom和tom会被认为是重复的用户名使用utf8mb4_bin则可以区分。具体用哪种取决于你的业务需求但要注意排序规则会同时影响唯一索引和普通索引的匹配规则这个一旦建表后想改、对存量数据的影响非常大。第二是逻辑删除字段is_deleted。逻辑删除本身是一个取舍的结果保护数据不物理删除方便审计和回收但代价是每次查询都要带上is_deleted0的条件。这个字段有好几个注意点一是要有默认值并加注释标明0和1的含义二是唯一索引要小心逻辑删除的数据占用唯一键——比如phone上有唯一索引当用户注销删除了逻辑记录后再注册同一手机号会出现唯一键冲突。解决办法是要么在注销时把phone改成一个带时间戳的假号码要么把唯一索引改成复合索引phone, is_deleted。这些都必须在设计阶段想到。5. 公共字段与保留字段的设计智慧公共字段是每张业务表都会遇到的问题。很多团队在建表时完全靠各个开发临时临场发挥今天这个表加了created_at和updated_at明天那个表忘了加create_by后天又是另一个表把is_deleted写成了deleted_flag整个数据字典乱得难以收拾。所以公共字段必须在组织层面定出一套标准所有新表统一套用。5.1 公共字段的标配清单每个团队可以为自己的业务定制公共字段集但通常标配包含这么几类主键字段idbigint unsigned auto_increment时间审计字段created_at、updated_at操作人字段created_by、updated_by逻辑删除标记is_deleted版本号字段versionint用于乐观锁控制版本号字段可能被很多团队忽略但如果是修改频繁、并发冲突风险高的业务表版本号几乎是必备的。每次更新时带上version条件update where id? and versionold_version更新成功version加1这个做法可以有效避免并发覆盖问题。订单、库存这类高频更新的表尤其需要。公共字段的维护有两种方式一是直接在每个表的DDL中写死这就是最常用的方式二是通过数据库层面的模板生成工具来自动带上每次建表时顺手加进去即可。但公共字段不一定都要交给每个开发来手工写团队内可以统一一套建表模板所有人在这个模板基础上做业务字段扩展。5.2 主键策略自增主键还是业务主键主键设计是整个表结构里最牵动全局的决策。最常见的两种做法是自增主键和UUID主键各有各的适用场景。自增主键的优点写入性能好因为InnoDB中主键是聚簇索引自增主键在插入时是顺序追加页分裂概率最低占用空间小对二级索引的大小也有利。缺点是数据迁移时可能会发生主键冲突需要做映射业务数据会暴露规模量级例如用户id是10086基本能猜到用户大概刚过一万。UUID主键的优点分布式的场景下可以在多个节点生成不依赖数据库自增序列数据不会暴露业务规模。缺点是无序性导致聚簇索引页分裂频繁写入性能严重下降特别是数据量大之后这个问题极其明显。UUID是36个字符的字符串如果直接存成varchar(36)二级索引的存储空间也大得多。现实的折中方案是单库单表场景直接用自增主键分库分表场景用雪花ID或类似方案分布式ID生成器既保序又有全局唯一性同时避免了UUID在存储性能上的劣势。从查错角度来说雪花ID比UUID短不少对索引更友好。至于业务主键比如用身份证号、手机号直接做主键建议不要这么干。业务主键的问题在于业务规则一旦变化比如手机号换了主键就跟着变了主键关联的外键数据全部要跟着改。正确做法是物理主键用自增id业务唯一性通过唯一索引来保证。5.3 预留字段到底该不该用关于预留字段我的观点非常明确不要用reserved_1这种万能字段。字段的语义一旦預留得不够精确要么变成垃圾数据收集器要么沦为空列浪费存储。更好的做法是如果无法确定未来要扩展什么就什么都不预留等真正需要时再通过ALTER TABLE加字段。MySQL 8.0支持INSTANT算法加字段秒级完成这种操作已经不是什么伤筋动骨的事了。与其预留无意义字段不如留好“结构层”的扩展机制。比如用扩展表和JSON字段来应对不确定的属性需求这个在前面字段类型选型部分已经说过了。扩展表的方式比较重一点但每个语义都是精确的JSON字段比较轻适合属性零散不固定的场景。6. 常见问题与踩坑经验速查这一部分我把这几年在各种项目里实际遇到的字段设计问题汇总成一个速查表很多坑如果不是亲身踩过看文档根本看不出来。建议收藏备用。6.1 高频问题速查表问题场景典型错误做法推荐做法用户表的主键类型int类型业务量大了才改对持续累积的表一律用bigint金额字段float或double精度丢失decimal明确精度和标度手机号字段int类型手机号前导0被吃掉varchar或char长度至少按业务规划用户名字段varchar(255)大而无当按业务规则定长度如varchar(50)大文本内容与主表混存独立成表或用text并按需分表时间字段字符串存时间无法高效比较使用datetime/timestamp状态字段多个布尔字段叠加“锁定且禁用”使用status枚举类型统一管理枚举含义仅存0和1代码里到处是magic number建枚举字典确保代码枚举和表注释一致逻辑删除忘记唯一索引冲突问题定义好逻辑删除与唯一键的组合方案布尔字段is_deleted映射时被框架转换出问题让团队统一约定尽量规避is_前缀的坑备注信息字段啥备注都不写每个字段必须有注释能解释取值范围和业务含义上面每一项的背后基本都有血淋淋的线上事故或至少是查数据查到怀疑人生的经历。比如手机号用int存业务上线后某用户手机号是13812345678存进去变成小数点导出数据一堆问题时间字段用字符串存报表系统每次都要做字符串转换性能差了一大截还容易踩格式坑逻辑删除配合唯一索引冲突这个问题初期用户量不大可能遇到不多等上线一段时间大量注销用户开始复用手机号时才暴露改起来真的要命。6.2 排查线上问题的三板斧当线上遇到与字段设计相关的数据问题我习惯按这个顺序排查第一先看字段设计本身。如果某个字段存了预期之外的数据优先怀疑建表时的约束没到位。比如应该NOT NULL的字段允许为空、应该加唯一索引的字段没加、应该用decimal的地方用了float这类问题在数据量小的时候不痛不痒到了线上会以各种奇怪的方式显现。第二再看代码写入入口。锁定是哪个接口或哪个写入任务写入的数据。如果入口代码里对字段做了类型转换但没有做越界保护就很容易把超出数据库字段长度的数据截断。第三最后看数据流转链路。尤其是数据从消息队列或者同步任务批量写入时常常会有脏数据源头的问题。此时一张字段备注完整的表就格外重要——你得能看懂每个字段原本的业务含义才知道某个异常值到底“该不该出现”。6.3 几条我认为最值得分享的独家经验最后分享几条我觉得真正值钱的实操经验这些在教材里很难找到。第一在任何表上都尽量带上version字段。有些纯只读表、字典表可以不加但只要是可更新的业务表加上version字段几乎没坏处。就算现在没有乐观锁需求后面接缓存、做分布式架构时version字段都是非常有价值的基础设施。第二枚举字段不要只靠数据库注释来维护含义。更好的做法是建立枚举字典表或者至少把枚举定义统一收敛到代码中的一个枚举类并通过数据库校验约束比如MySQL 8.0的check约束确保写入值不会超出枚举范围。数据库的注释是不够的——注释永远不会报错也不会拦住非法数据只能供人查阅。第三对“万能字段”保持警惕。如果一个字段设计的初衷是“既可以存这个又可以存那个”那它最终很可能什么都没有正确地存好。与其设计万能字段不如把每个字段的边界定义清楚数据质量才是后续所有数据处理工作的根本。从最开始的那次大迁移到后来每一次新业务建表我越来越觉得字段设计做得好不好短时间内看不出什么差距但时间线拉长后一个字段设计扎实的数据库和一个字段设计随意的数据库在数据质量、开发效率、排障速度上的差距是数量级的。数据库表设计这个系列我会继续写下去后续准备聊一聊索引设计规范、SQL写法规范、大表分库分表经验这几个方向。如果你正在做系统建模或者准备对老系统的表结构做一次规范治理希望这篇能成为你的参考手册。
返回列表