<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Composite Indexes: Theory to Practice]]></title><description><![CDATA[Composite Indexes: Theory to Practice]]></description><link>https://guidetocompositeindex.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 09 Sep 2026 16:01:40 GMT</lastBuildDate><atom:link href="https://guidetocompositeindex.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Complete Guide to Composite Indexes: From Theory to Practice]]></title><description><![CDATA[As developers, we often focus on writing clean code and building features, but database performance optimization is equally crucial. Today, I'll share my deep dive into composite indexes - a powerful technique that can transform your query performanc...]]></description><link>https://guidetocompositeindex.hashnode.dev/complete-guide-to-composite-indexes-from-theory-to-practice</link><guid isPermaLink="true">https://guidetocompositeindex.hashnode.dev/complete-guide-to-composite-indexes-from-theory-to-practice</guid><category><![CDATA[Databases]]></category><category><![CDATA[OptimizationStrategies]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[SQL]]></category><category><![CDATA[database-indexes]]></category><category><![CDATA[Composite Index]]></category><dc:creator><![CDATA[Khan Mohammed ahmed]]></dc:creator><pubDate>Thu, 26 Jun 2025 13:23:31 GMT</pubDate><content:encoded><![CDATA[<p>As developers, we often focus on writing clean code and building features, but database performance optimization is equally crucial. Today, I'll share my deep dive into <strong>composite indexes</strong> - a powerful technique that can transform your query performance from seconds to milliseconds.</p>
<h2 id="heading-what-are-composite-indexes">What Are Composite Indexes?</h2>
<p>A <strong>composite index</strong> (also called a multi-column or compound index) is a database index that spans multiple columns, treating them as a single unit for optimization.</p>
<pre><code class="lang-plaintext">sql-- Traditional approach: Separate indexes
CREATE INDEX idx_customer_id ON orders (customer_id);
CREATE INDEX idx_status ON orders (status);
CREATE INDEX idx_order_date ON orders (order_date);

-- Composite approach: Single multi-column index
CREATE INDEX idx_orders_composite ON orders (customer_id, status, order_date);
</code></pre>
<p>Think of it like organizing a library. Instead of having separate filing systems for author, genre, and publication year, you create one system that organizes books by author-genre-year combinations.</p>
<h2 id="heading-the-problem-when-individual-indexes-fall-short">The Problem: When Individual Indexes Fall Short</h2>
<p>Let's examine a realistic e-commerce scenario:</p>
<pre><code class="lang-plaintext">sql-- Our sample table
CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_id INT,
    status VARCHAR(20),
    order_date DATE,
    total_amount DECIMAL(10,2)
);

-- Sample data: 1,000,000 orders
-- customer_id: 1-10,000 (avg 100 orders per customer)
-- status: 'pending', 'shipped', 'delivered', 'cancelled', 'returned'
-- order_date: Last 2 years
</code></pre>
<p><strong>Common query pattern:</strong></p>
<pre><code class="lang-plaintext">sqlSELECT order_id, total_amount 
FROM orders 
WHERE customer_id = 7829 
  AND status = 'shipped' 
  AND order_date &gt;= '2024-01-01';
</code></pre>
<h2 id="heading-database-strategies-for-multi-column-queries">Database Strategies for Multi-Column Queries</h2>
<p>Let's analyze how different indexing strategies handle this query:</p>
<h3 id="heading-strategy-1-no-indexes-full-table-scan">Strategy 1: No Indexes (Full Table Scan)</h3>
<pre><code class="lang-plaintext">sql-- No indexes on any column
</code></pre>
<p><strong>Execution:</strong></p>
<ol>
<li><p>Scan every single row (1,000,000 records)</p>
</li>
<li><p>Check all three conditions for each row</p>
</li>
<li><p><strong>Time Complexity:</strong> O(n) where n = total records</p>
</li>
<li><p><strong>Estimated Time:</strong> 2-5 seconds 😱</p>
</li>
</ol>
<pre><code class="lang-plaintext">sql-- Query plan would show:
Seq Scan on orders (cost=0.00..25000.00 rows=5 width=16)
  Filter: (customer_id = 7829 AND status = 'shipped' AND order_date &gt;= '2024-01-01')
</code></pre>
<h3 id="heading-strategy-2-individual-indexes-single-index-usage">Strategy 2: Individual Indexes (Single Index Usage)</h3>
<pre><code class="lang-plaintext">sqlCREATE INDEX idx_customer_id ON orders (customer_id);
CREATE INDEX idx_status ON orders (status);
CREATE INDEX idx_order_date ON orders (order_date);
</code></pre>
<p><strong>Database's decision-making process:</strong></p>
<ol>
<li><p><strong>Analyze selectivity</strong> of each condition:</p>
<ul>
<li><p><code>customer_id = 7829</code>: ~100 records (high selectivity)</p>
</li>
<li><p><code>status = 'shipped'</code>: ~200,000 records (low selectivity)</p>
</li>
<li><p><code>order_date &gt;= '2024-01-01'</code>: ~500,000 records (very low selectivity)</p>
</li>
</ul>
</li>
<li><p><strong>Choose most selective index</strong> (customer_id)</p>
</li>
<li><p><strong>Execution:</strong></p>
<ul>
<li><p>Use customer_id index → retrieve 100 records</p>
</li>
<li><p>Filter those 100 records for status and date</p>
</li>
</ul>
</li>
</ol>
<p><strong>Time Complexity:</strong> O(log n) + O(k) where k = records from first index <strong>Estimated Time:</strong> 50-200ms</p>
<h3 id="heading-strategy-3-index-intersection-advanced">Strategy 3: Index Intersection (Advanced)</h3>
<p>Some database engines (PostgreSQL, SQL Server) can use multiple indexes simultaneously:</p>
<pre><code class="lang-plaintext">sql-- Same individual indexes as Strategy 2
</code></pre>
<p><strong>Execution:</strong></p>
<ol>
<li><p><strong>customer_id index lookup</strong> → Record IDs: [245, 1001, 1567, 2103, ...]</p>
</li>
<li><p><strong>status index lookup</strong> → Record IDs: [432, 1001, 1789, 2103, ...]</p>
</li>
<li><p><strong>Find intersection</strong> of both sets → [1001, 2103, ...]</p>
</li>
<li><p><strong>Apply date filter</strong> on intersected results</p>
</li>
</ol>
<p><strong>Time Complexity:</strong> O(log n) + O(log n) + O(intersection) <strong>Estimated Time:</strong> 20-100ms</p>
<p><strong>Caveat:</strong> Not all databases support this efficiently, and intersection operations have overhead.</p>
<h3 id="heading-strategy-4-composite-index">Strategy 4: Composite Index ⭐</h3>
<pre><code class="lang-plaintext">sqlCREATE INDEX idx_orders_composite ON orders (customer_id, status, order_date);
</code></pre>
<p><strong>Execution:</strong></p>
<ol>
<li><p><strong>Single index traversal</strong> handling all three conditions simultaneously</p>
</li>
<li><p><strong>Direct navigation</strong> to matching records</p>
</li>
<li><p><strong>No additional filtering</strong> required</p>
</li>
</ol>
<p><strong>Time Complexity:</strong> O(log n) <strong>Estimated Time:</strong> 1-10ms 🚀</p>
<h2 id="heading-time-complexity-analysis">Time Complexity Analysis</h2>
<pre><code class="lang-plaintext">StrategyIndex LookupsFilter OperationsTotal Time ComplexityPractical PerformanceFull Scan0O(n)O(n)2-5 secondsSingle Index1O(k)O(log n) + O(k)50-200msIndex Intersection2-3O(intersection)O(log n) + O(intersection)20-100msComposite Index10O(log n)1-10ms
</code></pre>
<p><strong>Key Insight:</strong> Composite indexes eliminate the filtering phase entirely by incorporating all conditions into the index structure.</p>
<h2 id="heading-best-practices-column-ordering">Best Practices: Column Ordering</h2>
<p>The order of columns in a composite index is crucial. Follow the <strong>selectivity rule</strong>:</p>
<h3 id="heading-selectivity-analysis">Selectivity Analysis</h3>
<pre><code class="lang-plaintext">sql-- Calculate selectivity for each column
SELECT 
    'customer_id' as column_name,
    COUNT(DISTINCT customer_id) as unique_values,
    COUNT(*) as total_rows,
    COUNT(DISTINCT customer_id) * 100.0 / COUNT(*) as selectivity_percent
FROM orders

UNION ALL

SELECT 
    'status',
    COUNT(DISTINCT status),
    COUNT(*),
    COUNT(DISTINCT status) * 100.0 / COUNT(*)
FROM orders

UNION ALL

SELECT 
    'order_date',
    COUNT(DISTINCT order_date),
    COUNT(*),
    COUNT(DISTINCT order_date) * 100.0 / COUNT(*)
FROM orders;
</code></pre>
<p><strong>Sample Results:</strong></p>
<pre><code class="lang-plaintext">column_name  | unique_values | total_rows | selectivity_percent
-------------|---------------|------------|-------------------
customer_id  | 10000         | 1000000    | 1.0%
order_date   | 730           | 1000000    | 0.073%
status       | 5             | 1000000    | 0.0005%
</code></pre>
<h3 id="heading-optimal-column-ordering">Optimal Column Ordering</h3>
<pre><code class="lang-plaintext">sql-- ✅ OPTIMAL: Most selective first
CREATE INDEX idx_orders_optimal ON orders (customer_id, order_date, status);

-- ❌ SUBOPTIMAL: Least selective first
CREATE INDEX idx_orders_bad ON orders (status, order_date, customer_id);
</code></pre>
<p><strong>Why order matters:</strong></p>
<ul>
<li><p>B-tree indexes work like a multi-level directory</p>
</li>
<li><p>First column determines the primary organization</p>
</li>
<li><p>Subsequent columns create sub-organizations within each primary group</p>
</li>
</ul>
<p><strong>Visualization:</strong></p>
<pre><code class="lang-plaintext">Optimal Index Structure (customer_id, order_date, status):
Customer 7829
  ├── 2024-01-15 → [shipped, delivered]
  ├── 2024-01-20 → [pending, shipped]
  └── 2024-02-01 → [shipped]

Suboptimal Index Structure (status, order_date, customer_id):
Status: shipped
  ├── 2024-01-15 → [customers: 1, 15, 29, 7829, ...]
  ├── 2024-01-20 → [customers: 3, 22, 67, 7829, ...]
  └── ... (much more data to traverse)
</code></pre>
<h2 id="heading-verification-with-explain-plans">Verification with EXPLAIN Plans</h2>
<p>Always verify your optimization works using database-specific EXPLAIN commands:</p>
<h3 id="heading-postgresql">PostgreSQL</h3>
<pre><code class="lang-plaintext">sqlEXPLAIN (ANALYZE, BUFFERS, VERBOSE) 
SELECT order_id, total_amount 
FROM orders 
WHERE customer_id = 7829 
  AND status = 'shipped' 
  AND order_date &gt;= '2024-01-01';
</code></pre>
<p><strong>Without Composite Index:</strong></p>
<pre><code class="lang-plaintext">Index Scan using idx_customer_id on orders (cost=0.43..25.89 rows=5 width=16) 
                                           (actual time=0.123..2.456 rows=5 loops=1)
  Index Cond: (customer_id = 7829)
  Filter: ((status = 'shipped') AND (order_date &gt;= '2024-01-01'))
  Rows Removed by Filter: 78
  Buffers: shared hit=12
</code></pre>
<p><strong>With Composite Index:</strong></p>
<pre><code class="lang-plaintext">Index Scan using idx_orders_composite on orders (cost=0.43..8.45 rows=5 width=16) 
                                                (actual time=0.034..0.041 rows=5 loops=1)
  Index Cond: ((customer_id = 7829) AND (status = 'shipped') AND (order_date &gt;= '2024-01-01'))
  Buffers: shared hit=3
</code></pre>
<h3 id="heading-mysql">MySQL</h3>
<pre><code class="lang-plaintext">sqlEXPLAIN FORMAT=JSON 
SELECT order_id, total_amount 
FROM orders 
WHERE customer_id = 7829 
  AND status = 'shipped' 
  AND order_date &gt;= '2024-01-01';
</code></pre>
<p><strong>Look for these indicators:</strong></p>
<ul>
<li><p>✅ <code>"using_index": true</code> - Good</p>
</li>
<li><p>✅ <code>"key": "idx_orders_composite"</code> - Using your composite index</p>
</li>
<li><p>❌ <code>"Extra": "Using where"</code> - Additional filtering happening</p>
</li>
<li><p>❌ <code>"type": "ALL"</code> - Full table scan</p>
</li>
</ul>
<h3 id="heading-sql-server">SQL Server</h3>
<pre><code class="lang-plaintext">sqlSET STATISTICS IO ON;
SET STATISTICS TIME ON;

SELECT order_id, total_amount 
FROM orders 
WHERE customer_id = 7829 
  AND status = 'shipped' 
  AND order_date &gt;= '2024-01-01';
</code></pre>
<h2 id="heading-when-not-to-use-composite-indexes">When NOT to Use Composite Indexes</h2>
<h3 id="heading-1-independent-column-queries">1. Independent Column Queries</h3>
<pre><code class="lang-plaintext">sql-- If you frequently query individual columns:
SELECT * FROM orders WHERE status = 'pending';  -- Won't use (customer_id, status, date) efficiently
SELECT * FROM orders WHERE order_date = '2024-01-15';  -- Won't use the composite index
</code></pre>
<p><strong>Solution:</strong> Create both composite and individual indexes based on query patterns.</p>
<h3 id="heading-2-high-write-frequency">2. High Write Frequency</h3>
<pre><code class="lang-plaintext">sql-- Heavy INSERT/UPDATE workload
-- Each insert must update ALL indexes
-- More indexes = slower writes
</code></pre>
<p><strong>Benchmark Example:</strong></p>
<pre><code class="lang-plaintext">sql-- With 5 individual indexes: 1000 INSERTs/second
-- With 1 composite index: 2500 INSERTs/second
-- Trade-off: Read performance vs Write performance
</code></pre>
<h3 id="heading-3-low-selectivity-combinations">3. Low Selectivity Combinations</h3>
<pre><code class="lang-plaintext">sql-- All columns have poor selectivity
CREATE INDEX idx_poor ON orders (status, payment_method, shipping_type);
-- If most orders are 'shipped', 'credit_card', 'standard'
-- Index won't provide significant benefit
</code></pre>
<h3 id="heading-4-unused-query-patterns">4. Unused Query Patterns</h3>
<pre><code class="lang-plaintext">sql-- Creating indexes for queries that rarely run
-- Wastes storage and slows down writes
-- Monitor query patterns before optimizing
</code></pre>
<h2 id="heading-real-world-case-study">Real-World Case Study</h2>
<p><strong>Company:</strong> E-commerce platform <strong>Problem:</strong> Order dashboard queries timing out (30+ seconds) <strong>Table Size:</strong> 50 million orders</p>
<p><strong>Original Query:</strong></p>
<pre><code class="lang-plaintext">sqlSELECT o.order_id, o.total_amount, c.customer_name
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.merchant_id = 12345
  AND o.status IN ('shipped', 'delivered')
  AND o.order_date BETWEEN '2024-01-01' AND '2024-12-31'
ORDER BY o.order_date DESC
LIMIT 100;
</code></pre>
<p><strong>Before Optimization:</strong></p>
<pre><code class="lang-plaintext">sql-- Individual indexes on each column
-- Query time: 35 seconds
-- Scanned: 15 million rows
-- Filtered: 99.9% rows discarded
</code></pre>
<p><strong>After Composite Index:</strong></p>
<pre><code class="lang-plaintext">sqlCREATE INDEX idx_orders_dashboard ON orders (merchant_id, status, order_date, customer_id);
-- Column order based on selectivity analysis
-- Query time: 180ms
-- Scanned: 15,000 rows
-- 200x improvement! 🎉
</code></pre>
<p><strong>Key Lessons:</strong></p>
<ol>
<li><p><strong>Measure before optimizing</strong> - Use query profiling tools</p>
</li>
<li><p><strong>Consider covering indexes</strong> - Include frequently selected columns</p>
</li>
<li><p><strong>Monitor after deployment</strong> - Ensure production benefits match testing</p>
</li>
<li><p><strong>Regular maintenance</strong> - Update index statistics periodically</p>
</li>
</ol>
<h2 id="heading-advanced-tips">Advanced Tips</h2>
<h3 id="heading-covering-indexes">Covering Indexes</h3>
<pre><code class="lang-plaintext">sql-- Include frequently selected columns
CREATE INDEX idx_orders_covering ON orders (customer_id, status, order_date) 
INCLUDE (total_amount, shipping_address);
-- Eliminates table lookups entirely
</code></pre>
<h3 id="heading-partial-indexes">Partial Indexes</h3>
<pre><code class="lang-plaintext">sql-- Index only relevant data
CREATE INDEX idx_orders_active ON orders (customer_id, order_date) 
WHERE status NOT IN ('cancelled', 'returned');
-- Smaller index, better performance for active orders
</code></pre>
<h3 id="heading-index-maintenance">Index Maintenance</h3>
<pre><code class="lang-plaintext">sql-- PostgreSQL: Update statistics
ANALYZE orders;

-- MySQL: Optimize table
OPTIMIZE TABLE orders;

-- SQL Server: Update statistics
UPDATE STATISTICS orders;
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Composite indexes are powerful tools for optimizing multi-column queries, but they require thoughtful implementation:</p>
<p><strong>Key Takeaways:</strong></p>
<ol>
<li><p><strong>Use composite indexes</strong> when columns are frequently queried together</p>
</li>
<li><p><strong>Order columns by selectivity</strong> (most selective first)</p>
</li>
<li><p><strong>Always verify with EXPLAIN plans</strong> - theory and practice can differ</p>
</li>
<li><p><strong>Consider the write performance trade-off</strong> - more indexes = slower writes</p>
</li>
<li><p><strong>Monitor query patterns</strong> before and after optimization</p>
</li>
<li><p><strong>Regular maintenance</strong> ensures continued performance</p>
</li>
</ol>
<p><strong>Remember:</strong> Database optimization is about understanding your data, your queries, and your database engine's behavior. Composite indexes are just one tool in your optimization toolkit.</p>
<hr />
<p><em>Have you implemented composite indexes in your projects? What performance improvements did you see? Share your experiences in the comments below!</em></p>
<p><strong>Tags:</strong> #Database #SQL #Performance #Optimization #Indexing #PostgreSQL #MySQL #SQLServer</p>
]]></content:encoded></item></channel></rss>