Arrays PostgreSQL交叉表-可变列数

Arrays PostgreSQL交叉表-可变列数,arrays,postgresql,crosstab,Arrays,Postgresql,Crosstab,在向MS Access用户宣传学习徒手SQL的好处时,我遇到的一个常见问题是,以Access的方式创建交叉表查询的效果非常复杂。我意识到严格地说,在SQL中它不是这样工作的——在Access中它之所以可能是因为它处理数据的呈现 具体来说,当我有一个包含实体、日期和数量的表时,我们经常希望在一行中看到一个实体,日期表示为列: 这: 变成这样: Entity 1/1/16 2/1/16 3/1/16 ---------- ------ ------ ------ 2787

在向MS Access用户宣传学习徒手SQL的好处时,我遇到的一个常见问题是,以Access的方式创建交叉表查询的效果非常复杂。我意识到严格地说,在SQL中它不是这样工作的——在Access中它之所以可能是因为它处理数据的呈现

具体来说,当我有一个包含实体、日期和数量的表时,我们经常希望在一行中看到一个实体,日期表示为列:

这:

变成这样:

Entity      1/1/16   2/1/16   3/1/16
----------  ------   ------   ------
278700-002    5       11        1
278700-003            12
也就是说,我们处理这一问题的一般方式与此类似:

with vals as (
  select
    entity,
    case when order_date = '2016-01-01' then qty else 0 end as q16_01,
    case when order_date = '2016-02-01' then qty else 0 end as q16_02,
    case when order_date = '2016-03-01' then qty else 0 end as q16_02
  from mydata
)
select
  entity, sum (q16_01) as q16_01, sum (q16_02) as q16_02, sum (q16_03) as q16_03
from vals
group by entity
这完全是过于简单化了,但我相信大多数人都会明白我的意思

这方面的主要问题不是对列数的限制——数据通常是有界的,我可以使用固定数量的日期列进行到期——36个月,或者其他什么,这取决于数据的上下文。我的问题是,我必须每个月改变日期,以使这项工作顺利进行

我有一个想法,我可以利用数组,根据距离当前日期的月份,动态地将数量分配给数组的索引。通过这种方式,我的数据最终将如下所示:

Entity      Values
----------  ------
278700-002  {5,11,1}
278700-003  {0,12,0}
foreach my $ref (@data) {
  my ($entity, $month_offset, $qty) = @$ref;
  $values{$entity}->[$month_offset] += $qty;
}
WITH cte AS (
  WITH minmax AS (
    SELECT min(extract(month from order_date))::int,
           max(extract(month from order_date))::int
    FROM mytable
  )
  SELECT entity, mon, 0 AS qty
  FROM (SELECT DISTINCT entity FROM mytable) entities,
       (SELECT generate_series(min, max) AS mon FROM minmax) allmonths
  UNION
  SELECT entity, extract(month from order_date)::int, qty FROM mytable
)
SELECT entity, array_agg(sum) AS values
FROM (
  SELECT entity, mon, sum(qty) FROM cte
  GROUP BY 1, 2) sub
GROUP BY 1
ORDER BY 1;
这是可以接受的,因为我可以在我使用的任何渲染工具(例如Excel)中管理实际列的渲染

问题是我被卡住了。。。我如何从我的数据中获取这些信息。如果这是Perl,我将遍历数据并执行如下操作:

Entity      Values
----------  ------
278700-002  {5,11,1}
278700-003  {0,12,0}
foreach my $ref (@data) {
  my ($entity, $month_offset, $qty) = @$ref;
  $values{$entity}->[$month_offset] += $qty;
}
WITH cte AS (
  WITH minmax AS (
    SELECT min(extract(month from order_date))::int,
           max(extract(month from order_date))::int
    FROM mytable
  )
  SELECT entity, mon, 0 AS qty
  FROM (SELECT DISTINCT entity FROM mytable) entities,
       (SELECT generate_series(min, max) AS mon FROM minmax) allmonths
  UNION
  SELECT entity, extract(month from order_date)::int, qty FROM mytable
)
SELECT entity, array_agg(sum) AS values
FROM (
  SELECT entity, mon, sum(qty) FROM cte
  GROUP BY 1, 2) sub
GROUP BY 1
ORDER BY 1;
这不是Perl。。。到目前为止,这就是我所拥有的,而现在我正处于一种精神僵局

with offset as (
  select
    entity, order_date, qty,
    (extract (year from order_date ) - 2015) * 12 +
     extract (month from order_date ) - 9 as month_offset,
    array[]::integer[] as values
  from mydata
)
select
  prod_id, playgrd_dte, -- oh my...  how do I load into my array?
from fcst
“2015”和“9”并不是真正的硬编码——为了简单起见,我把它们放在这里


此外,如果我的方法或假设完全错误,我相信有人会纠正我。

就像所有可以想象和不可想象的事情一样,PostgreSQL有一种方法可以做到这一点。看起来是这样的:

Entity      Values
----------  ------
278700-002  {5,11,1}
278700-003  {0,12,0}
foreach my $ref (@data) {
  my ($entity, $month_offset, $qty) = @$ref;
  $values{$entity}->[$month_offset] += $qty;
}
WITH cte AS (
  WITH minmax AS (
    SELECT min(extract(month from order_date))::int,
           max(extract(month from order_date))::int
    FROM mytable
  )
  SELECT entity, mon, 0 AS qty
  FROM (SELECT DISTINCT entity FROM mytable) entities,
       (SELECT generate_series(min, max) AS mon FROM minmax) allmonths
  UNION
  SELECT entity, extract(month from order_date)::int, qty FROM mytable
)
SELECT entity, array_agg(sum) AS values
FROM (
  SELECT entity, mon, sum(qty) FROM cte
  GROUP BY 1, 2) sub
GROUP BY 1
ORDER BY 1;
几句解释:

在SQL语句中生成数组的标准方法是使用
array\u agg()
函数。你的问题是,你有几个月没有数据,然后
array\u agg()
高兴地什么也不产生,留下长度不等的数组,并且没有关于数据在时间段中来自何处的信息。您可以通过为“实体”的每个组合和利息期间的月份添加0来解决此问题。这就是这段代码的作用:

SELECT entity, mon, 0 AS qty
FROM (SELECT DISTINCT entity FROM mytable) entities,
     (SELECT generate_series(min, max) AS mon FROM minmax) allmonths
所有这些0都从“mytable”合并到实际数据中,然后(在主查询中),您可以首先按实体和月份对数量进行汇总,然后将这些汇总汇总到每个实体的数组中。因为它是一个双重聚合,所以您需要子查询。(您也可以对
联合
中的数量求和,但是您还需要一个子查询,因为
联合
不允许聚合。)

minmax
CTE也可以调整为包含年份(您的样本数据不需要它)。请注意,实际的
min
max
值对数组中的索引无关紧要:如果
min
为743,它仍将占据数组中的第一个位置;这些值仅用于
GROUP
ing,而不是索引


为了便于使用,您可以将此查询包装在SQL语言函数中,其中包含起始月份和结束月份的参数。调整
minmax
CTE,为
generate_series()
调用生成适当的
min
max
值,并在
UNION
中过滤要考虑的“mytable”中的行。

这正是我想要的——感谢您的详细解释。一个简单的问题——在您的子查询
sub
中,订单是有保证的,还是我需要按1,2添加一个
订单,以确保月份在分组时没有按顺序列出?在
sub
子查询中,订单是不相关的。对
mon
的值进行分组,无论顺序如何。在许多(但不是所有!)SQL操作中,顺序并不重要,但考虑到人类对有序事物的偏好,
orderby
包含在主查询中。您可以通过在SQLfiddle中无序添加一些数据来验证这一点:答案总是一样的。