How to fix cannot turn regular table into partitioned table in PostgreSQL

PostgreSQLADVANCEDMEDIUMUpdated Dec 22, 2025· 154 views

PostgreSQL does not support direct conversion of regular tables to partitioned tables. You must create a new partitioned table and migrate data using methods like ATTACH PARTITION or data copying.

Quick fix

Fixes cannot turn regular table into partitioned table

CREATE TABLE events_partitioned (
    id SERIAL,
    event_name VARCHAR(255),
    created_at TIMESTAMP NOT NULL,
    -- other columns...
    PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);

-- Create partitions
CREATE TABLE events_2024 PARTITION OF events_partitioned
    FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

CREATE TABLE events_2025 PARTITION OF events_partitioned
    FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
  1. Create a new partitioned table with the desired schema
  2. Choose a migration strategy based on your requirements
  3. Method A: Use ATTACH PARTITION (fast, minimal downtime)
  4. Method B: Copy data to new partitioned table
postgresql — error
CREATE TABLE events_partitioned (
id SERIAL,
event_name VARCHAR(255),
created_at TIMESTAMP NOT NULL,
-- other columns...
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
-- Create partitions
CREATE TABLE events_2024 PARTITION OF events_partitioned
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');
CREATE TABLE events_2025 PARTITION OF events_partitioned
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
cannot turn regular table into partitioned table

What this error means

This error occurs when you attempt to convert an existing regular table into a partitioned table using ALTER TABLE commands. PostgreSQL does not provide a direct ALTER TABLE ... PARTITION BY command to transform a regular table into a partitioned one. PostgreSQL's partitioning system requires tables to be created as partitioned from the start. Once a table is created as a regular table, it cannot be retroactively converted to use declarative partitioning. This is a fundamental architectural constraint in PostgreSQL's design—the table's partitioning strategy must be defined at creation time. However, PostgreSQL does provide mechanisms to add existing tables as partitions to a partitioned table structure, or to migrate data from regular tables to new partitioned tables, which allows you to achieve the desired partitioning without direct conversion.

Typical symptoms

  • Error message "cannot turn regular table into partitioned table" when attempting ALTER TABLE
  • ALTER TABLE ... PARTITION BY command fails
  • Migration scripts fail when trying to convert existing tables to partitioned tables
  • Database schema changes rejected during partitioning implementation

Common causes

  1. Attempting to use ALTER TABLE ... PARTITION BY on an existing regular table
  2. Trying to convert a table to partitioned table without understanding PostgreSQL partitioning constraints
  3. Using migration scripts designed for databases that support in-place partition conversion
  4. Misunderstanding PostgreSQL documentation about ATTACH PARTITION vs creating partitioned tables

How to fix "cannot turn regular table into partitioned table"

1Create a new partitioned table with the desired schema

First, create a new partitioned table with your partitioning strategy. For example, to partition by range on a date column:

sql
CREATE TABLE events_partitioned (
    id SERIAL,
    event_name VARCHAR(255),
    created_at TIMESTAMP NOT NULL,
    -- other columns...
    PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);

-- Create partitions
CREATE TABLE events_2024 PARTITION OF events_partitioned
    FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

CREATE TABLE events_2025 PARTITION OF events_partitioned
    FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');

Note: The partition key (created_at) must be part of the primary key or unique constraints.

2Choose a migration strategy based on your requirements

Select the appropriate migration method:

Option A: Minimal downtime with ATTACH PARTITION
Best for tables that already match partition constraints.

Option B: Data copy migration
Most straightforward but requires downtime or read-only mode.

Option C: Trigger-based live migration
Best for large tables requiring zero downtime.

Option D: Using pg_partman extension
Best for automated partition management.

Choose based on your downtime tolerance and data size.

3Method A: Use ATTACH PARTITION (fast, minimal downtime)

If your existing table can serve as a partition:

sql
-- Rename the original table
ALTER TABLE events RENAME TO events_old;

-- Create the partitioned parent table
CREATE TABLE events (
    id SERIAL,
    event_name VARCHAR(255),
    created_at TIMESTAMP NOT NULL,
    PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);

-- Add a CHECK constraint to the old table matching partition bounds
ALTER TABLE events_old
    ADD CONSTRAINT events_old_check
    CHECK (created_at >= '2024-01-01' AND created_at < '2025-01-01');

-- Attach the old table as a partition
ALTER TABLE events ATTACH PARTITION events_old
    FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

-- Create additional partitions for other date ranges
CREATE TABLE events_2025 PARTITION OF events
    FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');

This method is fast because it doesn't copy data—it reuses the existing table as a partition.

4Method B: Copy data to new partitioned table

For straightforward migration with acceptable downtime:

sql
-- Rename original table
ALTER TABLE events RENAME TO events_old;

-- Create partitioned table (from step 1)
CREATE TABLE events (
    id SERIAL,
    event_name VARCHAR(255),
    created_at TIMESTAMP NOT NULL,
    PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);

-- Create partitions
CREATE TABLE events_2024 PARTITION OF events
    FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

CREATE TABLE events_2025 PARTITION OF events
    FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');

-- Copy data in batches to avoid locks
INSERT INTO events
SELECT * FROM events_old
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';

INSERT INTO events
SELECT * FROM events_old
WHERE created_at >= '2025-01-01' AND created_at < '2026-01-01';

-- Verify data integrity
SELECT COUNT(*) FROM events;
SELECT COUNT(*) FROM events_old;

-- After verification, drop old table
DROP TABLE events_old;

For large tables, use batch inserts with LIMIT and OFFSET to avoid long-running transactions.

5Method C: Trigger-based live migration (zero downtime)

For large tables requiring continuous availability:

sql
-- Create partitioned table alongside existing table
CREATE TABLE events_new (
    id SERIAL,
    event_name VARCHAR(255),
    created_at TIMESTAMP NOT NULL,
    migrated BOOLEAN DEFAULT FALSE,
    PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);

-- Create partitions
CREATE TABLE events_2024 PARTITION OF events_new
    FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

-- Create trigger to redirect new writes
CREATE OR REPLACE FUNCTION redirect_to_partitioned()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO events_new VALUES (NEW.*);
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER redirect_inserts
    AFTER INSERT ON events
    FOR EACH ROW
    EXECUTE FUNCTION redirect_to_partitioned();

-- Migrate existing data in batches
DO $$
DECLARE
    batch_size INT := 10000;
    last_id INT := 0;
BEGIN
    LOOP
        INSERT INTO events_new
        SELECT * FROM events
        WHERE id > last_id
        ORDER BY id
        LIMIT batch_size;

        IF NOT FOUND THEN EXIT; END IF;

        SELECT MAX(id) INTO last_id FROM events_new;
        COMMIT;
    END LOOP;
END $$;

-- After migration: swap tables
ALTER TABLE events RENAME TO events_old;
ALTER TABLE events_new RENAME TO events;

This approach allows reads and writes to continue during migration.

6Verify the partitioned table structure

After migration, verify your partitioned table is working correctly:

sql
-- Check partition structure
SELECT
    parent.relname AS parent_table,
    child.relname AS partition_name,
    pg_get_expr(child.relpartbound, child.oid) AS partition_bounds
FROM pg_inherits
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
WHERE parent.relname = 'events'
ORDER BY partition_name;

-- Verify data distribution
SELECT
    tableoid::regclass AS partition_name,
    COUNT(*) AS row_count
FROM events
GROUP BY tableoid
ORDER BY partition_name;

-- Test partition pruning (should only scan relevant partition)
EXPLAIN SELECT * FROM events
WHERE created_at >= '2024-06-01'
  AND created_at < '2024-07-01';

Look for "Seq Scan on events_2024" in the EXPLAIN output, indicating partition pruning is working.

7Update application code and indexes

Ensure your application and database are optimized for partitioned tables:

sql
-- Create indexes on partitions (automatically inherited)
CREATE INDEX idx_events_created_at ON events (created_at);
CREATE INDEX idx_events_event_name ON events (event_name);

-- Update sequences if using SERIAL
SELECT setval('events_id_seq',
    (SELECT MAX(id) FROM events)
);

-- Grant appropriate permissions
GRANT SELECT, INSERT, UPDATE, DELETE ON events TO app_user;

Update application queries to include the partition key in WHERE clauses for optimal partition pruning:

sql
-- Good: Uses partition key
SELECT * FROM events
WHERE created_at >= '2024-01-01' AND event_name = 'login';

-- Less optimal: Missing partition key
SELECT * FROM events WHERE event_name = 'login';

Advanced notes

Partition Key Requirements: The partition key must be included in all unique constraints and primary keys. This is why we use PRIMARY KEY (id, created_at) in the examples—id alone would fail.

Partition Pruning: PostgreSQL will only scan relevant partitions when queries include the partition key in WHERE clauses. This is the primary performance benefit of partitioning. Use EXPLAIN to verify partition pruning is occurring.

Automatic Partition Creation: Consider using pg_partman extension for automatic partition management. It can create future partitions automatically and help with partition maintenance:

sql
-- Install pg_partman
CREATE EXTENSION pg_partman;

-- Set up automatic partition creation
SELECT partman.create_parent(
    p_parent_table => 'public.events',
    p_control => 'created_at',
    p_type => 'native',
    p_interval => '1 month',
    p_premake => 3  -- Create 3 months of future partitions
);

Partition Detachment: If you need to archive old data, you can detach partitions without affecting the parent table:

sql
-- Detach old partition (PostgreSQL 12+)
ALTER TABLE events DETACH PARTITION events_2020 CONCURRENTLY;

-- Archive or drop the detached table
CREATE TABLE events_archive_2020 AS SELECT * FROM events_2020;
DROP TABLE events_2020;

Performance Considerations: Partitioning is most beneficial for very large tables (hundreds of GB or TB) where queries typically filter on the partition key. For smaller tables or queries that don't use the partition key, partitioning may add overhead without benefits.

Foreign Keys: Foreign keys referencing partitioned tables require PostgreSQL 11+. If you have foreign key relationships, test thoroughly after migration.

List and Hash Partitioning: While these examples use RANGE partitioning, PostgreSQL also supports LIST (discrete values) and HASH (distributed by hash function) partitioning strategies depending on your use case.

Frequently asked questions

What does "cannot turn regular table into partitioned table" mean?
This error occurs when you attempt to convert an existing regular table into a partitioned table using ALTER TABLE commands. PostgreSQL does not provide a direct ALTER TABLE ... PARTITION BY command to transform a regular table into a partitioned one.
How do I fix "cannot turn regular table into partitioned table"?
PostgreSQL does not support direct conversion of regular tables to partitioned tables. You must create a new partitioned table and migrate data using methods like ATTACH PARTITION or data copying.
What causes "cannot turn regular table into partitioned table"?
Attempting to use ALTER TABLE ... PARTITION BY on an existing regular table Trying to convert a table to partitioned table without understanding PostgreSQL partitioning constraints Using migration scripts designed for databases that support in-place partition conversion
Is the "cannot turn regular table into partitioned table" error serious?
It is a medium-severity error — more of a blocker than a risk. The steps above resolve it.

Official resources & further reading