课程 10.2 · 阅读时间:~10 分钟
本课程介绍编写高性能 SQL 查询的基础知识。您将学习如何避免不必要的数据库负载,为什么 SELECT * 通常会影响性能,以及如何有效地过滤数据。我们将回顾一些实用的技巧,帮助查询在大型数据集上更快运行。到本课程结束时,您将能够编写高效的 SQL,合理使用服务器资源。
课程 10.2:编写高效的 SQL 查询
在上一节中,我们关注了人类的可读性。但 SQL 也必须对数据库引擎可读且高效。即使是格式完美的代码,如果迫使服务器做不必要的工作,也可能表现不佳。
查询效率直接影响应用程序和报告的速度。在大数据或高负载系统中,“有效”和“优化”之间的差异可能是几分钟甚至几小时的运行时间。
现代 DBMS 引擎拥有强大的优化器,可以在后台重写查询。然而,优化器并不是全能的。它不知道您的业务意图,无法始终修复根本上低效的设计选择。代码质量仍然是开发人员的责任。
黄金法则:仅检索所需内容
查询缓慢的最常见原因是数据库服务器与客户端之间传输了过多不必要的数据。
避免 SELECT *
虽然 SELECT * 方便用于快速探索,但在生产 SQL 中应避免使用。
- 额外流量: 您传输了不需要的列。
- 索引限制: 当请求所有列时,覆盖索引策略变得更加困难。
- 脆弱的代码: 添加新列可能会意外改变行为和性能。
-- 不佳
SELECT * FROM film;
-- 更好
SELECT film_id, title, release_year
FROM film;
过滤优化
您限制行的方式决定了 DBMS 必须扫描和处理多少数据。
在服务器端过滤
尽早应用 WHERE 条件,在进行大量聚合或返回给客户端之前。您越早减少行数,下游步骤(JOIN、GROUP BY)就会变得越快。
避免在 WHERE 中使用函数(SARGable 查询)
为了有效使用索引,WHERE 谓词应该是 SARGable (Search ARGumentable)。如果您将索引列包装在函数中,优化器通常无法有效使用索引,可能会扫描整个表。
-- 缓慢(非 SARGable:可能无法使用 rental_date 的索引)
SELECT count(*)
FROM rental
WHERE YEAR(rental_date) = 2005;
-- 快速(SARGable:可以使用索引)
SELECT count(*)
FROM rental
WHERE rental_date >= '2005-01-01' AND rental_date < '2006-01-01';
使用 JOIN
连接表是最耗资源的查询操作之一。
- 先过滤,再连接: 在连接之前减少子表中的行量。
- 检查连接键上的索引: 通常是主键和外键。
- 避免不必要的
CROSS JOIN: 笛卡尔积可能迅速膨胀。 - 使用
EXISTS进行存在性检查: 如果您只需要知道相关行是否存在,EXISTS通常比JOIN更便宜。
-- 效率较低(JOIN 强制通过所有付款进行匹配)
SELECT DISTINCT c.first_name, c.last_name
FROM customer c
JOIN payment p ON c.customer_id = p.customer_id;
-- 效率更高(EXISTS 可以在第一次匹配时停止)
SELECT c.first_name, c.last_name
FROM customer c
WHERE EXISTS (
SELECT 1 FROM payment p WHERE p.customer_id = c.customer_id
);
在测试时使用 LIMIT
在调试查询时,始终使用 LIMIT 以避免意外返回数百万行。
SELECT customer_id, first_name, last_name
FROM customer
WHERE active = 1
LIMIT 10;
实际示例:报告优化
假设我们需要租赁超过 30 次的电影,限制为一个类别。
效率较低的方法:
SELECT f.title, COUNT(r.rental_id)
FROM film f
JOIN inventory i ON f.film_id = i.film_id
JOIN rental r ON i.inventory_id = r.inventory_id
JOIN film_category fc ON f.film_id = fc.film_id
JOIN category c ON fc.category_id = c.category_id
WHERE c.name = 'Action'
GROUP BY f.title
HAVING COUNT(r.rental_id) > 30;
效率更高的方法: 如果我们知道类别 ID,可以跳过连接类别名称查找。
SELECT f.title, COUNT(r.rental_id) AS rental_count
FROM film f
JOIN film_category fc ON f.film_id = fc.film_id
JOIN inventory i ON f.film_id = i.film_id
JOIN rental r ON i.inventory_id = r.inventory_id
WHERE fc.category_id = 1 -- 使用 ID 而不是字符串查找
GROUP BY f.film_id, f.title
HAVING COUNT(r.rental_id) > 30;
注意:通过数字 ID 进行过滤通常比通过文本名称进行过滤更快,并且通常允许更少的连接。
本课的关键要点:
- 在生产查询中避免使用
SELECT *;仅列出所需的列。 - 尽早使用
WHERE进行过滤。 - 编写 SARGable 谓词,以便可以使用索引。
- 对于纯存在性检查,优先使用
EXISTS而不是JOIN。 - 在探索和调试期间使用
LIMIT。 - 优先通过数字键而不是文本标签进行过滤。
常见问题解答
为什么 SELECT * 在生产查询中有害?
它返回不必要的列,增加流量,并可能阻塞友好的索引计划。显式列列表通常更快且更安全。
SARGable 在实践中意味着什么?
SARGable 谓词允许基于索引的搜索。将索引列包装在函数中通常会阻止有效使用索引。
何时应使用 EXISTS 而不是 JOIN?
当您只需要知道相关行是否存在,并且不需要第二个表中的列时,使用 EXISTS。
面试问题
当 SQL 查询缓慢时,您的第一步是什么?
检查它是否使用 SELECT *,审查 WHERE 的选择性,并识别非 SARGable 谓词。然后在查询的早期检查连接策略和行量。
为什么提前过滤可以提高性能?
它减少了参与连接、排序和聚合的行数,从而降低了整体计划成本。
从性能的角度来看,JOIN 和 EXISTS 有何不同?
JOIN 组合行集,并且在您需要两侧的列时是必需的。EXISTS 通常在布尔存在性检查中更快,因为它可以在第一次匹配时停止。
在下一节中,我们将深入探讨执行分析,并查看索引如何在物理层面加速查询。