Dev0 03 SQL Row
Dev0 03 SQL Row
Composite Types
17
Copyright
© Postgres Professional, 2017–2025
Authors Egor Rogov, Pavel Luzanov, Ilya Bashtanov, Igor Gnatyuk
Translated by Liudmila Mantrova and Alexander Meleshko
Photo by Oleg Bartunov (Tukuche peak, Nepal)
Feedback
Please send your feedback, comments and suggestions to:
[email protected]
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.
Topics
2
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
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/17/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 the row of the table. An important difference is that
composite types do not have constraints.
https://postgrespro.com/docs/postgresql/17/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/17/functions-comparisons
https://postgrespro.com/docs/postgresql/17/functions-subquery
Explicit declaration of a composite type
CREATE TYPE
=> \dT
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:
CREATE TABLE
Whether it’s 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.
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:
INSERT 0 1
INSERT 0 1
If the composite type contains more than one field, you can omit the ROW keyword:
INSERT 0 1
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:
In most cases, the composite value has to be enclosed into brackets, e.g., to distinguish between a type attribute and a table column:
amount | amount
--------+--------
| 100.00
80.00 |
20.00 |
(3 rows)
amount
--------
10.00
(1 row)
A composite value does not necessarily belong to a particular type, it can be an indefinite value of the record pseudotype:
row
-------------
(10.00,EUR)
(1 row)
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:
CREATE TABLE
INSERT 0 3
The \dT command hides such implicit types, but you can take a look at them in the pg_type table if you like:
typtype
---------
c
(1 row)
line | number
------+--------
A | 42
B | 1
(2 rows)
PostgreSQL also supports IS [NOT] NULL and IS [NOT] DISTINCT FROM clauses for composite values.
CREATE TABLE
INSERT 0 2
Now we can write the following query to search for seats in tickets for a today’s movie:
line | number
------+--------
A | 42
(1 row)
Other options
views
GENERATED ALWAYS columns
Let’s declare a function that takes a composite value as an input parameter and returns a string with a seat number.
CREATE FUNCTION
Note that concatenation is normally stable, not immutable: casting some data types to a string can give different results depending
on the current settings.
seat_no
---------
A42
(1 row)
It comes in handy that such functions allow you to pass the actual table row as a parameter:
The syntax allows calling a function as if it were a table column (and vice versa, you can access a table 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.
CREATE VIEW
CREATE TABLE
=> \d seats2
Table "public.seats2"
Column | Type | Collation | Nullable | Default
---------+---------+-----------+----------+-----------------------------------------------
----------
line | text | | |
number | integer | | |
seat_no | text | | | generated always as (seat_no(ROW(line,
number))) stored
INSERT 0 3
If we later wish to define the value explictly, we can just drop the expression:
ALTER TABLE
The data is still in the column, but the column is no longer computed.
=> \d seats2
Table "public.seats2"
Column | Type | Collation | Nullable | Default
---------+---------+-----------+----------+---------
line | text | | |
number | integer | | |
seat_no | text | | |
Functions returning one row
Let’s create a function that constructs and returns a table row from separate components.
CREATE FUNCTION
seat
--------
(A,42)
(1 row)
The returned result is of a composite type. It can be “unfolded” into a one-row table:
line | number
------+--------
A | 42
(1 row)
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:
line | number
------+--------
A | 42
(1 row)
By the way, can we use the same calling method for a function that returns a single (scalar) value?
abs
-----
1.5
(1 row)
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 and Procedures” lecture is to define output parameters.
Note that you do not have to manually construct the composite type from separate fields in the query; it will be done automatically:
DROP FUNCTION
CREATE FUNCTION
seat
--------
(A,42)
(1 row)
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.
DROP FUNCTION
CREATE FUNCTION
seat
--------
(A,42)
(1 row)
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:
line | number
------+--------
A | 42
(1 row)
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.
Set returning functions
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 whole sets 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
Set returning functions
Let’s create a function that returns all seats in a rectangular cinema hall of the specified size.
CREATE FUNCTION
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.
line | number
------+--------
A | 1
A | 2
A | 3
B | 1
B | 2
B | 3
(6 rows)
DROP FUNCTION
CREATE FUNCTION
But as we have already seen, in this case you have to specify the structure of the composite type when calling a function:
a_line | a_number
--------+----------
A | 1
A | 2
A | 3
B | 1
B | 2
B | 3
(6 rows)
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:
DROP FUNCTION
=> CREATE FUNCTION rect_hall(
max_line integer, max_number integer,
OUT p_line text, OUT p_number integer
)
RETURNS SETOF record
IMMUTABLE LANGUAGE sql
BEGIN ATOMIC
SELECT chr(line+64), number
FROM generate_series(1,max_line) AS line,
generate_series(1,max_number) AS number;
END;
CREATE FUNCTION
p_line | p_number
--------+----------
A | 1
A | 2
A | 3
B | 1
B | 2
B | 3
(6 rows)
Another equivalent way to declare a set-returning function (which is even defined by the SQL standard) is to use the TABLE
keyword:
DROP FUNCTION
CREATE FUNCTION
t_line | t_number
--------+----------
A | 1
A | 2
A | 3
B | 1
B | 2
B | 3
(6 rows)
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:
=> SELECT *
FROM rect_hall(max_line => 2, max_number => 3) WITH ORDINALITY;
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:
=> CREATE FUNCTION amphitheatre(max_line integer)
RETURNS TABLE(t_line text, t_number integer)
IMMUTABLE LANGUAGE sql
BEGIN ATOMIC
SELECT chr(line + 64), number
FROM generate_series(1,max_line) AS line, -- <--+
generate_series(1, -- |
line ----------------------+
) AS number;
END;
CREATE FUNCTION
t_line | t_number
--------+----------
A | 1
B | 1
B | 2
C | 1
C | 2
C | 3
(6 rows)
It’s interesting that you can call a function returning a set of rows as part of the SELECT list:
rect_hall
-----------
(A,1)
(A,2)
(A,3)
(A,4)
(B,1)
(B,2)
(B,3)
(B,4)
(C,1)
(C,2)
(C,3)
(C,4)
(12 rows)
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?
rect_hall | rect_hall
-----------+-----------
(A,1) | (A,1)
(A,2) | (A,2)
(A,3) | (B,1)
(B,1) | (B,2)
(B,2) |
(B,3) |
(6 rows)
We get six rows, while prior to version 10 we would get the least common multiple of all 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.
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 a Function Scan must be completed before additional conditions defined in the query can
be applied.
=> EXPLAIN (costs off)
SELECT * FROM rect_hall(3,4) WHERE t_line = 'A';
QUERY PLAN
--------------------------------
Function Scan on rect_hall
Filter: (t_line = 'A'::text)
(2 rows)
It could become a problem if the function performed a long and complex query.
In some cases, a function body can be inlined, e.g., inserted into the calling query. The requirements for set-returning functions are
more relaxed. The main restrictions are:
In this case, we did not specify the volatility category when creating the function, so it was implicitly declared volatile.
ALTER FUNCTION
QUERY PLAN
------------------------------------------------
Nested Loop
-> Function Scan on generate_series line
Filter: (chr((line + 64)) = 'A'::text)
-> Function Scan on generate_series number
(4 rows)
There is virtually no function call now, and the condition is inserted into the query itself, which is more efficient.
Takeaways
11
Practice
12
1.
FUNCTION onhand_qty(book books) RETURNS integer
2.
FUNCTION get_catalog(
author_name text, book_title text, in_stock boolean
)
RETURNS TABLE(
book_id integer, display_name text, onhand_qty integer
)
The obvious solution is to use the existing view catalog_v, 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?
Task 1. The onhand_qty function
CREATE FUNCTION
CREATE VIEW
Let’s extend the catalog_v view by adding book titles and the full list of authors (the application ignores unknown fields).
CREATE FUNCTION
Let’s use this function in the catalog_v view. The view already exists, but we will recreate it with a different row order and use a
new query:
DROP VIEW
CREATE VIEW
CREATE FUNCTION