827
技術社區[雲棲]
MSSQL · 應用案例 · 日誌表設計優化與實現
摘要
這篇文章從日誌表問題引入、日誌表的共有特性、日誌表的設計需求、設計思路以及設計詳細實現的角度,闡述了在SQL Server數據庫中如何最優化設計日誌表來降低係統資源的占用和提高係統吞吐量。
問題引入
在平時與客戶服務與交流過程中,我們不止一次的被客人問及這樣的場景:我們現在麵臨如何設計SQL Server日誌表方案,如何最優化設計數據庫日誌記錄表。因為,日誌表設計會麵對如下問題:
表記錄數大:日誌表由於記錄了應用程序的很多操作日誌,有的業務有很多步驟,甚至每個步驟操作都會被記錄到日誌表中,所以通常日誌記錄表都很大,表記錄數據很多,表空間占用很大。
事務操作頻繁:由於日誌記錄表寫入(INSERT)操作非常頻繁,加之表變得很大,通常的做法是會刪除過期的日誌信息(DELETE),所以事務操作非常頻繁。
占用了昂貴的IOPS資源:日誌表原本寫入操作就非常頻繁了,加之需要刪除過時數據操作,這一寫一刪操作,使得事務量加倍,導致了數據庫係統IOPS資源的過度被日誌表的操作所占用了。
吞吐量上不去:日誌表事務操作頻繁,如果刪除操作的事務沒有控製好(比如:我見過很多客人一個DELETE語句下去刪掉幾十、幾百萬記錄數的操作),很有可能會導致鎖等待的發生(Blocking),從而影響應用的吞吐量和並發能能力。
日誌表設計麵臨的這一係列問題給我們設計帶來了不小的挑戰,而我們今天這篇文章就是要解決日誌表設計麵臨的這些問題。這篇文章將會分享SQL Server日誌表設計的優化方案以及方案的實現細節,聰明的你甚至可以將這個設計方案推廣到其他的關係型數據庫。
日誌表特性
首先,在分享設計方案之前,我們來看看關係型數據中的日誌表,具有哪些共同的特性:
事務特性
日誌表最為明顯的特性是事務特性,或者稱之為最重要的特性也不為過,即:寫多讀少,再準確一點說,是INSERT或者DELETE操作多;SELECT或者UPDATE操作少,這個很好理解。
INSERT操作:INSERT操作是記錄日誌信息,當然是非常頻繁且是少不了的,幾乎是每時每刻都在發生。對係統吞吐量和並發要求高。
UPDATE操作:日誌表中記錄一旦寫入,幾乎不會被修改,所以UPDATE操作應該非常少,更或者沒有。
SELECT操作:SELECT操作也不會太頻繁,隻會在排查問題,查詢日誌的時候使用,所以,概率也比較小。
DELETE操作:而為了保證表記錄數盡可能的少和查詢操作(SELECT)的高效性,我們往往會定時清理過時的日誌記錄,所以DELETE操作相對還是很頻繁的,和INSERT操作的頻繁度是對等的。當然這裏是可以通過設計來優化的,這也恰恰是這篇文章的中心思想。
設計特性
由於日誌表的事務特性,所以,我們對日誌表結構的設計就十分考究,比如:
主鍵設計:日誌表因為幾乎不會被UPDATE,數據都是追加寫入,因此,主鍵最好選擇是INT或者BIGINT數據類型的自增列(IDENTITY屬性列),且做為CLUSTERED升序排列索引,即表按照主鍵列物理升序排列。這樣設計的好處是,日誌記錄追加寫入數據時,表不會被部分(或全部)重新排序,且幾乎不會產生碎片。舉一個反例,如果表主鍵列為UNIQUEIDENTIFIER數據類型,且值通常為默認值NEWID,那麼當新追加的記錄主鍵值小於之前的記錄主鍵值時,會導致這一頁的數據重排序,而且容易產生索引碎片。
索引設計適可而止:基於日誌表事務特性的分析,我們很少在日誌表上進行SELECT查詢操作,所以,索引不宜過多,適可而止。理由是:索引過多,影響日誌表INSERT操作的性能,因為,係統需要額外維護索引結構中數據和基表數據的一致性。按照我們的經驗,通常,我們隻需要在時間字段上建立索引即可,然後通過時間來過濾查詢結果集。
索引填充因子設計:由於日誌表數據記錄按時間升序追加寫入特性加之很少UPDATE操作,因此,索引數據頁也具備了按時間升序追加寫入的相似特性。所以,我們可以把索引的填充因子調高,可以設置為90 - 95都沒有問題,換句話說,我們可以讓索引數據頁僅留下5% ~ 10%的剩餘空間。
索引碎片維護設計:可能沒有人會關心發生在兩個月前的日誌記錄,因此,日誌表記錄會隨著時間推移而刪除過時的數據(DELETE)。這樣也會導致索引碎片隨著時間的推移變得越來越高,進而影響查詢效率,消耗更多的係統IOPS資源。所以,我們需要定期維護(或重建或者重組)索引。當然,我們可以通過設計優化來避免DELETE操作,從而避免索引碎片的產生,也因此可以避免索引維護的成本。
重要性分析
日誌表數據的最大功用是供我們排查應用係統問題時使用的,因此重要性相對於核心業務係統中的業務表數據,沒有那麼重要,所以我們會定期清理日誌表過時數據。但是,它的確實實在在占用了我們昂貴的數據庫係統資源。包括:存儲開銷、係統I/O開銷、CPU開銷、網絡開銷、連接開銷以及係統吞吐量開銷等。因此,針對日誌表設計優化也顯得尤為重要,這也是這篇文章分享的意義所在。
需求分析
基於以上“問題引入”和“日誌表特性”章節的描述與分析,我們大致可以得出日誌表最優化設計的需求與目標:
減少係統IOPS消耗:寫入操作(INSERT)無法避免和減少了,我們可以考慮減少或者消除刪除操作(DELETE)對IOPS的消耗。
增大日誌表寫入的吞吐量:要達到增大日誌表寫入吞吐量的目的,我們可以采用分區表的方式來解決,或者是分庫分表。
減少索引碎片維護對係統性能消耗:索引維護的目的是為了減少索引的碎片率,提高I/O利用率,使得執行計劃更加準確,查詢高效。但是,這個操作也會帶來數據庫係統性能的大量開銷,有時候可能會導致鎖等待,甚至可能嚴重到死鎖的地步。
設計思路
在講解設計思路之前,我們需要先來看看SQL Server中刪除表中所有數據的一個叫著TRUNCATE的語句。
TRUNCATE
TRUNCATE是SQL Server中一種刪除表中所有數據的一種執行非常快速的方法,它僅僅隻記錄非常少的日誌信息,哪怕表記錄數達到千萬級,億級別,正常情況下同樣可以秒級別執行成功。
使用方法
TRUNCATE使用方法非常簡單,即:TRUNCATE TABLE後麵跟表名字即可。
TRUNCATE TABLE dbo.tb_Table
注意:
TRUNCATE TABLE非常簡單高效的刪除表中所有數據,但是有可能也非常危險,因為,你一旦使用了該方法清理了表中所有的數據,你很難將表中的數據在不使用備份集的情況下把數據找回來。因此,在使用這個操作之前請確保你十分清楚它的風險。
使用場景
在如下的場景中,我們可以考慮使用TRUNCATE操作:
重置表狀態:當你使用TRUNCATE操作以後,表的所有數據會被清理,Identity屬性列值會被重置為初始值。
快速清理表數據:你想要非常高效,快速清理大表數據,而又不希望導致長時間的鎖等待或者死鎖。
清理表數據又不想導致事務日誌文件暴漲:常規的DELETE事務操作,在清理大表或者一次性操作大量數據時,會導致數據庫的日誌文件空間暴漲,而使用TRUNCATE操作因為僅會寫入非常少的日誌信息,因此不會導致事務日誌空間暴漲。
刪除表數據又不想觸發表上的Trigger:刪除表數據,但不希望觸發表上刪除操作的Trigger。
設計實現
基於以上的詳細分析,我們對日誌表的特定,需求有了比較明確的認知,接下來,讓我們看看日誌表的常規的設計和優化方案的設計,以及這些方案的優缺點。
常規設計
以下是關於日誌表的常規設計,在我接觸的客戶場景中,幾乎所有客人都是按照這樣的思路和方法來實現日誌表的,當然有可能表結構的設計還沒有這般考究。
實現方法
首先,建立一個日誌表,這裏在主鍵、聚集索引、主鍵列設計上非常有講究,我們選擇BIGINT數據類型的IDENTITY屬性列做為聚集索引升序排列的主鍵列,以最大限度的符合日誌表的特性。詳情參見下麵代碼中創建表部分的注釋文字。
其次,建立日誌表查詢必須的索引,這裏我們在時間字段上建立索引,以便後麵的數據清理工作高效運行,這裏我們將索引的FILLFACOTR設置為95。即,索引數據頁預留5%的空間,以避免索引數據頁中數據的移動。
接下來,實現清理過時數據的方法,這裏使用一個存儲過程來模擬。這裏需要強調的是,請務必采用循環刪除的方法,且每批次刪除後,需要有一段時間的停頓,以釋放進程獲取到的資源,這裏的資源包含但不僅限於鎖資源、IOPS資源、CPU等,供其他的進程使用。我們在平時服務過程中,有的客人沒有將日誌表的清理過程采用批量刪除的方式,有的是直接使用一個大事務刪除所有過時數據,導致長時間鎖住甚至無法寫入。
日誌表常規設計的實現方法和數據清理過程,參見下麵的代碼,請注意代碼中的注釋,非常重要。
USE master
GO
-- 創建測試數據庫
IF DB_ID('TestDb') IS NULL
CREATE DATABASE [TestDb];
GO
USE [TestDb]
GO
-- 創建測試表
IF OBJECT_ID('dbo.tb_ApplicationLogs', 'U') IS NOT NULL
DROP TABLE dbo.tb_ApplicationLogs
GO
/*
創建表的時候,需要特別注意以下幾點:
1、RowID被設計為IDENTITY屬性,以便日誌表追加寫入
2、RowID做為表CLUSTERED(聚集)類型主鍵,且升序排列
3、Indate表示記錄進入表中的時間,采用GETDATE做為默認值
4、需要在Indate字段上建立索引,以提高查詢和清理數據效率
*/
CREATE TABLE dbo.tb_ApplicationLogs(
RowID BIGINT IDENTITY(1,1) NOT NULL,
AppName SYSNAME NOT NULL,
HostName SYSNAME NOT NULL,
Indate DATETIME NOT NULL
CONSTRAINT DF_Indate DEFAULT(GETDATE()),
LogInfo NVARCHAR(2000) NULL,
CONSTRAINT PK_tb_ApplicationLogs_RowID
PRIMARY KEY CLUSTERED (RowID ASC)
);
--創建時間字段上的索引,以加快數據清理和查詢效率
CREATE NONCLUSTERED INDEX IX_Indate
ON dbo.tb_ApplicationLogs(Indate) WITH(FILLFACTOR = 95)
;
GO
IF OBJECT_ID('dbo.Up_CleanAppLogs', 'P') IS NOT NULL
DROP PROC dbo.Up_CleanAppLogs
GO
/*
Function:
這個存儲過程用來模擬清理dbo.tb_ApplicationLogs日誌表中的數據,
采用循環刪除超過@days_before天的數據。
參數含義:
@days_before:超過這個參數值天數的數據將會被刪除掉。
@batch_Size: 每個批次刪除的記錄條數
注意:
1、在計算時間的地方,需要使用DATEADD(day, -ABS(@days_before), GETDATE())方法,
這裏的取絕對值非常重要,否則當傳入一個負數時,表中的所有數據都會被刪除。
2、為了避免一個大的事務,一定是采用循環刪除的方式,這裏一次性刪除萬條
3、每個刪除萬條數據的批次之間,要有一段時間的停頓,釋放資源供其他進程使用。
*/
CREATE PROC dbo.Up_CleanAppLogs(
@days_before INT = 7,
@batch_Size INT = 10000
)
AS
BEGIN
SET NOCOUNT ON
DECLARE
@dt DATETIME
,@row_affected INT
,@row_removed_total INT
;
SELECT
@dt = DATEADD(day, -ABS(@days_before), GETDATE())
,@row_removed_total = 0
;
WHILE EXISTS(
SELECT TOP 1 1
FROM dbo.tb_ApplicationLogs WITH(NOLOCK)
WHERE Indate <= @dt)
BEGIN
DELETE TOP(@batch_Size) A
FROM dbo.tb_ApplicationLogs AS A
WHERE Indate <= @dt --請注意,這個條件非常重要,否則有可能會導致當前數據被誤刪除
SELECT
@row_affected = @@ROWCOUNT
,@row_removed_total = @row_removed_total + @row_affected
;
RAISERROR('--%d rows removed, totaly %d rows cleared and waiting for next batch', 10, 1, @row_affected, @row_removed_total) WITH NOWAIT
WAITFOR DELAY '00:00:02'
END
END
GO
缺點分析
以上的日誌表常規設計方法,對於日誌量不大(每分鍾在1000條記錄以內)的場景當然可以應付自如。當隨著日誌的不斷增長,可能會帶來如下問題:
單表吞吐量上不去:單日誌表,日誌寫入吞吐量始終是有極限的,當每分鍾需要寫入的日質量大於單表吞吐量時,瓶頸就出現了。
DELETE操作消耗I/O資源:大量的日誌操作過期時,會導致DELETE操作的頻繁進行,會導致I/O資源的消耗,浪費數據庫係統昂貴的I/O資源。
DELETE操作無法按預期刪除所有過時數據:因為我們每個刪除批次之間有短暫停頓,當日誌寫入量非常巨大,甚至超過刪除的速度時,會導致日誌寫入量大於刪除量,數據清理工作無法按預期清理完畢,進而導致日誌表數據不斷累加,數據量越來越大。
DELETE操作導致索引碎片:如此頻繁的DELETE數據清理操作,會導致時間字段(Indate)上的索引碎片產生,影響查詢和數據清理效率。
索引維護成本:為了解決時間字段Indate上的索引碎片產生帶來的問題,我們必須對此索引進行維護重建或者重整工作,進一步導致I/O的消耗,嚴重者甚至導致鎖等待或者死鎖,進一步影響日誌係統吞吐量。
為了解決日誌表常規設計中的種種問題和缺點,我們利用TRUNCATE操作來優化設計表的設計和數據清理方案。
優化設計
我們通過“常規設計”中“缺點分析”部分,了解到了常規設計的眾多缺點,優化設計的原則就是要來解決這些問題。假設我們的日誌表數據記錄按天為單位歸檔,建立分區表和分區視圖,那麼優化後的具體的設計思路是:
建立分區表
使用循環語句與動態SQL相配合的方式來一次性建立31張表,表的尾號預示寫入數據的日期號數。分區表的設計需要:
分區字段為LogDay,表示Log寫入的表編號,也即是寫入的日期號數據。
分區字段上建立的Check約束是用來實現分區作用。
分區字段必須是主鍵的一部分,所以主鍵建立在RowId和LogDay的聯合
請在Indate字段上建立索引,加快分區視圖、分區表的查詢效率,FILLFACTOR應當選擇90以上,此處選擇為95。
建立分區視圖
為了操作日誌表簡單方便,實現一個分區視圖用來直接操作數據,否則,我們的日誌寫入應用必須要先確定當前日誌寫入哪一張表,然後才能寫入到對應的表中。使用分區視圖的話,我們就可以一股腦兒的直接寫入到分區視圖中,不用事先區分寫入的表。分區視圖的實現方法也非常簡單,隻需要將所有的分區表UNION ALL起來,構成成一個分區視圖即可。
注意:
分區視圖中的分區表RowID不能為Identity屬性列。
清理過時數據
使用TRUNCATE TABLE方法清理過時數據,僅會寫非常少的事務日誌信息,事務日誌不會記錄數據回滾所需要的所有信息,並且在很少概率情況下會發生鎖等待,所以,該操作非常簡單、快捷、高效而又節約係統資源。即使表數據記錄數達到億級甚至十億百億級,這個操作也會在秒級別完成。
實現代碼
基於以上的實現方法分析,具體的實現代碼如下,請注意參考代碼中的注釋。
USE master
GO
-- 創建測試數據庫
IF DB_ID('TestDb') IS NULL
CREATE DATABASE [TestDb];
GO
USE [TestDb]
GO
-- 刪除表之前,需要先刪除分區視圖
IF OBJECT_ID('dbo.V_ApplicationAllLogs', 'V') IS NOT NULL
DROP VIEW dbo.V_ApplicationAllLogs
GO
/**
-- 循環創建張表,每張表以數字尾號結尾,比如:XXX_01,XXX_02,...,XXX_31
-- 這裏需要注意,為了方便數據寫入直接使用分區視圖,表有如下限製:
1. 分區字段為logDay,表示Log寫入的表編號
2. 分區字段上建立Check約束來實現分區
3. 分區字段必須是主鍵的一部分,所以主鍵建立在RowId和LogDay的聯合
4. 為了使用分區視圖直接操作數據,RowID不能為Identity屬性列
請在Indate字段上建立索引,FILLFACTOR選擇以上。
*/
DECLARE
@drop_sql NVARCHAR(MAX)
,@create_sql NVARCHAR(MAX)
,@do INT = 1
,@loop INT = 31
,@postfix CHAR(2)
;
WHILE @do <= @loop
BEGIN
SELECT
@postfix = RIGHT(CAST(@do+100 AS VARCHAR(3)), 2)
,@drop_sql = N'
IF OBJECT_ID(''dbo.tb_ApplicationLogs_' + @postfix + N''', ''U'') IS NOT NULL
DROP TABLE dbo.tb_ApplicationLogs_' + @postfix
,@create_sql = N'
CREATE TABLE dbo.tb_ApplicationLogs_' + @postfix +N'(
RowID BIGINT NOT NULL,
AppName SYSNAME NOT NULL,
HostName SYSNAME NOT NULL,
LogDay INT,
CONSTRAINT CHK_tb_ApplicationLogs_' + @postfix +N'_LogDay CHECK(LogDay= ' + CAST(@do AS NVARCHAR(2)) + N'),
Indate DATETIME NOT NULL,
LogInfo NVARCHAR(2000) NULL,
CONSTRAINT PK_tb_ApplicationLogs_' + @postfix + N'_RowID
PRIMARY KEY CLUSTERED (RowID ASC,LogDay ASC)
);
CREATE NONCLUSTERED INDEX IX_Indate
ON dbo.tb_ApplicationLogs_' + @postfix + N'(Indate) WITH(FILLFACTOR = 95)
;
'
EXEC sys.sp_executesql @drop_sql
--print @create_sql
EXEC sys.sp_executesql @create_sql
SET @do = @do + 1;
END
GO
USE [TestDb]
GO
IF OBJECT_ID('dbo.V_ApplicationAllLogs', 'V') IS NOT NULL
DROP VIEW dbo.V_ApplicationAllLogs
GO
/*
-- 將所有的分區表UNION ALL起來,構成分區視圖
供我們查詢、數據寫入和數據更新
*/
CREATE VIEW dbo.V_ApplicationAllLogs
AS
SELECT * FROM dbo.tb_ApplicationLogs_01 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_02 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_03 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_04 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_05 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_06 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_07 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_08 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_09 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_10 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_11 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_12 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_13 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_14 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_15 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_16 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_17 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_18 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_19 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_20 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_21 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_22 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_23 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_24 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_25 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_26 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_27 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_28 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_29 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_30 WITH(NOLOCK)
UNION ALL
SELECT * FROM dbo.tb_ApplicationLogs_31 WITH(NOLOCK)
GO
USE [TestDb]
GO
-- 這裏模擬應用程序寫入日誌信息
;WITH a
AS (
SELECT *
FROM (VALUES(1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) AS a(a)
), RoundData
AS(
SELECT TOP(100)
RowId = ROW_NUMBER() OVER (ORDER BY a.a)
,AppName = NEWID()
,HostName = NEWID()
,Indate = GETDATE() - 8
,LogInfo = NEWID()
FROM a, a AS b, a AS c, a AS d, a AS e, a AS f, a AS g, a AS h
)
INSERT INTO dbo.V_ApplicationAllLogs(RowId, AppName, LogDay, HostName,Indate, LogInfo)
SELECT
RowId
,AppName
,LogDay = RowId % 31 + 1
,HostName
,Indate = CAST('2017-08-1 22:54:39.873' AS DATETIME)+ RowId % 31
,LogInfo
FROM RoundData;
GO
-- 從分區視圖中查詢日誌信息
SELECT * FROM dbo.V_ApplicationAllLogs WITH(NOLOCK)
WHERE Indate >='2017-08-1' AND Indate <= '2017-08-3'
IF OBJECT_ID('dbo.Up_CleanAppLogs', 'P') IS NOT NULL
DROP PROC dbo.Up_CleanAppLogs
GO
USE [TestDb]
GO
/*
Function:
這個存儲過程用來模擬清理dbo.tb_ApplicationLogs日誌表中的數據,
采用TRUNATE TABLE的方式清理超過@days_before天的數據,非常迅速。
參數含義:
@days_before:超過這個參數值天數的數據將會被直接TRUNCATE刪除掉。
注意:
1、在計算時間的地方,需要使用DATEADD(day, -ABS(@days_before), GETDATE())方法,
這裏的取絕對值非常重要,否則當傳入一個負數時,表中的所有數據都會被刪除。
2、這個存儲過程隻需要每天執行一次就好了
*/
CREATE PROC dbo.Up_CleanAppLogs(
@days_before INT = 7
)
AS
BEGIN
SET NOCOUNT ON
DECLARE
@log_postfix INT
,@sql NVARCHAR(MAX)
;
SELECT
@log_postfix = DATEPART(dd, DATEADD(day, -ABS(@days_before), GETDATE()))
,@sql = N'TRUNCATE TABLE dbo.tb_ApplicationLogs_' + RIGHT(CAST(@log_postfix+100 AS VARCHAR(3)), 2)
;
EXEC sys.sp_executesql @sql
RAISERROR('Executing sql: %s', 10, 1, @sql) WITH NOWAIT
END;
GO
最後總結
這篇文章從日誌表特性、日誌表設計思路、和兩種日誌表設計方案等方麵分享了日誌表的優化設計方案與具體實現。最優化的設計方案,減少了係統資源的占用的同時,還進一步提高了係統的吞吐量,非常有價值。
最後更新:2017-09-21 09:03:50