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

94

Je devais écrire une requête simple où je cherchais le nom des personnes commençant 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. Donc je peux éviter oret / ou like?

Lucas Kauffman
la source
Pourquoi essayez-vous de réécrire? Performance? Propreté? Est s.nameindexé?
Martin Smith
Je veux écrire pour la performance, le nom n'est pas indexé.
Lucas Kauffman
8
Bien que vous effectuez une recherche sans grands caractères génériques et que vous ne sélectionniez pas de colonnes supplémentaires sur lesquelles un index namepourrait être utile ici si vous vous souciez de la performance.
Martin Smith

Réponses:

161

Votre requête est à peu près l'optimum. 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 des branches :

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

Ou légèrement plus vite, avec une classe de personnage :

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

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

Lisez les bases sur la correspondance des modèles dans le manuel .

Indice pour une performance supérieure

Si les performances vous préoccupent, créez un index comme celui-ci pour les tables plus volumineuses:

CREATE INDEX spelers_name_special_idx ON spelers (name text_pattern_ops);

Rend ce genre de requête plus rapidement par ordre de grandeur. Des considérations spéciales s'appliquent à l'ordre de tri spécifique à l'environnement local. En savoir plus sur les 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 utile que pour les modèles ancrés à gauche (correspondant dès le début de la chaîne).

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

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

Vue d'ensemble des opérateurs de filtrage

  • LIKE( ~~) est simple et rapide, mais limité dans ses capacités.
    ILIKE( ~~*) la variante insensible à la casse.
    pg_trgm étend le support d'index pour les deux.

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

  • SIMILAR TOest juste inutile . Demi-sang particulier LIKEet expressions régulières. Je ne l'utilise jamais. Voir ci-dessous.

  • % est l'opérateur de "similarité" fourni par le module supplémentairepg_trgm. Voir ci-dessous.

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

pg_trgm - correspondance de trigramme

À partir de PostgreSQL 9.1, vous pouvez faciliter l’extension pg_trgmpour fournir un support d’index pour tout motif LIKE/ ILIKEpattern (et de simples modèles d’expression rationnelle avec ~) à l’aide d’un index GIN ou GiST.

Détails, exemple et liens:

pg_trgmfournit également ces opérateurs :

  • % - l'opérateur "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èle avec des types d’infrastructure et d’index séparés. Il utilise des dictionnaires et est un outil génial pour trouver des mots dans les documents, en particulier pour les langues naturelles.

La correspondance de préfixe est également prise en charge:

Ainsi que la recherche de phrase depuis Postgres 9.6:

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

Outils supplémentaires pour la correspondance de chaîne fuzzy

Le module supplémentaire fuzzystrmatch offre quelques options supplémentaires, mais les performances sont généralement inférieures à toutes les solutions ci-dessus.

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

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

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

Et de toute façon, les expressions simples pouvant être réalisées avec LIKE( ~~) sont plus rapides LIKE.

SIMILAR TOn’est supporté que par 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 l'enlever et d'inclure plutôt des correspondances d'expressions rationnelles - ou du moins l'ai-je entendu dire.

EXPLAIN ANALYZEle révèle. Essayez juste 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 TOa été réécrit avec une expression régulière ( ~).

Performance ultime pour ce cas particulier

Mais EXPLAIN ANALYZEré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 indice qui est locale-conscient de ne pas ( text_pattern_opsou à l' aide locale C) simples expressions ancrées à gauche sont réécrites avec ces opérateurs de modèle de texte: ~>=~, ~<=~, ~>~, ~<~. C'est le cas pour ~, ~~ou SIMILAR TOsemblable.

Il en va de même pour les index sur les varchartypes avec varchar_pattern_opsou charavec bpchar_pattern_ops.

Donc, appliqué à la question initiale, 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 arrivait de rechercher des initiales adjacentes , vous pouvez simplifier:

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

Le gain par rapport à l'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 pour arriver à ce que vous avez déjà dans la question.

Erwin Brandstetter
la source
Le PO n'a pas d'index sur le nom, mais savez-vous que s'il le faisait, sa requête initiale impliquerait 2 recherches de plage et similarun balayage?
Martin Smith
2
@MartinSmith: un test rapide avec EXPLAIN ANALYZEdeux analyses d'index bitmap. Plusieurs analyses d'index bitmap peuvent être combinées assez rapidement.
Erwin Brandstetter
Merci. Alors, y aurait-il lieu de remplacer le remplacement ORpar par UNION ALLou name LIKE 'B%'par name >= 'B' AND name <'C'Postgres?
Martin Smith
1
@MartinSmith: UNIONne le fera pas mais, oui, la combinaison des plages dans une WHEREclause accélérera la requête. J'ai ajouté plus à ma réponse. Bien sûr, vous devez tenir compte de vos paramètres régionaux. La recherche tenant compte des paramètres régionaux est toujours plus lente.
Erwin Brandstetter
2
@a_horse_with_no_name: J'espère que non. Les nouvelles fonctionnalités de pg_tgrm avec les index GIN sont un régal pour la recherche de texte générique. Une recherche ancrée au départ est déjà plus rapide que cela.
Erwin Brandstetter
11

Que diriez-vous d'ajouter une colonne à la table. En fonction de 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 les colonnes calculées dans les tables de base à la manière de SQL Server, mais la nouvelle colonne peut être gérée via un déclencheur. De toute évidence, 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 au moment de la création ou de la modification des données; elle ne peut donc être appropriée que dans un environnement peu actif (c.-à-d. Beaucoup moins d'écritures que de lectures).

un jour quand
la source
8

Tu pourrais essayer

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

Je ne sais pas si ce qui précède ou votre expression originale sont vraisemblables dans Postgres.

Si vous créez l'index suggéré, vous voudriez également savoir comment il 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
Martin Smith
la source
1
Cela a fonctionné et j'ai eu un coût de 1,19 alors que j'avais 1,25. Merci !
Lucas Kauffman
2

Par le passé, face à un problème de performances similaire, j’ai incrémenté le caractère ASCII de la dernière lettre et effectué un BETWEEN. Vous obtenez alors les meilleures performances, pour un sous-ensemble de la fonctionnalité LIKE. Bien sûr, cela ne fonctionne que dans certaines situations, mais pour les très grands ensembles de données dans lesquels vous recherchez un nom, par exemple, les performances deviennent catastrophiques à acceptables.

Mel Padden
la source
2

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

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

Sole021
la source
1
Est-ce que cela utilise un index (name)?
Ypercubeᵀᴹ
2

Pour vérifier les initiales, j’utilise souvent le casting avec "char"(avec les guillemets). Ce n'est pas portable, mais très rapide. En interne, il détache simplement le texte et renvoie le premier caractère. Les opérations de comparaison "char" sont très rapides car le type est une 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 le casting sur "char"est plus rapide que la ascii()résolution de @ Sole021, mais il n'est pas compatible UTF8 (ni aucun autre codage d'ailleurs), renvoyant simplement le premier octet. caractères ASCII à 2 bits.

Ziggy Crueltyfree Zeitgeister
la source
1

Deux méthodes ne sont pas encore mentionnées pour traiter de tels cas:

  1. index partiel (ou partitionné - s'il est créé manuellement pour l'ensemble de la plage) - utile lorsque seulement un sous-ensemble de données est requis (par exemple, lors de certaines tâches de 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 est particulièrement intéressante dans PostgreSQL 10+ (partitionnement moins pénible) et 11+ (élagage de partition lors de l'exécution de la requête).

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

Tomasz Pala
la source
-4

Probablement plus rapide de faire une comparaison de caractère unique:

SUBSTR(s.name,1,1)='B' OR SUBSTR(s.name,1,1)='D'
utilisateur2653985
la source
1
Pas vraiment. column LIKE 'B%'sera plus efficace que d'utiliser la fonction de sous-chaîne sur la colonne.
ypercubeᵀᴹ