Maneira simples de transpor colunas e linhas em SQL?


110

Como posso simplesmente alternar colunas com linhas no SQL? Existe algum comando simples para transpor?

ou seja, vire este resultado:

        Paul  | John  | Tim  |  Eric
Red     1       5       1       3
Green   8       4       3       5
Blue    2       2       9       1

nisso:

        Red  | Green | Blue
Paul    1       8       2
John    5       4       2
Tim     1       3       9
Eric    3       5       1

PIVOT parece muito complexo para este cenário.


1
stackoverflow.com/questions/10699997/… é uma pergunta um tanto semelhante.
Tim Dearborn de

Respostas:


145

Existem várias maneiras de transformar esses dados. Em sua postagem original, você afirmou que PIVOTparece muito complexo para este cenário, mas pode ser aplicado facilmente usando o UNPIVOTePIVOT funções do SQL Server.

No entanto, se você não tiver acesso a essas funções, isso pode ser replicado usando UNION ALLpara UNPIVOTe, em seguida, uma função agregada com uma CASEinstrução paraPIVOT :

Criar a tabela:

CREATE TABLE yourTable([color] varchar(5), [Paul] int, [John] int, [Tim] int, [Eric] int);

INSERT INTO yourTable
    ([color], [Paul], [John], [Tim], [Eric])
VALUES
    ('Red', 1, 5, 1, 3),
    ('Green', 8, 4, 3, 5),
    ('Blue', 2, 2, 9, 1);

Versão de união de todos, agregado e CASE:

select name,
  sum(case when color = 'Red' then value else 0 end) Red,
  sum(case when color = 'Green' then value else 0 end) Green,
  sum(case when color = 'Blue' then value else 0 end) Blue
from
(
  select color, Paul value, 'Paul' name
  from yourTable
  union all
  select color, John value, 'John' name
  from yourTable
  union all
  select color, Tim value, 'Tim' name
  from yourTable
  union all
  select color, Eric value, 'Eric' name
  from yourTable
) src
group by name

Vejo SQL Fiddle com Demo

O UNION ALLexecuta a eliminação UNPIVOTdos dados transformando as colunas Paul, John, Tim, Ericem linhas separadas. Em seguida, você aplica a função de agregação sum()com a caseinstrução para obter as novas colunas para cadacolor .

Versão Estática Unpivot e Pivot:

As funções UNPIVOTe PIVOTno servidor SQL tornam essa transformação muito mais fácil. Se você conhece todos os valores que deseja transformar, pode codificá-los em uma versão estática para obter o resultado:

select name, [Red], [Green], [Blue]
from
(
  select color, name, value
  from yourtable
  unpivot
  (
    value for name in (Paul, John, Tim, Eric)
  ) unpiv
) src
pivot
(
  sum(value)
  for color in ([Red], [Green], [Blue])
) piv

Vejo SQL Fiddle com Demo

A consulta interna com UNPIVOTexecuta a mesma função que UNION ALL. Ele pega a lista de colunas e a transforma em linhas, oPIVOT então realiza a transformação final em colunas.

Versão dinâmica do pivô:

Se você tiver um número desconhecido de colunas ( Paul, John, Tim, Ericem seu exemplo) e, em seguida, um número desconhecido de cores para transformar, você pode usar sql dinâmico para gerar a lista para UNPIVOTe então PIVOT:

DECLARE @colsUnpivot AS NVARCHAR(MAX),
    @query  AS NVARCHAR(MAX),
    @colsPivot as  NVARCHAR(MAX)

select @colsUnpivot = stuff((select ','+quotename(C.name)
         from sys.columns as C
         where C.object_id = object_id('yourtable') and
               C.name <> 'color'
         for xml path('')), 1, 1, '')

select @colsPivot = STUFF((SELECT  ',' 
                      + quotename(color)
                    from yourtable t
            FOR XML PATH(''), TYPE
            ).value('.', 'NVARCHAR(MAX)') 
        ,1,1,'')


set @query 
  = 'select name, '+@colsPivot+'
      from
      (
        select color, name, value
        from yourtable
        unpivot
        (
          value for name in ('+@colsUnpivot+')
        ) unpiv
      ) src
      pivot
      (
        sum(value)
        for color in ('+@colsPivot+')
      ) piv'

exec(@query)

Vejo SQL Fiddle com Demo

A versão dinâmica consulta ambos yourtablee a sys.columnstabela para gerar a lista de itens para UNPIVOTe PIVOT. Isso é então adicionado a uma string de consulta a ser executada. A vantagem da versão dinâmica é se você tem uma lista de alteração de colorse / ou namesisso irá gerar a lista em tempo de execução.

Todas as três consultas produzirão o mesmo resultado:

| NAME | RED | GREEN | BLUE |
-----------------------------
| Eric |   3 |     5 |    1 |
| John |   5 |     4 |    2 |
| Paul |   1 |     8 |    2 |
|  Tim |   1 |     3 |    9 |

Eu deveria ter enfatizado que quero um método para transpor os resultados de uma consulta em um banco de dados. Um comando de tipo de transposição simples que pode ser executado no conjunto de resultados de uma consulta sem a necessidade de saber nada sobre a consulta em si, ou as colunas, tabelas etc. de onde extrai informações. Algo como: (TRANSPOSE (SELECT ......)) No meu exemplo dado acima, imagine que a primeira tabela é um conjunto de resultados gerado a partir de uma consulta muito complexa envolvendo várias tabelas, uniões, pivôs etc. No entanto, o resultado é um mesa simples. Eu só quero inverter as colunas com linhas neste resultado.
edezzie

Estou passando o resultado para c # DataTable. Posso construir um método simples 'Transpose (Datatable dt)' para fazer isso lá, mas há algo em SQL para fazer isso.
edezzie

@edezzie, não existe uma maneira simples no SQL de fazer isso. Você provavelmente poderia alterar a versão dinâmica para passar o nome da tabela e obter as informações das tabelas do sistema.
Taryn

Existe algum motivo pelo qual você não sinalizou esta resposta brilhante como a RESPOSTA? Dê uma medalha a @bluefeet!
Fandango68 de

Isso está funcionando bem, selecione * na tabela unpivot (valor para o nome em ([Paul], [John], [Tim], [Eric])) pivô para cima (max (valor) para a cor em ([Red], [Green] , [Azul])) p mas é possível escrever este select * da tabela unpivot (valor para o nome em ([Paul], [John], [Tim], [Eric])) pivô superior (max (valor) para color in (SELECT COLOR FROM TABLENAME)) p em vez de inserir o valor entre aspas não pode recuperar os valores diretamente usando a consulta selecionada.
Mogli,

20

Isso normalmente requer que você conheça TODOS os rótulos de coluna E linha de antemão. Como você pode ver na consulta abaixo, os rótulos estão todos listados inteiramente nas operações UNPIVOT e (re) PIVOT.

Configuração do esquema do MS SQL Server 2012 :

create table tbl (
    color varchar(10), Paul int, John int, Tim int, Eric int);
insert tbl select 
    'Red' ,1 ,5 ,1 ,3 union all select
    'Green' ,8 ,4 ,3 ,5 union all select
    'Blue' ,2 ,2 ,9 ,1;

Consulta 1 :

select *
from tbl
unpivot (value for name in ([Paul],[John],[Tim],[Eric])) up
pivot (max(value) for color in ([Red],[Green],[Blue])) p

Resultados :

| NAME | RED | GREEN | BLUE |
-----------------------------
| Eric |   3 |     5 |    1 |
| John |   5 |     4 |    2 |
| Paul |   1 |     8 |    2 |
|  Tim |   1 |     3 |    9 |

Notas Adicionais:

  1. Dado um nome de tabela, você pode determinar todos os nomes de coluna de sys.columns ou truques FOR XML usando local-name () .
  2. Você também pode construir a lista de cores distintas (ou valores para uma coluna) usando FOR XML.
  3. Os itens acima podem ser combinados em um lote sql dinâmico para lidar com qualquer tabela.

11

Eu gostaria de apontar mais algumas soluções para transpor colunas e linhas em SQL.

O primeiro é - usando o CURSOR. Embora o consenso geral na comunidade profissional seja ficar longe dos cursores do SQL Server, ainda há casos em que o uso de cursores é recomendado. De qualquer forma, os cursores nos apresentam outra opção para transpor linhas em colunas.

  • Expansão vertical

    Semelhante ao PIVOT, o cursor tem a capacidade dinâmica de anexar mais linhas à medida que seu conjunto de dados se expande para incluir mais números de política.

  • Expansão horizontal

    Ao contrário do PIVOT, o cursor se destaca nesta área, pois é capaz de se expandir para incluir o documento recém-adicionado, sem alterar o script.

  • Análise de desempenho

    A principal limitação de transpor linhas em colunas usando CURSOR é uma desvantagem associada ao uso de cursores em geral - eles têm um custo de desempenho significativo. Isso ocorre porque o Cursor gera uma consulta separada para cada operação FETCH NEXT.

Outra solução para transpor linhas em colunas é usar XML.

A solução XML para transpor linhas em colunas é basicamente uma versão ideal do PIVOT, pois trata da limitação da coluna dinâmica.

A versão XML do script aborda essa limitação usando uma combinação de Caminho XML, T-SQL dinâmico e algumas funções embutidas (isto é, STUFF, QUOTENAME).

  • Expansão vertical

    Semelhante ao PIVOT e ao Cursor, as políticas recém-adicionadas podem ser recuperadas na versão XML do script sem alterar o script original.

  • Expansão horizontal

    Ao contrário do PIVOT, os documentos recém-adicionados podem ser exibidos sem alterar o script.

  • Análise de desempenho

    Em termos de IO, as estatísticas da versão XML do script são quase semelhantes às do PIVOT - a única diferença é que o XML tem uma segunda varredura da tabela dtTranspose, mas desta vez de um cache lógico de leitura de dados.

Você pode encontrar mais informações sobre essas soluções (incluindo alguns exemplos reais de T-SQL) neste artigo: https://www.sqlshack.com/multiple-options-to-transposing-rows-into-columns/


8

Com base nesta solução do bluefeet, aqui está um procedimento armazenado que usa sql dinâmico para gerar a tabela transposta. Exige que todos os campos sejam numéricos, exceto para a coluna transposta (a coluna que será o cabeçalho da tabela resultante):

/****** Object:  StoredProcedure [dbo].[SQLTranspose]    Script Date: 11/10/2015 7:08:02 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:      Paco Zarate
-- Create date: 2015-11-10
-- Description: SQLTranspose dynamically changes a table to show rows as headers. It needs that all the values are numeric except for the field using for     transposing.
-- Parameters: @TableName - Table to transpose
--             @FieldNameTranspose - Column that will be the new headers
-- Usage: exec SQLTranspose <table>, <FieldToTranspose>
-- =============================================
ALTER PROCEDURE [dbo].[SQLTranspose] 
  -- Add the parameters for the stored procedure here
  @TableName NVarchar(MAX) = '', 
  @FieldNameTranspose NVarchar(MAX) = ''
AS
BEGIN
  -- SET NOCOUNT ON added to prevent extra result sets from
  -- interfering with SELECT statements.
  SET NOCOUNT ON;

  DECLARE @colsUnpivot AS NVARCHAR(MAX),
  @query  AS NVARCHAR(MAX),
  @queryPivot  AS NVARCHAR(MAX),
  @colsPivot as  NVARCHAR(MAX),
  @columnToPivot as NVARCHAR(MAX),
  @tableToPivot as NVARCHAR(MAX), 
  @colsResult as xml

  select @tableToPivot = @TableName;
  select @columnToPivot = @FieldNameTranspose


  select @colsUnpivot = stuff((select ','+quotename(C.name)
       from sys.columns as C
       where C.object_id = object_id(@tableToPivot) and
             C.name <> @columnToPivot 
       for xml path('')), 1, 1, '')

  set @queryPivot = 'SELECT @colsResult = (SELECT  '','' 
                    + quotename('+@columnToPivot+')
                  from '+@tableToPivot+' t
                  where '+@columnToPivot+' <> ''''
          FOR XML PATH(''''), TYPE)'

  exec sp_executesql @queryPivot, N'@colsResult xml out', @colsResult out

  select @colsPivot = STUFF(@colsResult.value('.', 'NVARCHAR(MAX)'),1,1,'')

  set @query 
    = 'select name, rowid, '+@colsPivot+'
        from
        (
          select '+@columnToPivot+' , name, value, ROW_NUMBER() over (partition by '+@columnToPivot+' order by '+@columnToPivot+') as rowid
          from '+@tableToPivot+'
          unpivot
          (
            value for name in ('+@colsUnpivot+')
          ) unpiv
        ) src
        pivot
        (
          sum(value)
          for '+@columnToPivot+' in ('+@colsPivot+')
        ) piv
        order by rowid'
  exec(@query)
END

Você pode testá-lo com a tabela fornecida com este comando:

exec SQLTranspose 'yourTable', 'color'

argg. mas eu quero um em que todos os campos sejam nvarchar (máx.)
hamish

1
Para todos os campos nvarchar (max) apenas altere a soma (valor) com max (valor) na 6ª linha a partir do final
Paco Zarate

2

Estou fazendo UnPivotprimeiro e armazenando os resultados CTEe usando o CTEemPivot operação .

Demo

with cte as
(
select 'Paul' as Name, color, Paul as Value 
 from yourTable
 union all
 select 'John' as Name, color, John as Value 
 from yourTable
 union all
 select 'Tim' as Name, color, Tim as Value 
 from yourTable
 union all
 select 'Eric' as Name, color, Eric as Value 
 from yourTable
 )

select Name, [Red], [Green], [Blue]
from
(
select *
from cte
) as src
pivot 
(
  max(Value)
  for color IN ([Red], [Green], [Blue])
) as Dtpivot;

2

Acrescentando à excelente resposta de @Paco Zarate acima, se você quiser transpor uma tabela que tem vários tipos de colunas, adicione isso ao final da linha 39, para que transponha apenas intcolunas:

and C.system_type_id = 56   --56 = type int

Aqui está a consulta completa que está sendo alterada:

select @colsUnpivot = stuff((select ','+quotename(C.name)
from sys.columns as C
where C.object_id = object_id(@tableToPivot) and
      C.name <> @columnToPivot and C.system_type_id = 56    --56 = type int
for xml path('')), 1, 1, '')

Para encontrar outros system_type_id, execute este:

select name, system_type_id from sys.types order by name

1

Gosto de compartilhar o código que estou usando para transpor um texto dividido com base na resposta do + bluefeet. Nesta abordagem, estou implementado como um procedimento no MS SQL 2005

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:      ELD.
-- Create date: May, 5 2016.
-- Description: Transpose from rows to columns the user split function.
-- =============================================
CREATE PROCEDURE TransposeSplit @InputToSplit VARCHAR(8000)
    ,@Delimeter VARCHAR(8000) = ','
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @colsUnpivot AS NVARCHAR(MAX)
        ,@query AS NVARCHAR(MAX)
        ,@queryPivot AS NVARCHAR(MAX)
        ,@colsPivot AS NVARCHAR(MAX)
        ,@columnToPivot AS NVARCHAR(MAX)
        ,@tableToPivot AS NVARCHAR(MAX)
        ,@colsResult AS XML

    SELECT @tableToPivot = '#tempSplitedTable'

    SELECT @columnToPivot = 'col_number'

    CREATE TABLE #tempSplitedTable (
        col_number INT
        ,col_value VARCHAR(8000)
        )

    INSERT INTO #tempSplitedTable (
        col_number
        ,col_value
        )
    SELECT ROW_NUMBER() OVER (
            ORDER BY (
                    SELECT 100
                    )
            ) AS RowNumber
        ,item
    FROM [DB].[ESCHEME].[fnSplit](@InputToSplit, @Delimeter)

    SELECT @colsUnpivot = STUFF((
                SELECT ',' + quotename(C.NAME)
                FROM [tempdb].sys.columns AS C
                WHERE C.object_id = object_id('tempdb..' + @tableToPivot)
                    AND C.NAME <> @columnToPivot
                FOR XML path('')
                ), 1, 1, '')

    SET @queryPivot = 'SELECT @colsResult = (SELECT  '','' 
                    + quotename(' + @columnToPivot + ')
                  from ' + @tableToPivot + ' t
                  where ' + @columnToPivot + ' <> ''''
          FOR XML PATH(''''), TYPE)'

    EXEC sp_executesql @queryPivot
        ,N'@colsResult xml out'
        ,@colsResult OUT

    SELECT @colsPivot = STUFF(@colsResult.value('.', 'NVARCHAR(MAX)'), 1, 1, '')

    SET @query = 'select name, rowid, ' + @colsPivot + '
        from
        (
          select ' + @columnToPivot + ' , name, value, ROW_NUMBER() over (partition by ' + @columnToPivot + ' order by ' + @columnToPivot + ') as rowid
          from ' + @tableToPivot + '
          unpivot
          (
            value for name in (' + @colsUnpivot + ')
          ) unpiv
        ) src
        pivot
        (
          MAX(value)
          for ' + @columnToPivot + ' in (' + @colsPivot + ')
        ) piv
        order by rowid'

    EXEC (@query)

    DROP TABLE #tempSplitedTable
END
GO

Estou misturando esta solução com as informações sobre como ordenar as linhas sem ordenar por ( SQLAuthority.com ) e a função de divisão no MSDN ( social.msdn.microsoft.com )

Quando você executa o produto

DECLARE @RC int
DECLARE @InputToSplit varchar(MAX)
DECLARE @Delimeter varchar(1)

set @InputToSplit = 'hello|beautiful|world'
set @Delimeter = '|'

EXECUTE @RC = [TransposeSplit] 
   @InputToSplit
  ,@Delimeter
GO

você obtém o próximo resultado

  name       rowid  1      2          3
  col_value  1      hello  beautiful  world

1

Desta forma, converta todos os dados de Filelds (colunas) na tabela para registro (linha).

Declare @TableName  [nvarchar](128)
Declare @ExecStr    nvarchar(max)
Declare @Where      nvarchar(max)
Set @TableName = 'myTableName'
--Enter Filtering If Exists
Set @Where = ''

--Set @ExecStr = N'Select * From '+quotename(@TableName)+@Where
--Exec(@ExecStr)

Drop Table If Exists #tmp_Col2Row

Create Table #tmp_Col2Row
(Field_Name nvarchar(128) Not Null
,Field_Value nvarchar(max) Null
)

Set @ExecStr = N' Insert Into #tmp_Col2Row (Field_Name , Field_Value) '
Select @ExecStr += (Select N'Select '''+C.name+''' ,Convert(nvarchar(max),'+quotename(C.name) + ') From ' + quotename(@TableName)+@Where+Char(10)+' Union All '
         from sys.columns as C
         where (C.object_id = object_id(@TableName)) 
         for xml path(''))
Select @ExecStr = Left(@ExecStr,Len(@ExecStr)-Len(' Union All '))
--Print @ExecStr
Exec (@ExecStr)

Select * From #tmp_Col2Row
Go

0

Consegui usar a solução de Paco Zarate e ela funciona perfeitamente. Tive de adicionar uma linha ("SET ANSI_WARNINGS ON"), mas pode ser algo exclusivo da maneira como usei ou chamei. Há um problema com meu uso e espero que alguém possa me ajudar com isso:

A solução funciona apenas com uma tabela SQL real. Eu tentei com uma tabela temporária e também uma tabela na memória (declarada), mas não funciona com elas. Portanto, em meu código de chamada, crio uma tabela em meu banco de dados SQL e chamo SQLTranspose. Novamente, funciona muito bem. É exatamente o que eu quero. Aqui está o meu problema:

Para que a solução geral seja realmente dinâmica, preciso criar aquela tabela onde armazeno temporariamente as informações preparadas que estou enviando para SQLTranspose "em tempo real" e, em seguida, excluo essa tabela quando SQLTranspose é chamado. A exclusão da tabela está apresentando um problema com meu plano de implementação final. O código precisa ser executado a partir de um aplicativo de usuário final (um botão em um formulário / menu do Microsoft Access). Quando eu uso este processo SQL (criar uma tabela SQL, chamar SQLTranspose, excluir tabela SQL), o aplicativo do usuário final atinge um erro porque a conta SQL usada não tem o direito de eliminar uma tabela.

Portanto, acho que existem algumas soluções possíveis:

  1. Encontre uma maneira de fazer SQLTranspose funcionar com uma tabela temporária ou uma variável de tabela declarada.

  2. Descubra outro método para a transposição de linhas e colunas que não exija uma tabela SQL real.

  3. Descobrir um método apropriado para permitir que a conta SQL usada por meus usuários finais elimine uma tabela. É uma única conta SQL compartilhada codificada em meu aplicativo Access. Parece que a permissão é um privilégio do tipo dbo que não pode ser concedido.

Reconheço que parte disso pode justificar outro tópico e pergunta separados. Porém, como existe a possibilidade de que uma solução possa ser simplesmente uma maneira diferente de fazer a transposição de linhas e colunas, farei meu primeiro post aqui neste tópico.

EDIT: Eu também substituí sum (valor) por max (valor) na 6ª linha do final, como Paco sugeriu.

EDITAR:

Eu descobri algo que funciona para mim. Não sei se é a melhor resposta ou não.

Tenho uma conta de usuário somente leitura que é usada para executar procedimentos armazenados e, portanto, gerar saída de relatório de um banco de dados. Como a função SQLTranspose que criei só funcionará com uma tabela "legítima" (não uma tabela declarada e não uma tabela temporária), tive que descobrir uma maneira de uma conta de usuário somente leitura criar (e depois excluir) uma tabela .

Concluí que, para meus objetivos, não há problema em que a conta do usuário tenha permissão para criar uma tabela. O usuário ainda não conseguiu excluir a tabela. Minha solução foi criar um esquema onde a conta do usuário é autorizada. Então, sempre que eu criar, usar ou excluir essa tabela, refiro-a com o esquema especificado.

Emiti primeiro este comando de uma conta 'sa' ou 'sysadmin': CREATE SCHEMA ro AUTHORIZATION

Sempre que me refiro à minha tabela "tmpoutput", eu a especifico como este exemplo:

drop table ro.tmpoutput


Eu descobri algo que funciona para mim. Não sei se é a melhor resposta ou não.
CraigD de
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.