2011年6月18日

          [轉]怎樣看懂Oracle的執行計劃

          盡量用鳥語描述了,翻譯成中文反而容易誤解。


          一、什么是執行計劃

          An explain plan is a representation of the access path that is taken when a query is executed within Oracle.


          二、如何訪問數據

          At the physical level Oracle reads blocks of data. The smallest amount of data read is a single Oracle block, the largest is constrained by operating system limits (and multiblock i/o). Logically Oracle finds the data to read by using the following methods:
          Full Table Scan (FTS)    --全表掃描
          Index Lookup (unique & non-unique)    --索引掃描(唯一和非唯一)
          Rowid    --物理行id


          三、執行計劃層次關系

          When looking at a plan, the rightmost (ie most inndented) uppermost operation is the first thing that is executed. --采用最右最上最先執行的原則看層次關系,在同一級如果某個動作沒有子ID就最先執行

          1.看一個簡單的例子

          Query Plan
          -----------------------------------------
          SELECT STATEMENT [CHOOSE] Cost=1234
          **TABLE ACCESS FULL LARGE [:Q65001] [ANALYZED] --[:Q65001]表示是并行方式,[ANALYZED]表示該對象已經分析過了

          優化模式是CHOOSE的情況下,看Cost參數是否有值來決定采用CBO還是RBO:
          SELECT STATEMENT [CHOOSE] Cost=1234
           --Cost有值,采用CBO
          SELECT STATEMENT [CHOOSE] Cost= --Cost為空,采用RBO

          2.層次的父子關系,看比較復雜的例子:

          PARENT1

          **FIRST CHILD
          ****FIRST GRANDCHILD
          **SECOND CHILD

          Here the same principles apply, the FIRST GRANDCHILD is the initial operation then the FIRST CHILD followed by the SECOND CHILD and finally the PARENT collates the output. 


          四、例子解說

          Execution Plan
          ----------------------------------------------------------
          0 
          **SELECT STATEMENT Optimizer=CHOOSE (Cost=3 Card=8 Bytes=248)
          1 0 
          **HASH JOIN (Cost=3 Card=8 Bytes=248)
          2 1 
          ****TABLE ACCESS (FULL) OF 'DEPT' (Cost=1 Card=3 Bytes=36)
          3 1 
          ****TABLE ACCESS (FULL) OF 'EMP' (Cost=1 Card=16 Bytes=304) 

          左側的兩排數據,前面的是序列號ID,后面的是對應的PID(父ID)。

          A shortened summary of this is:
          Execution starts with ID=0: SELECT STATEMENT but this is dependand on it's child objects
          So it executes its first child step: ID=1 PID=0 HASH JOIN but this is dependand on it's child objects
          So it executes its first child step: ID=2 PID=1 TABLE ACCESS (FULL) OF 'DEPT'
          Then the second child step: ID=3 PID=2 TABLE ACCESS (FULL) OF 'EMP'
          Rows are returned to the parent step(s) until finished 


          五、表訪問方式

          1.Full Table Scan (FTS) 全表掃描

          In a FTS operation, the whole table is read up to the high water mark (HWM). The HWM marks the last block in the table that has ever had data written to it. If you have deleted all the rows then you will still read up to the HWM. Truncate resets the HWM back to the start of the table. FTS uses multiblock i/o to read the blocks from disk.   --全表掃描模式下會讀數據到表的高水位線(HWM即表示表曾經擴展的最后一個數據塊),讀取速度依賴于Oracle初始化參數db_block_multiblock_read_count

          Query Plan
          ------------------------------------
          SELECT STATEMENT [CHOOSE] Cost=1
          **INDEX UNIQUE SCAN EMP_I1   --如果索引里就找到了所要的數據,就不會再去訪問表了

          2.Index Lookup 索引掃描

          There are 5 methods of index lookup:

          index unique scan   --索引唯一掃描
          Method for looking up a single key value via a unique index. always returns a single value, You must supply AT LEAST the leading column of the index to access data via the index. 

          eg:
          SQL> explain plan for select empno,ename from emp where empno=10;

          index range scan   --索引局部掃描
          Index range scan is a method for accessing a range values of a particular column. AT LEAST the leading column of the index must be supplied to access data via the index. Can be used for range operations (e.g. > < <> >= <= between) .
          eg:
          SQL> explain plan for select mgr from emp where mgr = 5;

          index full scan   --索引全局掃描
          Full index scans are only available in the CBO as otherwise we are unable to determine whether a full scan would be a good idea or not. We choose an index Full Scan when we have statistics that indicate that it is going to be more efficient than a Full table scan and a sort. For example we may do a Full index scan when we do an unbounded scan of an index and want the data to be ordered in the index order. 
          eg: 

          SQL> explain plan for 
          select empno,ename from big_emp order by empno,ename;

          index fast full scan   --索引快速全局掃描,不帶order by情況下常發生
          Scans all the block in the index, Rows are not returned in sorted order, Introduced in 7.3 and requires V733_PLANS_ENABLED=TRUE and CBO, may be hinted using INDEX_FFS hint, uses multiblock i/o, can be executed in parallel, can be used to access second column of concatenated indexes. This is because we are selecting all of the index. 
          eg: 
          SQL> explain plan for 
          select empno,ename from big_emp;

          index skip scan   --索引跳躍掃描,where條件列是非索引的前導列情況下常發生
          Index skip scan finds rows even if the column is not the leading column of a concatenated index. It skips the first column(s) during the search.
          eg:

          SQL> 
          create index i_emp on emp(empno, ename);
          SQL> select /*+ index_ss(emp i_emp)*/ job from emp where ename='SMITH';

          3.Rowid 物理ID掃描

          This is the quickest access method available.Oracle retrieves the specified block and extracts the rows it is interested in. --Rowid掃描是最快的訪問數據方式


          六、表連接方式

          有三種連接方式:

          1.Sort Merge Join (SMJ)    --由于sort是非常耗資源的,所以這種連接方式要避免

          Rows are produced by Row Source 1 and are then sorted Rows from Row Source 2 are then produced and sorted by the same sort key as Row Source 1. Row Source 1 and 2 are NOT accessed concurrently. 

          SQL> explain plan for
          select /*+ ordered */ e.deptno,d.deptno
          from emp e,dept d
          where e.deptno = d.deptno
          order by e.deptno,d.deptno;

          Query Plan
          -------------------------------------
          SELECT STATEMENT [CHOOSE] Cost=17
          **MERGE JOIN
          ****SORT JOIN
          ******TABLE ACCESS FULL EMP [ANALYZED]
          ****SORT JOIN
          ******TABLE ACCESS FULL DEPT [ANALYZED]

          Sorting is an expensive operation, especially with large tables. Because of this, SMJ is often not a particularly efficient join method.

          2.Nested Loops (NL)    --比較高效的一種連接方式

          Fetches the first batch of rows from row source 1, Then we probe row source 2 once for each row returned from row source 1.
          For nested loops to be efficient it is important that the first row source returns as few rows as possible as this directly controls the number of probes of the second row source. Also it helps if the access method for row source 2 is efficient as this operation is being repeated once for every row returned by row source 1.

          SQL> explain plan for
          select a.dname,b.sql
          from dept a,emp b
          where a.deptno = b.deptno;

          Query Plan
          -------------------------
          SELECT STATEMENT [CHOOSE] Cost=5
          **NESTED LOOPS
          ****TABLE ACCESS FULL DEPT [ANALYZED]
          ****TABLE ACCESS FULL EMP [ANALYZED]

          3.Hash Join    --最為高效的一種連接方式

          New join type introduced in 7.3, More efficient in theory than NL & SMJ, Only accessible via the CBO. Smallest row source is chosen and used to build a hash table and a bitmap The second row source is hashed and checked against the hash table looking for joins. The bitmap is used as a quick lookup to check if rows are in the hash table and are especially useful when the hash table is too large to fit in memory.

          SQL> explain plan for
          select /*+ use_hash(emp) */ empno
          from emp,dept
          where emp.deptno = dept.deptno;

          Query Plan
          ----------------------------
          SELECT STATEMENT [CHOOSE] Cost=3
          **HASH JOIN
          ****TABLE ACCESS FULL DEPT
          ****TABLE ACCESS FULL EMP

          Hash joins are enabled by the parameter HASH_JOIN_ENABLED=TRUE in the init.ora or session. TRUE is the default in 7.3.

          3.Cartesian Product    --卡迪爾積,不算真正的連接方式,sql肯定寫的有問題

          A Cartesian Product is done where they are no join conditions between 2 row sources and there is no alternative method of accessing the data. Not really a join as such as there is no join! Typically this is caused by a coding mistake where a join has been left out.
          It can be useful in some circumstances - Star joins uses cartesian products.Notice that there is no join between the 2 tables:

          SQL> explain plan for
          select emp.deptno,dept,deptno
          from emp,dept

          Query Plan
          ------------------------------
          SLECT STATEMENT [CHOOSE] Cost=5
          **MERGE JOIN CARTESIAN
          ****TABLE ACCESS FULL DEPT
          ****SORT JOIN
          ******TABLE ACCESS FULL EMP

          The CARTESIAN keyword indicate that we are doing a cartesian product. 

          七、運算符

          1.sort    --排序,很消耗資源

          There are a number of different operations that promote sorts:
          order by clauses
          group by
          sort merge join

          2.filter    --過濾,如not in、min函數等容易產生

          Has a number of different meanings, used to indicate partition elimination, may also indicate an actual filter step where one row source is filtering, another, functions such as min may introduce filter steps into query plans.

          3.view    --視圖,大都由內聯視圖產生

          When a view cannot be merged into the main query you will often see a projection view operation. This indicates that the 'view' will be selected from directly as opposed to being broken down into joins on the base tables. A number of constructs make a view non mergeable. Inline views are also non mergeable.
          eg: 
          SQL> explain plan for
          select ename,tot
          from emp,(select empno,sum(empno) tot from big_emp group by empno) tmp
          where emp.empno = tmp.empno;

          Query Plan
          ------------------------
          SELECT STATEMENT [CHOOSE]
          **HASH JOIN
          **TABLE ACCESS FULL EMP [ANALYZED]
          **VIEW
          ****SORT GROUP BY
          ******INDEX FULL SCAN BE_IX 

          4.partition view     --分區視圖

          Partition views are a legacy technology that were superceded by the partitioning option. This section of the article is provided as reference for such legacy systems.

          posted @ 2011-06-18 16:33 ... 閱讀(177) | 評論 (0)編輯 收藏

          [轉]Oracle執行計劃的相關概念

          本文介紹了ORACLE執行計劃的一些基本概念,供學習應用。

          一.相關的概念

          Rowid的概念:rowid是一個偽列,既然是偽列,那么這個列就不是用戶定義,而是系統自己給加上的。 對每個表都有一個rowid的偽列,但是表中并不物理存儲ROWID列的值。不過你可以像使用其它列那樣使用它,但是不能刪除改列,也不能對該列的值進行 修改、插入。一旦一行數據插入數據庫,則rowid在該行的生命周期內是唯一的,即即使該行產生行遷移,行的rowid也不會改變。

          Recursive SQL概念:有時為了執行用戶發出的一個sql語句,Oracle必須執行一些額外的語句,我們將這些額外的語句稱之為'recursive calls'或'recursive SQL statements'。如當一個DDL語句發出后,ORACLE總是隱含的發出一些recursive SQL語句,來修改數據字典信息,以便用戶可以成功的執行該DDL語句。當需要的數據字典信息沒有在共享內存中時,經常會發生Recursive calls,這些Recursive calls會將數據字典信息從硬盤讀入內存中。用戶不比關心這些recursive SQL語句的執行情況,在需要的時候,ORACLE會自動的在內部執行這些語句。當然DML語句與SELECT都可能引起recursive SQL。簡單的說,我們可以將觸發器視為recursive SQL。

          Row Source(行源):用在查詢中,由上一操作返回的符合條件的行的集合,即可以是表的全部行數據的集合;也可以是表的部分行數據的集合;也可以為對上2個row source進行連接操作(如join連接)后得到的行數據集合。

          Predicate(謂詞):一個查詢中的WHERE限制條件

          Driving Table(驅動表):該表又稱為外層表(OUTER TABLE)。這個概念用于嵌套與HASH連接中。如果該row source返回較多的行數據,則對所有的后續操作有負面影響。注意此處雖然翻譯為驅動表,但實際上翻譯為驅動行源(driving row source)更為確切。一般說來,是應用查詢的限制條件后,返回較少行源的表作為驅動表,所以如果一個大表在WHERE條件有有限制條件(如等值限 制),則該大表作為驅動表也是合適的,所以并不是只有較小的表可以作為驅動表,正確說法應該為應用查詢的限制條件后,返回較少行源的表作為驅動表。在執行 計劃中,應該為靠上的那個row source,后面會給出具體說明。在我們后面的描述中,一般將該表稱為連接操作的row source 1。

          Probed Table(被探查表):該表又稱為內層表(INNER TABLE)。在我們從驅動表中得到具體一行的數據后,在該表中尋找符合連接條件的行。所以該表應當為大表(實際上應該為返回較大row source的表)且相應的列上應該有索引。在我們后面的描述中,一般將該表稱為連接操作的row source 2。

          組合索引(concatenated index):由多個列構成的索引,如create index idx_emp on emp(col1, col2, col3, ……),則我們稱idx_emp索引為組合索引。在組合索引中有一個重要的概念:引導列(leading column),在上面的例子中,col1列為引導列。當我們進行查詢時可以使用”where col1 = ? ”,也可以使用”where col1 = ? and col2 = ?”,這樣的限制條件都會使用索引,但是”where col2 = ? ”查詢就不會使用該索引。所以限制條件中包含先導列時,該限制條件才會使用該組合索引。

          可選擇性(selectivity):比較一下列中唯一鍵的數量和表中的行數,就可以判斷該列的可選擇性。 如果該列的”唯一鍵的數量/表中的行數”的比值越接近1,則該列的可選擇性越高,該列就越適合創建索引,同樣索引的可選擇性也越高。在可選擇性高的列上進 行查詢時,返回的數據就較少,比較適合使用索引查詢。

          二.oracle訪問數據的存取方法

          1) 全表掃描(Full Table Scans, FTS)

          為實現全表掃描,Oracle讀取表中所有的行,并檢查每一行是否滿足語句的WHERE限制條件一個多塊讀 操作可以使一次I/O能讀取多塊數據塊(db_block_multiblock_read_count參數設定),而不是只讀取一個數據塊,這極大的減 少了I/O總次數,提高了系統的吞吐量,所以利用多塊讀的方法可以十分高效地實現全表掃描,而且只有在全表掃描的情況下才能使用多塊讀操作。在這種訪問模 式下,每個數據塊只被讀一次。

          使用FTS的前提條件:在較大的表上不建議使用全表掃描,除非取出數據的比較多,超過總量的5% -- 10%,或你想使用并行查詢功能時。

          使用全表掃描的例子:

          ~~~~~~~~~~~~~~~~~~~~~~~~ SQL> explain plan for select * from dual;

          Query Plan

          -----------------------------------------

          SELECT STATEMENT[CHOOSE] Cost=

          TABLE ACCESS FULL DUAL

          2) 通過ROWID的表存取(Table Access by ROWID或rowid lookup)

          行的ROWID指出了該行所在的數據文件、數據塊以及行在該塊中的位置,所以通過ROWID來存取數據可以快速定位到目標數據上,是Oracle存取單行數據的最快方法。

          這種存取方法不會用到多塊讀操作,一次I/O只能讀取一個數據塊。我們會經常在執行計劃中看到該存取方法,如通過索引查詢數據。

          使用ROWID存取的方法: SQL> explain plan for select * from dept where rowid = 'AAAAyGAADAAAAATAAF';

          Query Plan

          ------------------------------------

          SELECT STATEMENT [CHOOSE] Cost=1

          TABLE ACCESS BY ROWID DEPT [ANALYZED]


          3)索引掃描(Index Scan或index lookup)

          我們先通過index查找到數據對應的rowid值(對于非唯一索引可能返回多個rowid值),然后根據rowid直接從表中得到具體的數據,這 種查找方式稱為索引掃描或索引查找(index lookup)。一個rowid唯一的表示一行數據,該行對應的數據塊是通過一次i/o得到的,在此情況下該次i/o只會讀取一個數據庫塊。

          在索引中,除了存儲每個索引的值外,索引還存儲具有此值的行對應的ROWID值。索引掃描可以由2步組成:(1) 掃描索引得到對應的rowid值。 (2) 通過找到的rowid從表中讀出具體的數據。每步都是單獨的一次I/O,但是對于索引,由于經常使用,絕大多數都已經CACHE到內存中,所以第1步的 I/O經常是邏輯I/O,即數據可以從內存中得到。但是對于第2步來說,如果表比較大,則其數據不可能全在內存中,所以其I/O很有可能是物理I/O,這 是一個機械操作,相對邏輯I/O來說,是極其費時間的。所以如果多大表進行索引掃描,取出的數據如果大于總量的5% -- 10%,使用索引掃描會效率下降很多。如下列所示:
          SQL> explain plan for select empno, ename from emp where empno=10;

          Query Plan

          ------------------------------------

          SELECT STATEMENT [CHOOSE] Cost=1

          TABLE ACCESS BY ROWID EMP [ANALYZED]

          INDEX UNIQUE SCAN EMP_I1


          但是如果查詢的數據能全在索引中找到,就可以避免進行第2步操作,避免了不必要的I/O,此時即使通過索引掃描取出的數據比較多,效率還是很高的

          SQL> explain plan for select empno from emp where empno=10;-- 只查詢empno列值

          Query Plan

          ------------------------------------

          SELECT STATEMENT [CHOOSE] Cost=1

          INDEX UNIQUE SCAN EMP_I1

          進一步講,如果sql語句中對索引列進行排序,因為索引已經預先排序好了,所以在執行計劃中不需要再對索引列進行排序
          SQL> explain plan for select empno, ename from emp

          where empno > 7876 order by empno;

          Query Plan

          --------------------------------------------------------------------------------

          SELECT STATEMENT[CHOOSE] Cost=1

          TABLE ACCESS BY ROWID EMP [ANALYZED]

          INDEX RANGE SCAN EMP_I1 [ANALYZED]


          從這個例子中可以看到:因為索引是已經排序了的,所以將按照索引的順序查詢出符合條件的行,因此避免了進一步排序操作。

          根據索引的類型與where限制條件的不同,有4種類型的索引掃描:

          索引唯一掃描(index unique scan)

          索引范圍掃描(index range scan)

          索引全掃描(index full scan)

          索引快速掃描(index fast full scan)

          (1) 索引唯一掃描(index unique scan)

          通過唯一索引查找一個數值經常返回單個ROWID。如果存在UNIQUE 或PRIMARY KEY 約束(它保證了語句只存取單行)的話,Oracle經常實現唯一性掃描。

          使用唯一性約束的例子:

          SQL> explain plan for

          select empno,ename from emp where empno=10;

          Query Plan

          ------------------------------------

          SELECT STATEMENT [CHOOSE] Cost=1

          TABLE ACCESS BY ROWID EMP [ANALYZED]

          INDEX UNIQUE SCAN EMP_I1

          (2) 索引范圍掃描(index range scan)

          使用一個索引存取多行數據,在唯一索引上使用索引范圍掃描的典型情況下是在謂詞(where限制條件)中使用了范圍操作符(如>、<、<>、>=、<=、between)

          使用索引范圍掃描的例子:


          SQL> explain plan for select empno,ename from emp

          where empno > 7876 order by empno;

          Query Plan

          --------------------------------------------------------------------------------

          SELECT STATEMENT[CHOOSE] Cost=1

          TABLE ACCESS BY ROWID EMP [ANALYZED]

          INDEX RANGE SCAN EMP_I1 [ANALYZED]

          在非唯一索引上,謂詞col = 5可能返回多行數據,所以在非唯一索引上都使用索引范圍掃描。

          使用index rang scan的3種情況:

          (a) 在唯一索引列上使用了range操作符(> < <> >= <= between)

          (b) 在組合索引上,只使用部分列進行查詢,導致查詢出多行

          (c) 對非唯一索引列上進行的任何查詢。

          (3) 索引全掃描(index full scan)

          與全表掃描對應,也有相應的全索引掃描。而且此時查詢出的數據都必須從索引中可以直接得到。

          全索引掃描的例子:

          An Index full scan will not perform single block i/o's and so it may prove to be inefficient.

          e.g.

          Index BE_IX is a concatenated index on big_emp (empno, ename)


          SQL> explain plan for select empno, ename from big_emp order by empno,ename;

          Query Plan

          --------------------------------------------------------------------------------

          SELECT STATEMENT[CHOOSE] Cost=26

          INDEX FULL SCAN BE_IX [ANALYZED]


          (4) 索引快速掃描(index fast full scan)

          掃描索引中的所有的數據塊,與 index full scan很類似,但是一個顯著的區別就是它不對查詢出的數據進行排序,即數據不是以排序順序被返回。在這種存取方法中,可以使用多塊讀功能,也可以使用并行讀入,以便獲得最大吞吐量與縮短執行時間。

          索引快速掃描的例子:

          BE_IX索引是一個多列索引: big_emp (empno,ename)

          SQL> explain plan for select empno,ename from big_emp;

          Query Plan

          ------------------------------------------

          SELECT STATEMENT[CHOOSE] Cost=1

          INDEX FAST FULL SCAN BE_IX [ANALYZED]


          只選擇多列索引的第2列:

          SQL> explain plan for select ename from big_emp;

          Query Plan

          ------------------------------------------

          SELECT STATEMENT[CHOOSE] Cost=1

          INDEX FAST FULL SCAN BE_IX [ANALYZED]

          三.表之間的連接

          Join是一種試圖將兩個表結合在一起的謂詞,一次只能連接2個表,表連接也可以被稱為表關聯。在后面的敘 述中,我們將會使用”row source”來代替”表”,因為使用row source更嚴謹一些,并且將參與連接的2個row source分別稱為row source1和row source 2。Join過程的各個步驟經常是串行操作,即使相關的row source可以被并行訪問,即可以并行的讀取做join連接的兩個row source的數據,但是在將表中符合限制條件的數據讀入到內存形成row source后,join的其它步驟一般是串行的。有多種方法可以將2個表連接起來,當然每種方法都有自己的優缺點,每種連接類型只有在特定的條件下才會 發揮出其最大優勢。

          row source(表)之間的連接順序對于查詢的效率有非常大的影響。通過首先存取特定的表,即將該表作為驅動表,這樣可以先應用某些限制條件,從而得到一個 較小的row source,使連接的效率較高,這也就是我們常說的要先執行限制條件的原因。一般是在將表讀入內存時,應用where子句中對該表的限制條件。

          根據2個row source的連接條件的中操作符的不同,可以將連接分為等值連接(如WHERE A.COL3 = B.COL4)、非等值連接(WHERE A.COL3 > B.COL4)、外連接(WHERE A.COL3 = B.COL4(+))。上面的各個連接的連接原理都基本一樣,所以為了簡單期間,下面以等值連接為例進行介紹。

          在后面的介紹中,都已:

          SELECT A.COL1, B.COL2

          FROM A, B

          WHERE A.COL3 = B.COL4;

          為例進行說明,假設A表為Row Soruce1,則其對應的連接操作關聯列為COL 3;B表為Row Soruce2,則其對應的連接操作關聯列為COL 4;

          連接類型:

          目前為止,無論連接操作符如何,典型的連接類型共有3種:

          排序 - - 合并連接(Sort Merge Join (SMJ) )

          嵌套循環(Nested Loops (NL) )

          哈希連接(Hash Join)

          排序 - - 合并連接(Sort Merge Join, SMJ)

          內部連接過程:

          1) 首先生成row source1需要的數據,然后對這些數據按照連接操作關聯列(如A.col3)進行排序。

          2) 隨后生成row source2需要的數據,然后對這些數據按照與sort source1對應的連接操作關聯列(如B.col4)進行排序。

          3) 最后兩邊已排序的行被放在一起執行合并操作,即將2個row source按照連接條件連接起來

          下面是連接步驟的圖形表示:

          MERGE

          /\

          SORTSORT

          ||

          Row Source 1Row Source 2

          如果row source已經在連接關聯列上被排序,則該連接操作就不需要再進行sort操作,這樣可以大大提高這種連接操作的連接速度,因為排序是個極其費資源的操 作,特別是對于較大的表。預先排序的row source包括已經被索引的列(如a.col3或b.col4上有索引)或row source已經在前面的步驟中被排序了。盡管合并兩個row source的過程是串行的,但是可以并行訪問這兩個row source(如并行讀入數據,并行排序).

          SMJ連接的例子:
          SQL> explain plan for

          select /*+ ordered */ e.deptno, d.deptno

          from emp e, dept d

          where e.deptno = d.deptno

          order by e.deptno, d.deptno;


          Query Plan

          -------------------------------------

          SELECT STATEMENT [CHOOSE] Cost=17

          MERGE JOIN

          SORT JOIN

          TABLE ACCESS FULL EMP [ANALYZED]

          SORT JOIN

          TABLE ACCESS FULL DEPT [ANALYZED]


          排序是一個費時、費資源的操作,特別對于大表。基于這個原因,SMJ經常不是一個特別有效的連接方法,但是如果2個row source都已經預先排序,則這種連接方法的效率也是蠻高的。

          嵌套循環(Nested Loops, NL)

          這個連接方法有驅動表(外部表)的概念。其實,該連接過程就是一個2層嵌套循環,所以外層循環的次數越少越好,這也就是我們為什么將小表或返回較小 row source的表作為驅動表(用于外層循環)的理論依據。但是這個理論只是一般指導原則,因為遵循這個理論并不能總保證使語句產生的I/O次數最少。有時 不遵守這個理論依據,反而會獲得更好的效率。如果使用這種方法,決定使用哪個表作為驅動表很重要。有時如果驅動表選擇不正確,將會導致語句的性能很差、很 差。

          內部連接過程:

          Row source1的Row 1 ---------------- Probe ->Row source 2

          Row source1的Row 2 ---------------- Probe ->Row source 2

          Row source1的Row 3 ---------------- Probe ->Row source 2

          …….

          Row source1的Row n ---------------- Probe ->Row source 2

          從內部連接過程來看,需要用row source1中的每一行,去匹配row source2中的所有行,所以此時保持row source1盡可能的小與高效的訪問row source2(一般通過索引實現)是影響這個連接效率的關鍵問題。這只是理論指導原則,目的是使整個連接操作產生最少的物理I/O次數,而且如果遵守這 個原則,一般也會使總的物理I/O數最少。但是如果不遵從這個指導原則,反而能用更少的物理I/O實現連接操作,那盡管違反指導原則吧!因為最少的物理 I/O次數才是我們應該遵從的真正的指導原則,在后面的具體案例分析中就給出這樣的例子。

          在上面的連接過程中,我們稱Row source1為驅動表或外部表。Row Source2被稱為被探查表或內部表。

          在NESTED LOOPS連接中,Oracle讀取row source1中的每一行,然后在row sourc2中檢查是否有匹配的行,所有被匹配的行都被放到結果集中,然后處理row source1中的下一行。這個過程一直繼續,直到row source1中的所有行都被處理。這是從連接操作中可以得到第一個匹配行的最快的方法之一,這種類型的連接可以用在需要快速響應的語句中,以響應速度為 主要目標。

          如果driving row source(外部表)比較小,并且在inner row source(內部表)上有唯一索引,或有高選擇性非唯一索引時,使用這種方法可以得到較好的效率。NESTED LOOPS有其它連接方法沒有的的一個優點是:可以先返回已經連接的行,而不必等待所有的連接操作處理完才返回數據,這可以實現快速的響應時間。

          如果不使用并行操作,最好的驅動表是那些應用了where 限制條件后,可以返回較少行數據的的表,所以大表也可能稱為驅動表,關鍵看限制條件。對于并行查詢,我們經常選擇大表作為驅動表,因為大表可以充分利用并 行功能。當然,有時對查詢使用并行操作并不一定會比查詢不使用并行操作效率高,因為最后可能每個表只有很少的行符合限制條件,而且還要看你的硬件配置是否 可以支持并行(如是否有多個CPU,多個硬盤控制器),所以要具體問題具體對待。

          NL連接的例子:

          SQL> explain plan for

          select a.dname,b.sql

          from dept a,emp b

          where a.deptno = b.deptno;

          Query Plan

          -------------------------

          SELECT STATEMENT [CHOOSE] Cost=5

          NESTED LOOPS

          TABLE ACCESS FULL DEPT [ANALYZED]

          TABLE ACCESS FULL EMP [ANALYZED]


          哈希連接(Hash Join, HJ)

          這種連接是在oracle 7.3以后引入的,從理論上來說比NL與SMJ更高效,而且只用在CBO優化器中。

          較小的row source被用來構建hash table與bitmap,第2個row source被用來被hansed,并與第一個row source生成的hash table進行匹配,以便進行進一步的連接。Bitmap被用來作為一種比較快的查找方法,來檢查在hash table中是否有匹配的行。特別的,當hash table比較大而不能全部容納在內存中時,這種查找方法更為有用。這種連接方法也有NL連接中所謂的驅動表的概念,被構建為hash table與bitmap的表為驅動表,當被構建的hash table與bitmap能被容納在內存中時,這種連接方式的效率極高。

          HASH連接的例子:

          SQL> explain plan for

          select /*+ use_hash(emp) */ empno

          from emp, dept

          where emp.deptno = dept.deptno;

          Query Plan

          ----------------------------

          SELECT STATEMENT[CHOOSE] Cost=3

          HASH JOIN

          TABLE ACCESS FULL DEPT

          TABLE ACCESS FULL EMP


          要使哈希連接有效,需要設置HASH_JOIN_ENABLED=TRUE,缺省情況下該參數為TRUE,另外,不要忘了還要設置 hash_area_size參數,以使哈希連接高效運行,因為哈希連接會在該參數指定大小的內存中運行,過小的參數會使哈希連接的性能比其他連接方式還 要低。

          總結一下,在哪種情況下用哪種連接方法比較好:

          排序 - - 合并連接(Sort Merge Join, SMJ):

          a) 對于非等值連接,這種連接方式的效率是比較高的。

          b) 如果在關聯的列上都有索引,效果更好。

          c) 對于將2個較大的row source做連接,該連接方法比NL連接要好一些。

          d) 但是如果sort merge返回的row source過大,則又會導致使用過多的rowid在表中查詢數據時,數據庫性能下降,因為過多的I/O。

          嵌套循環(Nested Loops, NL):

          a) 如果driving row source(外部表)比較小,并且在inner row source(內部表)上有唯一索引,或有高選擇性非唯一索引時,使用這種方法可以得到較好的效率。

          b) NESTED LOOPS有其它連接方法沒有的的一個優點是:可以先返回已經連接的行,而不必等待所有的連接操作處理完才返回數據,這可以實現快速的響應時間。

          哈希連接(Hash Join, HJ):

          a) 這種方法是在oracle7后來引入的,使用了比較先進的連接理論,一般來說,其效率應該好于其它2種連接,但是這種連接只能用在CBO優化器中,而且需要設置合適的hash_area_size參數,才能取得較好的性能。

          b) 在2個較大的row source之間連接時會取得相對較好的效率,在一個row source較小時則能取得更好的效率。

          c) 只能用于等值連接中

          笛卡兒乘積(Cartesian Product)

          當兩個row source做連接,但是它們之間沒有關聯條件時,就會在兩個row source中做笛卡兒乘積,這通常由編寫代碼疏漏造成(即程序員忘了寫關聯條件)。笛卡爾乘積是一個表的每一行依次與另一個表中的所有行匹配。在特殊情 況下我們可以使用笛卡兒乘積,如在星形連接中,除此之外,我們要盡量使用笛卡兒乘積,否則,自己想結果是什么吧!

          注意在下面的語句中,在2個表之間沒有連接。

          SQL> explain plan for

          select emp.deptno,dept,deptno

          from emp,dept

          Query Plan

          ------------------------

          SLECT STATEMENT [CHOOSE] Cost=5

          MERGE JOIN CARTESIAN

          TABLE ACCESS FULL DEPT

          SORT JOIN

          TABLE ACCESS FULL EMP


          CARTESIAN關鍵字指出了在2個表之間做笛卡爾乘積。假如表emp有n行,dept表有m行,笛卡爾乘積的結果就是得到n * m行結果。

          posted @ 2011-06-18 16:16 ... 閱讀(148) | 評論 (0)編輯 收藏

          [轉]你是否懂得Oracle UNION ALL

          Oracle有很多值得學習的地方,這里我們主要介紹Oracle UNION ALL,包括介紹UNION等方面。通常情況下,用UNION替換WHERE子句中的OR將會起到較好的效果。對索引列使用OR將造成全表掃描。注意,以上規則只針對多個索引列有效。假如有column沒有被索引,查詢效率可能會因為您沒有選擇OR而降低。在下面的例子中,LOC_ID REGION上都建有索引。

          高效:

          1.  SELECT LOC_ID  LOC_DESC REGION FROM LOCATION WHERE LOC_ID = 10 UNION SELECT LOC_ID 
          LOC_DESC 
          REGION FROM LOCATION WHERE REGION = “MELBOURNE” 

          低效:

          1.  SELECT LOC_ID LOC_DESC REGION FROM LOCATION WHERE LOC_ID = 10 OR REGION = “MELBOURNE” 

          IN來替換OR

          這是一條簡單易記的規則,但是實際的執行效果還須檢驗,在Oracle8i下,兩者的執行路徑似乎是相同的:

          低效:

          1.  SELECT…. FROM LOCATION WHERE LOC_ID = 10 OR LOC_ID = 20 OR LOC_ID = 30 

          高效:

          1.  SELECT… FROM LOCATION WHERE LOC_IN IN (10,20,30); 

          避免在索引列上使用IS NULLIS NOT NULL

          避免在索引中使用任何能夠為空的列,Oracle將無法使用該索引。對于單列索引,假如列包含空值,索引中將不存在此記錄。對于復合索引,假如每個列都為空,索引中同樣不存在此記錄。假如至少有一個列不為空,則記錄存在于索引中。舉例:假如唯一性索引建立在表的A列和B列上,并且表中存在一條記錄的 AB值為(123null) Oracle將不接受下一條具備相同AB值(123,null)的記錄(插入)。然而假如任何的索引列都為空,Oracle將認為整個鍵值為空而空不等于空。因此您能夠插入1000 條具備相同鍵值的記錄,當然他們都是空! 因為空值不存在于索引列中,所以WHERE子句中對索引列進行空值比較將使ORACLE停用該索引。

          總是使用索引的第一個列:

          假如索引是建立在多個列上,只有在他的第一個列(leading column)where子句引用時,優化器才會選擇使用該索引。這也是一條簡單而重要的規則,當僅引用索引的第二個列時,優化器使用了全表掃描而忽略了索引。

          Oracle UNION ALL替換UNION ( 假如有可能的話)

          SQL語句需要UNION兩個查詢結果集合時,這兩個結果集合會以Oracle UNION ALL的方式被合并,然后在輸出最終結果前進行排序。假如用 Oracle UNION ALL替代UNION,這樣排序就不是必要了。效率就會因此得到提高。需要注意的是,Oracle UNION ALL將重復輸出兩個結果集合中相同記錄。因此各位還是要從業務需求分析使用Oracle UNION ALL的可行性。 UNION 將對結果集合排序,這個操作會使用到SORT_AREA_SIZE這塊內存。對于這塊內存的優化也是相當重要的。

          posted @ 2011-06-18 16:01 ... 閱讀(496) | 評論 (0)編輯 收藏

          [轉]hibernate抓取策略

          原文:http://jiangxuwen7515.blog.163.com/blog/static/817523502010730481252/?fromdm&fromSearch&isFromSearchEngine=yes

          一、
          hibernate抓取策略(單端代理的批量抓取fetch=select(默認)/join

          測試用例:

          Student student = (Student)session.get(Student.class, 1);
          System.out.println(student.getName());
          System.out.println(student.getClasses().getName());

          1)保持默認,同fetch="select",如:
          <many-to-one name="classes" column="classesid"
          fetch="select"/>

          fetch="select",另外發送一條select語句抓取當前對象關聯實體或集合

          執行結果:2條語句

          Hibernate: select student0_.id as id1_0_, student0_.name as name1_0_, student0_.class_id as class3_1_0_ from student_join student0_ where student0_.id=?
          學生1
          Hibernate: select classes0_.id as id0_0_, classes0_.name as name0_0_
          from classes_join classes0_ where classes0_.id=?
          高一(1)

          ======================================

          2)設置fetch="join",如:
          <many-to-one name="classes" column="classesid" fetch="join"/>

          fetch="join",hibernate會通過select語句使用外連接來加載其關聯實體或集合

          此時lazy會失效

          執行結果:一條join語句

          Hibernate: select student0_.id as id1_1_, student0_.name as name1_1_, student0_.class_id as class3_1_1_, classes1_.id as id0_0_, classes1_.name as name0_0_ from student_join student0_ left outer join classes_join classes1_ on student0_.class_id=classes1_.id where student0_.id=?
          學生1
          高一(1)

          ======================================================

          二、hibernate抓取策略(集合代理的批量抓取,fetch=select(默認)/join/subselect

          測試用例:

          Classes c = (Classes) session.load(Classes.class, new Integer(1));
              System.out.println("Class.name=" + c.getName());
              Set stuSet = c.getStudents();
              System.out.println(stuSet.size());
              if(stuSet != null && !stuSet.isEmpty()){
               for(Iterator it = stuSet.iterator(); it.hasNext();){
                Student s = (Student) it.next();
                System.out.println("student.name=" + s.getName());
               }
              }

          1)保持默認,同fetch="select",如:
          <set name="students" inverse="true"
          fetch="select">

          fetch="select",另外發送一條select語句抓取當前對象關聯實體或集合

          測試結果:2條獨立的查詢語句

          Hibernate: select classes0_.id as id0_0_, classes0_.name as name0_0_ from classes_join classes0_ where classes0_.id=?
          Class.name=高一(1)
          Hibernate: select students0_.class_id as class3_1_, students0_.id as id1_, students0_.id as id1_0_, students0_.name as name1_0_, students0_.class_id as class3_1_0_
          from student_join students0_ where students0_.class_id=?
          9
          student.name=
          學生7
          student.name=
          學生3
          student.name=
          學生1
          student.name=
          學生8
          student.name=
          學生2
          student.name=
          學生4
          student.name=
          學生5
          student.name=
          學生9
          student.name=
          學生6

          (2)設置fetch="join",如:
          <set name="students" inverse="true"
          fetch="join">

          fetch="join",hibernate會通過select語句使用外連接來加載其關聯實體或集合

          此時lazy會失效

          測試結果:1條獨立的join查詢語句

          Hibernate: select classes0_.id as id0_1_, classes0_.name as name0_1_, students1_.class_id as class3_3_, students1_.id as id3_, students1_.id as id1_0_, students1_.name as name1_0_, students1_.class_id as class3_1_0_ from classes_join classes0_ left outer join student_join students1_ on classes0_.id=students1_.class_id where classes0_.id=?
          Class.name=
          高一(1)
          9
          student.name=
          學生6
          student.name=
          學生4
          student.name=
          學生9
          student.name=
          學生7
          student.name=
          學生2
          student.name=
          學生3
          student.name=
          學生8
          student.name=
          學生1
          student.name=
          學生5

          (3)設置fetch="subselect",如:用在查詢語句
          <set name="students" inverse="true"
          fetch="subselect">

          fetch="subselect",另外發送一條select語句抓取在前面查詢到的所有實體對象的關聯集合

          測試用例:

          List classList = session.createQuery("from Classes where id in (1,2,3)").list();
              for(Iterator iter = classList.iterator(); iter.hasNext();){
               Classes c = (Classes)iter.next();
               System.out.println("Class.name=" + c.getName());
               Set stuSet = c.getStudents();
               System.out.println(stuSet.size());
               if(stuSet != null && !stuSet.isEmpty()){
                for(Iterator it = stuSet.iterator(); it.hasNext();){
                 Student s = (Student) it.next();
                 System.out.println("student.name=" + s.getName());
                }
               }
              }   

          當不設fetch="subselect" ,即:<set name="students" inverse="true">,結果如下:

          執行了3條查詢語句

          Hibernate: select classes0_.id as id0_, classes0_.name as name0_ from classes_join classes0_ where classes0_.id in (1 , 2 , 3)
          Class.name=
          高一(1)
          Hibernate:
          select students0_.class_id as class3_1_, students0_.id as id1_, students0_.id as id1_0_, students0_.name as name1_0_, students0_.class_id as class3_1_0_ from student_join students0_ where students0_.class_id=?
          9
          student.name=
          學生8
          student.name=
          學生5
          student.name=
          學生3
          student.name=
          學生9
          student.name=
          學生7
          student.name=
          學生1
          student.name=
          學生4
          student.name=
          學生6
          student.name=
          學生2
          Class.name=
          高一(2)
          Hibernate:
          select students0_.class_id as class3_1_, students0_.id as id1_, students0_.id as id1_0_, students0_.name as name1_0_, students0_.class_id as class3_1_0_ from student_join students0_ where students0_.class_id=?
          4
          student.name=
          學生3
          student.name=
          學生4
          student.name=
          學生1
          student.name=
          學生2
          Class.name=
          高一(3)
          Hibernate:
          select students0_.class_id as class3_1_, students0_.id as id1_, students0_.id as id1_0_, students0_.name as name1_0_, students0_.class_id as class3_1_0_ from student_join students0_ where students0_.class_id=?
          0

          當不設fetch="subselect" ,即:<set name="students" inverse="true" fetch="subselect">,結果如下:

          執行了1條查詢語句(嵌套子查詢)

          Hibernate: select classes0_.id as id0_, classes0_.name as name0_ from classes_join classes0_ where classes0_.id in (1 , 2 , 3)
          Class.name=
          高一(1)
          Hibernate:
          select students0_.class_id as class3_1_, students0_.id as id1_, students0_.id as id1_0_, students0_.name as name1_0_, students0_.class_id as class3_1_0_ from student_join students0_ where students0_.class_id in (select classes0_.id from classes_join classes0_ where classes0_.id in (1 , 2 , 3))
          9
          student.name=
          學生8
          student.name=
          學生4
          student.name=
          學生5
          student.name=
          學生9
          student.name=
          學生6
          student.name=
          學生2
          student.name=
          學生3
          student.name=
          學生1
          student.name=
          學生7
          Class.name=
          高一(2)
          4
          student.name=
          學生3
          student.name=
          學生4
          student.name=
          學生2
          student.name=
          學生1
          Class.name=
          高一(3)
          0

          posted @ 2011-06-18 15:33 ... 閱讀(148) | 評論 (0)編輯 收藏

          [轉]Hibernate性能優化要點

          Robbin總結的Hibernate性能優化要點:

          1.盡量使用many-to-one,避免使用單項one-to-many
          2.靈活使用單向one-to-many
          3.不用一對一,使用多對一代替一對一
          4.配置對象緩存,不使用集合緩存
          5.一對多使用Bag 多對一使用Set
          6.繼承使用顯示多態  HQL:from object  polymorphism="exlicit" 避免查處所有對象
          7.消除大表,使用二級緩存
              對于上面這些,Robbin進行了詳細的講解。
          one-to-many
               使用inverse=false(default),對象的關聯關系是由parent對象來維護的
               而inverse=true的情況下,一般用戶雙向多對多關聯,由子對象維護關聯關系,增加子對象的時候需要顯示:child.setParent(child)
               為了提高性能,應該盡量使用雙向one-to-many inverse=true,在MVC結構中的DAO接口中應該直接用Session持久化對象,避免通過關聯關系(這句話有點不理解),而在單項關系中正確使用二級緩存,則可以大幅提高以查詢為主的應用。
               多對一性能問題比較少,但是要避免經典N+1問題。
               通過主鍵進行關聯,相當于大表拆分小表。(這個是區分面向對象設計和面向過程設計的一個關鍵點)
          list、bag、set的正確運用
               one-to-many:
               A、使用list 需要維護Index Column字段,不能被用于雙向關聯,而且必須使用inverse=false,需要謹慎使用在某些稀有場合(基本上是不予考慮使用)
               B、bag/set在one-to-many中語義基本相同,推薦使用bag
               many-to-one:
               A、bag和set不同,bag允許重復插入,建議使用set
          在龐大的集合分頁中應該使用session.createFilter
              session.createFilter(parent.getChildren(),""),setFirstResult(0),setMaxResult(10))
          避免N+1 參考(http://www.iteye.com/post/266972)
              在多對一的情況下,查詢child對象,當在頁面上顯示每個子類的父類對象的時候會導致N+1次查詢,需要采用下面的方法避免:many-to-one fetch="join|select"(該方法可能有問題)
          inverse=true 無法維護集合緩存(還不是很理解集合緩存和對象緩存)
          OLTP類型的web應用,可以群集水平擴展,不可避免的出現數據庫瓶頸
              框架能降低訪問數據庫的壓力,采用緩存是衡量一個框架是否優秀的重要標準,從緩存方面看Hibernate
              A、對象緩存,細顆粒度,是針對表的級別,透明化訪問,因為有不改變代碼的好處,所以是ORM提高性能的法寶
              B、Hibernate是目前ORM框架中緩存性能最好的框架
              C、查詢緩存
          最后Robbin還針對大家經常出現的Hibernate vs iBatis的討論進行了一個總結:
             對于OLTP應用,使用ORM框架 而OLEB應用(不確定是什么應用)最好采用JDBC或者其他方法處理
             Hibernate傾向于細顆粒度設計,面向對象,將大表拆分為多個小表,消除冗余字段,通過二級緩存提升性能。
             iBatis傾向于粗顆粒度設計,面向關系,盡量把表合并,通過Column冗余,消除關聯關系,但是iBatis沒有有效的緩存手段。

             可以說Robbin的性能總結對于使用Hibernate的開發人員有著很重要的點撥作用。非常感謝他無私奉獻自己的經驗。

          posted @ 2011-06-18 12:27 ... 閱讀(666) | 評論 (0)編輯 收藏

          [轉]加速你的Hibernate引擎(下)

          原文: http://www.infoq.com/cn/articles/hibernate_tuning-ii

          4.6 HQL調優

          4.6.1 索引調優

          HQL看起來和SQL很相似。從HQL的WHERE子句中通常可以猜到相應的SQL WHERE子句。WHERE子句中的字段決定了數據庫將選擇的索引。

          大多數Hibernate開發者所常犯的一個錯誤是無論何時,當需要新WHERE子句的時候都會創建一個新的索引。因為索引會帶來額外的數據更新開銷,所以應該爭取創建少量索引來覆蓋盡可能多的查詢。
          4.1節讓你使用一個集合來處理所有可能的數據搜索條件。如果這不太實際,那么你可以使用后端剖析工具來創建一個針對應用程序涉及的所有SQL的集合。基于那些搜索條件的分類,你最終會得到一個小的索引集。與此同時,還可以嘗試向WHERE子句中添加額外的謂語來匹配其他WHERE子句。

          范例7

          有兩個UI搜索器和一個后端守護進程搜索器來搜索名為iso_deals的表。第一個UI搜索器在unexpectedFlag、dealStatus、tradeDate和isold屬性上有謂語。

          第二個UI搜索器基于用戶鍵入的過濾器,其中包括的內容除tradeDate和isold以外還有其他屬性。開始時所有這些過濾器屬性都是可選的。
          后端搜索器基于isold、participantCode和transactionType屬性。
          經過進一步業務分析,發現第二個UI搜索器實際是基于一些隱式的unexpectedFlag和dealStatus值來選擇數據的。我們還讓tradeDate成為過濾器的必要屬性(為了使用數據庫索引,每個搜索過濾器都應該有必要屬性)。

          鑒于這一點,我們依次使用unexpectedFlag、dealStatus、tradeDate和isold構造了一個復合索引。兩個UI搜索器都能共用它。(順序很重要,如果你的謂語以不同的順序指定這些屬性或在它們前羅列了其他屬性,數據庫就不會選擇該復合索引。)

          后端搜索器和UI搜索器區別太大,因此我們不得不為它構造另一個復合索引,依次使用isold、participantCode和transactionType。

          4.6.2綁定參數 vs.字符串拼接

          既可以使用綁定參數構造HQL的WHERE子句,也可以使用字符串拼接的方法,該決定對性能會有一定影響。使用綁定參數的原因是讓數據庫一次解析SQL,對后續的重復請求復用生成好的執行計劃,這樣做節省了CPU時間和內存。然而,為達到最優的數據訪問效率,不同的綁定值可能需要不同的SQL執行計劃。

          例如,一小段數據范圍可能只返回數據總量的5%,而一大段數據范圍可能返回數據總量的90%。前者使用索引更好,而后者則最好使用全表掃描。

          建議OLTP使用綁定參數,數據倉庫使用字符串拼接,因為OLTP通常在一個事務中重復插入和更新數據,只取少量數據;數據倉庫通常只有少量SQL查詢,有一個確定的執行計劃比節省CPU時間和內存更為重要。

          要是你知道你的OLTP搜索對不同綁定值應該使用相同執行計劃又該怎么辦呢?

          Oracle 9i及以后版本在第一次調用綁定參數并生成執行計劃時能探出參數值。后續調用不會再探測,而是重用之前的執行計劃。

          4.6.3聚合及排序

          你可以在數據庫中進行聚合和“order by”,也可以在應用程序的服務層中事先加載所有數據然后做聚合和“order by”操作。推薦使用前者,因為數據庫在這方面通常會比你的應用程序做得好。此外,這樣做還能節省網絡帶寬,這也是一種擁有跨數據庫移植性的做法。

          當你的應用程序對數據聚合和排序有HQL不支持的特定業務規則時除外。

          4.6.4覆蓋抓取策略

          詳見4.7.1節

          4.6.5本地查詢

          本地查詢調優其實并不直接與HQL有關。但HQL的確可以讓你直接向底層數據庫傳遞本地查詢。我們并不建議這么做,因為本地查詢在數據庫間不可移植。

          4.7抓取策略調優

          抓取策略決定了在應用程序需要訪問關聯對象時,Hibernate以何種方式以及何時獲取關聯對象。HRD中的第20章“改善性能”對該主題作了很好的闡述,我們在此將關注它的使用方法。

          4.7.1覆蓋抓取策略

          不同的用戶可能會有不同的數據抓取要求。Hibernate允許在兩個地方定義數據抓取策略,一處是在映射元數據中,另一處是在HQL或Criteria中覆蓋它。

          常見的做法是基于主要的抓取用例在映射元數據中定義默認抓取策略,針對少數用例在HQL和Criteria中覆蓋抓取策略。

          假設pojoA和pojoB是父子關系實例。如果根據業務規則,只是偶爾需要從實體兩端加載數據,那你可以聲明一個延遲加載集合或代理抓取(proxy fetching)。當你需要從實體兩端獲取數據時,可以用立即抓取(eager fetching)覆蓋默認策略,例如使用HQL或Criteria配置連接抓取(join fetching)。

          另一方面,如果業務規則在大多數時候需要從實體兩端加載數據,那么你可以聲明立即抓取并在Criteria中設置延遲加載集合或代理抓取來覆蓋它(HQL目前還不支持這樣的覆蓋)。

          4.7.2 N+1模式或是反模式?

          select抓取會導致N+1問題。如果你知道自己總是需要從關聯中加載數據,那么就該始終使用連接抓取。在下面兩個場景中,你可能會把N+1視為一種模式而非反模式。

          第一種場景,你不知道用戶是否會訪問關聯對象。如果他/她沒有訪問,那么你贏了;否則你仍然需要額外的N次select SQL語句。這是一種令人左右為難的局面。

          第二種場景,pojoA和很多其他POJO有one-to-many關聯,例如pojoB和pojoC。使用立即的內連接或外連接抓取會在結果集中將pojoA重復很多次。當pojoA中有很多非空屬性時,你不得不將大量數據加載到持久層中。這種加載需要很多時間,既有網絡帶寬的原因,如果Hibernate的會話是有狀態的,其中也會有會話緩存的原因(內存消耗和GC暫停)。

          如果你有一個很長的one-to-many關聯鏈,例如從pojoA到pojoB到pojoC以此類推,情況也是類似的。

          你也許會去使用HQL中的DISTINCT關鍵字或Cirteria中的distinct功能或是Java的Set接口來消除重復數據。但所有這些都是在Hibernate(在持久層)中實現的,而非數據庫中。

          如果基于你的網絡和內存配置的測試表明N+1性能更好,那么你可以使用批量抓取、subselect抓取或二級緩存來做進一步調優。

          范例8

          以下是一個使用批量抓取的HBM文件片段:

          <class name="pojoA" table="pojoA">
          …
          <set name="pojoBs" fetch="select" batch-size="10">
          
          <key column="pojoa_id"/>
          …
          </set>
          </class> 
          

          以下是多端pojoB生成的SQL:

          selectfrom pojoB where pojoa_id in(?,?,?,?,?, ?,?,?,?,?);

          問號數量與batch-size值相等。因此N次額外的關于pojoB的select SQL語句被減少到了N/10次。

          如果將fetch="select"替換成fetch="subselect",pojoB生成的SQL語句就是這樣的:

          selectfrom pojoB where pojoa_id in(select id from pojoA where …); 

          盡管N次額外的select減少到1次,但這只在重復運行pojoA的查詢開銷很低時才有好處。

          如果pojoA中的pojoB集合很穩定,或pojoB有pojoA的many-to-one關聯,而且pojoA是只讀引用數據,那么你可以使用二級緩存來緩存pojoA以消除N+1問題(4.8.1節中有一個例子)。

          4.7.3延遲屬性抓取

          除非有一張擁有很多你不需要的字段的遺留表,否則不應該使用這種抓取策略,因為它的延遲屬性分組會帶來額外的SQL。

          在業務分析和設計過程中,你應該將不同數據獲取或修改分組放到不同的領域對象實體中,而不是使用這種抓取策略。

          如果不能重新設計遺留表,可以使用HQL或Criteria提供的投影功能來獲取數據。

          4.8 二級緩存調優

          HRD第20.2節 “二級緩存”中的描述對大多數開發者來說過于簡單,無法做出選擇。3.3版及以后版本不再推薦使用基于“CacheProvider”的緩存,而用基于“RegionFactory”的緩存,這也讓人更糊涂了。但是就算是最新的3.5參考文檔也沒有提及如何使用新緩存方法。

          出于下述考慮,我們將繼續關注于老方法:

          • 所有流行的Hibernate二級緩存提供商中只有JBoss Cache 2Infinispan 4Ehcache 2支持新方法。OSCacheSwarmCacheCoherenceGigaspaces XAP-Data Grid只支持老方法。
          • 兩種方法共用相同的<cache>配置。例如,它們仍舊使用相同的usage屬性值“transactional|read-write|nonstrict-read-write|read-only”。
          • 多個cache-region適配器仍然內置老方法的支持,理解它能幫助你快速理解新方法。

          4.8.1 基于CacheProvider的緩存機制

          理解該機制是做出合理選擇的關鍵。關鍵的類/接口是CacheConcurrencyStrategy和它針對4中不同緩存使用的實現類,還有EntityUpdate/Delete/InsertAction。

          針對并發緩存訪問,有三種實現模式:

          • 針對“read-only”的只讀模式。

            無論是鎖還是事務都沒影響,因為緩存自數據從數據庫加載后就不會改變。

          • 針對“read-write”和“nonstrict-read-write”的非事務感知(non-transaction-aware)讀寫模式。

            對緩存的更新發生在數據庫事務完成后。緩存需要支持鎖。

          • 針對“transactional”的事務感知讀寫。

            對緩存和數據庫的更新被包裝在同一個JTA事務中,這樣緩存與數據庫總是保持同步的。數據庫和緩存都必須支持JTA。盡管緩存事務內部依賴于緩存鎖,但Hibernate不會顯式調用任何的緩存鎖函數。

          以數據庫更新為例。EntityUpdateAction對于事務感知讀寫、“read-write”的非事務感知讀寫,還有“nonstrict-read-write”的非事務感知讀寫相應有如下調用序列:

          • 在一個JTA事務中更新數據庫;在同一個事務中更新緩存。
          • 軟鎖緩存;在一個事務中更新數據庫;在上一個事務成功完成后更新緩存;否則釋放軟鎖。

            軟鎖只是一種特定的緩存值失效表述方式,在它獲得新數據庫值前阻止其他事務讀寫緩存。那些事務會轉而直接讀取數據庫。

            緩存必須支持鎖;事務支持則不是必須的。如果緩存是一個集群,“更新緩存”的調用會將新值推送給所有副本,這通常被稱為“推(push)”更新策略。

          • 在一個事務中更新數據庫;在上一個事務完成前就清除緩存;為了安全起見,無論事務成功與否,在事務完成后再次清除緩存。

            既不需要支持緩存鎖,也不需要支持事務。如果是緩存集群,“清除緩存”調用會讓所有副本都失效,這通常被稱為“拉(pull)”更新策略。

          對于實體的刪除或插入動作,或者集合變更,調用序列都是相似的。

          實際上,最后兩個異步調用序列仍能保證數據庫和緩存的一致性(基本就是“read committed”的隔離了級別),這要歸功于第二個序列中的軟鎖和“更新數據庫”后的“更新緩存”,還有最后一個調用序列中的悲觀“清除緩存”。

          基于上述分析,我們的建議是: 

          • 如果數據是只讀的,例如引用數據,那么總是使用“read-only”策略,因為它是最簡單、最高效的策略,也是集群安全的策略。
          • 除非你真的想將緩存更新和數據庫更新放在一個JTA事務里,否則不要使用“transactional”策略,因為JTA需要漫長的兩階段提交處理,這導致它基本是性能最差的策略。

            依筆者看來,二級緩存并非一級數據源,因此使用JTA也未必合理。實際上最后兩個調用序列在大多數場景下是個不錯的替代方案,這要歸功于它們的數據一致性保障。

          • 如果你的數據讀很多或者很少有并發緩存訪問和更新,那么可以使用“nonstrict-read-write”策略。感謝它的輕量級“拉”更新策略,它通常是性能第二好的策略。
          • 如果你的數據是又讀又寫的,那么使用“read-write”策略。這通常是性能倒數第二的策略,因為它要求有緩存鎖,緩存集群中使用重量級的“推”更新策略。

          范例9

          以下是一個ISO收費類型的HBM文件片段:

          <class name="IsoChargeType">
             <property name="isoId" column="ISO_ID" not-null="true"/>
          
             <many-to-one name="estimateMethod" fetch="join" lazy="false"/>
          
             <many-to-one  name="allocationMethod" fetch="join" lazy="false"/>
          
             <many-to-one name="chargeTypeCategory" fetch="join" lazy="false"/>
          
          </class> 

          一些用戶只需要ISO收費類型本身;一些用戶既需要ISO收費類型,還需要它的三個關聯對象。簡單起見,開發者會立即加載所有三個關聯對象。如果項目中沒人負責Hibernate調優,這是很常見的。

          4.7.1節中講過了最好的方法。因為所有的關聯對象都是只讀引用數據,另一種方法是使用延遲抓取,打開這些對象的二級緩存以避免N+1問題。實際上前一種方法也能從引用數據緩存中獲益。

          因為大多數項目都有很多被其他數據引用的只讀引用數據,上述兩種方法都能改善全局系統性能。

          4.8.2 RegionFactory

          下表是新老兩種方法中對應的主要類/接口:

          新方法

          老方法

          RegionFactory

          CacheProvider

          Region

          Cache

          EntityRegionAccessStrategy

          CacheConcurrencyStrategy

          CollectionRegionAccessStrategy

          CacheConcurrencyStrategy

          第一個改進是RegionFactory構建了特定的Region,例如EntityRegion和TransactionRegion,而不是使用一個通用的訪問Region。第二個改進是對于特定緩存的“usage”屬性值,Region要求構建自己的訪問策略,而不是所有Region都一直使用CacheConcurrencyStrategy的4種實現。

          要使用新方法,應該設置factory_class而非provider_class配置屬性。以Ehcache 2.0為例:

          <property name="hibernate.cache.region.factory_class">
                  net.sf.ehcache.hibernate.EhCacheRegionFactory  
          </property>

          其他相關的Hibernate緩存配置都和老方法一樣。

          新方法也能向后兼容遺留方法。如果還是只配了CacheProvider,新方法中將使用下列自說明(self-explanatory)適配器和橋隱式地調用老的接口/類:

          RegionFactoryCacheProviderBridge、EntityRegionAdapter、CollectionRegionAdapter、QueryResultsRegionAdapter、EntityAccessStrategyAdapter和CollectionAccessStrategyAdapter

          4.8.3 查詢緩存

          二級緩存也能緩存查詢結果。如果查詢開銷很大而且要重復運行,這也會很有幫助。

          4.9批量處理調優

          大多數Hibernate的功能都很適合那些每個事務都通常只處理少量數據的OLTP系統。但是,如果你有一個數據倉庫或者事務需要處理大量數據,那么就另當別論了。

          4.9.1使用有狀態會話的非DML風格批處理

          如果你已經在使用常規會話了,那這是最自然的方法。你需要做三件事:

          • 配置下列3個屬性以開啟批處理特性:
              hibernate.jdbc.batch_size 30
              hibernate.jdbc.batch_versioned_data true
              hibernate.cache.use_second_level_cache false

            batch_size設置為正值會開啟JDBC2的批量更新,Hibernate的建議值是5到30。基于我們的測試,極低值和極高值性能都很差。只要取值在合理范圍內,區別就只有幾秒而已。如果網絡夠快,這個結果是一定的。

            第二個配置設為true,這要求JDBC驅動在executeBatch()方法中返回正確的行數。對于Oracle用戶而言,批量更新時不能將其設為true。請閱讀Oracle的《JDBC Developer’s Guide and Reference》中的“標準批處理的Oracle實現中的更新計數”(Update Counts in the Oracle Implementation of Standard Batching)以獲得更多詳細信息。因為它對批量插入來說還是安全的,所以你可以為批量插入創建單獨的專用數據源。最后一個配置項是可選的,因為你可以在會話中顯式關閉二級緩存。

          • 像如下范例中那樣定期刷新(flush)并清除一級會話緩存:
             Session session = sessionFactory.openSession();
             Transaction tx = session.beginTransaction();
            

             for ( int i=0; i<100000; i++ ) {
                 Customer customer = new Customer(.....);
                 //if your hibernate.cache.use_second_level_cache is true, call the following:
                 session.setCacheMode(CacheMode.IGNORE);
                 session.save(customer);
                 if (i % 50 == 0) { //50, same as the JDBC batch size
                 //flush a batch of inserts and release memory:
                 session.flush();
                 session.clear();
                 }
             }
             tx.commit();
             session.close();

            批處理通常不需要數據緩存,否則你會將內存耗盡并大量增加GC開銷。如果內存有限,那這種情況會很明顯。

          • 總是將批量插入嵌套在事務中。

          每次事務修改的對象數量越少就意味著會有更多數據庫提交,正如4.5節所述每次提交都會帶來磁盤相關的開銷。

          另一方面,每次事務修改的對象數量越多就意味著鎖定變更時間越長,同時數據庫需要更大的redo log。

          4.9.2使用無狀態會話的非DML風格批處理

          無狀態會話執行起來比上一種方法更好,因為它只是JDBC的簡單包裝,而且可以繞開很多常規會話要求的操作。例如,它不需要會話緩存,也不和任何二級緩存或查詢緩存有交互。
          然而它的用法并不簡單。尤其是它的操作并不會級聯到所關聯的實例上;你必須自己來處理它們。

          4.9.3 DML風格

          使用DML風格的插入、更新或刪除,你直接在數據庫中操作數據,這和前兩種方法在Hibernate中操作數據的情況有所不同。

          因為一個DML風格的更新或刪除相當于前兩種方法中的多個單獨的更新或刪除,所以如果更新或刪除中的WHERE子句暗示了恰當的數據庫索引,那么使用DML風格的操作能節省網絡開銷,執行得更好。

          強烈建議結合使用DML風格操作和無狀態會話。如果使用有狀態會話,不要忘記在執行DML前清除緩存,否則Hibernate將會更新或清除相關緩存(見下面的范例10)。

          4.9.4批量加載

          如果你的HQL或Criteria會返回很多數據,那么要注意兩件事:

          • 用下列配置開啟批量抓取特性:
            hibernate.jdbc.fetch_size 10

            fetch_size設置為正值將開啟JDBC批量抓取特性。相對快速網絡,在慢速網絡中這一點更為重要。Oracle建議的經驗值是10。你應該基于自己的環境進行測試。

          • 在使用上述任一方法時都要關閉緩存,因為批量加載一般是一次性任務。受限于內存容量,向緩存中加載大量數據通常也意味著它們很快會被清除出去,這會增加GC開銷。

          范例10

          我們有一個后臺任務,分段加載大量的IsoDeal數據用于后續處理。我們還會在分段數據交給下游系統處理前將其更新為處理中狀態。最大的一段有50萬行數據。以下是原始代碼中截取出來的一段:

          Query query = session.createQuery("FROM IsoDeal d WHERE chunk-clause");
          query.setLockMode("d", LockMode.UPGRADE); //for Inprocess status update
          List<IsoDeal> isoDeals = query.list();
          for (IsoDeal isoDeal : isoDeals) { //update status to Inprocess
             isoDeal.setStatus("Inprocess");
          }
          return isoDeals; 

          包含上述代碼的方法加上了Spring 2.5聲明式事務的注解。加載并更新50萬行數據大約花了10分鐘。我們識別出了以下這些問題:

          • 由于會話緩存和二級緩存的原因,系統會頻繁地內存溢出。
          • 就算沒有內存溢出,當內存消耗很高時GC的開銷也會很大。
          • 我們還未設置fetch_size。
          • 就算我們設置了batch_size,for循環也創建了太多update SQL語句。

          不幸的是Spring 2.5不支持Hibernate無狀態會話,所以我們只能關閉二級緩存;設置fetch_size;用DML風格的更新來代替for循環,以此改善性能。

          但是,執行時間還是要6分鐘。將Hibernate的日志級別調成trace后,我們發現是更新會話緩存造成了延時。通過在DML更新前清除會話緩存,我們將時間縮短到了4分鐘,全部都是將數據加載到會話緩存中花費的時間。

          4.10 SQL生成調優

          本節將向你展示如何減少SQL生成的數量。

          4.10.1 N+1抓取問題

          “select抓取”策略會導致N+1問題。如果“連接抓取”策略適合你的話,你應該始終使用該策略避免N+1問題。

          但是,如果“連接抓取”策略執行效果不理想,就像4.7.2節中那樣,你可以使用“subselect抓取”、“批量抓取”或“延遲集合抓取”來減少所需的額外SQL語句數。

          4.10.2 Insert+Update問題

          范例11

          我們的ElectricityDeal與DealCharge有單向one-to-many關聯,如下列HBM文件片段所示:

          <class name="ElectricityDeal"
                 select-before-update="true" dynamic-update="true"
          
                 dynamic-insert="true">
              <id name="key" column="ID">
          
                  <generator class="sequence">
                      <param name="sequence">SEQ_ELECTRICITY_DEALS</param>
          
                  </generator>
              </id>
              …
              <set
          name="dealCharges" cascade="all-delete-orphan">         <key column="DEAL_KEY" not-null="false" update="true"              on-delete="noaction"/>         <one-to-many class="DealCharge"/>     </set> </class>

          在“key”元素中,“not-null”和“update”對應的默認值是false和true,上述代碼為了明確這些取值,將它們寫了出來。

          如果你想創建一個ElectricityDeal和十個DealCharge,會生成如下SQL語句:

          • 1句ElectricityDeal的插入語句;
          • 10句DealCharge的插入語句,其中不包括外鍵“DEAL_KEY”;
          • 10句DealCharge字段“DEAL_KEY”的更新語句。

          為了消除那額外的10句更新語句,可以在那10句DealCharge插入語句中包含“DEAL_KEY”,你需要將“not-null”和“update”分別修改為true和false。

          另一種做法是使用雙向或many-to-one關聯,讓DealCharge來管理關聯。

          4.10.3 更新前執行select

          在范例11中,我們為ElectricityDeal加上了select-before-update,這會對瞬時(transient)對象或分離(detached)對象產生額外的select語句,但卻能避免不必要的數據庫更新。

          你應該做出一些權衡,如果對象沒多少屬性,不需要防止不必要的數據庫更新,那么就不要使用該特性,因為你那些有限的數據既沒有太多網絡傳輸開銷,也不會帶來太多數據庫更新開銷。

          如果對象的屬性較多,例如是一張大的遺留表,那你應該開啟該特性,和“dynamic-update”結合使用以避免太多數據庫更新開銷。

          4.10.4 級聯刪除

          在范例11中,如果你想刪除1個ElectricityDeal和它的100個DealCharge,Hibernate會對DealCharge做100次刪除。

          如果將“on-delete”修改為“cascade”,Hibernate不會執行DealCharge的刪除動作;而是讓數據庫根據ON CASCADE DELETE約束自動刪除那100個DealCharge。不過,需要讓DBA開啟ON CASCADE DELETE約束,大多數DBA不愿意這么做,因為他們想避免父對象的意外刪除級聯到它的依賴對象上。此外,還要注意,該特性會繞過Hibernate對版本數據(versioned data)的常用樂觀鎖策略。

          4.10.5 增強的序列標識符生成器

          范例11中使用Oracle的序列作為標識符生成器。假設我們保存100個ElectricityDeal,Hibernate會將下面的SQL語句執行100次來獲取下一個可用的標識符:

          select SEQ_ELECTRICITY_DEALS.NEXTVAL from dual; 

          如果網絡不是很快,那這無疑會降低效率。3.2.3及后續版本中增加了一個增強的生成器“SequenceStyleGenerator”,它帶了兩個優化器:hilo和pooled。盡管HRD的第5章“基礎O/R映射” 講到了這兩個優化器,不過內容有限。兩個優化器都使用了HiLo算法,該算法生成的標識符等于Hi值加上Lo值,其中Hi值代表組號,Lo值順序且重復地從1迭代到最大組大小,組號在Lo值“轉回到”1時加1。

          假設組大小是5(可以用max_lo或increment_size參數來表示),下面是個例子:

          • hilo優化器

            組號取自數據庫序列的下一個可用值,Hi值由Hibernate定義,是組號乘以increment_size參數值。

          • pooled優化器

            Hi值直接取自數據庫序列的下一個可用值。數據庫序列的增量應該設置為increment_size參數值。

          直到內存組中的值耗盡后,兩個優化器才會去訪問數據庫,上面的例子每5個標識值符訪問一次數據庫。使用hilo優化器時,你的序列不能再被其他應用程序使用,除非它們使用與Hibernate相同的邏輯。使用pooled優化器,在其他應用程序使用同一序列時則相當安全。

          兩個優化器都有一個問題,如果Hibernate崩潰,當前組內的一些標識符值就會丟失,然而大多數應用程序都不要求擁有連續的標識符值(如果你的數據庫,比方說Oracle,緩存了序列值,當它崩潰時你也會丟失標識符值)。

          如果在范例11中使用pooled優化器,新的id配置如下:

          <id name="key" column="ID">
              <generator class="org.hibernate.id.enhance.SequenceStyleGenerator">
          <param name="sequence_name">SEQ_ELECTRICITY_DEALS</param> <param name="initial_value">0</param> <param name="increment_size">100</param> <param name="optimizer ">pooled</param> </generator> </id>

          5 總結

          本文涵蓋了大多數你在Hibernate應用程序調優時會覺得很有用的調優技巧,其中的大多數時間都在討論那些行之有效卻缺乏文檔的調優主題,例如繼承映射、二級緩存和增強的序列標識符生成器。

          它還提到了一些Hibernate調優所必需的數據庫知識。一些范例中包含了你可能遇到的問題的實際解決方案。

          除此之外,值得一提的是Hibernate也可以和In-Memory Data Grid(IMDG)一起使用,例如Oracle的Coherance或GigaSpaces IMDG,這能讓你的應用程序達到毫秒級別。

          6 資源

          [1] Latest Hibernate Reference Documentation on jboss.com

          [2] Oracle 9i Performance Tuning Guide and Reference

          [3] Performance Engineering on Wikipedia

          [4] Program Optimization on Wikipedia

          [5] Pareto Principle (the 80/20 rule) on Wikipedia

          [6] Premature Optimization on acm.org

          [7] Java Performance Tuning by Jack Shirazi

          [8] The Law of Leaky Abstractions by Joel Spolsky

          [9] Hibernate’s StatisticsService Mbean configuration with Spring

          [10] JProbe by Quest Software

          [11] Java VisualVM

          [12] Column-oriented DBMS on Wikipedia

          [13] Apache DBCP BasicDataSource

          [14] JDBC Connection Pool by Oracle

          [15] Connection Failover by Oracle

          [16] Last Resource Commit Optimization (LRCO)

          [17] GigaSpaces for Hibernate ORM Users



          posted @ 2011-06-18 12:03 ... 閱讀(207) | 評論 (0)編輯 收藏

          [轉]加速你的Hibernate引擎(上)

          http://www.infoq.com/cn/articles/hibernate_tuning

          1.引言

          Hibernate是最流行的對象關系映射(ORM)引擎之一,它提供了數據持久化和查詢服務。


          2.Hibernate性能調優

          調優是一個迭代的、持續進行的過程,涉及軟件開發生命周期(SDLC)的所有階段。在一個典型的使用Hibernate進行持久化的Java EE應用程序中,調優會涉及以下幾個方面:

          • 業務規則調優
          • 設計調優
          • Hibernate調優
          • Java GC調優
          • 應用程序容器調優
          • 底層系統調優,包括數據庫和OS。

          沒有一套精心設計的方案就去進行以上調優是非常耗時的,而且很可能收效甚微。好的調優方法的重要部分是為調優內容劃分優先級。可以用Pareto定律(又稱“80/20法則”)來解釋這一點,即通常80%的應用程序性能改善源自頭20%的性能問題[5]

          相比基于磁盤和網絡的訪問,基于內存和CPU的訪問能提供更低的延遲和更高的吞吐量。這種基于IO的Hibernate調優與底層系統IO部分的調優應該優先于基于CPU和內存的底層系統GC、CPU和內存部分的調優。

          范例1

          我們調優了一個選擇電流的HQL查詢,把它從30秒降到了1秒以內。如果我們在垃圾回收方面下功夫,可能收效甚微——也許只有幾毫秒或者最多幾秒,相比HQL的改進,GC方面的改善可以忽略不計。

          好的調優方法的另一個重要部分是決定何時優化[4]

          積極優化的提倡者主張開始時就進行調優,例如在業務規則和設計階段,在整個SDLC都持續進行優化,因為他們認為后期改變業務規則和重新設計代價太大。

          另一派人提倡在SDLC末期進行調優,因為他們抱怨前期調優經常會讓設計和編碼變得復雜。他們經常引用Donald Knuth的名言“過早優化是萬惡之源 [6]

          為了平衡調優和編碼需要一些權衡。根據筆者的經驗,適當的前期調優能帶來更明智的設計和細致的編碼。很多項目就失敗在應用程序調優上,因為上面提到的“過早優化”階段在被引用時脫離了上下文,而且相應的調優不是被推遲得太晚就是投入資源過少。

          但是,要做很多前期調優也不太可能,因為沒有經過剖析,你并不能確定應用程序的瓶頸究竟在何處,應用程序一般都是這樣演化的。

          對我們的多線程企業級應用程序的剖析也表現出大多數應用程序平均只有20-50%的CPU使用率。剩余的CPU開銷只是在等待數據庫和網絡相關的IO。

          基于上述分析,我們得出這樣一個結論,結合業務規則和設計的Hibernate調優在Pareto定律中20%的那個部分,相應的它們的優先級更高。

          一種比較實際的做法是:

          1. 識別出主要瓶頸,可以預見其中多數是Hibernate、業務規則和設計方面的(其數量視你的調優目標而定;但三到五個是不錯的開端)。
          2. 修改應用程序以便消除這些瓶頸。
          3. 測試應用程序,然后重復步驟1,直到達到你的調優目標為止。

          你能在Jack Shirazi的《Java Performance Tuning》 [7]一書中找到更多關于性能調優階段的常見建議。

          下面的章節中,我們會按照調優的大致順序(列在前面的通常影響最大)去解釋一些特定的調優技術。

          3. 監控和剖析

          沒有對Hibernate應用程序的有效監控和剖析,你無法得知性能瓶頸以及何處需要調優。

          3.1.1 監控SQL生成

          盡管使用Hibernate的主要目的是將你從直接使用SQL的痛苦中解救出來,為了對應用程序進行調優,你必須知道Hibernate生成了哪些SQL。JoeSplosky在他的《The Law of Leaky Abstractions》一文中詳細描述了這個問題。

          你可以在log4j中將org.hibernate.SQL包的日志級別設為DEBUG,這樣便能看到生成的所有SQL。你還可以將其他包的日志級別設為DEBUG,甚至TRACE來定位一些性能問題。

          3.1.2 查看Hibernate統計

          如果開啟hibernate.generate.statistics,Hibernate會導出實體、集合、會話、二級緩存、查詢和會話工廠的統計信息,這對通過SessionFactory.getStatistics()進行的調優很有幫助。為了簡單起見,Hibernate還可以使用MBean“org.hibernate.jmx.StatisticsService”通過JMX來導出統計信息。你可以在這個網站找到配置范例

          3.1.3 剖析

          一個好的剖析工具不僅有利于Hibernate調優,還能為應用程序的其他部分帶來好處。然而,大多數商業工具(例如JProbe [10])都很昂貴。幸運的是Sun/Oracle的JDK1.6自帶了一個名為“Java VisualVM” [11]的調試接口。雖然比起那些商業競爭對手,它還相當基礎,但它提供了很多調試和調優信息。

          4. 調優技術

          4.1 業務規則與設計調優

          盡管業務規則和設計調優并不屬于Hibernate調優的范疇,但此處的決定對后面Hibernate的調優有很大影響。因此我們特意指出一些與Hibernate調優有關的點。

          在業務需求收集與調優過程中,你需要知道:

          • 數據獲取特性包括引用數據(reference data)、只讀數據、讀分組(read group)、讀取大小、搜索條件以及數據分組和聚合。
          • 數據修改特性包括數據變更、變更組、變更大小、無效修改補償、數據庫(所有變更都在一個數據庫中或在多個數據庫中)、變更頻率和并發性,以及變更響應和吞吐量要求。
          • 數據關系,例如關聯(association)、泛化(generalization)、實現(realization)和依賴(dependency)。

          基于業務需求,你會得到一個最優設計,其中決定了應用程序類型(是OLTP還是數據倉庫,亦或者與其中某一種比較接近)和分層結構(將持久層和服務層分離還是合并),創建領域對象(通常是POJO),決定數據聚合的地方(在數據庫中進行聚合能利用強大的數據庫功能,節省網絡帶寬;但是除了像COUNT、SUM、AVG、MIN和MAX這樣的標準聚合,其他的聚合通常不具有移植性。在應用服務器上進行聚合允許你應用更復雜的業務邏輯;但你需要先在應用程序中載入詳細的數據)。

          范例2

          分析員需要查看一個取自大數據表的電流ISO(Independent System Operator)聚合列表。最開始他們想要顯示大多數字段,盡管數據庫能在1分鐘內做出響應,應用程序也要花30分鐘將1百萬行數據加載到前端UI。經過重新分析,分析員保留了14個字段。因為去掉了很多可選的高聚合度字段,從剩下的字段中進行聚合分組返回的數據要少很多,而且大多數情況下的數據加載時間也縮小到了可接受的范圍內。

          范例3

          過24個“非標準”(shaped,表示每小時都可以有自己的電量和價格;如果所有24小時的電量和價格相同,我們稱之為“標準”)小時會修改小時電流交易,其中包括2個屬性:每小時電量和價格。起初我們使用Hibernate的select-before-update特性,就是更新24行數據需要24次選擇。因為我們只需要2個屬性,而且如果不修改電量或價格的話也沒有業務規則禁止無效修改,我們就關閉了select-before-update特性,避免了24次選擇。

          4.2繼承映射調優

          盡管繼承映射是領域對象的一部分,出于它的重要性我們將它單獨出來。HRD [1]中的第9章“繼承映射”已經說得很清楚了,所以我們將關注SQL生成和針對每個策略的調優建議。

          以下是HRD中范例的類圖:


          4.2.1 每個類層次一張表

          只需要一張表,一條多態查詢生成的SQL大概是這樣的:

          select id, payment_type, amount, currency, rtn, credit_card_type from payment

          針對具體子類(例如CashPayment)的查詢生成的SQL是這樣的:

          select id, amount, currency from payment where payment_type=’CASH’ 

          這樣做的優點包括只有一張表、查詢簡單以及容易與其他表進行關聯。第二個查詢中不需要包含其他子類中的屬性。所有這些特性讓該策略的性能調優要比其他策略容易得多。這種方法通常比較適合數據倉庫系統,因為所有數據都在一張表里,不需要做表連接。

          主要的缺點整個類層次中的所有屬性都擠在一張大表里,如果有很多子類特有的屬性,數據庫中就會有太多字段的取值為null,這為當前基于行的數據庫(使用基于列的DBMS的數據倉庫處理這個會更好些)的SQL調優增加了難度。除非進行分區,否則唯一的數據表會成為熱點,OLTP系統通常在這方面都不太好。

          4.2.2每個子類一張表

          需要4張表,多態查詢生成的SQL如下:

          select id, payment_type, amount, currency, rtn, credit_card type,
                  case when c.payment_id is not null then 1
                       when ck.payment_id is not null then 2
                       when cc.payment_id is not null then 3
                       when p.id is not null then 0 end as clazz
          
          from payment p left join cash_payment c on p.id=c.payment_id left join
             cheque_payment ck on p.id=ck.payment_id left join 
          
             credit_payment cc on p.id=cc.payment_id; 

          針對具體子類(例如CashPayment)的查詢生成的SQL是這樣的:

          select id, payment_type, amount, currency
          from payment p left join cash_payment c on p.id=c.payment_id; 

          優點包括數據表比較緊湊(沒有不需要的可空字段),數據跨三個子類的表進行分區,容易使用超類的表與其他表進行關聯。緊湊的數據表可以針對基于行的數據庫做存儲塊優化,讓SQL執行得更好。數據分區增加了數據修改的并發性(除了超類,沒有熱點),OLTP系統通常會更好些。

          同樣的,第二個查詢不需要包含其他子類的屬性。

          缺點是在所有策略中它使用的表和表連接最多,SQL語句稍顯復雜(看看Hibernate動態鑒別器的長CASE子句)。相比單張表,數據庫要花更多時間調優數據表連接,數據倉庫在使用該策略時通常不太理想。

          因為不能跨超類和子類的字段來建立復合索引,如果需要按這些列進行查詢,性能會受影響。任何子類數據的修改都涉及兩張表:超類的表和子類的表。

          4.2.3每個具體類一張表

          涉及三張或更多的表,多態查詢生成的SQL是這樣的:

          select p.id, p.amount, p.currency, p.rtn, p. credit_card_type, p.clazz
          from (select id, amount, currency, null as rtn,null as credit_card type,
                       1 as clazz from cash_payment union all
          
                select id, amount, null as currency, rtn,null as credit_card type,
                       2 as clazz from cheque_payment union all
          
                select id, amount, null as currency, null as rtn,credit_card type,
                       3 as clazz from credit_payment) p;  

          針對具體子類(例如CashPayment)的查詢生成的SQL是這樣的:

          select id, payment_type, amount, currency from cash_payment; 

          優點和上面的“每個子類一張表”策略相似。因為超類通常是抽象的,所以具體的三張表是必須的[開頭處說的3張或更多的表是必須的],任何子類的數據修改只涉及一張表,運行起來更快。

          缺點是SQL(from子句和union all子查詢)太復雜。但是大多數數據庫對此類SQL的調優都很好。

          如果一個類想和Payment超類關聯,數據庫無法使用引用完整性(referential integrity)來實現它;必須使用觸發器來實現它。這對數據庫性能有些影響。

          4.2.4使用隱式多態實現每個具體類一張表

          只需要三張表。對于Payment的多態查詢生成三條獨立的SQL語句,每個對應一個子類。Hibernate引擎通過Java反射找出Payment的所有三個子類。

          具體子類的查詢只生成該子類的SQL。這些SQL語句都很簡單,這里就不再闡述了。

          它的優點和上節類似:緊湊數據表、跨三個具體子類的數據分區以及對子類任意數據的修改都只涉及一張表。

          缺點是用三條獨立的SQL語句代替了一條聯合SQL,這會帶來更多網絡IO。Java反射也需要時間。假設如果你有一大堆領域對象,你從最上層的Object類進行隱式選擇查詢,那該需要多長時間啊!

          根據你的映射策略制定合理的選擇查詢并非易事;這需要你仔細調優業務需求,基于特定的數據場景制定合理的設計決策。

          以下是一些建議:

          • 設計細粒度的類層次和粗粒度的數據表。細粒度的數據表意味著更多數據表連接,相應的查詢也會更復雜。
          • 如非必要,不要使用多態查詢。正如上文所示,對具體類的查詢只選擇需要的數據,沒有不必要的表連接和聯合。
          • “每個類層次一張表”對有高并發、簡單查詢并且沒有共享列的OLTP系統來說是個不錯的選擇。如果你想用數據庫的引用完整性來做關聯,那它也是個合適的選擇。
          • “每個具體類一張表”對有高并發、復雜查詢并且沒有共享列的OLTP系統來說是個不錯的選擇。當然你不得不犧牲超類與其他類之間的關聯。
          • 采用混合策略,例如“每個類層次一張表”中嵌入“每個子類一張表”,這樣可以利用不同策略的優勢。隨著你項目的進化,如果你要反復重新映射,那你可能也會采用該策略。
          • “使用隱式多態實現每個具體類一張表”這種做法并不推薦,因為其配置過于繁縟、使用“any”元素的復雜關聯語法和隱式查詢的潛在危險性。

          范例4

          下面是一個交易描述應用程序的部分領域類圖:

          開始時,項目只有GasDeal和少數用戶,它使用“每個類層次一張表”。

          OilDeal和ElectricityDeal是后期產生更多業務需求后加入的。沒有改變映射策略。但是ElectricityDeal有太多自己的屬性,因此有很多電相關的可空字段加入了Deal表。因為用戶量也在增長,數據修改變得越來越慢。

          重新設計時我們使用了兩張單獨的表,分別針對氣/油和電相關的屬性。新的映射混合了“每個類層次一張表”和“每個子類一張表”。我們還重新設計了查詢,以便允許針對具體交易子類進行選擇,消除不必要的列和表連接。

          4.3 領域對象調優

          基于4.1中對業務規則和設計的調優,你得到了一個用POJO來表示的領域對象的類圖。我們建議:

          4.3.1 POJO調優

          • 從讀寫數據中將類似引用這樣的只讀數據和以讀為主的數據分離出來。
            只讀數據的二級緩存是最有效的,其次是以讀為主的數據的非嚴格讀寫。將只讀POJO標識為不可更改的(immutable)也是一個調優點。如果一個服務層方法只處理只讀數據,可以將它的事務標為只讀,這是優化Hibernate和底層JDBC驅動的一個方法。
          • 細粒度的POJO和粗粒度的數據表。
            基于數據的修改并發量和頻率等內容來分解大的POJO。盡管你可以定義一個粒度非常細的對象模型,但粒度過細的表會導致大量表連接,這對數據倉庫來說是不能接受的。
          • 優先使用非final的類。
            Hibernate只會針對非final的類使用CGLIB代理來實現延時關聯獲取。如果被關聯的類是final的,Hibernate會一次加載所有內容,這對性能會有影響。
          • 使用業務鍵為分離(detached)實例實現equals()和hashCode()方法。
            在多層系統中,經常可以在分離對象上使用樂觀鎖來提升系統并發性,達到更高的性能。
          • 定義一個版本或時間戳屬性。
            樂觀鎖需要這個字段來實現長對話(應用程序事務)[譯注:session譯為會話,conversion譯為對話,以示區別]。
          • 優先使用組合POJO。
            你的前端UI經常需要來自多個不同POJO的數據。你應該向UI傳遞一個組合POJO而不是獨立的POJO以獲得更好的網絡性能。
            有兩種方式在服務層構建組合POJO。一種是在開始時加3.2載所有需要的獨立POJO,隨后抽取需要的屬性放入組合POJO;另一種是使用HQL投影,直接從數據庫中選擇需要的屬性。
            如果其他地方也要查找這些獨立POJO,可以把它們放進二級緩存以便共享,這時第一種方式更好;其他情況下第二種方式更好。

          4.3.2 POJO之間關聯的調優

          • 如果可以用one-to-one、one-to-many或many-to-one的關聯,就不要使用many-to-many。
          • many-to-many關聯需要額外的映射表。
            盡管你的Java代碼只需要處理兩端的POJO,但查詢時,數據庫需要額外地關聯映射表,修改時需要額外的刪除和插入。
          • 單向關聯優先于雙向關聯。
            由于many-to-many的特性,在雙向關聯的一端加載對象會觸發另一端的加載,這會進一步觸發原始端加載更多的數據,等等。
            one-to-many和many-to-one的雙向關聯也是類似的,當你從多端(子實體)定位到一端(父實體)
            這樣的來回加載很耗時,而且可能也不是你所期望的。
          • 不要為了關聯而定義關聯;只在你需要一起加載它們時才這么做,這應該由你的業務規則和設計來決定(見范例5)。
            另外,你要么不定義任何關聯,要么在子POJO中定義一個值類型的屬性來表示父POJO的ID(另一個方向也是類似的)。
          • 集合調優
            如果集合排序邏輯能由底層數據庫實現,就使用“order-by”屬性來代替“sort”,因為通常數據庫在這方面做得比你好。
            集合可以是值類型的(元素或組合元素),也可以是實體引用類型的(one-to-many或many-to-many關聯)。對引用類型集合的調優主要是調優獲取策略。對于值類型集合的調優,HRD [1]中的20.5節“理解集合性能”已經做了很好的闡述。
          • 獲取策略調優。請見4.7節的范例5

          范例5

          我們有一個名為ElectricityDeals的核心POJO用于描述電的交易。從業務角度來看,它有很多many-to-one關聯,例如和Portfolio、Strategy和Trader等的關聯。因為引用數據十分穩定,它們被緩存在前端,能基于其ID屬性快速定位到它們。

          為了有好的加載性能,ElectricityDeal只映射元數據,即那些引用POJO的值類型ID屬性,因為在需要時,可以在前端通過portfolioKey從緩存中快速查找Portfolio:

          <property name="portfolioKey" column="PORTFOLIO_ID" type="integer"/> 

          這種隱式關聯避免了數據庫表連接和額外的字段選擇,降低了數據傳輸的大小。

          4.4 連接池調優

          由于創建物理數據庫連接非常耗時,你應該始終使用連接池,而且應該始終使用生產級連接池而非Hibernate內置的基本連接池算法。

          通常會為Hibernate提供一個有連接池功能的數據源。Apache DBCP的BasicDataSource[13]是一個流行的開源生產級數據源。大多數數據庫廠商也實現了自己的兼容JDBC 3.0的連接池。舉例來說,你也可以使用Oracle ReaApplication Cluster [15]提供的JDBC連接池[14]以獲得連接的負載均衡和失敗轉移。

          不用多說,你在網上能找到很多關于連接池調優的技術,因此我們只討論那些大多數連接池所共有的通用調優參數:

          • 最小池大小:連接池中可保持的最小連接數。
          • 最大池大小:連接池中可以分配的最大連接數。
            如果應用程序有高并發,而最大池大小又太小,連接池就會經常等待。相反,如果最小池大小太大,又會分配不需要的連接。
          • 最大空閑時間:連接池中的連接被物理關閉前能保持空閑的最大時間。
          • 最大等待時間:連接池等待連接返回的最大時間。該參數可以預防失控事務(runaway transaction)。
          • 驗證查詢:在將連接返回給調用方前用于驗證連接的SQL查詢。這是因為一些數據庫被配置為會殺掉長時間空閑的連接,網絡或數據庫相關的異常也可能會殺死連接。為了減少此類開銷,連接池在空閑時會運行該驗證。

          4.5事務和并發的調優

          短數據庫事務對任何高性能、高可擴展性的應用程序來說都是必不可少的。你使用表示對話請求的會話來處理單個工作單元,以此來處理事務。

          考慮到工作單元的范圍和事務邊界的劃分,有3中模式:

          • 每次操作一個會話。每次數據庫調用需要一個新會話和事務。因為真實的業務事務通常包含多個此類操作和大量小事務,這一般會引起更多數據庫活動(主要是數據庫每次提交需要將變更刷新到磁盤上),影響應用程序性能。這是一種反模式,不該使用它。
          • 使用分離對象,每次請求一個會話。每次客戶端請求有一個新會話和一個事務,使用Hibernate的“當前會話”特性將兩者關聯起來。
            在一個多層系統中,用戶通常會發起長對話(或應用程序事務)。大多數時間我們使用Hibernate的自動版本和分離對象來實現樂觀并發控制和高性能。
          • 帶擴展(或長)會話的每次對話一會話。在一個也許會跨多個事務的長對話中保持會話開啟。盡管這能把你從重新關聯中解脫出來,但會話可能會內存溢出,在高并發系統中可能會有舊數據。

          你還應該注意以下幾點。 

          • 如果不需要JTA就用本地事務,因為JTA需要更多資源,比本地事務更慢。就算你有多個數據源,除非有跨多個數據庫的事務,否則也不需要JTA。在最后的一個場景下,可以考慮在每個數據源中使用本地事務,使用一種類似“Last Resource Commit Optimization”[16]的技術(見下面的范例6)。
          • 如果不涉及數據變更,將事務標記為只讀的,就像4.3.1提到的那樣。
          • 總是設置默認事務超時。保證在沒有響應返回給用戶時,沒有行為不當的事務會完全占有資源。這對本地事務也同樣有效。
          • 如果Hibernate不是獨占數據庫用戶,樂觀鎖會失效,除非創建數據庫觸發器為其他應用程序對相同數據的變更增加版本字段值。

          范例6

          我們的應用程序有多個在大多數情況下只和數據庫“A”打交道的服務層方法;它們偶爾也會從數據庫“B”中獲取只讀數據。因為數據庫“B”只提供只讀數據,我們對這些方法在這兩個數據庫上仍然使用本地事務。

          服務層上有一個方法設計在兩個數據庫上執行數據變更。以下是偽代碼:

          //Make sure a local transaction on database A exists
          @Transactional (readOnly=false, propagation=Propagation.REQUIRED)
          public void saveIsoBids() {
            //it participates in the above annotated local transaction
            insertBidsInDatabaseA();
          
            //it runs in its own local transaction on database B 
            insertBidRequestsInDatabaseB(); //must be the last operation 

          因為insertBidRequestsInDatabaseB()是saveIsoBids ()中的最后一個方法,所以只有下面的場景會造成數據不一致:

          在saveIsoBids()執行返回時,數據庫“A”的本地事務提交失敗。

          但是,就算saveIsoBids()使用JTA,在兩階段提交(2PC)的第二個提交階段失敗的時候,你還是會碰到數據不一致。因此如果你能處理好上述的數據不一致性,而且不想為了一個或少數幾個方法引入JTA的復雜性,你應該使用本地事務。

          (未完待續)

          posted @ 2011-06-18 11:59 ... 閱讀(179) | 評論 (0)編輯 收藏

          [轉]Spring3.05簡單集成MyBatis3.03

               摘要: 原文: http://blog.csdn.net/sustbeckham/archive/2010/12/17/6082677.aspxmybatis一直沒有發布release版本。所以spring也坐著看。但是spring還是必須用啊。 1. Pojo & mapper配置   view plaincopy to clipboardprint? package&...  閱讀全文

          posted @ 2011-06-18 11:42 ... 閱讀(455) | 評論 (0)編輯 收藏

          <2011年6月>
          2930311234
          567891011
          12131415161718
          19202122232425
          262728293012
          3456789

          導航

          統計

          常用鏈接

          留言簿

          隨筆分類

          隨筆檔案

          收藏夾

          搜索

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 浮梁县| 资阳市| 威远县| 柯坪县| 珠海市| 长顺县| 华亭县| 台前县| 出国| 叙永县| 江孜县| 二连浩特市| 三穗县| 莫力| 南溪县| 长白| 东乡族自治县| 丰台区| 杭锦旗| 阳新县| 邳州市| 卢湾区| 陵川县| 朝阳市| 方正县| 定州市| 石阡县| 民权县| 洮南市| 阳江市| 郓城县| 澎湖县| 巴南区| 通榆县| 陆川县| 嘉义市| 抚顺县| 水富县| 彝良县| 洛扎县| 留坝县|