Correspondência de padrões com LIKE, SIMILAR TO ou expressões regulares no PostgreSQL

94

Eu tive que escrever uma consulta simples em que vou procurar o nome das pessoas que começam com um B ou um D:

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

Fiquei me perguntando se existe uma maneira de reescrever isso para se tornar mais eficiente. Para que eu possa evitar ore / ou like?

Lucas Kauffman
fonte
Por que você está tentando reescrever? Atuação? Limpeza? É s.nameindexado?
Martin Smith
Eu quero escrever para desempenho, s.name não está indexado.
Lucas Kauffman
8
Bem, você está pesquisando sem caracteres curinga e não selecionando nenhuma coluna adicional em que um índice namepossa ser útil aqui, se você se preocupa com o desempenho.
Martin Smith

Respostas:

161

Sua consulta é praticamente a ideal. A sintaxe não ficará muito mais curta, a consulta não ficará muito mais rápida:

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

Se você realmente deseja reduzir a sintaxe , use uma expressão regular com branches :

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

Ou um pouco mais rápido, com uma classe de personagem :

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

Um teste rápido sem índice gera resultados mais rápidos do que SIMILAR TOem ambos os casos para mim.
Com um índice B-Tree apropriado, LIKEvence esta corrida por ordens de magnitude.

Leia o básico sobre correspondência de padrões no manual .

Índice para desempenho superior

Se você está preocupado com o desempenho, crie um índice como este para tabelas maiores:

CREATE INDEX spelers_name_special_idx ON spelers (name text_pattern_ops);

Torna esse tipo de consulta mais rápido em ordens de magnitude. Considerações especiais se aplicam à ordem de classificação específica do código do idioma. Leia mais sobre classes de operadores no manual . Se você estiver usando o código de idioma "C" padrão (a maioria das pessoas não), um índice simples (com classe de operador padrão) será suficiente.

Esse índice é bom apenas para padrões ancorados à esquerda (correspondendo desde o início da string).

SIMILAR TOou expressões regulares com expressões ancoradas à esquerda básicas também podem usar esse índice. Mas não com ramos (B|D)ou classes de caracteres [BD](pelo menos nos meus testes no PostgreSQL 9.0).

As correspondências de trigrama ou a pesquisa de texto usam índices especiais GIN ou GiST.

Visão geral dos operadores de correspondência de padrões

  • LIKE( ~~) é simples e rápido, mas limitado em suas capacidades.
    ILIKE( ~~*) a variante que não diferencia maiúsculas de minúsculas.
    pg_trgm estende o suporte ao índice para ambos.

  • ~ (correspondência de expressão regular) é poderoso, mas mais complexo e pode ser lento para algo além de expressões básicas.

  • SIMILAR TOé apenas inútil . Um mestiço peculiar LIKEe expressões regulares. Eu nunca uso isso. Ver abaixo.

  • % é o operador "similaridade", fornecido pelo módulo adicionalpg_trgm. Ver abaixo.

  • @@é o operador de pesquisa de texto. Ver abaixo.

pg_trgm - correspondência de trigrama

A partir do PostgreSQL 9.1, você pode facilitar a extensão pg_trgmpara fornecer suporte ao índice para qualquer padrão LIKE/ ILIKE(e padrões simples de regexp ~) usando um índice GIN ou GiST.

Detalhes, exemplo e links:

pg_trgmtambém fornece esses operadores :

  • % - o operador "similaridade"
  • <%(comutador %>:) - o operador "word_similarity" no Postgres 9.6 ou posterior
  • <<%(comutador %>>:) - o operador "strict_word_similarity" no Postgres 11 ou posterior

Pesquisa de texto

É um tipo especial de correspondência de padrões com tipos de infraestrutura e índice separados. Ele usa dicionários e stemming e é uma ótima ferramenta para encontrar palavras em documentos, especialmente para idiomas naturais.

A correspondência de prefixo também é suportada:

Assim como a pesquisa de frases desde o Postgres 9.6:

Considere a introdução no manual e a visão geral dos operadores e funções .

Ferramentas adicionais para correspondência de seqüência difusa

O módulo adicional fuzzystrmatch oferece mais algumas opções, mas o desempenho geralmente é inferior a todos os itens acima.

Em particular, várias implementações da levenshtein()função podem ser instrumentais.

Por que as expressões regulares ( ~) são sempre mais rápidas que SIMILAR TO?

A resposta é simples. SIMILAR TOexpressões são reescritas em expressões regulares internamente. Portanto, para cada SIMILAR TOexpressão, há pelo menos uma expressão regular mais rápida (que economiza a sobrecarga de reescrever a expressão). Não há ganho de desempenho ao usar SIMILAR TO sempre .

E expressões simples que podem ser feitas com LIKE( ~~) são mais rápidas de LIKEqualquer maneira.

SIMILAR TOsó é suportado no PostgreSQL porque acabou nos primeiros rascunhos do padrão SQL. Eles ainda não se livraram disso. Mas há planos para removê-lo e incluir correspondências regexp - ou pelo menos ouvi dizer.

EXPLAIN ANALYZErevela isso. Apenas tente com qualquer mesa!

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

Revela:

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

SIMILAR TOfoi reescrito com uma expressão regular ( ~).

Melhor desempenho para este caso em particular

Mas EXPLAIN ANALYZErevela mais. Tente, com o índice mencionado anteriormente:

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

Revela:

...
 ->  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, com um índice que não está locale-aware ( text_pattern_opsou usando local C) expressões simples ancorado-esquerda são reescritas com estes operadores padrão de texto: ~>=~, ~<=~, ~>~, ~<~. Este é o caso ~, ~~ou SIMILAR TOsimilar.

O mesmo vale para índices em varchartipos com varchar_pattern_opsou charcom bpchar_pattern_ops.

Portanto, aplicada à pergunta original, esta é a maneira mais rápida possível :

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

Obviamente, se você procurar iniciais adjacentes , poderá simplificar ainda mais:

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

O ganho sobre o uso simples de ~ou ~~é pequeno. Se o desempenho não for seu requisito primordial, você deve apenas manter-se com os operadores padrão - chegando ao que você já tem na pergunta.

Erwin Brandstetter
fonte
O OP não possui um índice no nome, mas você sabia que, se o fizessem, sua consulta original envolveria 2 buscas por intervalo e similaruma verificação?
Martin Smith
2
@ MartinSmith: Um teste rápido com EXPLAIN ANALYZEmostra 2 varreduras de índice de bitmap. Várias verificações de índice de bitmap podem ser combinadas rapidamente.
Erwin Brandstetter
Obrigado. Então haveria qualquer milage com substituindo o ORcom UNION ALLou substituir name LIKE 'B%'com name >= 'B' AND name <'C'no Postgres?
Martin Smith
1
@ MartinSmith: UNIONnão, mas sim, combinar os intervalos em uma WHEREcláusula acelerará a consulta. Eu adicionei mais à minha resposta. Obviamente, você deve levar seu código de idioma em consideração. A pesquisa com reconhecimento de localidade é sempre mais lenta.
Erwin Brandstetter
2
@a_horse_with_no_name: Espero que não. Os novos recursos do pg_tgrm com índices GIN são um tratamento para pesquisa de texto genérica. Uma pesquisa ancorada no início já é mais rápida que isso.
Erwin Brandstetter
11

Que tal adicionar uma coluna à tabela. Dependendo dos seus requisitos reais:

person_name_start_with_B_or_D (Boolean)

person_name_start_with_char CHAR(1)

person_name_start_with VARCHAR(30)

O PostgreSQL não suporta colunas computadas em tabelas base no SQL Server, mas a nova coluna pode ser mantida por meio de gatilho. Obviamente, essa nova coluna seria indexada.

Como alternativa, um índice em uma expressão forneceria o mesmo, mais barato. Por exemplo:

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

As consultas que correspondem à expressão em suas condições podem utilizar esse índice.

Dessa forma, o resultado do desempenho é obtido quando os dados são criados ou alterados; portanto, pode ser apropriado apenas para um ambiente de baixa atividade (ou seja, muito menos gravações do que leituras).

um dia quando
fonte
8

Você poderia tentar

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

Não tenho idéia se a expressão acima ou sua expressão original é sargável no Postgres.

Se você criar o índice sugerido, também ficaria interessado em saber como isso se compara com as outras opções.

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
fonte
1
Funcionou e eu recebi um custo de 1,19 onde eu tinha 1,25. Obrigado !
Lucas Kauffman
2

O que eu fiz no passado, diante de um problema de desempenho semelhante, é incrementar o caractere ASCII da última letra e fazer ENTRE OS. Você obtém o melhor desempenho, para um subconjunto da funcionalidade LIKE. Obviamente, ele só funciona em determinadas situações, mas para conjuntos de dados muito grandes, nos quais você procura um nome, por exemplo, faz com que o desempenho passe de péssimo para aceitável.

Mel Padden
fonte
2

Pergunta muito antiga, mas encontrei outra solução rápida para esse problema:

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

Como a função ascii () olha apenas para o primeiro caractere da string.

Sole021
fonte
1
Isso usa um índice (name)?
ypercubeᵀᴹ
2

Para verificar as iniciais, costumo usar a conversão para "char"(com aspas duplas). Não é portátil, mas muito rápido. Internamente, ele simplesmente desativa o texto e retorna o primeiro caractere, e as operações de comparação "char" são muito rápidas porque o tipo tem comprimento fixo de 1 byte:

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

Observe que a conversão para to "char"é mais rápida que a ascii()slution de @ Sole021, mas não é compatível com UTF8 (ou qualquer outra codificação), retornando simplesmente o primeiro byte; portanto, só deve ser usado nos casos em que a comparação for anterior à simples 7 caracteres ASCII de bits.

Ziggy Crueltyfree Zeitgeister
fonte
1

Existem dois métodos ainda não mencionados para lidar com esses casos:

  1. índice parcial (ou particionado - se criado para todo o intervalo manualmente) - mais útil quando apenas um subconjunto de dados é necessário (por exemplo, durante alguma manutenção ou temporário para alguns relatórios):

    CREATE INDEX ON spelers WHERE name LIKE 'B%'
  2. particionando a própria tabela (usando o primeiro caractere como chave de particionamento) - vale a pena considerar esta técnica no PostgreSQL 10+ (particionamento menos doloroso) e 11+ (remoção da partição durante a execução da consulta).

Além disso, se os dados em uma tabela são classificados, pode-se beneficiar do uso do índice BRIN (sobre o primeiro caractere).

Tomasz Pala
fonte
-4

Provavelmente mais rápido para fazer uma comparação de caracteres únicos:

SUBSTR(s.name,1,1)='B' OR SUBSTR(s.name,1,1)='D'
user2653985
fonte
1
Na verdade não. column LIKE 'B%'será mais eficiente do que usar a função de substring na coluna.
precisa saber é o seguinte