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.
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.
First, create a new partitioned table with your partitioning strategy. For example, to partition by range on a date column:
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.
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.
If your existing table can serve as a partition:
-- 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.
For straightforward migration with acceptable downtime:
-- 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.
For large tables requiring continuous availability:
-- 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.
After migration, verify your partitioned table is working correctly:
-- 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.
Ensure your application and database are optimized for partitioned tables:
-- 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:
-- 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';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:
-- 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:
-- 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.
ERROR: syntax error at end of input
Syntax error at end of input in PostgreSQL
Bind message supplies N parameters but prepared statement requires M
Bind message supplies N parameters but prepared statement requires M in PostgreSQL
Multidimensional arrays must have sub-arrays with matching dimensions
Multidimensional arrays must have sub-arrays with matching dimensions
ERROR: value too long for type character varying
Value too long for type character varying
insufficient columns in unique constraint for partition key
How to fix "insufficient columns in unique constraint for partition key" in PostgreSQL