作为一名常年和数据库打交道的开发,我在做数据脱敏导出时,踩过无数坑:UNION ALL 表头被覆盖、INTO OUTFILE 权限报错、脱敏规则处理不当、时间格式不直观......这篇博客就把我踩过的坑、最终成型的方案一次性整理出来,保证你照着就能用。
一、问题背景与核心需求
我当时的场景是:需要从 TiDB 数据库导出一条数据,要求:
-
导出为标准 CSV 格式,逗号分隔,可直接用 Excel 打开;
-
数据需要脱敏:手机号/身份证这类字段,按「首1位 + **** + 尾1位」处理;长度为2的字段,显示「首1位 + *」;长度为1的字段直接保留;
-
时间字段(DATETIME(3))要转换成直观的 yyyy-MM-dd HH:mm:ss.SSS 格式;
-
CSV 要带中文表头,且表头不会被数据覆盖;
-
全程尽量用原生命令,避免依赖复杂工具。
二、踩过的坑(避坑指南)
坑1:UNION ALL + INTO OUTFILE 表头被覆盖
很多人会想当然用 SELECT '表头' UNION ALL SELECT 数据 INTO OUTFILE,但 TiDB/MySQL 里,INTO OUTFILE 只会捕获最后一条 SELECT 的输出,前面的表头直接被忽略,导出后只有数据,没有表头。
❌ 错误示例:
SELECT 'ID,名称,手机号,创建时间' FROM dual
UNION ALL
SELECT CONCAT_WS(',', ID, NAME, phone, create_time)
FROM user_info LIMIT 1
INTO OUTFILE '/tmp/data.csv'
FIELDS TERMINATED BY ',';
导出结果:只有数据行,表头消失不见。
坑2:mysql 命令行导出时,-N 参数误删手动表头
-N 参数的作用是屏蔽 SQL 查询自带的字段名,但很多人误以为它会删掉文件里的手动表头。其实 -N 只对数据库返回的字段名生效,不会影响你用 echo 写入的表头,之前的误解是因为用了 > 覆盖文件。
坑3:> 重定向覆盖文件,表头被清空
这是最常见的错误:先 echo "表头" > 文件,再用 mysql -e "查询" > 文件,第二个 > 会直接清空文件内容,覆盖掉前面的表头,导致文件里只剩数据。
坑4:脱敏规则处理不当,特殊长度字段出错
比如手机号长度不固定,或身份证号有15/18位两种,没考虑长度=1、=2的场景,导致脱敏后变成空值或乱码。
坑5:DATETIME(3) 导出格式不直观
直接导出 DATETIME(3) 字段,在 CSV 里可能显示为一串数字或不标准的时间格式,Excel 无法直接识别。
三、最终成型方案(零坑版)
方案核心思路
-
表头单独写入:用 echo "表头" > 文件 先创建并写入表头;
-
数据追加写入:用 >> 把数据追加到文件末尾,避免覆盖表头;
-
SQL 里完成脱敏+时间格式化:不用额外工具,SQL 里直接处理,导出就是最终格式;
-
mysql -N 屏蔽自带字段名:只屏蔽数据库返回的字段名,不影响手动写入的表头。
完整示例(直接复制就能用)
- 建表&插入测试数据
先创建一张测试表,模拟真实业务场景:
CREATE TABLE user_info(
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
phone VARCHAR(20),
create_time DATETIME(3)
);
INSERT INTO user_info(name, phone, create_time)
VALUES
('张三', '13900112233', NOW(3)), -- 正常手机号
('李四', '58', NOW(3)), -- 两位数字
('王五', '9', NOW(3)); -- 一位数字
- 一键导出带表头、脱敏、格式化时间的 CSV
1. 写入中文表头(覆盖创建文件)
echo "ID,名称,脱敏手机号,创建时间" > /home/amm/user_info.csv
2. 追加脱敏数据(用 >> 追加,不会覆盖表头)
mysql -upactadm -h127.0.0.1 -P4000 -p pact -N \
-e "
SELECT CONCAT_WS(',',
id,
name,
-- 脱敏规则:首1位 + **** + 尾1位;长度=2则首1位+*;长度=1则保留
CASE
WHEN LENGTH(phone) >= 3 THEN CONCAT(SUBSTR(phone, 1, 1), '****', SUBSTR(phone, -1))
WHEN LENGTH(phone) = 2 THEN CONCAT(SUBSTR(phone, 1, 1), '*')
ELSE phone
END AS masked_phone,
-- 时间格式化:yyyy-MM-dd HH:mm:ss.SSS
DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s.%3f') AS fmt_create_time
) FROM user_info LIMIT 1;
" >> /home/amm/user_info.csv
- 验证导出结果
cat /home/amm/user_info.csv
输出结果(标准 CSV 格式,可直接用 Excel 打开):
ID,名称,脱敏手机号,创建时间
1,张三,1****3,2026-05-16 15:30:45.123
四、方案拆解与细节说明
- 表头写入:echo "表头" > 文件
• > 是覆盖模式,第一次写入时用它,创建文件并写入表头;
• 表头里的中文可以直接写,CSV 会自动兼容;
• 表头字段顺序要和后面 SQL 里的字段顺序保持一致,避免错位。
- 数据追加:mysql -N -e "查询" >> 文件
• -N:屏蔽 SQL 查询自带的字段名,避免文件里出现重复表头;
• >>:追加模式,数据会写入文件末尾,不会覆盖前面的表头;
• CONCAT_WS(',', 字段1, 字段2, ...):把多个字段用逗号拼接成一行,生成标准 CSV 数据;
• 所有脱敏、格式化操作都在 SQL 里完成,导出后无需额外处理。
- 脱敏规则详解
CASE
WHEN LENGTH(phone) >= 3 THEN CONCAT(SUBSTR(phone, 1, 1), '****', SUBSTR(phone, -1))
WHEN LENGTH(phone) = 2 THEN CONCAT(SUBSTR(phone, 1, 1), '*')
ELSE phone
END
• 长度≥3:取首1位,拼接4个星号,再拼接尾1位,比如 13900112233 → 1****3;
• 长度=2:取首1位,拼接1个星号,比如 58 → 5*;
• 长度=1:直接保留原值,比如 9 → 9;
• 适配手机号、身份证号、银行卡号等多种场景,不用修改规则。
- 时间格式化:DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s.%3f')
• %Y-%m-%d:年-月-日;
• %H:%i:%s:时:分:秒(24小时制);
• %3f:保留3位微秒,对应 DATETIME(3) 的毫秒级精度;
• 导出后时间格式直观,Excel 可以直接识别为日期类型。
五、进阶优化:批量导出多条数据
如果需要导出多条数据,只要把 LIMIT 1 改成你需要的数量即可,其他部分不用修改:
echo "ID,名称,脱敏手机号,创建时间" > /home/amm/user_info_batch.csv
mysql -upactadm -h127.0.0.1 -P4000 -p pact -N \
-e "
SELECT CONCAT_WS(',',
id,
name,
CASE
WHEN LENGTH(phone) >= 3 THEN CONCAT(SUBSTR(phone, 1, 1), '****', SUBSTR(phone, -1))
WHEN LENGTH(phone) = 2 THEN CONCAT(SUBSTR(phone, 1, 1), '*')
ELSE phone
END AS masked_phone,
DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s.%3f') AS fmt_create_time
) FROM user_info LIMIT 10;
" >> /home/amm/user_info_batch.csv
六、总结
这套方案解决了我之前遇到的所有坑,核心就是:
• 表头和数据分开写入,用 > 和 >> 区分覆盖/追加模式;
• SQL 里直接完成脱敏和时间格式化,导出就是最终可用的 CSV;
• 全程用原生命令,不依赖额外工具,适配所有 TiDB/MySQL 环境。
如果你也在做类似的数据脱敏导出,直接照着这个模板改表名和字段就能用,再也不用踩我踩过的这些坑啦!