10 Lesson 10: SQL / Composite Types
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_10_sql_row.html Archive: DEV1-handouts-16-en.zip PDF pages: 24
Page 1: SQL / Composite Types
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
- Composite Types and How to Use Them
- Composite Type Arguments
- Functions Returning a Single Row
- Functions Returning a Set of Rows
Page 3: Composite Types
Composite type
- set of named attributes (fields)
- same as a table row, but without constraints
- Creating a composite type
- explicit declaration of a new type
- implicitly when a table is created
- record: a placeholder composite type
- Using a composite type
- attributes as scalar values
- operations on composite type values: comparison, check for NULL,
- use with subqueries
Speaker notes
A composite type represents a set of attributes, with each attribute having its own name and type. A composite type is similar to a table row in many ways. It is often called a record (similar to a structure in C-like languages).
https://postgrespro.com/docs/postgresql/16/rowtypes
A composite type is a database object. When it is declared, a new type is registered in the system catalog, making it a full-fledged SQL type. A table creation automatically produces a composite type with the same name. This type represents a row of the table. An important difference is that composite types do not have constraints.
https://postgrespro.com/docs/postgresql/16/sql-createtype
Composite type attributes can be used as regular scalar values (although each attribute can also be of a composite type itself).
A composite type can be used just like any other SQL type; for example, you can create table columns of this type. Composite values can be compared, checked for NULL, used with subqueries in clauses like IN, ANY/SOME, ALL.
https://postgrespro.com/docs/postgresql/16/functions-comparisons
https://postgrespro.com/docs/postgresql/16/functions-subquery
Page 4: Explicit Declaration of a Composite Type
The first way to introduce a composite type is to explicitly declare it.
Such a type can be used just as any other SQL type. For example, we can create a table that has some columns of this type:
Whether it is a good idea is not an easy question: there are no universal solutions here. In some cases, it can be quite useful; in other situations it’s more convenient to follow the relational data model, i.e., move the entity represented by this type into a separate table and add references to this table. It enables you to avoid data redundancy (normalization) and simplify indexing (a composite type is likely to require an index on expression). In general, PostgreSQL offers quite a lot of built-in data types, so the need for a custom type is unlikely to arise too often.
Constructing Composite Type Values
Composite type values can be constructed in the form of a string, with all the attributes listed in brackets. Note that the attributes of the string type are enclosed in double quotes:
Another option is to use the ROW constructor:
If the composite type contains more than one field, you can omit the ROW keyword:
Using Composite Type Attributes as Separate Values
Accessing a separate attribute of a composite type is virtually the same operation as accessing a table column, since a table row actually represents a composite type:
Page 5: Page 5
In most cases, the composite value has to be enclosed into brackets, e.g., to distinguish between a type attribute and a table column:
Or if you are using an expression:
A composite value does not necessarily belong to a particular type, it can be a value of generic record pseudotype:
But can you access an attribute of such a value?
No, because attributes of such a type have no name.
An Implicit Composite Type for Tables
In practice, composite types are typically used to facilitate the use of functions for table processing. When a table is created, a composite type with same name is created implicitly. For example, seats in the cinema:
The \dT command hides such implicit types, but you can take a look at them in the pg_type table if you like:
Here c stands for a composite type.
Operations on Composite Values
Page 6: Page 6
Composite type values can be compared with each other. It is done element by element (similar to string comparison, which is performed symbol by symbol):
Beware multiple intricacies of using null values within records. PostgreSQL also supports IS [NOT] NULL and IS [NOT] DISTINCT FROM predicates for composite values. Composite types can be used in subqueries, which happens to be very convenient. Let’s add a table with tickets:
Now we can write the following query to search for seats in tickets for a today’s screening:
We would have to explicitly join tables if we could not use a subquery.
Page 7: Routine Arguments
A routine can take composite type arguments
- Implementing computed fields
- Other options
- views
- GENERATED ALWAYS columns
Speaker notes
Naturally, routines can take arguments of composite types.
It’s worth noting that apart from the usual table.column notation, you can access a table column using the functional form: column(table). It allows us to create computed fields by declaring a function that takes a composite value as an argument.
https://postgrespro.com/docs/postgresql/16/xfunc-sql
This approach is a bit odd because there is a more straightforward way to get the same outcome by using a view. The SQL standard also defines GENERATED ALWAYS columns, but their implementation in PostgreSQL does not fully comply with the standard yet: generated columns are STORED in a table instead of being generated on the fly. Such VIRTUAL columns are to be introduced in PostgreSQL 18.
https://postgrespro.com/docs/postgresql/16/ddl-generated-columns
Page 8: Composite Type Parameters
Let’s declare a function that takes a composite value as an input parameter and returns a string with a seat number.
Note Note that concatenation is stable in general, not immutable: casting some data types to a string can give different results depending on the current settings.
It comes in handy that such functions allow you to pass a table row as an argument:
We can also do without an asterisk:
The syntax allows calling a function as if it were a table column (and vice versa, you can access a column as if it were a function):
Using this syntax, you can use functions like table columns computed on the fly. What if the table contains a column with the same name? Previously, the column would always have priority; starting from version 11, the choice depends on the syntactic form. Clearly, you can get the same outcome by creating a view.
Page 9: Page 9
And starting from version 12, you can declare “true” computed columns as a part of a table. But instead of being computed on the fly as defined by the SQL standard, they are stored in the table:
If we later wish to define the value explicitly, we can just drop the expression:
The data is still in the column, but the column is no longer computed.
Page 10: Single Row Functions
Return a composite value
- Usually called in SELECT list
- When called within a FROM clause, return a one-row table
Speaker notes
Functions can both take arguments of a composite type and return composite type values.
Functions are usually called in SELECT lists, but it is possible to call a function within a FROM clause, as if it were a one-row table.
Page 11: Functions Returning One Row
Let’s create a function that constructs a table row from separate components and returns it. Such a function can be declared as RETURNS seats:
The returned result is of a composite type. It can be “unfolded” into a one-row table:
Column names and types are received from the definition of the seats composite type here. Apart from calling a function in the SELECT list or as part of an expression, you can also call it in the FROM clause, as if it were a table:
As a result, we are getting a one-row table again. By the way, can we use the same calling method for a function that returns a single (scalar) value?
Yes, it’s also possible: we get a single column with a single row. Another approach that we have already seen in the SQL. Functions lecture is to define output parameters.
Note Note that you do not have to manually construct the composite type from separate fields in the query; it will be done automatically:
Page 12: Page 12
We get the same outcome, but names and types of the columns are taken from the function input parameters, while the composite type itself remains anonymous. And one more approach is to declare a function that returns the record pseudotype, which denotes a composite type in general, without specifying its structure.
But you won’t be able to call such a function in the FROM clause not just because the return type is anonymous, but also because the number and types of the fields in the returned composite type are not known in advance (at the parsing stage):
In this case, you have to specify the exact structure of the composite type when calling a function:
You can use any of these three approaches when creating functions. But you should keep in mind the expected use cases from the very beginning: whether it will be convenient to use anonymous types and specify the structure of the type during function calls.
Page 13: Set Returning Functions
Declared as RETURNS SETOF or RETURNS TABLE
- Can return multiple rows
- Usually called in a FROM clause
- Can be used as a view with arguments
- very convenient when combined with function inlining
Speaker notes
We have tried calling functions in a FROM clause, but have only seen one-row outputs so far. However, there is nothing stopping us from declaring functions that would return a set of rows: the so-called table functions or set returning functions (SRF).
It’s only natural to call these functions in a FROM clause, turning them into a pseudo-views to some extent. (Technically, PostgreSQL allows calling such functions in SELECT lists as well, but it is not recommended.)
Like with regular functions, the planner can sometimes inline the function body into the main query. It allows creating “views with arguments” without additional overhead.
https://wiki.postgresql.org/wiki/Inlining\_of\_SQL\_functions
Page 14: Set Returning Functions
Let’s create a function that returns all seats in a rectangular cinema hall of the specified size.
The key difference is the SETOF usage. In this case, instead of returning the first row of the last query, as usual, the function returns all the rows of the last query.
Instead of SETOF seats you can also use SETOF record:
But as we have already seen, in this case you have to specify the structure of the composite type when calling a function:
Or we could declare a function with output parameters. But SETOF record would still be required to show that the function returns a set of rows, not a single row:
Page 15: Page 15
Another equivalent way to declare a set returning function (which is even defined by the SQL standard) is to use the TABLE keyword:
It is sometimes useful to enumerate the rows returned by the query, in the order they were received from the function. There is a special clause for that:
When a function is used in a FROM clause, the LATERAL keyword is assumed to implicitly precede it, which allows this function to access columns of the tables that were mentioned in the query to the left of the function. It can sometimes simplify query definitions. For example, let’s create a function that distributes seats in the cinema like in an amphitheatre, with front rows having fewer seats than back rows:
Page 16: Page 16
It’s interesting that you can call a function returning a set of rows as part of the SELECT list:
It seems logical in some cases, but occasionally the result can surprise you. For example, how many rows will be returned by the following query?
We get six rows, while prior to version 10 we would get the least common multiple of the number of rows returned by each function (12 in this case). What’s even worse is that the query can return fewer rows than expected if the function returns no rows when passed some particular parameters. So using this calling method is not recommended.
Functions as Views with Parameters
As we have already seen, a function can be used in the FROM clause, as if it were a table or a view. We can also pass additional parameters in this case, which is sometimes very convenient. The only issue with this approach is that when a function is called (Function Scan), the queries in it must be executed completely first, and additional conditions defined in the query can only be applied after that.
Page 17: Page 17
It could become a problem if the function performed a long and complex query. In some cases, a function body can be inlined, i.e., inserted into the calling query. The requirements for set returning functions are more relaxed. The main restrictions are: the function must be written in the SQL language the function itself must not be volatile and must not call other volatile functions the function must not be STRICT the function body must contain only one SELECT statement (although it can introduce a complex query) In this case, we did not specify the volatility category when creating the function, so it was implicitly declared volatile.
There is virtually no function call now, and the condition is inserted into the query itself, which is more efficient.
Page 18: Takeaways
Composite types combine values of other types
- Simplify and enrich function operations on tables
- Implement computed fields and views with parameters
- Functions can return multiple rows
Page 19: Practice
- Create a function onhand_qty to calculate books in stock. The function takes a composite type parameter (books) and returns an integer number.
- Use this function in the catalog_v view as a computed field.
- Verify that the application can now display the number of books. 2. Create a set returning function get_catalog for book search. The function takes values from the search fields (author, book title, in stock) and returns matching books in the catalog_v format.
- Verify that you can now search for books and browse in the Store
- tab.
Speaker notes
The obvious solution is to use the existing catalog_v view, just with some row filters. But this view displays book titles and authors in the same field, and authors’ names are abbreviated. Clearly, searching for “Reuel” in the “J. R. R. Tolkien” field will yield no results.
The get_catalog function could repeat the query from the catalog_v view, but it is code duplication, which is a bad practice. So you should extend the catalog_v view by adding the following fields: the book title and the full list of authors.
Verify that the empty fields in the form are handled correctly. When calling the get_catalog function, does the client pass empty strings or null values?
Page 20: Task 1. The onhand_qty Function
Task 2. The get\_catalog Function
Extend the catalog_v view by adding book titles and the full list of authors (the application ignores extra fields). This function returns the full list of authors:
Let’s use this function in the catalog_v view. The view already exists, but we will recreate it with a different column order and use a new query:
The get_catalog function now uses the extended view:
Page 21: Page 21
Page 22: Practice+
- Create a function that converts a string representation of a hexadecimal number into a regular integer number. For example: convert(‘FF’) → 255 2. Extend this function with an optional argument that defines the base of a numeral system (16 by default). For example: convert(‘0110’,2) → 6 3. The generate_series set returning function does not work with string types. Create your own function that generates string sequences of uppercase Latin letters.
Speaker notes
- For example:
To solve this problem, you can use a regexp_split_to_table set returning function, upper and reverse functions, and WITH ORDINALITY clause.
Another option is to use a recursive query.
You can check the implementation using hexadecimal constants:
- For example:
- Assume that the input strings have the same length. For example:
Page 23: 1. A Hexadecimal System Function
Define a function for a single digit first:
Now, the main function:
2. A Function for Any Base
Assume the base is from 2 to 36: a digit may be a number from 0 to 9 or a letter from A to Z. In this case, the changes are minimal.
Note Note that in PostgreSQL 16 and onwards, integer constants can be not only decimal, but also binary, hexadecimal, and octal. This feature comes from the modern SQL standard:
Page 24: 3. The generate_series Function for Strings
First, create two auxiliary functions that convert a string to a numeric representation and vice versa. The first one is very similar to the function from the previous assignment:
The inverse function will use a recursive query:
Now, create generate_series for strings using generate_series for integers.