Preparar
Estoy construyendo sobre la configuración de @ Jack para que sea más fácil para las personas seguir y comparar. Probado con PostgreSQL 9.1.4 .
CREATE TABLE lexikon (
lex_id serial PRIMARY KEY
, word text
, frequency int NOT NULL -- we'd need to do more if NULL was allowed
, lset int
);
INSERT INTO lexikon(word, frequency, lset)
SELECT 'w' || g -- shorter with just 'w'
, (1000000 / row_number() OVER (ORDER BY random()))::int
, g
FROM generate_series(1,1000000) g
De aquí en adelante tomo una ruta diferente:
ANALYZE lexikon;
Mesa auxiliar
Esta solución no agrega columnas a la tabla original, solo necesita una pequeña tabla auxiliar. Lo puse en el esquema public
, use cualquier esquema de su elección.
CREATE TABLE public.lex_freq AS
WITH x AS (
SELECT DISTINCT ON (f.row_min)
f.row_min, c.row_ct, c.frequency
FROM (
SELECT frequency, sum(count(*)) OVER (ORDER BY frequency DESC) AS row_ct
FROM lexikon
GROUP BY 1
) c
JOIN ( -- list of steps in recursive search
VALUES (400),(1600),(6400),(25000),(100000),(200000),(400000),(600000),(800000)
) f(row_min) ON c.row_ct >= f.row_min -- match next greater number
ORDER BY f.row_min, c.row_ct, c.frequency DESC
)
, y AS (
SELECT DISTINCT ON (frequency)
row_min, row_ct, frequency AS freq_min
, lag(frequency) OVER (ORDER BY row_min) AS freq_max
FROM x
ORDER BY frequency, row_min
-- if one frequency spans multiple ranges, pick the lowest row_min
)
SELECT row_min, row_ct, freq_min
, CASE freq_min <= freq_max
WHEN TRUE THEN 'frequency >= ' || freq_min || ' AND frequency < ' || freq_max
WHEN FALSE THEN 'frequency = ' || freq_min
ELSE 'frequency >= ' || freq_min
END AS cond
FROM y
ORDER BY row_min;
La tabla se ve así:
row_min | row_ct | freq_min | cond
--------+---------+----------+-------------
400 | 400 | 2500 | frequency >= 2500
1600 | 1600 | 625 | frequency >= 625 AND frequency < 2500
6400 | 6410 | 156 | frequency >= 156 AND frequency < 625
25000 | 25000 | 40 | frequency >= 40 AND frequency < 156
100000 | 100000 | 10 | frequency >= 10 AND frequency < 40
200000 | 200000 | 5 | frequency >= 5 AND frequency < 10
400000 | 500000 | 2 | frequency >= 2 AND frequency < 5
600000 | 1000000 | 1 | frequency = 1
Como la columna cond
se va a usar en SQL dinámico más abajo, debe hacer que esta tabla sea segura . Siempre califique el esquema de la tabla si no puede estar seguro de una corriente apropiada search_path
y revoque los privilegios de escritura de public
(y cualquier otra función no confiable):
REVOKE ALL ON public.lex_freq FROM public;
GRANT SELECT ON public.lex_freq TO public;
La tabla lex_freq
tiene tres propósitos:
- Cree los índices parciales necesarios automáticamente.
- Proporcionar pasos para la función iterativa.
- Metainformación para afinar.
Índices
Esta DO
declaración crea todos los índices necesarios:
DO
$$
DECLARE
_cond text;
BEGIN
FOR _cond IN
SELECT cond FROM public.lex_freq
LOOP
IF _cond LIKE 'frequency =%' THEN
EXECUTE 'CREATE INDEX ON lexikon(lset) WHERE ' || _cond;
ELSE
EXECUTE 'CREATE INDEX ON lexikon(lset, frequency DESC) WHERE ' || _cond;
END IF;
END LOOP;
END
$$
Todos estos índices parciales juntos abarcan la tabla una vez. Son aproximadamente del mismo tamaño que un índice básico en toda la tabla:
SELECT pg_size_pretty(pg_relation_size('lexikon')); -- 50 MB
SELECT pg_size_pretty(pg_total_relation_size('lexikon')); -- 71 MB
Solo 21 MB de índices para una tabla de 50 MB hasta ahora.
Creo la mayoría de los índices parciales en (lset, frequency DESC)
. La segunda columna solo ayuda en casos especiales. Pero como ambas columnas involucradas son de tipo integer
, debido a los detalles de la alineación de datos en combinación con MAXALIGN en PostgreSQL, la segunda columna no hace que el índice sea más grande. Es una pequeña victoria por casi ningún costo.
No tiene sentido hacerlo para índices parciales que solo abarcan una sola frecuencia. Esos son sólo en (lset)
. Los índices creados se ven así:
CREATE INDEX ON lexikon(lset, frequency DESC) WHERE frequency >= 2500;
CREATE INDEX ON lexikon(lset, frequency DESC) WHERE frequency >= 625 AND frequency < 2500;
-- ...
CREATE INDEX ON lexikon(lset, frequency DESC) WHERE frequency >= 2 AND frequency < 5;
CREATE INDEX ON lexikon(lset) WHERE freqency = 1;
Función
La función es algo similar en estilo a la solución de @ Jack:
CREATE OR REPLACE FUNCTION f_search(_lset_min int, _lset_max int, _limit int)
RETURNS SETOF lexikon
$func$
DECLARE
_n int;
_rest int := _limit; -- init with _limit param
_cond text;
BEGIN
FOR _cond IN
SELECT l.cond FROM public.lex_freq l ORDER BY l.row_min
LOOP
-- RAISE NOTICE '_cond: %, _limit: %', _cond, _rest; -- for debugging
RETURN QUERY EXECUTE '
SELECT *
FROM public.lexikon
WHERE ' || _cond || '
AND lset >= $1
AND lset <= $2
ORDER BY frequency DESC
LIMIT $3'
USING _lset_min, _lset_max, _rest;
GET DIAGNOSTICS _n = ROW_COUNT;
_rest := _rest - _n;
EXIT WHEN _rest < 1;
END LOOP;
END
$func$ LANGUAGE plpgsql STABLE;
Diferencias clave
SQL dinámico con RETURN QUERY EXECUTE
.
A medida que avanzamos por los pasos, un plan de consulta diferente puede ser beneficiario. El plan de consulta para SQL estático se genera una vez y luego se reutiliza, lo que puede ahorrar algo de sobrecarga. Pero en este caso la consulta es simple y los valores son muy diferentes. El SQL dinámico será una gran victoria.
DinámicoLIMIT
para cada paso de consulta.
Esto ayuda de varias maneras: primero, las filas solo se obtienen según sea necesario. En combinación con SQL dinámico, esto también puede generar diferentes planes de consulta para empezar. Segundo: no se necesita una LIMIT
llamada adicional en la función para recortar el excedente.
Punto de referencia
Preparar
Elegí cuatro ejemplos y realicé tres pruebas diferentes con cada uno. Tomé el mejor de cinco para comparar con el caché cálido:
La consulta SQL sin formato del formulario:
SELECT *
FROM lexikon
WHERE lset >= 20000
AND lset <= 30000
ORDER BY frequency DESC
LIMIT 5;
Lo mismo después de crear este índice.
CREATE INDEX ON lexikon(lset);
Necesita aproximadamente el mismo espacio que todos mis índices parciales juntos:
SELECT pg_size_pretty(pg_total_relation_size('lexikon')) -- 93 MB
La función
SELECT * FROM f_search(20000, 30000, 5);
Resultados
SELECT * FROM f_search(20000, 30000, 5);
1: Tiempo de ejecución total: 315.458 ms
2: Tiempo de ejecución total: 36.458 ms
3: Tiempo de ejecución total: 0.330 ms
SELECT * FROM f_search(60000, 65000, 100);
1: Tiempo de ejecución total: 294.819 ms
2: Tiempo de ejecución total: 18.915 ms
3: Tiempo de ejecución total: 1.414 ms
SELECT * FROM f_search(10000, 70000, 100);
1: Tiempo de ejecución total: 426.831 ms
2: Tiempo de ejecución total: 217.874 ms
3: Tiempo de ejecución total: 1.611 ms
SELECT * FROM f_search(1, 1000000, 5);
1: Tiempo de ejecución total: 2458.205 ms
2: Tiempo de ejecución total: 2458.205 ms - para grandes rangos de lset, la exploración seq es más rápida que el índice.
3: Tiempo de ejecución total: 0.266 ms
Conclusión
Como se esperaba, el beneficio de la función crece con rangos más grandes lset
y más pequeños LIMIT
.
Con rangos muy pequeños delset
, la consulta sin procesar en combinación con el índice es en realidad más rápida . Querrá probar y quizás ramificar: consulta sin procesar para pequeños rangos de lset
, de lo contrario, llamar a la función. Incluso podría incorporar eso en la función para "lo mejor de ambos mundos", eso es lo que haría.
Dependiendo de su distribución de datos y consultas típicas, más pasos lex_freq
pueden ayudar al rendimiento. Prueba para encontrar el punto ideal. Con las herramientas presentadas aquí, debería ser fácil de probar.