15 Lesson 15: Query Optimization / Functions
This page is extracted from the official PDF into native Markdown. Slide text, code and speaker notes are kept in reading order; the original PDF and HTML remain the layout references. HTML source: No matching course-page link was captured in the source inventory. Archive: QPT-handouts-16-en-20251018.zip PDF pages: 26
Page 1: Query Optimization / Functions
Copyright
© Postgres Professional, 2019–2024 Authors Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov Photo by: Oleg Bartunov (Phu monastery, Bhrikuti summit, Nepal)
Use of course materials
Non-commercial use of course materials (presentations, demonstrations) is allowed without restrictions. Commercial use is possible only with the written permission of Postgres Professional. It is prohibited to make changes to the course materials.
Feedback
Please send your feedback, comments and suggestions to: edu@postgrespro.ru
Disclaimer
In no event shall Postgres Professional company be liable for any damages or loss, including loss of profits, that arise from direct or indirect, special or incidental use of course materials. Postgres Professional company specifically disclaims any warranties on course materials. Course materials are provided “as is,” and Postgres Professional company has no obligations to provide maintenance, support, updates, enhancements, or modifications.
Page 2: Topics
- Volatility сategories
- Inlining function code into the query
- Invoking Table Functions
- Settings for COST and ROWS
- Planner Helper Functions
- Configuration Parameters
Page 3: Volatility сategories
Volatile
- may return different values for the same input arguments
- is used by default
- Stable
- the value cannot change within a single SQL operator
- the function cannot change the database state
- Immutable
- the value cannot change, the function is deterministic
- the function cannot change the database state
Speaker notes
Each function is mapped to a particular volatility category, which defines the properties of the return value for the same input arguments.
The Volatile category indicates that the return value may vary arbitrarily. Such functions will be executed each time they are called. If the function is declared without a category specification, it is assumed to be volatile.
The Stable category is used for functions whose return value remains constant during a single SQL statement. In particular, such functions cannot change the state of the database. It could execute such a function only once during the query and then use the computed value.
The immutable category is even more strict: the return value always remains the same. Such a function could be executed at the planning stage, before the query is actually executed.
It does not mean that it happens so all the time, but the planner has the right to perform such optimizations.
https://postgrespro.com/docs/postgresql/16/xfunc-volatility
Page 4: Categories of Mutability and Optimization
Thanks to the additional information about a function’s behavior provided by the mutability category, the optimizer can reduce function calls. For testing, let’s create a function that generates a random number:
Let’s examine the execution plan for the following query:
In the execution plan, the Function Scan node calls the set-returning function generate_series. The result is filtered, and the scalar function random is invoked for each row in the filter. You can see this in action:
Page 5: Page 5
Here, we randomly select between 0 and 10 rows. A function with the Stable mutability category will be invoked just once — because we’ve effectively indicated that its value remains unchanged throughout the statement:
The Result node generates a sample row, and the One-Time Filter expression is evaluated once, resulting in either 0 or 10 rows being returned.
Page 6: Page 6
Finally, the Immutable category enables the function’s value to be computed during the planning phase, eliminating the need to evaluate the filter condition at runtime:
For Immutable, we get a random execution plan! The developer is responsible for providing knowingly false information.
Page 7: Inlining of Function Code
SQL Scalar Functions
- a single SELECT statement a SELECT statement without a FROM clause,
- returns a single value
- Called functions should not be more volatile than the calling function.
- etc.
- Set-Returning Functions
- a single SELECT statement
- Immutable or Stable category
- Non-strict function
- etc.
Speaker notes
The PostgreSQL optimizer can inline the function body into an SQL query. This applies to both scalar and set-returning functions.
In both cases, there are several limitations: the function must be written in SQL, use a single SELECT statement, and so on. Additionally, scalar functions must not access database tables or call functions with a less strict category, while set-returning functions must be stable or immutable.
A key benefit of inlining the function body into the query is that the function becomes transparent to the query planner. For instance, additional conditions in the main query can be applied to the query within the function’s body, enabling early filtering of unnecessary rows.
Page 8: Inline substitution of function code into the SQL query
The body of very simple SQL scalar functions can be inlined directly into the main SQL statement during query parsing. This eliminates function call overhead. We’ve already seen an example: our function rnd(). Let’s see what the query execution plan looks like when the volatility category of the function rnd (Stable) differs from that of the function random (Volatile):
The filter references the rnd() function. Let’s change the function’s volatility category to Volatile:
Now the filter references the random() function instead of rnd(). It will be called directly, bypassing the rnd() function’s “wrapper”. Table functions offer significantly more inlining opportunities. For example, they can reference tables.
During inlining, table functions behave like parameterized views. The query planner optimizes the entire query, with the function being transparent to it:
Page 9: Page 9
Page 10: Invoking Table Functions
{#source-figure-010 caption=“Source slide page 10: Invoking Table Functions”}
Speaker notes
When a set-returning function is called in the FROM clause, the Function Scan node is responsible for its execution. In this case, the function’s returned rows are materialized first and then passed to the parent plan node. This is the current implementation limitation.
If the function is called in the SELECT clause, it is executed by the ProjectSet node in the query plan. In this case, the function can take advantage of the on-demand row return interface (value-per-call) without materialization.
SQL functions, most built-in functions (written in C), and PL/Python functions that return an iterator operate in this manner. Functions written in other programming languages can also use this interface if the feature is implemented.
Page 11: Set-Returning Functions Functions Returning Tables
Let’s call the generate_series function in the FROM clause with a row limit (LIMIT):
In the query plan, we see a Function Scan node — the function generated all rows first, and then the LIMIT was applied. Now let’s run the query again, but call the function from the SELECT clause:
Now, the query plan includes a ProjectSet node that produces ten sample rows — here, the optimizer was able to retrieve rows on demand. The query execution time decreased by orders of magnitude. The Result node here represents the omitted FROM clause in the query — it sends exactly one row to the parent node. However, not all functions can return rows one at a time. For example, invoking any PL/pgSQL function returns all result rows:
Let’s invoke the function within the SELECT clause:
The plan includes a ProjectSet node, but now the server must retrieve the function’s full result before applying the LIMIT. Execution time clearly demonstrates this. In the SELECT clause, you can have multiple set-returning function calls, and the functions can be nested within each other:
Page 12: Page 12
The bottom ProjectSet node creates a result set by executing two set-returning functions: generate_series(1,3) and unnest. This results in three rows. The top ProjectSet node generates the final result set by executing the outer generate_series call. Ignoring materialization, this query is equivalent to the following one, where functions are invoked in the FROM clause:
Page 13: Settings for COST and ROWS
CREATE FUNCTION fun( )
- Function Scan ( rows=1000 cost=100 )
- оценки
- by default
- CREATE FUNCTION fun( ) ROWS 12 COST 123
- Function Scan ( rows=12 cost=123 )
- ROWS
- COST
Speaker notes
Usually (when the function body cannot be integrated into the query), the optimizer can’t analyze the function code and treats it as a “black box”.
However, you can provide the optimizer with approximate information about the cost of calling the function and the number of rows it returns.
The COST parameter defines the cost of a user-defined function in units of cpu_operator_cost. By default, C functions are given a cost estimate of 1, whereas functions in other languages are estimated at 100.
The ROWS parameter specifies the approximate number of rows returned.
COST and ROWS can be set when creating a function or for existing functions.
https://postgrespro.com/docs/postgresql/16/sql-copy
Page 14: COST and ROWS Settings COST and ROWS Settings
We’ll create a set-returning function in PL/pgSQL that returns the days of the week:
Execution Plan:
The function execution cost is treated as constant, with user-defined functions defaulting to a cost of 100 operators:
But this value can be adjusted:
The initial cost in the execution plan has changed. The server estimates the cardinality of this function’s result as 1000, even though the actual value is seven. By specifying ROWS, you can provide the server with an estimate of how many rows the function will return:
Let’s repeat the query:
Page 15: Page 15
Now the server estimates that the function will return 10 rows, resulting in a lower total cost for the node:
Changed values are visible in the system catalog:
Alternatively, use the psql metacommand:
Page 16: Helper Functions
{#source-figure-016 caption=“Source slide page 16: Helper Functions”}
CREATE FUNCTION fun(x) ROWS 18
- CREATE FUNCTION fun(x) SUPPORT fun_support
Speaker notes
The COST and ROWS parameters enable you to specify the cost and the number of rows returned by the function as fixed values.But constants don’t always yield the intended result.
In PostgreSQL, a function can use a helper function that provides the planner with information based on the arguments of the main (target) function.
The helper function can provide information using the values of the target function’s arguments:
● cost estimation
● estimate of the number of rows returned
● an expression equivalent to the function call
Additionally, for functions returning boolean:
● selectivity estimate
● an equivalent predicate using an indexable operator
The auxiliary function must be implemented in C.
https://postgrespro.com/docs/postgresql/16/sql-copy
Page 17: Planner auxiliary functions
Let’s examine the execution plan when calling the generate_series function:
Unlike the days_of_week function, the optimizer correctly estimates the number of rows returned immediately. Moreover, this estimate depends on the function’s parameters:
The planner gains additional information via the auxiliary function (which can only be implemented in C), enabling it to determine the selectivity of conditions, the function’s cardinality, or its cost. You can check the pg_proc table to see if an auxiliary function exists:
As shown, auxiliary functions are not available for all overloaded versions of the generate_series function. Let’s examine the execution plan of the query that generates a date series:
Without an auxiliary function or specifying ROWS, the optimizer doesn’t know the number of rows in the result and defaults to 1000. New auxiliary functions are introduced with each PostgreSQL version.
Page 18: Parallelism Annotations
CREATE FUNCTION fun( ) PARALLEL …
- Parallel-safe
UNSAFE
- partially parallelizable
- RESTRICTED
- Parallel-safe
- SAFE
Speaker notes
The topic of “Parallel Processing” explained that not all queries can be run in parallel. Since the optimizer can’t analyze the function body, it uses parallelism annotations to determine if parallel processing is feasible.
When defining a function (or later), you can choose one of three annotations:
● UNSAFE — prohibits parallel execution plans when the query includes a function call;
● RESTRICTED — allows parallel execution plans, but prohibits function calls in the parallel section of the plan;
● SAFE — safe for parallel execution
The UNSAFE annotation is the default setting.
Parallelism annotations are also applied to user-defined aggregate functions.
https://postgrespro.com/docs/postgresql/16/sql-copy
Page 19: Parallelism annotations
Parallelism annotations are visible in the proparallel column of the pg_proc table (r=restricted, s=safe, u=unsafe):
All primary standard functions are safe. The \df+ command in psql also displays the parallelism annotations (Parallel column):
Let’s see how the parallelism annotation affects the query’s execution plan. Let’s write a function that calculates the ticket cost. It is marked as safe for parallel execution.
The query verifies that the total booking cost equals the total ticket cost:
Page 20: Page 20
The query plan is split into two parts: the subquery in the InitPlan 1 node aggregates tickets, and the subquery in the InitPlan 2 node aggregates bookings. Both subqueries are currently running in parallel. Let’s change the parallelism annotation to UNSAFE:
Now both subqueries run in sequence — the annotation prohibits parallel execution plans. Now let’s mark the function as restricted for parallelism (RESTRICTED):
The subquery containing the function runs sequentially on the primary process, while the second subquery employs a parallel execution plan.
Configuration parameters
In some cases, it may be beneficial to structure queries as stored routines (e.g., to grant the application access to them). In such scenarios, an added benefit is the ability to configure parameters for individual routines.
Page 21: Page 21
Let’s take the query as an example:
Suppose we want to use parallel execution plans, but this specific query is meant to run sequentially. In that case, we can configure the parameter at the function level:
We covered how to check the query plan for a query executing inside a function in the “Profiling” section. We’ll use the auto_explain extension:
Run the query:
Show the latest entries in the message log:
Page 22: Takeaways
A function is a black box for the planner if its body is not inlined into the query.
- The invocation of set-returning functions is typically materialized.
- The planner can be provided with additional information.
- function variability category
- Cardinality and cost
- parallelism flag
- Helper functions aid in optimizing invocations of built-in functions.
Page 23: Practice
- Disable the materialization of a common table expression that uses the random function. Provide an explanation for the result. 2. Write a SQL wrapper function for the querySELECT * FROM generate_series (1, 10_000_000). Consider three scenarios: the original query and function calls in the FROM and SELECT clauses. Compare the query execution plans and the use of temporary files. What changes when the volatility category is set to Stable? 3. What volatility category does the days_of_week function in the demonstration have? What is its actual volatility?
- Function Definition:
Page 24: Materializing Mutable Functions in CTEs
A Common Table Expression containing a mutable function is always materialized:
In this case, the NOT MATERIALIZED clause has no effect:
2. Wrapper Function 2. Function Wrapper
By default, the function is classified as volatile. Basic query:
Since the function is called in the FROM clause, materialization occurs. With insufficient work_mem, all rows are written to disk (temp written) and then read back (temp read). Invoking a function in the SELECT clause:
Page 25: Page 25
The ProjectSet node does not perform materialization, so the temporary disk write/read counts remained unchanged. However, the execution time increased significantly, as it involves transferring ten million rows between nodes one at a time. Calling a function in the FROM clause:
In the main query, an additional Function Scan node is added within the function, causing the number of temporary pages used to double. Let’s change the function’s mutability category:
Calling the function in the SELECT clause results in no changes:
Let’s invoke the function from the FROM clause:
Now the function’s body is inserted into the main query.
3. Days of the Week
The function was created using the following command:
If the mutability category isn’t specified, it defaults to Volatile. It may seem like a constant function (Immutable) since it has no parameters and the list of days of the week remains unchanged.
Page 26: Page 26
However, the names of the days of the week depend on the locale settings. Current value:
Let’s adjust the configuration:
Now the function returns the names of the days of the week in English, so the appropriate category is Stable: