14 Lesson 14: PL/pgSQL / Dynamic Commands
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: dev1_14_plpgsql_dynamic.html Archive: DEV1-handouts-16-en.zip PDF pages: 16
Page 1: PL/pgSQL / Dynamic Commands
Copyright
© Postgres Professional, 2017–2025 Authors: Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Igor Gnatyuk Translated by: Liudmila Mantrova, Alexander Meleshko, Elena Sharafutdinova 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
Postgres Professional assumes no responsibility for any damages and losses, including loss of income, caused by direct or indirect, intentional or accidental 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
- Objectives
- Executing Dynamic Queries
- Constructing Dynamic Commands
Page 3: Dynamic SQL
The text of an SQL query is constructed at run time
- Objectives
- provide additional flexibility for the application
- construct several specific queries for optimization purposes
- instead of using a single query that covers all possible cases
- Trade-off
- statements are not prepared
- the risk of SQL injection rises
- maintenance gets more complicated
Speaker notes
An SQL command is considered dynamic if its text is constructed and then executed within PL/pgSQL routine blocks or in anonymous blocks.
In most cases, you can do without dynamic commands, but sometimes they can provide additional flexibility. For example, an application can have a built-in capability to execute commands provided via system settings. Instead of being hard-coded by developers, these settings can be tuned by the support team in production.
When creating reports with a large number of optional parameters, it is sometimes easier to construct the text of a query at run time for the provided arguments only, instead of creating a complex query that includes all possible parameter combinations while developing the application.
The price you pay for using dynamic commands is inability to take advantage of prepared statements, which are used in PL/pgSQL by default. Besides, you have to take care of dynamic commands’ security as they are vulnerable to SQL injection.
We should also mention that maintenance gets more complicated. In particular, it will be impossible to scan the source code quickly for executable commands with tools like grep.
Page 4: Query Execution
EXECUTE construct
- runs a text representation of an SQL query
- allows using parameters
- PL/pgSQL variables do not become implicit parameters
- Can be used instead of an SQL query
- independently
- when opening a cursor
- in a loop over a query
- in the RETURN QUERY statement
Speaker notes
To run dynamic commands, PL/pgSQL uses the EXECUTE construct which launches the SQL command provided as a text string.
A dynamic query can contain explicit parameters. In the command’s text representation, parameters are denoted by $1, $2, etc.; their actual values are provided in the USING clause. Parameters are handled in the same way as in prepared statements (which is covered in Architecture. PostgreSQL Fundamentals). However, PL/pgSQL variables do not become implicit parameters, as it happens in the case of regular (as opposed to dynamic) use of SQL in PL/pgSQL.
The EXECUTE construct can be used as an independent statement (it will simply execute a dynamic command). It can also be used in loops over queries, when opening a cursor, or in the RETURN QUERY statement: in all these cases, EXECUTE replaces the SQL statement.
https://postgrespro.com/docs/postgresql/16/plpgsql-statements#PLPGSQL-STATEMENTS-EXECUTING-DYN
Note that a procedure cannot perform transaction control if it is called by an EXECUTE construct.
Page 5: Executing Dynamic Queries
The EXECUTE statement runs SQL commands provided as text.
The INTO clause of the EXECUTE statement enables saving a single row of the result (the first returned row) into a variable of a composite type or into several scalar variables. As with static commands, you can check the result of a dynamic command using GET DIAGNOSTICS (but not the FOUND variable).
You can use STRICT to ensure that the command processes only one row. The result of a dynamic query can be processed in a FOR loop.
Here is the same example using a cursor:
Page 6: Page 6
The RETURN QUERY statement can also use dynamic queries to return rows from functions. Let’s create a function that retrieves all buildings constructed by a particular architect and up to the specified year. We will have to use parameters for this purpose:
Page 7: Constructing Commands
Parameter values binding
- USING clause
- guarantees protection against SQL injection
- Escaping values
- identifiers: format(’%I’), quote_ident
- literals: format(’%L’), quote_literal, quote_nullable
- SQL injection is impossible if implemented correctly
- Regular string functions
- concatenation, etc.
- risk of SQL injection!
Speaker notes
Using the EXECUTE construct makes sense if the command is constructed dynamically. The previous examples could also do without EXECUTE.
Since the command is represented by a text string, it can be constructed using regular string functions that perform such operations as concatenation, etc. This should be done with great care as there is a risk of SQL injection.
If the values are passed as parameters in the USING clause, SQL injection is technically impossible.
However, it is not always possible to use parameters: you may have to concatenate specific parts of the query or insert a table name into the query. In this case, you should escape the values received from an unreliable source to protect your application against injections.
Identifiers are generated by either the format function with the %l specifier or the quote_ident function. These functions ensure that identifiers have valid names by double-quoting them and escaping special characters, if required.
https://postgrespro.com/docs/postgresql/16/functions-string
Page 8: Dealing with SQL Injection
Let’s rewrite the function returning buildings and add one more parameter: the name of the city. The idea is to allow this function to access tables only if their names start with city_.
The function works fine if its parameter values are “correct”:
But a malicious user can pick a value that will change the syntactic structure of the query and enable unauthorized access to data:
When you are using prepared statements or dynamic commands with parameters, such a situation is technically impossible because the structure of the SQL query is locked while the statement is parsed. An expression will always remain an expression; it is impossible to convert it, say, into a table name.
Constructing a Dynamic Command
It is impossible to provide the names of objects (such as tables or columns) as parameters of the USING clause in a dynamic command. Such identifiers must be escaped, so that it is impossible to modify the query structure:
Page 9: Page 9
The following function does the same thing:
Here is an example of creating a table:
Instead of using parameters, you can insert literals into a string. It also requires escaping, but in a bit different way:
The quote_nullable function also does the same thing:
The quote_literal function is quite similar, but it does not convert NULL values into literals:
As an example, let’s rewrite the function that returns the list of buildings of a particular city, so that it does not use any parameters, but still remains safe.
Page 10: Page 10
Note Note that we perform two extra type casting operations: first, the integer parameter is converted into a string, and then it is cast back to integer at run time (this can be avoided with USING parameters):
An attempt to pass an invalid value will not succeed:
Page 11: Takeaways
Dynamic commands provide additional flexibility
- Constructing separate queries for different arguments can improve performance
- Dynamic commands are not suitable for short, frequent queries
- Maintenance gets more complicated
Page 12: Practice
- Modify the get_catalog function so that the query to the catalog_v view is constructed dynamically and takes into account only those fields that are filled out in the search form of the Store tab. Make sure that your implementation is protected against SQL injection. Check your function in the application.
Speaker notes
- Suppose we have to generate the following query if these conditions are met: the “In stock” option is selected in the search form, but “Book Title” and “Author” fields are empty.
You should keep in mind that this implementation will not necessarily speed up search, but it will certainly be harder to maintain. Avoid such solutions in production environments unless you have a solid reason to use this technique. To learn more about query performance tuning, check out the QPT course.
Page 13: 1. The get_catalog Function
Page 14: Practice+
- Create a function that returns a matrix report on functions available in the database.
- The columns must contain names of function owners, the rows
- must provide schema names, while the cells must display the
- number of functions that belong to a particular owner in a
- particular schema.
- What statement should you write to invoke this function?
Speaker notes
- Here is a possible output:
The number of columns returned by a query is unknown beforehand. You need to construct a query and then execute it dynamically. The query text can be as follows:
The highlighted lines are a dynamic part that has to be constructed by a separate query. The start and the end of the query are static.
The proowner column has the oid type. To get the name of the owner, you can use the following construct: proowner::regrole::text.
Page 15: Building a Matrix Report
A function for generating dynamic query text:
The resulting query text:
Now create a matrix report function:
Simply executing the query will result in an error, as the structure of the returned records is not specified:
Page 16: Page 16
This is an important limitation on using functions that return an arbitrary set of values. You need to know and specify the structure of the record to be returned as the function is being called. In general, the returned record structure may be unknown, but for our matrix report, we can run another query that will show you how to call the matrix function correctly. Prepare the query text:
Now we can use matrix_call to get a query that reflects the structure of the matrix report, and then execute this query and get the report (psql allows you to do all this with single \\gexec command):
The matrix report is generated correctly. Repeatable Read isolation level guarantees that the report will be generated, even if a function with a new owner will come up between two queries. The query returned by form_query could have been run manually. However, you still need to return the list of columns to the client. The matrix_call function demonstrates how to solve this with an extra query. Another solution is to return a set of strings of a semi-structured type (such as JSON or XML) instead of an arbitrary structured set. These types are covered in the DEV2 course.