| 方案 | 速度 | 記憶體 | 功能豐富度 | 推薦場景 |
|---|---|---|---|---|
| MySQL LOAD DATA INFILE | ★★★★★ (最快,引擎層直寫) | ★★★★★ (無 PHP 記憶體) | ★★☆☆☆ (難處理複雜邏輯) | 純匯入、結構固定、大檔案 |
| PHP + PDO 批次 Prepared Statement | ★★★★☆ | ★★★★☆ (Generator 串流) | ★★★★★ (完整業務邏輯) | 需驗證、轉換、關聯寫入 |
| League\Csv + 批次 Upsert | ★★★★☆ | ★★★★☆ | ★★★★★ | 現代專案標準 |
| ETL 工具 | ★★★★★ | ★★★★★ | ★★★★★ | 企業級、跨系統 |
<?php
// 前置:確保 MySQL 啟用 local_infile=1,PHP PDO 設定 MYSQL_ATTR_LOCAL_INFILE => true
$pdo = new PDO('mysql:host=127.0.0.1;dbname=test', 'user', 'pass', [
PDO::MYSQL_ATTR_LOCAL_INFILE => true,
]);
$csvPath = '/absolute/path/to/data.csv'; // 必須絕對路徑
$sql = <<prepare($sql);
$stmt->bindParam(':file', $csvPath, PDO::PARAM_STR);
$stmt->execute();
echo "Imported: {$stmt->rowCount()} rows\n";
關鍵參數:CHARACTER SET 指定檔案編碼、FIELDS/LINES TERMINATED BY 對應 CSV 格式、IGNORE 1 LINES 跳過標題、SET 子句處理預設值/轉換。
<?php
function importCsvWithPdo(string $csvPath, PDO $pdo, int $batchSize = 1000): int {
$reader = new SplFileObject($csvPath, 'r');
$reader->setFlags(SplFileObject::READ_CSV | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);
$reader->setCsvControl(',', '"', '\\');
$header = null;
$batch = [];
$total = 0;
$pdo->beginTransaction();
try {
foreach ($reader as $row) {
if ($header === null) {
$header = $row;
continue;
}
$data = array_combine($header, $row);
// 業務驗證/轉換
$data['email'] = strtolower(trim($data['email']));
$data['created_at'] = $data['created_at'] ?? date('Y-m-d H:i:s');
$batch[] = $data;
if (count($batch) >= $batchSize) {
$total += insertBatch($pdo, $batch);
$batch = [];
}
}
if ($batch) $total += insertBatch($pdo, $batch);
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
throw $e;
}
return $total;
}
function insertBatch(PDO $pdo, array $batch): int {
$cols = array_keys($batch[0]);
$placeholders = '(' . implode(',', array_fill(0, count($cols), '?')) . ')';
$sql = "INSERT INTO users (" . implode(',', $cols) . ") VALUES " . implode(',', array_fill(0, count($batch), $placeholders));
// 扁平化參數
$params = array_merge(...array_values($batch));
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->rowCount();
}
composer require league/csv
use League\Csv\Reader;
use League\Csv\Statement;
$pdo = new PDO('mysql:host=127.0.0.1;dbname=test', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false, // 真正預處理
]);
$reader = Reader::createFromPath('data.csv', 'r');
$reader->setHeaderOffset(0);
$reader->setEncoding('UTF-8');
$stmt = (new Statement())
->where(fn($row) => !empty($row['email'])) // 簡單驗證
->limit(100000); // 安全上限
$pdo->beginTransaction();
try {
$batch = [];
foreach ($stmt->process($reader) as $row) {
$row['email'] = strtolower($row['email']);
$batch[] = $row;
if (count($batch) >= 500) {
upsertBatch($pdo, $batch);
$batch = [];
}
}
if ($batch) upsertBatch($pdo, $batch);
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
throw $e;
}
function upsertBatch(PDO $pdo, array $batch): void {
$cols = array_keys($batch[0]);
$updateCols = array_diff($cols, ['id', 'email']); // 唯一鍵不更新
$sql = "INSERT INTO users (" . implode(',', $cols) . ") VALUES " .
implode(',', array_fill(0, count($batch), '(' . implode(',', array_fill(0, count($cols), '?')) . ')')) .
" ON DUPLICATE KEY UPDATE " .
implode(',', array_map(fn($c) => "$c=VALUES($c)", $updateCols));
$params = array_merge(...array_values($batch));
$pdo->prepare($sql)->execute($params);
}
CHARACTER SET / setEncoding()fopen 讀前 3 bytes 判斷FIELDS TERMINATED BY / setCsvControl()ENCLOSED BY / setCsvControl()LINES TERMINATED BY (MySQL 自動處理)IGNORE 1 LINES / setHeaderOffset()split -l 100000 big.csv part_,平行匯入多張表再合併。ALTER TABLE users DISABLE KEYS (MyISAM) 或 SET FOREIGN_KEY_CHECKS=0; SET UNIQUE_CHECKS=0; (InnoDB),匯入後重建。pv big.csv | mysql ... 或 PHP 端記錄行數推送進度條。完整專案 (含 Laravel Command、Symfony Console、進度條、錯誤行記錄) 見 GitHub。