1. 批量导入大CSV这件事,坑比想象中多
手里攒了七八个CSV文件,每个少则几百MB,多则两三个GB,要一股脑塞进MySQL里做分析。这场景做数据的人都不陌生——可能是日志归档、可能是业务方丢过来的一批导出数据、也可能是从几个不同系统里扒下来的对账明细。标题里说的“快速导入多个大CSV文件”,核心诉求其实就三个字:快、稳、不折腾。
但真上手你会发现,事情没那么简单。用Navicat或者DBeaver这类图形工具直接右键导入?小文件还行,上到GB级别基本就是卡死、超时、内存溢出三连。用LOAD DATA INFILE?速度确实快,但默认参数下遇到脏数据直接整批报错回滚,你还得从头再来。更别提多个文件要按顺序导、字段格式还各有差异的情况。
这篇文章面向的是需要批量处理大体积CSV入库的开发和运维同学,不管你用的是MySQL 5.7还是8.0,是本地单机还是远程实例,下面这套思路和参数配置都能直接拿去用。我会把方案选型的逻辑、关键参数的取舍、实操中踩过的坑全部摊开讲,尽量让你少走弯路。
先说结论性的判断:批量大CSV导入,首选LOAD DATA LOCAL INFILE,配合合理的批次切分和参数调优,再辅以预处理清洗。存储过程、Python脚本、命令行工具各有适用场景,但核心引擎都是这条路径。下面逐层拆解。
2. 方案选型:为什么不用图形工具和逐行INSERT
2.1 三种主流导入方式的性能对比
在动手之前,先把可选路径理清楚。导入CSV到MySQL,常见的有这么几条路:
| 导入方式 | 典型速度 | 适用数据量 | 主要问题 |
|---|---|---|---|
| 图形工具导入 | 几千行/秒 | < 50MB | 内存占用高,大文件易崩溃 |
| 逐行INSERT / 批量INSERT | 1-5万行/秒 | < 100MB | 网络往返多,SQL解析开销大 |
| LOAD DATA INFILE | 10-50万行/秒 | 无上限 | 参数配置敏感,错误处理需额外设计 |
| mysqlimport命令行 | 同LOAD DATA | 无上限 | 本质是LOAD DATA的封装 |
图形工具的问题在于它把整个文件读进内存再拼SQL,几个GB的文件直接把客户端撑爆。逐行INSERT的问题在于每条语句都要经过SQL解析器、优化器、执行器,网络往返也是开销,数据量一大就线性变慢。
LOAD DATA INFILE之所以快,是因为它绕过了SQL解析层,直接走存储引擎的批量写入通道。MySQL官方文档里明确说过,这是导入数据最快的方式,没有之一。实测下来,在SSD、合理参数配置下,每秒导入30-50万行是常态,比逐行INSERT快20倍以上。
2.2 LOCAL关键字:权限与安全的权衡
LOAD DATA INFILE有两个变体:带LOCAL和不带LOCAL。区别在于文件从哪读:
- 不带LOCAL:文件必须在MySQL服务器所在主机上,由服务器进程直接读取。速度最快,但需要
FILE权限,且文件路径受secure_file_priv限制。 - 带LOCAL:文件在客户端主机上,由客户端读取后通过网络传给服务器。速度略慢,但不需要
FILE权限,适合远程导入。
注意:MySQL 8.0默认
local_infile是关闭的,需要显式开启。用SHOW VARIABLES LIKE 'local_infile';查看,用SET GLOBAL local_infile = 1;开启。客户端连接时也要加--local-infile=1参数。
如果你的文件已经在服务器上,优先用不带LOCAL的方式,省去网络传输。如果是远程导入,LOCAL方式更现实。我下面主要按LOCAL方式讲,因为大多数人的场景是本地文件往远程库导。
2.3 为什么不用存储过程或Python逐行读
有人会想,写个存储过程循环读文件,或者用Python的pandas读CSV再to_sql。这两条路我都试过,结论是:小数据量可以,大数据量别碰。
存储过程读文件受限于LOAD_FILE函数,它一次只能读一个文件且返回字符串,大文件直接超max_allowed_packet。Python的to_sql底层是批量INSERT,虽然比逐行快,但和LOAD DATA比还是差一个数量级。pandas读大CSV本身也吃内存,几个GB的文件读进来再写出去,机器内存先扛不住。
所以方案定了:LOAD DATA LOCAL INFILE为主,辅以文件预处理和批次控制。下面进入实操细节。
3. 核心参数拆解:每个选项背后的取舍逻辑
3.1 字段分隔符与行终止符的坑
CSV文件看着简单,实际格式五花八门。标准CSV用逗号分隔、换行结尾,但实际拿到的文件可能是:
- 制表符分隔(TSV)
- 分号分隔(欧洲格式)
- 字段里有逗号,用引号包裹
- Windows换行
\r\n和Unix换行\n混用 - 文件末尾有多余空行
LOAD DATA的默认分隔符是制表符\t,不是逗号。所以导入CSV必须显式指定FIELDS TERMINATED BY ','。如果字段里有引号包裹的逗号,还要加OPTIONALLY ENCLOSED BY '"'。
行终止符默认是\n,但Windows导出的文件是\r\n。如果不处理,最后一个字段会多一个\r字符,导致数据看起来正常但比较时对不上。解决办法是加LINES TERMINATED BY '\r\n',或者预处理时统一转成\n。
LOAD DATA LOCAL INFILE '/path/to/data.csv' INTO TABLE target_table FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES;IGNORE 1 LINES是跳过表头。如果文件没有表头,这行去掉。
3.2 字符集设置:乱码的根源
字符集不匹配是导入后中文变问号或乱码的头号原因。MySQL表有自己的字符集(通常是utf8mb4),CSV文件也有自己的编码(可能是utf8、gbk、latin1)。
LOAD DATA有个CHARACTER SET子句,用来指定文件的字符集。如果不指定,MySQL会按character_set_database来解析文件,很容易出问题。
LOAD DATA LOCAL INFILE '/path/to/data.csv' INTO TABLE target_table CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' ...实操心得:导入前先用
file -i data.csv看文件编码,或者用文本编辑器确认。如果是GBK编码的文件,指定CHARACTER SET gbk,让MySQL帮你转成表的字符集。千万别不指定,默认行为在不同版本下不一致。
3.3 错误处理:IGNORE还是直接失败
默认情况下,LOAD DATA遇到一行错误就整批回滚,前面导的全白费。对于脏数据多的文件,这简直是灾难。
两个应对策略:
IGNORE n ROWS:跳过前n行,适合跳过表头和已知的垃圾行。IGNORE n LINES:跳过n行错误继续,但要注意它跳过的是错误行数,不是行号。
更稳妥的做法是先把错误行导到一张临时表,用LOAD DATA的IGNORE配合SHOW WARNINGS查看具体错误。或者用mysqlimport的--force参数强制继续。
LOAD DATA LOCAL INFILE '/path/to/data.csv' IGNORE INTO TABLE target_table ...IGNORE关键字放在INTO前面,表示遇到错误行跳过而不是终止。但要注意,跳过的行数没有上限,可能悄悄丢掉大量数据。建议导入后核对行数。
3.4 事务与批次:大文件切分的必要性
一个3GB的CSV,几千万行,一次性LOAD DATA会有什么问题?
- 事务日志暴涨,可能撑爆磁盘
- 锁持有时间长,影响其他查询
- 一旦失败,回滚时间极长
- 主从复制延迟严重
所以大文件必须切分。切分方式有两种:按行数切(比如每100万行一个文件),或按文件大小切(比如每500MB一个)。Linux下用split命令最方便:
split -l 1000000 bigfile.csv part_这会生成part_aa、part_ab等文件,每个100万行。然后循环导入。
for f in part_*; do mysql -u user -p --local-infile=1 dbname -e " LOAD DATA LOCAL INFILE '$f' INTO TABLE target_table FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n' IGNORE 1 LINES;" done每个文件一个独立事务,失败只影响当前批次,重试成本低。
3.5 索引与约束:导入前该关的关掉
LOAD DATA导入时,每插一行都要维护索引、检查外键、触发唯一性约束。这些开销在批量导入时是巨大的。
标准做法是:
- 导入前禁用索引:
ALTER TABLE target_table DISABLE KEYS; - 导入前关闭唯一性检查:
SET unique_checks = 0; - 导入前关闭外键检查:
SET foreign_key_checks = 0; - 导入完成后重建索引:
ALTER TABLE target_table ENABLE KEYS;
注意:
DISABLE KEYS只对MyISAM有效,InnoDB不支持。InnoDB的优化方式是先删掉非唯一索引,导入完再重建。唯一索引和主键没法关,只能靠数据本身干净。
对于InnoDB,更实际的优化是调整innodb_buffer_pool_size和innodb_log_file_size,让写入更顺畅。导入期间把innodb_flush_log_at_trx_commit设为2或0,能显著提升速度,但会牺牲部分持久性——导入完记得改回来。
4. 完整实操流程:从文件准备到验证入库
4.1 环境检查与参数确认
动手前先确认几件事:
-- 查看local_infile是否开启 SHOW VARIABLES LIKE 'local_infile'; -- 查看最大包大小 SHOW VARIABLES LIKE 'max_allowed_packet'; -- 查看当前字符集 SHOW VARIABLES LIKE 'character_set%';max_allowed_packet默认可能是4MB或64MB,如果单行数据特别大(比如有长文本字段),需要调大。SET GLOBAL max_allowed_packet = 268435456;(256MB)。
客户端连接时:
mysql -u username -p --local-infile=1 -h host -P 3306 dbname--local-infile=1必须加,否则LOAD DATA LOCAL会报错。
4.2 文件预处理:清洗与格式统一
原始CSV往往需要预处理。常见操作:
- 统一换行符:
dos2unix *.csv - 去掉BOM头:
sed -i '1s/^\xEF\xBB\xBF//' file.csv - 替换字段内换行:如果字段里有换行符,会破坏行结构,需要先处理
- 检查列数一致性:
awk -F',' '{print NF}' file.csv | sort -u
列数不一致是最常见的导入失败原因。用上面这条命令能快速看出哪些行的列数不对。
# 找出列数不等于预期值的行 awk -F',' 'NF != 10 {print NR": "NF" columns"}' file.csv | head -204.3 建表与字段映射
导入前要确保表结构匹配。字段顺序、类型、是否允许NULL都要对上。如果CSV列顺序和表不一致,可以用LOAD DATA的列映射功能:
LOAD DATA LOCAL INFILE '/path/to/data.csv' INTO TABLE target_table FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n' IGNORE 1 LINES (col1, col3, @dummy, col2) SET col4 = NOW();这里@dummy跳过CSV中的某一列,SET子句可以给字段赋默认值或做转换。
实操心得:建表时把所有字段先设为
VARCHAR或允许NULL,导入成功后再改类型。这样能避免因类型不匹配导致的整行失败。数据清洗完再收紧约束。
4.4 分批导入脚本
把上面的要点整合成一个可复用的脚本:
#!/bin/bash DB="your_db" TABLE="target_table" USER="your_user" PASS="your_pass" HOST="your_host" DIR="/path/to/csv_parts" for f in "$DIR"/part_*; do echo "Importing $f ..." mysql -u"$USER" -p"$PASS" -h"$HOST" --local-infile=1 "$DB" -e " SET unique_checks=0; SET foreign_key_checks=0; LOAD DATA LOCAL INFILE '$f' INTO TABLE $TABLE CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' LINES TERMINATED BY '\n' IGNORE 1 LINES IGNORE; SET unique_checks=1; SET foreign_key_checks=1;" if [ $? -ne 0 ]; then echo "Failed on $f, stopping." exit 1 fi done echo "All done."这个脚本做了几件事:每个文件独立事务、关闭唯一性和外键检查、遇到错误行跳过、失败即停便于排查。
4.5 导入后验证
导入完必须核对:
-- 总行数 SELECT COUNT(*) FROM target_table; -- 抽样检查 SELECT * FROM target_table LIMIT 10; -- 检查是否有异常值 SELECT COUNT(*) FROM target_table WHERE key_column IS NULL;如果行数和CSV总行数对不上,用SHOW WARNINGS看跳过了多少行。也可以在导入时把错误行记到日志表:
LOAD DATA LOCAL INFILE '/path/to/data.csv' INTO TABLE target_table FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n' IGNORE 1 LINES IGNORE 1000 LINES;IGNORE 1000 LINES表示最多跳过1000行错误,超过就终止。这个数字根据数据质量调整。
5. 常见问题与排查技巧实录
5.1 报错“The MySQL server is running with the --secure-file-priv option”
这是不带LOCAL时最常见的问题。secure_file_priv限制了LOAD DATA INFILE能读的目录。查看当前值:
SHOW VARIABLES LIKE 'secure_file_priv';如果返回一个具体路径,文件必须放在那个目录下。如果返回NULL,表示禁止使用LOAD DATA INFILE(不带LOCAL)。解决办法是用LOCAL方式,或者修改配置文件里的secure_file_priv(需要重启)。
5.2 中文乱码
乱码的排查顺序:
- 确认CSV文件编码:
file -i data.csv - 确认表字符集:
SHOW CREATE TABLE target_table; - 导入时指定
CHARACTER SET匹配文件编码 - 连接字符集也要对:
SET NAMES utf8mb4;
如果文件是GBK,表是utf8mb4,导入时指定CHARACTER SET gbk,MySQL会自动转换。如果指定错了,比如文件是utf8却指定gbk,就会产生乱码。
5.3 导入速度慢
速度慢的原因和对应优化:
| 现象 | 可能原因 | 优化方向 |
|---|---|---|
| 每秒几千行 | 逐行INSERT | 改用LOAD DATA |
| 每秒几万行 | 索引维护开销 | 导入前删非唯一索引 |
| 速度波动大 | 磁盘IO瓶颈 | 用SSD,调整innodb_flush |
| 远程导入慢 | 网络带宽 | 压缩文件,或先传到服务器 |
innodb_flush_log_at_trx_commit设为0或2,sync_binlog设为0,能大幅提升写入速度。但这些都是用持久性换性能,导入完必须改回默认值(1)。
5.4 字段截断或类型错误
报错“Data truncated for column”或“Incorrect integer value”,说明CSV里的值和表字段类型不匹配。常见情况:
- 数字字段里有空字符串
- 日期格式不匹配(
2024/01/01vs2024-01-01) - 字符串超长
解决办法是用SET子句做转换:
LOAD DATA LOCAL INFILE '/path/to/data.csv' INTO TABLE target_table FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n' IGNORE 1 LINES (@date_str, @num_str, col3) SET date_col = STR_TO_DATE(@date_str, '%Y/%m/%d'), num_col = NULLIF(@num_str, '');NULLIF把空字符串转成NULL,STR_TO_DATE做日期格式转换。
5.5 导入后行数对不上
行数对不上的原因:
- 跳过了错误行(用
IGNORE时) - 文件末尾有空行被跳过
- 字段内有换行符导致行被拆分
- 主键冲突导致行被忽略
排查方法:先用wc -l file.csv看文件行数,再和表行数对比。如果差得多,检查字段内是否有未转义的换行符。用awk统计每行列数,找出异常行。
实操心得:导入前先用
head -1000 file.csv | mysql ...试导一小批,确认格式和参数没问题再全量导。这个习惯能省下大量返工时间。
6. 进阶技巧:让导入再快一点
6.1 用mysqlimport替代手写SQL
mysqlimport是MySQL自带的命令行工具,本质是LOAD DATA的封装,但用起来更简洁:
mysqlimport --local --user=user --password=pass \ --fields-terminated-by=',' \ --lines-terminated-by='\n' \ --ignore-lines=1 \ --columns='col1,col2,col3' \ dbname /path/to/data.csv注意文件名必须和表名一致(去掉扩展名)。比如target_table.csv对应target_table表。这个限制有时候不太方便,但胜在命令简洁,适合脚本化。
6.2 并行导入的可行性
多个文件能不能并行导入?可以,但有前提:
- 表没有自增主键竞争(或者用不同起始值)
- 没有唯一索引冲突
- 服务器IO和CPU有余量
并行导入能利用多核和磁盘带宽,但会增加锁竞争。建议并行度控制在2-4,再多收益递减。用xargs -P可以简单实现:
ls part_* | xargs -P 4 -I {} sh -c 'mysql ... -e "LOAD DATA LOCAL INFILE \"{}\" ..."'6.3 导入到临时表再合并
如果目标表有大量索引和约束,可以先导入到一张无索引的临时表,再INSERT INTO ... SELECT合并。这样导入阶段最快,合并阶段虽然慢但只做一次。
CREATE TABLE tmp_table LIKE target_table; ALTER TABLE tmp_table DROP INDEX ...; -- 删掉非唯一索引 -- 导入到tmp_table -- 合并 INSERT INTO target_table SELECT * FROM tmp_table; DROP TABLE tmp_table;这个方案适合对导入时间敏感、可以接受合并阶段耗时的场景。
6.4 监控导入进度
LOAD DATA执行期间没有进度条,只能靠间接指标:
-- 查看当前进程状态 SHOW PROCESSLIST; -- 查看InnoDB状态 SHOW ENGINE INNODB STATUS\G或者用pv命令监控文件读取进度:
pv data.csv | mysql ... -e "LOAD DATA LOCAL INFILE '/dev/stdin' ..."不过/dev/stdin方式对LOCAL支持有限,更实际的是看表行数增长:
watch -n 5 'mysql -e "SELECT COUNT(*) FROM target_table"'7. 参数配置速查与经验总结
把关键参数和推荐值整理成表,方便对照:
| 参数 | 默认值 | 导入时建议 | 说明 |
|---|---|---|---|
| local_infile | OFF | ON | 必须开启才能用LOCAL |
| max_allowed_packet | 64M | 256M | 大字段需要调大 |
| innodb_flush_log_at_trx_commit | 1 | 2或0 | 提升写入速度,牺牲持久性 |
| sync_binlog | 1 | 0 | 减少binlog同步开销 |
| unique_checks | 1 | 0 | 关闭唯一性检查 |
| foreign_key_checks | 1 | 0 | 关闭外键检查 |
| autocommit | 1 | 0 | 批量提交减少事务开销 |
导入完成后,这些参数都要改回默认值。特别是innodb_flush_log_at_trx_commit和sync_binlog,不改回来等于把数据库的持久性保障关了。
我个人在实际操作中的体会是,批量大CSV导入这件事,八成的坑在文件预处理阶段。格式不统一、编码混乱、字段内有换行、列数不一致,这些问题在导入前解决掉,导入过程本身其实很顺畅。剩下两成的坑在参数配置,尤其是字符集和错误处理,配错了要么乱码要么丢数据。
最后分享一个小技巧:如果经常要做这类导入,把预处理和导入逻辑写成一个脚本,参数化文件路径和表名。下次再来一批文件,改几个变量就能跑,比每次手动敲命令靠谱得多。脚本里加上行数校验和错误日志,出问题能快速定位,不用从头再导一遍。