在 Postgres 中聚合多个字段时填写缺失的行
Fill in missing rows when aggregating over multiple fields in Postgres
我每天使用 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-17
和 [=16 之间=] 给出如下内容:
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
任何 NULL
在缺失行中计数为 0
我遇到的问题是,这种方法似乎无法在聚合数据中重复日期,因为我不仅对多个日期进行分组,而且对多个产品进行分组。
它 感觉 就像我应该能够在这里用 window 函数做一些巧妙的事情来解决这个问题,例如加入由产品名称定义的分区的完整日期范围 - 但我看不到真正让它工作的方法。
您可以使用:
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;
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 汇总一组产品的销售额,不仅需要知道销售何时发生,还需要知道何时不进行进一步处理。
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-17
和 [=16 之间=] 给出如下内容:
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
任何NULL
在缺失行中计数为 0
我遇到的问题是,这种方法似乎无法在聚合数据中重复日期,因为我不仅对多个日期进行分组,而且对多个产品进行分组。
它 感觉 就像我应该能够在这里用 window 函数做一些巧妙的事情来解决这个问题,例如加入由产品名称定义的分区的完整日期范围 - 但我看不到真正让它工作的方法。
您可以使用:
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;
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