SQL SERVER 索引
一、索引(Index)
在 SQL Server 中,索引(Index) 是一种数据库对象,它基于表或视图的一列或多列创建,其核心目的是极大地提高数据检索操作的速度,就像书籍的目录可以帮助你快速找到特定内容一样。
索引(无论是聚集还是非聚集)底层是 B+树。所有键值都按指定顺序(升序或降序)排列在叶子节点中。
索引健支持的数据类型
1、索引健的数据类型:
索引健的数据类型是由其定义所基于的“索引列”(Index Key Columns)的数据类型直接决定的。
举例:
ID列 是int 类型,那么 ID列的索引类型就是int类型
Name列 是nvarchar(n)类型,那么Name列的索引就是nvarchar(n)类型
2、索引健支持的数据类型
| 数据类型 | 说明 | 
|---|---|
| 整数类型 | bit,tinyint ,smallint,int,bigint  ✅ 强烈推荐:尤其是 int和 bigint,常用于自增主键,性能极佳 | 
| 精确数值类型 | decimal(p,s), numeric(p,s) ✅ 支持: 注意 p(精度)不要过大 | 
| 日期和时间类型 | date, time, datetime2(n), datetimeoffset(n), smalldatetime, datetime  ✅ 推荐 | 
| 固定长度字符类型 | char(n), nchar(n) ✅ 支持,适合存储代码、缩写等短字符串 | 
| 可变长度字符类型 | varchar(n), nvarchar(n)✅ 常用:适合名称、地址等。关键限制:索引键的最大总大小为 900 字节 | 
| 唯一标识符 | uniqueidentifier ✅ 支持,用于全局唯一 id (guid)。虽然性能不如 int,但功能上完全支持。 | 
3、索引不推荐的数据类型
1、大对象类型: text, ntext, image
2、大值类型: varchar(max), nvarchar(max), varbinary(max)
3、极少不同值的列: bit(男/女)、状态标志列
二、索引分类
1、聚集索引
表中数据行的物理存储顺序与索引的顺序是一致的,这种索引我们将它成为聚集索引。表数据本身就被组织在聚集索引的 B+ 树的叶节点上。
2、非聚集索引
表中数据行的物理存储顺序与索引顺序不一致的时候,像这种快速查找数据的索引我们将它成为非聚集索引。
| 特性 | 聚集索引 | 非聚集索引 | 
|---|---|---|
| 数量限制 | 每表仅有一个 | 每表可有多个 | 
| 数据存储 | 叶子节点直接存储完整的行数据 | 叶子节点存储索引列值 + 主键值 | 
| 物理顺序 | 数据按索引顺序物理排序 | 索引逻辑连续,但对应的数据物理上无序 | 
| 查询效率 | 主键查询极快,无需回表 | 通过索引找到主键后,常需回表查询完整数据 | 
| 范围查询 | 极高效,可顺序读取连续数据块 | 效率较低,需多次随机I/O回表 | 
| 键值选择 | 通常使用主键 | 基于非主键的查询列 | 
对于主键查询或覆盖索引的查询,数据库只需遍历一次聚集索引的B+树,在叶子节点就能拿到全部数据,过程非常直接。相比之下,非聚集索引的叶子节点只存有主键值,查到所需数据的主键后,还需要拿着这些主键回到聚集索引里再查一次,这个过程称为“回表”,多了一次B+树的遍历,自然更慢
三、索引创建和使用方式
1、聚集索引
-- 语法
CREATE CLUSTERED INDEX index_name ON table_name (column_name [ASC | DESC]);
-- 案例
-- 对Users表的userid字段创建名称为IX_Users_UserID的聚集索引
CREATE CLUSTERED INDEX IX_Users_UserID on users (userid asc)
2、非聚集索引
-- 对Users表的Name字段创建名字为 IX_Users_Name的非聚集索引
-- 可以省略 NONCLUSTERED 关键字,默认情况下CREATE INDEX 创建的是非聚集索引。
CREATE NONCLUSTERED INDEX  IX_Users_Name ON Users (Name) 
3、联合索引(复合索引)
-- 针对Users表的 Age+City字段创建一个联合索引
CREATE INDEX IX_Users_age_city ON user (age, city);
这个索引适用于如下查询:
SELECT * FROM users WHERE age = 25 AND city = '永州';
SELECT * FROM users WHERE age = 25; -- 只用到了 age,也能走索引(最左前缀原则)
-- 注意:下面这句不能有效使用该索引(除非有 INCLUDE 或优化器特殊处理)
SELECT * FROM users WHERE city = '永州'; -- 跳过了 age,无法使用联合索引
对于创建的多列联合索引(如 (A, B, C)),只有当查询条件从索引的最左边列开始,并连续使用前面的列时,才能有效利用该索引
✅ 可用索引:WHERE A = 1、WHERE A = 1 AND B = 2、WHERE A = 1 AND B > 2 AND C = 3
❌ 不可用索引:WHERE B = 2、WHERE C = 3、WHERE A = 1 AND C = 3(跳过 B)
❌ 不可用索引:WHERE A = 1  OR B=2   ⚠️ 特别注意:联合索引对 OR 无效
3、唯一索引
-- 创建唯一索引可以是聚集或者非聚集-- 创建唯一聚集索引:
-- 对Users表的UserId字段创建名称为IX_Users_UserId的唯一聚集索引
CREATE UNIQUE CLUSTERED INDEX  IX_Users_UserId ON Users (UserId) -- 创建唯一非聚集索引:
-- 对Users表的Name字段创建名称为IX_Users_Name的唯一非聚集索引
CREATE UNIQUE NONCLUSTERED INDEX  IX_Users_Name ON Users (Name) -- 创建唯一非聚集索引
-- 可以省略 NONCLUSTERED 关键字,默认情况下CREATE INDEX 创建的是非聚集索引
CREATE UNIQUE INDEX IX_Users_Name ON Users (Name) 
案例1:带条件的唯一索引
--对Users表,在DeleteTime字段为空的数据中,为Name字段添加唯一索引
CREATE UNIQUE INDEX IX_Users_Name ON Users (Name) 
WHERE DeleteTime is null
举例:有以下Users表
| ID | Name | DeleteTime | 
|---|---|---|
| 1 | 张三 | 2022-02-10 | 
| 2 | 李四 | NULL | 
此时想插入Name为李四的数据,会报错:无法插入。
但可以插入Name为张三的数据,因为张三这一行的DeleteTime列数据不为Null
案例2:唯一联合索引
--对Users表,为Name+Email字段添加唯一联合索引
CREATE UNIQUE INDEX  Index_Users_Name  ON Users (Name,Email)  
| ID | Name | DeleteTime | |
|---|---|---|---|
| 1 | 张三 | abc@qq.com | NULL | 
此时想插入一条Name为 张三,Email为 abc@qq.com的数据是不行的
但可以插入一条Name为 张三,Email为除abc@qq.com以外的任意值的数据
也可以插入一条Email为abc@qq.com,Name为除张三以外的任意值的数据
其实就是将Name和Email看成一个字段,两个字段共同组成一个唯一
案例3:带条件的唯一联合索引
-- 针对Users表,在DeleteTime字段为空的数据中,为Name+Email字段添加唯一联合索引
-- 同时排除为DeleteTime不为空的数据添加唯一联合索引
-- 意思就是只为DeleteTime为空的数据添加唯一联合索引
CREATE UNIQUE INDEX  Index_Users_Name  ON Users (Name,Email)  
WHERE DeleteTime  is null
4、覆盖索引
-- 在 Users 表的 Status 列上创建一个非聚集索引,名称为:DEX IX_Users_Status,
-- 并且将 UserName、Email、CreatedDate 这三个列作为包含列(Included Columns) 
-- 存储在索引的叶子节点中,以便查询时无需回表(Key Lookup),
-- 直接从索引中获取所有所需数据。
CREATE NONCLUSTERED INDEX IX_Users_Status
ON Users (Status) 
INCLUDE (UserName, Email, CreatedDate);-- ON Users (Status) 作用:指定索引建立在 Users 表的 Status 列上-- INCLUDE (UserName, Email, CreatedDate) 作用如下:
-- 将 UserName、Email、CreatedDate 这三个列作为非键列(Included Columns)
-- 添加到索引的叶子节点中。
-- 这些列不参与索引的排序和查找逻辑,但会物理存储在索引页中
-- 目的是让查询可以直接从索引中获取这些列的值,避免回表查询(Key Lookup)
覆盖索引的优势: 假设你经常执行以下查询:
SELECT UserName, Email, CreatedDate
FROM Users
WHERE Status = 'Active';
如果没有 INCLUDE,执行过程是:
- 使用索引找到 
Status = 'Active'的行(通过Status索引)。 - 然后回表去主数据页查找 
UserName、Email、CreatedDate的值。 - 回表操作是I/O 密集型,性能较差。
 
而使用 INCLUDE 后,执行过程是:
- 索引叶子节点已经包含了 
UserName、Email、CreatedDate - 查询可以直接从索引中获取所有数据,无需回表。
 - 这种索引称为 覆盖索引
 
四、SQL语句优化
1、Like优化
LIKE 查询是否使用索引取决于通配符位置
✅ --可用索引:LIKE '张%'  以固定字符串开头,支持前缀匹配select * from Users where Name like '张%'✅ --可用索引:LIKE '张_'   以固定字符串开头,支持前缀匹配select * from Users where Name like '张_'🟨 --视情况而定:LIKE '_晶'(单字符模糊)在某些情况下仍可使用索引,但效率较低select * from Users where Name like '_晶'❌ --不可用索引:LIKE '%晶' 或 LIKE '%张%'   以 % 开头,无法确定起始位置select * from Users where Name like LIKE '%晶'
2、OR 优化
如果where条件中有or  则要求or的所有字段都必须有索引,否则不能用到索引
✅-- 如果Name和Age字段都创建了单列索引 以下查询才能用道索引
❌-- 如果只有Name或者只有Age字段创建了索引,则不能用到索引
SELECT * FROM Users WHERE Name = 'A' OR Age= 16
用IN替代OR
-- 不推荐(难读且不易优化)
WHERE Name = 'A' OR Name = 'B' OR Name = 'C'✅ -- 推荐(清晰且易用索引)
WHERE Name IN ('A', 'B', 'C')
3、IN 优化
IN 后面是表达式或函数 → 索引失效!
❌ -- 因为对字段用了函数 YEAR(),无法使用 CreateDate 上的索引
SELECT * FROM T_UserInfo WHERE YEAR(CreateDate) IN (2020, 2021, 2022)✅ --应改为范围查询:
WHERE CreateDate >= '2020-01-01' AND CreateDate < '2023-01-01'
4、IS NULL 值优化
应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描(聚集索引扫描或表扫描)
❌ -- 不能用到索引。
select id from t where age is null✅ --优化方案:此例可以在age上设置默认值0,确保表中age列没有null值,然后这样查询
select id from t where age = 0
5、<> 或 != 操作符 优化
尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。
当你使用!=或<>时,你是在寻找“不等于某个值”的所有行。这个集合通常是表中绝大部分的数据。对于数据库来说,扫描整个索引或直接扫描整个表可能比逐个查找并排除那个特定值更高效
