Ich dachte ursprünglich an einen lateralen Join wie in anderen vorgeschlagenen Ansätzen (zum Beispiel die letzte Abfrage von Erwin Brandstetter, wo er einfaches int8
verwendet Datentyp und einfache Indizes), wollte es aber nicht in die Antwort schreiben, weil ich dachte, dass es nicht wirklich effizient ist. Wenn Sie versuchen, alle netblock
zu finden Bereiche, die den angegebenen Bereich abdecken, hilft ein Index nicht viel.
Ich wiederhole hier die Frage von Erwin Brandstetter:
SELECT * -- only select columns you need to make it faster
FROM routing_details r
, LATERAL (
SELECT *
FROM netblock_details n
WHERE n.ip_min <= r.ip_min
AND n.ip_max >= r.ip_max
ORDER BY n.ip_max - n.ip_min
LIMIT 1
) n;
Wenn Sie einen Index für netblock_details haben, etwa so:
CREATE INDEX netblock_details_ip_min_max_idx ON netblock_details
(ip_min, ip_max DESC NULLS LAST);
können Sie schnell (in O(logN)
) finden Sie den Startpunkt des Scans in den netblock_details
Tabelle - entweder die maximale n.ip_min
das ist kleiner als r.ip_min
, oder das Minimum n.ip_max
das ist mehr als r.ip_max
. Aber dann müssen Sie den Rest des Indexes/der Tabelle scannen/lesen und für jede Zeile den zweiten Teil der Prüfung durchführen und die meisten Zeilen herausfiltern.
Mit anderen Worten, dieser Index hilft, schnell die Startzeile zu finden, die das erste Suchkriterium erfüllt:n.ip_min <= r.ip_min
, aber dann lesen Sie weiter alle Zeilen, die dieses Kriterium erfüllen, und führen für jede solche Zeile die zweite Prüfung n.ip_max >= r.ip_max
durch . Im Durchschnitt (wenn die Daten gleichmäßig verteilt sind) müssen Sie die Hälfte der Zeilen der netblock_details
lesen Tisch. Der Optimierer kann den Index verwenden, um nach n.ip_max >= r.ip_max
zu suchen zuerst und wenden Sie dann den zweiten Filter n.ip_min <= r.ip_min
an , aber Sie können diesen Index nicht verwenden, um beide Filter zusammen anzuwenden.
Endergebnis:für jede Zeile von routing_details
wir werden die Hälfte der Zeilen von netblock_details
lesen . 600K * 4M =2.400.000.000.000 Zeilen. Es ist zweimal besser als das kartesische Produkt. Sie können diese Zahl (kartesisches Produkt) in der Ausgabe von EXPLAIN ANALYZE
sehen in der Frage.
592,496 * 8,221,675 = 4,871,309,550,800
Kein Wunder, dass Ihren Abfragen der Speicherplatz ausgeht, wenn Sie versuchen, dies zu materialisieren und zu sortieren.
Der gesamte High-Level-Prozess, um zum Endergebnis zu gelangen:
-
sich zwei Tischen anschließen (ohne die beste Übereinstimmung noch zu finden). Im schlimmsten Fall handelt es sich um ein kartesisches Produkt, im besten Fall deckt es alle Bereiche von
netblock_details
ab für jeden Bereich vonrouting_details
. Sie sagten, dass es mehrere Einträge innetblock_details
gibt für jeden Eintrag inrouting_details
, alles zwischen 3 und etwa 10. Das Ergebnis dieser Verknüpfung könnte also ~6 Millionen Zeilen haben (nicht zu viel) -
sortieren/partitionieren Sie das Ergebnis des Joins nach
routing_details
Bereiche und finde für jeden solchen Bereich den besten (kleinsten) abdeckenden Bereich ausnetblock_details
.
Meine Idee ist, die Abfrage umzukehren. Anstatt alle abdeckenden Bereiche aus größeren netblock_details
zu finden für jede Zeile von kleineren routing_details
Tabelle schlage ich vor, alle kleineren Bereiche aus kleineren routing_details
zu finden für jede Zeile von größeren netblock_details
.
Zweistufiger Prozess
Für jede Zeile größerer netblock_details
finden Sie alle Bereiche von routing_details
die sich innerhalb des netblock
befinden Bereich.
Ich würde das folgende Schema in den Abfragen verwenden. Ich habe den Primärschlüssel ID
hinzugefügt zu beiden Tabellen.
CREATE TABLE routing_details (
ID int
,ip_min int8
,ip_max int8
,asn text
,netblock text
);
CREATE TABLE netblock_details (
ID int
,ip_min int8
,ip_max int8
,name text
,country text
,source text
);
SELECT
netblock_details.ID AS n_ID
,netblock_details.ip_max - netblock_details.ip_min AS n_length
,r.ID AS r_ID
FROM
netblock_details
INNER JOIN LATERAL
(
SELECT routing_details.ID
FROM routing_details
WHERE
routing_details.ip_min >= netblock_details.ip_min
AND routing_details.ip_min <= netblock_details.ip_max
-- note how routing_details.ip_min is limited from both sides
-- this would make it possible to scan only (hopefully) small
-- portion of the table instead of full or half table
AND routing_details.ip_max <= netblock_details.ip_max
-- this clause ensures that the whole routing range
-- is inside the netblock range
) AS r ON true
Wir brauchen einen Index für routing_details
auf (ip_min, ip_max)
. Die Hauptsache hier ist der Index auf ip_min
. Eine zweite Spalte im Index zu haben, hilft, indem die Suche nach dem Wert von ip_max
entfällt; es hilft nicht bei der Baumsuche.
Beachten Sie, dass die laterale Unterabfrage kein LIMIT
hat . Es ist noch nicht das Endergebnis. Diese Abfrage sollte ~6 Millionen Zeilen zurückgeben. Speichern Sie dieses Ergebnis in einer temporären Tabelle. Fügen Sie der temporären Tabelle einen Index für (r_ID, n_length, n_ID)
hinzu . n_ID
dient wiederum nur dazu, zusätzliche Lookups zu entfernen. Wir brauchen einen Index, vermeiden Sie das Sortieren der Daten für jede r_ID
.
Letzter Schritt
Für jede Zeile von routing_details
Suchen Sie die n_ID
mit der kleinsten n_length
. Jetzt können wir den seitlichen Join in der "richtigen" Reihenfolge verwenden. Hier temp
Tabelle ist das Ergebnis des vorherigen Schritts mit dem Index .
SELECT
routing_details.*
,t.n_ID
,netblock_details.*
FROM
routing_details
INNER JOIN LATERAL
(
SELECT temp.n_ID
FROM temp
WHERE temp.r_ID = routing_details.ID
ORDER BY temp.n_length
LIMIT 1
) AS t ON true
INNER JOIN netblock_details ON netblock_details.ID = t.n_ID
Hier sollte die Unterabfrage eine Suche nach dem Index sein, nicht ein Scan. Der Optimierer sollte aufgrund von LIMIT 1
schlau genug sein, nur die Suche durchzuführen und das erste gefundene Ergebnis zurückzugeben , also haben Sie 600.000 Indexsuchen in einer temporären Tabelle mit 6 Millionen Zeilen.
Ursprüngliche Antwort (ich behalte es nur für das Bereichsdiagramm):
Kann man "betrügen"?
Wenn ich Ihre Abfrage richtig verstanden habe, für jede routing_details.range
Sie möchten eine kleinste netblock_details.range
finden die routing_details.range
abdeckt/größer ist .
Im folgenden Beispiel, wobei r
Routingbereich ist und n1, ..., n8
Netblock-Bereiche sind, ist die richtige Antwort n5
.
|---|
n1
|------------------|
n2
|---------------|
n3
|-----|
n4
|------------------|
n5
|--------------------------------------|
n6
|---------------------------|
n7
|-----|
n8
|------------|
r
start end
n.start <= r.start AND n.end >= r.end
order by n.length
limit 1
Ihre Abfrage, die 14 Stunden gedauert hat zeigt, dass der Indexscan 6 ms dauerte, aber das Sortieren nach Bereichslänge 80 ms dauerte.
Bei dieser Art der Suche gibt es keine einfache 1D-Ordnung der Daten. Sie verwenden n.start
zusammen mit n.end
und zusammen mit n.length
. Aber vielleicht sind Ihre Daten nicht so generisch oder es ist in Ordnung, ein etwas anderes Ergebnis zurückzugeben.
Zum Beispiel, wenn es in Ordnung war, n6
zurückzugeben Infolgedessen könnte es viel schneller arbeiten. n6
ist der Abdeckungsbereich, der den größten start
hat :
n.start <= r.start AND n.end >= r.end
order by n.start desc
limit 1
Oder Sie könnten sich für n7
entscheiden , die das kleinste end
hat :
n.start <= r.start AND n.end >= r.end
order by n.end
limit 1
Diese Art der Suche würde einen einfachen Index auf n.start
verwenden (oder n.end
) ohne zusätzliche Sortierung.
Ein zweiter, ganz anderer Ansatz. Wie groß/lang sind die Reichweiten? Wenn sie relativ kurz sind (wenige Zahlen), könnten Sie versuchen, sie als explizite Liste von Ganzzahlen statt als Bereich zu speichern. Beispiel:Bereich [5-8]
würde als 4 Zeilen gespeichert werden:(5, 6, 7, 8)
. Mit diesem Speichermodell ist es möglicherweise einfacher, Schnittpunkte von Bereichen zu finden.