Postgres 中的时间序列查询

Posted

技术标签:

【中文标题】Postgres 中的时间序列查询【英文标题】:Time series querying in Postgres 【发布时间】:2013-10-26 20:47:34 【问题描述】:

这是@Erwin 对Efficient time series querying in Postgres 的回答中的后续问题。

为了简单起见,我将使用与该问题相同的表结构

id | widget_id | for_date | score |

最初的问题是获取某个范围内每个日期的每个小部件的分数。如果某个日期没有小部件条目,则显示该小部件上一个条目的分数。如果所有数据都包含在您查询的范围内,则使用交叉连接和窗口函数的解决方案效果很好。我的问题是我想要以前的分数,即使它超出了我们正在查看的日期范围。

示例数据:

INSERT INTO score (id, widget_id, for_date, score) values
(1, 1337, '2012-04-07', 52),
(2, 2222, '2012-05-05', 99),
(3, 1337, '2012-05-07', 112),
(4, 2222, '2012-05-07', 101);

当我查询 2012 年 5 月 5 日至 5 月 10 日的范围(即generate_series('2012-05-05'::date, '2012-05-10'::date, '1d'))时,我想得到以下信息:

DAY          WIDGET_ID  SCORE
May, 05 2012    1337    52
May, 05 2012    2222    99
May, 06 2012    1337    52
May, 06 2012    2222    99
May, 07 2012    1337    112
May, 07 2012    2222    101
May, 08 2012    1337    112
May, 08 2012    2222    101
May, 09 2012    1337    112
May, 09 2012    2222    101
May, 10 2012    1337    112
May, 10 2012    2222    101

目前最好的解决方案(也是@Erwin)是:

SELECT a.day, a.widget_id, s.score
FROM  (
   SELECT d.day, w.widget_id
         ,max(s.for_date) OVER (PARTITION BY w.widget_id ORDER BY d.day) AS effective_date
   FROM  (SELECT generate_series('2012-05-05'::date, '2012-05-10'::date, '1d')::date AS day) d
   CROSS  JOIN (SELECT DISTINCT widget_id FROM score) AS w
   LEFT   JOIN score s ON s.for_date = d.day AND s.widget_id = w.widget_id
   ) a
LEFT JOIN  score s ON s.for_date = a.effective_date AND s.widget_id = a.widget_id
ORDER BY a.day, a.widget_id;

但正如您在 SQL Fiddle 中看到的那样,它在前两天为小部件 1337 生成空分数。我想看看之前第 1 行的 52 分。

是否有可能以有效的方式做到这一点?

【问题讨论】:

【参考方案1】:

@Roman mentioned、DISTINCT ON 可以解决这个问题。此相关答案中的详细信息:

Select first row in each GROUP BY group?

不过,子查询通常比 CTE 快一点:

SELECT DISTINCT ON (d.day, w.widget_id)
       d.day, w.widget_id, s.score
FROM   generate_series('2012-05-05'::date, '2012-05-10'::date, '1d') d(day)
CROSS  JOIN (SELECT DISTINCT widget_id FROM score) AS w
LEFT   JOIN score s ON s.widget_id = w.widget_id AND s.for_date <= d.day
ORDER  BY d.day, w.widget_id, s.for_date DESC;

您可以使用集合返回函数,例如FROM 列表中的表格。

SQL Fiddle

一个multicolumn index应该是性能的关键:

CREATE INDEX score_multi_idx ON score (widget_id, for_date, score)

仅包含第三列score 以使其成为covering index in Postgres 9.2 or later。您不会在早期版本中包含它。

当然,如果您有很多小部件和广泛的日期,CROSS JOIN 会生成很多行,这些行都有价格标签。只选择您实际需要的小部件和日期。

【讨论】:

这可行,但随着行数的增加似乎真的变慢了。我有 40-50k 行,需要 2 多分钟才能完成。是交叉连接中的记录数变慢了吗? @bpaul 你的桌子上有索引吗? @bpaul:特别是(可能覆盖)多列索引。我添加了一些细节。 @RomanPekar, @Erwin 目前我分别对 widget_id 和 for_date 进行索引。我将添加多列索引并报告回来。我在 Postgres 9.1.10 所以我会做widget_id, for_date 多列索引没有多大帮助。我现在正在为聚合表中更大的查询缓存值【参考方案2】:

就像你写的那样,你应该找到匹配的分数,但如果有差距 - 用最接近的较早分数填补它。在 SQL 中它将是:

SELECT d.day, w.widget_id, 
  coalesce(s.score, (select s2.score from score s2
    where s2.for_date<d.day and s2.widget_id=w.widget_id order by s2.for_date desc limit 1)) as score
from (select distinct widget_id FROM score) AS w
cross join (SELECT generate_series('2012-05-05'::date, '2012-05-10'::date, '1d')::date AS day) d
left join score s ON (s.for_date = d.day AND s.widget_id = w.widget_id)
order by d.day, w.widget_id;

在这种情况下,Coalesce 的意思是“如果有差距”。

【讨论】:

非常好的解决方案,谢谢,这似乎是迄今为止大型数据集最快的解决方案【参考方案3】:

您可以在 PostgreSQL 中使用distinct on 语法

with cte_d as (
    select generate_series('2012-05-05'::date, '2012-05-10'::date, '1d')::date as day
), cte_w as (
    select distinct widget_id from score
)
select distinct on (d.day, w.widget_id)
    d.day, w.widget_id, s.score
from cte_d as d
    cross join cte_w as w
    left outer join score as s on s.widget_id = w.widget_id and s.for_date <= d.day
order by d.day, w.widget_id, s.for_date desc;

或通过子查询获取最大日期:

with cte_d as (
    select generate_series('2012-05-05'::date, '2012-05-10'::date, '1d')::date as day
), cte_w as (
    select distinct widget_id from score
)
select
    d.day, w.widget_id, s.score
from cte_d as d
    cross join cte_w as w
    left outer join score as s on s.widget_id = w.widget_id
where
    exists (
        select 1
        from score as tt
        where tt.widget_id = w.widget_id and tt.for_date <= d.day
        having max(tt.for_date) = s.for_date
    )
order by d.day, w.widget_id;

性能实际上取决于您在表上的索引(如果可能,唯一的widget_id, for_date)。我认为,如果每个 widget_id 有很多行,那么第二个会更有效,但您必须在数据上对其进行测试。

>> sql fiddle demo

【讨论】:

感谢您的回答。选择 distinct 似乎是要走的路,但我认为@Erwins 解决方案更清洁、更高效。

以上是关于Postgres 中的时间序列查询的主要内容,如果未能解决你的问题,请参考以下文章

Postgres 中的慢查询优化

postgres中的动态sql查询

FROM 子句中的 Postgres 子查询

如何对具有纪元值的列进行 JOIN 查询,忽略 postgres 中的时间部分

Postgres 中的动态 UNION ALL 查询

Postgres:计算子查询中的唯一数组条目