简介
在SQL Server中,索引是一种增强式的存在,这意味着,即使没有索引,SQL Server仍然可以实现应有的功能。但索引可以在大多数情况下大大提升查询性能,在OLAP中尤其明显.要完全理解索引的概念,需要了解大量原理性的知识,包括B树,堆,数据库页,区,填充因子,碎片,文件组等等一系列相关知识,这些知识写一本小书也不为过。所以本文并不会深入讨论这些主题。
索引是什么
索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。
精简来说,索引是一种结构.在SQL Server中,索引和表(这里指的是加了聚集索引的表)的存储结构是一样的,都是B树,B树是一种用于查找的平衡多叉树.理解B树的概念如下图:
理解为什么使用B树作为索引和表(有聚集索引)的结构,首先需要理解SQL Server存储数据的原理.
在SQL SERVER中,存储的单位最小是页(PAGE),页是不可再分的。就像细胞是生物学中不可再分的,或是原子是化学中不可再分的最小单位一样.这意味着,SQL SERVER对于页的读取,要么整个读取,要么完全不读取,没有折中.
在数据库检索来说,对于磁盘IO扫描是最消耗时间的.因为磁盘扫描涉及很多物理特性,这些是相当消耗时间的。所以B树设计的初衷是为了减少对于磁盘的扫描次数。如果一个表或索引没有使用B树(对于没有聚集索引的表是使用堆heap存储),那么查找一个数据,需要在整个表包含的数据库页中全盘扫描。这无疑会大大加重IO负担.而在SQL SERVER中使用B树进行存储,则仅仅需要将B树的根节点存入内存,经过几次查找后就可以找到存放所需数据的被叶子节点包含的页!进而避免的全盘扫描从而提高了性能.
下面,通过一个例子来证明:
在SQL SERVER中,表上如果没有建立聚集索引,则是按照堆(HEAP)存放的,假设我有这样一张表:
现在这张表上没有任何索引,也就是以堆存放,我通过在其上加上聚集索引(以B树存放)来展现对IO的减少:
理解聚集和聚集索引
在SQL SERVER中,最主要的两类索引是聚集索引和非聚集索引。可以看到,这两个分类是围绕聚集这个关键字进行的.那么首先要理解什么是聚集.
聚集在索引中的定义:
为了提高某个属性(或属性组)的查询速度,把这个或这些属性(称为聚集码)上具有相同值的元组集中存放在连续的物理块称为聚集。
简单来说,聚集索引就是:
在SQL SERVER中,聚集的作用就是将某一列(或是多列)的物理顺序改变为和逻辑顺序相一致,比如,我从adventureworks数据库的employee中抽取5条数据:
当我在ContactID上建立聚集索引时,再次查询:
在SQL SERVER中,聚集索引的存储是以B树存储,B树的叶子直接存储聚集索引的数据:
因为聚集索引改变的是其所在表的物理存储顺序,所以每个表只能有一个聚集索引.
非聚集索引
因为每个表只能有一个聚集索引,如果我们对一个表的查询不仅仅限于在聚集索引上的字段。我们又对聚集索引列之外还有索引的要求,那么就需要非聚集索引了.
非聚集索引,本质上来说也是聚集索引的一种.非聚集索引并不改变其所在表的物理结构,而是额外生成一个聚集索引的B树结构,但叶子节点是对于其所在表的引用,这个引用分为两种,如果其所在表上没有聚集索引,则引用行号。如果其所在表上已经有了聚集索引,则引用聚集索引的页.
一个简单的非聚集索引概念如下:
可以看到,非聚集索引需要额外的空间进行存储,按照被索引列进行聚集索引,并在B树的叶子节点包含指向非聚集索引所在表的指针.
MSDN中,对于非聚集索引描述图是:
可以看到,非聚集索引也是一个B树结构,与聚集索引不同的是,B树的叶子节点存的是指向堆或聚集索引的指针.
通过非聚集索引的原理可以看出,如果其所在表的物理结构改变后,比如加上或是删除聚集索引,那么所有非聚集索引都需要被重建,这个对于性能的损耗是相当大的。所以最好要先建立聚集索引,再建立对应的非聚集索引.
聚集索引 VS 非聚集索引
前面通过对于聚集索引和非聚集索引的原理解释.我们不难发现,大多数情况下,聚集索引的速度比非聚集索引要略快一些.因为聚集索引的B树叶子节点直接存储数据,而聚集索引还需要额外通过叶子节点的指针找到数据.
还有,对于大量连续数据查找,非聚集索引十分乏力,因为非聚集索引需要在非聚集索引的B树中找到每一行的指针,再去其所在表上找数据,性能因此会大打折扣.有时甚至不如不加非聚集索引.
因此,大多数情况下聚集索引都要快于非聚集索引。但聚集索引只能有一个,因此选对聚集索引所施加的列对于查询性能提升至关紧要.
索引的使用
索引的使用并不需要显式使用,建立索引后查询分析器会自动找出最短路径使用索引.
但是有这种情况.当随着数据量的增长,产生了索引碎片后,很多存储的数据进行了不适当的跨页,会造成碎片(关于跨页和碎片以及填充因子的介绍,我会在后续文章中说到)我们需要重新建立索引以加快性能:
比如前面的test_tb2上建立的一个聚集索引和非聚集索引,可以通过DMV语句查询其索引的情况:
SELECT index_type_desc,alloc_unit_type_desc,avg_fragmentation_in_percent,fragment_count,avg_fragment_size_in_pages,page_count,record_count,avg_page_space_used_in_percent FROM sys.dm_db_index_physical_stats(DB_ID('AdventureWorks'),OBJECT_ID('test_tb2'),NULL,NULL,'Sampled')
我们可以通过重建索引来提高速度:
还有一种情况是,当随着表数据量的增大,有时候需要更新表上的统计信息,让查询分析器根据这些信息选择路径,使用:
UPDATE STATISTICS 表名
那么什么时候知道需要更新这些统计信息呢,就是当执行计划中估计行数和实际表的行数有出入时:
使用索引的代价
我最喜欢的一句话是”everything has price”。我们通过索引获得的任何性能提升并不是不需要付出代价。这个代价来自几方面.
1.通过聚集索引的原理我们知道,当表建立索引后,就以B树来存储数据.所以当对其进行更新插入删除时,就需要页在物理上的移动以调整B树.因此当更新插入删除数据时,会带来性能的下降。而对于非聚集索引,当更新表后,非聚集索引也需要进行更新,相当于多更新了N(N=非聚集索引数量)个表。因此也下降了性能.
2.通过上面对非聚集索引原理的介绍,可以看到,非聚集索引需要额外的磁盘空间。
3.前文提过,不恰当的非聚集索引反而会降低性能.
所以使用索引需要根据实际情况进行权衡.通常我都会将非聚集索引全部放到另外一个独立硬盘上,这样可以分散IO,从而使查询并行.
总结
本文从索引的原理和概念对SQL SERVER中索引进行介绍,索引是一个很强大的工具,也是一把双刃剑.对于恰当使用索引需要对索引的原理以及数据库存储的相关原理进行系统的学习.
T-SQL中的GROUP BY GROUPING SETS
最近遇到一个情况,需要在内网系统中出一个统计报表。需要根据不同条件使用多个group by语句.需要将所有聚合的数据进行UNION操作来完成不同维度的统计查看.
直到发现在SQL SERVER 2008之后引入了GROUPING SETS这个对于GROUP BY的增强后,上面的需求实现起来就简单多了,下面我用AdventureWork中的表作为DEMO来解释一下GROUPING SETS.
假设我现在需要两个维度查询我的销售订单,查询T-SQL如下:
而使用SQL SERVER 2008之后新增的GROUPING SETS语句,仅仅需要这样写:
值得注意的是,虽然上面使用GROUPING SETS语句和多个GROUP BY语句产生的结果是完全一样的,但顺序却完全不同。
GROUPING SETS,仅仅是语法糖?
从上面结果来看,使用GROUPING SETS仅仅是一个可以少写些代码的语法糖.但实际情况是,GROUPING SETS在遇到多个条件时,聚合是一次性从数据库中取出所有需要操作的数据,在内存中对数据库进行聚合操作并生成结果。而UNION ALL是多次扫描表,将返回的结果进行UNION操作,这也就是为什么GROUPING SETS和UNION操作所返回的数据顺序是不同的.
下面通过查看上面两个语句的IO和CPU来进行对比:
通过上面的图来看GROUPING SETS不仅仅只是语法糖.而是从执行原理上做出了改变.
对于GROUPING SETS来说,还经常和GROUPING函数联合使用,这个函数是反映目标列是否聚合,如何聚合则返回1,否则返回0,如下:
T-SQL查询进阶--详解公用表表达式(CTE)
简介
对于SELECT查询语句来说,通常情况下,为了使T-SQL代码更加简洁和可读,在一个查询中引用另外的结果集都是通过视图而不是子查询来进行分解的.但是,视图是作为系统对象存在数据库中,那对于结果集仅仅需要在存储过程或是用户自定义函数中使用一次的时候,使用视图就显得有些奢侈了.
公用表表达式(Common Table Expression)是SQL SERVER 2005版本之后引入的一个特性.CTE可以看作是一个临时的结果集,可以在接下来的一个SELECT,INSERT,UPDATE,DELETE,MERGE语句中被多次引用。使用公用表达式可以让语句更加清晰简练.
除此之外,根据微软对CTE好处的描述,可以归结为四点:
- 可以定义递归公用表表达式(CTE)
- 当不需要将结果集作为视图被多个地方引用时,CTE可以使其更加简洁
- GROUP BY语句可以直接作用于子查询所得的标量列
- 可以在一个语句中多次引用公用表表达式(CTE)
公用表表达式(CTE)的定义
公用表达式的定义非常简单,只包含三部分:
- 公用表表达式的名字(在WITH之后)
- 所涉及的列名(可选)
- 一个SELECT语句(紧跟AS之后)
在MSDN中的原型:
按照是否递归,可以将公用表(CTE)表达式分为递归公用表表达式和非递归公用表表达式.
非递归公用表表达式(CTE)
非递归公用表表达式(CTE)是查询结果仅仅一次性返回一个结果集用于外部查询调用。并不在其定义的语句中调用其自身的CTE
比如一个简单的非递归公用表表达式:
当然,公用表表达式的好处之一是可以在接下来一条语句中多次引用:
前面我一直强调“在接下来的一条语句中”,意味着只能接下来一条使用:
由于CTE只能在接下来一条语句中使用,因此,当需要接下来的一条语句中引用多个CTE时,可以定义多个,中间用逗号分隔:
递归公用表表达式(CTE)
递归公用表表达式很像派生表(Derived Tables ),指的是在CTE内的语句中调用其自身的CTE.与派生表不同的是,CTE可以在一次定义多次进行派生递归.对于递归的概念,是指一个函数或是过程直接或者间接的调用其自身,递归的简单概念图如下:
递归在C语言中实现的一个典型例子是斐波那契数列:
long fib(int n) { if (n==0) return 0; if (n==1) return 1; if (n>1) return fib(n-1)+fib(n-2); }
上面C语言代码可以看到,要构成递归函数,需要两部分。第一部分是基础部分,返回固定值,也就是告诉程序何时开始递归。第二部分是循环部分,是函数或过程直接或者间接调用自身进行递归.
对于递归公用表达式来说,实现原理也是相同的,同样需要在语句中定义两部分:
- 基本语句
- 递归语句
在SQL这两部分通过UNION ALL连接结果集进行返回:
比如:在AdventureWork中,我想知道每个员工所处的层级,0是最高级
这么复杂的查询通过递归CTE变得如此优雅和简洁.这也是CTE最强大的地方.
当然,越强大的力量,就需要被约束.如果使用不当的话,递归CTE可能会出现无限递归。从而大量消耗SQL Server的服务器资源.因此,SQL Server提供了OPTION选项,可以设定最大的递归次数:
还是上面那个语句,限制了递归次数:
所提示的消息:
这个最大递归次数往往是根据数据所代表的具体业务相关的,比如这里,假设公司层级最多只有2层.
总结
CTE是一种十分优雅的存在。CTE所带来最大的好处是代码可读性的提升,这是良好代码的必须品质之一。使用递归CTE可以更加轻松愉快的用优雅简洁的方式实现复杂的查询。
T-SQL查询进阶--深入浅出视图
简介
视图可以看作定义在SQL Server上的虚拟表.视图正如其名字的含义一样,是另一种查看数据的入口.常规视图本身并不存储实际的数据,而仅仅存储一个Select语句和所涉及表的metadata.
视图简单的理解如下:
通过视图,客户端不再需要知道底层table的表结构及其之间的关系。视图提供了一个统一访问数据的接口。
为什么要使用视图(View)
从而我们不难发现,使用视图将会得到如下好处:
- 视图隐藏了底层的表结构,简化了数据访问操作
- 因为隐藏了底层的表结构,所以大大加强了安全性,用户只能看到视图提供的数据
- 使用视图,方便了权限管理,让用户对视图有权限而不是对底层表有权限进一步加强了安全性
- 视图提供了一个用户访问的接口,当底层表改变后,改变视图的语句来进行适应,使已经建立在这个视图上客户端程序不受影响
视图(View)的分类
视图在SQL中可以分为三类
- 普通视图(Regular View)
- 索引视图(Indexed View)
- 分割视图(Partitioned View)
下面从这几种视图类型来谈视图
普通视图(Rugular View)
普通视图由一个Select语句所定义,视图仅仅包含其定义和被引用表的metadata.并不实际存储数据。MSDN中创建视图的模版如下:
CREATE VIEW [ schema_name . ] view_name [ (column [ ,...n ] ) ] [ WITH <view_attribute> [ ,...n ] ] AS select_statement [ WITH CHECK OPTION ] [ ; ] <view_attribute> ::= { [ ENCRYPTION ] [ SCHEMABINDING ] [ VIEW_METADATA ] }
参数还是比较少的,现在解释一下上面的参数:
ENCRYPTION:视图是加密的,如果选上这个选项,则无法修改.创建视图的时候需要将脚本保存,否则再也不能修改了
SCHEMABINDING:和底层引用到的表进行定义绑定。这个选项选上的话,则视图所引用到的表不能随便更改构架(比如列的数据类型),如果需要更改底层表构架,则先drop或者alter在底层表之上绑定的视图.
VIEW_METADATA:这个是个很有意思的选项.正如这个选项的名称所指示,如果不选择,返回给客户端的metadata是View所引用表的metadata,如果选择了这个选项,则返回View的metadata.再通俗点解释,VIEW_METADATA可以让视图看起来貌似表一样。View的每一个列的定义等直接告诉客户端,而不是所引用底层表列的定义。
WITH Check Option:这个选项用于更新数据做限制,下面会在通过视图更新数据一节解释.
当然了,创建视图除了需要符合上面的语法规则之外,还有一些规则需要遵守:
- 在View中,除非有TOP关键字,否则不能用Order By子句(如果你一意孤行要用Order by,这里有个hack是使用Top 100 percent…..)
- View在每个Schema中命名必须独一无二
- View嵌套不能超过32层(其实实际工作中谁嵌套超过两层就要被打PP了-.-)
- Compute,compute by,INTO关键字不允许出现在View中
- View不能建立在临时表上
- View不能对全文索引进行查询
建立View一个简单的例子:
视图建立完成后,就可以像访问表一样访问视图了:
在Management studio中,我创建视图的时候更喜欢用这样一种方法,将会便捷很多:
索引视图(Indexed View)
在谈到索引视图之前,我突然想起以前看过的一个漫画.话说咱们高端产品买不起,但是省吃俭用攒点钱买个IPhone装装高端总还是可以的吧:
其实索引视图也很类似,在普通的视图的基础上,为视图建立唯一聚集索引,这时这个视图就变成了索引视图.套用上面漫画的公式:视图+聚集索引=索引视图
索引视图可以看作是一个和表(Table)等效的对象!
SQL Server中的索引视图和Oracle中的Materialized View是一个概念.想要理解索引视图,必须先理解聚集索引。聚集索引简单来说理解成主键,数据库中中的数据按照主键的顺序物理存储在表中,就像新华字典,默认是按照ABCD….这样的方式进行内容设置。ABCD….就相当于主键.这样就避免了整表扫描从而提高了性能.因此一个表中只能有一个聚集索引。
对于索引视图也是,为一个视图加上了聚集索引后。视图就不仅仅再是select语句和表的metadata了,索引视图会将数据物理存在数据库中,索引视图所存的数据和索引视图中所涉及的底层表保持同步。
理解了索引视图的原理之后,我们可以看出,索引视图对于OLAP这种大量数据分析和查询来说,性能将会得到大幅提升。尤其是索引视图中有聚合函数,涉及大量高成本的JOIN,因为聚合函数计算的结果物理存入索引视图,所以当面对大量数据使用到了索引视图之后,并不必要每次都进行聚合运算,这无疑会大大提升性能.
而同时,每次索引视图所涉及的表进行Update,Insert,Delete操作之后,SQL Server都需要标识出改变的行,让索引视图进行数据同步.所以OLTP这类增删改很多的业务,数据库需要做大量的同步操作,这会降低性能。
谈完了索引视图的基本原理和好处与坏处之后,来看看在SQL Server中的实现:
在SQL Server中实现索引视图是一件非常,简单的事,只需要在现有的视图上加上唯一聚集索引.但SQL Server对于索引视图的限制却使很多DBA对其并不青睐:
比如:
- 索引视图涉及的基本表必须ANSI_NULLS设置为ON
- 索引视图必须设置ANSI_NULLS和QUOTED_INDETIFIER为ON
- 索引视图只能引用基本表
- SCHEMABINDING必须设置
- 定义索引视图时必须使用Schema.ViewName这样的全名
- 索引视图中不能有子查询
- avg,max,min,stdev,stdevp,var,varp这些聚合函数不能用
………………
还有更多…就不一一列举了,有兴趣的请自行Google之.
下面我来通过一个例子来说明索引视图:
假设在adventureWorks数据库中,我们有一个查询:
SELECT p.Name,s.OrderQty FROM Production.Product p inner join Sales.SalesOrderDetail s ON p.ProductID=s.ProductID
这个查询的执行计划:
这时,我建立视图并在这个视图上建立唯一聚集索引:
--建立视图 CREATE VIEW v_Test_IndexedView WITH SCHEMABINDING AS SELECT p.Name,s.OrderQty,s.SalesOrderDetailID FROM Production.Product p inner join Sales.SalesOrderDetail s ON p.ProductID=s.ProductID GO --在视图上建立索引 CREATE UNIQUE CLUSTERED INDEX indexedview_test1 ON v_Test_IndexedView(SalesOrderDetailID)
接下来,套用刘谦的台词:见证奇迹的时刻到了,我们再次执行之前的查询:
从上面这个例子中,可以体会到索引视图的强大威力,即使你的查询语句中不包含这个索引视图,查询分析器会自动选择这个视图,从而大大的提高了性能.当然,这么强力的性能,只有在SQL SERVER企业版和开发版才有哦(虽然我见过很多SQL Server的开发人员让公司掏着Enterprise版的钱,用着Express版的功能……)
分割视图(Partitioned View)
分割视图其实从微观实现方式来说,整个视图所返回的数据由几个平行表(既是几个表有相同的表结构,也就是列和数据类型,但存储的行集合不同)进行UNION连接(对于UNION连接如果不了解,请看我之前的博文)所获得的数据集.
分割视图总体上可以分为两种:
1.本地分割视图(Local Partitioned View)
2.分布式分割视图(Distributed Partitioned View)
因为本地分割视图仅仅是为了和SQL Server 2005之前的版本的一种向后兼容,所以这里仅仅对分布式分割视图进行说明.
分布式分割视图其实是将由几个由不同数据源或是相同数据源获得的平行数据集进行连接所获得的,一个简单的概念图如下:
上面的视图所获得的数据分别来自三个不同数据源的表,每一个表中只包含四行数据,最终组成了这个分割视图.
使用分布式分割视图最大的好处就是提升性能.比如上面的例子中,我仅仅想取得ContactID为8这位员工的信息,如果通过分布式视图获取的话,SQL Server可以非常智能的仅仅扫描包含ContactID为8的表2,从而避免了整表扫描。这大大减少了IO操作,从而提升了性能.
这里要注意的是,分布式分割视图所涉及的表之间的主键不能重复,比如上面的表A ContactID是1-4,则表B的ContactID不能是2-8这个样子.
还有一点要注意的是,一定要为分布式分割索引的主键加Check约束,从而让SQL Server的查询分析器知道该去扫描哪个表,下面来看个例子.
在微软示例数据库AdventureWorks数据库,我通过ContactID从前100行和100-200行的数据分别存入两个表,Employee100和Employee200,代码如下:
--create Employee100 SELECT TOP 100 * INTO Employee100 FROM HumanResources.Employee ORDER BY EmployeeID --create Employee200 SELECT * INTO Employee200 FROM (SELECT TOP 100 * FROM HumanResources.Employee WHERE EmployeeID NOT IN (SELECT TOP 100 EmployeeID FROM HumanResources.Employee ORDER BY EmployeeID) ORDER BY HumanResources.Employee.EmployeeID)AS e
这时来建立分布式分割视图:
这时我们对这个索引进行查询操作:
下面是执行计划:
通过上图可以看出,通过这种分割的方式,执行计划仅仅是扫描Employee200,从而避免了扫描所有数据,这无疑提升了性能.
所以,当你将不同的数据表之间放到不同的服务器或是使用RAID5磁盘阵列时,分布式分割视图则进一步会提升查询性能.
使用分布式分割视图能够在所有情况下都提升性能吗?那妥妥的不可能.使用这种方式如果面对的查询包含了聚合函数,尤其是聚合函数中还包含distinct。或是不加where条件进行排序.那绝对是性能的杀手。因为聚合函数需要扫描分布式分割视图中所有的表,然后进行UNION操作后再进行运算.
通过视图(View)更新数据
通过视图更新数据是我所不推荐的.因为视图并不能接受参数.我更推荐使用存储过程来实现.
使用View更新数据和更新Table中数据的方式完全一样(前面说过,View可以看作是一个虚拟表,如果是索引视图则是具体的一张表)
通过视图来更新数据需要注意以下几点
1.视图中From子句之后至少有一个用户表
2.View的查询无论涉及多少张表,一次只能更新其中一个表的数据
3.对于表达式计算出来的列,常量列,聚合函数算出来的列无法更新
4.Group By,Having,Distinct关键字不能影响到的列不能更新
这里说一下创建View有一个WITH Check Option选项,如果选择这个选项,则通过View所更新的数据必须符合View中where子句所限定的条件,比如:
我创建一个View:
视图(View)中的几个小技巧
1.通过视图名称查到视图的定义
2.前面说过,普通视图仅仅存储的是select语句和所引用表的metadata,当底层表数据改变时,有时候视图中表的metadata并没有及时同步,可以通过如下代码进行手动同步
EXEC sp_refreshview 视图名称
视图(View)的最佳实践
这是我个人一些经验,欢迎补充
- 一定要将View中的Select语句性能调到最优(貌似是废话,不过真理都是废话…)
- View最好不要嵌套,如果非要嵌套,最多只嵌套一层
- 能用存储过程和自定义函数替代View的,尽量不要使用View,存储过程会缓存执行计划,性能更优,限制更少
- 在分割视图上,不要使用聚合函数,尤其是聚合函数还包含了Distinct
- 在视图内,如果Where子句能加在视图内,不要加在视图外(因为调用视图会返回所有行,然后再筛选,性能杀手,如果你还加上了order by…..)
总结
文中对视图的三种类型进行了详解.每种视图都有各自的使用范围,使用得当会将性能提升一个档次,而使用不当反而会拖累性能.
我想起一句名言:“everything has price,always trade-off”…..