Настроить
Я основываюсь на настройке @ 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
могут помочь производительности. Тест, чтобы найти сладкое место. С инструментами, представленными здесь, это должно быть легко проверить.