SQL Server Index Performance Tuning: Clustered vs. Nonclustered Indexes and Seek vs. Scan Explained

This article focuses on the underlying logic and practical methods of SQL Server index optimization. It explains the differences between clustered indexes, nonclustered indexes, and Seek vs. Scan, and addresses three common pain points: queries that remain slow even after adding indexes, slower writes, and uncertainty about whether an index is actually effective. Keywords: SQL Server, index optimization, execution plans.

The technical specification snapshot provides the baseline

Parameter Description
Technical topic SQL Server index optimization
Language T-SQL
Core structure B-Tree
Applicable versions SQL Server 2019+
Core protocols/interfaces T-SQL, DMV
Stars Not provided in the original
Core dependencies sys.indexes, sys.dm_db_index_usage_stats, sys.dm_db_missing_index_details

Indexes are the infrastructure behind SQL Server query acceleration

An index is not just an “extra configuration.” It is the design of the database access path. Without an index, the optimizer often has no choice but to scan the entire table. With the right index, it can use the B-Tree to locate target rows quickly.

You can think of an index as the table of contents in a book. Without it, you must search from the first page to the last. With it, the lookup cost drops from a linear scan to a logarithmic search.

Indexes significantly reduce read cost

Indexes reduce the number of data page accesses and can also lower the extra cost of sorting, aggregation, and joins. Creating indexes on frequently queried columns usually improves execution efficiency for WHERE, JOIN, GROUP BY, and ORDER BY at the same time.

SELECT *
FROM Orders
WHERE CustomerID = 12345; -- Filter by condition; whether an index is used determines the access path

The key difference in this query is not the SQL syntax itself, but whether the execution plan chooses a Table Scan or an Index Seek.

Clustered and nonclustered indexes serve different responsibilities

A clustered index determines the physical storage order of data rows, and its leaf nodes are the full data rows. That is why a table can have only one clustered index. In practice, it usually belongs on a primary key or a stable, incrementing column.

CREATE CLUSTERED INDEX IX_Orders_OrderDate
ON Orders(OrderDate); -- Organize stored data by order date

This script creates a clustered index on the Orders table. It works well for workloads that primarily retrieve rows by date range.

A nonclustered index works more like an independent directory. Its leaf nodes store the index key plus row locator information. It does not change the physical order of the table, but it can provide multiple efficient access paths for different query patterns.

CREATE NONCLUSTERED INDEX IX_Orders_CustomerID
ON Orders(CustomerID); -- Create an auxiliary lookup path for customer ID

This script accelerates queries that filter or join on CustomerID.

The key differences between the two index types are easy to remember

Comparison item Clustered index Nonclustered index
Count per table 1 Up to 999
Leaf nodes Full data rows Index key + pointer
Determines physical order Yes No
Query cost Can directly hit the data May require a key lookup
Typical use Primary access path Auxiliary query acceleration

Seek and Scan determine whether a query truly uses an index efficiently

Many people assume that “having an index” automatically means “fast.” In reality, performance depends on whether the execution plan chooses Seek or Scan. A Seek navigates down the tree level by level to locate matching rows, while a Scan reads the entire tree or table sequentially.

On a table with millions of rows, a Seek usually requires only a small number of logical reads. A Scan, by contrast, increases I/O cost and can reduce cache efficiency. The key to performance tuning is not to create indexes blindly, but to shape queries so they qualify for a Seek.

A test dataset makes the performance difference visible

DROP TABLE IF EXISTS Orders;
CREATE TABLE Orders (
    OrderID INT IDENTITY(1,1),
    OrderDate DATE,
    CustomerID INT,
    Amount DECIMAL(10,2)
);

WITH Numbers AS (
    SELECT TOP 1000000 ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS n
    FROM sys.all_columns a
    CROSS JOIN sys.all_columns b
)
INSERT INTO Orders (OrderDate, CustomerID, Amount)
SELECT DATEADD(day, n % 3650, '2020-01-01'),
       (n % 10000) + 1,
       ROUND(RAND(CHECKSUM(NEWID())) * 10000, 2) -- Generate a random amount
FROM Numbers;

This script generates 1 million rows of test data so you can observe changes in I/O and execution plans before and after indexing.

SET STATISTICS TIME ON;
SET STATISTICS IO ON;

SELECT *
FROM Orders
WHERE CustomerID = 12345; -- Without an index, this usually triggers a full table scan

CREATE NONCLUSTERED INDEX IX_Orders_CustomerID
ON Orders(CustomerID);

SELECT *
FROM Orders
WHERE CustomerID = 12345; -- After adding the index, this usually becomes an Index Seek

This comparison script verifies the performance difference for the same query before and after adding an index.

Common misconceptions often hurt performance more than missing indexes

The first misconception is that more indexes are always better. In reality, every additional nonclustered index must also be maintained during INSERT, UPDATE, and DELETE, so write cost continues to rise.

The second misconception is that adding an index to a WHERE column guarantees index usage. If you apply a function to the indexed column, trigger an implicit conversion, or use a leading wildcard, the optimizer will often abandon the index.

Three common patterns that make indexes ineffective

SELECT * FROM Orders WHERE YEAR(OrderDate) = 2024; -- Apply a function to an indexed column
SELECT * FROM Orders WHERE OrderID = '123'; -- May trigger an implicit conversion
SELECT * FROM Customers WHERE Name LIKE '%Smith'; -- A leading wildcard prevents an efficient Seek

These examples show the most common reasons why “the index exists but is not being used.”

DMV-based diagnostics reveal whether indexes are healthy

Index governance should never rely on guesswork. You should use DMV data to make decisions. Focus on three categories: unused indexes, missing indexes, and severe fragmentation. That is how you build a closed-loop optimization process instead of doing one-time tuning.

SELECT OBJECT_NAME(i.object_id) AS TableName,
       i.name AS IndexName,
       s.user_seeks,
       s.user_scans,
       s.user_lookups,
       s.user_updates
FROM sys.indexes i
LEFT JOIN sys.dm_db_index_usage_stats s
  ON i.object_id = s.object_id
 AND i.index_id = s.index_id
WHERE OBJECTPROPERTY(i.object_id, 'IsUserTable') = 1
  AND (s.user_seeks + s.user_scans + s.user_lookups = 0 OR s.user_seeks IS NULL)
  AND i.name IS NOT NULL;

This script identifies indexes that have not been used by queries for a long time but still incur maintenance overhead.

SELECT migs.avg_total_user_cost * migs.avg_user_impact * (migs.user_seeks + migs.user_scans) AS Score,
       mid.statement AS TableName,
       mid.equality_columns,
       mid.inequality_columns,
       mid.included_columns
FROM sys.dm_db_missing_index_groups mig
JOIN sys.dm_db_missing_index_group_stats migs
  ON migs.group_handle = mig.index_group_handle
JOIN sys.dm_db_missing_index_details mid
  ON mig.index_handle = mid.index_handle
WHERE mid.database_id = DB_ID()
ORDER BY Score DESC;

This script surfaces missing index recommendations that the optimizer believes could deliver significant benefit.

SELECT OBJECT_NAME(ips.object_id) AS TableName,
       i.name AS IndexName,
       ips.avg_fragmentation_in_percent,
       ips.page_count
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') ips
JOIN sys.indexes i
  ON ips.object_id = i.object_id
 AND ips.index_id = i.index_id
WHERE ips.avg_fragmentation_in_percent > 30;

This script helps you detect highly fragmented indexes so you can reorganize or rebuild them later.

Building a balanced mental model matters more than memorizing commands

The core of index optimization is not “create more indexes.” It is making sure high-value queries consistently use Seeks while keeping write maintenance cost under control. Clustered indexes define the primary storage path, and nonclustered indexes support common access patterns. Both should be designed around real business queries.

If you remember only one sentence, make it this: missing the right index causes slowness, but having too many indexes also causes slowness. Real optimization comes from balancing execution plans, data distribution, and index structure.

WeChat sharing prompt AI Visual Insight: This image is an animated sharing prompt intended to guide users toward social sharing actions. It does not contain technical information about SQL Server index structures, execution plans, or performance metrics, so it should not be used as a basis for technical judgment.

FAQ

Why does SQL Server still choose a Scan after I create an index?

Common reasons include applying functions to indexed columns, implicit type conversions, leading % wildcard matching, and low column selectivity. Start with the actual execution plan, then adjust the SQL pattern or index design.

Does a clustered index always belong on the primary key?

Not necessarily. A primary key is often implemented as a clustered index by default, but best practice is to place the clustered index on the most stable, most frequently range-filtered, and ideally incrementing column.

How do I decide whether to keep or remove an index?

First check whether user_seeks, user_scans, and user_lookups have stayed at 0 for a long time, then compare that with user_updates to estimate maintenance cost. If the index is rarely or never used but is frequently maintained, you should usually evaluate it for removal.

The core summary consolidates the practical guidance

This article systematically reconstructs the core knowledge behind SQL Server index optimization. It covers how clustered indexes, nonclustered indexes, and Seek vs. Scan work, along with common misconceptions and DMV diagnostic scripts, to help developers build practical methods for query acceleration and index governance.