404 Not Found

404 Not Found


nginx

PostgreSQL Index Internals and Optimization

1. What You'll Learn


2. The Story

Bob is the DBA for an e-commerce platform. Users report that the product search page keeps getting slower — a full scan of a JSONB column makes queries take 3 seconds.

After Bob adds a GIN index to the JSONB column, search drops to 300 ms. He then notices that active-user queries are also slow, yet 90% of users have already been deactivated, so building an index on the whole table wastes space. He uses a partial index that only indexes rows where status = 'active', shrinking the index size by 80% and making queries faster.


3. Concept: Index Type Overview

(1) The Six Index Types

Index type Full name Suitable data types Typical use case
B-Tree Balanced Tree All sortable types Equality, range, sorting, prefix LIKE
Hash Hash Table All types Simple equality queries
GIN Generalized Inverted Index Arrays, JSONB, full-text search Containment queries, full-text search
GiST Generalized Search Tree Geometry, ranges, full text Spatial queries, nearest neighbor
BRIN Block Range Index Large tables with ordered columns Time-series data, time-range scans
SP-GiST Space-Partitioned GiST Irregular partitioning structures Phone numbers, routing

▶ Example: View Existing Indexes on a Table

SQL
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'orders';
TEXT
 indexname              | indexdef
------------------------+--------------------------------------------------
 orders_pkey            | CREATE UNIQUE INDEX ... ON orders USING btree (order_id)
 idx_orders_customer_id | CREATE INDEX ... ON orders USING btree (customer_id)

(2) Index Type Selection Decision

100%
flowchart TD
    A{Query type?} -->|Equality/range/sort| B[B-Tree]
    A -->|Simple equality| C[Hash]
    A -->|Array/JSONB contains| D[GIN]
    A -->|Spatial/geometric/nearest| E[GiST]
    A -->|Large table seq scan| F[BRIN]
    A -->|Irregular partition structure| G[SP-GiST]

    B --> B1["Default index type<br/>90% of cases"]
    C --> C1["Rarely used<br/>B-Tree usually better"]
    D --> D1["JSONB @> ?<br/>array @> <br/>tsvector @@ "]
    E --> E1["PostGIS<br/>range overlap"]
    F --> F1["10M+ row time-series<br/>tiny size"]
    G --> G1["Phone prefix<br/>IP routing"]

    style A fill:#e1f5fe
    style B fill:#c8e6c9
    style D fill:#fff9c4
    style F fill:#fff9c4

4. Concept: B-Tree Index

(1) B-Tree Features and Use Cases

B-Tree is PostgreSQL's default index type; it supports equality, range, sorting, IS NULL, and prefix LIKE queries.

Supported operator Example
Equality WHERE col = 100
Range WHERE col > 100 AND col < 200
Sorting ORDER BY col
IS NULL WHERE col IS NULL
Prefix LIKE WHERE col LIKE 'abc%'
BETWEEN WHERE col BETWEEN 1 AND 10
Not supported Reason
LIKE '%abc' A leading wildcard can't use B-Tree ordering
col::text = '100' Type mismatch; needs an expression index
LOWER(col) = 'abc' Function result isn't indexed; needs an expression index

▶ Example: Create a B-Tree Index

SQL
CREATE INDEX idx_orders_amount ON orders (amount);

CREATE INDEX idx_orders_date_amount ON orders (order_date, amount DESC);

Output:

TEXT
CREATE TABLE

▶ Example: UNIQUE Index

SQL
CREATE UNIQUE INDEX idx_users_email ON users (email);

Output:

TEXT
CREATE TABLE

A UNIQUE index guarantees both data uniqueness and query performance.


5. Concept: GIN Index

(1) GIN Index Principle

GIN (Generalized Inverted Index) is an inverted index: a mapping from an element to the rows that contain it. It fits "contains"-style queries.

Type Operator Example
JSONB @> ? `? ?&`
Array @> <@ && WHERE tags @> ARRAY['sale']
tsvector @@ WHERE body @@ to_tsquery('postgres')

▶ Example: GIN Index on a JSONB Column

SQL
CREATE INDEX idx_products_attrs ON products USING GIN (attrs);

SELECT product_id, name
FROM products
WHERE attrs @> '{"category": "electronics"}';
TEXT
 product_id |    name
------------+------------
        101 | Laptop Pro
        205 | Smart Watch

▶ Example: GIN Index on an Array Column

SQL
CREATE INDEX idx_products_tags ON products USING GIN (tags);

SELECT product_id, name
FROM products
WHERE tags @> ARRAY['summer', 'sale'];

Output:

TEXT
  result  
----------
   42.50
(1 row)
SQL
CREATE INDEX idx_articles_body ON articles USING GIN (to_tsvector('english', body));

SELECT id, title
FROM articles
WHERE to_tsvector('english', body) @@ to_tsquery('postgresql & index');

Output:

TEXT
CREATE TABLE

(2) GIN vs B-Tree Comparison

Dimension B-Tree GIN
Query type Equality / range Containment / search
Write speed Fast Slow (inverted lists must be updated)
Index size Medium Larger
Suitable types Scalar Array / JSONB / full text
Sorting support Yes No

6. Concept: GiST / BRIN / SP-GiST / Hash

(1) GiST Index

GiST is a generalized search-tree framework that supports custom partitioning strategies. Its typical use is spatial data.

Use case Operator Extension
Geometric data && @ <@ PostGIS
Range types && @> <@ Built-in
Full-text search @@ Built-in

▶ Example: GiST Index on a Range Type

SQL
CREATE INDEX idx_events_time_range ON events USING GiST (time_range);

SELECT event_id, title
FROM events
WHERE time_range && daterange('2025-01-01', '2025-03-01');

Output:

TEXT
CREATE TABLE

(2) BRIN Index

BRIN (Block Range Index) stores summary information for each data block (min/max values). It is extremely compact and suits physically ordered large tables.

Dimension B-Tree BRIN
Index size Large Extremely small (about 1/1000)
Precision Exact Approximate (may scan a few extra blocks)
Maintenance cost High Very low
Use case Random queries Time-series range scans

▶ Example: BRIN Index on a Time-Series Table

SQL
CREATE INDEX idx_logs_created_at ON logs USING BRIN (created_at)
  WITH (pages_per_range = 32);

SELECT count(*) FROM logs
WHERE created_at BETWEEN '2025-06-01' AND '2025-06-30';

Output:

TEXT
 count 
-------
     5
(1 row)

(3) SP-GiST Index

SP-GiST suits unbalanced partitioning structures, such as phone-number prefixes and IP routing.

▶ Example: SP-GiST Index on a Phone-Number Prefix

SQL
-- Enable the btree_gist extension first: CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE INDEX idx_customers_phone ON customers USING SP-GiST (phone prefix_range);

Output:

TEXT
CREATE TABLE

(4) Hash Index

A Hash index only supports simple equality queries; it does not support range or sorting. Before PostgreSQL 10, Hash indexes had a WAL problem (now fixed), but B-Tree is generally the better choice.

Dimension B-Tree Hash
Equality query Fast Fast
Range query Supported Not supported
Sorting Supported Not supported
WAL Complete Complete since PostgreSQL 10
Recommendation Default choice Rarely used

7. Concept: Advanced Index Features

(1) Partial Index

A partial index contains only the rows that satisfy a WHERE condition, reducing index size and maintenance cost. This is a PostgreSQL specialty feature.

Dimension Full index Partial index
Rows included All rows Rows that meet the condition
Index size Large Small
Maintenance cost Updated on every write Only relevant rows updated
Use case General queries Queries that only care about a subset

▶ Example: Index Only Active Users

SQL
CREATE INDEX idx_users_active_email ON users (email)
WHERE status = 'active';

Output:

TEXT
CREATE TABLE
SQL
SELECT email FROM users WHERE status = 'active' AND email = 'alice@example.com';

This query hits the partial index. A query without the status = 'active' condition will not hit it.

▶ Example: Index Only Un-shipped Orders

SQL
CREATE INDEX idx_orders_pending ON orders (created_at)
WHERE shipped = false;

Output:

TEXT
CREATE TABLE

(2) Expression Index

When a query condition wraps a column in a function or computation, a normal index can't be used. An expression index builds the index on the computed result.

▶ Example: Case-Insensitive Query

SQL
CREATE INDEX idx_users_email_lower ON users (LOWER(email));

SELECT * FROM users WHERE LOWER(email) = 'alice@example.com';

Output:

TEXT
CREATE TABLE

▶ Example: Date-Truncation Query

SQL
CREATE INDEX idx_orders_date_trunc ON orders (DATE_TRUNC('day', created_at));

SELECT COUNT(*) FROM orders
WHERE DATE_TRUNC('day', created_at) = '2025-06-15'::date;

Output:

TEXT
 count 
-------
     5
(1 row)

(3) Composite Index and the Leftmost Prefix

The query patterns a composite index (a, b, c) can serve:

Query condition Hits? Reason
WHERE a = 1 Yes Leftmost prefix
WHERE a = 1 AND b = 2 Yes Leftmost prefix
WHERE a = 1 AND b = 2 AND c = 3 Yes Full match
WHERE b = 2 No Missing leftmost column
WHERE b = 2 AND c = 3 No Missing leftmost column
WHERE a = 1 AND c = 3 Partial Only column a used; c is not contiguous

▶ Example: Create a Composite Index

SQL
CREATE INDEX idx_orders_customer_date
ON orders (customer_id, order_date DESC);

Output:

TEXT
CREATE TABLE

(4) CONCURRENTLY Online Index Build

Building an index takes an exclusive lock by default, blocking writes. CONCURRENTLY does not block writes, but builds more slowly.

Method Lock Blocks writes Speed In transaction?
CREATE INDEX Exclusive lock Blocks Fast Yes
CREATE INDEX CONCURRENTLY Shared lock Does not block Slow No

▶ Example: Build an Index Online

SQL
CREATE INDEX CONCURRENTLY idx_orders_region
ON orders (region);

Output:

TEXT
CREATE TABLE
⚠️ Note: CONCURRENTLY cannot be executed inside a transaction block.


8. Concept: EXPLAIN Execution Plan

(1) EXPLAIN Basics

Command Description Executes query?
EXPLAIN Show the execution plan No
EXPLAIN ANALYZE Execute and show actual timing Yes
EXPLAIN BUFFERS Show buffer hits Yes
EXPLAIN (FORMAT JSON) JSON-format output No

▶ Example: View the Execution Plan

SQL
EXPLAIN
SELECT * FROM orders WHERE customer_id = 1;
TEXT
                              QUERY PLAN
----------------------------------------------------------------------
 Index Scan using idx_orders_customer_id on orders  (cost=0.29..8.31 rows=1 width=72)
   Index Cond: (customer_id = 1)

▶ Example: EXPLAIN ANALYZE

SQL
EXPLAIN ANALYZE
SELECT * FROM orders WHERE customer_id = 1;
TEXT
                              QUERY PLAN
----------------------------------------------------------------------
 Index Scan using idx_orders_customer_id on orders
   (cost=0.29..8.31 rows=1 width=72) (actual time=0.015..0.016 rows=2 loops=1)
   Index Cond: (customer_id = 1)
 Planning Time: 0.085 ms
 Execution Time: 0.032 ms

(2) Key Scan Types

Scan type Meaning Uses index?
Seq Scan Full sequential table scan No
Index Scan Index scan (fetch back table rows) Yes
Index Only Scan Index-only scan (no table lookup) Yes (covering index)
Bitmap Scan Bitmap scan (large batches) Partial
Parallel Seq Scan Parallel full table scan No

▶ Example: Index Only Scan

SQL
EXPLAIN
SELECT customer_id FROM orders WHERE customer_id = 1;
TEXT
 Index Only Scan using idx_orders_customer_id on orders
   Index Cond: (customer_id = 1)

Only the index is read — no table lookup for the data rows — giving the best performance.


9. Index Maintenance and Failure

(1) REINDEX Rebuild

After many inserts/deletes over time, an index may bloat; REINDEX rebuilds it to reclaim space.

Method Description Lock
REINDEX INDEX idx Rebuild a single index Exclusive lock
REINDEX TABLE tbl Rebuild all indexes on a table Exclusive lock
REINDEX INDEX CONCURRENTLY idx Online rebuild (PG 12+) Does not block

▶ Example: Rebuild an Index

SQL
REINDEX INDEX idx_orders_customer_id;

REINDEX INDEX CONCURRENTLY idx_orders_customer_id;

Output:

TEXT
-- SQL statement executed successfully

(2) Common Index-Failure Scenarios

Scenario Example Fix
Function wrapping a column WHERE LOWER(col) = 'x' Expression index
Implicit type conversion WHERE varchar_col = 123 Use consistent types
Leading wildcard WHERE col LIKE '%abc' GIN / pg_trgm
OR condition WHERE a=1 OR b=2 Separate indexes or UNION
Stale statistics After a large data change ANALYZE
Violates leftmost prefix Composite index (a,b) queried by b Adjust index or query
Partial-index condition mismatch WHERE status='active' querying everything Drop WHERE or build a full index

▶ Example: Implicit Type Mismatch Causes Index Failure

SQL
SELECT * FROM users WHERE phone = 13800138000;

SELECT * FROM users WHERE phone = '13800138000';

Output:

TEXT
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

phone is VARCHAR. The implicit conversion in the first line causes the index to be unused; the second line hits the index.


10. Comprehensive Example

Bob's index optimization plan — JSONB product search + active-user partial index + composite index + BRIN time-series index:

SQL
CREATE INDEX CONCURRENTLY idx_products_attrs_gin
  ON products USING GIN (attrs);

CREATE INDEX CONCURRENTLY idx_users_active_email
  ON users (email, last_login_at)
  WHERE status = 'active';

CREATE INDEX CONCURRENTLY idx_orders_customer_date
  ON orders (customer_id, order_date DESC);

CREATE INDEX CONCURRENTLY idx_audit_log_created_brin
  ON audit_log USING BRIN (created_at)
  WITH (pages_per_range = 32);

EXPLAIN ANALYZE
SELECT p.product_id, p.name, p.attrs
FROM products p
WHERE p.attrs @> '{"category": "electronics", "in_stock": true}';

EXPLAIN ANALYZE
SELECT user_id, email
FROM users
WHERE status = 'active'
  AND email LIKE 'alice%'
ORDER BY last_login_at DESC
LIMIT 10;

11. Execution Flow

Index type selection and optimization flow:

100%
flowchart TD
    A[Found slow query] --> B[EXPLAIN ANALYZE]
    B --> C{Scan type?}
    C -->|Seq Scan| D{Has suitable index?}
    C -->|Index Scan| E[Index hit<br/>optimize query/index]
    D -->|No| F{Data type?}
    D -->|Yes but miss| G[Check index invalidation]
    F -->|Scalar/sort| H[Create B-Tree]
    F -->|JSONB/array| I[Create GIN]
    F -->|Geometric/range| J[Create GiST]
    F -->|Large table seq| K[Create BRIN]
    F -->|Subset only| L[Create Partial Index]
    F -->|Function/computed| M[Create Expression Index]
    H --> N[CONCURRENTLY deploy]
    I --> N
    J --> N
    K --> N
    L --> N
    M --> N

    style B fill:#e1f5fe
    style G fill:#ffccbc
    style N fill:#c8e6c9

❓ FAQ

Q Is "more indexes" always better?
A No. Every index adds write overhead (INSERT/UPDATE/DELETE must maintain the index) and storage space. Only build indexes for high-frequency queries, and periodically check usage with pg_stat_user_indexes.
Q What if building with CONCURRENTLY fails?
A A failed CONCURRENTLY build leaves an INVALID index. Find it with SELECT indexname FROM pg_indexes WHERE indexdef LIKE '%INVALID%' or \d+ tbl, then drop it with DROP INDEX.
Q Why doesn't my LIKE query use an index?
A LIKE 'abc%' can use B-Tree; LIKE '%abc' with a leading wildcard cannot — it needs GIN plus the pg_trgm extension.
Q What scenarios suit a BRIN index?
A Physically ordered large tables (such as append-only log tables written in time order). If the data has no relation to physical order, BRIN's approximate filtering works poorly and is not recommended.
Q Does a partial index affect write performance?
A Less than a full index. Rows that don't meet the WHERE condition don't need their partial index updated on write, saving I/O.
Q Can a composite index (a, b) support WHERE a=1 ORDER BY b?
A Yes. The composite index is sorted by a then b, so it satisfies both the WHERE filter and the ORDER BY, avoiding an extra sort.

📖 Summary


📝 Exercises

  1. ⭐ Create a B-Tree index on the customer_id column of the orders table, and verify with EXPLAIN that the query hits the index.
  2. ⭐ Create a GIN index on the JSONB column of the products table, and observe how the execution plan changes for a @> query.
  3. ⭐⭐ Create a partial index: index only the created_at column of orders where status = 'pending', and compare the size difference against a full index.
  4. ⭐⭐ Create an expression index on LOWER(email), verify a case-insensitive query hits the index; then create a composite index (region, created_at DESC) to support queries sorted by region and time.
  5. ⭐⭐⭐ For a log table with tens of millions of rows, design a combined plan of BRIN index + B-Tree composite index + partial index, and use EXPLAIN ANALYZE to compare query time before and after optimization.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏