PostgreSQL 教程: 优化 SELECT 查询中的筛选条件

一月 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 倍。

Filter using native column types

警告

并非所有的函数都可以避免掉,因为可能需要一些函数,来检索列值中的某部分(考虑下substring的示例)或者重构它。不过,每次要在筛选条件中添加函数时,请考虑是否可以使用原始的数据类型运算符进行替代。

最佳实践

将筛选条件应用于一个列时,请尝试调整筛选条件的格式,而不是列的格式。

以上是一个完美的示例:将过滤条件的格式,从日期2024-02-01调整成时间戳2024-02-01 00:00:00,这样就允许我们使用原始的时间戳数据格式和运算符。

专业建议

如果必须要应用函数,您可以尝试以下两个方法:

  • 在表达式上创建一个索引,这在 PostgreSQLMySQL 中都是可行的
  • 使用数据库触发器,来填充要进行转换的附加列

改进子查询

原因

子查询通常用于筛选条件中,以检索出一个数值集合,来应用到筛选条件。一个常见的示例是,需要检索出最近活动过的用户列表时。

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

警告

在极端情况下,子查询中的细微变化可能会提供不同的结果。

最佳实践

当需要使用子查询时,请花点时间学习和了解有哪些方法,以及它们允许您实现的目标。很多时候有多个方法,某些功能会提供更好的响应时间。

对结果进行分页

原因

当需要显示一长串的行时,对从数据库中检索到的结果进行分页,会非常有用。PostgreSQLMySQL 都提供了对输出进行LIMIT的功能,可以只检索一定数量的行,并通过基于排序仅检索在特定范围内的行,来为结果集进行OFFSET的功能。要将发送到客户端的数据最小化为仅需要显示的数据,使用LIMITOFFSET是一个好方法。

Paginate results

警告

使用LIMITOFFSET的缺点是,您需要对数据库要发送和执行的每个 “分页” 进行查询。如果总行数与页面大小相差不大,这可能会很不方便。例如,如果您以10行的分页形式显示结果,但平均要显示15行的结果,则最好一次检索出整个数据集。

最佳实践

如果结果集的大小比页面大小要大一个数量级,则使用分页可能是一个实现更好性能的有效方法,因为只要从数据库中检索可见的数据集。

专业建议

LIMITOFFSET子句是大多数数据库中缺省的分页方法。但是,通过将当前页面的开始和结束偏移量存储在客户端,并在 SQL 语句的WHERE子句中推移对下一页的筛选条件,可以实现更高效的分页实现。分页查询教程中提供了这类实现的几个示例。

将筛选条件从 HAVING 子句移动到 WHERE 子句

原因

运行一个查询时,在WHEREHAVING子句中定义的筛选条件,会在不同的时间进行应用:

  • 在应用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语句转换/聚合数据之前应用。

了解更多

PostgreSQL 优化