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?

¿Fue útil?

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 de LIKE 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:

  1. Í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%'
    
  2. 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'
Licenciado bajo: CC-BY-SA con atribución
No afiliado a dba.stackexchange
scroll top