Prosty sposób na transpozycję kolumn i wierszy w SQL?

110

Jak po prostu zamienić kolumny z wierszami w języku SQL? Czy jest jakieś proste polecenie do transpozycji?

czyli włącz ten wynik:

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

zaangażowany w to:

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

PIVOT wydaje się zbyt skomplikowany dla tego scenariusza.

edezzie
źródło
1
stackoverflow.com/questions/10699997/… to trochę podobne pytanie.
Tim Dearborn

Odpowiedzi:

145

Dane te można przekształcić na kilka sposobów. W swoim oryginalnym poście stwierdziłeś, że PIVOTwydaje się to zbyt skomplikowane dla tego scenariusza, ale można go bardzo łatwo zastosować za pomocą funkcji UNPIVOTiPIVOT w SQL Server.

Jeśli jednak nie masz dostępu do tych funkcji, można to zreplikować za pomocą UNION ALLdo, UNPIVOTa następnie funkcji agregującej z CASEinstrukcją do PIVOT:

Utwórz tabelę:

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);

Wersja Union All, Aggregate i 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

Zobacz SQL Fiddle with Demo

Do UNION ALLwykonuje UNPIVOTdanych poprzez przekształcenie kolumny Paul, John, Tim, Ericw osobnych wierszach. Następnie stosujesz funkcję agregującą sum()z caseinstrukcją, aby uzyskać nowe kolumny dla każdej z nich color.

Wersja statyczna Unpivot i Pivot:

Zarówno UNPIVOTi PIVOTfunkcje w serwerze SQL dokonać tej przemiany jest znacznie łatwiejsze. Jeśli znasz wszystkie wartości, które chcesz przekształcić, możesz zakodować je na stałe do wersji statycznej, aby uzyskać wynik:

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

Zobacz SQL Fiddle with Demo

Zapytanie wewnętrzne z UNPIVOToperatorem spełnia tę samą funkcję co UNION ALL. Pobiera listę kolumn i zamienia ją na wiersze, a PIVOTnastępnie wykonuje ostateczną transformację w kolumny.

Wersja Dynamic Pivot:

Jeśli masz nieznaną liczbę kolumn ( Paul, John, Tim, Ericw twoim przykładzie), a następnie nieznaną liczbę kolorów do przekształcenia, możesz użyć dynamicznego sql do wygenerowania listy, UNPIVOTa następnie 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)

Zobacz SQL Fiddle with Demo

Wersja dynamiczna wysyła zapytanie zarówno yourtabledo sys.columnstabeli, jak i do tabeli, aby wygenerować listę elementów do UNPIVOTi PIVOT. To jest następnie dodawane do ciągu zapytania do wykonania. Zaletą wersji dynamicznej jest to, że masz listę zmian colorsi / lub namesspowoduje to wygenerowanie listy w czasie wykonywania.

Wszystkie trzy zapytania dadzą ten sam wynik:

| NAME | RED | GREEN | BLUE |
-----------------------------
| Eric |   3 |     5 |    1 |
| John |   5 |     4 |    2 |
| Paul |   1 |     8 |    2 |
|  Tim |   1 |     3 |    9 |
Taryn
źródło
Powinienem był podkreślić, że potrzebuję metody transponowania wyników zapytania w bazie danych. Proste polecenie typu transpozycji, które można wykonać na zestawie wyników z zapytania bez konieczności posiadania jakiejkolwiek wiedzy o samym zapytaniu, kolumnach, tabelach itp., Z których pobiera informacje. Coś w rodzaju: (TRANSPOSE (SELECT ......)) W moim przykładzie podanym powyżej wyobraź sobie, że pierwsza tabela jest zbiorem wyników wygenerowanym z bardzo złożonego zapytania obejmującego wiele tabel, związków, przestawne itp. Jednak wynik jest prosty stół. Chcę tylko odwrócić kolumny z wierszami w tym wyniku.
edezzie
Wynik przekazuję do ac # DataTable. Mogę zbudować prostą metodę „Transpose (Datatable dt)”, aby to zrobić, ale czy w SQL jest coś, co mogłoby to zrobić.
edezzie
@edezzie w SQL nie ma na to prostego sposobu. Prawdopodobnie mógłbyś zmienić wersję dynamiczną tak, aby przekazywała nazwę tabeli i pobierała informacje z tabel systemowych.
Taryn
Czy jest jakiś powód, dla którego nie oznaczyłeś tej genialnej odpowiedzi jako ODPOWIEDŹ? Daj @bluefeet medal!
Fandango68,
To działa dobrze wybierz * z tabeli unpivot (wartość dla nazwy w ([Paul], [John], [Tim], [Eric])) w górę pivot (max (value) for color in ([Red], [Green]) , [Blue])) p, ale czy można zapisać ten select * from Table unpivot (wartość dla imienia w ([Paul], [John], [Tim], [Eric])) up pivot (max (value) for color in (SELECT COLOR FROM TABLENAME)) p zamiast wprowadzania wartości w cudzysłowie nie może pobrać wartości bezpośrednio za pomocą zapytania wybierającego.
Mogli
20

Zwykle wymaga to wcześniejszej znajomości WSZYSTKICH kolumn ORAZ etykiet wierszy. Jak widać w poniższym zapytaniu, wszystkie etykiety są wymienione w całości zarówno w operacjach UNPIVOT, jak i (re) PIVOT.

Konfiguracja schematu 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;

Zapytanie 1 :

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

Wyniki :

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

Dodatkowe uwagi:

  1. Mając nazwę tabeli, możesz określić wszystkie nazwy kolumn z sys.columns lub oszustwa FOR XML za pomocą local-name () .
  2. Możesz także utworzyć listę różnych kolorów (lub wartości dla jednej kolumny) za pomocą FOR XML.
  3. Powyższe można połączyć w dynamiczną partię sql do obsługi dowolnej tabeli.
RichardTheKiwi
źródło
11

Chciałbym wskazać jeszcze kilka rozwiązań transpozycji kolumn i wierszy w SQL.

Pierwsza to - użycie CURSORA. Chociaż ogólna zgoda społeczności zawodowej polega na trzymaniu się z daleka od kursorów SQL Server, nadal istnieją przypadki, w których zalecane jest użycie kursorów. W każdym razie Cursors daje nam inną opcję transpozycji wierszy na kolumny.

  • Ekspansja pionowa

    Podobnie jak PIVOT, kursor ma dynamiczną możliwość dołączania większej liczby wierszy, gdy zestaw danych rozszerza się, aby objąć więcej numerów zasad.

  • Ekspansja pozioma

    W przeciwieństwie do PIVOT, kursor wyróżnia się w tym obszarze, ponieważ może rozszerzać się, aby objąć nowo dodany dokument, bez zmiany skryptu.

  • Zestawienie wydajności

    Głównym ograniczeniem transpozycji wierszy do kolumn za pomocą CURSOR jest wada związana z ogólnym użyciem kursorów - wiąże się to ze znacznym kosztem wydajności. Dzieje się tak, ponieważ Cursor generuje oddzielne zapytanie dla każdej operacji FETCH NEXT.

Innym sposobem transpozycji wierszy do kolumn jest użycie XML.

Rozwiązanie XML do transpozycji wierszy na kolumny jest w zasadzie optymalną wersją PIVOT, ponieważ rozwiązuje dynamiczne ograniczenie kolumn.

Wersja XML skryptu rozwiązuje to ograniczenie, używając kombinacji ścieżki XML, dynamicznego języka T-SQL i niektórych wbudowanych funkcji (np. STUFF, QUOTENAME).

  • Ekspansja pionowa

    Podobnie jak w przypadku PIVOT i Cursor, nowo dodane polityki można pobrać w wersji XML skryptu bez zmiany oryginalnego skryptu.

  • Ekspansja pozioma

    W przeciwieństwie do PIVOT, nowo dodane dokumenty mogą być wyświetlane bez zmiany skryptu.

  • Zestawienie wydajności

    Jeśli chodzi o IO, statystyki wersji XML skryptu są prawie podobne do PIVOT - jedyną różnicą jest to, że XML ma drugie skanowanie tabeli dtTranspose, ale tym razem z logicznego odczytu - pamięci podręcznej danych.

Więcej informacji na temat tych rozwiązań (w tym kilka rzeczywistych przykładów T-SQL) można znaleźć w tym artykule: https://www.sqlshack.com/multiple-options-to-transposing-rows-into-columns/

MikeS
źródło
8

Na podstawie tego rozwiązania od bluefeet jest tutaj procedura składowana, która używa dynamicznego sql do generowania transponowanej tabeli. Wymaga, aby wszystkie pola były numeryczne z wyjątkiem transponowanej kolumny (kolumny, która będzie nagłówkiem w wynikowej tabeli):

/****** 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

Możesz to przetestować za pomocą tabeli dostarczonej z tym poleceniem:

exec SQLTranspose 'yourTable', 'color'
Paco Zarate
źródło
argg. ale chcę taki, w którym wszystkie pola są nvarchar (max)
hamish
1
Dla wszystkich pól nvarchar (max) po prostu zmień sumę (wartość) z max (wartość) w szóstej linii od końca
Paco Zarate
2

Robię UnPivotpierwszy i przechowywania wyników w CTEi pomocą CTEw Pivotpracy.

Próbny

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;
Vishwanath Dalvi
źródło
2

Dodając do wspaniałej odpowiedzi @Paco Zarate powyżej, jeśli chcesz transponować tabelę, która ma wiele typów kolumn, dodaj to na końcu linii 39, aby transponowała tylko intkolumny:

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

Oto pełne zapytanie, które jest zmieniane:

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, '')

Aby znaleźć inne system_type_id, uruchom to:

select name, system_type_id from sys.types order by name
Jonathan Harris
źródło
1

Lubię udostępniać kod, którego używam do transpozycji podzielonego tekstu na podstawie odpowiedzi + bluefeet. W tym podejściu zostałem zaimplementowany jako procedura w 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

Łączę to rozwiązanie z informacjami o tym, jak porządkować wiersze bez kolejności według ( SQLAuthority.com ) i funkcją podziału na MSDN ( social.msdn.microsoft.com )

Kiedy wykonujesz procedurę

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

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

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

uzyskasz następny wynik

  name       rowid  1      2          3
  col_value  1      hello  beautiful  world
Elidio Marquina
źródło
1

W ten sposób przekonwertuj wszystkie dane z plików (kolumn) w tabeli na rekord (wiersz).

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
Mahmood Khezrian
źródło
0

Udało mi się zastosować rozwiązanie Paco Zarate i działa pięknie. Musiałem dodać jedną linię („SET ANSI_WARNINGS ON”), ale może to być coś unikalnego w sposobie, w jaki go użyłem lub nazwałem. Wystąpił problem z moim użytkowaniem i mam nadzieję, że ktoś może mi w tym pomóc:

Rozwiązanie działa tylko z rzeczywistą tabelą SQL. Wypróbowałem to z tabelą tymczasową, a także tabelą w pamięci (zadeklarowaną), ale nie działa z nimi. Więc w moim kodzie wywołującym tworzę tabelę w mojej bazie danych SQL, a następnie wywołuję SQLTranspose. Znowu działa świetnie. Właśnie tego chcę. Oto mój problem:

Aby całe rozwiązanie było naprawdę dynamiczne, muszę utworzyć tabelę, w której tymczasowo przechowuję przygotowane informacje, które wysyłam do SQLTranspose „w locie”, a następnie usunąć tę tabelę po wywołaniu SQLTranspose. Usunięcie tabeli stanowi problem z moim ostatecznym planem wdrożenia. Kod musi być uruchamiany z aplikacji użytkownika końcowego (przycisk w formularzu / menu programu Microsoft Access). Kiedy używam tego procesu SQL (tworzenie tabeli SQL, wywołanie SQLTranspose, usuwanie tabeli SQL), aplikacja użytkownika końcowego napotyka błąd, ponieważ używane konto SQL nie ma uprawnień do usuwania tabeli.

Więc myślę, że jest kilka możliwych rozwiązań:

  1. Znajdź sposób, aby SQLTranspose działał z tabelą tymczasową lub zadeklarowaną zmienną tabeli.

  2. Wymyśl inną metodę transpozycji wierszy i kolumn, która nie wymaga rzeczywistej tabeli SQL.

  3. Wymyśl odpowiednią metodę zezwalania kontu SQL używanemu przez moich użytkowników końcowych na upuszczenie tabeli. To pojedyncze współdzielone konto SQL zakodowane w mojej aplikacji Access. Wygląda na to, że uprawnienie jest uprawnieniem typu dbo, którego nie można nadać.

Zdaję sobie sprawę, że niektóre z tego mogą uzasadniać inny, oddzielny wątek i pytanie. Jednakże, ponieważ istnieje możliwość, że jednym rozwiązaniem może być po prostu inny sposób na transpozycję wierszy i kolumn, zrobię mój pierwszy post tutaj w tym wątku.

EDYCJA: Zamieniłem również sumę (wartość) na max (wartość) w szóstej linii od końca, jak zasugerował Paco.

EDYTOWAĆ:

Wymyśliłem coś, co mi pasuje. Nie wiem, czy to najlepsza odpowiedź, czy nie.

Mam konto użytkownika tylko do odczytu, które jest używane do wykonywania procedur strored, a tym samym generowania danych wyjściowych raportu z bazy danych. Ponieważ utworzona przeze mnie funkcja SQLTranspose będzie działać tylko z "legalną" tabelą (nie z zadeklarowaną tabelą ani z tabelą tymczasową) Musiałem znaleźć sposób na utworzenie (a następnie późniejsze usunięcie) konta użytkownika tylko do odczytu .

Doszedłem do wniosku, że dla moich celów jest w porządku, aby konto użytkownika mogło utworzyć tabelę. Jednak użytkownik nadal nie mógł usunąć tabeli. Moim rozwiązaniem było stworzenie schematu, w którym konto użytkownika jest autoryzowane. Następnie za każdym razem, gdy tworzę, używam lub usuwam tę tabelę, odwołuję się do niej z określonym schematem.

Najpierw wydałem to polecenie z konta „sa” lub „sysadmin”: TWORZENIE SCHEMATU ro AUTORYZACJA

Kiedy kiedykolwiek odwołuję się do mojej tabeli „tmpoutput”, określam ją w następujący sposób:

drop table ro.tmpoutput

CraigD
źródło
Wymyśliłem coś, co mi pasuje. Nie wiem, czy to najlepsza odpowiedź, czy nie.
CraigD