sql – 优化GROUP BY查询以检索每个用户的最新记录

Postgres 9.2中有下表(简化表)

CREATE TABLE user_msg_log (
    aggr_date DATE,
    user_id INTEGER,
    running_total INTEGER
);

它每个用户和每天最多包含一条记录。每天将有大约500K条记录300天。对于每个用户,running_total总是在增加。

我想在特定日期之前有效地检索每个用户的最新记录。我的查询是:

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)上有一个索引,但没有什么帮助。
有什么其他索引我应该用来加速这个,还是以其他方式实现我想要的?

为获得最佳性能,您需要一个multicolumn index

CREATE INDEX user_msg_log_combo_idx
ON user_msg_log (user_id, aggr_date DESC NULLS LAST)

为了使index only scans成为可能,添加否则不需要的列run​​ning_total:

CREATE INDEX user_msg_log_combo_covering_idx
ON user_msg_log (user_id, aggr_date DESC NULLS LAST, running_total)

为什么DESC NULLS LAST?

> Unused index in range of dates query

对于每个user_id的行几个简单的DISTINCT ON是最快的解决方案之一:

> Select first row in each GROUP BY group?

对于每个user_id的许多行,一个loose index scan将(效率)更高。这在Postgres中是没有实现的(至少高达Postgres 9.6),但有一些方法可以效仿它:

没有独立的用户表

以下解决方案超出了Postgres Wiki所述。
使用单独的用户表,下面的解决方案通常更简单和更快。

1A。递交CTE与LATERAL加入

Common Table Expressions需要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。递归CTE与相关子查询

方便检索单个列或整行。该示例使用表的整行类型。其他变种是可能的。

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 > (c.u).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  c.u IS NOT NULL
   )
SELECT (u).*  -- decompose row now
FROM   cte
WHERE  u IS NOT NULL
ORDER  BY (u).user_id;  -- again with parentheses!

更多的说明在第2b章。下面。
相关答案:

> Query last N related rows per row
> GROUP BY one column, while sorting by another in PostgreSQL

用独立的用户表

只要我们每个相关的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项。您可以获得一个索引(仅用于每个用户查找)。

> What is the difference between LATERAL and a subquery in PostgreSQL?

考虑可能的改进,方法是排序users表suggested by @opensrc in another answer.如果用户表的物理排序顺序与user_msg_log上的索引匹配,则不需要。

即使您在user_msg_log中有条目,您也不会在users表中收到用户缺少的结果。通常情况下,您将有一个外键约束执行参照完整性来排除这一点。

对于user_msg_log中没有匹配条目的任何用户,您也不会得到一行。这符合你原来的问题。如果需要在结果中包含这些行,请使用LEFT JOIN LATERAL … ON true而不是CROSS JOIN LATERAL:

> Call a set-returning function with an array argument multiple times

此表单最适合每个用户检索多个行(但不是全部)。只需使用LIMIT n而不是LIMIT 1。

有效地,所有这些都会做同样的事情:

JOIN LATERAL ... ON true
CROSS JOIN LATERAL ...
, LATERAL ...

后者具有较低的优先级。显式JOIN在逗号之前绑定。

2B。相关子查询

从单个行中检索单个列的良好选择。代码示例:

> Optimize groupwise maximum query

对于多个列也是可以的,但是您需要更多的智能:

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:在外部查询中,您无法区分子查询是否找不到一行或返回的所有值恰好是NULL – 相同的结果。您必须在子查询中包含一个NOT NULL列,以确保。
相关的子查询只能返回一个值。您可以将多个列包装成复合类型。但是为了分解它,Postgres需要一种众所周知的复合类型。只有提供列定义列表才能分解匿名记录。
>使用已注册的类型,如现有表的行类型,或创建类型。使用CREATE TYPE显式(并永久)注册复合类型,或者创建临时表(在会话结束时自动删除),以临时提供行类型。转换为该类型:(aggr_date,running_total):: combo
>最后,我们不想在同一查询级别分解组合。由于查询计划器的弱点,这将为每列评估一次子查询。 (直到Postgres 9.6 – 为下一个版本计划进行改进)而是将其作为子查询并在外部查询中分解。

有关:

> Get values from first and last row per group

SQL Fiddle展示了四个查询。
对1k用户和100k日志条目进行了大量测试。

翻译自:https://stackoverflow.com/questions/25536422/optimize-group-by-query-to-retrieve-latest-record-per-user

转载注明原文:sql – 优化GROUP BY查询以检索每个用户的最新记录