SQL Server OPTION (OPTIMIZE FOR UNKNOWN) 测试总结 - 潇湘隐者 - 博客园

mikel阅读(651)

来源: SQL Server OPTION (OPTIMIZE FOR UNKNOWN) 测试总结 – 潇湘隐者 – 博客园

关于SQL Server的查询提示OPTION (OPTIMIZE FOR UNKNOWN) ,它是解决参数嗅探的方法之一。 而且对应的SQL语句会缓存,不用每次都重编译。关键在于它的执行计划的准确度问题, 最近在优化的时候,和同事对于这个查询提示(Query Hint)有一点分歧,遂动手实验验证、总结了一些东西。

关于提示OPTION (OPTIMIZE FOR UNKNOWN),它会利用统计数据和标准算法生成一个折中、稳定的执行计划,但是它是无法利用直方图(histogram)信息来生成执行计划。官方文档的介绍如下:

OPTIMIZE FOR 编译和优化查询时提示查询优化器对本地变量使用特定值。仅在查询优化期间使用该值,在查询执行期间不使用该值。

 

UNKNOWN

指定查询优化器在查询优化期间使用统计数据而不是初始值来确定局部变量的值。OPTIMIZE FOR 可以抵消优化器的默认参数检测行为,也可在创建计划指南时使用

 

OPTIMIZE FOR UNKNOWN

指示查询优化器在查询已经过编译和优化时为所有局部变量使用统计数据而不是初始值,包括使用强制参数化创建的参数。有关强制参数化的详细信息,请参阅强制参数化

如果在同一查询提示中使用 OPTIMIZE FOR @variable\_name = literal_constant 和 OPTIMIZE FOR UNKNOWN,则查询优化器将对特定的值使用指定的 literal_constant,而对其余变量使用 UNKNOWN。这些值仅用于查询优化期间,而不会用于查询执行期间

OPTIMIZE FOR UNKNOWN是否会用直方图数据呢? 不会,OPTIMIZE FOR UNKNOWN只会用简单的统计数据。我们以how-optimize-for-unknown-works这篇博客中的例子来演示一下, 下面测试环境为SQL Server 2014,数据库为AdventureWorks2014

CREATE PROCEDURE test (@pid int)ASSELECT * FROM [Sales].[SalesOrderDetail]WHERE ProductID = @pid OPTION (OPTIMIZE FOR UNKNOWN);

为了消除统计信息不准确会干扰测试结果,我们手工更新一下统计信息。

UPDATE STATISTICS [Sales].[SalesOrderDetail] WITH FULLSCAN;

我们在SSMS里面点击“包含实际执行计划”选项,然后测试执行该存储过程,如下截图所示: 执行计划居然走聚集索引扫描

EXEC test @pid=709

Filter里面过滤的记录为456.079,而实际上ProductID=709的记录有188条,那么优化器是怎么估计判断记录数为456.709的呢?

 

 

 

其实优化器是这样来估计的:它使用ProductID列的密度(Density)* Rows来计算的

SELECT 0.003759399 *121317 ~= 456.079008483 ~= 456.079

而ProductID列的密度(Density)的计算是这样来的:

ProductID的值有266个,可以用下面SQL获取ProductID的值个数

SELECT COUNT(DISTINCT ProductID) FROM  Sales.SalesOrderDetail

SELECT 1.0/266  ~=  0.003759

然后你可以使用任意不同的参数测试,例如707、712……, 你会发现使用查询提示OPTION (OPTIMIZE FOR UNKNOWN)后,优化器会总是使用相同的执行计划。也就是说这个查询提示生成的执行计划是一个“折中的执行计划” ,对于数据分布倾斜的比较厉害(数据分布极度不均衡)的情况下,是极度不建议使用查询提示OPTION (OPTIMIZE FOR UNKNOWN)的。

本人曾经一度对使用OPTION(RECOMPILE)还是OPTION (OPTIMIZE FOR UNKNOWN)感到困惑和极度难以取舍,后面总结了一下:

1:执行不频繁的存储过程,使用OPTION(RECOMPILE)要优先与OPTION (OPTIMIZE FOR UNKNOWN)

2:执行频繁的存储过程,使用OPTION (OPTIMIZE FOR UNKNOWN)要优先于OPTION(RECOMPILE)

3:数据分布倾斜的厉害的情况下,优先使用OPTION(RECOMPILE)

    4: 使用OPTION (OPTIMIZE FOR UNKNOWN)会生成一个稳定、统一的执行计划,如果这个执行计划的效率基本能满足用户需求,那么优先使用OPTION (OPTIMIZE FOR UNKNOWN)

 

 

参考资料:

https://docs.microsoft.com/zh-cn/previous-versions/sql/sql-server-2008/ms181714(v=sql.100)

http://www.benjaminnevarez.com/2010/06/how-optimize-for-unknown-works/

https://blogs.msdn.microsoft.com/sqlprogrammability/2008/11/26/optimize-for-unknown-a-little-known-sql-server-2008-feature/

SQL Server中LIKE %search_string% 走索引查找(Index Seek)浅析 - 潇湘隐者 - 博客园

mikel阅读(461)

来源: SQL Server中LIKE %search_string% 走索引查找(Index Seek)浅析 – 潇湘隐者 – 博客园

 

SQL Server的SQL优化过程中,如果遇到WHERE条件中包含LIKE ‘%search_string%’是一件非常头痛的事情。这种情况下,一般要修改业务逻辑或改写SQL才能解决SQL执行计划走索引扫描或全表扫描的问题。最近在优化SQL语句的时候,遇到了一个很有意思的问题。某些使用LIKE ‘%’ + @search_string + ‘%'(或者 LIKE @search_string)这样写法的SQL语句的执行计划居然走索引查找(Index Seek)。下面这篇文章来分析一下这个奇怪的现象。

 

首先,我们来看看WHERE查询条件中使用LIKE的几种情况,这些是我们对LIKE的一些常规认识:

 

1: LIKE ‘condition%’

 

执行计划会走索引查找(Index Seek or Clustered Index Seek)。

 

2:  LIKE ‘%condition’

 

执行计划会走索引扫描(Index Scan or Clustered Index Scan)或全表扫描(Table Scan)

 

3:  LIKE ‘%condition%’

 

执行计划会走索引扫描(Index Scan or Clustered Index Scan)或全表扫描(Table Scan)

 

4: LIKE ‘condition1%condition%’;

 

执行计划会走索引查找(Index Seek)

 

下面我们以AdventureWorks2014示例数据库为测试环境(测试环境为SQL Server 2014 SP2),测试上面四种情况,如下所示:

 

 

 

 

 

 

 

 

其实复杂的情况下,LIKE ‘search_string%’也有走索引扫描(Index Scan)的情况,上面情况并不是唯一、绝对的。如下所示

 

在表Person.Person的 rowguid字段上创建有唯一索引AK_Person_rowguid

 

 

 

 

那么我们来看看上面所说的这个特殊案例(这里使用一个现成的案例,懒得构造案例了),如何让LIKE %search_string%走索引查找(Index Seek),这个技巧就是使用变量,如下SQL对比所示:

 

如下所示,表[dbo].[GEN_CUSTOMER]在字段CUSTOMER_CD有聚集索引。

 

 

 

 

可以看到CUSTOMER_CD LIKE ‘%’ + @CUSTOMER_CD + ‘%’这样的SQL写法(或者CUSTOMER_CD LIKE @CUSTOMER_CD也可以), 执行计划就走聚集索引查找(Clustered Index Seek)了, 而条件中直接使用CUSTOMER_CD LIKE ‘%00630%’ 反而走聚集索引扫描(Clustered Index Scan),另外可以看到实际执行的Cost开销比为4% VS 96% ,初一看,还真的以为第一个执行计划比第二个执行的代价要小很多。但是从IO开销,以及CPU time、elapsed time对比来看,两者几乎没有什么差异。在这个案例中,并不是走索引查找(Index Seek)就真的开销代价小很多。

 

 

 

 

考虑到这里数据量较小,我使用网上的一个脚本,在AdventureWorks2014数据库构造了一个10000000的大表,然后顺便做了一些测试对比

 

CREATE TABLE dbo.TestLIKESearches(     ID1         INT    ,ID2         INT    ,AString     VARCHAR(100)    ,Value       INT    ,PRIMARY KEY (ID1, ID2)); WITH Tally (n) AS(SELECT TOP 10000000 ROW_NUMBER() OVER (ORDER BY (SELECT NULL))FROM sys.all_columns a CROSS JOIN sys.all_columns b)INSERT INTO dbo.TestLIKESearches    (ID1, ID2, AString, Value)SELECT 1+n/500, n%500    ,CASE WHEN n%500 > 299 THEN            SUBSTRING(‘abcdefghijklmnopqrstuvwxyz’, 1+ABS(CHECKSUM(NEWID()))%26, 1) +            SUBSTRING(‘abcdefghijklmnopqrstuvwxyz’, 1+ABS(CHECKSUM(NEWID()))%26, 1) +            SUBSTRING(‘abcdefghijklmnopqrstuvwxyz’, 1+ABS(CHECKSUM(NEWID()))%26, 1) +            RIGHT(1000+n%1000, 3) +            SUBSTRING(‘abcdefghijklmnopqrstuvwxyz’, 1+ABS(CHECKSUM(NEWID()))%26, 1) +            SUBSTRING(‘abcdefghijklmnopqrstuvwxyz’, 1+ABS(CHECKSUM(NEWID()))%26, 1) +            SUBSTRING(‘abcdefghijklmnopqrstuvwxyz’, 1+ABS(CHECKSUM(NEWID()))%26, 1)          END    ,1+ABS(CHECKSUM(NEWID()))%100FROM Tally;  CREATE INDEX IX_TestLIKESearches_N1 ON dbo.TestLIKESearches(AString);

 

如下测试所示,在一个大表上面,LIKE @search_string这种SQL写法,IO开销确实要小一些,CPU Time也要小一些。个人多次测试都是这种结果。也就是说对于数据量较大的表,这种SQL写法性能确实要好一些。

 

 

 

 

现在回到最开始那个SQL语句,个人对执行计划有些疑惑,查看执行计划,你会看到优化器对CUSTOMER_CD LIKE ‘%’ + @CUSTOMER_CD + ‘%’ 进行了转换。如下截图或通过执行计划的XML,你会发现上面转换为使用三个内部函数LikeRangeStart, LikeRangeEnd,  LikeRangeInfo.

 

 

<OutputList>                    <ColumnReference Column=”Expr1007″ />                    <ColumnReference Column=”Expr1008″ />                    <ColumnReference Column=”Expr1009″ />                  </OutputList>                  <ComputeScalar>                    <DefinedValues>                      <DefinedValue>                        <ColumnReference Column=”Expr1007″ />                        <ScalarOperator ScalarString=”LikeRangeStart((N’%’+[@CUSTOMER_CD])+N’%’)”>                          <Identifier>                            <ColumnReference Column=”ConstExpr1004″>                              <ScalarOperator>                                <Intrinsic FunctionName=”LikeRangeStart”>                                  <ScalarOperator>                                    <Arithmetic Operation=”ADD”>                                      <ScalarOperator>                                        <Arithmetic Operation=”ADD”>                                          <ScalarOperator>                                            <Const ConstValue=”N’%'” />                                          </ScalarOperator>                                          <ScalarOperator>                                            <Identifier>                                              <ColumnReference Column=”@CUSTOMER_CD” />                                            </Identifier>                                          </ScalarOperator>                                        </Arithmetic>                                      </ScalarOperator>                                      <ScalarOperator>                                        <Const ConstValue=”N’%'” />                                      </ScalarOperator>                                    </Arithmetic>                                  </ScalarOperator>                                  <ScalarOperator>                                    <Const ConstValue=”” />                                  </ScalarOperator>                                </Intrinsic>                              </ScalarOperator>                            </ColumnReference>                          </Identifier>                        </ScalarOperator>                      </DefinedValue>                      <DefinedValue>                        <ColumnReference Column=”Expr1008″ />                        <ScalarOperator ScalarString=”LikeRangeEnd((N’%’+[@CUSTOMER_CD])+N’%’)”>                          <Identifier>                            <ColumnReference Column=”ConstExpr1005″>                              <ScalarOperator>                                <Intrinsic FunctionName=”LikeRangeEnd”>                                  <ScalarOperator>                                    <Arithmetic Operation=”ADD”>                                      <ScalarOperator>                                        <Arithmetic Operation=”ADD”>                                          <ScalarOperator>                                            <Const ConstValue=”N’%'” />                                          </ScalarOperator>                                          <ScalarOperator>                                            <Identifier>                                              <ColumnReference Column=”@CUSTOMER_CD” />                                            </Identifier>                                          </ScalarOperator>                                        </Arithmetic>                                      </ScalarOperator>                                      <ScalarOperator>                                        <Const ConstValue=”N’%'” />                                      </ScalarOperator>                                    </Arithmetic>                                  </ScalarOperator>                                  <ScalarOperator>                                    <Const ConstValue=”” />                                  </ScalarOperator>                                </Intrinsic>                              </ScalarOperator>                            </ColumnReference>                          </Identifier>                        </ScalarOperator>                      </DefinedValue>                      <DefinedValue>                        <ColumnReference Column=”Expr1009″ />                        <ScalarOperator ScalarString=”LikeRangeInfo((N’%’+[@CUSTOMER_CD])+N’%’)”>                          <Identifier>                            <ColumnReference Column=”ConstExpr1006″>                              <ScalarOperator>                                <Intrinsic FunctionName=”LikeRangeInfo”>                                  <ScalarOperator>                                    <Arithmetic Operation=”ADD”>                                      <ScalarOperator>                                        <Arithmetic Operation=”ADD”>                                          <ScalarOperator>                                            <Const ConstValue=”N’%'” />                                          </ScalarOperator>                                          <ScalarOperator>                                            <Identifier>                                              <ColumnReference Column=”@CUSTOMER_CD” />                                            </Identifier>                                          </ScalarOperator>                                        </Arithmetic>                                      </ScalarOperator>                                      <ScalarOperator>                                        <Const ConstValue=”N’%'” />                                      </ScalarOperator>                                    </Arithmetic>                                  </ScalarOperator>                                  <ScalarOperator>                                    <Const ConstValue=”” />                                  </ScalarOperator>                                </Intrinsic>                              </ScalarOperator>                            </ColumnReference>                          </Identifier>                        </ScalarOperator>                      </DefinedValue>                    </DefinedValues>

 

 

另外,你会发现Nested Loops & Compute Scalar 等步骤的Cost都为0.后面在“Dynamic Seeks and Hidden Implicit Conversions”这篇博客里面看到了一个新名词Dynamic Seeks。文字提到因为成本估算为0,所以,你看到的执行计划的Cost又是“不准确”的,具体描述如下:

 

The plan now contains an extra Constant Scan,  a Compute Scalar and a Nested Loops Join.  These operators are interesting because they have zero cost estimates: no CPU, no I/O, nothing.  That’s because they are purely architectural: a workaround for the fact that SQL Server cannot currently perform a dynamic seek within the Index Seek operator itself.  To avoid affecting plan choices, this extra machinery is costed at zero.

The Constant Scan produces a single in-memory row with no columns.  The Compute Scalar defines expressions to describe the covering seek range (using the runtime value of the @Like variable).  Finally, the Nested Loops Join drives the seek using the computed range information as correlated values.

The upper tooltip shows that the Compute Scalar uses three internal functions, LikeRangeStartLikeRangeEnd, and LikeRangeInfo.  The first two functions describe the range as an open interval.  The third function returns a set of flags encoded in an integer, that are used internally to define certain seek properties for the Storage Engine.  The lower tooltip shows the seek on the open interval described by the result of LikeRangeStart and LikeRangeEnd, and the application of the residual predicate ‘LIKE @Like’.

 

 

不管你返回的记录有多少,执行计划Nested Loops & Compute Scalar 等步骤的Cost都为0,如下测试所示,返回1000条记录,它的成本估算依然为0 ,显然这样是不够精确的。深层次的原因就不太清楚了。执行计划Cost不可靠的案例很多。

 

SET STATISTICS IO ON; SET STATISTICS TIME ON; DECLARE @CUSTOMER_CD NVARCHAR(10); SET @CUSTOMER_CD=N’%44%’   SELECT * FROM  [dbo].[GEN_CUSTOMER] WHERE CUSTOMER_CD LIKE @CUSTOMER_CD

 

 

 

另外,其实还一点没有搞清楚的时候在什么条件下出现Index Seek的情况。有些情况下,使用变量的方式,依然是索引扫描

 

 

 

 

不过我在测试过程,发现有一个原因是书签查找(Bookmark Lookup:键查找(Key Lookup)或RID查找 (RID Lookup))开销过大会导致索引扫描。如下测试对比所示:

 

CREATE NONCLUSTERED INDEX [IX_xriteWhite_N1] ON.[dbo].[xriteWhite] ([Item_NO]) INCLUDE ([Iden],[WI_CE],[CIE],[Operate_Time])

 

 

 

 

 

 

 

 

参考资料:

 

http://sqlblog.com/blogs/paul_white/archive/2012/01/18/dynamic-seeks-and-hidden-implicit-conversions.aspx

https://blogs.msdn.microsoft.com/varund/2009/11/30/index-usage-by-like-operator-query-tuning/

https://sqlperformance.com/2017/02/sql-indexes/seek-leading-wildcard-sql-server

https://stackoverflow.com/questions/1388059/sql-server-index-columns-used-in-like

SQL Server通过条件搜索获取相关的存储过程等对象 - 潇湘隐者 - 博客园

mikel阅读(874)

来源: SQL Server通过条件搜索获取相关的存储过程等对象 – 潇湘隐者 – 博客园

   在SQL Server中,我们经常遇到一些需求,需要去搜索存储过程(Procedure)、函数(Function)等对象是否包含某个对象或涉及某个对象,例如,我需要查找那些存储过程、函数是否调用了链接服务器(LINKED SERVER),我们如果从sys.SQL_modules去搜索的话,如果有多个用户数据库,需要切换数据库,执行多次SQL语句。这些都是非常麻烦的事情。本着“模块化定制脚本,减少重复工作量”的原则。写了一个脚本find_prc_from_src_txt.sql, 以后在根据不同的需求逐步完善!

--==================================================================================================================
--        ScriptName          :            find_prc_from_src_txt.sql
--        Author              :            潇湘隐者
--        CreateDate          :            2019-10-22
--        Description         :            在SQL Server实例中通过条件搜索所有数据库的存储过程、函数、视图,找出这些对象
--        Note                :
/*******************************************************************************************************************
        Parameters            :                                    参数说明
********************************************************************************************************************
            @src_text         :            你要搜索的条件,例如,想找出那些存储过程有调用某个链接服务器:@src_text=xxxx
********************************************************************************************************************
        Notice                :            由于效率问题,有时候会被阻塞,在tempdb等待LCK_M_SCH_S
********************************************************************************************************************
   Modified Date    Modified User     Version                 Modified Reason
********************************************************************************************************************
    2019-10-22        潇湘隐者         V01.00.00        新建该脚本。
*******************************************************************************************************************/
--==================================================================================================================

DECLARE @cmdText        NVARCHAR(MAX);
DECLARE @database_name  NVARCHAR(64);
DECLARE @src_text        NVARCHAR(128);


SET @src_text='xxxx' --根据实际情况输入查询、搜索条件

IF OBJECT_ID('TempDB.dbo.#databases') IS NOT NULL
    DROP TABLE dbo.#databases;

CREATE TABLE #databases
(
    database_id     INT,
    database_name   sysname
);

INSERT  INTO #databases
SELECT  database_id ,
        name
FROM    sys.databases
WHERE name NOT IN ('model') AND state = 0; --state_desc=ONLINE

IF OBJECT_ID('TempDB.dbo.#sql_modules') IS NOT NULL
    DROP TABLE #sql_modules;

/**********************************************************************************************************
此处如果用这种写法,就会报下面错误,所以用下面这种写法。

SELECT '' AS database_name,  t.* INTO #sql_modules
FROM sys.sql_modules t WITH(NOLOCK) WHERE 1=0;
------------------------------------------------------------------------———----------------------------
Msg 8152, Level 16, State 2, Line 2
将截断字符串或二进制数据。
**********************************************************************************************************/
SELECT 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' AS database_name
    ,  t.* INTO #sql_modules
FROM sys.sql_modules t WITH(NOLOCK) WHERE 1=0;


WHILE 1= 1
BEGIN


    SELECT TOP 1 @database_name= database_name
    FROM #databases
    ORDER BY database_id;

    IF @@ROWCOUNT =0
        BREAK;


    SET @cmdText =  N'USE ' + QUOTENAME(@database_name) + N';' +CHAR(10)

    //**********************************************************************************************************
    SELECT @cmdText += N'INSERT INTO ##sql_modules
    SELECT  *
    FROM    sys.sql_modules W
    WHERE   definition LIKE ''%@p_src_text%'';' + CHAR(10);


    EXEC SP_EXECUTESQL @cmdText, N'@p_src_text NVARCHAR(128)',@p_src_text=@src_text;

    此种方式不生效。这里弃用这种动态SQL执行方式
    ***********************************************************************************************************/
    SELECT @cmdText += N'INSERT INTO #sql_modules
                       SELECT @p_database_name
                             , t.*
                       FROM    sys.sql_modules t WITH(NOLOCK)
                       WHERE   definition LIKE ''%' +@src_text +'%'';' + CHAR(10);
    EXEC SP_EXECUTESQL @cmdText,N'@p_database_name NVARCHAR(64)',@p_database_name=@database_name;

    DELETE FROM #databases WHERE database_name=@database_name;
END

SELECT * FROM tempdb.dbo.#sql_modules;



IF OBJECT_ID('TempDB.dbo.#databases') IS NOT NULL
    DROP TABLE dbo.#databases;
IF OBJECT_ID('TempDB.dbo.#sql_modules') IS NOT NULL
    DROP TABLE #sql_modules;

OPTION(RECOMPILE)提高带参数执行SQL语句的索引效率_奋斗鱼的博客-CSDN博客

mikel阅读(699)

来源: OPTION(RECOMPILE)提高带参数执行SQL语句的索引效率_奋斗鱼的博客-CSDN博客

本例为MSSQL2008环境下测试。

1. 无参数查询
先看一个简单的查询语句在大数据表中的查询情况:

SELECT fmr.FID,fmr.BasicTime FROM feeMeterRead fmr
WHERE fmr.BasicTime>=’2015-12-02 00:00:00′
1
2
在查询分析器中执行后显示耗时5s,查询到的记录数为6223条

2. 带参数查询
现在引用参数再查询一次

DECLARE
@BasicTime DATETIME
SET @BasicTime= LEFT(CONVERT(VARCHAR(20),DATEADD(DAY,-2,GETDATE()),120),10)+’ 00:00:00′
–SELECT @BasicTime
SELECT fmr.FID,fmr.BasicTime FROM feeMeterRead fmr
WHERE fmr.BasicTime>=@BasicTime
1
2
3
4
5
6
为保证查询条件一样,参数@BasicTime的值也是’2015-12-02 00:00:00’
在查询分析器中执行后显示耗时15s,查询到的记录数为6223条。比不带参数的查询时间多了10s。

3. 分析对比
将例一查询语句选中,点击工具栏中的“显示估计的执行计划”按钮,可以看到该语句的执行计划。

可以看到,执行时使用了索引查找和键查找这两种最有效率的查询方式,所以耗时短。
同理查看例2中的执行计划

查询1是赋值参数的过程,我们主要看查询2、
执行计划明显比例1的无参数查询的复杂,例1的“键查找”在例2中变成了“索引扫描“。
由此可判断此处是耗时的主要关键。

4. 语句优化
在语句末尾添加OPTION(RECOMPILE),可以使执行计划重新编译,提高性能。

DECLARE
@BasicTime DATETIME
SET @BasicTime= LEFT(CONVERT(VARCHAR(20),DATEADD(DAY,-2,GETDATE()),120),10)+’ 00:00:00′
–SELECT @BasicTime
SELECT fmr.FID,fmr.BasicTime FROM feeMeterRead fmr
WHERE fmr.BasicTime>=@BasicTime
OPTION(RECOMPILE)
1
2
3
4
5
6
7
上面查询语句重新执行后,耗时为6s。优化成功。
我们现在来查看下执行计划,估计的执行计划还是没变,但是点击”包括实际的执行计划“,然后再点击”执行“查询,可以看到执行时的计划如下:

 

可以看到,实际执行时是重新编译了,执行计划已经与例1的无参数执行一致。
————————————————
版权声明:本文为CSDN博主「奋斗鱼」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/quan278905570/article/details/50171345

SQL SERVER中关于OR会导致索引扫描或全表扫描的浅析 - 潇湘隐者 - 博客园

mikel阅读(694)

来源: SQL SERVER中关于OR会导致索引扫描或全表扫描的浅析 – 潇湘隐者 – 博客园

SQL SERVER的查询语句中使用OR是否会导致不走索引查找(Index Seek)或索引失效(堆表走全表扫描 (Table Scan)、聚集索引表走聚集索引扫描(Clustered Index Scan))呢?是否所有情况都是如此?又该如何优化呢? 下面我们通过一些简单的例子来分析理解这些现象。下面的实验环境为SQL SERVER 2008,如果在不同版本有所区别,欢迎指正。

 

堆表单索引

首先我们构建我们测试需要实验环境,具体情况如下所示:

DROP TABLE TEST

CREATE TABLE TEST (OBJECT_ID  INT, NAME VARCHAR(32));

CREATE INDEX PK_TEST ON TEST(OBJECT_ID)

DECLARE @Index INT =0;

WHILE @Index < 500000
BEGIN
    INSERT INTO TEST
    SELECT @Index, 'kerry'+CAST(@Index AS VARCHAR(6));

    SET @Index = @Index +1;
END


UPDATE STATISTICS TEST WITH FULLSCAN

 

场景1:如下所示,并不是所有的OR条件都会导致SQL走全表扫描。具体情况具体分析,不要套用教条。

SELECT * FROM TEST WHERE (OBJECT_ID =5 OR OBJECT_ID = 105)

clipboard

 

场景2:加了条件1=1后,执行计划从索引查找(Index Seek)变为全表扫描(Table Scan),为什么会如此呢?个人理解为优化器将OR运算拆分为两个子集处理,由于一些原因,1=1这个条件导致优化器认定需要全表扫描才能完成1=1条件子集的计算处理(为了理解这个,煞费苦心,鉴于理论薄弱,如有错误或不足,敬请指出)。所以优化器在权衡代价后生成的执行计划最终选择了全表扫描(Table Scan)

SELECT * FROM TEST WHERE (1=1 OR OBJECT_ID =105);

clipboard[1]

 

场景3: 下面场景比较好理解,因为下面需要从500000条记录中取出499700条记录,而全表扫描(Table Scan)肯定是最优的选择,代价(Cost)最低。

SELECT * FROM TEST WHERE (OBJECT_ID >300 OR OBJECT_ID =105);

 

场景4:这种场景跟场景2的情况本质是一样的。所以在此略过。其实类似这种写法也是实际情况中最常出现的情况,还在迷糊的同学,赶紧抛弃这种写法吧

DECLARE @OBJECT_ID INT =150;

SELECT * FROM TEST WHERE (@OBJECT_ID IS NULL OR OBJECT_ID =@OBJECT_ID);

clipboard[2]

 

聚集索引表单索引

在聚集索引表中,我们也依葫芦画瓢,准备实验测试的数据环境。

DROP TABLE TEST

CREATE TABLE TEST (OBJECT_ID  INT, NAME VARCHAR(32));

CREATE CLUSTERED INDEX PK_TEST ON TEST(OBJECT_ID)

DECLARE @Index INT =0;

WHILE @Index < 500000
BEGIN
    INSERT INTO TEST
    SELECT @Index, 'kerry'+CAST(@Index AS VARCHAR(6));

    SET @Index = @Index +1;
END


UPDATE STATISTICS TEST WITH FULLSCAN

 

场景1 :索引查找(Index Seek)

 

SELECT * FROM TEST WHERE (OBJECT_ID =5 OR OBJECT_ID = 105)

 

场景2:聚集索引扫描(Clustered Index Scan)

clipboard[3]

 

场景3:似乎与堆表有所不同。聚集索引表居然还是走聚集索引查找。

clipboard[4]

 

场景4:OR导致聚集索引扫描

clipboard[5]

 

如果堆表或聚集索引表上建立有联合索引,情况也大致如此,在此不做过多案例讲解。下面仅仅讲述一两个案例场景。

DROP TABLE test1;

CREATE TABLE test1
  (
     a INT,
     b INT,
     c INT,
     d INT,
     e INT
  )

DECLARE @Index INT =0;

WHILE @Index < 10000
  BEGIN
      INSERT INTO test1
      SELECT @Index,
             @Index,
             @Index,
             @Index,
             @Index

      SET @Index = @Index + 1;
  END

CREATE INDEX idx_test_n1
  ON test1(a, b, c, d)

UPDATE STATISTICS test1 WITH fullscan;

SELECT * FROM TEST1 WHERE A=12 OR B> 500 OR C >100000

clipboard[6]

 

因为结果集是几个条件的并集,最多只能在查找A=12的数据时用索引,其它几个条件都需要表扫描,那优化器就会选择直接走一遍表扫描,以最低的代价COST完成,所以索引就失效了。

 

那么如何优化查询语句含有的OR的SQL语句呢?方法无外乎有三种:

1:通过索引覆盖,使包含OR的SQL走索引查找(Index Seek)。但是这个只能满足部分场景,并不能解决所有这类SQL。这个Solution具有一定的局限性。

SELECT * FROM TEST1 WHERE A=12 OR B=500

clipboard[7]

如果我们通过索引覆盖,在字段B上面也建立索引,那么下面OR查询也会走索引查找。

CREATE INDEX IDX_TEST1_B ON TEST1(B);

SELECT * FROM TEST1 WHERE A=12 OR B=500

clipboard[8]

 

2:使用IN替换OR。 但是这个Solution也有很多局限性。在此不做过多阐述。

 

3:一般将OR的字句分解成多个查询,并且通过UNION ALL 或UNION连接起来。在联合索引或有索引覆盖的场景下。大部分情况下,UNION ALL的效率更高。但是并不是所有的UNION ALL都会比OR的SQL的代价(COST),特殊的情况或特殊的数据分布也会出现UNION ALL比OR代价要高的情况。例如,上面特殊的要求,从全表中取两条记录,如下所示

SELECT * FROM TEST1 WHERE A=12

UNION ALL

SELECT * FROM TEST1 WHERE B=500

clipboard[9]

 

UNON ALL语句的代价(Cost)要高与OR是因为它做了两次索引查找(Index Seek),而OR语句只做一次索引查找(Index Seek)就完成了。开销明显小一些,但是实际情况这类特殊情况比较少,实际情况的取数条件、数据都比这个简单案例要复杂得多。所以在大部分情况下,拆分为UNION ALL语句的效率要高于OR语句

另外一个案例,就是最上面实验的堆表TEST, 在字段OBJECT_ID上建有索引

SELECT * FROM TEST WHERE (OBJECT_ID >300 OR OBJECT_ID =105);

SELECT * FROM TEST WHERE OBJECT_ID >300

UNION ALL

SELECT * FROM TEST WHERE OBJECT_ID =105;

clipboard[10]

可以从下面看出两者开销不同的地方在于IO方面,两者开销之所以有区别,是因为第二个SQL多了一次扫描(索引查找)

clipboard[11]

clipboard[12]

 

总结:

    在实际开发环境中,OR这种写法确实会带来很多不确定性,尽量使用UNION 或IN替换OR。我们需要遵循一些规则,但是也不能认为它就是一成不变的,永为真理。具体场景、具体环境具体分析。要知其然知其所以然。在微软亚太区数据库技术支持组的官方博客中就有一个案例SQL Server性能问题案例解析 (3)也是OR引起的性能案例。 博客中有个观点,我觉得挺赞的:”需要注意的是,对于OR或UNION,并没有确定的孰优孰劣,使用时要进行测试才能确定。“ 。

SQL Server优化技巧——如何避免查询条件OR引起的性能问题 - 潇湘隐者 - 博客园

mikel阅读(733)

来源: SQL Server优化技巧——如何避免查询条件OR引起的性能问题 – 潇湘隐者 – 博客园

之前写过一篇博客“SQL SERVER中关于OR会导致索引扫描或全表扫描的浅析”,里面介绍了OR可能会引起全表扫描或索引扫描的各种案例,以及如何优化查询条件中含有OR的SQL语句的几种方法,其实还有一些方法可以用来优化这种问题,这里简单介绍一下。

如下所示,下面的SQL语句之所有出现这种写法,是因为程序的查询界面,可能有多个输入性的查询条件,往往用户只填了一个或部分查询条件(业务情况,应该不用详细介绍,大家都能明白),但是程序里面没有通过判断查询条件生成不同的SQL语句,而是用一个SQL搞定,不管用户没有填写JobNo这个查询条件,下面这种写法:WHERE ISNULL(@JobNo, ”) = ”  OR JobNo = @JobNo都能满足条件,实现逻辑功能。

DECLARE @GenerateDateStart DATETIME ,    @GenerateDateEnd DATETIME ,    @JobNo NVARCHAR(200) ,    @GkNo NVARCHAR(200);SET @JobNo = ‘PT19B030’;SET @GkNo = ‘PV19-1-8050’;    SELECT    *  FROM      [dbo].[GEW_UnitConsumption] AS A            LEFT JOIN dbo.UnitConsumption_Relation AS B ON B.UsableFlag = ‘Y’                                                           AND A.GewUnitConsumptionId = B.RootUnitConsumptionID  WHERE     ( ISNULL(@JobNo, ”) = ”              OR A.JobNo = @JobNo            )            AND ( ISNULL(@GkNo, ”) = ”                  OR A.GkNo = @GkNo                );

其实,如果根据查询条件动态生成SQL语句,的确能避免查询条件中出现OR的情形,但是动态SQL语句没有上面语句简单和通熟易懂,尤其是查询条件较多的情况下。只能说各有利弊。这里暂且不讨论那种策略的优劣。

下面介绍一种技巧,如何避免OR引起的索引扫描或全表扫描问题。我们可以使用CASE WHEN改写一下这个SQL语句,就能避免OR引起的执行计划不走索引查找(Index Seek)的情况,如下所示:

DECLARE @GenerateDateStart DATETIME ,    @GenerateDateEnd DATETIME ,    @JobNo NVARCHAR(200) ,    @GkNo NVARCHAR(200);SET @JobNo = ‘PT19B030’;SET @GkNo = ‘PV19-1-8050’;   SELECT  *FROM    [dbo].[GEW_UnitConsumption] AS A        LEFT JOIN dbo.UnitConsumption_Relation AS B ON B.UsableFlag = ‘Y’                                                       AND A.GewUnitConsumptionId = B.RootUnitConsumptionIDWHERE   CASE WHEN ISNULL(@JobNo, ”) = ” THEN A.JobNo             ELSE @JobNo        END = JobNo        AND CASE WHEN ISNULL(@GkNo, ”) = ” THEN A.GkNo                 ELSE GkNo            END = @GkNo;

测试对比发现性能改善非常明显,当然这种优化技巧也是有局限性的,并不能解决所有OR引起的性能问题(没有银弹!)。如下所示,对于下面这种情况,这种技巧也是无能为力!

SELECT * FROM TEST1 WHERE A=12 OR B=500

——————————————分割线————————————————-

网友MSSQL123反馈:他测试的一个案例发现这种技巧无效,个人测试验证发现确实如此,后面发现个人遇到的仅仅是一个特殊个例(当时生产环境那个场景下确实生效了),后面经过大量测试发现,很多情况下CASE WHEN这种技巧无效,也就是说单个案例不具有通用性,后面进一步测试分析,发现我得出的结论是错误的

当然在错误的基础上,进一步测试验证,发现还是有技巧优化OR引起的性能问题的,这也是我后续补充的原因,请见下文分析:

我们首先简单构造一个测试环境案例,测试环境为SQL Server 2014

CREATE TABLE TEST_OPTION_COMPILE (OBJECT_ID  INT, NAME VARCHAR(16)); CREATE CLUSTERED INDEX PK_TEST_OPTION_COMPILE ON TEST_OPTION_COMPILE(OBJECT_ID);   DECLARE @Index INT =0; WHILE @Index < 100000BEGIN     INSERT INTO TEST_OPTION_COMPILE    SELECT @Index, ‘kerry’+CAST(@Index AS VARCHAR(7));        SET @Index = @Index +1;END  CREATE INDEX IX_TEST_OPTION_COMPILE_N1 ON TEST_OPTION_COMPILE(NAME);UPDATE STATISTICS TEST_OPTION_COMPILE WITH FULLSCAN;

如下测试所示,发现这个例子中,CASE WHEN完全无效,使用这种SQL写法,依然走Index Scan

DECLARE @name VARCHAR(8);SET @name = ‘kerry8’ SELECT  NAMEFROM    dbo.TEST_OPTION_COMPILEWHERE   CASE WHEN ISNULL(@name, ”) = ” THEN NAME             ELSE @name        END = NAME; SELECT  NAMEFROM    dbo.TEST_OPTION_COMPILEWHERE   ( ISNULL(@name, ”) = ”          OR NAME = @name        )

如果我们在SQL后面加上OPTION(RECOMPILE)的话,那么SQL就会走索引查找(Index Seek),其实下面两个SQL语句,如果都加上OPTION(RECOMPILE)的话,它们都会走索引。这是什么情况呢?

接下来我们对比分析一下,看看SQL语句有无OPTION(RECOMPILE)的区别,如下所示:

 DECLARE @name VARCHAR(8);SET @name = ‘kerry8’ SELECT  NAMEFROM    dbo.TEST_OPTION_COMPILEWHERE   CASE WHEN ISNULL(@name, ”) = ” THEN NAME             ELSE @name        END = NAME ; SELECT  NAMEFROM    dbo.TEST_OPTION_COMPILEWHERE   CASE WHEN ISNULL(@name, ”) = ” THEN NAME             ELSE @name        END = NAME OPTION(RECOMPILE)

如下所示,如果没有OPTION(RECOMPILE)的话,执行计划走Index Scan,预估行数(Estimated Number of Rows)是100000, 而实际行数(Actual Number of Rows)是1,

如果SQL中有OPTION(RECOMPILE)的话,执行计划走Index Seek,预估行数(Estimated Number of Rows)是1, 而实际行数(Actual Number of Rows)是1,从对比我们可以看出,加上OPTION(RECOMPILE)的话,SQL的执行计划要准确很多,那么为什么呢?这里是因为OPTION(RECOMPILE)开启了Parameter Embedding Optimization

关于Parameter Embedding Optimization,这里简单介绍一下,详情参考Parameter Sniffing, Embedding, and the RECOMPILE Options 和参考资料的相关文档。

参数嗅探值使优化器可以使用参数值来得出基数估计。 WITH RECOMPILE和OPTION(RECOMPILE)均会生成查询计划,并根据每次执行时的实际参数值计算出估算值。

  相比WITH RECOMPILE这种强制重编译的方式,OPTION(RECOMPILE)中的参数嵌入优化(Parameter Embedding Optimization)的机制更进一步:查询解析期间,查询参数被文字常量值替代。 解析器能够神奇的将复杂问题简单化,并且在随后的查询优化可能会进一步完善这些内容。

Microsoft在SQL Server 2008(后RTM)中引入了参数嵌入优化(Parameter Embedding Optimization)。 这个特性扩展了参数嗅探优化。 它能使用基数估计值来嗅探参数以影响计划。具体参考官方文档“Changed behaviour of OPTION RECOMPILE syntax in SQL Server 2008 SP1 cumulative update #5

总结: 我们可以使用OPTION(RECOMPILE)(确切的说,是Parameter Embedding Optimization)这种技巧来避免查询条件中OR引起的性能问题,这确实是一个SQL Server优化技巧,至于我前面的结论,这是一个错误结论(使用CASE WHEN改写一下这个SQL语句,就能避免OR引起的执行计划不走索引查找(Index Seek))。在缺乏严谨的论证、充分的测试就草率的得出了一个结论,以后要引以为戒!。

参考资料:

https://www.cnblogs.com/wy123/p/6262800.html

https://sqlperformance.com/2013/08/t-sql-queries/parameter-sniffing-embedding-and-the-recompile-options

c盘越来越大怎么清理?清理C:\Windows\System32\DriverStore\FileRepository_xl_leroy的博客-CSDN博客_filerepository文件夹可以删除吗

mikel阅读(1575)

来源: c盘越来越大怎么清理?清理C:\Windows\System32\DriverStore\FileRepository_xl_leroy的博客-CSDN博客_filerepository文件夹可以删除吗

c盘越来越大怎么清理?
装系统时划了50G给C盘,随着使用C盘的空间越来越小,该如何清理呢?

先解决2个基础软件设置造成的,为C盘腾出空间:

1、QQ
更换QQ文件存储路径。QQ设置->文件管理,图示:

 

2、微信桌面版:

完成以上2步,各位可再使用Windos自带的磁盘整理工具,清理一下C盘,比如清空回收站,删除windows.old等等。

这时候如果C盘空间依旧不够用,那就使用“Space seniffer”进行查看各个文件大小了,根据需要进行删除了(千万不能删除系统文件)。

必备工具有两个:
1、Space seniffer;

2、Driver Store Explorer;

Space seniffer是一款可直观查看各磁盘文件大小的软件;

下载、解压后,在“SpaceSniffer.exe”上鼠标右击,选择“管理员身份运行”,等待扫描一会。

 

可见“C:\Windows\System32\DriverStore\FileRepository”占据空间很大;

经过搜索查询可知道这个文件夹存储的是驱动文件。而驱动文件进行更新后,老的驱动文件没有删除,长久下来,就会造成很多老旧无效的驱动文件占据掉很大空间。

网上有很人说“FileRepository”,这个文件夹的内容可以删除;也有人说这个文件夹不能删除。

其实,准确点说,“FileRepository”文件夹中那些老旧不会使用到的文件是可以删除的,正在使用的文件就不能删除,以免会造成系统不稳定的影响。

其实,windows系统有一个“pnputil.exe”工具可以用来清理这个文件夹的,只不过是命令行方式,操作起来不是很方便。(参考资料)

世界上大神还是有的,编写了一个“Driver Store Explorer”使用图形界面来替换命令行的操作方式,Driver Store Explorer下载地址。

“Driver Store Explorer”下载、解压之后,选中“Rapr.exe”右键“管理员身份运行”,等待扫描一会。

如图:

 

“选取旧的驱动”,虽然使用工具选择旧驱动进行删除,但在删除之前我们还是需要查看一下的。

自信看下来就能发现,其实有大多驱动都是几十K,几百K的。也有几百M的,有可能那些几百M的驱动都没有选择。

这时候就需要我自己确认一下那些驱动其实是可以不要的,回到桌面:“我的电脑”右键选择“管理”->“设备管理器”;逐项查看目前那些驱动是在使用的,逐项查看的工作比较累,其实我们只需要抓住那些占据大空间的驱动即可。

可以查看的到我的“显示适配器”驱动有很多个,而且都是好几百M,“选择旧的驱动”功能键没有选中这些包。我就手动查看“显示适配器”的驱动,查看目前使用的驱动是哪一个,然后再选择删除掉那些不使用的驱动即可;

手动查看“显示适配器”驱动的方法:选择某个驱动,右键“属性”->“驱动程序”,就可以查看到当前使用的版本了;

如果选择了当前正在使用的驱动,软件会进行报错的,是无法删除的。

如果删除掉了旧的驱动依然没有解放出来足够的空间给C盘,那就继续使用“Space seniffer”,查看哪些文件占据空间比较大,再酌情进行删除,碰到不清楚的文件还是先查询弄清楚文件是做什么用的,再去删除它,毕竟稳定才是最重要的。
————————————————
版权声明:本文为CSDN博主「xl_leroy」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xl_leroy/article/details/88629159

SQL Server 监控统计阻塞脚本信息 - 潇湘隐者 - 博客园

mikel阅读(549)

来源: SQL Server 监控统计阻塞脚本信息 – 潇湘隐者 – 博客园

数据库产生阻塞(Blocking)的本质原因 :SQL语句连续持有锁的时间过长 ,数目过多, 粒度过大。阻塞是事务隔离带来的副作用,它是不可避免的,而且是一个数据库系统常见的现象。 但是阻塞的时间和出现频率要控制在一定的范围内,阻塞持续的时间过长或阻塞出现过多(过于频繁),就会对数据库性能产生严重的影响。

    很多时候,DBA需要知道数据库在出现性能问题时,有没有发生阻塞? 什么时候开始的?发生在那个数据库上? 阻塞发生在那些SQL语句之间? 阻塞的时间有多长? 阻塞发生的频率? 阻塞有关的连接是从那些客户端应用发送来的?…….

    如果我们能够知道这些具体信息,我们就能迅速定位问题,分析阻塞产生的原因,  从而找出出现性能问题的根本原因,并根据具体原因给出相应的解决方案(索引调整、优化SQL语句等)。

    查看阻塞的方法比较多, 我在这篇博客MS SQL 日常维护管理常用脚本(二)里面提到查看阻塞的一些方法:

方法1:查看那个引起阻塞,查看blk不为0的记录,如果存在阻塞进程,则是该阻塞进程的会话 ID。否则该列为零。

    EXEC sp_who active

方法2:查看那个引起阻塞,查看字段BlkBy,这个能够得到比sp_who更多的信息。

    EXEC sp_who2 active

方法3:sp_lock 系统存储过程,报告有关锁的信息,但是不方便定位问题

方法4:sp_who_lock存储过程

方法5:右键服务器-选择“活动和监视器”,查看进程选项。注意“任务状态”字段。

方法6:右键服务名称-选择报表-标准报表-活动-所有正在阻塞的事务。

但是上面方法,例如像sp_who、 sp_who2,sp_who_lock等,都有或多或少的缺点:例如不能查看阻塞和被阻塞的SQL语句。不能从查看一段时间内阻塞发生的情况等;没有显示阻塞的时间……. 我们要实现下面功能:

    1:  查看那个会话阻塞了那个会话

    2:阻塞会话和被阻塞会话正在执行的SQL语句

    3:被阻塞了多长时间

    4:像客户端IP、Proagram_Name之类信息

    5:阻塞发生的时间点

    6:阻塞发生的频率

    7:如果需要,应该通知相关开发人员,DBA不能啥事情都包揽是吧,那不还得累死,总得让开发人员员参与进来优化(有些问题就该他们解决),多了解一些系统运行的具体情况,有利于他们认识问题、解决问题。

    8:需要的时候开启这项功能,不需要关闭这项功能

于是为了满足上述功能,有了下面SQL 语句

SELECT wt.blocking_session_id                  AS BlockingSessesionId
      ,sp.program_name                         AS ProgramName
      ,COALESCE(sp.LOGINAME, sp.nt_username)   AS HostName
      ,ec1.client_net_address                  AS ClientIpAddress
      ,db.name                                 AS DatabaseName
      ,wt.wait_type                            AS WaitType
      ,ec1.connect_time                        AS BlockingStartTime
      ,wt.WAIT_DURATION_MS/1000                AS WaitDuration
      ,ec1.session_id                          AS BlockedSessionId
      ,h1.TEXT                                 AS BlockedSQLText
      ,h2.TEXT                                 AS BlockingSQLText
FROM sys.dm_tran_locks AS tl
INNER JOIN sys.databases db
  ON db.database_id = tl.resource_database_id
INNER JOIN sys.dm_os_waiting_tasks AS wt
  ON tl.lock_owner_address = wt.resource_address
INNER JOIN sys.dm_exec_connections ec1
  ON ec1.session_id = tl.request_session_id
INNER JOIN sys.dm_exec_connections ec2
  ON ec2.session_id = wt.blocking_session_id
LEFT OUTER JOIN master.dbo.sysprocesses sp
  ON SP.spid = wt.blocking_session_id
CROSS APPLY sys.dm_exec_sql_text(ec1.most_recent_sql_handle) AS h1
CROSS APPLY sys.dm_exec_sql_text(ec2.most_recent_sql_handle) AS h2

我们做一个测试例子来验证一下

 

1:打开第一会话窗口1,执行下面语句

USE DBMonitor;

GO

BEGIN TRANSACTION

SELECT * FROM dbo.TEST(TABLOCKX);

--COMMIT TRANSACTION;

2:打开第二个会话窗口2,执行下面语句

USE DBMonitor;
GO
SELECT * FROM dbo.TEST

3:打开第三个会话窗口3,执行下面语句

SELECT wt.blocking_session_id                  AS BlockingSessesionId
      ,sp.program_name                         AS ProgramName
      ,COALESCE(sp.LOGINAME, sp.nt_username)   AS HostName
      ,ec1.client_net_address                  AS ClientIpAddress
      ,db.name                                 AS DatabaseName
      ,wt.wait_type                            AS WaitType
      ,ec1.connect_time                        AS BlockingStartTime
      ,wt.WAIT_DURATION_MS/1000                AS WaitDuration
      ,ec1.session_id                          AS BlockedSessionId
      ,h1.TEXT                                 AS BlockedSQLText
      ,h2.TEXT                                 AS BlockingSQLText
FROM sys.dm_tran_locks AS tl
INNER JOIN sys.databases db
  ON db.database_id = tl.resource_database_id
INNER JOIN sys.dm_os_waiting_tasks AS wt
  ON tl.lock_owner_address = wt.resource_address
INNER JOIN sys.dm_exec_connections ec1
  ON ec1.session_id = tl.request_session_id
INNER JOIN sys.dm_exec_connections ec2
  ON ec2.session_id = wt.blocking_session_id
LEFT OUTER JOIN master.dbo.sysprocesses sp
  ON SP.spid = wt.blocking_session_id
CROSS APPLY sys.dm_exec_sql_text(ec1.most_recent_sql_handle) AS h1
CROSS APPLY sys.dm_exec_sql_text(ec2.most_recent_sql_handle) AS h2

如下图所,我们可以看到阻塞其它会话以及被阻塞会话的信息,如下所示

image

现在上面SQL已经基本实现了查看阻塞具体信息的功能,但是现在又有几个问题:

          1:上面SQL脚本只适合已经出现阻塞情况下查看阻塞信息,如果没有出现阻塞情况,我总不能傻傻的一直在哪里点击执行吧,因为阻塞这种情况有可能在那段时间都不会出现,只会在特定的时间段出现。

          2:我想了解一段时间内数据库出现的阻塞情况,那么需要将阻塞信息保留下来。

         3:有时候忙不过来,我想将这些具体阻塞信息发送给相关开发人员,让他们了解具体情况。

于是我想通过一个存储过程来实现这方面功能,通过设置参数@OutType,默认为输出阻塞会话信息,当参数为”Table” 时,将阻塞信息写入数据库表,如果参数为 “Email”表示将阻塞信息通过邮件发送开发人员。

正好这段时间,我在YourSQLDba上扩展一些功能,于是我将这个存储过程放置在YouSQLDba数据库中。

USE [YourSQLDba]
GO

IF NOT EXISTS(SELECT * FROM sys.objects WHERE object_id=OBJECT_ID(N'[Maint].[BlockingSQLHistory]') AND type='U')
BEGIN
CREATE TABLE Maint.BlockingSQLHistory
(
                RecordTime                        DATETIME           ,
                DatabaseName                      SYSNAME            ,
                BlockingSessesionId               SMALLINT           ,
                ProgramName                       NCHAR(128)         ,
                UserName                          NCHAR(256)         ,
                ClientIpAddress                   VARCHAR(48)        ,
                WaitType                          NCHAR(60)          ,
                BlockingStartTime                 DATETIME           ,
                WaitDuration                      BIGINT             ,
                BlockedSessionId                  INT                ,
                BlockedSQLText                    NVARCHAR(MAX)      ,
                BlockingSQLText                   NVARCHAR(MAX)      ,
                CONSTRAINT PK_BlockingSQLHistory  PRIMARY KEY(RecordTime)
)

END
GO

存储过程如下所示:

USE [YourSQLDba]
GO

IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Maint].[sp_who_blocking]') AND type in (N'P', N'PC'))
DROP PROCEDURE [Maint].[sp_who_blocking]
GO





SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO



--==================================================================================================================
--        ProcedureName         :            [Maint].[sp_who_blocking]
--        Author                :            Kerry    http://www.cnblogs.com/kerrycode/
--        CreateDate            :            2014-04-23
--        Description           :            监控数据库阻塞情况,显示阻塞会话信息或收集阻塞会话信息或发送告警邮件
/******************************************************************************************************************
        Parameters                   :                                    参数说明
********************************************************************************************************************
            @OutType         :            默认为输出阻塞会话信息,"Table", "Email"分别表示将阻塞信息写入表或邮件发送
            @EmailSubject    :            邮件主题.默认为Sql Blocking Alert,一般指定,例如“ServerName Sql Blocking Alert"
            @ProfileName     :            @profile_name 默认值为YourSQLDba_EmailProfile
            @RecipientsLst   :            收件人列表
********************************************************************************************************************
   Modified Date    Modified User     Version                 Modified Reason
********************************************************************************************************************
    2014-04-23             Kerry         V01.00.00         新建存储过程[Maint].[sp_who_blocking]

*******************************************************************************************************************/
--==================================================================================================================
CREATE PROCEDURE [Maint].[sp_who_blocking]
(
        @OutType
            VARCHAR(8) ='Default'                  ,
        @EmailSubject
            VARCHAR(120)='Sql Blocking Alert'      ,
        @ProfileName
            sysname='YourSQLDba_EmailProfile'      ,
        @RecipientsLst
             VARCHAR(MAX) = NULL
)
AS
BEGIN

SET NOCOUNT ON;

DECLARE @HtmlContent  NVARCHAR(MAX) ;

    IF @OutType NOT IN ('Default', 'Table','Email')
    BEGIN
        PRINT 'The parameter @OutType is not correct,please check it';

        return;
    END

    IF @OutType ='Default'
        BEGIN

              SELECT db.name                                 AS DatabaseName
                    ,wt.blocking_session_id                  AS BlockingSessesionId
                    ,sp.program_name                         AS ProgramName
                    ,COALESCE(sp.LOGINAME, sp.nt_username)   AS UserName
                    ,ec1.client_net_address                  AS ClientIpAddress
                    ,wt.wait_type                            AS WaitType
                    ,ec1.connect_time                        AS BlockingStartTime
                    ,wt.WAIT_DURATION_MS/1000                AS WaitDuration
                    ,ec1.session_id                          AS BlockedSessionId
                    ,h1.TEXT                                 AS BlockedSQLText
                    ,h2.TEXT                                 AS BlockingSQLText
              FROM sys.dm_tran_locks AS tl
              INNER JOIN sys.databases db
                ON db.database_id = tl.resource_database_id
              INNER JOIN sys.dm_os_waiting_tasks AS wt
                ON tl.lock_owner_address = wt.resource_address
              INNER JOIN sys.dm_exec_connections ec1
                ON ec1.session_id = tl.request_session_id
              INNER JOIN sys.dm_exec_connections ec2
                ON ec2.session_id = wt.blocking_session_id
              LEFT OUTER JOIN master.dbo.sysprocesses sp
                ON SP.spid = wt.blocking_session_id
              CROSS APPLY sys.dm_exec_sql_text(ec1.most_recent_sql_handle) AS h1
              CROSS APPLY sys.dm_exec_sql_text(ec2.most_recent_sql_handle) AS h2;
         END
     ELSE IF @OutType='Table'
        BEGIN

              INSERT INTO [Maint].[BlockingSQLHistory]
              SELECT GETDATE()                               AS RecordTime
                    ,db.name                                 AS DatabaseName
                    ,wt.blocking_session_id                  AS BlockingSessesionId
                    ,sp.program_name                         AS ProgramName
                    ,COALESCE(sp.LOGINAME, sp.nt_username)   AS UserName
                    ,ec1.client_net_address                  AS ClientIpAddress
                    ,wt.wait_type                            AS WaitType
                    ,ec1.connect_time                        AS BlockingStartTime
                    ,wt.WAIT_DURATION_MS/1000                AS WaitDuration
                    ,ec1.session_id                          AS BlockedSessionId
                    ,h1.TEXT                                 AS BlockedSQLText
                    ,h2.TEXT                                 AS BlockingSQLText
              FROM sys.dm_tran_locks AS tl
              INNER JOIN sys.databases db
                ON db.database_id = tl.resource_database_id
              INNER JOIN sys.dm_os_waiting_tasks AS wt
                ON tl.lock_owner_address = wt.resource_address
              INNER JOIN sys.dm_exec_connections ec1
                ON ec1.session_id = tl.request_session_id
              INNER JOIN sys.dm_exec_connections ec2
                ON ec2.session_id = wt.blocking_session_id
              LEFT OUTER JOIN master.dbo.sysprocesses sp
                ON SP.spid = wt.blocking_session_id
              CROSS APPLY sys.dm_exec_sql_text(ec1.most_recent_sql_handle) AS h1
              CROSS APPLY sys.dm_exec_sql_text(ec2.most_recent_sql_handle) AS h2;
         END
      ELSE IF @OutType='Email'
         BEGIN

            SET @HtmlContent =
               N'<head>'
             + N'<style type="text/css">h2, body {font-family: Arial, verdana;} table{font-size:11px; border-collapse:collapse;} td{background-color:#F1F1F1; border:1px solid black; padding:3px;} th{background-color:#99CCFF;}</style>'
             + N'<table border="1">'
             + N'<tr>
                 <th>DatabaseName</th>
                 <th>BlockingSessesionId</th>
                 <th>ProgramName</th>
                 <th>UserName</th>
                 <th>ClientIpAddress</th>
                 <th>WaitType</th>
                 <th>BlockingStartTime</th>
                 <th>WaitDuration</th>
                 <th>BlockedSessionId</th>
                 <th>BlockedSQLText</th>
                 <th>BlockingSQLText</th>
                </tr>' +
             CAST (
                    (SELECT db.name                                  AS TD, ''
                           ,wt.blocking_session_id                   AS TD, ''
                           ,sp.program_name                          AS TD, ''
                           ,COALESCE(sp.LOGINAME, sp.nt_username)    AS TD, ''
                           ,ec1.client_net_address                   AS TD, ''
                           ,wt.wait_type                             AS TD, ''
                           ,ec1.connect_time                         AS TD, ''
                           ,wt.WAIT_DURATION_MS/1000                 AS TD, ''
                           ,ec1.session_id                           AS TD, ''
                           ,h1.TEXT                                  AS TD, ''
                           ,h2.TEXT                                  AS TD, ''

                    FROM sys.dm_tran_locks AS tl
                    INNER JOIN sys.databases db
                            ON db.database_id = tl.resource_database_id
                    INNER JOIN sys.dm_os_waiting_tasks AS wt
                            ON tl.lock_owner_address = wt.resource_address
                    INNER JOIN sys.dm_exec_connections ec1
                            ON ec1.session_id = tl.request_session_id
                    INNER JOIN sys.dm_exec_connections ec2
                            ON ec2.session_id = wt.blocking_session_id
                    LEFT OUTER JOIN master.dbo.sysprocesses sp
                            ON SP.spid = wt.blocking_session_id
                    CROSS APPLY sys.dm_exec_sql_text(ec1.most_recent_sql_handle) AS h1
                    CROSS APPLY sys.dm_exec_sql_text(ec2.most_recent_sql_handle) AS h2

               FOR XML PATH('tr'), TYPE
                ) AS NVARCHAR(MAX) ) +
                N'</table>'


                IF @HtmlContent  IS NOT NULL

                BEGIN

                    EXEC msdb.dbo.sp_send_dbmail
                            @profile_name = @ProfileName    ,
                            @recipients   = @RecipientsLst    ,
                            @subject      = @EmailSubject    ,
                            @body         = @HtmlContent    ,
                            @body_format  = 'HTML' ;

                END
        END

END
GO

最后在数据库新建一个作业,调用该存储过程,然后在某段时间启用作业监控数据库的阻塞情况,作业的执行频率是个比较难以定夺的头痛问题,具体要根据系统情况来决定,我习惯2分钟执行一次。

最后,这个脚本还有一个问题,如果阻塞或被阻塞的SQL语句是某个存储过程里面的一段脚本,显示的SQL是整个存储过程,而不是正在执行的SQL语句,目前还没有想到好的方法解决这个问题。我目前手工去查看阻塞情况,如果非要查看存储过程里面被阻塞的正在执行的SQL,一般结合下面SQL语句查看(输入阻塞或被阻塞会话ID替代@sessionid)

SELECT   [Spid] = er.session_id
        ,[ecid]
        ,[Database] = DB_NAME(sp.dbid)
        ,[Start_Time]
        ,[SessionRunTime]    = datediff(SECOND, start_time,getdate())
        ,[SqlRunTime]=     RIGHT(convert(varchar,
                                 dateadd(ms, datediff(ms, sp.last_batch, getdate()), '1900-01-01'),
                            121), 12)
        ,[HostName]
        ,[Users]=COALESCE(sp.LOGINAME, sp.nt_username)
        ,[Status] = er.status
        ,[WaitType] = er.wait_type
        ,[Waitime] = er.wait_time/1000
        ,[Individual Query] = SUBSTRING(qt.text, er.statement_start_offset / 2,
                                       ( CASE WHEN er.statement_end_offset = -1
                                              THEN LEN(CONVERT(NVARCHAR(MAX), qt.text))
                                                   * 2
                                              ELSE er.statement_end_offset
                                         END - er.statement_start_offset ) / 2)
        ,[Parent Query] = qt.text
        ,[PROGRAM_NAME] = program_name
FROM    sys.dm_exec_requests er
        INNER JOIN sys.sysprocesses sp ON er.session_id = sp.spid
        CROSS APPLY sys.dm_exec_sql_text(er.sql_handle) AS qt
WHERE   session_Id = @sessionid;

为什么数据库有时候不能定位阻塞(Blocker)源头的SQL语句 - 潇湘隐者 - 博客园

mikel阅读(640)

来源: 为什么数据库有时候不能定位阻塞(Blocker)源头的SQL语句 – 潇湘隐者 – 博客园

   在SQL Server数据库或OACLE数据库当中,通常一个会话持有某个资源的锁,而另一个会话在请求这个资源,就会出现阻塞(blocking)。这是DBA经常会遇到的情况。当出现SQL语句的阻塞时,很多人想查看阻塞的源头(哪个SQL语句阻塞了哪个SQL),这样方便直观、简洁明了的定位问题。但是很多时候,很多场景,我们通过SQL语句并不能或者说不容易定位到阻塞者(Blocker)的SQL语句,当然我们可以很容易找到被阻塞的SQL语句,以及它在等待的锁资源。下面我们先分析一下SQL Server数据库的这类场景,然后分析一下ORACLE数据库的这类场景。如有不足的地方,敬请指出。

 

在SQL Server当中,我们先准备下面测试环境(测试用的表和数据)。

 
USE Test;
GO
CREATE TABLE Test 
(
    ID        INT ,
    NAME    VARCHAR(12)
);
 
 
INSERT INTO Test
VALUES (1000, 'Kerry');
 
INSERT INTO Test
VALUES(1001, 'Jimmy');

 

场景1:我们构造这样一个简单的场景,例如如下:

 

在会话81中执行下面SQL语句

BEGIN TRAN

    UPDATE Test SET NAME='Tina' WHERE ID=1000;

 

 

在会话72中执行下面SQL语句

SELECT * FROM TEST;

 

 

在另外一个会话窗口执行下面语句,查看阻塞(blocker)者和被阻塞者的SQL语句(这里能够定位到阻塞者(blocker)的SQL语句)。如下所示

SELECT wt.blocking_session_id                    AS BlockingSessesionId
      ,sp.program_name                           AS Blocking_ProgramName
      ,COALESCE(sp.LOGINAME, sp.nt_username)     AS Blocking_HostName
      ,ec1.client_net_address                    AS ClientIpAddress
      ,db.name                                   AS DatabaseName
      ,wt.wait_type                              AS WaitType
      ,ec1.connect_time                          AS BlockingStartTime
      ,wt.WAIT_DURATION_MS/1000                  AS WaitDuration
      ,ec1.session_id                            AS BlockedSessionId
      ,h1.TEXT                                   AS BlockedSQLText
      ,h2.TEXT                                   AS BlockingSQLText
FROM sys.dm_tran_locks  AS tl WITH(NOLOCK)
INNER JOIN sys.databases AS db  WITH(NOLOCK)
  ON db.database_id = tl.resource_database_id
INNER JOIN sys.dm_os_waiting_tasks AS wt  WITH(NOLOCK)
  ON tl.lock_owner_address = wt.resource_address
INNER JOIN sys.dm_exec_connections  ec1 WITH(NOLOCK)
  ON ec1.session_id = tl.request_session_id
INNER JOIN sys.dm_exec_connections  ec2 WITH(NOLOCK)
  ON ec2.session_id = wt.blocking_session_id
LEFT OUTER JOIN master.dbo.sysprocesses AS sp WITH(NOLOCK)
  ON SP.spid = wt.blocking_session_id
CROSS APPLY sys.dm_exec_sql_text(ec1.most_recent_sql_handle) AS h1
CROSS APPLY sys.dm_exec_sql_text(ec2.most_recent_sql_handle) AS h2

clip_image001

 

但是这个场景是一个非常理想化的场景,实际场景中,可能会话81接下来会去执行其它SQL语句,它并不会一直停留在这个SQL语句上,例如,我们在会话81中执行SELECT GETDATE();这个SQL语句

BEGIN TRAN

 UPDATE Test SET NAME='Tina' WHERE ID=1000;

 SELECT GETDATE();

clip_image002

 

如上所示,此时查到的Blocker者的SQL语句为”SELECT GETDATE();”, 而这个SQL其实和被阻塞的SQL没有半毛关系。即使使用sp_WhoIsActive这样专业的SQL亦是如此。

clip_image003

 

当然我们可以查看其等待的锁对象信息,这也是我们所能追踪、捕获的。如下所示:

<Database name="Test">
  <Locks>
    <Lock request_mode="S" request_status="GRANT" request_count="1" />
  </Locks>
  <Objects>
    <Object name="Test" schema_name="dbo">
      <Locks>
        <Lock resource_type="OBJECT" request_mode="IS" request_status="GRANT" request_count="1" />
        <Lock resource_type="PAGE" page_type="*" request_mode="IS" request_status="GRANT" request_count="1" />
        <Lock resource_type="RID" page_type="*" request_mode="S" request_status="WAIT" request_count="1" />
      </Locks>
    </Object>
  </Objects>
</Database>

clip_image004

 

这种场景,如果只是某个会话发出的即席查询,那么你几乎已经很难捕获到阻塞的源头UPDATE Test SET NAME=’Tina’ WHERE ID=1000这个SQL语句了。除非你结合其它一些手段,逆向推断。

 

场景2:上面查找SQL阻塞的SQL语句,有时候只能定位到某一个存储过程或一大段即席查询SQL。

 

例如,下面一个构造的存储过程,一个用户正在一个会话当中执行它,

CREATE PROCEDURE PRC_TEST
AS
BEGIN

    BEGIN TRAN TR1

    UPDATE Test SET NAME='YourName' WHERE ID=1000;

    SELECT * FROM sys.sysprocesses WHERE spid=@@SPID;

    WAITFOR DELAY '00:00:20';

    COMMIT TRAN TR1;

END
GO

 

另外一个用户在另外一个会话执行下面查询SQL语句


SELECT * FROM TEST;

 

查看阻塞的历史记录

clip_image005

 

你会看到捕获的是整个存储过程,当然这个测试案例很容易知道是那个SQL语句阻塞了,实际的存储过程可能业务很复杂,SQL语句也非常多,你想从一个存储过程里面找到阻塞者(Blocker)的SQL语句其实是非常麻烦的。需要你仔细甄别,当存储过程的业务逻辑复杂,SQL语句非常多时,这是一个头痛的事情

其实遇到这些场景,我们大可不必一定要查看阻塞这(Blocker)的具体SQL,我们只需要查看被阻塞者,等待的锁对象资源的相关信息即可,你可以大致判断到底是一个什么类型的SQL导致了这类阻塞。

 

 

那么我们接下来看看ORACLE数据库场景吧。我们先准备一个测试环境(测试表和相关数据)

 

CREATE TABLE "TEST"."TEST"
(    "ID" NUMBER,
    "NAME" VARCHAR2(12)
);

INSERT INTO TEST
SELECT 1001, 'jimmy' FROM DUAL UNION ALL
SELECT 1002, 'Kerry' FROM DUAL;

COMMIT;

 

 

接下来我们在会话窗口一执行下面SQL:

[oracle@DB-Server ~]$ sqlplus test/test

SQL*Plus: Release 11.2.0.1.0 Production on Tue Aug 30 10:16:43 2016

Copyright (c) 1982, 2009, Oracle.  All rights reserved.


Connected to:
Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production
With the Partitioning, OLAP, Data Mining and Real Application Testing options

SQL> show user;
USER is "TEST"
SQL> UPDATE TEST SET NAME='KKK' WHERE ID =1001;

1 row updated.

SQL>

 

在另外一个会话窗口二执行下面SQL

[oracle@DB-Server ~]$ sqlplus test/test

SQL*Plus: Release 11.2.0.1.0 Production on Tue Aug 30 10:17:22 2016

Copyright (c) 1982, 2009, Oracle.  All rights reserved.


Connected to:
Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production
With the Partitioning, OLAP, Data Mining and Real Application Testing options

SQL> show user;
USER is "TEST"
SQL> UPDATE TEST SET NAME='Ken' WHERE ID =1001;

 

然后我们在第三个窗口执行下面SQL语句,查看阻塞和被阻塞的SQL语句

SELECT dba_objects.object_name,
       locks_t.row#,
       locks_t.blocked_secs,
       locks_t.blocker_text,
       locks_t.blocked_text,
       locks_t.blocked_sql_text,
       locks_t.blocking_sql_text
  FROM (SELECT /*+ NO_MERGE */
               blocking_lock_session.username||'@'||blocking_lock_session.machine||'(SID='||blocking_lock_session.sid||') ['||
               blocking_lock_session.program||'/PID='||blocking_lock_session.process||']' as blocker_text,
               blocked_lock_session.username||'@'||blocked_lock_session.machine|| '(SID='||blocked_lock_session.sid||') ['||
               blocked_lock_session.program||'/PID='||blocked_lock_session.process||']' as blocked_text,
               blocked_lock_session.row_wait_obj#,
               blocked_lock_session.row_wait_file#,
               blocked_lock_session.row_wait_block#,
               blocked_lock_session.row_wait_row#,
               DBMS_ROWID.ROWID_CREATE (1,
                  blocked_lock_session.row_wait_obj#,
                  blocked_lock_session.row_wait_file#,
                  blocked_lock_session.row_wait_block#,
                  blocked_lock_session.row_wait_row#) row#,
               blocked_lock_session.seconds_in_wait blocked_secs,
               blocked_sql.sql_text blocked_sql_text,
               blocking_sql.sql_text blocking_sql_text
          FROM v$lock blocking_lock,
               v$session blocking_lock_session,
               v$lock blocked_lock,
               v$session blocked_lock_session,
               v$sql blocked_sql,
               v$sql blocking_sql
         WHERE blocking_lock.block = 1
           AND blocking_lock.id1 = blocked_lock.id1
           AND blocking_lock.id2 = blocked_lock.id2
           AND blocked_lock.request > 0
           AND blocking_lock.sid = blocking_lock_session.sid
           AND blocked_lock.sid = blocked_lock_session.sid
           AND blocked_lock_session.sql_id = blocked_sql.sql_id
           AND blocked_lock_session.sql_child_number = blocked_sql.child_number
           AND blocking_lock_session.PREV_SQL_ADDR(+)  =blocking_sql.ADDRESS
       ) locks_t,
       dba_objects
 WHERE locks_t.row_wait_obj# = dba_objects.object_id
ORDER BY locks_t.blocked_secs;

clip_image006

 

如果我们在会话窗口1,再执行一个语句,如下所示

 

SQL> show user;
USER is "TEST"
SQL> UPDATE TEST SET NAME='KKK' WHERE ID =1001;

1 row updated.

SQL> select * from dual;

D
-
X

 

此时捕获到的是select * from dual; 这个SQL跟被阻塞的SQL没有任何关系,当然如果你继续在会话窗口执行其它SQL语句,捕获的都是不相关的SQL语句,已经没有任何意义

clip_image007

 

出现这个问题,是因为当一个会话正在执行某个SQL语句,那么v$session视图中的SQL_ID记录的是正在执行SQL的SQL_ID,当会话空闲或执行其它SQL语句后,SQL_ID就会变化,PRE_SQL_ID记录上一个执行完的SQL的SQL_ID值,PREV_SQL_ADDR也是如此。如下英文所述 :

 

According to the Reference Manual entry for V$SESSION the SQL_ID column represents the current SQL statement being executed by a session.  If the session is idle there is no current SQL statement.  Also if a session performs an update then performs a query the SQL_ID would reflect the query and not the update which is the statement that is blocking.  There is in fact no query that is guaranteed to find the blocking SQL.  Unless the blocking statement is the current statement all you can find for sure I the blocking session

 

如果你不用SQL*Plus,使用PL/SQL Developer这个工具,你会看到BLOCKING_SQL_TEST永远都是begin sys.dbms_output.get_line(line => :line, status => :status); end; 这个是因为PL/SQL Developer在执行完SQL后,会调用其它SQL语句,当然SQL Developer不会有这样的问题。

 

所以综上述,想要找到阻塞的源头SQL语句,只用SQL查询,其实在很多场景是不太现实的,所以很多SQL语句都只给出阻塞者的会话信息或锁定对象信息。如下所示

 

会话ID为8的会话执行下面SQL

UPDATE TEST SET NAME='TEST' WHERE ID=1001;

 

会话ID为137的会话执行下面SQL

UPDATE TEST SET NAME='TES1' WHERE ID=1001;

然后我们使用get_locked_objects_rpt.sql查看被阻塞的SQL,以及锁定相关对象的信息(get_locked_objects_rpt.sql请参考get_locked_objects_rpt.sql

SQL> @get_locked_objects_rpt.sql
Enter value for 1: 6
old  42:    AND locks_t.blocked_secs > &1
new  42:    AND locks_t.blocked_secs > 6
========= $Revision: 1.4 $ ($Date: 2013/09/16 13:15:22 $) ===========
Locked object : TEST
Locked row#   : AAASEkAAEAAAADVAAA
Blocked for   : 19 seconds
Blocker info. : TEST@GFG1\GET253194(SID=8) [plsqldev.exe/PID=17988:14616]
Blocked info. : TEST@get253194(SID=137) [SQL Developer/PID=17780]
Blocked SQL   : UPDATE TEST SET NAME='TES1' WHERE ID=1001
Found 1 blocked session(s).
Disconnected from Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production
With the Partitioning, OLAP, Data Mining and Real Application Testing options
[oracle@DB-Server ~]$

clip_image008

然后我通过上面的Locked Object知道被锁定的对象为Test表的ROWID为AAASRCAAEAAAADVAAA的记录,如下所示

clip_image009

 

 

SQL Server中SELECT会真的阻塞SELECT吗? - 潇湘隐者 - 博客园

mikel阅读(681)

来源: SQL Server中SELECT会真的阻塞SELECT吗? – 潇湘隐者 – 博客园

SQL Server中,我们知道一个SELECT语句执行过程中只会申请一些意向共享锁(IS) 与共享锁(S), 例如我使用SQL Profile跟踪会话86执行SELECT * FROM dbo.TEST WHERE OBJECT_ID =1 这个查询语句,其申请、释放的锁资源的过程如下所示:

而且从最常见的锁模式的兼容性表,我们可以看到IS锁与S锁都是兼容的,也就是说SELECT查询是不会阻塞SELECT查询的。

现有的授权模式
请求的模式 IS S U IX SIX X
意向共享 (IS)
共享 (S)
更新 (U)
意向排他 (IX)
意向排他共享 (SIX)
排他 (X)

但是在某些特殊场景。你会看到SELECT语句居然“阻塞”SELECT操作,那么SQL Server中SELECT会真的阻塞SELECT操作吗?我们先构造测试的案例场景,那么先准备测试数据吧

CREATE TABLE TEST (OBJECT_ID  INT, NAME VARCHAR(8)); CREATE INDEX PK_TEST ON TEST(OBJECT_ID)   DECLARE @Index INT =0; WHILE @Index < 20BEGIN    INSERT INTO TEST    SELECT @Index, ‘kerry’;        SET @Index = @Index +1;END

在会话窗口A中,执行下面SQL语句,模拟一个UPDATE语句正在执行

 BEGIN TRANSACTION     UPDATE dbo.TEST SET NAME=’Kerry’ WHERE OBJECT_ID=1;    –ROLLBACK;

会话窗口B中,执行下面的SQL语句

SELECT * FROM dbo.TEST WHERE OBJECT_ID=1

会话窗口C中,执行下面的SQL语句

SELECT * FROM dbo.TEST WHERE OBJECT_ID=1

我实验的场景下,会话窗口A的会话ID为85,会话窗口B的会话ID为90,会话窗口C的会话ID为87,如下所示

如下所示,你会看到SELECT语句“阻塞”了SELECT语句,即会话90“阻塞”了会话87, 它们的等待事件都为LCK_M_S,也就是说它们都在等待获取共享锁,也许你会置疑这个SQL是否有问题,那么我们使用SP_WHO来查看,你会发现也是如此,如下所示:

如下所示,我们会发现会话ID为90 、87的会话都在等待类型为RID,Resource为1:24171:1的共享锁

其实应该说,会话87、90都在等待RID对象的共享锁,我们知道共享锁与意向共享锁都是兼容的,所以SELECT是不会阻塞SELECT的,那么又怎么解释这个现象呢?在宋大神的指点下,粗略的翻了Database System Implementaion这本书(很多原理性知识,看起来相当吃力)。里面介绍了在锁表(lock table)以及Element Info、Handling Lock Requests、Handling Unlocks等概念,有一个有意思的图所示,

在锁表(lock table)里,elements info里的锁的申请是在一个类似队列的结构。先进先出机制,所以当会话90先进入队列,它在等待共享锁(S), 会话87也进入队列等待共享锁(S),而且它在会话90的后面(即会话90这个elements info后面的Next指针指向会话87会话的事务),由于两个会话都被阻塞,这两个会话的Wait字段都是Yes,由于内部某些机制,会话87显示阻塞它的会话为90(这个是我个人臆测,实际具体原因有待考究),实质阻塞的源头还是会话85. 当会话85释放排它锁(X)后,会话队列根据下面几个原则来处理解锁(Handling Unlocks):

1: First-come-first-served: Grant the lock request that has been waiting the longest. This strategy guarantees no starvation, the situation where a transaction can wait forever for a lock

先来先服务(队列的原则):授予锁等待时间最长的锁请求,这种策略保证不会饿死(翻译感觉不贴切),即一个事务不会永远等待锁的情况。

  1. Priority to shared locks: First grant all the shared locks waiting. Then,grant one update lock, if there are any waiting. Only grant an exclusivelock if no others are waiting. This strategy can allow starvation, if atransaction is waiting for a U or X lock.

共享锁优先,首先授予所有等待共享锁(S),然后授予其中一个更新锁(U),如果有其它类型等待,只有在没有其它锁等待时,才授予排它锁、这一策略允许等待更新锁或排它锁的事务饿死(结束)

  1. Priority to upgrading: If there is a transaction with a U lock waiting toupgrade it to an X lock, grant that first. Otherwise, follow one of theother strategies mentioned.

锁升级优先,如果有一个持有共享锁(U)等待升级Wie排他锁(X),那么先授予它排它锁,否则采用前面已经提到的策略中的一个。

按照这些原则,当会话85释放了排它锁(X)后,调度器(Scheduler)应该会根据先后顺序依次授予会话90、87共享锁(S),两者的阻塞会几乎同时消失。 这个可以也可以通过实验进行一个大概的推断, 在上面实验中,你可以手工取消90会话的查询操作,然后再查看阻塞情况,就会发现会话87被85阻塞了。这个阻塞的源头就变成了85,而不是90了。

PS:上面是个人结合一些知识和理解,做的一些肤浅的判断与分析,如果不对的地方,敬请指正!

参考资料:

Database System Implementaion