索引自不用說了,幾乎是必須要考慮到的。select的時候儘量把使用索引的字段放前面,數據更新都會影響索引。查詢上聚集索引要快一些,關乎物理存儲也就知道有一個聚集索引。非聚集索引可以有多個,但是因爲更新的同時也會更新索引的緣故所以有太多的非聚集索引是個負擔。
這篇文章的很多細節並非出自我手,很多是網絡收集過來,所以對於版權,歸原作者,數據量大了之後,就必須做一些日常的計劃任務了,比如過一段時間做一些備份,做一些分區,把一些不常用到的歷史數據放到其他地方,比如按某字段分區存儲。壓縮數據等。
執行SQL查詢時,主要的幾個瓶頸在於:CPU運算速度、內存緩存區大小、磁盤IO速度。而對於大數據量數據的查詢,其瓶頸則一般集中於磁盤IO,以及內存緩存。那麼爲了提高SQL查詢的效率,一方面我們需要考慮儘量減少查詢設計的數據條目數——建立索引,設立分區;另一方面,我們也可以考慮切實減少數據表物理大小,從而減少IO大小。
在SQL Server 2008中,最新提供了一項功能“壓縮(Compression)”,就是定位於減少數據表、索引物理大小。
這裏可以看到幾點:
- 下方列表裏列出了該表所有的分區,也就是可以同一張表的不同分區應用不同的壓縮策略。
- 壓縮方式(Compression Type)分爲Row和Page兩種。
行級壓縮(Row):
一方面減少了動態長度字段元數據的大小(varchar、varbinary等),比如之前存儲字段實際長度需要2bytes,壓縮後只需要3bits。
另一方面也直接減少各字段存儲內容的大小,比如存儲數值1在一個int類型字段中,壓縮後只佔用了一個字節。頁級壓縮(Page):能在各行間共享相同的數據,這裏麪包含兩項技術:列前綴(Column Prefix)、頁字典(Page Dictionary)。
列前綴可以讓擁有同樣前綴的字段值擁有類似外鍵一樣的結構來存儲相同的前綴和各自的其餘部分。比如一張存儲了一個網站所有頁面URL的表,URL字段存儲的值分別是‘www.example.com/a.html’,‘www.example.com/b.html’,‘www.example.com/c.html’,‘www.example.com/d.html’。則壓縮後,它們同樣的前綴‘www.example.com/’會被提取出來,而其餘部分會被類似如下的形式存儲‘1a.html’,‘1b.html’,‘1c.html’,‘1d.html’。
頁字典則可以將在應用列前綴基礎上的其餘部分再次聚合存儲,比如同樣是一張存儲了一個網站所有頁面URL的表,假設有在表裏裏有多條URL字段的值相同,比如‘1a.html’,‘1b.html’,‘1c.html’,‘1b.html’,‘1a.html’,‘1a.html’,則通過頁字典技術壓縮後,實際存儲在字段中的值會進一步減少爲‘2’,‘3’,‘1c.html’(沒有重複的字段值不會被壓縮),‘3’,‘2’,‘2’。 - 點擊“Calculate”後,會計算出表當前佔用的空間大小,以及壓縮需要的空間大小。注意這裏與一般預想的不同,如果要對一張預存有數據但尚未壓縮的表進行壓縮,首先需要的是額外的空間大小。
執行壓縮
設置好之後,就可以選擇是生成腳本還是立即執行,一般壓縮的執行時間受表原有數據多少以及選擇壓縮方式的影響。筆者對一張有上千萬條記錄的表做頁級壓縮,耗時在10分鐘左右。
壓縮完成之後查看數據庫大小,會發現數據庫的大小變大了!這也和在設置階段計算出來的額外空間相關。但實際上這裏大部分空間是預佔的空間,並沒有實際數據。如果需要節省磁盤空間,需要進一步執行收縮(Shrink)操作。
與Compression不同,Shrink用來釋放數據庫佔據的沒有利用的空間,一般用來對無用的日誌文件收縮(如果操作頻繁,日誌文件很有可能大於數據庫實際數據的大小)。這裏我們對數據庫文件(mdf)做Shrink操作,完成之後再看數據庫的大小,果然減少了很多。筆者做壓縮、Shrink之後,一般都能將數據庫的大小減爲原來的1/3~1/2左右。當然,具體壓縮比率取決於壓縮方式、壓縮表的字段特點、壓縮表佔整個數據庫數據的比重等。
注意事項
•既然對錶行了壓縮,那麼在執行查詢時必然會有解壓縮的過程。而這一過程會佔用CPU時間,也就是我們在通過壓縮減少了磁盤佔用空間以及IO時間的同時,增大了CPU的消耗。所以在壓縮前需要考慮清楚查詢的瓶頸到底是磁盤IO還是內存還是CPU。而且如果表應用了壓縮,類似建立索引,對於增刪改等操作也會有一定的影響。所以同樣要考慮應用在表上的操作到底以哪種爲主。
•各頁面的壓縮是獨立進行的,頁字典和列前綴也分別存儲於各頁內。而且壓縮僅在數據頁快滿的時候進行,因爲一個頁的大小是固定的,壓縮半頁不會有性能上的提升。
•數據庫備份中也有Compression的選項,但這利用的是系統的文件壓縮技術,而且只能應用於整個數據庫上。
•容易被忽略的是,索引也能被壓縮,而且和表壓縮獨立,同樣也會提升所有應用到索引的查詢的性能。
•如果對錶進行壓縮,聚集索引會自動應用與表同樣的壓縮模式,而非聚集索引不會。
•在Shrink階段,可能會造成大量的索引碎片,所以可以在Shrink完成之後重建或者重組織索引,但同時,這些操作也會造成數據庫的體積變大……也就是,最小的數據庫體積和最小碎片比率的索引是魚與熊掌,不可兼得。
和壓縮(Compression)相比,數據庫分區(Partition)的操作更爲複雜繁瑣。而且與Compression一次操作,終身保持不同,分區是一項需要長期維護週期變更的操作。
分區的意義在於將大數據從物理上切割爲幾個相互獨立的小部分,從而在查詢時只取出其中一個或幾個分區,減少影響的數據;另外對於置於不同文件組的分區,並行查詢的性能也要高於對整個表的查詢性能。
事實上,在SQL Server 2005中就已經包含了分區功能,甚至在2005之前,還存在一個叫做“Partitioned Views”的功能,能通過將同樣結構的表Union在一個View中,實現類似現在分區表的效果。而在SQL Server 2008中,分區功能得到了顯著加強,使得我們不僅能夠對錶和索引做分區,
使得我們不僅能夠對錶和索引做分區,而且允許對分區上鎖,而不是之前的全表上鎖。
和Compression一樣,在SQL Server 2008中也提供了分區的嚮導界面。在企業管理器中,需要分區的表上右鍵選擇Storage-》Create Partition:
這裏會列出該表所有的字段,包括字段類型、長度、精度及小數位數的信息,可以選擇其中的任意一一列作爲分區列(Patitioning Column),不僅僅是數字或者日期類型,即使是字符串類型的列,也可以按照字母順序進行分區。而以下類型的列不可用於分區:text、ntext、image、xml、timestamp、varchar(max)、nvarchar(max)、varbinary(max)、別名、hierarchyid、空間索引或 CLR 用戶定義的數據類型。此外,如果使用計算列作爲分區列,則必須將該列設爲持久化列(Persisit)。
在列表下方,提供了兩個選項:
- 分配到可用分區表:
這要求在同一數據庫下有另一張已分好區的表,同時該表的分區列和當前選中的列的類型完全一致。
這樣的好處是當兩張表在查詢中有關聯時,並且其關聯列就是分區列時,使用同樣的分區策略會更有效率。 - 將非唯一索引和唯一索引的存儲空間調整爲與索引分區列一致:
這樣會將表中的所有索引也一同分區,實現“對齊”。這是一個重要而麻煩的選項,具體需求請參閱MSDN(已分區索引的特殊指導原則)。
這樣的好處是表和索引的分區一致,一方面查詢時利用索引更爲高效,而且在下文提到的移入移出分區也會更爲高效。
注意:這裏建議使用聚集索引列作爲分區列。一方面索引結構本身就應與查詢相關,那麼分區列與索引一致會保證查詢的最大效率;另一方面,保證索引對齊而且是聚集索引對齊是保證分區的移入移出操作順暢的前提,否則可能會出現無法移入移出的情況,而分區的移入移出又是管理大數據的重要策略——滑動窗口(SlideWindow)策略的基礎操作。另外,如果要進行索引對齊,需要所有索引和表的壓縮模式一致。
分區函數與分區方案
選好分區列後,如果沒有應用“分配到可用分區表”選項,接下來則會進入選擇/創建分區函數以及分區方案的界面。其中分區函數會指定分區邊界,而分區方案則規劃了每個分區所存儲的文件組。
嚮導操作界面如下:
其中Left boundary說明每個分區的邊界值被包含在邊界值左側的分區中,也就是每個分區內的數據約束是<=指定的邊界值,相應的,Right boundary則說明每個分區的邊界值被包含在邊界值右側的分區中,每個分區內的數據約束是<指定的邊界值。
在下方的列表中,列出了當前分區方案下現有的分區。其中文件組(Filegroup)指定了每個分區存放的位置,如果將分區放置於位於不同磁盤中的不同文件組中,由於不同磁盤的讀寫互不干擾,這將提高分區表並行處理的效率。一般情況下,將所有分區放置在同一個文件組是比較穩妥的做法。關於文件組的展開閱讀可以參閱:SQL Server Filegroups。
注意,在這裏最後一個分區是沒有指定邊界的,用於保存所有>(Left Boundary)或>=(Right boundary)最後一個分區邊界的數據。
如果選擇時間類型的字段作爲分區列,可以通過Set按鈕實現按條件分組:
這樣可以很方便得通過設置起止時間將表按照指定時間段自動分區,但之後依然需要手動指定每個分區的文件組。
制定好分區方案之後可以通過Estimate sotrage預估每個分區的行數、空間佔用情況,不過除非需要以佔用空間或行數來規劃你的分區策略,一般不建議在這裏進行預估,因爲如果對空表來說,預估的結果當然都是0,而如果表中已經包含大量數據,預估則會花費比較長的時間。
創建分區
通過以上設置,分區已經基本完畢,在嚮導的最後,可以選擇是創建腳本還是立即執行分區操作。
我們可以查看在不同情況下創建分區的腳本的情況:
1.在表沒有索引的情況下:
BEGIN TRANSACTION CREATE PARTITION FUNCTION [TestFunction](datetime) AS RANGE LEFT FOR VALUES (N'2010-01-01T00:00:00', N'2010-02-01T00:00:00', N'2010-03-01T00:00:00', N'2010-04-01T00:00:00', N'2010-05-01T00:00:00', N'2010-06-01T00:00:00') CREATE PARTITION SCHEME [TestScheme] AS PARTITION [TestFunction] TO ([PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY]) CREATE CLUSTERED INDEX [ClusteredIndex_on_TestScheme_634025264502439124] ON [dbo].[Account] ( [birthday] )WITH (SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF) ON [TestScheme]([birthday]) DROP INDEX [ClusteredIndex_on_TestScheme_634025264502439124] ON [dbo].[Account] WITH ( ONLINE = OFF ) COMMIT TRANSACTION
這裏先創建Partition Function以及Partition Scheme,之後在分區列上創建聚集索引並按照分區方案分區,最後刪除了這一索引。</>
2.在表有索引的情況下:
如果原先沒有聚集索引:
CREATE CLUSTERED INDEX [ClusteredIndex_on_TestScheme_634025229911990663] ON [dbo].[Account] ( [birthday] )WITH (SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF) ON [TestScheme]([birthday]) DROP INDEX [ClusteredIndex_on_TestScheme_634025229911990663] ON [dbo].[Account] WITH ( ONLINE = OFF )
這和沒有索引的情況一樣,如果表原先存在聚集索引,則腳本變爲:
CREATE CLUSTERED INDEX [IX_id] ON [dbo].[Account] ( [id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = ON, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [TestScheme]([birthday])
可以看到原有的聚集索引(IX_id)在分區方案上被重建了。
如果選擇了“對齊索引”選項,則會對所有索引都應用分區:
CREATE CLUSTERED INDEX [IX_id] ON [dbo].[Account] ( [id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = ON, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [TestScheme]([birthday]) CREATE NONCLUSTERED INDEX [UIX_birthday] ON [dbo].[Account] ( [birthday] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = ON, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [TestScheme]([birthday]) CREATE NONCLUSTERED INDEX [UIX_name] ON [dbo].[Account] ( [name] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = ON, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
這裏不僅對聚集索引IX_id進行了分區,也對非聚集索引UIX_name和UIX_birthday進行了分區。
注意事項
- 對一張表分好區後不可以進行再次分區,同時也沒有直接取消表分區的方法。
- 如果要查看已分區表的分區狀態以及每個分區中的行數和佔用空間,可以通過Storage-》Management Compression查看。同時可以在這裏爲每個分區指定壓縮方式。
- 如果分區表索引沒有對齊,則不可以對該表進行切入切出(Switch in/out)操作,同樣也不能執行滑動窗口操作。
- 分區實際上是在每個分區表都添加了約束,相應的插入操作的性能也會受到影響。
- 即使進行了分區,如果查詢的條件字段和分區列並沒有關聯,性能也未必會得到提升。
附:對分區並行查詢的說明
由於我在實際操作中主要考慮並行查詢方面的效率,所以文章裏只是略略帶過,但評論中有人提到,所以摘錄整理一些資料在下面:
- 並行查詢肯定需要多核支持,單核下並行是不可能的。
- 在2005中,如果有兩個以上的Partition,一個線程對應一個Partition,所以如果有10個線程,卻只有3個分區的話,就會有7個線程被浪費。
- 在2008中,這一問題被改進,所有的線程都被投入到所有的Partition中。具體可以參看
- http://sqlblog.com/blogs/erin_welker/archive/2008/02/10/partitioning-enhancements-in-sql-server-2008.aspx
附作者拖鞋不脫和回覆者的互動
如果我沒記錯的話,就算是分區,並且不同文件組放在不同的硬盤上面,也是順序讀取,分區並不能提高效率,只不過是提高了io而已
所謂並行查詢只是我們一廂情願的一種想法而已