top of page

Jak Power Query (M) vyhodnocuje dotazy a proč je výkon někdy nekonzistentní

  • Writer: Vojtěch Šíma
    Vojtěch Šíma
  • Jul 16
  • 13 min read

Updated: Jul 17

tl;dr Power Query / jazyk M ti nabízí poměrně intuitivní klikačku. Nicméně pokud bys chtěl trochu vymaxovat svoje schopnosti v v Power Query, dřív či později bys měl začít psát Mko ručně. Otevře ti to dveře k více projektům a zároveň budou tvoje dotazy většinou rychlejší. Pořádně tedy porozumět, jak Mko vyhodnocuje tvůj kód, je celkem zásadní. Dneska ti ukážu, jak se Mko vyhodnocuje na několika případech, projedeme si pár tipů a triků a na konci článku bys měl mít dostatek porozumění na to, aby tvůj kód byl stabilnější a výkon konzistentní.

Disclaimer: Budu tu zmiňovat věci jako let‑výrazy, funkce, rekordy a další podobné srandy. Jestli je zatím moc neznáš, klidně zůstaň, i tak ti to něco dá. Ale pokud si chceš zážitek trochu vyladit, dožeň mezery [tady] a pak se vrať.

Intro

Normálně „Intro“ přeskakuji, ale tady mi to přišlo vhod, protože chci trochu nastínit strukturu článku. Vyhodnocování v Power Query je docela komplexní, takže začneme tím, že si projdeme pár klíčových pojmů. Až pak skočíme na reálné příklady a ukážeme si, jak to celé vlastně funguje.


Jestli už máš základy v malíku, klidně přeskoč rovnou na příklady.


Vyhodnocuje Mko "lazy" nebo "eager"

Pokud s Mkem (nebo obecně s kódem) teprve začínáš, možná jsi tyhle pojmy ještě neslyšel. Ve zkratce: lazy znamená, že se kód spustí jen když je opravdu potřeba. Eager naopak znamená, že se provede hned, jakmile ho definuješ.


V Mku je většina věcí lazy. Konkrétně let‑výrazy, tabulky, seznamy a rekordy. Takže když si definuješ pár tabulek, ale nikdy je nepoužiješ ve finálním in bloku, jako bys je vůbec nenapsal. A to se vyplatí.


Na druhou stranu, parametry funkcí jsou ve výchozím stavu eager. To znamená, že když zavoláš funkci a předáš jí proměnnou, Mko ji vyhodnotí okamžitě; i když ji ta funkce nakonec vůbec nepotřebuje.


Nicméně, to neznamená, že ignoruje "lazy" pravidla. Vyhodnocují se jen ty části, které se doopravdy použijou ve finálním in. Takže pokud máš volání funkce uložené v proměnné, kterou nikdy nikde nepoužiješ, Mko to celé přeskočí. Tady se projevuje ta "lenost".


Takže co to M vlastně je? Technicky se označuje jako „partly" nebo "partially" (částečně) lazy. Ale protože pocházím z generaci, co pro všechno vymýšlí nové názvy, pojďme si to přejmenovat. Od teď tomu říkejme lazily eager.


Proměnné & Streamování

V Power Query M hrají proměnné zásadní roli; jak pro přehlednost, tak i pro výkon. Jakmile si v let bloku definuješ proměnnou, její výraz je neměnný (immutable); nemůžeš ji dál upravovat. Přesně kvůli tomu také nemůžeš definovat proměnné se stejným jménem (v rámci daného scopu). Pokud by ses o to pokusil, dostaneš syntax error.

Důležité upozornění: bavíme se o výrazu - tedy o kódu nebo logice, co za tou proměnnou stojí, ne nutně o výsledné hodnotě.

Každá proměnná v let bloku může být buď:

  • „recept na data“ – návod, jak uvařit/vygenerovat tabulku, seznam nebo záznam, když ho potřebuješ

  • jednoduchý výraz – číslo, text nebo datum/čas, který se vyhodnotí jednou. V daném scopu se pak hodnota vždy přepoužívá


Proč tomu říkám „recept“? Protože M nikdy neukládá celý dataset dopředu a neschovává si ho bokem. Je líné - pracuje odspodu nahoru (bottom up). Když vyhodnocuješ výsledek (in), Power Query se vrací zpět skrz proměnné a hledá, co je vlastně potřeba, abys dokázal složit výsledek. A teprve pak začne streamovat - tahá řádky (nebo malé dávky) ze zdroje a posílá je krok za krokem dál (filtry, přidávání sloupců atd.), přičemž vše zbytečné odhazuje.


To znamená, že proměnné obsahující tabulky nebo seznamy nedrží všechna data v paměti. Místo toho drží jen návod, jak ta data získat a upravit, kdykoli je použiješ. To znamená, že pokud se odkazuješ na stejnou proměnnou dvakrát, Mko "uvaří" recept znova, pokud ho výslovně neuložíš (buffer), což ale přeruší stream. Ve většině případů je lepší nechat stream běžet volně bez bufferování. Jen pokud víš, že budeš na tu samou proměnnou sahat víckrát, můžeš ji zabufferovat a výsledek držet v paměti.


A ještě k tomu opakovanému načítání, Power Query sice má nějakou formu trvalé cache (persistent cache) pro dotazy na zdroj, která občas potlačí druhý sběr (fetch), ale je hodně nespolehlivá. Je tedy lepší navrhnout si tok dat tak, aby ses drahým opakováním kompletně vyhnul.

Když proměnnou zabufferuješ, Power Query načte celý dataset naráz, uloží ho do paměti a dál už pracuje jen s načtenou verzí.

Je dobré vědět, že k bufferování, tedy přerušení streamu, může dojít i bez toho, že bys výslovně použil Table.Buffer nebo List.Buffer. Některé operace jako třídění, seskupování nebo spojování totiž můžou buffer spustit automaticky.

Na druhou stranu, skalární proměnné se chovají víceméně jako konstanty. Pokud proměnná drží jen číslo, text nebo nějakou uzavřenou funkci, která nesahá na externí zdroj, Mko ji vyhodnotí jen jednou, při prvním použití, a pak výsledek používá po zbytek dotazu (v rámci jejího scopu).


Buffering

Buffering znamená, že celý dataset se „zmaterializuje“/uloží do paměti; přeruší se tím jak folding (viz níže), tak streamování, ale výměnou za to máš okamžitý přístup k celým datům. Hodí se to hlavně, když chceš nad tabulkou jet víckrát nebo ji použít jako referenci pro jiné tabulky.


Ale má to i výrazné mínusy. Jak už zaznělo, buffering zruší folding i streamování a navíc sežere hodně paměti. Pokud data před tím nijak nezfiltruješ nebo nezmenšíš, můžeš u větších datasetů snadno narazit na limit paměti nebo výrazně zpomalit refresh. Takže před bufferingem vždycky zvaž, kolik toho opravdu potřebuješ a zredukuj bufferovaná data.


Buffering je navíc jen povrchový (shallow). Když máš strukturovaná data s vnořenými hodnotami (např. záznamy nebo seznamy uvnitř sloupců), nabufferuje se jen ta vrchní struktura, ne její obsah. Takže než něco nabufferuješ, zamysli se nad tím, jak ta data vlastně vypadají a co přesně chceš uchovat. V některých případech třeba vůbec vnořené hodnoty bufferovat nechceš.

Příklad: máš tabulku se sloupci, které obsahují nerozbalené rekordy nebo seznamy. Vnitřní hodnoty těch rekordů/seznamů se v bufferu neuchovají.

Jak už padlo dřív, bufferování můžeš vyvolat přímo (Table.Buffer, List.Buffer), nebo k němu dojde implicitně, třeba při seskupování, třídění nebo spojování dotazů. Dalším častým spouštěčem je privacy firewall. Když kombinuješ různé datové zdroje, M může každou část nejdřív zvlášť nahrát a uložit do paměti kvůli ochraně soukromí.


A pozor, Mku je úplně jedno, jestli ten buffer dává smysl. Třeba když bufferuješ poslední krok, tj. tu proměnnou, která jde přímo do in, a hned potom data načteš do modelu, bufferování proběhne tak či tak. Přitom to vůbec nic nepřinese, tabulka se načte, uloží do paměti… a vzápětí se celý buffer zahodí. Takže v tomhle případě jsi jen zbytečně plýtval pamětí.


Query Folding

Tohle už asi znáš. Ve zkratce, Mko umí některé kroky přeložit do nativního dotazu, který se spustí přímo na původním zdroji dat, mimo M engine. To může dramaticky zrychlit výkon.


Folding obvykle podporují relační databáze a OData zdroje. V praxi to znamená, že spousta základních kroků se může přeložit do SQL a provést přímo na databázi, úplně stejně jako kdybys psal SQL dotaz ručně.


Zmiňuju folding hlavně proto, že jsem předtím mluvil o streamování. Tyhle dvě věci si navzájem komplimentují. V ideálním případě, chceš nejdřív provést kroky, které lze "foldovat" zpět na zdroj a zbytek potom streamuješ. Tím se vyhneš zbytečnému načítání všeho do paměti.


Jenže stejně jako stream, i folding se může přerušit. Typicky třeba pivotem, přidáváním sloupců s nějakou složitější logikou, slučováním s externím zdrojem nebo bufferingem (ne asi).


Přesný seznam věcí, co folding rozbijou, závisí na konkrétním zdroji dat.

Myslím, že co se týče definic, to bude vše. Mrkneme teďka na nějaké to Mko.


Lazy evaluace a proměnné na jednoduchém příkladu

Jedním z nejlepších způsobů, jak si ukázat lazy vyhodnocení v praxi, jsou náhodná čísla.

Náhodné číslo
Náhodné číslo

Zatím žádné překvapení. Teď ale přidáme funkci na zaokrouhlování a pár dalších proměnných, a podíváme se, jak se Mko začne chovat:

let
    rounder = (num as number)=> Number.Round( num, 0),
    rNum = rounder(Number.RandomBetween(1, 10) ) ,
    rNumBig= rounder(Number.RandomBetween(80, 100) ),
    createList = { rNum, rNum }
in
    createList

Co myslíš, že bude za výsledek?

Seznam náhodných čísel
Seznam náhodných čísel
  • Skalární proměnné se vyhodnocují maximálně jednou.

    • Výraz přiřazený proměnné rNum se spustí až ve chvíli, kdy se použije v in. A i když se rNum objeví v seznamu dvakrát, výsledkem budou dvě stejné hodnoty. Proč? Protože Mko vyhodnotí proměnnou jednou a hodnotu si zapamatuje a přepoužívá. A není to štěstí, spusť kód klidně stokrát, vždycky dostaneš stejnou hodnotu dvakrát.

  • Proměnné, které se nevyužijí, se vůbec nevyhodnotí.

    • Proměnná rNumBig se v in části nikde nepoužívá, takže ji Mko prostě přeskočí. Výraz se vůbec nespustí, jako bys ho tam ani neměl. Typické líné chování v akci.


Teď přídáme pár dalších výrazů, abychom si fungování ujasnili:

let
    rounder = (num as number)=> Number.Round( num, 0),
    rNum = rounder(Number.RandomBetween(1, 10) ) ,
    rNumBig= rounder(Number.RandomBetween(80, 100) ),
    rNumFunction = () => rounder(Number.RandomBetween(1, 10) ),
    call_rNumFunction = rNumFunction(),
    createList = { 
        "variable rNum",
        rNum, 
        rNum, 
        "no variable direct expression",
        rounder(Number.RandomBetween(1, 10) ),
        rounder(Number.RandomBetween(1, 10) ) ,
        "call function rNumFunction",
        rNumFunction(),
        rNumFunction(),
        "variable that calls rNumFunction",
        call_rNumFunction,
        call_rNumFunction
    }
in
    createList

Výsledek:

Výstup s víc proměnnými a výrazy
Výstup s víc proměnnými a výrazy

Aby byl výstup přehlednější, přidal jsem do seznamu i textové popisky. Když ale texty ignorujeme, tady je, co se ve skutečnosti děje:

  • Řádky 2&3:  hodnota z rNum - skalární proměnná, takže se vyhodnotí jen jednou a výsledek se znovu použije. Proto jsou obě hodnoty stejné.


  • Řádky 5&6: pocházejí z přímého volání funkce rounder(). Nejsou uložené v proměnné, takže se vyhodnocují pokaždé znovu, proto mají různé hodnoty.


  • Řádky 8&9: výsledek z přímého volání rNumFunction(). Chová se stejně jako ty předchozí; každé volání vytvoří nové náhodné číslo, jen je to zabaleno do funkce.


  • Řádky 11&12: ukazují, co se stane, když rNumFunction() zavoláš jen jednou a výsledek uložíš do proměnné call_rNumFunction. Tím pádem je to zase skalární proměnná; vyhodnotí se jednou a výsledek se použije dvakrát.


  • A nakonec: rNumBig se opět ve výstupu vůbec neobjeví, protože není použitý v in části; Mko ho tedy vůbec nevyhodnotí.



Když záleží na pořadí

Spolu s tím, jak Mko vyhodnocuje proměnné, je důležité pochopit ještě jednu věc: na pořadí záleží. Konkrétně na tom, v jakém pořadí jsou výrazy uvnitř bloku in. Pokud vytváříš seznam hodnot, jejich pozice má přímý vliv na to, kdy a jak je engine vyhodnotí.


Mrkněme na jednoduchý příklad:

let
  start = DateTime.LocalNow(),
  end = DateTime.LocalNow(),
  createList = {start, end, DateTime.LocalNow() }
in
  createList

Použijeme DateTime.LocalNow() víckrát a výsledek ukládáme do proměnných. Z toho, co víme, by hodnoty měly být skoro identické; žádné triky.

Ve spoustě případů uvidíš, že jsou všechny timestampy úplně stejné. Důvod? Vyhodnocení probíhá tak rychle, že všechny hodnoty spadnou do stejného „ticku“ - engine pracuje s přesností někde mezi 0,5 až 15 milisekundami.
Identické nebo skoro identické timestampy
Identické nebo skoro identické timestampy

Pojďme si kód okořenit o zpoždění (delay). Pomocí Function.InvokeAfter() počkáme 10 sekund, než jednu z hodnot vyhodnotíme:

let
  start = DateTime.LocalNow(),
  end = DateTime.LocalNow(),
  createList = {
    start,
    end,
    Function.InvokeAfter( ()=> DateTime.LocalNow(), #duration(0,0,0,10) ) 
  }
in
  createList

Tady očekáváme, že první dvě hodnoty budou téměř totožné a ta třetí bude o 10 sekund opožděná.


Zpožděný timestamp
Zpožděný timestamp

A přesně to se stane. Žádná magie, všechno podle očekávání. Teď si to opepříme ještě víc a podíváme se, proč na pořadí položek v seznamu skutečně záleží.

let
  start = DateTime.LocalNow(),
  end = DateTime.LocalNow(),
  createList = {
    start,
    end,
    Function.InvokeAfter( ()=> end, #duration(0,0,0,10) ),
    DateTime.LocalNow(), 
    end
  }
in
  createList

Nějaké nápady?

Vyhodnocení timestampů podle pořadí
Vyhodnocení timestampů podle pořadí

V tomhle dotazu proměnné start a end zachytí čas téměř ve stejném okamžiku. Vyhodnotí se hned ve chvíli, kdy se dostanou na řadu v seznamu; tedy na řádcích 1 a 2.


Třetí prvek používá Function.InvokeAfter()  s desetivteřinovým zpožděním. Ale pozor: ta funkce už znovu nevolá DateTime.LocalNow() jen vrací hodnotu z proměnné end, která už byla dávno vyhodnocená (když jsme ji potřebovali pro druhý řádek). Takže reálně jen čekáme 10 sekund na to, až vrátíme tu samou hodnotu. Nový timestamp se nevytváří, jen zpožďujeme návrat už existujícího.


Čtvrtý prvek je nové volání DateTime.LocalNow(). To se spustí až po předchozím zpoždění, takže výsledkem je čerstvý čas přibližně o 10 sekund novější než první dva. Tím si potvrzujeme, že se opravdu čekalo, i když to třetí položka navenek nijak neukázala.


Pátý prvek znovu používá end, tedy pořád stejný timestamp jako dřív, jen teď se zobrazí o něco později. Hodnota zůstává stejná a odpovídá tomu, kdy byla promměná end poprvé vyhodnocená.



Funkce & eager vs. lazy vyhodnocení

Pojďme se podívat, jak Mko vyhodnocuje funkce:

let
    first = error "some error",
    second = 100,
    niceFunction = (optional a)=> 60,
    call = niceFunction()
in
    call

Co se stane:


V in části máme proměnnou call, která volá funkci niceFunction bez parametrů. Funkce vrací konstantu 60, takže to přesně dostaneme. Proměnné first a second se vůbec nevyhodnotí; nejsou potřeba.


Protože funkci voláme bez parametrů, (a jak si možná pamatuješ, parametry se v Mku vyhodnocují eagerly), tak se tady žádné eager vyhodnocení vůbec nespustí.


Teď to trochu upravíme:

let
    first = error "some error",
    second = 100,
    niceFunction = (optional a)=> 60,
    call = niceFunction(first) // added "first"
in
    call

Myslíš si, že i tady dostaneš 60, bez ohledu na to, co předáš? No… většinou jo, ale s jednou zásadní výjimkou; chyby (errors).


Parametry funkcí se v Mku vyhodnocují eagerly, takže ve chvíli, kdy zavoláš niceFunction(first), Mko se okamžitě pokusí vyhodnotit first. Kdyby first obsahovalo třeba jen 100 nebo jinou nevinnou hodnotu, Mko ji spočítá, zahodí a stejně ti funkce vrátí 60.


Jenže v tomhle případě je first chyba, a protože se vyhodnocuje okamžitě, celý dotaz spadne ještě dřív, než se funkce vůbec stihne spustit. Výsledkem nebude 60, ale přímo chyba z first .


Jakmile se objeví chyba, M přestane vyhodnocovat cokoli dalšího. Ta chyba se stává výsledkem dotazu.
Vlastní chyby ve výstupu
Vlastní chyby ve výstupu

Poslední příklad:

let
    first = error "some error",
    second = 100,
    niceFunction = (optional a)=> 60,
    call = niceFunction(first),
    anotherCall = niceFunction(second) // this line is added
in
    anotherCall

Voláme anotherCall, což znamená, že funkce niceFunction dostane jako parametr second. Ten se okamžitě vyhodnotí, ale protože je to jen 100, nic se neděje. Funkce niceFunction pak proběhne a vrátí 60, což se stane finálním výsledkem.



A teď to podstatné: proměnná call není nikde dál použitá. I když obsahuje volání funkce s parametrem first, na výsledek to nemá žádný vliv, protože se na ni nikde neodkazujeme. Za normálních okolností by se parametr vyhodnotil eager stylem, ale tady vítězí lenost, protože celý výraz je pro výstup úplně zbytečný a Mko ho proto vůbec nevyhodnocuje.


Lenost ve funkcích & Higher-Order

Možná si říkáš, jestli se v Mku dá dosáhnout úplně čistého lazy vyhodnocení. Bohužel ne úplně, ale existuje fajn workaround - říká se tomu higher-order funkce.

Higher-order funkce ti umožní odložit eager vyhodnocení. Místo toho, abys předal výsledek funkce, předáš samotnou funkci. Když Mko ten parametr zpracovává, zaregistruje si jen objekt funkce, ale její tělo zatím nespustí.


Když si vytvoříš prázdný dotaz a napíšeš:

= Table.AddColumn

Uvidíš vestavěný popis funkce, její parametry a pár tipů k použití. U vlastních funkcí ale dostaneš jen seznam parametrů a jednoduché klikací rozhraní, pokud si do definice nehodíš vlastní meta popis.


Tuhle techniku můžeš využít k tomu, abys se vyhnul eager vyhodnocení náročných výrazů. Mrkni na tenhle příklad:

let
    first = ()=> error "some error",
    second = ()=> 100,
    niceFunction = (
        isTrue as logical,
        a as function,
        b as function
        ) as any =>
            if isTrue then a() else b(),
    call = niceFunction(false, first, second) 
in
    call

Tohle je podobné předchozím příkladům. Jediný rozdíl je v tom, že jsme first a second zabalili do funkcí a naše niceFunction teď očekává tři povinné parametry — logickou hodnotu a dvě funkce.


Tady jsou first a second funkce bez argumentů (zero-argument) a niceFunction očekává logickou hodnotu plus dvě funkce jako parametry. Když vytvoříš proměnnou call , Mko každý argument jen rychle zkontroluje, jestli je to funkce. Tělo těch funkcí se ale v tu chvíli vůbec nespustí, takže náklady na výpočet jsou prakticky nulové. K samotnému volání dojde až ve chvíli, kdy niceFunction vyhodnocuje if a rozhodne se zavolat buď a(), nebo b(). Jeden z "thunks".


Tomuhle se říká thunk, funkce bez argumentů, která slouží čistě k tomu, aby odložila vyhodnocení nějakého výrazu, dokud ji výslovně nezavoláš.

Všimni si, že protože jsme jako první argument předali false , nedojde k žádné chybě z first. I když Mko proměnnou first (druhý parametr) vyhodnotí natolik, aby si ověřilo, že je to funkce, její tělo ale nespustí. Takže chyba se vůbec nevyvolá. Místo toho niceFunction zavolá second, a výstupem je 100.


Jinými slovy, když výraz zabalíš do funkce bez parametrů, tedy do thunku, můžeš tím odložit chybu (nebo jinou náročnou operaci), dokud ji sám neaktivuješ.


Tenhle přístup ti v Mku umožňuje simulovat lazy chování; výrazy zabalíš do funkcí a vyhodnotíš je až v momentě, kdy je jejich výsledek opravdu potřeba.


Scope proměnných

Proměnné v Mku můžou být zanořené jedna v druhé. Správné zvládnutí jejich scopu je důležitá součást psaní kvalitního M kódu. Pojďme si ukázat, jak scope může ovlivnit vyhodnocování. Pro někoho to může být samozřejmé, ale radši to ukážu pro jistotku, aby bylo jasno.


Vrátíme se k příkladu s náhodnými čísly. Představ si, že máme základní tabulku a chceme do ní přidat nové sloupce, které budou kombinovat existující hodnotu s nějakým náhodným číslem.

Výchozí tabulka
Výchozí tabulka

Abych nemusel opakovat stejný vzorec pro generování náhodného čísla, nejdřív si vytvořím proměnnou, která tu logiku obslouží. Pak ji jen volám. Chování se tím nijak nemění; je to stejné, jako kdybys ten výraz napsal přímo do funkce při přidávání sloupce:

rNum = ()=> Number.Round( Number.RandomBetween(0,10), 0 )

Teď přidáme sloupec klasickým způsobem:

addRandom  = Table.AddColumn(tbl, "addRandom", each let random = rNum() in [value] + random, Int64.Type)

Výsledkem je, že pro každý řádek dostaneš nové náhodné číslo.

Náhodné číslo pro kažý řádek
Náhodné číslo pro kažý řádek

Ať už to napíšeš s let uvnitř funkce, nebo ho úplně vynecháš a rovnou zavoláš rNum():

addRandom  = Table.AddColumn(tbl, "addRandom", each [value] + rNum(), Int64.Type)

výsledek je pořád stejný. Každý řádek dostane svoje vlastní náhodné číslo.


To, co ale chování zásadně změní, je když let přesuneš mimo samotnou funkci:

addRandom2  = Table.AddColumn(addRandom, "addRandom2", let random = rNum() in each [value] + random, Int64.Type)

Změna scopu proměnných změní výsledek.
Změna scopu proměnných změní výsledek.

Teď už uvidíš, že výsledek je ve všech řádcích stejný. A není to náhoda; tu stejnou hodnotu dostaneš pokaždé, pro každý řádek.


Aby bylo jasné, proč se to děje, musíš vědět, co vlastně Table.AddColumn() dělá. Po zadání vstupní tabulky a názvu nového sloupce očekává funkci, která se spustí řádek po řádku. Jak tu funkci napíšeš, to je na tobě.


A tady je ten klíčový bod: před samotnou funkcí pro jednotlivé řádky si můžeš nadefinovat cokoliv chceš. To znamená, že proměnnou můžeš vytvořit mimo scope konkrétního řádku. A když pak each funkce běží, vezme tuhle už vyhodnocenou proměnnou. Hodnota už je předem daná, zamčená a nebude se měnit mezi jednotlivými řádky.


Definovat proměnnou tímhle způsobem je prakticky totéž, jako kdybys měl jeden krok před addRandom2 a na něj se pak jen odkazoval. Scope funguje úplně stejně.


Takže si zapamatuj: tyhle dvě varianty se chovají naprosto rozdílně:

let random = rNum() in each [value] + random
each let random = rNum() in [value] + random

Vyhodnocování záznamů (records)

Za prvé, jestli jsi došel až sem, respekt. Za druhé, pravděpodobně už ti z předchozích příkladů došlo, jak se záznamy vyhodnocují. Ale ještě jsem ti neukázal jeden konkrétní a přímý důkaz, tak to hned napravíme.


Záznamy se vyhodnocují líně (lazy).


Hodnoty uvnitř záznamu se nevyhodnotí hned, ale až ve chvíli, kdy je opravdu potřebuješ. Skvěle to jde vidět na tabulce, která obsahuje sloupec se záznamy. S tímhle se často potkáš, třeba když pracuješ s REST API, které vrací JSON, nebo obecně s polostrukturovanými daty, která nejsou normalizovaná jako klasická relační databáze.


Začneme jednoduchou tabulkou, kde sloupec timestamp používá  DateTime.LocalNow().


Základní tabulka s časovým razítkem (timestamp).
Základní tabulka s časovým razítkem (timestamp).

Teď přidáme nový sloupec, a místo obyčejné hodnoty do něj vložíme záznam s novým timestampem:

addNewTimestamp = 
Table.AddColumn(
  tbl,
  "recordColumn",
  each [ timestamp2 = DateTime.LocalNow() ],
  type [ timestamp2 = datetime ]
)
Pokud tenhle styl neznáš: definuješ nové sloupce jako záznamy. V tomhle případě jedno pole odpovídá jednomu sloupci. Ve čtvrtém argumentu pak určuješ typ, a to pomocí stejné záznamové struktury. Díky tomu se při rozbalování později zachová typ daného pole.

Output:

Nový sloupec se záznamem a timestampem
Nový sloupec se záznamem a timestampem

A teď to hlavní:


To, co se vyhodnotí hned, je jen vnější struktura, tedy že sloupec se záznamem se přidá do tabulky. Obsah každého záznamu se ale zatím nevyhodnocuje. Zůstává neaktivní, dokud ho skutečně nepotřebuješ, třeba když záznam rozbalíš nebo na něj klikneš v náhledu.


Náhledové okno je perfektní způsob, jak tohle chování pozorovat. Protože když rozbalíš (expand) sloupec se záznamem, všimneš si, že oba sloupce s timestampem, ten původní i ten uvnitř záznamu, budou mít téměř stejnou hodnotu. Je to proto, že se oba vyhodnotí ve stejný moment, přesně v okamžiku potřeby. Pokud se někdy liší, rozdíl najdeš spíš mezi řádky než mezi sloupci, protože engine jede řádek po řádku.


Zpátky ale k náhledu:

Kdykoliv klikneš na buňku se záznamem, hodnota uvnitř se vyhodnotí přesně v ten moment. A když na stejnou buňku klikneš víckrát, pokaždé dostaneš jiný výsledek. Zkus si to proklikat, timestamp se ti bude aktualizovat s každým kliknutím.


Nové vyhodnocení při každém kliknutí.
Nové vyhodnocení při každém kliknutí.


bottom of page