Analyzing Scans in PostgreSQL

Understand the different types of Scans in PostgreSQL and why the query planner chooses them in different situations.

2 months ago   •   6 min read

By Zach Naimon
Table of contents

Introduction & Data Setup

Before we dive in, it is vital to understand the basic elementary blocks of PostgreSQL query plans. This has been covered in a separate blog post, and I highly encourage readers to go through first -

Anatomy of a PostgreSQL Query Plan
The first thing to look at to start optimizing a query is the Query Planner. In this post we will explain how a query gets executed and how to understand the EXPLAIN command.

There are several node types in PostgreSQL query plans,

  • Scans.
  • Joins.
  • Sort.
  • Aggregates etc.,

In this article, we will dive deep into the Scan node type, which is probably the most fundamental piece. Let's use the same fake_data table that was used in the query plan blog. For ease of understanding, we are going to disable parallel scans. We will enable this a bit later and also understand parallel scans in a separate section.

SET max_parallel_workers_per_gather = 0;

Scans

There will be other node types in the examples below, but the focus will be only on the scan nodes and highlighted throughout the examples below.

Sequential Scans

Let's search for an id with value 1000.

Sequential Scan
Sequential Scan

This is the simplest of them all; if there is no index or a lesser number of rows, then the Planner resorts to scanning all of the rows present. You would/should never encounter Sequential Scans in a typical production system since they are very slow and get slower as the data increases. There are exceptions when the table is too small where an index is pointless and Sequential Scans are fast enough that you don't have to worry.

Index Scans

Let's create a simple BTree index on the id column to speed up the above query.

CREATE INDEX id_idx ON fake_data USING BTREE(id)

After index creation, the Planner now uses the index to perform what is called an Index Scan.

Index Scan
Index Scan

As you can see, it is significantly faster (around 3500x) than a sequential scan. We should create appropriate indexes to speed up queries based on our access/query patterns. Here is an article that goes in-depth on what kind of indexes to create for different access patterns:

Complete Guide to Database Indexes - Increase Query Speeds by 99%+
Database indexes are a common solution for faster queries, but they also add overhead that can degrade performance. Keep reading to learn how to create the optimal database index.

Index Only Scans

Index Only scans are very similar to index scans except that they scan only the indexes and do not touch the table data. This is possible only if the query contains the indexed column in both the SELECT and WHERE clauses. A slight tweak to the above query demonstrates this,

Index Only Scan
Index Only Scan

One thing to note is that PostgreSQL might sometimes choose to do Index Scan in place of Index Only Scan if the table is not vacuumed well, i.e., the Planner will realize that there could be some mismatch between the indexes and the actual table data. This can happen if some large delete/insert in/from the table and the indexes have not been updated. It is good practice to time the auto vacuum correctly and vacuum if there is any large operation like a table load. Index Only Scans can be very fast in terms of I/O and time since indexes are practically cached in shared_buffers.

Bitmap Heap Scan & Bitmap Index Scan

Let's compare on a different column name which is text content.

CREATE INDEX name_str_idx on fake_data USING BTREE(name)
EXPLAIN ANALYZE
SELECT
    *
FROM
    fake_data
WHERE
    fake_data.name = 'David Bennett'
Bitmap Heap & Bitmap Index Scan
Bitmap Heap & Bitmap Index Scan

The Planner decided to go with Bitmap heap Scan even though there was a BTree index on the name column. The reason is that index scans cause random I/O if there is no ordering to the rows (name is text content). This is costly in rotational hard drives. To solve this, the Planner takes a two-stage approach. The Bitmap Index Scan constructs a data structure called a bitmap from the index present and represents the approximate location of pages in the disk and feeds it to the parent node, i.e., Bitmap Heap Scan, which then uses that to fetch the pages/rows.

There is a discussion in the PostgreSQL mailing list, which goes into depth on its workings, but on a high level, a Bitmap Heap Scan always works in tandem with Bitmap Index Scan to speed things up. For example, suppose there are a large number of matching rows. In that case, the Planner decides to do Bitmap Heap Scan + Bitmap Index Scan over a traditional Index Scan, in which the executor iterates the Bitmap instead of the Index itself for faster results.

Bitmap And & Bitmap Or

The Bitmap data structure is not only beneficial for single matches but can also be combined if there are two indexes.

Bitmap Or
Bitmap Or

Bitmap And is also similar,

Bitmap And
Bitmap And

Keep in mind that in certain situations Bitmap And or Bitmap Or won't work, and we will have to create composite indexes. But in many situations, the Planner can combine two individual indexes very efficiently.

Parallel Scans

Sequential Scans are the slowest of all the plans we have seen so far because there is nothing to optimize there. The Planner goes over the data sequentially and tries to find the result. PostgreSQL optimizes this further by adding parallelism in the queries. Let's simulate this by removing the index for the id column and running the query. If you are using the same session as before, restart your database client for max_parallel_workers_per_gather setting to reset to default.

Parallel Sequential Scans
Parallel Sequential Scans

The default workers configuration is 2 and hence two workers have been used to run the query. Workers are processes behind the scenes, which enables the executor to run the scans in parallel. Going deep into how parallelization works is perhaps a topic of another blog. Still, the general recommendation is to keep the workers equal to how many cores are there in the CPU for the most efficient results.

Conclusion

Hopefully, by now, you have a good idea of scans and why PostgreSQL uses specific types of scans in different places. Understanding these scan types will help users answer questions like,

  • Why is not PostgreSQL using my index in a direct WHERE comparison?
  • Why is PostgreSQL not able to combine two indexes using Bitmap Scan?
  • How to optimize our queries for Index Only Scan?

One thing to understand is that there are certain kinds of queries, such as Count, Avg and other aggregate queries, which always result in Sequential Scan because they have to scan the entirety of the table anyway for the result. Stay tuned for more articles on PostgreSQL node types.

JOIN the Arctype Newsletter
Programming stories, tutorials, and database tips every 2 weeks

Spread the word

Keep reading