Find() generuje dodatkowe zapytanie

Chcę pobierać jak najwięcej danych jak najmniejszą ilością pytań SQL’owych. Bez użycia includes() generują mi się trzy pytania. Z includes zostają mi nadal dwa zapytania, a tak na chłopski rozum powinno zostać tylko jedno.

Kod:

  def index
    @brand = Brand.includes(:bodies).find(params[:brand_id])
    @bodies = @brand.bodies
  end

I same SQL’e:

Parameters: {"brand_id"=>"2"}
  Brand Load (0.4ms)  SELECT  "brands".* FROM "brands" WHERE "brands"."id" = $1 LIMIT 1  [["id", 2]]
  Body Load (0.6ms)  SELECT "bodies".* FROM "bodies" WHERE "bodies"."brand_id" IN (2)

Mimo tego, że prosty JOIN:

SELECT bodies.id, bodies.name, brands.name AS brand_name, brands.id AS brand_id FROM bodies JOIN brands ON bodies.brand_id = brands.id WHERE brand_id = 2

Daje dokładnie takie wyniki jakie potrzebuję:

ID      NAME    BRAND_NAME   BRAND_ID
1	TEST	1231         2
2	12345	1231         2
3	KUPA	1231         2

Co muszę zrobić żeby RoR robiło tylko jedno pytanie do bazy danych?

UPDATE: Zmieniłem tytuł, wytłumaczenie w 5 poście.

warto przeczytać.

Brand.eager_load(:bodies).find(params[:id])

powinno zadziałać. Chyba :wink:

Dzięki za linka, bo to dobrze opisane jest.

Nie mniej jednak eager_load nadal robi dwa pytania tylko są po prostu o wiele dłuższe :wink:

Ale na tym etapie mnie po prostu ciekawi to. Nie będę próbował na siłę optymalizować tego, tylko przyzwyczajony do select_related() w Django jestem zdziwiony, że nie mogę tego samego w RoR osiągnąć, ale wiem że to brak doświadczenia a nie ograniczenia ORM’a.

Co w tym dziwnego, skoro w kontrolerze jawnie wywołujesz 2 zapytania? Najpierw instancjonujesz @brand, a później kolekcję @bodies. Jeśli potrzebujesz wyłącznie bodies, to wystarczy

@bodies = Body.where(brand_id: params[:brand_id])

Potrzebuję i brand i bodies, bo bodies dla danego brand może w ogóle nie być.

Potrzebuję brand, ale taki, który już będzie miał bodies wyciągnięte już z bazy. To znaczy takie query, które zwróci mi taki brand, gdzie:

brand.bodies == #<ActiveRecord::Associations::CollectionProxy [#<Body ...>, #<Body ...>]>

Ale bez tworzenia drugiego query. Bo to jest zupełnie bez sensu, żeby dla każdej asosjacji tworzyć osobne query.

Nie potrafię zrozumieć pewnego mechanizmu.

brand = Brand.eager_load(:bodies).find(2)

SQL (1.1ms)  SELECT  DISTINCT "brands"."id" FROM ...;
SQL (1.1ms)  SELECT "brands"."id" AS t0_r0, "brands"."name  AS ...;

irb(main):059:0> brand.name
=> "1231"
irb(main):060:0> brand.bodies.first.name
=> "12345"

Powyżej dwa pytania do bazy

brand = Brand.eager_load(:bodies).all()

SQL (0.7ms)  SELECT "brands"."id" AS t0_r0, "brands"."name" AS ....;

irb(main):063:0> brand.first.name
=> "1231"
irb(main):064:0> brand.first.bodies.first.name
=> "12345"

Powyższe jedno pytanie.

Nie potrafię zrozumieć dlaczego dodanie warunku WHERE sprawia że tworzy się dodatkowe pytanie.

UPDATE:
A żeby było jeszcze śmieszniej

Brand.eager_load(:bodies).where(id: 2)

Robi dokładnie jedno pytanie, a różnica jest tylko taka, że zwraca kolekcję obiektów Brand, zamiast jednego obiektu (ale to jest dla mnie oczywiste i z tym nie mam problemu)

Dlaczego find() generuje dodatkowe pytanie?

Dlatego, że wtedy instancjonujesz obiekt w pamięci, a żeby pobrać dane do niego robiony jest select * from brands ...
Dwa zapytania mają większy sens niż jedno, bo w jednym zapytaniu dostajesz tablicę dwa razy większą niż potrzebujesz, kolumny brand_name i brand_id mają takie same wartości dla każdego wyciągniętego rekordu.
Użycie all jest jeszcze słabsze, bo wtedy wyciągasz do pamięci jedną wielką tablicę wszystkich brands i bodies.

Ok. trafia to do mnie :slight_smile: Dzięki za pomoc.

Od Rails 4 all zwraca relację, nie tablicę.

Pogrzebałem trochę w konsoli i faktycznie wydaje się, że AR mógłby lepiej obsługiwać eager_load’a dla pojedynczego obiektu.
Co do

I dlaczego taki obiekt nie mógłby zostać utworzony na podstawie drugiego zapytania skoro wszystkie atrybuty są wyciągnięte z bazy danych?

W dwóch zapytaniach też dostajesz te informacje. Jeśli tablice są małe, to nie widzę powodu dla którego jeden obiekt i kilka obiektów, które eager loadujesz nie mogłyby zostać załadowane dzięki jednemu zapytaniu do bazy danych.

Wydaje się, że jeśli wymuszasz joina, AR dla każdego wiersza z wyniku instancjonuje obiekt odpowiadający tabeli “po lewej stronie”, w tym przypadku Brand, por. http://stackoverflow.com/questions/6246826/how-do-i-avoid-multiple-queries-with-include-in-rails

Miałem na myśli złączoną tabelę z bazy danych.

A jeśli nie ma żadnego powiązanego obiektu body, a potrzebujesz brand?

A jeśli są duże? 2 szybkie zapytania przeważnie są wydajniejsze od 1, który robi joina dwóch dużych tabel. Poza tym jak już wspominałem wyżej, w jednym zapytaniu dostajesz dane redundantne.

1 Like

Przy próbie wymuszania jednego zapytania, rzeczywiście Railsy tworzą więcej obiektów w pamięci niż to jest potrzebne. A to jedno zapytanie więcej w takim przypadku to jest naprawdę nic. Tak jak pisałem, po prosto nie rozumiałem dlaczego aż tak to działa w Railsach.
Tu jest przykład tego co się dzieje:

irb(main):021:0* brand = Brand.eager_load(:bodies).where(id: 2)
  SQL (1.1ms)  SELECT "brands"."id" AS t0_r0, "brands"."name" AS t0_r1, "brands"."created_at" AS t0_r2, "brands"."updated_at" AS t0_r3, "bodies"."id" AS t1_r0, "bodies"."name" AS t1_r1, "bodies"."brand_id" AS t1_r2, "bodies"."created_at" AS t1_r3, "bodies"."updated_at" AS t1_r4 FROM "brands" LEFT OUTER JOIN "bodies" ON "bodies"."brand_id" = "brands"."id" WHERE "brands"."id" = $1  [["id", 2]]
=> #<ActiveRecord::Relation [#<Brand id: 2, name: "1231", created_at: "2016-03-09 23:20:20", updated_at: "2016-03-09 23:20:20">]>
irb(main):022:0> brand.object_id
=> 47147137111880
irb(main):023:0> brand.first.bodies.first.brand.object_id
=> 47147137084860

Brand to nowy obiekt dla każdej asosjacji. Tak samo jest dla where i all.

irb(main):024:0> brand = Brand.eager_load(:bodies).find(2)
  SQL (1.2ms)  SELECT  DISTINCT "brands"."id" FROM "brands" LEFT OUTER JOIN "bodies" ON "bodies"."brand_id" = "brands"."id" WHERE "brands"."id" = $1 LIMIT 1  [["id", 2]]
  SQL (1.0ms)  SELECT "brands"."id" AS t0_r0, "brands"."name" AS t0_r1, "brands"."created_at" AS t0_r2, "brands"."updated_at" AS t0_r3, "bodies"."id" AS t1_r0, "bodies"."name" AS t1_r1, "bodies"."brand_id" AS t1_r2, "bodies"."created_at" AS t1_r3, "bodies"."updated_at" AS t1_r4 FROM "brands" LEFT OUTER JOIN "bodies" ON "bodies"."brand_id" = "brands"."id" WHERE "brands"."id" = $1 AND "brands"."id" IN (2)  [["id", 2]]
=> #<Brand id: 2, name: "1231", created_at: "2016-03-09 23:20:20", updated_at: "2016-03-09 23:20:20">
irb(main):025:0> brand.object_id
=> 47147136924460
irb(main):026:0> brand.bodies.first.brand.object_id
=> 47147136924460

Ten sam obiekt nadrzędny jak i w każdej asosjacji.

Jeszcze raz dzięki za wyjaśnienia.

Pozwolę sobie też dodać jedną odpowiedź. Mam sporo asosojacji, bardzo prostych has_many i belongs_tu, to jest taka prosta drzewiasta struktura danych.

  def index
    @dumps = Dump.eager_load(ecu: { engine: { body: :brand } }).all
  end

Zwraca mi de facto dwa obiekty, tyle mam teraz dla najprostszych testów. Robi to w jednym zapytaniu:

  SQL (3.3ms)  SELECT "dumps"."id" AS ...

Zwracam uwagę też na czas samego slq’a! Poniżej jak to wygląda od strony performance’u

DumpTableTest#test_dump_table (290 ms warmup)
           wall_time: 30 ms
              memory: 0 Bytes
        process_time: 35 ms
             objects: 4,733

I dla porównania

  @dumps = Dump.includes(ecu: { engine: { body: :brand } }).all

  Dump Load (0.4ms)  SELECT "dumps".* FROM "dumps";
  Ecu Load (0.4ms)  SELECT "ecus".* FROM "ecus" WHERE "ecus"."id" IN (1);
  Engine Load (0.4ms)  SELECT "engines".* FROM "engines" WHERE "engines"."id" IN (1);
  Body Load (0.5ms)  SELECT "bodies".* FROM "bodies" WHERE "bodies"."id" IN (2);
  Brand Load (0.7ms)  SELECT "brands".* FROM "brands" WHERE "brands"."id" IN (2);
  
DumpTableTest#test_dump_table (249 ms warmup)
           wall_time: 19 ms
              memory: 0 Bytes
        process_time: 24 ms
             objects: 3,238 

Oraz

  @dumps = Dump.all
  
  Dump Load (0.4ms)  SELECT "dumps".* FROM "dumps";
  Ecu Load (0.2ms)  SELECT "ecus".* FROM "ecus" WHERE "ecus"."id" IN (1);
  Engine Load (0.3ms)  SELECT "engines".* FROM "engines" WHERE "engines"."id" IN (1);
  Body Load (0.2ms)  SELECT "bodies".* FROM "bodies" WHERE "bodies"."id" IN (2);
  Brand Load (0.2ms)  SELECT "brands".* FROM "brands" WHERE "brands"."id" IN (2);
  
DumpTableTest#test_dump_table (245 ms warmup)
           wall_time: 19 ms
              memory: 0 Bytes
        process_time: 25 ms
             objects: 3,164

Mam za małą próbkę danych póki co. Ale wychodzi na to, że próbując optymalizować SQL’a dałem się złapać w pułapkę :slight_smile:

W tamtym SQL’u AR używa left join’a, więc brand w dalszym ciągu spokojnie może sobie zainicjalizować.

Tak, jeśli są duże, to nie są małe i to co napisałem (a zacząłem od “jeśli są małe”) nie ma zastosowania.

Anyway, autor chciał się dowiedzieć jak osiągnąć to co chciał jednym zapytaniem, prawie się da :wink: