it-swarm.it

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

Ho dovuto scrivere una semplice query in cui vado alla ricerca del nome delle persone che iniziano 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 or e/o like?

103
Lucas Kauffman

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 abbreviare la sintassi, usa un'espressione regolare con rami:

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

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

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

Un test rapido senza indice produce risultati più veloci rispetto a SIMILAR TO In entrambi i casi per me.
Con un indice B-Tree appropriato, LIKE vince questa gara per ordini di grandezza.

Leggi le basi su corrispondenza del modello 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 su 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 TO O espressioni regolari con espressioni di base ancorate a sinistra possono utilizzare anche questo indice. Ma no 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 motivi

  • 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 delle espressioni regolari) è potente ma più complesso e può essere lento per qualcosa di più che semplice espressioni.

  • SIMILAR TO è solo inutile. Un peculiare semiincrocio di LIKE ed espressioni regolari. Non lo uso mai. Vedi sotto.

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

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

pg_trgm - corrispondenza trigramma

A partire da PostgreSQL 9.1 puoi facilitare l'estensione pg_trgm per fornire supporto all'indice per qualsiasi LIKE/ILIKE pattern (e semplici schemi regexp con ~) usando un indice GIN o Gist.

Dettagli, esempio e collegamenti:

pg_trgm Fornisce anche questi operatori :

  • % - l'operatore "similarità"
  • <% (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.

Corrispondenza prefisso è anche supportato:

Così come ricerca di frasi da Postgres 9.6:

Considera introduzione nel manuale e panoramica di operatori e 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 funzione levenshtein() possono essere strumentali.

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

La risposta è semplice Le espressioni SIMILAR TO Vengono riscritte internamente in espressioni regolari. Quindi, per ogni espressione SIMILAR TO, C'è almeno un'espressione regolare più veloce (che salva il sovraccarico di riscrivere l'espressione). Non si ottiene alcun miglioramento delle prestazioni utilizzando SIMILAR TO mai .

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

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 ANALYZE Lo 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 ANALYZE Rivela 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 è a conoscenza delle impostazioni locali (text_pattern_ops O che utilizza le impostazioni locali C) le espressioni semplici ancorate a sinistra vengono riscritte con questi operatori del modello di testo: ~>=~, ~<=~, ~>~, ~<~. Questo è il caso di ~, ~~ O SIMILAR TO.

Lo stesso vale per gli indici su varchar tipi con varchar_pattern_ops O char con 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 di ~ O ~~ È minuscolo. Se le prestazioni non sono il tuo requisito fondamentale, dovresti semplicemente attenerti agli operatori standard, arrivando a quello che hai già nella domanda.

171

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 colonne calcolate nelle tabelle di base su SQL Server ma la nuova colonna può essere mantenuta 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à (vale a dire molto meno scritture rispetto alle letture).

11
onedaywhen

Potresti provare

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

Non ho idea se la precedente o la tua espressione originale siano o meno rilevanti 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
8
Martin Smith

Per controllare le iniziali, uso spesso il casting su "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

Notare che il casting in "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 può essere utilizzato solo nei casi in cui il confronto è con semplici vecchi 7 bit ASCII.

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.

2
Sole021

Quello che ho fatto in passato, di fronte a un problema di prestazioni simile, è quello di aumentare il carattere ASCII dell'ultima lettera e fare un TRA. Quindi si ottiene la migliore prestazione, per un sottoinsieme della funzionalità LIKE. Ovviamente, funziona solo in determinate situazioni, ma per i set di dati ultra-grandi in cui stai cercando un nome, ad esempio, rende le prestazioni da abissali a accettabili.

2
Mel Padden

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'uso di indice BRIN (sopra il primo carattere).

1
Tomasz Pala