Chia sẻ
//Hiểu rõ get(), chunk(), lazy(), cursor() trong Laravel

Hiểu rõ get(), chunk(), lazy(), cursor() trong Laravel

Giới thiệu

Khi truy vấn dữ liệu, một số yếu tố có thể ảnh hưởng đến hiệu suất ứng dụng. Nếu không sử dụng đúng cách, có thể làm tốn RAM và làm giảm tốc độ tải trang đáng kể.

Ngoài việc sử dụng Laravel Octane để tối ưu hiệu suất, Laravel còn cung cấp nhiều phương thức hỗ trợ để truy vấn dữ liệu hiệu quả hơn, tùy vào từng trường hợp cụ thể. Trong bài viết này, mình sẽ sử dụng Laravel 11 và MySQL để minh họa một số ví dụ.

Tổng quan về các phương thức

  • get(): Thực thi truy vấn và tải toàn bộ dữ liệu vào bộ nhớ PHP.
  • chunk(): Chia dữ liệu thành từng nhóm và xử lý lần lượt từng nhóm.
  • lazy(): Tương tự như chunk(), nhưng dùng PHP Generator để tiết kiệm bộ nhớ hơn.
  • cursor(): Truy xuất từng bản ghi từ database buffer để tiết kiệm bộ nhớ.

get()

Phương thức get() trong Laravel thực hiện truy vấn và tải toàn bộ kết quả vào bộ nhớ PHP dưới dạng một Collection.

  • Thực thi toàn bộ truy vấn ngay lập tức.
  • Không tối ưu bộ nhớ, vì toàn bộ dữ liệu sẽ được nạp vào một lần.
  • Hỗ trợ các phương thức của Collection như map(), filter().
  • Hỗ trợ Eager Load Relationships.

⚠️ Lưu ý: Chỉ nên dùng cho các câu truy vấn với dữ liệu nhỏ.

🧪 Ví dụ: Lấy tất cả employees.

Laravel:

$employees = Employee::get();

SQL thực thi:

SELECT * FROM employees;

RAM sử dụng:

Với cấu hình memory_limit = 128MB trong file php.ini, nếu bảng employees có 300,000 rows thì khi dùng method get() sẽ bị lỗi “Allowed memory size of 134217728 bytes exhausted” do vượt quá mức bộ nhớ được cấp.

Trong ví dụ thực tế mình chỉ có thể lấy ra khoảng 70,000 rows với dữ liệu như bên dưới.

chunk()

Phương thức chunk() cho phép chia dữ liệu thành từng nhóm (chunk) nhỏ và xử lý lần lượt từng nhóm. Đây là giải pháp phù hợp khi làm việc với tập dữ liệu lớn, giúp tiết kiệm bộ nhớ so với việc tải toàn bộ dữ liệu vào cùng lúc.

  • Nên dùng cho các câu truy vấn với dữ liệu lớn và có thể xử lý lần lượt theo từng nhóm.
  • Hỗ trợ các phương thức của Collection như map(), filter().
  • Hỗ trợ Eager Load Relationships.

🧪 Ví dụ: Lấy ra danh sách với 500 employees mỗi chunk.

Laravel:

Employee::chunk(500, function ($employees) {
    foreach ($employees as $employee) {
        echo $employee->first_name . "\n";
    }
});

SQL thực thi:

Chunk 1:
SELECT * FROM `employees` ORDER BY `employees`.`emp_no` ASC LIMIT 500 OFFSET 0;

Chunk 2:
SELECT * FROM `employees` ORDER BY `employees`.`emp_no` ASC LIMIT 500 OFFSET 500;

Chunk 3:
SELECT * FROM `employees` ORDER BY `employees`.`emp_no` ASC LIMIT 500 OFFSET 1000;

RAM sử dụng:

Với 300.000 rows, chunk 500 thì bộ nhớ sử dụng khoảng 6.07MB.

⚠️ Lưu ý: Nếu bạn lọc kết quả dựa trên một cột nào đó, và trong quá trình xử lý bạn cũng thực hiện cập nhật giá trị của chính cột đó, thì có thể dẫn đến kết quả không nhất quán hoặc bị bỏ sót dữ liệu ngoài ý muốn.

🧪 Ví dụ minh họa: Giả sử có 1 danh sách 10 employees với status là ‘accepted’, và bạn muốn cập nhật status thành ‘pending’ bằng cách xử lý mỗi lần 2 bản ghi:

Laravel:

Employee::where('status', 'accepted')->chunk(2, function ($employees) {
    foreach ($employess as $employee) {
        $employee->status = 'pending';
        $employee->save();
    }
});

SQL thực thi:

Chunk 1:
select * from `employees` where `status` = 'accepted' order by `employees`.`emp_no` asc limit 2 offset 0

Chunk 2:
select * from `employees` where `status` = 'accepted' order by `employees`.`emp_no` asc limit 2 offset 2

Dữ liệu trước khi thực hiện update:

Dữ liệu sau khi thực hiện update:

Vấn đề: các employees với emp_no: 10,003 – 10,004 – 10,007 – 10,008 không được update sang status là ‘pending’.

Nguyên nhân: khi chunk 1 chạy xong thì 2 employees với emp_no: 10,003 – 10,004 đã được update status thành ‘pending’, khi này số dữ liệu employees có status ‘accepted’ lúc này chỉ còn lại 8 rows.

Khi chunk 2 được chạy với OFFSET 2 thì thay vì employees với emp_no: 10,003 – 10,004 được lấy ra thì lúc các emp_no: 10,005 – 10,006 sẽ được lấy ra để thực hiện update, như vậy data của chúng ta đã bị vô tình bỏ qua 2 employees với emp_no: 10,003 – 10,004, tương tự với 2 employees với emp_no: 10,007 – 10,008 cũng sẽ bị bỏ qua.

Giải pháp: thay vì dùng OFFSET, phương thức chunkById() sử dụng một cột làm mốc để truy vấn các bản ghi lớn hơn bản ghi cuối cùng trong chunk trước đó. Nhờ vậy, bạn tránh được việc bỏ sót dữ liệu trong khi cập nhật.

Laravel:

Employee::where('status', 'accepted')->chunkById(2, function ($employees) {
    foreach ($employess as $employee) {
        $employee->status = 'pending';
        $employee->save();
    }
}, 'emp_no');

SQL thực thi:

Chunk 1:
select * from `employees` where `status` = 'accepted' order by `emp_no` asc limit 2

Chunk 2:
select * from `employees` where `status` = 'accepted' and `emp_no` > 10002 order by `emp_no` asc limit 2

Chunk 3:
select * from `employees` where `status` = 'accepted' and `emp_no` > 10004 order by `emp_no` asc limit 2

lazy()

Phương thức lazy() cho phép xử lý từng bản ghi một cách tuần tự bằng cách trả về một LazyCollection, giúp tiết kiệm bộ nhớ hiệu quả.

Không giống như chunk(), lazy() không yêu cầu callback và hoạt động tương tự như cursor() ở bên trong.

  • Hỗ trợ các phương thức của Collection như map(), filter().
  • Hỗ trợ Eager Load Relationships.
  • Không tải toàn bộ dữ liệu vào bộ nhớ như get().

🧪 Ví dụ: Cập nhật status cho các employees có status = ‘accepted’.

Laravel:

$employees = Employee::where('status', 'accepted')->lazy(2);
foreach ($employees as $employee) {
    $employee->status = 'pending';
    $employee->save();
}

⚠️ Lưu ý: Khi bạn thực hiện cập nhật trong quá trình lặp, lazy() cũng có thể gặp lỗi nhảy OFFSET như chunk(). Để tránh điều đó, hãy dùng lazyById():

$employees = Employee::where('status', 'accepted')->lazyById(2, 'emp_no');
foreach ($employees as $employee) {
    $employee->status = 'pending';
    $employee->save();
}

RAM sử dụng:

Với 300.000 rows, lazy 500 thì bộ nhớ sử dụng khoảng 6.42MB.

cursor()

Phương thức cursor() chỉ thực thi một truy vấn duy nhất và trả về từng bản ghi một cách tuần tự từ database buffer, thay vì tải toàn bộ kết quả vào bộ nhớ PHP. Điều này giúp xử lý dữ liệu với dung lượng lớn mà vẫn tiết kiệm bộ nhớ hiệu quả.

  • Thích hợp cho xử lý 1 lượng lớn data.
  • Do các bản ghi được lấy lần lượt nên không hỗ trợ các phương thức Collection như map() hoặc filter().
  • Do các bản ghi được lấy lần lượt nên không hỗ trợ Eager Load Relationships.

🧪 Ví dụ: Hiển thị first name của tất cả employees.

Laravel:

foreach (Employee::cursor() as $employee) {
    echo $employee->first_name . "\n";
}

SQL thực thi:

SELECT * FROM employees;

RAM sử dụng:

Với 300.000 rows thì bộ nhớ sử dụng khoảng 1.87MB.

⚠️ Lưu ý:

Mặc dù cursor() giúp tiết kiệm bộ nhớ bằng cách chỉ giữ một bản ghi trong bộ nhớ tại một thời điểm, nhưng nó vẫn có thể gây lỗi memory exhausted với dữ liệu cực lớn do cách hoạt động của PDO driver (lưu toàn bộ kết quả trong bộ nhớ trước khi đọc).

Trong trường hợp này, bạn có thể cần tối ưu thêm ở mức PDO layer hoặc query logic, hoặc sử dụng một giải pháp như pagination, streaming thông qua queue, hoặc xử lý song song bằng job queue để tránh tải quá nhiều bản ghi trong một tiến trình duy nhất.

Tổng kết

Phương thứcCách tải dữ liệuBộ nhớCó thể dùng collectionHỗ trợ eager load relationships
get()Tải tất cả dữ liệu vào bộ nhớ một lầnTốn nhiều RAM (có thể gây lỗi nếu dữ liệu lớn) 
chunk()Tải dữ liệu theo từng nhóm nhỏ dùng LIMIT OFFSETTốn ít RAM hơn get()
lazy()Tương tự chunk(), nhưng có dùng PHP Generator để tối ưu bộ nhớTốn ít RAM hơn chunk()
cursor()Duyệt từng bản ghi một từ Database Buffer và dùng PHP Generator để tối ưu bộ nhớÍt tốn RAM nhất, nhưng vẫn có thể bị hết bộ nhớ nếu tập dữ liệu quá lớnKhôngKhông
Huỳnh Hữu Phát
Developer

ỨNG TUYỂN







    Chế độ phúc lợi

    CHÍNH SÁCH LƯƠNG & THƯỞNG

    Thấu hiểu tâm tư nguyện vọng của nhân viên, công ty Rivercrane Việt Nam đặc biệt thiết lập chế độ xét tăng lương định kỳ 2lần/năm. Xét đánh giá vào tháng 06 và tháng 12 hàng năm và thay đổi lương vào tháng 01 và tháng 07 hàng năm. Ngoài ra, nhân viên còn được thưởng thành tích định kỳ cho các cá nhân xuất sắc trong tháng, năm.

    CHẾ ĐỘ ĐÀO TẠO TẠI NHẬT

    Luôn luôn mong muốn các kỹ sư và nhân viên trong công ty có cái nhìn toàn diện về lập trình những mảng kỹ thuật trên thế giới, công ty Rivercrane Việt Nam quyết định chế độ 3 tháng 1 lần đưa nhân viên đi học tập tại Nhật. Các bạn kỹ sư hoàn toàn đều có thể quyết định khả năng phát triển bản thân theo hướng kỹ thuật hoặc theo hướng quản lý.

    CHẾ ĐỘ ĐI DU LỊCH HÀNG NĂM

    Không chỉ đưa đến cho nhân viên những công việc thử thách thể hiện bản thân, công ty Rivercrane Việt Nam muốn nhân viên luôn thích thú khi đến với những chuyến hành trình thú vị hàng năm. Những buổi tiệc Gala Dinner sôi động cùng với những trò chơi Team Building vui nhộn sẽ giúp cho đại gia đình Rivercrane thân thiết hơn.

    CHẾ ĐỘ EVENT CÔNG TY

    Những hoạt động Team building, Company Building, Family Building, Summer Holiday, Mid-Autumn Festival… sẽ là những khoảnh khắc gắn kết đáng nhớ của mỗi một nhân viên trong từng dự án, hoặc sẽ là những điều tự hào khi giới thiệu công ty mình với với gia đình thân thương, cùng nhau chia sẻ yêu thương với thông điệp “We are One”

    BẢO HIỂM

    Công ty Rivercrane Việt Nam đảm bảo tham gia đầy đủ chế độ Bảo hiểm xã hội, bảo hiểm y tế và bảo hiểm thất nghiệp. Cam kết chặt chẽ về mọi thủ tục phát sinh công ty đều hỗ trợ và tiến hành cho nhân viên từ đầu đến cuối. Những chế độ bảo hiểm khác công ty cũng đặc biệt quan tâm và từng bước tiến hành.

    CHẾ ĐỘ PHÚC LỢI KHÁC

    Hỗ trợ kinh phí cho các hoạt động văn hóa, văn nghệ, thể thao; Hỗ trợ kinh phí cho việc mua sách nghiên cứu kỹ thuật; Hỗ trợ kinh phí thi cử bằng cấp kỹ sư, bằng cấp dành cho ngôn ngữ. Hỗ trợ kinh phí tham gia các lớp học về quản lý kỹ thuật bên ngoài; Các hỗ trợ phúc lợi khác theo quy định công ty…

    © 2012 RiverCrane Vietnam. All rights reserved.

    Close