滚动总和/计数/日期间隔内的平均值


20

在一个涵盖18个月内成千上万个实体的交易的数据库中,我想运行一个查询,以将每个可能的30天期限entity_id与该30天内的交易金额和COUNT 个交易的总和进行分组。以我可以查询的方式返回数据。经过大量测试,此代码完成了我想要的大部分工作:

SELECT id, trans_ref_no, amount, trans_date, entity_id,
    SUM(amount) OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_total,
    COUNT(id)   OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_count
  FROM transactiondb;

我将在更大的查询中使用类似以下内容的结构:

SELECT * FROM (
  SELECT id, trans_ref_no, amount, trans_date, entity_id,
      SUM(amount) OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_total,
      COUNT(id)   OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_count
    FROM transactiondb ) q
WHERE trans_count >= 4
AND trans_total >= 50000;

该查询不涉及的情况是交易计数跨越多个月,但彼此之间的间隔仍在30天内。Postgres是否可以进行此类查询?如果是这样,我欢迎任何投入。其他许多主题都在讨论“ 运行 ”聚合,而不是滚动聚合。

更新资料

CREATE TABLE脚本:

CREATE TABLE transactiondb (
    id integer NOT NULL,
    trans_ref_no character varying(255),
    amount numeric(18,2),
    trans_date date,
    entity_id integer
);

示例数据可以在这里找到。我正在运行PostgreSQL 9.1.16。

理想的输出将包括连续30天的所有交易中的SUM(amount)COUNT()。参见此图像,例如:

理想情况下将包含在“集合”中但并非因为我的集合按月静态的行的示例。

绿色日期突出显示表示查询中包含的内容。黄色行突出显示表示我想成为集合的一部分的记录。

以前的阅读:


1
通过every possible 30-day period by entity_id你的意思期间可以开始任何一天,在(非闰年)年365期可能?或者,您是否只想将发生实际交易的天数视作任何一个期间的开始entity_id ?无论哪种方式,请提供表定义,Postgres版本,一些示例数据和示例的预期结果。
Erwin Brandstetter 2015年

从理论上讲,我指的是任何一天,但实际上,没有必要考虑没有交易的日子。我已经发布了示例数据和表定义。
tufelkinder

因此,您希望在每笔实际交易开始entity_id的30天窗口中累积相同的行。可以有多个交易是相同的,还是该组合定义为唯一的?您的表定义没有或PK约束,但约束似乎丢失了(trans_date, entity_id)UNIQUE
Erwin Brandstetter

唯一的约束是在id主键上。每个实体每天可能有多个交易。
tufelkinder

关于数据分发:大多数天是否有条目(每个entity_id)?
Erwin Brandstetter,2015年

Answers:


26

您拥有的查询

您可以使用WINDOW子句简化查询,但这只是缩短语法,而不更改查询计划。

SELECT id, trans_ref_no, amount, trans_date, entity_id
     , SUM(amount) OVER w AS trans_total
     , COUNT(*)    OVER w AS trans_count
FROM   transactiondb
WINDOW w AS (PARTITION BY entity_id, date_trunc('month',trans_date)
             ORDER BY trans_date
             ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING);
  • 还使用稍微快一点count(*),既然id确定了NOT NULL
  • 而且您不需要,ORDER BY entity_id因为您已经PARTITION BY entity_id

但是,您可以进一步简化:
根本不添加ORDER BY窗口定义,它与您的查询无关。然后,您无需定义自定义窗口框架:

SELECT id, trans_ref_no, amount, trans_date, entity_id
     , SUM(amount) OVER w AS trans_total
     , COUNT(*)    OVER w AS trans_count
FROM   transactiondb
WINDOW w AS (PARTITION BY entity_id, date_trunc('month',trans_date);

简单,快速,但仍然只是一个更好的版本,你什么都有,用静态个月。

您可能想要的查询

...没有明确定义,因此我将基于以下假设:

在任何交易的第一笔和最后一笔交易中,每30天计算一次交易和金额entity_id。排除没有活动的前期和尾期,但包括那些外部范围内的所有可能的30天时段。

SELECT entity_id, trans_date
     , COALESCE(sum(daily_amount) OVER w, 0) AS trans_total
     , COALESCE(sum(daily_count)  OVER w, 0) AS trans_count
FROM  (
   SELECT entity_id
        , generate_series (min(trans_date)::timestamp
                         , GREATEST(min(trans_date), max(trans_date) - 29)::timestamp
                         , interval '1 day')::date AS trans_date
   FROM   transactiondb 
   GROUP  BY 1
   ) x
LEFT JOIN (
   SELECT entity_id, trans_date
        , sum(amount) AS daily_amount, count(*) AS daily_count
   FROM   transactiondb
   GROUP  BY 1, 2
   ) t USING (entity_id, trans_date)
WINDOW w AS (PARTITION BY entity_id ORDER BY trans_date
             ROWS BETWEEN CURRENT ROW AND 29 FOLLOWING);

这会列出每个30天的时段entity_id以及您的汇总,并将其trans_date作为该时段的第一天(含)。要获取每个单独行的值,请再次连接到基表...

基本困难与此处讨论的相同:

窗口的框架定义不能取决于当前行的值。

而是generate_series()timestamp输入调用:

您实际想要的查询

在问题更新和讨论之后:从每笔实际交易开始,在30天的窗口中
累积相同的行entity_id

由于您的数据分布稀疏,因此使用范围条件运行LATERAL联接应该更加有效,因为Postgres 9.1还没有联接:

SELECT t0.id, t0.amount, t0.trans_date, t0.entity_id
     , sum(t1.amount) AS trans_total, count(*) AS trans_count
FROM   transactiondb t0
JOIN   transactiondb t1 USING (entity_id)
WHERE  t1.trans_date >= t0.trans_date
AND    t1.trans_date <  t0.trans_date + 30  -- exclude upper bound
-- AND    t0.entity_id = 114284  -- or pick a single entity ...
GROUP  BY t0.id  -- is PK!
ORDER  BY t0.trans_date, t0.id

SQL提琴。

在大多数情况下,滚动窗口仅对数据有意义(就性能而言)。

这确实不是对总重复(trans_date, entity_id)每一天,但当天的所有行始终包含在30天的窗口。

对于一张大桌子,这样的覆盖索引可能会有所帮助:

CREATE INDEX transactiondb_foo_idx
ON transactiondb (entity_id, trans_date, amount);

最后一栏amount,如果你仅索引扫描的出来才有用。否则放下。

但是无论如何选择整个表都不会使用它。它会支持查询一小部分。


这看起来真的很好,现在就对数据进行测试,并试图理解您的查询实际上正在做的所有事情……
tufelkinder 2015年

@tufelkinder:为更新的问题添加了解决方案。
Erwin Brandstetter

现在进行审查。我很感兴趣它可以在SQL Fiddle中运行...当我尝试直接在我的transactiondb上运行它时,它会出错column "t0.amount" must appear in the GROUP BY clause...
tufelkinder 2015年

@tufelkinder:我将测试用例减少到100行。sqlfiddle限制了测试数据的大小。杰克(作者)在几个月前降低了限制限制,因此该站点不容易停顿。
Erwin Brandstetter

1
很抱歉延迟,需要在完整的数据库上对其进行测试。一如既往,您的回答是极好的深度和教育意义。谢谢!
tufelkinder
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.