<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jéssica Coelho</title>
    <description>The latest articles on DEV Community by Jéssica Coelho (@jssica_coelho).</description>
    <link>https://dev.to/jssica_coelho</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2675581%2F4c1717a7-6136-4ee7-b8c3-1f9f294efa68.png</url>
      <title>DEV Community: Jéssica Coelho</title>
      <link>https://dev.to/jssica_coelho</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jssica_coelho"/>
    <language>en</language>
    <item>
      <title>🇺🇸 PostgreSQL Partitioning Strategies for Massive Databases — Part 1</title>
      <dc:creator>Jéssica Coelho</dc:creator>
      <pubDate>Tue, 25 Nov 2025 18:40:53 +0000</pubDate>
      <link>https://dev.to/jssica_coelho/postgresql-partitioning-strategies-for-massive-databases-part-1-43hh</link>
      <guid>https://dev.to/jssica_coelho/postgresql-partitioning-strategies-for-massive-databases-part-1-43hh</guid>
      <description>&lt;p&gt;Every year, companies face an exponential increase in data volume. Solutions that work perfectly for smaller tables often start to fail when the dataset reaches massive scales. That’s why we say:&lt;br&gt;
“What works for 10 million rows does not necessarily scale well for 10 billion.”&lt;/p&gt;

&lt;p&gt;When tables reach this level, database administrators must completely rethink their storage and indexing strategies.&lt;/p&gt;

&lt;p&gt;And that’s where partitioning becomes a powerful and essential technique.&lt;/p&gt;

&lt;p&gt;Before diving into technical details, let’s go through a bit of history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before PostgreSQL 10 — Inherited Tables&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Partitioning was implemented using table inheritance (INHERITS):&lt;/li&gt;
&lt;li&gt;A physical parent table was created.&lt;/li&gt;
&lt;li&gt;Multiple child tables represented each partition.&lt;/li&gt;
&lt;li&gt;Inserts were routed using triggers or rules, which redirected rows to the correct partition.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Disadvantages:
There was no automatic exclusion of partitions from the execution plan (partition pruning).
The optimizer had to scan all partitions, even those that were not relevant.
Operations like UPDATE and DELETE were costly and impractical.
Managing partitions (creating, dropping, and maintaining them) was labor-intensive.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Arrival of Declarative Partitioning (PostgreSQL 10, 2017)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL introduced native declarative partitioning, allowing developers to declare partition structures without triggers or manual logic.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CREATE TABLE sales (&lt;br&gt;
  id serial,&lt;br&gt;
  sale_date date,&lt;br&gt;
  amount numeric&lt;br&gt;
) PARTITION BY RANGE (sale_date);&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Partitions are created using CREATE TABLE ... PARTITION OF.&lt;/li&gt;
&lt;li&gt;Initially supported RANGE and LIST partitioning.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Syntax defines the method, keys, and value boundaries.&lt;/p&gt;

&lt;p&gt;📈 Partitioning Evolution Since PostgreSQL 10&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6fzpdszpyz2e25oozpd8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6fzpdszpyz2e25oozpd8.png" alt=" " width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🧩 What Is Partitioning?&lt;/p&gt;

&lt;p&gt;Partitioning is the technique of splitting a large table into smaller pieces called partitions.&lt;/p&gt;

&lt;p&gt;For the application, it still behaves like one logical table, but internally PostgreSQL distributes the data across multiple physical tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key concepts:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The parent table is logical and stores only metadata.&lt;/li&gt;
&lt;li&gt;Child tables are real PostgreSQL tables with schemas and tablespaces.&lt;/li&gt;
&lt;li&gt;Child tables inherit from the parent table using declarative partitioning.&lt;/li&gt;
&lt;li&gt;Child tables can have their own indexes.&lt;/li&gt;
&lt;li&gt;Child tables can be sub-partitioned with different keys.
-PostgreSQL automatically routes INSERT/SELECT/UPDATE/DELETE to the correct partition.
-You cannot add partitioning to an existing non-partitioned table.
-Partitioning is beneficial mainly for huge tables (typically larger than RAM).
-PK/UNIQUE constraints must include the partition key.
-Indexes are local to each partition.
-FKs had limitations before PostgreSQL 15.
-Partition boundaries must not overlap.
-There is a DEFAULT partition. If you don’t create one, when you try to insert data outside the partition ranges, you will get an error like this:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ERROR: no partition of relation “mother_table” found for row&lt;br&gt;
DETAIL: Failing row contains (coluna_particao = X, …).&lt;/p&gt;

&lt;p&gt;-Partition keys cannot be NULL.&lt;br&gt;
-VACUUM/ANALYZE operate on each partition.&lt;br&gt;
-Partitions can be ATTACHED or DETACHED without losing data.&lt;br&gt;
-Partition sizes do not need to be identical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partitioning Syntax Example&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Base table:&lt;/p&gt;

&lt;p&gt;CREATE TABLE payment (&lt;br&gt;
  payment_id serial,&lt;br&gt;
  payment_date date NOT NULL,&lt;br&gt;
  amount numeric(10,2) NOT NULL&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;Partitioned version:&lt;/p&gt;

&lt;p&gt;CREATE TABLE payment (&lt;br&gt;
  payment_id serial PRIMARY KEY,&lt;br&gt;
  payment_date date NOT NULL,&lt;br&gt;
  amount numeric(10,2) NOT NULL&lt;br&gt;
) PARTITION BY RANGE (payment_date);&lt;/p&gt;

&lt;p&gt;Creating monthly partitions:&lt;br&gt;
CREATE TABLE payment_2024_01&lt;br&gt;
PARTITION OF payment&lt;br&gt;
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');&lt;/p&gt;

&lt;p&gt;CREATE TABLE payment_2024_02&lt;br&gt;
PARTITION OF payment&lt;br&gt;
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');&lt;/p&gt;

&lt;p&gt;CREATE TABLE payment_2024_03&lt;br&gt;
PARTITION OF payment&lt;br&gt;
FOR VALUES FROM ('2024-03-01') TO ('2024-04-01');&lt;/p&gt;

&lt;p&gt;CREATE TABLE payment_2024_04&lt;br&gt;
PARTITION OF payment&lt;br&gt;
FOR VALUES FROM ('2024-04-01') TO ('2024-05-01');&lt;/p&gt;

&lt;p&gt;Inserts remain unchanged&lt;br&gt;
INSERT INTO payment (payment_date, amount)&lt;br&gt;
VALUES ('2024-04-20', 120.00);&lt;/p&gt;

&lt;p&gt;PostgreSQL automatically routes the data to the correct partition.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdw9eibbau3kjpjpoxpco.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdw9eibbau3kjpjpoxpco.png" alt=" " width="800" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⚡ Partition Pruning&lt;/p&gt;

&lt;p&gt;Partition pruning is the PostgreSQL technique that skips irrelevant partitions during query execution.&lt;/p&gt;

&lt;p&gt;Instead of scanning every child table, the optimizer reads only the partitions that match the WHERE condition.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
SELECT *&lt;br&gt;
FROM payment&lt;br&gt;
WHERE payment_date = '2024-04-20';&lt;/p&gt;

&lt;p&gt;Before:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feuf2nefm83shzvhzeqpr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feuf2nefm83shzvhzeqpr.png" alt=" " width="800" height="712"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With partitioning, PostgreSQL only scans the partition &lt;strong&gt;payment_2024_04&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhyzj66cjkqzy7rpbca4u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhyzj66cjkqzy7rpbca4u.png" alt=" " width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Smaller indexes → better caching → predictable performance&lt;/li&gt;
&lt;li&gt;Reduced I/O and CPU usage &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Partitioning transforms one single big index into many smaller, more efficient indexes.&lt;/p&gt;

&lt;p&gt;🧰 Partitioning Methods&lt;br&gt;
➤ 1. RANGE Partitioning&lt;/p&gt;

&lt;p&gt;Divide by continuous intervals (dates, numbers, sequences).&lt;/p&gt;

&lt;p&gt;CREATE TABLE payment (&lt;br&gt;
  payment_id serial PRIMARY KEY,&lt;br&gt;
  payment_date date NOT NULL,&lt;br&gt;
  amount numeric(10,2) NOT NULL&lt;br&gt;
) PARTITION BY RANGE (payment_date);&lt;/p&gt;

&lt;p&gt;CREATE TABLE payment_2024_q1&lt;br&gt;
PARTITION OF payment&lt;br&gt;
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');&lt;/p&gt;

&lt;p&gt;CREATE TABLE payment_2024_q2&lt;br&gt;
PARTITION OF payment&lt;br&gt;
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');&lt;/p&gt;

&lt;p&gt;📌 Best for: time-series tables (logs, sales, metrics)&lt;/p&gt;

&lt;p&gt;➤ 2. LIST Partitioning&lt;/p&gt;

&lt;p&gt;Divide by specific discrete values.&lt;/p&gt;

&lt;p&gt;CREATE TABLE payment (&lt;br&gt;
  payment_id serial PRIMARY KEY,&lt;br&gt;
  country text NOT NULL,&lt;br&gt;
  amount numeric(10,2) NOT NULL&lt;br&gt;
) PARTITION BY LIST (country);&lt;/p&gt;

&lt;p&gt;CREATE TABLE payment_br&lt;br&gt;
PARTITION OF payment&lt;br&gt;
FOR VALUES IN ('BR');&lt;/p&gt;

&lt;p&gt;CREATE TABLE payment_us&lt;br&gt;
PARTITION OF payment&lt;br&gt;
FOR VALUES IN ('US');&lt;/p&gt;

&lt;p&gt;📌 Best for: country, product type, category&lt;/p&gt;

&lt;p&gt;➤ 3. HASH Partitioning&lt;/p&gt;

&lt;p&gt;(Introduced in PostgreSQL 11)&lt;/p&gt;

&lt;p&gt;Uniform distribution via hash function.&lt;/p&gt;

&lt;p&gt;CREATE TABLE payment (&lt;br&gt;
  payment_id serial PRIMARY KEY,&lt;br&gt;
  customer_id int NOT NULL,&lt;br&gt;
  amount numeric(10,2) NOT NULL&lt;br&gt;
) PARTITION BY HASH (customer_id);&lt;/p&gt;

&lt;p&gt;CREATE TABLE payment_hash_0&lt;br&gt;
PARTITION OF payment&lt;br&gt;
FOR VALUES WITH (MODULUS 4, REMAINDER 0);&lt;/p&gt;

&lt;p&gt;CREATE TABLE payment_hash_1&lt;br&gt;
PARTITION OF payment&lt;br&gt;
FOR VALUES WITH (MODULUS 4, REMAINDER 1);&lt;/p&gt;

&lt;p&gt;📌 Best for: evenly distributing random data (customer IDs, hashed keys)&lt;/p&gt;

&lt;p&gt;➤ 4. Sub-Partitioning (Composite Partitioning)&lt;/p&gt;

&lt;p&gt;Combine methods (e.g., RANGE + LIST).&lt;/p&gt;

&lt;p&gt;CREATE TABLE payment (&lt;br&gt;
  payment_id serial PRIMARY KEY,&lt;br&gt;
  payment_date date NOT NULL,&lt;br&gt;
  country text NOT NULL,&lt;br&gt;
  amount numeric(10,2) NOT NULL&lt;br&gt;
) PARTITION BY RANGE (payment_date);&lt;/p&gt;

&lt;p&gt;CREATE TABLE payment_2024&lt;br&gt;
PARTITION OF payment&lt;br&gt;
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01')&lt;br&gt;
PARTITION BY LIST (country);&lt;/p&gt;

&lt;p&gt;CREATE TABLE payment_2024_br&lt;br&gt;
PARTITION OF payment_2024&lt;br&gt;
FOR VALUES IN ('BR');&lt;/p&gt;

&lt;p&gt;CREATE TABLE payment_2024_us&lt;br&gt;
PARTITION OF payment_2024&lt;br&gt;
FOR VALUES IN ('US');&lt;/p&gt;

&lt;p&gt;📌 Best for: combinations like&lt;br&gt;
time + region&lt;br&gt;
time + status&lt;br&gt;
category + product&lt;/p&gt;

&lt;p&gt;🆚 Partitioning vs. Sharding&lt;br&gt;
Technique   What It Does    Scope&lt;br&gt;
Partitioning    Splits one table into smaller internal partitions within a single database server   Vertical scaling&lt;br&gt;
Sharding    Splits data across multiple servers/databases, each storing a portion of the dataset    Horizontal scaling&lt;/p&gt;

&lt;p&gt;Summary:&lt;/p&gt;

&lt;p&gt;Partitioning = optimize performance inside one PostgreSQL instance:&lt;br&gt;
Server 1&lt;br&gt;
 └── Parent Table&lt;br&gt;
      ├── Partition Jan&lt;br&gt;
      ├── Partition Feb&lt;br&gt;
      └── Partition Mar&lt;/p&gt;

&lt;p&gt;Sharding = distribute data across multiple servers to scale beyond hardware limits&lt;br&gt;
Server 1&lt;br&gt;
 └── Shard A&lt;/p&gt;

&lt;p&gt;Server 2&lt;br&gt;
 └── Shard B&lt;/p&gt;

&lt;p&gt;Server 3&lt;br&gt;
 └── Shard C&lt;/p&gt;




&lt;p&gt;What’s Coming Next — Part 2&lt;/p&gt;

&lt;p&gt;In this first article, we explored how PostgreSQL has evolved, why partitioning is crucial for massive datasets, and which internal mechanisms the database uses to make queries faster and more efficient.&lt;/p&gt;

&lt;p&gt;But theory alone doesn’t solve real-world problems.&lt;/p&gt;

&lt;p&gt;In Part 2, I’ll demonstrate practical, hands-on examples:&lt;/p&gt;

&lt;p&gt;how to partition huge tables,&lt;br&gt;
how to choose between RANGE, LIST, HASH, or sub-partitioning,&lt;br&gt;
how to migrate an existing table into a partitioned structure,&lt;br&gt;
how to measure actual performance gains,&lt;/p&gt;

&lt;p&gt;If you want real-world examples that work at the scale of billions of rows, don’t miss the next article.&lt;/p&gt;

&lt;p&gt;See you there. &lt;/p&gt;

</description>
      <category>postgressql</category>
      <category>partitioning</category>
      <category>performance</category>
      <category>database</category>
    </item>
  </channel>
</rss>
