将未定义的行数转置为列

Posted

技术标签:

【中文标题】将未定义的行数转置为列【英文标题】:Transposing Undefined Number of Rows to Columns 【发布时间】:2014-03-12 14:44:33 【问题描述】:

我有一个如下所示的表格:

Member, Contract_Start, Contract_End  
1,  1/1/2011,   12/30/2011  
1,  1/1/2012,   12/30/2012  
1,  1/1/2013,   12/30/2013  
2,  7/1/2012,   12/30/2012  
2,  1/1/2013,   12/30/2013  

成员最多可以拥有一份合约,并且合约数量没有上限。 我想将表格切换为如下所示:

Member, Contract_Start1, Contract_End1, Contract_Start2, Contract_End2.....   
1, 1/1/2011, 12/30/2011, 1/1/2012, 12/30/2012  
2, 7/1/2012, 12/30/2012, 1/1/2013, 12/30/2013  

感谢您提供的任何帮助。

【问题讨论】:

理想情况下在您的报告层或消费应用程序中进行。 SQL 不擅长处理不同长度的记录。 您必须准备动态 SQL 以动态提供 Select 和 PIVOT 列列表。只是好奇,以这种方式转置数据的基本需求是什么。因为当您查看 Member-1 的转置数据时,可能有 10 个合同日期,而 member-2 可能只有 1 个。所以 9 列将为空。 Anup,我对此的基本需求是确定会员合同覆盖范围的差距。 什么数据库?后格雷斯?甲骨文? SQLite? 【参考方案1】:

我已经使用过这样的解决方案,建议不要走这条路,因为长时间维护/调试这种动态 sql 变得很困难。

你可以试试演示Here

    IF object_id('Test_Transpose') IS NOT NULL
        DROP TABLE Test_Transpose
    GO
    CREATE TABLE Test_Transpose
    (
        memberID    INT NOT NULL
        ,csdate     datetime2(2) NULL
        ,cedate     datetime2(2) NULL
    )
    INSERT INTO Test_Transpose (memberid,csdate,cedate)
                SELECT 1,'1/1/2001','1/1/2001'
    UNION ALL   SELECT 1,'1/2/2001','1/2/2001'
    UNION ALL   SELECT 1,'1/3/2001','1/2/2001'
    UNION ALL   SELECT 1,'1/4/2001','1/2/2001'
    UNION ALL   SELECT 1,'1/5/2001','1/2/2001'
    UNION ALL   SELECT 2,'1/2/2001','1/2/2001'
    UNION ALL   SELECT 2,'1/3/2001','1/3/2001'
    UNION ALL   SELECT 3,'1/2/2001','1/2/2001'
    UNION ALL   SELECT 3,'1/3/2001','1/3/2001'
    UNION ALL   SELECT 3,'1/4/2001','1/4/2001'
    UNION ALL   SELECT 4,'1/2/2001','1/2/2001'


    DECLARE @SQL NVARCHAR(MAX)=''
            ,@Startdate_SelectColumnList NVARCHAR(MAX)=''
            ,@EndDate_SelectColumnList NVARCHAR(MAX)=''
            ,@Final_SelectColumnList NVARCHAR(MAX)=''
            ,@PivotINColumnList NVARCHAR(MAX)=''
            ,@MaxContractDateCount INT=1

    SELECT TOP 1 @MaxContractDateCount = COUNT(1)
    FROM Test_Transpose
    GROUP BY memberid
    ORDER BY COUNT(1) desc


    WHILE @MaxContractDateCount > 0
    BEGIN
        SELECT @Startdate_SelectColumnList =    N'['+CAST(@MaxContractDateCount AS sysname)+N'] AS '
                                                +N'StartDate'+CAST(@MaxContractDateCount AS sysname)
                                                +CASE WHEN @Startdate_SelectColumnList=N'' THEN N'' ELSE N',' END
                                                +@Startdate_SelectColumnList

        SELECT @Enddate_SelectColumnList =      N'['+CAST(@MaxContractDateCount AS sysname)+N'] AS '
                                                +N'EndDate'+CAST(@MaxContractDateCount AS sysname)
                                                +CASE WHEN @Enddate_SelectColumnList=N'' THEN N'' ELSE N',' END
                                                +@Enddate_SelectColumnList

        SELECT @Final_SelectColumnList =        N'StartDate'+CAST(@MaxContractDateCount AS sysname)+N','
                                                +N'EndDate'+CAST(@MaxContractDateCount AS sysname)
                                                +CASE WHEN @Final_SelectColumnList=N'' THEN N'' ELSE N',' END
                                                +@Final_SelectColumnList

        SELECT @PivotINColumnList =             N'['+CAST(@MaxContractDateCount AS sysname)+N']'
                                                +CASE WHEN @PivotINColumnList=N'' THEN N'' ELSE N',' END
                                                +@PivotINColumnList

        SET @MaxContractDateCount=@MaxContractDateCount-1
    END

    --debug stmt
    --SELECT @Startdate_SelectColumnList,@Enddate_SelectColumnList,@Final_SelectColumnList,@PivotINColumnList

    SET @SQL = N'
                SELECT q1.memberid,'
                +@Final_SelectColumnList
                +N'
                FROM
                (
                    SELECT  memberid,'
                +@Startdate_SelectColumnList
                +N'
                    FROM
                    (
                    SELECT memberid,csdate,ROW_NUMBER() OVER (PARTITION BY memberid ORDER BY memberid,csdate) rowid
                    FROM test_transpose
                    )q
                    PIVOT
                    (MAX(csdate) FOR rowid IN ('+@PivotINColumnList+N'))pvt
                )q1
                JOIN
                (
                    SELECT  memberid,'
                +@Enddate_SelectColumnList
                +N'
                    FROM
                    (
                    SELECT memberid,cedate,ROW_NUMBER() OVER (PARTITION BY memberid ORDER BY memberid,csdate) rowid
                    FROM test_transpose
                    )q
                    PIVOT
                    (MAX(cedate) FOR rowid IN ('+@PivotINColumnList+N'))pvt
                )q2
                ON q1.memberid = q2.memberid
                '
    PRINT @SQL
    EXECUTE sp_executesql @SQL

【讨论】:

【参考方案2】:

我有一个 Postgres db 版本明智地做这项工作。这是我的代码示例。

DROP TABLE IF EXISTS x;
CREATE TABLE x ( member NUMERIC , contract_start DATE, contract_end DATE);

INSERT INTO x VALUES( 1, '1/1/2011', '12/30/2011' )
,( 1, '1/1/2012', '12/30/2012' )
,( 1, '1/1/2013', '12/30/2013' )
,( 2, '7/1/2012', '12/30/2012' )
,( 2, '1/1/2013', '12/30/2013' )
,( 3, '1/1/2012', '12/30/2012' )
,( 3, '1/1/2013', '12/30/2013' )
,( 3, '8/1/2013', '12/30/2013' )
,( 3, '8/1/2013', '12/30/2013' );

-- CREATE LANGUAGE 'plpgsql';
-- DROP SEQUENCE IF EXISTS seq;
-- CREATE SEQUENCE seq;

CREATE OR REPLACE FUNCTION make_table() RETURNS void AS
$BODY$
DECLARE
  i RECORD;
  colcnt INTEGER;
BEGIN
  DROP TABLE IF EXISTS tmptable;
  SELECT max(count) INTO colcnt FROM (SELECT count(*) FROM x GROUP BY member) a;
  EXECUTE 'CREATE TABLE tmptable (member integer ,' || 
    (SELECT array_to_string(array_agg(x1.col1 || ' date, ' || x1.col2 || ' date'), ', ')
      FROM (SELECT 'contract_start' || col col1
      , 'contract_end'|| col col2 FROM generate_series(1,colcnt) t(col) ) x1 ) || ')';

  EXECUTE 'INSERT INTO tmptable (member) SELECT DISTINCT member FROM x';

  FOR i IN SELECT * FROM x ORDER BY member LOOP
    PERFORM setval('seq',1);
    EXECUTE 'UPDATE tmptable SET ' || x1.col 
      FROM (SELECT array_to_string(array_agg(' contract_start' 
      || nextval('seq')-1 || ' = ''' || i.contract_start || ''', contract_end' 
      || currval('seq')-1 || ' = ''' || i.contract_end ), ''', ') || ''' WHERE member = ' || member || ';' col 
      FROM x WHERE member = i.member GROUP BY member) x1;
  END LOOP;
END;
$BODY$ LANGUAGE plpgsql;

SELECT * FROM make_table();
SELECT * FROM tmptable;

这是我第一次回答。希望这是相关的。 (请只运行一次注释)。

进一步每次插入、更新或删除时调用函数的过程都可以通过触发器自动执行。以下代码将在替换上述代码时执行此操作。

-- following commented lines for first time run :
-- CREATE LANGUAGE 'plpgsql';
-- DROP SEQUENCE IF EXISTS seq;
-- CREATE SEQUENCE seq;
-- DROP FUNCTION IF EXISTS make_table();

CREATE OR REPLACE FUNCTION make_table() RETURNS TRIGGER AS
$BODY$
DECLARE
  i RECORD;
  colcnt INTEGER;
BEGIN
  DROP TABLE IF EXISTS tmptable;
  SELECT max(count) INTO colcnt FROM (SELECT count(*) FROM x GROUP BY member) a;
  EXECUTE 'CREATE TABLE tmptable (member integer ,' || 
    (SELECT array_to_string(array_agg(x1.col1 || ' date, ' || x1.col2 || ' date'), ', ')
      FROM (SELECT 'contract_start' || col col1
      , 'contract_end'|| col col2 FROM generate_series(1,colcnt) t(col) ) x1 ) || ')';

  EXECUTE 'INSERT INTO tmptable (member) SELECT DISTINCT member FROM x';

  FOR i IN SELECT * FROM x ORDER BY member LOOP
    PERFORM setval('seq',1);
    EXECUTE 'UPDATE tmptable SET ' || x1.col 
      FROM (SELECT array_to_string(array_agg(' contract_start' 
      || nextval('seq')-1 || ' = ''' || i.contract_start || ''', contract_end' 
      || currval('seq')-1 || ' = ''' || i.contract_end ), ''', ') || ''' WHERE member = ' || member || ';' col 
      FROM x WHERE member = i.member GROUP BY member) x1;
  END LOOP;
  RETURN new;
END;
$BODY$ LANGUAGE plpgsql;

CREATE TRIGGER create_tmptable AFTER INSERT OR UPDATE OR DELETE OR TRUNCATE 
ON x FOR STATEMENT EXECUTE PROCEDURE make_table();

-- For Test insert:
INSERT INTO x VALUES( 4, '1/1/2011', '12/30/2011' );
SELECT * FROM tmptable;

【讨论】:

以上是关于将未定义的行数转置为列的主要内容,如果未能解决你的问题,请参考以下文章

将单行转置为列

mysql 将行转置为列

将 SQL 行数据转置为列

SQL 将行转置为列

Oracle SQL Developer:如何使用 PIVOT 函数将行转置为列

在 BigQuery 中将行转置为列