+5

Cách nhanh hơn để kiểm tra sự tồn tại của một bản ghi

Nhiều người nói Ruby and Rail chậm, điều này cũng không hẳn là sai. Nói chung, Ruby chậm hơn so với các đối thủ cạnh tranh như Node.js hay Python. Tuy nhiên, nhiều doanh nghiệp lớn nhỏ vẫn sử dụng nó làm xương sống cho các hoạt động của mình. Vì sao lại có sự mâu thuẫn này?

Điều gì làm cho một ứng dụng bị chậm

Mặc dù có nhiều lý do khiến cho một ứng dụng bị chậm, nhưng các truy vấn vào cơ sở dữ liệu thường đóng vai trò lớn nhất ảnh hưởng trực tiếp đến hiệu suất của ứng dụng. Nạp quá nhiều dữ liệu vào bộ nhớ, N+1 câu truy vấn, không sử dụng chỉ mục (index),... là những thủ phạm lớn nhất làm chậm tốc độ.

Ngay cả khi hiện giờ ứng dụng của bạn chạy rất nhanh, nó vẫn có thể chậm hơn nhiều chỉ sau vài tháng. Xét cho cùng, thao tác với một cơ sở dữ liệu chỉ có vài trăm bản ghi khác rất nhiều với một cơ sở dữ liệu có hàng triệu bản ghi.

Kiểm tra sự tồn tại trong Rails

Kiểm tra một bản ghi có tồn tại trong cơ sở dữ liệu hay không có lẽ là câu lệnh hay được sử dụng nhất với cơ sở dữ liệu. Có nhiều cách để kiểm tra sự tồn tại của một bản ghi trong Rail. Chúng ta có present?, any?, empty?, exists?, và một số cách dựa trên việc đếm số bản ghi, mỗi cách lại có hiệu suất khác nhau.

Mình thích sử dụng exists? nhất và dưới đây sẽ minh họa lý do vì sao mình thích dùng exists?. Mình sẽ tìm kiếm dữ liệu trong bảng Build trong 7 ngày vừa qua. Hãy quan sát cách mà chúng thao tác với cơ sở dữ liệu.

Build.where(:created_at => 7.days.ago..1.day.ago).passed.present?

# SELECT "builds".* FROM "builds" WHERE ("builds"."created_at" BETWEEN
# '2017-02-22 21:22:27.133402' AND '2017-02-28 21:22:27.133529') AND
# "builds"."result" = $1 [["result", "passed"]]


Build.where(:created_at => 7.days.ago..1.day.ago).passed.any?

# SELECT COUNT(*) FROM "builds" WHERE ("builds"."created_at" BETWEEN
# '2017-02-22 21:22:16.885942' AND '2017-02-28 21:22:16.886077') AND
# "builds"."result" = $1 [["result", "passed"]]


Build.where(:created_at => 7.days.ago..1.day.ago).passed.empty?

# SELECT COUNT(*) FROM "builds" WHERE ("builds"."created_at" BETWEEN
# '2017-02-22 21:22:16.885942' AND '2017-02-28 21:22:16.886077') AND
# "builds"."result" = $1 [["result", "passed"]]


Build.where(:created_at => 7.days.ago..1.day.ago).passed.exists?

# SELECT 1 AS one FROM "builds" WHERE ("builds"."created_at" BETWEEN
# '2017-02-22 21:23:04.066301' AND '2017-02-28 21:23:04.066443') AND
# "builds"."result" = $1 LIMIT 1 [["result", "passed"]]

Cách đầu tiên sử dụng present? rất không hiệu quả, nó tải tất cả các bản ghi trong cơ sở dữ liệu vào bộ nhớ, tạo các đối tượng Active Record, và tìm xem mảng đấy rỗng hay không. Trong một cơ sở dữ liệu khổng lồ, việc này có thể gây ra sự cố và có khả năng phải tải hàng triệu bản ghi, thậm chí có thể làm hệ thống bị downtime (không truy cập được).

Cách thứ hai và thứ ba sử dụng any?empty? thì tối ưu hóa hơn, cách này chỉ tải COUNT(*) vào bộ nhớ. COUNT(*) là cách truy vấn khá hiệu quả, bạn có thể sử dụng chúng trên các cơ sở dữ liệu lớn mà không gây nên tác dụng phụ nguy hiểm nào.

Các thứ ba là exists? còn tối ưu hóa hơn, và nó nên là lựa chọn đầu tiên khi muốn kiểm tra sự tồn tại của bản ghi. Nó sử dụng phương pháp SELECT 1 ... LIMIT 1, tức là chỉ lấy cột đầu tiên của bản ghi đầu tiên tìm được.

Đây là thời gian truy vấn đối với các câu lệnh trên:

present? =>  2892.7 ms
any?     =>   400.9 ms
empty?   =>   403.9 ms
exists?   =>     1.1 ms

Trong ví dụ này, thời gian khi sử dụng exists? nhanh hơn gần 400 lần. Nếu bạn coi 200 ms là giới hạn thời gian phản hồi có thể chấp nhận được, thì sự thay đổi nhỏ này có thể là tiêu chuẩn để người dùng đánh giá hệ thống tốt hay không.

Có nên lúc nào cũng sử dụng exists?

exists? mang lại hiệu suất tốt nhất, nhưng vẫn có một số trường hợp ngoại lệ. Ví dụ khi kiểm tra sự tồn tại của một association record (bản ghi liên kết, các bạn xem ví dụ sẽ hiểu hơn về khái niệm này), any? hay empty? cũng sử dụng cách truy vấn cực kì tối ưu là SELECT 1 ... LIMIT 1, tuy nhiên any? không tác động lại vào cơ sở dữ liệu nếu bản ghi đó đã được lưu trong bộ nhớ. Điều này làm any? nhanh hơn các cách khác.

project = Project.find_by_name("semaphore")

project.builds.load    # eager loads all the builds into the association cache

project.builds.any?    # no database hit
project.builds.exists? # hits the database

# nếu xóa bộ nhớ
project.builds(true).any?    # hits the database
project.builds(true).exists? # hits the database

Nói chung, lời khuyên là vẫn nên thường xuyên dùng exists? để cải thiện tốc độ truy cập.

Nguồn tham khảo: https://semaphoreci.com/blog/2017/03/14/faster-rails-how-to-check-if-a-record-exists.html


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.