| 動作描述 | 聚集索引 | 非聚集索引 |
|---|---|---|
| 列經常分組排序 (GROUP BY / ORDER BY) | ✅ 應 | ✅ 應 |
| 返回某範圍內資料 (BETWEEN / > / <) | ✅ 應 | ❌ 不應 |
| 極少不同值 (性別/布林) | ❌ 不應 | ❌ 不應 |
| 少數不同值 (幾十種狀態) | ✅ 應 | ❌ 不應 |
| 大量不同值 (流水號/ID) | ❌ 不應 | ✅ 應 |
| 頻繁更新的列 | ❌ 不應 | ✅ 應 |
| 外鍵列 | ✅ 應 | ✅ 應 |
| 主鍵列 | ✅ 應 (視情況) | ✅ 應 |
場景: 查詢近 3 個月文件 (25 萬筆),`fariqi` (發文日期) 有 5003 個不同值,符合「少數不同值、頻繁範圍查詢」特徵。
| 方案 | 索引配置 | 查詢語句 | 耗時 | 備註 |
|---|---|---|---|---|
| 1 | PK (GID) 聚集 | `SELECT ... FROM TGongwen` (全表) | 128,470 ms (128 秒) | 全表掃描 |
| 2 | PK 聚集 + `fariqi` 非聚集 | `WHERE fariqi > DATEADD(day,-90,GETDATE())` | 53,763 ms (54 秒) | 非聚集索引掃描 + 大量書籤查找 |
| 3 | `fariqi` 聚集 | `WHERE fariqi > DATEADD(day,-90,GETDATE())` | 2,423 ms (2.4 秒) | 聚集索引範圍掃描,快 22 倍! |
結論: **主鍵 (GID) 做聚集索引是資源浪費** (業務從不按 ID 查範圍)。將聚集索引給 `fariqi` (日期),查詢速度從「超時」降至「秒級」。
建立複合聚集索引 `(fariqi, neibuyonghu)` (日期、發文單位):
| 查詢條件 | 耗時 | 分析 |
|---|---|---|
| `WHERE fariqi > '2004-05-05'` | 2,513 ms | 用到前導列,聚集索引生效 |
| `WHERE fariqi > '2004-05-05' AND neibuyonghu = '辦公室'` | 2,516 ms | 全用上,索引覆蓋可能更快 |
| `WHERE neibuyonghu = '辦公室'` | 60,280 ms | 僅用非前導列,聚集索引完全失效! |
鐵律: 複合索引**前導列必須是最高頻篩選列**;僅用後綴列查詢需另建單列索引。
`WHERE name='zhangsan' AND tid>10000` vs `WHERE tid>10000 AND name='zhangsan'` **效率相同**。優化器會根據統計資訊選擇最佳存取路徑 (SARG)。
形式:`Column Operator Constant/Variable` (列在運算子左側)。滿足 SARG 才能 Index Seek。
| 模式 | 範例 | 是否 SARG |
|---|---|---|
| 等值/範圍 | `name='張三'`, `price>5000`, `5000| ✅ | |
| AND 組合 | `name='張三' AND price>5000` | ✅ |
| LIKE 前綴 | `name LIKE '張%'` | ✅ |
| LIKE 後綴/中間 | `name LIKE '%張'`, `name LIKE '%張%'` | ❌ (掃描) |
| OR | `name='張三' OR price>5000` | ❌ (全表掃描) |
| 負向邏輯 | `NOT`, `!=`, `<>`, `!<`, `!>`, `NOT IN`, `NOT EXISTS`, `NOT LIKE` | ❌ |
| 函數/運算在列上 | `ABS(price)<5000`, `price*2>5000` | ❌ (優化器可能簡化 `price>2500`,但不保證) |
| IN 子查詢 | `tid IN (2,3)` 等同 `tid=2 OR tid=3` | ❌ (掃描) |
| 頁碼 | 方案 1: 臨時表/表變量 + IDENTITY (俄羅斯存儲過程改良) | 方案 2: TOP + NOT IN / NOT EXISTS (經典子查詢) | 方案 3: TOP + MAX/MIN 鍵值定位 (Keyset Pagination) ⭐ |
|---|---|---|---|
| 1 | 60 ms | 30 ms | 76 ms |
| 10 | 46 ms | 16 ms | 63 ms |
| 100 | 1,076 ms | 720 ms | 130 ms |
| 500 | 540 ms | 12,943 ms | 83 ms |
| 1,000 | 17,110 ms | 470 ms | 250 ms |
| 10,000 | 24,796 ms | 4,500 ms | 140 ms |
| 100,000 | 38,326 ms | 42,283 ms | 1,553 ms |
| 250,000 | 28,140 ms | 128,720 ms | 2,330 ms |
| 500,000 | 121,686 ms | 127,846 ms | 7,168 ms |
-- 升序分頁 (第 @PageIndex 頁,每頁 @PageSize)
SELECT TOP @PageSize *
FROM TGongwen
WHERE GID > (
SELECT MAX(GID) FROM (
SELECT TOP ((@PageIndex - 1) * @PageSize) GID
FROM TGongwen
ORDER BY GID
) AS T
)
ORDER BY GID;
-- 降序分頁
SELECT TOP @PageSize *
FROM TGongwen
WHERE GID < (
SELECT MIN(GID) FROM (
SELECT TOP ((@PageIndex - 1) * @PageSize) GID
FROM TGongwen
ORDER BY GID DESC
) AS T
)
ORDER BY GID DESC;
原理: 利用 **唯一、有序、可比較** 的鍵值 (主鍵/聚集索引列) 作為「分水嶺」,透過 `>` / `<` 運算子 (滿足 SARG) 直接定位下一頁起始位置,**完全避開 `NOT IN/EXISTS` 反向掃描**。
CREATE PROCEDURE pagination3
@tblName VARCHAR(255), -- 表名
@strGetFields VARCHAR(1000) = '*', -- 回傳欄位
@fldName VARCHAR(255) = '', -- 排序欄位 (必須唯一,建議 PK/聚集鍵)
@PageSize INT = 10,
@PageIndex INT = 1,
@doCount BIT = 0, -- 1=回傳總筆數
@OrderType BIT = 0, -- 1=DESC, 0=ASC
@strWhere VARCHAR(1500) = '' -- WHERE 條件 (不含 WHERE 關鍵字)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @strSQL VARCHAR(5000), @strTmp VARCHAR(110), @strOrder VARCHAR(400);
-- 1. 總數統計 (可選)
IF @doCount != 0
BEGIN
SET @strSQL = 'SELECT COUNT(*) AS Total FROM [' + @tblName + ']' +
CASE WHEN @strWhere != '' THEN ' WHERE ' + @strWhere ELSE '' END;
EXEC(@strSQL);
RETURN; -- 只回總數
END
-- 2. 構建排序與比較運算子
IF @OrderType != 0
BEGIN
SET @strTmp = '< (SELECT MIN('; -- 降序:找上一頁最小值
SET @strOrder = ' ORDER BY [' + @fldName + '] DESC';
END
ELSE
BEGIN
SET @strTmp = '> (SELECT MAX('; -- 升序:找上一頁最大值
SET @strOrder = ' ORDER BY [' + @fldName + '] ASC';
END
-- 3. 第一頁直走 TOP (最快)
IF @PageIndex = 1
BEGIN
SET @strSQL = 'SELECT TOP ' + CAST(@PageSize AS VARCHAR) + ' ' + @strGetFields +
' FROM [' + @tblName + ']' +
CASE WHEN @strWhere != '' THEN ' WHERE ' + @strWhere ELSE '' END +
@strOrder;
END
ELSE
BEGIN
-- 4. 非第一頁:Keyset 定位
DECLARE @skipRows INT = (@PageIndex - 1) * @PageSize;
SET @strSQL = 'SELECT TOP ' + CAST(@PageSize AS VARCHAR) + ' ' + @strGetFields +
' FROM [' + @tblName + '] ' +
' WHERE [' + @fldName + '] ' + @strTmp + '[' + @fldName + ']) FROM (' +
' SELECT TOP ' + CAST(@skipRows AS VARCHAR) + ' [' + @fldName + '] ' +
' FROM [' + @tblName + ']' +
CASE WHEN @strWhere != '' THEN ' WHERE ' + @strWhere ELSE '' END +
@strOrder + ') AS tblTmp)' +
CASE WHEN @strWhere != '' THEN ' AND ' + @strWhere ELSE '' END +
@strOrder;
END
EXEC(@strSQL);
END
案例: 業務高頻按 `fariqi` (日期) 篩選 → 聚集索引宜給 `fariqi`。但分頁需按唯一鍵 `GID` 排序 → `fariqi` 有重複值,無法 `MAX/MIN` 定位。
雙贏效果:
實測 1000 萬筆複雜查詢/分頁,**CPU 持續 70%-100% (雙核超執緒)**,記憶體佔用平穩 (1GB 足夠)。結論:SQL Server 大量邏輯讀/排序/雜湊/並行計算為 **CPU 密集型**。升級 CPU 核心數/主頻、優化並行度 (`MAXDOP`)、減少編譯/重編譯 (參數化查詢、計畫指南) 為擴容關鍵。
最終驗證: 同一 1000 萬筆環境,優化前首頁載入超時,優化後 **分頁穩定 < 10ms,範圍查詢 < 3s**,系統正式上線運行至今穩定。