筆記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 --去掉那些對語句運行貢獻不大的索引。不能隨便往表格上加索引