Mysql 匹配 "Same" 封电子邮件

Mysql Matching "Same" Emails

我有一个 table,有 2 列 emailid。我需要找到密切相关的电子邮件。例如:

john.smith12@example.com

john.smith12@some.subdomains.example.com

这些应该被认为是相同的,因为用户名 (john.smith12) 和最顶级域 (example.com) 是相同的。它们目前在我的 table 中有 2 个不同的行。 我写了下面的表达式,它应该进行比较,但执行起来需要几个小时(possibly/probably 因为正则表达式)。有没有更好的写法:

  select c1.email, c2.email 
  from table as c1
  join table as c2
   on (
             c1.leadid <> c2.leadid 
        and 
             c1.email regexp replace(replace(c2.email, '.', '[.]'), '@', '@[^@]*'))

此查询的解释返回为:

id, select_type, table, type, possible_keys, key, key_len, ref,  rows,   Extra
1,  SIMPLE,      c1,    ALL,   NULL,         NULL,  NULL,  NULL, 577532, NULL
1,  SIMPLE,      c2,    ALL,   NULL,         NULL,  NULL,  NULL, 577532, Using where; Using join buffer (Block Nested Loop)

创建 table 是:

CREATE TABLE `table` (
 `ID` int(11) NOT NULL AUTO_INCREMENT,
 `Email` varchar(100) DEFAULT NULL,
 KEY `Table_Email` (`Email`),
 KEY `Email` (`Email`)
) ENGINE=InnoDB AUTO_INCREMENT=667020 DEFAULT CHARSET=latin1

我想索引没有被使用是因为正则表达式。

正则表达式结果为:

john[.]smith12@[^@]*example[.]com

应该匹配两个地址。

更新:

我已将 on 修改为:

on (c1.email <> '' and c2.email <> '' and c1.leadid <> c2.leadid and substr(c1. email, 1, (locate('@', c1.email) -1)) = substr(c2. email, 1, (locate('@', c2.email) -1))
and    
substr(c1.email, locate('@', c1.email) + 1) like concat('%', substr(c2.email, locate('@', c2.email) + 1)))

并且使用这种方法的 explain 至少使用了索引。

id, select_type, table, type, possible_keys, key, key_len, ref, rows, Extra
1, SIMPLE, c1, range, table_Email,Email, table_Email, 103, NULL, 288873, Using where; Using index
1, SIMPLE, c2, range, table_Email,Email, table_Email, 103, NULL, 288873, Using where; Using index; Using join buffer (Block Nested Loop)

到目前为止这已经执行了 5 分钟,如果有很大的改进将会更新。

更新 2:

我已经拆分了电子邮件,所以用户名是一列,域是一列。我以相反的顺序存储域,因此它的索引可以与尾随通配符一起使用。

CREATE TABLE `table` (
     `ID` int(11) NOT NULL AUTO_INCREMENT,
     `Email` varchar(100) DEFAULT NULL,
     `domain` varchar(100) CHARACTER SET utf8 DEFAULT NULL,
     `username` varchar(500) CHARACTER SET utf8 DEFAULT NULL,
     KEY `Table_Email` (`Email`),
     KEY `Email` (`Email`),
     KEY `domain` (`domain`)
    ) ENGINE=InnoDB AUTO_INCREMENT=667020 DEFAULT CHARSET=latin1

填充新列的查询:

update table
set username = trim(SUBSTRING_INDEX(trim(email), '@', 1)), 
domain = reverse(trim(SUBSTRING_INDEX(SUBSTRING_INDEX(trim(email), '@', -1), '.', -3)));

新查询:

select c1.email, c2.email, c2.domain, c1.domain, c1.username, c2.username, c1.leadid, c2.leadid
from table as c1
join table as c2
on (c1.email is not null and c2.email is not null and c1.leadid <> c2.leadid
    and c1.username = c2.username and c1.domain like concat(c2.domain, '%'))

新解释结果:

1, SIMPLE, c1, ALL, table_Email,Email, NULL, NULL, NULL, 649173, Using where
1, SIMPLE, c2, ALL, table_Email,Email, NULL, NULL, NULL, 649173, Using where; Using join buffer (Block Nested Loop)

从那个解释看来 domain 索引没有被使用。我还尝试使用 USE 强制使用,但这也没有用,导致没有使用索引:

select c1.email, c2.email, c2.domain, c1.domain, c1.username, c2.username, c1.leadid, c2.leadid
from table as c1
USE INDEX (domain)
join table as c2
USE INDEX (domain)
on (c1.email is not null and c2.email is not null and c1.leadid <> c2.leadid
    and c1.username = c2.username and c1.domain like concat(c2.domain, '%'))

use解释:

1, SIMPLE, c1, ALL, NULL, NULL, NULL, NULL, 649173, Using where
1, SIMPLE, c2, ALL, NULL, NULL, NULL, NULL, 649173, Using where; Using join buffer (Block Nested Loop)

不需要REGEXP_REPLACE,所以它适用于MySQL/MariaDB的所有版本:

UPDATE tbl
    SET email = CONCAT(SUBSTRING_INDEX(email, '@', 1),
                       '@',
                       SUBSTRING_INDEX(
                           SUBSTRING_INDEX(email, '@', -1),
                           '.',
                           -2);

因为没有索引是有用的,所以你最好不要使用 WHERE 子句。

您告诉我们 table 有 70 万行。

这并不多,但您正在将其连接到自身,因此在最坏的情况下引擎将不得不处理 700K * 700K = 490 000 000 000 = 490B 行。

索引绝对能帮上忙。

最佳索引取决于数据分布。

下面的查询是什么 return?

SELECT COUNT(DISTINCT username) 
FROM table

如果结果接近 700K,比如 100K,则意味着有很多不同的用户名,您最好关注它们,而不是 domain。如果结果很低,比如 100,则索引 username 不太可能有用。

我希望有很多不同的用户名,所以,我会在 username 上创建一个索引,因为查询使用简单的相等比较在该列上连接,而这个连接将从中受益匪浅指数.

另一个要考虑的选项是 (username, domain) 上的复合索引,甚至是 (username, domain, leadid, email) 上的覆盖索引。索引定义中列的顺序很重要。

我会删除所有其他索引,这样优化器就无法做出其他选择,除非有其他查询可能需要它们。

很可能在 table 上定义一个主键也没有坏处。


还有一件不太重要的事情需要考虑。您的数据真的有 NULL 吗?如果不是,则将列定义为 NOT NULL。此外,在许多情况下,最好使用空字符串而不是 NULL,除非您有非常具体的要求并且必须区分 NULL 和 ''.

查询会稍微简单一点:

select 
    c1.email, c2.email, 
    c1.domain, c2.domain, 
    c1.username, c2.username, 
    c1.leadid, c2.leadid
from 
    table as c1
    join table as c2
        on  c1.username = c2.username 
        and c1.domain like concat(c2.domain, '%')
        and c1.leadid <> c2.leadid

如果你搜索相关数据,你应该看看一些数据挖掘工具或弹性搜索,例如,它们可以满足你的需要。

我有另一个可能的 "database-only" 解决方案,但我不知道它是否可行,或者它是否是最佳解决方案。如果我必须这样做,我会尝试制作 table of "word references",通过将所有电子邮件按所有非字母数字字符拆分来填充。

在您的示例中,此 table 将填充:john、smith12、some、子域、example 和 com。每个单词都有一个唯一的 id。然后,另一个 table,一个联合 table,它将 link 带有自己文字的电子邮件。 table 都需要索引。

要搜索密切相关的电子邮件,您必须使用正则表达式拆分源电子邮件并在每个子词上循环,like this one in the answer(通过连接),然后对于每个词,在单词引用 table,然后联合 table 以查找与其匹配的电子邮件。

通过此请求,您可以创建一个 select 来汇总所有匹配的电子邮件,方法是按电子邮件分组以计算与找到的电子邮件匹配的单词数,并仅保留最匹配的电子邮件(不包括源电子邮件) , 当然).

很抱歉 "not-sure-answer",但是评论太长了。我将尝试举个例子。


这里有一个例子(在 oracle 中,但应该与 MySQL 一起使用)和一些数据:

---------------------------------------------
-- Table containing emails and people info
CREATE TABLE PEOPLE (
     ID NUMBER(11) PRIMARY KEY NOT NULL,
     EMAIL varchar2(100) DEFAULT NULL,
     USERNAME varchar2(500) DEFAULT NULL
);

-- Table containing word references
CREATE TABLE WORD_REF (
     ID number(11) NOT NULL PRIMARY KEY,
     WORD varchar2(20) DEFAULT NULL
);

-- Table containg id's of both previous tables
CREATE TABLE UNION_TABLE (
     EMAIL_ID number(11) NOT NULL,
     WORD_ID number(11) NOT NULL,
     CONSTRAINT EMAIL_FK FOREIGN KEY (EMAIL_ID) REFERENCES PEOPLE (ID),
     CONSTRAINT WORD_FK FOREIGN KEY (WORD_ID) REFERENCES WORD_REF (ID)
);

-- Here is my oracle sequence to simulate the auto increment
CREATE SEQUENCE MY_SEQ
  MINVALUE 1
  MAXVALUE 999999
  START WITH 1
  INCREMENT BY 1
  CACHE 20;

---------------------------------------------
-- Some data in the people table
INSERT INTO PEOPLE (ID, EMAIL, USERNAME) VALUES (MY_SEQ.NEXTVAL, 'john.smith12@example.com', 'jsmith12');
INSERT INTO PEOPLE (ID, EMAIL, USERNAME) VALUES (MY_SEQ.NEXTVAL, 'john.smith12@some.subdomains.example.com', 'admin');
INSERT INTO PEOPLE (ID, EMAIL, USERNAME) VALUES (MY_SEQ.NEXTVAL, 'john.doe@another.domain.eu', 'jdo');
INSERT INTO PEOPLE (ID, EMAIL, USERNAME) VALUES (MY_SEQ.NEXTVAL, 'nathan.smith@example.domain.com', 'nsmith');
INSERT INTO PEOPLE (ID, EMAIL, USERNAME) VALUES (MY_SEQ.NEXTVAL, 'david.cayne@some.domain.st', 'davidcayne');
COMMIT;

-- Word reference data from the people data
INSERT INTO WORD_REF (ID, WORD) 
  (select MY_SEQ.NEXTVAL, WORD FROM
   (select distinct REGEXP_SUBSTR(EMAIL, '\w+',1,LEVEL) WORD
    from PEOPLE
    CONNECT BY REGEXP_SUBSTR(EMAIL, '\w+',1,LEVEL) IS NOT NULL
  ));
COMMIT;

-- Union table filling
INSERT INTO UNION_TABLE (EMAIL_ID, WORD_ID)
select words.ID EMAIL_ID, word_ref.ID WORD_ID
FROM 
(select distinct ID, REGEXP_SUBSTR(EMAIL, '\w+',1,LEVEL) WORD
 from PEOPLE
 CONNECT BY REGEXP_SUBSTR(EMAIL, '\w+',1,LEVEL) IS NOT NULL) words
left join WORD_REF on word_ref.word = words.WORD;
COMMIT;    

---------------------------------------------
-- Finaly, the request which orders the emails which match the source email 'john.smith12@example.com'
SELECT COUNT(1) email_match
      ,email
FROM   (SELECT word_ref.id
              ,words.word
              ,uni.email_id
              ,ppl.email
        FROM   (SELECT DISTINCT regexp_substr('john.smith12@example.com'
                                             ,'\w+'
                                             ,1
                                             ,LEVEL) word
                FROM   dual
                CONNECT BY regexp_substr('john.smith12@example.com'
                                        ,'\w+'
                                        ,1
                                        ,LEVEL) IS NOT NULL) words
        LEFT   JOIN word_ref
        ON     word_ref.word = words.word
        LEFT   JOIN union_table uni
        ON     uni.word_id = word_ref.id
        LEFT   JOIN people ppl
        ON     ppl.id = uni.email_id)
WHERE  email <> 'john.smith12@example.com'
GROUP  BY email_match DESC;

请求结果:

    4    john.smith12@some.subdomains.example.com
    2    nathan.smith@example.domain.com
    1    john.doe@another.domain.eu

您通过

获得名称(即“@”之前的部分)
substring_index(email, '@', 1)

您获得的域名为

substring_index(replace(email, '@', '.'), '.', -2))

(因为如果我们用点替换'@',那么它总是倒数第二个点之后的部分)。

因此您找到了

的重复项
select *
from users
where exists
(
  select *
  from mytable other
  where other.id <> users.id
    and substring_index(other.email, '@', 1) = 
        substring_index(users.email, '@', 1)
    and substring_index(replace(other.email, '@', '.'), '.', -2) =
        substring_index(replace(users.email, '@', '.'), '.', -2)
);

如果这太慢,那么您可能需要在两者的组合上创建一个计算列并为其编制索引:

alter table users add main_email as 
  concat(substring_index(email, '@', 1), '@', substring_index(replace(email, '@', '.'), '.', -2));

create index idx on users(main_email);

select *
from users
where exists
(
  select *
  from mytable other
  where other.id <> users.id
    and other.main_email = users.main_email
);

当然你也可以把两者分开索引:

alter table users add email_name as substring_index(email, '@', 1);
alter table users add email_domain as substring_index(replace(email, '@', '.'), '.', -2);

create index idx on users(email_name, email_domain);

select *
from users
where exists
(
  select *
  from mytable other
  where other.id <> users.id
    and other.email_name = users.email_name
    and other.email_domain = users.email_dome
);

当然,如果您允许在电子邮件地址栏中同时使用大写和小写,您还需要在上面的表达式中对其应用 LOWER (lower(email))。