Indexes
Improving query performance with indexes
Run the following query to find the number of rows in the orders table:
12SELECT count(*) AS order_countFROM orders;The result shows that the orders table contains 100,000 rows.
Now run the following query to view one of those orders:
123SELECT *FROM ordersWHERE id = 4242;Notice that this order has the payment reference pay_00004242.
Suppose you want to find the order using that payment reference:
123SELECT *FROM ordersWHERE payment_reference = 'pay_00004242';Only one order has this payment reference.
Before running the query normally, place EXPLAIN ANALYZE before it so that you can inspect its actual execution:
1234EXPLAIN ANALYZESELECT *FROM ordersWHERE payment_reference = 'pay_00004242';Type the statement into the SQL editor and run it. You should see output similar to the following:
1234567891011 QUERY PLAN---------------------------------------------------------------------------------------------------------- Seq Scan on orders (cost=0.00..2271.00 rows=1 width=52) (actual time=0.353..7.099 rows=1.00 loops=1) Filter: (payment_reference = 'pay_00004242'::text) Rows Removed by Filter: 99999 Buffers: shared hit=1021 Planning: Buffers: shared hit=40 Planning Time: 0.085 ms Execution Time: 7.146 ms(8 rows)Your timing and buffer values will probably differ from the ones shown here.
The plan contains a Seq Scan node:
1Seq Scan on ordersThis means PostgreSQL performed a sequential scan of the orders table. In other words, PostgreSQL went through the rows in the table one by one, checking the payment_reference value of each row to determine whether it matched 'pay_00004242'.
Now notice these two values in the plan:
1rows=1.001Rows Removed by Filter: 99999rows=1.00 tells us that the sequential scan produced one matching row. Rows Removed by Filter: 99999 tells us that PostgreSQL examined another 99,999 rows that didn't match the condition.
Since the sequential scan ran once (loops=1), we can add these two values directly: 1 matching row + 99,999 rows removed by the filter = 100,000 rows examined.
PostgreSQL therefore examined all 100,000 rows in the table to return just one row.
The plan also reports:
1Execution Time: 7.146 msThe exact execution time will vary between runs and between computers. More importantly, PostgreSQL had to examine 100,000 rows to return just one.
Is there a way for PostgreSQL to find this row without examining all 100,000 rows?
Yes. This is one of the problems that indexes are designed to solve.
What is an index?
An index is a separate data structure that PostgreSQL maintains for a table. It stores values from one or more table columns in a form that allows PostgreSQL to locate matching table rows efficiently.
Without a suitable index, PostgreSQL may need to examine every row in a table:
1234Table row -> Does it match?Table row -> Does it match?Table row -> Does it match?...With a suitable index, PostgreSQL may be able to search the index for the required value and then retrieve only the matching table rows:
1Search index -> Locate matching table row -> Retrieve rowIndexes are particularly useful for selective conditions. A condition is selective when it matches only a small proportion of the table.
The condition in our query is highly selective:
1WHERE payment_reference = 'pay_00004242'It matches one row out of 100,000, making it a good candidate for an index.
Note that there is no fixed number or percentage of matching rows at which PostgreSQL decides to use an index. When planning a query, PostgreSQL compares the estimated cost of using the available index with other possible plans, such as a sequential scan, and chooses the plan with the lowest estimated cost.
Also, PostgreSQL automatically keeps an index up to date as the table data changes. When rows are inserted, updated, or deleted, PostgreSQL updates the index when necessary so that it continues to point to the correct table rows.
PostgreSQL index types
PostgreSQL provides several built-in index types:
B-tree
Hash
GiST
SP-GiST
GIN
BRIN
Each index type is designed for particular kinds of data and operations.
In this chapter, we will concentrate on the B-tree index. B-tree is the default index type created by PostgreSQL and supports many common equality and range conditions, including:
12345=<<=>>=Our query uses an equality condition:
1WHERE payment_reference = 'pay_00004242'A B-tree index can support this type of condition, so it is a suitable index type for our example.
Creating an index
The basic form of CREATE INDEX is:
12CREATE INDEX index_nameON table_name (column_name);The statement contains three important parts:
index_nameis the name assigned to the index.table_nameis the table the index belongs to.column_nameis the column whose values will be indexed.
Create an index on the payment_reference column:
12CREATE INDEX idx_orders_payment_referenceON orders (payment_reference);Type the statement into the SQL editor and run it.
The index name is idx_orders_payment_reference. The name describes both the table and the indexed column, making the purpose of the index easier to recognize.
We didn't specify an index type. PostgreSQL therefore created a B-tree index, which is the default. The same index could be defined explicitly like this:
12CREATE INDEX idx_orders_payment_referenceON orders USING btree (payment_reference);Specifying USING btree is optional in this case.
Running the query again
Now run the same statement again:
1234EXPLAIN ANALYZESELECT *FROM ordersWHERE payment_reference = 'pay_00004242';You should now see a different execution plan:
1234567891011 QUERY PLAN-------------------------------------------------------------------------------------------------------------------------------------------------- Index Scan using idx_orders_payment_reference on orders (cost=0.42..8.44 rows=1 width=52) (actual time=0.009..0.010 rows=1.00 loops=1) Index Cond: (payment_reference = 'pay_00004242'::text) Index Searches: 1 Buffers: shared hit=1 read=3 Planning: Buffers: shared hit=15 read=1 Planning Time: 0.078 ms Execution Time: 0.018 ms(8 rows)The Seq Scan has been replaced by an Index Scan:
1Index Scan using idx_orders_payment_reference on ordersThis tells us that PostgreSQL used the newly created index to locate the matching order.
The next line contains:
1Index Cond: (payment_reference = 'pay_00004242'::text)Index Cond means that PostgreSQL used this condition while searching the index. Previously, the same condition appeared as a Filter applied while scanning every table row.
Notice that the new plan doesn't contain:
1Rows Removed by Filter: 99999PostgreSQL no longer needs to apply the condition to every row in the table. It can use the index to locate the matching row directly.
Comparing the two plans
The difference between the two executions can be summarized as follows:
Scan node: Before the index, PostgreSQL used a
Seq Scan. After the index, it used anIndex Scan.Rows returned: Both executions returned one row.
Rows removed by the filter: The sequential scan removed 99,999 rows. The index scan didn't need to filter those rows.
Shared-buffer accesses: The sample sequential scan reported 1,021. The sample index scan reported four.
Estimated total cost: The estimate changed from
2271.00to8.44.Execution time: In the sample executions, the time changed from
7.146milliseconds to0.018milliseconds.
For this particular execution, the reported execution time was approximately 400 times faster after the index was created.
You shouldn't expect the same timing on every computer or every execution. Timing is affected by caching, hardware, other database activity, and measurement overhead.
The more important result is the reduction in work. Before the index was created, PostgreSQL examined 100,000 rows and accessed 1,021 shared blocks. Afterward, it used the index and made only four shared-buffer accesses to locate and retrieve the matching row.
An index doesn't guarantee an index scan
Creating an index doesn't force PostgreSQL to use it.
The planner considers the available execution plans and selects the one with the lowest estimated cost. For a query that returns one row from a large table, an index scan will often be cheaper.
However, if a query returns most or all of a table, a sequential scan may be faster. Reading the table sequentially can require less work than repeatedly moving between an index and the table.
For example, the following statement returns every order:
12SELECT *FROM orders;An index on payment_reference doesn't help this query because PostgreSQL still needs to retrieve every row.
A sequential scan is therefore not automatically a sign of poor performance. It becomes worth investigating when PostgreSQL examines a large number of rows but returns only a small number of them.
Indexes have costs
The performance improvement provided by an index isn't free.
An index requires additional storage. You can inspect the size of the table and the index with the following statement:
12345SELECT pg_size_pretty(pg_relation_size('orders')) AS table_size, pg_size_pretty( pg_relation_size('idx_orders_payment_reference') ) AS index_size;You should see values similar to these:
1234 table_size | index_size------------+------------ 8168 kB | 3104 kB(1 row)The exact sizes may differ, but the important point is that the index occupies additional storage.
Indexes also add work when table data changes. PostgreSQL must keep the index synchronized when relevant rows are inserted, updated, or deleted. Additional indexes can therefore make write operations more expensive.
For this reason, you shouldn't create an index on every column. An index is most valuable when it supports queries that are important and frequently executed.
Removing an index
Before removing an index, you can check which indexes currently exist on the orders table. Run the following query:
1234SELECT indexname, indexdefFROM pg_indexesWHERE schemaname = 'public' AND tablename = 'orders';The result includes the name and definition of each index on the table.
Suppose you no longer need the idx_orders_payment_reference index. You can remove it using the following statement:
1DROP INDEX idx_orders_payment_reference;Dropping the index removes only the index. It doesn't remove the orders table or any of its rows.
The important lesson isn't that index scans are always better than sequential scans. It is that EXPLAIN ANALYZE can reveal when PostgreSQL is performing substantially more work than the result appears to require, and a suitable index can give the planner a more efficient option.