【问题标题】:MySQL SELECT DISTINCT with tolerance of variationMySQL SELECT DISTINCT 具有容差
【发布时间】:2011-09-14 17:25:49
【问题描述】:

在我的数据库中,我有很多非常相似但不相同的条目。例如,只有两个字符可能不同,例如:

Row1:“天气不错,见http://xyz56.com

Row2:“天气不错,见http://xyz31.com

我想摆脱这些部分重复,只收到这两行的一个结果。不管是哪一个,我都建议使用出现的第一个。

我可以利用 MySQL 的任何功能来有效地做到这一点吗?我的第一个想法是提取更多数据并对字符串进行比较,如果匹配字符超过某个阈值而不是忽略它。缺点是我永远不知道我必须从数据库中提取多少条目,而且效率也很低,因为我必须将每一行与所有其他行进行比较 (O(n²))。

更新: 更具体地说,用例:方差的位置并不总是在字符串的末尾,也可能不仅仅是 2 个字符发生变化。字符串长度因每一行而异。

【问题讨论】:

    标签: mysql distinct


    【解决方案1】:

    MySQL 的 Levenshtein 距离算法:

    请看:Implementation of Levenshtein distance for mysql/fuzzy search?

    Levenshtein 距离 两个字符串之间的 Levenshtein 距离是将一个字符串转换为另一个字符串所需的最小操作数,其中一个操作可能是插入、删除或替换一个字符。 Jason Rust 在http://www.codejanitor.com/wp/ 上发布了这个 MySQL 算法。

    CREATE FUNCTION levenshtein( s1 VARCHAR(255), s2 VARCHAR(255) ) 
      RETURNS INT 
      DETERMINISTIC 
      BEGIN 
        DECLARE s1_len, s2_len, i, j, c, c_temp, cost INT; 
        DECLARE s1_char CHAR; 
        -- max strlen=255 
        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; 
        ELSE 
          WHILE j <= s2_len DO 
            SET cv1 = CONCAT(cv1, UNHEX(HEX(j))), j = j + 1; 
          END WHILE; 
          WHILE i <= s1_len DO 
            SET s1_char = SUBSTRING(s1, i, 1), c = i, cv0 = UNHEX(HEX(i)), j = 1; 
            WHILE j <= s2_len DO 
              SET c = c + 1; 
              IF s1_char = SUBSTRING(s2, j, 1) THEN  
                SET cost = 0; ELSE SET cost = 1; 
              END IF; 
              SET c_temp = CONV(HEX(SUBSTRING(cv1, j, 1)), 16, 10) + cost; 
              IF c > c_temp THEN SET c = c_temp; END IF; 
                SET c_temp = CONV(HEX(SUBSTRING(cv1, j+1, 1)), 16, 10) + 1; 
                IF c > c_temp THEN  
                  SET c = c_temp;  
                END IF; 
                SET cv0 = CONCAT(cv0, UNHEX(HEX(c))), j = j + 1; 
            END WHILE; 
            SET cv1 = cv0, i = i + 1; 
          END WHILE; 
        END IF; 
        RETURN c; 
      END; 
    

    辅助函数:

    CREATE FUNCTION levenshtein_ratio( s1 VARCHAR(255), s2 VARCHAR(255) ) 
      RETURNS INT 
      DETERMINISTIC 
      BEGIN 
        DECLARE s1_len, s2_len, max_len INT; 
        SET s1_len = LENGTH(s1), s2_len = LENGTH(s2); 
        IF s1_len > s2_len THEN  
          SET max_len = s1_len;  
        ELSE  
          SET max_len = s2_len;  
        END IF; 
        RETURN ROUND((1 - LEVENSHTEIN(s1, s2) / max_len) * 100); 
      END; 
    

    Levenshtein 距离算法:Oracle PL/SQL 实现

    来源:http://www.merriampark.com/ldplsql.htm

    CREATE OR REPLACE FUNCTION ld -- Levenshtein distance
      (p_source_string   IN VARCHAR2,
       p_target_string   IN VARCHAR2)
      RETURN                NUMBER
      DETERMINISTIC
    AS
      v_length_of_source    NUMBER := NVL (LENGTH (p_source_string), 0);
      v_length_of_target    NUMBER := NVL (LENGTH (p_target_string), 0);
      TYPE mytabtype IS     TABLE OF NUMBER INDEX BY BINARY_INTEGER;
      column_to_left        mytabtype;
      current_column        mytabtype;
      v_cost                NUMBER := 0;
    BEGIN
      IF v_length_of_source = 0 THEN
        RETURN v_length_of_target;
      ELSIF v_length_of_target = 0 THEN
        RETURN v_length_of_source;
      ELSE
        FOR j IN 0 .. v_length_of_target LOOP
          column_to_left(j) := j;
        END LOOP;
        FOR i IN 1.. v_length_of_source LOOP
          current_column(0) := i;
          FOR j IN 1 .. v_length_of_target LOOP
            IF SUBSTR (p_source_string, i, 1) =
               SUBSTR (p_target_string, j, 1)
            THEN v_cost := 0;
            ELSE v_cost := 1;
            END IF;
            current_column(j) := LEAST (current_column(j-1) + 1,
                                        column_to_left(j) + 1,
                                        column_to_left(j-1) + v_cost);
          END LOOP;
          FOR j IN 0 .. v_length_of_target  LOOP
            column_to_left(j) := current_column(j);
          END LOOP;
        END LOOP;
      END IF;
      RETURN current_column(v_length_of_target);
    END ld;
    

    如果您假设有一个名为 EMPLOYEES 的表,其列名为 FIRST_NAME,类型为 VARCHAR2,您可以轻松找到 Levenshtein Distance = 1 的记录,如下所示:

    SELECT *
      FROM employees alfa
     WHERE EXISTS
              (SELECT 'X'
                 FROM employees beta
                WHERE ld (beta.first_name, alfa.first_name) = 1);
    

    使用此查询,您可以在结果集中的每一行中显示 Levenshtein Distance = 1 的 first_name 列表:

    SELECT a.first_name, b.first_name
      FROM    employees a
           INNER JOIN
              employees b
           ON ld (b.first_name, a.first_name) = 1;
    

    一个例子:

    SELECT DISTINCT a.first_name, b.first_name
      FROM    employees a
           INNER JOIN
              employees b
           ON ld (b.first_name, a.first_name) <= 2
              AND ld (b.first_name, a.first_name) > 0;
    
    FIRST_NAME;FIRST_NAME_1
    Jean;John
    Nancy;Vance
    Alana;Allan
    Alana;Clara
    Ellen;Eleni
    John;Jean
    Daniel;Danielle
    Danielle;Daniel
    Shelley;Shelli
    Sundita;Nandita
    Lisa;Luis
    Stephen;Steven
    Nanette;Janette
    Diana;Alana
    TJ;Ki
    Luis;Lisa
    Sarath;Sarah
    Louise;Luis
    Ki;TJ
    Allan;Ellen
    Luis;Louise
    Den;Lex
    Clara;Alana
    Matthew;Mattea
    Shelli;Shelley
    Sarah;Sarath
    Girard;Gerald
    Vance;Nancy
    Mattea;Martha
    Allan;Alana
    Nandita;Sundita
    Ellen;Allan
    Jean;Den
    Eleni;Ellen
    Gerald;Girard
    Lex;Den
    Janette;Nanette
    Steven;Stephen
    Mattea;Matthew
    Den;Jean
    Martha;Mattea
    Alana;Diana
    

    【讨论】:

      【解决方案2】:

      我的建议是使用Levenshtein distance,这是衡量字符串相似度的指标。要让 MySQL 直接计算它,您必须在存储过程中实现它,这里有一个示例:http://www.artfulsoftware.com/infotree/queries.php#552

      PHP 和 Java 也有常见的实现。

      【讨论】:

      • 这看起来很有希望,但我不明白如何在查询大量数据时应用它,而不仅仅是比较两个字符串?
      • 非常有趣的解决方案,保罗,恭维!我从未听说过 Levenshtein 距离,我发现它是一个非常有用的概念。
      • 如果你已经有很多(可能是半重复的)数据,就没有办法高效地做到这一点。一种直截了当的方法将比较效率为二次的任何 2 个字符串。我认为最好的方法是对当前数据进行重复数据删除或“修复”,修复可能效率低下,然后计算可能的半重复数据插入点。更好的方法是完全不诉诸字符串差异,通过添加为您的模型提供更多结构,以便您可以根据记录的存在进行中继,并使用您的数据库索引。
      • 在 Oracle PL/SQL 中,您可以轻松找到满足 Levenshtein distance = 1 条件的记录,例如 SELECT * FROM employees alfa WHERE EXISTS (SELECT 'X' FROM employees beta WHERE ld (beta .first_name, alfa.first_name) = 1);
      • 谢谢。我实现了该过程并运行了一个查询,该查询计算了我选择的结果(20 行)到硬编码字符串的 Levenshtein 距离。这已经使运行时间慢了大约四倍,所以不幸的是我认为这不适合限制查询,特别是因为我有 > 20.000 行。
      【解决方案3】:

      SELECT * FROM test GROUP BY SUBSTR(mytext, 1, 10);

      【讨论】:

      • 每行字符串的长度和变化的位置不同
      【解决方案4】:

      您可以使用 SOUNDEX。

      SOUNDEX(str)
      Returns a soundex string from str. Two strings that sound almost the same should have identical soundex strings. A standard soundex string is four characters long, but the SOUNDEX() function returns an arbitrarily long string. You can use SUBSTRING() on the result to get a standard soundex string. All non-alphabetic characters in str are ignored. All international alphabetic characters outside the A-Z range are treated as vowels.
      
      mysql> SELECT SOUNDEX('Hello');
      +---------------------------------------------------------+
      | SOUNDEX('Hello')                                        |
      +---------------------------------------------------------+
      | H400                                                    |
      +---------------------------------------------------------+
      1 row in set (0.00 sec)
      

      来源:http://www.tutorialspoint.com/mysql/mysql-string-functions.htm#operator_sounds-like

      例如,在 Oracle PL/SQL 中,您的字符串具有相同的 SOUNDEX 并且 SOUNDEX 无法区分:

      select soundex ('The weather is nice, see http://xyz56.com') from dual;
      
      SOUNDEX('THEWEATHERISNICE,SEEHTTP://XYZ56.COM')
      -----------------------------------------------
      T362                                           
      1 row selected.
      
      select soundex ('The weather is nice, see http://xyz31.com') from dual;
      
      SOUNDEX('THEWEATHERISNICE,SEEHTTP://XYZ31.COM')
      -----------------------------------------------
      T362                                           
      1 row selected.
      

      【讨论】:

      • Soundex 会在大多数数字变化时工作,但它并不总是有效,例如对于这些用例:第 1 行:“我的 iPhone 上的 Original Gangstaz 刚刚达到 55 级!单击链接加入我的帮派bit.ly/originalgz #iphone #ipod #ipad #OG”第 2 行:“全球战争在线刚刚达到 35 级在我的 iPhone 上!点击链接加入我的小队 bit.ly/globalwarapp1 #iphone #ipod #ipad #Gl"
      猜你喜欢
      • 2011-11-16
      • 2015-01-28
      • 2015-03-12
      • 1970-01-01
      • 1970-01-01
      • 2021-07-06
      • 2013-05-27
      • 2017-11-25
      相关资源
      最近更新 更多