用 SQL 向数据库提问
SQL 是向数据库要数据时使用的语言。你只要说清楚想要什么,数据库会自己想办法取出来。在这一课里,你会用点击来拼出查询,并亲眼看到每一步对真实的数据行做了什么。
SQL 是声明式的:你描述想要的结果集,由查询规划器决定怎么算出来。本课涵盖过滤与排序、连接的语义、聚合与窗口函数、逻辑执行顺序、三值逻辑以及 SQL 注入。
每个单元都会用到的商店数据
NULL 的意思是“这里没有值”。Eli 没有填写城市,有一笔订单是没注册账号的访客下的,还有些订单暂时没有评分。Gus 和 Hana 什么都没买过。
NULL 表示缺失或未知的值:Eli 的 customers.city、访客下单时的 orders.customer_id,以及尚未评分的订单。Gus 和 Hana 没有订单,也没有任何订单引用台灯(Desk lamp)。
SELECT、WHERE、ORDER BY、LIMIT
点一点就能拼出一条查询。选一张表,选要显示的列,再加上过滤条件、排序方式和行数上限。然后点“运行”,看看每一行会怎样。
组合一条单表查询。虚线边框的选项是进阶情形:NULL 比较、范围条件和 OFFSET。留意关于 NULL 排序位置以及 LIMIT 边界处并列值的提示。
FROM 这张表里,只要 WHERE 条件成立的行,按 ORDER BY 排好序,最多给我 LIMIT 行,并且只显示 SELECT 这些列。”文本值要放在单引号里,比如 'Berlin',数字则不用。
- 没有
ORDER BY时,行的顺序是未定义的。换一次运行、换个版本或换个执行计划,顺序都可能变。 - NULL 的位置因数据库而异:PostgreSQL 和 Oracle 在 ASC 中把 NULL 排在最后,MySQL 和 SQLite 则排在最前。支持的话就写明
NULLS FIRST或NULLS LAST。 - 用
LIMIT … OFFSET分页需要唯一的排序键,否则翻页时行可能重复或丢失。页数很深时,优先用键集分页:WHERE (price, id) > (?, ?)。 - 探索数据时用
SELECT *没问题,但在应用代码里要写明列名,这样表结构变化时代码不会出错。
JOIN:把多张表合起来
订单里只存了顾客编号。想在每笔订单旁边显示顾客名字,就要把两张表连接起来:每笔订单都会和 id 等于它 customer_id 的那位顾客配对。选一种连接方式,顺着连线看看。
连接谓词负责给行配对。外连接随后会把一侧或两侧没配上的行加回来,缺的部分用 NULL 补齐。把四种类型都试一遍,再看看对右表加过滤条件会让 LEFT JOIN 变成什么样。
INNER JOIN,比如“订单及其顾客”。左表的每一行即使没有配对也必须出现时用 LEFT JOIN,比如“所有顾客,以及他们的订单(如果有的话)”。缺少的值会以 NULL 返回。
RIGHT JOIN就是把两张表对调后的 LEFT JOIN。大多数团队只写 LEFT JOIN,让查询更好读。- 关于可选一侧的条件应该写在
ON里。写在WHERE里的条件在连接之后才执行,会把用 NULL 补齐的行过滤掉。 - 要找没有配对的行(反连接),用
LEFT JOIN … WHERE o.id IS NULL或NOT EXISTS。 - 一对多连接会让左表的行按匹配数重复出现。之后再聚合(比如对顾客表的某列求
SUM)可能会重复计算。
GROUP BY 与聚合函数
GROUP BY 会把某个值相同的行放进同一个桶,再把每个桶变成一行汇总。COUNT 用来计数,SUM 用来求和,AVG 用来求平均。WHERE 在分桶之前过滤行,HAVING 在分桶之后过滤整个桶。
聚合会把每个分组压缩成一行。对比 COUNT(*) 和 COUNT(rating),看看 AVG 如何跳过 NULL,以及 NULL 键为何会归入同一组。然后再把 GROUP BY 和保留每一行的窗口函数做个对比。
COUNT 这类聚合函数里。一个装了三笔订单的桶有三个金额,数据库需要知道你要的是总和、平均值还是别的什么。
GROUP BY 与窗口函数
窗口函数也是在一组相关的行上计算,就像分组一样,但不会把它们压缩成一行。每一行都保持原样,只是旁边多了一个计算出来的值。
查询到底是怎么执行的
你先写的是 SELECT,但数据库并不从这里开始。它先找到表,然后过滤,再分组,最后才挑选列。逐步执行下面的查询,留意每一行旁边的数字。
逻辑处理顺序决定了每个子句能看到什么。物理执行计划可能会把过滤条件下推、调整连接顺序或使用索引,但结果必须和按这个顺序算出来的一样。
为什么会出错?
NULL 与三值逻辑
在 SQL 里,一个条件的结果可以是 TRUE、FALSE 或 NULL(未知)。WHERE 只保留条件为 TRUE 的行。预测一下每个表达式的结果。
SQL 注入与参数化查询
一个搜索框把名字发给服务器,服务器用它拼出查询。试试预设的输入,并在“拼接字符串”和“把值作为参数传递”之间切换。