Pattern matching con LIKE, SIMILAR TO o espressioni regolari in PostgreSQL


94

Ho dovuto scrivere una semplice query dove vado alla ricerca del nome della gente che inizia con una B o una D:

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

Mi chiedevo se c'è un modo per riscriverlo per diventare più performanti. Quindi posso evitare ore / o like?


Perché stai cercando di riscrivere? Prestazione? Pulizia? È s.nameindicizzato?
Martin Smith,

Voglio scrivere per le prestazioni, il nome non è indicizzato.
Lucas Kauffman,

8
Bene, mentre stai cercando senza i caratteri jolly e senza selezionare colonne aggiuntive, un indice su namepotrebbe essere utile qui se ti preoccupi delle prestazioni.
Martin Smith,

Risposte:


161

La tua query è praticamente ottimale. La sintassi non sarà molto più breve, la query non sarà molto più veloce:

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

Se vuoi davvero accorciare la sintassi , usa un'espressione regolare con rami :

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

O leggermente più veloce, con una classe di caratteri :

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

Un test rapido senza indice produce risultati più rapidi rispetto a quelli SIMILAR TOin entrambi i casi per me.
Con un indice B-Tree appropriato, LIKEvince questa gara per ordini di grandezza.

Leggi le nozioni di base sulla corrispondenza dei modelli nel manuale .

Indice per prestazioni superiori

Se sei interessato alle prestazioni, crea un indice come questo per tabelle più grandi:

CREATE INDEX spelers_name_special_idx ON spelers (name text_pattern_ops);

Rende più veloce questo tipo di query per ordini di grandezza. Considerazioni speciali si applicano all'ordinamento specifico della locale. Maggiori informazioni sulle classi di operatori nel manuale . Se si utilizza la locale "C" standard (la maggior parte delle persone non lo fa), lo farà un indice semplice (con classe operatore predefinita).

Un tale indice è valido solo per i motivi ancorati a sinistra (corrispondenti dall'inizio della stringa).

SIMILAR TOoppure anche espressioni regolari con espressioni di base ancorate a sinistra possono utilizzare questo indice. Ma non con rami (B|D)o classi di caratteri [BD](almeno nei miei test su PostgreSQL 9.0).

Le corrispondenze trigramma o la ricerca di testo utilizzano indici GIN o GiST speciali.

Panoramica degli operatori di corrispondenza dei modelli

  • LIKE( ~~) è semplice e veloce ma limitato nelle sue capacità.
    ILIKE( ~~*) la variante insensibile al maiuscolo / minuscolo.
    pg_trgm estende il supporto dell'indice per entrambi.

  • ~ (corrispondenza di espressioni regolari) è potente ma più complesso e può essere lento per qualcosa di più delle espressioni di base.

  • SIMILAR TOè semplicemente inutile . Un singolare ibrido di LIKEed espressioni regolari. Non lo uso mai. Vedi sotto.

  • % è l'operatore di "somiglianza", fornito dal modulo aggiuntivopg_trgm. Vedi sotto.

  • @@è l'operatore di ricerca del testo. Vedi sotto.

pg_trgm - corrispondenza trigramma

A partire da PostgreSQL 9.1 è possibile facilitare l'estensione pg_trgmper fornire supporto all'indice per qualsiasi LIKE / ILIKEpattern (e semplici schemi regexp con ~) utilizzando un indice GIN o GiST.

Dettagli, esempio e collegamenti:

pg_trgmfornisce inoltre questi operatori :

  • % - l'operatore di "somiglianza"
  • <%(commutatore %>:) - l'operatore "word_similarity" in Postgres 9.6 o successivo
  • <<%(commutatore %>>:) - l'operatore "strict_word_similarity" in Postgres 11 o successivo

Ricerca di testo

È un tipo speciale di pattern matching con infrastrutture e tipi di indice separati. Utilizza dizionari e stemming ed è un ottimo strumento per trovare parole nei documenti, specialmente per le lingue naturali.

La corrispondenza dei prefissi è inoltre supportata:

Oltre alla ricerca di frasi da Postgres 9.6:

Considerare l' introduzione nel manuale e la panoramica degli operatori e delle funzioni .

Strumenti aggiuntivi per la corrispondenza delle stringhe fuzzy

Il modulo aggiuntivo fuzzystrmatch offre alcune opzioni in più, ma le prestazioni sono generalmente inferiori a tutto quanto sopra.

In particolare, varie implementazioni della levenshtein()funzione possono essere strumentali.

Perché le espressioni regolari ( ~) sono sempre più veloci di SIMILAR TO?

La risposta è semplice SIMILAR TOle espressioni vengono riscritte internamente in espressioni regolari. Quindi, per ogni SIMILAR TOespressione, esiste almeno un'espressione regolare più veloce (che consente di risparmiare il sovraccarico di riscrivere l'espressione). Non si ottiene SIMILAR TO mai un miglioramento delle prestazioni nell'uso .

E le espressioni semplici che possono essere fatte con LIKE( ~~) sono LIKEcomunque più veloci .

SIMILAR TOè supportato solo in PostgreSQL perché è finito nelle prime bozze dello standard SQL. Non se ne sono ancora liberati. Ma ci sono piani per rimuoverlo e includere invece corrispondenze regexp - o almeno così ho sentito.

EXPLAIN ANALYZElo rivela. Prova tu stesso con qualsiasi tavolo!

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

rivela:

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

SIMILAR TOè stato riscritto con un'espressione regolare ( ~).

Massime prestazioni per questo caso particolare

Ma EXPLAIN ANALYZErivela di più. Prova, con l'indice di cui sopra in atto:

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

rivela:

...
 ->  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))

Internamente, con un indice che non è informativi sulla localizzazione ( text_pattern_opso utilizzando impostazioni locali C) semplici espressioni di sinistra-ancorata vengono riscritti con questi operatori modello di testo: ~>=~, ~<=~, ~>~, ~<~. Questo è il caso per ~, ~~o SIMILAR TOsimili.

Lo stesso vale per gli indici sui varchartipi con varchar_pattern_opso charcon bpchar_pattern_ops.

Quindi, applicato alla domanda originale, questo è il modo più veloce possibile :

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

Naturalmente, se dovessi cercare iniziali adiacenti , puoi semplificare ulteriormente:

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

Il guadagno rispetto all'uso semplice ~o ~~è minimo. Se le prestazioni non sono il tuo requisito fondamentale, dovresti semplicemente attenerti agli operatori standard, arrivando a ciò che hai già nella domanda.


L'OP non ha un indice sul nome ma ti capita di sapere, se lo facessero, la loro query originale comporterebbe 2 ricerche di intervallo e similaruna scansione?
Martin Smith,

2
@MartinSmith: un test rapido con EXPLAIN ANALYZE2 scansioni dell'indice bitmap. Le scansioni multiple di indici bitmap possono essere combinate piuttosto rapidamente.
Erwin Brandstetter,

Grazie. Quindi ci sarebbe qualche milage con la sostituzione ORdi UNION ALLo name LIKE 'B%'con name >= 'B' AND name <'C'Postgres?
Martin Smith,

1
@MartinSmith: UNIONno ma, sì, combinando gli intervalli in una WHEREclausola si accelera la query. Ho aggiunto altro alla mia risposta. Ovviamente, devi tenere conto delle tue impostazioni locali. La ricerca in base alle impostazioni locali è sempre più lenta.
Erwin Brandstetter,

2
@a_horse_with_no_name: non mi aspetto. Le nuove funzionalità di pg_tgrm con indici GIN sono un piacere per la ricerca di testo generica. Una ricerca ancorata all'inizio è già più veloce di così.
Erwin Brandstetter,

11

Che ne dici di aggiungere una colonna alla tabella. A seconda delle esigenze effettive:

person_name_start_with_B_or_D (Boolean)

person_name_start_with_char CHAR(1)

person_name_start_with VARCHAR(30)

PostgreSQL non supporta le colonne calcolate nelle tabelle di base su SQL Server, ma la nuova colonna può essere gestita tramite trigger. Ovviamente, questa nuova colonna verrebbe indicizzata.

In alternativa, un indice su un'espressione ti darebbe lo stesso, più economico. Per esempio:

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

Le query che corrispondono all'espressione nelle loro condizioni possono utilizzare questo indice.

In questo modo, l'hit di performance viene rilevato quando i dati vengono creati o modificati, quindi potrebbe essere appropriato solo per un ambiente a bassa attività (ovvero molto meno scritture rispetto alle letture).


8

Potresti provare

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

Tuttavia, non ho idea se la precedente o la tua espressione originale siano estesi in Postgres.

Se si crea l'indice suggerito sarebbe anche interessato a sapere come questo si confronta con le altre opzioni.

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

1
Ha funzionato e ho ottenuto un costo di 1,19 dove avevo 1,25. Grazie !
Lucas Kauffman,

2

Quello che ho fatto in passato, di fronte a un problema di prestazioni simile, è aumentare il carattere ASCII dell'ultima lettera e fare un TRA. Quindi ottieni le migliori prestazioni, per un sottoinsieme della funzionalità LIKE. Ovviamente, funziona solo in determinate situazioni, ma per insiemi di dati di grandi dimensioni in cui stai cercando un nome, ad esempio, rende le prestazioni da abissali a accettabili.


2

Domanda molto vecchia, ma ho trovato un'altra soluzione rapida a questo problema:

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

Poiché la funzione ascii () guarda solo al primo carattere della stringa.


1
Questo utilizza un indice su (name)?
ypercubeᵀᴹ

2

Per il controllo delle iniziali, uso spesso casting "char"(con le doppie virgolette). Non è portatile, ma molto veloce. Internamente, detoast semplicemente il testo e restituisce il primo carattere e le operazioni di confronto "char" sono molto veloci perché il tipo ha una lunghezza fissa di 1 byte:

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

Nota che il casting su "char"è più veloce della ascii()slution di @ Sole021, ma non è compatibile con UTF8 (o qualsiasi altra codifica del caso), restituendo semplicemente il primo byte, quindi dovrebbe essere usato solo nei casi in cui il confronto è contro il vecchio 7 -bit caratteri ASCII.


1

Esistono due metodi non ancora menzionati per affrontare tali casi:

  1. indice parziale (o partizionato - se creato manualmente per l'intero intervallo) - molto utile quando è richiesto solo un sottoinsieme di dati (ad esempio durante alcuni interventi di manutenzione o temporaneo per alcuni rapporti):

    CREATE INDEX ON spelers WHERE name LIKE 'B%'
  2. partizionamento della tabella stessa (usando il primo carattere come chiave di partizionamento) - questa tecnica è particolarmente degna di considerazione in PostgreSQL 10+ (partizionamento meno doloroso) e 11+ (potatura della partizione durante l'esecuzione della query).

Inoltre, se i dati in una tabella vengono ordinati, si può beneficiare dell'utilizzo dell'indice BRIN (sul primo carattere).


-4

Probabilmente più veloce per fare un confronto di un singolo personaggio:

SUBSTR(s.name,1,1)='B' OR SUBSTR(s.name,1,1)='D'

1
Non proprio. column LIKE 'B%'sarà più efficiente dell'utilizzo della funzione di sottostringa sulla colonna.
ypercubeᵀᴹ
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.