+3

Các mẹo tăng tốc độ truy vấn Active Record trong Rails

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ặc any? thay vì sử dụng blank?hoặc present?

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ùng map để 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

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