学数据库学到这里,很多人会写增删改查了,也能用 Python 去连 SQLite 和 MySQL 了。但真正一做项目,最容易出问题的地方,往往不是 SQL 不会写,而是表设计得不对。
很多初学者第一次设计数据库时,特别容易走两个极端。
一个极端是,什么都想塞进一张表。 用户表里既有用户名,又有订单金额,又有收货地址,又有商品名称,甚至还想顺手把评论内容也放进去。
另一个极端是,拆得太碎。 明明一张表就能讲清楚的东西,硬拆成七八张表,结果查一次数据要连好多张表,自己都绕晕。
所以数据库设计真正难的地方,不在于你知不知道 CREATE TABLE 怎么写。 而在于你能不能判断:
哪些东西应该放一起 哪些东西应该拆开 拆到什么程度才算合理 以后查起来、改起来、维护起来,会不会顺手
这一章我们就专门讲这个问题。
一、为什么说数据库设计比写 SQL 更影响项目质量
先说一句特别实在的话:
SQL 写错了,通常报错,你会知道。 表设计错了,程序也许还能跑,但后面会越来越难受。
这种难受体现在很多地方。
查数据越来越绕。 更新一个信息要改很多行。 数据开始重复,而且越重复越乱。 删数据时不敢删,因为不知道会不会影响别的东西。 统计和扩展越来越别扭。 项目一加功能,表结构就开始崩。
也就是说,SQL 更像“你怎么操作数据”。 而数据库设计,更像“你一开始把数据放成了什么样”。
放得合理,后面很多事都顺。 放得不合理,后面每一步都像在补漏洞。
所以真正有经验的人看数据库,不是先看你会不会 SELECT,而是先看:
这表拆得合理不合理。
二、什么叫“表拆得合理”
这个问题没有一句绝对标准答案,但你可以先抓住一个最核心的感觉:
一张表,应该尽量只描述一类相对明确的对象或事实。
比如:
用户表,就主要描述用户。 订单表,就主要描述订单。 文章表,就主要描述文章。 评论表,就主要描述评论。
这句话看着普通,实际上特别重要。
因为很多初学者表设计出问题,根源恰恰就是: 一张表里混进了太多不同层次的东西。
比如你本来想做一个电商系统,结果设计了这样一张表:
idusernamephoneproduct_nameproduct_priceorder_timeshipping_addresscomment_content
你一看就知道不对劲。
用户信息 商品信息 订单信息 评论信息
全混一块了。
这时候程序也许勉强能跑,但数据库已经失去结构感了。
所以“表拆得合理”的第一层标准,其实就是:
一张表别什么都装。 先想清楚它到底在描述谁。
三、初学者最常犯的第一个错误:一张大表走天下
这真的是特别高频的错误。
为什么会这样
因为初学者脑子里通常是“我要做一个功能”,而不是“我要描述几类数据对象”。
比如做一个学生成绩系统,很多人会下意识写成:
idstudent_namestudent_ageclass_nameteacher_namesubject_namescoreexam_time
表面看,好像也不是不能用。 因为一条记录里,确实能把学生、班级、老师、科目、成绩、考试时间都写进去。
但问题马上就来了。
如果一个学生考了 5 门课,会出现什么
同一个学生的姓名、年龄、班级,会在多行里重复出现。 老师名字也会重复。 班级名也会重复。
这种表一开始最容易出现两个问题:
第一,重复严重。 第二,维护麻烦。
比如老师改名了,你可能得改很多行。 学生转班了,你也得改很多行。 而且你还会越来越不确定,到底哪几行该一起改。
所以“大表走天下”最大的问题不是丑,而是:
它让本来应该只存一份的数据,到处重复。
而重复,通常就是混乱的开始。
四、为什么数据重复是数据库设计里的大敌
这个点一定要真正理解。
很多新手会觉得,重复一点没关系,反正也能查。 但数据库里最麻烦的,不是“重复占空间”,而是“重复之后不一致”。
例如你有一张表里存了 100 行“张三”的信息。 有一天张三手机号变了。
你要改几处
理论上都要改。 但现实里,你很可能漏掉几处。
这时数据库会出现什么
有些行里张三还是旧手机号。 有些行里张三已经是新手机号。
这就叫数据不一致。
这类问题特别麻烦,因为它不会像语法错误一样立刻报出来。 但它会慢慢腐蚀整个系统的可靠性。
所以数据库设计里,一个非常重要的目标就是:
尽量减少不必要的重复。
不是说数据库里一点重复都不能有。 而是说,某些“本来只该有一份”的信息,不应该到处复制粘贴。
五、那表到底该怎么拆,最核心的判断方式是什么
你可以先问自己一个特别实用的问题:
这几个字段,是不是在描述同一个东西
比如:
name、phone、email它们都在描述用户。 那很自然就该放在用户表里。
order_no、amount、create_time它们都在描述订单。 那很自然就该放在订单表里。
title、content、author_id它们都在描述文章。 那很自然就该放在文章表里。
但如果你把:
用户名 订单金额 商品名称 评论内容
硬塞在一张表里,那你就要警觉了。 因为这些字段明显不是在描述同一类对象。
所以表拆分时,一个特别好用的思路就是:
先识别“对象”。 再按对象组织字段。
这是数据库设计的第一层功夫。
六、用一个学生成绩系统,把“怎么拆表”讲透
我们就拿特别常见的学生成绩系统来讲。
如果你不认真设计,最容易做成一张大表:
student_namestudent_ageclass_nameteacher_namesubject_namescoreexam_time
现在我们换个思路。
先识别这里面到底有几类对象:
学生 班级 老师 科目 成绩记录
这时你会发现,它天然就不像是一张表能讲清的世界了。
更合理的拆法,通常会像这样:
学生表 students班级表 classes老师表 teachers科目表 subjects成绩表 scores
例如学生表只管学生自己的信息:
idnameageclass_id
班级表只管班级信息:
idclass_nameteacher_id
老师表只管老师:
idnamephone
科目表只管科目:
idsubject_name
成绩表则记录“谁在什么时候考了哪门课,分数是多少”:
idstudent_idsubject_idscoreexam_time
你看,一拆开之后,整个世界立刻清楚很多。
学生信息不再为每门成绩重复存一遍。 科目名也不再重复。 老师信息也独立了。
这就是“按对象拆表”最直观的价值。
七、为什么“成绩表”本身很有代表性
因为它特别适合你理解一种重要概念:
有些表不是在描述“一个独立对象本身”,而是在描述“对象之间的一次事实”。
比如成绩表并不主要描述学生是谁,也不主要描述科目是什么。 它描述的是:
某个学生 在某次考试 某门科目 拿了多少分
也就是说,成绩表更像“事件记录”或“事实记录”。
这类表在真实项目里非常常见。
比如订单表,很多时候也是事实记录。 评论表也是事实记录。 打卡记录表也是事实记录。 支付流水表也是事实记录。
所以你在设计数据库时,不要总以为每张表都只能是“一个实体对象表”。 很多时候,系统真正最核心的表,恰恰是这种记录事实的表。
这个理解非常重要。
八、什么时候应该拆表,什么时候不必拆得太碎
这是数据库设计里特别容易拿捏不准的一件事。
有些人一学会拆表,就开始什么都拆。 结果系统变得特别绕。
你可以先记一个很实用的判断法:
如果某些字段总是一起出现、一起被使用、一起描述同一个对象,那通常可以放在一张表里。 如果某些字段明显属于另一类对象,或者会大量重复,或者生命周期完全不同,那就应该认真考虑拆开。
举个简单例子。
用户表里放这些字段通常就比较合理:
idusernamephoneemailregister_time
因为这些都在描述用户本身。
但如果你想把“用户的每一笔订单”也塞进用户表,那就明显不合理。 因为订单不是用户字段,而是一类独立记录。
再比如文章系统里:
标题 正文 发布时间 作者 id
这些放文章表里通常没问题。
但评论内容,不适合放在文章表里。 因为一篇文章可以有很多条评论。 评论是一类独立记录,不是文章自己的字段。
所以表该不该拆,一个很核心的判断点就是:
这部分数据,是“这个对象自己的属性”,还是“围绕它发生的一组独立记录”。
这两个东西一定要分清。
九、一对多关系,是数据库设计里最常见的关系之一
这一点非常重要,而且你从现在开始就得有感觉。
什么叫一对多。
一个用户,可以有很多订单。 一篇文章,可以有很多评论。 一个班级,可以有很多学生。
这就叫一对多。
在数据库里,一对多通常不应该粗暴地塞进一张表里。 更合理的做法通常是:
“一”的那部分单独一张表。 “多”的那部分单独一张表。 然后通过外键字段或关联字段把它们连起来。
比如:
用户表:
idusernamephone
订单表:
iduser_idamountcreate_time
这里的 user_id 就是在说:
这条订单属于哪个用户。
你看,这种设计就特别自然。
用户信息只存一份。 订单有多少条都没关系。 通过 user_id 就能找到它对应的用户。
这就是为什么学数据库设计,绕不开“关系”这个词。
十、为什么文章和评论一定不能混成一张表
这个例子特别适合新手。
很多人第一次做博客系统时,会想当然地写一张表:
article_titlearticle_contentcomment_contentcomment_usercomment_time
一开始如果每篇文章只有一条评论,好像还能凑合。 但只要评论一多,问题立刻爆炸。
因为你会发现:
文章标题和正文会被重复很多遍。 每多一条评论,就得重复一份文章信息。
这就特别不合理。
更自然的设计应该是:
文章表 articles:
idtitlecontentauthor_idpublish_time
评论表 comments:
idarticle_iduser_namecontentcreate_time
这里的 article_id 就是在说:
这条评论属于哪篇文章。
这一下就顺了。
文章写一次。 评论想有多少条都行。 而且评论删了、加了,都不会影响文章本体。
这就是合理拆表带来的清晰感。
十一、订单系统为什么天然适合拆成多张表
订单系统是最典型的数据库设计练手机会之一。
因为它天然就包含多类对象:
用户 商品 订单 订单明细
很多新手会想把“订单”和“商品”直接塞一起。 比如:
order_nousernameproduct_nameproduct_pricebuy_countorder_time
但你很快就会发现问题:
如果一张订单里买了多个商品怎么办 那这张表到底是一行表示一个订单,还是一行表示一个商品项
这时你就会意识到,订单世界其实至少应该拆成两层。
订单主表 orders:
idorder_nouser_idtotal_amountcreate_timestatus
订单明细表 order_items:
idorder_idproduct_idpricequantity
商品表 products:
idnamepricestock
这样一拆,你会发现逻辑一下子清楚了。
订单主表描述的是“这张订单本身”。 订单明细描述的是“这张订单里买了哪些商品”。 商品表则独立管理商品信息。
这是数据库设计中非常典型的一种拆分思路。
十二、到底什么是“这张表的职责”
这是设计数据库时一个特别核心的词。
你以后设计一张表时,最好先问自己一句:
这张表到底负责记录什么
比如用户表的职责就是记录用户信息。 订单表的职责就是记录订单本身。 订单明细表的职责就是记录订单里的商品项。 评论表的职责就是记录评论内容。
一旦一张表开始同时承担太多职责,设计通常就开始变坏了。
比如一张表同时记录:
用户信息 登录日志 订单数据 评论数据
那它几乎肯定已经越界了。
所以表应该怎么拆,很多时候不是靠死记规则。 而是靠你不断问:
这张表的职责是不是清晰
如果职责不清晰,表大概率就该重新拆。
十三、字段设计也很重要,不是只会拆表就够了
很多人一听数据库设计,就只想到“拆几张表”。 其实字段设计同样重要。
比如用户表里,你会不会把年龄设计成文本 手机号会不会允许为空 邮箱要不要唯一 用户名长度大概多大 时间字段是存字符串还是日期时间类型
这些都属于数据库设计的一部分。
也就是说,数据库设计不是只讨论“表有几张”。 还包括:
字段怎么命名 字段类型怎么定 哪些字段必须有 哪些字段不能重复 哪些字段适合做主键 哪些字段适合做关联
所以你要慢慢建立一个更完整的理解:
数据库设计 = 表设计 + 字段设计 + 关系设计
这三层其实是一起工作的。
十四、数据库设计里特别重要的一条原则:不要把列表硬塞进一个字段
这是很多初学者非常容易犯的错。
比如一个用户可能有多个爱好。 你可能会想:
hobbies = "篮球,足球,游泳"
或者一篇文章有多个标签:
tags = "Python,数据库,爬虫"
这样看起来好像很省事。 但数据库里,这类设计往往会让后续查询和维护非常痛苦。
比如你想查:
喜欢足球的人有哪些 带有数据库标签的文章有哪些
这时字符串拼着放就会特别别扭。
更好的设计,通常不是把多个值硬塞进一个字段,而是认真考虑它是不是一类独立关系。
比如:
文章表 标签表 文章标签关联表
或者:
用户表 爱好表 用户爱好关联表
你现在不用一上来把所有关联表都学得很深。 但至少要先建立一个意识:
一个字段,最好尽量存一个原子信息。 不要把一串本该拆开的东西,粗暴拼进一个字段。
这是数据库设计里非常经典、也非常重要的基本功。
十五、什么叫“不要把将来可能变化很多的信息到处复制”
这一条特别适合实战。
比如商品价格。
如果你把商品价格复制到很多地方,表面上查起来也许方便。 但你要意识到:
哪些价格是“商品当前价格” 哪些价格是“下单当时成交价格”
这两类信息,语义其实不同。
数据库设计不好时,最容易出现的问题就是:
把“当前状态”和“历史快照”混成一团。
例如:
商品表里有当前价格。 订单明细里也许需要存下单时的价格。
这时候订单明细里的价格不是多余重复,而是业务上必须保留的历史事实。 因为后面商品价格变了,老订单不能跟着一起变。
所以这里你要学会一个更成熟的判断:
不是一切重复都不合理。 关键是,这个重复是“无意义的复制”,还是“业务上必须保留的历史状态”。
这一步很关键。 因为它会让你从机械反重复,走向真正理解业务数据。
十六、什么时候应该单独拆一张“记录表”
这个特别实用。
你可以这样判断:
如果某类数据会不断新增,而且每一条都代表一次事件、一次行为、一次结果,那它通常很适合单独做成记录表。
例如:
订单记录 评论记录 登录记录 支付记录 考试成绩记录 库存变动记录
这些表的共同特点是:
一条一条不断增加 每条记录都代表一次发生过的事实 历史通常要保留
所以它们不太适合塞进主表字段里。 而更适合独立成表。
这也是为什么很多系统看起来表很多。 不是设计者喜欢拆,而是因为业务世界本来就包含很多“事件记录”。
十七、数据库设计时,一个特别重要的习惯:先画关系,再写 SQL
这一点非常实用。
很多初学者一上来就写:
CREATE TABLE ...
结果写到一半才发现,诶,商品和订单怎么连 评论和文章怎么连 学生和班级怎么连
更稳的方式其实是:
先用纸或者脑子,画出有哪些表 每张表的职责是什么 表和表之间怎么关联 再开始写 SQL
比如一个最简单的博客系统,你可以先画成:
用户表 文章表 评论表
然后再标出来:
文章属于某个用户 评论属于某篇文章 评论也可能属于某个用户
这一步一清楚,SQL 反而很好写。
所以数据库设计本质上不是“先写建表语句”。 而是“先理关系,再落结构”。
十八、初学者设计数据库时,最容易踩的几个坑
第一个坑,是一张大表装一切。
一开始觉得省事,后面数据重复、修改麻烦、扩展困难,全都会冒出来。
第二个坑,是表拆得过度。
本来一张表能清楚表达的东西,硬拆成很多张,结果自己查数据都要绕半天。
第三个坑,是不会区分“对象表”和“记录表”。
结果把评论、订单、成绩这类明显应该独立记录的东西,硬塞进主表字段里。
第四个坑,是喜欢把多个值拼成一个字段。
例如多个标签、多个爱好、多个商品,全部逗号拼一起。 短期看省事,后面查询最痛苦。
第五个坑,是只图当前能跑,不考虑后续改动。
数据库设计一旦进入项目,最怕的不是“现在能不能凑合用”,而是“以后功能一加,会不会立刻崩”。
十九、那到底有没有一个“足够够用”的设计标准
有,而且特别实用。
你现在先不用追求特别复杂的范式理论。 先抓住这几个足够实战的标准:
一张表的职责是否清晰。 字段是不是都在描述同一类对象或事实。 是不是出现了大量无意义重复。 以后修改某个信息时,会不会要改很多行。 一条记录是不是表达了一件明确的事情。 查询时是否自然,还是总要靠奇怪拼接。 以后加新功能时,这个设计会不会特别别扭。
如果这几条你都能想清楚,那你的数据库设计基本已经比很多初学者稳很多了。
二十、这章最该真正建立的,不是规则,而是结构感
学到这里,你最该真正形成的能力,其实是一种结构感。
看到一个业务场景时,你会开始本能地想:
这里有几类对象 它们各自应该是什么表 哪些是一对多 哪些是记录表 哪些字段是对象本身属性 哪些是围绕对象发生的事件
这就是数据库设计真正值钱的地方。
不是会背一句“减少冗余”。 而是你开始能把现实问题拆成结构化数据关系。
这一步一旦建立起来,后面你做用户系统、订单系统、博客系统、成绩系统时,数据库会越来越顺。
本章小结
数据库设计最核心的问题,不是 SQL 怎么写,而是表到底该怎么拆。
一张表应该尽量只描述一类明确的对象或事实。 如果把多类信息硬塞进一张表,就很容易产生重复、混乱和维护困难。
这一章你最重要的几个收获应该是:
先按对象来思考表,而不是按功能堆字段。 区分“对象表”和“记录表”。 看到一对多关系时,别硬塞进一张表。 不要把多个值粗暴拼进一个字段。 拆表不是越多越好,而是职责越清晰越好。
真正合理的数据库设计,不是靠死记几条规则。 而是靠你越来越能看懂:
一组业务数据,背后到底有怎样的结构。