BRIN Index was introduced in PostgreSQL 9.5, but many users postponed the usage of it in their design and development just because it was “new”. But now we understand that it has stood the test-of-time! It is time to reconsider BRIN if you have not done it yet. I often see users who forget there is a provision to select the type of Index by specifying USING clause when creating an index.
BRIN Index is a revolutionary idea in indexing first proposed by PostgreSQL contributor Alvaro Herrera. BRIN stands for “Block Range INdex”. A block range is a group of pages adjacent to each other, where summary information about all those pages is stored in Index. For example, Datatypes like integers – dates where sort order is linear – can be stored as min and max value in the range. Other database systems including Oracle announced similar features later. BRIN index often gives similar gains as Partitioning a table.
BRIN usage will return all the tuples in all the pages in the particular range. So the index is lossy and extra work is needed to further filter out records. So while one might say that is not good, there are a few advantages.
Let’s take a simple example to examine the benefits of BRIN index by creating a simple table.
|
1 |
postgres=# CREATE TABLE testtab (id int NOT NULL PRIMARY KEY,date TIMESTAMP NOT NULL, level INTEGER, msg TEXT);<br>CREATE TABLE |
Now let’s Insert some data into this table.
|
1 |
postgres=# INSERT INTO testtab (id, date, level, msg) SELECT g, CURRENT_TIMESTAMP + ( g || 'minute' ) :: interval, random() * 6, md5(g::text) FROM generate_series(1,8000000) as g;<br>INSERT 0 8000000 |
Please note that values in id column and date columns keep increasing for new records, which is common for transaction records. This is an important property which BRIN make use off.
A query at this stage may have to do a full scan of the table and it will be quite expensive.
|
1 |
postgres=# explain analyze select * from public.testtab where date between '2019-08-08 14:40:47.974791' and '2019-08-08 14:50:47.974791';<br> QUERY PLAN <br>---------------------------------------------------------------------------------------------------------------------------------------------------------------<br>Gather (cost=1000.00..133476.10 rows=11 width=49) (actual time=31.835..1766.409 rows=11 loops=1)<br> Workers Planned: 2<br> Workers Launched: 2<br> -> Parallel Seq Scan on testtab (cost=0.00..132475.00 rows=5 width=49) (actual time=1162.029..1739.713 rows=4 loops=3)<br> Filter: ((date >= '2019-08-08 14:40:47.974791'::timestamp without time zone) AND (date <= '2019-08-08 14:50:47.974791'::timestamp without time zone))<br> Rows Removed by Filter: 2666663<br>Planning Time: 0.296 ms<br>Execution Time: 1766.454 ms<br>(8 rows) |
As we can see above, PostgreSQL employs 2 workers and does the scan in parallel, where it takes 1766.454 ms. In a nutshell, it is heavy on the system and takes a good amount of time.
As usual, our tendency is to create an index on the filtering column. (B-Tree by default)
|
1 |
postgres=# create index testtab_date_idx on testtab(date);<br>CREATE INDEX |
Now let’s see how the previous SELECT query works:
|
1 |
postgres=# explain analyze select * from public.testtab where date between '2019-08-08 14:40:47.974791' and '2019-08-08 14:50:47.974791';<br> QUERY PLAN <br>-------------------------------------------------------------------------------------------------------------------------------------------------------------<br>Index Scan using testtab_date_idx on testtab (cost=0.43..8.65 rows=11 width=49) (actual time=1.088..1.090 rows=11 loops=1)<br> Index Cond: ((date >= '2019-08-08 14:40:47.974791'::timestamp without time zone) AND (date <= '2019-08-08 14:50:47.974791'::timestamp without time zone))<br>Planning Time: 22.225 ms<br>Execution Time: 2.657 ms<br>(4 rows) |
Obviously, B-Tree is lossless, and it can give a tremendous boost to the performance and efficiency of SELECT queries, especially when Index is freshly created. But a B-Tree index has following side effects:
It will be interesting to check the Index to table size ratio of this fresh index.
|
1 |
postgres=# di+ testtab_date_idx;<br>List of relations<br>Schema | Name | Type | Owner | Table | Size | Description<br>--------+------------------+-------+----------+---------+--------+-------------<br>public | testtab_date_idx | index | postgres | testtab | 171 MB |<br>(1 row) |
So we have B-Tree index 171MB for a 1.3GB table. So Index to table size ratio is 0.13, for this is a fresh index.
But this index-table ratio can keep deteriorating over time as index undergoes continuous updates. Index ratio crossing 0.5 is common in many production environments. As the ratio becomes bad, the efficiency of the index goes bad and it starts occupying more shared buffers.
Things get much worse if the table grows to a bigger size (hundreds of GB or TB) as Index also grows in the same ratio. The impact of B-Tree index on DMLs is heavy – sometimes I measured up to 30% overhead especially for bulk loads.
Now let’s see what happens if we replace a B-Tree index with a BRIN index.
|
1 |
postgres=# create index testtab_date_brin_idx on testtab using brin (date);<br>CREATE INDEX |
The very first observation is that the impact of Index on the same bulk insert operation is measured to be 3% which is within my noise/error limits. 30% overhead Vs 3% overhead.
If we consider the size of the newly created BRIN index:
|
1 |
postgres=# di+ testtab_date_brin_idx;<br> List of relations<br>Schema | Name | Type | Owner | Table | Size | Description<br>--------+-----------------------+-------+----------+---------+-------+-------------<br>public | testtab_date_brin_idx | index | postgres | testtab | 64 kB |<br>(1 row) |
As we can see it is just 64kB! 171MB of B-Tree index Vs 64 kb of BRIN index.
So far BRIN wins my heart. Now its time to look at how much query performance improvement it can bring in.
|
1 |
postgres=# explain analyze select * from public.testtab where date between '2019-08-08 14:40:47.974791' and '2019-08-08 14:50:47.974791';<br> QUERY PLAN <br>-------------------------------------------------------------------------------------------------------------------------------------------------------------------<br>Bitmap Heap Scan on testtab (cost=20.03..33406.84 rows=11 width=49) (actual time=62.762..87.471 rows=11 loops=1)<br> Recheck Cond: ((date >= '2019-08-08 14:40:47.974791'::timestamp without time zone) AND (date <= '2019-08-08 14:50:47.974791'::timestamp without time zone))<br> Rows Removed by Index Recheck: 12405<br> Heap Blocks: lossy=128<br> -> Bitmap Index Scan on testtab_date_brin_idx (cost=0.00..20.03 rows=12403 width=0) (actual time=1.498..1.498 rows=1280 loops=1)<br> Index Cond: ((date >= '2019-08-08 14:40:47.974791'::timestamp without time zone) AND (date <= '2019-08-08 14:50:47.974791'::timestamp without time zone))<br>Planning Time: 0.272 ms<br>Execution Time: 87.703 ms<br>(8 rows) |
As I expected, it is not that efficient as a fully cached B-Tree index. However, performance improvement from 1766.454 ms to 87.703 ms means approximately 20 times better! That’s with a single worker consuming fewer resources. With just a 64kb Overhead, the cost-benefit of BRIN is very positive.
As we saw the BRIN index uses much less space compared to normal B-Tree index, but the lossy index can reduce the effectiveness of Index. Luckily this can be adjusted using the storage parameter pages_per_range. BRIN stores entries for a range of pages in the corresponding table. The larger the range of pages, the smaller the index, and it gets lossier.
|
1 |
create index testtab_date_brin_idx on testtab using brin (date) with (pages_per_range = 32); |
As we already discussed, part of the index maintenance during DML is offloaded to vacuum. In fact, there are 2 cases.
In the first case, summary in BRIN is updated straight away along with DML. But if new pages are not summarized already, it will be done by VACUUM or AUTOVACUUM.
There are 2 functions provided for this. A high-level, function call which can be executed against the index like:
|
1 |
postgres=# select brin_summarize_new_values('testtab_date_brin_idx'::regclass);<br>brin_summarize_new_values<br>---------------------------<br>2577<br>(1 row) |
This summarizes all ranges that are not currently summarized. The return value indicates the number of new page range summaries that were inserted into the index. This function operates on the top of a lower-level function brin_summarize_range which accepts the range of pages.
|
1 |
postgres=# SELECT brin_summarize_range('testtab_date_brin_idx', 10);<br>brin_summarize_range<br>----------------------<br>0<br>(1 row) |
A key point to note is that auto-summarization is off by default, which we can enable by storage parameter auto summarize:
|
1 |
postgres=# alter index testtab_date_brin_idx set (pages_per_range = 64,autosummarize = on);<br>ALTER INDEX |
In this case, automatic summarization will be executed by autovacuum as insertions occur.
BRIN indexes are efficient if the ordering of the key values follows the organization of blocks in the storage layer. In the simplest case, this could require the physical ordering of the table, which is often the creation order of the rows within it, to match the key’s order. Keys on generated sequence numbers or created data are best candidates for BRIN index.
Resources
RELATED POSTS