前言

索引是关系数据库中最重要的对象之中的一个,他能显著降低磁盘I/O及逻辑读取的消耗,并以此来提升 SELECT 语句的查找性能。但它是一把双刃剑。使用不当反而会影响性能:他须要额外的空间来存放这些索引信息。而且当数据更新时须要一些额外开销来保持索引的同步。

形象的来说索引就像字典里的文件夹,你要查找某一个字的时候能够依据它的比划/拼音先在文件夹中找到相应的页码范围,然后在该范围中找到这个字。假设没有这个文件夹(索引),你可能须要翻遍整本字典来找到要找的字。

SQL Server 中的索引以 B-Tree 的形式存储,例如以下图:

建立聚集索引(clustered index)来改进性能

RDBMS 随着数据的增长都会面临查询性能的下降,索引就是专门设计来解决问题的。聚集索引是全部索引的基础。没有它数据表就是一个堆(heap)。聚集索引决定了数据的物理存储形态,所以一张表上仅仅能有一个聚集索引。

SQL Server 的 sys.partitions 系统视图中记录着全部聚集索引的信息(它们的 Index_ID为1)。

聚集索引能够包括多个字段(列),通常应挑选绝大多数查询语句中常常涉及到的筛选字段,而且事先了解下面几点:

  • 字段应当包括大量的非反复的值。比如:身份证号
  • 默认情况下主键字段将自己主动建立聚集索引。但这不是必须的,你能够手工改动为非聚集索引(non-clustered index)
  • 字段常常參与筛选,即:常常在 WHERE, JOIN, ORDER BY, GROUP BY 语句中使用
  • 字段常常參与比較。即:常常參与 >, <, >=, <=, BETWEEN, IN 运算
  • 字段长度越短越好

另外在可能的情况下建议对聚集索引实施下面规则:

  • 包括的字段都设为唯一(unique)且非空(NOT NULL)
  • 包括字段的长度越短越好,包括的字段越少越好
  • 每张表都有聚集索引,而且把 WHERE 中常常使用到的字段作为该聚集索引的字段
  • 尽量避免在 varchar 列上建立聚集索引

我们来做一次10w条数据的性能比較(測试数据的生成SQL请參照附录):

SELECT OrderDate,Amount,Refno FROM ordDemo WHERE Refno<3

索引建立前的运行计划:

CREATE CLUSTERED INDEX idx_refno ON ordDemo(refno)
GO
--再次运行同样的查询语句
SELECT OrderDate,Amount,Refno FROM ordDemo WHERE Refno<3
GO

建立索引后的运行计划:

通过对照我们可发现I/O 消耗从 0.379421 减少为 0.0571991,而且从 Table Scan 处理转变为 Index Seek。

建立非聚集索引(non-clustered index)来改善性能

上面提到了索引能有效改善查询性能,但因为一张表仅仅能有一个聚集索引。而一个聚集索引通常无法包括全部必要的列,所以 SQL Server 同意我们建立非聚集索引来实现这个需求。

【 SQL Server 2005 及之前的版本号同意建立249 个非聚集索引;SQL Server 2008 及 SQL Server 2012 同意999个非聚集索引】

通常当你在某一个字段上建立一个唯一键(unique key)的时候。SQL Server 会自己主动在该列上建立一个非聚集索引。

sys.partitions 系统表中存放着非聚集索引的相关信息(Index_ID>1)。

在为某张表建立非聚集索引之前请先确认两点:该表是否真的须要非聚集索引?该表是否有合适的字段来建立非聚集索引?

这是由于索引建得不好不但不能带来性能的提高,还会花费额外的空间来存放索引并产生额外的 I/O 操作!

建立非聚集索引选择字段时应遵循下面规则:

  • 字段应当包括大量的非反复的值。
  • 字段常常參与等值(=)运算
  • 字段常常參与筛选,即:常常在 JOIN, ORDER BY, GROUP BY 语句中使用

我们继续之前的測试。来看看非聚集索引带来的速度提升:

SELECT OrderDate FROM ordDemo
WHERE OrderDate='2011-11-28 20:29:00.000'
GO

运行计划例如以下图:

建立非聚集索引,并再次运行查询:

CREATE NONCLUSTERED INDEX idx_orderdate
on ordDemo(orderdate)
GO SELECT OrderDate FROM ordDemo
WHERE OrderDate='2011-11-28 20:29:00.000'
GO



比較结果很明显,非聚集索引建立之后 I/O Cost, CPU Cost, Operator Cost 等消耗大幅下降。

在我们的样例中因为OrderDate 字段并不在聚集索引中,所曾经一次的查询被解释成一个index scan。当我们在OrderDate 上建立一个非聚集索引后,查询将利用起该索引并解释成 index seek。

随着表的数据越来越多,用来存放非聚集索引的空间也会越来越大。并逐渐对性能造成影响。遇到这样的情况能够把非聚集索引建立在独立的数据库文件或文件组(filegroup)中,从而降低对同一个文件的 I/O 操作压力。

合理的索引覆盖来改善性能

运行以下的測试 SQL

SELECT OrderDate,OrderID FROM ordDemo
WHERE OrderDate='2011-11-28 20:29:00.000'
GO

观察运行计划后你会发现查询被解析为 index scan,而不是先前的 index seek?这是由于我们已建立的两个索引都没有包括 OrderId 字段。

把 non-clustered Index 删掉了,又一次建一下(把OrderId 字段也作为索引的字段)

CREATE NONCLUSTERED INDEX idx_orderdate_orderId
on ordDemo(orderdate DESC,OrderId ASC)
GO

再次运行查询,运行计划例如以下图

查询不出意料的再次被解析为 index seek。

注意:

一个索引中最多包括16个字段,而且这些字段的长度必须小于 900 byte。

下面类型不能作为索引的keyword段(text, ntext, image, nvarchar(max), varchar(max), varbinary(max))

调整索引的包括字段(including columns)来提高性能

索引的包括字段的概念起源自 SQL Server 2005,SQL Server 2008 及 2012 也具备该功能。它同意你在非聚集索引中包括非键值(non-key)字段,这些字段不会记入索引的大小(这样我们也就不太会促发上文提到的索引字段上限)。另外这些字段的类型能够是除 text, ntext, image 之外的不论什么类型。

在前文的測试案例中 OrderId 并非一个keyword段,由于他并没有在 WHERE 子句中进行筛选,所以把他作为索引的keyword段并不合适。如今我们用 INCLUDE 来把它建立为包括字段:

--删除前文的索引
DROP INDEX idx_orderdate_orderId ON ordDemo
GO --重建索引
CREATE NONCLUSTERED INDEX idx_orderdate_Included
on ordDemo(orderdate DESC)
INCLUDE(OrderID)
GO --再次查询
SELECT OrderDate,OrderID FROM ordDemo
WHERE OrderDate='2011-11-28 20:29:00.000'
GO

运行计划例如以下图:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc3FsY2hlbg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">

从性能上来说本节的优化结果与上一节的差点儿一致,但採用了包括字段索引(include column index) 后,你受到的限制更小,并伴随着索引keyword段的降低,索引的占用也变小查询起来更高效。

总结下区分索引keyword段及包括字段的基本原则:

  • WHERE, ORDER BY, GROUP BY, JOIN-ON 中的使用到的字段适用于keyword段
  • SELECT, HAVING 中的使用到的字段适用于包括字段

使用过滤索引(filtered index)来提高性能

过滤索引起源自 SQL Server 2008 ,SQL Server 2012 也具备该功能,你能够把它看成一个带着 WHERE 子句的非聚集索引。

适当地使用能降低索引的存储尺寸及维护消耗,同一时候提高查询性能。

常规的索引都是对整张表的每条数据进行索引,而过滤索引只对满足特定条件的记录进行索引,这个特定条件在建立过滤索引时通过 WHERE 子句来定义。

类似下面的场景你能够考虑採用过滤索引:

一张包括多年数据的巨型表。实际使用中仅查询当年数据。

一张记录产品类别的表,包括很多过期不再使用的类别。

一个订单表。包括OrderStartDate 及 OrderEndDate 字段。当订单完毕时更新OrderEndDate,其它情况为 null。你能够在 OrderEndDate 上建立过滤索引,这样当你须要查询哪些订单未完毕时能够利用。

在建立过滤索引时须要进行一些设定:

  • ARITHABORT = ON
  • CONCAT_NULL_YIELDS_NULL = ON
  • QUOTED_IDENTIFIER = ON
  • ANSI_WARNINGS = ON
  • ANSI_NULLS = ON
  • ANSI_PADDING = ON
  • NUMERIC_ROUNDABORT = OFF
来看一下演示样例:
SET ANSI_NULLS ON
SET ANSI_PADDING ON
SET ANSI_WARNINGS ON
SET ARITHABORT ON
SET CONCAT_NULL_YIELDS_NULL ON
SET QUOTED_IDENTIFIER ON
SET NUMERIC_ROUNDABORT OFF
GO CREATE NONCLUSTERED INDEX idx_orderdate_Filtered
on ordDemo(orderdate DESC)
INCLUDE(OrderId)
WHERE OrderDate = '2011-11-28 20:29:00.000'
GO SELECT OrderDate,OrderID FROM ordDemo WHERE OrderDate='2011-11-28 20:29:00.000'
GO

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc3FsY2hlbg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">

I/O 消耗从上一节的0.0078751 降低为 0.003125。优化效果很显著。

使用列存储索引(columnstore index)来提高性能

眼下为止我们讨论的都是行存储索引(rowstore index),SQL Server 2012 開始支持列存储索引。

行存储索引在数据页(data page)中保存数据行,列存储索引在数据页中保存数据列。

如果我们有一张表(tblEmployee)。包含 empId, FirstName, LastName 三列。行存储索引/列存储索引表现为下面存储形式:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc3FsY2hlbg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">

显然当你须要对某几列值进行查找筛选的时候,列存储索引须要訪问的数据页更少,从而减少了I/O开销,并因此提高了运行效率。

在你决定採用列存储索引之前建议你确认一下3点:

  • 你的数据表能否够设定为仅仅读(read-only)
  • 你的数据表是否很巨大(百万级以上)
  • 假设你的数据库是个OLTP。能否同意你切换(开/关)列存储索引

假设以上3点的答案都是OK的。那么你能够開始使用列存储索引了。只是你还会受到下面限制:

  • 你不能包括1024个以上字段
  • 字段类型仅仅能是下面几种:

int

big int

small int

tiny int

money

smallmoney

bit

float

real

char(n)

varchar(n)

nchar(n)

nvarchar(n)

date

datetime

datetime2

small datetime

time

datetimeoffset (precision <=2)

decimal 或 numeric (precision <=18)

好。我们来试验一下列存储索引:

运行下面的代码,依据输出的运行计划能够发现它已经利用了我们先前建立的聚集索引(行存储索引)。

SELECT
Refno
,sum(Amount) as SumAmt
,avg(Amount) as AvgAmt
FROM
ordDemo
WHERE
Refno>3
Group By
Refno
Order By
Refno
GO

接着我们把已经存在的行存储索引删除,建立列存储索引:

DROP INDEX idx_refno ON ordDemo

CREATE NONCLUSTERED COLUMNSTORE INDEX
idx_columnstore_refno
ON ordDemo (Amount,refno)

再次运行同样的查询语句,运行计划例如以下图:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc3FsY2hlbg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">

通过比較。我们能够发现I/O消耗显著下降:)

注意:因为建立了列存储索引,此时该表是仅仅读的,假设你要恢复成可写的状态必须删除这个列存储索引!

附录

生成測试数据的SQL代码:

--建表
CREATE TABLE ordDemo (OrderID INT IDENTITY, OrderDate DATETIME,Amount MONEY, Refno INT)
GO --插入 100000 条測试数据
INSERT INTO ordDemo (OrderDate, Amount, Refno)
SELECT TOP 100000
DATEADD(minute, ABS(a.object_id % 50000 ), CAST('2011-11-04' AS DATETIME)), ABS(a.object_id % 10), CAST(ABS(a.object_id % 13) AS VARCHAR)
FROM sys.all_objects a
CROSS JOIN sys.all_objects b
GO

SQL Server 性能调优2 之索引(Index)的建立的更多相关文章

  1. SQL Server 性能调优培训引言

    原文:SQL Server 性能调优培训引言 大家好,这是我在博客园写的第一篇博文,之所以要开这个博客,是我对MS SQL技术学习的一个兴趣记录. 作为计算机专业毕业的人,自己对技术的掌握总是觉得很肤 ...

  2. sql server 性能调优之 资源等待PAGELATCH

    一.概述 在前几章介绍过 sql server 性能调优资源等待之PAGEIOLATCH,PAGEIOLATCH是出现在sql server要和磁盘作交互的时候,所以加个IO两个字.这次来介绍PAGE ...

  3. sql server 性能调优之 资源等待 LCk

    一.  概述 这次介绍实例级别资源等待LCK类型锁的等待时间,关于LCK锁的介绍可参考 “sql server 锁与事务拨云见日”.下面还是使用sys.dm_os_wait_stats 来查看,并找出 ...

  4. sql server 性能调优之 CPU消耗最大资源分析1 (自sqlserver服务启动以后)

    一. 概述 上次在介绍性能调优中讲到了I/O的开销查看及维护,这次介绍CPU的开销及维护, 在调优方面是可以从多个维度去发现问题如I/O,CPU,  内存,锁等,不管从哪个维度去解决,都能达到调优的效 ...

  5. sql server性能调优

    转自:https://www.cnblogs.com/woodytu/tag/%E6%80%A7%E8%83%BD%E8%B0%83%E4%BC%98%E5%9F%B9%E8%AE%AD/defaul ...

  6. CPU开销sql server 性能调优

    sql server 性能调优 CPU开销分析 一. 概述 上次在介绍性能调优中讲到了I/O的开销查看及维护,这次介绍CPU的开销及维护, 在调优方面是可以从多个维度去发现问题如I/O,CPU, 内存 ...

  7. [转]SQL Server 性能调优(io)

      目录 诊断磁盘io问题 常见的磁盘问题 容量替代了性能 负载隔离配置有问题 分区对齐配置有问题 总结 关于io这一块,前面的东西如磁盘大小,磁盘带宽,随机读取写入,顺序读取写入,raid选择,DA ...

  8. sql server 性能调优 资源等待之网络I/O

    原文:sql server 性能调优 资源等待之网络I/O 一.概述 与网络I/O相关的等待的主要是ASYNC_NETWORK_IO,是指当sql server返回数据结果集给客户端的时候,会先将结果 ...

  9. sql server 性能调优 资源等待之内存瓶颈的三种等待类型

    原文:sql server 性能调优 资源等待之内存瓶颈的三种等待类型 一.概述 这篇介绍Stolen内存相关的主要三种等待类型以及对应的waittype编号,CMEMTHREAD(0x00B9),S ...

随机推荐

  1. linux中fork()函数详解(搬砖)

    一.fork入门知识 一个进程,包括代码.数据和分配给进程的资源.fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同, ...

  2. 自己动手写shell命令之more

    unix下more命令的简单实现: #include <stdio.h> #define PAGELEN 24 #define LINELEN 512 int do_more(FILE * ...

  3. vargrind 安卓apk

    上层为安卓, 下层为调用c/c++ 库 1.将vargind 按官网方法下载源代码编译  得Inst文件夹 2.通过win 下安卓sdk 中 platform-tools 中的adb push Ins ...

  4. 怎样在Web项目中的service业务层获取项目根路劲

    这里我们有两个前提 1.没有使用struts2框架.没有使用servlet,无法给service层传递request对象. 2.使用了Spring框架. 那你可能问.会有这样的情况吗?答案是有的,比方 ...

  5. MySQL事件调度器Event Scheduler

    我们都知道windows的计划任务和linux的crontab都是用来实现一些周期性的任务和固定时间须要运行的任务. 在mysql5.1之前我们完毕数据库的周期性操作都必须借助这些操作系统实现. 在m ...

  6. CentOS6.3升级GCC到GCC4.8.2

    server上安装的GCC版本号过旧.已经不满足个人使用的版本号需求,故决定对其进行升级操作.由当前版本号3.4.6升级到4.8.2.然受权限制约.仅仅能安装到个人文件夹.因此假设您的server能够 ...

  7. php利用href进行页面传值的正确姿势

    首先在a.php中 <?php $a = "world"; echo "<a href='b.php?m=$a'>删除</a>"; ...

  8. [NOIP2017] 逛公园 解题报告(DP)

    我很不想说 在我的AC代码上我打了表,但实在没有办法了.莫名的8,9个点RE.然而即便是打表...也花了我很久. 这大概是NOIP2017最难的题了,为了让不懂的人更容易理解,这篇题解会比较详细 我的 ...

  9. 安卓-活动Activity

    Android有4大组件,活动 Activity,服务 Service ,广播接收器 Brostcast receiver,内容提供器 Content Provider 安卓活动的生命周期有7种, o ...

  10. word2tex之类的问题

    首先就是这个word2tex一般是在word和tex文本互相转换的时候用的. 以前win7下用的chikrii忘了使用方法.. 之后再win10下用的excel2tex,但是转换时候总感觉不如word ...