top of page

DAX User Defined Functions (předběžný náhled)

  • Writer: Vojtěch Šíma
    Vojtěch Šíma
  • Sep 21
  • 6 min read

Updated: 6 days ago

tl;dr Uživatelské funkce v DAXu (dál jen UDFx, protože UDF už označuje Fabric User Defined Functions) jsou nové objekty v sémantickém modelu. Klíčové slovo je „function“: lze zabalit DAXový výraz do znovupoužitelné funkce a volat ji v measure, vypočteném sloupci, vizuálním výpočtu nebo i uvnitř jiné UDFx.

Nechystám se vyjmenovat každou vychytávku, kterou lze zabalit do funkce. Fantazie ti určitě nechybí. Raději projedu pár rychlých technických drobností, které mě zaujaly.

Na počtu záleží

Rychlé upozornění, než zboříš model a kapacitu. Stejně jako u měr (measures), na počtu záleží. Nicméně, reálná změna začíná až od několika tisícovek measurů. Ale, říkám to proto, že už teď existují knihovny s UDFX funkcemi, které teoreticky můžeš "importovat" a už teď vidím scénáře, kde tě napadne, "jo, raději si tam přidám toto, i toto..." až nakonec máš 30 tisíc measurů, kde už rozdíl poznáš, jak skrz práci s modelem, tak může ovlivňovat i rychlost renderování vizuálů.



Vlastní funkce v DAXu (User-Defined Functions)

Uživatelské funkce v DAXu (dál UDFx, protože UDF už znamená Fabric User Defined Functions) jsou nové objekty v sémantickém modelu. Klíčové slovo je „function“: lze zabalit DAXový výraz do volatelné jednotky a použít ho později v míře, vypočteném sloupci, vizuálním výpočtu nebo i uvnitř jiné UDFx.


UDFx se chovají jako běžné vestavěné funkce typu CALCULATE nebo SUM. Mohou přijímat nula až více parametrů s danými typy a podtypy. Návratový typ se nedefinuje explicitně. Výsledek závisí na tom, co UDFx dělá a jaké funkce nebo výrazy používá, případně lze výsledek přetypovat či převést. Prakticky tedy pracuj s výstupy UDFx stejně jako s výstupy vestavěných funkcí.


ady je pár definic, které vycházejí (většinou) přímo z Microsoft Learn.


Typy

Jako vždy platí: chceš-li navrhovat lepší UDFx, definuj typy parametrů. Typy se dělí do dvou kategorií:

  • Value types - eager vyhodnocení (okamžité):

    • AnyVal: Přijímá skalar nebo tabulku. Výchozí typ, když typ u parametru vynecháš.

      Scalar: Přijímá skalární hodnotu. Lze doplnit o podtyp.

      Table: Přijímá tabulku, ať už odkaz na tabulku, nebo tabulkový výraz.

  • Expression types - lazy vyhodnocení (až když je hodnota potřeba):

    • AnyRef: Přijímá odkaz, tedy sloupec, tabulku, kalendář nebo measure.


Subtypes

Pokud je potřeba, lze typy zpřesnit pomocí podtypů:

  • Variant: Přijímá libovolný skalar.

  • Int64: Přijímá celé číslo.

  • Decimal: Přijímá pevnou desetinnou přesnost (např. Currency nebo Money).

  • Double: Přijímá číslo s plovoucí desetinnou čárkou.

  • String: Přijímá text.

  • DateTime: Přijímá datum a čas.

  • Boolean: Přijímá TRUE/FALSE.

  • Numeric: Přijímá libovolnou číselnou hodnotu (subtypy Int64, Decimal nebo Double).


ParameterMode

ParameterMode řídí, kdy a kde se výraz parametru vyhodnocuje. Možnosti:


  • val (eager vyhodnocení): Výraz se spočítá jednou před voláním funkce. Do funkce se předá už hotová hodnota. Běžné pro jednoduché skalary a tabulky. Výchozí mód, když jej u parametru vynecháš.

  • expr (lazy vyhodnocení): Výraz se vyhodnocuje uvnitř funkce, potenciálně v jiném kontextu (třeba řádkovém nebo filtračním) a klidně i víckrát, pokud se na něj odkazuje opakovaně nebo uvnitř iterací. Povinné pro referenční parametry a užitečné, když chceš mít pod kontrolou kontext vyhodnocení.


Teď si k tomu přidáme vlastní vysvětlení a příklady.


Definice UDFx v DAX Query

Pojďme to dát dohromady a ukázat si, jak vytvořit UDFx.


Definice UDFx
Definice UDFx
DEFINE
    /// This is a description - Calculation percentage value of a column
    FUNCTION percentOf = (
    			toCalc : scalar numeric expr,
			byColumn : anyref expr
        ) =>
        DIVIDE( toCalc, CALCULATE( toCalc, REMOVEFILTERS( byColumn ) ) )

V DAX query se nová funkce začíná klíčovým slovem DEFINE nahoře v dotazu. Volitelně lze hned přidat popis. Na dalším řádku následuje FUNCTION a název funkce.


Řádek „Update model“ se generuje automaticky. Nepíše se ručně. Slouží k indikaci, zda se funkce přidá, nebo upraví.

Funkce mohou mít parametry. Syntaxe je podobná M, například () =>. Parametry se oddělují čárkami a mohou používat typy a parameterMode bez ohledu na velikost písmen. Struktura je type -> subtype -> parameterMode.


Jsou doporučené, ale dobrovolné:

Parametry bez typů a parameterMode
Parametry bez typů a parameterMode

Lze části signatury vynechat. Stačí type -> parameterMode nebo subtype -> parameterMode, jen drž pořadí. Typ nepatří za podtyp.


Za signaturou následuje tělo funkce. Dá se použít libovolný DAX výraz, klidně i proměnné.


Pozor na typy parametrů. Je dobré je sladit s reálným použitím. Pokud se bude odkazovat na tabulku nebo sloupec, nepoužívej AnyVal. Rozbije to measure i cokoli, co UDFx volá.


Definice sama na chyby neupozorní. Odpovědnost za správnost leží na autorovi.


Po dokončení klikni na „Update model with changes“. Pouhé spuštění query nic neudělá.


Uložení funkce do modelu
Uložení funkce do modelu

Definice UDFx v TMDL

V TMDL se to píše skoro stejně. Je to o něco otravnější, protože je potřeba hlídat odsazení závorek a bloků. Může to vypadat třeba takhle:

Definice UDFx v TMDL
Definice UDFx v TMDL

Použití UDFx v measurech

Použití je přímočaré. Ber ji jako jakoukoli vestavěnou funkci: napiš název, přidej závorky a předdej potřebné argumenty. IntelliSense pomůže, pokud máš funkci i její parametry správně definované.


total percentage of letter = percentOf( [total], 'Table'[letter] )

IntelliSense vypadá takto:

IntelliSense
IntelliSense

Použití UDFx v calculation group

V calculation groups se nic zásadního nemění. Použij SELECTEDMEASURE() a zavolej svou UDFx jako jakoukoli jinou funkci.


_ci PercentOf = percentOf( SELECTEDMEASURE(), 'Table'[letter] )

Pak to použij jako obvykle, třeba v matici/matrixu, kde je udfx showcase sloupec calculation group.


Použití UDFx v calculation group
Použití UDFx v calculation group

Použití UDFx ve visual calculation

UDFx lze použít ve visual calculation, jen ne v téhle konkrétní podobě. Spadne to na části s REMOVEFILTERS. Je potřeba úprava, aby se odkazovalo jen na pole, která existují přímo v téhle visual calculation.


Chceš-li stejný výsledek, předpočítej total, přidej to pole do vizualizace a procento pak spočítej upravenou UDFx, která funguje v kontextu dané visual calculation..


Použití UDFx ve visual calculation
Použití UDFx ve visual calculation

Podtržení a hláška, že „percentOfVisualsCalc“ neexistuje, není "bug". Je to "feature". Nenech se tím odradit, fungovat to bude.

Samotná funkce je spíš jednoduchá a primitivní (a upřímně k ničemu):

    FUNCTION percentOfVisualCalc = (
		    toCalc,
			byColumn
        ) =>
        FORMAT( DIVIDE( toCalc, byColumn), "Percent")

Použití UDFx v počítaném sloupci

Stejný příběh i u počítaného sloupce. Původní definice funkce se tu zadýchá. Jakmile se uvnitř CALCULATE sáhne na řádkový kontext [value], nebo se v REMOVEFILTERS odkáže na [letter], celé se sesype. Jako ukázka nestačí, chce to drobné doladění.


Tato funkce nebyla designovaná pro počítané sloupce. Jen heads-up: míchat UDFx vzory s řádkovým kontextem bolí (nebo může bolet).


Použití UDFx v počítaném sloupci
Použití UDFx v počítaném sloupci

Použití UDFx v jiné UDFx

Tady jde asi o nejmíň svázaný scénář, aspoň ve fázi definice. Editor na tebe nebude křičet, když něco pokazíš. Vezmeme původní funkci, zabalíme ji a v další funkci na ni napojíme dynamické formátování nějak takto:

UDFx formatter
UDFx formatter
	/// Formatter
    FUNCTION percentOfFormatter = (
			toCalc : scalar numeric expr,
			byColumn: anyref expr,
		    byFormat: scalar string val
        ) =>
        FORMAT( percentOf( toCalc, byColumn), byFormat )

Ukázkové použití může vypadat třeba takhle:

percentOfDynamicFormat = percentOfFormatter( [total], 'Table'[letter], "##.000%" )
Dynamicky formátovaná míra pomocí UDFx
Dynamicky formátovaná míra pomocí UDFx
Disclaimer: jen rychlý náhled. Použij správné dynamické formátování v nastavení formátu, ne přímo ve výrazu míry.

Rozdíl mezi val a expr

Mrkněme blíž na volby parameterMode: val a expr. Když se použijí správně, chovají se docela odlišně a klidně dají jiné výsledky.


val se vyhodnocuje hned. Argument se spočítá jednou ještě před startem funkce. Ber to jako lehkou proměnnou, která získá hodnotu na začátku a tu si drží po celé volání.


expr je líné. Argument se vyhodnocuje až uvnitř funkce, klidně v jiném filter kontextu, a může se vyhodnotit i víckrát.


Příklad, na kterém to ukazuju, je tady:

Příklad val a expr
Příklad val a expr
    /// Receives a materialized table
    FUNCTION CountRowsNow = ( t : table val) =>
        COUNTROWS( CALCULATETABLE( t, REMOVEFILTERS( 'Table' ) ) )

    /// Receives an unevaluated table expression
    FUNCTION CountRowsLater = ( t : table expr) =>
        COUNTROWS( CALCULATETABLE( t, REMOVEFILTERS( 'Table' ) ) )

Jsou stejné, liší se jen názvem a hodnotou parameterMode. Kdybych je volal, vypadalo by to takhle:

Volání val a expr
Volání val a expr

Výsledek bude:

Výsledek volání val a expr
Výsledek volání val a expr

Kdybych přeložil, co se dál děje v DAXu, šlo by to napsat i takhle:

UDFx přepsané
UDFx přepsané

Na první dobrou vypadají volání stejně. Rozdíl je jen v tom, kam spadnou filtry. V jednom případě sedí letter = "a"  ve vnitřním kontextu, v druhém ve vnějším.


Přemýšlej o val takto: vnějšek se nastaví s letter = "a".  Uvnitř funkce pak běží REMOVEFILTERS('Table'), což smaže filtry na 'Table', včetně vnějšího letter = "a". Výsledek: jde spočítat všechny řádky v 'Table'.


U expr se vše vyhodnocuje až uvnitř funkce. Po REMOVEFILTERS('Table') může vizuál nebo volající pořád nasadit letter = "a" zvenku, takže skončíš jen se dvěma řádky pro "a".


Takhle jde líp vidět, jak se funkce chová s expr vs val. Snad pomůže.


Vypsání všech UDFx

Pokud chceš vypsat všechny svoje UDFx, můžeš to udělat přes DAX query. Konkrétně zavolej funkci INFO.FUNCTIONS(). Je to trochu zákeřné, protože musíš přidat konkrétní omezení, aby funkce vrátila výsledky.


INFO.FUNCTIONS("ORIGIN", "2")

Seznam UDFx
Seznam UDFx

Tohle vypíše tvoje funkce. Pokud chceš vidět vestavěné funkce a svoje UDFx pohromadě s rozumným filtrováním, nejde to jedním tahem. Potřebuješ samostatné dotazy a pak výsledky sjednotit přes UNION.


Když nespecifikuješ původ, nenajdeš UDFx ani podle názvu. Příklad: FUNCTION_NAME = "wrapper" nevrátí tvoji UDFx, i když stejným způsobem bez problému najdeš třeba CALCULATE.

INFO.FUNCTIONS("FUNCTION_NAME", "wrapper")
Prázdný výsledek
Prázdný výsledek

Takže je nutné vždy použít omezení na origin:

Správný výsledek
Správný výsledek

Je nutné použít origin. Když zkusíš interface_name, taky ti nepojede. DMVs a zjistil, že UDFx sídlí v TMSCHEMA_FUNCTIONS, zatímco vestavěné funkce jsou v MDSCHEMA_FUNCTIONS. Tipuju, že když neurčíš origin, dotaz defaultne na MDSCHEMA. Jen popisuji, co jsem našel.


Zmiňuju pro případ, že budeš spouštět DAX dotazy nad datasetem a tahat přehledy o UDFx napříč workspacy.


Zobrazení UDFx ve skupinách sémantického modelu

Pokud chceš svoje UDFx vidět vizuálně v modelu. Otevři v Power BI Desktopu Zobrazení modelu. Uvidíš novou skupinu s názvem Funkce. Tam žijí všechny tvoje UDFx.


Funkce v sémantickém modelu
Funkce v sémantickém modelu

1 Comment

Rated 0 out of 5 stars.
No ratings yet

Add a rating
Kotya666
Sep 22
Rated 5 out of 5 stars.

Very useful, very informative!

Like
bottom of page