原创

MySQL 导出逗号分隔文件:空值、转义与无损导回演练

导出的文件能被表格软件打开,不代表它可以无损导回数据库。空字符串与 SQL NULL 可能已经合并,正文里的换行可能被当成下一条记录,反斜线也可能在第二次解析时消失。行数看着一样,数据仍可能变了。

这里做一次 MySQL 8.4 的往返演练:准备包含逗号、引号、换行、反斜线、空值和零字节的数据,用 INTO OUTFILE 导出,再用同一套字段协议导入隔离表,最后按值核对。文件采用逗号分隔,但保留 MySQL 的反斜线转义规则;它适合两个 MySQL 操作之间交换数据,不能直接冒充通用 CSV 协议。

先确认文件究竟落在哪台机器

SELECT ... INTO OUTFILE 由数据库服务器创建文件。客户端连接地址是远程主机时,下面的 /var/lib/mysql-files 也在远程服务器上,不能在自己电脑的同名目录寻找结果。

在专用实验连接里先执行:

SELECT VERSION(), CURRENT_USER(), @@GLOBAL.secure_file_priv;
SHOW GRANTS;

需要表的 SELECT 权限和 FILE 权限,还需要 mysqld 进程有目录写入权限。secure_file_priv 为指定目录时,只能在允许的目录内操作;为 SQL NULL 时,这条服务器文件导出路线被禁用。它不是给当前会话随手改一个路径的动态参数。

后文假设服务器允许 /var/lib/mysql-files/,并且文件 csv34-roundtrip.csv 尚不存在。实际环境应按查询结果替换两条文件语句中的绝对路径。不要为了复现而把整个导出限制清空;没有这项权限时,改用应用程序读取结果集并写客户端文件更合适。

为了让示例 SQL 中的转义含义明确,使用独立连接保存旧模式,再设定本次演练模式。不要在正在承接业务的连接上运行这一段。

SET @saved_mode = @@SESSION.sql_mode;
SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION';
CREATE DATABASE IF NOT EXISTS csv34_lab
  CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE csv34_lab;

CREATE TABLE export_item (
    id BIGINT UNSIGNED NOT NULL,
    label VARCHAR(40) NOT NULL,
    note VARCHAR(256) NULL,
    amount DECIMAL(10,2) NULL,
    PRIMARY KEY (id)
) ENGINE=InnoDB;

INSERT INTO export_item (id,label,note,amount) VALUES
(1,'ordinary','中文说明',12.30),
(2,'punctuation','逗号,以及"引号"',0.00),
(3,'multiline',CONCAT('第一行',CHAR(10),'第二行'),-4.50),
(4,'backslash',CONCAT('path',CHAR(92),'part; literal ',CHAR(92),'N'),8.00),
(5,'null-value',NULL,NULL),
(6,'empty-value','',1.00),
(7,'null-word','NULL',2.00),
(8,'zero-byte',CONCAT('before',CHAR(0),'after'),3.00);

CREATE TABLE imported_item LIKE export_item;
SELECT id, label, note IS NULL AS is_null, HEX(note), amount
FROM export_item ORDER BY id;

HEX(note) 的作用很具体:第三行应包含 0A,第四行的反斜线是 5C,第八行包含 00。空字符串的十六进制表示为空,SQL NULL 的结果则仍是 NULL。这样在导出之前,就已经有了可以逐项比较的原始值。

导出端和导入端共用一份协议

下面明确固定编码、列顺序、分隔符、引用字符、转义字符和记录终止符,不依赖默认选项。

SELECT id, label, note, amount
FROM export_item
ORDER BY id
INTO OUTFILE '/var/lib/mysql-files/csv34-roundtrip.csv'
CHARACTER SET utf8mb4
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
ESCAPED BY '\\'
LINES TERMINATED BY '\n';

ENCLOSED BY 没写 OPTIONALLY,使普通字段都使用一致的引用形式。转义字符保留为反斜线,导回时也必须使用相同设置。SQL NULL 通过该协议的空值表示传递,字面字符串 NULL、空字符串和包含反斜线的 \N 不能擅自做全局替换。

导出文件没有额外表头。第一行就是数据,后面的导入语句也不跳过第一行。若为方便人工阅读加了列名,却忘记同步 IGNORE 1 LINES,表头可能成为错误记录;反过来,对没有表头的文件跳一行会直接少一条数据。

在服务器上可以检查文件大小和原始字节。运行命令的位置应当能读取刚才的服务器文件:

stat /var/lib/mysql-files/csv34-roundtrip.csv
od -An -tx1 -N 160 /var/lib/mysql-files/csv34-roundtrip.csv
sha256sum /var/lib/mysql-files/csv34-roundtrip.csv

SHA-256 用于确认传输前后是不是同一个文件,并不能证明导入结果相同。不要把 wc -l 当成这份文件的业务行数校验:被引用字段可以含有换行,物理行与逻辑记录不是同一个计数。真正的行数检查仍要落到解析后的记录。

导出返回错误或中途断开时,不要仅凭路径存在就把文件交给导入端:文件可能不完整。先核对语句结果与文件状态,失败产物交由文件所有者清理或隔离,再用新文件名重做。导出文件不是能随 SQL ROLLBACK 自动撤回的表记录。

同一路径再执行一次导出会失败,因为 OUTFILE 不覆盖已有文件。这是有意保留的保护。需要重跑时使用新文件名,并同步修改导入路径;清理旧文件应由文件所有者在操作系统中完成,不要给普通业务连接增加删除服务器文件的能力。

导入完成后,先查警告,再查数据

这里不加 LOCAL,由服务器读取同一份文件。它既避免客户端文件位置混淆,也让这次实验只验证一套服务器文件往返流程。

LOAD DATA INFILE '/var/lib/mysql-files/csv34-roundtrip.csv'
INTO TABLE imported_item
CHARACTER SET utf8mb4
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
ESCAPED BY '\\'
LINES TERMINATED BY '\n'
(id, label, note, amount);

SHOW WARNINGS;
SELECT COUNT(*) AS imported_rows FROM imported_item;
SELECT id, label, note IS NULL AS is_null, HEX(note), amount
FROM imported_item ORDER BY id;

应有八条记录。一次干净的往返不应该产生截断、列数不足或数值转换警告。SHOW WARNINGS 紧跟导入执行,避免后续语句改变诊断上下文。即使客户端显示受影响行数正确,也不能略过警告检查。

真正比较内容时,字符串要按字节核对。表的默认排序规则可以认为某些大小写或重音形式相等,不适合充当导入完整性的标准。空值则使用 NULL 安全等号 <=>,因为普通 = 遇到 NULL 不会得到真值。

SELECT 'changed_or_missing' AS problem, s.id
FROM export_item AS s
LEFT JOIN imported_item AS t ON t.id=s.id
WHERE t.id IS NULL
   OR NOT (CAST(s.label AS BINARY) <=> CAST(t.label AS BINARY))
   OR NOT (CAST(s.note AS BINARY) <=> CAST(t.note AS BINARY))
   OR NOT (s.amount <=> t.amount)
UNION ALL
SELECT 'unexpected_row', t.id
FROM imported_item AS t
LEFT JOIN export_item AS s ON s.id=t.id
WHERE s.id IS NULL;

这条查询应返回空集。第一部分找缺失或内容变化,第二部分找多出的记录,只做单向 JOIN 会漏掉后一类问题。这里金额比较的是 DECIMAL 值,不要求文件中的十进制文本保留某一种展示方式;文本字段则要求字节一致。

故意破坏一次,让校验确实能报警

检查器写出来以后,最好证明它能发现问题。只在导入隔离表修改一条记录,再重跑前面的差异查询:

UPDATE imported_item SET note='' WHERE id=5;

结果应指出第五条记录变化。源值是 SQL NULL,目标是空字符串,它们在不少界面上都显示成空白,SQL 校验仍能分辨。随后恢复:

UPDATE imported_item AS t
JOIN export_item AS s ON s.id=t.id
SET t.note=s.note
WHERE t.id=5;

再运行差异查询,应重新得到空集。继续测试多行和特殊字符时,不要用文本编辑器“整理一下文件”,编辑器可能转换换行、编码或零字节,最终验证的是被编辑后的另一个文件。

同一个文件名后缀,可能是不同的数据协议

如果下游是 Python 的默认 CSV 阅读器、电子表格或另一种数据库,就不能据此假定它理解 MySQL 反斜线转义、SQL NULL 标记和零字节。通用交换格式需要另行约定空值表示,并选择双方一致的引用规则。面向电子表格还要考虑以 = 等字符开头的文本如何展示,那属于消费端解释问题,不应通过修改源数据库值来补救。

导出查询也决定了一次数据快照的范围。本例只有八条静态数据;真实业务若在多个独立 SELECT 中分页导出,期间发生更新,就需要再设计一致性快照或稳定的批次边界。文件转义正确,不能替代跨查询的一致性。

本次实验结束后恢复连接模式,确认文件已经按需要保留或由服务器文件所有者清理,再删除专用实验库:

SET SESSION sql_mode = @saved_mode;
DROP DATABASE csv34_lab;
正文到此结束
评论插件初始化中...
Loading...
本文目录