Custom Types
PostgreSQL is built around an extensible type system — the built-in types like integer, text, and timestamp are just the ones that ship in the box. You can define your own types with CREATE TYPE, giving names to structures that show up repeatedly in your schema. This page covers the two most useful forms: composite types, which bundle several fields into one reusable structure, and domains, which layer a named constraint on top of an existing base type.
Composite types
A composite type is a structured type made up of multiple named fields, similar to a row in a table, but usable as the type of a single column, a function parameter, or a function's return value. It is defined with CREATE TYPE ... AS, followed by a list of fields much like a table definition.
Defining a composite address type
CREATE TYPE address AS ( street TEXT, city TEXT, postal_code TEXT, country TEXT );
Once defined, address can be used as the type of a column, exactly like any built-in type. This is convenient when several tables in a schema need the same shape of data — a customer's shipping address and a warehouse's location, for instance — without duplicating four separate columns in every table.
Using a composite type as a column
CREATE TABLE customers ( customer_id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, home_address address ); CREATE TABLE warehouses ( warehouse_id SERIAL PRIMARY KEY, name TEXT NOT NULL, location address );
Inserting a composite value uses row-constructor syntax, and reading its fields back out uses dot notation (often with parentheses around the column reference, to keep the parser happy).
Inserting and reading composite fields
INSERT INTO customers (name, email, home_address)
VALUES (
'Jordan Blake',
'jordan@example.com',
ROW('221B Baker Street', 'London', 'NW1', 'UK')
);
SELECT name, (home_address).city, (home_address).country
FROM customers
WHERE customer_id = 1;name | city | country ---------+--------+--------- Jordan Blake | London | UK
Domains
A domain is different from a composite type: it is not a new structure with multiple fields, it is a single existing base type with a name and, usually, a constraint attached. Once created, a domain can be used as a column type anywhere the base type would be used, and PostgreSQL automatically enforces the constraint on every column of that domain.
A positive_int domain
CREATE DOMAIN positive_int AS INTEGER
CHECK (VALUE > 0);
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
stock_qty positive_int NOT NULL DEFAULT 0
);
INSERT INTO products (name, stock_qty) VALUES ('Wireless Mouse', -5);ERROR: value for domain positive_int violates check constraint "positive_int_check"
The real payoff of a domain shows up once the same constraint needs to apply to several columns across several tables — an email domain that requires a basic pattern match, or a currency_amount domain that forbids negative values, for example. Defining the rule once as a domain means every column that uses it stays consistent, and a future change to the rule only has to happen in one place.
A reusable email domain
CREATE DOMAIN email AS TEXT CHECK (VALUE ~ '^[^@\s]+@[^@\s]+\.[^@\s]+$'); ALTER TABLE customers ALTER COLUMN email TYPE email;
Composite type | Domain | |
|---|---|---|
What it is | A structured type with multiple named fields | A single base type plus an optional constraint |
Created with | CREATE TYPE ... AS (...) | CREATE DOMAIN ... AS base_type CHECK (...) |
Typical use | Grouping related columns (address, money with currency) | Enforcing a rule everywhere a base type is used |
Storage shape | Multiple values bundled together | Same storage as the underlying base type |
CREATE TYPE ... AS (...) defines a composite type — a reusable, multi-field structure.
CREATE DOMAIN ... AS base_type CHECK (...) defines a named constraint layered on a single base type.
Domains enforce their constraint automatically on every column that uses them.
Use composite types to group related fields; use domains to centralize a validation rule.