What you will be able to do
- Write a SQL UDTF with RETURNS TABLE, and call it in a FROM clause with the TABLE keyword, including in joins
- Implement the five handler methods a Python UDAF needs, and register the UDAF with Snowpark
- Register anonymous, named, temporary and permanent UDFs with the Snowpark Python API, and manage their dependencies
1.User-defined table functions (UDTFs) in SQL
A user-defined table function (UDTF) returns a set of rows: zero, one or many, each with one or more columns. You declare the output shape with RETURNS TABLE(...), listing name-and-type pairs. At least one output column is required. In a SQL UDTF, the body must be a SELECT expression, and its columns must match the declared types position by position. A UDTF can take at most 500 input arguments and return at most 500 output columns.
create or replace function orders_for_product(PROD_ID varchar)
returns table (Product_ID varchar, Quantity_Sold numeric(11, 2))
as
$$
select product_ID, quantity_sold
from orders
where product_ID = PROD_ID
$$
;
select product_id, quantity_sold
from table(orders_for_product('compostable bags'))
order by product_id;You don't call a UDTF in the SELECT list as you would a scalar function. You call it in the FROM clause, with its name and arguments inside TABLE( ... ). Its output columns can then be used anywhere a normal table column can.
UDTFs can also take part in joins. A column from the other table can be passed in as the argument, so the function runs once for each row. In the example below, each year from favorite_years produces that year's favorite colors. If you need extra join conditions, put them in a WHERE clause, not in ON.
select *
from favorite_years y join table(favorite_colors(y.year)) c
order by year, color;Checkpoint 1 of 6· Fill the gap
Which keyword must wrap the UDTF call in the FROM clause?
select product_id, quantity_sold
from ? (orders_for_product('compostable bags'))
order by product_id;A UDTF is called in the FROM clause, with its name and arguments inside the parentheses after the TABLE keyword.
Source: docs.snowflake.comCheckpoint 2 of 6· Exam question
A team is migrating logic from an older Snowflake project that used JavaScript UDFs for string parsing and now wants to add unit-testable, module-based Python code with third-party libraries such as `regex`, while keeping the function callable from plain SQL. Which change best meets the new requirement?
Correct answer: A — Rewrite the handler as a Python UDF with `LANGUAGE PYTHON`, list `regex` in the `PACKAGES` clause, and keep the SQL signature so existing callers invoke it unchanged.
- A. Switching to a Python handler is correct because Python UDFs support the `PACKAGES` clause for Anaconda-hosted third-party libraries and can still be called with ordinary SQL syntax, satisfying both the tooling and compatibility needs.
- B. This is incorrect because JavaScript UDFs have no `PACKAGES` mechanism for installing external libraries; only Python (and staged imports for Java/Scala) support that dependency model.
- C. This is incorrect because JavaScript-based objects in Snowflake do not have access to npm packages, and converting to a procedure would also break the requirement to call the logic from plain SQL as a value.
- D. This is incorrect because Scala UDFs require staged custom JARs for external code and have no native `regex` package akin to the Anaconda channel Python draws from, making this a poor fit for the stated need.
Sources1
2.User-defined aggregate functions (UDAFs) in Python
A user-defined aggregate function (UDAF) takes one or more rows as input and produces a single row of output. Creating one takes two steps. First, write a handler class. Then register it, either with the Snowpark API or with SQL's CREATE FUNCTION. The same class works for both.
Snowflake calls the handler's methods at runtime. All five are required.
| Method | What it does |
|---|---|
| __init__ | Initializes the internal state of an aggregate |
| aggregate_state | Returns the internal state; must have a @property decorator; the state must be serializable by the Python pickle library |
| accumulate | Accumulates the state of the aggregate based on the new input row |
| merge | Combines two intermediate aggregated states |
| finish | Produces the final result based on the aggregated state |
class PythonSumUDAF:
def __init__(self):
# This aggregate state is a primitive Python data type.
self._partial_sum = 0
@property
def aggregate_state(self):
return self._partial_sum
def accumulate(self, input_value):
self._partial_sum += input_value
def merge(self, other_partial_sum):
self._partial_sum += other_partial_sum
def finish(self):
return self._partial_sum
sum_udaf = udaf(PythonSumUDAF, name="sum_int", replace=True, return_type=IntegerType(), input_types=[IntegerType()])Like a scalar UDF, a UDAF can be registered as a named or an anonymous function. There are three ways to register it:
- the udaf function, used directly or as a @udaf decorator on the class - UDAFRegistration.register - register_from_file, which points to a Python file or zip file
After registration, you can call the UDAF from SQL or from a DataFrame, for example df.agg(sum_udaf("a")).
Checkpoint 3 of 6· Match them up
Match each UDAF handler method to its role
Tap a term, then the definition that fits it.
The handler builds partial states row by row, combines partial states, and then returns the final result from the combined state.
“Combines two intermediate aggregated states.”Source: docs.snowflake.com
Sources2
3.Snowpark UDFs in Python: anonymous, named, temporary and permanent
With the Snowpark API, you can turn a Python lambda or function into a UDF and use it on DataFrames. Snowpark uploads your function's code to an internal stage. When you call the UDF, it runs on the server where the data is. A UDF created with SQL's CREATE FUNCTION can also be called from Snowpark.
There are two kinds of registration:
- An anonymous UDF is assigned to a variable. You can use it for as long as that variable is in scope. - A named UDF is registered with a name argument. Use this when you need to call it by name, or reuse it in a later session.
Calling register or udf creates a temporary UDF by default, which lasts only for the current session. To make it permanent, set is_permanent=True and give a stage_location where the code and its dependencies are uploaded. In code that may run in several sessions, use the register method rather than udf, so that the default Session object can always be found.
Checkpoint 4 of 6· Fill the gap
Which argument makes this named UDF survive beyond the current session?
@udf(name="minus_one", ? =True, stage_location="@my_stage", replace=True)
def minus_one(x: int) -> int:
return x-1Setting is_permanent=True, together with a stage_location, creates a permanent UDF instead of a session-scoped temporary one.
Source: docs.snowflake.comDependencies have to be declared:
- Your own modules, zip files, resource files or directories are added with session.add_import(). Snowpark uploads them to an internal stage and imports them when the UDF runs. The Snowpark library itself is not uploaded automatically. Python's built-in libraries are already available on the server. - Third-party packages come from the Snowflake Anaconda channel. Add them for the whole session with session.add_packages or session.add_requirements, or for one UDF with @udf(packages=[...]). UDF-level packages override session-level ones.
Pinning versions matters for production. A permanent UDF resolves its dependencies once, when it is registered. A temporary UDF should list exact versions, or a newer package version may be picked up later.
Checkpoint 5 of 6· Check yourself
In a local development environment, how do you make your own my_module.py available to a Snowpark UDF?
Local files, directories and staged zip files are added with Session.add_import(). add_packages is for Anaconda packages.
“To do this using your local development environment, you must call Session.add_import() in your code.”Source: docs.snowflake.com
Checkpoint 6 of 6· Exam question
An engineering team already has a scoring model packaged as a compiled JAR that exposes a `MyScorer.score(String)` method, and they need to invoke that exact compiled artifact from Snowflake SQL without rewriting the model logic in another language. Which UDF definition matches this need?
Correct answer: A — Define a Java UDF with `LANGUAGE JAVA`, an `IMPORTS` clause pointing to the staged JAR, and `HANDLER = 'MyScorer.score'`, loading the existing class directly.
- A. A Java UDF is correct because it is designed exactly for this case: `IMPORTS` stages the existing JAR, and `HANDLER` points at the class and method to invoke, reusing the compiled artifact with no rewrite.
- B. This is incorrect because there is no `SYSTEM$RUN_JAR` function in Snowflake, and SQL UDFs cannot execute compiled bytecode; SQL handlers only evaluate SQL expressions.
- C. This is incorrect because pasting decompiled source defeats the goal of reusing the compiled artifact as-is, and JavaScript UDFs are not the mechanism for loading staged JAR classes.
- D. This is incorrect because Python's `PACKAGES` clause references Anaconda-channel Python packages, not compiled Java classes, so a JAR path cannot be loaded that way.
Sources3
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.A UDTF is called in the SELECT list like a scalar function, and extra join conditions go in an ON clause.Why is that wrong?
A UDTF is called in the FROM clause, inside TABLE( ... ). When it is joined to another table, extra conditions go in a WHERE clause, not ON.
Covered in User-defined table functions (UDTFs) in SQL
2.Giving a Snowpark UDF a name makes it permanent.Why is that wrong?
Named and anonymous UDFs are both temporary by default. A permanent UDF needs is_permanent=True and a stage_location.
Covered in Snowpark UDFs in Python: anonymous, named, temporary and permanent
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.
“return a set of rows, consisting of 0, 1, or multiple rows, each of which has 1 or more columns.”
↩︎ User-defined table functions (UDTFs) in SQL“of a SQL UDTF must be a SELECT expression.”
↩︎ User-defined table functions (UDTFs) in SQL“Tabular functions (UDTFs) have a limit of 500 input arguments and 500 output columns.”
↩︎ User-defined table functions (UDTFs) in SQL“you must include the UDTF name and arguments inside parentheses following the TABLE keyword.”
↩︎ User-defined table functions (UDTFs) in SQL“note that the join column from the table is passed as an argument to the function.”
↩︎ User-defined table functions (UDTFs) in SQL“note that a WHERE clause, rather than ON, must be used for additional join conditions”
↩︎ Exam trap 1 - 2.
“A UDAF takes one or more rows as input and produces a single row of output.”
↩︎ User-defined aggregate functions (UDAFs) in Python“You can also use udaf as a @udaf decorator on the handler class.”
↩︎ User-defined aggregate functions (UDAFs) in Python“Use the register_from_file function, pointing to a Python file or zip file containing Python source code.”
↩︎ User-defined aggregate functions (UDAFs) in Python“Combines two intermediate aggregated states.”
↩︎ Checkpoint - 3.
“When you call the UDF, the Snowpark library executes your function on the server, where the data is.”
↩︎ Snowpark UDFs in Python: anonymous, named, temporary and permanent“use the register method to register UDFs, rather than using the udf function.”
↩︎ Snowpark UDFs in Python: anonymous, named, temporary and permanent“You can add the UDF-level packages to overwrite the session-level packages you might have added previously.”
↩︎ Snowpark UDFs in Python: anonymous, named, temporary and permanent“When you create a permanent UDF, the UDF is created and registered only once.”
↩︎ Snowpark UDFs in Python: anonymous, named, temporary and permanent“you must also set the stage_location argument to the stage location where the Python file for the UDF and its dependencies are uploaded.”
↩︎ Exam trap 2“Calling register or udf will create a temporary UDF that you can use in the current session.”
↩︎ Prediction“To do this using your local development environment, you must call Session.add_import() in your code.”
↩︎ Checkpoint