Medium-High Impact

Partition Large Tables for Better Performance

5-20x faster queries and maintenance

Why This Matters

Partitioning splits a large table into smaller, manageable pieces called partitions. Queries that filter on the partition key only scan relevant partitions, dramatically improving performance.

When to partition: Consider partitioning when tables exceed 10-50GB or contain millions of rows, especially for time-series data like logs, events, or analytics.

The Problem

Incorrect - Single large table
-- Single table with 100M+ rows
CREATE TABLE events (
  id BIGINT PRIMARY KEY,
  user_id INT,
  event_type TEXT,
  created_at TIMESTAMPTZ,
  data JSONB
);

-- Query for last 7 days scans entire table
SELECT * FROM events
WHERE created_at >= NOW() - INTERVAL '7 days';

-- VACUUM takes hours
-- Deleting old data is slow
DELETE FROM events WHERE created_at < NOW() - INTERVAL '1 year';  -- Very slow

The Solution

Correct - Range partitioned by month
-- Create partitioned table
CREATE TABLE events (
  id BIGINT,
  user_id INT,
  event_type TEXT,
  created_at TIMESTAMPTZ,
  data JSONB,
  PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);

-- Create monthly partitions
CREATE TABLE events_2024_01 PARTITION OF events
  FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

CREATE TABLE events_2024_02 PARTITION OF events
  FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

-- Query only scans relevant partitions
SELECT * FROM events
WHERE created_at >= NOW() - INTERVAL '7 days';  -- Fast!

-- Drop old partition instantly
DROP TABLE events_2023_01;  -- Instant, no DELETE needed

Result: Queries filtering on created_at only scan relevant partitions. A query for the last 7 days scans 1 partition instead of 100M rows, reducing query time from 5 seconds to 50ms.

Automatic Partition Management

Use a function to automatically create future partitions:

-- Function to create next month's partition
CREATE OR REPLACE FUNCTION create_monthly_partition(table_name TEXT, start_date DATE)
RETURNS VOID AS $$
DECLARE
  partition_name TEXT;
  end_date DATE;
BEGIN
  partition_name := table_name || '_' || TO_CHAR(start_date, 'YYYY_MM');
  end_date := start_date + INTERVAL '1 month';
  
  EXECUTE FORMAT(
    'CREATE TABLE IF NOT EXISTS %I PARTITION OF %I FOR VALUES FROM (%L) TO (%L)',
    partition_name, table_name, start_date, end_date
  );
END;
$$ LANGUAGE plpgsql;

-- Create partitions for next 12 months
SELECT create_monthly_partition('events', DATE_TRUNC('month', NOW()) + (i || ' months')::INTERVAL)
FROM GENERATE_SERIES(0, 11) i;

Common Mistakes

Important: The partition key column must be part of the PRIMARY KEY. If your primary key is just id, change it to PRIMARY KEY (id, created_at) when partitioning by created_at.

Check Your Project Automatically

Connect your Supabase project to identify large tables that would benefit from partitioning.

Start Supabase Audit

Learn More