Coincidencia de patrones con expresiones similares, similares o regulares en PostgreSQL
-
16-10-2019 - |
Pregunta
Tuve que escribir una consulta simple en la que busque el nombre de la gente que comience con una B o A 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 representante. Para que pueda evitar or
y / o like
?
Solución
Tu consulta es más o menos la óptima. La sintaxis no se pondrá mucho más corta, la consulta no se volverá mucho más rápido:
SELECT name
FROM spelers
WHERE name LIKE 'B%' OR name LIKE 'D%'
ORDER BY 1;
Si de verdad quieres acortar la sintaxis, usa una expresión regular con sucursales:
...
WHERE name ~ '^(B|D).*'
O un poco más rápido, con un clase de carácter:
...
WHERE name ~ '^[BD].*'
Una prueba rápida sin índice produce resultados más rápidos que para SIMILAR TO
En cualquier caso para mí.
Con un índice B-Tree apropiado en su lugar, LIKE
gana esta carrera por órdenes de magnitud.
Lea los conceptos básicos sobre 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 órdenes de magnitud. Consideraciones especiales se aplican para el orden de clasificación específica de locales. Leer más sobre clases de operador en el manual. Si está utilizando la configuración regional estándar "C" (la mayoría de las personas no), un índice simple (con la clase de operador predeterminada) servirá.
Tal índice solo es bueno para los patrones con ancho a la izquierda (coincidir desde el inicio de la cadena).
SIMILAR TO
o expresiones regulares con expresiones básicas a ancla a la izquierda también pueden usar este índice. Pero no con ramas (B|D)
o clases de personajes [BD]
(al menos en mis pruebas en PostgreSQL 9.0).
Las coincidencias de trigram o la búsqueda de texto usan índices especiales de ginebra 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 al caso.
PG_TRGM extiende el soporte de índice para ambos.~
(Match de expresión regular) es poderoso pero más complejo y puede ser lento para cualquier cosa más que expresiones básicas.SIMILAR TO
es solo inútil. Una mimes peculiar deLIKE
y expresiones regulares. Nunca lo uso. Vea abajo.% es el operador de "similitud", proporcionado por el módulo adicional
pg_trgm
. Vea abajo.@@
es el operador de búsqueda de texto. Vea abajo.
PG_TRGM - Trigram Matching
Empezando con PostgreSQL 9.1 puedes facilitar la extensión pg_trgm
Para proporcionar soporte de índice para ningún LIKE
/ ILIKE
Patrón (y patrones simples de regexp con ~
) usando un índice Gin o GIST.
Detalles, ejemplo y enlaces:
pg_trgm
también provee estos operadores:
%
- El operador de "similitud"<%
(conmutador:%>
) - El operador "Word_Similarity" en Postgres 9.6 o posterior<<%
(conmutador:%>>
) - El operador "strict_word_similarity" 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.
Coincidencia de prefijo también es compatible:
Tanto como búsqueda de frases Desde Postgres 9.6:
Considera el Introducción en el manual y el Descripción general de los 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, varias implementaciones del levenshtein()
La función puede ser instrumental.
¿Por qué son las expresiones regulares (~
) siempre más rápido que SIMILAR TO
?
La respuesta es simple. SIMILAR TO
Las expresiones se reescriben en expresiones regulares internamente. Entonces, por cada SIMILAR TO
expresión, hay al menos Una expresión regular más rápida (que salva la sobrecarga de reescribir la expresión). No hay ganancia de rendimiento en el uso SIMILAR TO
alguna vez.
Y expresiones simples que se pueden hacer con LIKE
(~~
) son más rápidos con LIKE
de todos modos.
SIMILAR TO
Solo se admite en 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 de regexp en su lugar, o eso escuché.
EXPLAIN ANALYZE
lo revela. ¡Solo 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 TO
ha sido reescrito con una expresión regular (~
).
Rendimiento final para este caso particular
Pero EXPLAIN ANALYZE
revela más. Prueba, con el índice mencionado antes:
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 es consciente de locales (text_pattern_ops
o usando localidad C
) Las expresiones simples a la izquierda se reescriben con estos operadores de patrones de texto: ~>=~
, ~<=~
, ~>~
, ~<~
. Este es el caso de ~
, ~~
o SIMILAR TO
similar.
Lo mismo es cierto para los índices en varchar
tipos con varchar_pattern_ops
o char
con bpchar_pattern_ops
.
Entonces, aplicado a la pregunta original, esta es la 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 buscas iniciales adyacentes, puede simplificar más:
WHERE name ~>=~ 'B' AND name ~<~ 'D' -- strings starting with B or C
La ganancia sobre el uso simple de ~
o ~~
es pequeño. Si el rendimiento no es su requisito supremo, debe seguir con los operadores estándar, llegando a lo que ya tiene en la pregunta.
Otros consejos
¿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 es compatible columnas calculadas en tablas base a la 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 Te 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 manera, el éxito de rendimiento se toma cuando los datos se crean o enmendan, por lo que solo puede ser apropiado para un entorno de baja actividad (es decir, muchas menos escrituras que las lecturas).
Tú podrías probar
SELECT s.name
FROM spelers s
WHERE s.name SIMILAR TO '(B|D)%'
ORDER BY s.name
Sin embargo, no tengo idea de si lo anterior o su expresión original son sargables en Postgres.
Si crea el índice sugerido también estaría interesado en escuchar 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
Lo que he hecho en el pasado, enfrentado a un problema de rendimiento similar, es incrementar el carácter ASCII de la última letra y hacer un medio. Luego obtienes el mejor rendimiento, para un subconjunto de la funcionalidad similar. Por supuesto, solo funciona en ciertas situaciones, pero para conjuntos de datos ultra grandes donde está buscando en un nombre, por ejemplo, hace que el rendimiento pase de abismal a aceptable.
Muy antigua pregunta, 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
Ya que la función ASCII () solo parece en el primer carácter de la cadena.
Para verificar las iniciales, a menudo uso el casting para "char"
(con las cotizaciones dobles). No es portátil, pero muy rápido. Internamente, simplemente desata el texto y devuelve el primer carácter, y las operaciones de comparación de "char" son muy rápidas porque el tipo es de 1 byte de longitud fija:
SELECT s.name
FROM spelers s
WHERE s.name::"char" =ANY( ARRAY[ "char" 'B', 'D' ] )
ORDER BY 1
Tenga en cuenta que lanzar a "char"
es más rápido que el ascii()
Slution por @Sole021, pero no es compatible con UTF8 (o cualquier otra codificación para el caso), devolviendo simplemente el primer byte, por lo que solo debe usarse en los casos en que la comparación está contra los caracteres ASCII de 7 bits simples.
Todavía hay dos métodos que no se mencionan para tratar tales casos:
Índice parcial (o dividido - si se crea para el rango completo manualmente) Índice, 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%'
Partición de la tabla en sí (usando el primer carácter como clave de partición): vale la pena considerar especialmente esta técnica 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 en una tabla están ordenados, uno puede beneficiarse de usar Índice de brin (sobre el primer personaje).
Probablemente más rápido hacer una comparación de personajes individuales:
SUBSTR(s.name,1,1)='B' OR SUBSTR(s.name,1,1)='D'