Tengo una tabla que contiene datos que se extraen de documentos de texto. Los datos se almacenan en una columna llamada "CONTENT"
para la cual he creado este índice usando GIN:
CREATE INDEX "File_contentIndex"
ON "File"
USING gin
(setweight(to_tsvector('english'::regconfig
, COALESCE("CONTENT", ''::character varying)::text), 'C'::"char"));
Utilizo la siguiente consulta para realizar una búsqueda de texto completo en la tabla:
SELECT "ITEMID",
ts_rank(setweight(to_tsvector('english', coalesce("CONTENT",'')), 'C') ,
plainto_tsquery('english', 'searchTerm')) AS "RANK"
FROM "File"
WHERE setweight(to_tsvector('english', coalesce("CONTENT",'')), 'C')
@@ plainto_tsquery('english', 'searchTerm')
ORDER BY "RANK" DESC
LIMIT 5;
La tabla Archivo contiene 250 000 filas y cada "CONTENT"
entrada consta de una palabra aleatoria y una cadena de texto que es igual para todas las filas.
Ahora, cuando busco una palabra aleatoria (1 hit en toda la tabla) la consulta se ejecuta muy rápido (<100 ms). Sin embargo, cuando busco una palabra que está presente en todas las filas, la consulta se ejecuta extremadamente lenta (10 minutos o más).
EXPLAIN ANALYZE
muestra que para la búsqueda de 1 acierto se realiza un Análisis de índice de mapa de bits seguido de un Análisis de montón de mapa de bits . Para la búsqueda lenta, se realiza un Seq Scan , que es lo que lleva tanto tiempo.
Por supuesto, no es realista tener los mismos datos en todas las filas. Pero dado que no puedo controlar los documentos de texto que cargan los usuarios, ni las búsquedas que realizan, es posible que surja un escenario similar (buscar términos con una ocurrencia muy alta en DB). ¿Cómo puedo aumentar el rendimiento de mi consulta de búsqueda para tal escenario?
Ejecutar PostgreSQL 9.3.4
Consultar planes de EXPLAIN ANALYZE
:
Búsqueda rápida (1 acierto en DB)
"Limit (cost=2802.89..2802.90 rows=5 width=26) (actual time=0.037..0.037 rows=1 loops=1)"
" -> Sort (cost=2802.89..2806.15 rows=1305 width=26) (actual time=0.037..0.037 rows=1 loops=1)"
" Sort Key: (ts_rank(setweight(to_tsvector('english'::regconfig, (COALESCE("CONTENT", ''::character varying))::text), 'C'::"char"), '''wfecg'''::tsquery))"
" Sort Method: quicksort Memory: 25kB"
" -> Bitmap Heap Scan on "File" (cost=38.12..2781.21 rows=1305 width=26) (actual time=0.030..0.031 rows=1 loops=1)"
" Recheck Cond: (setweight(to_tsvector('english'::regconfig, (COALESCE("CONTENT", ''::character varying))::text), 'C'::"char") @@ '''wfecg'''::tsquery)"
" -> Bitmap Index Scan on "File_contentIndex" (cost=0.00..37.79 rows=1305 width=0) (actual time=0.012..0.012 rows=1 loops=1)"
" Index Cond: (setweight(to_tsvector('english'::regconfig, (COALESCE("CONTENT", ''::character varying))::text), 'C'::"char") @@ '''wfecg'''::tsquery)"
"Total runtime: 0.069 ms"
Búsqueda lenta (250k hits en DB)
"Limit (cost=14876.82..14876.84 rows=5 width=26) (actual time=519667.404..519667.405 rows=5 loops=1)"
" -> Sort (cost=14876.82..15529.37 rows=261017 width=26) (actual time=519667.402..519667.402 rows=5 loops=1)"
" Sort Key: (ts_rank(setweight(to_tsvector('english'::regconfig, (COALESCE("CONTENT", ''::character varying))::text), 'C'::"char"), '''cyberspace'''::tsquery))"
" Sort Method: top-N heapsort Memory: 25kB"
" -> Seq Scan on "File" (cost=0.00..10541.43 rows=261017 width=26) (actual time=2.097..519465.953 rows=261011 loops=1)"
" Filter: (setweight(to_tsvector('english'::regconfig, (COALESCE("CONTENT", ''::character varying))::text), 'C'::"char") @@ '''cyberspace'''::tsquery)"
" Rows Removed by Filter: 6"
"Total runtime: 519667.429 ms"
ORDER BY "RANK" DESC
. Investigaríapg_trgm
con el índice GiST y los operadores de similitud / distancia como alternativa. Considere: dba.stackexchange.com/questions/56224/… . Incluso podría producir "mejores" resultados (además de ser más rápido).explain (analyze, buffers)
, preferiblemente con track_io_timing establecido enON
? No hay forma de que se tarde 520 segundos en explorar esa tabla, a menos que la tenga almacenada en un RAID de disquetes. Algo es definitivamente patológico allí. Además, ¿cuál es su configuraciónrandom_page_cost
y los otros parámetros de costo?Respuestas:
Caso de uso cuestionable
Una cadena de texto que es igual para todas las filas es solo carga muerta. Elimínelo y concatenelo en una vista si necesita mostrarlo.
Obviamente, eres consciente de eso:
Actualiza tu versión de Postgres
Mientras todavía esté en Postgres 9.3, al menos debería actualizar a la última versión de punto (actualmente 9.3.9). La recomendación oficial del proyecto:
Mejor aún, actualice a 9.4, que ha recibido importantes mejoras para los índices GIN .
Problema principal 1: estimaciones de costos
El costo de algunas funciones de búsqueda de texto se ha subestimado seriamente hasta la versión 9.4 incluida. Ese costo es elevado por el factor 100 en la próxima versión 9.5 como @jjanes describe en su respuesta reciente:
Aquí está el hilo respectivo donde se discutió esto y el mensaje de confirmación de Tom Lane.
Como puede ver en el mensaje de confirmación, se
to_tsvector()
encuentra entre esas funciones. Puede aplicar el cambio inmediatamente (como superusuario):lo que debería hacer que sea mucho más probable que se use su índice funcional.
Problema principal 2: KNN
El problema central es que Postgres tiene que calcular un rango con
ts_rank()
260k filas (rows=261011
) antes de que pueda ordenar y elegir los 5 mejores. Esto será costoso , incluso después de que haya solucionado otros problemas como se discutió. Es un problema de K-vecino más cercano (KNN) por naturaleza y hay soluciones para casos relacionados. Pero no puedo pensar en una solución general para su caso, ya que el cálculo del rango en sí depende de la entrada del usuario. Intentaría eliminar la mayor parte de las partidas de bajo rango temprano para que el cálculo completo solo tenga que hacerse para unos pocos buenos candidatos.Una forma en la que puedo pensar es combinar su búsqueda de texto completo con la búsqueda de similitud de trigrama , que ofrece una implementación funcional para el problema KNN. De esta manera, puede preseleccionar las "mejores" coincidencias con el
LIKE
predicado como candidatos (en una subconsulta con,LIMIT 50
por ejemplo) y luego seleccionar las 5 filas de mayor clasificación según su cálculo de clasificación en la consulta principal.O aplique ambos predicados en la misma consulta y elija las coincidencias más cercanas de acuerdo con la similitud de trigrama (que produciría resultados diferentes) como en esta respuesta relacionada:
Investigué un poco más y no eres el primero en encontrarte con este problema. Publicaciones relacionadas en pgsql-general:
Se está trabajando para implementar eventualmente un
tsvector <-> tsquery
operador.Oleg Bartunov y Alexander Korotkov incluso presentaron un prototipo funcional (utilizando
><
como operador en lugar de en<->
aquel entonces) pero es muy complejo de integrar en Postgres, toda la infraestructura para los índices de GIN tiene que ser modificada (la mayoría de los cuales ya está hecha).Problema principal 3: pesos e índice
E identifiqué un factor más que aumenta la lentitud de la consulta. Por documentación:
El énfasis en negrita es mío. Tan pronto como interviene el peso, cada fila debe recuperarse del montón (no solo un control de visibilidad barato) y los valores largos deben eliminarse, lo que aumenta el costo. Pero parece haber una solución para eso:
Definición de índice
Mirando su índice nuevamente, no parece tener sentido para comenzar. Asigne un peso a una sola columna, lo que no tiene sentido siempre que no concatene otras columnas con un peso diferente .
COALESCE()
tampoco tiene sentido siempre que no concatene más columnas.Simplifica tu índice:
Y tu consulta:
Todavía es costoso para un término de búsqueda que coincide con cada fila, pero probablemente mucho menos.
Aparte
Todos estos problemas combinados, el costo loco de 520 segundos para su segunda consulta está comenzando a tener sentido. Pero aún puede haber más problemas. ¿Configuraste tu servidor?
Se aplican todos los consejos habituales para la optimización del rendimiento.
Te hace la vida más fácil si no trabajas con identificadores de mayúsculas y minúsculas CaMeL:
fuente
USING gin (to_tsvector('english', "CONTENT")
Tuve un problema similar Me ocupé de ello precalculando el ts_rank de cada término de consulta de texto popular en un campo: tupla de tabla y almacenándolo en una tabla de búsqueda. Esto me ahorró mucho tiempo (factor de 40X) durante la búsqueda de palabras populares en el corpus pesado de texto.
Consulta: busque esta tabla y obtenga los identificadores de documentos ordenados por su rango respectivo. si no está allí, hágalo a la antigua usanza.
fuente