Composite Types in PostgreSQL

Learn how to define and use PostgreSQL composite types to group related fields into a reusable structure for table columns, function return types, and structured data containers.

5 min read · Back to overview

Quick Answer

A PostgreSQL composite type groups multiple named fields into a single structure using CREATE TYPE ... AS (...). Use it as a column type, function return type, or function argument — just like any built-in type. Access individual fields with the (column).field syntax.

Spin up a Postgres database in 20 seconds with Vela.

Try Vela Sandbox

PostgreSQL composite types let you bundle related fields into a single named structure. Once defined, a composite type can be used as the data type of a table column, returned from a function, or passed as a function argument — just like any built-in type.

Syntax

CREATE TYPE type_name AS (
  field_name1  data_type1,
  field_name2  data_type2,
  ...
);

Practical Example

Define a composite type for shipping addresses and use it in an orders table:

CREATE TYPE shipping_address AS (
  street      TEXT,
  city        TEXT,
  state       CHAR(2),
  postal_code VARCHAR(20),
  country     VARCHAR(50)
);

CREATE TABLE orders (
  id          SERIAL PRIMARY KEY,
  product_sku VARCHAR(50)       NOT NULL,
  quantity    INTEGER           NOT NULL CHECK (quantity > 0),
  ship_to     shipping_address,
  placed_at   TIMESTAMPTZ       NOT NULL DEFAULT now()
);

Inserting Composite Values

Construct a composite value using the ROW() expression:

INSERT INTO orders (product_sku, quantity, ship_to)
VALUES (
  'BOLT-TI-M6',
  10,
  ROW('123 Industrial Way', 'Portland', 'OR', '97201', 'USA')
);

-- Individual field notation using dot syntax
INSERT INTO orders (product_sku, quantity,
                    ship_to.street, ship_to.city, ship_to.state,
                    ship_to.postal_code, ship_to.country)
VALUES ('PANEL-CF-A4', 2, '500 Tech Park Blvd', 'Austin', 'TX', '78701', 'USA');

Accessing Composite Fields

Wrap the column name in parentheses and append the field name with a dot — the parentheses are required:

SELECT
  id,
  product_sku,
  (ship_to).city    AS ship_city,
  (ship_to).state   AS ship_state,
  (ship_to).country AS ship_country
FROM orders;
 id | product_sku |  ship_city | ship_state | ship_country
----+-------------+------------+------------+--------------
  1 | BOLT-TI-M6  | Portland   | OR         | USA
  2 | PANEL-CF-A4 | Austin     | TX         | USA

Expand all fields at once with .*:

SELECT id, product_sku, (ship_to).* FROM orders;

Updating Composite Fields

Use dot notation in the SET clause (no parentheses around the column name in SET):

UPDATE orders
SET ship_to.postal_code = '97202',
    ship_to.city = 'Portland'
WHERE id = 1
RETURNING id, product_sku, (ship_to).city, (ship_to).postal_code;

Using Composite Types as Function Return Types

CREATE TYPE order_stats AS (
  total_orders   INTEGER,
  total_quantity BIGINT,
  avg_quantity   NUMERIC(10,2)
);

CREATE OR REPLACE FUNCTION get_product_stats(p_sku VARCHAR(50))
RETURNS order_stats AS $$
  SELECT
    COUNT(*)::INTEGER,
    SUM(quantity)::BIGINT,
    AVG(quantity)::NUMERIC(10,2)
  FROM orders
  WHERE product_sku = p_sku;
$$ LANGUAGE SQL;

SELECT * FROM get_product_stats('BOLT-TI-M6');

Managing Composite Types

-- Add a field to an existing composite type
ALTER TYPE shipping_address ADD ATTRIBUTE apartment TEXT;

-- Drop a field (requires CASCADE for dependent columns)
ALTER TYPE shipping_address DROP ATTRIBUTE apartment CASCADE;

-- List user-defined types
\dT+

-- Drop the type (fails if columns reference it unless CASCADE)
DROP TYPE IF EXISTS shipping_address CASCADE;

Testing with Vela

Composite type changes are high-impact schema operations that lock every table using the type. Before running ALTER TYPE ... ADD ATTRIBUTE or DROP ATTRIBUTE on a production composite type used by a large orders table, create a database branch, apply the migration, verify that all dependent functions and queries still work correctly, and measure the lock duration — then schedule the production deployment accordingly.

Production Tips

  • Composite types are best for logically grouped fields that always travel together — addresses, coordinates, price-with-currency pairs.
  • Querying individual fields requires the (column_name).field syntax — forgetting the parentheses causes a parse error.
  • Altering a composite type is disruptive — plan migrations carefully and prefer adding optional fields over removing required ones.
  • Document composite types with COMMENT ON TYPE type_name IS 'description'; so their purpose is visible in schema tools.
  • For structured column data that needs flexible queries, JSONB is often more practical than a composite type — it does not require DDL changes when the structure evolves.

Continue in PostgreSQL Data Types: Back to tutorial overview.

Related in this section: Boolean · CHAR, VARCHAR, and TEXT · NUMERIC

Frequently Asked Questions

What is a composite type in PostgreSQL?
A composite type is a user-defined data type that groups multiple named fields into a single structure, similar to a struct in C. You define one with CREATE TYPE ... AS (...) and then use it wherever a regular data type is accepted — as a column type, function parameter, or function return type.
Does altering a composite type lock dependent tables?
Yes. ALTER TYPE ... ADD ATTRIBUTE or DROP ATTRIBUTE acquires an ACCESS EXCLUSIVE lock on every table that has a column of the composite type. Plan composite type changes carefully and test on a branch before deploying to production.
What happens to functions that return a composite type when I alter that type?
Functions with RETURNS composite_type will continue to compile but may fail at runtime if the column set returned by the function body no longer matches the updated composite type definition. Always test functions after altering their return type.
Can I use IF NOT EXISTS when creating a composite type?
PostgreSQL does not support IF NOT EXISTS for CREATE TYPE. Use a DO block to check pg_type before creating: DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'address_type') THEN CREATE TYPE address_type AS (...); END IF; END $$;
What is the difference between a composite type and a table row type?
Every table in PostgreSQL automatically creates a composite type with the same name. A standalone composite type created with CREATE TYPE exists independently of any table. Both can be used as column types or function parameters. The table row type includes all columns; the standalone type includes only the fields you declare.