czwartek, 17 listopada 2016

Lateral join w PostgreSQLu

Od wersji 9.3 w zapytaniach typu SELECT pojawiło się nowe słowo kluczowe LATERAL. Główny format zapytania przyjmuję postać:

SELECT * FROM something [INNER/LEFT] JOIN LATERAL subquery;

Dla obrazowania problemu stworzyłam kilka małych tabel: users (imię dziecka z informacją o kraju z którego pochodzi), toys (nazwa zabawi z jego ceną), user_to_toys (połączenie dwóch poprzednich tabel relacją wiele do wielu - które dziecko ma jaką zabawkę), country_rise (procentowa wartość podwyżki cen zabawek dla danego kraju).


Interesują mnie następujące informacje:
  • które dziecko jakie dostało zabawki, 
  • ile było tych zabawek, 
  • ile wyniósł koszt kupionych zabawek,
  • ile rodzice by zapłacili po podwyżce.

W głównym podzapytaniu wyciągam informacje o dzieciach, zabawkach i sumie obecnych kosztów tych zabawek:

select 
     u.id, 
     u.name, 
     u.country, 
     string_agg(t.name, ',') as toys_names, 
     sum(t.price) as price_sum, 
     count(t.id) as count_toys 
   from users u 
   LEFT JOIN user_to_toy ut ON u.id = ut.userid 
   LEFT JOIN toys t ON t.id = ut.toyid 
   group by u.id, u.name, u.country

Podwyżkę kosztów chce obliczyć JOINując odpowiednią tabelę, która także jest podzapytaniem:

 select 
     cr.rise, 
     round( CAST(float8 (foo1.price_sum+(foo1.price_sum*cr.rise)/100.0) as numeric),2) as new_price 
   from country_rise cr 
   where cr.country = foo1.country

Oto całe moje zapytanie:

test=# select 
   foo1.name, 
   foo1.toys_names, 
   foo1.price_sum, 
   foo1.count_toys, 
   foo2.rise || '%' as rise, 
   foo2.new_price 
from (
   select 
     u.id, 
     u.name, 
     u.country, 
     string_agg(t.name, ',') as toys_names, 
     sum(t.price) as price_sum, 
     count(t.id) as count_toys 
   from users u 
   LEFT JOIN user_to_toy ut ON u.id = ut.userid 
   LEFT JOIN toys t ON t.id = ut.toyid 
   group by u.id, u.name, u.country
) foo1 
JOIN (
   select 
     cr.rise, 
     round( CAST(float8 (foo1.price_sum+(foo1.price_sum*cr.rise)/100.0) as numeric),2) as new_price 
   from country_rise cr 
   where cr.country = foo1.country
) foo2 ON country;
ERROR:  invalid reference to FROM-clause entry for table "foo1"
LINE 1: ...y) foo1 JOIN (select cr.rise, round( CAST(float8 (foo1.price...
                                                             ^
HINT:  There is an entry for table "foo1", but it cannot be referenced from this part of the query.


Wyświetlany błąd, wynika z tego że nie możemy odwoływać się do tabel zewnętrznych, które są podzapytaniami w łączeniach. W takim przypadku musimy użyć właśnie LATERAL. Bez tego słowa kluczowego, każde zapytanie jest oceniane niezależnie i nie możemy się odwoływać do żadnego elementu w FROM.

A to już poprawione nieco powyższe zapytanie:

test=# select 
  foo1.name,
  foo1.toys_names,
  foo1.price_sum,
  foo1.count_toys,
  foo2.rise || '%' as rise,
  foo2.new_price 
from (
   select 
      u.id, 
      u.name, 
      u.country, 
      string_agg(t.name, ',') as toys_names, 
      sum(t.price) as price_sum, 
      count(t.id) as count_toys 
   from users u 
   LEFT JOIN user_to_toy ut ON u.id = ut.userid 
   LEFT JOIN toys t ON t.id = ut.toyid 
   group by u.id, u.name, u.country
) foo1, 
LATERAL (
   select 
      cr.rise, 
      round( CAST(float8 (foo1.price_sum+(foo1.price_sum*cr.rise)/100.0) as numeric),2) as new_price 
   from country_rise cr 
   where cr.country = foo1.country) foo2;
 name  |                      toys_names                      | price_sum | count_toys | rise | new_price 
-------+------------------------------------------------------+-----------+------------+------+-----------
 Kasia | Kolorowanka                                          |         6 |          1 | 10%  |      6.60
 Marta |                                                      |           |          0 | 10%  |          
 Ula   | Pluszowa Żyrafa,Pluszowa Żabka,Samochodzik,Gryzaczek |        69 |          4 | 10%  |     75.90
 Beata | Kolorowanka,Pluszowa Żyrafa                          |        16 |          2 | 10%  |     17.60
 Ela   | Gryzaczek,Pluszowa Żyrafa                            |        15 |          2 | 35%  |     20.25
 Kuba  | Pluszowa Żabka                                       |        20 |          1 | 35%  |     27.00
 Ola   | Samochodzik,Pluszowa Żabka                           |        54 |          2 | 50%  |     81.00
(7 rows)


Według dokumentacji (którą możecie przeczytać pod tym linkiem), teraz możemy się odwoływać w sub-SELECT do elementów kolumn z FROM, które pojawiają się przed nim w tej liście (każdy wiersz z tabeli źródłowej (FROM) jest wykorzystywany do oszacowania wiersza w LATERAL. Utworzony w ten sposób wiersz(e) są joinowane). 

Sprawdźmy jeszcze koszt takiego zapytania:

test=# explain select foo1.name, foo1.toys_names, foo1.price_sum, foo1.count_toys, foo2.rise || '%' as rise, foo2.new_price 
test-# from (
test(#    select 
test(#       u.id, 
test(#       u.name, 
test(#       u.country, 
test(#       string_agg(t.name, ',') as toys_names, 
test(#       sum(t.price) as price_sum, 
test(#       count(t.id) as count_toys 
test(#    from users u 
test(#    LEFT JOIN user_to_toy ut ON u.id = ut.userid 
test(#    LEFT JOIN toys t ON t.id = ut.toyid 
test(#    group by u.id, u.name, u.country
test(# ) foo1, 
test-# LATERAL (
test(#    select 
test(#       cr.rise, 
test(#       round( CAST(float8 (foo1.price_sum+(foo1.price_sum*cr.rise)/100.0) as numeric),2) as new_price 
test(#    from country_rise cr 
test(#    where cr.country = foo1.country) foo2;
                                              QUERY PLAN                                              
------------------------------------------------------------------------------------------------------
 Hash Join  (cost=124.18..199.38 rows=1141 width=568)
   Hash Cond: (cr.country = foo1.country)
   ->  Seq Scan on country_rise cr  (cost=0.00..26.30 rows=1630 width=16)
   ->  Hash  (cost=122.43..122.43 rows=140 width=576)
         ->  Subquery Scan on foo1  (cost=119.28..122.43 rows=140 width=576)
               ->  HashAggregate  (cost=119.28..121.03 rows=140 width=1056)
                     Group Key: u.id, u.name, u.country
                     ->  Hash Left Join  (cost=26.30..96.81 rows=1498 width=1056)
                           Hash Cond: (ut.toyid = t.id)
                           ->  Hash Right Join  (cost=13.15..67.56 rows=1498 width=536)
                                 Hash Cond: (ut.userid = u.id)
                                 ->  Seq Scan on user_to_toy ut  (cost=0.00..31.40 rows=2140 width=8)
                                 ->  Hash  (cost=11.40..11.40 rows=140 width=532)
                                       ->  Seq Scan on users u  (cost=0.00..11.40 rows=140 width=532)
                           ->  Hash  (cost=11.40..11.40 rows=140 width=524)
                                 ->  Seq Scan on toys t  (cost=0.00..11.40 rows=140 width=524)
(16 rows) 

Poprawione nieco zapytanie (tak abyśmy mogli zobaczyć jakieś wyniki i ocenić jego koszt) bez użycia LATERAL:

test=# select name, toys_names, price_sum, count_toys, rise || '%' as rise, round( CAST(float8 (price_sum+(price_sum*rise)/100.0) as numeric),2) as new_price 
from (
   select 
      u.id, 
      u.name, 
      u.country, 
      string_agg(t.name, ',') as toys_names, 
      sum(t.price) as price_sum, 
      count(t.id) as count_toys, rise 
   from users u 
   LEFT JOIN user_to_toy ut ON u.id = ut.userid 
   LEFT JOIN toys t ON t.id = ut.toyid 
   LEFT JOIN country_rise cr ON  cr.country = u.country 
   group by u.id, u.name, u.country, cr.rise) foo1;
 name  |                      toys_names                      | price_sum | count_toys | rise | new_price 
-------+------------------------------------------------------+-----------+------------+------+-----------
 Ula   | Samochodzik,Pluszowa Żyrafa,Gryzaczek,Pluszowa Żabka |        69 |          4 | 10%  |     75.90
 Ola   | Pluszowa Żabka,Samochodzik                           |        54 |          2 | 50%  |     81.00
 Ela   | Pluszowa Żyrafa,Gryzaczek                            |        15 |          2 | 35%  |     20.25
 Kuba  | Pluszowa Żabka                                       |        20 |          1 | 35%  |     27.00
 Kasia | Kolorowanka                                          |         6 |          1 | 10%  |      6.60
 Beata | Kolorowanka,Pluszowa Żyrafa                          |        16 |          2 | 10%  |     17.60
 Marta |                                                      |           |          0 | 10%  |          
(7 rows)

test=# explain select name, toys_names, price_sum, count_toys, rise || '%' as rise, round( CAST(float8 (price_sum+(price_sum*rise)/100.0) as numeric),2) as new_price 
from (
   select 
      u.id, 
      u.name, 
      u.country, 
      string_agg(t.name, ',') as toys_names, 
      sum(t.price) as price_sum, 
      count(t.id) as count_toys, rise 
   from users u 
   LEFT JOIN user_to_toy ut ON u.id = ut.userid 
   LEFT JOIN toys t ON t.id = ut.toyid 
   LEFT JOIN country_rise cr ON  cr.country = u.country 
   group by u.id, u.name, u.country, cr.rise) foo1;
                                               QUERY PLAN                                               
--------------------------------------------------------------------------------------------------------
 Subquery Scan on foo1  (cost=6793.36..7647.99 rows=12209 width=568)
   ->  GroupAggregate  (cost=6793.36..7190.15 rows=12209 width=1060)
         Group Key: u.id, u.name, u.country, cr.rise
         ->  Sort  (cost=6793.36..6823.88 rows=12209 width=1060)
               Sort Key: u.id, u.name, u.country, cr.rise
               ->  Hash Right Join  (cost=84.39..287.63 rows=12209 width=1060)
                     Hash Cond: (ut.userid = u.id)
                     ->  Hash Left Join  (cost=13.15..67.56 rows=2140 width=528)
                           Hash Cond: (ut.toyid = t.id)
                           ->  Seq Scan on user_to_toy ut  (cost=0.00..31.40 rows=2140 width=8)
                           ->  Hash  (cost=11.40..11.40 rows=140 width=524)
                                 ->  Seq Scan on toys t  (cost=0.00..11.40 rows=140 width=524)
                     ->  Hash  (cost=56.97..56.97 rows=1141 width=536)
                           ->  Hash Right Join  (cost=13.15..56.97 rows=1141 width=536)
                                 Hash Cond: (cr.country = u.country)
                                 ->  Seq Scan on country_rise cr  (cost=0.00..26.30 rows=1630 width=16)
                                 ->  Hash  (cost=11.40..11.40 rows=140 width=532)
                                       ->  Seq Scan on users u  (cost=0.00..11.40 rows=140 width=532)
(18 rows)



Wyniki są takie same (choć inna kolejność), ale koszt zapytania o wiele większy!

Oczywiście zawsze można by napisać lepsze zapytania, przy którym okazało by się, że koszt nie jest tak wysoki a i nie było by konieczne użycie LATERAL JOINa, ale w tym poście nie o to chodziło :-)


Brak komentarzy:

Prześlij komentarz