全表扫描
在任何查询前面加上 EXPLAIN QUERY PLAN,它就不会被执行,回来的是计划。
1
EXPLAIN QUERY PLAN
SELECT * FROM products WHERE name = 'Quill Helmet';
结果 — 1 行
| id | parent | notused | detail |
|---|---|---|---|
| 2 | 0 | 216 | SCAN products |
浏览器会另开一个标签页打开 Planche,示例数据库已经载入,这条查询也已经放好。本页其他示例都会复用同一个标签页;在你按下 F9 之前,什么都不会执行。
SCAN products
SCAN 表示数据库把表里每一行都读了一遍并逐行判断。products.name 上没有索引,所以也没有更好的办法。400 行谁也感觉不到。要在意它,是因为这个代价和表的大小成正比:同样的计划放到一千万行上,每跑一次就读一千万行。
索引查找
price 上是有索引的。
2
EXPLAIN QUERY PLAN
SELECT * FROM products WHERE price > 2000;
结果 — 1 行
| id | parent | notused | detail |
|---|---|---|---|
| 3 | 0 | 202 | SEARCH products USING INDEX idx_products_price (price>?) |
SEARCH products USING INDEX idx_products_price (price>?)
SEARCH 才是你想看到的词。数据库走了索引,直接跳到价格超过 2000 的起点,只从那里往后读——答案之外的行一行都没碰。别的引擎用不同的词表达同一个区别:PostgreSQL 说 Seq Scan 和 Index Scan,MySQL 的 EXPLAIN 显示 type: ALL 和 type: range,SQL Server 画出来的是 Table Scan 和 Index Seek。
连接的计划
3
EXPLAIN QUERY PLAN
SELECT o.order_number, c.last_name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'delivered';
结果 — 2 行
| id | parent | notused | detail |
|---|---|---|---|
| 4 | 0 | 61 | SEARCH o USING INDEX idx_orders_status (status=?) |
| 9 | 0 | 45 | SEARCH c USING INTEGER PRIMARY KEY (rowid=?) |
SEARCH o USING INDEX idx_orders_status (status=?)
SEARCH c USING INTEGER PRIMARY KEY (rowid=?)
把它当成一个循环来读:通过状态索引找出已送达的订单,再对其中每一张按主键查它的客户。两步都是查找,健康的连接就长这样。要警惕的计划是内侧出现 SCAN——那意味着对外侧的每一行都完整读一遍表,也是开发库上很快的连接到了生产库上要跑几分钟的常见原因。外键列需要索引;包括 SQLite 和 MySQL 在内的大多数数据库都不会替你建。
怎样把索引弄丢
orders.ordered_at 上有索引。下面这条查询按它筛选,却仍然读了整张表。
4
EXPLAIN QUERY PLAN
SELECT * FROM orders WHERE substr(ordered_at, 1, 4) = '2025';
结果 — 1 行
| id | parent | notused | detail |
|---|---|---|---|
| 2 | 0 | 216 | SCAN orders |
SCAN orders
索引里存的是 ordered_at,不是 substr(ordered_at, 1, 4)。列一旦被包进函数,那份存好的顺序就什么也说明不了了,数据库只能对每一行都把表达式算一遍才知道结果。WHERE YEAR(created_at) = 2025、WHERE UPPER(email) = ... 和 WHERE price * 1.1 > 100 都是同一回事。
改写成让裸列直接面对比较:
5
EXPLAIN QUERY PLAN
SELECT * FROM orders
WHERE ordered_at >= '2025-01-01'
AND ordered_at < '2026-01-01';
结果 — 1 行
| id | parent | notused | detail |
|---|---|---|---|
| 3 | 0 | 163 | SEARCH orders USING INDEX idx_orders_ordered_at (ordered_at… |
SEARCH orders USING INDEX idx_orders_ordered_at (ordered_at>? AND ordered_at<?)
行还是那些行,现在索引在干活了。一般规则是:让索引列单独待在比较的一侧,把运算挪到另一侧。把日期上的函数换成半开区间,是这条规则最常见的应用。
ORDER BY 可以不花钱
6
EXPLAIN QUERY PLAN
SELECT * FROM products ORDER BY price DESC LIMIT 20;
结果 — 1 行
| id | parent | notused | detail |
|---|---|---|---|
| 5 | 0 | 222 | SCAN products USING INDEX idx_products_price |
SCAN products USING INDEX idx_products_price
第三种形状:不是查找,但也不是无序的扫描。查询要的是按价格排好的行,而索引本来就按价格有序,于是数据库沿着索引走,取到 20 行就停——完全没有排序。没有那个索引的话,它得读完 400 行、排好序、再扔掉 380 行。
这就是为什么索引对 ORDER BY ... LIMIT 的帮助不亚于对 WHERE 的帮助,也是为什么"按 X 取前 20"这类查询值得检查一下:有合适的索引就只碰 20 行,没有就得碰整张表。
一份简短的检查清单
- 给你用来筛选和连接的列建索引——尤其是外键,它们很少会自动有。
- 别在
WHERE里把索引列套进函数调用。 - 能用
LIKE 'abc%'就别用LIKE '%abc%';开头的通配符用不上索引。 - 索引不是免费的——每次写入都要更新它们,所以只给你真的会查询的列建,别每个列都建。
- 要测,别猜。改动前后各跑一次
EXPLAIN,把两份计划对着看。
接下来去哪儿
基础路线到此结束。方言页把三件事——分页、upsert、字符串与日期处理——在 MySQL、PostgreSQL、SQL Server、Oracle 和 SQLite 里并排放好,因为正是这三处的语法差得足够多,一条能跑的查询换个引擎就得先改写。