【问题标题】:Selecting sum and running balance for last 18 months with generate_series使用 generate_series 选择过去 18 个月的总和和运行余额
【发布时间】:2015-02-24 12:42:35
【问题描述】:

我有这个工作查询,但我需要将所有月份添加到我的结果中,无论该月内售出的商品是否:

select * from (
select
to_char(max(change_date), 'YYYY-MON')::varchar(8) as yyyymmm,
max(change_date) as yearmonth,
sum(vic.sold_qty / item_size.qty)::numeric(18,2) as sold_qty,  -- sold monthly
sum(sum(on_hand)) OVER (PARTITION BY vic.item_id order by year,month) as on_hand --running balance
from (((view_item_change vic
left join item on vic.item_id = item.item_id)
left join item_size on item_size.item_id = vic.item_id and item_size.name = item.sell_size)
left join item_plu on vic.item_id = item_plu.item_id and item_plu.seq_num = 0)
where 1 = 1   -- cannot limit date here as its used to show running balance.
and vic.change_date < current_date - date_part('day',current_date)::integer --show only till end of last month
and item.item_id = (select item_id from item_plu where number = '51515')
group by vic.item_id, year, month
) as t 
where yearmonth > current_date - date_part('day',current_date)::integer - 540 -- 18 months

这给了我这样的东西:

"2013-JUN";"2013-06-29";0.00;7.0000
"2013-JUL";"2013-07-22";0.00;6.0000
"2013-AUG";"2013-08-28";2.00;4.0000
"2013-SEP";"2013-09-02";0.00;4.0000
"2013-OCT";"2013-10-28";0.00;4.0000
"2013-NOV";"2013-11-15";0.00;4.0000
"2013-DEC";"2013-12-16";0.00;6.0000
"2014-FEB";"2014-02-10";1.00;5.0000
"2014-APR";"2014-04-09";0.00;5.0000

但我还想显示月份 2014-JAN2014-MAR 以便我的图表更好地按时间缩放。

我知道如何执行generate_series(start_date, end_date, '1 month') 间隔,但我不太明白如何将这个系列加入到上面的结果集中。

我接受了第一个答案,但经过 2 周的测试,发现了一个问题。左加入系列,然后添加一个合并以显示 0 而不是空月份的空值,这会导致运行平衡出现问题。

查询结果:

SELECT yyyymmm, yyyymmm, 
coalesce(sold_qty,0) sold_qty, coalesce(on_hand,0) on_hand
FROM  (
   SELECT date_trunc('month', month_series)::date as yyyymmm
   FROM   generate_series(current_date - date_part('day',current_date)::integer - 540  
   ,current_date- date_part('day',current_date)::integer
   , interval '1 month') month_series
   ) month_series
LEFT  JOIN (
    select * from (
        select
        date_trunc('month', max(change_date))::date as yyyymmm,
        max(change_date) as yearmonth,
        sum(vic.sold_qty / item_size.qty)::numeric(18,2) as sold_qty,
        sum(sum(on_hand)) OVER (PARTITION BY vic.item_id order by year,month) as on_hand
        from (((view_item_change vic
        left join item on vic.item_id = item.item_id)
        left join item_size on item_size.item_id = vic.item_id and item_size.name = item.sell_size)
        left join item_plu on vic.item_id = item_plu.item_id and item_plu.seq_num = 0)
        where 1 = 1   -- cannot limit date here as its used to show running balance.
        --vic.change_date >= current_date - date_part('day',current_date)::integer - 730  -- only get results for last 
        --show only till end of last month
        and vic.change_date <= current_date - date_part('day',current_date)::integer
        and item.item_id = (select item_id from item_plu where number = '19M7077')
        group by vic.item_id, year, month
    ) as a 
    where yyyymmm > current_date - date_part('day',current_date)::integer - 540 -- 18 months
) q USING (yyyymmm)
order by 1

我得到的结果:

"2013-07-01";"2013-07-01";0;0
"2013-08-01";"2013-08-01";0;0
"2013-09-01";"2013-09-01";1.00;53.0000
"2013-10-01";"2013-10-01";0;0
"2013-11-01";"2013-11-01";0;0
"2013-12-01";"2013-12-01";0.00;53.0000
"2014-01-01";"2014-01-01";0.00;52.0000
"2014-02-01";"2014-02-01";0;0
"2014-03-01";"2014-03-01";0;0
"2014-04-01";"2014-04-01";0;0

但我想要:

"2013-07-01";"2013-07-01";0;53.0000
"2013-08-01";"2013-08-01";0;53.0000
"2013-09-01";"2013-09-01";1.00;53.0000
"2013-10-01";"2013-10-01";0;53.0000
"2013-11-01";"2013-11-01";0;0;53.0000
"2013-12-01";"2013-12-01";0.00;53.0000
"2014-01-01";"2014-01-01";0.00;52.0000
"2014-02-01";"2014-02-01";0;0;52.0000
"2014-03-01";"2014-03-01";0;0;52.0000
"2014-04-01";"2014-04-01";0;0;52.0000

表定义

CREATE TABLE item
(
  item_id character(22) NOT NULL,
  version integer NOT NULL,
  created_by character varying(16) NOT NULL,
  updated_by character varying(16),
  inactive_by character varying(16),
  created_on date NOT NULL,
  updated_on date,
  inactive_on date,
  external_id numeric(14,0),
  description character varying(40) NOT NULL,
  dept_id character(22),
  subdept_id character(22),
  sell_size character varying(8) NOT NULL,
  purch_size character varying(8) NOT NULL
);
CREATE TABLE item_change
(
  item_id character(22) NOT NULL,
  size_name character varying(8) NOT NULL,
  store_id character(22) NOT NULL,
  change_date date NOT NULL,
  on_hand numeric(18,4) NOT NULL,   -- sum column / item_id = total on_hand
  total_cost numeric(18,4) NOT NULL,
  on_order numeric(18,4) NOT NULL,
  sold_qty numeric(18,4) NOT NULL,
  sold_cost numeric(18,4) NOT NULL,
  sold_price numeric(18,4) NOT NULL,
  recv_qty numeric(18,4) NOT NULL,
  recv_cost numeric(18,4) NOT NULL,
  adj_qty numeric(18,4) NOT NULL,
  adj_cost numeric(18,4) NOT NULL
);

CREATE TABLE item_size
(
  item_id character(22) NOT NULL,
  seq_num integer NOT NULL,
  name character varying(8) NOT NULL,
  qty numeric(18,4) NOT NULL,
  weight numeric(18,4) NOT NULL,
  CONSTRAINT item_size_pkey PRIMARY KEY (item_id, seq_num),
  CONSTRAINT item_size_c0 FOREIGN KEY (item_id)
      REFERENCES item (item_id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE CASCADE,
  CONSTRAINT item_size_c1 UNIQUE (item_id, name)
);

CREATE TABLE item_plu
(
  item_id character(22) NOT NULL,
  seq_num integer NOT NULL,
  "number" character varying(18) NOT NULL,
  size_name character varying(8)
);


CREATE OR REPLACE VIEW view_item_change AS 
 SELECT date_part('year'::text, item_change.change_date) AS year,
date_part('month'::text, item_change.change_date) AS month,
date_part('week'::text, item_change.change_date) AS week,
date_part('quarter'::text, item_change.change_date) AS quarter,
date_part('dow'::text, item_change.change_date) AS dow,
item_change.item_id,
item_change.size_name,
item_change.store_id,
item_change.change_date,
item_change.on_hand,
item_change.total_cost,
item_change.on_order,
item_change.sold_qty,
item_change.sold_cost,
item_change.sold_price,
item_change.recv_qty,
item_change.recv_cost,
item_change.adj_qty,
item_change.adj_cost
FROM item_change;

每个月只有一行,因为我也按年、月进行分组。如您所见,该视图具有年、季度、月、周、日列,以便于报告。

如果需要,我可以为此获取工作数据,但需要手动创建。我已经简化了表格并省略了所有约束和一些列。

【问题讨论】:

  • GROUP BY item_id,但结果中没有item_id,而且每个月只有一行?这很奇怪。此外,yeardatechange_date 不符合牌桌条件,因此这是一场猜谜游戏。使用实际的表定义和示例数据来解决会简单得多。至少对查询中的所有列进行表限定,给我们一个战斗的机会。
  • 我没有看到item_size 的定义?您的 Postgres 版本?
  • 在上面添加了 item_size。 Postgres v 9.3。
  • 要么我们在结果中得到 多个 项:那么你应该在结果中包含item_id。或者我们只获取单个项的数据:那么您可以进一步简化查询。
  • 我只需要一个单项结果。这个当前的查询执行时间与我可以使用的不符。请注意,item_change 表有 540 万条记录。上一个查询在 32 毫秒内运行,而这一个需要 80 多秒且仍未完成。

标签: sql postgresql window-functions generate-series running-balance


【解决方案1】:

基本解决方案

生成完整的月份列表和LEFT JOIN 其余月份:

SELECT *
FROM  (
   SELECT to_char(m, 'YYYY-MON') AS yyyymmm
   FROM   generate_series(<start_date>, <end_date>, interval '1 month') m
   ) m
LEFT  JOIN ( <your query here> ) q USING (yyyymmm);

更多解释的相关答案:

为您的案例提供高级解决方案

您的查询比我最初理解的要复杂。您需要所选项目的所有行的运行总和,然后您想要修剪早于最小日期的行,并用上个月的预先计算的总和填充缺失的月份。 p>

我现在通过LEFT JOIN LATERAL 实现了这一点。

SELECT COALESCE(m.yearmonth, c.yearmonth)::date, sold_qty, on_hand
FROM  (
   SELECT yearmonth
        , COALESCE(sold_qty, 0) AS sold_qty
        , sum(on_hand_mon) OVER (ORDER BY yearmonth) AS on_hand
        , lead(yearmonth)  OVER (ORDER BY yearmonth)
                                - interval '1 month' AS nextmonth
   FROM (
      SELECT date_trunc('month', c.change_date) AS yearmonth
           , sum(c.sold_qty / s.qty)::numeric(18,2) AS sold_qty
           , sum(c.on_hand) AS on_hand_mon
      FROM   item_change      c         
      LEFT   JOIN item        i USING (item_id)
      LEFT   JOIN item_size   s ON s.item_id = i.item_id AND s.name = i.sell_size
      LEFT   JOIN item_plu    p ON p.item_id = i.item_id AND p.seq_num = 0
      WHERE  c.change_date < date_trunc('month', now()) - interval '1 day'
      AND    c.item_id = (SELECT item_id FROM item_plu WHERE number = '51515')
      GROUP  BY 1
      ) sub
   ) c
LEFT   JOIN LATERAL generate_series(c.yearmonth
                                  , c.nextmonth
                                  , interval '1 month') m(yearmonth) ON TRUE
WHERE  c.yearmonth > date_trunc('year', now()) - interval '540 days'
ORDER  BY COALESCE(m.yearmonth, c.yearmonth);

SQL Fiddle 带有最少的测试用例。

要点:

  • 我从查询中完全删除了您的视图。多花一分钱。

  • 既然你选择了item_id,你就不需要GROUP BY item_idPARTITION BY item_id

  • 使用简短的表格别名并明确所有引用 - 特别是在公共论坛上发帖时。

  • 联接中的括号只是杂音。默认情况下,连接无论如何都是从左到右执行的。

  • 简化的日期范围(因为我使用时间戳进行操作):

    date_trunc('year', current_date)  - interval '540 days'
    date_trunc('month', current_date) - interval '1 day'
    

    等效,但比以下更简单、更快:

    current_date - date_part('day',current_date)::integer - 540
    current_date - date_part('day',current_date)::integer
  • 我现在用每行generate_series() 调用来填充所有计算后缺失的月份。

  • 它必须是LEFT JOIN LATERAL ... ON TRUE,而不是JOIN LATERAL 的缩写形式,才能捕捉最后一行的角落。详细解释:

重要的旁注:

character(22) 是主键(或任何列)的可怕数据类型。详情:

理想情况下,这将是 intbigint 列,或者可能是 UUID

此外,将货币金额存储为 money 类型或 integer(代表 Cents)的整体性能要好得多。

从长远来看,性能必然会下降,因为您必须从一开始就将所有行都包含在计算中。你应该剪掉旧的行,并按年实现on_hold 的余额。

【讨论】:

  • 我上周接受了答案,但现在我发现它不正确。如果缺少一个月,则运行余额应保持以前的记录。使用左连接,然后将合并添加到总和中,我得到的运行余额为 0,但这是不正确的。有什么建议,或者我应该使用 generate_series 作为基础重写查询吗?
  • @markdueck:我添加了一个高级解决方案。基本上重写了您的查询以解开它。
  • - 非常感谢您的帮助,但正如前面评论中提到的,此查询的时间太长,并且它没有返回正确的起始运行余额 - 这意味着它开始从开始日期开始的流动余额,不包括所有以前的on_hand的总和。我正在考虑更改为运行这样的内容:select (select sum(sold_qty) from item_change ...) as sold_qty, (select sum(on_hand) from item_change where clause .... ) as OH from (series ) -- where 子句将包括该系列的日期。
  • @markdueck:我被愚弄了,以为您实际上想通过您的GROUP BY item_id 返回多个item_id。另外,我有一个错误:子选择不能以这种方式合并到连接条件中。添加了一个大大简化的版本,带有更新说明和小提琴。
  • 你好。不幸的是,上面的查询因为日期限制太早而不起作用,所以运行余额仍然是错误的。我调整了工作查询并将其放在另一个选择中,从中我限制了日期here. 我不确定是否应该将原始帖子更改为并包含正确答案,或者您想这样做?谢谢谢谢。非常感谢您的帮助!
猜你喜欢
  • 1970-01-01
  • 2015-08-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-09-16
相关资源
最近更新 更多