SELECT INTO in PostgreSQL

Learn how to use PostgreSQL SELECT INTO to create a new table and populate it from a query result, including the TEMP option and key differences from CREATE TABLE AS.

4 min read · Back to overview

Quick Answer

SELECT column_list INTO [TEMP] new_table FROM source WHERE condition creates a new table and fills it with query results in one step. Use the TEMP keyword to create a session-scoped table that is dropped automatically when the connection closes.

Spin up a Postgres database in 20 seconds with Vela.

Try Vela Sandbox

SELECT INTO is a PostgreSQL extension that combines a SELECT query with a table-creation step: it creates a new table and fills it with the query’s result set in a single statement. It is commonly used for ad-hoc data exploration, creating working copies, and staging data for pipeline processing.

Syntax

SELECT select_list
INTO [TEMPORARY | TEMP] [TABLE] new_table_name
FROM source_table
[WHERE condition];
  • The new table inherits column names and data types from the SELECT output.
  • TEMP or TEMPORARY creates a session-scoped table dropped automatically at session end.
  • The TABLE keyword is optional and exists only for readability.
  • Any SELECT clause is valid: JOIN, GROUP BY, HAVING, ORDER BY, subqueries.

Note: You cannot use SELECT INTO inside a PL/pgSQL function — PL/pgSQL interprets INTO as a variable assignment. Use CREATE TABLE AS in that context.

Practical Example

Create a snapshot of in-progress orders for analysis:

-- Source tables
CREATE TABLE orders (
  order_id     SERIAL PRIMARY KEY,
  customer_ref VARCHAR(50) NOT NULL,
  status       VARCHAR(20) NOT NULL,
  total        NUMERIC(10,2) NOT NULL,
  placed_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Snapshot active orders into a working table
SELECT order_id, customer_ref, total, placed_at
INTO TABLE active_orders_snapshot
FROM orders
WHERE status = 'in_progress'
ORDER BY placed_at;

Verify the snapshot:

SELECT * FROM active_orders_snapshot LIMIT 5;

Create a temporary staging table for a single session:

SELECT order_id, total
INTO TEMP TABLE order_totals_tmp
FROM orders
WHERE placed_at >= now() - INTERVAL '30 days';

-- Use it within the session
SELECT SUM(total) FROM order_totals_tmp;

Inspect the new table — no indexes will be present:

\d active_orders_snapshot

SELECT INTO vs CREATE TABLE AS

FeatureSELECT INTOCREATE TABLE AS
SQL standardNo (PostgreSQL extension)Yes
Works in PL/pgSQLNoYes
IF NOT EXISTS supportNoYes
UNLOGGED supportNoYes
Custom column namesNoYes

For all new code, prefer CREATE TABLE AS.

Testing with Vela

When building a data pipeline that stages intermediate results with SELECT INTO, Vela database branching lets you test the full pipeline on a production-sized copy of the database before running it against live data. Create a branch, execute your staging workflow, validate the result, then tear down the branch — no risk to production data.

Production Tips

  • Prefer CREATE TABLE AS in application code and migration scripts — it is standard, works in functions, and supports IF NOT EXISTS.
  • SELECT INTO is best suited for quick ad-hoc work in psql or notebook-style sessions.
  • The new table does not inherit indexes, primary key, or foreign key constraints — add them explicitly after creation.
  • Use TEMP for intermediate computation tables within a session to avoid cluttering the permanent schema.
  • Use RETURNING in your source INSERT statements to feed SELECT INTO directly without re-querying when building audit trails.

Continue in Managing Tables: Create Table As.

Related in this section: PostgreSQL Data Types · Create Table · Create Table As

Frequently Asked Questions

What does SELECT INTO do in PostgreSQL?
SELECT INTO creates a new table and populates it with the rows returned by a SELECT statement, all in a single operation. Unlike a normal SELECT, no rows are returned to the client — the data goes directly into the new table.
Does SELECT INTO lock the source table and block queries?
SELECT INTO holds a shared lock on the source table for the duration of the query, just like any regular SELECT. It does not block concurrent reads, but it does block schema changes on the source table during execution.
What happens to indexes and constraints from the source table?
They are not copied. SELECT INTO only transfers column names, data types, and row data. You must add indexes, primary keys, and foreign keys to the new table manually with ALTER TABLE and CREATE INDEX.
Can I use IF NOT EXISTS with SELECT INTO?
No. SELECT INTO does not support IF NOT EXISTS. If the target table already exists, PostgreSQL raises an error. Use DROP TABLE IF EXISTS before running SELECT INTO, or switch to CREATE TABLE IF NOT EXISTS ... AS.
What is the safest way to create a table copy in production?
Prefer CREATE TABLE AS over SELECT INTO. It is SQL-standard, works inside PL/pgSQL functions, supports IF NOT EXISTS and UNLOGGED modifiers, and makes the intent clearer. Reserve SELECT INTO for quick ad-hoc table creation in interactive sessions.