做数据库相关工作久了,你会发现一件事:数据量一旦上到几十万行,最耗时间的往往不是业务逻辑,而是数据搬运。有一次接到一个需求,要把三十万行业务流水导入线上库,最开始用程序一条一条 INSERT,跑了快二十分钟还没结束。同事在旁边说了一句,怎么不用 LOAD DATA INFILE?我查了一下,改成 load data infile 之后,同样是这批数据,几秒钟就进去了。从那次之后,凡是涉及 MySQL 批量入库、批量导出的活,我都会先想想三种方法——LOAD DATA INFILE、SELECT INTO OUTFILE、mysqldump——到底该用哪个。这个标题相关的经验,就是我当时踩了一圈坑之后整理出来的。
无论你是做数据分析、系统开发,还是专职 DBA,数据迁移和备份几乎是躲不开的日常操作。小文件用工具拖一下无所谓,但量一大,方式选错了,轻则等上几十分钟,重则把线上库拖垮。这篇文章把三种常用的导入导出方式讲透,包括它们各自的适用边界、参数细节、以及普通文档里不会告诉你的那些坑。顺手记录了一些我自己实际碰到的报错和排查思路,希望能帮你少走点弯路。
1. 先说结论:三种方式分别适合什么场景
1.1 同样是导入导出,差异到底在哪
很多人容易把 LOAD DATA INFILE 和 mysqldump 混为一谈,觉得反正都是“把数据搬进搬出”。其实它们的工作层级完全不同。
LOAD DATA INFILE 是数据库服务端直接读取磁盘上的数据文件,把解析后的行数据批量写入目标表。它的核心优势在于:绕过了一条条 INSERT 语句反复经历 SQL 解析、权限检查、执行计划生成这些环节,用一条 LOAD 语句完成整批数据的写入路径,性能损耗大幅下降。配合 InnoDB 的插入机制,大批量数据进入的速度可以比逐行 INSERT 快几十倍,这是我在实际中测试过的差距,不是理论上的说法。
SELECT INTO OUTFILE 则是把一张表的查询结果直接写到服务端磁盘上,生成一个纯文本文件。它和 LOAD DATA INFILE 正好是一对:一个把文件往库里导,一个把库里的数据往文件里导。两者配合起来,可以构造出非常规整的数据交换格式。
而 mysqldump 是逻辑备份工具,它产生的是 SQL 语句集合,里面包含了建表语句、INSERT 语句,以及可选的各种参数控制信息。它的主要场景是跨环境、跨版本迁移,或者生成逻辑上可读的备份文件。你可以在任何一台 MySQL 上通过执行 SQL 文件把数据还原出来,但它的执行速度天然不如直接文件导入,因为它本质上还是在跑大量 INSERT。
1.2 一张表理清选型思路
在实际确认用哪种方式前,我会先按数据量、场景、权限限制三条线来判断:
| 方式 | 原理 | 适合场景 | 性能特点 | 主要限制 |
|---|---|---|---|---|
| LOAD DATA INFILE | 服务端读文件批量写入 | 大量数据快速入库、初始化数据 | 极高,通常比 INSERT 快数十倍 | 需要 FILE 权限,受 secure_file_priv 限制 |
| SELECT INTO OUTFILE | 查询结果写到服务端文件 | 按需导出明细数据、生成交换文件 | 高,批量一次性写出 | 需要有 FILE 权限,服务端目录可控 |
| mysqldump | 生成逻辑 SQL 备份 | 跨库迁移、整库备份、结构导出 | 中,导入时以 INSERT 为主 | 数据量大时备份和还原都比较慢 |
我一般这样选:如果是几万行以上的数据要进库,优先 LOAD DATA INFILE;如果只是把某个结果集交付给数据分析同事,用 SELECT INTO OUTFILE 导出文件最干净;如果目标是备份整个库,或者把库从测试环境同步到生产环境,那无脑 mysqldump 就是最成熟的选择。
2. LOAD DATA INFILE 高效导入的实操细节
2.1 基础语法与让导入真正变快的关键配置
完整的基础用法是这样的:
LOAD DATA INFILE '/data/import/orders.csv' INTO TABLE orders CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES (order_id, user_id, amount, order_time);这里有几个容易踩的点。默认情况下,服务端读取文件时会把反斜杠当成转义符,如果你的数据里恰好有反斜杠或者特殊字符,最好显式指定 ESCAPED BY;CSV 中常见的是双引号包字段,所以要用 OPTIONALLY ENCLOSED BY '"' 把引号剥掉。
想让导入速度最大化,除了语法本身,我强烈建议在导入前做两件事:
SET FOREIGN_KEY_CHECKS = 0; SET UNIQUE_CHECKS = 0;如果有外键和唯一索引,InnoDB 在插入每条记录时都要检查一遍约束,这个开销非常大。临时关掉,导入完再打开,几十万条数据的耗时差距肉眼可见。同理,如果目标表有多个非唯一索引,可以考虑先删索引、导入完成后重建,这种手法在做数据仓库初始化时很常见。
2.2 FIELDS、LINES 子句里的坑与字符集处理
很多人写 LOAD DATA INFILE 时只写了文件路径和表名,剩下的全用默认值,然后就会遇到各种离奇问题。默认的 FIELDS TERMINATED BY 是制表符 \t,默认的 LINES TERMINATED BY 是换行符 \n,默认还会把空字段导入成 NULL。如果你的源文件不是制表符分隔,就必须把 FIELDS 子句写清楚。
字符集也是一个高频坑点。文件是什么编码,就得在 LOAD 语句里指明 CHARACTER SET,否则服务端会按它自己的 character_set_connection 去理解文件字节。之前我导过一份 UTF-8 编码的 CSV,但库里表的默认字符集是 latin1,导入后中文字段全部变成了问号。解决办法很简单:导入时显式加一句 CHARACTER SET utf8mb4,同时确认目标表字段的字符集也是 utf8mb4。
另外要注意文件路径的语义:不带 LOCAL 时,文件路径是相对于 MySQL 服务端的,必须放在 secure_file_priv 限定的目录里;带 LOCAL 时,文件是客户端本地的,通过客户端协议传给服务端。这里有一个性能差异:LOCAL 模式数据要经过客户端到服务端的网络传输,速度会受影响,能用服务端本机文件就尽量别用 LOCAL。
2.3 空值、默认值和类型转换的经典处理
源文件中经常会出现 NULL、空字符串、日期格式不一致等问题。LOAD DATA INFILE 在处理这些情况时有一套自己的规则:文件里的 \N 会被识别为 NULL;如果字段定义为 NOT NULL 且没有默认值,空字符串可能会被转为隐式默认值,这个行为在不同 sql_mode 下不同,很容易让人头大。
我有一次导用户表,文件里的性别字段是空字符串,字段本身没有默认值,结果导入后所有空字符串都变成了 0,业务方拿到数据后一脸懵。后来我学乖了,遇到这种字段,用临时变量接住再转换:
LOAD DATA INFILE '/data/import/users.csv' INTO TABLE users FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES (user_id, user_name, @gender, created_at) SET gender = IF(@gender = '', 'unknown', @gender);这样的写法不仅能把空字符串处理成业务想要的值,还能做简单的数据清洗。日期格式不标准时也可以用 STR_TO_DATE 转换,比如:
SET order_time = STR_TO_DATE(@order_time, '%Y/%m/%d %H:%i:%s');LOAD DATA INFILE 看起来只是导数据,实际上可以当做一个轻量的清洗管道使用,这一招在很多临时数据接入场景下非常有用。
3. SELECT INTO OUTFILE 及配套导出方案
3.1 先弄懂 outfile 的文件权限与服务端路径
SELECT INTO OUTFILE 的语法看起来很容易理解:
SELECT order_id, user_id, amount, order_time FROM orders WHERE order_time >= '2024-01-01' INTO OUTFILE '/data/export/orders_202401.csv' CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n';但实际用起来有几个先决条件会卡得人很难受。第一,MySQL 8.0 之后 secure_file_priv 默认是 NULL,也就是说服务端文件导入导出功能默认关闭。你必须先在 my.cnf 里配置 secure_file_priv 指向一个具体目录,然后重启服务。第二,执行用户需要 FILE 权限。第三,目标文件不能已存在,每次导出前需要先清理上一次的文件,否则会直接报错。
还有一个容易忽略的点:这个文件是写在 MySQL 服务端所在的主机上,不是你执行 SQL 的客户端机器。如果你通过远程工具执行这条语句,并不会把文件下载到你本地。跨机器使用场景下,一般需要先导出到服务端,然后在服务端上再拷贝走,或者直接让服务端提供文件下载通道。
3.2 导出格式设计与回导入的闭环配合
很多人导文件时不在意格式,随便定个分隔符,结果下游程序解析得欲仙欲死。我的习惯是:先想清楚这份文件将来要卖给谁。如果只是给人临时看一眼,逗号分隔就好了;如果是程序自动化消费,最好是 \t 分隔或自定义分隔符,避开内容里的逗号干扰。
把导出的文件重新导回去是很常见的闭环操作。为了让回导过程不折腾,导出和导入的 FIELDS 子句要保持一致。我通常固定一套组合:
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' ESCAPED BY '"' LINES TERMINATED BY '\n'导出时用这一套,导入时也用这一套。特别是 ESCAPED BY,如果没有指定,默认是反斜杠,数据里如果有引号或反斜杠就会被加一层转义,再导回时会引入脏数据。我吃过这个亏,导出的字段值里带了双引号,导入时因为转义规则不一致,解析错位,整批数据错行。后来我统一了导出和导入的转义符,问题才彻底解决。
3.3 本地导出时的备选方案
并不是所有场景都能用 SELECT INTO OUTFILE。有时候服务端目录权限受限,或者只是想临时拿一部分数据看看,直接用 mysql 客户端把查询结果重定向到本地文件更方便:
mysql -h 127.0.0.1 -uroot -p database_name -N -e "SELECT order_id, user_id, amount FROM orders LIMIT 1000" > /tmp/orders_sample.tsv加上 -N 参数可以去掉表头,如果不加 -N,第一行就是列名。默认输出以制表符分隔,适合快速查看。但如果要保证数据和特殊字符的完整性,还是建议用 INTO OUTFILE,因为客户端重定向在遇到某些特殊字符时不会做转义处理,可能导致数据与原值不一致。
另外,如果你只是想要一份带表头的 CSV 文件,INTO OUTFILE 不会自动生成表头,需要先手工 UNION 一个行再导出,或者导出后用 sed 在文件首行插入表头。这些小技巧在处理交付给业务方的文件时很实用。
4. mysqldump 备份导出的高效用法
4.1 常用关键参数组合
mysqldump 最常见的使用方式是整库导出:
mysqldump -h 127.0.0.1 -uroot -p --single-transaction --quick --default-character-set=utf8mb4 mydb > /backup/mydb_$(date +%F).sql这个组合有几个参数是必须理解的。--single-transaction 让 InnoDB 表在一个事务内做一致性快照备份,备份过程中不锁表,业务还能继续写入,这是线上备份的首选。--quick 告诉 mysqldump 不要先把所有数据缓冲在内存里再输出,而是边读边写,避免大表备份时内存被打爆。
如果只需要导出某张表的数据而不包含建表语句,可以用:
mysqldump -h 127.0.0.1 -uroot -p --single-transaction --no-create-info mydb orders > /backup/orders_data.sql反向只导结构不要数据,用 --no-data。这两个参数组合起来,可以灵活拆分结构和数据,在迁移时经常一起用。
4.2 大数据量下的 mysqldump 优化
当数据量上了十多个 GB,mysqldump 的默认行为就会让人捉急。最常见的优化是加 --extended-insert,默认其实已经开启,它的作用是把多行数据合并成一条 INSERT 语句中的多个值,减少执行时的语句开销。如果为了兼容性需要拆成单条 INSERT,可以加 --skip-extended-insert,但导入速度会明显变慢。
管道压缩是另一个立竿见影的手段:
mysqldump -h 127.0.0.1 -uroot -p --single-transaction --quick mydb | gzip > /backup/mydb_$(date +%F).sql.gz备份文件能小三四倍,传输和磁盘占用都友好很多。导入前先解压:
gunzip -c /backup/mydb_$(date +%F).sql.gz | mysql -h 127.0.0.1 -uroot -p mydb这里我特别提醒一句:超大库用 mysqldump 还原非常慢,因为它在导入时本质是执行海量 INSERT。如果要恢复几十 GB 以上的数据,在 MySQL 里通常需要考虑物理备份工具或者导入文件用 LOAD DATA INFILE。mysqldump 更适合做逻辑备份、跨版本迁移、异构环境同步。
4.3 导入备份文件时的注意事项
mysqldump 产生的 SQL 文件导入方式有很多种,最简单的就是重定向:
mysql -h 127.0.0.1 -uroot -p mydb < /backup/mydb.sql也可以进 MySQL 客户端之后用 source:
SOURCE /backup/mydb.sql;导入时最容易遇到的问题是 max_allowed_packet 不够。mysqldump 默认生成的单条 INSERT 可能非常大,如果目标库的 max_allowed_packet 设置小于单个 INSERT 语句的字节数,会直接报 Packet too large 错误。解决办法是导入前调大:
SET GLOBAL max_allowed_packet = 1073741824;另外,导入时关闭自动提交和外键检查,也能显著提速:
SET autocommit = 0; SET FOREIGN_KEY_CHECKS = 0; SOURCE /backup/mydb.sql; COMMIT;如果你的 SQL 文件里有多条 INSERT 数据,打开 autocommit=0 之后要注意,导入结束后务必手动 COMMIT,否则数据可能只停留在当前会话中。
5. 常见问题与排查技巧实录
5.1 高频报错速查表
我把平时遇到的高频问题整理成了一张表,照着排查基本能定位:
| 报错信息或现象 | 常见原因 | 解决思路 |
|---|---|---|
| The MySQL server is running with the --secure-file-priv option | secure_file_priv 默认为 NULL,限制了导入导出路径 | 在配置文件中指定目录,放到该目录下执行 |
| File not found | 服务端路径不存在或权限不足 | 确认路径相对于 MySQL 服务端,检查文件属主权限 |
| Cannot create file | SELECT INTO OUTFILE 目标文件已存在 | 先删除旧文件,文件名换不重复的 |
| Packet too large | max_allowed_packet 过小 | 调大该参数后重新导入 |
| Incorrect string value | 字符集不匹配 | 统一文件编码并把 CHARACTER SET 指定为 utf8mb4 |
| Column count doesn't match value count | 文件列数与表字段数不一致 | 核对 CSV 表头和建表字段,或者用临时变量接收占位 |
| 导入后中文字段乱码 | 文件编码与列字符集不一致 | 用 hex 查看文件编码,明确 CHARACTER SET |
5.2 三个我踩过的深坑
第一个坑是路径理解错位。我在一台跳板机上执行 LOAD DATA INFILE,文件明明放在跳板机上,却一直在报找不到文件。后来才意识到,不带 LOCAL 时,MySQL 解析文件路径用的是服务端主机视角,跳板机只是客户端,文件根本不在服务端。要么把文件传到服务端指定目录,要么改用 LOAD DATA LOCAL INFILE。
第二个坑是 CSV 中的数据特有逗号。有一次导第三方提供的 CSV,字段里有逗号,但没有用引号包起来,字段数量对不上,整批导入失败。遇到这类文件,先做一次预处理,把多余逗号统一替换或加引号,再执行导入。不要指望 LOAD 语句能智能识别 CSV 的字段边界,它只处理简单的分隔符和引号包裹。
第三个坑是 mysqldump 备份大库时把源库压垮。当时没有加 --single-transaction,默认对 MyISAM 表做全表锁,线上的 SELECT 全部排队。换成 --single-transaction 之后,备份对在线业务的影响降到了最低。如果你的库里有 MyISAM 表,这一点尤其重要,因为 --single-transaction 对 MyISAM 不起作用,需要额外评估或者考虑分区备份。
5.3 导入性能的关键调优思路
同样的文件,导入时间可以从十几分钟压到几十秒,核心思路无非是三条。第一,减少约束检查,暂时关闭外键和唯一性检查,甚至删除非必要索引,导入完后再重建。第二,减少事务开销,导入过程放在一个事务里,避免每行都提交。第三,调整 InnoDB 相关参数,比如合理增大 innodb_buffer_pool_size,让更多数据页在内存中操作,而不是频繁落磁盘。
如果你的数据文件非常大,还可以考虑先按主键或逻辑维度拆分成多个小文件,然后并行执行多个 LOAD DATA INFILE。并行导入的效果受 IO 和 CPU 资源限制,并不是无脑提高并发就一定更快,需要实测。我一般在四核八 GB 以下的小机器上会控制并发数不超过 4 个,再往上提升有限,反而容易出现锁等待和资源争抢。
回到最初那个三十万行导入的问题,用 LOAD DATA INFILE 配合关闭约束检查,几秒钟完成;同样的数据如果用 mysqldump 导出的 SQL 文件去还原,可能就要好几分钟。这两种方式没有谁绝对更好,就看你到底是要“搬文件”还是“搬库”,目标不同,工具就不同。最后再分享一个小习惯:每次导入导出后,我都会用 select count(*) 对比一次源数据和目标库的行数,再抽查几条关键字段,不盲目相信工具的返回状态。数据出入这件事,永远是核对完了才安心。