<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>SAGES:: Blog &#187; Piotr Kołaczkowski</title>
	<atom:link href="http://blog.sages.com.pl/author/pkolaczkowski/feed/" rel="self" type="application/rss+xml" />
	<link>http://blog.sages.com.pl</link>
	<description>Blog konsultantów firmy Sages - artykuły o technologiach Java EE i pokrewnych</description>
	<lastBuildDate>Mon, 13 Jun 2011 15:50:18 +0000</lastBuildDate>
	
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
			<item>
		<title>Implementacja i używanie wielosłowników w języku Scala</title>
		<link>http://blog.sages.com.pl/2010/12/implementacja-i-uzywanie-wieloslownikow-w-jezyku-scala/</link>
		<comments>http://blog.sages.com.pl/2010/12/implementacja-i-uzywanie-wieloslownikow-w-jezyku-scala/#comments</comments>
		<pubDate>Sun, 19 Dec 2010 18:51:24 +0000</pubDate>
		<dc:creator>Piotr Kołaczkowski</dc:creator>
				<category><![CDATA[Problemy i rozwiązania]]></category>
		<category><![CDATA[scala]]></category>
		<category><![CDATA[struktury danych]]></category>

		<guid isPermaLink="false">http://blog.sages.com.pl/?p=126</guid>
		<description><![CDATA[Większość języków programowania wysokiego poziomu dostarcza struktury implementujące tablice asocjacyjne, inaczej zwane słownikami. Struktury te umożliwiają przechowywanie zbioru wartości skojarzonych z obiektami &#8211; kluczami &#8211; w taki sposób, że wyszukiwanie wartości na podstawie klucza jest bardzo szybkie. Scala dysponuje standardowo słownikami wykorzystującymi tablice haszujące (HashMap) oraz drzewa czerwono-czarne (TreeMap). Obie implementacje są dostępne w wersjach [...]]]></description>
			<content:encoded><![CDATA[<p>Większość języków programowania wysokiego poziomu dostarcza struktury implementujące tablice asocjacyjne, inaczej zwane słownikami. Struktury te umożliwiają przechowywanie zbioru wartości skojarzonych z obiektami &#8211; kluczami &#8211; w taki sposób, że wyszukiwanie wartości na podstawie klucza jest bardzo szybkie. Scala dysponuje standardowo słownikami wykorzystującymi tablice haszujące (<code>HashMap</code>) oraz drzewa czerwono-czarne (<code>TreeMap</code>). Obie implementacje są dostępne w wersjach mutowalnej i niemutowalnej.<br />
<span id="more-126"></span><br />
Co jednak zrobić, jeśli chcemy z jednym kluczem skojarzyć więcej niż jeden obiekt-wartość? Najprościej użyć zwykłego słownika ale zamiast pojedynczego obiektu wartości użyć zbioru wartości. Np. chcemy skonstruować wielosłownik przechowujący informacje o rolach użytkowników: </p>
<pre>
import scala.collection.mutable.HashMap
case class User(name: String)
case class Role(name: String)
val roles = new HashMap[User, Set[Role]]
</pre>
<p>Żeby dodać do takiego słownika jakieś dane, możemy napisać:</p>
<pre>
roles += (User("Bob") -> Set(Role("User")))
roles += (User("Mike") -> Set(Role("User")))
</pre>
<p>Co jeśli teraz chcemy Boba awansować na Admina, ale zachowując wszystkie pozostałe role?</p>
<pre>
roles += (User("Bob") -> (roles(User("Bob")) + Role("Admin")))
</pre>
<p>Niestety takie proste rozwiązanie ma wadę. Co się stanie jeśli użytkownika Bob nie było wcześniej w kolekcji? Dostaniemy wyjątek. Poniżej poprawiona wersja:</p>
<pre>
roles += (User("Bob") -> (roles.getOrElse(User("Bob"), Set.empty)
  + Role("Admin")))
</pre>
<p>Jak widać, jest trochę za dużo kodu, jak na tak prostą czynność. Z pomocą przychodzi nam <code>scala.collection.mutable.MultiMap</code>. Dzięki <code>MultiMap</code> będziemy mogli dodawać nowe obiekty wprost i, co najważniejsze, nie zapomnimy obsłużyć sytuacji, gdy nie było w kolekcji wcześniej klucza:</p>
<pre>
import scala.collection.mutable.{MultiMap, Set}

val roles = new HashMap[User, Set[Role]] with MultiMap[User, Role]
roles.addBinding(User("Bob"), Role("User"))
roles.addBinding(User("Bob"), Role("Admin"))
</pre>
<p>Prawda, że dużo prościej? Należy jednak przy tym zwrócić wagę na dwie rzeczy: <code>Set</code> musi być zaimportowany z pakietu <code>mutable</code>, a nie domyślnego oraz <code>MultiMap</code> parametryzujemy bezpośrednio klasą <code>Role</code> a nie <code>Set[Role]</code>. W przeciwnym przypadku dostaniemy błąd kompilacji.</p>
<p>Można powiedzieć, że to już koniec tego wpisu, bo przecież w końcu tytułowy wielosłownik udało się utworzyć. Jednak przeoczyliśmy pewien szczegół: wszystkie przykłady tu przedstawione opierają się na kolekcjach mutowalnych. Natomiast w Scali zalecanym sposobem jest programowanie z użyciem kolekcji niemutowalnych (czemu tak jest i czemu jest to ogólnie lepsza strategia projektowania aplikacji, to już temat na osobny wpis). Niestety jak zajrzymy dokładniej do API, to nie znajdziemy tam interfejsu <code> scala.collection.immutable.MultiMap </code>. Ponadto, jeśli nie kontrolujemy kodu tworzącego słownik, to nie mamy jak udekorować go dobrodziejstwami <code>MultiMap</code>. Czy jesteśmy zatem zdani na niewygodne operowanie zwykłym słownikiem tak jakby był wielosłownikiem? Na szczęście nie. Tylko tym razem musimy się nieco bardziej napracować.</p>
<p>Scala posiada bardzo potężny mechanizm automatycznych konwersji. W momencie, gdy potrzebujemy wywołać na obiekcie nieistniejącą metodę, to nastąpi próba przekonwertowania tego obiektu do innego obiektu, który ową metodę posiada. W ten sposób można dodawać nowe metody do klas, bez konieczności modyfikacji ich kodu, poprzez dostarczenie odpowiednich konwersji. Czyli można dodać też metody <code>addBinding</code> i <code>removeBinding</code> do dowolnego obiektu klasy <code>Map[_, Set[_]]</code>:</p>
<pre>
import collection.generic.CanBuildFrom

object MultiMapUtil {

  class MultiMap[K, V, M <: Map[K, Set[V]]](map: M, emptySet: Set[V])
  {

    def addBinding(k: K, v: V): M =
      (map + (k -> (map.getOrElse(k, emptySet) + v))).asInstanceOf[M]

    def removeBinding(k: K, v: V): M = {
      val r = (map.getOrElse(k, emptySet) - v)
      (if (r.isEmpty) map - k else map + (k -> r)).asInstanceOf[M]
    }
  }

  implicit def mapToMultiMap[K, V, S[V] <: Set[V], M[K, S] <:
      Map[K, S]] (map: M[K, S[V]])(implicit bf:
      CanBuildFrom[Nothing, V, S[V]]) =
      new MultiMap[K, V, M[K, S[V]]](map, bf.apply.result)
}
</pre>
<p>Co tu się dzieje? Definiujemy nową klasę MultiMap, która dostarcza pożądane metody <code>addBinding</code> i <code>removeBinding</code>, operujące na obiekcie słownika przekazanym w konstruktorze obiektu. Konstruktor dodatkowo potrzebuje obiektu reprezentującego pusty zbiór. Nie można wpisać w kodzie na stałe domyślnego zbioru pustego <code>Set.empty</code>, ponieważ użytkownik może chcieć operować na innej implementacji zbioru np. <code>TreeSet</code>.</p>
<p>Za tworzenie obiektów naszej klasy MultiMap na podstawie obiektów dowolnej podklasy <code>Map</code> odpowiada metoda implicit <code>mapToMultiMap</code>. Słowo kluczowe implicit mówi kompilatorowi, że może samodzielnie wstawić wywołanie tej metody w celu dokonania odpowiedniej konwersji. Parametry typów K, V, S i M oznaczają kolejno: typ klucza, typ wartości, typ zbioru użytego do przechowywania wartości i typ słownika. Nie trzeba ich podawać, kompilator sam dopasuje odpowiednie typy na podstawie typu obiektu podanego jako argument metody. </p>
<p>Drugi argument metody <code>mapToMultiMap</code> to obiekt implementujący interfejs <code>CanBuildFrom</code>. Obiekty CanBuildFrom dostarczają fabryki służące do produkowania kolekcji - w tym przypadku obiekt ten został użyty do utworzenia pustego zbioru typu <code>S[V]</code>. Biblioteka standardowa Scali zawiera implementacje <code>CanBuildFrom</code> dla wszystkich standardowych typów kolekcji. Ponieważ argument został oznaczony jako implicit, to odpowiednia implementacja <code>CanBuildFrom</code> zostanie podstawiona przez kompilator automatycznie. Warto zapoznać się z tym mechanizmem dokładniej, bo wiele metod w bibliotece standardowej z niego korzysta.</p>
<p>Na zakończenie sesja REPL pokazująca użycie powyższego kodu:</p>
<pre>
scala> import MultiMapUtil._
import MultiMapUtil._

scala> val roles1 = Map.empty[User, Set[Role]]
roles1: scala.collection.immutable.Map[User,Set[Role]] = Map()

scala> val roles2 = roles1.addBinding(User("Kowalski"), Role("User"))
roles2: scala.collection.immutable.Map[User,Set[Role]] =
  Map((User(Kowalski),Set(Role(User))))

scala> val roles3 = roles2.addBinding(User("Kowalski"), Role("Admin"))
roles3: scala.collection.immutable.Map[User,Set[Role]] =
  Map((User(Kowalski),Set(Role(User), Role(Admin))))
</pre>
<p>Z innymi klasami słowników i zbiorów też działa - proszę zwrócić uwagę na typ wyniku oraz kolejność kluczy:</p>
<pre>
import scala.collection.immutable.TreeMap
import scala.collection.immutable.TreeSet

scala> val m = TreeMap[String, TreeSet[Int]]()
m: scala.collection.immutable.TreeMap[String,
  scala.collection.immutable.TreeSet[Int]] = Map()

scala> m.addBinding("a", 10).addBinding("b", 20).addBinding("a", 15)
res3: scala.collection.immutable.TreeMap[String,
  scala.collection.immutable.TreeSet[Int]] =
  Map((a,TreeSet(10, 15)), (b,TreeSet(20)))
</pre>
]]></content:encoded>
			<wfw:commentRss>http://blog.sages.com.pl/2010/12/implementacja-i-uzywanie-wieloslownikow-w-jezyku-scala/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Optymalizacja zapytań z podzapytaniami</title>
		<link>http://blog.sages.com.pl/2010/03/optymalizacja-zapytan-z-podzapytaniami/</link>
		<comments>http://blog.sages.com.pl/2010/03/optymalizacja-zapytan-z-podzapytaniami/#comments</comments>
		<pubDate>Tue, 02 Mar 2010 12:38:32 +0000</pubDate>
		<dc:creator>Piotr Kołaczkowski</dc:creator>
				<category><![CDATA[Problemy i rozwiązania]]></category>
		<category><![CDATA[bazy danych]]></category>
		<category><![CDATA[optymalizacja]]></category>
		<category><![CDATA[sql]]></category>
		<category><![CDATA[zapytania]]></category>

		<guid isPermaLink="false">http://blog.sages.com.pl/?p=17</guid>
		<description><![CDATA[
Współczesne systemy relacyjnych baz danych, również te z otwartym kodem źródłowym, umożliwiają tworzenie bardzo złożonych zapytań SQL. Poprzez umieszczanie podzapytań w seksji SELECT, FROM oraz WHERE, jak również przez łączenie zapytań za pomocą operatorów takich jak UNION czy INTERSECT, nie trudno napisać zapytanie, które nie zmieści się na monitorze. Ta elastyczność ma niestety swoją cenę: [...]]]></description>
			<content:encoded><![CDATA[<p>
Współczesne systemy relacyjnych baz danych, również te z otwartym kodem źródłowym, umożliwiają tworzenie bardzo złożonych zapytań SQL. Poprzez umieszczanie podzapytań w seksji SELECT, FROM oraz WHERE, jak również przez łączenie zapytań za pomocą operatorów takich jak UNION czy INTERSECT, nie trudno napisać zapytanie, które nie zmieści się na monitorze. Ta elastyczność ma niestety swoją cenę: analiza takiego złożonego zapytania jest nie lada wyzwaniem dla silnika bazy danych i, jak pokazuje praktyka, niektóre systemy baz danych nie najlepiej sobie z tym radzą. Najprościej wytłumaczyć to na przykładzie, wzorowanym na prawdziwym przypadku.
</p>
<p> <span id="more-17"></span></p>
<p>Wyobraźmy sobie, że tworzymy system, którego jednym z zadań jest zapisywanie informacji o aktywności użytkowników umieszczających pliki na serwerze. Dla każdego zarejestrowanego użytkownika, chcemy wiedzieć, kiedy i jaki plik umieścił. Schemat bazy danych dla tej funkcjonalności wygląda następująco:
</p>
<pre>
             Table "public.test_user"
     Column      |         Type          | Modifiers
-----------------+-----------------------+-----------
 user_id         | integer               | not null
 name            | character varying(64) | not null
 login           | character varying(16) | not null
 hashed_password | character varying(32) | not null

Indexes:
    "test_user_pkey" PRIMARY KEY, btree (user_id)
    "test_user_login_key" UNIQUE, btree ("login")
</pre>
<pre>
            Table "public.test_upload"
   Column    |          Type          | Modifiers
-------------+------------------------+-----------
 upload_id   | integer                | not null
 user_id     | integer                | not null
 path        | character varying(255) | not null
 upload_time | timestamp              | not null

Indexes:
    "test_upload_pkey" PRIMARY KEY, btree (upload_id)
Foreign-key constraints:
    "test_upload_user_id_fkey" FOREIGN KEY (user_id)
        REFERENCES test_user(user_id)
</pre>
<p>
Tabela <i>test_user</i> zawiera ok. 10 tys. użytkowników. Jeden użytkownik średnio umieścił na serwerze 5 plików, choć są oczywiście tacy, którzy nie umieścili ani jednego, jak i tacy, którzy umieścili ich kilkaset. Każdemu umieszczonemu plikowi odpowiada jeden rekord w tabeli <i>test_upload</i>.
</p>
<p>
Powiedzmy, że nasz przykładowy serwis potrzebuje wyświetlić 10 różnych użytkowników, którzy dodawali ostatnio jakieś pliki. Użytkownik, który dodał plik najdawniej, ma znaleźć się na końcu listy, użytkownik, którego plik jest &#8220;najświeższy&#8221; &#8211; na samym początku. Jednak żaden użytkownik nie powinien znaleźć się na liście &#8220;top 10&#8243; więcej niż raz.
</p>
<p>
Pierwsze podejście do tego zapytania mogłoby wyglądać następująco:
</p>
<pre>
SELECT name, path, upload_time
FROM test_user u JOIN test_upload l ON (u.user_id = l.user_id)
ORDER BY upload_time DESC
LIMIT 10;
</pre>
<p>
Niestety, zapytanie, choć faktycznie wyświetla ostatnio dodane pliki, niezupełnie robi to, czego oczekujemy &#8211; jeden użytkownik może wystąpić więcej niż raz na liście. Z pomocą przychodzi dodatkowy warunek, który wyeliminuje starsze wpisy dla tego samego użytkownika:
</p>
<pre>
SELECT name, path, upload_time
FROM test_user u JOIN test_upload l ON (u.user_id = l.user_id)
WHERE upload_time =
   (SELECT max(upload_time) FROM test_upload WHERE user_id = u.user_id)
ORDER BY upload_time DESC
LIMIT 10;
</pre>
<p>
Teraz zapytanie już działa poprawnie, ale w zależności od systemu baz danych, na którym pracujemy, mogliśmy wprowadzić inny poważny problem: zapytanie będzie się wykonywać dużo wolniej. Na naszym systemie testowym opartym o PostgreSQL 8.2 pierwsza (niepoprawna) wersja wykonywała się 200 ms. Tymczasem wersja &#8220;poprawiona&#8221;, wykonywała się nieco ponad 10 minut, czyli ok. 3 tys. razy wolniej. Przyczynę tej powolności pokazuje wynik EXPLAIN:
</p>
<div style="width: 580px; overflow: auto; background: #FFFFB3">
<pre class="noborder">
                                                                QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=50967080.34..50967080.36 rows=10 width=40) (actual time=617733.543..617733.554 rows=10 loops=1)
   ->  Sort  (cost=50967080.34..50967080.52 rows=75 width=40) (actual time=617733.542..617733.548 rows=10 loops=1)
         Sort Key: l.upload_date
         ->  Nested Loop  (cost=0.00..50967078.00 rows=75 width=40) (actual time=16.545..617691.016 rows=9967 loops=1)
               Join Filter: (l.upload_date = (subplan))
               ->  Seq Scan on test_upload l  (cost=0.00..894.00 rows=50000 width=28) (actual time=0.012..41.359 rows=50000 loops=1)
               ->  Index Scan using test_user_pkey on test_user u  (cost=0.00..0.28 rows=1 width=20) (actual time=0.009..0.011 rows=1 loops=50000)
                     Index Cond: (u.user_id = l.user_id)
               SubPlan
                 ->  Aggregate  (cost=1019.02..1019.03 rows=1 width=4) (actual time=12.333..12.333 rows=1 loops=50000)
                       ->  Seq Scan on test_upload  (cost=0.00..1019.00 rows=6 width=4) (actual time=1.967..12.323 rows=6 loops=50000)
                             Filter: (user_id = $0)
 Total runtime: 617733.733 ms
</pre>
</div>
<p>
Po pierwsze, podzapytanie jest wykonywane dosyć nieoptymalnie &#8211; skanowana jest cała tabela, aby znaleźć pliki jednego użytkownika. Po drugie, podzapytanie jest wykonywane 50000 razy, raz na każdy rekord analizowany w głównym zapytaniu. Z pierwszym problemem można sobie poradzić przez dodanie odpowiedniego indeksu:
</p>
<pre>
CREATE INDEX upload_user_id_idx ON test_upload(user_id);
</pre>
<p>
Ta prosta zmiana spowodowała, że czas zapytania skrócił się do ok 0,6 sekundy:
</p>
<div style="width: 580px; overflow: auto; background: #FFFFB3"">
<pre class="noborder">
                                                                          QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=422099.29..422099.31 rows=10 width=40) (actual time=614.207..614.214 rows=10 loops=1)
   ->  Sort  (cost=422099.29..422099.47 rows=74 width=40) (actual time=614.205..614.208 rows=10 loops=1)
         Sort Key: l.upload_time
         ->  Merge Join  (cost=0.00..422096.99 rows=74 width=40) (actual time=0.071..602.762 rows=9967 loops=1)
               Merge Cond: (u.user_id = l.user_id)
               Join Filter: (l.upload_time = (subplan))
               ->  Index Scan using test_user_pkey on test_user u  (cost=0.00..378.25 rows=10000 width=20) (actual time=0.021..5.243 rows=10000 loops=1)
               ->  Index Scan using upload_user_id_idx on test_upload l  (cost=0.00..1595.25 rows=50000 width=28) (actual time=0.014..24.648 rows=50000 loops=1)
               SubPlan
                 ->  Aggregate  (cost=8.38..8.39 rows=1 width=4) (actual time=0.009..0.009 rows=1 loops=50000)
                       ->  Index Scan using upload_user_id_idx on test_upload  (cost=0.00..8.36 rows=6 width=4) (actual time=0.003..0.006 rows=6 loops=50000)
                             Index Cond: (user_id = $0)
 Total runtime: 614.338 ms
</pre>
</div>
<p>
W tym artykule jednak chcieliśmy przedstawić inną technikę optymalizacji podzapytań: poprzez ich eliminację. Wiadomo, że podzapytanie, którego nie ma, nie potrzebuje czasu. Zależy nam jednak na tym, aby eliminując podzapytanie, całość była nadal poprawna, tj. zwracała właściwe wyniki, czyli musimy je czymś zastąpić. Kluczem do zastosowania tej techniki jest zamiana podzapytania na złączenie. Złączenia są łatwiejsze dla systemu do obliczenia chociażby z tego względu, że istnieją różne algorytmy realizacji złączeń i optymalizator ma tutaj większe &#8220;pole do popisu&#8221;. Poza tym hurtowy dostęp do dużej tabeli jest zwykle tańszy niż tysiące małych, prostych dostępów wybierających po kilka rekordów.</p>
<p>Wiele silników baz danych potrafi wykonać takie przekształcenie automatycznie dla podzapytań nieskorelowanych, jednak w tym przypadku mamy do czynienia z podzapytaniem skorelowanym, ponieważ odwołuje się ono do zapytania otaczającego. Komercyjne systemy baz danych poradziłyby sobie i z tym przypadkiem, ale jeśli nie mamy tego szczęścia ich używać, musimy poradzić sobie sami.
</p>
<p>
W pierwszej kolejności usuńmy niepotrzebny już indeks <i>upload_user_id_idx</i>. Następnie wykonajmy zapytanie:
</p>
<pre>
SELECT user_id, max(upload_time)
FROM test_upload
GROUP BY user_id;
</pre>
<p>
Wykonuje się jedynie 63 ms i zawiera wszystkie potrzebne dane do sprawdzenia, czy dany plik użytkownika jest tym &#8220;ostatnim&#8221; i powinien być uwzględniony w wyniku. Teraz tylko trzeba to zapytanie połączyć z pełną zawartością tabeli z użytkownikami i plikami, i na końcu odpowiednio posortować:
</p>
<pre>
SELECT name, path, upload_time
FROM test_user u
  JOIN test_upload l ON (u.user_id = l.user_id)
  JOIN (
          SELECT user_id, max(upload_time) AS ud
          FROM test_upload
          GROUP BY user_id
       ) x
  ON (x.user_id = l.user_id AND l.upload_time = ud)
ORDER BY upload_time DESC
LIMIT 10;
</pre>
<p>
Czas wykonania tego zapytania wyniósł 167 ms, czyli wyeliminowanie podzapytania zapewniło prawie 4-krotne przyspieszenie:
</p>
<div style="width: 580px; overflow: auto; background: #FFFFB3"">
<pre  class="noborder">
                                                                    QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=4324.97..4324.99 rows=10 width=40) (actual time=166.005..166.011 rows=10 loops=1)
   ->  Sort  (cost=4324.97..4325.13 rows=66 width=40) (actual time=166.003..166.004 rows=10 loops=1)
         Sort Key: l.upload_date
         ->  Hash Join  (cost=2053.31..4322.97 rows=66 width=40) (actual time=93.562..155.832 rows=9967 loops=1)
               Hash Cond: ((l.user_id = u.user_id) AND (l.upload_date = x.ud))
               ->  Seq Scan on test_upload l  (cost=0.00..894.00 rows=50000 width=28) (actual time=0.010..18.370 rows=50000 loops=1)
               ->  Hash  (cost=1920.04..1920.04 rows=8885 width=28) (actual time=93.524..93.524 rows=9922 loops=1)
                     ->  Hash Join  (cost=1498.00..1920.04 rows=8885 width=28) (actual time=68.164..86.451 rows=9922 loops=1)
                           Hash Cond: (x.user_id = u.user_id)
                           ->  HashAggregate  (cost=1144.00..1255.06 rows=8885 width=8) (actual time=52.218..57.378 rows=9922 loops=1)
                                 ->  Seq Scan on test_upload  (cost=0.00..894.00 rows=50000 width=8) (actual time=0.008..18.568 rows=50000 loops=1)
                           ->  Hash  (cost=229.00..229.00 rows=10000 width=20) (actual time=15.925..15.925 rows=10000 loops=1)
                                 ->  Seq Scan on test_user u  (cost=0.00..229.00 rows=10000 width=20) (actual time=0.007..5.835 rows=10000 loops=1)
 Total runtime: 166.287 ms
</pre>
</div>
<p>
Testy przeprowadziliśmy na małym zbiorze danych, który całkowicie mieścił się w pamięci. Co się stanie jednak, jeśli zwiększymy ilość danych? Aby to sprawdzić, wygenerowaliśmy drugi, duży zbiór danych tak, by tabela użytkowników zawierała 500 tys. rekordów, a tabela z dodanymi plikami &#8211; 10 mln. Tym razem wersja z podzapytaniem wykorzystującym indeks na test_upload(user_id) wykonywała się ponad 30 minut i musieliśmy przerwać test. Natomiast wersja bez podzapytania skorelowanego zajęła 44 sekundy. Z kolei stosunek kosztów obu zapytań oszacowany przez optymalizator PostgreSQL wyniósł ok. 1600:1. Różnice są dlatego tak znaczne, że tym razem dane nie mieszczą się w całości w pamięci i każdorazowe wykonanie podzapytania wymagało fizycznego dostępu do przypadkowego miejsca dysku. Tymczasem w drugim przypadku rekordy są pobierane sekwencyjnie i nie traci się czasu na pozycjonowanie głowic dysku. Wersji z podzapytaniem bez indeksu nie sprawdzaliśmy na tym zbiorze danych. Gdybyśmy to zrobili, prawdopodobnie datę publikacji tego artykułu należałoby przesunąć o rok.
</p>
<p>Jak widać, użycie złączenia i podzapytania nieskorelowanego we FROM zamiast podzapytania skorelowanego w WHERE może zapewnić duże zyski wydajności. Należy jednak pamiętać też o zagrożeniach jakie niesie ta technika. Przede wszystkim zmieniając postać zapytania, ryzykujemy, że nowe zapytanie nie będzie równoważne oryginałowi. W wielu przypadkach zamiana może wydawać się mechaniczna, ale należy bardzo uważać, żeby dodając kolejne złączenie nie wprowadzić duplikatów rekordów. Złączenia, w przeciwieństwie do zastosowania operatorów EXISTS, IN czy &#8216;=&#8217; w sekcji WHERE, mogą nie tylko eliminować rekordy, ale również je powielać. Problem ten rozwiązuje się zwykle przez upewnienie się, że nigdy nie zostanie dołączony więcej niż jeden rekord (w naszym przypadku poprzez proste spostrzeżenie, że user_id jest unikalne), albo przez dodanie słowa DISTINCT, tak aby ewentualne duplikaty usunąć na końcu. Drugim zagrożeniem jest próba stosowania tej techniki zawsze i wszędzie, gdzie się tylko da. A niestety nie zawsze daje ona zyski w wydajności. Prezentowane zapytanie udało się nam przyspieszyć do 0,4 ms na małym zbiorze danych i 1 s na zbiorze dużym. Jak? To już temat na osobny artykuł.</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.sages.com.pl/2010/03/optymalizacja-zapytan-z-podzapytaniami/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>

