我假设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 lengths
for前缀,因此我们可以构建一种类似于此处提供的带有部分索引的解决方案。
说,我们的前缀范围是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;
但是,尽管此查询还不错-它的性能与带有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;
终于突破了!
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();
具有动态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;
或使用另一个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();
由于需要计划开销,因此速度稍慢。但是比SQL更通用,对于更长的前缀则更短。
code
在第一个表中是否与后面的前缀相同。你能澄清一下吗?并且还将欢迎对示例数据和所需的输出进行一些修复(以便更轻松地解决问题)。