Coincidencia de patrones con LIKE, SIMILAR TO o expresiones regulares en PostgreSQL

94

Tuve que escribir una consulta simple donde busco el nombre de las personas que comienzan 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

Me preguntaba si hay una manera de reescribir esto para ser más eficiente. ¿Entonces puedo evitar ory / o like?

Lucas Kauffman
fuente
¿Por qué estás tratando de reescribir? ¿Actuación? ¿Pulcritud? ¿Está s.nameindexado?
Martin Smith
Quiero escribir para el rendimiento, s.name no está indexado.
Lucas Kauffman el
8
Bueno, ya que está buscando sin comodines iniciales y sin seleccionar ninguna columna adicional, un índice namepodría ser útil aquí si le importa el rendimiento.
Martin Smith

Respuestas:

161

Su consulta es más o menos la óptima. La sintaxis no será mucho más corta, la consulta no será mucho más rápida:

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

Si realmente quiere acortar la sintaxis , use una expresión regular con ramas :

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

O un poco más rápido, con una clase de personaje :

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

Una prueba rápida sin índice produce resultados más rápidos que SIMILAR TOen cualquier caso para mí.
Con un índice B-Tree apropiado, LIKEgana esta carrera por orden de magnitud.

Lea los conceptos básicos sobre la coincidencia de patrones en el manual .

Índice para un rendimiento superior

Si le preocupa el rendimiento, cree un índice como este para tablas más grandes:

CREATE INDEX spelers_name_special_idx ON spelers (name text_pattern_ops);

Hace este tipo de consulta más rápido por orden de magnitud. Se aplican consideraciones especiales para el orden de clasificación específico de la localidad. Lea más sobre las clases de operador en el manual . Si está utilizando la configuración regional "C" estándar (la mayoría de la gente no lo hace), un índice simple (con la clase de operador predeterminada) servirá.

Tal índice solo es bueno para patrones anclados a la izquierda (coincidencia desde el inicio de la cadena).

SIMILAR TOo expresiones regulares con expresiones básicas ancladas a la izquierda también pueden usar este índice. Pero no con ramas (B|D)o clases de caracteres [BD](al menos en mis pruebas en PostgreSQL 9.0).

Las coincidencias de trigrama o la búsqueda de texto utilizan índices especiales GIN o GiST.

Descripción general de los operadores de coincidencia de patrones

  • LIKE( ~~) es simple y rápido pero limitado en sus capacidades.
    ILIKE( ~~*) la variante insensible a mayúsculas y minúsculas.
    pg_trgm extiende el soporte de índice para ambos.

  • ~ (coincidencia de expresión regular) es potente pero más complejo y puede ser lento para cualquier cosa más que expresiones básicas.

  • SIMILAR TOsimplemente no tiene sentido . Un mestizo peculiar de LIKEy expresiones regulares. Nunca lo uso Vea abajo.

  • % es el operador de "similitud", proporcionado por el módulo adicionalpg_trgm. Vea abajo.

  • @@es el operador de búsqueda de texto. Vea abajo.

pg_trgm - coincidencia de trigram

Comenzando con PostgreSQL 9.1 , puede facilitar la extensión pg_trgmpara proporcionar soporte de índice para cualquier LIKE / ILIKEpatrón (y patrones de expresión regular simples con ~) usando un índice GIN o GiST.

Detalles, ejemplo y enlaces:

pg_trgmTambién proporciona estos operadores :

  • % - el operador de "similitud"
  • <%(conmutador %>:) - el operador "word_similarity" en Postgres 9.6 o posterior
  • <<%(conmutador %>>:) - el operador "estricta_palabra_similaridad" en Postgres 11 o posterior

Búsqueda de texto

Es un tipo especial de coincidencia de patrones con infraestructura separada y tipos de índice. Utiliza diccionarios y derivaciones y es una gran herramienta para encontrar palabras en documentos, especialmente para idiomas naturales.

La coincidencia de prefijos también es compatible:

Además de la búsqueda de frases desde Postgres 9.6:

Considere la introducción en el manual y el resumen de operadores y funciones .

Herramientas adicionales para la coincidencia de cadenas difusas

El módulo adicional fuzzystrmatch ofrece algunas opciones más, pero el rendimiento generalmente es inferior a todo lo anterior.

En particular, diversas implementaciones de la levenshtein()función pueden ser instrumentales.

¿Por qué las expresiones regulares ( ~) siempre son más rápidas que SIMILAR TO?

La respuesta es simple. SIMILAR TOLas expresiones se reescriben en expresiones regulares internamente. Entonces, para cada SIMILAR TOexpresión, hay al menos una expresión regular más rápida (que ahorra la sobrecarga de reescribir la expresión). No hay ganancia de rendimiento al usar SIMILAR TO nunca .

Y las expresiones simples que se pueden hacer con LIKE( ~~) son más rápidas con de LIKEtodos modos.

SIMILAR TOsolo es compatible con PostgreSQL porque terminó en los primeros borradores del estándar SQL. Todavía no se han librado de eso. Pero hay planes para eliminarlo e incluir coincidencias regexp, o eso escuché.

EXPLAIN ANALYZElo revela ¡Intenta con cualquier mesa tú mismo!

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

Revela:

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

SIMILAR TOha sido reescrito con una expresión regular ( ~).

Máximo rendimiento para este caso particular

Pero EXPLAIN ANALYZErevela más. Pruebe, con el índice mencionado anteriormente en su lugar:

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, con un índice que no está al tanto de la configuración regional ( text_pattern_opso el uso de la configuración regional C) simples expresiones de izquierda-anclado se reescriben con estos operadores patrón de texto: ~>=~, ~<=~, ~>~, ~<~. Este es el caso para ~, ~~o SIMILAR TOpor igual.

Lo mismo es cierto para los índices en varchartipos con varchar_pattern_opso charcon bpchar_pattern_ops.

Entonces, aplicado a la pregunta original, esta es la forma más rápida posible :

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

Por supuesto, si por casualidad busca iniciales adyacentes , puede simplificar aún más:

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

La ganancia sobre el uso simple de ~o ~~es pequeña. Si el rendimiento no es su requisito primordial, debe seguir con los operadores estándar, llegando a lo que ya tiene en la pregunta.

Erwin Brandstetter
fuente
El OP no tiene un índice de nombre, pero ¿sabe si su consulta original implicaría 2 búsquedas de rango y similarun escaneo?
Martin Smith
2
@ MartininSmith: Una prueba rápida con EXPLAIN ANALYZEmuestra 2 escaneos de índice de mapa de bits. Se pueden combinar múltiples escaneos de índice de mapa de bits con bastante rapidez.
Erwin Brandstetter
Gracias. Entonces, ¿habría algún milagro con reemplazar el ORcon UNION ALLo reemplazar name LIKE 'B%'con name >= 'B' AND name <'C'en Postgres?
Martin Smith
1
@MartinSmith: UNIONno, pero sí, combinar los rangos en una WHEREcláusula acelerará la consulta. He agregado más a mi respuesta. Por supuesto, debe tener en cuenta su configuración regional. La búsqueda local es siempre más lenta.
Erwin Brandstetter
2
@a_horse_with_no_name: espero que no. Las nuevas capacidades de pg_tgrm con índices GIN son un placer para la búsqueda de texto genérico. Una búsqueda anclada al inicio ya es más rápida que eso.
Erwin Brandstetter
11

¿Qué tal agregar una columna a la tabla? Dependiendo de sus requisitos reales:

person_name_start_with_B_or_D (Boolean)

person_name_start_with_char CHAR(1)

person_name_start_with VARCHAR(30)

PostgreSQL no admite columnas calculadas en tablas base a SQL Server, pero la nueva columna se puede mantener a través del disparador. Obviamente, esta nueva columna sería indexada.

Alternativamente, un índice en una expresión le daría lo mismo, más barato. P.ej:

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

Las consultas que coinciden con la expresión en sus condiciones pueden utilizar este índice.

De esta forma, el impacto en el rendimiento se toma cuando se crean o modifican los datos, por lo que solo puede ser apropiado para un entorno de baja actividad (es decir, muchas menos escrituras que lecturas).

un día cuando
fuente
8

Podrías intentar

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

Sin embargo, no tengo idea de si la expresión anterior o la original son compatibles en Postgres.

Si crea el índice sugerido, también le interesaría saber cómo se compara con las otras opciones.

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
fuente
1
Funcionó y obtuve un costo de 1.19 donde tenía 1.25. Gracias !
Lucas Kauffman el
2

Lo que he hecho en el pasado, frente a un problema de rendimiento similar, es incrementar el carácter ASCII de la última carta y hacer un ENTRE. Entonces obtienes el mejor rendimiento, para un subconjunto de la funcionalidad LIKE. Por supuesto, solo funciona en ciertas situaciones, pero para los conjuntos de datos ultra grandes en los que está buscando un nombre, por ejemplo, hace que el rendimiento pase de abismal a aceptable.

Mel Padden
fuente
2

Pregunta muy antigua, pero encontré otra solución rápida a este problema:

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

Dado que la función ascii () solo busca el primer carácter de la cadena.

Suela021
fuente
1
¿Utiliza esto un índice (name)?
ypercubeᵀᴹ
2

Para verificar las iniciales, a menudo uso la conversión a "char"(con comillas dobles). No es portátil, pero es muy rápido. Internamente, simplemente desintoxica el texto y devuelve el primer carácter, y las operaciones de comparación "char" son muy rápidas porque el tipo tiene una longitud fija de 1 byte:

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

Tenga en cuenta que la conversión a "char"es más rápida que la ascii()solución de @ Sole021, pero no es compatible con UTF8 (o cualquier otra codificación), devuelve simplemente el primer byte, por lo que solo debe usarse en casos donde la comparación es contra el viejo 7 -bit caracteres ASCII.

Ziggy Crueltyfree Zeitgeister
fuente
1

Existen dos métodos que no se han mencionado aún para tratar estos casos:

  1. índice parcial (o particionado, si se creó para el rango completo manualmente), más útil cuando solo se requiere un subconjunto de datos (por ejemplo, durante algún mantenimiento o temporal para algunos informes):

    CREATE INDEX ON spelers WHERE name LIKE 'B%'
  2. particionando la tabla en sí (usando el primer carácter como clave de partición): vale la pena considerar esta técnica especialmente en PostgreSQL 10+ (partición menos dolorosa) y 11+ (poda de partición durante la ejecución de la consulta).

Además, si los datos de una tabla están ordenados, uno puede beneficiarse del uso del índice BRIN (sobre el primer carácter).

Tomasz Pala
fuente
-4

Probablemente más rápido para hacer una comparación de un solo personaje:

SUBSTR(s.name,1,1)='B' OR SUBSTR(s.name,1,1)='D'
usuario2653985
fuente
1
Realmente no. column LIKE 'B%'será más eficiente que usar la función de subcadena en la columna.
ypercubeᵀᴹ