按计算值筛选
了解对列使用函数为何会导致索引失效,以及面试官如何考查这一点
按计算值筛选 是 CoddyKit 上的免费 SQL Interview Prep 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 SQL Interview Prep 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 SQL Interview Prep 课程共包含 4 节课。
为什么这个问题能区分不同水平
这个问题听起来很简单:这个查询是正确的,但为什么很慢?通常,答案是 WHERE 子句在带索引的列上套用了函数。这会使谓词变得不可搜索:优化器无法再使用索引,只能扫描每一行。
本课将解释可搜索性,展示面试官希望看到的改写方式,并说明计算后的筛选条件实际应该放在哪里。
用一个定义理解可搜索性
可搜索(表示搜索参数可用)是指谓词可以使用索引,直接定位匹配的行。一个经验规则是:索引列必须在比较运算符的一侧直接出现,不能埋在函数或表达式中。
- 可搜索:
col = 5、col > 100、col LIKE 'abc%' - 不可搜索:
FUNC(col) = 5、col + 1 > 100
对列使用函数的反模式
这里的目标是筛选 2024 年下单的订单。在列上套用 YEAR() 会迫使数据库引擎在比较之前为每一行计算年份,因此 order_date 上的索引就无法发挥作用。
查询会返回正确结果,但会扫描整张表。对于大表来说,这可能意味着从几毫秒增加到几分钟。
-- non-sargable: function on the indexed column
SELECT *
FROM orders
WHERE YEAR(order_date) = 2024;改写为范围条件
解决方法是让 order_date 直接出现,并将条件表达为半开范围。这样,order_date 上的索引就可以直接定位到 2024 年的起点,并在 2025 年停止。
结果相同,但使用的是索引范围扫描,而不是全表扫描。这种范围改写是面试中最常考的可搜索性修复方法。
-- sargable: column stays bare
SELECT *
FROM orders
WHERE order_date >= '2024-01-01'
AND order_date < '2025-01-01';对列进行算术运算
算术运算中也隐藏着同样的问题。WHERE salary + bonus > 100000 或 WHERE price * 0.9 < 50 都会对列进行计算,从而阻止使用索引。
请尽可能把计算移到常量一侧:将 price * 0.9 < 50 改写为 price < 50 / 0.9。字面量只需计算一次,同时让 price 保持直接出现并可使用索引。
-- before: math on the column (non-sargable)
WHERE price * 0.9 < 50
-- after: math on the constant (sargable)
WHERE price < 50 / 0.9不区分大小写的搜索变体
相对于 email 上的普通索引,WHERE LOWER(email) = 'a@b.com' 是不可搜索的,因为必须先将每一行的电子邮件地址转换为小写。
生产环境中有两种修复方式:存储一份经过规范化并转换为小写的副本,然后为其建立索引;或者在 LOWER(email) 上创建函数索引,直接为表达式建立索引。提到函数索引这一选项,能够体现您具有实际项目经验。
-- functional index makes the expression sargable
CREATE INDEX idx_email_lower ON users (LOWER(email));
SELECT * FROM users WHERE LOWER(email) = 'a@b.com';确实需要计算时
有时筛选条件确实依赖某个无法改写为范围的计算值,例如按比率筛选。不过,您仍然不能在 WHERE 中引用 SELECT 别名,因为 WHERE 会在 SELECT 列表之前求值。
因此,您可以在 WHERE 中重复该表达式,或者将查询包在子查询或 CTE 中,再在外层查询中筛选计算列。
SELECT *
FROM (
SELECT *, revenue / NULLIF(visits, 0) AS rev_per_visit
FROM stats
) t
WHERE t.rev_per_visit > 2.5;聚合条件放在 HAVING,而不是 WHERE
聚合计算完全不能放在 WHERE 中,因为 WHERE 会在分组之前筛选单独的行。WHERE SUM(amount) > 1000 会产生错误。
聚合筛选条件应放在 HAVING 中,因为它会在 GROUP BY 之后执行。了解哪个子句可以使用该计算结果,本身就是一个常见的执行顺序问题。
SELECT customer_id, SUM(amount) AS total
FROM orders
GROUP BY customer_id
HAVING SUM(amount) > 1000;面试官如何考察这一点
面试官会展示一个在列上使用函数的慢查询,并要求您在不改变结果的情况下让它变快。您的处理方式是:
- 识别出对列使用函数属于不可搜索条件
- 改写查询,让列直接出现(使用范围条件或将计算移到常量一侧)
- 如果无法改写,则提出函数索引或存储的计算列
提到使用 EXPLAIN 确认执行计划已从顺序扫描变为索引扫描,就能让答案更加完整。
权衡意识
请保持客观:索引和函数索引可以加快读取,但会降低写入速度并占用存储空间。对于很小的表,全表扫描完全没有问题,添加索引反而是浪费精力。
更成熟的回答应当视情况而定:如果该列对应的数据量很大,并且经常以这种方式进行筛选,就让谓词变得可搜索,或添加函数索引;否则就保持现状。面试中,具体情境比教条更重要。
函数索引让计算表达式可使用索引
有时您确实必须根据转换后的值进行筛选,例如进行不区分大小写的匹配。与其放弃使用索引,不如针对筛选时使用的确切表达式创建表达式(函数)索引。
- 这样一来,即使列被函数包裹,优化器仍可使用该索引。
- 索引表达式必须与谓词表达式完全一致。
-- index the expression you filter on
CREATE INDEX idx_users_lower_email ON users (lower(email));
-- now this predicate stays sargable
SELECT * FROM users WHERE lower(email) = 'amy@example.com';快速检查
找出优化器可以使用索引的谓词。
回顾
要点:
- 当索引列直接出现,而不是位于函数或算术运算中时,谓词就是可搜索的
- 将
YEAR(col) = 2024改写为半开范围;把计算移到常量一侧 - 对于无法避免的表达式,使用函数索引或存储的计算列
- 不能在
WHERE中使用SELECT别名;聚合条件应放在HAVING中
经典问题是慢查询;经典修复方式是让列直接出现。
常见问题解答
「按计算值筛选」课时是免费的吗?
是的 — 「按计算值筛选」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 SQL Interview Prep 课程的其余内容,请升级到 CoddyKit PRO。 SQL Interview Prep 课程共包含 4 节课。
「按计算值筛选」这节课中我会学到什么?
了解对列使用函数为何会导致索引失效,以及面试官如何考查这一点 你通过在浏览器中直接运行的动手代码来练习 SQL Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 SQL Interview Prep 需要有经验吗?
无需任何先前经验。CoddyKit 上的 SQL Interview Prep 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「按计算值筛选」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 SQL Interview Prep 课中编写并运行代码吗?
能。每节 SQL Interview Prep 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。