
Khi cần nhập một triệu dòng vào cơ sở dữ liệu trong Laravel, việc chọn đúng phương pháp có thể ảnh hưởng lớn đến hiệu suất. Dưới đây là đánh giá chi tiết về từng cách tiếp cận và cách chọn giải pháp phù hợp.
Thông tin tôi đang sử dụng như sau:
- php version: 8.2.19
- Memory limit: 512M
- Mysql version: 3.39.2
🔹 Cách 1: Basic Collection Insert
-
📌 Phương pháp: Duyệt qua từng dòng dữ liệu và sử dụng
Model::create()
để chèn vào database. -
✅ Dễ triển khai, mã nguồn đơn giản.
-
⛔ Chậm, tiêu tốn nhiều tài nguyên do tạo quá nhiều truy vấn SQL.
-
⛔ Không phù hợp với dữ liệu lớn.
// Code private function basicInsert($data) { foreach ($data as $row) { User::create($row); } }
Hiệu suất thực thi:
Số lượng Users Thời gian Bộ nhớ Số Queries 10 20 ms 0.21 MB 10 100 125 ms 0.37 MB 100 1K 1.07s 1.62 MB 1,000 10K >2,000 44 10,000 100K memory issue (from mapping)1M memory issue (from file loading)
🔹 Cách 2: The Basic Collection with Big Insert
-
📌 Phương pháp: Sử dụng
Model::insert($data)
để chèn hàng loạt dữ liệu một lần. -
✅ Chèn hàng loạt giúp giảm số lượng truy vấn.
-
⛔ Bị giới hạn bởi
max_allowed_packet
trong MySQL. -
⛔ Không phù hợp nếu cần nhập dữ liệu vượt quá giới hạn của batch insert.
Hiệu suất thực thi:// Code private function batchInsert($data) { $now = now()->format('Y-m-d H:i:s'); $allUsers = collect(file($filePath)) ->skip(1) ->map(fn($line) => str_getcsv($line)) ->map(fn($row) => [ 'name' => $row[0] ?? null, 'email' => $row[1] ?? null, 'password' => $row[2] ?? null, 'created_at' => $now, 'updated_at' => $now, ]); User::insert($allUsers->all()); }
Số lượng Users Thời gian Bộ nhớ Số Queries 10 4 ms 0.01 MB 1 100 7 ms 0.03 MB 1 1K 42 ms 0.33 MB 1 10K 300 ms 2.82 MB 1 100K Prepared statement contains too many placeholders 1M Memory issue10M
🔹 Cách 3: Improving with Chunk Processing
-
📌 Phương pháp: Chia nhỏ dữ liệu thành từng phần (
array_chunk()
) và xử lý batch insert. -
✅ Giảm tải bộ nhớ bằng cách chia nhỏ dữ liệu.
-
✅ Hiệu quả với dữ liệu lớn.
-
⛔ Vẫn có giới hạn về số lượng bản ghi mỗi batch.
// Code private function chunkInsert($data) { $now = now()->format('Y-m-d H:i:s'); collect(file($filePath)) ->skip(1) ->map(fn($line) => str_getcsv($line)) ->map(fn($row) => [ 'name' => $row[0] ?? null, 'email' => $row[1] ?? null, 'password' => $row[2] ?? null, 'created_at' => $now, 'updated_at' => $now, ]) ->chunk(1000) ->each(fn($chunk) => User::insert($chunk->all())); }
Hiệu suất thực thi:
Số lượng Users Thời gian Bộ nhớ Số Queries 10 4 ms 0.01 MB 1 100 6 ms 0.03 MB 1 1K 36 ms 0.33 MB 1 10K 325 ms 3.14 MB 10 100K 2.93 s 32.68 MB 100 1M Allowed memory size of 536870912 bytes exhausted (tried to allocate 4096 bytes) 10M
🔹 Cách 4: Exploring Lazy Collections
-
📌 Phương pháp: Đọc dữ liệu từng dòng một thông qua
LazyCollection
, không tải toàn bộ vào bộ nhớ. -
✅ Tiết kiệm RAM bằng cách xử lý từng dòng một.
-
✅ Phù hợp với tập dữ liệu lớn từ file hoặc API.
-
⛔ Vẫn có độ trễ do phải xử lý từng dòng liên tục.
// Code private function lazyCollectionInsert($path) { $now = now()->format('Y-m-d H:i:s'); LazyCollection::make(function () use ($path) { $handle = fopen($path, 'r'); fgets($handle); while (($line = fgets($handle)) !== false) { yield str_getcsv($line); } fclose($handle); }) ->each(function ($row) use ($now) { User::insert([ 'name' => $row[0], 'email' => $row[1], 'password' => $row[2], 'created_at' => $now, 'updated_at' => $now, ]); }); }
Hiệu suất thực thi:
Số lượng Users Thời gian Bộ nhớ Số Queries 10 36 ms 0.2 MB 10 100 78 ms 0.32 MB 100 1K 724 ms 1.12 MB 1,000 10K 6.77s 9 MB 10,000 100K 1m 16s 90.65 MB 100,000 1M memory issue10M - - -
🔹 Cách 5: Streaming Files with Chunks
-
📌 Phương pháp: Sử dụng
fgetcsv()
để đọc dữ liệu từ file theo từng phần nhỏ và chèn vào database. -
✅ Đọc file trực tiếp giúp tiết kiệm bộ nhớ.
-
✅ Phù hợp với dữ liệu rất lớn.
-
⛔ Phụ thuộc vào tốc độ I/O của ổ đĩa.
// Code private function fileChunkInsert($filePath) { $now = now(); $userDataChunk = []; $handle = fopen($filePath, 'rb'); fgetcsv($handle); while (($row = fgetcsv($handle)) !== false) { $userDataChunk[] = [ 'name' => $row[0], 'email' => $row[1], 'password' => $row[2], 'created_at' => $now, 'updated_at' => $now ]; if (count($userDataChunk) >= 1000) { User::insert($userDataChunk); $userDataChunk = []; } } if (!empty($userDataChunk)) { User::insert($userDataChunk); } fclose($handle); }
Hiệu suất thực thi:
Số lượng Users Thời gian Bộ nhớ Số Queries 10 5 ms 0.19 MB 1 100 7 ms 0.22 MB 1 1K 38 ms 0.52 MB 1 10K 307 ms 3.47 MB 10 100K 3.01 s 33.01 MB 100 1M 30.92 s 328.03 MB 1,000 10M Allowed memory size of 536870912 bytes exhausted
🔹 Cách 6: Optimized PDO Chunk Processing
- 📌 Phương pháp: Sử dụng
PDO::prepare()
với batch insert để giảm số lượng truy vấn SQL. - ✅ Kết hợp PDO và batch insert giúp tăng tốc độ.
- ✅ Giảm tải tài nguyên hệ thống.
- ⛔ Phức tạp hơn trong triển khai.
-
private function pdoInsert($filePath) { $now = now()->format('Y-m-d H:i:s'); $pdo = DB::connection()->getPdo(); LazyCollection::make(function () use ($filePath) { $handle = fopen($filePath, 'rb'); fgetcsv($handle); while (($line = fgetcsv($handle)) !== false) { yield $line; } fclose($handle); }) ->filter(fn($row) => filter_var($row[2], FILTER_VALIDATE_EMAIL)) ->chunk(1000) ->each(function ($chunk) use ($pdo, $now) { $placeholders = rtrim(str_repeat('(?, ?, ?, ?, ?),', $chunk->count()), ','); $query = $pdo->prepare("INSERT INTO users (name, email, password, created_at, updated_at) VALUES $placeholders"); // Prepare values $values = $chunk->flatMap(fn($row) => [ $row[0], $row[1], $row[2], $now, $now, ])->all(); $pdo->prepare($query)->execute($values); }); }
Hiệu suất thực thi:
Số lượng Users Thời gian Bộ nhớ Số Queries 10 4 ms 0.19 MB 0 100 5 ms 0.19 MB 0 1K 9 ms 0.19 MB 0 10K 77 ms 0.19 MB 0 100K 695 ms 0.19 MB 0 1M 6.98 s 0.19 MB 0 10M 1m 10s 0.19 MB 0
🔹 Cách 7: Manual Streaming
- Xử lý và chèn dữ liệu theo từng phần
- Giúp quản lý bộ nhớ hiệu quả hơn
✅ Cách này khá hiệu quả.
private function pdoStreaming($filePath) { $data = []; $handle = fopen($filePath, 'rb'); fgetcsv($handle); $now = now()->format('Y-m-d H:i:s'); while (($row = fgetcsv($handle)) !== false) { $data[] = [ 'name' => $row[0], 'email' => $row[1], 'password' => $row[2], 'created_at' => $now, 'updated_at' => $now, ]; if (count($data) === 1000) { User::insert($data); $data = []; } } if (!empty($data)) { User::insert($data); } fclose($handle); }
Hiệu suất thực thi:
Số lượng Users Thời gian Bộ nhớ Số Queries 10 10 ms 0.01 MB 1 100 11 ms 0.03 MB 1 1K 37 ms 0.33 MB 1 10K 319 ms 3.28 MB 10 100K 2.74 s 32.82 MB 100 1M 26.83 s 327.84 MB 1,000 10M Allowed memory size of 536870912 bytes exhausted (tried to allocate 155648 bytes)
🔹 Cách 8: Manual Streaming with Pdo
- Xử lý và chèn dữ liệu theo từng phần sử dụng pdo
private function manualStreamingWithPdo ($filePath) { $data = []; $handle = fopen($filePath, 'rb'); fgetcsv($handle); $now = now()->format('Y-m-d H:i:s'); $pdo = DB::connection()->getPdo(); while (($row = fgetcsv($handle)) !== false) { $data[] = [ 'name' => $row[0], 'email' => $row[1], 'password' => $row[2], 'created_at' => $now, 'updated_at' => $now, ]; if (count($data) === 1000) { // Build the SQL query for the chunk $columns = array_keys($data[0]); $placeholders = rtrim(str_repeat('(?, ?, ?, ?, ?),', count($data)), ','); $sql = 'INSERT INTO users (' . implode(',', $columns) . ') VALUES ' . $placeholders; // Flatten the data array for the query $values = []; foreach ($data as $row) { $values = array_merge($values, array_values($row)); } $pdo->prepare($sql)->execute($values); $data = []; } } if (! empty($data)) { $columns = array_keys($data[0]); $placeholders = rtrim(str_repeat('(?, ?, ?, ?, ?),', count($data)), ','); $sql = 'INSERT INTO users (' . implode(',', $columns) . ') VALUES ' . $placeholders; $values = []; foreach ($data as $row) { $values = array_merge($values, array_values($row)); } $pdo->prepare($sql)->execute($values); } fclose($handle); }
Hiệu suất thực thi:
Số lượng Users Thời gian Bộ nhớ Số Queries 10 3 ms 0 MB 1 100 3 ms 0 MB 1 1K 26 ms 0 MB 1 10K 217 ms 0 MB 10 100K 1.97 s 0 MB 100 1M 19.68 s 0 MB 1,000 10M 3m 50s 0 MB 10,000
🔹 Cách 9: Parallel Processing with Concurrency
- Chia dữ liệu và dùng queue hoặc multi-thread để nhập song song.
- Có thể kết hợp với
Queue::batch()
trong Laravel.
✅ Hiệu quả nếu máy chủ mạnh.
private function concurrent(string $filePath) { $now = now()->format('Y-m-d H:i:s'); $numberOfProcesses = 10; $tasks = []; for ($i = 0; $i < $numberOfProcesses; $i++) { $tasks[] = function () use ($filePath, $i, $numberOfProcesses, $now) { DB::reconnect(); $handle = fopen($filePath, 'r'); fgets($handle); // Skip header $currentLine = 0; $users = []; while (($line = fgets($handle)) !== false) { // Each process takes every Nth line if ($currentLine++ % $numberOfProcesses !== $i) { continue; } $row = str_getcsv($line); $users[] = [ 'name' => $row[0], 'email' => $row[1], 'password' => $row[2], 'created_at' => $now, 'updated_at' => $now, ]; if (count($users) === 1000) { DB::table('users')->insert($users); $users = []; } } if (! empty($users)) { DB::table('users')->insert($users); } fclose($handle); return true; }; } Concurrency::run($tasks); }
Hiệu suất thực thi:
Số lượng Users Thời gian Bộ nhớ Số Queries 10 1.56s 1.15 MB 0 100 1.56s 1.15 MB 0 1K 1.57s 1.15 MB 0 10K 1.57s 1.15 MB 0 100K 2.17 s 1.15 MB 0 1M 8.2 s 1.15 MB 0 10M Illuminate\Process\Exceptions\ProcessTimedOutException
🔹 Cách 10: MySQL Native Import Solution
- Dùng
LOAD DATA INFILE
hoặcmysqlimport
. - Siêu nhanh (nhanh hơn bất kỳ cách nào ở trên).
⛔ Cần có quyền truy cập MySQL server.
private function mysqlLoadData(string $filePath) { $pdo = DB::connection()->getPdo(); $pdo->setAttribute(PDO::MYSQL_ATTR_LOCAL_INFILE, true); $filepath = str_replace('\\', '/', $filePath); $query = <<<SQL LOAD DATA LOCAL INFILE '$filepath' INTO TABLE users FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES (@col1, @col2, @col3) SET name = @col1, email = @col2, password = @col3, created_at = NOW(), updated_at = NOW() SQL; $pdo->exec($query); }
🎯 Khuyến nghị lựa chọn phương pháp phù hợp
-
📂 Nhập dữ liệu từ file CSV lớn → Sử dụng Streaming Files with Chunks hoặc MySQL
LOAD DATA INFILE
. -
📡 Nhập dữ liệu từ API hoặc xử lý bộ nhớ lớn → Sử dụng Lazy Collections hoặc Chunk Processing.
-
⚡ Cần tốc độ cao nhất → Parallel Processing hoặc MySQL
LOAD DATA INFILE
. -
🛠️ Dễ triển khai, ít phức tạp → Chunk Processing hoặc Batch Insert.
Nếu bạn có quyền admin trên MySQL, LOAD DATA INFILE
sẽ là lựa chọn tối ưu nhất. Nếu không, hãy sử dụng Chunk Processing hoặc Queue để có hiệu suất tốt mà không gặp giới hạn hệ thống. 🚀
Danh mục
Bài viết liên quan

9 Mẹo Hữu Ích Khi Sử Dụng Blade Trong Laravel
Author: | ADMIN |
---|

Ví dụ về các vấn đề truy vấn N+1
Author: | ADMIN |
---|

Laravel Routing – 8 Advanced Tips
Author: | ADMIN |
---|

Một số lệnh Artisan Make với các tham số
Author: | ADMIN |
---|
Bài viết khác

Blade Basics
Author: | ADMIN |
---|

Hiển thị giá trị trong Blade
Author: | ADMIN |
---|

Cấu Trúc Điều Kiện và Vòng Lặp Trong Blade
Author: | ADMIN |
---|