Composite types let a single PostgreSQL column carry a structured value, and they are a common building block in server-side programming: stored procedures use them to bundle arguments and to shape return values. The catch is that how you call a function returning a composite type has a measurable effect on runtime.

A composite value can be decomposed back into its individual fields when needed:

1

2

3

4

5

test=# SELECT ('(10, 'hans', 500)'::person).*;

id | name | income

----+------+--------

10 | hans | 500

(1 row)

Columns may also be declared with a composite type directly, just like any other data type:

1

2

3

4

5

6

7

8

test=# CREATE TABLE data (p person, gender char(1));

CREATE TABLE

test=# d data

Table 'public.data'

Column |     Type     | Collation | Nullable | Default

--------+--------------+-----------+----------+---------

      p |       person |           |          |

gender | character(1) |           |          |

Here the column type is person.

Setup: a three-million-row table

The pgstattuple extension is a good illustration. It exists to detect table bloat, and it reports its findings through a composite data type. Installing it is straightforward:

1

2

test=# CREATE EXTENSION pgstattuple;

CREATE EXTENSION

For the test below, a type holding three million entries is created first:

1

2

3

4

5

6

7

test=# CREATE TABLE x (id int);

CREATE TABLE

test=# INSERT INTO x SELECT *

FROM generate_series(1, 3000000);

INSERT 0 3000000

test=# vacuum ANALYZE ;

VACUUM

Inspecting all fields of x is then a matter of asking for the composite result:

1

2

3

4

5

6

7

test=# explain analyze SELECT (pgstattuple('x')).*;

                                        QUERY PLAN

-------------------------------------------------------------------------------------------

Result (cost=0.00..0.03 rows=1 width=72) (actual time=1909.217..1909.219 rows=1 loops=1)

   Planning Time: 0.016 ms

   Execution Time: 1909.279 ms

(3 rows)

That took close to two seconds. Rewriting the call, however, produces a very different picture:

1

2

3

4

5

6

7

8

test=# explain analyze SELECT * FROM pgstattuple('x');

                                           QUERY PLAN

----------------------------------------------------------------------------------------------

Function Scan on pgstattuple (cost=0.00..0.01 rows=1 width=72) (actual time=212.056..212.057

rows=1 loops=1)

    Planning Time: 0.019 ms

    Execution Time: 212.093 ms

(3 rows)

Moving the call into the FROM clause is substantially faster, and the same holds for a subselect:

1

2

3

4

5

6

7

test=# explain analyze SELECT (y).* FROM (SELECT pgstattuple('x') ) AS y;

                                       QUERY PLAN

-----------------------------------------------------------------------------------------

Result (cost=0.00..0.01 rows=1 width=32) (actual time=209.666..209.666 rows=1 loops=1)

    Planning Time: 0.034 ms

    Execution Time: 209.698 ms

(3 rows)

Why the FROM clause is faster

The reason is that PostgreSQL expands the FROM clause. An expression of the form (pgstattuple('x')) is rewritten into something else:

1

2

3

4

5

6

…

(pgstattuple('x')).table_len,

(pgstattuple('x')).tuple_count,

(pgstattuple('x')).tuple_len,

(pgstattuple('x')).tuple_percent,

…

In the expanded form the function is invoked more often, which accounts for the runtime difference. Knowing what the planner does under the hood therefore pays off directly — the speedup between the two spellings can be dramatic, and support cases involving this pattern have been seen recently.