第一部分 棉花數據庫問題和分析
1.問題sql
數據庫的版本是9i,問題sql有兩個:
Sql1:
|
SELECT c_lotno FROM b_ctn_normal WHERE d_prodatetime BETWEEN to_date('2011-07-01', 'yyyy-mm-dd HH24:MI:SS') AND to_date('2012-07-03', 'yyyy-mm-dd HH24:MI:SS') AND n_madein = 65 AND rownum < 31 |
Sql2:
|
SELECT count(c_bale) FROM b_ctn_normal WHERE d_prodatetime BETWEEN to_date('2011-07-01', 'yyyy-mm-dd HH24:MI:SS') AND to_date('2012-07-03', 'yyyy-mm-dd HH24:MI:SS') |
這倆sql其實非常簡單,就是一個按照時間的分頁查詢,一個查詢時間范圍內的總數據量。
但是這個表的數據量很大,41803656條數據,單表容量超過21G。因此查詢非常慢,僅僅查詢30條數據就需要耗費十幾分鍾。甚至查不出結果。
2 表概況
表b_ctn_normal是一個分區表,按照D_VERIFYDATETIME進行了range分區,分區的策略為2010年前每年一個分區,2010年后每月一個分區.該表的數據量為41803656條。
|
partition by range (D_VERIFYDATETIME) partition PART_20080101 values less than (TO_DATE(' 2008-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIAN')) partition PART_20090101 values less than (TO_DATE(' 2009-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIAN'))—后續的省略
|
另外該表上建了大量的索引,見表1:
3.表存在的問題
以下是索引的概況統計信息。
| NDEX_NAME |
DISTINCT_KEYS |
NUM_ROWS |
SAMPLE_SIZE |
| I_CTN_NORMAL_99 |
4 |
41805079.0630631 |
8459008 |
| I_NORMAL_MADEIN1 |
17 |
41804897.3546108 |
9544621 |
| I_CTN_NORMAL_66 |
80 |
41767143.7096454 |
9580744 |
| I_CTN_NORMAL_77 |
86 |
41473125.0963043 |
9479665 |
| I_CTN_NORMAL_22 |
366 |
41875654.4438373 |
9937607 |
| I_CTN_NORMAL_11 |
889 |
41867424.9314059 |
11007070 |
| I_CTN_NORMAL_55 |
1957 |
40169648.695544 |
9058949 |
| I_NORMAL_UPLOADTIME1 |
17253 |
41866396.7193889 |
10087608 |
| I_CTN_NORMAL_33 |
384472 |
41842227.8696896 |
10621842 |
| I_NORMAL_D_COLORGRADETIME |
1485863 |
41727490.2734861 |
9119757 |
| GLOBAL_INDEX_D_VERIFYDATETIME |
21162573 |
41804256 |
9592128 |
| PRIMARY1_ID |
41804473 |
41804473 |
9540784 |
| UNI_NORMAL_C_BALE1 |
41841809 |
41841809.1781587 |
7023237 |
【表1】索引概況
l 表不是很寬,但是竟然建了13個索引,而且8個索引可選性很差,每個索引都占據不少段空間,極大的浪費了存儲空間。
l 索引沒有分區,千萬級別的數據量,本身查找索引就很耗時,因此應當對索引分區其高索引檢索性能。
l 表的索引和表應該建立在不同的表空間分開存放,同時表的分區在不同的表空間存放。
l 分區的記錄不均勻,分區不合理
分區的統計信息顯示,大量的數據集中在了PART_20100101和PART_20090101分區,分區很不合理,大大削弱了分區表的作用。應該對分區進行細粒度的划分,均勻分布數據。
| TABLE_NAME |
PARTITION_NAME |
NUM_ROWS |
SAMPLE_SIZE |
| B_CTN_NORMAL |
PART_20100101 |
15580400 |
2883811 |
| B_CTN_NORMAL |
PART_20090101 |
13007483 |
2420607 |
| B_CTN_NORMAL |
PART_20101201 |
3809673 |
709735 |
| B_CTN_NORMAL |
PART_20110101 |
2656138 |
494675 |
| B_CTN_NORMAL |
PART_20101101 |
2641196 |
492471 |
| B_CTN_NORMAL |
PART_20110201 |
1169697 |
217919 |
| B_CTN_NORMAL |
PART_20100201 |
1106187 |
205854 |
| B_CTN_NORMAL |
PART_20110401 |
662618 |
123426 |
| B_CTN_NORMAL |
PART_20110301 |
271190 |
50600 |
| B_CTN_NORMAL |
PART_20100501 |
205173 |
37933 |
| B_CTN_NORMAL |
PART_20100401 |
194223 |
35804 |
| B_CTN_NORMAL |
PART_20100601 |
154195 |
28641 |
| B_CTN_NORMAL |
PART_20110501 |
137085 |
25587 |
| B_CTN_NORMAL |
PART_20100301 |
105747 |
19575 |
| B_CTN_NORMAL |
PART_20101001 |
64424 |
11960 |
| B_CTN_NORMAL |
PART_20100701 |
33743 |
6206 |
| B_CTN_NORMAL |
PART_20100801 |
4044 |
725 |
| B_CTN_NORMAL |
PART_20100901 |
283 |
283 |
| B_CTN_NORMAL |
PART_20111001 |
80 |
80 |
| B_CTN_NORMAL |
PART_20080101 |
53 |
53 |
| B_CTN_NORMAL |
PART_20111201 |
21 |
21 |
| B_CTN_NORMAL |
PART_20120601 |
2 |
2 |
| B_CTN_NORMAL |
PART_20120301 |
1 |
1 |
| B_CTN_NORMAL |
PART_20110601 |
0 |
|
| B_CTN_NORMAL |
PART_20110701 |
0 |
|
| B_CTN_NORMAL |
PART_20120401 |
0 |
|
| B_CTN_NORMAL |
PART_20120801 |
0 |
|
| B_CTN_NORMAL |
PART_20120701 |
0 |
|
| B_CTN_NORMAL |
PART_20120501 |
0 |
|
| B_CTN_NORMAL |
PART_20120201 |
0 |
|
| B_CTN_NORMAL |
PART_20120101 |
0 |
|
| B_CTN_NORMAL |
PART_20111101 |
0 |
|
| B_CTN_NORMAL |
PART_20110801 |
0 |
|
| B_CTN_NORMAL |
PART_20110901 |
0 |
|
注:以上的統計信息都是最新收集的.
4.分析制定策略
結合問題sql發現, 兩個查詢共同依賴於d_prodatetime字段的過濾,但是該字段重復值很多,根據統計信息,該列的NUM_DISTINCT為456,因此只依靠索引沒有意義,CBO不會選擇索引而是全表掃描。執行這兩個查詢的時候對數據沒有區分度,選擇了4K萬數據進行全表掃描,效率可寫而至。
而該查詢較為簡單,經過仔細的分析並研究目前的分區策略,我認為最佳的策略是增加范圍分區字段,將表重新分區,分區條件納入d_prodatetime字段。這樣查詢時可以以d_prodatetime進行分區裁減從而減少掃描的數據量。需要分析字段的值的分布區間,平均分配到各分區。經過分析表的數據分布,從節省時間上考慮,就以每兩個月為一個范圍進行分區。
|
partition by range (D_VERIFYDATETIME, d_prodatetime) partition PART_20080101 values less than (TO_DATE(' 2008-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIAN'), TO_DATE(' 2008-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIAN')) partition PART_20090101 values less than (TO_DATE(' 2009-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIAN'), TO_DATE(' 2009-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIAN'))
|
考慮到sql1中有對n_madein的過濾,分析該字段,字段值總共為17種,為number類型,而且該字段值在字段D_VERIFYDATETIME, d_prodatetime表示的區間中分布很均勻,因此,在上述分區基礎上增加list分區,使分區策略變為Rang-list復合分區:
|
partition by range (D_VERIFYDATETIME, d_prodatetime) subpartition by list(N_MADEIN) partition PART_20080101 values less than (range1,range2) ( subpartition p31 values(64), subpartition p32 values(37), subpartition p33 values(48), subpartition p34 values(55), …………… ), partition PART_20090101 values less than (range3,range4) ( subpartition p31 values(64), subpartition p32 values(37), subpartition p33 values(48), subpartition p34 values(55), ………….. ), )
|
這樣,sql1在執行時,會首先根據d_prodatetime裁減掉部分數據,然后再根據N_MADEIN再次裁減掉一部分,這樣sql1的性能應該會得到很大提升。而對於僅僅含有N_MAADIN的過濾條件的查詢,都會進行分區裁減,減少數據量。具體性能提高多少,需要測試。
同時,之前的分區字段D_VERIFYDATETIME的粒度應該適當的減小。因為d_prodatetime的重復值較多,以之前的分區粒度,2010年前的是每年一個分區,這樣會導致d_prodatetime分區后數據會很不均勻,若查詢2010年之前的數據,則d_prodatetime裁減的效果會不好,因此需要考慮d_prodatetime的字段值,重新規划分區粒度。分區粒度的大小需要考慮到d_prodatetime的范圍分布情況。通過分析決定和d_prodatetime字段使用同樣的分區范圍。
由於需要對表重新分區,因此需要重建表。如果在已有的分區策略下增加分區,則直接alter表即可,oracle提供了豐富的方法為不同的分區增加新的分區;但是修改分區策略,必須重建表。而表數據量巨大,單表超過20G,因此數據的加載成為頭疼的問題,如果在生產環境,產生的日志也很巨大。
第二部分 試驗過程結果及分析
為了能試驗本文預測的效果,於是我在我本機騰出了30G的空間,將庫置於非歸檔模式,然后用database link以insert append的方式直接加載數據。通過仔細的權衡,我創建的分區表如下(限於時間關系,將所有表分區和索引分區建在一個表空間內):
--ddl過長,省略(見附件)
(注:復合分區可以為二級分區創建一個template,從而減少建表DDL的篇幅)
考慮只有本文開頭的兩個查詢問題突出,因此我只建立了倆索引,選擇了全局范圍分區索引。
|
--創建分區索引GLOBAL_INDEX_D_VERIFYDATETIME CREATE INDEX GLOBAL_INDEX_D_VERIFYDATETIME ON b_ctn_normal(D_VERIFYDATETIME) GLOBAL PARTITION BY RANGE(D_VERIFYDATETIME)( partition part_index_0 values less than(to_date('2008-03-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_1 values less than(to_date('2008-05-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_2 values less than(to_date('2008-07-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_3 values less than(to_date('2008-09-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_4 values less than(to_date('2008-11-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_5 values less than(to_date('2009-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_6 values less than(to_date('2009-03-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_7 values less than(to_date('2009-05-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_8 values less than(to_date('2009-07-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_9 values less than(to_date('2009-09-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_10 values less than(to_date('2009-11-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_11 values less than(to_date('2010-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_12 values less than(to_date('2010-03-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_13 values less than(to_date('2010-05-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_14 values less than(to_date('2010-07-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_15 values less than(to_date('2010-09-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_16 values less than(to_date('2010-11-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_17 values less than(to_date('2011-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_18 values less than(to_date('2011-03-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_19 values less than(to_date('2011-05-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_20 values less than(to_date('2011-07-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_21 values less than(to_date('2011-09-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_22 values less than(to_date('2011-11-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_23 values less than(to_date('2012-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_24 values less than(to_date('2012-03-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_25 values less than(to_date('2012-05-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_26 values less than(to_date('2012-07-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_27 values less than(to_date('2012-09-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_28 values less than(to_date('2012-11-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index_29 values less than(maxvalue) ) |
注:分區索引也可以使用本地前綴索引,可以減少DDL篇幅
考慮到D_PRODATETIME的值較多,當查詢來臨時,oracle首先以查詢where條件中的D_PRODATETIME進行裁減,到目標分區之后,如果有索引的話,應該可以進行INDEX RANGE SCAN進行掃描,因此先建立分區索引試試。同樣選擇了全局范圍分區索引。
|
--創建分區索引GLOBAL_INDEX_D_PRODATETIME CREATE INDEX GLOBAL_INDEX_D_PRODATETIME ON b_ctn_normal(D_PRODATETIME) GLOBAL PARTITION BY RANGE(D_PRODATETIME)( partition part_index1_0 values less than(to_date('2008-03-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_1 values less than(to_date('2008-05-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_2 values less than(to_date('2008-07-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_3 values less than(to_date('2008-09-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_4 values less than(to_date('2008-11-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_5 values less than(to_date('2009-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_6 values less than(to_date('2009-03-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_7 values less than(to_date('2009-05-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_8 values less than(to_date('2009-07-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_9 values less than(to_date('2009-09-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_10 values less than(to_date('2009-11-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_11 values less than(to_date('2010-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_12 values less than(to_date('2010-03-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_13 values less than(to_date('2010-05-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_14 values less than(to_date('2010-07-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_15 values less than(to_date('2010-09-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_16 values less than(to_date('2010-11-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_17 values less than(to_date('2011-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_18 values less than(to_date('2011-03-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_19 values less than(to_date('2011-05-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_20 values less than(to_date('2011-07-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_21 values less than(to_date('2011-09-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_22 values less than(to_date('2011-11-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_23 values less than(to_date('2012-01-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_24 values less than(to_date('2012-03-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_25 values less than(to_date('2012-05-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_26 values less than(to_date('2012-07-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_27 values less than(to_date('2012-09-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_28 values less than(to_date('2012-11-01 00:00:00', 'SYYYY-MM-DD HH24:MI:SS')), partition part_index1_29 values less than(maxvalue) )) |
下面是加載數據以及實施步驟:
l 表數據加載
創建db link “ctn”.
Insert /*+append*/ into b_ctn_normal select * from b_ctn_normal@ctn;
加載速度還可以,大概30分鍾加載完,加載數據量4400W。
l 分別按照上述的策略創建分區索引
GLOBAL_INDEX_D_VERIFYDATETIME和GLOBAL_INDEX_D_PRODATETIME
索引創建約為30分鍾
l 執行sql1和sql2和原始庫進行對比
Sql1的測試結果對比:
|
|
返回行數 |
Pl/sql查詢 |
Sqlplus跟蹤 |
| 優化前 |
30 |
>15分鍾 |
00: 04: 31.62 |
| 優化后 |
30 |
<0.4秒 |
00: 00: 00.03 |
Sql2的測試結果對比:
|
|
返回行數 |
Pl/sql查詢 |
Sqlplus跟蹤 |
| 優化前 |
5529 |
? |
00: 05: 42.67 |
| 優化后 |
5529 |
<0.02秒 |
00: 00: 00.01 |
通過以上對比結果,顯示新的分區策略帶來了巨大的性能提升,顯示了oracle分區技術的強大威力。原來十幾分鍾甚至返回不了結果的查詢現在毫秒就返回數據。
下面分析優化后的執行計划:
Sql1執行計划:
通過執行計划可以看出,查詢正確的使用了d_prodatetime字段進行了分區裁減,然后使用到了該列的分區索引,但是並沒有使用n_madein進行裁減。於是改變了下查詢條件,將查詢的數據量增大:
|
SELECT c_lotno FROM b_ctn_normal WHERE d_prodatetime BETWEEN to_date('2011-07-01', 'yyyy-mm-dd HH24:MI:SS') AND to_date('2012-07-03', 'yyyy-mm-dd HH24:MI:SS') AND n_madein = 65 AND rownum < 5000 |
這次,查詢使用了二級分區裁減。先是對一級分區進行裁減,然后又對二級分區進行裁減,最后對二級分區使用N_MADEIN進行全表掃描。執行計划顯示,查詢5000條數據時耗時增加了很多,因為掃描的數據量實在太大了,查詢需要掃描很多分區。這樣只能通過減少一次查詢的數據量來保證性能。通過和開發人員確認,一次查詢一般不用返回這么多數據。
Sql2執行計划:
執行計划已經顯示的很明確,一級分區按照新分區的字段進行裁減,然后使用建立的分區索引,性能很高。
雖然新的分區策略顯示了巨大的性能提升,有效的解決了性能問題,但是仔細分析一下,仍然存在一些問題:
u 分區較多,在4K萬級別的表上,分區多達493個,這有些過分了。需要減少分區數量。目前的分區是每倆月一個分區,目前的數據分布比重新分區前均勻了很多,但是仍然存在不均勻現象,而且每倆月一個分區仍然較多。因此需要維持現在的范圍分區字段不變,將現在的倆月一個分區的條件變化一下,分析數據的分布區間,制定一個不均勻的分區條件。如2010年8月的數據很多,那可以分別以2010-08-01~2010-08-15~2010-08-30為區間划分。如果2010年9-12月數據很少,那么可以將9-12月合並為一個分區。盡可能的均勻划分分區記錄數,也減少分區數量。
u 評估二級分區的必要性。總的分區數是1級分區和二級分區的乘積,為M*N的關系。二級分區的增加,大大增加了分區數。分析發現,有接近一半的二級分區是空閑的,並沒有記錄裝入,浪費了大量的空間。而且目前的sql並沒有使用到二級分區裁減,因此需要評估二級分區帶來的性能提高。然后考慮是否將二級分區去掉只采用范圍分區。去掉二級分區,目前對性能是沒有壞處的,而且未來如果用到對N_MADEIN字段的裁剪,直接alter表即可增加二級分區,不用重建。因此建議去掉。
l 總結
分區是處理大表的首要應對策略,但是分區字段的選取和分區的方法需要仔細權衡,一般第一想到的分區字段都是合理的,但是一些隱含的字段沒有考慮到,未來數據量上去了,這些隱含的條件造成的性能問題就暴露出來了,因此還是需要全面的分析。
對表進行了分區,相應的也要對索引進行分區,這樣可以裁減掉部分索引,然后裁減掉記錄,雖然是海量數據,但是卻擁有極高的查詢速度。記得在一本書上看過,作者說,正是因為有了分區技術,oracle才敢號稱是海量數據庫。
Reference:
【http://docs.oracle.com/cd/B19306_01/server.102/b14231/partiti.htm#i1009216】
