【问题标题】:Transform long rows to wide, filling all cells将长行转换为宽行,填充所有单元格
【发布时间】:2014-06-25 07:21:12
【问题描述】:

我有关于企业的长格式数据,每次移动到不同位置时对应一行,以企业 ID 为键 - 任何一个企业机构都可以有多个移动事件。

我希望重塑为宽格式,这通常是每个 tablefunc 模块的跨表区域。

+-------------+-----------+---------+---------+
| business_id | year_move |  long   |   lat   |
+-------------+-----------+---------+---------+
|   001013580 |      1991 | 71.0557 | 42.3588 |
|   001015924 |      1993 | 71.0728 | 42.3504 |
|   001015924 |      1996 | -122.28 | 37.654  |
|   001020684 |      1992 | 84.3381 | 33.5775 |
+-------------+-----------+---------+---------+

然后我像这样变换:

SELECT longbyyear.*
FROM crosstab($$
    SELECT 
    business_id, 
    year_move, 
    max(longitude::float)
    from business_moves
    where year_move::int between 1991 and 2010 
    group by business_id, year_move
    order by business_id, year_move;
    $$
) 
AS longbyyear(biz_id character varying, "long91" float,"long92" float,"long93" float,"long94" float,"long95" float,"long96" float,"long97" float, "long98" float, "long99" float,"long00" float,"long01" float,
"long02" float,"long03" float,"long04" float,"long05" float, 
"long06" float, "long07" float, "long08" float, "long09" float, "long10" float);

它——主要是——让我得到想要的输出。

+---------+----------+----------+----------+--------+---+--------+--------+--------+
| biz_id  |  long91  |  long92  |  long93  | long94 | … | long08 | long09 | long10 |
+---------+----------+----------+----------+--------+---+--------+--------+--------+
| 1000223 | 121.3784 | 121.3063 | 121.3549 | 82.821 | … |        |        |        |
| 1000678 | 118.224  |          |          |        | … |        |        |        |
| 1002158 | 121.98   |          |          |        | … |        |        |        |
| 1004092 | 71.2384  |          |          |        | … |        |        |        |
| 1007801 | 118.0312 |          |          |        | … |        |        |        |
| 1007855 | 71.1769  |          |          |        | … |        |        |        |
| 1008697 | 71.0394  | 71.0358  |          |        | … |        |        |        |
| 1008986 | 71.1013  |          |          |        | … |        |        |        |
| 1009617 | 119.9965 |          |          |        | … |        |        |        |
+---------+----------+----------+----------+--------+---+--------+--------+--------+

唯一的障碍是,理想情况下,我会为每年填充值,而不仅仅是移动年份的值。因此,所有字段都将被填充,每年都有一个值,最近的地址会延续到下一年。如果每个都是空白的,我可以通过手动更新来解决这个问题,使用上一列,我只是想知道是否有一个聪明的方法可以使用 crosstab() 函数或其他方式,可能与自定义函数相结合。

【问题讨论】:

  • 没有自动的方法来执行此操作 afaik 因为您需要未存储在数据库中的信息。实际上,您想从 event 数据(我的企业在哪一年迁移到新的经度?)创建 state 信息(我的企业在某年位于哪里?) .可以这样做,但会很丑陋。你能详细说明你想要达到的目标吗?而且,如果你想得到一个真正可行的答案,请张贴表格结构。
  • @Patrick,表格结构显示在顶部,前三行无论如何 - 尽管列名略有不同。但我基本上是在为每个业务查找表,因此给定一年,我可以从表中获取相应的位置(纬度、经度对,或者作为二维数组,或者在单独的表中)。
  • 那么我假设您想回答“我的企业在 x 年位于哪里?”这样的问题是否正确?
  • 确实,总结一下——我意识到存储每年的数据有大量冗余,因为大多数企业不会移动那么多,但整个数据库是一组宽格式平面文件键入业务 ID,按年份为每个业务(就业、行业代码等)提供各种变量。
  • 我假设您对每个business_move 都有一个日期,而不仅仅是年份?我还假设,您实际上并不想要每年最大的latitude?最后我假设您不仅想要latitude,还想要longitude

标签: sql postgresql postgresql-9.1 crosstab generate-series


【解决方案1】:

要获取任何给定年份的每个 business_id 的当前位置,您需要两件事:

  1. 用于选择年份的参数化查询,作为 SQL 语言函数实现。
  2. 按年汇总、按业务 ID 分组并保持坐标不变的肮脏技巧。这是通过 CTE 中的子查询完成的。

函数如下所示:

CREATE FUNCTION business_location_in_year_x (int) RETURNS SETOF business_moves AS $$
  WITH last_move AS (
    SELECT business_id, MAX(year_move) AS yr
    FROM business_moves
    WHERE year_move <= $1
    GROUP BY business_id)
  SELECT lm.business_id, $1::int AS yr, longitude, latitude
  FROM business_moves bm, last_move lm
  WHERE bm.business_id = lm.business_id
  AND bm.year_move = lm.yr;
$$ LANGUAGE sql;

子查询仅选择每个营业地点的最新移动。然后,主查询添加经度和纬度列,并将请求的年份放在返回的表中,而不是最近发生移动的年份。一个警告:您需要在此表中有一条记录,提供每个 business_id 的建立和初始位置,否则直到它移动到其他地方后才会显示。

使用通常的SELECT * FROM business_location_in_year_x(1997) 调用此函数。另请参阅SQL fiddle

如果您真的需要交叉表,那么您可以调整此代码以提供多年的营业地点,然后将其输入crosstab()功能。

【讨论】:

  • 这是完美的。在数以百万计的机构上运行此查询可能会对性能造成很大影响(因此最初存储每年的数据),但这要简单得多。
【解决方案2】:

我假设您有每个业务迁移的实际日期,因此我们可以每年做出有意义的选择

CREATE TEMP TABLE business_moves (
  business_id int,  -- why would you use inefficient varchar here?
  move_date date,
  longitude float,
  latitude float);

在此基础上,一个更有意义的测试用例:

INSERT INTO business_moves VALUES 
  (001013580, '1991-1-1', 71.0557, 42.3588),
  (001015924, '1993-1-1', 71.0728, 42.3504),
  (001015924, '1993-3-3', 73.0728, 43.3504),  -- 2nd move this year
  (001015924, '1996-1-1', -122.28, 37.654),
  (001020684, '1992-1-1', 84.3381, 33.5775);

完整、非常快速的解决方案

SELECT *
FROM crosstab($$
   SELECT business_id, year
        , first_value(x) OVER (PARTITION BY business_id, grp ORDER BY year) AS x
   FROM  (
      SELECT *
           , count(x) OVER (PARTITION BY business_id ORDER BY year) AS grp
      FROM  (SELECT DISTINCT business_id FROM business_moves) b
      CROSS  JOIN generate_series(1991, 2010) year
      LEFT   JOIN (
         SELECT DISTINCT ON (1,2)
                business_id
              , EXTRACT('year' FROM move_date)::int AS year
              , point(longitude, latitude) AS x
         FROM   business_moves
         WHERE  move_date >= '1991-1-1'
         AND    move_date <  '2011-1-1'
         ORDER  BY 1,2, move_date DESC
         ) bm USING (business_id, year)
      ) sub
   $$
   ,'VALUES
    (1991),(1992),(1993),(1994),(1995),(1996),(1997),(1998),(1999),(2000)
   ,(2001),(2002),(2003),(2004),(2005),(2006),(2007),(2008),(2009),(2010)'
    ) AS t(biz_id int
         , x91 point, x92 point, x93 point, x94 point, x95 point
         , x96 point, x97 point, x98 point, x99 point, x00 point
         , x01 point, x02 point, x03 point, x04 point, x05 point
         , x06 point, x07 point, x08 point, x09 point, x10 point);

结果:

 biz_id  |        x91        |        x92        |        x93        |        x94        |        x95        |        x96        |        x97        ...
---------+-------------------+-------------------+-------------------+-------------------+-------------------+-------------------+-------------------
 1013580 | (71.0557,42.3588) | (71.0557,42.3588) | (71.0557,42.3588) | (71.0557,42.3588) | (71.0557,42.3588) | (71.0557,42.3588) | (71.0557,42.3588) ...
 1015924 |                   |                   | (73.0728,43.3504) | (73.0728,43.3504) | (73.0728,43.3504) | (-122.28,37.654)  | (-122.28,37.654)  ...
 1020684 |                   | (84.3381,33.5775) | (84.3381,33.5775) | (84.3381,33.5775) | (84.3381,33.5775) | (84.3381,33.5775) | (84.3381,33.5775) ...

一步一步

步骤 1

修复你所拥有的:

SELECT *
FROM crosstab($$
   SELECT DISTINCT ON (1,2)
          business_id
        , EXTRACT('year' FROM move_date) AS year
        , point(longitude, latitude) AS long_lat
   FROM   business_moves
   WHERE  move_date >= '1991-1-1'
   AND    move_date <  '2011-1-1'
   ORDER  BY 1,2, move_date DESC
   $$
   ,'VALUES
    (1991),(1992),(1993),(1994),(1995),(1996),(1997),(1998),(1999),(2000)
   ,(2001),(2002),(2003),(2004),(2005),(2006),(2007),(2008),(2009),(2010)'
   ) AS t(biz_id int
        , x91 point, x92 point, x93 point, x94 point, x95 point
        , x96 point, x97 point, x98 point, x99 point, x00 point
        , x01 point, x02 point, x03 point, x04 point, x05 point
        , x06 point, x07 point, x08 point, x09 point, x10 point);
  • 你想让 lat & lon 使它有意义,所以从两者中形成一个point。或者,您可以只连接一个 text 表示。

  • 您可能需要更多数据。使用DISTINCT ON 而不是max() 来获取每年最新(完整)的行。详情请看:
    Select first row in each GROUP BY group?

  • 只要整个网格可能存在缺失值,您必须使用带有两个参数的 crosstab() 变体。详细解释在这里:
    PostgreSQL Crosstab Query

  • 调整函数以使用 move_date date 而不是 year_move

第二步

解决您的要求:

理想情况下,我会为每年填充值

使用CROSS JOIN 的企业和年份构建完整的值网格(每个企业和年份一个单元格):

SELECT *
FROM  (SELECT DISTINCT business_id FROM business_moves) b
CROSS  JOIN generate_series(1991, 2010) year
LEFT   JOIN (
   SELECT DISTINCT ON (1,2)
          business_id
        , EXTRACT('year' FROM move_date)::int AS year
        , point(longitude, latitude) AS x
   FROM   business_moves
   WHERE  move_date >= '1991-1-1'
   AND    move_date <  '2011-1-1'
   ORDER  BY 1,2, move_date DESC
   ) bm USING (business_id, year)
  • 年份集合来自generate_series() 调用。

  • 与单独的SELECT 不同的业务。你可能有一张企业表,你可以用它来代替(而且更便宜)?这也包括从未搬家的企业。

  • LEFT JOIN 到每年的实际业务变动,以达到完整的价值网格

第三步

填写默认值:

最近的地址延续到下一年。

SELECT business_id, year
     , COALESCE(first_value(x) OVER (PARTITION BY business_id, grp ORDER BY year)
               ,'(0,0)') AS x
FROM  (
   SELECT *, count(x) OVER (PARTITION BY business_id ORDER BY year) AS grp
   FROM  (SELECT DISTINCT business_id FROM business_moves) b
   CROSS  JOIN generate_series(1991, 2010) year
   LEFT   JOIN (
      SELECT DISTINCT ON (1,2)
             business_id
           , EXTRACT('year' FROM move_date)::int AS year
           , point(longitude, latitude) AS x
      FROM   business_moves
      WHERE  move_date >= '1991-1-1'
      AND    move_date <  '2011-1-1'
      ORDER  BY 1,2, move_date DESC
      ) bm USING (business_id, year)
   ) sub;
  • 在子查询 sub 中,基于步骤 2 中的查询构建共享相同位置的单元格组 (grp)。

    为此,使用众所周知的聚合函数count() 作为窗口聚合函数。 NULL 值不计算在内,因此该值会随着每次实际移动而增加,从而形成共享同一位置的单元组。

  • 在外部查询中,使用窗口函数first_value() 为同一组中的每一行选择每组的第一个值。瞧。

  • 最重要的是,可以选择(!)将其包裹在COALESCE 中,以使用(0,0) 填充具有未知位置(尚未移动)的剩余单元格。如果这样做,则没有剩余的NULL 值,您可以使用更简单的crosstab() 形式。这是口味问题。

SQL Fiddle 带有基本查询。 crosstab() 当前未安装在 SQL Fiddle 上。

第四步

在更新的crosstab() 调用中使用步骤 3 中的查询。
总而言之,这应该尽可能快速。索引可能会有所帮助。

【讨论】:

  • 在 SQL Fiddle 上安装 crosstab() 需要做什么?谢谢欧文
  • 像往常一样,很棒的帖子,为此 +1,但仍然有一些评论:(1)您是否应该做出所有假设(business_id as int,移动日期而不是年份)? OP 显然正在使用现有数据库,并且提供的有关结构的详细信息可能是他必须使用的。 (2)我将generate_series()基于已知的首次建立业务的年份或在适当的表(低端)和当前年份(高端)中的搜索,以便在新年进行此查询-证明 - 这不太可能是一次性的。
  • @Patrick:假设就是这样:假设。我可能错了,但should 就是这样。我没有耐心等待OP澄清。答案同样适用于varchar id。仅更改 biz_id int -> biz_id varchar. The bigger change is move_date`。但也很容易适应。答案是针对普通大众的,而不仅仅是针对 OP。但他似乎喜欢这两种方式。
  • @ErwinBrandstetter:是的,没错,但一个不清楚的问题不会邀请公众阅读答案。仅在经度上的 crosstab() 在概念上当然是公然错误的,但即使在某一点上,也很难相信 OP 真的想要那样。我确实通过评论询问了这一点,他的澄清导致了我的回答。 (不过,我认为您的回答很棒。无论是在演示概念(例如,在为 crosstab() 准备数据时使用 generate_series())和在完美的演示中。坚持下去。)
  • @Patrick:顺便说一句,将第一年基于第一条记录(或时间框架之前的最新记录)是个好主意。为此,我想到了LATERAL JOINcrosstab() 的返回类型不灵活。但是使用双参数形式,我们可以“截断”结果行以适应从后一年开始的框架 - 并且不会保留 NULL 值。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-07-18
  • 2016-05-07
相关资源
最近更新 更多