一個進銷存系統性能優化總結
系統的資料量分析及存在的關鍵效能瓶頸
系統設計應支援每年2億部上以終端的銷售量業務處理,一般的終端裝置從生產到消費整個生命週期最長為1年,超過一年的資料只需備份儲存即可,不需要在系統中處理,即必須完整的儲存1年發貨、排程、零售等資料。初略的計算,最細粒度的表將超過2億條記錄。大多業務表(發貨、排程、零售)的資料量也在1000萬以上。由於系統中資料都有著明顯的維度層次關係,如地區層次為國家、地區部、辦事處,運營商層次為省公司、市分公司,產品層次為頻段、制式、型號等。所有業務表中都有這些層次關係的資料欄位。而處理多個維度時,每個維度下又分為多層次的樹型結構資料,由於涉及到遞迴,效能一般很差,所以系統中普遍存在著對樹型層次結構資料的處理優化問題。通過以上分析不難發現,系統關鍵效能瓶頸在於大資料表的操作和對樹型層次結構資料的處理。
1.3優化方向
經過對系統的瞭解和簡單分析後,系統中普遍存在大資料量的表和層次結構的欄位。對於大資料量表進行優化時除了必要的索引和從業務上劃分為小的子表外,一個最重要的優化技術就是表分割槽了,依據業務特點進行適當分割槽後,它可有效的分離業務資料,達到高效能的更新和查詢操作。系統中還有很多樹型的資料需要處理,由於樹型結構的儲存一般非常簡單,核心的問題在於對這個層次結構資料高效的查詢上,如查詢某個節點的所有子代節點。往往涉及到遞迴等查詢操作,效能較差,它也是系統的瓶頸之一,必須提供一種高效查詢的方案。接下來本文將詳細的分析這兩方面優化技巧。
2. 表分割槽優化
2.1 表分割槽概述
對大表的分割可從兩個方面來入手,第一依據不同的業務場景特點來物理分割,因為不同的業務處理所關心的資料範圍不同,按這個規則可將表分成多個子表,由於這部分處理較為簡單,在此不作詳細分析。第二就是資料表分割槽,對單個子表,如果資料仍然很大(超過百萬)可進一步做分割槽處理。即按查詢最頻繁、效能提升明顯的欄位作為分割槽依據。這樣不但可充分發揮CPU、硬碟並行處理能力而且可減少資料查詢範圍,即配合前臺合適的查詢條件可以不用掃描整個表,而只需要從一個或數個分割槽中並行讀取資料,以提高查詢效率。
2.2表分割槽具體實施
下面以排程業務(分銷商之間調入調出業務)表為例來進行分割槽憂化,而對其分割槽的規則是建立時間,這是因為各欄位中與業務關係最緊密的就是時間,往往使用者最關心的是近三個月的資料,所以資料以月份為區間進行劃分。為了讓分割槽更智慧,日後能自動維護分割槽,即交替、重複地使用固定的幾個檔案組(編號01~12),使資料能均勻的分佈在各檔案組上。
2.2.1建立模擬資料組及檔案組
createdatabase GPMS;
go
useGPMS;
ALTERDATABASE GPMS ADD FILEGROUP FG1;
ALTERDATABASE GPMS ADD FILEGROUP FG2;
ALTERDATABASE GPMS ADD FILEGROUP FG3;
ALTERDATABASE GPMS ADD FILEGROUP FG4;
ALTERDATABASE GPMS ADD FILEGROUP FG5;
ALTERDATABASE GPMS ADD FILEGROUP FG6;
ALTERDATABASE GPMS ADD FILEGROUP FG7;
ALTERDATABASE GPMS ADD FILEGROUP FG8;
ALTERDATABASE GPMS ADD FILEGROUP FG9;
ALTERDATABASE GPMS ADD FILEGROUP FG10;
ALTERDATABASE GPMS ADD FILEGROUP FG11;
ALTERDATABASE GPMS ADD FILEGROUP FG12;
ALTERDATABASE GPMS ADD FILE (NAME = 'FG1', FILENAME = 'c:\DataBase\GPMSF1.NDF') TOFILEGROUP FG1;
ALTERDATABASE GPMS ADD FILE (NAME = 'FG2', FILENAME = 'd:\DataBase\GPMSF2.NDF') TOFILEGROUP FG2;
ALTERDATABASE GPMS ADD FILE (NAME = 'FG3', FILENAME = 'e:\DataBase\GPMSF3.NDF') TOFILEGROUP FG3;
ALTERDATABASE GPMS ADD FILE (NAME = 'FG4', FILENAME = 'F:\DataBase\GPMSF4.NDF') TOFILEGROUP FG4;
ALTERDATABASE GPMS ADD FILE (NAME = 'FG5', FILENAME = 'c:\DataBase\GPMSF5.NDF') TOFILEGROUP FG5;
ALTERDATABASE GPMS ADD FILE (NAME = 'FG6', FILENAME = 'd:\DataBase\GPMSF6.NDF') TOFILEGROUP FG6;
ALTERDATABASE GPMS ADD FILE (NAME = 'FG7', FILENAME = 'e:\DataBase\GPMSF7.NDF') TOFILEGROUP FG7;
ALTERDATABASE GPMS ADD FILE (NAME = 'FG8', FILENAME = 'F:\DataBase\GPMSF8.NDF') TOFILEGROUP FG8;
ALTERDATABASE GPMS ADD FILE (NAME = 'FG9', FILENAME = 'c:\DataBase\GPMSF9.NDF') TOFILEGROUP FG9;
ALTERDATABASE GPMS ADD FILE (NAME = 'FG10', FILENAME = 'd:\DataBase\GPMSF10.NDF') TOFILEGROUP FG10;
ALTERDATABASE GPMS ADD FILE (NAME = 'FG11', FILENAME = 'e:\DataBase\GPMSF11.NDF') TOFILEGROUP FG11;
ALTERDATABASE GPMS ADD FILE (NAME = 'FG12', FILENAME = 'F:\DataBase\GPMSF12.NDF') TOFILEGROUP FG12;
上述程式碼建立了一個數據庫GPMS(Global Promotion Manager System)。使用表分割槽前必須為資料庫分配檔案組,這樣系統才能把分割槽中的資料分離到不同碟符下的檔案中,預設情況下所有資料都會存放在PRIMARY檔案組中。因為系統中大多資料的有效期為一年,所以為資料庫建立了除主檔案組以外的十二個檔案組,這十二個檔案組用於存放每一個月份的資料,為了能同時並行的讀取檔案組,把這些檔案組均勻的分佈在各個碟符中(當然這還要依賴於具體伺服器硬體配置)。完成後開啟資料庫屬性的檔案組如下圖所示:
2.2.1建立分割槽函式和分割槽方案
完成檔案組的建立後,就可以為系統建立分割槽方案和分割槽函數了,一般一個分割槽函式服務於一個分割槽方案。由於前面分析過了,系統中最重要的分割槽規則為時間,很多業務資料查詢最頻繁的條件也是時間,所以在此處以時間為例來建立分割槽函式和分割槽方案,這樣在後面的業務表建立時可共享該分割槽方案。
IF EXISTS (SELECT * FROM sys.partition_schemesWHERE name = N'PSch_Month')
DROPPARTITION SCHEME [PSch_Month]
GO
IF EXISTS (SELECT * FROM sys.partition_functionsWHERE name = N'PFun_Month')
DROPPARTITION FUNCTION [PFun_Month]
GO
CREATEPARTITION FUNCTION PFun_Month(datetime)
AS
RANGERIGHT FOR VALUES ('2011-2-1','2011-3-1','2011-4-1','2011-5-1','2011-6-1',
'2011-7-1','2011-8-1','2011-9-1','2011-10-1','2011-11-1','2011-12-1');
CREATEPARTITION SCHEME PSch_Month
AS
PARTITIONPFun_Month TO (FG1, FG2,FG3,FG4,FG5,FG6,FG7, FG8,FG9,FG10,FG11,FG12);
2.2.2使用分割槽方案建立業務表
IF EXISTS (SELECT * FROM sys.objects WHEREobject_id = OBJECT_ID(N'[dbo].[Distribute]') AND type in (N'U'))
DROPTABLE [dbo].[Distribute]
GO
CREATETABLE [dbo].[Distribute](
[ID] [int] IDENTITY(1,1) NOT NULL,
[OutAreaID] [int] NOT NULL,
[OutCustomer] [int] NOT NULL,
[ProductID] [int] NOT NULL,
[InAreaID] [int] NOT NULL,
[InCustomer] [int] NOT NULL,
[DistributeNum] [int] NOT NULL,
[Status] [smallint] NOT NULL,
[SendDate] [datetime] NOT NULL,
CONSTRAINT [PK_Distribute] PRIMARY KEYCLUSTERED
(
[ID], [SendDate] ASC
)WITH(PAD_INDEX = OFF,STATISTICS_NORECOMPUTE = OFF,IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS =ON, ALLOW_PAGE_LOCKS = ON)
) ONPSch_Month([SendDate]);
IF EXISTS (SELECT * FROM sys.objects WHEREobject_id = OBJECT_ID(N'[dbo].[Distribute_NoPartition]') AND type in (N'U'))
DROPTABLE [dbo].[Distribute_NoPartition]
GO
CREATETABLE [dbo].[Distribute_NoPartition](
[ID] [int] IDENTITY(1,1) NOT NULL,
[OutAreaID] [int] NOT NULL,
[OutCustomer] [int] NOT NULL,
[ProductID] [int] NOT NULL,
[InAreaID] [int] NOT NULL,
[InCustomer] [int] NOT NULL,
[DistributeNum] [int] NOT NULL,
[Status] [smallint] NOT NULL,
[SendDate] [datetime] NOT NULL,
CONSTRAINT [PK_Distribute_NoPartition]PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH(PAD_INDEX = OFF,STATISTICS_NORECOMPUTE = OFF,IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS =ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON[PRIMARY]
這裡分別建立了兩個結構相同,但一個使用了分割槽方案,一個沒有使用分割槽方案,以便比對出它們的效能差異。
2.2.3生成模擬資料
按照業務的需求,排程業務表將至少有1000萬的資料量,下面我們將模擬的插入這些業務資料,分別插入到分割槽表和未分割槽表中,程式碼下如:
--為表生成模擬資料,為保證資料不失一般性先插入十萬條記錄
truncatetable Distribute;
truncatetable Distribute_NoPartition;
setnocount on;
[email protected] int;
[email protected] = 100000;
while(@cnt> 0)
begin
INSERT INTO [dbo].[Distribute]
([OutAreaID]
,[OutCustomer]
,[ProductID]
,[InAreaID]
,[InCustomer]
,[DistributeNum]
,[Status]
,[SendDate])
VALUES
(@cnt%100,
@cnt%99,
@cnt%98,
@cnt%100,
@cnt%101,
@cnt%102,
@cnt%200,
DATEADD(dd,@cnt%365,'2011-1-1'));
set @cnt = @cnt -1;
end;
--為高效快速生成測試資料採用每次在原來記錄數上增加一倍的方法生成,以下將生成萬條記錄
[email protected] int;
[email protected] = 7;
while(@cnt> 0)
begin
INSERT INTO [dbo].[Distribute]
SELECT [OutAreaID]
,[OutCustomer]
,[ProductID]
,[InAreaID]
,[InCustomer]
,[DistributeNum]
,[Status]
,[SendDate]
FROMDistribute;
set @cnt = @cnt -1;
end;
--把分割槽表中的資料全部複製到未分割槽表中
INSERTINTO [dbo].[Distribute_NoPartition]
SELECT[OutAreaID]
,[OutCustomer]
,[ProductID]
,[InAreaID]
,[InCustomer]
,[DistributeNum]
,[Status]
,[SendDate]
FROM Distribute;
selectcount(ID) from Distribute_NoPartition;
selectcount(ID) from Distribute;
2.2.4效能對照
完成上述操作後,我們就可以進行比較分割槽和未分割槽方案的優劣了,以下為查詢分析器中跟蹤的真實資料,由於執行機器的不同執行效率也會有差異,但可以肯定的是在生產環境中分割槽方案執行效率將更優越,因為在硬體環境的配製上一般優於個人電腦,且都能同行平行計算和並行讀取多個分割槽資料。以下為本機筆記本中效率對比:
首先開啟指令碼執行中效能跟蹤開關:
setstatistics time on
setstatistics io on
分別使用分割槽和非分割槽來查詢某個月份的記錄,結果如下:
select* from [Distribute] where SendDate > '2011-6-1' and SendDate <'2011-6-30'
表 'Distribute'。掃描計數 1,邏輯讀取 3256 次,物理讀取 0 次,預讀0 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。
SQL Server 執行時間:
CPU 時間 = 484 毫秒,佔用時間= 7721 毫秒。
select* from dbo.Distribute_NoPartition where SendDate > '2011-6-1' and SendDate< '2011-6-30'
表 'Distribute_NoPartition'。掃描計數 3,邏輯讀取 38533 次,物理讀取 459 次,預讀 24007 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。
SQL Server 執行時間:
CPU 時間 = 1594 毫秒,佔用時間 = 9603 毫秒
通過以上執行結果,可以看出使用了分割槽的查詢時間為7721 毫秒,而未使用分割槽的也只有9603 毫秒,似乎區別不大。按設想把整個表分為了十二塊,未使用分割槽的表查詢需要全表掃描,而使用分割槽方案的表查詢只需要查詢十二塊中的一塊,理論上應分割槽應只需要未分割槽時間的十二分之一。而我們再次檢視另一個關鍵效能引數即掃描次數和邏輯讀取次數,使用分割槽方案的表的查詢掃描計數 1邏輯讀取了3256 次,而未使用分割槽方案的表的掃描計數 3,邏輯讀取 38533 次,所以從記憶體訪問情況分析,基本符合我們最初的設想,而時間差異不大的原因是查詢中資料量很大,SQL Server把結果呈現出來佔用了大量時間。所以後面我們再次用一個查詢驗證它們效能差異,程式碼如下:
withDistribute_CTE as
(
select *,ROW_NUMBER() Over(order by ID desc) as rowid fromdbo.Distribute where SendDate > '2011-6-1' and SendDate < '2011-6-30'
)select* from Distribute_CTE where rowidbetween 30 and 40;
表 'Distribute'。掃描計數 1,邏輯讀取 35 次,物理讀取 0 次,預讀 0 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。
SQL Server 執行時間:
CPU 時間 = 325 毫秒,佔用時間= 214 毫秒。
withDistribute_NoPartition_CTE as
(
select *,ROW_NUMBER() Over(order by ID desc) as rowid fromdbo.Distribute_NoPartition where SendDate > '2011-6-1' and SendDate <'2011-6-30'
)select* from Distribute_NoPartition_CTE whererowid between 30 and 40;
(11 行受影響)
表 'Distribute_NoPartition'。掃描計數 1,邏輯讀取 19297 次,物理讀取 18 次,預讀 10036 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。
SQL Server 執行時間:
CPU 時間 = 2313 毫秒,佔用時間 = 2678 毫秒。
上述用了一個分頁的查詢,為簡明清晰使用一個表格來對比它們之間效能差異:
是否分割槽 |
CPU 時間 |
掃描計數 |
邏輯讀取 |
佔用時間 |
使用分割槽查詢 |
325 |
1 |
35 |
214 |
未分割槽查詢 |
2313 |
1 |
19297 |
2678 |
對比以上資料後,我們很清晰的發現分割槽後效能有明顯的提升,將近提升10倍左右,當然這與分割槽的數量和分割槽欄位及提供給使用者的查詢條件有關,本方案正是利用了使用者關心業務資料的時間範圍來進行分割槽,所以能很好的解決效能問題。
2.2.5分割槽方案實現自動護維
儘管做了詳細的預先規劃,分割槽方案可能已按照預期方式進行工作。但隨著時間的推移分割槽方案將不能滿足需求了。以上示例程式碼中定義的分割槽到2011年12月,現在假設到了2012年1月了,這時資料都會放入到最後一個分割槽中,且後面一直如此,這樣就不能均勻的把資料分配到分割槽上了,所以需要能夠在現在分割槽上擴充套件。這時只需要在原有分割槽方案基礎上擴充套件一個分割槽即可,而該分割槽重複迴圈的利用已有的檔案組,具體程式碼如下:
ALTERPARTITION SCHEME PSch_Month NEXT USED FG2;
ALTERPARTITION FUNCTION PFun_Month() SPLIT RANGE ('2012-2-1');
這時我們插入一條2012年的記錄將會放入到新擴充套件的分割槽中了。
--插入一條年的記錄然後檢視它所有的分割槽號
INSERTINTO [dbo].[Distribute] VALUES (10,99,98,100,101,102,200,'2012-1-1');
select*, $partition.PFun_Month(SendDate) as 分割槽號 from [Distribute] whereSendDate='2012-1-1'
上述程式碼雖然完成了分割槽的擴充套件,但更為智慧的方法是通過一個後臺作業自動完成這一功能,而不需要人為的干預,在作業中執行的過程程式碼如下:
--改進後的維護過程
alterprocedure proc_AutoChangePartition
(
@now datetime
)
as
begin
declare @startDate datetime;
declare @partitionDate datetime;
declare @sql varchar(500);
declare @lastPartitionNo int;
declare @currentPartitionNo int;
--set @now = GETDATE();
set @startDate = '2010-12-1';
--約定日期與分割槽的關係為從-12-1後每增加一個月份則分割槽號+1
--獲取當前日期分割槽號
set @lastPartitionNo = (SELECT fanout FROMsys.partition_functions WHERE [name] = 'PFun_Month');
set @currentPartitionNo =DATEDIFF(MONTH,@startDate,@now);
if(@currentPartitionNo >@lastPartitionNo)
begin
set @sql = 'ALTER PARTITION SCHEMEPSch_Month NEXT USED FG' + LTRIM(@currentPartitionNo%12);
exec(@sql);
set @partitionDate =ltrim(YEAR(@now)) + '-' + LTRIM(Month(@now)) + '-1';
ALTER PARTITION FUNCTIONPFun_Month() SPLIT RANGE(@partitionDate);
end;
end;
因為業務發生時間不超過當前時間,所以只需要每月定時執行該過程即可。該過程將會檢查分割槽方案中是否有存放到當前月份的分割槽,如沒有則建立一個,這樣就可實現自動的實現分割槽了。
3. 樹型層次結構資料優化
3.1 樹型層次結構資料概述
系統中的地區、客戶、產品都存在著樹型結構的層次關係。如地區有國家、省、市等層次,而儲存這種結構通常用二維表的父/子欄位的關係來表示。而最關鍵的是對這些資料的查詢,經常需要查詢出某個節點下屬的所有節點,在SQL Server2000前只能使用遞迴方式,這種方式不但複雜且效能很差。SQL Server 2005開始支援的CTE(公共表表達式)從一定程式上方便了該工作的實現,但仍未解決根本問題,因為它僅僅只是簡化了SQL語句的編寫,沒真正提高查詢效能。SQL 2008的最新支援的hierarchyid型別讓這個工作更加簡化和高效,該型別其實是一個CLR(公共語言執行時)自定義資料型別。以下通過例項來說明傳統方式(CTE)和新技術方式(hierarchyid)的效能差異。
3.2 兩種方案效能對照
首先分別建立兩種解決樹型結構資料儲存的方案,這裡以地區表為例,第一種方式是父/子兩欄位表示層次結構記錄,第二種是採用最新支援的hierarchyid型別來表示這種層次關係,程式碼如下:
IFNOT EXISTS (SELECT * FROM sys.objects WHERE object_id =OBJECT_ID(N'[dbo].[Area]') AND type in (N'U'))
BEGIN
CREATETABLE [dbo].[Area](
[ID] [int] NOT NULL,
[Tree_ID] [hierarchyid] NOT NULL,
[AreaName] [nvarchar](50) NOT NULL,
CONSTRAINT [PK_Area] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH(PAD_INDEX = OFF,STATISTICS_NORECOMPUTE = OFF,IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS =ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON[PRIMARY]
END
IFNOT EXISTS (SELECT * FROM sys.objects WHERE object_id =OBJECT_ID(N'[dbo].[Area_NoTree]') AND type in (N'U'))
BEGIN
CREATETABLE [dbo].[Area_NoTree](
[ID] [int] NOT NULL,
[ParentID] [int] NOT NULL,
[AreaName] [nvarchar](50) NOT NULL,
CONSTRAINT [PK_Area_NoTree] PRIMARY KEYCLUSTERED
(
[ID] ASC
)WITH(PAD_INDEX = OFF,STATISTICS_NORECOMPUTE = OFF,IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS =ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON[PRIMARY]
END
使用以下程式碼匯入測試資料
--生成模擬地區資料
truncatetable [Area];
--一級
INSERTINTO [dbo].[Area]
([ID]
,[Tree_ID]
,[AreaName])
values
(
0,'/',N'根地區名'
);
[email protected] int,
@Leve1cnt int,
@Leve2cnt int,
@Leve3cnt int;
[email protected] = 10;
[email protected] = 50;
[email protected] = 250;
[email protected] = 1;
while(@i<= @Leve1cnt)
begin
INSERT INTO [dbo].[Area]
([ID]
,[Tree_ID]
,[AreaName])
values
(
@i,'/'+ LTRIM(@i) + '/',N'一級地區名' + LTRIM(@i)
);
set @i = @i + 1;
end;
--二級
[email protected] = 1;
while(@i<= @Leve2cnt)
begin
INSERT INTO [dbo].[Area]
([ID]
,[Tree_ID]
,[AreaName])
values
(
@i + @Leve1cnt,'/' +ltrim(@i%@Leve1cnt+1) + '/' + LTRIM(@i/@Leve1cnt+1) + '/',N'二級地區名' + LTRIM(@i)
);
set @i = @i + 1;
end;
--三級
[email protected] = 1;
while(@i<= @Leve3cnt)
begin
INSERT INTO [dbo].[Area]
([ID]
,[Tree_ID]
,[AreaName])
values
(
@i + @Leve2cnt + @Leve1cnt,'/' +ltrim(@i/(@Leve3cnt/@Leve1cnt)+1) + '/' +LTRIM(@i/(@Leve2cnt/@Leve1cnt)%(@Leve2cnt/@Leve1cnt)+1) + '/' +LTRIM(@i%(@Leve2cnt/@Leve1cnt)+1) + '/',N'三級地區名' + LTRIM(@i)
);
set @i = @i + 1;
end;
--生成模擬地區資料
--一級
truncatetable [Area_NoTree];
[email protected] int,@i int;
[email protected] = 10;
[email protected] = 1;
while(@i<= @Leve1cnt)
begin
INSERT INTO [dbo].[Area_NoTree]
([ID]
,[ParentID]
,[AreaName])
values
(
@i,0,N'一級地區名' + LTRIM(@i)
);
set @i = @i + 1;
end;
--二級
[email protected] int;
[email protected] = 50;
[email protected] = 1;
while(@i<= @Leve2cnt)
begin
INSERT INTO [dbo].[Area_NoTree]
([ID]
,[ParentID]
,[AreaName])
values
(
@i + @Leve1cnt,@i%@Leve1cnt + 1,N'二級地區名' + LTRIM(@i)
);
set @i = @i + 1;
end;
--三級
[email protected] int;
[email protected] = 150;
[email protected] = 1;
while(@i<= @Leve3cnt)
begin
INSERT INTO [dbo].[Area_NoTree]
([ID]
,[ParentID]
,[AreaName])
values
(
@i + @Leve2cnt [email protected],@i%@Leve2cnt + 1,N'三級地區名' + LTRIM(@i)
);
set @i = @i + 1;
end;
完成模擬資料插入後,就可以時行具體的測試了。
--某節點下的所有子節點
[email protected]_ID hierarchyid;
[email protected]_ID=tree_ID FROM area WHERE id=7;
SELECT*,tree_ID.GetLevel()AS Level FROM area WHERE tree_ID.IsDescendantOf(@tree_ID)=1
(31 行受影響)
表 'Area'。掃描計數 1,邏輯讀取4 次,物理讀取 0 次,預讀 0 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。
SQL Server 執行時間:
CPU 時間 = 0 毫秒,佔用時間 =0 毫秒。
withcte
as
(
select ID,parentid,areaname from area_notreewhere ID=1
union all
select A.ID,A.parentid,A.areaname fromarea_notree A join cte C on A.parentID = C.ID
)
select* from cte;
(31 行受影響)
表 'Worktable'。掃描計數 2,邏輯讀取 187 次,物理讀取 0 次,預讀 0次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。
表 'Area_NoTree'。掃描計數 1,邏輯讀取 126 次,物理讀取 0 次,預讀 0次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。
SQL Server 執行時間:
CPU 時間 = 6 毫秒,佔用時間 =7 毫秒。
上述使用了一個典型查詢某節點下所有子節點的查詢,為簡明清晰使用一個表格來對比它們之間效能差異:
儲存方式 |
CPU 時間 |
掃描計數 |
邏輯讀取 |
佔用時間 |
傳統方式 |
6 |
2 |
187 |
7 |
新技術 |
0 |
1 |
4 |
0 |
對比以上資料後,使用新技術查詢時所花費時間幾乎為0。
3.3 hierarchyid 資料型別深入分析
通過以上實驗例子可以看出使用hierarchyid 資料型別來表現樹型結構在效能上的優異性。下面將詳細分析該型別的特點。
l 非常緊湊
在具有 n 個節點的樹中,表示一個節點所需的平均位數取決於平均端數(節點的平均子級數)。端數較小時 (0-7),大小約為 6*logAn 位,其中 A 是平均端數。對於平均端數為 6 級、包含 100,000 個人的組織層次結構,一個節點大約佔 38 位。儲存時,此值向上舍入為 40 位,即 5 位元組。
l 按深度優先順序進行比較
給定兩個 hierarchyid 值 a 和 b,a<b 表示在對樹進行深度優先遍歷時,先找到 a,後找到 b。hierarchyid 資料型別的索引按深度優先順序排序,在深度優先遍歷中相鄰的節點的儲存位置也相鄰。例如,一條記錄的子級的儲存位置與該記錄的儲存位置是相鄰的。
l 支援任意插入和刪除
通過使用 GetDescendant方法,始終可以在任意給定節點的右側、左側或任意兩個同級節點之間生成同級節點。在層次結構中插入或刪除任意數目的節點時,該比較屬性保持不變。大多數插入和刪除操作都保留了緊湊性屬性。但是,對於在兩個節點之間執行的插入操作,所產生的 hierarchyid 值的表示形式在緊湊性方面將稍微降低。
3.4結論
通過以上例項對比和內部的原理機制分析,不難看出本系統中的樹型結構的資料完全適合於使用該方案來解決,並可保證獲得優異效能。
4. 報表效能優化
4.1 本系統報表的特點分析
報表是本系統中最為重要的一部分,它對系統中各類資料統計展示,為各種決策提供依據。但由於系統複雜、資料量大,一個報表往往需要關聯好幾個表或檢視,按常規查詢或只作索引優化無法解決根本問題,時常會造成一個報表需要數分鐘或更長的等待時間才能展現給使用者,甚至經常出現超時情況。這樣會降低系統的使用者體驗和可用性,另外使用該功能的多般為公司主要領導,應儘可能的達到好的使用者體驗,而體驗最重要的指標之一就是快捷。以下分析的都是指業務表中資料量很大,且需要關聯多個業務,還需要經過大量過濾和分組統計才能得出最終結果的報表。
4.2 實現方案分析
對於查詢業務複雜而且查詢的表及條件過多時,報表查詢的效能大大下降,因為這會涉及到大量的表關聯、分組統計、過濾等操作,其中對效能影響最大的就是多表之間的聯接(join)和分組(Group by)統計。而使用者輸入的查詢條件進行過慮可通過索引來解決,所以如果能把每次聯接和統計這部分工作事先完成則可解決該效能問題。為了便於問題的分析,下面列出具體的一個報表場景,為了描述方便僅對其關鍵欄位時行描述。
報表將要展示樣例如下,即需要了解某地區某種型號的裝置在某一日期發生的所有業務數量,這些業務有發貨、調入、調出、零售,如下表所示:
地區 |
產品型號 |
日期 |
發貨 |
調入 |
調出 |
零售 |
… |
深圳 |
C2800 |
2011-1-2 |
100 |
200 |
100 |
150 |
… |
北京 |
ET340 |
2011-1-2 |
300 |
100 |
200 |
100 |
… |
… |
… |
… |
… |
… |
… |
… |
但上述的各種業務發生的數量都單獨存放在各自的表中,以下僅列出其中的發貨表樣例,其它表類似。
地區 |
產品型號 |
發貨數 |
發貨日期 |
發貨廠家 |
… |
深圳 |
C2800 |
100 |
2011-7-29 |
中興 |
… |
北京 |
ET340 |
300 |
2011-5-23 |
某公司 |
… |
… |
… |
… |
… |
… |
… |
如上述分析一致,如果按常規的方式(用JOIN關聯)將至少關聯5張表,系統開銷非常大,並且所編寫的語句非常複雜,本人使用UNION ALL後再統計(sum求和)的方法,各業務表的資料量為1000萬時,查詢分析器耗費近5(5*60*1000毫秒)分鐘,才計算出結果,可見此方法不可能被使用者所接受,通過檢視其實際的執行計劃發現,有大量的關聯(JOIN)操作佔了絕大部分開銷,按上述分析需要解決這一效能瓶頸。通常的做法把所需要的全體資料一次查找出來放入到另一個專供報表訪問的表中,簡稱該表為報表表,然後在該表上為經常查詢的且選擇度較高的欄位加上索引,另外為了能更新原始業務表中的變化(新插入或更新了記錄等)到報表表中,需要通過一個作業定時查詢並更新到報表表中。但由於不能確定那些記錄更新了或是新插入(這裡沒有刪除,因為業務資料不充許刪除)的所以每次需要把已經統計好的報表記錄全部刪除,然後關聯多個表查詢出所有記錄插入到報表表中。顯然該方案可以解決多表查詢時大量的聯接操作,如果頻繁的執行該作業也可以保證資料有較好的實時性,通常可以滿足報表的需求,因為絕大多數場景下報表資料是允許一定延時的,但存在以下不足之處:
l 業務表不管改變是否巨大,或者跟本就沒有記錄更新,但作業仍會全量統計所有資料,這明顯示浪費了系統資源;
l 各個業務表資料量越來越大時,每次作業會刪除和重新生成的資料越來越多,系統負擔越來越大,甚至超時;
l 當要求頻繁執行該作業或需要在業務繁忙時更新報表資料將變得不可能,因為系統已有大量使用者使用情況下執行一個笨重的作業會影響前臺使用者操作;
l 在刪除報表資料後,到全部插入完成這段時間內使用者的任何報表請求都無法查詢,處於等待或超時狀態。
基於以上分析,通過作業的方式頻繁的更新資料到報表表中,唯一不能解決的問題是增量更新,即在業務表中大量的資料在某一段時間相對穩定的,只會有少量的資料做了更新,在一小段時間內一般也只有少量的新增記錄,如果能夠得到這些資料,那麼就可以在原有的報表表上只更新和插入這些資料將變得非常“輕量級” 。但是要找出這批記錄是非常困難的,常規的方式是記錄日誌,即只要業務資料變化都寫一條日誌,然後依據日誌來獲取變更的業務資料,但業務表很多,對其操作又分很多種類(如更新、刪除、審批等),該方案需要大量的人力投入且容易出現差錯,保障性差,統計複雜,所以需要更為先進的方案,接下來將詳細的分析通過加入rowversion資料型別來解決這一問題。
4.3 rowversion資料型別分析
經過分析和網上查詢發現rowversion型別欄位恰好適用這一場景,能解決上述增量更新問題。每個資料庫都有一個計數器,當對資料庫中包含 rowversion 列的表執行插入或更新操作時,該計數器值就會增加。此計數器是資料庫行版本。這樣就可以跟蹤資料庫內的相對時間,而不是時鐘相關聯的實際時間。一個表只能有一個rowversion 列。每次修改或插入包含 rowversion 列的行時,就會在 rowversion 列中插入經過增量的資料庫行版本值。通過這一列值就很容易的得到表中更新和新增的記錄了,接下來將詳細分析怎麼在作業中使用行版本來增量更新報表資料。
4.3方案具體實施
通過以上分析後,已經有了解決問題的基本思路了,下面重點以發貨業務為代表來分析該方案的具體實施。下方為作業具體實現的流程圖:
對照以上流程圖的說明如下:
1. 獲取上次行版本
如果作業是首次執行,則無法獲取上次執行後的行版本值,這時取出的是空值,在接下來的步驟中要做全量更新,則全量更新後直接跳到步驟5;而如果不是首次執行,則在上次更新完資料後會記錄一個最新的行版本值。
2. 依據行版本查找出變更的記錄
因為上次執行完成後可以保證各業務表與報表表是同步的,而這時產生的行版本值之前的記錄是已同步過的,但作業完成後的更新的記錄都會比該行版本值大。所以查詢這部分記錄變得非常容易,只需判斷該表中那個記錄的行版本比上次的值大即可,可以把這部分記錄儲存到臨時表中。
3. 從報表表中刪除已變更記錄
如果業務表中的記錄只做了更新操作,那麼這部分記錄之前已經同步到報表表中了,所以要先刪除這部分記錄。
4. 插入變更和新增加記錄
插入到報表表中的記錄有兩部分,第一部分是更新過的記錄,第二部分是業務表中新增的記錄,把這兩部分記錄一併插入到報表表中即可。
5. 儲存最新行版本
取出最新的行版本值,儲存起來,以便下次執行作業時使用。
由於本示例中涉及到的表過多,很多都是基礎的編碼(如建立表、新增欄位)。為了能說明重點內容,以下僅列出部分更新作業中程式碼。
CreateProcedure Proc_GenerateReportByJob
Begin
declare @lastRowversion rowversion;
declare @currentRowversion rowversion;
--1.獲取上次更新的行版本,如果沒有則全量更新
set @lastRowversion=(select lastRowversionfrom ConfigData where KEY = 'Rowversion');
set @[email protected]@DBTS;
--2.獲取更新的範圍
select
地區, 型號, min(日期) as 最小更新日期into #TMP
from
dbo.SendOrder
where
CheckVersion > @lastRowversion
group by
地區, 型號;
--3.從報表表中刪除這部分記錄
Delete from
Report
from
ReportTable as Report join #TMP onReport.地區=#TMP.地區 and Report.型號=#TMP.型號 and Report.日期>#TMP.日期
--4.插入變更和新增加記錄
Insert into ReportTable
Select 地區,產品型號,發貨數,調入數,調出數,零售數,庫存數
From
(
Select 地區,產品型號,發貨數 as 數量, ‘發貨數’as 型別 from dbo.SendOrder where 地區=#TMP.地區 and 型號=#TMP.型號 and 日期>#TMP.日期
UNION ALL
…
)T Pivot(sum(數量) for型別(發貨數,調入數,調出數,零售數,庫存數) T2
--5.儲存最新行版本
update ConfigData [email protected] where KEY = 'Rowversion'
End
相關推薦
一個進銷存系統性能優化總結
系統的資料量分析及存在的關鍵效能瓶頸 系統設計應支援每年2億部上以終端的銷售量業務處理,一般的終端裝置從生產到消費整個生命週期最長為1年,超過一年的資料只需備份儲存即可,不需要在系統中處理,即必須完整的儲存1年發貨、排程、零售等資料。初略的計算,最細粒度的表將超過2億條記
kubernetes 環境搭建 及 基礎架構介紹 及 一個進銷存管理系統 簡單搭建
kubernetes是一個全新的基於容器技術的分散式架構領先方案。它是谷歌十幾年來大規模應用容器技術的經驗積累和昇華的一個重要成果 kubernetes是一個完備的分散式系統支撐平臺。kubernetes具有完備的叢集管理能力,包括多層次的安全防護和准入機制,多租戶應用支撐能力,透明的服務註冊和
docker compose安裝與常用命令介紹 及使用docker-compose執行一個進銷存管理系統
使用微服務架構的應用系統一般包含若干個微服務,每個微服務一般都會部署多個例項。如果每個微服務都要手動啟停,那麼效率之低,維護量之大可想而知 docker compose 是一個用於定義和執行多容器docker應用程式的工具 安裝: 下在並安裝適應系統版本的compose
一個進銷存資料庫設計的例子
CREATE TABLE user( User_Id varchar(6), User_Pwd varchar(8) NULL, Again_Pwd varchar(8) NULL, Bel_Group varchar(3) NULL, Div_Typ
自己無聊做的一個簡單的企業進銷存管理系統
最近2個星期家裡沒網,上班回家後很無聊,還好手中有下載下來的api,就利用起來每天寫一點。我是計算機業餘愛好者,忘大家指點: 測試執行平臺:Windows、Linux各個版本、MAC等任何平臺。 JavaDB位於位於JDK的安裝位置,例如:C:\Program Files\
java進銷存管理專案總結
進銷存管理系統個人心得 遇到過的困難: <1>. 在資料庫中插入了資料,通過查詢語句也能查詢到,但在eclipse讀取後臺顯示空值。 <2>. 打架包放到工程裡面後,呼叫架包裡面的方法卻不能實現(架包裡面的JInternalFrame視窗不能顯示出
【HBase調優】Hbase萬億級存儲性能優化總結
控制 其他 最大連接數 報警 低延時 導致 消息 應用開發 files 背景:HBase主集群在生產環境已穩定運行有1年半時間,最大的單表region數已達7200多個,每天新增入庫量就有百億條,對HBase的認識經歷了懵懂到熟的過程。為了應對業務數據的壓力,HBase入庫
復雜進銷存樣例
src com 動態 mage log 主從 代碼生成器 1-1 http 為方便開發參考,實現復雜的交互及子從表的操作,後面版本將提供復雜的進銷存樣例,並集成代碼生成器生成復雜的表操作主要功能:主從表聯合顯示批量提交,批量導入彈窗選擇動態計算行等等... 復雜進銷存
Unity遊戲項目性能優化總結 (難度3 推薦4)
節點 alloc debug.log 系統 form 都是 開發 變量聲明 oid 原文地址: https://zhuanlan.zhihu.com/p/24392681 本文就Unity遊戲項目性能優化作出了總結。包括Profile工具、Unity使用、機制設計、腳本編寫
java 庫存 商戶 用戶 進銷存 管理系統 SSM springmvc 項目源碼
地址 ext 多代理 管理後臺 月份 圖標 src 無限級別 點對點 需求分析: 有個廠家,下面有很多代理商(商戶或門頭等),之前商戶進貨、庫存、銷售、客戶資料等記錄在excel表格中 或者無記錄,管理比較混亂,盈利情況不明。不能有效了解店鋪經營情況和客戶跟蹤記錄 廠家也不
java 進銷存 商戶管理系統 客戶管理 庫存管理 銷售報表 SSM項目
進銷存 ssm 商戶 系統介紹:1.系統采用主流的 SSM 框架 jsp JSTL bootstrap html5 (PC瀏覽器使用)2.springmvc +spring4.2.5+ mybaits3.3 SSM 普通java web(非maven) 數據庫:mysql3.開發工具:mye
java性能優化總結
框架 nbsp 100% java 性能優化 分析 服務器 相關 cdata 本人在java中積累了一些性能優化相關的經驗,現在總結如下: 批量處理服務性能優化 RTB服務性能優化 BasicData線上問題解決,瘋狂FullGC的問題 BasicData線上部分服
頁面性能優化總結
客戶端 字符串 服務 原因 -c sql expire 設置 其他 頁面性能優化總結 由於本人前幾天一直在解決頁面性能、加載慢等問題。解決後,對於該方面知識有一定的認識與理解,現將經驗
手機進銷存軟件主要功能有哪些?
避免 幫助 管理工具 效率 所有 自動 完整 str 數據 一款好用的手機進銷存軟件,不僅功能可以最大程度滿足采購、銷售、庫存、財務等方面管理需求,還要在業務流程上,靈活適應企業復雜多變的業務需求。一款好用的手機進銷存軟件,可以在手機上實現客戶、銷售、采購、庫存、產品、
手機進銷存軟件對外勤人員管理的幫助是什麽?
系統 一款好用的手機進銷存軟件,不僅功能可以最大程度滿足采購、銷售、庫存、財務等方面管理需求,還要在業務流程上,靈活適應企業復雜多變的業務需求。一款好用的手機進銷存軟件,可以在手機上實現客戶、銷售、采購、庫存、產品、合同、售後、財務、OA等一體化管理,Android、iOS、Windows Phone全面
Java門店管理系統 客戶資料檔案管理 庫存管理 進銷存 SSM項目源碼
進銷存 ssm 商戶管理系統 客戶管理 庫存管理 系統介紹:1.系統采用主流的 SSM 框架 jsp JSTL bootstrap html5 (PC瀏覽器使用)2.springmvc +spring4.3.7+ mybaits3.3 SSM 普通java web(非maven, 附贈
java 庫存 進銷存 商戶 多用戶管理系統 SSM springmvc 項目源碼
進銷存 商戶管理系統 ssm 庫存管理 客戶管理 系統介紹:1.系統采用主流的 SSM 框架 jsp JSTL bootstrap html5 (PC瀏覽器使用)2.springmvc +spring4.3.7+ mybaits3.3 SSM 普通java web(非maven, 附贈
談毛豆鋼材進銷存管理系統:長沙一款專門為鋼材經銷商和鋼材生產企業設計開發的鋼材類軟件
長沙毛豆科技 毛豆鋼材進銷存管理系統 長沙軟件開發 毛豆鋼材進銷存管理系統:專門為鋼材經銷商和鋼材生產企業設計開發的鋼材類軟件 長沙毛豆科技小編給大家介紹“毛豆鋼材進銷存管理系統”的基本功能:毛豆鋼材進銷存管理系
web前端性能優化總結
函數定義 network 繼承 執行 strong bigpipe view pan odin 1em=16px(但不完全是) em會繼承父元素的字體大小。ie 部分瀏覽器不支持em。 rem繼承根元素的字體大小html。 px和rem vue裏面用jq只能在mounte
35+ 個 Java 代碼性能優化總結
tab 共享 這就是我 目標 date() 要花 writer 維護 開發者 前言 代碼優化,一個很重要的課題。可能有些人覺得沒用,一些細小的地方有什麽好修改的,改與不改對於代碼的運行效率有什麽影響呢?這個問題我是這麽考慮的,就像大海裏面的鯨魚一樣,它吃一條小蝦米有