How POSTGRES indexing is more efficient than MYSQL

Published: (January 20, 2026 at 04:46 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

Problem setup (How Index Scan works internally for both DBs)

Table definition (MySQL & PostgreSQL)

CREATE TABLE "user" (
  id   BIGINT PRIMARY KEY,
  name VARCHAR(100),
  age  INT
);

Index

CREATE INDEX idx_user_name ON "user"(name);

Query

SELECT age FROM "user" WHERE name = 'Rahim';
  • Table size: 10 million rows
  • Index type: B‑Tree
  • name is not unique
  • age is not part of the index

MySQL execution

Important InnoDB rule

Secondary indexes store the PRIMARY KEY, not the physical row location.
idx_user_name entries look like: (name, primary_key_id).

Step‑by‑step

  1. Traverse secondary index (name)
    MySQL searches the B‑Tree for 'Rahim'.
    Cost: O(log N)

    Example leaf entry: ('Rahim', id=73482)

  2. Traverse PRIMARY KEY index (clustered index)
    In InnoDB the primary key index is the table; rows are stored in PK order.
    MySQL uses id = 73482 to search the PK B‑Tree.
    Cost: O(log N)

    Leaf entry returns the full row: (id=73482, name='Rahim', age=29)

Summary

  • ✅ Clustered index gives good locality
  • ❌ Requires two B‑Tree traversals
  • ❌ PK lookup cost grows with table size

PostgreSQL execution

Important PostgreSQL rule

Indexes store a TID (tuple ID) – a pointer to the heap location.
idx_user_name entries look like: (name, (block_id, offset)).

Step‑by‑step

  1. Traverse B‑Tree index (name)
    PostgreSQL searches the index for 'Rahim'.
    Cost: O(log N)

    Leaf entry: ('Rahim', TID=(block=102345, offset=7))

  2. Heap fetch (direct pointer)
    PostgreSQL goes directly to heap page 102345 and reads the row at offset 7.
    Cost: O(1) (conceptually)

    Row returned: (id=73482, name='Rahim', age=29)

Summary

  • ✅ Only one B‑Tree traversal
  • ✅ Heap fetch is constant‑time
  • ❌ Heap pages may be scattered (less locality)
  • ❌ Additional visibility checks due to MVCC

Performance considerations (Big‑O is not the full story)

When MySQL can be faster

  • Primary key is small
  • Data fits in the buffer pool
  • Rows are accessed sequentially
  • Heavy read workloads (OLTP)

➡️ Clustered index provides better locality, making MySQL win in these cases.

When PostgreSQL can be faster

  • Very large tables
  • Many secondary indexes
  • Random access patterns
  • Index‑only scans are possible

➡️ Pointer‑based access and constant‑time heap fetch give PostgreSQL an advantage.

Index‑only scan example

If you change the index to include age:

CREATE INDEX idx_user_name_age ON "user"(name, age);

Running the same query:

SELECT age FROM "user" WHERE name = 'Rahim';
  • PostgreSQL can return the result without touching the heap (index‑only scan).
  • MySQL cannot perform a true index‑only scan in the same way because its secondary indexes do not contain the needed column.

Comparative summary

AspectMySQLPostgreSQL
Index lookup costO(log N) + O(log N) (secondary + PK)O(log N) + O(1) (secondary + direct heap)
Index‑only scanNot supported nativelySupported when all needed columns are in the index
Small / medium datasetsOften feels fasterComparable
Large datasets & many indexesScaling can degradeScales better
Analytics / complex queriesLess optimalGenerally superior
Simple OLTP workloadsExcellentGood, but may have extra overhead

Conclusion

  • MySQL follows the “find index → find PK → find row” path, which is efficient for small to medium, sequential workloads.
  • PostgreSQL’s pointer‑based approach (O(log N) + O(1)) provides better scalability for large, random‑access workloads and enables true index‑only scans.
Back to Blog

Related posts

Read more »

[인터뷰] 토스인슈어런스는 왜 디지털 온보딩 시스템을 개발했나

법인보험대리점GA 토스인슈어런스는 신규 설계사가 업무를 시작하기 위한 초기 준비 절차온보딩를 전면 전산화한 ‘디지털 온보딩 시스템’을 구축했다. 복잡하게 흩어져 있던 필수 과정을 하나의 흐름으로 통합해 설계사의 정착 속도와 사용자 경험을 동시에 개선했다는 평가다. 토스인슈어런스의 디지털...