我有一张表可以跟踪某些商店和产品的库存变化情况。该值是绝对库存,但我们只在库存发生变化时插入新行。这种设计是为了保持桌面小,因为它预计会迅速增长。
这是一个示例模式和一些测试数据:
CREATE TABLE stocks (
id serial NOT NULL,
store_id integer NOT NULL,
product_id integer NOT NULL,
date date NOT NULL,
value integer NOT NULL,
CONSTRAINT stocks_pkey PRIMARY KEY (id),
CONSTRAINT stocks_store_id_product_id_date_key
UNIQUE (store_id, product_id, date)
);
insert into stocks(store_id, product_id, date, value) values
(1,10,'2013-01-05', 4),
(1,10,'2013-01-09', 7),
(1,10,'2013-01-11', 5),
(1,11,'2013-01-05', 8),
(2,10,'2013-01-04', 12),
(2,11,'2012-12-04', 23);
我需要能够确定每个产品和商店的开始日期和结束日期之间的平均库存,但我的问题是,简单的avg()没有考虑到库存在两者之间保持不变变化。
我想要的是这样的:
select s.store_id, s.product_id , special_avg(s.value)
from stocks s where s.date between '2013-01-01' and '2013-01-15'
group by s.store_id, s.product_id
结果是这样的:
store_id product_id avg
1 10 3.6666666667
1 11 5.8666666667
2 10 9.6
2 11 23
为了使用SQL平均函数,我需要"传播"及时转发store_id和product_id的先前值,直到发生新的更改。任何想法如何实现这一目标?
答案 0 :(得分:3)
此任务的特殊困难:您不能只选择时间范围内的数据点,而必须在之前考虑最新的数据点 >时间范围和最早的数据点之后时间范围。这对于每一行都是不同的,并且每个数据点可能存在也可能不存在。需要复杂的查询并且难以使用索引。
您可以使用range types和operators(Postgres 9.2 + )来简化计算:
WITH input(a,b) AS (SELECT '2013-01-01'::date -- your time frame here
, '2013-01-15'::date) -- inclusive borders
SELECT store_id, product_id
, sum(upper(days) - lower(days)) AS days_in_range
, round(sum(value * (upper(days) - lower(days)))::numeric
/ (SELECT b-a+1 FROM input), 2) AS your_result
, round(sum(value * (upper(days) - lower(days)))::numeric
/ sum(upper(days) - lower(days)), 2) AS my_result
FROM (
SELECT store_id, product_id, value, s.day_range * x.day_range AS days
FROM (
SELECT store_id, product_id, value
, daterange (day, lead(day, 1, now()::date)
OVER (PARTITION BY store_id, product_id ORDER BY day)) AS day_range
FROM stock
) s
JOIN (
SELECT daterange(a, b+1) AS day_range
FROM input
) x ON s.day_range && x.day_range
) sub
GROUP BY 1,2
ORDER BY 1,2;
注意,我使用列名day
而不是date
。我从不使用基本类型名称作为列名。
在子查询sub
中,我使用窗口函数lead()
从每个项目的下一行获取日期,使用内置选项提供"今天"默认情况下没有下一行。
有了这个,我形成一个daterange
并使用重叠运算符&&
将其与输入匹配,使用交集运算符*
计算结果日期范围强>
此处的所有范围均为独占上边框。这就是为什么我在输入范围内添加一天的原因。这样,我们只需从lower(range)
中减去upper(range)
即可获得天数。
我认为"昨天"是有可靠数据的最新日子。 "今天"仍然可以改变现实生活中的应用程序。因此,我今天使用"" (now()::date
)作为开放范围的独占上边界。
我提供了两个结果:
your_result
同意您显示的结果。
您无条件地除以日期范围内的天数。例如,如果某个项目仅在最后一天列出,则会得到一个非常低的(误导!)"平均值"。
my_result
计算相同或更高的数字
我除以列出项目的实际天数。例如,如果某个项目仅列在最后一天,我会将列出的值作为平均值返回。
为了弄清楚差异,我添加了项目列出的天数:days_in_range
对于此类数据,旧行通常不会更改。这对于物化视图:
来说是一个很好的例子CREATE MATERIALIZED VIEW mv_stock AS
SELECT store_id, product_id, value
, daterange (day, lead(day, 1, now()::date) OVER (PARTITION BY store_id, product_id
ORDER BY day)) AS day_range
FROM stock;
然后您可以添加GiST index which supports the relevant operator &&
:
CREATE INDEX mv_stock_range_idx ON mv_stock USING gist (day_range);
我用20万行进行了更现实的测试。使用MV的查询速度大约是后者的6倍,而后者的速度是@Joop查询的~10倍。性能严重依赖于数据分布。 MV可以帮助大多数大表和高频率的条目。此外,如果表具有与此查询无关的列,则MV可以更小。成本与收益的问题。
我已将迄今为止发布的所有解决方案(以及改编后的内容)放在一个小提琴中:
SQL Fiddle with big test case.
SQL Fiddle with only 40k rows - 避免在sqlfiddle.com上超时
答案 1 :(得分:3)
这是相当快速和肮脏的:只需加入日历表并将它们全部加起来,而不是进行讨厌的区间运算。
WITH calendar(zdate) AS ( SELECT generate_series('2013-01-01'::date, '2013-01-15'::date, '1 day'::interval)::date )
SELECT st.store_id,st.product_id
, SUM(st.zvalue) AS sval
, COUNT(*) AS nval
, (SUM(st.zvalue)::decimal(8,2) / COUNT(*) )::decimal(8,2) AS wval
FROM calendar
JOIN stocks st ON calendar.zdate >= st.zdate
AND NOT EXISTS ( -- this calendar entry belongs to the next stocks entry
SELECT * FROM stocks nx
WHERE nx.store_id = st.store_id AND nx.product_id = st.product_id
AND nx.zdate > st.zdate AND nx.zdate <= calendar.zdate
)
GROUP BY st.store_id,st.product_id
ORDER BY st.store_id,st.product_id
;
答案 2 :(得分:0)
这个答案是基于这样一个隐含的想法,即你在寻找平均天数,所以每一天都算作一个新的行。虽然这可以在行形式的其他SQL引擎中处理,但通过将平均值(Sum(值)/ count(值))拆分并将其外推到该值的天数,可以更容易地实现这一点。使用您的表格格式和这个目标,我提出了这个解决方案(SQLFiddle)
select store_id, product_id, CASE WHEN sum(nextdate-date) > 0 THEN sum(Value*(nextdate-date)) / sum(nextdate-date) END as Avg_Value
from (
select *
, (
select value
from stocks b
where a.store_id = b.store_id
and a.product_id = b.product_id
and a.date >= b.date
order by b.date
limit 1
)*1.0 "value"
, coalesce((
select date
from stocks b
where a.store_id = b.store_id
and a.product_id = b.product_id
and a.date < b.date
order by b.date
limit 1
),case when current_date > '2013-01-12' then '2013-01-12' else current_date end) nextdate
from (
select store_id, product_id, min(case when date < '2013-01-07' then '2013-01-07' else date end) date
from stocks z
where date < '2013-01-12'
group by store_id, product_id
) a
union all
select store_id, product_id, date, value*1.0 "value"
, coalesce((
select date
from stocks b
where a.store_id = b.store_id
and a.product_id = b.product_id
and a.date < b.date
order by b.date
limit 1
),case when current_date > '2013-01-12' then '2013-01-12' else current_date end) nextdate
from stocks a
where a.date between '2013-01-07' and '2013-01-12'
) t
group by store_id, product_id
;
查询在start参数('2013-01-07'
)之前首次出现每个商店/产品,如果它大于表的记录日期,则将参数作为日期交换,选择该早期条目的值,以及start参数之后表中第一次更改的日期,并将下一个日期限制为end参数('2013-01-12'
)。联合查询的第二部分抓取两个参数之间的所有更改,以及由end参数约束的下一个更改或当前日期。最后,对结果进行计算,其中值乘以求和时的日期差异除以日期之间的天数之和。由于所有日期都在查询中受到约束,因此平均值将是作为参数传入的确切窗口的平均值。
在PostgreSQL上没有那么多,我建议如果你计划在一个函数中实现它,复制这个查询并用你的起始参数名替换'2013-01-07'
的所有实例,以及所有实例{{1}使用您的结束参数名称将为您提供您在任何给定日期窗口中查找的结果。
编辑:如果您想要一个不同时间单位的平均值,只需将'2013-01-12'
的两个实例替换为您正在寻找的任何日期间隔计算。 nextdate-date
返回两者之间的天数。