<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="cs">
	<id>http://postgres.cz/index.php?action=history&amp;feed=atom&amp;title=Automatick%C3%A9_ke%C5%A1ov%C3%A1n%C3%AD_prov%C3%A1d%C4%9Bc%C3%ADch_pl%C3%A1n%C5%AF_v_PL%2FpgSQL</id>
	<title>Automatické kešování prováděcích plánů v PL/pgSQL - Historie editací</title>
	<link rel="self" type="application/atom+xml" href="http://postgres.cz/index.php?action=history&amp;feed=atom&amp;title=Automatick%C3%A9_ke%C5%A1ov%C3%A1n%C3%AD_prov%C3%A1d%C4%9Bc%C3%ADch_pl%C3%A1n%C5%AF_v_PL%2FpgSQL"/>
	<link rel="alternate" type="text/html" href="http://postgres.cz/index.php?title=Automatick%C3%A9_ke%C5%A1ov%C3%A1n%C3%AD_prov%C3%A1d%C4%9Bc%C3%ADch_pl%C3%A1n%C5%AF_v_PL/pgSQL&amp;action=history"/>
	<updated>2026-05-13T00:41:30Z</updated>
	<subtitle>Historie editací této stránky</subtitle>
	<generator>MediaWiki 1.43.3</generator>
	<entry>
		<id>http://postgres.cz/index.php?title=Automatick%C3%A9_ke%C5%A1ov%C3%A1n%C3%AD_prov%C3%A1d%C4%9Bc%C3%ADch_pl%C3%A1n%C5%AF_v_PL/pgSQL&amp;diff=80&amp;oldid=prev</id>
		<title>imported&gt;Pavel v 24. 7. 2013, 06:37</title>
		<link rel="alternate" type="text/html" href="http://postgres.cz/index.php?title=Automatick%C3%A9_ke%C5%A1ov%C3%A1n%C3%AD_prov%C3%A1d%C4%9Bc%C3%ADch_pl%C3%A1n%C5%AF_v_PL/pgSQL&amp;diff=80&amp;oldid=prev"/>
		<updated>2013-07-24T06:37:44Z</updated>

		<summary type="html">&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Nová stránka&lt;/b&gt;&lt;/p&gt;&lt;div&gt;[[category:Články]]&lt;br /&gt;
Poznámka: &amp;lt;b&amp;gt;Ve verzi 9.2 došlo k zásadním změnám v implementaci cache prováděcích plánů - a některé zde popisované problémy byly odstraněny.&amp;lt;/b&amp;gt; Tento článek byl napsán vůči verzi 8.3.&lt;br /&gt;
&lt;br /&gt;
Když jsem před několika lety psal první článek o PL/pgSQL, narazil jsem v dokumentaci na doporučení nepoužívat řetězec &amp;#039;now&amp;#039; v tomto jazyce. Doporučení jsem akceptoval, vyzkoušel si, že to skutečně nefunguje, ale po pravdě řečeno moc jsem nechápal proč. Nejasně jsem si představoval, že se chybně přeloží do mezikódu (byte-code) i tato aktuální hodnota této pseudofunkce, což způsobí problém, na který upozorňovala dokumentace (čas vrácený řetězcem &amp;#039;now&amp;#039; odpovídal překladu funkce).&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
INSERT INTO (..) VALUES(&amp;#039;now&amp;#039;) -- nepouzivat v plpgsql!&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
Zde poprvé odbočím. Lexikální a syntaktická analýza PL/pgSQL funkcí se provádí pouze jednou, při prvním zavolání funkce v rámci přihlášení. Jejím výsledkem je syntaktický strom uložený v session cache. Něco jako překlad do mezikódu v PL/pgSQL neexistuje, syntaktický strom je vstupem pro interpret, tj. PL/pgSQL je klasický interpret postavený na lex, yacc generátoru. Síla PL/pgSQL není v rychlosti, ale v jeho vazbě s SQL. Díky této vazbě může být implementace PL/pgSQL docela jednoduchá, až tak, že neobsahuje ani jednoduché vyhodnocení výrazů. Vše, co lze, se předhodí interpretu SQL. Interpret PL/pgSQL řeší jen proměnné a řídící konstrukce.&lt;br /&gt;
&lt;br /&gt;
Pro uložení SQL výrazu v PL/pgSQL se používá typ PLpgSQL_expr (plpgsql.h). Pro další popis jsou významná pouze pole char *query (obsahuje text sql příkazu) a void *plan (ukazatel na kešovaný prováděcí plán SQL příkazu). Prováděcí plán se generuje pouze jednou, při prvním požadavku na vyhodnocení SQL výrazu.&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
if (expr-&amp;gt;plan == NULL) &lt;br /&gt;
  exec_prepare_plan(estate, expr) &lt;br /&gt;
.... &lt;br /&gt;
rc = SPI_execute_plan(expr-&amp;gt;plan, ...&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
Kešování je nutnost. V případě jednodušších výrazů může generování plánu trvat několikanásobně déle než samotné vyhodnocení výrazu. Změřte si rozdíl mezi prvním a druhým spuštěním PL/pgSQL funkce. Tento čas můžete připsat na vrub právě generování prováděcích plánů. Kešování vyřešilo problém s efektivitou provádění PL/pgSQL funkcí a přineslo dva problémy: nakešované prováděcí plány nejsou sdílené a ani persistentní (zpomaluje se start aplikace, roste spotřeba paměti (řešením je např. pgpool)), nakešované prováděcí plány mohou být občas neadekvátní a jejich provedení způsobí run-time error.&lt;br /&gt;
&lt;br /&gt;
Jakmile je plán jednou sestaven a je uložen v keši, existuje bez možnosti změny až do odhlášení nebo rekompilace funkce. V PostgreSQL není žadný atribut analogický atributu WITH RECOMPILE MSSQL. Naštěstí k chybám z důvodu chybného prováděcího plánu dochází výjimečně a pouze ze dvou možných důvodů.&lt;br /&gt;
&lt;br /&gt;
První příčinou je změna změna struktury databáze - jinak pokud po prvním volání funkce zrušíte některý z datových objektů (tabulka, sekvence), který byl ve funkci použit, následující volání funkce skončí chybou. Řešením není vytvoření objektu stejného typu a jména, protože nový objekt dostane nový (a tedy jiný) oid (object identifikátor). Rušit za provozu datové tabulky nikoho nenapadne, dočasnou tabulku asi každého - proto je tento problém spojen v ToDo s dočasnými tabulkami. Řešením je všechny dočasné tabulky vytvářet před prvním zavoláním PL/pgSQL funkcí a pak je nerušit, maximálně mazat. (Silně to svádí, zvlášť pokud jste na PostgresSQL přešli z MSSQL, kde je ale mechanismus předávání výsledných recordsetů z procedur úplně jiný.)&lt;br /&gt;
&lt;br /&gt;
Jelikož se prováděcí plán ukládá do keše až v okamžiku prvního použití objektu a nikoliv v čase překladu, můžeme dočasné tabulky vytvářet i v těle funkce.&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
CREATE OR REPLACE FUNCTION ... &lt;br /&gt;
  BEGIN &lt;br /&gt;
      PERFORM 1 &lt;br /&gt;
       FROM pg_catalog.pg_class c &lt;br /&gt;
            JOIN &lt;br /&gt;
            pg_catalog.pg_namespace n ON n.oid = c.relnamespace &lt;br /&gt;
      WHERE c.relkind IN (&amp;#039;r&amp;#039;,&amp;#039;&amp;#039;) AND c.relname = &amp;#039;tmptab&amp;#039; &lt;br /&gt;
        AND pg_catalog.pg_table_is_visible(c.oid) AND n.nspname LIKE &amp;#039;pg_temp%&amp;#039;; &lt;br /&gt;
    IF FOUND THEN &lt;br /&gt;
      TRUNCATE tmptab; &lt;br /&gt;
    ELSE &lt;br /&gt;
      CREATE TEMP TABLE tmptab(... &lt;br /&gt;
    END IF;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
Druhou možnou příčinou jsou dynamické dotazy. Jejich prováděcí plán se sice nekešuje, ale jejich výsledek může způsobit chybu jinde.&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
CREATE OR REPLACE FUNCTION foo() RETURNS void AS $$ &lt;br /&gt;
  DECLARE _t varchar[] = &amp;#039;{integer, varchar}&amp;#039;; &lt;br /&gt;
    _v varchar; _r record; &lt;br /&gt;
  BEGIN &lt;br /&gt;
    FOR _i IN 1 .. 2 LOOP &lt;br /&gt;
      FOR _r IN EXECUTE &amp;#039;SELECT 1::&amp;#039;||_t[_i]||&amp;#039; AS _x&amp;#039; LOOP &lt;br /&gt;
        _v := _r._x; &lt;br /&gt;
      END LOOP; &lt;br /&gt;
    END LOOP; &lt;br /&gt;
END; $$ LANGUAGE plpgsql; &lt;br /&gt;
select foo();&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Spuštění funkce skončí s chybou (_v := _r._x;) při druhém průchodu cyklu FOR _i IN ..&lt;br /&gt;
ERROR: type of &amp;quot;_r._x&amp;quot; does not match that when preparing the plan&lt;br /&gt;
&lt;br /&gt;
Proč? Příkaz přiřazení obsahuje SQL výraz (ještě jednou, každý výraz v PL/pgSQL je SQL výrazem). Při první iteraci se vytvořil prováděcí plán, který předpokládal, že hodnota _r._x je integer, při druhé iteraci je ale typu varchar, a tudíž je plán v keši neadekvátní. Řešením je mít tolik přiřazovacích příkazů, kolik je možných kombinací prováděcích plánů. Na první pohled nesmyslný kód funkce je správný:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
FOR _i IN 1 .. 2 LOOP &lt;br /&gt;
  FOR _r IN EXECUTE &amp;#039;SELECT 1::&amp;#039;||_t[_i]||&amp;#039; AS _x&amp;#039; LOOP &lt;br /&gt;
    IF _i = 1 THEN _v := _r._x; &lt;br /&gt;
    ELSIF _i = 2 THEN _v := _r._x; &lt;br /&gt;
    END IF; &lt;br /&gt;
  END LOOP; &lt;br /&gt;
END LOOP;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
Přiznám se, že mi toto chování PL/pgSQL žíly netrhá. Znám příčinu problému a umím se podle toho zařídit. Nicméně konečnému řešení (regeneraci plánu v případě detekce chyby) nic nestojí v cestě. Zkusmo jsem použil makra PG_TRY(), PG_CATCH() a PG_END_TRY() pro zachycení chyby a regeneroval chybný plán bez měřitelné ztráty rychlosti. Je jen otázkou času, kdy se někdo ujme řešení a vytvoří patch. Pravděpodobně se tak ale nestane ve verzi 8.1.&lt;br /&gt;
&lt;br /&gt;
Odbočení druhé: abych dokázal vysvětlit, kde je zakopaný pes v případě &amp;#039;now&amp;#039;, musím se ještě zmínit o procesu generování prováděcího plánu SQL příkazů.&lt;br /&gt;
&lt;br /&gt;
Jednou z etap přípravy plánu je redukce konstant a zjednodušení funkcí (např. 2+2=4, True Or cokoliv = True, náhrada immutable funkcí s konstantními parametry výsledkem funkce - což je právě případ &amp;#039;now&amp;#039; (backend/optimizer/util/clause.c - evaluate_function()), náhrada hodnotou NULL STRICT funkcí, pokud některý z jejich parametrů je NULL, atd.&lt;br /&gt;
&lt;br /&gt;
V našem případě bylo &amp;#039;now&amp;#039; nahrazeno konstantou, která zůstala uložena v prováděcím plánu a samozřejmě byla opakovaně vyhodnocována stejně. Za normálních okolností to nezpůsobovalo žádné problémy. Kromě PL/pgSQL PostgreSQL neobsahovala žádné nástroje schopné kešovat prováděcí plány. Výjimkou bylo PL/pgSQL, kde byl poprvé tento problém detekován (a stále je třeba si na něj dávat pozor).&lt;br /&gt;
&lt;br /&gt;
Nepamatuji se, že bych kdy použil &amp;#039;now&amp;#039;. Automaticky používám magické proměnné CURENT_DATE a CURRENT_TIMESTAMP, které na kešování netrpí. A pokud bych, snad z nostalgie, &amp;#039;now&amp;#039; chtěl použít, pak jedině v kombinaci s proměnnou:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
DECLARE d date; BEGIN d := &amp;#039;now&amp;#039;; INSERT INTO (..) VALUES(d); ...&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
Proč? Na funkci obsahující proměnnou je optimalizátor krátký (v tomto případě funkce datein()). PL/pgSQL sám něco na způsob optimalizace neprovádí, tudíž není schopen detekovat, že d je vlastně konstanta, takže řetězec &amp;#039;now&amp;#039; se bude přetypovávat na odpovídající typ skutečně až v době vyhodnocení výrazu a výsledkem bude odpovídající čas vyhodnocení výrazu.&lt;/div&gt;</summary>
		<author><name>imported&gt;Pavel</name></author>
	</entry>
</feed>