Tìm hiểu về N + 1 Query
Bài đăng này đã không được cập nhật trong 8 năm
Rails có thể nói là một sự lựa chọn phổ biến nhất khi bạn muốn phát triển một sản phẩm có thể sử dụng được ở mức độ vừa phải nhất (Minimum Viable Products). Bạn có thể sử dụng bootstrap, prototype, và phát hành một ứng dụng rất dễ dàng với Rails, và nó cũng là sự lựa chọn của rất nhiều lập trình viên.
Nhưng thường thì trong quá trình phát triển sản phẩm, hầu hết các lập trình viên chưa chú trọng đến việc tăng cường performance cho hệ thống mà thực ra dù sao cũng chưa nên để ý nhiều ở giai đoạn đó. Nhưng khi sản phẩm trong quá trình hoàn thiện và cần được thu nhỏ cũng như tối ưu, các chỉ số hiệu suất này bắt đầu đóng vai trò quan trọng. Lập trình viên cần cân nhắc tối ưu hoá tốc độ xử lý cũng như không làm thay đổi logic của các dòng code.
Một điểm cần được tối ưu đó là những truy vấn mà application của bạn gửi tới database, tốc độ của sản phẩm sẽ tăng lên khi số lượng các truy vấn được giảm đi.
Phần lớn các application dùng Rails đều có dữ liệu được phân bổ qua các model và các mối quan hệ giữa chúng và sử dụng các ORM để truy vấn. Các ORM đó có thể giúp phát hiện các sai lệch giữa các mối quan hệ trong database với các mô hình hướng đối tượng, hi vọng sẽ giúp mọi thứ dễ dàng hơn. Nhưng nếu không ý thức để tránh được một số "cạm bẫy" có thể làm cho tốc độ xử lý của Application giảm xuống một cách khủng khiếp. Một trong những "cạm bẫy" đó chính là vấn đề gửi quá nhiều truy vấn (N+1 queries).
Vậy N+1 Query là gì ?
Vấn đề xuất hiện khi cần load dữ liệu của một model "con" trong một mối quan hệ parent-child (hay phần "nhiều" trong quan hệ một-nhiều). Phần lớn các ORM mặc định kích hoạt chức năng lazy-loading, nên các truy vấn được tìm tới bản ghi "cha", sau đó mới thực hiện từng truy vấn đối với các bản ghi "con". Bạn có thể nhận ra rằng, thực hiện N+1 truy vấn thay vì 1 truy vấn sẽ có thể làm tràn bộ nhớ của database, điều này đương nhiên chúng ta có thể và nên tránh.
Giả sử như một blog đơn giản có rất nhiều bài báo được viết bởi các tác giả khác nhau:
#Articles model
class Article < ActiveRecord::Base
belongs_to :author
end
#Authors model
class Author < ActiveRecord::Base
has_many :posts
end
Ta muốn lấy ra 5 bài báo mới nhất cùng với tiêu đề và tên tác giả của các bài báo đó.
Chúng ta có thể làm như sau:
#In our controller
@recent_articles = Article.order(published_at: :desc).limit(5)
#in our view file
@recent_articles.each do |article|
Title: <%= article.title %>
Author:<%= article.author.name %>
end
Dòng code trên sẽ gửi 6 (5+1) truy vấn đến database, 1 truy vấn để lấy 5 bài báo mới nhất và 5 truy vấn để lấy tên tác giả cho từng bài báo. Trong trường hợp trên, chúng ta chỉ giới hạn số lượng truy vấn ở 5 nên sẽ không thể nhìn thấy được vấn đề này ảnh hưởng đến tốc độ xử lý một cách rõ ràng. Nhưng nếu tăng số lượng truy vấn lên, không biết điều gì có thể xảy ra.
Mỗi truy vấn sẽ tốn một chút tài nguyên. Sẽ tốt hơn nếu chúng ta đưa ra 1 truy vấn và có được 100 kết quả, hơn là đưa ra 100 truy vấn và cũng chỉ có 100 kết quả. Điều này đặc biệt có ý nghĩa khi database được đặt riêng biệt và có sự tồn tại độ lag giữa application và server.
Giải pháp - Eager Loading
Eager Loading là một cơ chế cho phép load các bản ghi có liên quan đến đối tượng được trả về bởi
Model.find
và sử dụng số truy vấn ít nhất có thể. Ở ví dụ trên, nếu chúng ta sử dụng eager loading để lấy dữ liệu, thông tin tác giả sẽ được load cùng với bài báo.
Trong Rails, ActiveRecord có một phương thức là includes, nó đảm bảo rằng các dữ liệu liên quan được load với số lượng truy vấn ít nhất có thể.
Hãy tối ưu dòng code của chúng ta sử dụng cơ chế Eager loading :
#In our controller
#Using includes(:authors) will include authors model.
@recent_articles = Article.order(published_at: :desc).includes(:authors).limit(5)
#in our view file
@recent_articles.each do |article|
Title: <%= article.title %>
Author:<%= article.author.name %>
end
Eager Loading xử lý vấn đề N+1 như thế nào ?
Truy vấn của chúng ta khi chưa dùng Eager Loading sẽ như sau :
Article Load (0.9ms) SELECT 'articles'.* FROM 'articles'
Author Load (0.4ms) SELECT 'authors'.* FROM 'authors' WHERE 'authors'.'id' = ? ORDER BY 'authors'.'id' ASC LIMIT 1 [["id", 1]]
Author Load (0.3ms) SELECT 'authors'.* FROM 'authors' WHERE 'authors'.'id' = ? ORDER BY 'authors'.'id' ASC LIMIT 1 [["id", 2]]
Author Load (0.4ms) SELECT 'authors'.* FROM 'authors' WHERE 'authors'.'id' = ? ORDER BY 'authors'.'id' ASC LIMIT 1 [["id", 3]]
Author Load (0.3ms) SELECT 'authors'.* FROM 'authors' WHERE 'authors'.'id' = ? ORDER BY 'authors'.'id' ASC LIMIT 1 [["id", 4]]
Author Load (0.4ms) SELECT 'authors'.* FROM 'authors' WHERE 'authors'.'id' = ? ORDER BY 'authors'.'id' ASC LIMIT 1 [["id", 5]]
Khi đã xử lý Eager Loading:
Article Load (0.4ms) SELECT 'articles'.* FROM 'articles'
Author Load (0.4ms) SELECT 'authors'.* FROM 'authors' WHERE 'authors'.'id' IN (1,2,3,4,5)
số lượng truy vấn đã giảm
Bullet Gem
Bullet Gem được viết bởi Richard Huang, giúp giảm số lượng truy vấn mà Application đưa ra lần đầu tiên vào năm 2009 dưới dạng plugin, và cho tới giờ nó vẫn là một gem rất hữu ích trong việc giám sát tốc độ xử lí của Application. Bullet giúp bạn phát hiện những truy vấn gây nên N+1 và báo cho bạn biết. Điều thú vị hơn là nó còn giúp bạn phát hiện những Eager Load thừa không được dùng đến.
Bullet có rất nhiều cách để thông báo cho bạn biết vị trí gây nên N+1: Growl notifications, JavaScript alerts hay XMPP. Thêm nữa nó còn ghi lại log rất cụ thể trong file bullet.log, và nó cũng có thể xuất log cho application nếu bạn mong muốn.
Cách cài đặt và sử dụng
Thêm vào Gemfile và chạy bundle install:
group :development do
gem 'bullet'
end
Gem này chỉ nên được sử dụng, vì chắc hẳn bạn ko muốn người dùng application của bạn nhận được thông báo về N+1 phải không?
Tiếp theo đó là cấu hình để Bullet thông báo cho bạn biết
config.after_initialize do
#Enable bullet in your application
Bullet.enable = true
end
Thông báo qua Javascript:
Bullet có thể được cấu hình để thông báo cho lập trình viên thông qua một alert box đơn giản trong khi đang load trang web bất kì, hãy thêm dòng code sau đây:
Bullet.alert = true
Thông báo qua console:
Nếu bạn không muốn nhìn alert box trên trang web, bạn có thể sử dụng console để biết được tại đâu xảy ra N+1 bằng cách thêm đoạn code sau:
Bullet.console= true
Thông báo qua Rails Logs:
Bullet cũng có thể lưu lại logs về các truy vấn chưa được tối ưu. Vì vậy, nếu bạn đang sử dụng một số công cụ để phân tích cho các logs, bạn có thể thêm đoạn code sau: Bullet.rails_logger = true
Ghi lại log:
Nếu bạn muốn các truy vấn được log lại, Bullet có thể giúp bạn bằng cách:
Bullet.bullet_logger = true
sẽ lưu lại một file log với tên bullet.log với tất cả những truy vấn chưa được tối ưu hoá.
Thông báo qua Growl Notifications:
Bullet.growl = true
Tham khảo
Rubygems : https://rubygems.org/gems/bullet
Github : http://github.com/flyerhzm/bullet
RailsCast : http://railscasts.com/episodes/372-bullet
All rights reserved