查找最长前缀的算法


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. 从调用表中获取一个数字,在循环中从length(number)到1 => $ prefix做子字符串

  2. 进行查询:从前缀中选择count(*),例如“ $ prefix”之类的代码

  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”。谢谢。
伊万

2
前缀必须在开头,对吗?
dezso

是。从第二名。8 $ prefix $ numbers
Korjavin Ivan

您的表格的基数是多少?10万个数字?多少个前缀?
Erwin Brandstetter,

Answers:


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是SQL标准的Postgres扩展DISTINCT在SO的相关答案中找到有关所用查询技术的详细说明。
ORDER BY p.code DESC选择最长的匹配项,因为'1234'排序后'123'(按升序排列)。

简单的SQL Fiddle

如果没有索引,查询将运行长的时间(等不及要看它是否完成)。为了快速完成,您需要索引支持。您提到的由附加模块提供的Trigram索引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 ms(trigram GiST)
  • 总运行时间:912.329毫秒三字 GIN)

快得多了

尝试失败 text_pattern_ops

一旦我们忽略了分散注意力的第一个噪声字符,它就会归结为基本的左锚定模式匹配。因此,我尝试了一个带有操作符类的text_pattern_ops B树索引功能 (假设列类型为text)。

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.816毫秒(trgm_gin_idx)
  • 总运行时间:0.147毫秒(text_pattern_idx)

但是,查询计划者将不考虑将该索引用于联接两个表。我以前见过这个限制。我对此没有有意义的解释。

部分/功能B树索引

另一种选择是对具有部分索引的部分字符串使用相等性检查。这可能是在用过的JOIN

由于通常通常只有数量有限的different lengthsfor前缀,因此我们可以构建一种类似于此处提供的带有部分索引的解决方案。

说,我们的前缀范围是15个字符。创建多个部分功能索引,每个不同的前缀长度都使用一个:

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毫秒

但是,尽管此查询还不错-它的性能与带有Trigram 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毫秒(!!!)

具有动态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.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
哇!这是相当的编辑。我希望我能再次投票。
swasheck

3
从过去的两年中,我从您的惊人答案中学到了更多。我的循环解决方案需要几个小时才能达到17-30毫秒?那是魔术。
伊万

1
@KorjavinIvan:嗯,正如所证明的那样,我测试了减少的2k前缀/ 17k数字设置。但这应该可以很好地扩展,并且我的测试机器是一台小型服务器。因此,您在现实生活中应该保持一秒钟的时间。
Erwin Brandstetter,

1
不错的答案...您知道dimitri的前缀扩展名吗?您可以将其包括在测试用例比较中吗?
MatheusOl 2013年

0

字符串S是字符串T的前缀,如果T在S和SZ之间,则Z在字典上比任何其他字符串都大(例如99999999,数字9足以超过数据集中可能的最长电话号码,有时0xFF可以使用)。

对于任何给定的T,最长的公共前缀在字典上也是最大的,因此一个简单的group by和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'实例化为具有自己索引的codes表中的列,等等。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.