如何设计一个始终正确的数据库
数据库是应用存放信息的地方。如果设计得不好,数据就会自相矛盾、丢失,或者很难快速找到。在这一课里,你会故意弄坏一个设计糟糕的数据库,然后把它修好。
关系型数据库设计首先关乎完整性,其次才是性能:通过规范化消除异常,显式地建模基数关系,按访问路径建索引,并用事务在故障和并发下维持不变式。
从一张大表到规范的多张表
一家小店把所有订单都记在一张大表里。看起来没问题,但同样的信息被一遍遍地复制(橙色单元格)。用下面的按钮看看会出什么问题,然后把它拆成多张表。
一张存在重复事实的非规范化单表。触发三种经典异常(更新、删除、插入),然后规范化到 3NF,再重复同样的操作。
CREATE TABLE customers ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL, email text NOT NULL UNIQUE ); CREATE TABLE products ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL, price numeric(10,2) NOT NULL CHECK (price >= 0) ); CREATE TABLE orders ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, customer_id bigint NOT NULL REFERENCES customers(id), product_id bigint NOT NULL REFERENCES products(id), unit_price numeric(10,2) NOT NULL -- 下单时的价格,有意为之 );
customer_id、product_id)。这些指针叫作外键,而每一行自己独有的编号叫作主键。
- 1NF:值是原子的,没有重复组(不能出现
products = "Mouse, Keyboard")。 - 2NF / 3NF:每个非主键列都依赖于主键、依赖于完整的主键、并且只依赖于主键。
email依赖的是客户而不是订单,所以要移出去。 - 有意的反规范化:
orders.unit_price故意复制了价格。它记录的是一个历史事实(实际收取的金额),而不是当前价格。 - 只有在有明确“真相来源”的前提下,才为读性能做反规范化(物化视图、缓存,或由触发器维护的列)。
关系:一个还是多个?
数据库里的东西是相互关联的。关键问题永远是:“多少个这个对应多少个那个?”选出答案,图会显示这些表是如何连接的。
选择基数,图会显示外键放在哪里,以及由哪些约束来保证。进阶模式会加入自引用关系。
(作答后显示表结构)
索引:快速找到东西
一张有一百万用户的表。没有索引时,数据库必须查看每一行。索引就像电话簿上的字母标签:几乎能直接跳到正确的位置。
顺序扫描对堆是 O(n) 的。B-tree 索引查找只需要 O(log n) 次页读取,而且扇出可达数百,所以即使是非常大的表也只有 3–4 层。索引的代价是写放大和存储空间。
没有索引
email 上有索引
这次查找能用上索引吗?
复合索引:哪些条件能用上它?
email 上有一个索引。逐个判断下面的查找。
CREATE INDEX ON people (last_name, first_name); 可以把它想成先按姓排序,同一个姓里面再按名排序。
WHERE deleted_at IS NULL)和覆盖索引(INCLUDE)。用 EXPLAIN (ANALYZE, BUFFERS) 检查执行计划,并删掉没有被任何查询用到的索引。事务:要么全做,要么全不做
Alice 给 Bob 转 $100。这是两次修改:先从 Alice 那里扣 $100,再给 Bob 加 $100。如果服务器在这两步之间崩溃了怎么办?
一笔转账包含两次写入,它们必须维持一个不变式:总余额保持不变。在两次写入之间注入一次崩溃,分别对比有事务和没有事务的情况。
并发:同时发生两笔取款
两个会话在同一时刻各从 Alice 的 $500 中取出 $100。正确结果是 $300。