将多行合并为一行

Combining multiple rows into one

我在 PostgreSQL 中有一个数据库结构,看起来像这样:

DROP TABLE IF EXISTS  medium  CASCADE;
DROP TABLE IF EXISTS  works   CASCADE;
DROP DOMAIN IF EXISTS nameVal CASCADE;
DROP DOMAIN IF EXISTS numID   CASCADE;
DROP DOMAIN IF EXISTS alphaID CASCADE;

CREATE DOMAIN alphaID   AS VARCHAR(10);
CREATE DOMAIN numID     AS INT;
CREATE DOMAIN nameVal   AS VARCHAR(40);

CREATE TABLE works (
   w_alphaID    alphaID     NOT NULL,
   w_numID      numID       NOT NULL,
   w_title      nameVal     NOT NULL,
   PRIMARY KEY(w_alphaID,w_numID));


CREATE TABLE medium (
   m_alphaID    alphaID     NOT NULL,
   m_numID      numID       NOT NULL,
   m_title      nameVal     NOT NULL,
   FOREIGN KEY(m_alphaID,m_numID) REFERENCES 
      works ON UPDATE CASCADE ON DELETE CASCADE);

INSERT INTO works VALUES('AB',1,'Sunset'),
                        ('CD',2,'Beach'),
                        ('EF',3,'Flower');

INSERT INTO medium VALUES('AB',1,'Wood'),
                         ('AB',1,'Oil'),
                         ('CD',2,'Canvas'),
                         ('CD',2,'Oil'),
                         ('CD',2,'Bronze'),
                         ('EF',3,'Paper'),
                         ('EF',3,'Pencil');
SELECT * FROM works;
SELECT * FROM medium;

SELECT w_alphaID AS alphaID, w_numID AS numID, w_title AS
       Name_of_work, m_title AS Material_used 
     FROM works, medium WHERE 
       works.w_alphaID = medium.m_alphaID 
       AND works.w_numID = medium.m_numID;

输出看起来像这样:

 w_alphaid | w_numid | w_title 
-----------+---------+---------
 AB        |       1 | Sunset
 CD        |       2 | Beach
 EF        |       3 | Flower
(3 rows)

 m_alphaid | m_numid | m_title 
-----------+---------+---------
 AB        |       1 | Wood
 AB        |       1 | Oil
 CD        |       2 | Canvas
 CD        |       2 | Oil
 CD        |       2 | Bronze
 EF        |       3 | Paper
 EF        |       3 | Pencil
(7 rows)

 alphaid | numid | name_of_work | material_used 
---------+-------+--------------+---------------
 AB      |     1 | Sunset       | Wood
 AB      |     1 | Sunset       | Oil
 CD      |     2 | Beach        | Canvas
 CD      |     2 | Beach        | Oil
 CD      |     2 | Beach        | Bronze
 EF      |     3 | Flower       | Paper
 EF      |     3 | Flower       | Pencil
(7 rows)

现在我的问题是我应该使用什么查询来使最后一个 SELECT 语句的格式看起来像这样:

 alphaid | numid | name_of_work | material_used_1 | material_used_2 | material_used_3 
---------+-------+--------------+-----------------+-----------------+---------------
 AB      |     1 | Sunset       | Wood            | Oil             |
 CD      |     2 | Beach        | Canvas          | Oil             | Bronze
 EF      |     3 | Flower       | Paper           | Pencil          |
(3 rows)

我研究过使用 string_agg() 但这会将值放入一个单元格中,但我希望每个值都有一个单独的单元格。我尝试使用 join 来查看是否可以实现这样的输出,但到目前为止没有成功。感谢您抽出宝贵的时间来查看这个问题。

您可以在子查询中使用 string_agg(),然后将字符串分成单独的列。另请参阅 how to split string into columns

上的此问题
SELECT alphaID, numID, Name_of_Work
      ,split_part(Material_used, ',', 1) AS Material_used_1
      ,split_part(Material_used, ',', 2) AS Material_used_2
      ,split_part(Material_used, ',', 3) AS Material_used_3
      ,split_part(Material_used, ',', 4) AS Material_used_4
FROM (
    SELECT w_alphaID AS alphaID, w_numID AS numID, w_title AS Name_of_work,
           String_Agg( m_title, ',' ) AS Material_used 
    FROM works, medium 
    WHERE works.w_alphaID = medium.m_alphaID 
       AND works.w_numID = medium.m_numID 
    GROUP BY w_alphaID, w_numID, w_title ) t

使用更简单的架构会更简单:

  • 没有域类型(目的是什么?)
  • 添加实际PK到tablemedium
  • 而是使用代理 PK(serial 列)而不是两种域类型的多列 PK 和 FK。
    或者至少对具有相同内容的列使用相同(更简单)的列名:只是 alpha_id 而不是 m_alphaIDw_alphaID

除此之外,这里有针对您的设置的解决方案原样:

crosstab()

您的 crosstab() 查询有几个具体困难:

  • 没有一个列可以作为row_name.
  • 多个额外的列。
  • 没有 类别 列。
  • 没有定义值的顺序(因此我改用任意顺序)。

基础知识(先阅读此内容!):

  • PostgreSQL Crosstab Query

对于您的特殊情况:

  • Pivot on Multiple Columns using Tablefunc
  • Dynamic alternative to pivot with CASE and GROUP BY

解决方案:

SELECT alphaid, numid, name_of_work, material_1, material_2, material_3
FROM   crosstab(
  'SELECT rn, w.alphaid, w.numid, w.name_of_work
        , row_number() OVER (PARTITION BY rn) AS mat_nr  -- order undefined!
        , m_title AS Material_used 
   FROM  (
      SELECT w_alphaID AS alphaid, w_numID AS numid, w_title AS name_of_work
           , row_number() OVER (ORDER BY w_alphaID, w_numID) AS rn
       FROM  works
      ) w
   JOIN   medium m ON w.alphaid = m.m_alphaID 
                  AND w.numid   = m.m_numID
   ORDER  BY rn, mat_nr'
 , 'VALUES (1), (2), (3)'  -- add more ...
)
 AS ct (
    rn bigint, alphaid text, numid int, name_of_work text
  , material_1 text, material_2 text, material_3 text  -- add more ...
   );

穷人与标准的交叉表 SQL

如果附加模块 tablefunc 无法安装,或者顶级性能不重要,这个更简单的查询也可以,但速度较慢:

SELECT w_alphaid AS alphaid, w_numid AS numid, w_title AS name_of_work
     , arr[1] AS material_used_1
     , arr[2] AS material_used_2
     , arr[3] AS material_used_3 -- add more?
FROM   works w
LEFT  JOIN (
   SELECT m_alphaid, m_numid, array_agg(m_title::text) AS arr
   FROM   medium
   GROUP  BY m_alphaid, m_numid
   ) m ON w.w_alphaid = m.m_alphaid 
      AND w.w_numid   = m.m_numid;
  • 强制转换为 text(或 varchar ...)是必要的,因为您的自定义域没有预定义的数组类型。或者,您可以定义缺少的数组类型。

  • 与上面的一个细微差别:在此处使用 LEFT JOIN 而不是仅 JOIN 来保留 works 中具有 no[ 的行=89=] 相关资料 medium 完全没有。

  • 由于您 return 整个 table,在您加入 medium 之前聚合行会更便宜 。对于少量选择,先加入然后 然后聚合 可能更便宜。相关:

    • GROUP or DISTINCT after JOIN returns duplicates