如何提高包含部分公共子查询的 SQL 查询性能
Posted
技术标签:
【中文标题】如何提高包含部分公共子查询的 SQL 查询性能【英文标题】:How to improve SQL query performance containing partially common subqueries 【发布时间】:2021-07-03 17:49:58 【问题描述】:我在 PostgreSQL 13 中有一个简单的表 tableA
,其中包含事件计数的时间序列。在程式化的形式中,它看起来像这样:
event_count sys_timestamp
100 167877672772
110 167877672769
121 167877672987
111 167877673877
... ...
两个字段都定义为numeric
。
借助 *** 的答案,我能够创建一个查询,该查询基本上计算给定时间跨度内正负过量事件的数量,以当前事件计数为条件。查询如下所示:
SELECT t1.*,
(SELECT COUNT(*) FROM tableA t2
WHERE t2.sys_timestamp > t1.sys_timestamp AND
t2.sys_timestamp <= t1.sys_timestamp + 1000 AND
t2.event_count >= t1.event_count+10)
AS positive,
(SELECT COUNT(*) FROM tableA t2
WHERE t2.sys_timestamp > t1.sys_timestamp AND
t2.sys_timestamp <= t1.sys_timestamp + 1000 AND
t2.event_count <= t1.event_count-10)
AS negative
FROM tableA as t1
查询按预期工作,并在此特定示例中为每一行返回给定时间窗口(+ 1000 [毫秒])的正负超额计数(范围 + / - 10)。
但是,我必须对具有数百万(甚至可能超过 100+ 百万)条目的表运行此类查询,即使有大约 50 万行,查询也需要很长时间才能完成。此外,虽然给定查询中的时间范围始终保持不变[但窗口大小可能因查询而异],但在某些情况下,我将不得不使用可能 10 个附加条件,类似于同一查询中的正/负超额.
因此,我正在寻找改进上述查询的方法,主要是考虑到设想数据集的大小,然后考虑更多条件,以实现更好的性能。
我的具体问题:
如何重用子查询的公共部分以确保它不会被执行两次(或多次),即如何在查询中重用它?
(SELECT COUNT(*) FROM tableA t2
WHERE t2.sys_timestamp > t1.sys_timestamp
AND t2.sys_timestamp <= t1.sys_timestamp + 1000)
将当前为numeric
的sys_timestamp
字段转换为时间戳字段并尝试使用任何PostgreSQL Windows 函数是否有一些性能优势? (不幸的是,我对此没有足够的经验。)
除了重用(部分)子查询以显着提高大型数据集的性能之外,是否还有一些巧妙的方法可以重写查询?
这些类型的查询使用 Java、Scala、Python 等在数据库之外运行它们可能更快吗?
【问题讨论】:
样本数据和期望的结果真的很有帮助。 如果您提供(相关部分)表定义(CREATE TABLE
语句)显示数据类型和约束、现有索引和一些示例数据,这会容易得多。 Consider instructions for performance questions here.
【参考方案1】:
如何重用子查询的公共部分...?
在单个LATERAL
子查询中使用条件聚合:
SELECT t1.*, t2.positive, t2.negative
FROM tableA t1
CROSS JOIN LATERAL (
SELECT COUNT(*) FILTER (WHERE t2.event_count >= t1.event_count + 10) AS positive
, COUNT(*) FILTER (WHERE t2.event_count <= t1.event_count - 10) AS negative
FROM tableA t2
WHERE t2.sys_timestamp > t1.sys_timestamp
AND t2.sys_timestamp <= t1.sys_timestamp + 1000
) t2;
它可以是CROSS JOIN
,因为子查询总是返回一行。见:
使用带有FILTER
子句的条件聚合,将多个聚合基于同一时间范围。见:
event_count
应该是integer
或bigint
。见:
sys_timestamp
应该是timestamp
或timestamptz
。见:
(sys_timestamp)
上的索引是最低要求。 (sys_timestamp, event_count)
上的多列索引通常会有所帮助。如果该表被清理得足够多,您就会从中获得仅索引扫描。
根据确切的数据分布(最重要的是有多少时间框架重叠)和其他数据库特征,定制的程序解决方案可能会更快,但是。可以用任何客户端语言完成。但是服务器端 PL/pgsql 解决方案更胜一筹,因为它节省了到数据库服务器的所有往返行程和类型转换等。请参阅:
Window Functions or Common Table Expressions: count previous rows within range What are the pros and cons of performing calculations in sql vs. in your application【讨论】:
【参考方案2】:你的想法是对的。 编写可以在查询中重用的语句的方法是“with”语句(AKA 子查询分解)。 “with”语句作为主查询的子查询运行一次,可以被后续子查询或最终查询重用。
第一步包括创建父子详细信息行 - 表乘以自身并按时间戳过滤。
然后下一步是对其他所有内容重用相同的详细查询。
假设 event_count 是一个主索引,或者您在 event_count 和 sys_timestamp 上有一个复合索引,这看起来像:
with baseQuery as
(
SELECT distinct t1.event_count as startEventCount, t1.event_count+10 as pEndEventCount
,t1.eventCount-10 as nEndEventCount, t2.event_count as t2EventCount
FROM tableA t1, tableA t2
where t2.sys_timestamp between t1.sys_timestamp AND t1.sys_timestamp + 1000
), posSummary as
(
select bq.startEventCount, count(*) as positive
from baseQuery bq
where t2EventCount between bq.startEventCount and bq.pEndEventCount
group by bq.startEventCount
), negSummary as
(
select bq.startEventCount, count(*) as negative
from baseQuery bq
where t2EventCount between bq.startEventCount and bq.nEndEventCount
group by bq.startEventCount
)
select t1.*, ps.positive, nv.negative
from tableA t1
inner join posSummary ps on t1.event_count=ps.startEventCount
inner join negSummary ns on t1.event_count=ns.startEventCount
注意事项:
-
根据您的实际键,baseQuery 的 distinct 可能不是必需的。
最终连接是使用 tableA 完成的,但也可以使用 baseQuery 的摘要作为单独的“with”语句,该语句已经运行过一次。似乎没有必要。
您可以四处看看有什么效果。
当然还有其他方法,但这最能说明可以改进的方式和地方。
With 语句用于多维数据仓库查询,因为当您有如此多的数据要与如此多的表(维度和事实)连接时,隔离查询的策略有助于了解需要索引的位置以及如何最小化查询需要进一步处理以完成的行。 例如,很明显,如果您可以最大限度地减少 baseQuery 中返回的行或使其运行得更快(检查解释计划),您的查询总体上会得到改善。
【讨论】:
以上是关于如何提高包含部分公共子查询的 SQL 查询性能的主要内容,如果未能解决你的问题,请参考以下文章
优化 SQL:如何重写此查询以提高性能? (使用子查询,摆脱 GROUP BY?)