11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Mongoid 全リレーション種別で生成されるクエリを調べたみた

Last updated at Posted at 2019-05-20

@koshi_life です。

Mongoidの各リレーションでどのようなクエリが発行されるか高速化するために興味があり、
Mongoidリレーションの公式ドキュメントを読んで、実験した内容の備忘です。

前提

  • Ruby 2.6.3
  • Rails 5.2.3
  • Mongo 4.0.3
  • Mongoid 7.0.2

Mongoidのリレーションは5種

記事投稿時点のMongoid(v7)では、
以下5種のリレーションがサポートされています。

mongoid-Relations より。

マクロ リレーション種別 データ保存先のコレクション
has_one - belongs_to 1:1 両コレクション
embeds_one - embedded_in 1:1 親のみ
has_many - belongs_to 1:N 両コレクション
embeds_many - embedded_in 1:N 親のみ
has_and_belongs_to_many N:N 両コレクション

embeds_one/embeds_many が Railsのリレーション にはなく Mongoid 特有の関連付けです。
Modelは分けたいが、参照/更新タイミングが一緒で別々のコレクションへのクエリを発行させたくない時に利用するリレーションと解釈しました。

リレーション定義例

適当な例として
フランチャイズチェーンのお店の売上、在庫、商品、オーナー、店長、バイトを管理するための各モデルを定義してみます。

関連図

qiita-20190520_1.png

お店モデル

app/models/store.rb
# お店
class Store
  include Mongoid::Document

  field :name, type: String

  has_one :manager
  embeds_one :owner

  has_many :sales
  has_many :stocks
  has_and_belongs_to_many :part_timers
end

人関連モデル

app/models/owner.rb
# オーナー
class Owner
  include Mongoid::Document

  field :name, type: String

  embedded_in :store
end
app/models/manager.rb
# 店長
class Manager
  include Mongoid::Document

  field :name, type: String

  belongs_to :store

  # Index貼るとよいかも (後述)
  index({ store_id: 1 })
end
app/models/part_timer.rb
# バイト
class PartTimer
  include Mongoid::Document

  field :name, type: String

  has_and_belongs_to_many :stores
end

商品、在庫、売上管理系モデル

app/models/product.rb
# 商品
class Product
  include Mongoid::Document

  field :name, type: String

  has_one :product_quantity
end
app/models/product_quantity.rb
# 商品と数量の組み合わせを持つ抽象モデル
class ProductQuantity
  include Mongoid::Document

  field :quantity, type: Integer

  belongs_to :product
end
app/models/stock.rb
# 在庫
class Stock < ProductQuantity
  include Mongoid::Document

  belongs_to :store

  # Index貼るとよいかも (後述)
  index({ _type: 1, store_id: 1, product_id: 1 })
end

app/models/sale.rb
# 売上
class Sale
  include Mongoid::Document

  field :price, type: Integer

  belongs_to :store
  embeds_many :sold_products

  # Index貼るとよいかも (後述)
  index({ store_id: 1 })
end
app/models/sold_product.rb
# 売った商品
class SoldProduct < ProductQuantity
  include Mongoid::Document

  field :unit_price, type: Integer

  embedded_in :sale
end

サンプルデータ

sample_data.rb
module SampleData
  STORE_DATA = [
    Store.new(id: 's001', name: 'Store A', part_timer_ids: %w[p001 p004], owner: Owner.new(name: 'Owner A')),
    Store.new(id: 's002', name: 'Store B', part_timer_ids: %w[p002], owner: Owner.new(name: 'Owner B')),
    Store.new(id: 's003', name: 'Store C', part_timer_ids: %w[p003 p004], owner: Owner.new(name: 'Owner C'))
  ]

  MANAGER_DATA = [
    Manager.new(id: 'm001', name: 'Manager A', store_id: 's001'),
    Manager.new(id: 'm002', name: 'Manager B', store_id: 's002'),
    Manager.new(id: 'm003', name: 'Manager C', store_id: 's003')
  ]

  PRODUCT_DATA = [
    Product.new(id: 'p001', name: 'Product A'),
    Product.new(id: 'p002', name: 'Product B'),
    Product.new(id: 'p003', name: 'Product C')
  ]

  SALE_DATA = [
    Sale.new(
      id: 'sa001001',
      store_id: 's001',
      price: 10,
      sold_products: [SoldProduct.new(product_id: 'p001', quantity: 1, unit_price: 10)]
    ),
    Sale.new(
      id: 'sa001002',
      store_id: 's001',
      price: 40,
      sold_products: [SoldProduct.new(product_id: 'p002', quantity: 2, unit_price: 20)]
    ),
    Sale.new(
      id: 'sa001003',
      store_id: 's001',
      price: 80,
      sold_products: [
        SoldProduct.new(product_id: 'p001', quantity: 2, unit_price: 10),
        SoldProduct.new(product_id: 'p003', quantity: 2, unit_price: 30)
      ]
    )
  ]

  STOCK_DATA = [
    Stock.new(id: 'st001001', product_id: 'p001', quantity: 10, store_id: 's001'),
    Stock.new(id: 'st001002', product_id: 'p002', quantity: 10, store_id: 's001'),
    Stock.new(id: 'st001003', product_id: 'p003', quantity: 10, store_id: 's001'),
    Stock.new(id: 'st002001', product_id: 'p001', quantity: 20, store_id: 's002'),
    Stock.new(id: 'st002002', product_id: 'p002', quantity: 20, store_id: 's002'),
    Stock.new(id: 'st002003', product_id: 'p003', quantity: 20, store_id: 's002'),
    Stock.new(id: 'st003001', product_id: 'p001', quantity: 30, store_id: 's003'),
    Stock.new(id: 'st003002', product_id: 'p002', quantity: 30, store_id: 's003'),
    Stock.new(id: 's003003', product_id: 'p003', quantity: 30, store_id: 's003')
  ]

  PART_TIMER_DATA = [
    PartTimer.new(id: 'p001', name: 'Part A', store_ids: %w[s001 s002]),
    PartTimer.new(id: 'p002', name: 'Part B', store_ids: %w[s002]),
    PartTimer.new(id: 'p003', name: 'Part C', store_ids: %w[s003]),
    PartTimer.new(id: 'p004', name: 'Part D', store_ids: %w[s001 s003])
  ]

  def initialize()
    # Drop Collections
    Store.collection.drop
    Manager.collection.drop
    Product.collection.drop
    Sale.collection.drop
    SoldProduct.collection.drop
    Stock.collection.drop
    PartTimer.collection.drop
    # Insert Data
    STORE_DATA.each(&:save!)
    MANAGER_DATA.each(&:save!)
    PRODUCT_DATA.each(&:save!)
    STOCK_DATA.each(&:save!)
    SALE_DATA.each(&:save!)
    PART_TIMER_DATA.each(&:save!)
  end
  module_function :initialize
end

クエリの確認1

Storeモデルから各モデルへリレーション経由でアクセスした時のMongoへのクエリを確認。

ruby

# Storeの全要素を一括で取得 (Mongoクエリあり★☆★)
irb:001> stores = Store.all.to_a
=> [#<Store _id: s001, name: "Store A", part_timer_ids: ["p001", "p004"]>, #<Store _id: s002, name: "Store B", part_timer_ids: ["p002", "p001"]>, #<Store _id: s003, name: "Store C", part_timer_ids: ["p003", "p004"]>]
irb:002> store1 = stores[0]

# has_one (Mongoクエリあり★☆★)
irb:003> store1.manager
=> #<Manager _id: m001, name: "Manager A", store_id: "s001">

# has_many (Mongoクエリあり★☆★)
irb:004> store1.stocks

# has_many (Mongoクエリあり★☆★)
irb:005> s1_sales = store1.sales
=> [#<Sale _id: sa001001, price: 10, store_id: "s001">, #<Sale _id: sa001002, price: 40, store_id: "s001">, #<Sale _id: sa001003, price: 80, store_id: "s001">]
irb:006> s1_sale1 = s1_sales[0]

# embeds_many (Mongoクエリなし)
irb:007> s1_sale1.sold_products
=> [#<SoldProduct _id: 5ce29a8f48cca4564c626022, quantity: 1, product_id: "p001", _type: "SoldProduct", unit_price: 10>]

# embeds_one (Mongoクエリなし)
irb:008> store1.owner
=> #<Owner _id: 5ce29a8f48cca4564c62601f, name: "Owner A">

# has_and_belongs_to_many (Mongoクエリあり★☆★)
irb:009> store1.part_timers
=> [#<PartTimer _id: p001, name: "Part A", store_ids: ["s001", "s002"]>, #<PartTimer _id: p004, name: "Part D", store_ids: ["s001", "s003"]>]

Mongo クエリ内容

irb No ruby Mongoへのクエリ インデックス利用有無
001 Store.all.to_a {"find"=>"stores", "filter"=>{}, ...}
003 store1.manager {"find"=>"managers", "filter"=>{"store_id"=>"s001"}, ...}
004 store1.stocks {"find"=>"product_quantities", "filter"=>{"store_id"=>"s001", "_type"=>"Stock", ...}
005 store1.sales {"find"=>"sales", "filter"=>{"store_id"=>"s001"}, ...}
009 store1.part_timers {"find"=>"part_timers", "filter"=>{"$and"=>[{"_id"=>{"$in"=>["p001", "p004"]}}]}, ...}

確認できたこと

  • embeds_one/embeds_many は取得したドキュメント内に関連データが内包されているので追加で別コレクションへのクエリは発行されない
  • has_one/has_many-belongs_to のリレーションは、デフォルトではインデックスが効かないクエリ(フルスキャン)が発行されるので、使用頻度/データ量によってはインデックス作成を検討したほうがよい。
  • モデルを継承した場合は、継承元のコレクションに対して_type属性付きのドキュメントが作られる。(例だとStockコレクションではなくProductQuantitiesコレクションに_type:Stockで作成された)

クエリの確認2 (N+1問題は includes を用いる)

Mongoid::Criteria#includes を用いると一括でリレーションの関連データを取得することができる。

No.001 の rubyコードを以下のように変更すると、それ以降のNo.003,4,5,9 のMongoへのアクセスは発生しない。

.rb
irb:001> Store.all.includes(:manager, :stocks, :sales, :part_timers).to_a
=> [#<Store _id: s001, name: "Store A", part_timer_ids: ["p001", "p004"]>, #<Store _id: s002, name: "Store B", part_timer_ids: ["p002", "p001"]>, #<Store _id: s003, name: "Store C", part_timer_ids: ["p003", "p004"]>]

Mongoのクエリ内容

.js
{"find"=>"stores", "filter"=>{}, ...}
{"find"=>"managers", "filter"=>{"store_id"=>{"$in"=>["s001", "s002", "s003"]}}, ...}
{"find"=>"product_quantities", "filter"=>{"store_id"=>{"$in"=>["s001", "s002", "s003"]}, "_type"=>"Stock"}, ...}
{"find"=>"sales", "filter"=>{"store_id"=>{"$in"=>["s001", "s002", "s003"]}}, ...}
{"find"=>"part_timers", "filter"=>{"_id"=>{"$in"=>["p001", "p004", "p002", "p003"]}}, ...}

includesで指定した各リレーション先のコレクションに対して、inであらかじめ全取得している。

クエリの確認3 pluck

Mongoid::Contextual::Mongo#pluck リレーション関係ないが、効率よいメソッドらしいので。確認してみる。

.rb
Sale.all.pluck(:id, :price)
.js
{"find"=>"sales", "filter"=>{}, "projection"=>{"_id"=>1, "price"=>1}, ...}

射影(projection)でメモリ展開領域が少なくなるのが効率が良い!?と解釈。

参考

11
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?