博客 / 詳情

返回

一次受限環境下的 MySQL 數據導出與“可交付化”實踐

平時其實很少會專門寫數據庫導出的事情。

這種活本身並不複雜,零零散散也做過很多次,大多數時候也不會留下什麼記錄。

這一次之所以單獨記下來,主要還是因為當時遇到了一些​比較具體、也比較現實的限制條件

我需要在比較短的時間裏接手一個並不熟悉的 MySQL 實例,把裏面的數據整理出來,而且這些數據最終並不是只給工程師看。


從一開始就意識到的一個問題

在動手之前,其實有一件事情我是比較明確的:

.sql文件對工程師很友好,但對非技術人員幾乎沒有可用性。

對工程師來説:

  • .sql 是最可靠的備份形式
  • 可以恢復、可以校驗、可以長期保存

但換一個視角:

  • 很多人甚至不知道怎麼打開 .sql
  • 就算打開了,也很難直接理解表結構
  • 想篩選、查某一條記錄,幾乎是不可能的

也就是説,​單純把數據庫備份下來,並不等於問題已經解決了

後面遲早還是要把數據整理成一種“能被直接使用”的形式。

所以我當時心裏其實是把這件事拆成了兩步:

  1. 先保證數據完整地留下來
  2. 再考慮怎麼把數據變成別人也能看懂的樣子

先做一份完整的數據庫備份

基於這個判斷,我做的第一件事,還是先把整個 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 才是他們真正熟悉的工具

這套腳本對我而言,並不是什麼通用方案,只是當時在那個條件下,一種比較順手、也能把事情做完的辦法。

記錄下來,也只是給自己以後再遇到類似情況時,留一個參考。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.