将多行合并为一行

时间:2015-10-19 20:23:42

标签: sql postgresql join pivot crosstab

我在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来查看我是否可以实现这样的输出但到目前为止没有成功。感谢您抽出宝贵时间来研究这个问题。

2 个答案:

答案 0 :(得分:1)

您可以在子查询中使用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

答案 1 :(得分:1)

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

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

除此之外,以下是您的设置 的解决方案

crosstab()解决方案

crosstab()查询存在以下几个具体问题:

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

基础知识(首先阅读!):

针对您的特殊情况:

解决方案:

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 相关资料的行完全medium

  • 由于您返回整个表格,因此在加入之前汇总medium 中的行会更便宜。对于较小的选择,首先加入并且然后聚合可能更便宜。相关: