The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Create a session temporary tableCREATE TEMP TABLE work_items (item_id bigint PRIMARY KEY, payload jsonb NOT NULL);View examples
Preserve rows across commitsCREATE TEMP TABLE work_items (item_id bigint) ON COMMIT PRESERVE ROWS;View examples
Clear rows after each commitCREATE TEMP TABLE work_items (item_id bigint) ON COMMIT DELETE ROWS;View examples
Drop the table at commitCREATE TEMP TABLE work_items (item_id bigint) ON COMMIT DROP;View examples
Materialize a transaction-scoped resultCREATE TEMP TABLE recent_orders ON COMMIT DROP AS SELECT order_id, total FROM app.orders WHERE created_at >= CURRENT_DATE;View examples
Copy a table shapeCREATE TEMP TABLE staged_orders (LIKE app.orders INCLUDING DEFAULTS INCLUDING CONSTRAINTS);View examples
Index a populated temporary tableCREATE INDEX work_items_customer_idx ON work_items (customer_id);View examples
Collect temporary-table statisticsANALYZE work_items;View examples
Bypass temporary name shadowingSELECT order_id FROM app.orders;View examples
Restrict temporary-table creationREVOKE TEMPORARY ON DATABASE appdb FROM PUBLIC;View examples
Delegate temporary-table creationGRANT TEMPORARY ON DATABASE appdb TO batch_worker;View examples
Budget session-local temp buffersSET temp_buffers = '64MB';View examples
Create a shared unlogged tableCREATE UNLOGGED TABLE app.ingest_buffer (event_id bigint PRIMARY KEY, payload jsonb NOT NULL);View examples
Materialize an unlogged resultCREATE UNLOGGED TABLE app.daily_rollup AS SELECT account_id, sum(amount) AS total FROM app.ledger GROUP BY account_id;View examples
Promote data to crash-safe storageALTER TABLE app.ingest_buffer SET LOGGED;View examples
Convert rebuildable data to unloggedALTER TABLE app.daily_rollup SET UNLOGGED;View examples
Audit relation persistenceSELECT n.nspname, c.relname, c.relpersistence FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'r';View examples
Preserve unlogged data in a logical dumppg_dump --format=custom --file=appdb.dump appdbView examples

Temporary and unlogged tables both reduce durability overhead, but they solve different problems. A temporary table belongs to one database session and can have transaction-scoped contents or identity; an unlogged table is a shared persistent object whose data survives clean restarts but is truncated after crash recovery and whose contents are unavailable on standbys. Choose from the failure and visibility contract, not from benchmark speed alone.

Step by step

Detailed examples

01

Make temporary-table lifetime explicit

PostgreSQL creates each temporary table in a session-specific schema; other sessions cannot see its definition or data, and the table disappears when the session ends. ON COMMIT PRESERVE ROWS is the PostgreSQL default, unlike the SQL standard's default. DELETE ROWS performs an automatic commit-time truncation, while DROP removes the table at commit. Creation and population still follow transaction semantics: a rollback undoes them. GLOBAL and LOCAL TEMPORARY are accepted but currently have no effect and are deprecated.

Keep a reusable shape but clear each transaction's batch
CREATE TEMP TABLE work_items (
  item_id bigint PRIMARY KEY,
  customer_id bigint NOT NULL,
  payload jsonb NOT NULL
) ON COMMIT DELETE ROWS;

BEGIN;
INSERT INTO work_items (item_id, customer_id, payload)
VALUES (1001, 42, '{"op":"reprice"}');
COMMIT;
-- The definition remains in this session; its rows were cleared at COMMIT.
Back to quick reference ↑
02

Use CREATE TABLE AS or a deliberate LIKE subset

CREATE TEMP TABLE AS evaluates its query once and is preferred over PostgreSQL's historical SELECT INTO form, which conflicts with procedural and embedded-SQL meanings. LIKE copies only the features requested: defaults, generated expressions, constraints, indexes, identity, statistics, storage, comments, or all. Copied defaults can retain dependencies on source sequences, and copied constraints do not imply copied foreign keys, so inspect the result before using it as an isolated staging contract.

Materialize only the columns needed for a bounded operation
BEGIN;
CREATE TEMP TABLE recent_orders
  ON COMMIT DROP
AS
SELECT order_id, customer_id, total
FROM app.orders
WHERE created_at >= CURRENT_DATE - INTERVAL '7 days';

ALTER TABLE recent_orders ADD PRIMARY KEY (order_id);
COMMIT;
Copy selected structural properties for validation
CREATE TEMP TABLE staged_orders (
  LIKE app.orders
    INCLUDING DEFAULTS
    INCLUDING GENERATED
    INCLUDING CONSTRAINTS
) ON COMMIT DROP;

SELECT column_name, data_type, column_default
FROM information_schema.columns
WHERE table_schema = pg_my_temp_schema()::regnamespace::text
  AND table_name = 'staged_orders'
ORDER BY ordinal_position;
Back to quick reference ↑
03

Analyze temporary data after its shape stabilizes

Indexes on temporary tables are temporary automatically. For performance, build them only when repeated probes, joins, uniqueness checks, or ordering repay their build cost. Autovacuum workers cannot access session-private temporary tables, so they neither vacuum nor analyze them. Run ANALYZE after the representative load and manual VACUUM when a long-lived session generates substantial dead rows. ANALYZE uses a read-compatible lock, but its sample and planning overhead still belong in the batch budget.

Load, index, analyze, and verify the join plan
CREATE INDEX work_items_customer_idx ON work_items (customer_id);
ANALYZE work_items;

EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
SELECT w.item_id, c.segment
FROM work_items AS w
JOIN app.customers AS c USING (customer_id)
WHERE c.active;
Back to quick reference ↑
04

Control TEMPORARY privilege and relation-name shadowing

TEMPORARY is a database privilege and is granted to PUBLIC by default on newly created databases. Revoke it when untrusted roles should not allocate session-local relations, then grant it to bounded worker roles. Once a temporary schema exists, it is searched before pg_catalog for relation and data-type names, though never for function or operator names. A temporary table can therefore hide an unqualified permanent table from new plans. Qualify security-sensitive permanent relations and use a controlled search_path in privileged code.

Restrict creation and avoid an ambiguous relation reference
REVOKE TEMPORARY ON DATABASE appdb FROM PUBLIC;
GRANT TEMPORARY ON DATABASE appdb TO batch_worker;

-- This remains the permanent table even if the session creates pg_temp.orders.
SELECT order_id, total
FROM app.orders
WHERE status = 'pending';

SELECT current_schemas(true), pg_my_temp_schema();
Back to quick reference ↑
05

Design for physical sessions, pools, and two-phase commit

Temporary objects belong to a PostgreSQL backend session, not to a logical request. Under session pooling, PRESERVE ROWS data can leak into the next request unless cleanup is guaranteed; under transaction pooling, a later transaction may run on another backend and cannot rely on the table. Prefer ON COMMIT DROP for transaction-scoped work. A transaction that touched a temporary table or the session's temporary namespace cannot be prepared with PREPARE TRANSACTION. temp_buffers is per session and must be set before that session first accesses temporary tables, so large settings multiplied across many active backends require capacity planning.

Configure resources before creating transaction-scoped work
SET temp_buffers = '64MB';

BEGIN;
CREATE TEMP TABLE request_keys (
  key bigint PRIMARY KEY
) ON COMMIT DROP;
INSERT INTO request_keys (key) VALUES (101), (205), (309);
SELECT a.* FROM app.accounts AS a JOIN request_keys AS r ON r.key = a.account_id;
COMMIT;
Back to quick reference ↑
06

Reserve unlogged tables for shared, rebuildable state

An unlogged table is visible to every authorized session and remains after clean shutdown, but its data changes bypass normal write-ahead logging. PostgreSQL automatically truncates it after a crash or unclean shutdown, and its contents are not available on physical standbys or eligible for logical publications. Indexes and identity or serial sequences created with the table become unlogged too. Unlogged partitioned tables are not supported in PostgreSQL 18. Never use this persistence for a system of record, failover-critical queue, or the only copy of an ingest batch.

Create a rebuildable shared ingestion buffer
CREATE UNLOGGED TABLE app.ingest_buffer (
  event_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  source_id text NOT NULL,
  received_at timestamptz NOT NULL DEFAULT now(),
  payload jsonb NOT NULL,
  UNIQUE (source_id)
);

COMMENT ON TABLE app.ingest_buffer IS
  'Rebuildable staging data; empty after crash recovery and absent from standbys';
Back to quick reference ↑
07

Schedule logged and unlogged conversions as blocking migrations

ALTER TABLE SET LOGGED or SET UNLOGGED requires table ownership, takes ACCESS EXCLUSIVE unless a command-specific exception applies, and is unsuitable for latency-sensitive traffic without a maintenance plan. It cannot target temporary or partitioned tables. Linked identity and serial sequences change persistence with the table. Converting to LOGGED establishes WAL-backed durability for subsequent recovery and replication, but do not advertise the data as failover-safe until the conversion commits and standby or backup recovery has been verified.

Promote validated staging data under a lock timeout
BEGIN;
SET LOCAL lock_timeout = '5s';
LOCK TABLE app.ingest_buffer IN ACCESS EXCLUSIVE MODE;
ALTER TABLE app.ingest_buffer SET LOGGED;
COMMIT;

SELECT c.relpersistence
FROM pg_class AS c
WHERE c.oid = 'app.ingest_buffer'::regclass;
Back to quick reference ↑
08

Audit persistence and test the intended recovery path

pg_class.relpersistence reports p for permanent, u for unlogged, and t for temporary relations. Logical pg_dump includes unlogged table and sequence data by default; --no-unlogged-table-data deliberately omits it. That dump behavior does not make unlogged data crash-safe between backups, nor does WAL-based point-in-time recovery reconstruct its changes. Document a rebuild source and objective, monitor buffer growth, and exercise crash, failover, and logical-restore tests before adopting unlogged storage in production.

Inventory nonpermanent tables and their sizes
SELECT n.nspname AS schema_name, c.relname,
       CASE c.relpersistence
         WHEN 'p' THEN 'permanent'
         WHEN 'u' THEN 'unlogged'
         WHEN 't' THEN 'temporary'
       END AS persistence,
       pg_total_relation_size(c.oid) AS total_bytes
FROM pg_class AS c
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'p')
  AND c.relpersistence <> 'p'
ORDER BY total_bytes DESC;
Choose whether a logical backup carries unlogged rows
# Default: include unlogged table and sequence data.
pg_dump --format=custom --file=appdb.dump appdb

# Schema remains, but omit rebuildable unlogged data.
pg_dump --format=custom --no-unlogged-table-data --file=appdb-without-stage.dump appdb
Back to quick reference ↑

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. PostgreSQL Global Development GroupPostgreSQL 18: CREATE TABLEpostgresql.org
  2. PostgreSQL Global Development GroupPostgreSQL 18: CREATE TABLE ASpostgresql.org
  3. PostgreSQL Global Development GroupPostgreSQL 18: ALTER TABLEpostgresql.org
  4. PostgreSQL Global Development GroupPostgreSQL 18: Client Connection Defaultspostgresql.org
  5. PostgreSQL Global Development GroupPostgreSQL 18: Resource Consumptionpostgresql.org
  6. PostgreSQL Global Development GroupPostgreSQL 18: Privilegespostgresql.org
  7. PostgreSQL Global Development GroupPostgreSQL 18: PREPARE TRANSACTIONpostgresql.org
  8. PostgreSQL Global Development GroupPostgreSQL 18: CREATE PUBLICATIONpostgresql.org
  9. PostgreSQL Global Development GroupPostgreSQL 18: pg_dumppostgresql.org

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback