2016-06-30 11 views
3

Ich habe 3 Tabellen - eine für Benutzer, eine für ihre eingehenden Zahlungen und eine für ihre ausgehenden Zahlungen. Ich möchte alle eingehenden und ausgehenden Zahlungen in einer einzigen Ergebnismenge anzeigen. Ich kann dies mit mehreren select s und einem union tun, aber es scheint umständlich, und ich vermute, es ist langsam aufgrund der Unterabfragen - und die Tabellen sind extrem groß (obwohl ich Indizes verwende). Gibt es einen schnelleren Weg, dies zu erreichen? Vielleicht mit einem full outer join?MySQL optimiert eine Union-Abfrage mit einer Join-Abfrage statt

Hier ist eine vereinfachte Version des Schemas mit einigen Beispieldaten:

create table users (
    id int auto_increment, 
    name varchar(20), 
    primary key (id) 
) engine=InnoDB; 
insert into users (name) values ('bob'),('fred'); 

create table user_incoming_payments (
    user_id int, 
    funds_in int 
) engine=InnoDB; 
insert into user_incoming_payments 
values (1,100),(1,101),(1,102),(1,103), 
(2,200),(2,201),(2,202),(2,203); 

create table user_outgoing_payments (
    user_id int, 
    funds_out int 
) engine=InnoDB; 
insert into user_outgoing_payments 
values (1,100),(1,101),(2,200),(2,201); 

Und hier ist die hässliche suchen Abfrage, die das Ergebnis, das ich für Benutzer bob wollen erzeugt:

select * from (
(select u.name, i.funds_in, 0 as 'funds_out' from users u 
inner join user_incoming_payments i on u.id = i.user_id) 
union 
(select u.name, 0 as 'funds_in', o.funds_out from users u 
inner join user_outgoing_payments o on u.id = o.user_id) 
) a where a.name = 'bob' 
order by a.funds_in asc, a.funds_out asc; 

Und hier ist so nah, wie ich mit join s das gleiche Ding tun kann - es ist nicht korrekt obwohl, weil ich möchte, dass dieses Ergebnis gleich aussieht wie das vorherige und ich war mir nicht sicher, wie man full outer join:

verwenden
select * 
from users u 
right join user_incoming_payments i on u.id = i.user_id 
right join user_outgoing_payments o on u.id = o.user_id 
where u.name = 'bob'; 

SQL Fiddle here

+2

In MySQL gibt es kein FULL OUTER JOIN - obwohl Sie das natürlich auf verschiedene Arten simulieren können. Aber wenn diese Abfrage das gewünschte Ergebnis erzeugt, dann ist ein FULL OUTER JOIN nicht das, was gewünscht wird. Ihre Anfrage ist in Ordnung (obwohl ich nicht sicher bin, dass die Super-Abfrage notwendig ist) – Strawberry

+0

Haben Sie Beispieldaten und Ergebnisse? –

+2

@Ivan: Sie können die Beispieldaten und Ergebnisse nicht bereits in der Frage sehen? Wenn nicht, liest du nicht sehr gründlich. –

Antwort

3

Mit diesem Modell würde ich wahrscheinlich die Abfrage wie folgt schreiben, aber ich bezweifle es viel Unterschied macht ...

select u.name 
    , i.funds_in 
    , 0 funds_out 
    from users u 
    join user_incoming_payments i 
    on u.id = i.user_id 
where u.name = 'bob' 
union all 
select u.name 
    , 0 funds_in 
    , o.funds_out 
    from users u 
    join user_outgoing_payments o 
    on u.id = o.user_id 
where u.name = 'bob' 
order 
    by funds_in asc 
    , funds_out asc; 

Beachten Sie jedoch, dass es keine PK hier , die sich als problematisch erweisen können.

Wenn ich es wäre, hätte ich eine Tabelle für Transaktionen, die eine Transaktions-ID-PK, einen Zeitstempel für jede Transaktion und eine Spalte enthalten würde, um aufzuzeichnen, ob es sich bei einem Wert um eine Gutschrift oder eine Lastschrift handelt.

+2

Beachten Sie, dass "UNION" Duplikate entfernen wird, was in diesem Szenario wahrscheinlich nicht wünschenswert ist. Wir sehen keine Garantie, dass es zwei (oder mehr) Zeilen in "user_incoming_payments" mit dem gleichen Wert von 'funds_in' gibt. Wir wollen wahrscheinlich einen 'UNION ALL'-Mengenoperator verwenden, um zu vermeiden, dass Duplikate entfernt werden. (Wo immer das Entfernen von Duplikaten nicht erforderlich ist, bevorzugen wir die Verwendung einer "UNION ALL" für die Leistung, um den Aufwand für die Überprüfung auf Duplikate zu vermeiden. – spencer7593

+1

Außerdem schließe ich bei dieser Art von Abfrage normalerweise eine Diskriminatorspalte ein. eine zusätzliche Spalte in jeder Abfrage, wobei jede Abfrage einen anderen, kurzen Literalwert zurückgibt (vielleicht ''i' 'und' '' in diesem Szenario, der mich darüber informiert, welche Abfrage eine Zeile zurückgegeben hat) – spencer7593

+0

@ spencer7593 Behoben (mit Vorbehalte) – Strawberry

3

MySQL unterstützt FULL OUTER JOIN nicht. Selbst wenn es das unterstützt, glaube ich nicht, dass Sie das wollen, da es ein semikartesisches Produkt einführen würde ... mit jeder Zeile von incoming_, die jede Zeile in outgoing_ abgleicht und zusätzliche Zeilen erzeugt.

Wenn es vier Zeilen von incoming_ und sechs Zeilen von outgoing_ gäbe, würde der von einer Join-Operation erzeugte Satz 24 Zeilen enthalten.

Dies sieht wirklich mehr wie Sie eine Set-Verkettung-Operation wollen. Das heißt, Sie haben zwei separate Mengen, die Sie verketten möchten. Das ist kein JOIN Betrieb. Das ist ein UNION ALL Set-Betrieb.

SELECT ... FROM ... 
UNION ALL 
SELECT ... FROM ... 

Wenn Sie nicht brauchen, um Duplikate zu entfernen (und es sieht aus wie Sie nicht in diesem Szenario wollen würden, wenn es mehrere Reihen in incoming_ mit dem gleichen Wert von funds_in sind, ich glaube nicht, Sie möchten alle Zeilen entfernen.) ...

Verwenden Sie dann den UNION ALL-Set-Operator, der nicht die Überprüfung und das Entfernen von doppelten Zeilen durchführt.

Der Operator UNION entfernt doppelte Zeilen. Welche (wieder) ich glaube nicht, dass du willst.


Die abgeleitete Tabelle ist nicht erforderlich.

Und MySQL "Push" das Prädikat von der äußeren Tabelle in die Inline-Ansicht. Das bedeutet, dass MySQL eine abgeleitete Tabelle mit allen eingehenden und ausgehenden Daten für alle Benutzer materialisiert. Und die äußere Abfrage wird das durchsehen, um die Zeilen zu finden. Bis zu den neuesten Versionen von MySQL wurden keine Indizes für abgeleitete Tabellen erstellt.

Siehe die Antwort von Strawberry für ein Beispiel für eine effizientere Abfrage.

Mit dem kleinen Beispielsatz werden Indizes keinen Unterschied machen. Bei einem großen Satz werden Sie jedoch geeignete Deckungsindizes hinzufügen wollen.

Auch bei Abfragen wie dieser, neige ich dazu, eine Diskriminatorspalte zu enthalten, die mir sagt, welche Abfrage eine Zeile zurückgegeben hat.

(
    SELECT 'i' AS src 
     , ... 
    FROM ... 
) 
    UNION ALL 
(
    SELECT 'o' AS src 
     , ... 
    FROM ... 
) 
    ORDER BY ... 
+0

Er wird keine doppelten Zeilen bekommen, weil er 'funds_in' und' funds_out' in verschiedene Spalten der Unterabfragen stellt. – Barmar

+1

@Barmar: Aber wenn zwei Zeilen von 'incoming_' die gleichen Werte für' user_id' und haben 'funds_in'. Die Beispieldaten zeigen keine Duplikate ... Aber was ist, wenn wir mehr Zeilen in die' incoming_' Tabelle (1.100), (1.100), (1.100) einfügen '' Wir sehen keine Einschränkung das verhindert das. Beachten Sie, dass die Operation 'UNION' entfernt wird Es * alle * Duplikate aus dem kombinierten Satz. Es spielt keine Rolle, aus welchem ​​Satz die Zeilen stammen ... Der gesamte kombinierte Satz wird nach Duplikaten durchsucht. – spencer7593

+1

Guter Punkt. Ich vermute, dass es in der realen Anwendung auch eine Transaktionszeit gibt, die sie unterscheidet, aber mit dem Schema, das wir Ihnen gegeben haben, sind Sie richtig. – Barmar

Verwandte Themen