news 2026/10/22 5:58:40

MySQL数据导入导出提速指南:LOAD DATA INFILE与mysqldump实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL数据导入导出提速指南:LOAD DATA INFILE与mysqldump实践

1. 数据搬运这件事,为什么总是卡在"导入导出"这一步

做数据库的人,早晚都会遇到这一关:要把一百多万行数据从一套环境搬到另一套环境,或者把某张大表导出来给数据分析团队。我见过太多人第一反应是打开图形化客户端,选中表、右键、导出,然后看着进度条跑一个下午,导出完了又是一下午导入。等我把LOAD DATA INFILE和mysqldump的组合拳打完之后,整个迁移时间从四小时压缩到了十几分钟,这个反差是很大的。

先摆个结论:MySQL 里真正高效的数据导入导出,核心就三样——LOAD DATA INFILE、SELECT INTO OUTFILE,以及mysqldump。这三者各有边界,组合起来覆盖了绝大多数业务场景。这篇文章我打算把这三种方式背后的原理、适用场景、参数细节和踩坑经验一次性讲透,适合正在做数据库迁移、数据备份恢复、或者定期要给业务方提供数据文件的开发者参考。

先说一下三者的定位,方便你在脑子里建立坐标系:

工具方向产出物典型场景
LOAD DATA INFILE文件进MySQL表数据大批量导入,如日志入库、迁移恢复
SELECT INTO OUTFILEMySQL出文件文本文件(CSV/TSV等)导出给其他系统,配套导入前的导出
mysqldumpMySQL出文件SQL语句或分隔文本逻辑备份、建库迁移、结构+数据导出

很多人以为mysqldump就能解决一切,实际远不是这样。mysqldump导出的 SQL 文件在回放时本质上是逐条执行INSERT,单行插入开销极大,到了千万级数据量就会让你等得怀疑人生。而LOAD DATA INFILE走的是 MySQL 内部的高速加载通道,速度能比逐条INSERT快数十倍。后面我会把这条"速度差异从哪来"的逻辑讲清楚。

2. LOAD DATA INFILE 的加速原理与参数拆解

2.1 为什么 LOAD DATA 比单条 INSERT 快那么多

一句话概括:INSERT是客户端把 SQL 文本发给服务器,服务器解析、优化、执行,每条语句都有固定的解析开销和网络往返;而LOAD DATA INFILE是文件直接交给 MySQL 服务端,由服务端直接读取文件内容,跳过了一大半 SQL 解析和网络传输环节。

打个比方。你去银行存一百张存单,一张一张递进窗口,柜员每张都要验、要录、要复核,这是INSERT。而你把一整捆存单直接交进去,银行走批量清分通道,整捆过机器,最后只登一笔总账,这是LOAD DATA INFILE。

两者的底层差异主要体现在四方面:

  • 网络开销:INSERT语句要从客户端传到服务器,网络延迟叠加每行都产生一次往返。LOAD DATA则一次读取整个文件,网络只在文件传输阶段产生一次开销(如果是LOCAL模式),或完全没有网络开销(服务端本地文件模式)。
  • SQL解析开销:每一条INSERT都要经过 SQL 词法、语法、语义解析,生成执行计划。LOAD DATA绕过了这些步骤,直接把文本行按字段规则切分进存储引擎。
  • 日志和事务提交:逐条INSERT如果不开事务,每条都触发一次提交(sync_binlog、innodb_flush_log_at_trx_commit参数决定刷盘频率),这些 fsync 操作非常费时。LOAD DATA在执行过程中记录 binlog 和 undo/redo 日志的方式是打包处理的,刷盘次数少得多。
  • 索引更新:逐条插入时每行都要走索引查找和 B+ 树更新路径。LOAD DATA会对批量加载做一些合并优化,尤其在导入前临时禁用或延迟唯一性检查的情况下,差距更加悬殊。

所以你会发现一个现象:用mytop或者SHOW PROCESSLIST观察执行中的LOAD DATA,状态显示通常是 "Reading from net" 或 "Bulk insert",server 端资源消耗处于高位,但整体耗时非常短。

2.2 核心语法结构与参数详解

LOAD DATA的完整语法很长,但日常 90% 的场景只需要这一套骨架:

LOAD DATA [LOCAL] INFILE '/path/to/file.csv' INTO TABLE `your_table` CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' ESCAPED BY '\\' LINES TERMINATED BY '\n' IGNORE 1 LINES (`col1`, `col2`, `col3`, ...) SET `create_time` = NOW();

逐个拆开说,每个参数都不是随便写的。

LOCAL关键字

LOCAL决定了文件在哪台机器上。

  • 不带LOCAL:文件必须在 MySQL 服务器本地,MySQL 服务进程直接读取。
  • 带LOCAL:文件在客户端机器上,客户端先把文件内容传送到服务器临时目录,再由服务器加载。

带LOCAL更灵活,比如你在自己电脑上用mysql命令行连接远程实例时,文件在本地,就必须加LOCAL。但它有个副作用:文件内容要从客户端经过网络传到服务端,大数据量(几个G)时会有网络传输耗时,同时受local_infile参数和客户端编译选项控制,某些发行版默认是关闭的。

FIELDS TERMINATED BY

字段分隔符。CSV 常用逗号,TSV 常用\t。有一点容易被忽略:如果某些字段值里本身包含逗号,单纯的TERMINATED BY ','会把一个字段截成两个,需要配合ENCLOSED BY来解决。

OPTIONALLY ENCLOSED BY

我这里的OPTIONALLY意思是"字段值可能被引号包裹,也可能没有,两种都能处理"。比如这一行:

101,"张三","北京市朝阳区",28

如果只写FIELDS TERMINATED BY ',',"张三"会被当成含引号的字符串存进去,引号被原样保留,后面查询你还要自己REPLACE去引号,很麻烦。加上OPTIONALLY ENCLOSED BY '"'之后,MySQL 会自动剥掉字段两边的引号,存进入的是干净值。

LINES TERMINATED BY

行结束符。Linux 下文件通常是\n,Windows 下可能是\r\n,Mac 老式文件可能是\r。如果导入的行数总是比预期少一行或者最后一行内容异常,八成是行结束符没配对。从 Windows 机器导出的 CSV 在 Linux 服务器上导入时,一定要先检查文件里有没有\r。

IGNORE 1 LINES

跳过前 N 行。最常见的用途是跳过 CSV 文件的表头行。我见过有人不跳表头,把列名那一行当成数据导进去,然后表里多了一行id='id', name='name'的脏数据,排查半天才发现。

字段列表

(col1,col2, ...)指定文件中每一列对应表的哪些字段。文件列顺序和表字段顺序不一致时,这一步是必须的。没有列出的字段要么走默认值(DEFAULT、自增、NULL),要么在后面的SET里赋值。

SET子句

可以在导入过程中做表达式转换,比如把导入时间统一写成当前时间、把字符串类型做STR_TO_DATE转换,或者把源文件的某个状态码映射成业务状态。它的执行顺序是:先按字段映射填入数据,再执行SET里的表达式覆盖对应列。这比先导入再UPDATE性能好太多,因为省了一次全表更新。

2.3 实际速度对照:从"一小时"到"三分钟"

说点实测感受。我这边一套库,单表 800 万行,每行 20 个字段,含两个索引。用第三方客户端工具的"导入数据"功能,本质是批量拼接INSERT逐条执行,跑完花了将近两个小时。后来改用LOAD DATA INFILE,同样的数据,CSV 文件 3.2GB,导入耗时大约 3 分 40 秒。差距到了 30 倍以上。

这不是个例。官方文档曾经给过参考数据,几千到一万行的INSERT批量插入,和LOAD DATA相比差距在 20 到 50 倍之间,具体取决于表结构复杂度和磁盘性能。你可以在自己环境做一个快速验证:

-- 先创建一张空表 CREATE TABLE load_test ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50), score INT ); -- 用存储过程生成100万行测试数据 DELIMITER $$ CREATE PROCEDURE gen_data() BEGIN DECLARE i INT DEFAULT 1; START TRANSACTION; WHILE i <= 1000000 DO INSERT INTO load_test (name, score) VALUES (CONCAT('user_', i), FLOOR(RAND() * 100)); SET i = i + 1; END WHILE; COMMIT; END$$ DELIMITER ; CALL gen_data();

这个跑完你应该会明显感知到:一百万条INSERT哪怕是包在一个事务里,也要几十秒到几分钟。然后你把它导出成 CSV,再LOAD DATA导入一张同构空表,时间通常只要十秒上下。这种直观对比比我在这说破嘴都管用。

不过要注意,LOAD DATA的速度受目标表索引数量影响很大。表上索引越多,导入越慢。一个不加压测的表和一个五个索引的表,导入时长差两三倍是家常便饭。

3. SELECT INTO OUTFILE:先把数据变成文件,才有资格谈导入

3.1 权限前提与默认目录限制

很多人忽略一个问题:SELECT INTO OUTFILE不是你在 SQL 客户端里随手敲一行就能跑的。它有两个硬性前提:

  1. 当前用户必须有FILE权限。
  2. MySQL 服务端secure_file_priv参数不能把它禁止掉。

secure_file_priv这个参数是个老坑。它的含义是"允许LOAD DATA INFILE和SELECT INTO OUTFILE读写文件的目录限定"。

  • 如果值是/var/lib/mysql-files/,那你只能往这个目录里写文件。
  • 如果值是空字符串(在配置里写成secure_file_priv=''),表示不限制目录。
  • 如果值是NULL,表示彻底禁用了文件导入导出功能。

我遇到过不止一次这样的情况:SQL 执行完提示ERROR 1290 (HY000): The MySQL server is running with the --secure-file-priv option so it cannot execute this statement。你去看一眼SHOW VARIABLES LIKE 'secure_file_priv';,多半是指向了一个固定目录或者返回NULL。解决办法是在my.cnf里配置:

[mysqld] secure_file_priv=/data/exchange

配置完重启 MySQL,记得确认/data/exchange目录存在并且mysql用户有读写权限,否则下一轮报错就是权限问题了。

3.2 导出语法与字段格式对齐

SELECT INTO OUTFILE的基本语法如下:

SELECT id, name, score INTO OUTFILE '/data/exchange/users.csv' CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' ESCAPED BY '\\' LINES TERMINATED BY '\n' FROM users;

注意一个细节:INTO OUTFILE导出的文件,换行符在 Linux 默认是\n,但如果你在 Windows 上用记事本打开,可能看起来是乱码一样的一整行。这不影响数据本身,但如果你要把文件传给业务方,最好先统一约定行结束符为\r\n,或者用unix2dos转一下。

字段分隔符和封闭符的选择,需要和你后面导入端的解析规则完全对齐。这里有一个选型逻辑,不只是"看心情":

  • 如果字段值里没有逗号、没有引号,用FIELDS TERMINATED BY ','就够了,不要画蛇添足加ENCLOSED BY,否则文件体积会变大。
  • 如果字段值是中文、包含特殊符号,强烈建议加OPTIONALLY ENCLOSED BY '"',这样值里的逗号、换行符都被引号保护起来。
  • 如果字段里可能存在换行(比如地址、备注类字段),LINES TERMINATED BY就要小心。理想情况是业务上禁止这类字段含换行,否则导出文件和导入方的解析规则都会变得很脆弱。

我自己的习惯是:统一导出成 CSV 格式,FIELDS TERMINATED BY ',',OPTIONALLY ENCLOSED BY '"',LINES TERMINATED BY '\n'。这套组合在绝大多数下游系统(Python 的csv模块、Shell 的awk、Excel 的导入向导)都能正常解析。

3.3 和 mysqldump 的本质区别:导出的是"数据"还是"命令"

接着聊个关键区别。SELECT INTO OUTFILE导出的是"干净的纯数据",文件里没有任何 SQL 语句,只有分隔开的字段值。而mysqldump导出的是"SQL 语句集合",是CREATE TABLE和INSERT INTO这种回放命令。这两种产物的应用场景完全两码事:

比较项SELECT INTO OUTFILEmysqldump
产物内容纯文本数据(CSV等)SQL 语句脚本
是否含表结构不含默认包含,可只用--no-data导结构
回放方式LOAD DATA INFILE速度快mysql < dump.sql逐条执行,速度慢
适用场景跨系统数据交换、大数据量迁移结构备份、小数据量迁移、异地重建
依赖工具MySQL 客户端和导入机制mysqldump二进制和 mysql 客户端

一句话总结:要做数据交换,用SELECT INTO OUTFILE导纯文件,再LOAD DATA导入目标库,这是性能路径。要做结构和整库迁移,用mysqldump更省心,但数据量大时回放会慢。两者不是替代关系,而是互补关系,下一篇我展开讲怎么组合。

4. mysqldump 的正确打开方式:它没那么慢,是你用错了

4.1 常用参数的作用和取舍

mysqldump人人都会用,但不是每个人都明白每个参数的代价。我把日常最常用的一组列出来:

mysqldump -h 127.0.0.1 -P 3306 -u backup_user -p \ --single-transaction \ --quick \ --opt \ --default-character-set=utf8mb4 \ --hex-blob \ --set-gtid-purged=OFF \ mydb > mydb_dump.sql

逐个说:

--single-transaction

这是 InnoDB 表导出一致性快照的关键。它通过开启一个可重复读事务拿到一致性快照,导出的过程中其他会话的 DML 不会污染导出结果,同时也不会阻塞其他会话的正常读写。这对在线备份是刚需。

但注意,它只对事务表(InnoDB)有效。如果库里混着 MyISAM 表,--single-transaction对它们不生效,会退化为LOCK TABLES,直接锁住 MyISAM 表直到导出结束。生产环境要提前排查哪些表是 MyISAM,别等到导出的时候才发现写不进去了。

--quick

逐行从服务器读取数据而不是一次性把所有数据加载到内存。不加这个参数时,对于大表,mysqldump可能先把整张表的数据拉到客户端内存,内存不够就直接 OOM。加上--quick后是流式输出,默认情况下mysqldump也会带这个行为,但显式写出来更稳妥。

--opt

这个参数是个组合开关,等价于启用了--quick、--add-drop-table、--add-locks、--extended-insert、--lock-tables等一堆优化选项。其中--extended-insert很关键:它会把多条INSERT合并成一条多值的INSERT INTO table VALUES (...),(...),(...)语句。回放时,这个比逐条插入快非常多,因为减少了客户端和服务器之间的交互次数。

但--opt里自带--lock-tables,导出时会锁所有表。如果你用--single-transaction,建议再显式加--skip-lock-tables,让锁表逻辑彻底不触发。

--set-gtid-purged=OFF

如果你在用 GTID 模式(MySQL 5.7 之后的常见配置),不带这个参数导出的文件里会包含SET @@GLOBAL.GTID_PURGED=...语句。导入到另一个实例时经常遇到 GTID 冲突或者空事务报错。在不需要同步 GTID 信息的数据迁移场景,直接加OFF干净利落。

--hex-blob

二进制字段(BLOB、BINARY、VARBINARY)如果不用 hex 编码导出,回放时可能因为字符集转换导致数据损坏。加了这个参数,二进制数据会以十六进制字符串写入 SQL 文件,语义无损。

4.2 只导结构、只导数据怎么拆

我实际迁移中经常遇到一种情况:目标库的表结构已经建好了,只需要搬数据。这时你用完整mysqldump导出再全部回放,等于重复建表,浪费时间还容易因为表已存在报错。正确姿势是拆分成两种模式:

# 只导表结构(不含数据) mysqldump --single-transaction --no-data mydb > mydb_schema.sql # 只导数据(不含建表语句) mysqldump --single-transaction --no-create-info --complete-insert mydb > mydb_data.sql

--no-data好理解。--no-create-info表示不输出CREATE TABLE,但--complete-insert值得说道一下:它生成的INSERT语句会包含完整的列名列表,比如:

INSERT INTO `users` (`id`, `name`, `score`) VALUES (1, 'a', 90);

而不是:

INSERT INTO `users` VALUES (1, 'a', 90);

前者在目标表字段顺序不同、缺列或新增列的情况下依然能正确回放,后者全靠列顺序猜。做跨环境迁移时,源库和目标库结构大概率不是完全一致的,建议始终带上--complete-insert。

4.3 mysqldump 回放时的加速技巧

数据量一大,回放 SQL 文件也会很痛苦。mysql < mydb_data.sql这种简单粗暴的方式不是不行,但有几个加速手段可以叠加:

第一,加大max_allowed_packet。多值INSERT可能体积很大,默认的 4MB 或 16MB 不够用。客户端导入前先执行:

SET GLOBAL max_allowed_packet = 128 * 1024 * 1024; SET SESSION max_allowed_packet = 128 * 1024 * 1024;

不然你会看到ERROR 1153 (08S01): Got a packet bigger than 'max_allowed_packet' bytes这类报错。

第二,导入前临时关闭外键检查和唯一性检查。在 SQL 文件开头加上:

SET FOREIGN_KEY_CHECKS = 0; SET UNIQUE_CHECKS = 0; SET SQL_LOG_BIN = 0;

第三行SQL_LOG_BIN=0需要谨慎:在 binlog 开启的实例上,这会让导入操作不记录 binlog。如果这个导入本身不需要被从库复制,那它能省掉大量 binlog 写入的开销。但一旦这是主库,且从库需要同样数据,这个开关反而会导致数据不一致。所以挂从库的环境不要用,单机导入或者导入后马上重建从库的场景才适用。

第三,分割文件并行导入。一个 20GB 的 SQL 文件,串行导入可能得一小时。如果用split按行拆成多个小文件,每个文件一个连接并行导入,时间能成倍压缩。但主从环境要小心:并行导入的多个连接在从库回放时依然是串行的,所以对从库延迟没有改善作用。

第四,导入时把innodb_flush_log_at_trx_commit临时调到 2 甚至 0。它的默认值是 1,每次事务提交都要把 redo log 刷到磁盘;调到 0 后每秒才刷一次,对导入速度提升显著。但要注意,这是以掉电丢日志为代价的,只能用于导入这种一次性、可重来、丢数据无所谓的过程,导入完恢复成 1。

5. 大数据量迁移的完整链路:结构、数据、校验一个都不能少

5.1 组合拳设计思路

前面讲到三个工具各有分工,现在就来说说我实际迁移一套复杂业务库的标准流程。假设源库source_db要整体迁到目标库target_db,数据总量大约 300GB,表结构有几百张表,其中最大一张表 2 亿行。

这个数据量级,直接mysqldump整库导出再整库回放是很不理智的,耗时以天计。实操我通常拆成三步走:

第一步:结构迁移

mysqldump -h source-host --single-transaction --no-data \ --set-gtid-purged=OFF --default-character-set=utf8mb4 \ source_db > schema.sql mysql -h target-host target_db < schema.sql

结构迁移很快,几百张表几分钟搞定。这里有个细节:schema.sql里可能包含CREATE DATABASE和USE语句,如果你只迁移部分表而不是整库,建议用sed过滤掉CREATE DATABASE和USE行,只在目标库内回放建表语句:

grep -v "CREATE DATABASE\|USE \`" schema.sql | mysql -h target-host target_db

第二步:数据迁移

大表用SELECT INTO OUTFILE+LOAD DATA INFILE,小表直接mysqldump --no-create-info。判断"大表"的阈值我一般定在 500 万行,超过就走文件通道。

# 大表导出到文件 mysql -h source-host -u backup_user -p source_db <<'SQL' SELECT * FROM big_table INTO OUTFILE '/data/exchange/big_table.csv' FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n'; SQL # 传输到目标服务器(假设你有权限操作目标服务器文件系统) scp /data/exchange/big_table.csv target-host:/data/exchange/ # 目标库导入 mysql -h target-host -u backup_user -p target_db <<'SQL' LOAD DATA INFILE '/data/exchange/big_table.csv' INTO TABLE big_table FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n'; SQL

这个操作的前提是secure_file_priv允许你往/data/exchange写。如果不行,就改用SELECT ... INTO OUTFILE ...配合LOAD DATA LOCAL INFILE组合,文件留在本地客户端传输,但速度会打折。

第三步:数据校验

迁移完最怕的是"看起来迁移了,实际缺行"。我习惯做三件事交叉验证:

先看行数是否一致:

SELECT COUNT(*) FROM source_db.big_table; SELECT COUNT(*) FROM target_db.big_table;

再抽关键业务字段的校验和:

SELECT SUM(CRC32(CONCAT_WS('|', id, name, score))) FROM source_db.big_table; SELECT SUM(CRC32(CONCAT_WS('|', id, name, score))) FROM target_db.big_table;

两个值一致,基本可以确信数据没丢。第三步随机抽几条记录比对明细,特别是含时间类型、浮点数的字段,确认精度没有因字符集或格式转换受损。

5.2 迁移中自增ID、外键和字符集的坑

先说自增 ID。源库表AUTO_INCREMENT当前值可能是 100000,但用LOAD DATA导入纯数据时,并不会把当前自增计数器带过去。如果导入后你执行INSERT,可能从 1 开始,直接碰撞已有记录的主键。正确的修复姿势:

-- 导入后把自增计数器调整到当前最大值+1 ALTER TABLE big_table AUTO_INCREMENT = 1;

如果你用的是mysqldump导 SQL 文件回放,且--opt默认带的--complete-insert保留了自增值的时间点逻辑,那表结构里的AUTO_INCREMENT=xxxxx会被一起带上,不会出现这个问题。但走LOAD DATA文件通道就必须手工处理。

再说外键。mysqldump导出的 SQL 文件默认会带SET FOREIGN_KEY_CHECKS=0;和恢复的语句,所以直接回放没问题。但LOAD DATA不会处理外键关系,如果你的表有外键依赖,导入时按依赖关系顺序处理,先导主表再导子表,或者导入期间临时关闭外键检查:

SET FOREIGN_KEY_CHECKS = 0; LOAD DATA INFILE '...' INTO TABLE child_table ...; LOAD DATA INFILE '...' INTO TABLE parent_table ...; SET FOREIGN_KEY_CHECKS = 1;

字符集是最容易埋雷的点。源库表是latin1,导出文件里中文已经按latin1存成了字节,导入到utf8mb4表时,如果你在LOAD DATA里指定的CHARACTER SET不是对应的转换字符集,中文全部变成乱码。稳妥做法是导出时统一指定源库真实字符集,导入时指定目标库字符集,并在连接层也保持一致:

mysql --default-character-set=utf8mb4 -h target-host ...

如果已经导入乱了,优先用CONVERT函数修正,而不是删了重导。但更靠谱的是在导入之前就在小数据集上试跑一遍,用SELECT HEX(content)对比源库和目标库的字节,确认无转换损失再全量导入。

5.3 binlog 和从库视角下的特殊处理

如果你迁移的目标库挂着从库,或者本身就是主从架构的一部分,要考虑的就不只是导入速度了,还有复制一致性。

主从环境下,LOAD DATA在 binlog 里的记录方式是LOAD DATA事件,其中包含文件内容本身。从库回放时会在本地重建临时文件再执行,耗时和主库差不多,不会因为主库的"高速"造成从库接收瞬时的巨大 I/O 风暴,但会出现从库延迟拉大。这也是为什么超大文件的导入,我建议拆成多个中等文件,分批导入,让从库有机会追平延迟。

mysqldump时,如果你的主库开启了 GTID,导出文件里可能包含GTID_PURGED设置,导入到另一个已有 GTID 执行历史的实例,会报ERROR 3546 (@@GLOBAL.GTID_PURGED cannot be changed). 解决方案是导出时加--set-gtid-purged=OFF,如前面所说。但要注意,关闭 GTID 信息后再导入,主库上这条导入操作也会以普通事务方式执行,不影响复制,但要求从库也具备同样的表结构和数据基础。

6. 实战中绕不开的坑:文件权限、编码、NULL值和性能毛刺

6.1 secure_file_priv 与导入文件所有权

你按官方文档写完LOAD DATA INFILE命令,报错ERROR 13 (HY000): Can't get stat of '/path/to/file.csv' (Errcode: 13),十有八九是这两类问题之一:

一是文件不在secure_file_priv限定的目录里。查看一下:

SHOW VARIABLES LIKE 'secure_file_priv';

确认你读的文件在返回的目录前缀下。

二是 MySQL 服务进程的运行用户(通常是mysql)没有文件的读权限。把 CSV 文件放到/tmp下,权限是 777,虽然能用但不好,生产环境secure_file_priv指向专用目录,并把目录 owner 改成mysql:mysql,权限 750 就够。

LOAD DATA LOCAL INFILE走客户端本地路径时,服务端仅校验local_infile变量,不校验secure_file_priv,但客户端和服务端都需要把local_infile打开,否则报:

ERROR 1148 (42000): The used command is not allowed with this MySQL version

这里特别提醒:线上生产环境默认把local_infile关闭是安全实践,不应当为了图省事全局开启。如果确实有需求,在会话级别临时开启再关闭即可。

6.2 空字符串和 NULL 的界定

导入数据时最常见的数据失真场景,就是源文件里""和NULL被混为一谈。CSV 文件里,一个字段写成什么都不写、写成""、写成NULL,在业务语义上是三种不同的东西:

  • 什么都不写,LOAD DATA默认置为该列的默认值(可能是NULL,可能是DEFAULT 0)。
  • 写成"",空字符串真的会被存成空串''。
  • 写成字符串NULL,如果不用SET处理,会被原样存成'NULL'这四个字符。

如果你希望文件里的空位和NULL文本都变成 SQL NULL,可以这样写:

LOAD DATA INFILE '/data/exchange/users.csv' INTO TABLE users FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' (id, @name, @score) SET name = NULLIF(@name, ''), score = NULLIF(@score, '');

NULLIF(expr, '')的语义是:如果表达式结果等于空字符串,则返回NULL,否则返回原值。配合@变量接收原始字段值,等于在导入管道里做了一层"空值清洗"。

反过来,导出时如果表里某些字段是NULL,SELECT INTO OUTFILE默认输出的是\N两个字符(不是 NULL,也不是空)。导入方如果不认识\N,就会把 NULL 当成字符串\N存回去,这是另一种"看起来对、实则丢了语义"的坑。解决办法是导出时用IFNULL(col, '')把你业务里的 NULL 统一成空串:

SELECT id, IFNULL(name, ''), IFNULL(score, 0) INTO OUTFILE '...' FROM users;

这样下游解析逻辑简单统一,不需要专门处理\N。

6.3 编码不一致的连锁反应

字符集问题不一定会报错,但报出来就很隐蔽。比如你LOAD DATA一个 UTF-8 的 CSV 到utf8mb4表,一切正常。但如果文件是 GBK 编码,你忘了指定CHARACTER SET gbk,MySQL 会把 GBK 字节流按 UTF-8 解析,轻则插入非法字符报错Incorrect string value,重则"顺利插入"但全是乱码——后者更麻烦,因为不报错意味着你已经污染了数据。

判断文件编码的快速方法是用file命令:

file -i users.csv # 输出可能类似: users.csv: text/plain; charset=utf-8

然后对比源表本身的SHOW CREATE TABLE users\G里声明的 charset,不一致就转码:

iconv -f GBK -t UTF-8 users.csv > users_utf8.csv

这里建议建立一条不成文的规矩:任何数据交换文件,宁可多此一举全部统一成 UTF-8,再在LOAD DATA里显式写CHARACTER SET utf8mb4,也不要指望 MySQL"自动识别"。它能猜,但猜错的成本太高了。

6.4 大文件导入时性能毛刺的来源

大文件导入时,你盯着服务器监控会看到几个奇怪的"毛刺":导入开始阶段 CPU 突然飙高,中间出现一段 IO 等待,快要结束的时候又有一波缓冲池刷盘。这些不是故障,是正常的资源节奏:

  • 开始阶段:文件读取、解析、行切分,CPU 密集。
  • 中间段:随着导入行数增加,脏页越来越多,触发了innodb_buffer_pool的 LRU 淘汰和刷盘,IO 打到峰值。
  • 结束阶段:事务提交或 binlog 落盘,可能有一次小规模的同步 IO。

如果毛刺过于严重,比如导入速度骤降十倍,多半是innodb_buffer_pool_size太小,或者innodb_io_capacity没调大。更好的做法是导入前把这些参数临时调大:

[mysqld] innodb_buffer_pool_size = 16G innodb_io_capacity = 2000 innodb_io_capacity_max = 4000

导入完成后恢复原值。这些参数调整在某些版本里支持动态设置,生产环境可以先在会话或实例级别临时SET GLOBAL innodb_io_capacity = 2000;,不用改配置文件重启。

7. 数据量不同,打法完全不同:我的分档经验

聊到最后,我想把这些年做数据导入导出的一些经验按数据量档位做个归纳。技术方案永远不能脱离数据量谈,同样的操作在 10 万行和 10 亿行面前完全是两回事。

万级以内

这个量级没什么好纠结的,mysqldump导出再mysql < dump.sql回放,或者直接客户端工具导出成 SQL 导入,全程都在你的耐心范围之内。没必要为了省几秒钟引入LOAD DATA的权限配置复杂度。

百万级到千万级

这是LOAD DATA INFILE的主场。导出用SELECT INTO OUTFILE生成 CSV,导入用LOAD DATA,配合--single-transaction的mysqldump做结构备份,整体迁移时间在一小时内完成。这个量级优化收益最明显,投入产出比最高。

亿级以上

单靠LOAD DATA已经不够了。亿级表导入前必须做的几件事:

  1. 评估是否可以先删掉非必要索引,导入完再重建。索引重建的时间远小于带索引导入多出的时间。
  2. 拆分成多个千万级的 CSV 分片,多个LOAD DATA并行导入(注意实例并发度,别把 IO 打满)。
  3. 导入完成后立即ANALYZE TABLE更新统计信息,防止优化器用错误的统计值生成烂执行计划。
  4. 考虑用pt-table-schema-change这类外部工具做在线结构变更,而非直接ALTER TABLE。

这里分享一个小技巧:并行导入的时候,每个LOAD DATA文件的行数要对齐到大致相等的量级,不要让某一个文件特别大,否则最后那个连接拖后腿,整体完成时间会无限接近最慢的那个文件。

我自己常用的并行方式是用xargs拉多进程:

ls /data/exchange/chunk_*.csv | xargs -P 4 -I {} \ mysql -h target-host -u load_user -p target_db \ -e "LOAD DATA INFILE '{}' INTO TABLE big_table FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' LINES TERMINATED BY '\n'"

-P 4表示四个并发,数字根据目标机器的磁盘能力和 MySQL 并发写能力调整,压测后通常 2 到 6 是比较合理的区间。

最后说一个我个人的体会:数据导入导出看起来是一个"零门槛"的基础操作,但真正把速度拉开差距的,往往不是某一个单点技巧,而是对整个链路的理解——从字符集对齐、索引策略、事务刷盘、binlog 代价,到并发度的控制。每一条路径省出来的时间一点点叠加,最终才是那个让你"去喝杯茶回来就完事"的效果。

我在实际项目里见过最快的迁移,就是把mysqldump只用来导表结构,大表全部走SELECT INTO OUTFILE配合并行LOAD DATA INFILE,全程没有一条 SQL 是逐行插入的。这套打法迁移 100GB 数据,从开始到校验通过,四十分钟内完成。理解了上面这些原理,你自己也能按同样的思路搭出一套适合你业务的数据搬运流水线。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 5:58:31

声明式Agent框架实战:一句话造Agent,告别手搓脚手架

1. 从“手搓 Agent”到“一句话造 Agent”的转变逻辑1.1 为什么“手搓 Agent”正在成为过去式如果你最近半年在折腾大模型应用&#xff0c;大概率经历过这样一个阶段&#xff1a;想做一个能自动查资料、写代码、跑流程的智能体&#xff0c;结果光是搭骨架就耗掉两三天。工具调用…

作者头像 李华
网站建设 2026/10/22 5:58:28

Python上下文管理器原理与实战:用with语句优雅管理资源

先问一个问题&#xff1a;你有没有在代码里写过这种“手动开关”的资源处理逻辑&#xff1f;f open("data.txt", "r") data f.read() f.close()再普通不过的对吧&#xff1f;我早年写Python的时候&#xff0c;这种代码写得特别多。直到有一次&#xff0c…

作者头像 李华
网站建设 2026/10/22 5:58:15

Java String进阶:常量池、不可变性与高频API实战避坑

Java字符串&#xff0c;谁没写过几行&#xff1f;可面试题里最刁钻的角落&#xff0c;往往也藏在String里。你也许被问过“new String("abc")到底创建了几个对象”&#xff0c;也许被split(".")坑到怀疑人生&#xff0c;或者在线上看到过莫名奇妙的内存飙升…

作者头像 李华
网站建设 2026/10/22 5:57:53

ModelArts实战避坑指南:训练部署全链路工程化落地

1. 为什么ModelArts不是“另一个云平台”&#xff0c;而是训练部署流程的“重力中心”我第一次在某高校实验室接触ModelArts&#xff0c;不是因为被宣传页上“一站式AI开发平台”打动&#xff0c;而是被一个真实问题逼到墙角&#xff1a;导师要求两周内把某图像分割模型跑通并生…

作者头像 李华
网站建设 2026/10/22 5:57:53

Graph Engineering:当关系成为可计算的第一公民

1. “Loop Engineering 已死&#xff1f;”——这个标题不是危言耸听&#xff0c;而是工程范式迁移的现场直播最近在几个技术社区刷到“Loop Engineering 已死&#xff1f;”这个标题&#xff0c;不少刚接触图计算或现代数据系统架构的开发者第一反应是&#xff1a;Loop 还没学…

作者头像 李华
网站建设 2026/10/22 5:57:20

MongoShake分片集群同步原理与高可用实战

1. 项目概述&#xff1a;为什么MongoShake是分片集群数据同步的“稳压器”在某高校实验室搭建的实时分析平台中&#xff0c;我们遇到一个典型但棘手的问题&#xff1a;上游业务系统使用的是3节点Shard2节点Config Server1节点Mongos构成的生产级MongoDB分片集群&#xff0c;数据…

作者头像 李华