SQL Server 对非聚簇索引的 INCLUDE 列数量和大小有限制(最多 1023 列,但总大小 ≤ 900 字节键 + 8060 字节 INCLUDE)。
你的表有 大量 nvarchar(500)、decimal(25,12) 等大字段,无法将所有列放入 INCLUDE。
因此,完全覆盖所有字段不现实。我们应优先让索引快速定位行,然后通过 Key Lookup(回表) 获取其余字段。
Key Lookup(回表) ,这是 SQL Server 执行计划中一个非常关键的概念。我们来结合你的场景(app_contractlist_ext_three 表 + 非聚集索引 + SELECT *)深入解释:
🔍 什么是 Key Lookup(回表)?
- 当你使用 非聚集索引(Nonclustered Index) 查询数据时:
- 如果该索引 不包含查询所需的所有列,
- SQL Server 会先通过非聚集索引 找到符合条件的行的"指针"(即聚簇索引键 或 RID),
- 然后再 回到主表(堆表或聚簇索引)中查找完整行数据。
- 这个"再回去查一次"的操作,就叫 Key Lookup (如果表有聚簇索引)或 RID Lookup(如果表是堆表)。
✅ 在你的表中,由于没有定义
PRIMARY KEY或CLUSTERED INDEX,默认是堆表(Heap) ,所以实际执行的是 RID Lookup,但大家习惯统称 "Key Lookup"。
📌 为什么你的查询一定会有 Key Lookup?
因为你写的是:
SELECT * FROM app_contractlist_ext_three WHERE ...
而你的非聚集索引(比如 idx_app_contractlist_ext_three_dt_del_filter)只包含 6 个字段 ,但表总共有 50+ 个字段,包括大字段如:
product_title NVARCHAR(500)contract_title NVARCHAR(100)- 多个
DECIMAL(25,12)等
👉 非聚集索引无法包含所有列 (受 900 字节键限制 + 8060 字节 INCLUDE 限制,且大字段太多),所以 必须回表 才能返回完整数据。
✅ 这是正常且不可避免的 ,只要你是 SELECT *。
⚠️ Key Lookup 的性能影响
表格
| 情况 | 性能影响 |
|---|---|
| 返回行数少(< 100 行) | 影响很小,可忽略 |
| 返回行数多(> 1万行) | 每一行都要额外 I/O 回表,性能急剧下降 |
| 高并发查询 | 大量随机 I/O,可能成为瓶颈 |
💡 例如:索引找到 5 万行,就要做 5 万次 RID Lookup ------ 这比一次聚簇索引扫描还慢!
✅ 如何优化或避免(在无法避免 SELECT * 的前提下)?
1. 确保非聚集索引能快速过滤出最少的行
- 你的索引设计(
dt, del, ...)已经很好,能大幅减少需要回表的行数。 - ✅ 这是最关键的优化点 :让索引尽可能精准定位,减少回表次数。
2. 考虑将表改为聚簇索引表(推荐)
目前你的表是堆表(无聚簇索引),回表用的是 物理行地址(RID),效率较低。
如果你给表加一个 聚簇索引,比如:
-- 假设 id 是唯一且递增的
CREATE CLUSTERED INDEX CX_app_contractlist_ext_three_id ON app_contractlist_ext_three (id);
那么:
- 非聚集索引会以
(id)作为行指针; - 回表变成 聚簇索引查找(Clustered Index Seek),通常更高效(尤其数据按 id 顺序存储时);
- 同时,很多分析查询也可直接按
id范围扫描。
✅ 对于日志型、明细型大表,强烈建议有聚簇索引(通常选自增 ID 或 dt + id)。
3. 避免 SELECT(理想但常不现实)
如果前端/报表真的只需要部分字段,改成明确列名,并把它们放入 INCLUDE,就能 消除 Key Lookup。
例如:
CREATE NONCLUSTERED INDEX IX_covering
ON app_contractlist_ext_three (dt, del, gate_ord)
INCLUDE (contract_title, product_title, shipment_num, PriceAfterTax);
→ 查询这些字段时,无需回表。
但你说"要返回所有字段",所以这条不适用。
🔎 如何查看是否发生了 Key Lookup?
执行查询时,点击 "显示实际执行计划"(Actual Execution Plan),你会看到:
- 一个 "Index Seek" 或 "Index Scan"(你的非聚集索引)
- 旁边连着一个 "Key Lookup (Clustered) " 或 "RID Lookup (Heap)" 操作符
- 鼠标悬停可看到 Estimated Number of Rows 和 I/O Cost
如果 "Lookup" 的行数很大(比如几万+),就要警惕性能问题。
✅ 总结
表格
| 问题 | 回答 |
|---|---|
我的 SELECT * 查询会有 Key Lookup 吗? |
✅ 一定会,因为非聚集索引不包含所有列 |
| 这是 bad 吗? | ❌ 不一定 ------ 只要过滤后行数少,就完全 OK |
| 如何减轻影响? | 1. 让索引精准过滤 2. 考虑加聚簇索引 3. 避免大结果集查询 |
| 需要为消除 Key Lookup 而建覆盖索引吗? | ❌ 不需要(因你要所有字段,不可能覆盖) |
🛠️ 建议行动项
-
确认表是否有聚簇索引 :
SELECT i.name, i.type_desc FROM sys.indexes i WHERE i.object_id = OBJECT_ID('dbo.app_contractlist_ext_three') AND i.type IN (1, 2); -- 1=clustered, 2=nonclustered- 如果没有 type=1 的索引 → 是堆表 → 建议加聚簇索引(如
id或(dt, id))
- 如果没有 type=1 的索引 → 是堆表 → 建议加聚簇索引(如
-
监控高频查询的执行计划 ,关注:
- 是否用了你的新索引?
- Key Lookup 的行数是否合理?
这样就能在"返回所有字段"的硬需求下,获得最佳性能。