Benchmark: preload vs. eager_load

ActiveRecord::QueryMethods#eager_load und ActiveRecord::QueryMethods#preload generieren unterschiedliches SQL. Entweder eine einzelne aber große Anfrage mit JOINs oder aber mehrere, kleine und optimierte Anfragen.
Im Falle, dass die Selektion nicht auf gejointe Tabellen referenzieren muss, sind beide Ansätze zielführend.
Die Frage ist, welcher Ansatz ist schneller?\

Behauptung

Das Parsen und Instantiieren von preload Ergebnissen sollte schneller sein, als von eager_load.
Zunächst die Testgrundlage:

class Order < ApplicationRecord
end

class User < ApplicationRecord
  has_many :orders
end

SQL mit eager_load:

SELECT "users"."id" AS t0_r0, "users"."email" AS t0_r1, "users"."created_at" AS t0_r2, "users"."updated_at" AS t0_r3, "orders"."id" AS t1_r0, "orders"."user_id" AS t1_r1, "orders"."product_id" AS t1_r2, "orders"."created_at" AS t1_r3, "orders"."updated_at" AS t1_r4
  FROM "users"
  LEFT OUTER JOIN "orders"
    ON "orders"."user_id" = "users"."id"
  WHERE "users"."created_at" = '2017-02-27 22:23:34';

SQL mit preload:

SELECT "users".* FROM "users";
SELECT "orders".* FROM "orders" WHERE "orders"."user_id" IN (1, 2, 3);

Benchmark

Der Benchmark test vergleicht eager_load mit preload:

require 'ips_benchmark_helper'

Benchmark.ips do |bm|
  bm.report('#eager_load') do
    User.eager_load(:orders)
  end

  bm.report('#preload') do
    User.preload(:orders)
  end

  bm.report('#includes') do
    User.includes(:orders)
  end
end

Zusätzlich wurden ActiveRecord::QueryMethods#includes ebenfalls in den Benchmark aufgenommen, da es auf preload und eager_load basiert.

Resultate

Die Ergebnisse:

Warming up --------------------------------------
         #eager_load     6.020k i/100ms
            #preload     6.326k i/100ms
           #includes     4.961k i/100ms
Calculating -------------------------------------
         #eager_load     71.768k (± 2.7%) i/s -    361.200k in   5.036819s
            #preload     72.981k (± 2.3%) i/s -    366.908k in   5.030214s
           #includes     58.485k (± 2.3%) i/s -    292.699k in   5.007564s

zeigen, dass preload tatsächlich im Vergleich zu eager_load eine bessere Performanz aufweist. Auch wenn der Unterschied nur marginal erscheint, ist zu erwarten, dass die Abstand größer wird, je mehr JOINs gemacht werden müssen.
Allerdins ist es überraschend, dass der overhead von includes 20% der benötigten Ressourcen ausmacht.

Fazit

Wenn es für die Anfragen syntaktisch unbedeutend ist, ob preload oder eager_load gemacht wird, dann sollte preload vorgezogen werden.
Außerdem sollte aus Gründen der Performanz auf includes nur zurückgegriffen werden, wenn erst zur Laufzeit ermittelt wird, ob der preload oder eager_load Ansatz angewendet wird.