在 Postgres 中聚合多个字段时填写缺失的行

Posted

技术标签:

【中文标题】在 Postgres 中聚合多个字段时填写缺失的行【英文标题】:Fill in missing rows when aggregating over multiple fields in Postgres 【发布时间】:2017-08-17 14:35:39 【问题描述】:

我每天使用 Postgres 汇总一组产品的销售额,我不仅需要知道何时发生销售,还需要知道何时不进行进一步处理。

SELECT 
sd.date, 
COUNT(sd.sale_id) AS sales, 
sd.product

FROM sales_data sd
-- sales per product, per day
GROUP BY sd.product, sd.date
ORDER BY sd.product, sd.date

这会产生以下内容:

    date    | sales |       product                           
------------+-------+-------------------
 2017-08-17 |  10   | soap
 2017-08-19 |   2   | soap
 2017-08-20 |   5   | soap
 2017-08-17 |   2   | shower gel
 2017-08-21 |   1   | shower gel

如您所见 - 每个产品的日期范围并不连续,因为 sales_data 在某些日子里没有包含这些产品的任何信息。

我的目标是为某个范围内任何一天未售出的每个产品添加一个 sales = 0 行 - 例如在这里,在 2017-08-172017-08-21 之间给出类似以下:

    date    | sales |      product                           
------------+-------+-------------------
 2017-08-17 |  10   | soap
 2017-08-18 |   0   | soap
 2017-08-19 |   2   | soap
 2017-08-20 |   5   | soap
 2017-08-21 |   0   | soap
 2017-08-17 |   2   | shower gel
 2017-08-18 |   0   | shower gel
 2017-08-19 |   0   | shower gel
 2017-08-20 |   0   | shower gel
 2017-08-21 |   1   | shower gel

在只有一个产品的更简单的情况下,解决方案似乎是使用generate_series(),即:

使用 generate_series 创建完整的日期范围 LEFT JOIN 已经汇总到日期系列的销售数据 COALESCE any NULL 在缺失的行中计数为 0

我遇到的问题是,这种方法似乎无法在聚合数据中重复工作日期,因为我不仅对多个日期进行分组,而且还对多个产品进行分组。

感觉我应该能够在这里用窗口函数做一些狡猾的事情来解决这个问题,例如加入产品名称定义的分区的完整日期范围 - 但我看不到实际让它工作的方法。

【问题讨论】:

【参考方案1】:

你可以使用:

WITH cte AS (
   SELECT date, s.product
   FROM  ... -- some way to generate date series
   CROSS JOIN (SELECT DISTINCT product FROM sales_data) s
)
SELECT 
    c.date,
    c.product,
    COUNT(sd.sale_id) AS sales
FROM cte c
LEFT JOIN sales_data sd
  ON c.date = sd.date AND c.product= sd.product
GROUP BY c.date, c.product
ORDER BY c.date, c.product;

首先创建日期和产品的笛卡尔积,然后LEFT JOIN 对实际数据进行计算。


Oracle 为这种场景提供了强大的功能,称为 Partitioned Outer Joins

SELECT times.time_id, product, quantity 
FROM inventory  PARTITION BY  (product) 
RIGHT OUTER JOIN times ON (times.time_id = inventory.time_id) 
WHERE times.time_id BETWEEN TO_DATE('01/04/01', 'DD/MM/YY') 
      AND TO_DATE('06/04/01', 'DD/MM/YY') 
ORDER BY  2,1; 

【讨论】:

完美 - 完全按照需要工作,谢谢。另外 - Oracle 版本确实看起来很棒 这对我遇到的类似问题非常有帮助。谢谢!【参考方案2】:
select 
    date, 
    count(sale_id) as sales, 
    product
from
    sales_data
    right join (
        (
            select d::date as date
            from generate_series (
                (select min(date) from sales_data),
                (select max(date) from sales_data),
                '1 day'
            ) gs (d)
        ) gs
        cross join
        (select distinct product from sales_data) p
    ) cj using (product, date)
group by product, date
order by product, date

【讨论】:

以上是关于在 Postgres 中聚合多个字段时填写缺失的行的主要内容,如果未能解决你的问题,请参考以下文章

使用 Activerecord、Rails 和 Postgres 查找具有多个重复字段的行

Postgres - 如何返回缺失数据计数为 0 的行?

按日期和组聚合并在大查询中填写缺失的日期

使用带有多个键的 Grouper 时填写缺失的日期

转发新行填写缺失日期的帐户

在 Postgres 中的数组字段上应用聚合函数?