POSTGRESQL QUERY EXECUTION PLAN: UNDERSTANDING NODE TYPES FOR DATABASE PERFORMANCE ENGINEERING
1. Introduction
Query optimization is one of the fundamental disciplines of Database Engineering.
In PostgreSQL, query performance depends not only on SQL syntax, indexes, or hardware resources, but also on the execution strategy selected by the database query optimizer.
The PostgreSQL Query Planner (also known as the Query Optimizer) evaluates alternative execution strategies and selects a plan based on estimated costs, data distribution, table statistics, and query structure.
The resulting execution plan is represented as a tree of operations called Plan Nodes.
Understanding these nodes is essential for Database Engineers because they reveal how PostgreSQL accesses data, combines relations, filters records, sorts results, performs aggregations, and manages intermediate operations.
This article explores the main PostgreSQL Plan Node types, their purposes, and their relevance to database performance engineering.
2. What Are PostgreSQL Plan Nodes?
A Plan Node is an individual execution operation within a PostgreSQL query execution plan.
Each node performs a specific task and can consume data produced by other nodes.
A query execution plan is structured as a tree:
Leaf nodes generally retrieve or generate the initial data.
Intermediate nodes transform, combine, or filter data.
Upper-level nodes produce the final result.
For example, consider:
EXPLAIN SELECT c.customer_name, o.total_amount FROM customers c JOIN orders o ON o.customer_id = c.customer_id WHERE c.country = 'Brazil';
PostgreSQL might generate a plan containing:
Hash Join Hash Cond: (o.customer_id = c.customer_id) -> Seq Scan on orders o -> Hash -> Seq Scan on customers c Filter: (country = 'Brazil')
This plan describes a sequence of operations:
Scan the customers table.
Filter customers by country.
Build an in-memory hash table from the selected customers.
Scan the orders table.
Match orders against the hash table.
Return matching records.
The execution plan is therefore more than a description of SQL operations. It reveals the physical strategy selected by the optimizer.
3. Classification of PostgreSQL Plan Nodes
For Database Engineering purposes, Plan Nodes can be organized into functional categories:
Scan Nodes: data access operations.
Join Nodes: relational combination algorithms.
Filter and Transformation Nodes: data filtering and intermediate processing.
Sorting Nodes: ordering operations.
Aggregation Nodes: grouping and aggregate calculations.
Parallel Execution Nodes: coordination of parallel workers.
Data Modification Nodes: insertion, update, deletion, and merge operations.
This classification is intended as a practical engineering taxonomy, not an exhaustive list of every internal PostgreSQL node.
4. Scan Nodes: Data Access Operations
Scan Nodes are generally located at the bottom of an execution plan.
Their responsibility is to retrieve records from tables, indexes, or other data sources.
Choosing the appropriate access method is one of the most important decisions made by the query optimizer.
4.1 Seq Scan (Sequential Scan)
A Sequential Scan reads a table's pages sequentially.
Example:
Seq Scan on customers Filter: (country = 'Brazil')
PostgreSQL reads the table and evaluates the filter against its records.
From a performance engineering perspective, a Sequential Scan is not necessarily inefficient.
It can be the optimal strategy when:
A large percentage of the table must be retrieved.
The table is relatively small.
An available index has insufficient selectivity.
Random access through an index would be more expensive than sequential reading.
Optimization considerations:
Investigate Sequential Scans when they process large volumes of data but return relatively few records.
Potential improvements include:
Creating an appropriate index.
Improving predicate selectivity.
Reviewing data distribution.
Updating table statistics.
However, forcing index usage without understanding the workload can make performance worse.
4.2 Index Scan
An Index Scan uses an index to locate matching records and then retrieves the corresponding table rows.
Example:
Index Scan using idx_customers_country on customers Index Cond: (country = 'Brazil')
The index identifies the relevant tuple locations, and PostgreSQL fetches the corresponding table records.
Optimization considerations:
Evaluate:
Index selectivity.
Number of heap accesses.
Buffer hits and reads.
Table access patterns.
Whether the index supports the query predicates.
Index Scan is especially useful when the query retrieves a relatively small subset of the table.
4.3 Index Only Scan
An Index Only Scan retrieves the required columns directly from the index without necessarily accessing the table heap.
Example:
Index Only Scan using idx_orders_customer_amount on orders Index Cond: (customer_id = 100)
This can significantly reduce I/O when the index contains all required columns.
However, PostgreSQL's visibility rules still matter.
The visibility map determines whether PostgreSQL can avoid checking the heap for tuple visibility. If visibility information is insufficient, heap fetches may still occur.
Optimization considerations:
Evaluate covering indexes.
Consider the INCLUDE clause.
Monitor Heap Fetches.
Maintain appropriate VACUUM activity.
Balance read performance against index size and write overhead.
Example:
CREATE INDEX idx_orders_customer_amount ON orders (customer_id) INCLUDE (total_amount);
4.4 Bitmap Index Scan
A Bitmap Index Scan uses an index to identify matching tuple locations and constructs a bitmap representing those locations.
Example:
Bitmap Index Scan on idx_orders_customer Index Cond: (customer_id = 100)
It generally works together with a Bitmap Heap Scan.
The bitmap helps PostgreSQL organize the retrieval of table pages.
4.5 Bitmap Heap Scan
A Bitmap Heap Scan retrieves table records using the bitmap generated by one or more bitmap index scans.
Example:
Bitmap Heap Scan on orders Recheck Cond: (customer_id = 100) -> Bitmap Index Scan on idx_orders_customer Index Cond: (customer_id = 100)
Instead of retrieving rows individually through a conventional Index Scan, PostgreSQL can organize heap access by physical page.
This can be beneficial when retrieving a moderate number of records distributed across a table.
Optimization considerations:
Examine the number of heap blocks accessed.
Check for lossy bitmap pages and rechecks.
Evaluate whether multiple indexes can be combined.
Compare the plan with a conventional Index Scan.
4.6 TID Scan
A TID Scan accesses a table using tuple identifiers (TIDs).
Example:
SELECT * FROM orders WHERE ctid = '(0,1)';
The ctid identifies the physical location of a row version within a table.
TID Scans are useful for understanding physical tuple access, but ctid should not normally be treated as a permanent business identifier.
4.7 Other Scan Nodes
PostgreSQL also has specialized scan nodes:
Node TypePurposeFunction ScanRetrieves rows from a set-returning function in FROM.Values ScanRetrieves rows from a VALUES expression.CTE ScanReads the output of a Common Table Expression.WorkTable ScanReads recursive intermediate results.Subquery ScanProcesses the output of a subquery.Foreign ScanRetrieves data through a foreign data wrapper.Sample ScanRetrieves sampled table data.
These nodes are particularly relevant when analyzing complex SQL, recursive queries, and foreign data access.
5. Join Nodes: Relational Combination Algorithms
Join Nodes combine records from two input relations.
PostgreSQL supports three fundamental join algorithms:
Nested Loop Join.
Hash Join.
Merge Join.
The optimizer chooses among them according to estimated cardinality, available indexes, ordering, and estimated execution costs.
5.1 Nested Loop
A Nested Loop Join processes one relation and repeatedly searches the other relation for matching records.
Example:
Nested Loop -> Seq Scan on customers -> Index Scan using idx_orders_customer Index Cond: (customer_id = customers.customer_id)
Conceptually:
FOR EACH customer: FIND matching orders
This algorithm is particularly effective when:
The outer relation is relatively small.
The inner relation has an appropriate index.
The number of matching records is limited.
It can become expensive when the outer relation contains many records and the inner operation must be repeated extensively.
Optimization considerations:
Examine the number of loops.
Check the actual number of rows processed.
Verify whether the inner operation uses an appropriate index.
Investigate cardinality estimation errors.
A Nested Loop is not inherently a bad plan. Its suitability depends on the input sizes and access methods.
5.2 Hash Join
A Hash Join builds an in-memory hash table from one input relation and probes it using records from the other relation.
Example:
Hash Join Hash Cond: (orders.customer_id = customers.customer_id) -> Seq Scan on orders -> Hash -> Seq Scan on customers
Conceptually:
Read the build-side relation.
Create a hash table using the join key.
Read the other relation.
Calculate hash values for its records.
Find matching records.
Hash Join is frequently effective for large relations when equality conditions are available.
Optimization considerations:
Investigate hash table size.
Monitor memory consumption.
Examine temporary block reads and writes.
Check for multiple hash batches.
Evaluate work_mem and concurrent workload demands.
If the hash table cannot be processed efficiently in memory, PostgreSQL may partition the operation into multiple batches, potentially introducing additional I/O.
5.3 Merge Join
A Merge Join combines two inputs that are ordered according to their join keys.
Example:
Merge Join Merge Cond: (customers.customer_id = orders.customer_id) -> Index Scan using customers_pkey -> Index Scan using idx_orders_customer
Conceptually:
Read the first ordered relation.
Read the second ordered relation.
Advance through both inputs.
Match records with equal join keys.
If the inputs are not already appropriately ordered, PostgreSQL may introduce Sort nodes.
Optimization considerations:
Investigate whether indexes can provide the required ordering.
Examine the cost of sorting.
Evaluate the cardinality of both inputs.
Compare Merge Join with Hash Join for the workload.
Merge Join can be especially effective when ordered data is already available.
6. Sorting Nodes
Sorting is necessary for operations such as ORDER BY, certain DISTINCT operations, and some join strategies.
6.1 Sort
A Sort node orders records according to specified expressions.
Example:
Sort Sort Key: order_date DESC -> Seq Scan on orders
The operation may use memory or temporary disk files.
Optimization considerations:
Investigate whether an existing index can provide the required ordering.
Check sort method and memory usage.
Monitor temporary blocks.
Evaluate work_mem.
Consider whether the query really requires all sorted records.
A large external merge sort can indicate that the operation exceeded its available memory and used temporary storage.
6.2 Incremental Sort
Incremental Sort takes advantage of input data that is already partially ordered.
Example:
Incremental Sort Sort Key: customer_id, order_date Presorted Key: customer_id -> Index Scan using idx_orders_customer
Instead of sorting the entire relation from scratch, PostgreSQL sorts groups that share the already-ordered prefix.
This can reduce sorting work and memory requirements.
Optimization considerations:
Examine the presorted key.
Investigate indexes that provide partial ordering.
Compare estimated and actual group sizes.
Evaluate whether the plan avoids a more expensive full sort.
7. Aggregation Nodes
Aggregation Nodes calculate values over groups of records.
They are fundamental to analytical SQL, reporting, and data processing workloads.
7.1 Aggregate
An Aggregate node performs aggregate calculations without grouping into multiple groups.
Example:
Get Leandro Silveira’s stories in your inbox
Join Medium for free to get updates from this writer.
Aggregate -> Seq Scan on orders
SQL example:
SELECT COUNT(*), SUM(total_amount), AVG(total_amount) FROM orders;
7.2 GroupAggregate
GroupAggregate processes ordered input and calculates aggregate values for each group.
Example:
GroupAggregate Group Key: customer_id -> Sort Sort Key: customer_id -> Seq Scan on orders
The input must be ordered according to the grouping keys.
Optimization considerations:
Investigate whether an index can provide the required ordering.
Examine the number of groups.
Evaluate the cost of the preceding Sort operation.
Consider the number of records processed per group.
7.3 HashAggregate
HashAggregate uses a hash table to maintain aggregate states for different groups.
Example:
HashAggregate Group Key: customer_id -> Seq Scan on orders
This can avoid an explicit sorting operation.
However, its efficiency depends on the number of groups, available memory, and hash table size.
Optimization considerations:
Monitor memory usage.
Investigate disk spilling.
Examine the number of groups.
Evaluate work_mem and concurrent workloads.
7.4 Partial Aggregate and Finalize Aggregate
These nodes are particularly important in parallel execution plans.
Partial Aggregate calculates intermediate aggregate results in parallel workers.
Finalize Aggregate combines those intermediate results into the final answer.
A typical parallel aggregation plan may contain:
Finalize Aggregate -> Gather -> Partial Aggregate -> Parallel Seq Scan on orders
This allows PostgreSQL to distribute aggregation work among cooperating processes.
8. Filter and Transformation Nodes
Not every Plan Node retrieves records or combines relations.
Some nodes perform intermediate processing.
8.1 Filter
A Filter condition removes records that do not satisfy a Boolean expression.
Example:
Seq Scan on orders Filter: (total_amount > 1000)
An important distinction exists between Filter and Index Cond.
Index Cond identifies records through an index.
Filter evaluates a condition against records produced by the underlying operation.
A filter can be perfectly reasonable, but a large number of discarded records may indicate an opportunity for optimization.
For example:
Rows Removed by Filter: 500000
This is worth investigating, especially when the query returns very few records.
8.2 Materialize
Materialize stores the output of its child node so that it can be read again without repeating the underlying operation.
Example:
Nested Loop -> Seq Scan on customers -> Materialize -> Seq Scan on customer_categories
This is particularly useful when a Nested Loop repeatedly needs the same inner relation.
Optimization considerations:
Determine why repeated access is necessary.
Examine memory consumption.
Monitor temporary block usage.
Investigate whether the materialized input is unnecessarily large.
Materialize is not automatically a performance problem. It can prevent much more expensive repeated execution.
8.3 Memoize
Memoize caches the results of parameterized scans.
Example:
Nested Loop -> Seq Scan on orders -> Memoize Cache Key: orders.customer_id -> Index Scan using customers_pkey
If the same parameter value is encountered repeatedly, PostgreSQL can reuse a cached result rather than executing the inner scan again.
Optimization considerations:
Examine cache hits and misses.
Monitor evictions and overflows.
Investigate repeated parameter values.
Evaluate the size of the cached result set.
Memoize can significantly improve Nested Loop performance when parameter values repeat.
8.4 Unique
Unique removes duplicate records from appropriately ordered input.
It can appear in plans involving DISTINCT or certain set operations.
Example:
Unique -> Sort Sort Key: customer_id -> Seq Scan on customers
Optimization considerations:
Investigate the cost of sorting and whether the required ordering can be supplied by an index.
8.5 SetOp
SetOp implements set operations such as INTERSECT and EXCEPT.
Depending on the operation and strategy, PostgreSQL may use sorting or hashing to identify records belonging to the relevant sets.
9. Parallel Execution Nodes
PostgreSQL can distribute certain operations among parallel workers.
Several Plan Nodes are especially important when analyzing parallel execution.
9.1 Gather
Gather collects tuples produced by parallel workers and returns them to the leader process.
Example:
Gather Workers Planned: 4 -> Parallel Seq Scan on orders
9.2 Gather Merge
Gather Merge collects ordered streams from parallel workers while preserving their ordering.
Example:
Gather Merge Workers Planned: 4 -> Sort Sort Key: order_date -> Parallel Seq Scan on orders
9.3 Parallel Seq Scan
Parallel Seq Scan distributes the scanning of a table among parallel workers.
Example:
Gather -> Parallel Seq Scan on orders
9.4 Parallel Hash
Parallel Hash supports parallel hash table construction for parallel hash joins.
It allows workers to cooperate in building a shared hash table rather than independently building identical copies.
Optimization considerations for parallel execution:
Compare Workers Planned and Workers Launched.
Examine work distribution.
Investigate the cost of gathering results.
Consider CPU availability.
Evaluate I/O bottlenecks.
Assess whether the query contains enough work to justify parallel execution.
Parallel execution is not automatically faster. Coordination and data transfer can introduce overhead.
10. Data Modification Nodes
PostgreSQL also exposes Plan Nodes for data modification statements.
These nodes are relevant to transactional performance engineering.
10.1 Insert
Represents the insertion of records.
Example:
Insert on orders -> ...
10.2 Update
Represents the modification of existing records.
Example:
Update on orders -> Index Scan using orders_pkey
10.3 Delete
Represents the removal of records.
Example:
Delete on orders -> Index Scan using orders_pkey
10.4 Merge
Represents the execution of a SQL MERGE statement.
The underlying plan locates and processes source and target records according to the MERGE conditions.
Optimization considerations:
Examine the access path used to locate affected records.
Evaluate index maintenance overhead.
Investigate locking and concurrency.
Consider WAL generation.
Examine triggers and foreign key effects.
Evaluate transaction size and batch processing.
One important detail: the cost model does not necessarily account for all the physical work associated with modifying data. The cost displayed for a modification plan should not be interpreted as a complete estimate of its transactional overhead.
11. How Database Engineers Use Plan Nodes for Optimization
Understanding Plan Nodes is only the first step.
The real engineering value comes from interpreting their behavior and identifying opportunities for improvement.
A practical methodology can be organized into four phases.
Phase 1: Baseline
Collect the original execution plan and performance metrics.
EXPLAIN (ANALYZE, BUFFERS) SELECT ...
Record:
Execution time.
Planning time.
Estimated rows.
Actual rows.
Buffer hits and reads.
Temporary block usage.
Number of loops.
Join strategies.
Scan strategies.
Phase 2: Diagnosis
Identify the operators responsible for excessive work.
Examples:
ObservationInvestigationLarge Seq Scan with few returned rowsIndex selectivity and filteringNested Loop with many loopsOuter cardinality and inner accessHash Join with multiple batchesMemory pressure and hash sizeExternal Merge SortSorting and memory requirementsLarge Rows Removed by FilterPredicate selectivity and access pathLarge estimate/actual discrepancyStatistics and cardinality estimationHigh Heap Fetches in Index Only ScanVisibility map and VACUUM
Phase 3: Optimization
Formulate a hypothesis and make a controlled change.
Possible interventions include:
SQL rewriting.
Index creation or redesign.
Composite and covering indexes.
Statistics updates.
Extended statistics.
Memory configuration.
Partitioning.
Data model adjustments.
The intervention must address the diagnosed cause rather than simply attempting to eliminate a particular Plan Node.
Phase 4: Validation
Execute the same workload again and compare the results.
EXPLAIN (ANALYZE, BUFFERS) SELECT ...
Evaluate whether the intervention reduced execution time, I/O, CPU consumption, or other relevant costs without introducing regressions.
Performance optimization is an empirical process. The new execution plan must be validated under representative workload conditions.
12. Important Considerations When Reading Execution Plans
A few principles are particularly important:
1. A Sequential Scan is not inherently bad.
Scanning a small table or retrieving most of a large table can be more efficient than using an index.
2. An Index Scan is not always better than a Sequential Scan.
Random heap access can be expensive when a large percentage of the table must be retrieved.
3. A Nested Loop is not inherently inefficient.
It can be an excellent strategy for small outer inputs and indexed inner lookups.
4. High estimated cost does not necessarily mean high execution time.
PostgreSQL costs are relative units used by the optimizer, not milliseconds.
5. The execution plan must be interpreted as a whole.
A slow operator may be the consequence of an earlier cardinality estimation problem.
6. EXPLAIN ANALYZE executes the statement.
For data modification statements, use an appropriate transaction and ROLLBACK when testing changes that must not persist.
13. Conclusion
PostgreSQL Plan Nodes provide a practical view of how the query optimizer translates SQL into executable operations.
For Database Engineers, understanding these operators means understanding the interaction between:
SQL query structure.
Physical data access.
Index architecture.
Join algorithms.
Cardinality estimation.
Memory management.
Parallel execution.
Storage and I/O behavior.
The objective is not to eliminate expensive-looking operators or force the database to use a particular execution strategy.
The objective is to understand why the optimizer selected a given plan, determine whether its assumptions are accurate, identify the actual performance bottleneck, and validate an improvement through measurement.
Database Performance Engineering is not about guessing which SQL statement is faster. It is about understanding execution behavior, formulating hypotheses, measuring results, and continuously improving the database workload.
Professional Services
I offer mentoring, training, consulting, and fractional Database Engineering services to help organizations improve database performance, reliability, and operational efficiency.
References
PostgreSQL Documentation — Using EXPLAIN: https://www.postgresql.org/docs/18/using-explain.html
PostgreSQL Documentation — EXPLAIN Command: https://www.postgresql.org/docs/18/sql-explain.html
PostgreSQL Documentation — Planner/Optimizer: https://www.postgresql.org/docs/18/planner-optimizer.html
PostgreSQL Documentation — Parallel Plans: https://www.postgresql.org/docs/18/parallel-plans.html
PostgreSQL Documentation — Combining Multiple Indexes: https://www.postgresql.org/docs/18/indexes-bitmap-scans.html
This article focuses on PostgreSQL 18 and its documented execution-plan concepts. Exact node availability and displayed details can vary by PostgreSQL version and query structure.





