DAX funkce INDEX a OFFSET

Úvodní obrázek

Funkce INDEX() seřadí tabulku zadanou ve druhém argumentu a vrátí n-tý řádek z této tabulky v absolutním pořadí. Funkce OFFSET() funguje na podobném principu jako funkce INDEX(), pouze vrací n-tý řádek relativně k aktuálnímu řádku, ve kterém je tato funkce vyhodnocena. Funkce INDEX() a funkce OFFSET() vracejí obě tabulku a můžeme je tedy  používat přímo jako filtry ve funkci CALCULATE(), což je také jejich nejčastější použití.

Obě popisované funkce patří do kategorie WINDOW funkcí, mezi které patří také funkce WINDOW(), RANK() a ROWNUMBER(). Všechny tyto funkce mají několik společných jmenovatelů a hlavně přináší nový přístup k psaní DAX výpočtů.

V první části tohoto příspěvku jsou popsány jednotlivé argumenty funkcí INDEX()OFFSET(). V druhé části příspěvku následují jednoduché příklady s použitím těchto funkcí. Tyto příklady nemají příliš velký analytický význam, ale slouží spíše pro vysvětlení toho jakým způsobem můžeme používat funkce INDEX() a OFFSET() v DAX výpočtech. Ve třetí části příspěvku jsou popsány dvě situace, ve kterých mohou funkce INDEX() a OFFSET() vracet překvapivé, složitě interpretovatelné výsledky.

Všechny příklady v tomto příspěvku jsou vytvořeny ve cvičném Power BI souboru Adventure Works DW 2020.pbix. Soubor s řešením je dostupný ke stažení níže pod tímto příspěvkem.

Funkce INDEX a OFFSET

Čtenáři, pro kterého je tento příspěvek první interakce s novými WINDOW funkcemi, doporučuji nejdříve přejít k jednoduchým příkladům pro získání základního přehledu o fungování WINDOW funkcí, a až následně se vrátit k syntaxi popsané v této části příspěvku. Popisky argumentů funkcí INDEX() a OFFSET() v této části příspěvku mohou bez předchozího kontaktu s těmito funkcemi vyznít složitěji, než ve skutečnosti jsou.

Počeštěná syntaxe funkce INDEX() vypadá následovně.

Syntaxe funkce INDEX:

INDEX
(
    <pozice>,
    [, <tabulka>]
    [, <řazení>]
    [, <prázdné hodnoty>]
    [, <rozdělení podle>]
    [, <spárovat s>]
)

Funkce OFFSET() má následující, velmi podobnou syntaxi.

Syntaxe funkce OFFSET:

OFFSET
(
    <delta>
    [, <tabulka>]
    [, <řazení>]
    [, <prázdné hodnoty>]
    [, <rozdělit podle>]
    [, <spárovat s>]
)

Jediným rozdílem v syntaxi obou funkcí je první argument, proto si popíšeme první argument pro každou funkci samostatně, a až následně přejdeme k dalším, společným argumentům těchto funkcí.

První argument funkce INDEX() nazvaný <pozice> (originálně position) odkazuje na absolutní pozici řádku ze seřazené tabulky zadané ve druhém argumentu. V tomto argumentu můžeme zadat kladné nebo záporné číslo, nebo jakýkoliv skalární výraz. Pokud zadáme například číslo 1, výsledkem bude první řádek ze seřazené tabulky ve druhém argumentu, pro číslo 2 – druhý řádek, a tak dále. Zadáme-li záporné číslo, například číslo -1, výsledkem bude poslední řádek, pro -2 předposlední řádek atd. Pokud je zadaná pozice mimo rozsah tabulky ve druhém argumentu, výsledkem bude prázdná tabulka. To samé platí, pokud v tomto argumentu zadáme číslo nula nebo prázdnou hodnotu BLANK.

První argument funkce OFFSET() nazvaný <delta> určuje změnu polohy relativně k aktuálnímu řádku, ve kterém je funkce OFFSET() vyhodnocena. Aktuální řádek je určen na základě takzvaného univerzálního kontextu, který může obsahovat jak kontext řádku, tak kontext filtru, ve kterém je funkce vyhodnocena. Kladná čísla zadaná v tomto argumentu budou odkazovat na řádky za aktuálním řádkem v seřazené tabulce zadané ve druhém argumentu, záporná čísla budou odkazovat na řádky před aktuálním řádkem. Pokud zadáme číslo nula nebo hodnotu BLANK, výsledkem bude aktuální řádek. Pokud zadáme hodnotu mimo rozsah tabulky zadané ve druhém argumentu funkce OFFSET(), výsledkem bude prázdná tabulka. Stejně jako v případě funkce INDEX() i zde můžeme v tomto argumentu použít jakýkoliv skalární výraz vracející číslo ve formátu INTEGER nebo hodnotu kterou lze automaticky převést na číslo ve formátu INTEGER.

Kromě prvního argumentu funkcí OFFSET() a INDEX() jsou všechny ostatní argumenty nepovinné s tím, že některé jsou nepovinné pouze při vyplnění jiných argumentů. Následuje popis společných argumentů funkcí OFFSET() a INDEX().

Popis společných argumentů funkce INDEX() a OFFSET()

Tabulka (relation) – tabulka nebo funkce vracející tabulku, která bude seřazena a následně použita k nalezení n-tého řádku. Tento argument je nepovinný v případě, kdy je zadán argument <řazení>. V případě vynechání argumentu <tabulka> ale musí sloupce v argumentu <řazení> a v argumentu <rozdělit podle> pocházet ze stejné tabulky v modelu. V takovémto případě je na pozadí místo tohoto argumentu vytvořena tabulka pomocí funkce ALLSELECTED(), se sloupci zadanými v argumentu <řazení> a případně v argumentu <rozdělit podle>, pokud je použit.

Řazení (orderBy) – volitelný argument vyhrazený pro speciální funkci ORDERBY(). Pokud je tento argument vynechán, tabulka v argumentu <tabulka> musí být zadána. Argumenty ve funkci ORDERBY() se zadávají v páru sloupec nebo výraz určený pro řazení, a nepovinně způsob řazení. Pokud není zadán způsob řazení ve funkci ORDERBY(), defaultní nastavení je ASC – vzestupné. Ve funkci ORDERBY() můžeme určit řazení pomocí hodnot ve sloupci nebo řazení na základě hodnoty výrazu, tedy i měřítka. Řazení může být sestupné (DESC, 0 nebo FALSE) nebo vzestupné (ASC, 1 nebo TRUE). V argumentu určenému pro způsob řazení ve funkci ORDERBY() můžeme specifikovat také zařazení prázdných BLANK hodnot. Například výraz DESC BLANK FIRST seřadí hodnoty sestupně, ale prázdné hodnoty budou vždy v pořadí na začátku.

Prázdné hodnoty (blanks) – volitelný argument, jehož vyplněním můžeme specifikovat, jak bude zacházeno při řazení s prázdnými hodnotami BLANK. Toto nastavení platí pro všechny řazené hodnoty, kromě těch, pro které je případně určen vlastní způsob zacházení s prázdnými hodnotami ve funkci ORDERBY() v argumentu <řazení>, viz předchozí odstavec. V době psaní tohoto příspěvku můžeme zadat pouze DEFAULT – pro STRING hodnoty jsou hodnoty BLANK vždy na prvním místě, pro numerické hodnoty jsou BLANK hodnoty vždy mezi nulou a zápornými čísly. Další volby pro zacházení s prázdnými hodnotami mohou být přidány v budoucnu.

Rozdělení podle (partitionBy) - volitelný argument vyhrazený pro funkci PARTITIONBY(), pomocí které můžeme určit rozdělení tabulky z argumentu <tabulka> na dílčí tabulky. Pokud je tabulka rozdělena na dílčí tabulky podle sloupce zadaného ve funkci PARTITIONBY(), tak dílčí tabulka která bude použita v aktuálním kontextu bude ta, jejíž hodnota ve sloupci určeném pro rozdělení odpovídá hodnotě ze stejného sloupce ve vnějším univerzálním kontextu, ve kterém je celá funkce vyhodnocena.

Spárovat s (matchBy) – volitelný argument vyhrazený pro funkci MATCHBY(). Ve funkci MATCHBY() můžeme specifikovat sloupce, které budou použity pro určení aktuálního řádku, ve kterém je použitá WINDOW funkce vyhodnocena. Tento argument je užitečný zejména při použití v počítaných sloupcích, kde se díky specifikaci jednoho nebo více sloupců z tabulky, ve které je funkce INDEX() nebo OFFSET() použita, můžeme vyhnout problémům s cyklickou závislostí. Použití funkce MATCHBY() může také v některých typech výpočtů pomoci při optimalizaci, a to díky menšímu počtu sloupců které budou použity pro určení aktuálního řádku v porovnání s vnějším univerzálním kontextem.

Všechny WINDOW funkce fungují při jejich správném použití velmi intuitivně a jejich použití není příliš složité, jak si ukážeme na několika jednoduchých příkladech. V některých složitějších situacích však mohou být výsledky WINDOW funkcí složitě interpretovatelné. Tyto situace si ale popíšeme až v závěru tohoto příspěvku.

Příklady použití funkcí INDEX a OFFSET

Začít můžeme co možná nejjednoduššími příklady, které samy o sobě nemají příliš velký analytický význam, nicméně jsou to dobré příklady pro vysvětlení chování funkcí INDEX() a OFFSET(). V prvním kroku si můžeme vytvořit jednoduchý vizuál tabulka, kde budou v řádcích barvy produktů a měřítko [Prodeje], které má následující definici.

Měřítko:

Prodeje = SUM(Sales[Sales Amount])

Výchozí vizuál, se kterým budeme v prvních příkladech pracovat, může vypadat například následovně.

DAX funkce INDEX a OFFSET

Funkce INDEX() a OFFSET() jsou funkce vracející tabulku. Díky tomu, že v jazyku DAX mohou být tabulky s jedním řádkem a jedním sloupcem automaticky převedeny na skalární hodnotu, můžeme funkci INDEX() použít přímo v měřítku následujícím způsobem.

Měřítko:

První barva =
INDEX
(
    1,
    ALLSELECTED('Product'[Color]),
    ORDERBY([Prodeje], DESC)
)

Prvním argumentem v měřítku [První barva] je číslo 1, které odkazuje na první řádek v seřazené tabulce zadané ve druhém argumentu této funkce. Tabulka ve druhém argumentu v měřítku [První barva] obsahuje všechny barvy ze sloupce 'Product'[Color] zobrazené v řádcích vizuálu. Tato tabulka s barvami je seřazena podle hodnoty měřítka [Prodeje], a to sestupně od barvy s nejvyššími prodeji po barvu s nejnižšími prodeji. Výsledkem proto bude název barvy s nejvyšší hodnotou prodejů.

DAX funkce INDEX a OFFSET 2

Měřítko [První barva] vrací v každém řádku vizuálu hodnotu "Black", což je název barvy s nejvyššími prodeji ze všech zobrazených barev ve vizuálu. Ačkoliv měřítko [První barva] funguje a vrací přesně požadovanou hodnotu podle zadaných argumentů, tímto způsobem se obvykle funkce INDEX() nepoužívá. Výsledná tabulka, kterou vrací funkce IDNEX(), ale také funkce OFFSET(), totiž může obsahovat v aktuálním kontextu a v závislosti na zadaných argumentech více sloupců a i více řádků. Takovouto tabulku již není možné automaticky převést na skalární hodnotu.

V příkladech uvedených v tomto příspěvku proto budeme používat funkce INDEX() a OFFSET() ve filtru funkce CALCULATE(), což je daleko typičtější způsob použití těchto funkcí. Následující měřítko tak již bude vracet sumu za prodeje produktů v černé barvě, tedy v barvě, která je z pohledu prodejů nejlepší.

Měřítko:

Prodeje (INDEX 1) =
CALCULATE
(
    [Prodeje],
    INDEX
    (
        1,
        ALLSELECTED('Product'[Color]),
        ORDERBY([Prodeje], DESC)
    )
)

Pokud měřítko [Prodeje (INDEX 1)] přidáme do dříve vytvořeného vizuálu Tabulka, výsledek bude vypadat následovně.

DAX funkce INDEX a OFFSET 3

Na základě takto vytvořeného měřítka bychom již mohli jednoduchým odečtením zjistit například rozdíl mezi sumou za prodeje produktů v aktuální barvě v porovnání s prodeji za produkty v barvě s největšími prodeji. Tento jednoduchý příklad slouží pouze jako ukázka jakým způsobem budeme v tomto příspěvku používat funkce INDEX() a OFFSET() ve funkci CALCULATE(). V následující části si na příkladech postupně ukážeme další jednoduché způsoby použití funkcí INDEX() a OFFSET(), ze  kterých by mělo být patrné, jaký je rozdíl mezi těmito dvěma funkcemi.

Rozdíl mezi funkcí IDNEX() a OFFSET()

Funkce INDEX() i funkce OFFSET() fungují na podobném principu, kdy z tabulky zadané ve druhém argumentu vyberou určitý řádek (někdy také více řádků, jak bude možné vidět v dalších příkladech). Tento řádek je určen v prvním argumentu. Funkce INDEX() vrací řádek v absolutním pořadí v seřazené tabulce zadané ve druhém argumentu. Funkce OFFSET() vrací řádek v relativním pořadí ve vztahu k aktuálnímu řádku. Aktuální řádek je pak určen na základě vnějšího univerzálního kontextu (kontext řádku, kontext filtru nebo oba), ve kterém je funkce vyhodnocena.

Rozdíl mezi těmito dvěma funkcemi si můžeme znázornit pomocí jednoduchých příkladů. Měřítko [Prodeje (INDEX 1)] s použitím funkce INDEX() již jsme si vytvořili dříve, nicméně jeho definici si můžeme pro připomenutí zopakovat. Chování tohoto měřítka si pak můžeme porovnat s měřítkem [Prodeje (OFFSET -1)], ve kterém je, jak už název měřítka napovídá, použita funkce OFFSET().

Měřítka:

Prodeje (INDEX 1) =
CALCULATE
(
    [Prodeje],
    INDEX
    (
        1,
        ALLSELECTED('Product'[Color]),
        ORDERBY([Prodeje], DESC)
    )
)

Prodeje (OFFSET -1) =
CALCULATE
(
    [Prodeje],
    OFFSET
    (
        -1,
        ALLSELECTED('Product'[Color]),
        ORDERBY([Prodeje], DESC)
    )
)

Obě měřítka si můžeme opět vložit do původního vizuálu tabulka s barvami produktů v řádcích a s měřítkem [Prodeje].

DAX funkce INDEX a OFFSET 4

Základem pro pochopení fungování všech WINDOW funkcí je vždy seřazená tabulka zadaná ve druhém argumentu. Ekvivalent této tabulky můžeme shodou okolností vidět přímo na obrázku výše. Tabulka použitá ve funkci OFFSET() a INDEX() obsahuje všechny barvy zobrazené v prvním sloupci vizuálu (modré zvýraznění). Tyto barvy jsou pak seřazeny podle hodnoty měřítka [Prodeje], které je na obrázku výše ve druhém sloupci (červené zvýraznění).

Funkce INDEX() v měřítku [Prodeje (INDEX 1)] vrací první barvu z této tabulky v absolutní pořadí a filtruje tak měřítko [Prodeje] ve funkci CALCULATE() pouze na prodeje za produkty v této nejlepší barvě.

Funkce OFFSET() vrací z této tabulky předchozí barvu, relativně k aktuálnímu řádku. Aktuální řádek pro tabulku ve funkci OFFSET() je v tomto případě určen na základě filtru, který tvoří aktuální barva v aktuálním řádku vizuálu. Funkce OFFSET() pak vrátí předchozí barvu relativně k tomuto aktuálnímu řádku. Tato barva je pak opět použita jako filtr ve funkci CALCULATE() pro ovlivnění výsledku měřítka [Prodeje].

I v dalším příkladu budeme pracovat s tabulkou obsahující barvy produktů, která bude seřazena podle hodnoty měřítka [Prodeje] od barvy s nejvyššími prodeji po barvu s nejnižšími prodeji. Pokud bychom z této tabulky chtěli získat například předposlední barvu, to znamená druhou nejhorší barvu z pohledu velikosti prodejů, stačí ve funkci INDEX() změnit první argument na číslo -2 následujícím způsobem.

Měřítko:

Prodeje (INDEX -2) =
CALCULATE
(
    [Prodeje],
    INDEX
    (
        -2,
        ALLSELECTED('Product'[Color]),
        ORDERBY([Prodeje], DESC)
    )
)

Nové měřítko si můžeme opět vložit do vizuálu a zobrazit si jeho výsledek.

DAX funkce INDEX a OFFSET 5

Měřítko [Prodeje (INDEX -2)] nyní vrací prodeje za produkty v bílé barvě, to znamená za produkty s druhou nejhorší barvou z pohledu sumy inkasované za prodeje produktů. Proč vybíráme zrovna předposlední barvu není v tomto příkladu důležité. Důležité je že se pouhou změnou prvního argumentu ve funkci INDEX(), který může být vyplněn také dynamicky například pomocí parametru, můžeme pohybovat v zadané a seřazené tabulce na kteroukoliv pozici.

Obdobným způsobem můžeme změnit také první argument ve funkci OFFSET() například na číslo 2 následujícím způsobem.

Měřítko:

Prodeje (OFFSET 2) =
CALCULATE
(
    [Prodeje],
    OFFSET
    (
        2,
        ALLSELECTED('Product'[Color]),
        ORDERBY([Prodeje], DESC)
    )
)

Nové měřítko [Prodeje (OFFSET 2)] nyní bude vracet prodeje za produkty v barvě, která je v seřazené tabulce s barvami o dvě barvy níže oproti aktuální barvě.

DAX funkce INDEX a OFFSET 6

V prvním řádku tabulky na obrázku výše vrací měřítko [Prodeje (OFFSET 2)] sumu za prodeje produktů v barvě "Silver", což je barva o dvě pozice níže oproti aktuální barvě "Black". Tento princip je pak aplikován pro každou barvu. Pokud pod aktuální barvou v pozici o dvě níže žádná další barva není, výsledkem výpočtu je jednoduše prázdná hodnota BLANK.

Podobným způsobem se můžeme jednoduše pohybovat v tabulce nahoru a dolů pouhou změnou prvního argumentu ve funkcích INDEX() a OFFSET(). Rozdíl mezi funkcí INDEX() a OFFSET() je ten, že funkce INDEX() vrací řádek ze zadané a seřazené tabulky v absolutním pořadí, zatímco funkce OFFSET() vrací řádek ze seřazené tabulky zadané ve druhém argumentu relativně ve vztahu k aktuálnímu řádku.

V dalším příkladu si ukážeme použití funkce PARTITIONBY() pro rozdělení tabulky zadané ve druhém argumentu na jednotlivé dílčí tabulky.

Příklad použití funkce PARTITIONBY() ve funkci INDEX() a OFFSET()

Začněme opět tím, že si vytvoříme vizuál, se kterým budeme v příklad pracovat. Do vizuálu Tabulka si tentokrát můžeme vložit postupně kategorie produktů, barvy produktů a měřítko [Prodeje].

DAX funkce INDEX a OFFSET 7

Pokud se podíváme na obrázek výše, tak můžeme vidět, že v každé kategorii se prodávají produkty v různých barvách. Současně se stejné barvy mohou vyskytovat v různých kategoriích. Představme si situaci, kdy bychom se chtěli posouvat v tomto vizuálu nahoru nebo dolů, na začátek nebo nakonec, ale pouze odděleně v rámci jednotlivých kategorií. Právě k tomuto účelu je k dispozici funkce PARTITIONBY(), pomocí které můžeme tabulku ve druhém argumentu funkcí INDEX() a OFFSET() rozdělit na jednotlivé dílčí tabulky.

Následující měřítko tak bude vracet hodnotu prodejů za produkty v nejlepší barvě podle prodejů, nicméně pouze v rámci aktuální kategorie.

Měřítko:

Prodeje (INDEX 1 v kategorii) =
CALCULATE
(
    [Prodeje],
    INDEX
    (
        1,
        ALLSELECTED('Product'[Color], 'Product'[Category]),
        ORDERBY([Prodeje], DESC),
        PARTITIONBY('Product'[Category])
    )
)

Funkci PARTITIONBY() můžeme použít také ve funkci OFFSET(). Následující měřítko bude vracet prodeje za produkty v barvě, která je o jednu pozici lepší než aktuální barva, opět v rámci aktuální kategorie produktů.

Měřítko:

Prodeje (OFFSET -1 v kategorii) =
CALCULATE
(
    [Prodeje],
    OFFSET
    (
        -1,
        ALLSELECTED('Product'[Color], 'Product'[Category]),
        ORDERBY([Prodeje], DESC),
        PARTITIONBY('Product'[Category])
    )
)

Obě nová měřítka si můžeme vložit do připraveného vizuálu a zobrazit si výsledky.

DAX funkce INDEX a OFFSET 8

Jak je možné vidět na obrázku výše, tabulka zadaná ve druhém argumentu funkcí INDEX() a OFFSET() je nyní rozdělena na jednotlivé dílčí tabulky podle kategorií produktů, a to právě díky použití funkce PARTITIONBY().

Kdy může výsledek funkcí INDEX a OFFSET překvapit

Všechny příklady, které jsme si do této chvíle vytvořili mají dva společné jmenovatele. Za prvé, tabulka ve druhém argumentu funkcí INDEX() nebo OFFSET() obsahovala stejné sloupce, které byly použity ve vizuálech, v rámci kterých byly tyto funkce vyhodnoceny. Za druhé, výraz použitý pro řazení tabulky zadané ve druhém argumentu, tzn. měřítko [Prodeje], neobsahoval v rámci jednotlivých řádků duplicitní hodnoty. Jinak řečeno, suma za prodeje produktů v jednotlivých barvách byla vždy odlišná – jiná barva, jiná hodnota prodejů.

Za těchto okolností je použití WINDOW funkcí intuitivní a předvídatelné. V Power BI reportech se ale kontext ve kterém jsou vyhodnocena měřítka může rychle změnit, a proto si v krátkosti ukážeme také situace, kdy použití funkcí IDNEX() a OFFSET() může vracet překvapivé výsledky.

Funkce OFFSET() v řádcích souhrnů a v řádku Celkem

První problematická situace nastává, pokud není možné určit pro tabulku zadanou ve druhém argumentu funkce OFFSET() (platí také pro funkci WINDOW(), která není součástí tohoto příspěvku) aktuální řádek na základě vnějšího univerzálního kontextu. Tato situace se může v reportu vyskytnout relativně jednoduše. 

Začněme opět přípravou vizuálu, kdy pro tentokrát použijeme vizuál Matice s kategoriemi a barvami produktů v řádcích a s měřítkem [Prodeje] v hodnotách.

DAX funkce INDEX a OFFSET 9

Pro ukázku potencionálního problému můžeme použít již dříve vytvořené měřítko [Prodeje (OFFSET -1)], které vrací prodeje za předchozí barvu a které vypadá následovně.

Měřítko:

Prodeje (OFFSET -1) =
CALCULATE
(
    [Prodeje],
    OFFSET
    (
        -1,
        ALLSELECTED('Product'[Color]),
        ORDERBY([Prodeje], DESC)
    )
)

Pokud nové měřítko vložíme do připraveného vizuálu Matice, tak na úrovni jednotlivých barev je výsledkem tohoto měřítka podle očekávání hodnota prodejů za předchozí barvu.

DAX funkce INDEX a OFFSET 10

Při pohledu na vizuál na obrázku výše je zřejmé, že v řádcích s jednotlivými barvami vrací měřítko [Prodeje (OFFSET -1)] hodnotu prodejů předchozí barvy. Podíváme-li se například na barvu "Silver" v kategorii "Bikes", tak měřítko [Prodeje (OFFSET -1)] vrací prodeje za barvu "Red", tedy za předchozí barvu z pohledu sumy za prodeje produktů. Tento princip se opakuje pro každou barvu v každé kategorii. Tento výpočet jsme si již popsali v jednom z prvních příkladů a výsledky měřítka [Prodeje (OFFSET -1)] na úrovni jednotlivých barev by tedy neměly být překvapující. 

Otázkou ale je, co za hodnotu vrací měřítko [Prodeje (OFFSET -1)] v řádcích s kategoriemi produktů a v řádku souhrnů "Celkem".

Abychom mohli lépe pochopit chování funkce OFFSET() v řádcích souhrnů, vypíšeme si výsledek této funkce přímo ve vizuálu.

DAX funkce INDEX a OFFSET 11

Pro jednotlivé barvy vrací funkce OFFSET() předchozí barvu, což už by nyní nemělo být překvapivé. V řádcích souhrnů s kategoriemi produktů a v řádku "Celkem" ale funkce OFFSET() vrací více než jednu barvu, a to přesto že v prvním argumentu této funkce je použita hodnota -1, která odkazuje na předchozí řádek relativně k aktuálnímu řádku.

Zaměřme se například na kategorii "Bikes" v prvním řádku použitého vizuálu. V tomto řádku je aktivní pouze filtr na kategorii "Bikes".  Funkce OFFSET() proto nemůže na základě vnějšího univerzálního kontextu určit pouze jednu barvu, od které by se mohla posunout k předchozí barvě. Pro výpočet jsou proto použity všechny barvy v kategorii "Bikes", a pro každou jednu barvu je použit algoritmus podle zadaných argumentů. V našem příkladu jde o posun o jednu pozici nad aktuální barvu. Pro každou jednu barvu je tedy proveden posun k předchozí barvě. Výsledkem jsou proto všechny barvy kromě barvy v pořadí poslední, protože pod barvou "Blue" již není žádná další barva která by mohla odkazovat po posunu na tuto barvu. Měřítko [Prodeje (OFFSET -1)] proto vrací v řádku "Bikes" sumu za prodeje produktů v kategorii "Bikes" a v barvách "Black", "Red", "Silver" a "Yellow".

Tento princip je možné aplikovat na každou kategorii a také na řádek "Celkem". V řádku "Celkem" je nicméně situace méně intuitivní, protože měřítko [Prodeje (OFFSET -1)] v tomto řádku vrací stejnou hodnotu jako měřítko [Prodeje]. Na první pohled se tedy může zdát že v řádku souhrnů "Celkem" není poslední barva v pořadí vyloučena a že algoritmus výpočtu je v tomto řádku jiný. Jak si ale vysvětlíme algoritmus výpočtu je stejný a hodnota v řádku "Celkem" vrací správné výsledky v souladu se zadanými argumenty a v souladu s popsaným algoritmem výpočtu.

V použitém modelu jsou také produkty v barvě "Gray". Tyto produkty v barvě "Gray" se ale vůbec neprodávaly. Barva "Gray" má hodnotu prodejů BLANK, a je proto v řádku souhrnů zařazena funkcí OFFSET() na poslední místo. Tuto barvu pak funkce OFFSET() nevrací, protože pod touto barvou není žádná další barva, která by na barvu "Gray" po posunu na předchozí barvu odkazovala. Nicméně protože prodeje pro barvu "Gray" jsou BLANK – nejsou žádné, je jedno jestli je tato barva z výpočtu vyloučena. Jinak řečeno, pokud bychom k hodnotě měřítka [Prodeje (OFFSET -1)] přičetli prodeje za produkty v barvě "Gray", na hodnotě měřítka se nic nezmění. Z tohoto důvodu v řádku souhrnů vrací měřítko [Prodeje] a měřítko [Prodeje (OFFSET -1)] stejnou hodnotu, protože i když je barva "Gray" vyloučena ve filtru měřítka [Prodeje (OFFSET -1)], na hodnotu tohoto měřítka to nemá vliv.

Problém s určení aktuálního řádku na základě vnějšího univerzálního kontextu (kontext filtru, kontext řádku nebo oba) může být ještě daleko složitější. V tomto příkladu jsme pracovali ve funkci OFFSET() s tabulkou s jedním sloupcem - barvami produktů. Tato tabulka ve druhém argumentu funkce OFFSET() ale může obsahovat také více sloupců, a pouze některé z těchto sloupců mohou v aktuálním univerzálním kontextu vracet jednu hodnotu, na základě které je možné určit aktuální řádek nebo více řádků v tabulce ve druhém argumentu.

Pokud se chceme vyhnout této složité interpretaci výsledků v situacích kdy není možné jednoznačně určit jeden aktuální řádek na základě vnějšího univerzálního kontextu, řešením může být zobrazovat výsledky měřítka pouze v situaci kdy je možné aktuální řádek jednoznačně určit, v našem příkladu například následujícím způsobem.

Měřítko:

Prodeje (OFFSET -1) - pouze pro barvy =
IF
(
    HASONEVALUE('Product'[Color]),
    CALCULATE
    (
        [Prodeje],
        OFFSET
        (
            -1,
            ALLSELECTED('Product'[Color]),
            ORDERBY([Prodeje], DESC)
        )
    )
)

Takto vytvořené měřítko již bude vracet výsledky pouze pokud je vyhodnoceno v kontextu jedné barvy.

DAX funkce INDEX a OFFSET 12

Jak je možné vidět na obrázku výše, měřítko [Prodeje (OFFSET – 1)  - pouze pro barvy] již nevrací žádnou hodnotu v řádcích s kategoriemi a v řádku "Celkem".

Další složitě interpretovatelná situace může nastat v případě, kdy výraz nebo sloupec použitý pro řazení tabulky ve druhém argumentu obsahuje stejné hodnoty pro více řádků.

Funkce INDEX() a OFFSET() a shoda v hodnotách použitých pro řazení

V této části příspěvku se přesuneme od barev a prodejů a vytvoříme si jiný pohled, na kterém si ukážeme jak se chovají funkce INDEX() a OFFSET() v případě, kdy výraz určený pro řazení obsahuje duplicitní hodnoty. Začneme tím že si vytvoříme měřítko [Počet zákazníků], které má následující definici.

Měřítko:

Počet zákazníků = COUNTROWS(Customer)

Nové měřítko si vložíme do vizuálu Tabulka, spolu s hodnotami ze sloupce 'Customer'[City] v řádcích.

DAX funkce INDEX a OFFSET 13

Na obrázku výše můžeme nyní vidět názvy měst a počet zákazníků kteří mají bydliště v daném městě. Představme si nyní hypotetickou situaci, kdy bychom chtěli z nějakého důvodu zobrazit v řádcích vizuálu hodnotu prodejů ve městě s třetím největším počtem zákazníků. Takovéto měřítko může vypadat například následovně.

Měřítko:

Prodeje třetí město (podle počtu zákazníků) =
CALCULATE
(
    [Prodeje],
    INDEX
    (
        3,
        ALLSELECTED(Customer[City]),
        ORDERBY([Počet zákazníků], DESC)
    )
)

Nové měřítko bude vracet sumu za prodeje produktů ve třetím nejlepším městě, kde kritérium pro sestavení pořadí měst je počet zákazníků. Měřítko si můžeme vložit do dříve vytvořeného vizuálu a zobrazit si výsledek.

DAX funkce INDEX a OFFSET 14

Zaměřme se nejdříve na měřítko [Počet zákazníků], jehož hodnota je kritériem pro sestavení pořadí jednotlivých měst ve funkci OFFSET(). Na třetím místě v pořadí měst podle počtu zákazníků jsou dvě města – Burien a Concord. Pro obě tato města je hodnota měřítka [Počet zákazníků] číslo 212. Otázkou nyní je, které z těchto dvou měst je použito ve filtru měřítka [Prodeje třetí město (podle počtu zákazníků)]. Odpovědí může být samotné měřítko [Prodeje], které si můžeme vložit do vizuálu a zjistit tak ze kterého ze dvou možných měst jsou zákazníci kteří nakoupili produkty za celkovou částku 230 359,50, což je nyní hodnota měřítka [Prodeje třetí město (podle počtu zákazníků)].

DAX funkce INDEX a OFFSET 15

Při pohledu na obrázek výše již můžeme říci, že funkce OFFSET() v měřítku [Prodeje třetí město (podle počtu zákazníků)] vybrala jako třetí město v pořadí město Burien. Postup pro výběr tohoto města je následující. Funkce OFFSET() našla na třetím místě dvě města se stejným počtem zákazníků. V takové situace se na pozadí, bez jakékoliv instrukce v DAX výrazu v měřítku, vybere pro rozhodnutí o pořadí na třetím místě sloupec s názvy měst ve druhém argumentu funkce OFFSET(), a ze dvou možných měst je vybráno město první v abecedním pořadí, protože všechny WINDOW funkce používají jako defaultní řazení ASC.

Opět jako v případě prvního příkladu v této části příspěvku, situace může být i zde daleko složitější. Sloupců v tabulce v prvním argumentu funkce OFFSET() může být více, výrazů pro řazení může být použitých více a při nejednoznačném pořadí může být výsledek funkce OFFSET() sice předvídatelný a stabilní, nicméně složitě interpretovatelný.

Preferovaným řešením v takovéto situaci bude zadat ve funkci ORDERBY() dostatečný počet argumentů určených pro způsob řazení tak, aby nedocházelo k automatickému přidávání sloupců do algoritmu který je použitý pro určení pořadí v případě nejednoznačnosti.

V našem příkladu s prodeji pro třetí nejlepší město tak můžeme například přidat do funkce ORDERBY() měřítko [Prodeje], které bude použité pro řazení v případě, kdy měřítko [Počet zákazníků] bude vracet pro některá města stejnou hodnotu. Jako další způsob řazení pak můžeme přidat sloupec 'Customer'[City] s názvy jednotlivých měst pro případ, kdy by stejnou hodnotu pro více měst vracelo jak měřítko [Počet zákazníků], tak měřítko [Prodeje].

Měřítko:

Prodeje třetí město (podle počtu zákazníků 2) =
CALCULATE
(
    [Prodeje],
    INDEX
    (
        3,
        ALLSELECTED(Customer[City]),
        ORDERBY
        (
            [Počet zákazníků], DESC,
            [Prodeje], DESC,
            Customer[City], ASC
        )
    )
)

Třetí způsob řazení, tedy způsob řazení podle sloupce 'Customer'[City] vzestupně je způsob řazení, který by funkce OFFSET() automaticky přidala do výpočtu v případě shody měřítka [Počet zákazníků] a měřítka [Prodeje] pro více měst. Může se tedy zdát že tento argument je nyní ve funkci OFFSET() zbytečný. Na druhou stranu vždy je lepší použít pro řazení explicitní výraz než se spoléhat na automatický způsob řazení, vzhledem k čitelnosti a srozumitelnosti výpočtů.

Ke způsobu řazení zbývá ještě jedna poznámka. V případě shody podle explicitně zadaných argumentů pro řazení bude funkce OFFSET() postupně přidávat jeden sloupec za druhým z tabulky ve druhém argumentu do chvíle, kdy dokáže jednoznačně určit pořadí jednotlivých řádků v zadané tabulce. Všechny postupně přidávané sloupce použité pro řazení budou řazeny vzestupně (ASC). I přesto ale může nastat situace, kdy ani po přidání všech sloupců z tabulky ve druhém argumentu nebude možné určit jednoznačné pořadí řádků v tabulce zadané ve druhém argumentu. V takovémto případě bude výsledkem funkce chyba a výpočet se nevyhodnotí.

Shrnutí

Funkce INDEX() a OFFSET() jsou funkce z kategorie WINDOW funkcí, které přinášení nový způsob tvorby výpočtů – výpočtů určených pro konkrétní vizuál s jeho předem danou strukturou. Výsledky měřítek, ve kterých budou použity WINDOW funkce tak v některých případech budou postrádat univerzálnost použití. To samo o sobě nemusí být problém, protože i bez WINDOW funkcí můžeme snadno vytvořit měřítko určené pouze pro vyhodnocení v konkrétním kontextu – v konkrétním vizuálu s předem danou strukturou. V těchto typech výpočtů je ale důležité ošetřit předem situaci tak, aby nedošlo k zobrazení výsledků měřítka v kontextu, pro který není toto měřítko určeno, například pomocí funkce IF() v kombinaci s funkcí HASONEVALUE() nebo ISINSCOPE().

Všechny příklady v tomto příspěvku byly vytvořeny za účelem vysvětlení chování funkcí INDEX() a OFFSET(). Praktické příklady vytvořené v Power BI pomocí DAX výpočtů můžete najít na stránce DAX – příklady nebo na stránce Power BI.

Oficiální Microsoft dokumentace funkcí INDEX a OFFSET:
https://learn.microsoft.com/cs-cz/dax/index-function-dax
https://learn.microsoft.com/cs-cz/dax/offset-function-dax

Stáhnout soubor s řešením.
č. 85

Komentáře