Я припускаю тип даних text
для відповідних стовпців.
CREATE TABLE prefix (code text, name text, price int);
CREATE TABLE num (number text, time int);
«Просте» рішення
SELECT DISTINCT ON (1)
n.number, p.code
FROM num n
JOIN prefix p ON right(n.number, -1) LIKE (p.code || '%')
ORDER BY n.number, p.code DESC;
Основні елементи:
DISTINCT ON
є розширенням Postgres стандарту SQL DISTINCT
. Знайдіть детальне пояснення використаної методики запитів у цій відповідній відповіді на SO .
ORDER BY p.code DESC
вибирає найдовший збіг, тому що '1234'
сортує після '123'
(у порядку зростання).
Простий скрипт SQL .
Без індексу запит працюватиме дуже довго (не дочекався його завершення). Щоб зробити це швидко, вам потрібна підтримка індексу. Індекси триграм, які ви згадали, надані додатковим модулем, pg_trgm
є хорошим кандидатом. Ви повинні вибрати між GIN та індексом GiST. Перший символ чисел - це просто шум і його можна виключити з індексу, що робить його додатково функціональним індексом.
У моїх тестах функціональний триграмовий індекс GIN виграв гонку над триграмовим індексом GiST (як очікувалося):
CREATE INDEX num_trgm_gin_idx ON num USING gin (right(number, -1) gin_trgm_ops);
Розширений dbfiddle тут .
Усі результати тестування є місцевою тест-установкою Postgres 9.1 зі зменшеними налаштуваннями: 17k номери та 2k коди:
- Загальна тривалість виконання: 1719,552 мс (триграма GiST)
- Загальна тривалість виконання: 912,329 мс (триграма GIN)
Набагато швидше поки
Помилка спроби с text_pattern_ops
Після того, як ми ігноруємо відволікаючий перший шум-символ, він зводиться до базового лівого прив’язаного малюнка. Тому я спробував функціональний індекс B-дерева з класом оператораtext_pattern_ops
(якщо вважати тип стовпця text
).
CREATE INDEX num_text_pattern_idx ON num(right(number, -1) text_pattern_ops);
Це чудово працює для прямих запитів з одним пошуковим терміном і робить показник триграми погано порівняно:
SELECT * FROM num WHERE right(number, -1) LIKE '2345%'
- Загальна тривалість виконання: 3.816 мс (trgm_gin_idx)
- Загальна тривалість виконання: 0,147 мс (text_pattern_idx)
Однак планувальник запитів не враховує цей індекс для об'єднання двох таблиць. Я раніше бачив це обмеження. Я поки не маю значущого пояснення цьому.
Часткові / функціональні індекси B-дерева
Альтернативою є використання перевірок рівності часткових рядків з частковими індексами. Це можна використовувати в JOIN
.
Оскільки у нас є лише обмежена кількість different lengths
префіксів, ми можемо створити рішення, подібне до представленого тут, з частковими індексами.
Скажімо, у нас є префікси від 1 до 5 символів. Створіть декілька часткових функціональних індексів, по одному на кожну окрему довжину префікса:
CREATE INDEX prefix_code_idx5 ON prefix(code) WHERE length(code) = 5;
CREATE INDEX prefix_code_idx4 ON prefix(code) WHERE length(code) = 4;
CREATE INDEX prefix_code_idx3 ON prefix(code) WHERE length(code) = 3;
CREATE INDEX prefix_code_idx2 ON prefix(code) WHERE length(code) = 2;
CREATE INDEX prefix_code_idx1 ON prefix(code) WHERE length(code) = 1;
Оскільки це часткові індекси, всі вони разом ледве перевищують один повний індекс.
Додайте відповідні індекси для чисел (враховуючи провідний символ шуму):
CREATE INDEX num_number_idx5 ON num(substring(number, 2, 5)) WHERE length(number) >= 6;
CREATE INDEX num_number_idx4 ON num(substring(number, 2, 4)) WHERE length(number) >= 5;
CREATE INDEX num_number_idx3 ON num(substring(number, 2, 3)) WHERE length(number) >= 4;
CREATE INDEX num_number_idx2 ON num(substring(number, 2, 2)) WHERE length(number) >= 3;
CREATE INDEX num_number_idx1 ON num(substring(number, 2, 1)) WHERE length(number) >= 2;
Хоча ці індекси містять лише підрядку і є частковими, кожен охоплює більшу частину або всю таблицю. Таким чином, вони значно більше, ніж один загальний індекс - за винятком довгих чисел. І вони накладають більше роботи для операцій з написанням. Ось і коштує дивовижна швидкість.
Якщо ця вартість занадто висока для вас (важлива продуктивність запису / занадто багато операцій запису / проблема дискового простору), ви можете пропустити ці індекси. Решта все-таки швидше, якщо не зовсім так швидко, як могло б бути ...
Якщо цифри ніколи не бувають меншими за n
символи, відмініть зайві WHERE
пропозиції від деяких або всіх, а також випустіть відповідний WHERE
пункт із усіх наступних запитів.
Рекурсивний CTE
З усіма налаштуваннями до цього часу я сподівався на дуже елегантне рішення з рекурсивним CTE :
WITH RECURSIVE cte AS (
SELECT n.number, p.code, 4 AS len
FROM num n
LEFT JOIN prefix p
ON substring(number, 2, 5) = p.code
AND length(n.number) >= 6 -- incl. noise character
AND length(p.code) = 5
UNION ALL
SELECT c.number, p.code, len - 1
FROM cte c
LEFT JOIN prefix p
ON substring(number, 2, c.len) = p.code
AND length(c.number) >= c.len+1 -- incl. noise character
AND length(p.code) = c.len
WHERE c.len > 0
AND c.code IS NULL
)
SELECT number, code
FROM cte
WHERE code IS NOT NULL;
- Загальна тривалість виконання: 1045.115 мс
Однак, хоч цей запит непоганий - він працює настільки ж добре, як і проста версія з триграмовим індексом GIN - він не забезпечує те, що я прагнув. Рекурсивний термін планується лише один раз, тому він не може використовувати найкращі індекси. Лише нерекурсивний термін може.
СОЮЗ ВСІХ
Оскільки ми маємо справу з невеликою кількістю рекурсій, ми можемо просто прописати їх повторно. Це дозволяє оптимізувати плани для кожного з них. (Ми втрачаємо рекурсивне виключення вже успішних чисел, хоча так є. Ще деякі можливості для поліпшення, особливо для більш широкого діапазону довжин префіксів)):
SELECT DISTINCT ON (1) number, code
FROM (
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(number, 2, 5) = p.code
AND length(n.number) >= 6 -- incl. noise character
AND length(p.code) = 5
UNION ALL
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(number, 2, 4) = p.code
AND length(n.number) >= 5
AND length(p.code) = 4
UNION ALL
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(number, 2, 3) = p.code
AND length(n.number) >= 4
AND length(p.code) = 3
UNION ALL
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(number, 2, 2) = p.code
AND length(n.number) >= 3
AND length(p.code) = 2
UNION ALL
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(number, 2, 1) = p.code
AND length(n.number) >= 2
AND length(p.code) = 1
) x
ORDER BY number, code DESC;
- Загальна тривалість виконання: 57.578 мс (!!)
Прорив, нарешті!
Функція SQL
Якщо перетворити цю функцію на функцію SQL, ви видаляєте накладні витрати на повторне використання запитів:
CREATE OR REPLACE FUNCTION f_longest_prefix()
RETURNS TABLE (number text, code text) LANGUAGE sql AS
$func$
SELECT DISTINCT ON (1) number, code
FROM (
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(number, 2, 5) = p.code
AND length(n.number) >= 6 -- incl. noise character
AND length(p.code) = 5
UNION ALL
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(number, 2, 4) = p.code
AND length(n.number) >= 5
AND length(p.code) = 4
UNION ALL
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(number, 2, 3) = p.code
AND length(n.number) >= 4
AND length(p.code) = 3
UNION ALL
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(number, 2, 2) = p.code
AND length(n.number) >= 3
AND length(p.code) = 2
UNION ALL
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(number, 2, 1) = p.code
AND length(n.number) >= 2
AND length(p.code) = 1
) x
ORDER BY number, code DESC
$func$;
Виклик:
SELECT * FROM f_longest_prefix_sql();
- Загальна тривалість виконання: 17.138 мс (!!!)
PL / pgSQL функція з динамічним SQL
Ця функція plpgsql дуже нагадує рекурсивний CTE, наведений вище, але динамічний SQL EXECUTE
змушує запит перепланувати для кожної ітерації. Тепер він використовує всі спеціалізовані індекси.
Крім того, це працює для будь-якого діапазону довжин префікса. Функція приймає два параметри для діапазону, але я підготував її зі DEFAULT
значеннями, тому він працює і без явних параметрів:
CREATE OR REPLACE FUNCTION f_longest_prefix2(_min int = 1, _max int = 5)
RETURNS TABLE (number text, code text) LANGUAGE plpgsql AS
$func$
BEGIN
FOR i IN REVERSE _max .. _min LOOP -- longer matches first
RETURN QUERY EXECUTE '
SELECT n.number, p.code
FROM num n
JOIN prefix p
ON substring(n.number, 2, $1) = p.code
AND length(n.number) >= $1+1 -- incl. noise character
AND length(p.code) = $1'
USING i;
END LOOP;
END
$func$;
Заключний крок не можна легко перетворити на одну функцію.
Або просто назвіть це так:
SELECT DISTINCT ON (1)
number, code
FROM f_longest_prefix_prefix2() x
ORDER BY number, code DESC;
- Загальна тривалість виконання: 27.413 мс
Або використовуйте іншу функцію SQL в якості обгортки:
CREATE OR REPLACE FUNCTION f_longest_prefix3(_min int = 1, _max int = 5)
RETURNS TABLE (number text, code text) LANGUAGE sql AS
$func$
SELECT DISTINCT ON (1)
number, code
FROM f_longest_prefix_prefix2($1, $2) x
ORDER BY number, code DESC
$func$;
Виклик:
SELECT * FROM f_longest_prefix3();
- Загальний час виконання: 37,622 мс
Трохи повільніше через необхідне накладне планування. Але більш універсальний, ніж SQL і коротший для довших префіксів.
code
в першій таблиці те саме, що і префікс пізніше. Не могли б ви уточнити це? І деякі виправлення прикладних даних та бажаний вихід (щоб було легше наслідувати вашу проблему) також будуть вітатися.