Installer
Je m'appuie sur la configuration de @ Jack pour faciliter le suivi et la comparaison des utilisateurs. Testé avec 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
A partir de là, je prends un itinéraire différent:
ANALYZE lexikon;
Table auxiliaire
Cette solution n'ajoute pas de colonnes à la table d'origine, elle a juste besoin d'une petite table auxiliaire. Je l'ai placé dans le schéma public
, utilisez n'importe quel schéma de votre choix.
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;
Le tableau ressemble à ceci:
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
Comme la colonne cond
va être utilisée dans SQL dynamique plus bas, vous devez faire ce tableau sécurisé . Toujours qualifier la table par schéma si vous ne pouvez pas être sûr d'un courant approprié search_path
et révoquer les privilèges d'écriture de public
(et de tout autre rôle non approuvé):
REVOKE ALL ON public.lex_freq FROM public;
GRANT SELECT ON public.lex_freq TO public;
Le tableau lex_freq
sert à trois fins:
- Créez automatiquement les index partiels nécessaires .
- Fournissez des étapes pour la fonction itérative.
- Méta-informations pour le réglage.
Index
Cette DO
instruction crée tous les index nécessaires:
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
$$
Tous ces index partiels couvrent ensemble la table une fois. Ils ont à peu près la même taille qu'un index de base sur toute la table:
SELECT pg_size_pretty(pg_relation_size('lexikon')); -- 50 MB
SELECT pg_size_pretty(pg_total_relation_size('lexikon')); -- 71 MB
Jusqu'à présent, seuls 21 Mo d'index pour une table de 50 Mo.
Je crée la plupart des index partiels sur (lset, frequency DESC)
. La deuxième colonne n'aide que dans des cas particuliers. Mais comme les deux colonnes impliquées sont de type integer
, en raison des spécificités de l' alignement des données en combinaison avec MAXALIGN dans PostgreSQL, la deuxième colonne n'agrandit pas l'index. C'est une petite victoire pour presque aucun coût.
Cela ne sert à rien de le faire pour des index partiels qui ne couvrent qu'une seule fréquence. Ce sont juste sur (lset)
. Les index créés ressemblent à ceci:
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;
Une fonction
La fonction est quelque peu similaire dans le style à la solution de @ Jack:
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;
Différences clés:
SQL dynamique avec RETURN QUERY EXECUTE
.
Au fur et à mesure que nous parcourons les étapes, un plan de requête différent peut être bénéficiaire. Le plan de requête pour le SQL statique est généré une fois, puis réutilisé, ce qui peut économiser des frais généraux. Mais dans ce cas, la requête est simple et les valeurs sont très différentes. Dynamic SQL sera une grande victoire.
DynamiqueLIMIT
pour chaque étape de requête.
Cela aide de plusieurs façons: Premièrement, les lignes ne sont extraites que si nécessaire. En combinaison avec SQL dynamique, cela peut également générer différents plans de requête pour commencer. Deuxièmement: pas besoin d'un supplément LIMIT
dans l'appel de fonction pour couper le surplus.
Référence
Installer
J'ai choisi quatre exemples et effectué trois tests différents avec chacun. J'ai pris le meilleur des cinq pour comparer avec le cache chaud:
La requête SQL brute du formulaire:
SELECT *
FROM lexikon
WHERE lset >= 20000
AND lset <= 30000
ORDER BY frequency DESC
LIMIT 5;
La même chose après la création de cet index
CREATE INDEX ON lexikon(lset);
A besoin du même espace que tous mes index partiels ensemble:
SELECT pg_size_pretty(pg_total_relation_size('lexikon')) -- 93 MB
La fonction
SELECT * FROM f_search(20000, 30000, 5);
Résultats
SELECT * FROM f_search(20000, 30000, 5);
1: Autonomie totale: 315,458 ms
2 : Autonomie totale: 36,458 ms
3: Autonomie totale: 0,330 ms
SELECT * FROM f_search(60000, 65000, 100);
1: Autonomie totale: 294.819 ms
2 : Autonomie totale: 18.915 ms
3: Autonomie totale: 1.414 ms
SELECT * FROM f_search(10000, 70000, 100);
1: Autonomie totale: 426.831 ms
2 : Autonomie totale: 217.874 ms
3: Autonomie totale: 1.611 ms
SELECT * FROM f_search(1, 1000000, 5);
1: Durée totale d'exécution: 2458,205 ms
2 : Durée totale d'exécution: 2458,205 ms - pour de grandes plages de lset, le balayage séquentiel est plus rapide que l'index.
3: Durée d'exécution totale: 0,266 ms
Conclusion
Comme prévu, l'avantage de la fonction augmente avec des gammes plus grandes lset
et plus petitesLIMIT
.
Avec de très petites plages delset
, la requête brute en combinaison avec l'index est en fait plus rapide . Vous voudrez tester et peut-être branch: requête brute pour de petites plages de lset
, sinon appel de fonction. Vous pouvez même simplement intégrer cela dans la fonction pour un "meilleur des deux mondes" - c'est ce que je ferais.
En fonction de la distribution de vos données et des requêtes typiques, d'autres étapes lex_freq
peuvent améliorer les performances. Testez pour trouver le bon endroit. Avec les outils présentés ici, cela devrait être facile à tester.