Налаштування
Я будую на установці @ Джека, щоб людям було легше наслідувати та порівняти. Тестовано з 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;
Функція
Функція дещо схожа на рішення @ рішення Джека:
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 сканування послідовності швидше, ніж індекс.
3: Загальний час виконання: 0,266 мс
Висновок
Як і очікувалося, вигода від функції зростає з більшими діапазонами lset
та меншими LIMIT
.
З дуже малим діапазономlset
, необроблений запит у поєднанні з індексом насправді швидше . Ви хочете перевірити і, можливо, відгалуження: необроблений запит для невеликих діапазонів lset
, виклик функції else Ви могли навіть просто вбудувати це у функцію для "кращого з обох світів" - ось що я би робив.
Залежно від вашого розповсюдження даних та типових запитів, ефективність lex_freq
може допомогти більше кроків . Тест, щоб знайти солодке місце. З інструментами, представленими тут, це слід легко перевірити.