Dev

Achieving Pruning on Non-Partition Key Columns in PostgreSQL

Partition pruning in PostgreSQL is typically effective only with partition key columns. However, by leveraging certain data patterns, it's possible to achieve pruning even on non-partition key columns. This article explains techniques such as utilizing the Constraint Exclusion parameter and handling outliers.

7 min read Reviewed & edited by the SINGULISM Editorial Team

Achieving Pruning on Non-Partition Key Columns in PostgreSQL
Photo by Shubham Dhage on Unsplash

Partition pruning is one of the most powerful features for maximizing query performance in PostgreSQL partition tables. It works by excluding unnecessary partitions from being scanned based on query predicates (WHERE clauses). However, pruning generally functions only when filtering is done on partition key columns. This limitation makes it challenging to select an appropriate partition key.

Interestingly, when specific patterns exist in the data, a combination of clever techniques can enable pruning even when filtering non-partition key columns. This approach is thoroughly explained in an article by Haki Benita posted on Lobsters. Based on that article, this piece introduces practical methods to achieve this.

Basics and Challenges of Partition Design

Consider a typical example of a table storing event logs. Many web services record events on a user session basis, partitioning them by timestamp. For instance, one partition could be created per year, storing 2025 data in event_y2025 and 2026 data in event_y2026.

In this design, queries for specific date ranges are fast as they only scan relevant partitions. However, when filtering queries involve non-partition key columns like session_id, all partitions need to be scanned, failing to leverage the benefits of partitioning. This limitation can lead to severe performance issues for user-specific analyses or investigations into specific sessions.

Local Indexes and Global Indexes

Index strategies for partitioned tables are crucial for complementing pruning. Local indexes are created independently for each partition, providing fast searches within a partition after pruning by partition key. On the other hand, global indexes span all partitions, accelerating searches on non-partition key columns but incurring higher maintenance costs.

Traditionally, speeding up queries on non-partition key columns has relied on global indexing. However, for large tables, building and updating global indexes can become a bottleneck. This is where Benita’s proposed techniques prove effective.

Leveraging the Constraint Exclusion Parameter

PostgreSQL includes a configuration parameter called constraint_exclusion. This parameter uses CHECK constraints defined on tables to exclude unnecessary tables from the scan list. While partition pruning works based on the partition key, constraint_exclusion evaluates arbitrary CHECK constraints.

By default, constraint_exclusion is set to partition (effective only for partitions). Changing this setting to on enables the exclusion of tables based on CHECK constraints even for non-partitioned tables. However, note that this comes with overhead, as all tables need to be checked for constraints.

Benita’s approach involves using this constraint_exclusion under specific conditions to achieve pruning based on non-partition key columns. Specifically, when data exhibits “outlier” or “gap and island” patterns, effective constraints can be defined to capitalize on these properties.

Pruning with Outliers

If a session ID appears only within a specific time range, the relationship between the session ID and the time frame can be added as a CHECK constraint to the relevant partition. For example, if session_id = 1 exists only between December 28 and 29, 2025, a constraint like session_id = 1 AND timestamp BETWEEN '2025-12-28' AND '2025-12-29' can be added to the corresponding partition.

Normally, manually managing such constraints is impractical. However, if the data generation pattern is regular (e.g., sequential session IDs with incrementally increasing start times), a correlation between session ID ranges and time ranges can be established. Using this correlation, each partition can have a CHECK constraint for the range of session IDs it covers.

For instance, during partition creation, the minimum and maximum session IDs corresponding to the time range covered by the partition can be calculated, and a constraint like CHECK (session_id BETWEEN min AND max) added. This enables PostgreSQL’s planner to evaluate the CHECK constraints and exclude out-of-range partitions when executing queries like WHERE session_id = 12345.

Gap and Island Patterns

Even when time ranges and session IDs are not perfectly continuous (i.e., there are gaps), similar techniques can be applied. When data exists as “islands” (continuous blocks), each island can correspond to a partition, with the session ID range within the island defined as a CHECK constraint. This ensures that only the relevant partitions are scanned, even if gaps exist.

However, this method assumes that the data generation pattern is predictable. For example, if event logs are inserted in timestamp order and session IDs are auto-incremented, a strong correlation between time and session ID emerges. Conversely, if session IDs are assigned randomly or generated independently of time, this technique may not work.

Handling Outliers

Managing outliers is crucial for enabling pruning on non-partition key columns. For instance, if some sessions span a very long time, a single session may span multiple partitions. This kind of data can disrupt pruning based on CHECK constraints.

Benita’s article suggests isolating outliers into dedicated partitions. For example, long-duration sessions could be moved to a special partition without session ID range constraints. This way, typical queries can prune most partitions, while the partition containing outliers is always scanned—but its quantity will be limited.

Interacting with the Planner

PostgreSQL’s query planner selects the optimal execution plan based on statistical and constraint information. For pruning to work on non-partition key columns, the planner must accurately evaluate each partition’s constraints. Proper configuration of the constraint_exclusion parameter and regular updates to statistical information (via ANALYZE) are essential.

Using the EXPLAIN command can help verify the planner’s behavior. If the child nodes of the “Append” node decrease in the execution plan, pruning has succeeded. Conversely, if all partitions are listed, it indicates that pruning has failed.

Practical Considerations

Several precautions must be taken when adopting these techniques in production environments:

  1. Adding CHECK constraints must be done carefully to maintain data integrity. Incorrect constraints can prevent valid data from being inserted. Constraints must always align with actual data distribution, and maintenance processes must accommodate changes in data insertion patterns.

  2. Setting constraint_exclusion to on results in evaluating CHECK constraints for all tables, increasing CPU overhead. For large databases, this overhead can become significant. Keeping it set to partition and enabling it only for specific query sessions may be more practical.

  3. Pruning effectiveness depends on query predicates. Equality (=) and range conditions (BETWEEN, <, >) work well, while complex conditions like IN lists or OR conditions may not perform as expected.

Comparison with Traditional Methods

Traditionally, methods like global indexes and materialized views have been used to achieve fast queries on non-partition key columns. While global indexes are straightforward, they pose challenges in index maintenance during partition addition or deletion. Materialized views are suitable for non-real-time analysis queries, as they store aggregated results.

Compared to these methods, Benita’s approach offers the following advantages: First, it provides the benefits of pruning for non-partition key columns without increasing partition management overhead. Second, it relies solely on schema design, requiring no special infrastructure or licenses. However, as it is highly dependent on data patterns, it is not a universal solution. Before adopting it, verify that your data meets the prerequisites.

Editorial Opinion

In the short term, this technique offers a valuable option for improving the performance of session-based analytical queries in systems handling event logs or time-series data. Particularly in existing systems where partition keys cannot be changed, this tuning method is highly beneficial. For systems with regular data generation patterns, avoiding global indexes makes it applicable even in high-write environments.

In the long term, advancements in PostgreSQL’s partition pruning capabilities can be expected. While pruning based on non-partition key columns is not currently a standard feature, the planner may eventually infer constraints from statistical information automatically. Additionally, creative solutions like these can provide valuable feedback for PostgreSQL’s core development. Database architects must understand the limitations of the available features while exploring innovative workarounds.

As an editorial note, we believe it is crucial to analyze data distribution and query patterns thoroughly before adopting these techniques.

References

Frequently Asked Questions

Can this method be used with all PostgreSQL versions?
The `constraint_exclusion` parameter has been available since PostgreSQL 8.4. Partition tables, however, have been supported only since version 10. This technique, which leverages CHECK constraints for each partition, is effective in version 10 and later. It works even in the latest version 17, but since the planner's behavior varies slightly across versions, testing in a controlled environment is recommended.
How does performance scale with a high number of partitions?
As the number of partitions increases, the planner needs to evaluate the CHECK constraints for all partitions, which can increase optimization time. Typically, up to a few hundred partitions pose no significant issues, but with thousands of partitions, the overhead becomes noticeable. Consider subpartitioning or applying constraints only to frequently queried ranges to mitigate this issue.
Source: Lobsters

Comments

← Back to Home