Chinh phục triệu dòng dữ liệu: Các phương pháp tối ưu để nhập liệu hàng loạt trong Laravel
6791

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.

    // 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());
    }
    
    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 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 issue
    10M

     

🔹 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 issue
    10M - - -

     

🔹 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ặc mysqlimport.
  • 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ấtParallel Processing hoặc MySQL LOAD DATA INFILE.

  • 🛠️ Dễ triển khai, ít phức tạpChunk 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


  1. Khác
  2. ThreeJS
  3. Ubuntu/Linux
  4. HTML/CSS
  5. Git
  6. Amazon Web Services
  7. Javascript
  8. Docker
  9. Laravel

Bài viết liên quan


9 Mẹo Hữu Ích Khi Sử Dụng Blade Trong Laravel

9 Mẹo Hữu Ích Khi Sử Dụng Blade Trong Laravel

01.08.2024
Author: ADMIN
Khám phá 9 mẹo Blade giúp bạn viết code Laravel sạch, tối ưu và chuyên nghiệp hơn. Từ @forelse, @auth, @guest, đến format ngày, tối ưu SEO – tất cả trong một bài viết súc tích, dễ áp dụng!
Ví dụ về các vấn đề truy vấn N+1

Ví dụ về các vấn đề truy vấn N+1

01.08.2024
Author: ADMIN
Tìm hiểu vấn đề N+1 Query trong Eloquent Laravel, cách phát hiện và tối ưu hóa với Eager Loading, withCount, và Strict Loading Mode để cải thiện hiệu suất ứng dụng.
Laravel Routing – 8 Advanced Tips

Laravel Routing – 8 Advanced Tips

01.08.2024
Author: ADMIN
Laravel Routing không chỉ là Route::get(). Khám phá 8 mẹo nâng cao giúp bạn kiểm soát route tốt hơn, tối ưu API, subdomain, rate limit và caching! 🚀
Một số lệnh Artisan Make với các tham số

Một số lệnh Artisan Make với các tham số

01.08.2024
Author: ADMIN
Tổng hợp các lệnh php artisan make quan trọng trong Laravel giúp bạn tối ưu hóa quy trình phát triển! 🚀💡

Bài viết khác

Routing

Routing

01.08.2024
Author: ADMIN
Hướng dẫn chi tiết về Basic Routing trong Laravel, từ cách định nghĩa route, sử dụng middleware, route caching đến route naming giúp tối ưu hóa ứng dụng.
Blade Basics

Blade Basics

01.08.2024
Author: ADMIN
Khám phá Blade trong Laravel: từ if-else, loops, kế thừa layout đến include sub-views. Giúp code gọn gàng, dễ quản lý và bảo trì hơn!
Hiển thị giá trị trong Blade

Hiển thị giá trị trong Blade

01.08.2024
Author: ADMIN
Hướng dẫn hiển thị biến trong Laravel Blade: escape HTML tự động, hiển thị dữ liệu thô, giá trị mặc định và cách truy xuất mảng, đối tượng. Giúp bạn tối ưu hiển thị dữ liệu một cách an toàn!
Cấu Trúc Điều Kiện và Vòng Lặp Trong Blade

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

01.08.2024
Author: ADMIN
Khám phá các cấu trúc điều kiện và vòng lặp trong Laravel Blade. Tận dụng @if, @foreach, @forelse để hiển thị dữ liệu linh hoạt, giúp mã nguồn dễ đọc, sạch sẽ và tối ưu hơn!