web-dev-qa-db-fra.com

Correspondance de modèles avec LIKE, SIMILAR TO ou des expressions régulières dans PostgreSQL

J'ai dû écrire une simple requête où je vais chercher le nom des gens qui commencent par un B ou un D:

SELECT s.name 
FROM spelers s 
WHERE s.name LIKE 'B%' OR s.name LIKE 'D%'
ORDER BY 1

Je me demandais s'il y avait un moyen de réécrire cela pour devenir plus performant. Je peux donc éviter or et/ou like?

103
Lucas Kauffman

Votre requête est à peu près optimale. La syntaxe ne sera pas beaucoup plus courte, la requête ne sera pas beaucoup plus rapide:

SELECT name
FROM   spelers
WHERE  name LIKE 'B%' OR name LIKE 'D%'
ORDER  BY 1;

Si vous voulez vraiment raccourcir la syntaxe, utilisez une expression régulière avec branches:

...
WHERE  name ~ '^(B|D).*'

Ou légèrement plus rapide, avec une classe de caractères:

...
WHERE  name ~ '^[BD].*'

Un test rapide sans index donne des résultats plus rapides que pour SIMILAR TO Dans les deux cas pour moi.
Avec un index B-Tree approprié en place, LIKE remporte cette course par ordre de grandeur.

Lisez les bases de correspondance des motifs dans le manuel .

Index pour des performances supérieures

Si vous êtes préoccupé par les performances, créez un index comme celui-ci pour les plus grandes tables:

CREATE INDEX spelers_name_special_idx ON spelers (name text_pattern_ops);

Rend ce type de requête plus rapide par ordre de grandeur. Des considérations spéciales s'appliquent à l'ordre de tri spécifique aux paramètres régionaux. En savoir plus sur classes d'opérateurs dans le manuel . Si vous utilisez les paramètres régionaux "C" standard (la plupart des gens ne le font pas), un index simple (avec la classe d'opérateur par défaut) fera l'affaire.

Un tel index n'est bon que pour les modèles ancrés à gauche (correspondance depuis le début de la chaîne).

SIMILAR TO Ou des expressions régulières avec des expressions basiques ancrées à gauche peuvent également utiliser cet index. Mais pas avec des branches (B|D) Ou des classes de caractères [BD] (Au moins dans mes tests sur PostgreSQL 9.0).

Les correspondances de trigrammes ou la recherche de texte utilisent des index GIN ou Gist spéciaux.

Présentation des opérateurs de mise en correspondance de modèles

  • LIKE (~~) est simple et rapide mais limité dans ses capacités.
    ILIKE (~~*) la variante insensible à la casse.
    pg_trgm étend la prise en charge de l'index pour les deux.

  • ~ (correspondance d'expression régulière) est puissant mais plus complexe et peut être lent pour autre chose que de base expressions.

  • SIMILAR TO est juste inutile. Un métis particulier de LIKE et d'expressions régulières. Je ne l'utilise jamais. Voir ci-dessous.

  • % est l'opérateur de "similitude", fourni par le module supplémentaire pg_trgm. Voir ci-dessous.

  • @@ est l'opérateur de recherche de texte. Voir ci-dessous.

pg_trgm - correspondance de trigrammes

À partir de PostgreSQL 9.1 , vous pouvez faciliter l'extension pg_trgm pour fournir un support d'index pour any LIKE/ILIKE modèle (et modèles d'expressions rationnelles simples avec ~) en utilisant un index GIN ou Gist.

Détails, exemple et liens:

pg_trgm Fournit également ces opérateurs :

  • % - l'opérateur de "similitude"
  • <% (commutateur: %>) - l'opérateur "Word_similarity" dans Postgres 9.6 ou version ultérieure
  • <<% (commutateur: %>>) - l'opérateur "strict_Word_similarity" dans Postgres 11 ou version ultérieure

Recherche de texte

Est un type spécial de correspondance de modèles avec des types d'infrastructure et d'index distincts. Il utilise des dictionnaires et des racines et est un excellent outil pour trouver des mots dans des documents, en particulier pour les langues naturelles.

Correspondance de préfixe est également pris en charge:

Ainsi que recherche de phrases depuis Postgres 9.6:

Considérez le introduction dans le manuel et le aperçu des opérateurs et des fonctions .

Outils supplémentaires pour la correspondance de chaînes floues

Le module supplémentaire fuzzystrmatch offre quelques options supplémentaires, mais les performances sont généralement inférieures à toutes les précédentes.

En particulier, diverses implémentations de la fonction levenshtein() peuvent être instrumentales.

Pourquoi les expressions régulières (~) Sont-elles toujours plus rapides que SIMILAR TO?

La réponse est simple. Les expressions SIMILAR TO Sont réécrites en expressions régulières en interne. Ainsi, pour chaque expression SIMILAR TO, Il y a au moins une expression régulière plus rapide (qui évite la surcharge de réécriture de l'expression). Il n'y a aucun gain de performances en utilisant SIMILAR TO jamais .

Et les expressions simples qui peuvent être faites avec LIKE (~~) Sont plus rapides avec LIKE de toute façon.

SIMILAR TO N'est pris en charge que dans PostgreSQL car il s'est retrouvé dans les premières versions du standard SQL. Ils ne s'en sont toujours pas débarrassés. Mais il est prévu de le supprimer et d'inclure des correspondances d'expression rationnelle à la place - ou du moins j'ai entendu.

EXPLAIN ANALYZE Le révèle. Essayez avec n'importe quelle table vous-même!

EXPLAIN ANALYZE SELECT * FROM spelers WHERE name SIMILAR TO 'B%';

Révèle:

...  
Seq Scan on spelers  (cost= ...  
  Filter: (name ~ '^(?:B.*)$'::text)

SIMILAR TO A été réécrit avec une expression régulière (~).

Performance ultime pour ce cas particulier

Mais EXPLAIN ANALYZE En révèle plus. Essayez, avec l'index susmentionné en place:

EXPLAIN ANALYZE SELECT * FROM spelers WHERE name ~ '^B.*;

Révèle:

...
 ->  Bitmap Heap Scan on spelers  (cost= ...
       Filter: (name ~ '^B.*'::text)
        ->  Bitmap Index Scan on spelers_name_text_pattern_ops_idx (cost= ...
              Index Cond: ((prod ~>=~ 'B'::text) AND (prod ~<~ 'C'::text))

En interne, avec un index qui n'est pas sensible aux paramètres régionaux (text_pattern_ops Ou utilisant les paramètres régionaux C), les expressions simples ancrées à gauche sont réécrites avec ces opérateurs de modèle de texte: ~>=~, ~<=~, ~>~, ~<~. C'est le cas pour ~, ~~ Ou SIMILAR TO.

Il en va de même pour les index sur les types varchar avec varchar_pattern_ops Ou char avec bpchar_pattern_ops.

Donc, appliqué à la question d'origine, c'est le moyen le plus rapide possible :

SELECT name
FROM   spelers  
WHERE  name ~>=~ 'B' AND name ~<~ 'C'
    OR name ~>=~ 'D' AND name ~<~ 'E'
ORDER  BY 1;

Bien sûr, s'il vous arrive de rechercher les initiales adjacentes , vous pouvez simplifier davantage:

WHERE  name ~>=~ 'B' AND name ~<~ 'D'   -- strings starting with B or C

Le gain par rapport à une utilisation simple de ~ Ou ~~ Est minime. Si les performances ne sont pas votre exigence primordiale, vous devez simplement vous en tenir aux opérateurs standard - arriver à ce que vous avez déjà dans la question.

171
Erwin Brandstetter

Que diriez-vous d'ajouter une colonne à la table. Selon vos besoins réels:

person_name_start_with_B_or_D (Boolean)

person_name_start_with_char CHAR(1)

person_name_start_with VARCHAR(30)

PostgreSQL ne prend pas en charge colonnes calculées dans les tables de base à la SQL Server mais la nouvelle colonne peut être maintenue via le déclencheur. Évidemment, cette nouvelle colonne serait indexée.

Alternativement, un index sur une expression vous donnerait la même chose, moins cher. Par exemple.:

CREATE INDEX spelers_name_initial_idx ON spelers (left(name, 1)); 

Les requêtes qui correspondent à l'expression dans leurs conditions peuvent utiliser cet index.

De cette façon, la performance est prise lors de la création ou de la modification des données, elle peut donc ne convenir qu'à un environnement à faible activité (c'est-à-dire beaucoup moins d'écritures que de lectures).

11
onedaywhen

Vous pourriez essayer

SELECT s.name
FROM   spelers s
WHERE  s.name SIMILAR TO '(B|D)%' 
ORDER  BY s.name

Cependant, je ne sais pas si ce qui précède ou votre expression d'origine sont discutables dans Postgres.

Si vous créez l'index suggéré serait également intéressé de savoir comment cela se compare aux autres options.

SELECT name
FROM   spelers
WHERE  name >= 'B' AND name < 'C'
UNION ALL
SELECT name
FROM   spelers
WHERE  name >= 'D' AND name < 'E'
ORDER  BY name
8
Martin Smith

Pour vérifier les initiales, j'utilise souvent la conversion en "char" (Avec les guillemets doubles). Ce n'est pas portable, mais très rapide. En interne, il détoxifie simplement le texte et renvoie le premier caractère, et les opérations de comparaison "char" sont très rapides car le type est de longueur fixe de 1 octet:

SELECT s.name 
FROM spelers s 
WHERE s.name::"char" =ANY( ARRAY[ "char" 'B', 'D' ] )
ORDER BY 1

Notez que la conversion en "char" Est plus rapide que la slution ascii() de @ Sole021, mais elle n'est pas compatible UTF8 (ou tout autre encodage d'ailleurs), renvoyant simplement le premier octet, donc devrait ne doit être utilisé que dans les cas où la comparaison est faite avec de vieux caractères 7 bits ASCII.

Très vieille question, mais j'ai trouvé une autre solution rapide à ce problème:

SELECT s.name 
FROM spelers s 
WHERE ascii(s.name) in (ascii('B'),ascii('D'))
ORDER BY 1

Puisque la fonction ascii () ne regarde que le premier caractère de la chaîne.

2
Sole021

Ce que j'ai fait dans le passé, face à un problème de performance similaire, est d'incrémenter le caractère ASCII de la dernière lettre, et de faire une ENTRE. Vous obtenez alors les meilleures performances, pour un sous-ensemble Bien sûr, cela ne fonctionne que dans certaines situations, mais pour les ensembles de données ultra-volumineux où vous recherchez un nom par exemple, il fait passer les performances d'abyssales à acceptables.

2
Mel Padden

Il existe deux méthodes non encore mentionnées pour traiter de tels cas:

  1. index partiel (ou partitionné - s'il est créé pour une plage complète manuellement) - plus utile lorsque seul un sous-ensemble de données est requis (par exemple pendant une maintenance ou temporaire pour certains rapports):

    CREATE INDEX ON spelers WHERE name LIKE 'B%'
    
  2. partitionner la table elle-même (en utilisant le premier caractère comme clé de partitionnement) - cette technique vaut particulièrement la peine d'être envisagée dans PostgreSQL 10+ (partitionnement moins pénible) et 11+ (élagage de partition pendant l'exécution de la requête).

De plus, si les données d'une table sont triées, il peut être avantageux d'utiliser index BRIN (sur le premier caractère).

1
Tomasz Pala