如何有效地对复杂 SQL 查询的结果进行分页?

How can I efficiently paginate the results of a complex SQL query?

我有一个相当复杂的 SQL 查询,它首先将一些数据提取到 CTE 中,然后在 CTE 上执行多个自连接以计算一个值。这是一个简化的例子,简化了我们应用程序的一些复杂性:

WITH subset AS (
  SELECT time, value, device_id FROM raw_data
  WHERE device_id IN (1, 2, 3)
  AND time BETWEEN '2019-01-01 00:00:00'::timestamp AND '2019-01-15 00:00:00'::timestamp
)

SELECT
  time,
  (("device_1".value + "device_2".value) / "device_3".value) as value
FROM 

(
  SELECT * FROM subset 
  WHERE device_id = 1
) "device_1"

INNER JOIN
(
  SELECT * FROM subset 
  WHERE device_id = 2
) "device_2"
ON "device_1".time = "device_2".time

INNER JOIN
(
  SELECT * FROM subset 
  WHERE device_id = 3
) "device_3"
ON "device_3".time = "device_2".time

查询是自动生成的,并且可以扩展到对潜在数十台设备的值进行复杂计算。出于性能原因,我们希望对该查询的结果进行分页,因为使用的时间范围可能很大。一个关键的约束是数据可能有时间间隔,但我们希望 return 每页的行数恒定。

我们考虑过在查询末尾使用 LIMIT per_page OFFSET start,这是标准方法,但这不会给我们带来任何加速,而且查询执行相同。这是有道理的,因为在这种情况下 LIMIT/OFFSET 是在所有数据都被获取、连接和计算之后执行的,它只是 return 已经计算的数据的一部分。这不会明显降低查询的运行速度。

我们考虑过对提取到 CTE 中的数据进行分页,即计算与感兴趣的页面对应的时间范围,然后在 CTE 的 BETWEEN 子句中使用该时间范围。这可行,但问题是我们无法可靠地计算这个时间范围,因为某些变量可能存在间隙。因此,如果我们将 100 行计算为 2 天的 window 并且我们获取 2 天,如果 device_2 在 window。对于计算,这些数据点将被丢弃在 INNER JOINS 中。

问题是,在给定这些限制的情况下,是否有一种有效的方法来对该查询进行分页或对其进行重组以启用快速分页?例如,是否有某种方法可以指示查询规划器 "join until you match 100 results matching the join conditions, and stop there"。我们 运行 在 PostgreSQL 上这样做,如果有影响的话。

假设您的用例可以容忍没有绝对最新的数据,您可以考虑创建一个物化视图:

WITH subset AS ( ... )

CREATE MATERIALIZED VIEW yourView AS SELECT ...

使用 LIMITOFFSET 对实体化视图进行分页应该比 运行 每次从头开始的完整查询更快。这里的缺点是您将从视图返回数据,必须以满足您要求的某个频率进行更新。

作为对实体化视图的 alternative/in 补充,您可以考虑使用索引调整查询。例如,可以加速 subset CTE 查询的索引可能是:

CREATE INDEX idx1 ON raw_data (time, device_id, value);

或者也许:

CREATE INDEX idx2 ON raw_data (device_id, time, value);

1) 创建复合索引,顺序如下device_id,时间desc.

2) 尝试以这种方式生成查询

select device_1.time,
 (("device_1".value + "device_2".value) / "device_3".value) as value 
from raw_data as device_1 ,raw_data as device_2 ,raw_data as device_3 
where device_1.devise_id = 1 
and device_2.devise_id = 2 
and device_3.devise_id = 3 
and device_1.time BETWEEN '2019-01-01 00:00:00'::timestamp AND '2019-01-15 00:00:00'::timestamp 
and device_2.time BETWEEN '2019-01-01 00:00:00'::timestamp AND '2019-01-15 00:00:00'::timestamp 
and device_3.time BETWEEN '2019-01-01 00:00:00'::timestamp AND '2019-01-15 00:00:00'::timestamp 
and device_1.time = device_2.time 
and device_2.time = device_3.time