AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 15 — Indexes and Schema Design: The Final Layer

15 — Indexes and Schema Design: The Final Layer

August 13, 20267 min read
Download as Markdown

"B-Tree is the only index, normalize everything" was my schema doctrine, and both halves were wrong. The framing that organized it: Postgres has six index types, each engineered for a different query shape, and schema design is the discipline of choosing structures — normalized for integrity, star for analytics, materialized views for precomputed aggregates — that fit the workload [1][2]. Once the index types stopped being interchangeable and the schema patterns mapped to workload shapes, "add an index" became a precise choice. These notes close the roadmap, and the last node is the one that keeps the whole ecosystem alive: contributing back.

Why indexes matter

Without an index, finding rows matching a condition means a sequential scan — reading every row in the table. With an index, Postgres looks up matching entries in a smaller, ordered structure and fetches only the relevant pages [1]. The tradeoff: indexes speed up reads but slow down writes (every insert/update maintains the index) and consume disk. The skill is indexing the columns that queries actually filter and join on, and nothing more.

The six index types

B-Tree (default) equality + range on sortable values WHERE x = ? · WHERE x BETWEEN ORDER BY x Hash equality only — no range, no sort WHERE x = ? compact, fast point lookups GIN arrays, JSONB, full-text search WHERE arr && ARRAY[...] to_tsvector @@ to_tsquery GiST geometric, range overlap WHERE range && range PostGIS spatial queries SP-GiST space-partitioned, non-balanced quadtree, k-d tree non-uniform distributions BRIN block-range summary huge, naturally-sorted tables tiny size, scan acceleration pick the index type whose match shape mirrors the query predicate plus specialized forms: partial, expression, composite
  • B-Tree — the default, and the right choice for most cases. Handles equality (=) and range (<, >, BETWEEN) on sortable values, and supports ORDER BY because entries are stored sorted [1].
  • Hash — equality only, no range or ordering. Useful for simple point lookups where the smaller structure helps; historically less used since B-Tree covers it.
  • GIN (Generalized Inverted Index) — the index for "one row maps to many values." Powers array containment, JSONB key/path lookups, and full-text search (tsvector) [3]. The right choice whenever the predicate tests membership in a multi-valued column.
  • GiST (Generalized Search Tree) — a framework for custom indexing, used for geometric data, range overlap (the exclusion constraint on time ranges uses GiST), and PostGIS spatial queries [4].
  • SP-GiST (Space-Partitioned GiST) — for non-balanced, space-partitioning structures like quadtrees and k-d trees; suits non-uniform spatial or hierarchical data [5].
  • BRIN (Block Range Index) — stores a small summary (min/max) per block of pages, tiny compared to a B-Tree. Ideal for huge tables whose data is naturally ordered by the indexed column (like a timestamp on an append-only events table), where it accelerates range scans at minimal cost [6].

Specialized index forms

On top of the types, three forms shape which rows an index covers:

  • Partial index — CREATE INDEX ... WHERE active = true indexes only the matching rows. Smaller and faster when queries always include the predicate.
  • Expression index — CREATE INDEX ON users (lower(email)) indexes the result of an expression, speeding up WHERE lower(email) = ?.
  • Composite index — CREATE INDEX ON orders (user_id, created_at) indexes multiple columns together, useful for queries that filter or sort on both.

Schema design patterns

Indexing is one half of design; the other is the table structures themselves [2]:

  • Normalized — minimize redundancy via the normal forms; the default for transactional (OLTP) data, protecting integrity.
  • Denormalized — duplicate data to avoid joins; for read-heavy cases where the join cost dominates.
  • Star schema — a central fact table (measurable events) surrounded by dimension tables (the context), the standard for data warehousing [2].
  • Snowflake schema — a star with the dimension tables further normalized; saves space at the cost of more joins.
  • Materialized views — CREATE MATERIALIZED VIEW precomputes and stores a query result, refreshed on demand. The right tool when many queries read the same heavy aggregate.

The mapping: OLTP workloads → normalized + B-Tree indexes on filter/join columns. Analytics → star/snowflake + materialized views + BRIN on time-ordered fact tables + GIN for full-text search. The structure follows the workload.

The closing node: get involved

The roadmap's last section is the one that's easy to skip but matters most [7]. Postgres is open-source, developed by a community, and its quality depends on contributors. The on-ramps, roughly in order of effort:

  • Mailing lists — pgsql-general for usage, pgsql-hackers for core development, pgsql-novice for beginners. Reading the archives is itself a contribution to one's own understanding.
  • Bug reporting and testing — reporting real bugs with a reproducible case, and testing patches in the commitfests, materially improves quality.
  • Reviewing patches — the commitfest process runs on community review; even non-core contributors can review correctness, performance, and docs.
  • Writing patches — fixing bugs or adding features, following the coding standards, submitting via the mailing list.
  • Documentation and translations — improving the docs is high-leverage and welcomes subject-matter experts.
  • Support and advocacy — answering questions, giving talks, writing posts like these.

The reason this belongs in a learning-notes series: the deepest understanding comes from engaging with the project, not just consuming it. Reading pgsql-hackers discussions on, say, a new vacuum behavior, teaches the internals faster than any tutorial.

How I use this

The index-type-by-query-shape matrix is the daily habit. For equality and range on regular columns, B-Tree. For JSONB, arrays, and full-text, GIN. For range-overlap and exclusion constraints, GiST. For huge append-only tables ordered by time, BRIN. I add partial and expression indexes when the query pattern justifies them, and I drop indexes that aren't used — they cost writes. Schema-side, I normalize for transactional data, reach for star schemas and materialized views in analytics, and partition time-series tables. And, closing the loop the roadmap insists on, I read the mailing lists and contribute back where I can — because the database I rely on is built in the open by people who showed up. Mastery of Postgres ends where it began: with the community that builds it.

References

[1] PostgreSQL Global Development Group, "Index Types," 2024. [Online]. Available: https://www.postgresql.org/docs/current/indexes-types.html

[2] Timescale, "How to design your PostgreSQL database: two schema examples," 2024. [Online]. Available: https://www.timescale.com/learn/how-to-design-postgresql-database-two-schema-examples

[3] PostgreSQL Global Development Group, "GIN Introduction," 2024. [Online]. Available: https://www.postgresql.org/docs/current/gin-intro.html

[4] PostgreSQL Global Development Group, "GiST Indexes," 2024. [Online]. Available: https://www.postgresql.org/docs/current/gist.html

[5] Sling Academy, "PostgreSQL SP-GiST," 2024. [Online]. Available: https://www.slingacademy.com/article/postgresql-sp-gist-space-partitioned-generalized-search-tree/

[6] PostgreSQL Global Development Group, "BRIN Indexes," 2024. [Online]. Available: https://www.postgresql.org/docs/current/brin.html

[7] PostgreSQL Global Development Group, "Mailing Lists," 2024. [Online]. Available: https://www.postgresql.org/list/

Knowledge check · Question 1 of 5

Which index type is the right default for equality and range queries on a sortable column, and supports ORDER BY?

Comments

Leave a Comment

You must be signed in to comment

0 Comments

No comments yet. Be the first to comment!