Ich habe ein Abfragemuster, das sehr häufig vorkommt, aber ich weiß nicht, wie ich eine effiziente Abfrage dafür schreiben soll. Ich möchte die Zeilen einer Tabelle nachschlagen, die "dem letzten Datum nicht nach" den Zeilen einer anderen Tabelle entsprechen.
Ich habe zum Beispiel einen Tisch, inventory
der das Inventar darstellt, das ich an einem bestimmten Tag habe.
date | good | quantity
------------------------------
2013-08-09 | egg | 5
2013-08-09 | pear | 7
2013-08-02 | egg | 1
2013-08-02 | pear | 2
und eine Tabelle, "Preis" sagen, die den Preis eines Gutes an einem bestimmten Tag hält
date | good | price
--------------------------
2013-08-07 | egg | 120
2013-08-06 | pear | 200
2013-08-01 | egg | 110
2013-07-30 | pear | 220
Wie kann ich effizient den "neuesten" Preis für jede Zeile der Inventartabelle ermitteln, d. H
date | pricing date | good | quantity | price
----------------------------------------------------
2013-08-09 | 2013-08-07 | egg | 5 | 120
2013-08-09 | 2013-08-06 | pear | 7 | 200
2013-08-02 | 2013-08-01 | egg | 1 | 110
2013-08-02 | 2013-07-30 | pear | 2 | 220
Ich kenne einen Weg, dies zu tun:
select inventory.date, max(price.date) as pricing_date, good
from inventory, price
where inventory.date >= price.date
and inventory.good = price.good
group by inventory.date, good
Fügen Sie diese Abfrage dann erneut zum Inventar hinzu. Bei großen Tabellen ist sogar die erste Abfrage (ohne erneut eine Verbindung zum Inventar herzustellen) sehr langsam. Dasselbe Problem lässt sich jedoch schnell lösen, wenn ich einfach mit meiner Programmiersprache eine max(price.date) ... where price.date <= date_of_interest ... order by price.date desc limit 1
Abfrage für jede Abfrage date_of_interest
aus der Inventartabelle stelle, sodass ich weiß, dass es keine rechnerischen Hindernisse gibt. Ich würde es jedoch vorziehen, das gesamte Problem mit einer einzigen SQL-Abfrage zu lösen, da ich damit das Ergebnis der Abfrage in SQL weiterverarbeiten könnte.
Gibt es eine Standardmethode, um dies effizient zu tun? Es fühlt sich so an, als müsste es oft auftauchen und es sollte eine Möglichkeit geben, eine schnelle Abfrage dafür zu schreiben.
Ich verwende Postgres, aber eine SQL-generische Antwort wäre wünschenswert.
\d tbl
in psql), Ihre Version von Postgres und min. / max. Anzahl der Preise pro Ware.Antworten:
Es kommt sehr auf die Umstände und die genauen Anforderungen an. Betrachten Sie meinen Kommentar zur Frage .
Einfache lösung
Mit
DISTINCT ON
in Postgres:Geordnetes Ergebnis.
Oder mit
NOT EXISTS
Standard-SQL (funktioniert mit jedem mir bekannten RDBMS):Gleiches Ergebnis, aber mit beliebiger Sortierreihenfolge - außer Sie fügen hinzu
ORDER BY
.Abhängig von der Datenverteilung, den genauen Anforderungen und den Indizes kann einer davon schneller sein.
Im Allgemeinen
DISTINCT ON
ist der Sieger und Sie erhalten ein sortiertes Ergebnis darüber. In bestimmten Fällen sind andere Abfragetechniken jedoch (viel) schneller. Siehe unten.Lösungen mit Unterabfragen zur Berechnung von Max / Min-Werten sind im Allgemeinen langsamer. Varianten mit CTEs sind im Allgemeinen noch langsamer.
Einfache Ansichten (wie in einer anderen Antwort vorgeschlagen) tragen in Postgres überhaupt nicht zur Leistung bei.
SQL-Geige.
Richtige Lösung
Zeichenfolgen und Kollatierung
Zuallererst leiden Sie unter einem suboptimalen Tabellenlayout. Es mag trivial erscheinen, aber die Normalisierung Ihres Schemas kann sehr weit gehen.
Sortierung nach Zeichentypen (
text
,varchar
, ...) werden muss , erfolgt nach dem locale - der COLLATION im Besonderen. Höchstwahrscheinlich verwendet Ihre Datenbank einige lokale Regeln (wie in meinem Fall:)de_AT.UTF-8
. Finden Sie es heraus mit:Dadurch werden das Sortieren und die Indexsuche verlangsamt . Je länger Ihre Saiten (Warennamen) sind, desto schlechter. Wenn Sie die Kollatierungsregeln in Ihrer Ausgabe (oder die Sortierreihenfolge überhaupt) nicht beachten, kann dies schneller sein, wenn Sie Folgendes hinzufügen
COLLATE "C"
:Beachten Sie, wie ich die Kollatierung an zwei Stellen hinzugefügt habe.
In meinem Test doppelt so schnell mit jeweils 20.000 Zeilen und sehr einfachen Namen ('good123').
Index
Wenn Ihre Abfrage einen Index verwenden soll, müssen Spalten mit Zeichendaten eine übereinstimmende Sortierung verwenden (
good
im Beispiel):Lesen Sie unbedingt die letzten beiden Kapitel dieser Antwort zu SO:
Sie können sogar mehrere Indizes mit unterschiedlichen Sortierungen in denselben Spalten haben - wenn Sie in anderen Abfragen auch Waren benötigen, die nach einer anderen (oder der Standard-) Sortierung sortiert sind.
Normalisieren
Redundante Zeichenfolgen (name of good) belasten auch Ihre Tabellen und Indizes, wodurch alles noch langsamer wird. Mit einem korrekten Tabellenlayout könnten Sie die meisten Probleme zunächst vermeiden. Könnte so aussehen:
Die Primärschlüssel liefern automatisch (fast) alle benötigten Indizes.
Je nach fehlenden Details, einen mehrspaltigen Index auf
price
mit absteigender Reihenfolge auf der zweiten Spalte kann die Leistung verbessern:Auch hier muss die Sortierung mit Ihrer Suchanfrage übereinstimmen (siehe oben).
In Postgres 9.2 oder höher kann das "Abdecken von Indizes" für Index-Only-Scans noch weiter helfen - insbesondere, wenn Ihre Tabellen zusätzliche Spalten enthalten und die Tabelle somit wesentlich größer als der abdeckende Index ist.
Diese resultierenden Abfragen sind viel schneller:
EXISTIERT NICHT
DISTINCT ON
SQL-Geige.
Schnellere Lösungen
Wenn das immer noch nicht schnell genug ist, kann es schnellere Lösungen geben.
Rekursiver CTE /
JOIN LATERAL
/ korrelierte UnterabfrageSpeziell für Datenverteilungen mit vielen Preisen pro Ware :
Materialisierte Ansicht
Wenn Sie dies häufig und schnell ausführen müssen, sollten Sie eine materialisierte Ansicht erstellen. Ich denke, es ist davon auszugehen, dass sich Preise und Lagerbestände für vergangene Daten selten ändern. Berechnen Sie das Ergebnis einmal und speichern Sie einen Schnappschuss als materialisierte Ansicht.
Postgres 9.3+ bietet automatisierte Unterstützung für materialisierte Ansichten. Sie können eine Basisversion problemlos in älteren Versionen implementieren.
quelle
price_good_date_desc_idx
Ihnen empfohlene Index hat die Leistung für eine ähnliche Abfrage von mir erheblich verbessert. Mein Abfrageplan ging von einem Preis von42374.01..42374.86
bis zu0.00..37.12
!Zu Ihrer Information, ich habe mssql 2008 verwendet, sodass Postgres nicht über den Index "include" verfügt. Die Verwendung der unten gezeigten grundlegenden Indizierung ändert sich jedoch von Hash-Joins zu Merge-Joins in Postgres: http://explain.depesz.com/s/eF6 (kein Index) http://explain.depesz.com/s/j9x ( mit Index auf Join-Kriterien)
Ich schlage vor, Ihre Anfrage in zwei Teile zu unterteilen. Erstens eine Ansicht (die nicht dazu gedacht ist, die Leistung zu verbessern) , die in einer Vielzahl anderer Kontexte verwendet werden kann und die die Beziehung zwischen Inventurdaten und Preisdaten darstellt.
Dann kann Ihre Anfrage für andere Arten einfacher und einfacher zu bearbeiten sein (z. B. durch Verwendung von Links-Joins, um Inventar ohne aktuelle Preisdaten zu finden):
Dies ergibt den folgenden Ausführungsplan: http://sqlfiddle.com/#!3/24f23/1
... Alle Scans mit einer vollständigen Sortierung. Beachten Sie, dass die Performancekosten von Hash-Matches einen Großteil der Gesamtkosten ausmachen ... und wir wissen, dass die Tabellenscans und -sortierungen langsam sind (verglichen mit dem Ziel: Index-Suchvorgänge).
Fügen Sie nun grundlegende Indizes hinzu, um die Kriterien zu unterstützen, die in Ihrem Join verwendet wurden (ich behaupte nicht, dass dies optimale Indizes sind, aber sie veranschaulichen den Punkt): http://sqlfiddle.com/#!3/5ec75/1
Dies zeigt eine Verbesserung. Die verschachtelten Schleifenoperationen (innere Verknüpfungen) verursachen keine relevanten Gesamtkosten mehr für die Abfrage. Der Rest der Kosten verteilt sich nun auf Index-Suchvorgänge (ein Scan nach Inventar, da wir jede Inventarzeile ziehen). Aber wir können es noch besser machen, weil die Abfrage Menge und Preis zieht. Um diese Daten zu erhalten, müssen nach Auswertung der Join-Kriterien Lookups durchgeführt werden.
Die letzte Iteration verwendet "include" für die Indizes, damit der Plan einfach darüber gleiten und die zusätzlich angeforderten Daten direkt aus dem Index selbst abrufen kann. Die Suchanfragen sind also weg: http://sqlfiddle.com/#!3/5f143/1
Jetzt haben wir einen Abfrageplan, in dem die Gesamtkosten der Abfrage gleichmäßig auf sehr schnelle Indexsuchvorgänge verteilt sind. Dies wird fast so gut sein, wie es nur geht. Sicherlich können andere Experten dies weiter verbessern, aber die Lösung behebt einige wichtige Bedenken:
quelle
Wenn Sie zufällig PostgreSQL 9.3 (heute veröffentlicht) haben, können Sie LATERAL JOIN verwenden.
Ich habe keine Möglichkeit, dies zu testen, und habe es noch nie zuvor verwendet, aber nach dem, was ich aus der Dokumentation entnehmen kann, würde die Syntax etwa so lauten:
Dies ist im Grunde genommen gleichbedeutend mit APPLY von SQL-Server , und es gibt ein funktionierendes Beispiel für SQL-Fiddle zu Demonstrationszwecken .
quelle
Wie Erwin und andere angemerkt haben, hängt eine effiziente Abfrage von vielen Variablen ab, und PostgreSQL ist sehr bemüht, die Abfrageausführung basierend auf diesen Variablen zu optimieren. Im Allgemeinen möchten Sie zuerst aus Gründen der Übersichtlichkeit schreiben und dann nach dem Erkennen von Engpässen aus Gründen der Leistung ändern.
Zusätzlich hat PostgreSQL eine Reihe von Tricks, mit denen Sie die Effizienz steigern können (Teilindizes für einen). Abhängig von Ihrer Lese- / Schreiblast können Sie dies möglicherweise sehr weit optimieren, indem Sie sich mit sorgfältiger Indizierung befassen.
Das erste, was Sie versuchen sollten, ist, eine Ansicht zu erstellen und sich dieser anzuschließen:
Dies sollte gut funktionieren, wenn Sie Folgendes tun:
Dann können Sie sich dem anschließen. Die Abfrage verbindet sich dann mit der Ansicht der zugrunde liegenden Tabelle. Vorausgesetzt, Sie haben einen eindeutigen Index für (Datum, in dieser Reihenfolge gültig ), sollten Sie bereit sein (da dies eine einfache Cache-Suche ist). Dies funktioniert mit ein paar nachgeschlagenen Zeilen sehr gut, ist jedoch sehr ineffizient, wenn Sie versuchen, Millionen von Warenpreisen zu verdauen.
Das zweite, was Sie tun können, ist, der Inventartabelle die Spalte most_recent bool und hinzuzufügen
Sie möchten dann Trigger verwenden, um most_recent auf false zu setzen, wenn eine neue Zeile für eine Ware eingefügt wurde. Dies erhöht die Komplexität und erhöht die Wahrscheinlichkeit von Fehlern, ist jedoch hilfreich.
Auch hier hängt vieles davon ab, ob geeignete Indizes vorhanden sind. Bei den letzten Datumsabfragen sollten Sie wahrscheinlich einen Index für das Datum und möglicherweise einen Index für mehrere Spalten haben, der mit dem Datum beginnt und Ihre Beitrittskriterien enthält.
Update Per Erwins Kommentar unten, es sieht so aus, als hätte ich das falsch verstanden. Ich lese die Frage noch einmal und bin mir nicht sicher, was gefragt wird. Ich möchte im Update erwähnen, welches potenzielle Problem ich sehe und warum dies unklar bleibt.
Das angebotene Datenbankdesign hat keinen wirklichen IME-Nutzen mit ERP- und Buchhaltungssystemen. Es würde in einem hypothetischen perfekten Preismodell funktionieren, in dem alles, was an einem bestimmten Tag eines bestimmten Produkts verkauft wird, den gleichen Preis hat. Dies ist jedoch nicht immer der Fall. Dies gilt nicht einmal für Dinge wie Währungsumtausch (obwohl einige Modelle so tun, als ob dies der Fall wäre). Wenn dies ein erfundenes Beispiel ist, ist es unklar. Wenn es sich um ein echtes Beispiel handelt, gibt es größere Probleme beim Entwurf auf Datenebene. Ich gehe hier davon aus, dass dies ein echtes Beispiel ist.
Sie können nicht davon ausgehen, dass dieses Datum allein den Preis für eine bestimmte Ware angibt. Preise in jedem Geschäft können pro Gegenpartei und manchmal sogar pro Transaktion ausgehandelt werden. Aus diesem Grund sollten Sie den Preis wirklich in der Tabelle speichern, die das Inventar tatsächlich verarbeitet (die Inventartabelle). In einem solchen Fall gibt Ihre Datums- / Waren- / Preistabelle lediglich einen Grundpreis an, der sich aufgrund von Verhandlungen ändern kann. In einem solchen Fall handelt es sich bei diesem Problem nicht mehr um ein Berichtsproblem, sondern um ein Transaktionsproblem, bei dem jeweils eine Zeile aus jeder Tabelle bearbeitet wird. Sie können dann beispielsweise den Standardpreis für ein bestimmtes Produkt an einem bestimmten Tag wie folgt nachschlagen:
Mit einem Preisindex (gut, Datum) wird dies eine gute Leistung bringen.
Wenn dies ein erfundenes Beispiel ist, hilft vielleicht etwas, an dem Sie näher arbeiten.
quelle
most_recent
Ansatz sollte für den aktuellsten Preis durchaus funktionieren . Es scheint jedoch, dass das OP den aktuellsten Preis in Bezug auf jedes Inventardatum benötigt.Eine andere Möglichkeit wäre, die Fensterfunktion
lead()
zu verwenden, um den Datumsbereich für jede Zeile im Tabellenpreis abzurufen und dannbetween
beim Zusammenführen des Inventars zu verwenden. Ich habe dies tatsächlich im wirklichen Leben verwendet, aber hauptsächlich, weil dies meine erste Idee war, wie man das löst.SqlFiddle
quelle
Verwenden Sie eine Verknüpfung von Inventar zu Preis mit Verknüpfungsbedingungen, die die Aufzeichnungen aus der Preistabelle auf diejenigen beschränken, die sich am oder vor dem Inventardatum befinden, und extrahieren Sie dann das maximale Datum, wobei das Datum das höchste Datum aus dieser Teilmenge ist
Also für Ihren Inventarpreis:
Wenn sich der Preis für eine bestimmte Ware am selben Tag mehrmals geändert hat und Sie wirklich nur Datums- und Uhrzeitangaben in diesen Spalten haben, müssen Sie möglicherweise die Joins stärker einschränken, um nur einen der Preisänderungsdatensätze auszuwählen.
quelle