在MySQL日常数据治理中,源表常因业务写入不规范而积累重复记录。所谓去重后导出,并不是在导出命令里加一个开关,而是先通过查询逻辑把重复行剔除,再把结果集持久化到文件或另一个实例。核心在于明确重复的定义:是整行完全一致,还是某几个业务字段相同而保留最新一条。不同的定义直接决定使用DISTINCT、GROUP BY还是窗口函数,也影响导出时的性能与一致性。

使用SELECT INTO OUTFILE直接导出去重结果
最直观的方案是在MySQL服务端用SELECT语句完成去重,并通过INTO OUTFILE把结果写成文本文件。这种方式避免了把大量数据拉到客户端再写盘的网络开销,但要求运行MySQL的操作系统用户对被写入目录有写权限,且secure_file_priv参数不能限制该路径。去重逻辑可以包裹在子查询里,例如按用户手机号去重并保留id最大的记录。
下面示例假设表user_log有重复手机号,我们导出每个手机号最新的一条。注意代码块内所有小于号都做了转义,防止被解析为标签。
-- 导出每个手机号最新记录到服务器本地文件
SELECT t.id, t.phone, t.create_time
FROM user_log t
INNER JOIN (
SELECT phone, MAX(id) AS max_id
FROM user_log
GROUP BY phone
) tmp ON t.phone = tmp.phone AND t.id = tmp.max_id
INTO OUTFILE '/var/lib/mysql-files/user_dedup.csv'
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY 'n';
该方法的优势是服务端一气呵成,尤其适合百万级数据。缺点是文件落在数据库服务器上,需要通过scp等手段取回;同时导出期间若表被持续写入,未加锁可能读到中间状态。若业务允许短暂只读,可配合FLUSH TABLES WITH READ LOCK或在事务隔离级别REPEATABLE READ下开启一致性快照。
借助mysqldump与临时表完成去重导出
当团队习惯用mysqldump做迁移时,可先建临时表存入去重数据,再dump该表。这样做既复用既有备份脚本,又避免直接改原表结构。临时表可建在专门的管理库,用CREATE TABLE ... SELECT语法一步到位,之后用mysqldump指定单表导出为SQL文件。
示例先创建去重表,再用mysqldump导出。注意mysqldump是命令行工具,不在SQL内执行。
-- 建立去重结果表
CREATE TABLE clean_db.user_clean AS
SELECT u.* FROM user_log u
LEFT JOIN (
SELECT phone, MIN(id) AS keep_id
FROM user_log GROUP BY phone
) k ON u.phone = k.phone AND u.id = k.keep_id
WHERE k.keep_id IS NOT NULL;
# 导出干净表为SQL mysqldump -u root -p clean_db user_clean > user_clean.sql
这种方案的容错性更好,因为临时表可反复验证条数后再导出。劣势是占用额外存储空间,且多步操作增加脚本复杂度。对于超大型表,CREATE TABLE AS SELECT可能锁表较长时间,建议放在低峰期,或改用CREATE TEMPORARY TABLE配合应用层分批插入。
客户端程序分页拉取去重数据并写文件
如果数据库服务器禁止写文件,或网络策略限制直接取路径,可以用Python等客户端脚本连接MySQL,执行去重查询后分页fetch并写入本地CSV。关键是利用服务端游标或LIMIT OFFSET分批,避免一次性结果集撑爆内存。去重查询本身可用窗口函数ROW_NUMBER()按重复键分区排序,取行号为一的记录。
以下Python示例展示用pymysql分页导出。代码内反斜杠保留原样用于换行符。
import pymysql
import csv
conn = pymysql.connect(host='127.0.0.1', user='root', password='x', db='test')
cur = conn.cursor()
sql = """
SELECT id, phone, create_time FROM (
SELECT id, phone, create_time,
ROW_NUMBER() OVER (PARTITION BY phone ORDER BY id DESC) rn
FROM user_log
) t WHERE rn = 1
"""
with open('out.csv', 'w', newline='') as f:
w = csv.writer(f)
cur.execute(sql)
while True:
rows = cur.fetchmany(10000)
if not rows:
break
w.writerows(rows)
cur.close()
conn.close()
客户端方案最灵活,能顺带做格式转换或脱敏。但网络往返多,总体耗时高于服务端导出。生产环境应控制并发,并在WHERE条件里增加索引过滤,防止窗口函数对全表扫描。若MySQL版本低于8.0不支持窗口函数,可改成分组后查最小id再关联,逻辑等价。
无论选哪种路径,导出前都要用SELECT COUNT(*)核对去重后行数是否符合预期,并抽样比对原表。只有这样,MySQL去重后数据导出才是可信且可回溯的操作。