alisvo.dev DBA
#Partitioning #PostgreSQL

Partitioning existing table in PostgreSQL

How to partition an existing large table in PostgreSQL with range partitioning, automated partition creation, and indexing.

AS
Ali Sağırvelioğulları
Senior Database Administrator
2 dk okuma

1. Create Sample Unpartitioned Table

Let’s create a table with 20 million random rows and add an index to compare performance:

CREATE TABLE not_partitioned_table (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL,
    transaction_date DATE NOT NULL,
    amount DECIMAL(10, 2),
    status VARCHAR(20),
    description TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Create an index on the transaction_date column
CREATE INDEX idx_transaction_date ON not_partitioned_table(transaction_date);

2. Populate 20 Million Sample Records

DO $$
DECLARE
    i INT;
BEGIN
    FOR i IN 1..20000000 LOOP
        INSERT INTO not_partitioned_table (user_id, transaction_date, amount, status, description)
        VALUES (
            (RANDOM() * 100000)::INT,
            CURRENT_DATE - ((RANDOM() * 3650)::INT), -- Random date within the last 10 years
            (RANDOM() * 1000)::DECIMAL(10,2),
            CASE WHEN RANDOM() < 0.5 THEN 'completed' ELSE 'pending' END,
            md5(random()::text)
        );
    END LOOP;
END $$;

3. Create the Partitioned Table

After data generation completes, create the new partitioned table structure partitioned by range on transaction_date:

CREATE TABLE partitioned_table_new (
    id SERIAL,
    user_id INTEGER NOT NULL,
    transaction_date DATE NOT NULL,
    amount DECIMAL(10, 2),
    status VARCHAR(20),
    description TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id, transaction_date) -- Include transaction_date in the primary key
) PARTITION BY RANGE (transaction_date);

4. Automatically Generate Monthly Partitions

Create monthly partitions between your required minimum and maximum dates:

DO $$
DECLARE
    start_date DATE := '2014-09-01';
    end_date DATE := '2024-10-01';
    partition_end DATE;
    partition_name TEXT;
BEGIN
    WHILE start_date < end_date LOOP
        partition_end := start_date + INTERVAL '1 month';
        partition_name := 'partition_' || TO_CHAR(start_date, 'YYYY_MM');

        EXECUTE format(
            'CREATE TABLE %I PARTITION OF partitioned_table_new FOR VALUES FROM (%L) TO (%L);',
            partition_name,
            start_date,
            partition_end
        );

        start_date := partition_end;
    END LOOP;
END $$;

5. Create Indexes on All Partitions

DO $$
DECLARE
    partition_name RECORD;
BEGIN
    FOR partition_name IN
        SELECT tablename
        FROM pg_tables
        WHERE schemaname = 'public'
          AND tablename LIKE 'partition_%'
    LOOP
        EXECUTE format('CREATE INDEX idx_%I_transaction_date ON %I (transaction_date);', partition_name.tablename, partition_name.tablename);
    END LOOP;
END $$;

6. Migrate Data from Old Table

Copy the existing rows into the partitioned table:

INSERT INTO partitioned_table_new (user_id, transaction_date, amount, status, description, created_at, updated_at)
SELECT user_id, transaction_date, amount, status, description, created_at, updated_at 
FROM not_partitioned_table;

7. Performance Comparison

Now query the same date range from both tables to inspect heap block reads and execution performance:

Unpartitioned Table Query:

SELECT * FROM not_partitioned_table WHERE transaction_date BETWEEN '2024-01-01' AND '2024-01-10';

Unpartitioned Execution Plan

Partitioned Table Query:

SELECT * FROM partitioned_table_new WHERE transaction_date BETWEEN '2024-01-01' AND '2024-01-10';

Partitioned Execution Plan