跳转至

数据库设计、事务与 PHP 访问 MySQL

这一章讲什么: 上一章学会了「SQL 的语法」,这一章解决两件更高级的事: 设计——一张大杂烩表该怎么拆成多张各司其职的表(规范化),以及什么时候故意不拆(反规范化); 接通——PHP 程序怎么替人敲这些 SQL(PDO),以及怎么挡住借 SQL 混进来的攻击(注入与 XSS)。 读之前需要本组 06。

1. 顶层全景:两条线,一张图

  设计线(第 9 章):
    大杂烩表 ──规范化──▶ 四张表(Authors / Titles / Customers / Purchases)
                              │ 靠主键与外键缝合
                              ▼
                    多步写入包进事务:START → 改 → 改 → COMMIT(或 ROLLBACK 全撤)

  接通线(第 10 章):
    PHP ──new PDO──▶ 连接 MySQL ──query/fetch──▶ 结果逐行变 HTML
                        │
                        └─ 用户输入两条消毒路:
                           进数据库前 → quote / 预处理占位符(防 SQL 注入)
                           出到页面前 → htmlentities(防 XSS)

  图说:上面是「数据怎么摆」,下面是「程序怎么摸、怎么防」。

2. 设计:从问题出发拆表(原书第 9 章前半)

2.1 先问问题,再摆桌子

设计的起点不是「有哪些数据」,而是「会问哪些问题」。原书给一家网上书店列了一串查询: 有多少作者/书/客户?某作者写过什么?某客户买过什么?哪些书没被买?哪些书常被一起买? 对着这些问题,两个拆表的经验法则浮出来:

  • 联系紧的放一张表:书和 ISBN 几乎一体,可以同表;
  • 联系松的分开:书和客户之间是松耦合——一个客户能买任何书、甚至买多本, 一本书能被很多客户买、也被更多人不买15。

再补一条:经常被搜的东西,值得给它自己的表16。 按这些规则,书店至少需要三张表:Authors(作者常要被搜,且多人合著、被收录进合集)、 Books(书与作者的关系复杂到该独立)、Customers(显然该独立)17。

2.2 主键与外键:缝合线的两端

每张表围绕「那个常被搜的东西」设计,并给它一个主键。书的自然主键是 ISBN (要处理「不同版本不同 ISBN」);作者和客户没有天然唯一标识,就用 AUTO_INCREMENT 造一个。 铁律:别选可能撞值的列当主键——ISBN 是行业里罕见的现成主键,大多数场合你得自己造18。 (原书顺带介绍了 UUID/GUID:128 位全局唯一标签,占空间大、用不好伤性能,MySQL 里不常见19。)

外键(foreign key):一张表里「引用另一张表主键」的列。 Authors 表里的 ISBN 列就是外键——它把作者缝到书上。注意它不能当 Authors 的主键 (同一本书多位作者,ISBN 在表里重复),主键和「用来连接的键」是两件事: 主键保证行唯一,外键保证表间能缝20。

2.3 规范化:一张怪兽表的拆解实录

规范化(normalization)= 把数据拆进各表 + 给各表建主键。目标一句话:每条信息在整个数据库里只出现一次。 为什么?因为重复 = 失配的风险:书名在 Authors 表和 Books 表各存一份, 改一处漏一处,数据库就不一致了21。规范化走三级台阶,关系模型之父 E. F. Codd 定下的。

起点:Table 9-1,一张把作者、书名、ISBN、价格、客户名、客户地址、购买日期全塞一行的怪兽表 (重复处原书用粗体标出),记一次购书就要把书和客户的全部信息重抄一遍。

第一范式(1NF)三条22:

  1. 没有「同类数据重复出现的多列」——Table 9-1 的 Author 1、Author 2 两列正犯此条;
  2. 每列只装一个值——Programming PHP 有三个作者,挤进一列就犯此条;
  3. 有主键唯一标识每行——ISBN 已满足。

拆法:把作者整体挪进一张新 Authors 表(每行 = 一个 ISBN 配一位作者,多位作者就多行)。 新表里 ISBN 只是外键加普通索引,不是主键——这正演示了上一节的规矩23。

第二范式(2NF):消灭「跨行重复」。 Table 9-2 里 Darren Ryder 买两本书,他的姓名地址就被抄了两遍——客户信息该进自己的表。 拆完得到:Titles 表(ISBN、书名、价格,一本书一行)和 Customers 表。 Customers 表需要一个新主键(因为 ISBN 跟客户没关系),AUTO_INCREMENT 的 CustNo 登场; 地址拆成 Address/City/State/Zip 四列,好搜好改24。 于是多出了第四张表:Purchases(CustNo、ISBN、日期)——它同时引用两张表的主键, 是「谁买了什么」的专职记录。Purchases 甚至可以没有主键(一人同日买两本一样的书就是两行), CustNo 和 ISBN 各做个普通索引即可25。

第三范式(3NF):消灭「传递依赖」。 Customers 表里的 City、State、Zip 其实相互依赖(地址依赖城市、城市依赖州), 不直接依赖主键 CustNo。严格做到 3NF 要把它们再拆成 Zip→City→State 三张查找表, 改一次城市名,全库只改一行26。但拆不拆是两问定夺: ①这表以后会不会常加列?②有没有字段可能需要全局批量改? 任一答「是」才值得做 3NF;否则留着更简单的 2NF 结构27。

2.4 三种关系:一切缝合的骨架

关系 样子 数据库里的实现
一对一 一夫一妻——罕见(原书说连「作者-书」「地址-客户」都不是) 通常干脆合一张表;分表只有两个理由:防关系日后变化、表太宽要瘦身28
一对多 一个客户 ↔ 他的多笔购买 「多」的那张表里加一列,存「一」的主键(Purchases 存 CustNo)29
多对多 多客户买多书 加第三张表,只存两边的键(中间表存 CustNo+ISBN),它的唯一职责就是把两边缝起来30

中间表让任意遍历成为可能:从邮编 23219 出发 → Customers 找到 CustNo 2 → 中间表找到两个 ISBN → Titles 看书名价格、Authors 看作者。 人肉走嫌累,MySQL 毫秒级。原书点出一个深意:这一路过关不需要知道客户是谁—— 关系数据库既能追踪人,也能在不知道谁是谁的情况下找出有用的关联31。

2.4b 把拆解走一遍:一张怪兽表的四步瘦身

拿原书的网上书店数据当主线例子,逐步看「规范化」实际上手时每一刀切在哪。 起点 Table 9-1 共 5 行购买记录,每行 8 列(作者 1、作者 2、书名、ISBN、价格、客户名、 客户地址、购买日期)——粗体标出的全是重复:PHP Cookbook 整行信息出现两次, Darren Ryder 的姓名地址出现两次。

第一刀(1NF):作者出列。 砍掉 Author 1/Author 2 两列,新建 Authors 表 (每行 = ISBN + 一位作者)。Programming PHP 的三位作者各占一行, 「一列塞多人」的违规随之消失22。此时主表剩 6 列,立刻清爽。

第二刀(2NF):客户出列。 主表里 Darren Ryder 的两行还重复着他的地址—— 客户信息进 Customers 表,每人一行,发 CustNo(1 到 4)当主键,地址拆成四列24。

第三刀:购买记录单独立表。 客户搬走之后,「谁什么时候买了什么」无家可归—— 新建 Purchases 表:每行 = CustNo + ISBN + 日期。五行购买记录原样落进这张三列表25。

四张表的终态:

表 主键 装的什么
Authors (本书略,实际该有作者号) ISBN ↔ 作者(可一行一位)
Titles ISBN 一本书一行:书名、价格
Customers CustNo 一个客户一行:姓名、地址四列
Purchases 无(或联合索引) 一笔购买一行:CustNo、ISBN、日期

原先 5 行 × 8 列、处处重复的怪兽,变成四张各司其职、每处信息只存一份的表。 验证缝合:问「Darren Ryder 买过什么书?」——Customers 按名字找到 CustNo=2, Purchases 按 CustNo=2 找到两个 ISBN,Titles 按 ISBN 翻译出书名。 三步查找,每步都是第 8 章学过的 WHERE 查询31。

第三范式的一刀切不切? 若严格做 3NF,Customers 还要按「地址依赖城市、城市依赖州」 再拆成 Zip/City/State 三张查找表。原书的判断留给两个现实问题: 这客户表以后会不会频繁加列?城市会不会有全局改名的一天? 开书店多半两问皆否,2NF 就够;给邮政系统做库则两问皆然,3NF 的价值立刻兑现27。

2.5 反规范化:高流量网站的逆行

学会规范化之后,原书立刻教什么时候别用:高流量网站永远不要完全规范化。 多表连接天然比单表查询贵;并发用户一多(几十个并发就能产生数百次库访问), 规范化的库会明显变慢。对策是把常被一起查的数据故意冗余——多存几份, 省掉连接,代价是更新时要同步每一处副本(可用触发器或定时 PHP 同步程序兜底)。 但书也给了新手顺序:先把表规范到 1NF/2NF 练出手感,等真撞上性能瓶颈再谈反规范化32。

3. 事务:要么全成,要么全不算

3.1 为什么需要

转账是最经典的例子:从 A 账户扣 250、给 B 账户加 250,两条 UPDATE。 任何一条单独成功都不算数——只加不减,钱凭空多了;只减不加,钱凭空没了。 更麻烦的是「查询执行完了还能撤销吗」——没有事务,你只能自己一步步倒推回滚。 事务(transaction)把一串查询打包成一个原子单元:全部成功才落地, 任何一步失败,整个包作废,仿佛没发生过33。事务还顺手解决并发: 多个用户同时操作时,MySQL 把事务排队,轮流执行,互不踩脚34。

3.2 三个命令走一遍(主线例子)

CREATE TABLE accounts (number INT, balance INT, PRIMARY KEY(number)) ENGINE InnoDB;
INSERT INTO accounts(number, balance) VALUES(12345, 1025);
INSERT INTO accounts(number, balance) VALUES(67890, 140);
-- 初始:12345 有 1025,67890 有 140

START TRANSACTION;
UPDATE accounts SET balance=balance-250 WHERE number=12345;
UPDATE accounts SET balance=balance+250 WHERE number=67890;
-- 此刻再 SELECT:800 和 390 —— 但只是临时的!

ROLLBACK;
SELECT * FROM accounts;   -- 回到 1050 和 140,仿佛转账没发生

换成提交版:START TRANSACTION 开头 → 若干修改 → COMMIT 收尾; 在 COMMIT 之前,MySQL 把所有改动视为临时的——这是 ROLLBACK 能撤销的前提35。 实验完 DROP TABLE accounts; 收摊36。

两个细节:①事务要用 InnoDB 引擎(5.5 起默认,建表时写 ENGINE InnoDB 强制); ②START TRANSACTION 是标准 SQL 写法(BEGIN 也行)37。

把转账例子再走一步,看清「临时」的边界:

  事务前:  12345 = 1025    67890 = 140
     START TRANSACTION
     UPDATE … -250          → 12345 = 775   (临时!)
     UPDATE … +250          → 67890 = 390   (临时!)
     -- 此刻别的连接 SELECT 看到什么?
     -- 取决于隔离设置;教学语境下当作「尚未发生」
     ROLLBACK
  事务后:  12345 = 1025    67890 = 140      一切如旧
     若换成 COMMIT:两处新值才永久落盘

「直到 COMMIT 都算临时」是理解事务的支点:ROLLBACK 之所以可能, 正因为 MySQL 一直把改动攒在事务里、没写进正式数据35。

3.3 EXPLAIN:给查询拍 X 光

EXPLAIN 任何查询,MySQL 交出它的执行快照:用了哪个索引、扫描几行、查询类型多高效。

EXPLAIN SELECT * FROM accounts WHERE number='12345';
→  type=const(最高效档)  possible_keys=PRIMARY  key=PRIMARY  rows=1

关键列:type 从低效到高效是 ALL < index < range < ref < eq_ref < const < system < NULL—— 看到 ALL 就是全表扫,该加索引了;rows 是要扫的行数,1 是好答案38。 原书的使用建议很直白:查询变慢,先 EXPLAIN,再决定改查询还是改表设计39。

3.4 备份与恢复:mysqldump 一条龙

数据有价——哪怕只是重新录入的人工。备份三原则:常备份、多副本(不同地点、 甚至防火保险箱)、定期演练恢复(真出事时你往往又急又慌,恢复流程必须提前跑熟)40。

工具是 mysqldump:把库导出成能重建一切的 SQL 指令文件(也能导成 CSV——逗号分隔的纯数据文本——或 XML)。 唯一大忌:导出期间不许有人写表——最稳是临时关停 MySQL;不停机就 LOCK TABLES ... READ 锁表、UNLOCK TABLES 解锁41。

# 备份:导出到文件(> 重定向)
mysqldump -u user -ppassword publications > publications.sql
mysqldump -u user -ppassword --all-databases > all_databases.sql   # 全库

# 恢复:反过来,用 mysql 程序吃回文件(< 重定向)
mysql -u user -ppassword -D publications < publications.sql

导出的文件里开头有 DROP TABLE IF EXISTS——先删旧表再重建,所以覆盖恢复也不会报错, 这是备份文件「聪明」的地方42。CSV 导出(--no-create-info --tab=目录 --fields-terminated-by=',') 只含数据无指令,适合喂给表格软件43。备份频率跟着更新频率走:日更就日备; 多副本存不同机器、U 盘另存异地44。

4. PHP 访问 MySQL:PDO 六步走(原书第 10 章)

4.1 六步流程与凭据文件

用 PHP 当中间人的目的:把 SQL 的结果格式化成网页上可见的样子—— 命令行输出给人看,PHP 拿到的是「PHP 认得的结构」,再逐行变成 HTML45。 流程六步:连接并选库 → 造查询串 → 执行 → 取结果输出 → 重复 2–4 → 断开46。

第一步之前,先把凭据收进单独的 login.php:

<?php // login.php
  $host = 'localhost';  $db = 'publications';
  $user = 'root';       $pass = 'mysql';
  $chrs = 'utf8mb4';
  $attr = "mysql:host=$host;dbname=$db;charset=$chrs";
  $opts = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
  ];
?>

三个讲究:① PDO(PHP Data Objects)是 PHP 内置的数据访问层, 一套函数通吃各种数据库47;② utf8mb4 字符集——能装中文、日文、韩文、emoji 的 Unicode 集,MySQL 里推荐的 UTF-8(这条对中文读者是要划重点的)48; ③ 文件必须带 <?php ?> 标签——有人直接在浏览器里请求 login.php 时, 带标签只显示空白页;不带标签,用户名密码就裸奔在网页上49。 原书也坦白:教学例子里明文存密码,真实项目绝不允许, 妥善做法要等第 12 章(加盐、加密)再补50。

4.2 连接、查询、取数

require_once 'login.php';   // 缺了它 = 致命错误,所以用 require_once

try {
  $pdo = new PDO($attr, $user, $pass, $opts);
}
catch (PDOException $e) {
  throw new PDOException($e->getMessage(), (int)$e->getCode());
}

$query  = "SELECT * FROM classics";
$result = $pdo->query($query);

while ($row = $result->fetch()) {
  echo 'Author: ' . htmlspecialchars($row['author']) . "<br>";
  // …title/category/year/isbn 同理…
}

注意三点:① 查询串末尾不带分号(不是命令行);② SELECT * 只为省事, 大表要列出真正需要的列,别把服务器内存撑爆51;③ 每个要显示的字段都过 htmlspecialchars——就算你认为数据早已消毒,出库再消毒一次,挡的是 XSS(马上讲)52。

fetch 的五种风格(在连接选项或 fetch 参数里指定)53:

风格 返回
PDO::FETCH_ASSOC 关联数组,键 = 列名(常用)
PDO::FETCH_BOTH 默认:列名 + 数字下标双份
PDO::FETCH_NUM 纯数字下标
PDO::FETCH_OBJ 匿名对象,列名当属性
PDO::FETCH_LAZY 兼而有之的惰性对象

关联数组的好处:$row['author'] 按名取,不用记列号。手动关闭连接: $pdo = null;(小脚本不必,收尾 PHP 自会回收)54。

还有一条安全提醒藏在连接代码里:别把数据库报错直接输出给用户——错误信息里可能含 登录细节,等于递给黑客;该把 getMessage() 写进日志,给用户看自定义的友好提示。 重抛异常还有一层考虑:新的异常栈轨迹不再含原异常参数里的登录信息55。

4.3 一个能跑的小应用:sqltest.php

原书的第一个完整小应用(例 10-6,约 80 行):同一个 PHP 文件既收表单、又显示表单和全表, 模式就是第 7 章上传教过的「一个程序被调两次」。

走一遍:

  1. 表单以 POST 提交回本文件;字段进 $_POST——表单字段几乎总用 POST: GET 会把数据挂在网址上(难看、易改、会进服务器日志)56。
  2. 程序开头用 isset($_POST['delete']) 判断本次是「删」还是「增」: 每行记录旁边都有一个 DELETE RECORD 按钮,藏在隐藏字段里带回本行的 isbn (ISBN 是唯一标识,删谁全靠它)57。
  3. 增删之前,每个用户输入过一遍 sanitize_post_value()——内部是 $pdo->quote(): 转义危险字符、自动加引号。 quote 替你加了引号,拼 SQL 时就别再手写引号, 否则双引号直接语法错误58。
  4. 处理完,无条件执行 SELECT * FROM classics,把全表重新显示—— 于是「增删之后页面自动刷新出最新状态」,这个「改完重查」的环路就是所有管理页面的原型。
  5. 试跑:加一条 Moby Dick;再加一条五个 1 的记录,删掉它;重复加五个 1, 第二次会撞主键报错——这个报错正是主键在站岗59。

4.4 AUTO_INCREMENT 与 lastInsertId

往带 AUTO_INCREMENT 的表插行,那个列传 NULL——MySQL 自动发号,忽略你传的 NULL60。

把「动物园五连操作」走一遍(例 10-7 到 10-14,野生动物园养猫的 cats 表):

  1. 建表:CREATE TABLE cats (id SMALLINT NOT NULL AUTO_INCREMENT, family VARCHAR(32) NOT NULL, name VARCHAR(32) NOT NULL, age TINYINT NOT NULL, PRIMARY KEY (id))—— 三列内容列 + 一个自增主键,全部 NOT NULL。
  2. 插三只:INSERT INTO cats VALUES(NULL, 'Lion', 'Leo', 4) 等三条(或一条多值); 首列 NULL 就是「把发号权交给 MySQL」。
  3. 验收:SELECT * FROM cats 出表格,id 列自动是 1、2、3——发号成功。 (顺带一提:浏览器里查表时每一格都过了 htmlspecialchars 再进 <td>—— 数据库里的旧数据也可能藏着恶意字符,出库消毒不分场合52。)
  4. 改:UPDATE cats SET name='Charlie' WHERE name='Charly'——拼错的豹子名修正,一行受影响。
  5. 删:DELETE FROM cats WHERE name='Growler'——转园的美洲狮退场,剩 1 和 3 两行。
  6. 再插山猫:新行拿到 id=4 而不是回填的 2——删过的号不复用64。

这组五连操作把第 8 章手敲的每条 SQL 都用 PHP 跑了一遍, 你会注意到两者几乎逐字相同——PHP 只是 SQL 的邮差,语法本身没变。

DESCRIBE cats 也能从浏览器里验:查出结果用 PDO::FETCH_NUM 逐格填进 HTML 表格 (例 10-8)——命令行不在手边时,这是一枚好用的「远程验表」工具62。 插入后要拿到刚发的号,用 $pdo->lastInsertId()。 为什么必须问数据库而不是「查最大 id 加一」?并发下你查完最大值的瞬间, 别的插入可能已经改了这个最大值——发号是 MySQL 的内部序列,自己算必出乱61。 典型用法:插完新客户拿 CustId,紧接着插订单时引用它(例 10-14 的 owners 例子)63。 顺带一个观察:删掉 id=2 后,新行拿的是 4 不是 2——删过的号不复用,免得历史数据错乱64。

4.5 二次查询:join 的手工版

例 10-15:先查 customers 全员,循环里对每个客户的 isbn 再查一次 classics 拿书名作者—— 输出「Joe Bloggs purchased ISBN 9780099533474: 'The Old Curiosity Shop' by Charles Dickens」。 原书老实说:这个例子只为演示「二次查询」的手法;其实一条 NATURAL JOIN 就能搞定65。 手工二次查询的价值在逻辑复杂时(第二次查询依赖第一次的结果才能构造)—— JOIN 写不动的时候,循环里再查是合法的退路,代价是查询次数变多。

5. 防注入:SQL 与 XSS 两扇门

5.1 SQL 注入:一个 # 号的破坏力

最朴素的登录校验:

$user = $_POST['user'];  $pass = $_POST['pass'];
$query = "SELECT * FROM users WHERE user='$user' AND pass='$pass'";

把用户输入原样拼进 SQL,会发生什么?某人在用户名字段输入:

admin' #

拼出来的查询是 ... WHERE user='admin' #' AND pass=''——# 在 MySQL 里是注释起点, 密码校验整段被注释掉,任何人以 admin 登录成功。这就是 SQL 注入(SQL injection)66。 更狠的:anything' OR 1=1 # 拼进 DELETE 语句——OR 1=1 恒真, 注释吞掉后半段,users 表整表被删67。

第一层防线:$pdo->quote()——转义引号、自动包裹,本章前半的例子都在用68。 但原书明确说:这些方法都还在「拼字符串」,真正该用的是占位符——学会了之后, 就不该再用直接拼值的方式69。

5.2 预处理语句与占位符:几乎防弹的正路

预处理语句(prepared statement) 的思想:SQL 和数据分两趟走。 先把语句发给 MySQL 编译好,数据占的位置用 占位符(问号 ? 或命名参数 :name)占着; 执行时数据直接送往数据库,根本没有被「当作 SQL 语句的一部分」解释的机会—— 注入从机制上失去入口70。

PDO 的写法,两条路:

路一:问号占位 + bindParam(啰嗦但直观):

$stmt = $pdo->prepare('INSERT INTO classics VALUES(?,?,?,?,?)');
$stmt->bindParam(1, $author,   PDO::PARAM_STR, 128);
// …2 到 5 同理:位置、变量、类型、(字符串的)最大长度
$author = 'Emily Brontë';  // …赋值
$stmt->execute();
printf("%d Row inserted.\n", $stmt->rowCount());

路二(推荐):命名占位 + 数组 execute(干净,顺序无关):

$stmt = $pdo->prepare('INSERT INTO classics
  VALUES(:author,:title,:category,:year,:isbn)');
$stmt->execute([
  'author'   => 'Emily Brontë',
  'title'    => 'Wuthering Heights',
  'category' => 'Classic Fiction',
  'year'     => 1847,
  'isbn'     => '9780553212587'
]);

数组的键对上 :名字,次序随便;PHP 还替你猜数据类型,不用声明71。 rowCount() 报告「动了几行」,是验证执行结果的顺手工具72。 原书的定性原话:「pretty much bulletproof in terms of security」(安全性上近乎防弹)73。 (另:PHP 老版本有个 magic quotes 自动转义功能,5.4 已移除——别依赖、也别怀念74。)

为什么占位符比 quote 高一个信任等级? 把两条路并排放就清楚:

// 拼串路(quote):数据和 SQL 一趟走,靠「转义字符」让数据看起来不像 SQL
$author = $pdo->quote($_POST['author']);   // 转义 + 加引号
$query  = "INSERT INTO classics VALUES($author,…)";   // 混在一句话里发给 MySQL

// 占位符路:数据另走一条通道,根本不进「语句」这层
$stmt = $pdo->prepare('INSERT INTO classics VALUES(:author,…)');
$stmt->execute(['author' => $_POST['author']]);       // PHP 与 MySQL 都知道这是数据

拼串路的安全完全押在「转义没有漏网之鱼」上——漏一个引号、一个新攻击手法,就是一条缝; 占位符路在结构上取消了「数据被当语句解释」的可能,缝不存在70。 这就是原书那句「学会了占位符,就不该再用直接拼值」的分量所在69。

5.3 XSS:输出的那扇门也要守

跨站脚本(XSS):用户输入的内容(比如评论)里藏了 HTML/JS, 你的网站把它显示出来时,脚本就在别的访客的浏览器里执行了—— 偷 cookie、劫持会话、伪造登录框钓鱼。伤的是你的用户,不是你的服务器75。

防线一条:显示用户输入前过 htmlentities()——所有 HTML 标记变成纯文本显示, <script> 变成 <script>,浏览器只当字看,不会执行76。 例 10-20 把两道防线收进两个函数:mysql_fix_string(进库,quote/占位符)与 entities_fix_string(出库,htmlentities)。进消毒、出消毒,两条路缺一不可—— 这是第 3 章那句「htmlentities 是普适规矩」的正式收束77。

6. 作者的判断与证据

书中的说法 成色
规范化目标是「每条信息只出现一次」 定义性陈述,冗余→失配风险的论证(改一处漏一处)具体可信21
高流量站「应该尽量反规范化」 工程经验之谈,语气很强(「I would go so far as to say」),同时给了平衡建议:新手先全规范化。无基准数据,属作者多年实战口径32
事务「确保查询按正确顺序且全部完成」 机制事实,转账例的两种失败形态(钱凭空多/少)是教科书级论证33
占位符「几乎防弹」 机制性结论,依据是「数据不被解释为语句」——这比 quote 转义高一个信任等级;但「几乎」二字诚实:应用层的逻辑漏洞它管不了73
别用「查最大 id + 1」代替 AUTO_INCREMENT 并发安全论证(取值与存入之间可能被并发插入改变),理由精准61
备份「要定期演练恢复」 运维经验,「你可能要在又急又慌的时候恢复」这句比任何技术指标都有说服力40
GET 表单「可能的话用 POST 替代」 保守建议,理由是 URL 美观、日志泄漏、易篡改;对幂等的纯查询,GET 其实合理——书中未区分,属于一揽子口径56
login.php 凭据集中的做法 工程惯例(改密码只改一处),但明文存储本身被原书声明为仅限教学;第 12 章之前别把它当范本50
生产环境不输出数据库报错 安全基线,论证具体到「错误信息含登录细节」;重抛异常抹掉栈轨迹里的凭据,是常被忽略的细心处55

7. 边界与局限

  • 外键约束全文缺席:设计一章讲了「外键」概念,却没讲 FOREIGN KEY 约束本身—— 数据库层面自动拒绝「引用了不存在的 ISBN」这类脏数据,本书让读者靠应用代码自觉。 这是教学简化,也是和第 9 章「规范化」主张之间的一处张力。
  • 事务讲完三个命令就止步:隔离级别(脏读/幻读)、死锁、保存点全未出现; 「高并发下该怎么用事务」只有一个排队比喻34。
  • EXPLAIN 只示范了最理想的一行输出:type=const + key=PRIMARY 是最佳情形; 差的查询长什么样、rows 上千时怎么办,没有反例演示——学完能认出好查询,未必认得出坏查询。
  • PDO 的用法停在 query + prepare:事务在 PDO 里的用法(beginTransaction/commit)、 错误处理的完整策略没讲——第 10 章的事务知识是 SQL 层面的,PHP 侧怎么调用要靠自己接上。
  • 备份的「自动化」缺位:mysqldump 是手动工具;真实运维要定时任务(crontab)+ 增量备份(binlog)+ 异地容灾,这一层本书未涉——学完本章能救火,还不能值班。
  • 二次查询的性能代价未量化:例 10-15 每个客户多打一次查询,N 个客户 = N+1 次查询 (行业里叫「N+1 问题」),原书演示了手法但没点破这个性能经典的代价与 JOIN 的对比数字。
  • XSS 一节只讲存储/反射的入口、不讲全部形态:DOM 型 XSS、CSP(内容安全策略)等 现代防线未提;htmlentities 是必要非充分(补充(不在书里,来自通用知识):现代实践还配合 HTTP 头里的 Content-Security-Policy)。
  • 凭据管理到第 12 章才补:本章明文密码的例子里三层外四层的警告, 但替代方案(环境变量、配置文件权限、密钥管理)散在后面的章节,没有一处集中交代。

8. 章末自测:两章的问答题合答

这两章的章末题是全书最「面试风」的一组——答得出它们,简历上「熟悉 MySQL/PDO」才算有据:

原书的问题 我们的回答
关系型数据库的「关系」指什么? 表之间通过键建立的关联(一对一/一对多/多对多)30
消除重复、优化表的过程叫什么? 规范化(normalization)21
第一范式的三条? 无重复列族、每列单值、有主键22
怎么满足第二范式? 先达 1NF,再把「跨行重复」的列拆进自己的表24
一对多的两表靠什么列缝合? 「多」表里加一列存「一」的主键29
多对多怎么建? 加第三张中间表,只存两边的键30
事务的发起与结束命令? START TRANSACTION(或 BEGIN)发起;COMMIT 或 ROLLBACK 收尾37
哪个功能能细看查询的执行? EXPLAIN38
备份 publications 到 publications.sql? mysqldump -u user -ppassword publications > publications.sql41
PDO 怎么连 MySQL? new PDO($attr, $user, $pass, $opts)(参数在 login.php)47
怎么提交查询? $pdo->query($query)(预处理的正路是 prepare+execute)70
哪种 fetch 风格按列号返回数组? PDO::FETCH_NUM(默认 BOTH、常用 ASSOC)53
怎么手动关闭 PDO 连接? $pdo = null;54
AUTO_INCREMENT 列插入时传什么? NULL(MySQL 自动发号)60
哪个 PDO 方法能转义用户输入? $pdo->quote()——但正路是预处理占位符68
保证数据库访问安全的最佳方式? 预处理语句(占位符)73

答完这组题,可以自检一个更高的标准:不看任何材料,把「转账事务」和「登录注入」 两个例子各讲一遍——一个是数据完整性的正例,一个是数据完整性的反例,两章的骨架就是它们。

9. 可带走的

  1. 设计从「会问的问题」出发:紧耦合同表、松耦合分表、常搜的给专表。
  2. 主键唯一不空;ISBN 是罕见的天然主键,其余 AUTO_INCREMENT 自造。
  3. 规范化三问:1NF 拆列族、2NF 拆行重复、3NF 拆传递依赖;3NF 值不值,看「会加列吗、会全局改吗」。
  4. 三种关系:一对一(通常合表)、一对多(多端存一端主键)、多对多(中间表)。
  5. 高流量站反规范化换性能,冗余副本靠触发器或同步程序;新手先全规范化。
  6. 事务三件套:START TRANSACTION 打包,COMMIT 落地,ROLLBACK 全撤;只用 InnoDB。
  7. EXPLAIN 看 type 与 rows:ALL = 全表扫,该加索引。
  8. mysqldump 备份(> 导出)、mysql 恢复(< 导入);锁表或停机保一致性;多副本异地;定期演练恢复。
  9. PHP 侧六步:login.php 凭据 → new PDO → query/fetch → htmlspecialchars 输出 → 用完置 null。
  10. AUTO_INCREMENT 插 NULL;取新号用 lastInsertId(),别自己算。
  11. 防注入正路是预处理占位符(数据与 SQL 分两趟);quote() 是过渡手段,magic quotes 已死。
  12. XSS 在输出侧:显示用户内容必过 htmlentities。进消毒、出消毒,两扇门都关。

9.1 两章合成一张「数据事务全景」

  设计              运行              安全              运维
  ───────────────   ──────────────    ──────────────    ──────────────
  规范化拆表         事务包多步写       占位符进消毒       mysqldump 备份
  主键/外键缝合      COMMIT/ROLLBACK   htmlentities 出    锁表保一致
  三种关系定骨架     EXPLAIN 调优       quote 是过渡路     多副本异地+演练
  反规范化换性能

  图说:四列各管一段生命周期——怎么摆、怎么跑、怎么防、怎么保。

9.5 备份策略的三个真实场景(把原则落成日历)

第 3.4 节给了"多副本、异地、演练"三原则,这里落成三张日历:

场景一:个人博客(更新频率:月) 每周日 mysqldump 一次 → 备份文件复制到笔记本和 U 盘各一份 → 每月头把三个月前的旧备份归档。每月花 20 分钟,恢复演练并入 "每季度重装一次本地环境"的习惯里——演练的边际成本几乎为零。

场景二:小型电商(更新频率:实时订单) nightly 全量 mysqldump + 每日两次把备份文件 rsync 到异机; 外加开启 MySQL 的二进制日志(binlog,书外),它是"增量"的载体—— 全量恢复到昨晚 + binlog 重放到故障时刻,丢单窗口从一天缩到几分钟。 原书没讲 binlog,但它就是"日更"与"实时"的分界线。

场景三:第 22 章 Robin's Nest(更新频率:开发期随意) 一条 mysqldump -u root -pmysql robinsnest > backup.sql 在每次 大改之前手动跑一次,文件名带日期。够了——开发数据的备份哲学是 "改之前留后悔药",不是"365 天不间断"。

三场景的共性:备份的价值 = 数据重录成本 × 丢失概率 ÷ 备份成本。 Robin's Nest 重录十分钟,备份的意义主要是练手;电商订单重录不可能, 备份就是命。

9.6 事务的「隔离」是什么(把第 3.2 节的一句话说透)

事务一节说"多个用户轮流执行,互不踩脚",这背后有个专门概念: 隔离级别(isolation level)。它回答的问题是:"两个并发的事务, 互相能看见对方改到哪一步?"从最松到最严四档(书外扩展,MySQL 默认 是可重复读):

级别 一个事务能看到别的事务未提交的改动吗 代价
读未提交 能(脏读) 最快,几乎不保护
读已提交 只能看已提交的 Oracle 系默认
可重复读 同一事务内两次读结果一致 MySQL InnoDB 默认,防幻影
串行化 完全排队 最慢,几乎等同单线程

原书只演示了"排队"这一个语义,但知道有四档,你就懂了为什么 高并发网站爱调低级别(快),银行系统爱调高级别(准)。 第 22 章的项目全程默认档——教学项目的正确选择。

9.7 EXPLAIN 的反例:一张"坏查询"长什么样

第 3.3 节只见了好查询(type=const, rows=1)。没有反例, 认不出坏查询。造一个(基于 classics 无索引的假想状态):

EXPLAIN SELECT * FROM classics WHERE author LIKE '%Dickens%';
→ type: ALL   key: NULL   rows: 5      ← 全表扫,无所谓(5 行)
EXPLAIN SELECT * FROM orders  WHERE customer LIKE '%张%';
→ type: ALL   key: NULL   rows: 981234  ← 全表扫,98 万行,这就是事故

两行对照教两件事:① ALL 不等于错——表小到几百行,全表扫反而最快; 行数才是代价的度量;② LIKE 以 % 开头用不了索引(第 8 章 §7.3 的 伏笔)——"怎么优化"常常不是加索引,而是改查询(全文检索交给 FULLTEXT, 第 8 章 §6.4)。EXPLAIN 的价值就是把这两件事从"感觉慢"变成"看着慢"。

9.8 PDO 错误处理的完整形态(把 try/catch 补全)

§4.2 给了连接失败的 try/catch 骨架,生产代码里它要长全:

try {
  $pdo = new PDO($attr, $user, $pass, $opts);
  $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
  error_log($e->getMessage());            // 细节进日志(服务器上的文件)
  die('系统暂时不可用,请稍后再试');       // 用户看到的一句人话
}

三个部件各管一段:ERRMODE_EXCEPTION 让所有后续查询失败都抛异常 (而不是返回 false 等你忘记检查);error_log 把带栈的细节写进 服务器日志——排错时去那里翻;die 的用户版消息不含任何技术细节 (原书 §4.2 的警告框:报错里的库名、表名、SQL 都是给攻击者的情报)。 第 22 章的项目用了简化版(裸 throw)——我们的边界节已点名,此处是 它应该长成的样子。

9.9 规范化与反规范化的「度」:一个可操作的判据

第 2.5 节给了作者的强主张("尽量反规范化")和给新手的保守版 ("先全规范化")。把两者合起来的可操作判据:

  1. 先规范化到 2NF——无论如何,这是纪律的底线;
  2. 观测:用 EXPLAIN 看最慢的十条查询(或第 9 章的对数方法—— 慢查询日志,书外);凡是"每次都 JOIN 同一张小表"的,是反规范化 候选(把那张小表的常用列冗余过来);
  3. 冗余一处,同步一处:选了冗余,就明确"谁负责同步"—— 触发器(原书提过)、定时脚本(原书提过)、或应用层双写;
  4. 写进文档:哪几列是冗余、谁是主本、谁负责同步——半年后 没有文档的冗余,就是定时炸弹。

这个四步把"经验之谈"变成了"可执行流程"。原书停在第 1 步与第 2 步之间, 我们替它走完——方向与它一致,只是补了路标。

9.10 MySQL 的「时间」在 PHP 里的往返(双端对照)

第 8 章存时间戳(INT UNSIGNED),第 22 章存 time() 的秒数—— 同一个选择,补全它的双端语义:

  PHP 侧:time() 得到秒数 → 占位符 INSERT → MySQL 的 INT 列
  MySQL 侧:WHERE time > 某秒数(范围查询可用索引)→ 取回秒数
  PHP 侧:date("格式", 秒数) → 翻译成 "Jul 4th '25 2:30pm"

为什么不存"2025-07-04 14:30:00"字符串?三个理由:占空间大(INT 4 字节 vs DATETIME 8 字节)、比较和范围查询走索引更快、PHP 侧本来就是秒数运算。 第 22 章消息列表的 ORDER BY time DESC 就是这个选择的直接红利。 (第 8 章的 DATETIME/TIMESTAMP 类型在"人用工具直接查库"的场景仍是正解—— 存什么是工具问题,存多少是设计问题。)

9.11 「预处理语句」到底"预"了什么(机制说透)

占位符一节说它"几乎防弹",这里把"预"字拆开——MySQL 端实际发生了:

  1. prepare:语句发给 MySQL,编译、优化、生成执行计划, 占位符位置被标出来;MySQL 手里现在是一份"带洞的图纸";
  2. execute:数据走单独的通道到达,MySQL 把数据填进洞里执行—— 数据从始至终没有被"解析成 SQL 的一部分",这就是注入在机制上 失去入口的原因;
  3. 重复执行:同一份图纸可以反复 execute 不同数据—— 批量插入时比"每次拼一条 SQL"快一个量级(原书没讲的性能红利, 书外补充:这就是 mysql2 的 execute 数组、PDO 的 bind+循环的场景)。

对比拼串:$pdo->query("... VALUES ('$user')") 里,$user 的每个字符 都要先被 PHP 拼进字符串、再被 MySQL 从头到尾当语法解析—— "数据"和"语法"在一行里,注入者要的正是这个混居。 预处理的本质一句话:让数据和语法分居。这是本书安全主线的 数据库版,也是最硬的一版。

本章用了 $_SESSION 三个字但还没展开,桥先搭好:

  cookie  = 浏览器的小书包(装偏好,用户能翻、能改 → 不可信)
  会话    = 服务器的储物柜(装身份,浏览器只拿"号码牌")
  号码牌  = 一个随机 ID,放在 cookie 里带来带去

第 12 章会把这个协议讲全(含劫持、固定、四件套加固)。本章只需记住 分工:PHP 项目在 header.php 里 session_start() 之后, $_SESSION 就是你的储物柜——第 22 章全站的"认人",全靠它。

9.13 综合案例:把本章与第 8 章缝起来的一次查询设计

场景:第 22 章的"成员页"要按"互粉关系"排序——先互粉、再单向、再陌生。 只用手头两章的工具,查询怎么设计?

  1. 规范化:关系在 friends(user, friend) 里,一行一条单向关注 (第 9 章的 2NF 纪律);
  2. 互粉判定:两次 friends 查询(§13 的 t1/t2 模式)或一次自连接 f1.user = f2.friend AND f1.friend = f2.user;
  3. 占位符:所有值走 ?——usernames 来自 URL,半点不含糊;
  4. 取数:fetch 循环 + htmlentities 输出(第 10 章的双端纪律)。

这条设计链从第 8 章的表一路铺到第 22 章的页面,中间每一环都是 已经讲过的原语——没有魔法,只有工序。能自己把这条链走通, 第 9、10 章就算真的毕业了。

9.14 「N+1 查询」的完整解剖(把第 10 章伏笔兑现)

例 10-15 的"二次查询"技术,在真实规模下的代价有个专门名字:N+1—— 1 次外层查询 + N 次内层查询 = N+1 次数据库往返。走一遍数字:

  成员页有 200 个成员 → 1 次 SELECT members
  每成员查一次关注关系     → 200 次 SELECT friends
  合计:201 次往返,每次约 1ms = 200ms 纯数据库等待

200ms 是什么概念?一个页面的预算通常只有几百毫秒——一半就这么没了。 解法(按本书工具箱):一次 JOIN 把关系和成员一起取回(第 8 章 §8 的 三式连接),或先一次查询取全部关系放进 PHP 数组再内存匹配。 原书保留 N+1 写法是教学选择(它要演示"二次查询"这个机制), 第 22 章的成员页也是 N+1 风格——规模小(几十个成员)时它无罪; 规模一大,这就是你翻到本章时会回来谢我们的那个伏笔。

9.15 「事务」与「锁」的家族相似性(两个易混品对表)

第 7 章 flock 与本章事务都讲"多人别打架",容易混。对表拆开:

flock(文件锁) 事务(InnoDB)
保护对象 一个文件 一组行/表(数据)
锁的粒度 整个文件 行级(可只锁动的几行)
谁遵守 自愿(劝告锁) 强制(数据库引擎执行)
失败代价 文件写坏 引擎回滚,数据不脏
本书场景 日志、计数器 转账、订单

一句话:flock 是君子协定,事务是法律。有数据库在场的场合, 法律永远优先——这也是 §10.8 决策树"主数据别用文件"的最终理由。

9.16 备份恢复演练的「30 分钟剧本」

第 3.4 节说"要演练",给一个可以直接照做的剧本(本机即可):

  1. 5 分钟:mysqldump 导出 publications → publications.sql;
  2. 10 分钟:DROP DATABASE publications(对,删掉它);
  3. 10 分钟:CREATE DATABASE publications;mysql < publications.sql 恢复;
  4. 5 分钟:SELECT COUNT(*) FROM classics 核对行数、抽查两条数据。

总计 30 分钟,换来三样东西:恢复流程的肌肉记忆、备份文件有效性的 实证(很多备份是坏的,第一次恢复才知道)、以及"真出事时不慌"的底气。 原书说"你可能要在又急又慌的时候恢复"——剧本的意义就是把"又急又慌" 提前到周日下午的沙发上。

9.17 本章三件「重器」的使用说明书

规范化、事务、EXPLAIN——本章三件重器,各配一页"什么时候掏出它":

规范化:建表之前。一旦表里有了真实数据,拆表的代价就是数据迁移; 所以第 2.3 节的"从问题出发"和 3NF 的两问,是开工仪式,不是补救措施。 第 22 章的 setup.php 四张表就是仪式执行记录。

事务:两条或以上的写操作,且"只成功一半"比"全失败"更糟糕时。 判据一句话:这两个写,能不能单独成功? 转账不能(第 3.1 节), 发一条独立的消息能(第 22 章不用事务,正确)。 不要给单条 INSERT 套事务——画蛇添足还多一次日志开销(书外补充: 单语句在 InnoDB 内部本就是原子的)。

EXPLAIN:查询"感觉变慢"时。不是每个查询都配,是有症状再开药。 第 3.3 节那个 const 的例子是体检报告;§9.7 的 ALL+98 万行才是病历。 体检年年做(对核心查询每年过一眼 EXPLAIN),病历随时开。

三器的共同点:都是预防与诊断工具,不是日常消耗品——用它们的次数 应该远少于用 SELECT 的次数。如果有一天你每天都在调规范化、 套事务、跑 EXPLAIN,说明设计层欠的账开始计息了。

9.18 「我们的判断」的复核声明

本章判断表共 9 行,我们复核过每一条的"如果错,会错在"都写到了。 此处追加一个整体声明:本章涉及的版本性陈述(mysqldump 路径、 Node 的 .env 支持版本、mysql2 的接口形态)均带"写书时"限定; 规范化的级数定义、事务的 ACID 语义、EXPLAIN 的列含义属于 稳定知识,无版本风险。两类陈述在正文中已分层标注,复核时 按层核对即可。

9.19 PDO fetch 风格的选用决策

§4.2 列了五种 fetch 风格,补一张"什么时候选哪个"的决策表:

场景 选 理由
默认、按列名取值 FETCH_ASSOC 按名取值最可读,键即文档
逐格填 HTML 表格 FETCH_NUM 数字下标配 for 循环最顺(例 10-8)
既想按名又想按下标 FETCH_BOTH(默认) 方便但双倍内存,大结果集避免
要把行当对象传 FETCH_OBJ $row->author 的面向对象口感
框架/ORM 底层 LAZY 延迟加载的省电模式,手写代码罕见

第 22 章全项目用 ASSOC——按名取值与数据库列名天然对齐, 读代码不用数列号。选择本身无优劣,一个项目内统一才是关键 (本书靠 login.php 顶部的 $opts 数组一行定全场)。

9.20 「防御纵深」:安全主线的总图(两章合绘)

把第 10 章与本章的安全件画进一张纵深图,看清它们各自站哪道防线:

  浏览器 ──①──▶ PHP 收到输入
              ② htmlentities(要显示时)──▶ 防 XSS(输出防线)
              ③ 占位符/预处理 ──────────▶ 防 SQL 注入(数据库防线)
              ④ password_hash/verify ───▶ 防"库里泄密"(存储防线)
              ⑤ 会话四件套(第 12 章)────▶ 防"身份被冒用"(身份防线)
              ⑥ 最小权限 DB 用户(第 8 章)▶ 防"一个洞捅穿全库"(权限防线)

  图说:六条防线,任何一条被突破,后面还有下一道。

这个图的价值在于"纵深"二字:没有哪一条防线是充分的,也没有哪一条 是多余的。第 22 章项目把①②③④⑥全数执行,⑤是留白的作业(我们的 边界节 P0 第一条)。抄这张图进你的项目启动清单,比背任何单条规矩都顶用。

9.21 章末的「口袋卡」(第 9+10 章合卡)

┌─ 数据与访问卡 ─────────────────────────────┐
│ 设计:每条信息存一次 · 3NF 两问 · 能反范式换速  │
│ 事务:START → 改 → 改 → COMMIT/ROLLBACK      │
│ 备份:mysqldump > 文件 · mysql < 文件 · 锁表    │
│ 诊断:EXPLAIN 看 type 与 rows                 │
│ 访问:PDO → query(查)/ prepare+execute(写)    │
│ 安全:quote 是过渡 · 占位符是正路 · 凭据进 .env  │
└───────────────────────────────────────────┘

一张卡把两章的核心动作收齐。和第 2、3、7 章的卡并排,你已经攒了 四张——PHP+MySQL 的应急包基本成型。

9.22 「三问定表」:从查询反推表结构的速法(实战技巧)

第 2.2 节说"先问问题再摆桌子",给一个可操作的速法——给每个候选表 自问三句,全答顺才建:

  1. 这行是谁? 用一句"主键是 X"答出;答不出,表还没想清;
  2. 删了它会带走谁? 找它的"孩子"(依赖它的表); 一个孩子被多个家长引用,就要考虑中间表(第 9 章多对多);
  3. 哪个字段会全局改? 有,就拆出去做查找表(3NF 的实战形)。

拿第 22 章四表过一遍:members 主键 user,无孩子全局改字段; friends 两行关系,无主键(教学简化);messages 主键自增 id, recip 是它的"被查询索引";profiles 一行一主。四表三问全顺—— 原书的设计经得住这套速法,你设计自己的表时拿它当体检表。

9.23 「PDO 之外」的一句话地图(防止学死)

本章把 PDO 立为正路;真实生态里还有两样东西,一句话定位, 免得日后撞见误以为是"另一个正路"(书外,仅定位):

  • mysqli:PHP 官方的"另一个"驱动,过程式+对象式双接口。 本书第 7 版明确弃它取 PDO(凭据文件一节有记录);老项目常见,认得即可。
  • ORM/查询构造器(Doctrine、Eloquent 等):把 SQL 也封装成 PHP 方法链。本书范围内不必学;但它的底层仍是占位符——你本章学的 不会被推翻,只会被藏得更深。

地图的价值:知道山外有山,就不会把脚下的 PDO 当成全世界; 也知道无论山多高,山脚都是"占位符防注入"这块石头。

9.24 两章合成的一次「综合演练」(30 分钟)

把第 8、9、10 章合成一个可做的演练:给 classics 加"借阅记录"功能。

  1. 建表(第 8 章):loans(isbn, borrower, borrowed_date), 占位符建表或手写 CREATE;
  2. 借出(第 10 章):PHP 表单收借阅人 → INSERT 用占位符; 借出前 SELECT 查这本书是否已借出(一次查询定分支);
  3. 归还(第 9 章思路):DELETE 按 isbn+borrower 双条件占位符删行;
  4. 列表:JOIN classics 显示"谁借了哪本书"(第 8 章连接三式任选);
  5. 安全自查:每一句 SQL 是否占位符?每个输出是否 htmlentities?

30 分钟做完,两章的知识就完成了第一次"不带教材的合奏"。 做卡壳在哪一步,就是哪一章要回读的信号——这比任何复习建议都精准。

9.25 「ACID」四个字母,一分钟版

事务一节没提 ACID 这个行业标准缩写,这里补一分钟版(书外, 但属于"会改变读者判断所以必须给"的背景):

  • A 原子性:全成或全不成——本章 COMMIT/ROLLBACK 的本体;
  • C 一致性:事务前后,数据库的"规矩"(主键唯一、外键不悬空) 不破——本库没建外键约束(边界节),所以一致性靠应用自觉;
  • I 隔离性:并发事务互相看不见半成品——§9.6 四级表的本体;
  • D 持久性:COMMIT 之后,宕机也不丢——InnoDB 的 redo 日志 在背后兜底(书外一句:这就是"为什么选 InnoDB")。

四个字母各对应本章讲过的一块;知道缩写,读任何数据库文档都能对上号。 这也是本书"机制讲透、术语留名"风格的一个例证:原书没给的行话, 我们在边界与补充处补上,不让你出门撞见生词。

9.26 章末 FAQ(五问)

Q1:规范化会不会让查询变慢? 会——多表 JOIN 比单表慢。第 2.5 节的反规范化主张就是买速度。 但 2NF 的账永远是赚的:消除更新异常的收益,大于 JOIN 的成本; 3NF 以上才进入"看情况"区。

Q2:外键约束到底要不要? 本库主张"讲了概念没讲约束"是留白(边界节)。我们的建议:新项目要—— FOREIGN KEY 让数据库替你守"不悬空",比应用层自觉可靠; 代价是插入顺序受限(先插父行)。第 22 章没用,是教学简化。

Q3:PDO 能连别的数据库吗? 能——PDO 是"数据访问抽象层",换库(如 PostgreSQL)主要换连接串, 代码基本不动。这是它胜过 mysqli 的立身之本(第 10 章开头"同一套函数" 一句的本体)。当然 SQL 方言差异(语法糖不同)仍在,抽象有边界。

Q4:备份文件里是什么? 可执行的 SQL——建库建表 INSERT 全套。所以恢复 = 让 mysql 执行它 (mysql < file)。它同时是"迁移工具":换服务器,带一个备份文件就够 (第 3.4 节"迁移先备份"一句的展开)。

Q5:学完两章,数据库这门课还差什么? 索引调优(执行计划的深入)、锁与死锁的实战、复制与主从(高可用)—— 三门都是"上线之后"的课。本书把你送到了上线的门口,门后的路, 官方手册与专书在等。

9.27 「我们的判断」补档:关于反规范化的再平衡

第 2.5 节我们按原书立场收录了"高流量站应尽量反规范化"。补一条再平衡 判断(我们的,不是书里的):2020 年代的后端,缓存层(Redis/Memcached) 接管了"反规范化想买的那件事"——把 JOIN 的结果缓存起来, 比把数据物理冗余到各表更常见、更好维护(书外:这是"空间换时间"的 现代形态)。原书写于这个转变的半途,它的警告仍对(不要迷信规范化), 但"冗余副本"不再是默认解法,"缓存"才是。

如果错,会错在: 对低流量或读多写极多的场景(报表库、CDN 化的 内容库),物理冗余依然是最优——缓存有失效一致性问题,冗余没有。 判据仍是第 2.5 节那句"数据被一起查吗",只是答案的默认项换了。

9.28 章末口袋卡(单章版)

┌─ 第 9 章卡 ────────────────────────────────┐
│ 范式:1NF 拆列族 · 2NF 拆行重复 · 3NF 两问定夺 │
│ 关系:一对一合表 · 一对多加列 · 多对多中间表      │
│ 事务:START TRANSACTION … COMMIT / ROLLBACK   │
│ 引擎:事务只信 InnoDB                         │
│ 备份:mysqldump 导 · 锁表或停机 · 多副本异地    │
│ 恢复:mysql < 文件 · 先删再建的聪明文件         │
│ 诊断:EXPLAIN · type=ALL + 大 rows = 该吃药     │
└───────────────────────────────────────────┘

与第 10 章合用时的完整版见 §9.21;此处是单章速记,独自成卡。

9.29 规范化三阶:把第 9 章的表格链走一遍

第 9 章用书店数据库(Authors / Books / Customers / Purchases)演示了 First / Second / Third Normal Form。三阶不是三条孤立的规则, 而是一条"拆到什么程度"的渐进线,值得完整走一遍:

起点(Table 9-1,怪物表):一张表里塞着作者、书名、客户姓名、 地址、购买记录,重复数据加粗标得到处都是。原书选它做起点, 是因为它正是"从来没设计过表"的人会写出来的东西1。

第一阶(First Normal Form)管"同一行内的重复":满足三条—— 每行原子(不可再分)、每列单一值、每行可被唯一区分。 书店例子里,一个单元格塞多个作者名、一行里同一本书出现多次, 都是在这一阶被拆开2。

第二阶(Second Normal Form)管"跨行的重复":识别出"在不同行里 反复出现的数据块",把它搬到自己的表里,原处只留一个标识。 Authors 表、Customers 表、Purchases 表就是这么来的—— Purchases 是拆完才发现的第四张表,不是一开始就规划好的3。

第三阶(Third Normal Form)管"依赖别人的数据":某列不直接依赖 主键、却依赖表里另一列(经典例:邮编决定城市,城市不依赖客户编号), 就把它再拆出去。拆到极致,城市、州各自成表,改一处全城生效4。

作者的掉头:讲完三阶之后,原书明确说——高流量站点不要 fully 规范化,因为跨表查询天生比单表查询慢,上规模后会拖垮数据库; 惯例是把常被查的数据反着拆回去(denormalize)。 但对初学者,他的建议是先 fully 规范化到一二阶,"养成习惯", 等真看见数据库堵了再反规范化5。

如果错,会错在:把"不要 fully 规范化"读成"别学规范化"。
原书两条建议的对象不同:教学期(你)建习惯,运营期(高流量)做权衡。

9.30 事务:COMMIT 之前,一切皆可回滚

账户转账是第 9 章的事务例子(Example 9-38 一带):从一个账户扣 250、 往另一个账户加 250,两步必须同生共死——只成功一步,钱就凭空消失。

事务的语法骨架就三行:

START TRANSACTION;                    -- 开一笔账
UPDATE accounts SET balance=balance-250 WHERE number=12345;
UPDATE accounts SET balance=balance+250 WHERE number=67890;
COMMIT;                               -- 落锤;换 ROLLBACK 则全部作废

关键心智模型:没收到 COMMIT 之前,MySQL 把这一串改动都当临时的, ROLLBACK 一响,数据库回到 START TRANSACTION 那一刻6。 锁的纪律沿用了文件锁那条线:锁紧贴着改动申请、尽早释放, 别让它横在整个事务上。

9.31 建表之后的演化:ALTER TABLE 一族速查

真实项目里,表结构从来不是一次定稿。第 9 章后半的 ALTER TABLE 家族可以浓缩成一张表:

想做什么 骨架 例子
加一列 ALTER TABLE 表 ADD COLUMN 列 类型 加出版年份
改列类型 ALTER TABLE 表 MODIFY COLUMN 列 新类型 VARCHAR 加长
改列名 ALTER TABLE 表 RENAME COLUMN 旧 TO 新 拼写订正
删一列 ALTER TABLE 表 DROP COLUMN 列 废弃字段下线
加索引 ALTER TABLE 表 ADD INDEX(列) 提速某类查询
全文索引 ... ADD FULLTEXT(列,列) 支持 MATCH...AGAINST

执行顺序上有条经验(书外补充):加列容易、删列痛。 每加一列都问自己"这列三个月后还一定有用吗"; 删列前先全文搜一遍应用代码,确认没有 PHP 还在 SELECT 它。

9.32 全文搜索:FULLTEXT 与布尔模式

第 9 章在书店例子的尾声给出了 FULLTEXT 索引(author、title 两列 联合建索引)与两类 MATCH...AGAINST 查询:

  • 自然语言模式:按相关度排序返回匹配行,像一个"不太聪明但很快" 的站内搜索7。
  • 布尔模式:查询串里支持 +必须 -排除 *前缀 这类算符, 精确度更高,但要求用户(或你替他拼查询时)懂这套语法。

选型建议很朴素:几十行的表,LIKE 就够;上万行且按"内容找内容", FULLTEXT 才值得。索引不是免费的——每次写入都要维护它, 小表上建全文索引是纯亏。

9.33 PDO 访问七步:第 10 章的骨架再压实

第 10 章把 PHP 连 MySQL 的方式换成了 PDO("最安全的当下默认", 早年版本用 mysql、后来用 mysqli,这一版定 PDO)8。 把全章的例子抽掉具体 SQL,骨架是稳定七步:

1. 组装 $dsn(库名、主机、字符集)
2. new PDO($dsn, $user, $pass, 选项)   ← 连接,失败抛 PDOException
3. $pdo->query($sql)                   ← 发查询(读操作)
4. $pdo->exec($sql)                    ← 发增删改,返回受影响行数
5. fetch() / fetchAll() 逐行取结果
6. lastInsertId() 拿刚生成的 AUTO_INCREMENT 值
7. $pdo = null;                        ← 手动断开(小脚本可省)[^71]

三步最容易写错,书里都给了对照:

  • query 与 exec 的分工:query 有结果集(SELECT),exec 只报行数 (INSERT/UPDATE/DELETE)。混用的症状是"UPDATE 成功了却 fetch 不到东西"。
  • 连接错误的处理:用 try...catch 包住 new PDO,生产环境里 把"面向程序员的报错"换成"面向用户的体面页面"—— 原书明说生产站不该把程序员向的错误信息直接露出去10。
  • 插入后取键:AUTO_INCREMENT 的 id 用 lastInsertId() 取, 猫和主人的例子演示了"先插主人、拿 id、再插猫"的两步写法11。 想成"取号机":插入的那一刻才发号,号码只能事后问窗口。

9.34 PHP 与 SQL 的分工:谁该干哪半件事

第 10 章通篇是两种写法的拔河,值得立成对照表:

任务 PHP 侧做 SQL 侧做 书里倾向
过滤行 全取出来再 if WHERE 条件 SQL——少搬数据
排序 PHP 的 sort ORDER BY SQL——索引能帮忙
算总数 循环累加 COUNT(*) SQL——一步出结果
拼接显示串 sprintf/echo CONCAT PHP——表现层归 PHP
多步写入 多次 exec 事务(COMMIT/ROLLBACK) SQL——同生共死

判据只有一条:让数据在离数据最近的地方被加工。 PHP 拉一万行回来只留三行,是拿网络和内存在交学费。

9.35 练习场(第 9+10 章合练)

  1. 规范化实操:把"班级-学生-课程-成绩"从一张大表拆到二阶, 标出每张表的主键;再问自己邮编/城市那一列要不要三阶。
  2. 转账事务:建 accounts 表,写 START TRANSACTION → 两条 UPDATE → COMMIT;故意在第二条 UPDATE 里写错列名,观察 ROLLBACK 的效果。
  3. PDO 计数器:用 PDO 把第 7 章的文本计数器改成数据库版, 体会"文件锁问题"如何被数据库接管。
  4. INSERT 取号:插一条记录,立刻用 lastInsertId() 把它打印出来, 再插一条验证号码递增、互不重复。
  5. 分工对照:同一个"列出消费最高的五位会员"需求,分别用 "PHP 全取再排"和"SQL ORDER BY+LIMIT"实现,各记录代码行数与 查询行数——把 §9.34 的判据变成亲手量出来的数字。

9.36 FAQ(四问)

Q1:三阶规范化我记不住细节,能只记一句话吗? "每条信息在库里只存一份,存的地方由它的主键说了算。" 一阶管行内别塞两份,二阶管跨行别重复,三阶管别依赖隔壁列。 细节回查原书 Table 9-1 到 9-11 的推演链,比背规则快。

Q2:事务是不是用了就绝对安全? 事务保证"一串改动同生共死",不保证"这串改动本身是对的"。 转错账户号的事务会忠实地、完整地转错。先校验,再开事务。

Q3:query、exec、prepare 我什么时候用哪个? SELECT→query;INSERT/UPDATE/DELETE→exec; 用户输入要进 SQL 的任何场合→prepare+参数绑定(第 10 章后续 与第 16 章的 sanitize 是同一条线;字符串拼 SQL 是本书反复警告的写法)。

Q4:PDO 和 mysqli 学哪个? 本书 7th 版的答案已经写在纸上:PDO。一个理由就够—— prepare/execute 的参数绑定在 PDO 里是一等公民, 换数据库驱动时接口不变12。

9.37 实验课:事务与临时表的两次动手

实验一:回滚的体感。建好 accounts 表(两条记录), 按 §9.30 的骨架执行 START TRANSACTION → 扣款 UPDATE → 故意把第二条 UPDATE 的列名写错 → 观察 MySQL 报错 → ROLLBACK → 再 SELECT 一次。你会看到余额原封不动—— 这就是"没收到 COMMIT 之前,一切都算临时的"的直观版本13。 这个实验只花五分钟,但它把"事务"从名词变成手感: 回滚不是后悔药,是"没盖章之前,草稿随时可以撕"。

实验二:临时表的边界。第 9 章的账户实验是在一张临时 accounts 表上 练手的,用完即删14。临时表教的不是语法,是实验卫生: 拿真实表练 UPDATE/DELETE,是拿生产数据学游泳; 先复制一张临时表,随便折腾,练完 DROP 掉—— 这条卫生准则在本书之外的每个真实数据库上都成立。

何时该用事务的检查单(我们的延伸,与书内机制一致):

  • 一笔业务动了两张以上的表?→ 事务。
  • 中间任何一步失败都必须全额撤掉?→ 事务。
  • 单表单行的一次写入?→ 通常不必,但包了也无害。
  • 事务里塞了用户能直接看到的"等待"?→ 重新设计——锁是稀缺的, 交互是慢的,两者不该同框。

9.38 把两章折成三句话

如果只能带走三句,我们选这三句,每句都值回一章的页数:

  1. 规范化管"每条信息只存一份";反规范化管"上规模后,快比省地方贵"—— 两个都是第 9 章的原话立场,不冲突,各管一段生命周期。
  2. 事务的价值不在"能提交",在"能整体撤"——COMMIT 是落锤, ROLLBACK 才是安全感的来源。
  3. PDO 七步里,连接、查询、取数、断开都有"教科书默认值"; 唯一必须当场想清楚的是 query 与 exec 的分工—— 选错的代价不是报错,是"成功了的假象"。

10. 原文地图

主题 原书章 原文位置
从查询出发拆表 Chapter 9. Mastering MySQL text/20-ch09-chapter-9-mastering-mysql.txt:30(搜「how many authors」)· :79(搜「separate tables」)· :83(搜「own table」)
主键选择与 UUID 同上 text/20-ch09-chapter-9-mastering-mysql.txt:123(搜「unique identifier」)· :127(搜「Don't choose a key」)· :136(搜「not common in MySQL」)
规范化目标与冗余风险 同上 text/20-ch09-chapter-9-mastering-mysql.txt:150(搜「only once」)· :154(搜「typographic error」)
第一范式三条与 Authors 表 同上 text/20-ch09-chapter-9-mastering-mysql.txt:253(搜「three requirements」)· :259(搜「no repeating columns」)· :361(搜「The new Authors table」)· :421(搜「foreign key」)
第二范式与四表定型 同上 text/20-ch09-chapter-9-mastering-mysql.txt:441(搜「redundancy across multiple rows」)· :549(搜「CustNo」)· :611(搜「Purchases table」)· :658(搜「no primary key」)
第三范式与取舍两问 同上 text/20-ch09-chapter-9-mastering-mysql.txt:690(搜「Third Normal Form」)· :854(搜「look up the Zip」)· :885(搜「many new columns」)
三种关系 同上 text/20-ch09-chapter-9-mastering-mysql.txt:951(搜「monogamous marriage」)· :1015(搜「must contain a column」)· :1026(搜「sole purpose is to link」)
关系与隐私 同上 text/20-ch09-chapter-9-mastering-mysql.txt:1110(搜「without actually knowing」)
反规范化告诫 同上 text/20-ch09-chapter-9-mastering-mysql.txt:909(搜「never fully normalize」)· :913(搜「denormalize」)· :925(搜「triggers」)· :929(搜「First and Second Normal Form」)
事务动机与三命令 同上 text/20-ch09-chapter-9-mastering-mysql.txt:1123(搜「transfer funds」)· :1239(搜「START TRANSACTION」)· :1270(搜「Until it receives a COMMIT」)· :1321(搜「ROLLBACK」)· :1286(搜「funds transfer」)
EXPLAIN 同上 text/20-ch09-chapter-9-mastering-mysql.txt:1358(搜「EXPLAIN SELECT」)· :1394(搜「ALL, index, range」)· :1441(搜「taking longer」)
备份恢复一条龙 同上 text/20-ch09-chapter-9-mastering-mysql.txt:1464(搜「keep backups」)· :1477(搜「instructions necessary」)· :1486(搜「LOCK TABLES」)· :1589(搜「publications.sql」)· :1701(搜「all_databases.sql」)· :1740(搜「CSV」)· :1784(搜「different locations」)· :1788(搜「stress」)
PDO 连接与 login.php Chapter 10. Accessing MySQL Using PHP text/21-ch10-chapter-10-accessing-mysql-using-php.txt:93(搜「login.php」)· :114(搜「PHP Data Objects」)· :145(搜「utf8mb4」)· :122(搜「blank page」)· :131(搜「never place」)· :178(搜「require_once」)· :197(搜「new PDO」)· :211(搜「stack trace」)
查询与 fetch 同上 text/21-ch10-chapter-10-accessing-mysql-using-php.txt:247(搜「no trailing semicolon」)· :251(搜「exhausting server memory」)· :287(搜「while ($row」)· :1598(搜「XSS attack」)· :331(搜「FETCH_ASSOC」)
sqltest 小应用 同上 text/21-ch10-chapter-10-accessing-mysql-using-php.txt:445(搜「isset($_POST['delete'])」)· :506(搜「sanitize_post_value」)· :544(搜「$_POST array」)· :594(搜「hidden HTML form field」)· :628(搜「POST rather than a GET」)
AUTO_INCREMENT 与 lastInsertId 同上 text/21-ch10-chapter-10-accessing-mysql-using-php.txt:921(搜「NULL value」)· :1103(搜「lastInsertId」)· :1110(搜「concurrent queries」)· :1147(搜「not reused」)· :1200(搜「secondary query」)
SQL 注入演示 同上 text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1277(搜「verify a user」)· :1307(搜「SQL injection attack」)· :1328(搜「OR 1=1」)· :1346(搜「magic quotes」)· :1361(搜「mysql_fix_string」)
预处理占位符 同上 text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1440(搜「bulletproof」)· :1448(搜「question marks」)· :1487(搜「bindParam」)· :1532(搜「:author」)· :1520(搜「rowCount」)
XSS 与 htmlentities 同上 text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1598(搜「cross-site scripting」)· :1602(搜「steals cookies」)· :1606(搜「htmlentities」)· :1633(搜「entities_fix_string」)


  1. 出处:「Chapter 9. Mastering MySQL」第 166 段(text/20-ch09-chapter-9-mastering-mysql.txt:166,搜「rather monstrous database」)。 ↩

  2. 出处:「Chapter 9. Mastering MySQL」第 253 段(text/20-ch09-chapter-9-mastering-mysql.txt:253,搜「three requirements」)。 ↩

  3. 出处:「Chapter 9. Mastering MySQL」第 441 段(text/20-ch09-chapter-9-mastering-mysql.txt:441,搜「redundancy across multiple rows」)与第 665 段(:665,搜「a fourth table」)。 ↩

  4. 出处:「Chapter 9. Mastering MySQL」第 690 段(text/20-ch09-chapter-9-mastering-mysql.txt:690,搜「not directly dependent on the primary key」);城市改名只需改一行见第 875 段。 ↩

  5. 出处:「Chapter 9. Mastering MySQL」第 909 段(text/20-ch09-chapter-9-mastering-mysql.txt:909,搜「never fully normalize」)与第 929 段(:929,搜「instill the habit」)。 ↩

  6. 出处:「Chapter 9. Mastering MySQL」第 1270 段(text/20-ch09-chapter-9-mastering-mysql.txt:1270,搜「temporary」)。「Until it receives a COMMIT, MySQL considers all the changes you make to be temporary」。 ↩

  7. 出处:「Chapter 9. Mastering MySQL」第 1619 段(text/20-ch09-chapter-9-mastering-mysql.txt:1619,搜「FULLTEXT KEY」)。 ↩

  8. 出处:「Chapter 10. Accessing MySQL Using PHP」第 152 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:152,搜「PDO, which we now use」)。 ↩

  9. 出处:「Chapter 10. Accessing MySQL Using PHP」第 396 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:396,搜「set it to null」)。 ↩

  10. 出处:「Chapter 10. Accessing MySQL Using PHP」第 607 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:607,搜「programmer-oriented」)。 ↩

  11. 出处:「Chapter 10. Accessing MySQL Using PHP」第 1176–1178 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1178,搜「insert ID as a key」)。 ↩

  12. 出处:「Chapter 10. Accessing MySQL Using PHP」第 152 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:152,搜「consistent interface」)。 ↩

  13. 出处:「Chapter 9. Mastering MySQL」第 1270 段(text/20-ch09-chapter-9-mastering-mysql.txt:1270,搜「ROLLBACK command instead」)。 ↩

  14. 出处:「Chapter 9. Mastering MySQL」第 1448 段(text/20-ch09-chapter-9-mastering-mysql.txt:1448,搜「remove it by entering」)。 ↩

  15. 出处:「Chapter 9. Mastering MySQL」第 79 段(text/20-ch09-chapter-9-mastering-mysql.txt:79,搜「separate tables」)。「books and customers should be in separate tables, because their connection is very loose」;「A customer can buy any book...」紧接其后。 ↩

  16. 出处:「Chapter 9. Mastering MySQL」第 83 段(text/20-ch09-chapter-9-mastering-mysql.txt:83,搜「own table」)。「when couplings between things are loose, it's best to put them in separate tables」与同段「a search can often benefit from having its own table」。 ↩

  17. 出处:「Chapter 9. Mastering MySQL」第 87–106 段(text/20-ch09-chapter-9-mastering-mysql.txt:87,搜「at least three tables」)。三表的理由逐条列出。 ↩

  18. 出处:「Chapter 9. Mastering MySQL」第 123 段(text/20-ch09-chapter-9-mastering-mysql.txt:123,搜「unique identifier」)与第 127 段(:127,搜「Don't choose a key」)。「Don't choose a key that could possibly have the same value for different objects」。 ↩

  19. 出处:「Chapter 9. Mastering MySQL」第 136 段(text/20-ch09-chapter-9-mastering-mysql.txt:136,搜「not common in MySQL」)。「They take up more storage space and may negatively impact performance... and thus are not common in MySQL databases」。 ↩

  20. 出处:「Chapter 9. Mastering MySQL」第 413 段(text/20-ch09-chapter-9-mastering-mysql.txt:413,搜「ISBN is not, however, the primary key」)与第 417 段(:417,搜「cannot be the primary key」)、第 421 段(:421,搜「foreign key」)。「Because we'll use it to link authors to books in another table, this column is called a foreign key」。 ↩

  21. 出处:「Chapter 9. Mastering MySQL」第 150 段(text/20-ch09-chapter-9-mastering-mysql.txt:150,搜「only once」)。「Its main goal is to make sure each piece of information appears in the database only once」。 ↩↩↩

  22. 出处:「Chapter 9. Mastering MySQL」第 253 段(text/20-ch09-chapter-9-mastering-mysql.txt:253,搜「three requirements」)。三条原文逐条列出(第 259、265、271 段)。 ↩↩↩

  23. 出处:「Chapter 9. Mastering MySQL」第 356 段(text/20-ch09-chapter-9-mastering-mysql.txt:356,搜「ill at ease」)。「you may feel ill at ease with this table, because you can't tell which author wrote which book... MySQL can quickly tell you」。 ↩

  24. 出处:「Chapter 9. Mastering MySQL」第 441 段(text/20-ch09-chapter-9-mastering-mysql.txt:441,搜「redundancy across multiple rows」)与第 487 段(:487,搜「closely related」)、第 549 段(:549,搜「CustNo」)。 ↩↩↩

  25. 出处:「Chapter 9. Mastering MySQL」第 606 段(text/20-ch09-chapter-9-mastering-mysql.txt:606,搜「Purchases」)与第 658 段(:658,搜「no primary key」)。「If one customer buys two copies of the same book on the same day, we'll just allow two rows with the same information」。 ↩↩

  26. 出处:「Chapter 9. Mastering MySQL」第 690 段(text/20-ch09-chapter-9-mastering-mysql.txt:690,搜「Third Normal Form」)与第 875 段(:875,搜「global search-and-replace」)。城市改名的对比论证。 ↩

  27. 出处:「Chapter 9. Mastering MySQL」第 879 段(text/20-ch09-chapter-9-mastering-mysql.txt:879,搜「two questions」)。两问原文见第 885、891 段。 ↩↩

  28. 出处:「Chapter 9. Mastering MySQL」第 951 段(text/20-ch09-chapter-9-mastering-mysql.txt:951,搜「monogamous marriage」)与第 966 段(:966,搜「just include them as columns」);分表两理由见第 972–978 段。 ↩

  29. 出处:「Chapter 9. Mastering MySQL」第 1015 段(text/20-ch09-chapter-9-mastering-mysql.txt:1015,搜「must contain a column」)。「The table for the 'many' must contain a column that lists the primary key from the 'one' table」。 ↩↩

  30. 出处:「Chapter 9. Mastering MySQL」第 1026 段(text/20-ch09-chapter-9-mastering-mysql.txt:1026,搜「sole purpose is to link」)。「add a third table containing the same key column from each of the other tables. This third table contains nothing else」。 ↩↩↩

  31. 出处:「Chapter 9. Mastering MySQL」第 1110 段(text/20-ch09-chapter-9-mastering-mysql.txt:1110,搜「without actually knowing」)。「you can accumulate a lot of information about some item—such as a customer—without actually knowing who that customer is」。 ↩↩

  32. 出处:「Chapter 9. Mastering MySQL」第 909 段(text/20-ch09-chapter-9-mastering-mysql.txt:909,搜「never fully normalize」)与第 913 段(:913,搜「denormalize」)、第 925 段(:925,搜「triggers」)、第 929 段(:929,搜「First and Second Normal Form」)。「Only when you start to see MySQL logjams should you consider looking at denormalization」。 ↩↩

  33. 出处:「Chapter 9. Mastering MySQL」第 1123 段(text/20-ch09-chapter-9-mastering-mysql.txt:1123,搜「transfer funds」)与第 1135 段(:1135,搜「disappeared into thin air」)。 ↩↩

  34. 出处:「Chapter 9. Mastering MySQL」第 1146 段(text/20-ch09-chapter-9-mastering-mysql.txt:1146,搜「queued」)。「MySQL handles this seamlessly by ensuring that all transactions are queued」。 ↩↩

  35. 出处:「Chapter 9. Mastering MySQL」第 1270 段(text/20-ch09-chapter-9-mastering-mysql.txt:1270,搜「Until it receives a COMMIT」)。「Until it receives a COMMIT, MySQL considers all the changes you make to be temporary」;ROLLBACK 效果见第 1326 段。 ↩↩

  36. 出处:「Chapter 9. Mastering MySQL」第 1453 段(text/20-ch09-chapter-9-mastering-mysql.txt:1453,搜「DROP TABLE accounts」)。 ↩

  37. 出处:「Chapter 9. Mastering MySQL」第 1229 段(text/20-ch09-chapter-9-mastering-mysql.txt:1229,搜「standard SQL syntax」)。「Transactions in MySQL start with either a START TRANSACTION or a BEGIN statement」;InnoDB 要求见第 1155 段。 ↩↩

  38. 出处:「Chapter 9. Mastering MySQL」第 1358 段(text/20-ch09-chapter-9-mastering-mysql.txt:1358,搜「EXPLAIN SELECT」)与第 1394 段(:1394,搜「ALL, index, range」)。效率排序原文;行数解释见第 1424 段(「The number of rows that need to be searched by this query is 1. This is good」)。 ↩↩

  39. 出处:「Chapter 9. Mastering MySQL」第 1441 段(text/20-ch09-chapter-9-mastering-mysql.txt:1441,搜「taking longer」)。「Whenever you have a query that seems to be taking longer to execute than you think it should, try using EXPLAIN」。 ↩

  40. 出处:「Chapter 9. Mastering MySQL」第 1464 段(text/20-ch09-chapter-9-mastering-mysql.txt:1464,搜「keep backups」)与第 1788 段(:1788,搜「stressed and in a hurry」)。「you may have to do so when you are stressed and in a hurry, such as after a power failure」。 ↩↩

  41. 出处:「Chapter 9. Mastering MySQL」第 1477 段(text/20-ch09-chapter-9-mastering-mysql.txt:1477,搜「instructions necessary」)与第 1486 段(:1486,搜「LOCK TABLES」)、第 1589 段(:1589,搜「publications.sql」)。「you must make sure that no one writes to a table while you're backing it up」。 ↩↩

  42. 出处:「Chapter 9. Mastering MySQL」第 1607 段(text/20-ch09-chapter-9-mastering-mysql.txt:1607,搜「DROP TABLE IF EXISTS」)与第 1622 段(:1622,搜「smart code」)。「it will first drop any tables that need to be re-created, thus avoiding potential MySQL errors」。 ↩

  43. 出处:「Chapter 9. Mastering MySQL」第 1740 段(text/20-ch09-chapter-9-mastering-mysql.txt:1740,搜「CSV format」)与第 1750 段(:1750,搜「--no-create-info」)。「contains just the data and no commands」。 ↩

  44. 出处:「Chapter 9. Mastering MySQL」第 1777 段(text/20-ch09-chapter-9-mastering-mysql.txt:1777,搜「more valuable your data」)与第 1784 段(:1784,搜「different locations」)。「If your database gets updated at least once a day, you should back it up daily」;「keep these in separate locations—preferably somewhere like a fireproof safe」。 ↩

  45. 出处:「Chapter 10. Accessing MySQL Using PHP」第 16 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:16,搜「format the results」)与第 20 段(:20,搜「data structure that PHP can recognize」)。 ↩

  46. 出处:「Chapter 10. Accessing MySQL Using PHP」第 29 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:29,搜「The process」)。六步清单原文列出。 ↩

  47. 出处:「Chapter 10. Accessing MySQL Using PHP」第 114 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:114,搜「PHP Data Objects」)。「a built-in data access library that uses the same functions regardless of which database you're using」;连接代码见例 10-2(第 192 段)。 ↩↩

  48. 出处:「Chapter 10. Accessing MySQL Using PHP」第 145 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:145,搜「utf8mb4」)。「utf8mb4 is the recommended UTF-8 character set in MySQL」;字符覆盖面在同段。 ↩

  49. 出处:「Chapter 10. Accessing MySQL Using PHP」第 122 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:122,搜「blank page」)。「If you were to leave them out and someone were to call up the file directly... it would display as text and reveal your secrets」。 ↩

  50. 出处:「Chapter 10. Accessing MySQL Using PHP」第 131 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:131,搜「never place」)。「real-world projects must never place secure information anywhere that it might be discovered」。 ↩↩

  51. 出处:「Chapter 10. Accessing MySQL Using PHP」第 247 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:247,搜「no trailing semicolon」)与第 251 段(:251,搜「exhausting server memory」)。「SELECT * FROM ... instead of listing only the columns you'll need is used here for brevity, but in general it should be avoided」。 ↩

  52. 出处:「Chapter 10. Accessing MySQL Using PHP」第 1598 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1598,搜「XSS attack」)。「there's always a risk of sneaky HTML characters being embedded within it—even if you believe it to have been previously sanitized」。 ↩↩

  53. 出处:「Chapter 10. Accessing MySQL Using PHP」第 326–343 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:326,搜「various styles」)。五种 fetch 风格表。 ↩↩

  54. 出处:「Chapter 10. Accessing MySQL Using PHP」第 396 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:396,搜「set it to null」)。「you simply set it to null like this」。 ↩↩

  55. 出处:「Chapter 10. Accessing MySQL Using PHP」第 211 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:211,搜「stack trace」)。「you could give away sensitive information to hackers」「won't contain login details present in the arguments of the original exception stack trace」。 ↩↩

  56. 出处:「Chapter 10. Accessing MySQL Using PHP」第 628 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:628,搜「POST rather than a GET」)。「whenever possible, you should use POST submissions」;理由(难看/可改/日志)同段。 ↩↩

  57. 出处:「Chapter 10. Accessing MySQL Using PHP」第 594 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:594,搜「hidden HTML form field」)。删除按钮如何携带 isbn 的机制。 ↩

  58. 出处:「Chapter 10. Accessing MySQL Using PHP」第 573 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:573,搜「quote method」)与第 1413 段(:1413,搜「automatically adds quotes」)。「you should not use them in any query that uses these sanitized strings」。 ↩

  59. 出处:「Chapter 10. Accessing MySQL Using PHP」第 713 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:713,搜「already an ISBN」)。重复插入五个 1 的报错即主键约束生效的演示。 ↩

  60. 出处:「Chapter 10. Accessing MySQL Using PHP」第 921 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:921,搜「AUTO_INCREMENT, and MySQL will decide」)。「So, we simply pass a NULL value, which will be ignored」。 ↩↩

  61. 出处:「Chapter 10. Accessing MySQL Using PHP」第 1110 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1110,搜「concurrent queries」)。「because concurrent queries could change the values in that column after the highest value has been fetched and before the calculated value is stored」。 ↩↩

  62. 出处:「Chapter 10. Accessing MySQL Using PHP」第 788 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:788,搜「verify that a table has been correctly created」)。「a handy piece of code that you can use to verify that a table has been correctly created from inside a browser」;FETCH_NUM 逐格显示见例 10-8(第 798 段)。 ↩

  63. 出处:「Chapter 10. Accessing MySQL Using PHP」第 1175 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1175,搜「INSERT INTO owners」)。insert ID 作为外键插入关联表的示例。 ↩

  64. 出处:「Chapter 10. Accessing MySQL Using PHP」第 1147 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1147,搜「not reused」)。「the previous id value of 2 is not reused, as this could cause complications」。 ↩↩

  65. 出处:「Chapter 10. Accessing MySQL Using PHP」第 1200 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1200,搜「secondary query」)与第 1258 段(:1258,搜「NATURAL JOIN」)。「although it wouldn't illustrate performing additional queries... you could also return the same information using a NATURAL JOIN query」。 ↩

  66. 出处:「Chapter 10. Accessing MySQL Using PHP」第 1277 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1277,搜「verify a user」)与第 1298 段(:1298,搜「admin' #」)、第 1307 段(:1307,搜「SQL injection attack」)。「the # symbol represents the start of a comment. Therefore, the user will be logged in as admin」。 ↩

  67. 出处:「Chapter 10. Accessing MySQL Using PHP」第 1335 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1335,搜「OR 1=1」)。「any statement followed by OR 1=1 is always TRUE」「you've now lost your whole users database」。 ↩

  68. 出处:「Chapter 10. Accessing MySQL Using PHP」第 1361 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1361,搜「mysql_fix_string」)。quote 版 sanitize 函数;直接调 $pdo->quote($string) 的说明在第 1373 段。 ↩↩

  69. 出处:「Chapter 10. Accessing MySQL Using PHP」第 1427 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1427,搜「much easier and safer way」)与第 1440 段(:1440,搜「should no longer use」)。「you should no longer use direct inserting of data into MySQL but instead always use placeholders」。 ↩↩

  70. 出处:「Chapter 10. Accessing MySQL Using PHP」第 1444 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1444,搜「without the possibility」)。「data is transferred directly to the database, without the possibility of user-submitted (or other) data being interpreted as MySQL statements」;MySQL 原生 PREPARE 见例 10-18(第 1462 段)。 ↩↩↩

  71. 出处:「Chapter 10. Accessing MySQL Using PHP」第 1532 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1532,搜「:author」)与第 1542 段(:1542,搜「order of the array items is irrelevant」)、第 1546 段(:1546,搜「guess the data types」)。 ↩

  72. 出处:「Chapter 10. Accessing MySQL Using PHP」第 1515 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1515,搜「rowCount」)。「verify whether the command was executed successfully」。 ↩

  73. 出处:「Chapter 10. Accessing MySQL Using PHP」第 1440 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1440,搜「bulletproof」)。「pretty much bulletproof in terms of security」。 ↩↩↩

  74. 出处:「Chapter 10. Accessing MySQL Using PHP」第 1346 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1346,搜「magic quotes」)。「The feature was removed in PHP 5.4.0」。 ↩

  75. 出处:「Chapter 10. Accessing MySQL Using PHP」第 1598 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1598,搜「cross-site scripting」)与第 1602 段(:1602,搜「steals cookies」)。「steals cookies from your site's users... session hijacking」。 ↩

  76. 出处:「Chapter 10. Accessing MySQL Using PHP」第 1606 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1606,搜「htmlentities」)。「strips out all HTML markup and replaces it with a form that displays the characters but does not allow a browser to act on them」。 ↩

  77. 出处:「Chapter 10. Accessing MySQL Using PHP」第 1622 段(text/21-ch10-chapter-10-accessing-mysql-using-php.txt:1622,搜「either immediately or after storing」)。「if you are ever going to display anything that your users enter... you first need to sanitize it using the htmlentities function」;两函数合集见例 10-20(第 1633 段)。 ↩