가장 긴 접두사를 찾기위한 알고리즘


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'을 삭제하고 접두사는 시작 부분에 있어야합니다.

나는 오래 전에이 작업을 매우 나쁜 방법으로 해결했습니다. 각 레코드마다 많은 쿼리를 수행하는 끔찍한 펄 스크립트였습니다. 이 스크립트는 :

  1. 호출 테이블에서 숫자를 가져 와서 루프에서 length (number)에서 1 => $ prefix까지 하위 문자열을 수행하십시오.

  2. 쿼리를 수행하십시오 : '$ prefix'와 같은 코드가있는 접두사에서 count (*)를 선택하십시오.

  3. count> 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'에 관한 글을 잊었다. 감사합니다.
Korjavin Ivan

2
접두사가 시작에 있어야합니까?
dezso

예. 두 번째부터. 8 $ 접두사 $ 숫자
Korjavin Ivan

테이블의 카디널리티는 무엇입니까? 100k 숫자? 접두사는 몇 개입니까?
Erwin Brandstetter

답변:


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 ONSQL 표준의 Postgres 확장입니다 DISTINCT. SO에 대한관련 답변 에서 사용 된 쿼리 기술에 대한 자세한 설명을 찾으십시오 .
ORDER BY p.code DESC가장 긴 일치를 선택합니다 때문에 '1234'종류 후'123' 오름차순으로 .

간단한 SQL 바이올린 .

인덱스가 없으면 쿼리가 매우 오랫동안 실행 됩니다 (완료되기를 기다리지 않았습니다). 이를 빠르게하려면 인덱스 지원이 필요합니다. 추가 모듈에서 제공 한 언급 한 트라이 그램 인덱스가 적합합니다 pg_trgm. GIN과 GiST 지수 중에서 선택해야합니다. 숫자의 첫 번째 문자는 노이즈 일 뿐이며 인덱스에서 제외되어 기능 인덱스가됩니다.
필자의 테스트에서 기능적인 트라이 그램 GIN 지수 는 예상대로 Trigram GiST 지수보다 경쟁에서 승리했습니다.

CREATE INDEX num_trgm_gin_idx ON num USING gin (right(number, -1) gin_trgm_ops);

고급 dbfiddle 여기에 .

모든 테스트 결과는 17k 숫자 및 2k 코드 설정이 축소 된 로컬 Postgres 9.1 테스트 설치 결과입니다.

  • 총 런타임 : 1719.552ms (trigram GiST)
  • 총 런타임 : 912.329ms (트리 그램 GIN)

훨씬 더 빠른

시도 실패 text_pattern_ops

주의가 산만해진 첫 번째 노이즈 특성을 무시하면 기본 왼쪽 고정 패턴 일치가 발생합니다. 따라서 연산자 클래스text_pattern_ops (열 유형 가정 text) 로 기능적인 B- 트리 인덱스를 시도했습니다 .

CREATE INDEX num_text_pattern_idx ON num(right(number, -1) text_pattern_ops);

이것은 하나의 검색어직접 쿼리 를 수행하는 데 매우 효과적 이며 trigram 인덱스를 비교하면 나쁘게 보입니다.

SELECT * FROM num WHERE right(number, -1) LIKE '2345%'
  • 총 런타임 : 3.816ms (trgm_gin_idx)
  • 총 런타임 : 0.147 MS (text_pattern_idx)

그러나 쿼리 플래너는 두 테이블을 조인하는 데이 인덱스를 고려하지 않습니다. 나는 전에이 제한을 보았습니다. 아직 이것에 대한 의미있는 설명이 없습니다.

부분 / 기능적 B- 트리 인덱스

대안으로 부분 인덱스가있는 부분 문자열에서 동등 검사를 사용합니다. 이 A의 사용 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.115ms

그러나이 쿼리는 나쁘지 않습니다-그것은 trigram GIN 인덱스가있는 간단한 버전만큼 성능이 뛰어납니다. 그러나 내가 목표로 한 것을 제공하지는 않습니다. 재귀 용어는 한 번만 계획되므로 최상의 인덱스를 사용할 수 없습니다. 비 재귀 항만 가능합니다.

UNION ALL

우리는 적은 수의 재귀를 다루기 때문에 반복적으로 철자를 쓸 수 있습니다. 이를 통해 각 계획에 최적화 된 계획이 가능합니다. (그러나 우리는 이미 성공적인 숫자의 재귀 적 제외를 잃습니다. 따라서 여전히 더 넓은 범위의 접두사 길이에 대한 개선의 여지가 여전히 남아 있습니다)) :

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.578ms (!!)

마침내 획기적인!

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.138ms (!!!)

동적 SQL을 사용한 PL / pgSQL 함수

이 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.413ms

또는 다른 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.622ms

필요한 계획 오버 헤드로 인해 조금 느려집니다. 그러나 SQL보다 다재다능하고 접두어가 길수록 짧습니다.


나는 아직도 확인하고 있지만 훌륭해 보인다! 당신의 생각은 연산자처럼 "반전"합니다 – 훌륭합니다. 내가 왜 그렇게 멍청한 지; (
Korjavin Ivan

5
우와! 그것은 꽤 편집입니다. 다시 투표 할 수 있으면 좋겠다.
swasheck

3
지난 2 년 동안 당신의 놀라운 답변을 통해 배웁니다. 루프 솔루션에서 몇 시간 동안 17-30ms? 그거 마술이야
Korjavin Ivan

1
@ KorjavinIvan : 글쎄, 문서화 된 것처럼, 나는 2k 접두사 / 17k 숫자의 축소 된 설정으로 테스트했습니다. 그러나 이것은 꽤 잘 확장되어야하며 테스트 시스템은 작은 서버였습니다. 그래서 당신은 당신의 실제 사례와 함께 잠깐 동안 유지해야합니다.
Erwin Brandstetter

1
좋은 대답 ... dimitri의 접두사 확장명 을 알고 있습니까? 테스트 케이스 비교에 포함시킬 수 있습니까?
MatheusOl

0

문자열 S는 문자열의 접두사입니다. T는 S와 SZ 사이에 있으며, 여기서 Z는 다른 문자열보다 사전 식적으로 더 큽니다 (예 : 데이터 세트에서 가능한 가장 긴 전화 번호를 초과 할만큼 9가 충분한 99999999이거나 때로는 0xFF가 작동 함).

주어진 T에 대한 가장 긴 공통 접두어도 사 전적으로 최대이므로 간단한 그룹별로 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'를 자체 색인 등으로 코드 테이블의 열에 구체화 할 수도 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.