平時其實很少會專門寫數據庫導出的事情。
這種活本身並不複雜,零零散散也做過很多次,大多數時候也不會留下什麼記錄。
這一次之所以單獨記下來,主要還是因為當時遇到了一些比較具體、也比較現實的限制條件:
我需要在比較短的時間裏接手一個並不熟悉的 MySQL 實例,把裏面的數據整理出來,而且這些數據最終並不是只給工程師看。
從一開始就意識到的一個問題
在動手之前,其實有一件事情我是比較明確的:
.sql 文件對工程師很友好,但對非技術人員幾乎沒有可用性。
對工程師來説:
-
.sql是最可靠的備份形式 - 可以恢復、可以校驗、可以長期保存
但換一個視角:
- 很多人甚至不知道怎麼打開
.sql - 就算打開了,也很難直接理解表結構
- 想篩選、查某一條記錄,幾乎是不可能的
也就是説,單純把數據庫備份下來,並不等於問題已經解決了。
後面遲早還是要把數據整理成一種“能被直接使用”的形式。
所以我當時心裏其實是把這件事拆成了兩步:
- 先保證數據完整地留下來
- 再考慮怎麼把數據變成別人也能看懂的樣子
先做一份完整的數據庫備份
基於這個判斷,我做的第一件事,還是先把整個 MySQL 實例完整備份下來。
這一步本身並不複雜,也談不上什麼技巧,只是對我來説,先有一份全量、可恢復的備份,會比較安心。後面無論怎麼處理數據,至少不會有“回不去”的問題。
為了省事,我寫了一個簡單的 shell 腳本,用來:
- 自動獲取所有業務數據庫
- 排除系統庫
- 逐個數據庫執行
mysqldump - 直接流式壓縮成
.sql.gz
腳本本身也只是把平時常用的命令整理了一下:
#!/usr/bin/env bash
## gunzip < app.sql.gz | mysql -u root -p
## nohup ./dump_all_dbs.sh host port root 'password' > 備份日誌.log 2>&1 &
set -e
HOST="$1"
PORT="$2"
USER="$3"
PASS="$4"
if [ $# -ne 4 ]; then
echo "Usage: $0 <host> <port> <user> <password>"
exit 1
fi
OUT_DIR="Mysql備份_$(date +%F_%H%M%S)"
mkdir -p "$OUT_DIR"
MYSQL="mysql -h${HOST} -P${PORT} -u${USER} -p${PASS} --batch --skip-column-names"
DUMP_BASE_OPTS="
--single-transaction
--routines
--events
--triggers
--hex-blob
--set-gtid-purged=OFF
--default-character-set=utf8mb4
"
echo "==> 正在從獲取數據庫列表 ${HOST}:${PORT}"
DATABASES=$($MYSQL -e "
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name NOT IN
('mysql','information_schema','performance_schema','sys');
")
if [ -z "$DATABASES" ]; then
echo "未找到數據庫!"
exit 0
fi
echo "==> 要轉儲的數據庫:"
echo "$DATABASES"
echo
for DB in $DATABASES; do
FILE="${OUT_DIR}/${DB}.sql.gz"
echo "==> 轉儲數據庫: ${DB}"
mysqldump \
-h${HOST} -P${PORT} -u${USER} -p${PASS} \
$DUMP_BASE_OPTS \
--databases "$DB" \
| gzip > "$FILE"
echo " -> 完成: $FILE"
done
echo
echo "所有數據庫均已成功轉儲."
echo "輸出目錄: ${OUT_DIR}"
做到這裏,其實“數據有沒有丟”這個問題就已經基本不用擔心了。
按需導出某一部分數據
接下來遇到的,是更偏實際使用層面的問題。
在整理數據的過程中,經常會有一些很具體的需求,比如:
- 只需要看某一張表
- 或者想先篩選一部分數據出來看看
這時候,如果只剩下一堆 .sql 文件,其實並不太好用。
所以我寫了一個很簡單的 PHP CLI 腳本,用來把一條 SQL 查詢的結果直接導出成 CSV。
這個腳本的目標也很單純:
- 能處理數據量比較大的表
- 不一次性把數據全部讀進內存
- 導出的文件可以直接用 Excel 打開
<?php
// 單文件 CLI:MySQL 導出 CSV
if ($argc < 2) {
echo <<<HELP
Usage:
php export.php <output_csv_path>
Example:
php export.php /data/output/users.csv
HELP;
exit(1);
}
$outputCsv = $argv[1];
// MySQL 配置
$dbConfig = [
'host' => '127.0.0.1',
'port' => 3306,
'dbname' => 'dbname',
'username' => 'root',
'password' => 'password',
'charset' => 'utf8mb4',
];
// SQL
$sql = <<<SQL
select * from bl_danmu_logs
SQL;
$dsn = sprintf(
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
$dbConfig['host'],
$dbConfig['port'],
$dbConfig['dbname'],
$dbConfig['charset']
);
try {
$pdo = new PDO(
$dsn,
$dbConfig['username'],
$dbConfig['password'],
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false,
]
);
} catch (PDOException $e) {
fwrite(STDERR, "數據庫連接失敗: {$e->getMessage()}\n");
exit(1);
}
$dir = dirname($outputCsv);
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
$fp = fopen($outputCsv, 'w');
if ($fp === false) {
fwrite(STDERR, "無法寫入 CSV 文件\n");
exit(1);
}
fwrite($fp, "\xEF\xBB\xBF");
$stmt = $pdo->prepare($sql);
$stmt->execute();
$rowCount = 0;
$headerWritten = false;
while ($row = $stmt->fetch()) {
if (!$headerWritten) {
fputcsv($fp, array_keys($row));
$headerWritten = true;
}
fputcsv($fp, array_values($row));
$rowCount++;
if ($rowCount % 100000 === 0) {
echo "已導出 {$rowCount} 行\n";
}
}
fclose($fp);
echo "導出完成,共 {$rowCount} 行\n";
這個腳本更多是用來應對一些臨時、零散的導出需求,本身也不復雜。
真正的難點在“交付”這一步
真正讓我花時間的,其實是後面這一部分。
如果只是從工程角度看,.sql 已經足夠完整;
但從使用角度看,這些數據仍然很難被直接消費。
問題包括:
- 表很多,一個一個手工導出不現實
- Excel 有行數限制,大表沒法一次性打開
- 字段名是英文或縮寫,不看錶結構根本不知道是什麼意思
所以後來我又寫了一個腳本,用來把整個數據庫的數據,整理成一組 CSV 文件。
這個腳本做的事情也很樸素:
- 遍歷數據庫中的所有表
- 讀取字段註釋,作為 CSV 的表頭
- 數據量大的表按行數自動拆分文件
- 所有文件都可以直接用 Excel 打開
這些邏輯都不復雜,只是把原本需要重複做的事情集中到了一起。
這段代碼略長,我把它放在了我的個人網站中:點擊查看
一點事後的感受
這次整理下來,我的感受其實挺明確的:
- 技術本身並不複雜
- 真正需要花心思的,是站在使用者的角度去看數據
對工程師來説,數據庫和 SQL 已經很直觀了;
但對不直接使用數據庫的人來説,Excel 才是他們真正熟悉的工具。
這套腳本對我而言,並不是什麼通用方案,只是當時在那個條件下,一種比較順手、也能把事情做完的辦法。
記錄下來,也只是給自己以後再遇到類似情況時,留一個參考。