Алгоритм знаходження найдовшого префікса


11

У мене дві таблиці.

Перший - це таблиця з префіксами

code name price
343  ek1   10
3435 nt     4
3432 ek2    2

По-друге, це записи записів з номерами телефонів

number        time
834353212     10
834321242     20
834312345     30

Мені потрібно написати скрипт, який знайде найдовший префікс з префіксів для кожного запису, і записати всі ці дані до третьої таблиці, як це:

 number        code   ....
 834353212     3435
 834321242     3432
 834312345     343

Для номера 834353212 ми повинні обрізати '8', а потім знайти найдовший код із таблиці префіксів, його 3435.
Ми завжди повинні опускати спочатку '8', а префікс повинен бути на початку.

Я вирішив це завдання давно, дуже погано. Це був жахливий сценарій Perl, який робить багато запитів для кожного запису. Цей сценарій:

  1. Візьміть число з таблиці викликів, виконайте підрядку від довжини (числа) до 1 => $ префікса в циклі

  2. Виконайте запит: виберіть count (*) з префіксів, де код на зразок '$ prefix'

  3. Якщо підрахунок> 0, то візьміть перші префікси та запишіть у таблицю

Перша проблема - кількість запитів - це call_records * length(number). Друга проблема - LIKEвирази. Боюся, що це повільно.

Я спробував вирішити другу проблему:

CREATE EXTENSION pg_trgm;
CREATE INDEX prefix_idx ON prefix USING gist (code gist_trgm_ops);

Це прискорює кожен запит, але загалом не вирішує проблему.

Зараз у мене є префікси 20k і 170k , і моє старе рішення погано. Схоже, мені потрібно нове рішення без петель.

Лише один запит для кожного запису дзвінків або щось подібне.


2
Я не дуже впевнений, чи є codeв першій таблиці те саме, що і префікс пізніше. Не могли б ви уточнити це? І деякі виправлення прикладних даних та бажаний вихід (щоб було легше наслідувати вашу проблему) також будуть вітатися.
dezso

Так. Ти правий. Я забув написати про «8». Дякую.
Корявін Іван

2
префікс повинен бути на початку, правда?
dezso

Так. З другого місця. 8 $ префікс $ номерів
Корявін Іван

Яка простота ваших столів? 100k номери? Скільки префіксів?
Ервін Брандстеттер

Відповіді:


21

Я припускаю тип даних 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 і коротший для довших префіксів.


Я все ще перевіряю, але виглядає чудово! Ваша ідея "зворотна" як оператор - геніальна. Чому я був такий дурний; (
Корявін Іван

5
ого! це цілком редагування. Я б хотів, щоб я міг повторити свою пропозицію
сварка

3
Я дізнаюся з вашої дивовижної відповіді більше, ніж за останні два роки. 17-30 мс проти декількох годин у моєму циклі? Це магія.
Корявін Іван

1
@KorjavinIvan: Ну, як це було документовано, я перевірив зменшене налаштування 2-х префіксів / 17-к. Але це має масштабуватися досить добре, і моя тестова машина була крихітним сервером. Тож вам слід затриматися трохи більше секунди зі своїм реальним життєвим випадком.
Ервін Брандстеттер

1
Приємна відповідь ... Чи знаєте ви розширення префікса dimitri ? Чи можете ви включити це до порівняння ваших тестових випадків?
MatheusOl

0

Рядок S - це префікс рядка T iff T знаходиться між S та SZ, де Z лексикографічно більший, ніж будь-який інший рядок (наприклад, 99999999 з достатньою кількістю 9-х, щоб перевищити найдовший можливий номер телефону в наборі даних, або іноді 0xFF буде працювати).

Найдовший загальний префікс для будь-якого даного Т також є лексикографічно максимальним, тому проста група по і max знайде його.

select n.number, max(p.code) 
from prefixes p
join numbers n 
on substring(n.number, 2, 255) between p.code and p.code || '99999999'
group by n.number

Якщо це повільно, це, ймовірно, пов'язано з обчисленими виразами, тому ви також можете спробувати матеріалізувати p.code || '999999' у стовпчик таблиці кодів із власним індексом тощо.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.