【问题标题】:How can I "merge", "flatten" or "pivot" results from a query which returns multiple rows into a single result?如何从将多行返回到单个结果的查询中“合并”、“展平”或“透视”结果?
【发布时间】:2011-02-25 22:57:11
【问题描述】:

我对一个表有一个简单的查询,它返回如下结果:

id    id_type  id_ref
2702  5        31
2702  16       14
2702  17       3
2702  40       1
2703  23       4
2703  23       5
2703  34       6
2704  1        14

我想将结果合并到一行中,例如:

id    concatenation 
2702  5,16,17,40:31,14,3,1
2703  23,23,34:4,5,6
2704  1:14

有没有办法在触发器中做到这一点?

注意:我知道我可以使用光标,但除非没有更好的方法,否则我真的不想这样做。

数据库是 Sybase 版本 12.5.4。

【问题讨论】:

  • 稍微编辑了标签 “pivot”是这里最常用的术语,如果您更新使用它,可能会让合适的人看到这个问题,尤其是在标题中。

标签: sql sybase pivot flatten


【解决方案1】:

使用行级函数。

您的查询:

select distinct id ,fn(id) from table1

功能:

fn(@id int)
(
declare @res varchar
select @res = @res+id_ref+"," from table1 where id=@id
return @res
)

【讨论】:

    【解决方案2】:

    由于在 Sybase 中使用 select 语句来完成这项工作相当困难,我建议使用类似以下的 while 循环。 While 循环比游标更受欢迎,因为它更快。假设表名是 MYTABLE:

    CREATE TABLE #temp
    (                               
    aa            numeric(5,0)  identity,                            
    id            int           not null,
    id_type       int           not null,
    id_ref        int           not null
    )
    
    CREATE TABLE #results
    (                                                        
    id            int           not null,
    concatenation varchar(1000) not null,
    )
    
    insert into #temp
    select id, id_type, id_ref from MYTABLE order by id
    
    declare @aa int, @maxaa int, @idOld int, @idNew int
    declare @str1 varchar(1000), @str2 varchar(1000)
    
    set @aa = 1
    set @maxaa = (select max(aa) from #temp) 
    set @idNew = (select id from #temp where aa = 1) 
    , @idOld = @idNew
    
    while @aa <= @maxaa 
        begin
            set @idNew = (select id from #temp where aa = @aa) 
            IF @idNew = @idOld
              BEGIN
                 set @str1 = @str1 + convert(varchar,(select id_type from #temp where aa = @aa)) + ','  
                 , @str2 = @str2 + convert(varchar,(select id_ref from #temp where aa = @aa)) + ','
    
                 IF @aa = @maxaa  
                 insert into #results (id, concatenation) 
                 VALUES (@idOld, left(@str1,len(@str1) - 1) + ':' + left(@str2,len(@str2) - 1) )
    
              END
            ELSE
              BEGIN
                 insert into #results (id, concatenation) 
                 VALUES (@idOld, left(@str1,len(@str1) - 1) + ':' + left(@str2,len(@str2) - 1) )
                 set @str1 = NULL, @str2 = NULL
                 set @str1 = @str1 + convert(varchar,(select id_type from #temp where aa = @aa)) + ','  
                 , @str2 = @str2 + convert(varchar,(select id_ref from #temp where aa = @aa)) + ',' 
    
                 IF @aa = @maxaa  
                 insert into #results (id, concatenation) 
                 VALUES (@idNew, left(@str1,len(@str1) - 1) + ':' + left(@str2,len(@str2) - 1) )
              END
    
            set @idOld = @idNew 
            set @aa = @aa+1
        end
    
    select * from #results
    

    编辑 以下版本快 45% 左右

    CREATE TABLE #temp
    (                               
    aa            numeric(5,0)  identity,                            
    id            int           not null,
    id_type       int           not null,
    id_ref        int           not null
    )
    
    CREATE TABLE #results
    (                                                        
    id            int           not null,
    concatenation varchar(1000) not null,
    )
    
    insert into #temp
    select id, id_type, id_ref from MYTABLE order by id
    declare @aa int, @maxaa int, @idOld int, @idNew int
    declare @str1 varchar(1000), @str2 varchar(1000), @j int
    
    set @aa = 1
    set @maxaa = (select max(aa) from #temp) 
    set @idNew = (select id from #temp where aa = 1) 
    , @idOld = @idNew
    set @str1 = ':'
    
    while @aa <= @maxaa 
        begin
            set @idNew = (select id from #temp where aa = @aa) 
            IF @idNew = @idOld
              BEGIN
                 set @str2 = (select convert(varchar,id_type) + ':' + convert(varchar,id_ref) from #temp where aa = @aa)
                 set @j = (select charindex(':',@str2))
                 set @str1 = str_replace(@str1, ':', substring(@str2,1,@j - 1) + ',:') + right(@str2,len(@str2) - @j) + ',' 
    
                 IF @aa = @maxaa  
                 insert into #results (id, concatenation) 
                 VALUES (@idOld, left(str_replace(@str1, ',:', ':'),len(@str1) - 2) )
    
              END
            ELSE
              BEGIN
                 insert into #results (id, concatenation) 
                 VALUES (@idOld, left(str_replace(@str1, ',:', ':'),len(@str1) - 2) )
                 set @str1 = ':'
                 set @str2 = (select convert(varchar,id_type) + ':' + convert(varchar,id_ref) from #temp where aa = @aa)
                 set @j = (select charindex(':',@str2))
                 set @str1 = str_replace(@str1, ':', substring(@str2,1,@j - 1) + ',:') + right(@str2,len(@str2) - @j) + ','
    
                 IF @aa = @maxaa  
                 insert into #results (id, concatenation) 
                 VALUES (@idNew, left(str_replace(@str1, ',:', ':'),len(@str1) - 2) )
              END
    
            set @idOld = @idNew 
            set @aa = @aa+1
        end
    
    select * from #results
    

    【讨论】:

    • 这种方法比我目前使用的实际数据的光标花费的时间要长得多
    • +1 用于生成正确的结果。在我完成对所有其他答案的评估之前,我会保留这个问题
    • @dsm 性能下降的一个明显原因是重新创建初始表(如#temp)。如果您的真实表具有标识列或另一个唯一 id,则可以省略重新创建表的步骤,因此您可以将该列用作计数器。
    • #temp 表的填充速度非常快。延迟发生在 while 循环期间。
    【解决方案3】:

    好的,如果我在这里遗漏了一些重要的东西,请原谅我,因为我对 Sybase 的第一件事一无所知。但是在 mysql 中,这非常简单,所以我认为它不会像到目前为止的答案那么糟糕。因此,从可能相关或不相关的文档中提取:

    SELECT id, LIST(id_type) + ":" + LIST(id_ref) AS concatentation
    

    如果我读错了什么,请告诉我,我会删除它。

    【讨论】:

    • 为了我自己的启迪,介意解释一下这不起作用吗?
    • 啊,看起来这是不同版本的问题:“Sybase Adaptive Server Anywhere(但不是 Adaptive Server Enterprise)有这个非常简洁的、尽管是专有的、非标准的聚合函数,称为 list() 。”
    【解决方案4】:

    我没有要测试的sybase 服务器,但是在线阅读文档,似乎支持公用表表达式。我不确定其他解决方案中使用的 ROW_NUMBER,所以这里有一个不使用它的解决方案。

    我相信 sybase 使用 ||对于字符串连接,尽管我阅读的文档提到也可以使用“+”,所以我使用了它。请酌情更改。

    我已对查询进行了评论,试图解释发生了什么。

    查询连接所有具有相同 id 的 id_type 和 id_ref 值,以“id_type”递增的顺序。

    /* a common table expression is used to concatenate the values, one by one */
    WITH ConcatYourTable([id],  /* the id of rows being concatenated */
          concat_id_type,       /* concatenated id_type so far */
          concat_id_ref,        /* concatenated id_ref so far */
          last_id_type,         /* the last id_type added */
          remain)               /* how many more values are there to concatenate? */
    AS 
    (
      /* start with the lowest id_type value for some id */
      SELECT id, id_type, id_ref, 
         id_type, /* id_type was concatentated (it's presently the only value) */
         (SELECT COUNT(*) FROM YourTable f2 WHERE f2.id=f.id)-1
         /* how many more values to concatenate -1 because we've added one already */
      FROM YourTable f 
      WHERE NOT EXISTS
      /* start with the lowest value - ensure there are no other values lower. */
         (SELECT 1 FROM YourTable f2 WHERE f2.id=f.id AND f2.id_type<f.id_type)
      UNION ALL
      /* concatenate higher values of id_type for the same id */
      SELECT f.id, 
        c.id_type + ',' + f.id_type,   /* add the new id_type value to the current list */
        c.id_ref + ',' + f.id_ref,     /* add the new id_ref value to the current list */
        f.id_type,  /* the last value added - ensured subsequent added values are greater */
        c.remain-1  /* one less value to add */
      FROM ConcatYourTable c           /* take what we have concatenated so far */    
       INNER JOIN YourTable f  /* add another row with the same id, and > id_type */
         ON f.id = c.id AND f.id_type > c.last_id_type
         /* we really want the next highest id_type, not just one that is greater */
       WHERE NOT EXISTS (SELECT 1 FROM YourTable f2
         WHERE f2.id=f.id AND f2.id_type<f.id_type AND
         f2.id_type>c.last_id_type)
    )
    /* Select the rows where all values for and id were concatenated (remain=0) */
    /* Concatenate the cumulated id_type and id_ref fields to format id_type values:id_ref values*/
    SELECT id, id_type+':'+id_ref FROM ConcatYourTable 
    WHERE remain=0
    

    该查询非常“野蛮”,因为它没有使用可能会提高可读性或性能的更复杂的功能。我之所以这样做,是因为我不太了解 sybase,并且使用了我有理由相信的那些特性是受支持的。为获得最佳性能,请确保 id 和 (id,id_type) 已编入索引。

    要在触发器(例如 INSERT 或 UPDATE 触发器)中使用它来维护基于此连接查询的表,请扩展基本案例的 WHERE 子句(在 UNION ALL 之前)以包含 id=@changed_id。这将确保只计算更改后的 id 的连接行。然后,您可以使用计算的连接行做您想做的事情。如果要将串联查询具体化到表中,则删除表中@changed_id 的当前串联行,并从上述串联查询的结果中插入新行。您还可以检查您的连接表是否已经包含一个带 changed_id 的值,并改用 UPDATE 语句。

    【讨论】:

    • 这不是有效的 Sybase 语法。无论如何谢谢:-)
    • 我一定是查阅了错误的手册。你知道我在哪里可以在线找到语法吗?如果是这样,我会在适当的地方重写查询。
    【解决方案5】:

    另一种适用于 Sybase ASE 12.5.4 的方法。该表必须在 id 上具有聚集索引,才能使其正常工作。假设表名是 MYTABLE:

    declare @strNew varchar(10), @strOld varchar(10), @str1 varchar(1000), @str2 varchar(1000)
    set @str1 = NULL, @str2 = NULL, @strNew = NULL, @strOld = NULL
    
    UPDATE MYTABLE
    SET @strNew = convert(varchar,id) 
    , @str1 = case when @strNew = @strOld then @str1 + convert(varchar,id_type) + "," else @str1 +  '$' + @strNew + '$' + convert(varchar,id_type) + "," end  
    , @str2 = case when @strNew = @strOld then @str2 + convert(varchar,id_ref) + "," else @str2 + '$' + @strNew + '$' + convert(varchar,id_ref) + "," end
    , @strOld = convert(varchar,id) 
    
    
    select id, substring(@str1,charindex("$" + convert(varchar,id) + "$",@str1) + len("$" + convert(varchar,id) + "$"),
    case when
        charindex(",$",substring(@str1,charindex("$" + convert(varchar,id) + "$",@str1) + len("$" + convert(varchar,id) + "$") + 1,len(@str1)))
        = 0 then len(@str1) - (charindex("$" + convert(varchar,id) + "$",@str1) + len("$" + convert(varchar,id) + "$"))
    else
        charindex(",$",substring(@str1,charindex("$" + convert(varchar,id) + "$",@str1) + len("$" + convert(varchar,id) + "$") + 1,len(@str1)))
    end
    ) 
    + ':' + 
    substring(@str2,charindex("$" + convert(varchar,id) + "$",@str2) + len("$" + convert(varchar,id) + "$"),
    case when 
        charindex(",$",substring(@str2,charindex("$" + convert(varchar,id) + "$",@str2) + len("$" + convert(varchar,id) + "$") + 1,len(@str2)))
        = 0 then len(@str2) - (charindex("$" + convert(varchar,id) + "$",@str2) + len("$" + convert(varchar,id) + "$"))
    else
        charindex(",$",substring(@str2,charindex("$" + convert(varchar,id) + "$",@str2) + len("$" + convert(varchar,id) + "$") + 1,len(@str2)))
    end
    ) as concatenation
    from MYTABLE 
    group by id
    

    【讨论】:

    • @dsm 在更新语句后添加以下内容以查看它创建的字符串:print "%1!:%2!",@str1,@str2。随后的 select 语句根据id 的值提取这些字符串的适当子字符串。必须小心计算正确的长度。需要case 语句,因为在最后一个子字符串之后不再有,$,它表示子字符串的结尾。希望对您有所帮助。
    • 我已经对此进行了测试,它适用于小型 MYTABLE,但任意大的表会破坏它。开始显示如下结果:62126 $16,17,6,6,22:$11,5,11,28,1
    • @dsm 是的,我知道它不能用于大型 MYTABLE,因为解决方案受到 @str1 的长度限制,@str2 不能大于 varchar(16384)。这就是我提出另一种解决方案的原因。
    【解决方案6】:

    我现在能想到的最好的是下一个:

    select  a.id id,
            str (a.id_type,4,0)||
            ','||str (b.id_type,4,0)||
            ','||str (c.id_type,4,0)||
            ','||str (d.id_type,4,0)||
            ','||str (e.id_type,4,0)||':'||
            str (a.id_ref,4,0)||
            ','||str (b.id_ref,4,0)||
            ','||str (c.id_ref,4,0)||
            ','||str (d.id_ref,4,0)||
            ','||str (e.id_ref,4,0) concatenation
      from  dbo.merge_test a,
            dbo.merge_test b,
            dbo.merge_test c,
            dbo.merge_test d,
            dbo.merge_test e
    where a.id = b.id
    and a.id = b.id
    and a.id = c.id
    and a.id = d.id
    and a.id = e.id
    and a.id_type < b.id_type
    and b.id_type <c.id_type
    and c.id_type < d.id_type
    and d.id_type < e.id_type
    

    但是结果和你输入的有点不同……!!!

    【讨论】:

    • 太简单了,但不需要额外难的sql编码
    • 请使用“代码”块格式化您的答案以便于阅读。
    • 我尝试重新格式化您的代码,但没有成功。您的解决方案确实适用于这个特定示例,但现在假设我们希望多个 id 相同,或者重复 id_types
    【解决方案7】:

    这里有一个解决方案:

    SELECT DISTINCT
            id, 
            concatenation = LEFT(id_types, LEN(id_types) - 1) + ':' + LEFT(id_refs, LEN(id_refs) - 1) 
    FROM (
    SELECT  id, 
            id_types = (SELECT CAST(b.id_type AS nvarchar) + ',' FROM Table1 b WHERE b.id = a.id FOR XML PATH('')), 
            id_refs = (SELECT CAST(c.id_ref AS nvarchar) + ',' FROM Table1 c WHERE c.id = a.id FOR XML PATH('')) 
    FROM    Table1 a
    ) t
    

    更新:另一种方法

    ;WITH r(id, rnk, id_type, id_ref) AS 
    (
        SELECT  id, 
                rnk = ROW_NUMBER() OVER(ORDER BY id),
                id_type = CAST(id_type AS nvarchar(MAX)), 
                id_ref = CAST(id_ref AS nvarchar(MAX)) 
        FROM Table1
    ), anchor(id, rnk, id_type, id_ref) AS 
    (
        SELECT  id, 
                rnk, 
                id_type, 
                id_ref 
        FROM r
        WHERE rnk = 1
    ), result(id, rnk, id_type, id_ref) AS 
    (
        SELECT  id, 
                rnk, 
                id_type, 
                id_ref 
        FROM anchor
        UNION ALL 
        SELECT  r.id, 
                r.rnk, 
                result.id_type + ',' + r.id_type, 
                result.id_ref + ',' + r.id_ref 
        FROM r
        INNER JOIN result ON r.id = result.id AND r.rnk = result.rnk + 1 
    )
    SELECT id, concatenation = MAX(id_type) + ':' +  MAX(id_ref)
    FROM result
    GROUP BY id
    

    【讨论】:

    • 您使用的是什么版本的 Sybase?我似乎无法让“FOR XML ...”工作。我的版本是 ASE 12.5.4
    • 至于第二种方案,不是Oracle吗?
    • 我用 MSSQL 制作了这个解决方案,但第二个应该适用于 Sybase,据我发现它支持公用表表达式
    • @Oleg,我认为我们在这里陷入了旧的“不兼容的扩展”陷阱 :)
    • @dsm,也许,对不起,如果我误导了你。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-07-10
    • 1970-01-01
    • 1970-01-01
    • 2014-01-01
    • 1970-01-01
    • 2019-02-12
    • 2015-01-20
    相关资源
    最近更新 更多