【问题标题】:MySQL: how to efficiently select strings that have only one character difference?MySQL:如何有效地选择只有一个字符差异的字符串?
【发布时间】:2016-12-09 14:39:05
【问题描述】:

在 MySQL 中是否可以从表中选择与给定字符串只有一个字符差异的所有字符串?

例如。 Iphone 5Iphone 5sModel x-1Models x1

【问题讨论】:

  • 这一个字符的区别必须在字符串的末尾吗?
  • 没有。可以在字符串中的任何位置
  • 这取决于,你的意思是 1 个字符的差异。检查this answer

标签: mysql


【解决方案1】:

如果你可以向 MySQL 添加一个用户函数,那么你可以使用 Levenshtein 的距离。代码见this other question

然后你可以查询WHERE LEVENSHTEIN(description, 'iphone 5') <= 2例如。你会发现“iphone 5S”和“ipohne 5”,这可能是一个加分项。

否则,特定案例很容易(例如,REGEX 'iphone.*' 或类似的),但一般案例将是一场噩梦。

使用 Levenshtein 作为最后的手段

“文本距离”计算的成本是惊人的。因此,在使用它之前,请尽一切可能减少所需的计算次数。

例如,基本的 Levenshtein 假定插入和删除每个花费 1。这意味着“好”和“优秀”的最小距离为 5,只是因为 1 长了五个字符这个信息是两个LENGTHs 的差异,几乎没有成本。

如果可以根据第一个字母对行进行分区,则可以计算两个长度相等的余数之间的距离,如果第一个字母相等则加 0,如果不相等则加 1(如果余数不 长度相等,您可能会引入错误:“SLAUGHTER”和“LAUGHTER”相隔 1 次插入,但比较“LAUGHTER”和“AUGHTER”也会产生 1,导致得分为 1+1=2 1+0=1。相反,'LAUGHTER' 和 'DAUGHTER' 的长度相同,第一个字母会产生 1 加上 'AUGHTER' 和 'AUGHTER' 之间的差值,即 0)。

这种方法(有时称为 Jordan 集合缩减)可能会将您的单词列表从 10,000 个单词分成 50 个桶,每个桶包含 200 个词,只比较基数为 4 的桶子集;这意味着大约 50*(4*200)^2 = 3200 万次比较,而不是 10000^2 = 1 亿次,减少了 68%。

(实际上,假设字长从 5 到 15 并且所有字母的表示相同,您的列表将分为大约 250 个桶,每个桶 40 个词;250*(440)^2 是 640 万。如果您希望最大 两个 字符差异,则为 250(2*40)^2 = 160 万。节省的时间可能非常值得为单词拼凑成本)。

一旦您减少了需要进行实际比较的行数,然后对剩余的行调用 Levenshtein。

表演

此版本已修改为接受第三个参数 MAXCOST。当达到完整 Levenshtein 子循环的最大成本时,搜索功能将简单地中止。这减少了Levenshtein 距离不合理 的情况,而牺牲了距离 合理的情况。

所以如果你想要距离小于 3 的字符串,你可以使用WHERE levenshtein(string1, string2, 3) < 3最小单循环距离为 3 或以上的所有字符串现在将返回 3,并被排除在外。这使得函数失败安全,即它永远不会过度报告距离,并且只有在失败的情况下才会少报告距离(因此,如果两个字符串的距离为 1,则函数将永远不会返回大于 1。但如果他们的距离为 14 并且限制设置为 10,则可能会低估 12 而不是 14 的距离。

这意味着如果您的大部分查询当前返回小距离,此功能对您来说将不方便。如果您的近距离比赛很少而远距离比赛很多,那么这将起作用。

测试,使用短距离。请记住,此处未修改的 Levenshtein 函数将在我的机器上在 1.49 毫秒内返回。

SELECT 
BENCHMARK(1000, levenshtein('mogilifski', 'mogiliski', 999)) AS _,
levenshtein('mogilifski', 'mogiliski', 999) AS result;

1 row in set (1.656 sec)

因此,距离 1 将产生净损失 - 它慢 12%。降低限制不会改变时间,因为函数永远不会超过 2。

现在让我们尝试使用非常不同的字符串:“mogiliski”和“fridrich”。在这种情况下,实际距离为 9,将限制从 999 降低到 3 会将执行时间从 1.47 减少到 0.71 毫秒 - 节省了 50%。

更极端的情况要好得多:“Darmok and Jalad at Tanagra”和“Kiteo,他的眼睛睁开”是更长的字符串,距离为 23。执行时间通常为 9.672 ms(未修改版本为 9.05) .将限制降低到 5 会将执行时间降低到 2.89 毫秒。在 3 处进一步降低,因为 很明显 距离永远不会那么小,因此在第一个子循环之前立即终止,产生 0.03 毫秒。

这是original function 的修改代码。在定义这个新函数之前,您必须删除旧函数,除非您更改名称。

DELIMITER $$
DROP FUNCTION IF EXISTS LEVENSHTEIN $$
CREATE FUNCTION LEVENSHTEIN(s1 VARCHAR(255) CHARACTER SET utf8, s2 VARCHAR(255) CHARACTER SET UTF8, maxcost INT)
  RETURNS INT
  DETERMINISTIC
  BEGIN
    DECLARE s1_len, s2_len, i, j, c, c_temp, cost, mincost INT;
    DECLARE s1_char CHAR CHARACTER SET utf8;
    -- max strlen=255 for this function
    DECLARE cv0, cv1 VARBINARY(256);

    SET s1_len = CHAR_LENGTH(s1),
        s2_len = CHAR_LENGTH(s2),
        cv1 = 0x00,
        j = 1,
        i = 1,
        c = 0;

    IF (s1 = s2) THEN
      RETURN (0);
    ELSEIF (s1_len = 0) THEN
      RETURN (s2_len);
    ELSEIF (s2_len = 0) THEN
      RETURN (s1_len);
    END IF;

    IF (ABS(s1_len - s2_len) > maxcost) THEN
        RETURN maxcost;
    END IF;


    WHILE (j <= s2_len) DO
      SET cv1 = CONCAT(cv1, CHAR(j)),
          j = j + 1;
    END WHILE;

    WHILE (i <= s1_len) DO
      SET s1_char = SUBSTRING(s1, i, 1),
          c = i,
          cv0 = CHAR(i),
          j = 1;
      SET mincost = 255;
      WHILE (j <= s2_len) DO
        SET c = c + 1,
            cost = IF(s1_char = SUBSTRING(s2, j, 1), 0, 1);

        SET c_temp = ORD(SUBSTRING(cv1, j, 1)) + cost;
        IF (c > c_temp) THEN
          SET c = c_temp;
        END IF;

        SET c_temp = ORD(SUBSTRING(cv1, j+1, 1)) + 1;
        IF (c > c_temp) THEN
          SET c = c_temp;
        END IF;

        SET cv0 = CONCAT(cv0, CHAR(c)),
            j = j + 1;
        IF (c < mincost) THEN
            SET mincost = c;
         END IF;
      END WHILE;
      IF (mincost > maxcost) THEN
         RETURN (mincost);
      END IF;

      SET cv1 = cv0,
          i = i + 1;
    END WHILE;

    RETURN (c);
  END $$

DELIMITER ;

【讨论】:

  • 我确实添加了描述的功能,但是性能呢?如果我有一个 20,000 行的表怎么办?
  • 该功能即使在巨大的限制下也无法正常工作。 select levenshtein('mogilifski', 'mogiliski', 10) 当距离为 1 时返回 10。如果你运行 select levenshtein('mogilifski', 'mogiliski', 20) 你会得到 1。但第一种情况实际上使函数无用。如果更新,我很乐意改变我的看法
  • @AdriánE 你是对的。有一整套边缘案例打破了“快速退出”的概念。我已经相应地修改了代码。
  • @LSerni 我还没有彻底测试过它,但至少在我尝试过的情况下,它现在可以完美运行了!当然,我推翻了我对答案的投票,我还要感谢您花时间纠正它!
  • @AdriánE 我的谢谢!你不认为我高兴有错误的代码到处都是我的名字,是吗? :-D
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-16
  • 2011-05-13
  • 1970-01-01
  • 2014-04-01
  • 2015-04-22
相关资源
最近更新 更多