下篇的內容很多都會在工作中用到,尤其是可編程對象,那些年我們寫過的存儲過程,有木有?到目前為止很多大型傳統企業仍然很依賴存儲過程。這部分主要難理解的部分是事務和鎖機制這塊,本文會進行簡單的闡述。雖然很多SQL命令可以通過工具自動生成,但如果能通過記憶的話速度會更快,那么留給自己思考的時間就越多。此外,由於鎖這部分知識比較復雜,不同的數據庫廠商的實現也有不同,SQLSERVER除了我們常見的共享鎖、排它鎖(包括表級、頁級、行級),意向鎖,還有一些更復雜的鎖,如自旋鎖等,這部分內容會在之后的T-SQL深入解析部分再做介紹。言歸正傳,讓我們回到T-SQL的世界咯,希望一天我能成為這個世界里的一只小小的功夫熊貓,寫起SQL來,下筆如有神。
熊二恭祝大家猴年猴賽雷!
此外,剛仔細學習了下湯雪華大神關於12306領域模型設計的文章,對於思路是一種很好的啟迪,推薦下http://www.cnblogs.com/netfocus/p/5187241.html,博主xuanbg在評論中提及的動態分裂的思路也是棒棒噠。
本節主要介紹常見的DML操作,一般的添刪改查INSERT、UPDATE、DELETE(TRUNCATE),以及特殊一點的MERGE。其中T-SQL支持一下五種類型的INSERT,如下所示。
語句類型 | 解釋與示例 |
INSERT VALUES | 標准方式:INSERT INTO dbo.Orders(orderid, orderdate, empid, custid) VALUES(10001, '20160207', 3, 'A') |
INSERT SELECT | 使用便捷:INSERT INTO dbo.Orders(orderid, orderdate, empid, custid) SELECT 10001, '20120207', 2, 'B' UNION ALL SELECT 10002, '20120207', 3, 'C' |
INSERT EXEC | INSERT INTO dbo.Orders(orderid, orderdate, empid, custid) EXEC Sales.GetOrder @country = 'China' |
SELECT INTO | SELECT courtry, region, city INTO dbo.locations FROM Sales.Customers EXCEPT SELECT courtry, region, city FROM HR.Employees |
BULK INSERT | 類似SSIS的導入功能 BULK INSERT dbo.Orders FROM 'C;\orders.txt' WITH (DATAFILETYPE = 'char', FIELDTERMINATOR = ',', ROWTERMINATOR = '\n' ) |
接下來,介紹IDENTITY標識列的相關知識,以及T-SQL對sequence的支持,和ORACLE中序列一致。需要注意的是標識值無論操作是否成功都會自動增長,因此當需要真正連續的記錄號時,需要自己的替代方案。新增的序列對象是標准的SQL功能,它與標識列屬性不同,是一個不會綁定到特定表中列的對象,需要時查詢獲取即可。
操作類型 | 解釋與示例 |
創建標識列 | CREATE TABLE dbo.T1 (keycol INT NOT NULL IDENTITY(1, 1) CONSTRAINT PK_T1 PRIMARY KEY) |
插入數據(有標識列的情況下) | 注意忽略標識列,INSERT INTO dbo.T1(datacol) VALUES('AAAAA') |
獲取當前標識號 | SELECT SCOPE_IDENTITY(), @@identity, IDENT_CURRENT('Sales.Orders') 第一列獲取當前作用域下的標識號,第二列獲取會話生成的最后一個標識號(無論作用域),最后一個獲取全局的標記號,與會話無關 |
顯示插入標識 | SET IDENTITY_INSERT表名 ON/OFF |
創建序列 | CREATE SEQUENCE dbo.SeqOrderIDS AS INT MIN VALUE 1 CYCLE; |
使用序列 | SELECT NEXT VALUE FOR dbo.SeqOrderIDS |
-
刪除和更新操作
操作類型 | 解釋與示例 |
一般刪除 | DELETE FROM dbo.Orders WHERE orderdate < '20160207' |
TRANCATE語句 | TRANCATE TABLE dbo.T1,測試時經常使用,更高效,其實相當於先刪除表再創建表,而不是delete那樣按條刪除。DELETE是用完全日志模式,TRANCATE使用最小日志模式 |
基於聯接的DELETE | 場景:從Orders表中刪除所有與Customers表中美國客戶相關的行 標准方式:DELETE FROM dbo.Orders WHERE EXISTS ( SELECT * FROM dbo.Customer AS WHERE Orders.Custid = C.Custid AND C.Country = 'USA') 聯接DELETE:DELETE FROM O FROM dbo.Orders AS O JOIN dbo.Customers AS C ON O.custid = C.custid WHERE C.country = 'China' |
一般更新 | UPDATE dbo.OrderDetails SET discount = discount + 0.05 WHERE productid = 50 |
基於聯接的UPDATE | 場景:對客戶1的所有訂單明細增加5%折扣 標准方式:UPDATE dbo.OrderDetails SET discount += 0.05 WHERE EXISTS( SELECT * FROM dbo.Orders AS O WHERE O.orderid = OrderDetails.orderid AND O.custid = 1) 聯接UPDATE:UPDATE OD SET discount += 0.05 FROM dbo.OrderDetails AS OD JOIN dbo.Orders AS O ON OD.orderid = O.orderid WHERE O.custid =1 |
賦值UPDATE | DECLARE @nextval AS INT; UPDATE dbo.Sequences SET @nextval = val+= 1 WHERE id = 'SEQ1' |
-
合並數據和OUTPUT字句
從2008版本開始,T-SQL新增了一個MERGE操作符,相當於其他DML操作的組合,此外為了減少查詢次數,可以通過OUTPUT字句將更新的操作輸出(類似於觸發器的功能,包含inserted、deleted隱藏表),便於構建相應的流水表,不過實話實說通過業務來執行流水操作,比SQL組合更加合理,繼續使用表格將相關應用表述出來。
操作類型 | 解釋與示例 |
合並數據MERGE | MERGE INTO dbo.Customers AS TGT USING dbo.CustomerStage AS SRC ON TGT.custid = SRC.custid WHEN MATCHED THEN UPDATE SET TGT.company = SRC.companyname, TGT.phone = SRC.phone, TGT.address = SRC.address WHEN NOT MATCHED THEN INSERT (custid, companyname, phone, address) VALUES (SRC.custid, SRC.companyname, SRC.phone, SRC.address) |
通過表表達式修改數據 | WITH C AS( SELECT custid, OD.orderid, productid, discount, discount + 0.05 AS newdiscount FROM dbo.OrderDetails AS OD JOIN dbo.Orders AS O ON OD.orderid = O.orderid WHERE O.custid = 1 ) UPDATE C SET discount = newdiscount 補充一點是,內部的查詢操作支持TOP關鍵字哦 |
OUTPUT字句 | 場景:從Orders表中刪除所有與Customers表中美國客戶相關的行 標准方式:DELETE FROM dbo.Orders WHERE EXISTS ( SELECT * FROM dbo.Customer AS WHERE Orders.Custid = C.Custid AND C.Country = 'USA') 聯接DELETE:DELETE FROM O FROM dbo.Orders AS O JOIN dbo.Customers AS C ON O.custid = C.custid WHERE C.country = 'China' |
INSERT OUTPUT | INSERT INTO dbo.T1(datacol) OUTPUT inserted.keycol, inserted.datacol SELECT lastname FROM HR.Employees WHERE country = 'China' 其中datacol是需要返回的屬性 |
DELETE OUTPUT | DELETE FROM dbo.Orders OUTPUT deleted.orderid, deleted.orderid, deleted.empid, deleted.custid WHERE orderdate < '20160101' |
UPDATE OUTPUT | UPDATE dbo.OrderDetails SET discount += 0.05 OUTPUT inserted.productid, deleted.discount AS olddiscount, inserted.discount AS newdiscount WHERE productid = 51 |
事務的概念早已為大家所熟知,想提的一點是其也可以稱之為工作單元,包含查詢和修改數據的多種活動,UnitOfWork工作單元這個企業架構設計模式實際上也是其實現之一。實際中,最常見的是將插入訂單和插入訂單詳細放入一個事務中,事務的ACID屬性及簡單事務應用示例如下。
原子性(Atom): 事務是一個原子的工作單元,一起提交或撤銷。
一致性(Consistency): 其是一個主觀概念,取決於應用程序的需求,指數據的狀態,與之后數據庫的隔離級別緊密關聯。
隔離性(Isolation): 其實一種控制訪問數據的機制,在T-SQL中,支持鎖和行版本控制兩種模式來處理隔離。
持久性(Duration): 數據修改在寫入數據文件前,會先寫入日志文件,但出現故障時,會通過重做和撤銷來恢復數據。
DECLARE @neworderid AS INT INSERT INTO Sales.Order(custid, orderdate) VALUES (34, '20160213') SET @neworderid = SCOPE_IDENTITY() INSERT INTO Sales.OrderDetail(ordered, productid, unitprice, qty, discount) VALUES (@@orderid, 11, 14.00, 12, 0.000) |
-
鎖和阻塞
正如之前所提到的T-SQL支持兩種模式來處理隔離,一種是鎖,這是一種"悲觀式並發",在默認的READ COMMITED隔離級別下,一旦一個事務中修改數據,那么這個將不能被其他事務讀取,因為會給該數據加上排它鎖,而當讀取數據時獲取共享鎖,其他事務可以並行讀取;另一種是行版本控制技術,是一種"樂觀式並發",其默認的隔離級別為READ COMMITED SNAPSHOT,事務中修改數據時,其他事務時可以進行讀取操作的。
接下來介紹數據庫中可以鎖定的資源,包括行、頁、表(對象)、數據庫,按序鎖定的資源粒度越來越大。行駐留在頁中,而是包含表或索引數據的物理數據塊。更復雜的可鎖定資源包括范圍、分配單元、堆&B樹等,這兒暫不深究。在SQL SERVER中,如果要獲得某個資源類型的鎖,首先要獲得起對應更高粒度級別上的意向鎖,例如獲得一個行上排他鎖,那么該事務需要獲取行所在頁的意向排它鎖和一個擁有該頁對象的意向排它鎖,意向鎖的目的在於便於在更高粒度級別有效檢測不相容的鎖請求,用一個簡單的表格來描述鎖模式情況下鎖的兼容性。
請求的鎖模式 |
授予了排它鎖(X) |
授予了共享鎖(S) |
授予了意向排它鎖(IX) |
授予了意向共享鎖(IS) |
能否授予請求排它鎖 | No | No | No | No |
能否授予請求共享鎖 | No | Yes | No | Yes |
能否授予意向排它鎖 | No | No | Yes | Yes |
能否授予意向排它鎖 | No | Yes | Yes | Yes |
看到這兒不禁要問,為什么不都使用最小粒度的鎖,這樣的並發性不是更好么?實際上鎖是需要消耗資源的,因此需要在時間和空間上折衷。在默認情況下,系統首先獲取細粒度的鎖,並在某些情況下,觸發鎖升級,例如一條語句中獲取5000個行鎖,那么將升級為頁鎖。此外,T-SQL支持ALTER TABLE語句設置LOCK_ESCALATION控制鎖升級行為,包括是否支持鎖升級和發生升級時的粒度(如頁升級為表或分區)。
一般情況下,阻塞的出現是正常的,比如一個讀操作等待排他鎖的釋放,但有時阻塞時間過長,嚴重影響響應時需要排除阻塞。可以通過如下幾種查詢動態視圖的方式來查看系統中當前的阻塞並排除相關阻塞。
操作類型 |
解釋與示例 |
查詢會話相關鎖信息 |
SELECT request_session_id AS spid, resource_type AS restype, resource_database_id AS dbid, DB_NAME(resource_database_id) AS dbname, resource_description AS res, resource_associated_entity_id AS resid, request_mode AS mode, request_status AS status FROM sys.dm_tran_locks 其中spid表示進程ID,restype表示鎖定的資源類型(KEY, PAGE, Database, object),mode表示鎖模式,status表示是否授予了鎖 |
查詢連接相關信息 |
SELECT session_id AS spid, connect_time, last_read, last_write, most_recent_sql_handle FROM sys.dm_exec_connections WHERE session_id IN (60, 61) 其中connect_time表示連接時間,write&read_time表示讀寫時間,most_recent_sql_handle表示該連接中最近的批處理語句的句柄。接下來可以通過一個簡單的APPLY表運算符獲取相應的SQL語句 SELECT session_id AS spid, text FROM sys.dm_exec_connections CROSS APPLY sys.dm_exec_sql_text(most_recent_sql_handle) AS st WHERE session_id IN (60, 61) |
查詢會話相關信息 |
SELECT session_id as spid, login_time, host_name, program_name ,login_name, nt_user_name, last_request_start_time, last_request_end_time FROM sys.dm_exec_sessions WHERE session_id in (60, 61) 其中包括會話的登陸時間、主機名、程序名、登錄名、WindowsNT用戶名,最后的請求開始和結束時間等信息 |
查詢請求相關信息 |
SELECT session_id AS spid, blocking_session_id, command, sql_handle, database_id, wait_type, wait_time, wait_resource FROM sys.dm_exec_requests WHERE blocking_session_id > 0 其中包括阻塞該會話的某個會話ID、阻塞的毫秒數等,可以通過blocking_session_id > 0判斷是否為阻塞會話 |
處理阻塞 |
可以通過kill <spid>方式關閉會話,此外還可以設置會話中鎖的時間,包括0立即超時,-1無超時(默認值),和n>0超時毫秒數 |
-
隔離級別
數據庫的隔離級別決定了並發用戶讀取和寫入的行為,一般來說隔離級別越高,數據的一致性越好,並發性越弱,接下來首先鎖機制下的隔離級別。
READ UNCOMMITED: 最低的隔離級別,讀取時不需要請求共享鎖,會出現臟讀,在對數據一致性要求不高的情況下使用,在實際中通過WITH NOLOCK方式使用。
READ COMMITED: 系統默認的隔離級別,支持讀取已提交的數據,通過要求讀取者獲取共享鎖來防止未提交的讀取,但由於其會在讀取完成后釋放鎖,因而會存在在兩次讀取之間數據不一致的問題(也稱之為不可重復讀)。
REPEATABLE READ: 可重復讀通過在事務中始終持有讀共享鎖的方式防止兩次不同的讀取。同時由於在該隔離級別下,共享鎖會一直持有,因而無法獲取排它鎖,也防止了丟失更新的情況,比如在低級別的隔離級別下,兩個事務中均修改某個值,那么后面一個修改會奏效。
SERIALIZABLE: 最高的隔離級別,其除了在讀請求時一直持有讀共享鎖,同時還會限定查詢篩選所限的key鍵的范圍(之間提及的鎖范圍),用於阻止其他事務嘗試添加新行(被限定情況下),防止了出現幻讀的情況。
接下來,介紹行版本模式(該模式通過tempdb存儲已提交行的之前版本,之后的深入剖析文章中還會重點介紹tempdb)下的隔離級別,,包括SNAPSHOT和READ COMMITTED SNAPSHOT,分別對應鎖模式下的SERIALIZABLE和READ COMMITTED,區別是行版本模式下不會發出讀共享鎖,所以請求的數據以排他方式鎖定時不會等待,讀取的性能會獲得改善,在修改數據的操作DELETE和UPDATE中需要復制行的版本,因而會相對降低寫的性能。
SNAPSHOT: 讀取數據時會確保獲得事務啟動時最近提交的可用行版本,這兒需要強調事務啟動時的概念,比如兩個事務A、B先后開啟,B事務中修改數據並提交,這個數據修改是不會反應到事務A的,因為事務A獲取額是在其開啟前的行版本。值得一提的是,該級別可以防止更新沖突且不會造成死鎖,比如同時在事務A和B中修改數據,系統會拋出異常,快照隔離事務由於更新沖突而終止。可以通過語句SET TRANSACTION ISOLATION LEVEL SNAPSHOT設置事務的隔離級別為SNAPSHOT。
READ COMMITTED SNAPSHOT: 它與SNAPSHOT的區別是,獲取的"語句"啟動時可用的最后提交的行版本,也就是在查詢發起時最后提交的可用行版本,最后通過一個表格綜述之前介紹的6種不同的隔離級別。
隔離級別 |
臟讀 |
不可重復讀 |
丟失更新 |
幻讀 |
檢測更新沖突 |
使用行版本控制 |
READ UNCOMMITTED |
Yes |
Yes |
Yes |
Yes |
No |
No |
READ COMMITTED |
No |
Yes |
Yes |
Yes |
No |
No |
READ COMMITTED SNAPSHOT |
No |
Yes |
Yes |
Yes |
No |
Yes |
REPEATABLE READ |
No |
No |
No |
Yes |
No |
No |
SERIALIZABLE |
No |
No |
No |
No |
No |
No |
SNAPSHOT |
No |
No |
No |
No |
Yes |
Yes |
這部分的最后補充一下數據庫中死鎖的概念,其和操作系統中學到的死鎖改變一樣,也是兩個或多個進程相互阻塞的情況。在SQL SERVER中一旦出現死鎖,系統會通過DEADLOCK_PRIORITY的死鎖優先級來決定先終止哪一個進程,由於終止進程涉及事務的回滾等操作,會消耗一定的性能,通過更好的設計來避免死鎖是更好的選擇。
補充知識:鎖在常見開發中的應用
比如在很多Job處理中,需要對數據進行耗時很長的操作,包括很多的讀和寫等一系列操作,並需要在一個事務中,這是就很可能造成臟讀或記錄被鎖等待的現象,這是就需要合理的使用SQL SERVER的鎖機制了。實踐中,可以對准備操作的數據添加X互斥鎖,SELECT XX FROM XX (UPDLOCK)WHERE ID = XX,然后在允許臟讀的情況下使用SELECT XX FROM XX (NOLOCK),而不允許的情況下使用SELECT XX FROM XX (READPAST),其他的鎖信息如下表所示。
鎖 |
詮釋 |
NOLOCK(不加鎖) |
此選項被選中時,SQL Server 在讀取或修改數據時不加任何鎖。 在這種情況下,用戶有可能讀取到未完成事務(Uncommited Transaction)或回滾(Roll Back)中的數據, 即所謂的"臟數據",等於 READ UNCOMMITTED事務隔離級別 |
HOLDLOCK(保持鎖) |
此選項被選中時,SQL Server 會將此共享鎖保持至整個事務結束,而不會在途中釋放,等於SERIALIZABLE事務隔離級別 |
UPDLOCK(修改鎖) |
此選項被選中時,SQL Server 在讀取數據時使用修改鎖來代替共享鎖,並將此鎖保持至整個事務或命令結束。使用此選項能夠保證多個進程能同時讀取數據但只有該進程能修改數據。 |
TABLOCK(表鎖) |
此選項被選中時,SQL Server 將在整個表上置共享鎖直至該命令結束。 這個選項保證其他進程只能讀取而不能修改數據。 |
PAGLOCK(頁鎖) |
當被選中時,SQL Server 使用共享頁鎖。 |
TABLOCKX(排它表鎖) |
強制使用獨占表級鎖,這個鎖在事務期間阻止任何其他事務使用這個表 |
READPAST |
讓sql server跳過任何鎖定行,執行事務,適用於READ UNCOMMITTED事務隔離級別只跳過RID鎖,不跳過頁,區域和表鎖 |
ROWLOCK |
強制使用行鎖 |
Tip: @@version, DBCC USEROPTIONS , SP_WHO, kill 1003
可編程對象比較多,包括變量、批、流元素、游標和臨時表、用戶定義函數、存儲過程、觸發器、動態SQL等概念,部分內容使用的場景較少,通過表格簡述之,但對將對臨時表這一常見並較難理解的概念進行細致介紹。
對象 |
解釋與示例 |
變量 |
DECLARE @i AS INT; SET @i = 10; |
批 |
表示一個單元分析和執行的命令組,變量存在於批的生命周期中,並且一個批中只能包含一個DDL語句。 USE TSQL2012; GO |
語句塊和流元素 |
相對於PL/SQL,T-SQL中語法相對簡單,結構完整性要求沒有那么高 語句塊: BEGIN END 邏輯流:IF BEGIN XXX END ELSE IF BEGIN XXX END ELSE BEGIN XXX END 循環流: WHILE @i < 10 BEGIN XXX END,支持BREAK和CONTINUE |
游標 |
游標使用的步驟:1.基於查詢聲明游標;2.打開游標;3.從游標記錄中提取屬性值給變量;4.遍歷游標記錄並迭代;5.關閉游標;6.釋放游標 DECLARE C CURSOR FAST_FORWARD FOR SELECT custid, ordermonth, qty, FROM Sales.CustOrders ORDER BY custid, ordermonth OPEN C FETCH NEXT FROM C INTO @custid, @ordermonth, @qty SELECT @precustid = @custid, @runqty = 0; WHILE @@FETCH_STATUS = 0 BEGIN IF @custid <> @precustid SELECT @precustid = @custid, @runqty = 0 SET @runqty = @runqty + @qty INSERT INTO @Result VALUES(@custid, @ordermonth, @qty, @runqty) FETCH NEXT FROM C INTO @custid, @ordermonth, @qty END CLOSE C |
用戶函數、存儲過程 |
前者之前介紹內嵌表值函數時以有例子,這兒只介紹StoreProcedure,場景為獲取某客戶指定日期內訂單並返回記錄數 CREATE PROCEDURE Sales.GetCustomerOrders @custid AS INT, @fromdate AS DATETIME ='19010101', @todate AS DATETIME ='99991231', @numrows AS INT OUTPUT AS SET NOCOUNT NO SELECT orderid, custid, empid, orderdate FROM Sales.Orders WHERE custid = @custid AND orderdate > @fromdate AND orderdate < @todate SET @numrows = @@rowcount GO |
觸發器 |
CREATE TRIGGER trg_T1 ON dbo.T1 AFTER INSERT AS INSERT INTO dbo.T1_Audit(keycol, datacol) SELECT keycol, datacol FROM inserted GO |
動態SQL |
DECLARE @sql AS VARCHAR(100) SET @sql = 'PRINT ''XIONGER''' EXEC(@sql),此外為了防止SQL注入,還可以使用sp_executesql來達到參數化存儲過程數據參數的目的。 |
錯誤處理 |
BEGIN TRY END TRY BEGIN CATCH IF XXX ELSE THROW END CATCH |
-
臨時表
T-SQL支持3中類型的臨時表,分別是本地臨時表、全局臨時表和表變量。本地臨時表僅對創建它的會話可見,全局臨時表對所有會話可見,表變量僅對當前會話的當前批有效,粒度更小,在T-SQL它也是實際的表(易誤解為只存在內存)。臨時表對於大量數據時性能更好,而表變量是處理少量數據最好選擇,構建方式如下所示。
對象 |
解釋與示例 |
本地臨時表 |
IF OBJECT_ID('tempdbo.dbo.#MYTemp') IS NOT NULL CREATE TABLE #MYTemp(orderyear INT NOT NULL PRIMARY KEY) |
全局臨時表 |
將本地臨時表中的#換成##即可 |
表變量 |
DECLARE @MyOrder TABLE(orderyear INT NOT NULL PRIMARY KEY) |
補充部分常見SQL操作
元數據查詢類型 | 解釋與示例 |
創建架構 | SET SCHEMA HR AUTHPRIZATION dbo |
常見連接字符串 | Data Source=myServerAddress;Initial Catalog=myDataBase;Integrated Security=SSPI; |
最后附上英文原版參考書目(Microsoft SQL Server 2012 T-SQL Fundamentals)下載地址:http://pan.baidu.com/s/1eRbhnbk
非常感謝大家的閱讀,系列文章鏈接如下,有T-SQL方面的任何疑問請隨時和在下聯系。
那些年我們寫過的T-SQL(上篇):上篇介紹查詢的基礎,包括基本查詢的邏輯順序、聯接和子查詢
那些年我們寫過的T-SQL(中篇):中篇介紹表表達式、集合運算符和開窗函數
那些年我們寫過的T-SQL(下篇):下篇介紹數據修改、事務&並發和可編程對象
附錄:
導出insert腳本:insertScript.7z
參考資料:
-
(美)本咁. SQL Server 2012 T-SQL基礎教程[M]. 北京:人民郵電出版社, 2013.