笔记46-徐 一个常见的select ,update ,insert ,delete动作要申请的锁
笔记46-徐 一个常见的select ,update ,insert ,delete动作要申请的锁
1 --一个常见的select ,update ,insert ,delete动作要申请的锁 2 3 --两个表都加了索引的字段 4 --employeeID 5 --managerid 6 --modifieddate 7 8 --------------------一个常见的select动作要申请的锁----------------------------------------------- 9 --在可重复读的隔离级别下,共享锁要保留到事务提交的时候才释放,所以如果 10 --这个隔离级别下开启另一个事务,再运行一个查询语句,就能看到这个查询所申请的主要 11 --共享锁。因此可以使用这种简单方法分析一下一个查询语句会申请哪些锁,并且不需要 12 --SQL Trace 13 --(1)在连接A中,将事务隔离级别设置为可重复读(repeatable read) 14 --(2)在运行查询语句之前先开启一个事务 15 --(3)运行查询语句,但是不提交这个事务 16 --(4)在第二个连接里,查询sys.dm_tran_locks动态管理视图来分析查询结束后连接 17 --A还持有的锁 18 19 20 --我们先在有聚集索引的那张表上运行一句最简单的查询 在SSMS里新建一个查询--------- 21 --查询一: 22 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ 23 GO 24 SET STATISTICS PROFILE ON 25 GO 26 --以下查询使用了聚集索引查找 ctrl+l 27 BEGIN TRAN 28 SELECT [EmployeeID],[LoginID],[Title] FROM [dbo].[Employee_Demo_BTree] WHERE [EmployeeID] IN(3,30,200) 29 ------------------------------------------------------------------------------------------------------- 30 31 --查看DMV看一下有多少个锁被这个连接持有----------------------------------------------------------- 32 USE [AdventureWorks] --要查询申请锁的数据库 33 GO 34 SELECT 35 [request_session_id], 36 c.[program_name], 37 DB_NAME(c.[dbid]) AS dbname, 38 [resource_type], 39 [request_status], 40 [request_mode], 41 [resource_description],OBJECT_NAME(p.[object_id]) AS objectname, 42 p.[index_id] 43 FROM sys.[dm_tran_locks] AS a LEFT JOIN sys.[partitions] AS p 44 ON a.[resource_associated_entity_id]=p.[hobt_id] 45 LEFT JOIN sys.[sysprocesses] AS c ON a.[request_session_id]=c.[spid] 46 WHERE c.[dbid]=DB_ID('AdventureWorks') ----要查询申请锁的数据库 47 ORDER BY [request_session_id],[resource_type] 48 49 50 --查询一所持有的锁 51 --(1)因为连接正在访问数据库[AdventureWorks],所以在数据库一级加了一个共享锁,以防止 52 --别人将数据库删除 53 54 55 --(2)因为正在访问表格[Employee_Demo_BTree],所以在表格上加了一个意向共享锁,以防止 56 --别人修改表的定义 resource_type:object 57 58 59 --(3)查询有3条记录返回,所以在这3条记录所在的聚集索引键上,分别持有一个共享锁。 60 --在这3个键所在的页面上,持有一个意向共享锁 resource_type:page,key 锁住整页 61 62 --可以说,这个查询申请锁的数目是很少的。其他用户访问同一张表,只要不访问这3条记录 63 --,就不会影响到。这是因为查询使用了“聚集索引查找”的关系 64 65 --------------------------------------------------------------------------------------------- 66 67 --运行查询二:在SSMS里新建一个查询,运行之前记得将前面查询一的事务提交或者回滚 多次运行最后那句COMMIT TRAN 68 --BEGIN TRAN 69 --SELECT [EmployeeID],[LoginID],[Title] FROM [dbo].[Employee_Demo_BTree] WHERE [EmployeeID] IN(3,30,200) 70 --COMMIT TRAN 71 72 --查询二 73 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ 74 GO 75 SET STATISTICS PROFILE ON 76 GO 77 BEGIN TRAN 78 SELECT 79 [EmployeeID], 80 [LoginID], 81 [Title] 82 FROM [dbo].[Employee_Demo_Heap] 83 WHERE [EmployeeID]=3 84 85 --COMMIT TRAN 86 --运行之后最后记得加上COMMIT TRAN 87 88 ---------------分析------------------------------------------- 89 --因为[Employee_Demo_Heap]的[EmployeeID]上是一个非聚集索引,所以SQL在用非聚集索引 90 --找到这条记录之后,必须再到数据页面上把其他的行上面的数据找出来(所谓的“书签查找” bookmark lookup) 91 --从申请的锁上也能看出来,虽然只返回一条记录,可是他在[PK_Employee_EmployeeID_Demo_Heap] 92 --(index_id是2 表明是非聚集索引)上申请了一个key锁,在RID(datapage数据页上的行row) 93 --申请了一个row锁。在这两个资源所在的页面上各申请了一个page意向锁 94 95 96 --与上面的例子比较,虽然查询二返回的结果和查询一是一样的,但是由于他使用的是非聚集索引+书签查找 97 --bookmark lookup,所以申请的锁的数目要比查询一多。一个查询要使用的索引键(或者RID)数目越多 98 --,他申请的锁也就会越多。没有用到的索引上不会申请共享锁 99 100 101 102 103 --那么是不是所有的查询都只在返回的记录上加锁呢?现在再来做下面这个试验,运行他之前 104 --请记得将前面那个事务提交或回滚 105 106 -------------------------------------------------------------------------------------- 107 --修改一 :update语句:在SSMS里新建一个查询 108 --首先开启一个事务,修改一条查询不会返回记录 109 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ 110 GO 111 SET STATISTICS PROFILE ON 112 GO 113 BEGIN TRAN 114 UPDATE [dbo].[Employee_Demo_Heap] 115 SET [Title]='aaa' 116 WHERE [EmployeeID]=70 117 118 --再在另一个连接里运行查询三 119 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ 120 GO 121 SET STATISTICS PROFILE ON 122 GO 123 BEGIN TRAN 124 SELECT 125 [EmployeeID], 126 [LoginID], 127 [Title] 128 FROM [dbo].[Employee_Demo_Heap] 129 WHERE [EmployeeID] IN(3,30,200) 130 131 132 ---------------------分析-------------------------------------------------- 133 --由于要返回3条分布在不同数据页上的记录,SQL认为做非聚集索引+书签查找并不比 134 --做一个表扫描快,所以他直接选择了一个表扫描。 135 136 --DMV显示查询三已经得到了RID 1:3181:2和1:3181:29上的锁,他们应该是EmployeeID为3和30 137 --的,他在往下找EmployeeID为200的时候,读到了RID 1:3208:22。他是EmployeeID为70, 138 --被上面的update语句修改了,update那个事务还没有提交,所以查询三被阻塞了 139 140 --现在把修改一回滚,阻塞解除,查询三能够执行完毕,可以看到他的执行计划与他持有的锁 141 142 --SQL一直往下找 143 --书签 144 --70 ->还没有提交 145 --80 146 --90 147 --100 148 --. 149 --. 150 --. 151 --. 152 --. 153 --. 154 --200 155 156 --所以不能继续往下找,如果跳过的话,他不知道跳过的是不是就是他要找的那条记录,所以阻塞了 157 --一个RID对应一个EmployeeID 158 159 --与查询一不同的是,查询三不但在这三条记录所在页面(1:3211,1:3181)申请了IS锁, 160 --还在表格的所有页面上都申请了IS锁。所以这就是全表扫描,扫描了所有的数据页面带来 161 --的后果。更严重的是,查询三在扫描每一张页面的时候,会对读到的每一个数据记录加上 162 --一个共享锁(读完了这条记录就会释放,不用等到整个语句结束)只要有任何一个记录 163 --上的锁没有申请到,查询就会被阻塞 164 165 166 -------------------------------------------------------------------------------------------- 167 --运行修改二,再运行查询一 168 --修改二:update语句:在SSMS里新建一个查询 169 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ 170 GO 171 SET STATISTICS PROFILE ON 172 GO 173 BEGIN TRAN 174 UPDATE [dbo].[Employee_Demo_BTree] 175 SET [Title]='aaa' 176 WHERE [EmployeeID]=70 177 178 179 180 --再在另一个连接里运行查询一 181 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ 182 GO 183 SET STATISTICS PROFILE ON 184 GO 185 --以下查询使用了聚集索引查找 ctrl+l 186 BEGIN TRAN 187 SELECT [EmployeeID],[LoginID],[Title] FROM [dbo].[Employee_Demo_BTree] WHERE [EmployeeID] IN(3,30,200) 188 189 190 ---------------------------分析------------------------------------------------------------- 191 --这是因为查询一使用的是索引查找index seek,不需要每条记录都读一遍,所以就不用 192 --去读EmployeeID70,也就不会被阻塞住 因为select是select书签里面的内容根本不用到表里去读取 193 --数据 194 195 196 197 --------------------------总结select动作-------------------------------------------------------- 198 --规律:在非“未提交读”的隔离级别上 199 --已提交读 200 --可重复读 201 --可序列化 202 --(1)查询在运行过程中,会对每一条读到的记录或键值加共享锁。如果记录不用返回。 203 --那锁就会被释放。如果记录需要被返回,则视隔离级别而定,如果是“已提交读”,则也释放 204 --否则,不释放 205 206 --(2)对每一个使用到的索引,SQL也会对上面的键值加共享锁 207 208 --(3)对每个读过的页面,SQL会加一个意向锁 209 210 --(4)查询需要扫描页面和记录越多,锁的数目也会越多。查询用到的索引越多,锁的数目 211 --也会越多 212 213 214 --避免阻塞采取的方法 215 --(1)尽量返回少的记录集,返回的结果越多,需要的锁也就越多 216 217 --(2)如果返回结果集只是表格所有记录的一小部分,要尽量使用index seek,避免全表扫描这种 218 --执行计划 219 220 --(3)可能的话,设计好合适的索引,避免SQL通过多个索引才找到数据 221 222 --当然,这些对于“已提交读”以上隔离级别而言。如果使用“未提交读”,SQL就不会申请这些共享锁 223 --阻塞也不会发生 224 225 226 227 228 --------------------一个常见的update动作要申请的锁----------------------------------------------- 229 --对于update语句,可以简单理解为SQL先做查询,把需要修改的记录给找到,然后在这个记录 230 --上做修改。找记录的动作要加S锁,找到修改的记录后加U锁,再将U锁升级为X锁。 231 232 --这里用上面两张表做例子,选用repeatable read的隔离级别,运行一个update语句 233 USE [AdventureWorks] 234 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ 235 GO 236 BEGIN TRAN 237 UPDATE [dbo].[Employee_Demo_Heap] 238 SET [Title]='changeheap' 239 WHERE [EmployeeID] IN(3,30,200) 240 241 242 --这个update语句在非聚集索引上申请了3个U锁,在RID上申请了3个X锁。这是因为语句借助非聚集索引 243 --PK_Employee_EmployeeID_Demo_Heap(index_id是2)找到了这3条记录。非聚集索引PK_Employee_EmployeeID_Demo_Heap 244 --本身没有用到Title这一列,所以他自己不需要做修改。但是数据RID上有了修改,所以RID上加的是X锁,其他 245 --索引上没有加锁 246 247 --从这个例子可以看出,如果update借助了哪个索引,这个索引的键值上就会有U锁,没有用到的 248 --索引上没有锁。真正修改发生的地方会有X锁。对于查询涉及的页面,SQL加了IU锁意向更新锁,修改 249 --发生的页面,SQL加了IX锁 意向排他锁 (先查询再修改)锁key 锁索引键值 因为修改的列没有被索引 250 251 --如果修改的列被一个索引使用到了,会是什么情况呢?为了完成这个测试,先在Employee_Demo_BTree 252 --上建一个会被修改的索引 253 254 CREATE NONCLUSTERED INDEX [Employee_Demo_BTree_Title] ON [AdventureWorks].[dbo].[Employee_Demo_BTree] 255 ([Title] ASC) 256 257 258 --再运行下面语句 259 USE [AdventureWorks] 260 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ 261 GO 262 BEGIN TRAN 263 UPDATE [dbo].[Employee_Demo_Heap] 264 SET [Title]='changeheap' 265 WHERE [EmployeeID] IN(3,30,200) 266 267 268 269 --语句利用聚集索引找到要修改的3条记录.但是我们看到有9个键上有X锁。 270 --很有意思:PK_Employee_EmployeeID_Demo_BTree(index_id=1)聚集索引,也是数据存放的地方。 271 --刚才做的update语句没有改到他的索引列,他只需把Title这个列的值改掉。所以在index1上, 272 --他只申请3个X锁,每条记录一个 273 274 --但是表格在Title上面有一个非聚集索引IX_Employee_ManagerID_Demo_BTree(index_id=5), 275 --并且Title是第一列。他被修改后,原来的索引键值就要被删除掉,并且插入新的键值。 276 --所以在index_id=5 上要申请6个X锁,老的键值3个,新的键值3个 277 278 --因为其他索引没有使用到Title这一列,所以他们上面都没有申请锁 279 --这就是9个key锁的来源 280 281 282 --------------------update语句的规律--------------------------------------------------------- 283 --(1)对每一个使用到的索引,SQL会对上面的键值加U锁 284 285 --(2)SQL只对要做修改的记录或键值加X锁 286 287 --(3)使用到要修改的列的索引越多,锁的数目也会越多 288 289 --(4)扫描过的页面越多,意向锁也会越多。在扫描的过程中,对所有扫描到的记录也会加锁,哪怕 290 --上面没有修改 291 292 -----------------------------结论------------------------------------------------------- 293 --想降低一个update语句被别人阻塞住的几率,除了注意他的查询部分之外,数据库设计者 294 --还要做的事情有: 295 296 --(1)尽量修改少的记录集。修改的记录越多,需要的锁也就越多 297 298 --(2)尽量减少无谓的索引。索引的数目越多,需要的锁也可能越多 299 300 --(3)但是也要严格避免表扫描的发生。如果只是修改表格记录的一小部分,要尽量使用index seek索引查找 301 --避免全表扫描这种执行计划 302 303 304 305 306 307 308 --------------------一个常见的delete动作要申请的锁----------------------------------------------- 309 --这次使用read committed这个默认隔离级别 310 311 USE [AdventureWorks] 312 BEGIN TRAN 313 DELETE [dbo].[Employee_Demo_BTree] 314 WHERE [LoginID]='adventure-works\kim1' 315 316 317 --可以看到delete语句在聚集索引(index_id=1)和两个非聚集索引(index_id=2和3)上各申请了一个X锁 318 --在她们所在的页面上申请了一个IX锁 319 320 --如果使用repeatable read这个级别运行上面的delete命令,就能看出好像做select的时候一样,做 321 --delete的时候SQL也需要先找到要删除的记录。在找的过程中也会加锁 322 323 --现在运行一个新的delete语句,会使用全表扫描 324 325 USE [AdventureWorks] 326 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ 327 GO 328 BEGIN TRAN 329 DELETE [dbo].[Employee_Demo_Heap] 330 WHERE [LoginID]='adventure-works\tete0' 331 332 --可以看到delete语句在3个非聚集索引(index_id=2、3、4)上各申请了一个X锁。在她们 333 --所在的页面上申请了一个IX锁。在修改发生的heap数据页面上,申请了一个IX锁,相应的 334 --RID上(真正的数据记录)申请了一个X锁。其他扫描过的页面申请了IU锁 335 336 ----------------------------规律----------------------------------------------------------------- 337 --(1)delete的过程是先找到符号条件的记录,然后做删除。可以理解为先是一个select,然后 338 --是delete.所以,如果有合适的索引,第一步申请的锁就会比较少 不用表扫描 339 340 --(2)delete不但是把数据行本身删除,还要删除所有相关的索引键.所以一张表上索引数目越多 341 --锁的数目就会越多,也就越容易发生阻塞 342 343 --为了防止阻塞,我们既不能绝对地不建索引,也不能随随便便地建立很多索引, 344 --而是要建立对查找有利的索引.对于没有使用到的索引,还是去掉比较好 345 346 347 348 349 350 --------------------一个常见的insert动作要申请的锁----------------------------------------------- 351 --相对于select,update,delete,单条记录的insert操作对锁的申请比较简单。SQL会为新插入 352 --的数据本身申请一个X锁,在发生变化的页面上申请一个IX锁。由于这条记录是新插入的,被 353 --其他连接引用到的概率会相对小一些,所以出现阻塞的几率也要小 354 355 --还是用刚才的那两张表做例子。首先要插入的是heap结构的表 356 USE [AdventureWorks] 357 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ 358 GO 359 BEGIN TRAN 360 INSERT INTO [dbo].[Employee_Demo_Heap] 361 ( [EmployeeID] , 362 [NationalIDNumber] , 363 [ContactID] , 364 [LoginID] , 365 [ManagerID] , 366 [Title] , 367 [BirthDate] , 368 [MaritalStatus] , 369 [Gender] , 370 [HireDate] , 371 [ModifiedDate] 372 ) 373 SELECT 374 501, 375 480168528, 376 1009, 377 'adventure-works\thierry0', 378 263, 379 'Tool Desinger', 380 '1949-08-29 00:00:00.000', 381 'M', 382 'M', 383 '1998-01-11 00:00:00.000', 384 '2004-07-31 00:00:00.000' 385 386 387 388 389 --如果插入的是有B树结构的表格 390 391 USE [AdventureWorks] 392 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ 393 GO 394 BEGIN TRAN 395 INSERT INTO [dbo].[Employee_Demo_BTree] 396 ( [EmployeeID] , 397 [NationalIDNumber] , 398 [ContactID] , 399 [LoginID] , 400 [ManagerID] , 401 [Title] , 402 [BirthDate] , 403 [MaritalStatus] , 404 [Gender] , 405 [HireDate] , 406 [ModifiedDate] 407 ) 408 SELECT 409 501, 410 480168528, 411 1009, 412 'adventure-works\thierry0', 413 263, 414 'Tool Desinger', 415 '1949-08-29 00:00:00.000', 416 'M', 417 'M', 418 '1998-01-11 00:00:00.000', 419 '2004-07-31 00:00:00.000' 420 421 422 --两者都申请了以下锁资源: 423 --(1)数据库上的S锁(resource_type=DATABASE) 424 425 --(2)表上的IX锁(resource_type=OBJECT) 426 427 --(3)每个索引上都要插入一条新数据,所以有一个key上的X锁 428 429 --(4)在每个索引上发生变化的那个页面,申请了一个IX锁(resource_type=PAGE) 430 431 --唯一不同的是,是在heap结构上还得申请一个RID锁。因为真正的数据不是放在索引上,而是放在heap数据页面上 432 433 434 435 436 437 438 ----------------------------------------结论------------------------------------------- 439 --如果SQLDBA要控制SQL锁的申请和释放行为,以缓解阻塞和死锁问题,需要考虑的因素有: 440 --1、事务隔离级别的选定 441 --事务隔离级别越高,隔离度就越高,并发度也就越差。如果选择了比较高的隔离级别,SQL 442 --不可避免地要申请更多的锁,持有的时间也会增加。所以在设计应用的时候,一定要 443 --和用户谈好,尽量选择默认的隔离级别(read committed) 444 445 446 --2、事务的长短和事务的复杂度 447 --事务的长度和复杂度决定论这个事务在SQL内部会持续多长时间,也能决定SQL会同时在 448 --多少张表和索引上申请和持有锁。事务越简单,就越不容易发生阻塞和死锁。所以这 449 --也必须和用户商量好,尽量避免在一个事务里做很多事情 450 451 452 --3、从应用整体并发度考虑,单个事务一次处理的数据量不能过多 453 --应用的性能,不单要衡量单个连接的处理速度,也要衡量在并发处理的情况下,整体 454 --的平均速度怎么样。从连接个体来讲,可能在一个事务里把数据一次都处理掉比较快 455 --但是如果处理的数据量很大,就会影响到其他连接同时访问同一对象。所以,如果 456 --一个应用的并发要求比较高,就一定要严格控制单个事务处理的数据量。如果有什么 457 --事务操作需要访问或修改表格内的大量数据,最好调整到并发用户比较少的时候运行 458 459 460 --4、针对语句在表格上设计合适的索引 461 --合适的索引能使SQL在读取尽可能少的数据量的前提下,把需要处理的数据找到。如果 462 --没有合适的索引,SQL在做select,update,delete的时候,会申请比要处理的目标数据量 463 --多得多的锁,从而导致阻塞或死锁。这种情形可以通过加索引的方式提高并发度 464 --同时,SQL在做update,insert,delete的时候,会对有关联的所有索引都做修改,在她们 465 --上面申请锁。从这个角度讲,索引越多,产生的锁的数目也就越多,阻塞和死锁的几率 466 --也就会越高 467 468 --所以数据库设计员需要做的,是要确保有足够的索引,防止语句做全表扫描,但是也要 469 --去掉那些对语句运行贡献不大的索引。不能随便往表格上加索引