☰
数据库设计Step by Step:以博客系统为例的完整实战指南
2026/9/28 13:25:12 网站建设 项目流程

拿到“数据库设计 Step by Step”这个题目,我第一反应是:又一个新手要被网上那些零散教程带偏了。干这行这么多年,见过太多人拿到需求后第一时间打开 Navicat 就开始建表,等业务一到深处才发现问题,然后陷入“改表结构 → 迁移数据 → 改代码 → 又发现新问题”的循环里。这个系列“扬帆启航”的第一篇,我就带你从零开始走一遍数据库设计的完整路径:需求分析、概念设计、逻辑设计、物理设计、评审与迭代。

这篇内容不是给你背理论的,而是给你一套可以直接落地的思考方法和实操套路。全文以“博客系统”作为贯穿案例,重点讲清楚一边写 SQL 之前该如何想清楚你的表结构。适合刚接触数据库的学生、转行做后端的同学,以及那些已经会写 SQL 但没系统学过如何做设计的开发人员。

1. 拿到需求先别动手:数据库设计的第一步是搞清楚业务逻辑

这一步被绝大多数新手跳过,却是整个设计过程最关键的环节。你跳过它,后面每一步都是空中楼阁。

1.1 为什么上来就建表会翻车

很多人以为数据库设计的核心是 SQL 语法,是建表语句写得漂亮。实际上,SQL 只是最后一公里的翻译工作,真正的设计早在写任何 CREATE TABLE 之前就已经开始了。你连业务都没搞明白,建出来的表一定是“你以为的”业务,而不是“真实存在”的业务。

举个例子。你脑子里以为博客系统只需要“用户表、文章表、评论表”三张表就够了。等产品经理走过来告诉你,文章要打标签,评论要支持点赞,用户要能看到浏览历史,这时候你会发现自己设计的表根本顶不住。硬加字段?表越来越宽,越来越乱。重构表?线上数据怎么办?代码联动怎么改?没有想清楚业务就动手,后面全是在为自己的冲动买单。

我踩过这种坑。当年做第一个项目,心想“我写过增删改查,建表还不简单”,直接拍了拍脑袋建了 8 张表。结果第四周产品提需求,数据库改了 3 轮,代码改了 5 轮,最后项目延期,锅全在“需求变太多”。现在回头看,需求确实有变化,但 90% 的变数在一开始梳理业务时就能预判到。

1.2 需求分析到底要收敛哪些信息

做需求分析不是去开会记纪要,而是要围绕“谁、什么、在哪、何时、为什么、怎么”六个方向把业务拆干净。我最常用的方法是五步收敛法。

第一,找角色。系统里有哪几类用户?博客系统里至少有普通读者、注册用户、作者、管理员。第二,找对象。业务里有哪些核心名词?用户、文章、评论、分类、标签、点赞记录。第三,找动作。角色对对象做了什么?作者发布文章,读者评论文章,用户点赞评论,管理员审核文章。第四,找规则。每个动作有什么限制?一个用户可以有多篇文章,一篇文章可以有多个标签,一个评论只能属于一篇文章。第五,找指标。哪些数据需要被统计权衡?文章的阅读数、评论数、点赞数,这些指标是否需要实时精确,还是允许一定延迟。

这四个维度整理下来,你手里的就不再是笼统的需求文档,而是一张清晰的数据清单。别小看这种填空式梳理,它能强迫你把“大概差不多”变成“确定就是”。我习惯把梳理结果画成一张简单的表格,横轴写实体,纵轴写关系,一格一格填,填不出来的部分就是还没搞懂的点。

1.3 把业务拆成“实体 + 属性 + 关系”的最小清单

做完上面的梳理,你手上应该有一堆零散信息。接下来,把它们整理成三张清单。

实体清单,就是那些需要长期存储的核心名词。博客系统的实体有:用户、文章、评论、分类、标签。属性清单,就是每个实体有哪些值得存储的特征。用户的属性有手机号、密码、昵称、头像;文章的属性有标题、正文、状态、发布时间。关系清单,就是实体与实体之间的连接方式。用户和文章是“一对多”,文章和标签是“多对多”,文章和评论是“一对多”。

我给你一张实际可抄作业的表,你想想自己在做项目的时候是不是这么列出来的:

实体关键属性与其他实体关系
用户(user_info)手机号、昵称、密码哈希、头像一对多:文章、评论、点赞
文章(article)标题、正文、状态、发布时间属于用户;一对多:评论;多对多:标签
评论(comment)评论内容、评论时间属于用户、属于文章;一对多少:点赞
标签(tag)标签名、标签描述多对多:文章
点赞(like_record)点赞时间、点赞对象属于用户;多态:文章或评论

你注意,这里面的“用户”设计成了 user_info 而不是 user,是因为 user 在很多数据库里是关键字或者保留字,能避开就尽量避开。这属于命名规范,后面我还会细说。

1.4 确定范围:第一版先做最小闭环

我刚做设计的时候特别贪婪,总想把所有功能都考虑进去,结果就是设计出来的表像八爪鱼一样,什么都挂一点,什么都做不深。后来慢慢领悟到一个道理,数据库设计和产品迭代一样,需要考虑 MVP,先做最小可用版本。

第一版博客系统,你只需要用户能注册登录、作者能发布文章、读者能浏览文章并评论,这三件事跑通就够了。点赞、关注、消息通知、模板消息这些,可以等主流程稳定后再补。每个功能进入设计,都要问自己一句:现在不做它会死吗?答案是不会,那就划到下一期。

但“划到下一期”不等于不考虑。比如你知道用户未来会有收货地址,但没有设计地址表。那可以在用户表里预留一个逻辑字段?不用,预留本身也是过度设计。可靠的做法是:本期把用户表设计得干净稳定,下期要加地址表时再做独立扩展,这种加法在数据库层面是很自然的。真正痛苦的加法是改已有的核心字段,比如把手机号从 varchar(20) 改成 varchar(64)——那才是灾难。

2. 概念模型设计:用实体关系图把业务“画”出来

需求清单理清之后,第二步是把文字看成图形。这一步不需要写 SQL,甚至连表结构都不需要细化,你只需要画一张框框图。

2.1 实体、属性、关系三个词的真实含义

实体是名词,是业务里真正需要记录的“人、事、物”。属性是实体身上的形容词或名词,回答的是“这个实体长什么样子”。关系是动词,是实体之间发生的关联,回答的是“谁和谁之间怎么样了”。

拿博客系统的文章来举例。“文章”是一个实体,它有标题、正文、发布时间这些属性。“作者”也是一个实体,他有昵称、头像、手机号。文章和作者之间的关系叫“撰写”:一个作者能写多篇文章,所以这个关系是“一对多”。

听上去很简单,但筛掉属性里的水货需要经验。最常见的错误是,把应该独立成表的对象当成另外一个对象的属性。比如有人会把“标签”直接设计成文章表里的一个字段,存成逗号分隔的字符串:“标签: 后端,数据库,Python”。这样做查询“包含 Python 标签的文章”会变得异常痛苦,只能用 LIKE 模糊匹配,数据量一上来性能立马崩。这就是概念建模不彻底的代价。

2.2 三种关系类型:一对一、一对多、多对多

实体之间的数量关系,是概念模型最核心的内容。

一对一,意思是这个实体的一条记录只对应另一个实体的一条记录。比如一个用户只有一个登录账号,反过来一个登录账号也只属于一个用户。这种关系最简单,实际看看不需要单独建表,两张表共用同一个主键,或者把一方的外键放在另一方表里都行。

一对多,是最常见的关系。一个作者可以写多篇文章,一篇文章可以被多个评论回复。实现方式是在“多”的那一方放一个“一”那一方的主键,这个字段叫外键。文章表里放 author_id,评论表里放 article_id,就是典型设计。

多对多,稍微剥离它。一篇文章可以打多个标签,一个标签下面可以有多篇文章。这个是三位关系,在数据库里无法直接表达,必须拆成两个一对多,这时候就需要一张中间表。比如 article_tag 表,里面只有两个字段:article_id 和 tag_id,各自都指向原表主键。

给三种关系做个对比,你在建模时直接对号入座:

关系类型例子实现方式是否建中间表
一对一用户与登录账号共享主键/单方外键一般不需要
一对多用户发文章在“多”方加外键不需要
多对多文章与标签拆成两个一对多必须建中间表

2.3 识别业务约束:唯一、非空、默认值

图形画完之后,要开始给每个属性贴“限制标签”。别小看这一环节,约束的准确度直接决定你的数据质量。

唯一约束:哪些字段不允许重复?用户手机号必须唯一,用户昵称是否需要唯一?如果要,那就在昵称字段上建唯一索引。文章标题在同一用户下是否唯一?这些都要在画图阶段写下来,落库的时候就能变成实际的 unique key。

非空约束:哪些字段必须填?用户手机号不能为空,密码哈希不能为空,文章标题不能为空。但注意,“不能为空”不代表“不给默认值”。对于状态字段,默认值往往比空值更合理:文章状态默认 0(草稿)就比默认 NULL 清晰得多。

默认值:哪些字段应该自动生成?创建时间默认当前时间,更新时间默认当前时间,阅读数默认 0。这一块设计得好,后面写代码时你敢大量省事,甚至能在数据库层直接完成一部分初始化逻辑。

2.4 给实体和字段起名字,别在细节上埋雷

命名规范这件事,看起来没什么技术含量,但实际上对后续开发的幸福感影响极大。我看到太多项目里出现“user”“users”“t_user”混用的情况,每个开发都有自己的风格,最后连哪张表是核心表都分不清楚。

我给自己规定了三条铁律。第一,表名用复数还是单数统一定一个,我个人用单数,user_info 比 users 更自然。第二,字段名全部小写下划线,禁止驼峰,userName 和 user_name 混在一个项目里会让 SQL 写得极度别扭。第三,避免 MySQL 保留字,比如 order、group、describe、key 都要避开,不然每次写 SQL 都得加反引号,烦不胜烦。

还有一点值得注意,就是给字段起名要贴业务而不是贴实现。字段名叫 data 或者 info 这种,跟没起名没什么区别。正确的是 mobile、nickname、password_hash,一眼就能看出实际含义。

3. 逻辑结构设计:把模型翻译成关系表,关键看三范式

概念模型是一张设计图,逻辑结构设计则是按图施工,把实体和关系转化为可以在数据库里创建的表结构。这一步是很多人理解的“数据库设计”,但它的正确打开方式其实是规范化。

3.1 从实体到二维表:一个实体一张表的转换规则

转换规则其实就三条:实体到表,属性到字段,关系变成外键或者中间表。一对一关系,在任意一方放对方主键;一对多关系,在“多”的一方放“一”的一方主键;多对多关系,新建中间表。

实际设计时,我会把上一步整理的实体清单重新打开,逐个对照生成表结构。每个实体对应一张表,每个属性对应一个字段,然后给每张表加两个通用字段:created_at 和 updated_at。这两个字段能救命,无论实际查询还是排查线上问题,你都依赖时间信息。没有它们,出了问题你连数据是什么时候写入的都不知道。

代理主键我这里多说一句。我强烈建议你在每张业务表上设置一个与业务无关的自增主键 id,而不是直接用手机号、邮箱这类业务字段做主键。业务字段做主键,一旦规则变了,比如用户允许改绑手机号,你的主键也得跟着变,连带外键引用全部要改,成本极高。代理主键本身没有任何业务含义,永远不变,稳定可靠。

3.2 主键、外键、唯一约束怎么选

主键已经说过了,尽量选代理主键,int 或 bigint,视数据量而定。小项目 int 够用,做互联网产品直接上 bigint 无符号,别给自己留不够用的量级风险。

外键有两条路线。物理外键,就是数据库层面加 FOREIGN KEY 约束,保证引用完整性;逻辑外键,就是只在业务代码层面做关联,不建数据库约束。全互联网项目的普遍做法是逻辑外键。为什么?因为物理外键在删除、更新时可能锁表,分布式架构下也是个大麻烦,性能影响明显。我不建议你做项目时把外键约束建上,但要记得在下标上建索引,不然关联查询照样慢。

唯一约束该用就要用,这是数据脏乱差的最强防线。用户手机号必须唯一,那就加独特板 key。不要相信“代码逻辑里做了唯一校验就够”的话,并发场景下两个请求同时进来,代码判断都通过,数据库一写就是两条重复记录。真正兜底的只有唯一约束。

3.3 三范式:用大白话把 1NF、2NF、3NF 讲明白

三范式不是考试题,是实实在在让表结构变合理的规则。

第一范式,要求字段不可再分。在现代关系型数据库里,这个基本已经默认满足了。如果你在文章表里写一个字段叫“作者信息”,里面又存姓名又存手机号,那就违反了第一范式。现在的做法是把作者信息拆成 author_name、author_mobile 两个字段,或者干脆只存 author_id,再关联用户表。

第二范式,要求消除部分依赖。它主要针对联合主键的情况。比如文章标签关联表 (article_id, tag_id),如果你额外存了一个 tag_name 在这个关联表里,那 tag_name 就只依赖 tag_id 而不依赖 article_id,这就是部分依赖,违反第二范式。正确的做法是 tag_name 只存在标签表,关联表只存两个 id。

第三范式,要求消除传递依赖。比如文章表里如果存了 category_name,而分类名称由 category_id 决定,那么 category_name 通过 category_id 间接依赖文章主键,这就是传递依赖。解决办法也是只存 category_id,需要名字时再 join 分类表。

我当年学三范式的时候也觉得抽象,后来发现一个更好用的判断标准:如果一个字段的值可以由另一个字段推导出来,那这个字段就该被删除或者挪到别的表里去。

3.4 适度反规范化:别为了“规范”把性能搞没了

三范式是理想状态,但实际开发里你常常要“故意违反”它。反规范化不等于瞎设计,而是在性能和一致性之间做取舍。

最典型的例子是文章详情页需要显示作者昵称。如果严格按第三范式,文章表只存 author_id,页面展示时 join 用户表。这没问题,但当系统并发量到了几千 QPS,每次详情查询都 join 一次,压力不小。这时候很多团队直接选择在文章表里冗余一个 author_nickname 字段,减少一次 join,提升查询性能。

还有计数器的设计。文章表里字段 view_count 就是冗余统计值,如果完全按范式的思路,应该靠 count 评论表里的记录数来计算。但那样每次查询都要做聚合,非常慢。冗余一个计数字段,写入时加一,读的时候直接取,性能能提高一个量级。

反规范化的代价是数据一致性风险。你冗余了 nickname,用户改昵称之后,文章表里的旧昵称不会自动更新。这种时候只能通过代码层同步更新,或者接受昵称不是实时一致的现实。我的建议是:核心数据严谨按三范式,查询热点和统计展示可以适度反规范化,但你要非常清楚自己牺牲了什么。

4. 物理设计与实操落地:写出一份能上线的建表 SQL

逻辑模型定了,表结构也画出来了,接下来才轮到真正的建表。这一步像装修的“硬装”,字段类型、索引、存储引擎每一样都必须认真对待。

4.1 存储引擎和字符集选择

以 MySQL 为例,绝大多数场景直接选 InnoDB。它支持事务、行级锁、崩溃恢复,连 8.0 版本的默认引擎都是它。MyISAM 这种古董引擎到现在还偶尔见到,但它不支持事务和外键,并发场景下性能和安全都落后,没有理由再选。

字符集我果断用 utf8mb4。很多老项目还在用 utf8,但 utf8 在 MySQL 里实际上是 utf8mb3,最多只能存 3 个字节的字符,遇到 emoji 就会报错或者存成乱码。utf8mb4 是 utf8 的超集,能完整存储 Unicode,包括那些花里胡哨的 emoji。现在的 MySQL 8.0 默认就是 utf8mb4,如果你还在用 5.7,建表时也请显式指定。

排序规则随默认就好,utf8mb4_general_ci 已经够用,不需要太纠结。

4.2 字段类型的选取:别把日期存成字符串

字段类型选择是建表的核心,我踩过的坑几乎都能排成队。最常见的坑是把日期存成 varchar 类型。这导致完全没有日期函数的支持,比较大小靠字符串比较,排序严重依赖格式,数据一但格式不统一,查询结果就是一副扭曲的画面。正确做法是用 DATETIME 或 TIMESTAMP 存储日期时间,用 DATE 存储生日这类纯日期。

金额字段也经常出错。用 FLOAT 或 DOUBLE 存钱,加减运算会积累浮点误差,结果是我明明转出去 0.1 元,账上却显示 0.0999999999。金额这类精确数值,必须用 DECIMAL。DECIMAL(10,2) 一般够用,大额交易可以 DECIMAL(14,2) 或更高。

手机号这类字符串,我见到的第二个坑是用 INT 或 BIGINT 存。手机号本身不是用来做算术运算的,用整数类型纯属自我麻烦。更离谱的是手机号有前导零的情况,用 INT 直接丢位。用 VARCHAR(20) 存手机号才是正解。

给一张常用的类型选择参考表:

数据场景推荐类型说明
主键、外键BIGINT UNSIGNED大整数,无符号
状态、枚举TINYINT0/1/2 数字枚举
标题、名称、手机号VARCHAR(50/20/20)长度按业务预留
正文、长文本TEXT / LONGTEXT不建索引
价格、金额DECIMAL(10,2)精确小数
日期时间DATETIME时区处理直观
创建/更新时间DATETIME + 默认值见下方建表

4.3 索引设计:一开始别贪多,常用路径优先

索引能加速查询,但也是成本和负担。每次插入、更新都要同步维护索引,索引文件本身也要吃磁盘和内存。初学者容易犯的毛病是看哪个字段顺眼就加索引,结果索引比数据还大,查询性能不升反降。

我的索引设计原则只有记住一条序列:主键索引 → 唯一索引 → 高频查询路径。主键索引数据库自动建,不用管。唯一索引只加在有唯一约束的业务字段上,手机号、邮箱这类有全局唯一需求的字段才加。普通索引就是针对“WHERE 后面经常出现的过滤条件”和“ORDER BY 高频排序字段”加。

还要知道一个概念叫联合索引。当查询条件同时包含 author_id 和 status,就要建 (author_id, status) 联合索引,而不是分别建两个单列索引。联合索引遵循最左前缀原则,最左边的字段必须出现在查询条件里,索引才会生效。我实际开发里习惯把等值条件写在联合索引前面,范围条件写在后面。

但注意,索引不是建了就有用。对 LONGTEXT 字段加索引大概率会失败,即便能建成,查询效率也低得可怜。长文本如果需要查询,应该考虑加前缀索引,也就是只对前 N 个字符建立索引。

4.4 完整建表语句演示:从用户信息表到文章表

纸上谈兵得够了,上一份可以直接运行的参考建表 SQL。以下都以 MySQL 8.0 为例,核心思路同样适用于 MySQL 5.7。

先建用户信息表,对应热词里的“第1关:用户信息表”:

CREATE TABLE `user_info` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID,代理主键', `mobile` VARCHAR(20) NOT NULL COMMENT '登录手机号', `nickname` VARCHAR(64) NOT NULL COMMENT '用户昵称', `password_hash` VARCHAR(128) NOT NULL COMMENT '密码哈希值,禁止明文', `avatar_url` VARCHAR(512) DEFAULT NULL COMMENT '头像地址', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1正常 0禁用', `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_mobile` (`mobile`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';

注意点:mobile 加了唯一约束,防止同手机号二次注册;password_hash 用 varchar(128) 存放哈希结果,而不是明文密码;status 设置了默认值,创建用户时即使代码忘了传值,数据库也不会乱。

再建文章表:

CREATE TABLE `article` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '文章ID', `author_id` BIGINT UNSIGNED NOT NULL COMMENT '作者ID,关联用户表', `category_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '文章分类ID,可空', `title` VARCHAR(200) NOT NULL COMMENT '文章标题', `content` LONGTEXT COMMENT '文章正文', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0草稿 1已发布 2已下架', `view_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阅读数,冗余计数器', `published_at` DATETIME DEFAULT NULL COMMENT '发布时间', `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`), KEY `idx_author_status` (`author_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章表';

这里 category_id 是可空的,因为文章创建时允许先不选分类,发布时再补也不是不行。联合索引 idx_author_status 覆盖了“查询某个作者在某状态下的文章”这个高频场景,这类查询在作者后台列表页非常常见。

再补一张多对多关联表:

CREATE TABLE `article_tag` ( `article_id` BIGINT UNSIGNED NOT NULL COMMENT '文章ID', `tag_id` BIGINT UNSIGNED NOT NULL COMMENT '标签ID', PRIMARY KEY (`article_id`, `tag_id`), KEY `idx_tag` (`tag_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章标签关联表';

联合主键 (article_id, tag_id) 能保证一篇文章不会重复打同一个标签,按文章查标签很快,按标签反查文章可以在 idx_tag 索引加持下也很快。

4.5 插入数据验证设计

建完表不要直接跑业务代码,先用几条测试数据验证约束和索引是否符合预期。我习惯用几个 INSERT 语句快速压一遍。

INSERT INTO user_info (mobile, nickname, password_hash) VALUES ('13800138000', '小明', 'a665a45920422f9d'); INSERT INTO article (author_id, category_id, title, content, status) VALUES (1, NULL, '我的第一篇博客', '你好,欢迎来到我的博客。', 1); INSERT INTO article_tag (article_id, tag_id) VALUES (1, 1), (1, 2);

插入之后再写一条 JOIN 查询,看看能不能顺利把文章、作者昵称、标签名查出来。这个验证动作能帮你尽早发现外键引用错误、类型对不上这些基础问题,等代码写到你再来排查,成本高得多。

5. 常见问题与避坑指南:这些坑我踩过,你别再踩

做数据库设计这么多年,有些坑是新手必踩,有些坑是老手偶尔也会翻车的。盘点一下最常见的几类问题和我的排查思路,希望你少走弯路。

5.1 设计不足和过度设计,怎么掌握“刚刚好”

这个问题我刚才埋了个伏笔,现在展开讲。设计不足好理解,需求只覆盖了今天,没想到明天,表结构完全撑不住新需求。过度设计则相反,啥功能都想预留下来,一张用户表,把购物车、收货地址、收藏夹统统塞进去,最后表又大又乱,谁也看不懂。

我的经验是用“迭代思维”做设计,先定最小闭环,但要在设计时留出合理的扩展信号。比如用户表不要轻易把 username 和 mobile 混成一个字段,因为登录方式一变就麻烦。相反,不要因为想着用户会有“性别、年龄、星座”,就提前建一堆可空字段,空字段对你的查询没有任何帮助,长期维护只是负担。设计时做一个简单假设:凡是近期确定要用的字段,现在就建;确定未来用但暂不清楚形态的,等到用的时候再建。

5.2 外键到底要不要加,触发器能不能滥用

我见过一些项目,建表时把外键约束拉满,删除用户时 Database 层能把相关数据全部联动删干净。听起来很省心,但真的生产环境跑起来,这些级联操作可能引发锁竞争、死锁,甚至在删除一个超大用户时直接把主库拖死。我现在做项目偏向后端代码控制逻辑,数据库只保留主键和唯一键,外键关系由应用层管理。这种方案灵活度高,对性能也友好。

触发器我基本不推荐。触发器会把业务逻辑藏在数据库里,出了问题排查链路长,而且它不是在事务边界之外跑,容易把简单逻辑变得很折磨。如果你发现自己在触发器里写复杂计算,赶紧把这些逻辑挪到应用层去。

5.3 字段类型选错导致的问题

我挑几个高频翻车现场再说一遍。用 float 存金额,结算时会出现 0.1 + 0.2 != 0.3 的幻觉;用 varchar 存时间,日志查询和排序做起来疯狂想砸键盘;用 int 存手机号,遇到 13 位数字超int范围直接报错,或者前导零被吃掉;用 text 当索引字段,非但没有提速,反而把辅助索引的体积膨胀到吓人。

这些问题的排查思路很简单,写完建表 SQL 后不要急着执行,先按数据业务去验证一遍:这个字段会不会拿来参与计算?这个字段会不会参与比较大小?这个字段最长可能是多少位?带着这堆问题再review一遍字段类型,绝大多数错都能避免。

5.4 NULL 值用得太多,查询性能的隐形杀手

NULL 不是一个普通的空值,它在数据库里是一个特殊的标记,索引不会高效处理聚合会跳过它,WHERE 条件通常要另外加 IS NULL 判断,代码里也容易遇到 NULL 引发空指针。能用默认值的字段,尽量设置 NOT NULL DEFAULT xxx,千万不要放任字段默认可空。

比如文章状态字段,如果默认 NULL,查询“已发布文章”你要写成 status = 1,查询“草稿文章”要写成 status = 0,但如果某篇文章没设置状态,它既不在草稿也不在发布,整个数据逻辑就变模糊了。把 status 设为 NOT NULL DEFAULT 0,每一个语义都明确,代码判断也简单。

5.5 上线后表结构要改,别在线上直接 ALTER TABLE

人算不如天算,上线后总是会有新的需求要加字段。这里最忌讳的是一边登录线上环境,一边手敲 ALTER TABLE 给正在跑业务的表塞列。我经历过一次,凌晨三点加字段,表已经有 5000 万行数据,ALTER 跑了 40 分钟,期间写入全部阻塞,用户侧的报错铺天盖地。

正确做法是把表结构变更管起来。所有结构修改先走变更脚本,提交到 Git 仓库,经过 review 之后通过工具执行。小团队可以不用复杂工具,但至少要保证每一次 DDL 都有记录,能回滚,能追溯。你还得知道,大表加字段需要评估锁表时间,MySQL 8.0 对 INSTANT ADD COLUMN 做了优化,但也不是所有字段类型都支持零成本加列,所以线上操作要谨慎为上。

最后给你一个非常实用的习惯:每次建表,都把建表 SQL 导出来存到项目目录里的 sql/ 文件夹,带上日期和版本号。几个月后你想知道当时为什么这么设计,打开文件看注释就懂了。一个项目跑上两三年,你回看这些SQL文件,会感谢当时那个认真设计的自己。数据库设计这条路,真的没有一个统一答案,但方法论是相通的:先业务,再概念,再逻辑,最后物理实现,每一步都踩稳,后面才不会返工。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询