平均库存历史表

时间:2014-08-11 16:22:47

标签: sql postgresql average date-range window-functions

我有一张表可以跟踪某些商店和产品的库存变化情况。该值是绝对库存,但我们只在库存发生变化时插入新行。这种设计是为了保持桌面小,因为它预计会迅速增长。

这是一个示例模式和一些测试数据:

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的先前值,直到发生新的更改。任何想法如何实现这一目标?

3 个答案:

答案 0 :(得分:3)

此任务的特殊困难:您不能只选择时间范围内的数据点,而必须在之前考虑最新的数据点 >时间范围和最早的数据点之后时间范围。这对于每一行都是不同的,并且每个数据点可能存在也可能不存在。需要复杂的查询并且难以使用索引。

您可以使用range typesoperators(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

SQL Fiddle.

指数和表现

对于此类数据,旧行通常不会更改。这对于物化视图

来说是一个很好的例子
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返回两者之间的天数。