一月 7, 2025
摘要:在本教程中,您将学习如何通过改进筛选条件,来优化 SELECT 类的 SQL 查询。
目录
有时,我们需要评估和改进查询中的WHERE
子句部分,对筛选条件的细微更改都可能会对查询性能产生巨大影响。
避免在筛选条件中使用函数
原因
在筛选阶段将函数应用于列会降低性能。数据库需要在筛选之前将函数应用于数据集。让我们来看一个对 timestamp 字段进行筛选的简单示例:
SELECT count(*)
FROM orders
WHERE CAST(order_timestamp AS DATE) >= '2024-02-01';
上述对 100000000 行数据集的查询,运行了01 min 53 sec
,因为它需要在应用筛选条件之前,将order_timestamp
列的数据类型从 timestamp 更改为 date。但是,那不是必需的!请注意,对于上面的查询,根据表的元数据,可以将其重写为:
SELECT count(*)
FROM orders
WHERE order_timestamp >= '2024-02-01 00:00:00';
重写的查询使用了原始的时间戳字段,而不进行强制转换。在完成如此微小的更改之后,现在查询可以在20 sec
内运行完,速度比原始查询快了近 6 倍。
警告
并非所有的函数都可以避免掉,因为可能需要一些函数,来检索列值中的某部分(考虑下substring
的示例)或者重构它。不过,每次要在筛选条件中添加函数时,请考虑是否可以使用原始的数据类型运算符进行替代。
最佳实践
将筛选条件应用于一个列时,请尝试调整筛选条件的格式,而不是列的格式。
以上是一个完美的示例:将过滤条件的格式,从日期2024-02-01
调整成时间戳2024-02-01 00:00:00
,这样就允许我们使用原始的时间戳数据格式和运算符。
专业建议
如果必须要应用函数,您可以尝试以下两个方法:
- 在表达式上创建一个索引,这在 PostgreSQL 和 MySQL 中都是可行的
- 使用数据库触发器,来填充要进行转换的附加列
改进子查询
原因
子查询通常用于筛选条件中,以检索出一个数值集合,来应用到筛选条件。一个常见的示例是,需要检索出最近活动过的用户列表时。
SELECT *
FROM users
WHERE id IN (
SELECT DISTINCT user_id
FROM sessions
WHERE session_date = '2024-02-01');
上面的查询从SESSIONS
表中检索出不同的用户列表,然后对USERS
表应用筛选条件。但是,有几种性能更高的方法,可以实现相同的结果。一个例子是使用EXISTS
:
SELECT *
FROM users
WHERE EXISTS (
SELECT user_id
FROM sessions
WHERE user_id = id and session_date = '2024-02-01'
);
EXISTS
的速度更快,因为它不需要从SESSION
表中检索不同用户的列表,而是只验证对于特定的用户在表中是否存在至少一行。上面的用例只是更改了子查询部分,性能就从02 min 08 sec
变为了18 sec
。
警告
在极端情况下,子查询中的细微变化可能会提供不同的结果。
最佳实践
当需要使用子查询时,请花点时间学习和了解有哪些方法,以及它们允许您实现的目标。很多时候有多个方法,某些功能会提供更好的响应时间。
对结果进行分页
原因
当需要显示一长串的行时,对从数据库中检索到的结果进行分页,会非常有用。PostgreSQL 和 MySQL 都提供了对输出进行LIMIT
的功能,可以只检索一定数量的行,并通过基于排序仅检索在特定范围内的行,来为结果集进行OFFSET
的功能。要将发送到客户端的数据最小化为仅需要显示的数据,使用LIMIT
和OFFSET
是一个好方法。
警告
使用LIMIT
和OFFSET
的缺点是,您需要对数据库要发送和执行的每个 “分页” 进行查询。如果总行数与页面大小相差不大,这可能会很不方便。例如,如果您以10
行的分页形式显示结果,但平均要显示15
行的结果,则最好一次检索出整个数据集。
最佳实践
如果结果集的大小比页面大小要大一个数量级,则使用分页可能是一个实现更好性能的有效方法,因为只要从数据库中检索可见的数据集。
专业建议
LIMIT
和OFFSET
子句是大多数数据库中缺省的分页方法。但是,通过将当前页面的开始和结束偏移量存储在客户端,并在 SQL 语句的WHERE
子句中推移对下一页的筛选条件,可以实现更高效的分页实现。分页查询教程中提供了这类实现的几个示例。
将筛选条件从 HAVING 子句移动到 WHERE 子句
原因
运行一个查询时,在WHERE
和HAVING
子句中定义的筛选条件,会在不同的时间进行应用:
- 在应用
SELECT
语句中定义的任何数据转换之前,会对原始数据集应用WHERE
筛选条件 HAVING
筛选条件是在聚合后应用的,因此发生在检索、转换和汇总所有行之后。
因此,应优先考虑移动在WHERE
子句中的筛选条件,因为它允许您处理较小的数据集。例如,如果我们要尝试获取一个日期列表,其中有跟踪的user_id
的会话(user_id
字段不为空)
SELECT session_date, count(user_id) nr_sessions
FROM sessions
GROUP BY session_date
HAVING count(user_id) > 0;
我们可以通过将筛选条件推送到WHERE
子句中,来重写上面的查询:
SELECT session_date, count(user_id) nr_sessions
FROM sessions
WHERE user_id is not null
GROUP BY session_date
对100,000,000
行数据集的查询性能,从21 sec
提升到了18 sec
,这只是对筛选语句做了下更改。
警告
只有当我们可以确定有一个类似的条件,可以应用于行级别,而不是聚合的级别时,才能将筛选条件从HAVING
移动到WHERE
子句。
最佳实践
应该始终尝试在WHERE
子句中进行筛选,因为筛选会在SELECT
语句转换/聚合数据之前应用。