优化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。 用LATERALjoinrecursionCTE

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 NULLtesting行值可能会引起误解。 如果被testing行的每一列NOT NULLNOT 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字段上有一个索引。