SQL中两个字符串的公共子串

Tri*_*iTY 1 t-sql sql-server string

我需要在 SQL 中找到两个字符串的公共子字符串(不带空格)。

询问:

select *
from tbl as a, tbl as b
where a.str <> b.str
Run Code Online (Sandbox Code Playgroud)

样本数据:

str1      | str2      | max substring without spaces
----------+-----------+-----------------------------
aabcdfbas | rikcdfva  | cdf
aaab akuc | aaabir a  | aaab
ab akuc   | ab atr    | ab
Run Code Online (Sandbox Code Playgroud)

Ala*_*ein 8

我不同意那些说 SQL 不是完成这项工作的工具的人的观点。在有人能向我展示比我用任何编程语言的解决方案更快的方法之前,我会断言 SQL(以基于集合的、无副作用编写、仅使用不可变变量)是这项工作的唯一工具(当处理varchar(8000)-或 nvarchar(4000))。以下解决方案适用于 varchar(8000)。

1. 正确索引的计数(数字)表。

-- (1) build and populate a persisted (numbers) tally 
IF OBJECT_ID('dbo.tally') IS NOT NULL DROP TABLE dbo.tally;
CREATE TABLE dbo.tally (n int not null);

WITH DummyRows(V) AS(SELECT 1 FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) t(N))
INSERT dbo.tally
SELECT TOP (8000) ROW_NUMBER() OVER (ORDER BY (SELECT 1))
FROM DummyRows a CROSS JOIN DummyRows b CROSS JOIN DummyRows c CROSS JOIN DummyRows d;

-- (2) Add Required constraints (and indexes) for performance
ALTER TABLE dbo.tally 
ADD CONSTRAINT pk_tally PRIMARY KEY CLUSTERED(N) WITH FILLFACTOR = 100;

ALTER TABLE dbo.tally 
ADD CONSTRAINT uq_tally UNIQUE NONCLUSTERED(N);
Run Code Online (Sandbox Code Playgroud)

请注意,统计表功能的执行效果不佳。

2. 使用我们的计数表返回一个字符串的所有可能的子字符串

以“abcd”为例,让我们获取它的所有子字符串。请注意我的评论。

DECLARE @s1 varchar(8000) = 'abcd';

SELECT
  position  = t.N,
  tokenSize = x.N,
  string    = substring(@s1, t.N, x.N)  
FROM       dbo.tally t -- token position
CROSS JOIN dbo.tally x -- token length
WHERE t.N <=  len(@s1) -- all positions
AND   x.N <=  len(@s1) -- all lengths
AND   len(@s1) - t.N - (x.N-1) >= 0 -- filter unessesary rows [e.g.substring('abcd',3,2)]
Run Code Online (Sandbox Code Playgroud)

这返回

position    tokenSize   string
----------- ----------- -------
1           1           a
2           1           b
3           1           c
4           1           d
1           2           ab
2           2           bc
3           2           cd
1           3           abc
2           3           bcd
1           4           abcd
Run Code Online (Sandbox Code Playgroud)

3. dbo.getshortstring8K

这个功能是做什么用的?第一个重大优化。我们将把两个字符串中较短的一个分解成每个可能的子字符串,然后看看它是否存在于较长的字符串中。如果有两个字符串(S1 和 S2),并且 S1 比 S2 长,我们知道 S1 中比 S2 长的子串都不会是 S2 的子串。这就是 dbo.getshortstring 的目的:确保我们不执行任何不必要的子字符串比较。稍后这会变得更有意义。

这非常重要,因为可以使用三角形数字函数来计算字符串中子字符串的数量。以N为字符串的长度(字符数),子字符串的数量可以计算为N*(N+1)/2。例如“abc”有6个子串:3*(3+1)/2 = 6; a、b、c、ab、bc、abc。如果我们将“abc”与“abcdefgh”进行比较,则不需要检查“abcd”是否是“abc”的子字符串。

将“abcdefgh”(长度=8)分解为所有可能的子字符串需要 8*(8+1)/2 = 36 次操作( “abc”需要6 次操作)。

IF OBJECT_ID('dbo.getshortstring8k') IS NOT NULL DROP FUNCTION dbo.getshortstring8k;
GO
CREATE FUNCTION dbo.getshortstring8k(@s1 varchar(8000), @s2 varchar(8000))
RETURNS TABLE WITH SCHEMABINDING AS RETURN 
SELECT s1 = CASE WHEN LEN(@s1) < LEN(@s2) THEN @s1 ELSE @s2 END,
       s2 = CASE WHEN LEN(@s1) < LEN(@s2) THEN @s2 ELSE @s1 END;
Run Code Online (Sandbox Code Playgroud)

4. 查找较长字符串中存在的较短字符串的所有子环:

DECLARE @s1 varchar(8000) = 'bcdabc', @s2 varchar(8000) = 'abcd';

SELECT
  s.s1, -- test to make sure s.s1 is the shorter of the two strings
  position  = t.N,
  tokenSize = x.N,
  string    = substring(s.s1, t.N, x.N)
FROM dbo.getshortstring8k(@s1, @s2) s --<< get the shorter string
CROSS JOIN dbo.tally t  
CROSS JOIN dbo.tally x
WHERE t.N between 1 and len(s.s1)
AND   x.N between 1 and len(s.s1)
AND   len(s.s1) - t.N - (x.N-1) >= 0
AND   charindex(substring(s.s1, t.N, x.N), s.s2) > 0;
Run Code Online (Sandbox Code Playgroud)

5. 仅检索最长的公共子串

这是简单的部分。我们只需添加TOP (1) WITH TIES到我们的SELECT声明中就可以了。这里,最长公共子串是“bc”和“xx”

DECLARE @s1 varchar(8000) = 'xxabcxx', @s2 varchar(8000) = 'bcdxx';

SELECT TOP (1) WITH TIES 
  position  = t.N,
  tokenSize = x.N,
  string    = substring(s.s1, t.N, x.N)
FROM dbo.getshortstring8k(@s1, @s2) s
CROSS JOIN dbo.tally t  
CROSS JOIN dbo.tally x
WHERE t.N between 1 and len(s.s1)
AND   x.N between 1 and len(s.s1)
AND   len(s.s1) - t.N - (x.N-1) >= 0
AND   charindex(substring(s.s1, t.N, x.N), s.s2) > 0
ORDER BY x.N DESC;
Run Code Online (Sandbox Code Playgroud)

6. 将此逻辑应用到您的表中

使用 APPLY,我们将变量 @s1 和 @s2 替换为 t.str1 和 t.str2。我添加了一个过滤器来排除包含空格的匹配项(请参阅我的评论)...然后我们就出发了:

-- easily consumbable sample data
DECLARE @yourtable TABLE (str1 varchar(8000), str2 varchar(8000));
INSERT @yourtable 
VALUES ('aabcdfbas','rikcdfva'),('aaab akuc','aaabir a'),('ab akuc','ab atr');

SELECT str1, str2,  [max substring without spaces] = string
FROM @yourtable t
CROSS APPLY 
(
    SELECT TOP (1) WITH TIES 
      position  = t.N,
      tokenSize = x.N,
      string    = substring(s.s1, t.N, x.N)
    FROM dbo.getshortstring8k(t.str1, t.str2) s -- @s1 & @s2 replaced with str1 & str2 
    CROSS JOIN dbo.tally t  
    CROSS JOIN dbo.tally x
    WHERE t.N between 1 and len(s.s1)
    AND   x.N between 1 and len(s.s1)
    AND   len(s.s1) - t.N - (x.N-1) >= 0
    AND   charindex(substring(s.s1, t.N, x.N), s.s2) > 0
    AND   charindex(' ',substring(s.s1, t.N, x.N)) = 0 -- exclude substrings with spaces
    ORDER BY x.N DESC
) lcss;
Run Code Online (Sandbox Code Playgroud)

结果:

str1        str2      max substring without spaces
----------- --------- ------------------------------
aabcdfbas   rikcdfva  cdf
aaab akuc   aaabir a  aaab
ab akuc     ab atr    ab
Run Code Online (Sandbox Code Playgroud)

以及执行计划:

在此输入图像描述

...没有排序或不必要的操作。只是速度。对于较长的字符串(例如 50 个字符以上),我有一种更快的技术,您可以在此处阅读。

  • 可能是 stackoverflow 历史上最被低估的答案。来个updoote吧。 (3认同)