优化GROUP BY查询以检索每个用户的最新logging
我在Postgres 9.2中有下面的表格(简化forms)
CREATE TABLE user_msg_log ( aggr_date DATE, user_id INTEGER, running_total INTEGER );
它每个用户和每天最多包含一条logging。 300天内每天将会有大约50万条logging。 每个用户的running_total总是在增加。
我想在特定date之前有效地检索每个用户的最新logging。 我的查询是:
SELECT user_id, max(aggr_date), max(running_total) FROM user_msg_log WHERE aggr_date <= :mydate GROUP BY user_id
这是非常缓慢的。 我也试过了:
SELECT DISTINCT ON(user_id), aggr_date, running_total FROM user_msg_log WHERE aggr_date <= :mydate ORDER BY user_id, aggr_date DESC;
它有相同的计划,同样缓慢。
到目前为止,我在user_msg_log(aggr_date)上有一个索引,但没有多大帮助。 有没有其他的指标,我应该用来加快这一点,或者任何其他方式来实现我想要的?
为获得最佳性能,您需要一个多列索引 :
CREATE INDEX user_msg_log_combo_idx ON user_msg_log (user_id, aggr_date DESC NULLS LAST)
要使索引只扫描可能,请添加否则不需要的列running_total
:
CREATE INDEX user_msg_log_combo_covering_idx ON user_msg_log (user_id, aggr_date DESC NULLS LAST, running_total)
为什么selectDESC NULLS LAST
?
- date查询范围内未使用的索引
对于每个user_id
几行,简单的DISTINCT ON
是最快的解决scheme之一:
- 在每个GROUP BY组中select第一行?
对于每个user_id
的许多行, 松散的索引扫描将会(更多)更有效。 这在Postgres(至less达到Postgres 10)中没有实现,但有一些方法来模拟它:
1.没有独立用户的独立表格
以下解决scheme超越了Postgres Wiki所涵盖的内容。
使用单独的users
表,下面2.中的解决scheme通常更简单快捷。
1A。 用LATERAL
joinrecursionCTE
Common Tableexpression式需要Postgres 8.4+ 。
LATERAL
需要Postgres 9.3+ 。
WITH RECURSIVE cte AS ( ( -- parentheses required SELECT user_id, aggr_date, running_total FROM user_msg_log WHERE aggr_date <= :mydate ORDER BY user_id, aggr_date DESC NULLS LAST LIMIT 1 ) UNION ALL SELECT u.user_id, u.aggr_date, u.running_total FROM cte c , LATERAL ( SELECT user_id, aggr_date, running_total FROM user_msg_log WHERE user_id > c.user_id -- lateral reference AND aggr_date <= :mydate -- repeat condition ORDER BY user_id, aggr_date DESC NULLS LAST LIMIT 1 ) u ) SELECT user_id, aggr_date, running_total FROM cte ORDER BY user_id;
这在Postgres的当前版本中更好,检索任意列也很简单。 更多的解释在第2a章。 下面。
1B。 recursionCTE与相关的子查询
方便地检索单列或整行 。 该示例使用表的整个行types。 其他变体是可能的。
WITH RECURSIVE cte AS ( ( SELECT u -- whole row FROM user_msg_log u WHERE aggr_date <= :mydate ORDER BY user_id, aggr_date DESC NULLS LAST LIMIT 1 ) UNION ALL SELECT (SELECT u1 -- again, whole row FROM user_msg_log u1 WHERE user_id > (cu).user_id -- parentheses to access row type AND aggr_date <= :mydate -- repeat predicate ORDER BY user_id, aggr_date DESC NULLS LAST LIMIT 1) FROM cte c WHERE (cu).user_id IS NOT NULL -- any NOT NULL column of the row ) SELECT (u).* -- finally decompose row FROM cte WHERE (u).user_id IS NOT NULL -- any column defined NOT NULL ORDER BY (u).user_id;
使用cu IS NOT NULL
testing行值可能会引起误解。 如果被testing行的每一列NOT NULL
是NOT NULL
,只会返回true
如果包含单个NULL
值,将会失败。 (我在回答中有一段时间错误)。相反,要断言在上一次迭代中find一行,请testing定义为NOT NULL
的行(如主键)中的单个列。 更多:
- 对一组列的NOT NULL约束
- 不是NULLtesting一个logging不会在设置variables时返回TRUE
这个查询在第2b章有更多解释。 下面。
相关答案:
- 每行查询最后N个相关的行
- GROUP BY一列,而PostgreSQL中的另一列进行sorting
2.与单独的users
表
只要我们每个相关的user_id
只有一行,表格布局就不重要了。 例:
CREATE TABLE users ( user_id serial PRIMARY KEY , username text NOT NULL );
2A。 LATERAL
连接
SELECT u.user_id, l.aggr_date, l.running_total FROM users u CROSS JOIN LATERAL ( SELECT aggr_date, running_total FROM user_msg_log WHERE user_id = u.user_id -- lateral reference AND aggr_date <= :mydate ORDER BY aggr_date DESC NULLS LAST LIMIT 1 ) l;
JOIN LATERAL
允许在同一查询级别上引用前面的FROM
项目。 每个用户只能查询一个索引(只能查询)。
- LATERAL和PostgreSQL中的子查询之间有什么区别?
考虑通过对梁刚提出的users
表格进行sorting来改善可能性。 如果users
表的物理sorting顺序恰好与user_msg_log
上的索引相匹配,则不需要这样做。
即使您在user_msg_log
有条目,也不会在users
表中find缺lessusers
结果。 通常情况下,你将有一个外键约束强制引用完整性来排除。
对于user_msg_log
没有匹配项的任何用户,您也不会获得一行。 这符合你原来的问题。 如果您需要在结果中包含这些行,请使用LEFT JOIN LATERAL ... ON true
而不是CROSS JOIN LATERAL
:
- 使用数组参数多次调用set-returning函数
这种forms也是最好的检索每个用户多个行 (但不是全部)。 只需使用LIMIT n
而不是LIMIT 1
。
实际上,所有这些都会做同样的事情:
JOIN LATERAL ... ON true CROSS JOIN LATERAL ... , LATERAL ...
不过,后者的优先级较低。 显式JOIN
在逗号之前绑定。
2B。 相关的子查询
从单行中检索单个列的好select。 代码示例:
- 优化分组最大查询
多栏可能也是如此,但是你需要更多的智慧:
CREATE TEMP TABLE combo (aggr_date date, running_total int); SELECT user_id, (my_combo).* -- note the parentheses FROM ( SELECT u.user_id , (SELECT (aggr_date, running_total)::combo FROM user_msg_log WHERE user_id = u.user_id AND aggr_date <= :mydate ORDER BY aggr_date DESC NULLS LAST LIMIT 1) AS my_combo FROM users u ) sub;
-
像上面的
LEFT JOIN LATERAL
一样,这个变体包含所有的用户,即使没有user_msg_log
条目。 对于my_combo
,您将获得NULL
,如果需要,可以使用外部查询中的WHERE
子句轻松地进行筛选。
Nitpick:在外部查询中,您无法区分子查询是否未find行,或者返回的所有值是否为NULL – 结果相同。 您必须在子查询中包含NOT NULL
列才能确定。 -
相关的子查询只能返回一个值 。 您可以将多个列换成复合types。 但是为了以后分解,Postgres需要一个众所周知的复合types。 匿名logging只能分解提供列定义列表。
-
使用注册types,如现有表的行types,或创build一个types。 用
CREATE TYPE
显式(永久)注册复合types,或者创build一个临时表(在会话结束时自动删除)以临时提供行types。 转换为该types:(aggr_date, running_total)::combo
-
最后,我们不想在相同的查询级别上分解
combo
。 由于查询计划器的弱点,这将评估每个列的子查询一次(直到Postgres 9.6 – Postgres 10的改进计划)。 相反,使它成为一个子查询并在外部查询中分解。
有关:
- 从每组的第一行和最后一行获取值
SQL小提琴演示所有四个查询。
对1k用户和10万条日志条目进行了大量testing。
桌子上的不同索引也许会有所帮助。 试试这个: user_msg_log(user_id, aggr_date)
。 我不积极的Postgres将与distinct on
最佳使用。
所以,我会坚持这个指数,并尝试这个版本:
select * from user_msg_log uml where not exists (select 1 from user_msg_log uml2 where uml2.user_id = u.user_id and uml2.aggr_date <= :mydate and uml2.aggr_date > uml.aggr_date );
这应该用索引查找replacesorting/分组。 这可能会更快。
这不是一个独立的答案,而是对@ Erwin的回答的一个评论。 对于横向连接示例2a,可以通过对users
表进行sorting来改进查询以利用user_msg_log
上的索引的user_msg_log
。
SELECT u.user_id, l.aggr_date, l.running_total FROM (SELECT user_id FROM users ORDER BY user_id) u, LATERAL (SELECT aggr_date, running_total FROM user_msg_log WHERE user_id = u.user_id -- lateral reference AND aggr_date <= :mydate ORDER BY aggr_date DESC NULLS LAST LIMIT 1) l;
原理是,如果user_id
值是随机的,那么索引查找是很昂贵的。 通过首先整理user_id
,后续的横向连接就像对user_msg_log
索引的简单扫描。 即使两个查询计划看起来相似,运行时间也会有很大差别,特别是对于大型表格。
sorting的代价是最小的,特别是如果user_id
字段上有一个索引。