MySQL - wiersze do kolumn

187

Próbowałem wyszukiwać posty, ale znalazłem tylko rozwiązania dla SQL Server / Access. Potrzebuję rozwiązania w MySQL (5.X).

Mam tabelę (zwaną historią) z 3 kolumnami: hostid, nazwa elementu, wartość przedmiotu.
Jeśli zrobię select ( select * from history), zwróci

   +--------+----------+-----------+
   | hostid | itemname | itemvalue |
   +--------+----------+-----------+
   |   1    |    A     |    10     |
   +--------+----------+-----------+
   |   1    |    B     |     3     |
   +--------+----------+-----------+
   |   2    |    A     |     9     |
   +--------+----------+-----------+
   |   2    |    c     |    40     |
   +--------+----------+-----------+

Jak zapytać bazę danych, aby zwrócić coś takiego

   +--------+------+-----+-----+
   | hostid |   A  |  B  |  C  |
   +--------+------+-----+-----+
   |   1    |  10  |  3  |  0  |
   +--------+------+-----+-----+
   |   2    |   9  |  0  |  40 |
   +--------+------+-----+-----+
Bob Rivers
źródło
@Rob, czy możesz edytować pytanie, aby uwzględnić dokładne zapytanie?
Johan
UWAGA: Link @ako dotyczy tylko MariaDB.
ToolmakerSteve
Automatyczne generowanie i uruchamianie osi przestawnej: mysql.rjweb.org/doc.php/pivot
Rick James

Odpowiedzi:

275

Dodam nieco dłuższe i bardziej szczegółowe wyjaśnienie kroków, które należy podjąć, aby rozwiązać ten problem. Przepraszam, jeśli to za długo.


Zacznę od bazy, którą podałeś, i użyję jej do zdefiniowania kilku terminów, których użyję do końca tego postu. To będzie tabela bazowa :

select * from history;

+--------+----------+-----------+
| hostid | itemname | itemvalue |
+--------+----------+-----------+
|      1 | A        |        10 |
|      1 | B        |         3 |
|      2 | A        |         9 |
|      2 | C        |        40 |
+--------+----------+-----------+

To będzie nasz cel, ładna tabela przestawna :

select * from history_itemvalue_pivot;

+--------+------+------+------+
| hostid | A    | B    | C    |
+--------+------+------+------+
|      1 |   10 |    3 |    0 |
|      2 |    9 |    0 |   40 |
+--------+------+------+------+

Wartości w history.hostidkolumnie staną się wartościami Y w tabeli przestawnej. Wartości w history.itemnamekolumnie staną się wartościami x (z oczywistych powodów).


Kiedy muszę rozwiązać problem związany z tworzeniem tabeli przestawnej, rozwiązuję ten problem, stosując trzyetapowy proces (z opcjonalnym czwartym krokiem):

  1. wybierz kolumny interesów, czyli wartości y i x-wartości
  2. rozszerz tabelę podstawową o dodatkowe kolumny - po jednej dla każdej wartości x
  3. grupuj i agreguj tabelę rozszerzoną - jedna grupa na każdą wartość y
  4. (opcjonalnie) utrwalić zagregowaną tabelę

Zastosujmy te kroki do twojego problemu i zobaczmy, co otrzymamy:

Krok 1: wybierz interesujące kolumny . W pożądanym wyniku hostidpodaje wartości y i itemnamepodaje wartości x .

Krok 2: przedłuż tabelę podstawową o dodatkowe kolumny . Zwykle potrzebujemy jednej kolumny na wartość x. Przypomnijmy, że nasza kolumna wartości x to itemname:

create view history_extended as (
  select
    history.*,
    case when itemname = "A" then itemvalue end as A,
    case when itemname = "B" then itemvalue end as B,
    case when itemname = "C" then itemvalue end as C
  from history
);

select * from history_extended;

+--------+----------+-----------+------+------+------+
| hostid | itemname | itemvalue | A    | B    | C    |
+--------+----------+-----------+------+------+------+
|      1 | A        |        10 |   10 | NULL | NULL |
|      1 | B        |         3 | NULL |    3 | NULL |
|      2 | A        |         9 |    9 | NULL | NULL |
|      2 | C        |        40 | NULL | NULL |   40 |
+--------+----------+-----------+------+------+------+

Pamiętaj, że nie zmieniliśmy liczby wierszy - po prostu dodaliśmy dodatkowe kolumny. Zwróć również uwagę na wzorzec NULLs - wiersz z itemname = "A"ma wartość inną niż null dla nowej kolumny Ai wartości null dla innych nowych kolumn.

Krok 3: grupuj i agreguj tabelę rozszerzoną . Musimy to zrobić group by hostid, ponieważ zapewnia wartości y:

create view history_itemvalue_pivot as (
  select
    hostid,
    sum(A) as A,
    sum(B) as B,
    sum(C) as C
  from history_extended
  group by hostid
);

select * from history_itemvalue_pivot;

+--------+------+------+------+
| hostid | A    | B    | C    |
+--------+------+------+------+
|      1 |   10 |    3 | NULL |
|      2 |    9 | NULL |   40 |
+--------+------+------+------+

(Pamiętaj, że teraz mamy jeden wiersz na wartość y). Dobra, już prawie jesteśmy! Musimy tylko pozbyć się tych brzydkich NULL.

Krok 4: upiększ . Zamierzamy po prostu zastąpić dowolne wartości zerowe zerami, aby zestaw wyników był ładniejszy:

create view history_itemvalue_pivot_pretty as (
  select 
    hostid, 
    coalesce(A, 0) as A, 
    coalesce(B, 0) as B, 
    coalesce(C, 0) as C 
  from history_itemvalue_pivot 
);

select * from history_itemvalue_pivot_pretty;

+--------+------+------+------+
| hostid | A    | B    | C    |
+--------+------+------+------+
|      1 |   10 |    3 |    0 |
|      2 |    9 |    0 |   40 |
+--------+------+------+------+

I gotowe - zbudowaliśmy ładną, ładną tabelę przestawną za pomocą MySQL.


Uwagi dotyczące stosowania tej procedury:

  • jakiej wartości użyć w dodatkowych kolumnach. Użyłem itemvaluew tym przykładzie
  • jakiej „neutralnej” wartości użyć w dodatkowych kolumnach. Użyłem NULL, ale może to być 0lub "", w zależności od twojej dokładnej sytuacji
  • jakiej funkcji agregującej należy użyć podczas grupowania. Użyłem sum, ale counti maxczęsto też jest używany ( maxjest często używany podczas budowania jednorzędowych „obiektów”, które zostały rozmieszczone w wielu rzędach)
  • za pomocą wielu kolumn dla wartości y. To rozwiązanie nie ogranicza się do użycia jednej kolumny dla wartości y - wystarczy podłączyć dodatkowe kolumny do group byklauzuli (i nie zapomnij o selectnich)

Znane ograniczenia:

  • to rozwiązanie nie pozwala na n kolumn w tabeli przestawnej - każda kolumna przestawna musi być dodana ręcznie podczas rozszerzania tabeli podstawowej. Dla 5 lub 10 wartości x to rozwiązanie jest dobre. Za 100, nie tak miło. Istnieje kilka rozwiązań z procedurami przechowywanymi generującymi zapytanie, ale są one brzydkie i trudne do prawidłowego wykonania. Obecnie nie znam dobrego sposobu rozwiązania tego problemu, gdy tabela przestawna musi mieć wiele kolumn.
Matt Fenwick
źródło
25
+1 To zdecydowanie najlepsze / najbardziej jasne wyjaśnienie tabel przestawnych / tabulatorów w MySQL, jakie widziałem
cameron.bracken
6
Doskonałe wyjaśnienie, dzięki. Krok 4 można połączyć w krok 3 za pomocą IFNULL (suma (A), 0) JAKO A, dając ten sam wynik, ale bez potrzeby tworzenia kolejnej tabeli
nealio82,
1
To było najbardziej niesamowite rozwiązanie dla osi przestawnej, ale jestem ciekawy, czy w kolumnie nazwa elementu, która tworzy oś x, ma wiele wartości, tak jak tutaj mamy tylko trzy wartości, tj. A, B, C.Jeśli te wartości rozciągają się na A, B, C, D, E, AB, BC, AC, AD, H ..... n. w takim przypadku jakie byłoby rozwiązanie.
Deepesh
1
to naprawdę powinna być zaakceptowana odpowiedź tutaj. Jest o wiele bardziej szczegółowy, użyteczny i wyjaśnia, jak go zrozumieć, niż tylko link do jakiegoś artykułu, takiego jak obecnie akceptowany
EdgeCaseBerg
2
@WhiteBig, proszę spojrzeć na daty - ta odpowiedź StackOverflow została napisana 1,5 roku przed tym postem na blogu. Być może powinieneś zamiast tego poprosić bloga o kredyt.
Matt Fenwick,
55
SELECT 
    hostid, 
    sum( if( itemname = 'A', itemvalue, 0 ) ) AS A,  
    sum( if( itemname = 'B', itemvalue, 0 ) ) AS B, 
    sum( if( itemname = 'C', itemvalue, 0 ) ) AS C 
FROM 
    bob 
GROUP BY 
    hostid;
shantanuo
źródło
Tworzy trzy różne wiersze dla „A”, „B”, „C”
Palani
1
@Palani: Nie, nie ma. Zobaczyć group by.
ruakh
Dzięki, to zadziałało dla mnie! Jednak po prostu FYI kilka lat później, musiałem użyć MAXzamiast SUMponieważ moje itemValuesą ciągami, a nie wartościami liczbowymi.
Merricat
33

Inną opcją, szczególnie przydatną, jeśli masz wiele elementów, które musisz przestawić, jest pozwolenie mysqlowi zbudować zapytanie:

SELECT
  GROUP_CONCAT(DISTINCT
    CONCAT(
      'ifnull(SUM(case when itemname = ''',
      itemname,
      ''' then itemvalue end),0) AS `',
      itemname, '`'
    )
  ) INTO @sql
FROM
  history;
SET @sql = CONCAT('SELECT hostid, ', @sql, ' 
                  FROM history 
                   GROUP BY hostid');

PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

FIDDLE Dodano kilka dodatkowych wartości, aby zobaczyć, jak działa

GROUP_CONCAT ma domyślną wartość 1000, więc jeśli masz naprawdę duże zapytanie, zmień ten parametr przed jego uruchomieniem

SET SESSION group_concat_max_len = 1000000;

Test:

DROP TABLE IF EXISTS history;
CREATE TABLE history
(hostid INT,
itemname VARCHAR(5),
itemvalue INT);

INSERT INTO history VALUES(1,'A',10),(1,'B',3),(2,'A',9),
(2,'C',40),(2,'D',5),
(3,'A',14),(3,'B',67),(3,'D',8);

  hostid    A     B     C      D
    1     10      3     0      0
    2     9       0    40      5
    3     14     67     0      8
Mihai
źródło
@Maihai Może możesz mi pomóc. Spójrz na to: stackoverflow.com/questions/51832979/…
Success Man
Można uprościć za 'ifnull(SUM(case when itemname = ''',pomocą ''' then itemvalue end),0) AS „,” do 'SUM(case when itemname = '''z ''' then itemvalue else 0 end) AS ',. Dane wyjściowe takie jak SUM(case when itemname = 'A' then itemvalue else 0 end) AS 'A'.
ToolmakerSteve
24

Korzystając z pomysłu Matta Fenwicka, który pomógł mi rozwiązać problem (wielkie dzięki), zredukujmy go do jednego zapytania:

select
    history.*,
    coalesce(sum(case when itemname = "A" then itemvalue end), 0) as A,
    coalesce(sum(case when itemname = "B" then itemvalue end), 0) as B,
    coalesce(sum(case when itemname = "C" then itemvalue end), 0) as C
from history
group by hostid
Jalber
źródło
14

I EDIT Agung Sagita „s odpowiedź od podzapytaniu się przyłączyć. Nie jestem pewien, jak duża różnica między tym 2 sposobem, ale tylko dla innego odniesienia.

SELECT  hostid, T2.VALUE AS A, T3.VALUE AS B, T4.VALUE AS C
FROM TableTest AS T1
LEFT JOIN TableTest T2 ON T2.hostid=T1.hostid AND T2.ITEMNAME='A'
LEFT JOIN TableTest T3 ON T3.hostid=T1.hostid AND T3.ITEMNAME='B'
LEFT JOIN TableTest T4 ON T4.hostid=T1.hostid AND T4.ITEMNAME='C'
haudoing
źródło
2
Być może może to być szybsze rozwiązanie.
jave.web
Nie sądzę. ponieważ lewe dołączenie ma swoje własne opóźnienie!
Abadis
10

użyj podkwerendy

SELECT  hostid, 
    (SELECT VALUE FROM TableTest WHERE ITEMNAME='A' AND hostid = t1.hostid) AS A,
    (SELECT VALUE FROM TableTest WHERE ITEMNAME='B' AND hostid = t1.hostid) AS B,
    (SELECT VALUE FROM TableTest WHERE ITEMNAME='C' AND hostid = t1.hostid) AS C
FROM TableTest AS T1
GROUP BY hostid

ale będzie to stanowić problem, jeśli zapytanie podrzędne będzie skutkowało więcej niż wierszem, użyj dalszej funkcji agregującej w podzapytaniu

Agung Sagita
źródło
4

Moje rozwiązanie:

select h.hostid, sum(ifnull(h.A,0)) as A, sum(ifnull(h.B,0)) as B, sum(ifnull(h.C,0)) as  C from (
select
hostid,
case when itemName = 'A' then itemvalue end as A,
case when itemName = 'B' then itemvalue end as B,
case when itemName = 'C' then itemvalue end as C
  from history 
) h group by hostid

Daje oczekiwane wyniki w przedłożonej sprawie.

André Wéber
źródło
3

Robię Group By hostIdto, aby wyświetlać tylko pierwszy wiersz z wartościami,
takimi jak:

A   B  C
1  10
2      3
arpit
źródło
3

Wymyślam jeden sposób, aby moje raporty przekształcały wiersze w kolumny prawie dynamiczne za pomocą prostych zapytań. Możesz to zobaczyć i przetestować online tutaj .

Liczba kolumn zapytania jest stała, ale wartości są dynamiczne i oparte na wartościach wierszy. Możesz go zbudować Tak więc używam jednego zapytania do zbudowania nagłówka tabeli, a drugiego do wyświetlenia wartości:

SELECT distinct concat('<th>',itemname,'</th>') as column_name_table_header FROM history order by 1;

SELECT
     hostid
    ,(case when itemname = (select distinct itemname from history a order by 1 limit 0,1) then itemvalue else '' end) as col1
    ,(case when itemname = (select distinct itemname from history a order by 1 limit 1,1) then itemvalue else '' end) as col2
    ,(case when itemname = (select distinct itemname from history a order by 1 limit 2,1) then itemvalue else '' end) as col3
    ,(case when itemname = (select distinct itemname from history a order by 1 limit 3,1) then itemvalue else '' end) as col4
FROM history order by 1;

Możesz to także streścić:

SELECT
     hostid
    ,sum(case when itemname = (select distinct itemname from history a order by 1 limit 0,1) then itemvalue end) as A
    ,sum(case when itemname = (select distinct itemname from history a order by 1 limit 1,1) then itemvalue end) as B
    ,sum(case when itemname = (select distinct itemname from history a order by 1 limit 2,1) then itemvalue end) as C
FROM history group by hostid order by 1;
+--------+------+------+------+
| hostid | A    | B    | C    |
+--------+------+------+------+
|      1 |   10 |    3 | NULL |
|      2 |    9 | NULL |   40 |
+--------+------+------+------+

Wyniki RexTester :

Wyniki RexTester

http://rextester.com/ZSWKS28923

Dla jednego prawdziwego przykładu zastosowania, ten raport poniżej pokazuje w kolumnach godziny odlotów przylotów łodzi / autobusu z graficznym harmonogramem. Zobaczysz jedną dodatkową kolumnę, która nie zostanie użyta w ostatniej kolumnie bez pomylenia wizualizacji: sistema venda de passagens online e Consumeridor final and controle de frota - xsl tecnologia - xsl.com.br ** system biletowy do sprzedaży biletów online i prezentacyjnych

lynx_74
źródło
3

Jeśli możesz użyć MariaDB, istnieje bardzo bardzo proste rozwiązanie.

Od MariaDB-10.02 dodano nowy silnik pamięci o nazwie CONNECT, który może pomóc nam przekonwertować wyniki innego zapytania lub tabeli na tabelę przestawną, dokładnie tak, jak chcesz: możesz zajrzeć do dokumentacji .

Przede wszystkim zainstaluj silnik pamięci masowej Connect .

Teraz kolumna przestawna naszej tabeli jest, itemnamea dane dla każdego elementu znajdują się w itemvaluekolumnie, dzięki czemu możemy uzyskać wynikową tabelę przestawną za pomocą tego zapytania:

create table pivot_table
engine=connect table_type=pivot tabname=history
option_list='PivotCol=itemname,FncCol=itemvalue';

Teraz możemy wybrać, co chcemy z pivot_table:

select * from pivot_table

Więcej informacji tutaj

ako
źródło
1

Nie jest to dokładna odpowiedź, której szukasz, ale było to rozwiązanie, którego potrzebowałem w moim projekcie i mam nadzieję, że to komuś pomoże. Spowoduje to wyświetlenie pozycji od 1 do n wierszy oddzielonych przecinkami. Group_Concat umożliwia to w MySQL.

select
cemetery.cemetery_id as "Cemetery_ID",
GROUP_CONCAT(distinct(names.name)) as "Cemetery_Name",
cemetery.latitude as Latitude,
cemetery.longitude as Longitude,
c.Contact_Info,
d.Direction_Type,
d.Directions

    from cemetery
    left join cemetery_names on cemetery.cemetery_id = cemetery_names.cemetery_id 
    left join names on cemetery_names.name_id = names.name_id 
    left join cemetery_contact on cemetery.cemetery_id = cemetery_contact.cemetery_id 

    left join 
    (
        select 
            cemetery_contact.cemetery_id as cID,
            group_concat(contacts.name, char(32), phone.number) as Contact_Info

                from cemetery_contact
                left join contacts on cemetery_contact.contact_id = contacts.contact_id 
                left join phone on cemetery_contact.contact_id = phone.contact_id 

            group by cID
    )
    as c on c.cID = cemetery.cemetery_id


    left join
    (
        select 
            cemetery_id as dID, 
            group_concat(direction_type.direction_type) as Direction_Type,
            group_concat(directions.value , char(13), char(9)) as Directions

                from directions
                left join direction_type on directions.type = direction_type.direction_type_id

            group by dID


    )
    as d on d.dID  = cemetery.cemetery_id

group by Cemetery_ID

Ten cmentarz ma dwie wspólne nazwy, więc nazwy są wymienione w różnych wierszach połączonych jednym identyfikatorem, ale dwa identyfikatory nazw, a zapytanie tworzy coś w rodzaju tego

    CemeteryID Cemetery_Name Latitude
    1 Appleton, Sulpher Springs 35.4276242832293

James Humphrey
źródło
-2

Przykro mi to mówić i być może nie rozwiązuję dokładnie problemu, ale PostgreSQL jest o 10 lat starszy od MySQL i jest bardzo zaawansowany w porównaniu do MySQL i istnieje wiele sposobów na osiągnięcie tego z łatwością. Zainstaluj PostgreSQL i wykonaj to zapytanie

CREATE EXTENSION tablefunc;

wtedy voila! A oto obszerna dokumentacja: PostgreSQL: Dokumentacja: 9.1: tablefunc lub to zapytanie

CREATE EXTENSION hstore;

potem znowu voila! PostgreSQL: Dokumentacja: 9.0: hstore

gdarcan
źródło