Настроить
Я основываюсь на настройке @ Jack, чтобы людям было легче следить и сравнивать. Протестировано с 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
С этого момента я выбираю другой маршрут:
ANALYZE lexikon;
Вспомогательный стол
Это решение не добавляет столбцы в исходную таблицу, ему просто нужна крошечная вспомогательная таблица. Я поместил его в схему public, используйте любую схему по вашему выбору.
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;
Таблица выглядит так:
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
Поскольку этот столбец condбудет использоваться в динамическом SQL ниже, вам необходимо обеспечить безопасность этой таблицы . Всегда проверяйте схему таблицы, если вы не можете быть уверены в соответствующем токе search_path, и отзывайте права на запись из public(и любой другой ненадежной роли):
REVOKE ALL ON public.lex_freq FROM public;
GRANT SELECT ON public.lex_freq TO public;
Стол lex_freqслужит трем целям:
- Создайте необходимые частичные индексы автоматически.
- Укажите шаги для итеративной функции.
- Мета информация для тюнинга.
Индексы
Этот DOоператор создает все необходимые индексы:
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
$$
Все эти частичные индексы вместе охватывают таблицу один раз. Они имеют примерно одинаковый размер с одним основным индексом на всю таблицу:
SELECT pg_size_pretty(pg_relation_size('lexikon')); -- 50 MB
SELECT pg_size_pretty(pg_total_relation_size('lexikon')); -- 71 MB
Пока только 21 МБ индексов для таблицы 50 МБ.
Я создаю большинство частичных индексов (lset, frequency DESC). Второй столбец помогает только в особых случаях. Но поскольку оба задействованных столбца имеют тип integer, из-за особенностей выравнивания данных в сочетании с MAXALIGN в PostgreSQL, второй столбец не делает индекс больше. Это маленькая победа едва ли любой ценой.
Нет смысла делать это для частичных индексов, которые охватывают только одну частоту. Это только на (lset). Созданные индексы выглядят так:
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;
функция
Функция несколько похожа по стилю на решение @ 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;
Ключевые отличия:
динамический SQL с RETURN QUERY EXECUTE.
Поскольку мы переходим по шагам, другой план запроса может быть выгодным. План запроса для статического SQL генерируется один раз, а затем используется повторно, что может сэкономить некоторые накладные расходы. Но в этом случае запрос прост и значения очень разные. Динамический SQL будет большой победой.
ДинамическийLIMIT для каждого шага запроса.
Это помогает несколькими способами: во-первых, строки выбираются только по мере необходимости. В сочетании с динамическим SQL это также может генерировать различные планы запросов для начала. Второе: нет необходимости в дополнительном LIMITвызове функции для обрезки излишков.
эталонный тест
Настроить
Я выбрал четыре примера и провел три разных теста с каждым. Я взял лучшее из пяти, чтобы сравнить с теплым кешем:
Необработанный SQL-запрос формы:
SELECT *
FROM lexikon
WHERE lset >= 20000
AND lset <= 30000
ORDER BY frequency DESC
LIMIT 5;
То же самое после создания этого индекса
CREATE INDEX ON lexikon(lset);
Нужно примерно столько же места, сколько все мои частичные индексы вместе:
SELECT pg_size_pretty(pg_total_relation_size('lexikon')) -- 93 MB
Функция
SELECT * FROM f_search(20000, 30000, 5);
Полученные результаты
SELECT * FROM f_search(20000, 30000, 5);
1: общее время выполнения: 315,458 мс
2: общее время выполнения: 36,458 мс
3: общее время выполнения: 0,330 мс
SELECT * FROM f_search(60000, 65000, 100);
1: общее время выполнения: 294,819 мс
2: общее время выполнения: 18,915 мс
3: общее время выполнения: 1,414 мс
SELECT * FROM f_search(10000, 70000, 100);
1: общее время выполнения: 426,831 мс
2: общее время выполнения: 217,874 мс
3: общее время выполнения: 1,611 мс
SELECT * FROM f_search(1, 1000000, 5);
1: общее время выполнения: 2458,205 мс
2: общее время выполнения: 2458,205 мс - для больших диапазонов lset сканирование seq выполняется быстрее индекса.
3: общее время выполнения: 0,266 мс
Вывод
Как и ожидалось, выгода от функции возрастает с увеличением диапазона lsetи уменьшением LIMIT.
С очень маленькими диапазонамиlset необработанный запрос в сочетании с индексом на самом деле быстрее . Вы захотите проверить и, возможно, выполнить ответвление: необработанный запрос для небольших диапазонов lset, иначе вызов функции. Вы могли бы даже встроить это в функцию для «лучшего из двух миров» - вот что я бы сделал.
В зависимости от вашего распределения данных и типичных запросов, дополнительные шаги lex_freqмогут помочь производительности. Тест, чтобы найти сладкое место. С инструментами, представленными здесь, это должно быть легко проверить.