Come posso usare un SQL Pivot per questo?
-
08-07-2019 - |
Domanda
Ho un set di dati organizzato nel modo seguente:
Timestamp|A0001|A0002|A0003|A0004|B0001|B0002|B0003|B0004 ...
---------+-----+-----+-----+-----+-----+-----+-----+-----
2008-1-1 | 1 | 2 | 10 | 6 | 20 | 35 | 300 | 8
2008-1-2 | 5 | 2 | 9 | 3 | 50 | 38 | 290 | 2
2008-1-4 | 7 | 7 | 11 | 0 | 30 | 87 | 350 | 0
2008-1-5 | 1 | 9 | 1 | 0 | 25 | 100 | 10 | 0
...
Dove A0001 è il valore A dell'articolo n. 1 e B0001 è il valore B dell'articolo n. 1. In una tabella possono essere presenti più di 60 articoli diversi e ogni articolo ha una colonna di valore A e una colonna di valore B, che significa un totale di oltre 120 colonne nella tabella.
Dove voglio arrivare è un risultato di 3 colonne (indice articolo, valore A, valore B) che somma i valori A e B per ogni articolo:
Index | A Value | B Value
------+---------+--------
0001 | 14 | 125
0002 | 20 | 260
0003 | 31 | 950
0004 | 9 | 10
....
Mentre vado dalle colonne alle righe mi aspetterei un perno nella soluzione, ma non sono sicuro di come risolverlo. Parte del problema è come eliminare A e B per formare i valori per la colonna Indice. L'altra parte è che non ho mai dovuto usare un Pivot prima, quindi mi sto imbattendo anche nella sintassi di base.
Penso che alla fine ho bisogno di avere una soluzione multi-passo che costruisca prima le somme come:
ColName | Value
--------+------
A0001 | 14
A0002 | 20
A0003 | 31
A0004 | 9
B0001 | 125
B0002 | 260
B0003 | 950
B0004 | 10
Quindi modificare i dati ColName per eliminare l'indice:
ColName | Value | Index | Aspect
--------+-------+-------+-------
A0001 | 14 | 0001 | A
A0002 | 20 | 0002 | A
A0003 | 31 | 0003 | A
A0004 | 9 | 0004 | A
B0001 | 125 | 0001 | B
B0002 | 260 | 0002 | B
B0003 | 950 | 0003 | B
B0004 | 10 | 0004 | B
Infine self join per spostare i valori B in alto accanto ai valori A.
Questo sembra essere un processo lungo e tortuoso per ottenere ciò che voglio. Quindi sto chiedendo consiglio se sto seguendo la strada giusta, o c'è un altro approccio che ho guardato che renderà la mia vita molto più semplice.
Nota 1) La soluzione deve essere in T-SQL su MSSQL 2005.
Nota 2) Il formato della tabella non può essere modificato.
Modifica Un altro metodo a cui ho pensato utilizza UNION e singoli SUM () su ogni colonna:
SELECT '0001' as Index, SUM(A0001) as A, SUM(B0001) as B FROM TABLE
UNION
SELECT '0002' as Index, SUM(A0002) as A, SUM(B0002) as B FROM TABLE
UNION
SELECT '0003' as Index, SUM(A0003) as A, SUM(B0003) as B FROM TABLE
UNION
SELECT '0004' as Index, SUM(A0004) as A, SUM(B0004) as B FROM TABLE
UNION
...
Ma questo approccio non sembra neanche molto carino
MODIFICA Finora ci sono 2 ottime risposte. Ma vorrei aggiungere altre due condizioni alla query :-)
1) Devo selezionare le righe in base a un intervallo di timestamp (minv < timestamp < maxv).
2) Devo anche selezionare in modo condizionale le righe su un UDF che elabora il timestamp
Usando i nomi delle tabelle di Brettski, si tradurrebbe in:
...
(SELECT A0001, A0002, A0003, B0001, B0002, B0003
FROM ptest
WHERE timestamp>minv AND timestamp<maxv AND fn(timestamp)=fnv) p
unpivot
(val for item in (A0001, A0002, A0003, B0001, B0002, B0003)) as unpvt
...
Dato che ho aggiunto condizionalmente il requisito fn (), penso che dovrei anche seguire il percorso SQL dinamico come proposto da Jonathon. Soprattutto perché devo creare la stessa query per 12 tabelle diverse, tutte dello stesso stile.
Soluzione
Stessa risposta qui, è stato divertente:
-- Get column names from system table
DECLARE @phCols NVARCHAR(2000)
SELECT @phCols = COALESCE(@phCols + ',[' + name + ']', '[' + name + ']')
FROM syscolumns WHERE id = (select id from sysobjects where name = 'Test' and type='U')
-- Get rid of the column we don't want
SELECT @phCols = REPLACE(@phCols, '[Timestamp],', '')
-- Query & sum using the dynamic column names
DECLARE @exec nvarchar(2000)
SELECT @exec =
'
select
SUBSTRING([Value], 2, LEN([Value]) - 1) as [Index],
SUM(CASE WHEN (LEFT([Value], 1) = ''A'') THEN Cols ELSE 0 END) as AValue,
SUM(CASE WHEN (LEFT([Value], 1) = ''B'') THEN Cols ELSE 0 END) as BValue
FROM
(
select *
from (select ' + @phCols + ' from Test) as t
unpivot (Cols FOR [Value] in (' + @phCols + ')) as p
) _temp
GROUP BY SUBSTRING([Value], 2, LEN([Value]) - 1)
'
EXECUTE(@exec)
Non è necessario codificare i nomi delle colonne in questo file.
Altri suggerimenti
OK, ho trovato una soluzione che dovrebbe iniziare. Probabilmente ci vorrà del tempo per mettere insieme, ma funzionerà bene. Sarebbe bello se non dovessimo elencare tutte le colonne per nome.
Fondamentalmente si sta utilizzando UNPIVOT e posizionando quel prodotto in una tabella temporanea, quindi eseguendo una query nel set di dati finale. Ho chiamato il mio tavolo ptest quando ho messo insieme, questo è quello con tutte le colonne A0001, ecc.
-- Create the temp table
CREATE TABLE #s (item nvarchar(10), val int)
-- Insert UNPIVOT product into the temp table
INSERT INTO #s (item, val)
SELECT item, val
FROM
(SELECT A0001, A0002, A0003, B0001, B0002, B0003
FROM ptest) p
unpivot
(val for item in (A0001, A0002, A0003, B0001, B0002, B0003)) as unpvt
-- Query the temp table to get final data set
SELECT RIGHT(item, 4) as item1,
Sum(CASE WHEN LEFT(item, 1) = 'A' THEN val ELSE 0 END) as A,
Sum(CASE WHEN LEFT(item, 1) = 'B' THEN val ELSE 0 END) as B
from #s
GROUP BY RIGHT(item, 4)
-- Delete temp table
drop table #s
A proposito, grazie per la domanda, questa è stata la prima volta che ho usato UNPIVOT. Ho sempre voluto, non ne ho mai avuto bisogno.