當系統 DB 資料越來越多時,匯出資料就會開始遇到瓶頸,比如像是系統回應超時、記憶體不足等常見問題,今天就讓我們來用 laravel 和 Sql 實作範例探討一下。
環境
- Laravel 8.34.0
- mysql 8.0.23 (docker)
前置作業
建立 1 個資料表後
想辦法塞假資料
我自己是透過 factory 方式實作 artisan cli 命令
然後開多個 cli 背景程序來產生大量資料
本次測試數據量
1
2
3
4
5
6
7
8
9
10
11
12
|
CREATE TABLE `users` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`email_verified_at` timestamp NULL DEFAULT NULL,
`password` varchar(255) NOT NULL,
`remember_token` varchar(100) DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `users_email_unique` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
簡單的 csv 下載功能
1
|
Route::get('download', [ExampleController::class, 'download']);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public function download()
{
$headers = [
'Content-Encoding: UTF-8',
'Content-Type' => 'text/csv',
]; // 可有可無
return response()->streamDownload(function () {
echo "\xEF\xBB\xBF"; // UTF8 BOM
// Header
echo "欄位1,欄位2,欄位3\n";
// Data
echo "11,22,33\n";
echo "11,22,33\n";
}, 'example.csv', $headers);
}
|
- streamDownload 方法參考
- line 9 : csv 的 utf-8 bom ,可以讓你使用 excel 開起 csv 時,中文不會出現亂碼
- Header 和 Data 應該不用多作說明吧,聰明的你還會知道這邊可以做成 Service,之後可以重複用此下載小功能
撈取 DB 大量 User 資料並匯出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
public function download()
{
return response()->streamDownload(function () {
// Data
$this->getData();
}, 'example.csv');
}
protected function getData()
{
// 方法一
User::query()
// ->select('id', 'name', 'email')
->chunk(100000, function ($users) {
foreach ($users as $user) {
echo implode(',', $user->toArray()) . "\n";
}
});
// 方法二
DB::table('users')
// ->select('id', 'name', 'email')
->orderBy('id', 'desc')
->chunk(100000, function ($rows) {
foreach ($rows as $row) {
echo implode(',', (array) $row) . "\n";
}
});
}
|
上面兩種方法都可以
但是!!!
下面的 DB::table(‘users’) 速度比較快
可能 User::chunk 因為處理 Model 物件轉換的關係浪費了多餘的時間
下面 chunk 後直接得到 collection 後可以馬上進行處理
上面執行有可能會出現 php timeout
- 這邊就直接做 User 全部欄位一次匯出,來測試極限,當然你也可以透過
select(...) 限制撈取欄位
,每次撈取的欄位變少了,記憶體負擔單也跟著會變小
- 使用
chunk
分次執行 sql 查詢,避免超過 php 記憶體限制 (128M)
- 為什麼 chunk 會使用 100,000 這個數值 ? 這是
不斷 Try
出來的最合適值,你可能也會依據每個不同撈取狀況來做調整,chunk 越大表示 sql 執行回傳的資料量越大,你就需要確保當前有足夠的記憶體承載,否則就會直接接噴 PHP Fatal error: Allowed memory size of 134217728 bytes
的錯誤,當然我們也可以改 php.ini
來調整記憶體使用上限,但不建議改太大,如果有其他程序這時也要使用大記憶體,可能會發生未預期的系統錯誤。
- 如果 chunk 使用太小比如 500,這會導致你需要花更多時間去執行 sql 指令撈回資料,最後會因為撈資料撈太久 php 直接噴 timeout 錯誤
- 最後成功匯出 User
100 萬筆
的全部欄位資料,檔案大小為 282M
,處理平均時間約 3 ~ 5 秒
內。
總結
可以發現在大資料查詢匯出時
php-fpm 程序預設記憶體為 128M
的狀況下
分次查詢的 chunk
功能扮演非常重要的角色
開發人員需要確認當下撈取的資料量來決定 chunk 大小
在有限的記憶體和處理時間下得到最大效益
不過接下來有趣問題來了…
你某一天收到客戶說想要匯出超大查詢資料 (GB級)
這時你發現即便你怎麼 chunk 都會超過伺服器回應時間
…
…
…
聰明的你一定想到了還有其他解法
就讓我們下期待續
延伸問題
- 如果換成匯入,該怎麼實做 ?
- 能不能知道每次 chunk 執行查詢,撈出的資料大小多少,是否在 128M 以內 ?
- 還有哪邊可以優化 ? 節省記憶體,加快處理速度 ?
- 如果不使用 laravel,改用純 php,該怎麼實作 ?
有任何想法
歡迎在下方留言討論