PostgreSQL Schema and Indexing Fundamentals
Model constrained relational data, choose indexes from query shapes, read execution plans, and compare important Oracle differences.
Learning objectives
- Use database constraints to protect business invariants
- Match composite indexes to filters, ordering, selectivity, and real execution plans
What it is
Schema design defines valid durable states; indexing defines additional data structures the optimizer may use to reach those states efficiently. Constraints are correctness controls. Indexes are workload-dependent performance and access-path tools.
Why it matters
Application validation can be bypassed by races, scripts, imports, or another service. Database constraints close that gap. Poor indexes can waste storage and write throughput while still missing the actual filter and ordering pattern.
How it works
| Constraint/index | Primary job |
|---|---|
| PRIMARY KEY | Stable row identity and uniqueness |
| FOREIGN KEY | Referenced-row integrity |
| UNIQUE | Business uniqueness under concurrency |
| CHECK | Row-local valid-state rule |
| NOT NULL | Required value |
| B-tree index | Equality, ranges, and ordered access |
| Partial index | Index only rows matching a predicate |
| Expression index | Index a deterministic expression used by queries |
For a multicolumn B-tree, leading equality constraints and then the first range constraint usually determine how much of the index can bound the scan. PostgreSQL can apply later-column conditions, but they may not reduce the scanned portion in the same way.
Simple example
CREATE TABLE task (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
owner_id bigint NOT NULL REFERENCES app_user(id),
title varchar(160) NOT NULL CHECK (length(trim(title)) > 0),
status varchar(20) NOT NULL CHECK (status IN ('OPEN', 'DONE', 'CANCELLED')),
due_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX task_owner_status_created_idx
ON task (owner_id, status, created_at DESC, id DESC);
Backend example
SELECT id, owner_id, title, status, created_at
FROM task
WHERE owner_id = :ownerId
AND status = :status
AND (created_at, id) < (:cursorCreatedAt, :cursorId)
ORDER BY created_at DESC, id DESC
LIMIT :pageSize;
The index begins with the equality predicates, then matches the descending pagination order and unique id tie-breaker. Verify with representative data and EXPLAIN (ANALYZE, BUFFERS) in a safe non-production environment; ANALYZE executes the statement.
Production example
- 1
Start from queries
Collect the exact predicates, sort order, selected columns, frequency, and latency objective.
- 2
Inspect estimates
Compare estimated and actual rows, scan type, loops, buffers, sorts, and join strategy.
- 3
Change one thing
Add or revise a constraint/index/query and re-measure with representative distribution.
- 4
Account for writes
Every index consumes space and must be updated by INSERT, UPDATE, DELETE, vacuum, backup, and replication work.
| PostgreSQL | Oracle note |
|---|---|
| GENERATED ... AS IDENTITY | Identity is also available; sequences remain common |
| LIMIT | Use FETCH FIRST / OFFSET in modern Oracle |
| timestamptz | Oracle timestamp-with-time-zone semantics differ; test driver mappings |
| EXPLAIN (ANALYZE, BUFFERS) | Use DBMS_XPLAN and execution-plan tooling |
| Partial indexes | No direct identical feature; function-based/conditional strategies differ |
Common mistakes
- Relying only on application validation for uniqueness
- Adding an index for every column
- Using SELECT * in stable mapper contracts
- Assuming an index will always be chosen
- Running EXPLAIN ANALYZE for a write on production without a rollback plan
- Using offset pagination for unbounded deep pages
Best practices
- Name constraints and indexes clearly
- Prefer narrow stable keys and explicit foreign-key actions
- Keep statistics current and inspect actual row-count errors
- Use representative data volume and distribution
- Design online/backward-compatible migrations
- Test backup restore, not only backup creation
Trade-offs
| Choice | Benefit | Cost |
|---|---|---|
| Normalize | Stronger consistency and less update duplication | More joins and model navigation |
| Denormalize | Simpler/faster targeted reads | Synchronization and stale-data risk |
| Covering INCLUDE index | Potential index-only reads | Larger index and more write work |
| Soft delete | Recovery/audit convenience | Every uniqueness and query rule becomes more complex |
Interview questions
- Why does a UNIQUE constraint solve a race that a pre-check cannot?
- How does column order affect a composite B-tree index?
- What evidence do you read in EXPLAIN ANALYZE?
- When would a sequential scan be correct?
- How does keyset pagination work with duplicate timestamps?
Hands-on task
- 1
Model
Create users, roles, tasks, orders, and order items with named constraints and audit columns.
- 2
Load
Generate data with realistic skew across owners and statuses.
- 3
Measure
Compare no index, a poorly ordered index, and a query-aligned composite index using plans and timings.
- 4
Race
Run two concurrent inserts for the same business key and confirm the constraint is the final arbiter.
References
- PostgreSQL Global Development Group: PostgreSQL Constraints — https://www.postgresql.org/docs/18/ddl-constraints.html
- PostgreSQL Global Development Group: PostgreSQL Multicolumn Indexes — https://www.postgresql.org/docs/18/indexes-multicolumn.html
- PostgreSQL Global Development Group: Using EXPLAIN — https://www.postgresql.org/docs/18/using-explain.html
- PostgreSQL Global Development Group: Transaction Isolation — https://www.postgresql.org/docs/18/transaction-iso.html
- Oracle: Oracle Database SQL Language Reference 19c — https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/
Retrieval check
Before you continue
- Explain the main idea in your own words without rereading the page.
- Name one production problem this knowledge helps prevent.
- Describe where you would apply it in a Spring Boot project.
Source metadata
Primary references checked on 2026-07-16. Version scope is stated at the top of this page.
PostgreSQL Constraints (opens in a new tab)
PostgreSQL Global Development Group · official-docs
PostgreSQL Multicolumn Indexes (opens in a new tab)
PostgreSQL Global Development Group · official-docs
Using EXPLAIN (opens in a new tab)
PostgreSQL Global Development Group · official-docs
Oracle Database SQL Language Reference 19c (opens in a new tab)
Oracle · official-docs