Các mẹo tăng tốc độ truy vấn Active Record trong Rails
Bài đăng này đã không được cập nhật trong 6 năm
Active record
Active record:
Active record is an approach to accessing data in a database. A database table or view is wrapped into a class. Thus, an object instance is tied to a single row in the table. After creation of an object, a new row is added to the table upon save. Any object loaded gets its information from the database. When an object is updated the corresponding row in the table is also updated. The wrapper class implements accessor methods or properties for each column in the table or view.
Về cơ bản, Active Record
làm nhiệm vụ abstract hóa việc thao tác với các bảng trong cơ sở dữ liệu. Nói cách khác, programmer sẽ không cần viết các truy vấn trực tiếp vào CSDL giúp giảm công sức của programmer và giảm độ phức tạp của application.
Active record
như một cây cầu nối giúp truy cập vào cơ sở dữ liệu. Mỗi 1 bảng trong cơ sở dữ liệu như 1 class trong Model, và Rails hỗ trợ nhiều truy vấn giúp chúng ta có thể thao tác với CSDL một cách dễ dàng hơn. Ví dụ như: create (tạo mới) where, find, find_by (tìm kiếm) .... Vậy làm thế nào chúng ta có thể dùng những câu lệnh để truy vấn một cách hiệu quả để giảm tối đa truy vấn vào database giúp cải thiện tốc độ của application. Bài viết này mình xin giới thiệu một số mẹo nhỏ giúp tối ưu hóa truy vấn sử dụng những câu lệnh truy vấn rất hay dùng trong lúc lập trình.
Group SQL queries
Khi chúng ta có trigger nhiều queries
Client.limit(10).map(&:address)
Trong trường hợp này, thay vì thực hiện một truy vấn để lấy adress
, bạn lại tạo ra 10 query sau đó mới lấy address
SELECT * from "clients" LIMIT 10;
SELECT * from "addresses" WHERE "id" = 7;
SELECT * from "addresses" WHERE "id" = 8;
SELECT * from "addresses" WHERE "id" = 10;
SELECT * from "addresses" WHERE "id" = 12;
SELECT * from "addresses" WHERE "id" = 13;
SELECT * from "addresses" WHERE "id" = 15;
SELECT * from "addresses" WHERE "id" = 16;
SELECT * from "addresses" WHERE "id" = 17;
SELECT * from "addresses" WHERE "id" = 21;
Bằng cách sử dụng eager loading
để giải quyết vấn đề đó. Thay vì chúng ta dùng 10 query chúng ta có thể tối ưu nó bằng 2 query đơn giản. Điều này giúp thời gian truy xuất database giảm xuống và hiệu năng application được cải thiện.
Client.includes(:address).limit(10)
SQL:
SELECT * from "clients" LIMIT 10;
SELECT * from "addresses" WHERE "client_id" IN (7, 8, 10, 12, 13, 15, 16, 17, 21);
Đây là ví dụ điển hình của N + 1 query.
Sử dụng blank? hay empty? tốt hơn
Blank?
hay empty?
đều là cách để kiểm tra một mảng object có rỗng hay không?
# Using `blank?`
User.where(screen_name: ['user1','user2']).blank?
# 1. Queries database for all user data
# SELECT "users".* FROM "users" WHERE "users"."screen_name" IN ('user1','user2')
# 2. Loads users into an array
# [<#User:0x007fbf6413c510>,<#User:0x007fbf65ab1c70>]
# 3. Checks to see if the array size is zero
# => true
#Using `empty?`
User.where(screen_name: ['user1','user2').empty?
#1. Queries database for ONLY a count
#SELECT COUNT(*) FROM "users" WHERE "users"."screen_name" IN ('user1','user2')
#2. Checks to see if the count is zero
#=> true
blank?
sẽ load toàn bộ các phần tử thỏa mãn sau đó kiểm tra xem mảng có rỗng k?. Còn empty?
sẽ đếm số phần tử thỏa mãn sau đó sẽ kiểm tra tổng đó có bằng 0 hay k. Điều này không gây khác biệt với những database nhỏ (như development) nhưng nó nó làm nên khác biệt lớn với dữ liệu lớn (như production) .
Nên sử dụng
empty?
hoặcany?
thay vì sử dụngblank?
hoặcpresent?
Sử dụng map hay pluck
Giả sử ta muốn lấy trường screen_names
# Using `map?`
User.where(email: ['jane@example.com', 'john@example.com']).map(&:screen_name)
#1. Queries database for all user data
#SELECT "users".* FROM "users" WHERE "users"."email" IN ('jane@example.com','john@example.com')
#2. Loads users into an array
#[<#User:0x007fbf6413c510>,<#User:0x007fbf65ab1c70>]
#3. Iterates over users to collect screen_names
#['user1','user2']
# Using `pluck?`
User.where(email: ['jane@example.com', 'john@example.com']).pluck(:screen_name)
#1. Queries database for only screen_names
#SELECT "users"."screen_name" FROM "users" WHERE "users"."email" IN ('jane@example.com','john@example.com')
#2. Returns those screen_names in an array
#['user1','user2']
Sử dụng map
sẽ load các phần tử vào 1 array sau đó mới trả về screen_names. Pluck yêu cầu cơ sở dữ liệu trả về chính xác những gì nó cần và trả về một mảng giá trị kết quả. Một lần nữa ta có thể tăng performance một cách đáng kể khi dữ liệu User lớn
Nên sử dụng
pluck
thay vì dùngmap
để lấy dữ liệu
Sử dụng count, length hay size
Count, length, size
đều là cách để đếm số phần tử. Vậy cách nào dùng tốt hơn?
Count:
- Count đến số phần tử, sử dụng query: (SELECT COUNT(*) FROM…)
- Kết quả không được lưu trữ trong vòng đời của đối tượng nghĩa là mỗi lần chúng ta gọi phương thức này, truy vấn SQL sẽ được thực hiện lại
- Nó nhanh hơn so với sử dụng length
[19] pry(main)> a = User.all; nil
=> nil
[20] pry(main)> a.count
(0.3ms) SELECT COUNT(*) FROM `users` WHERE `users`.`deleted_at` IS NULL
=> 47666
[21] pry(main)> a.count
(0.1ms) SELECT COUNT(*) FROM `users` WHERE `users`.`deleted_at` IS NULL
=> 47666
Length:
- Trả về độ dài của 1 mảng các đối tượng mà k cần thực hiện truy vấn bổ sung, miễn là array đã có sẵn
- Kết quả được lưu trữ k cần thực hiện lại truy vấn
- Nhanh khi array đã được load sẵn
[11] pry(main)> a = User.all; nil
=> nil
[12] pry(main)> a.length
User Load (155.6ms) SELECT `users`.* FROM `users` WHERE `users`.`deleted_at` IS NULL
=> 47666
[13] pry(main)> a.length
=> 47666
Size:
- Là sự kết hợp giữa count và length
- Nếu array đã được load, sẽ đếm số phần tử mà k cần truy vấn thêm
[25] pry(main)> a = User.all; nil
=> nil
[26] pry(main)> a.count
(0.3ms) SELECT COUNT(*) FROM `users` WHERE `users`.`deleted_at` IS NULL
=> 47666
[27] pry(main)> a.count
(0.3ms) SELECT COUNT(*) FROM `users` WHERE `users`.`deleted_at` IS NULL
=> 47666
[28] pry(main)> a.size
(0.3ms) SELECT COUNT(*) FROM `users` WHERE `users`.`deleted_at` IS NULL
=> 47666
[29] pry(main)> a.to_a; nil
User Load (173.3ms) SELECT `users`.* FROM `users` WHERE `users`.`deleted_at` IS NULL
=> nil
[30] pry(main)> a.size
=> 47666
Vậy sử dụng size sẽ là sự lựa chọn tốt nhất và an toàn nhất Sử dụng câu lệnh nào cho tốt có thể tác động rất lớn đến hiệu suất ứng dụng của bạn. Và đây là sự khác biệt về hiệu suất của máy tính khi dữ liệu user lớn (16 nghìn người dùng)
user | system | total | real | |
---|---|---|---|---|
count | 0.010000 | 0.000000 | 0.010000 | 0.002989 |
length | 0.730000 | 0.060000 | 0.790000 | 0.846671 |
Chỉ với câu lệnh đơn giản nhưng có thể tác động nghiêm trọng với ưng dụng web của bạn.
uniq
Giả sử bạn muốn lấy danh sách user_id
chứa người dùng duy nhất từ bảng UserLikeJob
. Sử dụng pluck
trước chúng ta có thể dễ dàng lấy ra list user id. Kết hợp với uniq, ta có 2 cách có thể lấy được list user_id
UserLikeJob.pluck(:user_id).uniq
UserLikeJob.uniq.pluck(:user_id)
Chúng khác nhau ở cách lọc để lấy các user_id duy nhất. Ở trường hợp đầu tiên, pluck trả về 1 mảng user_id sau đó được lọc bằng Array#uniq. Ở trường hợp thứ 2 thì uniq thực sự là method của ActiveRecord nó sẽ thêm DISTINCT vào câu lệnh sql
SELECT DISTINCT `user_like_jobs`.`user_id` FROM `user_like_jobs`
Cơ sở dữ liệu sẽ nhanh hơn nếu có index ở cột user_id
find_each và each
Giả sử bạn cần phải thực hiện một số xử lý trên mỗi đối tượng, đối với tập dữ liệu nhỏ ta dùng each
có thể sử dụng hiệu quả:
Transaction.where(processed: false).each { |t| ... }
Nhưng với dữ liệu lớn có chục nghìn bản ghi. Ví dụ với .all.each toàn bộ kết quả sẽ phải được load vào bộ nhớ để lặp lại mảmng đó. Tại thời điểm này rất có thể bị thoát khỏi bộ nhớ
Để ngăn chặn điều đó ActiveRecord
cung cấp phương thức find_each
trong đó kết quả truy vấn sẽ được đặt theo đặt theo lô 1000, để toàn bộ tập kết quả không phải nạp vào bộ nhớ cùng một lúc. Chỉ khác nhau ở cách viết về mặt chức năng vẫn được đảm bảo. Bạn có thể tìm hiểu ActiveRecord::Batches#find_in_batches
Transaction.where(processed: false).find_each { |t| ... }
Hi vọng bài viết có thể giúp ích cho bạn.
Nguồn tham khảo
http://mensfeld.pl/2014/09/activerecord-count-vs-length-vs-size-and-what-will-happen-if-you-use-it-the-way-you-shouldnt/ https://hashrocket.com/blog/posts/rails-quick-tips-easy-activerecord-optimizations https://nguyenthethang.com/2013/08/04/keyword-active-record-la-gi-what-is-active-record/ http://www.webascender.com/Blog/ID/553/Rails-Tips-for-Speeding-up-ActiveRecord-Queries#.WNiG94h96kA
All rights reserved