技術分享 | 快速處理 MySQL 重複數據小妙招

愛可生 發佈 2020-02-10T16:22:44+00:00

比如無主鍵的表d1mysql-->show create table d1\G*************************** 1. row *************************** Table: d1Create Table: CREATE TA

作者:楊濤濤


正好最近在幫客戶從達夢資料庫遷移到 MySQL。我也來簡單說說重複數據的處理。

存放在資料庫中的數據分為三種:

1. 一種是經過嚴格意義過濾出來的數據。比如程序端過濾數據源、資料庫端在表欄位上設置 check 標記過濾數據源、設置觸發器過濾、調用存儲過程過濾等等;

2. 另一種是原始的沒有經過任何處理的數據。比如程序端代碼異常導致產生非正常的想要的數據、資料庫端沒有設置任何過濾規則的數據保留等等。這樣會產生一系列垃圾數據,當然也包含了我今天要說的重複的數據。

3. 最後一種是 SQL 語句在執行過程中可能產生的重複數據。比如兩表外聯,總會產生一系列 NULL。

今天我要說的重複數據,不包含 SQL 語句在執行中產生的重複數據,只包含了原始重複數據的處理。接下來,用幾個經典的場景來說下。


第一種,記錄完全重複,這其實是最最簡單的去重場景。

比如無主鍵的表 d1

mysql-(ytt/3305)->show create table d1\G*************************** 1. row ***************************       Table: d1Create Table: CREATE TABLE `d1` (  `r1` int(11) DEFAULT NULL,  `r2` int(11) DEFAULT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci1 row in set (0.00 sec)

記錄數總共為四百萬。

mysql-(ytt/3305)->select count(*) from d1 limit 2;+----------+| count(*) |+----------+|  4000000 |+----------+1 row in set (0.18 sec)

可以看到足足有四分之三的記錄是重複的。

mysql-(ytt/3305)->select count(distinct r1,r2) from d1 ;+-----------------------+| count(distinct r1,r2) |+-----------------------+|               1000000 |+-----------------------+1 row in set (2.68 sec)

比如記錄(1,1)就有四條。

mysql-(ytt/3305)-> select * from db1 order by r1,r2 limit 5;+------+------+| r1   | r2   |+------+------+|    1 |    1 ||    1 |    1 ||    1 |    1 ||    1 |    1 ||    2 |    2 |+------+------+5 rows in set (1.65 sec)

這種去重非常簡單,要麼在資料庫層做,要麼把數據導出來篩選好在導到資料庫里來。

在資料庫里做,無非就是新建一張克隆表,完了把正常數據篩選出來,再重新命名後,刪掉原來的表,步驟也不是非常繁瑣,例子如下:

mysql-(ytt/3305)->create table d2 like d1;Query OK, 0 rows affected (0.01 sec)

時間主要耗費在去重並且插入新表這裡

mysql-(ytt/3305)->insert into d2 select distinct r1,r2 from d1;Query OK, 1000000 rows affected (19.40 sec)Records: 1000000  Duplicates: 0  Warnings: 0mysql-(ytt/3305)->alter table d1 rename to d1_bak;Query OK, 0 rows affected (0.00 sec)mysql-(ytt/3305)->alter table d2 rename to d1;Query OK, 0 rows affected (0.00 sec)mysql-(ytt/3305)->drop table d1_bak;Query OK, 0 rows affected (0.00 sec)

上面總共花了大概 20 秒的樣子,再來看看在系統層面上去重,先導出數據,

mysql-(ytt/3305)->select * from db1 into outfile '/var/lib/mysql-files/d1.txt';Query OK, 4000000 rows affected (1.84 sec)

系統層面去重,用 OS 自帶的工具 sort 和 uniq。

root@ytt-pc:/var/lib/mysql-files# time cat d1.txt |sort -g  |uniq > d1_uniq.txtreal    0m7.345suser    0m7.528ssys     0m0.272s

導入到原表,

mysql-(ytt/3305)->truncate table d1;Query OK, 0 rows affected (0.05 sec)root@ytt-pc:/var/lib/mysql-files# mv d1_uniq.txt  d1.txt

把處理好的數據直接導入到資料庫

root@ytt-pc:/home/ytt/scripts# time mysqlimport -uytt -pytt -P3305 -h 127.0.0.1   --use-threads=2 -vvv ytt /var/lib/mysql-files/d1.txtmysqlimport: [Warning] Using a password on the command line interface can be insecure.Connecting to 127.0.0.1Selecting database yttLoading data from SERVER file: /var/lib/mysql-files/d1.txt into d1ytt.d1: Records: 1000000  Deleted: 0  Skipped: 0  Warnings: 0Disconnecting from 127.0.0.1real    0m3.272suser    0m0.012ssys     0m0.008s

看下處理好的記錄,

mysql-(ytt/3305)->select * from d1 where 1 order by r1,r2 limit 2;+------+------+| r1   | r2   |+------+------+|    1 |    1 ||    2 |    2 |+------+------+2 rows in set (0.40 sec)

OS 層面稍微效率高些,總體包括數據導出,數據去重,數據導入,差不多是資料庫層時間的一半。


第二種,其實和第一種類似,不同的是表有主鍵,但是其他的欄位記錄值是重複的。

舉個例子,表 d4 除了加了主鍵,其他的記錄和之前的一模一樣。記錄如下:

mysql-(ytt/3305)->select * from d4 order by r1,r2 limit 5;+---------+------+------+| id      | r1   | r2   |+---------+------+------+|       1 |    1 |    1 || 3000001 |    1 |    1 || 2000001 |    1 |    1 || 1000001 |    1 |    1 ||       2 |    2 |    2 |+---------+------+------+5 rows in set (1.08 sec)

但是這種一般就得需要和具體的業務商量了,比如我需要留下重複記錄的最大主鍵值,比如上面這個,留下最大的 id 為 3000001 這條記錄。這樣的去重一條 sql 就搞定了,

mysql-(ytt/3305)->delete a from d4 a left join (select max(id) id from d4 group by r1, r2) b using(id) where b.id is null;Query OK, 3000000 rows affected (23.29 sec)

去掉了 300W 行重複記錄,剩下四分之一的正常數據。

mysql-(ytt/3305)->select count(*) from d4;+----------+| count(*) |+----------+|  1000000 |+----------+1 row in set (0.06 sec)

來看下效果,保留了最大值,其他的刪掉了。

mysql-(ytt/3305)->select * from d4 order by r1,r2 limit 5;+---------+------+------+| id      | r1   | r2   |+---------+------+------+| 3000001 |    1 |    1 || 3000002 |    2 |    2 || 3000003 |    3 |    3 || 3000004 |    4 |    4 || 3000005 |    5 |    5 |+---------+------+------+5 rows in set (0.25 sec)


第三種,不同於前面兩種,這種體現在欄位值里多餘的字符,比如空格,多餘的換行符等。依然看看幾個例子:

1. 去掉欄位值內前後的空白字符,是這類里最簡單的。這種 MySQL 有現成的函數,一條基礎的 SQL 即可。 

表y11 有500W行示例數據mysql-(ytt/3305)->select count(*) from y11;+----------+| count(*) |+----------+|  5242880 |+----------+1 row in set (0.30 sec)利用trim函數。mysql-(ytt/3305)->update y11 set r1 = trim(r1), r2 = trim(r2);Query OK, 5242880 rows affected (2 min 1.56 sec)Rows matched: 5242880  Changed: 5242880  Warnings: 0mysql-(ytt/3305)->select * from y11 limit 5;+----+------------------------+------------------------+| id | r1                     | r2                     |+----+------------------------+------------------------+|  1 | sql server             | sql server             ||  2 | sql server             | sql server             ||  3 | sql server             | sql server             ||  6 | db2 mysql oracle mysql | db2 mysql oracle mysql ||  7 | db2 mysql oracle mysql | db2 mysql oracle mysql |+----+------------------------+------------------------+5 rows in set (0.00 sec)

2. 去掉單詞中間的各種空白字符(空格,換行,制表符等);單詞前後,中間都有空格的場景。

依然是表 y11,從結果來看,各種換行符,空格已經讓結果無法正常顯示了。

mysql-(ytt/3305)->select * from y11 limit 5;+----+-----------------------------------------------------+------------------------------------------------------+| id | r1                                                  | r2                                                   |+----+-----------------------------------------------------+------------------------------------------------------+|  1 | sql server                                          | sql server                                           ||  2 | sql       server                                    | sql            server                                |server                                         | sql      server                                         | |           mysql | db2         mysql         oracle | 7 | db2         mysql         oracle                 mysql | db2         mysql         oracle                mysql+----+-----------------------------------------------------+------------------------------------------------------+5 rows in set (0.00 sec)

可能最先想到的方法是把數據導出為文本文件,完了用 linux 上的各種工具處理完了再導進去,比如:

mysql-(ytt/3305)->select * from y11 into outfile '/var/lib/mysql-files/y11.txt'  fields terminated by ',' enclosed by '"';Query OK, 5242880 rows affected (3.54 sec)mysql-(ytt/3305)->truncate y11;Query OK, 0 rows affected (0.23 sec)

用 sed 處理下,替換掉所有的空白字符。

root@ytt-pc:/var/lib/mysql-files# time sed -i 's/\s\+/ /g' y11.txtreal    0m27.476suser    0m20.105ssys     0m7.233s

導入到表 y11

mysql-(ytt/3305)->load data infile '/var/lib/mysql-files/y11.txt' into table y11 fields terminated by ',' enclosed by '"';Query OK, 5242880 rows affected (30.25 sec)Records: 5242880  Deleted: 0  Skipped: 0  Warnings: 0

上面雖然達成了目的,但是過程過於繁瑣,如果 MySQL 層實在解決不了再考慮。

完全可以利用 MySQL 的正則替換功能直接替換掉多餘的字符為一個空格,也是一條簡單的 SQL。

mysql-(ytt/3305)->update y11 set r1 = regexp_replace(r1,'[[:space:]]+',' '), r2 = regexp_replace(r2,'[[:space:]]+',' ');Query OK, 4194304 rows affected (1 min 32.05 sec)Rows matched: 5242880  Changed: 4194304  Warnings: 0

只是時間上稍微長些,不過也影響不是很大。

mysql-(ytt/3305)->select * from y11 limit 5;+----+------------------------+-------------------------+| id | r1                     | r2                      |+----+------------------------+-------------------------+|  1 | sql server             | sql server              ||  2 | sql server             | sql server              ||  3 | sql server             | sql server              ||  6 | db2 mysql oracle mysql | db2 mysql oracle mysql  ||  7 | db2 mysql oracle mysql | db2 mysql oracle mysql  |+----+------------------------+-------------------------+5 rows in set (0.00 sec)

我想可能日常數據處理難免會有數據去重的場景,希望這部分內容對大家有幫助。

關鍵字: