1. Introduction
Every developer has encountered a query that works perfectly on a small dataset but slows to a crawl when the table grows to millions of rows. In most cases, the root cause is not a poorly written query — it is the absence of a well-placed index. Understanding SQL indexing, therefore, is one of the most impactful skills a developer can build.
To begin with, an index is a separate data structure that the database engine maintains alongside your table. Much like the index at the back of a textbook, it allows the engine to jump directly to the relevant pages instead of reading every single row. As a result, queries that would otherwise scan millions of records can return results in milliseconds.
However, indexes are not a magic solution. While they dramatically speed up read queries, they also add overhead to write operations and consume additional disk space. Consequently, knowing when to add an index — and which type to use — is just as important as knowing how to create one.
In this guide, we walk through every major index type, explain how each one works internally, show you exactly how to create and manage them, and — most importantly — teach you how to use EXPLAIN to verify that the database is actually using your indexes. By the end, you will have a complete, practical toolkit for SQL indexing.
2. How Indexes Work Internally
Before diving into the different types of indexes, it is important to first understand the problem they solve. When you run a query like SELECT * FROM employees WHERE email = ‘alice@example.com’, the database engine has two options. First, it can perform a full table scan — reading every single row from disk until it finds a match. Second, if an index exists on the email column, it can instead look up the value in the index structure and jump directly to the matching row.
The difference in performance is enormous. For a table with 10 million rows, a full table scan reads 10 million rows. A B-Tree index lookup, by contrast, traverses a tree of roughly log2(10,000,000) ≈ 23 levels and reads just a handful of pages. That is the fundamental power of indexing.
2.1 — The B-Tree: The Engine Behind Most Indexes
The B-Tree (Balanced Tree) is the data structure that underpins the majority of SQL indexes in PostgreSQL, MySQL, SQL Server, and Oracle. To understand why it is so effective, consider how it is structured.
At the top sits the root node, which contains key values and pointers to child nodes. Below the root are internal nodes that further divide the key space. Finally, at the bottom sit the leaf nodes, which contain the actual indexed values along with pointers (row IDs or physical addresses) to the corresponding rows in the table.
Because the tree is always balanced — meaning all leaf nodes are at the same depth — every search takes the same number of steps regardless of which value you are looking for. Furthermore, the leaf nodes are linked together in a doubly linked list, which makes range queries like BETWEEN or > and < extremely efficient: the engine finds the starting point and then simply walks the leaf chain.
| Operation | Without Index (Full Scan) | With B-Tree Index | Improvement |
| Point lookup (WHERE id = 5) | O(n) — reads all rows | O(log n) — tree traversal | ~23 steps for 10M rows |
| Range query (WHERE age BETWEEN) | O(n) — reads all rows | O(log n + k) — walk leaf chain | Reads only matching rows |
| ORDER BY on indexed column | Full sort required | Pre-sorted in index | No sort step needed |
| COUNT(*) on indexed column | Counts all rows | Uses index statistics | Near-instant in many DBs |
2.2 — Full Table Scan vs Index Scan: A Real Example
-- Table: employees with 5 million rows-- No index on email column yet-- This query triggers a FULL TABLE SCANSELECT emp_id, name, emailFROM employeesWHERE email = 'alice@example.com';-- Check the execution plan (PostgreSQL)EXPLAIN ANALYZESELECT emp_id, name, emailFROM employeesWHERE email = 'alice@example.com';-- Output (no index):-- Seq Scan on employees (cost=0.00..98421.00 rows=1 width=60)-- (actual time=1823.412..1823.414 rows=1)-- Execution Time: 1824.101 ms <- 1.8 seconds for one row!
| The Seq Scan in the EXPLAIN output is the key signal that no index is being used. Once you add an index, this will change to Index Scan or Index Only Scan. Always run EXPLAIN ANALYZE before and after adding an index to confirm it is being used. |
3. Creating Your First Index
Now that we understand why indexes matter, let us look at the syntax for creating them. The basic CREATE INDEX statement is straightforward and works across all major databases with minor variations.
-- Basic syntaxCREATE INDEX index_name ON table_name (column_name);-- Create an index on the email column of employeesCREATE INDEX idx_employees_emailON employees (email);-- After adding the index, run the same query againEXPLAIN ANALYZESELECT emp_id, name, emailFROM employeesWHERE email = 'alice@example.com';-- Output (with index):-- Index Scan using idx_employees_email on employees-- (cost=0.56..8.58 rows=1 width=60)-- (actual time=0.041..0.043 rows=1)-- Execution Time: 0.112 ms <- from 1824ms to 0.1ms!
OUTPUT: Performance improvement after adding an index
| Scenario | Execution Time | Plan Used | Rows Read |
| Without index | 1,824 ms | Seq Scan (full table scan) | 5,000,000 |
| With index | 0.112 ms | Index Scan | 1 |
3.1 — Dropping and Rebuilding Indexes
-- Drop an index (PostgreSQL / MySQL)DROP INDEX idx_employees_email;-- Drop an index (SQL Server)DROP INDEX idx_employees_email ON employees;-- Rebuild an index (PostgreSQL) — useful after heavy INSERT/UPDATE/DELETEREINDEX INDEX idx_employees_email;-- Rebuild all indexes on a table (PostgreSQL)REINDEX TABLE employees;-- Rebuild index online without locking the table (PostgreSQL 12+)REINDEX INDEX CONCURRENTLY idx_employees_email;
4. Types of SQL Indexes
Not all indexes are the same. In fact, different index types are designed for different data access patterns. Consequently, choosing the right type for each use case is just as important as deciding whether to add an index at all.
4.1 — Single-Column Index
The single-column index is the simplest and most commonly used type. As the name suggests, it indexes one column and is ideally suited for queries that filter, sort, or join on that specific column. For instance, if you frequently search employees by their department_id, a single-column index on department_id will significantly speed up those queries.
-- Single-column index on department_idCREATE INDEX idx_emp_deptON employees (department_id);-- This query will now use the indexSELECT name, salaryFROM employeesWHERE department_id = 10;-- This query will also use the index for sortingSELECT name, salaryFROM employeesORDER BY department_id;
4.2 — Composite Index (Multi-Column Index)
A composite index, on the other hand, spans two or more columns. It is particularly useful when queries filter on multiple columns together. However, there is one critical rule to understand: the leading column rule. A composite index on (last_name, first_name) can be used for queries that filter on last_name alone, or on both last_name AND first_name together. Nevertheless, it cannot be used for queries that filter on first_name alone.
-- Composite index on (last_name, first_name)CREATE INDEX idx_emp_nameON employees (last_name, first_name);-- Query 1: Uses the index (last_name is the leading column)SELECT * FROM employees WHERE last_name = 'Smith';-- Query 2: Also uses the index (both leading columns present)SELECT * FROM employeesWHERE last_name = 'Smith' AND first_name = 'John';-- Query 3: Does NOT use the index (skips the leading column)SELECT * FROM employees WHERE first_name = 'John';-- A full table scan is performed instead
| The leading column rule is one of the most common sources of confusion with composite indexes. Always place the most selective column first (the one with the most distinct values), or the column most frequently used in WHERE clauses, to maximise index reuse. |
4.3 — Unique Index
A unique index serves a dual purpose: it speeds up queries and simultaneously enforces data integrity by preventing duplicate values in the indexed column. In practice, when you declare a PRIMARY KEY or a UNIQUE constraint on a column, the database automatically creates a unique index behind the scenes. Additionally, you can create one explicitly for any column that must contain unique values.
-- Explicit unique indexCREATE UNIQUE INDEX idx_emp_email_uniqueON employees (email);-- This will succeedINSERT INTO employees (name, email) VALUES ('Alice', 'alice@co.com');-- This will FAIL with a duplicate key errorINSERT INTO employees (name, email) VALUES ('Bob', 'alice@co.com');-- ERROR: duplicate key value violates unique constraint-- Unique index on multiple columns (combination must be unique)CREATE UNIQUE INDEX idx_emp_dept_badgeON employees (department_id, badge_number);
4.4 — Partial Index (Filtered Index)
A partial index is a highly efficient specialisation that indexes only the rows satisfying a specified condition. This is especially valuable when you frequently query a small, well-defined subset of a large table. For example, if your orders table has 50 million rows but you only ever query the 100,000 active orders, indexing all 50 million rows wastes space and slows down writes. Instead, a partial index on active orders is much smaller and faster.
-- Instead of indexing all 50M orders, index only active onesCREATE INDEX idx_orders_activeON orders (customer_id)WHERE status = 'active';-- This query uses the partial index (matches the WHERE condition)SELECT order_id, totalFROM ordersWHERE status = 'active'AND customer_id = 12345;-- This query does NOT use the partial index (status != 'active')SELECT order_id, totalFROM ordersWHERE status = 'completed'AND customer_id = 12345;-- Falls back to a full table scan or a different index
| Partial indexes are supported in PostgreSQL and SQL Server (as Filtered Indexes). MySQL does not support them natively. They are most effective when the filtered subset is 1–20% of the total table — small enough that the index is significantly smaller than a full-column index. |
4.5 — Full-Text Index
Standard B-Tree indexes are not suitable for searching within long text fields. If you need to search for a word or phrase inside a column like article_body or product_description, a full-text index is the right tool. Unlike a B-Tree index, a full-text index tokenises the text into individual words and builds an inverted index — a structure that maps each word to the rows containing it.
-- PostgreSQL: Full-text index using GINCREATE INDEX idx_articles_ftsON articles USING GIN (to_tsvector('english', body));-- Query using full-text searchSELECT title, bodyFROM articlesWHERE to_tsvector('english', body) @@ to_tsquery('database & indexing');-- MySQL: Full-text indexCREATE FULLTEXT INDEX idx_products_ftsON products (product_name, description);-- MySQL full-text search querySELECT product_nameFROM productsWHERE MATCH(product_name, description) AGAINST ('wireless headphones');
4.6 — Covering Index (Index-Only Scan)
A covering index is not a distinct index type but rather a design strategy where the index contains all the columns a query needs. As a result, the database engine can answer the entire query from the index alone, without ever touching the main table. This technique, known as an Index-Only Scan, is one of the most powerful query optimisations available.
-- Query: find name and salary for all employees in dept 10SELECT name, salaryFROM employeesWHERE department_id = 10;-- Standard index: only covers the filter column-- Engine must read index, then fetch name+salary from table (extra I/O)CREATE INDEX idx_emp_dept_standardON employees (department_id);-- Covering index: includes all columns the query needs-- Engine answers entirely from the index -- no table access neededCREATE INDEX idx_emp_dept_coveringON employees (department_id)INCLUDE (name, salary); -- PostgreSQL / SQL Server syntax-- MySQL equivalent (list all columns in the index)CREATE INDEX idx_emp_dept_coveringON employees (department_id, name, salary);-- EXPLAIN now shows: Index Only Scan (fastest possible read)EXPLAIN SELECT name, salary FROM employees WHERE department_id = 10;-- Index Only Scan using idx_emp_dept_covering
| Index Type | Table Access? | When to Use | Performance |
| Standard Index | Yes — after index lookup | General filtering and sorting | Fast |
| Covering Index | No — index only | Queries that select a small fixed set of columns | Fastest |
4.7 — Clustered vs Non-Clustered Index
There is an important distinction between clustered and non-clustered indexes that every developer must understand. A clustered index determines the physical order of rows on disk — the table itself is sorted according to the index key. Consequently, each table can have only one clustered index. In SQL Server and MySQL InnoDB, the PRIMARY KEY is the clustered index by default.
A non-clustered index, by contrast, is a separate structure that contains the indexed key values and pointers back to the actual rows. Because it does not affect the physical row order, a table can have many non-clustered indexes. However, reading data through a non-clustered index requires an extra lookup — first into the index, then back to the table row.
-- SQL Server: explicit clustered indexCREATE CLUSTERED INDEX idx_emp_id_clusteredON employees (emp_id);-- SQL Server: non-clustered indexCREATE NONCLUSTERED INDEX idx_emp_email_ncON employees (email);-- PostgreSQL: all indexes are non-clustered by default-- Use CLUSTER command to physically reorder the table onceCLUSTER employees USING idx_emp_dept;-- Note: CLUSTER is a one-time operation -- new inserts are unordered
| Feature | Clustered Index | Non-Clustered Index |
| Physical row order | Determines disk order of rows | Does not affect disk order |
| Count per table | Only ONE per table | Many per table (up to 999 in SQL Server) |
| Range query performance | Excellent (rows are physically adjacent) | Good (pointer chasing required) |
| Default creation | PRIMARY KEY (SQL Server/MySQL InnoDB) | All other CREATE INDEX statements |
| Extra lookup required? | No — data IS the index | Yes — index points back to row |
5. When to Use (and Avoid) Indexes
Although indexes dramatically improve read performance, they are not free. In fact, every index you add comes with real costs that must be carefully weighed. Understanding both when to add indexes and when to hold back is, therefore, essential for maintaining a well-performing database.
5.1 — When Indexes Help
| Scenario | Why an Index Helps | Example |
| Columns in WHERE clauses | Avoids full table scan on filter | WHERE email = ‘alice@example.com’ |
| Columns in JOIN conditions | Speeds up row matching between tables | ON e.dept_id = d.dept_id |
| Columns in ORDER BY | Data already sorted in index | ORDER BY last_name ASC |
| Columns in GROUP BY | Groups can be formed without a sort step | GROUP BY department_id |
| High-cardinality columns | Many distinct values = precise lookups | email, SSN, order_id |
| Foreign key columns | Speeds up JOIN and referential checks | dept_id in employees table |
5.2 — When to Avoid Indexes
| Scenario | Why an Index Hurts or Is Useless | Better Approach |
| Small tables (< 1,000 rows) | Full scan is faster than index overhead | No index needed |
| Low-cardinality columns | Few distinct values = large index with little benefit | Consider partial index or skip |
| Columns rarely used in WHERE/JOIN | Index never consulted — pure overhead on writes | Remove unused indexes |
| Heavy write workloads (INSERT/UPDATE/DELETE) | Every write must also update the index | Fewer indexes; rebuild periodically |
| Columns with many NULLs (non-filtered) | NULLs usually excluded from index lookups | Use partial index WHERE col IS NOT NULL |
| Frequently updated indexed columns | Updates trigger index page splits | Evaluate if read benefit outweighs write cost |
| A general rule of thumb: tables with fewer than 1,000 rows rarely benefit from indexes — the query planner will often prefer a full table scan anyway. Additionally, low-cardinality columns like status (Active/Inactive) or gender provide little selectivity and are therefore poor index candidates on their own. |
6. Using EXPLAIN to Verify Index Usage
Creating an index does not guarantee it will be used. The query planner makes its own decision based on table statistics, data distribution, and estimated costs. Therefore, it is essential to use EXPLAIN (or EXPLAIN ANALYZE) to verify that your indexes are actually being consulted.
6.1 — Reading EXPLAIN Output
-- PostgreSQL: EXPLAIN ANALYZE gives actual runtime statsEXPLAIN ANALYZESELECT name, salaryFROM employeesWHERE department_id = 10AND salary > 80000;-- Sample output:-- Index Scan using idx_emp_dept on employees-- (cost=0.56..312.45 rows=87 width=34)-- (actual time=0.043..1.214 rows=87 loops=1)-- Index Cond: (department_id = 10)-- Filter: (salary > 80000)-- Rows Removed by Filter: 143-- Planning Time: 0.312 ms-- Execution Time: 1.389 ms
OUTPUT: Key signals in EXPLAIN output and what they mean
| EXPLAIN Term | Meaning | Good or Bad? |
| Seq Scan | Full table scan — no index used | Bad for large tables |
| Index Scan | Index used; then fetches row from table | Good |
| Index Only Scan | Query answered entirely from index — no table access | Best |
| Bitmap Index Scan | Multiple index scans combined (used for OR conditions) | Good |
| cost=X..Y | X = startup cost, Y = total estimated cost (lower is better) | Compare before/after |
| rows=N | Estimated number of rows returned | Should be close to actual |
| actual time=X..Y | Real measured time in milliseconds | Lower is better |
| Rows Removed by Filter | Rows read but discarded after index lookup | High value suggests better index needed |
6.2 — Why the Planner Might Ignore Your Index
-- Case 1: Function on indexed column breaks index usage-- BAD: UPPER() prevents index use on emailSELECT * FROM employees WHERE UPPER(email) = 'ALICE@EXAMPLE.COM';-- Seq Scan — index on email is NOT used-- GOOD: Use a function-based index insteadCREATE INDEX idx_emp_email_upper ON employees (UPPER(email));SELECT * FROM employees WHERE UPPER(email) = 'ALICE@EXAMPLE.COM';-- Index Scan on idx_emp_email_upper — index IS used-- Case 2: Leading wildcard prevents B-Tree index use-- BAD: Leading % means the engine cannot use the B-TreeSELECT * FROM employees WHERE name LIKE '%Smith';-- Seq Scan — cannot use a standard B-Tree index-- GOOD: Trailing wildcard CAN use a B-Tree indexSELECT * FROM employees WHERE name LIKE 'Sm%';-- Index Scan — engine uses B-Tree index on name-- Case 3: Implicit type conversion prevents index use-- BAD: emp_id is INT but compared to a stringSELECT * FROM employees WHERE emp_id = '12345';-- Seq Scan in some databases due to implicit cast-- GOOD: Match the data type exactlySELECT * FROM employees WHERE emp_id = 12345;-- Index Scan
| These three patterns — functions on indexed columns, leading wildcards in LIKE, and implicit type mismatches — are the most common reasons a carefully created index is silently ignored by the query planner. Always check EXPLAIN after adding an index to catch these issues early. |
7. Composite Index Design Best Practices
Composite indexes are among the most powerful tools in SQL performance tuning. Nevertheless, they are also among the most misunderstood. Following a few key design principles will help you get the maximum benefit from every composite index you create.
7.1 — Column Order Matters: Most Selective First
-- Query pattern: filter on status + sort by created_atSELECT order_id, total, created_atFROM ordersWHERE status = 'pending'ORDER BY created_at DESC;-- Option A: status first (more selective if few 'pending' rows)CREATE INDEX idx_orders_status_dateON orders (status, created_at DESC);-- Option B: created_at first (better for range-heavy queries)CREATE INDEX idx_orders_date_statusON orders (created_at DESC, status);-- Option A is preferred here because filtering by status first-- dramatically reduces the rows before the sort step.-- In contrast, Option B would index by date first, offering-- less benefit when filtering for a specific status.
7.2 — Equality Columns Before Range Columns
A critical rule for composite index design is to always place equality filter columns before range filter columns. The reason is that once a range condition is applied (such as >, <, or BETWEEN), the index cannot be used to filter subsequent columns — the engine must scan all matching range rows for the remaining conditions.
-- Query: filter by department (equality) AND salary rangeSELECT name, salaryFROM employeesWHERE department_id = 10 -- equality conditionAND salary BETWEEN 60000 AND 90000; -- range condition-- GOOD: equality column (department_id) firstCREATE INDEX idx_emp_dept_salaryON employees (department_id, salary);-- Engine uses department_id to narrow down, then scans salary range-- BAD: range column (salary) firstCREATE INDEX idx_emp_salary_deptON employees (salary, department_id);-- Engine must scan the entire salary range, then filter by department-- Much less efficient for this query pattern
7.3 — Include Frequently Selected Columns (Covering Index)
-- Frequently run report querySELECT emp_id, name, hire_dateFROM employeesWHERE department_id = 10AND status = 'active';-- Without covering index: Index Scan + table fetch for name, hire_dateCREATE INDEX idx_emp_dept_statusON employees (department_id, status);-- With covering index: Index Only Scan -- no table access at allCREATE INDEX idx_emp_dept_status_coveringON employees (department_id, status)INCLUDE (emp_id, name, hire_date); -- PostgreSQL / SQL ServerEXPLAIN SELECT emp_id, name, hire_dateFROM employees WHERE department_id = 10 AND status = 'active';-- Result: Index Only Scan using idx_emp_dept_status_covering
8. Index Maintenance and Monitoring
Creating an index is not a one-time task. Over time, as rows are inserted, updated, and deleted, indexes become fragmented and their statistics grow stale. Consequently, regular index maintenance is essential to sustain the performance gains they provide.
8.1 — Finding Unused Indexes
-- PostgreSQL: find indexes that have never been usedSELECT schemaname, tablename, indexname, idx_scan AS times_used, pg_size_pretty(pg_relation_size(indexrelid)) AS index_sizeFROM pg_stat_user_indexesWHERE idx_scan = 0AND schemaname = 'public'ORDER BY pg_relation_size(indexrelid) DESC;-- SQL Server: find unused indexesSELECT OBJECT_NAME(i.object_id) AS table_name, i.name AS index_name, ius.user_seeks + ius.user_scans AS total_reads, ius.user_updates AS total_writesFROM sys.indexes iLEFT JOIN sys.dm_db_index_usage_stats ius ON i.object_id = ius.object_id AND i.index_id = ius.index_idWHERE ius.user_seeks + ius.user_scans = 0OR ius.user_seeks IS NULL;
| Unused indexes are a hidden performance tax. They consume disk space and, more importantly, every INSERT, UPDATE, and DELETE must still update them — even though no queries benefit. Therefore, periodically identifying and dropping unused indexes is just as valuable as adding new ones. |
8.2 — Detecting Index Fragmentation
-- PostgreSQL: check index bloat using pgstattuple extensionCREATE EXTENSION IF NOT EXISTS pgstattuple;SELECT indexname, pg_size_pretty(pg_relation_size(indexrelid)) AS index_size, (pgstattuple(indexrelid)).dead_tuple_percent AS dead_pctFROM pg_stat_user_indexesWHERE schemaname = public'ORDER BY dead_pct DESC;-- If dead_pct > 20%, the index should be rebuilt-- Rebuild the fragmented index (PostgreSQL)REINDEX INDEX CONCURRENTLY idx_employees_email;-- SQL Server: check fragmentationSELECT index_id, avg_fragmentation_in_percentFROM sys.dm_db_index_physical_stats( DB_ID(), OBJECT_ID(employees'), NULL, NULL, SAMPLED');-- < 10% fragmentation: no action needed-- 10-30% fragmentation: ALTER INDEX ... REORGANIZE-- > 30% fragmentation: ALTER INDEX ... REBUILD
8.3 — Checking Index Size
-- PostgreSQL: size of each index on a tableSELECT indexname, pg_size_pretty(pg_relation_size(indexrelid)) AS sizeFROM pg_stat_user_indexesWHERE tablename = 'employees'ORDER BY pg_relation_size(indexrelid) DESC;-- MySQL: index statisticsSHOW INDEX FROM employees;-- MySQL: table and index sizesSELECT table_name, ROUND(data_length / 1024 / 1024, 2) AS data_mb, ROUND(index_length / 1024 / 1024, 2) AS index_mbFROM information_schema.tablesWHERE table_schema = 'your_database'AND table_name = 'employees';
9. Common Indexing Mistakes and How to Avoid Them
Even experienced developers make indexing mistakes that silently degrade performance. The following table summarises the most common pitfalls along with the correct approach, so that you can avoid them in your own work.
| Mistake | What Goes Wrong | Correct Approach |
| Indexing every column | Writes become very slow; disk space wasted | Index only columns used in WHERE, JOIN, ORDER BY, GROUP BY |
| Function on indexed column in WHERE | UPPER(email) bypasses the index entirely | Create a function-based index: CREATE INDEX ON (UPPER(email)) |
| Leading wildcard in LIKE | LIKE ‘%Smith’ cannot use B-Tree index | Use trailing wildcard ‘Sm%’ or switch to Full-Text index |
| Wrong column order in composite index | Leading column not used in query = index skipped | Put the most-used equality filter column first |
| Ignoring NULL values in index columns | NULLs excluded from standard indexes; skewed results | Use partial index WHERE col IS NOT NULL or handle NULLs in query |
| Never checking EXPLAIN | Index created but not actually used by the planner | Always run EXPLAIN ANALYZE before and after creating an index |
| Never dropping unused indexes | Write performance degrades; disk bloat grows | Monitor pg_stat_user_indexes; drop indexes with idx_scan = 0 |
| Implicit type mismatch | WHERE emp_id = ‘123’ may bypass index on INT column | Match the data type exactly in the WHERE clause |
10. Quick Reference — Index Cheat Sheet
| Index Type | Keyword | Best For | Supported In |
| Single-Column | CREATE INDEX ON (col) | Single-column filters, sorts, joins | All databases |
| Composite | CREATE INDEX ON (col1, col2) | Multi-column filters (leading column rule applies) | All databases |
| Unique | CREATE UNIQUE INDEX | Enforce uniqueness + speed up lookups | All databases |
| Partial | CREATE INDEX … WHERE condition | Frequently queried subsets of a large table | PostgreSQL, SQL Server |
| Full-Text | CREATE FULLTEXT INDEX / GIN | Word/phrase search inside text columns | MySQL, PostgreSQL, SQL Server |
| Covering | … INCLUDE (col1, col2) | Avoid table access for fixed SELECT column sets | PostgreSQL 11+, SQL Server |
| Clustered | CREATE CLUSTERED INDEX | Primary key; range scans on the clustered column | SQL Server, MySQL InnoDB |
| Function-Based | CREATE INDEX ON (UPPER(col)) | Queries with functions applied to indexed column | PostgreSQL, Oracle, MySQL 8+ |
| Action | Command (PostgreSQL) | When to Use |
| Create index | CREATE INDEX idx_name ON table (col); | Column used frequently in WHERE/JOIN/ORDER BY |
| Create unique index | CREATE UNIQUE INDEX idx ON table (col); | Column must contain unique values |
| Create covering index | CREATE INDEX idx ON table (col) INCLUDE (a, b); | Query selects only a fixed small set of columns |
| Drop index | DROP INDEX idx_name; | Index is unused or causing write overhead |
| Rebuild index | REINDEX INDEX CONCURRENTLY idx_name; | Fragmentation > 20-30% of index pages |
| Check usage stats | SELECT * FROM pg_stat_user_indexes; | Periodically — identify unused indexes |
| Verify index use | EXPLAIN ANALYZE SELECT …; | After every new index creation |
11. Conclusion
SQL indexing is, without doubt, one of the highest-leverage skills in database engineering. A single well-placed index can transform a query that takes two minutes into one that completes in under a millisecond. Moreover, understanding the different index types means you can choose the right tool for every situation — rather than relying on trial and error.
To summarise the key principles: first, always identify which columns are used in WHERE, JOIN, ORDER BY, and GROUP BY clauses before adding any index. Second, use composite indexes thoughtfully, placing equality columns before range columns and the most selective column first. Third, leverage covering indexes for high-frequency reporting queries to eliminate table access entirely. Fourth and most importantly, always use EXPLAIN ANALYZE to confirm that the query planner is actually using your index — because creating an index and having it used are two very different things.
Finally, remember that index maintenance is an ongoing responsibility. Unused indexes silently degrade write performance. Fragmented indexes degrade read performance. Consequently, regularly monitoring index usage statistics and rebuilding or dropping indexes as data patterns evolve is just as valuable as the initial design.
With the knowledge in this guide — covering index types, creation syntax, the EXPLAIN tool, composite index design, and maintenance patterns — you now have everything you need to build a fast, well-indexed SQL database that performs reliably at any scale.
Happy Querying!
Discover more from DataSangyan
Subscribe to get the latest posts sent to your email.