Hướng dẫn dùng chunk size trong PHP

Hướng dẫn dùng chunk size trong PHP

Đã đăng vào thg 12 21, 2018 10:13 SA 2 phút đọc

Tại sao chia sẻ điều này?

Phương thức Chunk của Laravel được sử dụng rộng rãi để phân chia các truy vấn lớn thành các phần truy vấn nhỏ hơn khi xử lý với một cơ sở dữ liệu lớn. Nhưng có một vấn đề nếu thuộc tính được sử dụng trong truy vấn sẽ bị thay đổi trong callback function. Nó rất dễ bị bỏ qua và do đó thường bị sử dụng sai.

Vấn đề ở đây là gì?

Vui lòng xem ví dụ dưới đây:

Giả định: tổng số người dùng chưa xử lý là 400 (Id là 1 - 400)

Vấn đề là: Chỉ có một nửa số người dùng được xử lý.

Phân tích

Mỗi chunk đang được fetch thông qua single query sử dụng kết hợp với limit và offset. Chúng ta hãy xem chức năng chunk sẽ hoạt động như thế nào.

  • Ban đầu, người dùng chưa được xử lý là 400 (Id 1 - 400), đối với truy vấn chunk thứ nhất: select * from `users` where `processed` = 0 limit 100 offset 0 và 100 người dùng đầu tiên Ids (1-100) được xử lý

  • Sau lần chunk thứ nhất, người dùng chưa được xử lý là 300 (Id 101 - 400), truy vấn chunk thứ 2: select * from `users` where `processed` = 0 limit 100 offset 100 và 100 người dùng tiếp theo với Ids (201-300) được xử lý

  • Sau lần chunk thứ 2, người dùng chưa được xử lý là 200 (Id 101 - 200 và 301 - 400), đối với truy vấn đoạn thứ 3: select * from `users` where `processed` = 0 limit 100 offset 200 và không có 1 người dùng nào được xử lý

Kết quả là: 200 người dùng được xử lý, nhưng 200 người dùng vẫn chưa được xử lý.

- Giải pháp

Bạn nên sử dụng phương thức chunkById thay cho phương thức chunk trong tình huóng này, chúng có vai trò tương tự như nhau. Sự khác biệt duy nhất là cách xây dựng truy vấn. ChunkById sử dụng 'id' và 'limit' trong khi chunk sử dụng 'limit' và 'offset'. Code sửa đổi sẽ như sau:

Khi sử dụng chunkById, một câu truy vấn chunk sẽ giống như dưới đây:

Truy vấn chunk thứ nhất: select * from `users` where ``processed` = 0 and `id` > 0 order by `id` asc limit 100

Truy vấn chunk thứ 2: select * from `users` where ``processed` = 0 and `id` > 100 order by `id` asc limit 100

Truy vấn chunk thứ 3: select * from `users` where ``processed` = 0 and `id` > 200 order by `id` asc limit 100

Truy vấn chunk thứ 4: select * from `users` where ``processed` = 0 and `id` > 300 order by `id` asc limit 100

Do đó, kết quả tìm nạp sẽ là 1-100, 101- 200, 201 -300, 301- 400 theo trình tự và tất cả người dùng đều được xử lý.

Bài viết được sưu tầm và dịch từ: https://engineering.carsguide.com.au/chunk-be-careful-b19c8197dc4d

All rights reserved

Facade DB

Trong Laravel, khi muốn lấy dữ liệu hoặc xây dựng query thì chúng ta có thể sử dụng facade DB. Khi đi kèm với table thì facade này sẽ trả về instance của query builder thuộc về bảng mà chúng ta gọi đến. Ví dụ như DB::table('posts') thì sẽ tạo ra một instance của query builder thuộc về bảng posts. Với việc trả về instance của query builder như vậy, thì dev có thể thêm nhiều câu query nữa vào trong cái instance đó như là lọc bởi lượt người xem lớn hơn hoặc bằng năm, sắp xếp theo cột title theo thứ tự Z-A, .... Và cuối cùng, chúng ta sẽ có được kết quả khi gọi đến hàm get, hàm này sẽ trả về instance là kiểu Collection và dev có thể viết function hoặc dùng những hàm có sẵn trong Collection để làm những việc mình mong muốn như map data, duyệt từng record.

Vấn đề

Với facade DB thì mọi chuyện sẽ khá là dễ dàng và vui vẻ. Như vậy, nếu muốn lấy tất cả dữ liệu của bảng posts thì chúng ta sẽ gọi facade DB như sau DB::table('posts')->get(). Nice, chỉ với một dòng đơn giản như vậy, mà chúng ta đã có thể lấy hết tất cả dữ liệu của bảng post, sau đó làm gì tùy thích. Chúng ta có thể duyệt từng record, map, filter, tính sum, count, vân vân và mây mây. Tuy nhiên, khi gọi như vậy, dữ liệu sẽ được load vào memory một lúc. Thử tưởng tượng, nếu dữ liệu không phải là vài chục, vài trăm, vài ngàn mà là vài chục ngàn, vài trăm ngàn hay có thể lên đến 1 triệu thì sao? Thì chúng ta sẽ tốn khá là nhiều memory để load và nếu memory không đủ thì ngủm củ tỏi. Vậy có cách nào để cải thiện không?

Chunk

Giải pháp cũng khá đơn giản, nếu như mà dữ liệu nhiều quá, load một lần như vậy hao tốn memory quá thì chúng ta sẽ load từ từ, từ từ mà tốn chứ không tốn một lần quá nhiều. Laravel cung cấp cho một giải pháp với tên gọi là Chunk. Ý tưởng khá đơn giản, có một trăm ngàn dữ liệu, thay vì load hết vô memory thì chia nhỏ ra. Có thể chia nhỏ ra thành 100 records mỗi lần, như vậy cứ load 100 records, xong giải phóng dữ liệu rồi sau đó tiếp tục load 100 records tiếp theo. Như vậy, memory chúng ta cần sử dụng sẽ ít hơn khá là nhiều. Thay vì cần dùng 8GB RAM thì với chunk đôi khi chúng ta chỉ cần dùng đến tầm 4GB hoặc 5GB RAM là đẹp.

Cách dùng chunk cũng khá đơn giản, viết câu query builder, gọi hàm chunk, pass số lượng record muốn chunk và closure để thực hiện công việc cần thiết vào. Lấy ví dụ mình muốn lấy tất cả post, chunk 100 records mỗi lần và in ra title của post đó thì mình sẽ làm như thế này:

DB::table('posts')->chunk(100, function ($posts) {
    foreach($post as $post) {
        echo $post->title;
    }
}

Ngoài ra, chúng ta cũng có thể dừng chunk lại nếu có gì đó sai sai. Giả sử đến record thứ 1000 mình dừng chunk thì mình chỉ cần return false.

DB::table('posts')->chunk(100, function ($posts) {
    if ($post->id >= 1000) {
        return false;
    }
}

Nếu mình cần update dữ liệu trong database thì cân nhắc dùng chunkById sẽ an toàn hơn vì phương thức này sẽ dựa vào primary key của record để mà chunk

Bonus thêm streaming lazy

Ngoài chunk ra, thì còn có streaming lazy, nó cũng hoạt động na ná. Với phương thức này thì nó sẽ thực thi những câu query ở trong những chunks. Chỉ cần thêm phương thức lazy vào là được và khi thêm lazy thì sẽ trả về kiểu LazyCollection do đó bạn có thể dùng những phương thức của LazyCollection để thao tác với dữ liệu như each, map, .... Cũng như chunk, nếu cần update dữ liệu trong DB thì nên cân nhắc dùng lazyById hoặc lazyByIdDesc, laravel sẽ tách kết quả dựa trên primary key của record đó.