DML操作符包括增删改查等操作方式。
insert into Person.Address
(AddressLine1,
AddressLine2,
City,
StateProvinceID,
PostalCode,
rowguid,
ModifiedDate)
values(
N'1313 Mockingbird Lane',
N'Basement',
N'Springfield',
79,
N'02134',
NEWID(),
GETDATE()
)
如此简单的INSERT语句,执行计划比前面的SELECT语句还要复杂,表中有自增列、外键约束和空间数据列。
出现新的操作符:常量扫描(Constant Scan),这个操作符会引入一个常量到查询中,由它创建了一列空行以便后面两个操作符有位置可以添加他们的输出。
这两个操作符一个是标量计算(Compute Scalar),调用getidentity函数,用于产生自增值和标量数据,当INSET失败时,可以通过这个值获取失败信息;另一个操作是标量(Scalar)操作,它会产生一个序列用于新的uniqueidentifier值(因为表中有NEWID()函数),同时会产生GETDATE()函数所需要的日期和时间,接下来就是聚集索引插入操作了,这个执行计划中最大开销的部分,然后进入Nested Loops中,由于有外键,所以需要额外关联一个表StateProvince获取与之关联的外键StateProvinceID,经过关联后产生一个新的表达式,用于被下一个操作符Assert检测,这个Assert操作是否存在特定的条件,如果限制条件不存在,就把符合条件的数据插入到Person.Address.StateProvinceID列,然后结束INSET操作。
2.UPDATE
update Person.Address set City='Munro',ModifiedDate=GETDATE()
where City='Monroe'
最右边是一个非聚集索引扫描,并不是什么高效的操作。
可以进行索引策略来提高性能。让优化器能快速定位City=‘Monroe’的那些数据。查出数据后,进入Top操作符。在Update语句中,TOP操作强制控制行数,但在本例中没有控制,因为UPDATE语句中没有用到Top条件。接下来是表假脱机(Talbe Spool)这个操作会把需要UPDATE的数据移到TempDB的隐藏临时对象中,这个操作有利于回滚操作。
然后进入一系列的标量计算操作符中。在这里主要用于评估表达式和计算表,比如GETDATE()函数生成的值。
最终进入UPDATE语句的核心步骤:聚集索引更新。由于表上有聚集索引,而聚集索引实际包含整个表,所以UPDATE操作会引起聚集索引的更新,最后一个就是标识UPDATE操作完成的操作符。
本例提升性能最重要的方式就是降低开销最大的非聚集索引扫描操作消耗的开销,可以通过调整列上的索引来提升。
3.DELETE
begin tran
delete from Person.EmailAddress where BusinessEntityID=42
rollback tran
由于没有聚集索引,因此只需要把聚集索引的键删除即可。
二:复杂查询
包含存储过程、临时表、表变量、MERGE语句等的执行计划。
1.存储过程
create procedure Sales.spTaxRateByState
@CountryRegionCode nvarchar(3)
as
set nocount on;
select st.SalesTaxRateID,
st.Name,
st.TaxRate,
st.TaxType,
sp.Name as StateName
from Sales.SalesTaxRate st
join Person.StateProvince sp on st.StateProvinceID =sp.StateProvinceID
where sp.CountryRegionCode =@CountryRegionCode
order by StateName
从右到左分析,第一个操作符是聚集索引扫描,用于获取Tax Rate的数据,这个可以从tooltips上查看。接下来通过参数查找Person.StateProvince上的表符号[sp].CountryRegionCode=@CountryRegionCode条件的,也就是调用过程中实际传入的数据sp.CountryRegionCode='US',可以看出由于表Person.StateProvince上有筛选条件,所以出现了聚集索引查找,而表SalesTaxRate上没有筛选条件,所以只能进行聚集索引扫描
然后以StateProvince表为inner set,SalesTaxRate为outer set进行Nested Loops关联,由于两表的数据量较少,选择了Nested Loops关联更为高效。
关联之后就是排序操作,排序是由于StateProvince.Name导致的,代码中的Order By使用的是别名,而真正的排序是基于基础表的。这个表的聚集索引在StateProvinceID,所以当使用Name排序时,优化器会引入一个Sort操作符,开销很大。达到了51%,要优化,要在Sort上做文章,比如调整聚集索引,检查是否有必要排序等。
2.子查询
select p.Name,p.ProductNumber,ph.ListPrice
from Production.Product p
inner join Production.ProductListPriceHistory ph on
p.ProductID=ph.ProductID
where ph.StartDate=(
select top(1) ph2.StartDate
from Production.ProductListPriceHistory ph2
where ph2.ProductID=p.ProductID
order by ph2.StartDate desc
);
两个表Product和ProductListPriceHistory都进行了Merge Join,他们均有聚集索引,并且数据量相对前面来说比较大,所以优化器决定使用合并关联算法。然后对子查询进行Nested Loops关联,因为只需要取每个结果集的最近日期的数据,所以数据量相对较小,
本例中出现了非多对多的Merge join,从tooltips中的框线中看出,如果出现多对多的情况,优化器会创建一个Worktable的空间,这个空间位于TempDB,会导致额外的开销。
从filter操作符中可以看出,实现了对TOP 1 的处理,这个操作把两个数据集相等的数据进行对比,然后只取时间最大的那个。这个例子中最主要的地方就是进行了只查询的部分。在执行计划中这部分的操作演变成一个筛选操作,把符合where条件的数据返回。
3.使用APPLY的派生表
我们可以在SELECT过程中对创建的虚拟表进行数据操作,这种虚拟表叫作派生表。可以使用APPLY操作符来产生派生表,有CROSS APPLY和OUTER APPLY两种。
select p.Name,p.ProductNumber,ph.ListPrice from Production.Product p
cross apply (select TOP(1) ph2.ProductID,ph2.ListPrice from Production.ProductListPriceHistory ph2
where ph2.ProductID=p.ProductID order by ph2.StartDate desc) ph;
通过APPLY,先对派生表中每行进行WHERE条件及其他条件的应用,产生的结果集和Product表进行聚集索引扫面后,得到的数据进行Nested Loops 关联,最后返回结果。
通过查看I/O情况,子查询开销更低。
如果加入where 条件 where p.ProductID='839'
当加入WHERE条件之后,派生表的数据集迅速降低,在JOIN的过程中更加快速和高效。
4.通用表表达式
它是SQL语句中的一个临时结果集
ALTER PROCEDURE [dbo].[uspGetManagerEmployees]
@BusinessEntityID [int]
AS
BEGIN
SET NOCOUNT ON;
WITH [EMP_cte] ( [BusinessEntityID], [OrganizationNode], [FirstName], [LastName], [RecursionLevel] )
-- CTE name and columns
AS ( SELECT e.[BusinessEntityID] ,
e.[OrganizationNode] ,
p.[FirstName] ,
p.[LastName] ,
0 -- Get the initial list of Employees
-- for Manager n
FROM [HumanResources].[Employee] e
INNER JOIN [Person].[Person] p ON p.[BusinessEntityID] = e.[BusinessEntityID]
WHERE e.[BusinessEntityID] = @BusinessEntityID
UNION ALL
SELECT e.[BusinessEntityID] ,
e.[OrganizationNode] ,
p.[FirstName] ,
p.[LastName] ,
[RecursionLevel] + 1 -- Join recursive
-- member to anchor
FROM [HumanResources].[Employee] e
INNER JOIN [EMP_cte] ON e.[OrganizationNode].GetAncestor(1) = [EMP_cte].[OrganizationNode]
--GetAncestor 返回表示this的第n个祖先的 hierarchyid。
INNER JOIN [Person].[Person] p ON p.[BusinessEntityID] = e.[BusinessEntityID]
)
SELECT [EMP_cte].[RecursionLevel] ,
[EMP_cte].[OrganizationNode].ToString() AS [OrganizationNode] ,
p.[FirstName] AS 'ManagerFirstName' ,
p.[LastName] AS 'ManagerLastName' ,
[EMP_cte].[BusinessEntityID] ,
[EMP_cte].[FirstName] ,
[EMP_cte].[LastName] -- Outer select from the CTE
FROM [EMP_cte]
INNER JOIN [HumanResources].[Employee] e ON [EMP_cte].[OrganizationNode].GetAncestor(1) = e.[OrganizationNode]
INNER JOIN [Person].[Person] p ON p.[BusinessEntityID] = e.[BusinessEntityID]
ORDER BY [RecursionLevel] ,
[EMP_cte].[OrganizationNode].ToString()
OPTION ( MAXRECURSION 25 ) --指定应在整个查询中使用所指定的查询提示
END;
XML格式的执行计划
set statistics xml ON;
exec dbo.uspGetEmployeeManagers @businessEntityID=9;
set statistics xml off
单击XML链接就可以看到图形化执行计划。
最右的执行计划:(从右到左)
进行一联串的Inner Join Join 嵌套的循环之后,进入Concatenation操作,这个操作是针对代码中的UNION ALL 的,会把数据合成一个数据集。
执行计划中的假表脱机(Table Spool 操作符)。这个操作符可以让数据集被多次循环,而不需要在每次需要时都临时创建了。另外这个和索引假脱机类似,他们都是把数据存放在TempDb的隐藏临时表中,其实就是对CTE的UNION ALL操作进行重绕(可以理解为递归操作)
在Concatenation之后,就是索引假脱机(Index Spool)这个操作会把数据汇总到TempDb中一个叫Worktable的地方,然后与查询中返回的地方就行索引关联,以便实现递归操作。
5.视图
视图的本质就是一个查询,有两种视图:标准视图和索引视图
(1)标准视图
select * from Sales.vIndividualCustomer where BusinessEntityID=8743
在标准视图中,SQL Server会认为这个视图仅仅是一个名称,优化器会忽略视图这个对象,取而代之的是直接处理底层表。
(2)索引视图
这类视图也叫物化视图,本质上就是视图加一个聚集索引,这种视图实际上等价于在数据库中创建了一个物理表,很多时候,索引视图能极大的提高性能,创建索引视图也是一个开销比较大的操作,只不过只需要创建一次。
select * from Person.vStateProvinceCountryRegion
没有where 条件,进行的是聚集索引扫描