J'ai une table Postgres avec plus de 20 m de tuples:
first_name | last_name | email
-------------------------------------------
bat | man | [email protected]
arya | vidal | [email protected]
max | joe | [email protected]
Pour filtrer les enregistrements que j'utilise:
SELECT *
FROM people
WHERE (first_name || '' || last_name) ILIKE '%bat%man%' OR
first_name ILIKE '%bat%man%' OR
last_name ILIKE '%bat%man%' OR
email ILIKE '%bat%man%'
LIMIT 25 OFFSET 0
Même avec des index, la recherche prend presque ne minute pour retourner les résultats.
[.____] Il y a des index pour (first_name || '' || last_name)
, first_name
, last_name
et email
.
Que puis-je faire pour améliorer la performance de cette requête?
Pour votre type de motif correspondant, vous utilisez le mieux un indice de trigramme. Lisez ceci en premier:
Je suppose qu'il y a une faute de frappe dans votre expression (first_name || '' || last_name)
, Qui n'a aucun sens avec une chaîne vide et que vous voulez vraiment (first_name || ' ' || last_name)
- avec un caractère spatial.
En supposant que l'une des deux colonnes puisse être NULL, vous auriez besoin d'une concaténation null-Safe, la solution simple est concat_ws()
:
Mais cette fonction n'est pas IMMUTABLE
(explication dans la réponse liée), vous ne pouvez donc pas l'utiliser directement dans une expression d'index. Vous pouvez utiliser un IMMUTABLE
wrapper de fonction:
CREATE OR REPLACE FUNCTION f_immutable_concat_ws(s text, t1 text, t2 text)
RETURNS text AS
$func$
SELECT concat_ws(s, t1, t2)
$func$ LANGUAGE sql IMMUTABLE;
L'emballage peut être IMMUTABLE
car il ne prend que text
paramètres.
[.____] de toute façon, c'est plus verbeux mais a moins de frais généraux internes et est considérablement plus rapide:
CREATE OR REPLACE FUNCTION f_immutable_concat_ws1(s text, t1 text, t2 text)
RETURNS text AS
$func$
SELECT CASE
WHEN t1 IS NULL THEN t2
WHEN t2 IS NULL THEN t1
ELSE t1 || s || t2
END
$func$ LANGUAGE sql IMMUTABLE;
Ou, avec caractère d'espace codé dur:
CREATE OR REPLACE FUNCTION f_concat_space(t1 text, t2 text)
RETURNS text AS
$func$
SELECT CASE
WHEN t1 IS NULL THEN t2
WHEN t2 IS NULL THEN t1
ELSE t1 || ' ' || t2
END
$func$ LANGUAGE sql IMMUTABLE;
Baser l'index sur cette fonction, je suggère:
CREATE INDEX people_special_gin_trgm_idx ON people
USING gin (f_concat_space(first_name, last_name) gin_trgm_ops, email gin_trgm_ops);
J'ai ajouté email
comme deuxième colonne d'index pour plusieurs considérations.
La création de l'index prendra un certain temps pour 20 millions de lignes, ce qui n'est préférable pas pendant la charge supérieure, ou peut-être utiliser CREATE INDEX CONCURRENTLY ...
. Un indice de gin est considérablement plus grand qu'un indice de BTRee ordinaire et également plus coûteux à entretenir. Assurez-vous d'exécuter la dernière version de Postgres, d'importantes améliorations pour les index GIN dans les versions récentes.
Ensuite, votre requête légèrement adaptée et simplifiée devrait être rapide et correct:
SELECT *
FROM people
WHERE f_concat_space(first_name, last_name) ILIKE '%bat%man%' OR
email ILIKE '%bat%man%'
LIMIT 25;
Vous n'avez besoin que de une Index pour cette requête.
Principes de base pour la correspondance des motifs: