Installieren
Ich baue auf @ Jacks Setup auf, um es den Leuten einfacher zu machen, zu folgen und zu vergleichen. Getestet mit PostgreSQL 9.1.4 .
CREATE TABLE lexikon (
lex_id serial PRIMARY KEY
, word text
, frequency int NOT NULL -- we'd need to do more if NULL was allowed
, lset int
);
INSERT INTO lexikon(word, frequency, lset)
SELECT 'w' || g -- shorter with just 'w'
, (1000000 / row_number() OVER (ORDER BY random()))::int
, g
FROM generate_series(1,1000000) g
Ab hier gehe ich einen anderen Weg:
ANALYZE lexikon;
Hilfstisch
Diese Lösung fügt der Originaltabelle keine Spalten hinzu, sondern benötigt lediglich eine winzige Hilfstabelle. Ich habe es in das Schema eingefügt public
und ein beliebiges Schema Ihrer Wahl verwendet.
CREATE TABLE public.lex_freq AS
WITH x AS (
SELECT DISTINCT ON (f.row_min)
f.row_min, c.row_ct, c.frequency
FROM (
SELECT frequency, sum(count(*)) OVER (ORDER BY frequency DESC) AS row_ct
FROM lexikon
GROUP BY 1
) c
JOIN ( -- list of steps in recursive search
VALUES (400),(1600),(6400),(25000),(100000),(200000),(400000),(600000),(800000)
) f(row_min) ON c.row_ct >= f.row_min -- match next greater number
ORDER BY f.row_min, c.row_ct, c.frequency DESC
)
, y AS (
SELECT DISTINCT ON (frequency)
row_min, row_ct, frequency AS freq_min
, lag(frequency) OVER (ORDER BY row_min) AS freq_max
FROM x
ORDER BY frequency, row_min
-- if one frequency spans multiple ranges, pick the lowest row_min
)
SELECT row_min, row_ct, freq_min
, CASE freq_min <= freq_max
WHEN TRUE THEN 'frequency >= ' || freq_min || ' AND frequency < ' || freq_max
WHEN FALSE THEN 'frequency = ' || freq_min
ELSE 'frequency >= ' || freq_min
END AS cond
FROM y
ORDER BY row_min;
Tabelle sieht so aus:
row_min | row_ct | freq_min | cond
--------+---------+----------+-------------
400 | 400 | 2500 | frequency >= 2500
1600 | 1600 | 625 | frequency >= 625 AND frequency < 2500
6400 | 6410 | 156 | frequency >= 156 AND frequency < 625
25000 | 25000 | 40 | frequency >= 40 AND frequency < 156
100000 | 100000 | 10 | frequency >= 10 AND frequency < 40
200000 | 200000 | 5 | frequency >= 5 AND frequency < 10
400000 | 500000 | 2 | frequency >= 2 AND frequency < 5
600000 | 1000000 | 1 | frequency = 1
Da die Spalte cond
später in dynamischem SQL verwendet werden soll, müssen Sie diese Tabelle sicher machen . Führen Sie immer eine Schemaqualifizierung der Tabelle durch, wenn Sie sich nicht sicher sind, ob ein entsprechender Stand vorliegt search_path
, und widerrufen Sie Schreibberechtigungen für public
(und jede andere nicht vertrauenswürdige Rolle):
REVOKE ALL ON public.lex_freq FROM public;
GRANT SELECT ON public.lex_freq TO public;
Die Tabelle lex_freq
dient drei Zwecken:
- Erstellen Sie benötigte Teilindizes automatisch.
- Stellen Sie Schritte für die iterative Funktion bereit.
- Metainformationen zur Abstimmung.
Indizes
Diese DO
Anweisung erstellt alle benötigten Indizes:
DO
$$
DECLARE
_cond text;
BEGIN
FOR _cond IN
SELECT cond FROM public.lex_freq
LOOP
IF _cond LIKE 'frequency =%' THEN
EXECUTE 'CREATE INDEX ON lexikon(lset) WHERE ' || _cond;
ELSE
EXECUTE 'CREATE INDEX ON lexikon(lset, frequency DESC) WHERE ' || _cond;
END IF;
END LOOP;
END
$$
Alle diese Teilindizes spannen zusammen , um die Tabelle einmal. Sie sind ungefähr so groß wie ein Basisindex für die gesamte Tabelle:
SELECT pg_size_pretty(pg_relation_size('lexikon')); -- 50 MB
SELECT pg_size_pretty(pg_total_relation_size('lexikon')); -- 71 MB
Bisher nur 21 MB Indizes für eine Tabelle mit 50 MB.
Ich erstelle die meisten Teilindizes auf (lset, frequency DESC)
. Die zweite Spalte hilft nur in Sonderfällen. Da beide beteiligten Spalten vom Typ sind integer
, wird der Index aufgrund der Besonderheiten der Datenausrichtung in Kombination mit MAXALIGN in PostgreSQL durch die zweite Spalte nicht größer. Es ist ein kleiner Gewinn für kaum Kosten.
Dies hat keinen Sinn für Teilindizes, die nur eine Frequenz umfassen. Die sind gerade an (lset)
. Erstellte Indizes sehen so aus:
CREATE INDEX ON lexikon(lset, frequency DESC) WHERE frequency >= 2500;
CREATE INDEX ON lexikon(lset, frequency DESC) WHERE frequency >= 625 AND frequency < 2500;
-- ...
CREATE INDEX ON lexikon(lset, frequency DESC) WHERE frequency >= 2 AND frequency < 5;
CREATE INDEX ON lexikon(lset) WHERE freqency = 1;
Funktion
Die Funktion ähnelt in etwa der @ Jack-Lösung:
CREATE OR REPLACE FUNCTION f_search(_lset_min int, _lset_max int, _limit int)
RETURNS SETOF lexikon
$func$
DECLARE
_n int;
_rest int := _limit; -- init with _limit param
_cond text;
BEGIN
FOR _cond IN
SELECT l.cond FROM public.lex_freq l ORDER BY l.row_min
LOOP
-- RAISE NOTICE '_cond: %, _limit: %', _cond, _rest; -- for debugging
RETURN QUERY EXECUTE '
SELECT *
FROM public.lexikon
WHERE ' || _cond || '
AND lset >= $1
AND lset <= $2
ORDER BY frequency DESC
LIMIT $3'
USING _lset_min, _lset_max, _rest;
GET DIAGNOSTICS _n = ROW_COUNT;
_rest := _rest - _n;
EXIT WHEN _rest < 1;
END LOOP;
END
$func$ LANGUAGE plpgsql STABLE;
Hauptunterschiede:
dynamisches SQL mit RETURN QUERY EXECUTE
.
Während wir die Schritte durchlaufen, kann ein anderer Abfrageplan nützlich sein. Der Abfrageplan für statisches SQL wird einmal generiert und dann wiederverwendet. Dies spart möglicherweise zusätzlichen Aufwand. In diesem Fall ist die Abfrage jedoch einfach und die Werte sind sehr unterschiedlich. Dynamic SQL wird ein großer Gewinn sein.
DynamischLIMIT
für jeden Abfrageschritt.
Dies hat mehrere Vorteile: Erstens werden Zeilen nur bei Bedarf abgerufen. In Kombination mit dynamischem SQL können dadurch auch verschiedene Abfragepläne erstellt werden. Zweitens: Es ist kein zusätzlicher LIMIT
Funktionsaufruf erforderlich , um den Überschuss zu kürzen.
Benchmark
Installieren
Ich habe vier Beispiele ausgewählt und mit jedem drei verschiedene Tests durchgeführt. Ich habe das Beste aus fünf genommen, um es mit dem warmen Cache zu vergleichen:
Die rohe SQL-Abfrage des Formulars:
SELECT *
FROM lexikon
WHERE lset >= 20000
AND lset <= 30000
ORDER BY frequency DESC
LIMIT 5;
Dasselbe nach dem Erstellen dieses Index
CREATE INDEX ON lexikon(lset);
Benötigt ungefähr den gleichen Platz wie alle meine Teilindizes zusammen:
SELECT pg_size_pretty(pg_total_relation_size('lexikon')) -- 93 MB
Die Funktion
SELECT * FROM f_search(20000, 30000, 5);
Ergebnisse
SELECT * FROM f_search(20000, 30000, 5);
1: Gesamtlaufzeit: 315,458 ms
2: Gesamtlaufzeit: 36,458 ms
3: Gesamtlaufzeit: 0,330 ms
SELECT * FROM f_search(60000, 65000, 100);
1: Gesamtlaufzeit: 294.819 ms
2: Gesamtlaufzeit: 18.915 ms
3: Gesamtlaufzeit: 1.414 ms
SELECT * FROM f_search(10000, 70000, 100);
1: Gesamtlaufzeit: 426.831 ms
2: Gesamtlaufzeit: 217.874 ms
3: Gesamtlaufzeit: 1.611 ms
SELECT * FROM f_search(1, 1000000, 5);
1: Gesamtlaufzeit: 2458.205 ms
2: Gesamtlaufzeit: 2458.205 ms - Für große Bereiche von lset ist der seq-Scan schneller als der Index.
3: Gesamtlaufzeit: 0,266 ms
Fazit
Wie erwartet wächst der Nutzen der Funktion mit größeren lset
und kleineren Reichweiten LIMIT
.
Mit sehr kleinen Reichweiten vonlset
ist die unformatierte Abfrage in Kombination mit dem Index tatsächlich schneller . Sie wollen testen und vielleicht verzweigen: rohe Abfrage für kleine Bereiche lset
, sonst Funktionsaufruf. Das könnte man sogar einfach in die Funktion für ein "Beste aus beiden Welten" einbauen - das würde ich tun.
Je nach Datenverteilung und typischen Abfragen werden weitere Schritte ausgeführt lex_freq
zur Leistungsverbesserung beitragen. Testen Sie, um den Sweet Spot zu finden. Mit den hier vorgestellten Tools sollte es einfach zu testen sein.