← 课程 动手学 SQL
0 / 5 单元
English
第 3 课

用 SQL 向数据库提问

SQL 是向数据库要数据时使用的语言。你只要说清楚想要什么,数据库会自己想办法取出来。在这一课里,你会用点击来拼出查询,并亲眼看到每一步对真实的数据行做了什么。

SQL 是声明式的:你描述想要的结果集,由查询规划器决定怎么算出来。本课涵盖过滤与排序、连接的语义、聚合与窗口函数、逻辑执行顺序、三值逻辑以及 SQL 注入。

每个单元都会用到的商店数据

NULL 的意思是“这里没有值”。Eli 没有填写城市,有一笔订单是没注册账号的访客下的,还有些订单暂时没有评分。Gus 和 Hana 什么都没买过。

NULL 表示缺失或未知的值:Eli 的 customers.city、访客下单时的 orders.customer_id,以及尚未评分的订单。Gus 和 Hana 没有订单,也没有任何订单引用台灯(Desk lamp)。

第 1 单元

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 * 没问题,但在应用代码里要写明列名,这样表结构变化时代码不会出错。
第 2 单元

JOIN:把多张表合起来

订单里只存了顾客编号。想在每笔订单旁边显示顾客名字,就要把两张表连接起来:每笔订单都会和 id 等于它 customer_id 的那位顾客配对。选一种连接方式,顺着连线看看。

连接谓词负责给行配对。外连接随后会把一侧或两侧没配上的行加回来,缺的部分用 NULL 补齐。把四种类型都试一遍,再看看对右表加过滤条件会让 LEFT JOIN 变成什么样。

JOIN
过滤
SQL

    
customers
orders
结果
该用哪一种?只想要有配对的行时用 INNER JOIN,比如“订单及其顾客”。左表的每一行即使没有配对也必须出现时用 LEFT JOIN,比如“所有顾客,以及他们的订单(如果有的话)”。缺少的值会以 NULL 返回。
关于连接的几点
  • RIGHT JOIN 就是把两张表对调后的 LEFT JOIN。大多数团队只写 LEFT JOIN,让查询更好读。
  • 关于可选一侧的条件应该写在 ON 里。写在 WHERE 里的条件在连接之后才执行,会把用 NULL 补齐的行过滤掉。
  • 要找没有配对的行(反连接),用 LEFT JOIN … WHERE o.id IS NULL 或 NOT EXISTS。
  • 一对多连接会让左表的行按匹配数重复出现。之后再聚合(比如对顾客表的某列求 SUM)可能会重复计算。
第 3 单元

GROUP BY 与聚合函数

GROUP BY 会把某个值相同的行放进同一个桶,再把每个桶变成一行汇总。COUNT 用来计数,SUM 用来求和,AVG 用来求平均。WHERE 在分桶之前过滤行,HAVING 在分桶之后过滤整个桶。

聚合会把每个分组压缩成一行。对比 COUNT(*) 和 COUNT(rating),看看 AVG 如何跳过 NULL,以及 NULL 键为何会归入同一组。然后再把 GROUP BY 和保留每一行的窗口函数做个对比。

SQL

    
orders
结果
选一列用来分组,然后点“运行”。
要记住的规则:用了 GROUP BY 之后,SELECT 里的每一列要么出现在 GROUP BY 里,要么放在 COUNT 这类聚合函数里。一个装了三笔订单的桶有三个金额,数据库需要知道你要的是总和、平均值还是别的什么。

GROUP BY 与窗口函数

窗口函数也是在一组相关的行上计算,就像分组一样,但不会把它们压缩成一行。每一行都保持原样,只是旁边多了一个计算出来的值。

SQL

      
第 4 单元

查询到底是怎么执行的

你先写的是 SELECT,但数据库并不从这里开始。它先找到表,然后过滤,再分组,最后才挑选列。逐步执行下面的查询,留意每一行旁边的数字。

逻辑处理顺序决定了每个子句能看到什么。物理执行计划可能会把过滤条件下推、调整连接顺序或使用索引,但结果必须和按这个顺序算出来的一样。

为什么会出错?

NULL 与三值逻辑

在 SQL 里,一个条件的结果可以是 TRUE、FALSE 或 NULL(未知)。WHERE 只保留条件为 TRUE 的行。预测一下每个表达式的结果。

在数据上跑一跑
SQL

      

SQL 注入与参数化查询

一个搜索框把名字发给服务器,服务器用它拼出查询。试试预设的输入,并在“拼接字符串”和“把值作为参数传递”之间切换。

试试
代码
服务器代码

        
数据库实际收到的内容

        
第 5 单元

自我检测