Working with SQL Indexes

1. Creating Indexes

SyntaxEffect
CREATE INDEX idx ON t(col);Default B-tree index
CREATE UNIQUE INDEXEnforces uniqueness
CREATE INDEX CONCURRENTLY (PG)Non-blocking build
USING GIN/GiST/BRIN/HashSpecialized index types
DROP INDEX [CONCURRENTLY]Remove index

2. Understanding Index Types

TypeBest ForNotes
B-treeEquality, range, ORDER BYDefault; O(log n) lookup
HashEquality onlyFaster than B-tree for =
GINArrays, JSONB, full-textInverted index
GiSTGeometric, ranges, full-textGeneralized search tree
BRINVery large, naturally orderedBlock-range, tiny size
SP-GiSTNon-balanced trees (quadtrees)Space-partitioned
Bitmap (Oracle)Low-cardinality columnsOLAP workloads
Columnstore (SQL Server)Analytics, compressionRead-heavy

3. Creating Composite Indexes

PrincipleDescription
Leftmost PrefixIndex (a,b,c) helps WHERE a / a,b / a,b,c — not just b
Most Selective FirstPut highest-cardinality column first
Equality Before Range(status, created_at) better than (created_at, status) for status=x AND created_at>y
Sort OrderMatch ORDER BY direction to avoid sort step

Example: Composite Index

CREATE INDEX idx_orders_cust_date
  ON orders(customer_id, placed_at DESC)
  INCLUDE (total);  -- covering column

4. Using Covering Indexes

FeatureBehavior
Covering IndexAll query columns in index → no heap lookup
INCLUDE clauseAdd non-key columns to leaf pages
Index-only scanEXPLAIN shows "Index Only Scan"
CostLarger index, slower updates

5. Creating Partial Indexes

UseExample
Skip nullsCREATE INDEX ... WHERE col IS NOT NULL
Active rows onlyWHERE deleted_at IS NULL
Rare valuesWHERE status = 'pending'
BenefitSmaller index, faster scans, lower write overhead

Example: Partial Unique Index

-- Unique only for active users
CREATE UNIQUE INDEX uq_user_email_active
  ON users(email) WHERE deleted_at IS NULL;

6. Implementing Full-Text Indexes

DBMechanism
Postgrestsvector + GIN; to_tsquery
MySQLFULLTEXT INDEX; MATCH ... AGAINST
SQL ServerFull-Text Search service + CONTAINS
Beyond DBElasticsearch, OpenSearch, Meilisearch for advanced

Example: Postgres FTS

ALTER TABLE articles ADD COLUMN tsv tsvector
  GENERATED ALWAYS AS (to_tsvector('english', title || ' ' || body)) STORED;
CREATE INDEX idx_articles_fts ON articles USING GIN(tsv);
SELECT id FROM articles WHERE tsv @@ websearch_to_tsquery('english','database design');

7. Analyzing Index Usage

ToolQuery/Command
Postgres usage statsSELECT * FROM pg_stat_user_indexes;
Postgres unusedWHERE idx_scan = 0
MySQL usageSELECT * FROM sys.schema_unused_indexes;
Plan inspectionEXPLAIN (ANALYZE, BUFFERS) ...
Missing indexSQL Server DMV sys.dm_db_missing_index_*

8. Dropping Unused Indexes

StepAction
Identifyidx_scan = 0 over weeks of representative traffic
Confirm not used by unique constraintCheck pg_constraint / information_schema
Drop concurrentlyDROP INDEX CONCURRENTLY idx_name;
Monitor regressionsWatch slow query log after drop

9. Managing Index Maintenance

OperationWhy
REINDEX (Postgres)Rebuild bloated indexes
ANALYZEUpdate planner statistics
VACUUMReclaim dead tuple space
REORGANIZE / REBUILD (SQL Server)Defrag indexes
Optimize TABLE (MySQL)Rebuild + reclaim space

10. Balancing Index Overhead vs Query Performance

Trade-offEffect
More indexesFaster reads, slower INSERT/UPDATE/DELETE, more disk
Wider indexesBetter covering, larger leaf pages
Partial indexesSmaller, less write overhead
Index-only scansBest read perf, demand vacuum for visibility map
Note: Every index roughly doubles write cost on its column. Audit indexes quarterly.