DAX User Defined Functions (předběžný náhled)
- 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.

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é:

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á.

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:

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:

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 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..

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 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:

/// 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%" )

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:

/// 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:

Výsledek bude:

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

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")

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")

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

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.

Very useful, very informative!