第 3 章 · 数据库第 4 页,共 4 页
所有数据都要放进表格吗?
数据表要求每一行都长一个样,一道菜谱还分在好几张表里。这一页把同样四道菜谱分别存成数据表、键值和整份文档,各做两次查询、一次改名,比一比;再看一项数据是读得多还是写得多。
为什么需要
这里的表格,指前几页的。一张表有哪些列、每列放什么、有哪些规则,合起来叫(schema),要先定好;表大了以后再改,可能要把整张表重写一遍。一道菜谱又分在菜谱表、配料表和用户表(每位作者一行,写着名字)几处,取整道菜时,要把几张表里对应的行拼在一起,叫(join);只拼在这一次的结果里,表本身不变。
有的数据只按一个名字来取,比如按编号取整道菜谱。不用「多张表加联接」这一套的数据库有好几类,统称(NoSQL)。这一页只看其中两类。
怎样做
(key-value store)只按名字存取:给出一个名字,叫(key),取回它下面存的内容,叫值(value)。你平时说的键是键盘上的按键,这里只是名字,比如「菜谱:1024」。给出键,数据库直接算出它存在哪里,存得再多也一样快;不知道键,比如要找值里写着番茄的,就只能把所有的值翻一遍,或者自己另建。
(document database)把一整份内容放在一起,比如一道菜谱的菜名、作者、配料都在同一份文档里。每一份的项目可以不同:下图里只有番茄炒蛋那一份多一项小贴士。这里的文档不是 Word 或 PDF 文件。文档里的项目也能查、能建索引。下图把同样四道菜谱存成三种样子,各做两次查询、一次改名,数据量还可以换成 300 万道。
换一种存法:同样四道菜谱,三种存法各做三件事
先看这里先选一件要数据库做的事,看按钮下面三种存法各取出了多少、结论是什么;再把数据量换成 300 万道,比一比取出的数怎样变。
换一种存法的示意图:上面是数据量和三件要数据库做的事(两次查询、一次改名),按钮下面是三种存法的结果,每一种写出结论、取出了多少、怎样找的;再下面是三种存法存着的同样四道菜谱,取出来的那几项加底色,要改的地方另加标记。
还没有选要做的事。按上面的一个按钮,三种存法的结果会写在这里。
数据表
主键之外,配料名也建了索引
| 编号 | 菜名 | 作者编号 |
|---|---|---|
| 1021 | 罗宋汤 | 7 |
| 1022 | 红烧茄子 | 5 |
| 1023 | 清炒菠菜 | 7 |
| 1024 | 番茄炒蛋 | 7 |
| 菜谱编号 | 配料名 |
|---|---|
| 1021 | 番茄 |
| 1021 | 牛肉 |
| 1022 | 茄子 |
| 1023 | 菠菜 |
| 1024 | 番茄 |
| 1024 | 鸡蛋 |
| 1024 | 葱 |
| 1024 | 盐 |
| 编号 | 名字 |
|---|---|
| 5 | 阿梅 |
| 7 | 小林 |
键值
只按键存取,没有别的索引
- 菜谱:1021罗宋汤;作者:小林;配料:番茄、牛肉
- 菜谱:1022红烧茄子;作者:阿梅;配料:茄子
- 菜谱:1023清炒菠菜;作者:小林;配料:菠菜
- 菜谱:1024番茄炒蛋;作者:小林;配料:番茄、鸡蛋、葱、盐
数据库只认得键;值里写了什么,要程序取出来以后自己看。
整份文档
编号之外,配料和作者也建了索引
- 编号
- 1021
- 菜名
- 罗宋汤
- 作者
- 小林
- 配料
- 番茄、牛肉
- 编号
- 1022
- 菜名
- 红烧茄子
- 作者
- 阿梅
- 配料
- 茄子
- 编号
- 1023
- 菜名
- 清炒菠菜
- 作者
- 小林
- 配料
- 菠菜
- 编号
- 1024
- 菜名
- 番茄炒蛋
- 作者
- 小林
- 配料
- 番茄、鸡蛋、葱、盐
- 小贴士
- 出锅前撒上葱花
四道菜谱只列出主要配料,做法都省略了。300 万道、其中 3 万道用到番茄,都是示意;两种数据量下,小林都只写了这 3 道。「取出」数的是从数据库里取出来的行、值或文档,用索引找位置的那几次不算在内。
没有哪一种处处都好。数据表取整道菜要拼三张表,但作者的名字只存一处,改名只改一处,几处数据还能靠和对得上;键值按键取最快,问别的就要翻遍全部;文档一次取回整份,但这里的作者名字抄进了这位作者的每一份,改名要改好几份。各家产品的界限也没有这么分明,比如关系型数据库 PostgreSQL 也能存整份文档;不同的数据库对事务的支持程度也不同。
选存法还要看数据是读得多还是写得多。一段时间里的次数和的次数之比,叫读写比。被读的次数远多于被写的次数,叫(read-heavy);反过来叫(write-heavy)。要一项数据一项数据地看,不是看整个网站。在下图里先猜,再看。
读多还是写多:一项数据被读、被写了几次
先看这里选一项数据,先看这段时间里读者和作者做了什么、各做了几次,猜它是读多写少还是写多读少;猜完再看每件事对这一项数据是读还是写。
菜名、配料和做法,菜谱页上都显示。
- 读者打开菜谱页600 次
- 作者修改菜谱1 次
只数对「菜谱内容」这一项数据的读和写。
- 一个点代表一批读取或写入,N 是这一批的次数
先猜一猜,或者直接看答案。
600、30 和 1 都是示意。从数据库里读出这一项算一次读取,改了它(包括加一)算一次写入;读写比是同一段时间里两者之比。
读多写少的数据,多建几个索引、多放几份只供读取的拷贝都划算:写得少,维护它们就不费事。英文维基百科就是读多写少:按 2026 年 9 月的统计推算,每编辑一次约被浏览 1,200 次(编辑数含机器人)。这里数的是读者浏览和编辑的次数,用来看读和写大致的比例;数据库内部实际读写了多少次,不在这个数里。
写多读少的数据,索引要少而精,宜选写入快、容易分到多台机器上的存法:数据多到一台放不下、请求多到一台忙不过来时,就能分给几台(第 9 章细讲)。网飞(Netflix)的播放记录就是写多读少,写和读约 9 比 1(2018 年)。浏览数这样只按菜谱编号存取、不停加一的数据,正适合键值存储。
代价是什么
代价是要先想清楚数据会被怎样问:键值和文档按「一起用到的就一起存」来组织,问题一变,原来的存法就可能不合适;数据表要先定好表结构,却答得了事先没想到的问题。很多网站几种存法一起用,各放各适合的数据。
回顾:数据库这一章
- 存在哪里才不会丢:只记在程序内存里的,程序一停下、重新运行就没了;交给数据库,它等写进硬盘才说保存成功,断电重启也还在(第 1 页)。
- 整理成表:一行是一条记录,一列是一类信息,主键指出是哪一行;一格只放一个值,配料才好按种准确地查,所以另起一张表,一种一行,靠外键连回菜谱(第 1 页)。
- 很快找到一行:没有索引就逐行翻,300 万行平均翻一半;有索引就对半找,最多 22 次;代价是每新增、删除一行,或改到被索引的那一列,都要顺手改索引(第 2 页)。
- 改到一半:要改两处的事放进事务,要么全做,要么全不做(第 3 页)。
- 换一种存法:键值按键取最快,文档一次取回整份,数据表最能灵活地查,各有吃亏的时候;读多写少和写多读少的数据,存法和索引也不同(第 4 页)。
网购下单时,「扣掉库存」和「生成订单」常放进同一个事务:要么一起成功,要么一起取消。
资料来源
以下资料均于 2026-10-23 访问。
- ALTER TABLE
- 2.6. Joins Between Tables
- What is a NoSQL Database?
- Nosql Definition
- What is a Key Value Database?
- GET
- Secondary indexing
- What Is a Document Database?
- Documents
- Indexes
- Data Modeling in MongoDB
- Guide to table relationships
- 8.14. JSON Types
- Transactions
- Transactions
- Wikimedia 统计接口:英文维基百科 2026 年 9 月的用户浏览合计
- Wikimedia 统计接口:英文维基百科 2026 年 9 月的编辑合计
- Scaling Time Series Data Storage — Part I