12/08/2018, 15:10

Khi nào includes làm một join, và khi nào nó làm một truy vấn thứ hai

Một ngày nào đó, chúng ta làm việc cho ứng dụng Rails của chúng ta và chúng ta gặp phải vấn đề khi một truy vấn ActiveRecord với một mệnh đề includes đã thực hiện join, chúng ta cảm thấy nó sẽ hiệu quả hơn và cải thiện hiệu suất để thực hiện truy vấn thứ hai. Chúng ta đã có nghĩ rằng ActiveRecord ...

Một ngày nào đó, chúng ta làm việc cho ứng dụng Rails của chúng ta và chúng ta gặp phải vấn đề khi một truy vấn ActiveRecord với một mệnh đề includes đã thực hiện join, chúng ta cảm thấy nó sẽ hiệu quả hơn và cải thiện hiệu suất để thực hiện truy vấn thứ hai. Chúng ta đã có nghĩ rằng ActiveRecord sẽ làm việc này, và chắc chắn là sẽ phỏng đoán đây là một sự lựa chọn thông minh. Hãy khai thác để xem khi sử dụng includes thực hiện một truy vấn thứ hai, khi nó thực hiện một join, và tại sao chúng ta đã ngớ ngẩn để nghĩ rằng chúng ta đã thông minh hơn ActiveRecord.

Chúng ta sẽ sử dụng hai mô hình này, với một cặp đơn giản has_many và belong_to:

class User < ActiveRecord::Base
  has_many :cards
end

class Card < ActiveRecord::Base
  belongs_to :user

  def self.with_phrase(phrase)
    where(phrase: phrase)
  end
end

Nếu chúng ta chỉ đơn giản includes (: cards) từ mối quan hệ user, chúng ta nhận được hai truy vấn:

User.where(id: 1).includes(:cards)
SELECT "users".*
FROM "users"
WHERE "users"."id" = 1

SELECT "cards".*
FROM "cards"
WHERE "cards"."user_id" IN (1)

Nếu chúng ta join vào bảng, thì đó là một truy vấn với một INNER JOIN:

User.where(id: 1).joins(:cards).includes(:cards)
SELECT
  "users"."id" AS t0_r0,
  "users"."name" AS t0_r1,
  "users"."created_at" AS t0_r2,
  "users"."updated_at" AS t0_r3,
  "cards"."id" AS t1_r0,
  "cards"."user_id" AS t1_r1,
  "cards"."phrase" AS t1_r2,
  "cards"."created_at" AS t1_r3,
  "cards"."updated_at" AS t1_r4
FROM "users"
  INNER JOIN "cards"
    ON "cards"."user_id" = "users"."id"
WHERE "users"."id" = 1

Điều này có ý nghĩa - chúng ta không còn chỉ muốn tải cards, nhưng bây giờ chúng ta chỉ tải user có card (và chúng ta cũng đang tải card). ActiveRecord cho chúng ta tầm quan trọng của user và sự tải mong muốn thông qua một truy vấn SQL, và điều đó khá gọn gàng. Nếu chúng ta tham chiếu bảng, sau đó chúng ta có được một truy vấn - với LEFT OUTER JOIN thay vì INNER JOIN. Điều này cho ActiveRecord rằng chúng ta sẽ được tham chiếu bảng đó trong truy vấn của chúng tôi. Nếu chúng ta không nói cho ActiveRecord và cố gắng tham chiếu bảng đó thì 1 vài điều tồi tệ xảy ra:

User.includes(:cards).merge(Card.with_phrase("banana"))
PG::UndefinedTable: ERROR:  missing FROM-clause entry for table "cards"
LINE 1: SELECT "users".* FROM "users" WHERE "cards"."phrase" = $1
                                           ^

Dưới đây là ví dụ về việc sử dụng tham chiếu chính xác. Bây giờ SQL được tạo ra có thể phạm vi dữ liệu trong bảng thẻ đúng cách.

User.includes(:cards).references(:cards).merge(Card.with_phrase("hammocks"))
SELECT
  "users"."id" AS t0_r0,
  "users"."name" AS t0_r1,
  "users"."created_at" AS t0_r2,
  "users"."updated_at" AS t0_r3,
  "cards"."id" AS t1_r0,
  "cards"."user_id" AS t1_r1,
  "cards"."phrase" AS t1_r2,
  "cards"."created_at" AS t1_r3,
  "cards"."updated_at" AS t1_r4
FROM "users"
  LEFT OUTER JOIN "cards"
    ON "cards"."user_id" = "users"."id"
WHERE "cards"."phrase" = "hammocks"

Vì vậy, trở lại với vấn đề chúng ta đã trải qua. Chúng ta đã join từ một bảng, thông qua một vài, vào một bảng có số lượng lớn bảng ghi. Truy vấn này không thể chấp nhận được. Chúng ta đã tìm kiếm và không thành công trong việc tìm kiếm cơ sở dữ liệu-tối ưu hóa, vì vậy chúng ta đã đi với chỉ cần phá vỡ các truy vấn thành từng phần. Ví dụ: hãy sửa đổi kiến trúc ví dụ của chúng ta và giới thiệu một bảng join:

class User < ActiveRecord::Base
  has_many :hands
  has_many :cards, through: :hands
end

class Hand < ActiveRecord::Base
  belongs_to :user
  belongs_to :card

  def self.with_cards(cards)
    where(card_id: cards)
  end
end

class Card < ActiveRecord::Base
  def self.with_phrase(phrase)
    where(phrase: phrase)
  end
end

Bây giờ nếu chúng ta chỉ có 5 uers, nhưng chúng ta có 10 tỷ cards, trước tiên chúng ta có thể muốn mở rộng phạm vi cho các cards có liên quan, sau đó lấy user và chỉ join bảng Hand - thay vì đi qua bảng Card. Chúng ta có thể làm điều này với hai truy vấn:

cards = Card.with_phrase('hammocks')
users = User.joins(:hands).merge(Hand.with_cards(cards))

Điều này đơn giản hóa SQL cho mỗi truy vấn và có thể nhanh hơn.

Cảm ơn bạn đã đọc bài! nguồn : https://www.foraker.com/blog/active-record-includes-query-logic

0