數據庫分頁查詢一般分為兩步,
(1)根據查詢條件,count 記錄總數 (2)根據當前頁的數據范圍(起始位置offset, 每頁數據個數span),從符合查詢條件的記錄集 取出對應范圍的數據。 一、根據范圍取數據的方法
如果單純用JDBC從ResultSet中取出一個指定范圍(offset, span)的數據,可以采用這樣的方法。 ps = con.prepareStatement(sql, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); ps.setMaxRows(offset + span); rs = ps.executeQuery(); rs.absolute(offset); while(rs.next())... 數據量大的時候,頁數很多,offset很大,這種方法不太適合。這時候,需要使用各數據庫的native SQL特性。 我們來看Hibernate dialect package的類,支持了各種數據庫的getLimitString方法。這里舉Mysql和Oracle的例子。假設查詢語句為 Select * from message where forum_id = ? and created_time > ? order by created_time desc 那么,Mysql 的limit SQL為 Select * from message where forum_id = ? and created_time > ? order by created_time desc limit ?, ? 后面的兩個limit ?, ? 分別為 offset, span。 Oracle的limit SQL為 select * from ( select row_.*, rownum rownum_ from ( select * from message where forum_id = ? and created_time > ? order by created_time desc ) row_ where rownum <= ?) where rownum_ > ? 后面的兩個limit ?, ? 分別為 offset + span, span。 二、緩存 & QueryKey
count語句可以根據查詢語句自動生成,比如 Select count(*) from ( Select * from message where forum_id = ? and created_time > ? order by created_time desc ) 這樣的自動count語句有些浪費,用了子查詢不說,還保留了沒有必要的order by。最好還是另外提供一個count語句。 Select count(*) from message where forum_id = ? and created_time > ? 在多頁翻動的情況下,這個count語句要被反復執(zhí)行。為了提高效率,我把這個count結果保存在全局緩存中,不僅本Session用戶可以重復使用,其他用戶在根據同樣條件翻找message的時候,也可以重復使用這個結果。 我在持久層中使用通用的QueryKey做為緩存鍵值。 QueryKey分成三個部分,SQL, Parameters, Range。比如: Query Key: SQL : Select count(*) from message where forum_id = ? and created_time > ? Parameters : [buaawhl, time long value] Range: (0, 1) 這個QueryKey的效率很關鍵。主要是hashCode和equals兩個方法的效率。 我們知道,當key放在Map等Hash數據結構中,首先hashCode,然后用equals比較hashCode后面的一串key。 舉個例子。Key1和key2 的hashCode一樣,都和key3的hashCode不一樣。 … [ 101 ] -> key1 -> key2 … [ 666 ] -> key3 … 可以看到,hashCode,equals,這兩個方法都是每次查找緩存都要調用的方法。尤其是equals方法更是重中之重,很可能需要被調用多次。 hashCode的優(yōu)化實現(xiàn)相對來說比較簡單,只要根據QueryKey中各部分的不同,盡量實現(xiàn)hashCode取值的擴散化,降低hashCode的重復率就可以了。 關鍵是equals的實現(xiàn)方案。這里有個原則,越小的結構越先比較,可以提高比較速度。 QueryKey中的parameters和range比較好辦。每次equals比較的時候,先比較range,如果不相等,返回false; 如果相等,再比較Parameters,如果有一個parameter value不相等,返回false。這樣,我們可以用很短的時間開銷 過濾掉一大批不相等的QueryKey。 但是parameters和range都相等的時候,我們還是無可避免的要比較SQL。String的equals方法如下: // from jdk src //這個方法沒有比較hashCode,直接比較長度和字符 public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = count; if (n == anotherString.count) { char v1[] = value; char v2[] = anotherString.value; int i = offset; int j = anotherString.offset; while (n-- != 0) { if (v1[i++] != v2[j++]) return false; } return true; } } return false; } 我們看到,兩個相同的長String具有不同的reference,那么比較起來是相當消耗時間的。所以說,字符串比較,不怕不同,就怕相同。大部分情況下,不同的String的長度不同,或者前幾個字符串開始就不相同,很快就能夠得出比較結果。 當然也有這種情況,兩個SQL String都很長,而且長度相等,而且前面大部分字符相同的時候,到了后面才有字符的不同。比如, Select * from message where forum_id = ? and created_time > ? order by created_time desc 和 Select * from message where forum_id = ? and created_time > ? order by updated_time desc 這兩個String的長度相等,前面大部分也相等,只有走到cre 和 upd 的時候,才能比較出不相同。如果兩個字符串內容一樣,那更是要走到頭,才能判斷出兩個字符串完全一樣了。 我的第一個做法就是,盡量使用static final String做為QueryKey的SQL。這樣兩個SQL的reference如果相等,那么可以迅速判斷出兩個SQL相同。 這個做法只能處理事先定義好的SQL語句,但實際需求中,存在很多需要動態(tài)拼接SQL的情況,不可能做到所有相同的SQL具有相同的reference。 我又采取了第二個做法:分而治之,把一個SQL String拆分成多個SQL常量的數組;泛化SQL的類型,SQL不限制為String類型,也可以是String[]類型。 比如。 String[] sql1 = { “Select * from message where forum_id = ?”, “ and created_time > ?”, “ order by ”, “created_time”, “desc” }; 和 String[] sql2 = { “Select * from message where forum_id = ?”, “ and created_time > ?”, “ order by ”, “created_time”, “desc” }; 和 String[] sql3 = { “Select * from message where forum_id = ?”, “ and created_time > ?”, “ order by ”, “updated_time”, “desc” }; 這個時候,比較sql1和sql2和sql3的效率就會大大提高,雖然sql1 和 sql2兩個數組的長度相等,還是要一個元素一個元素的比較,但由于里面大量用到了String常量,相同的String常量具有相同的reference,所以5步下來,就可以判斷出sql1和sql2數組的元素是完全相等的;4步下來,加上第一個字符的比較,就可以判斷sql1和sql3的第4個元素是不相等的。 我們看到,做法1和做法2,能夠提高SQL的比較效率,大部分情況下,也許比parameters的比較還快。 三、定長預取
多用戶訪問同一頁面的可能性比較大的情況下,比如,論壇的某些熱門話題,很可能被多人同時翻閱。這時候,如果把根據范圍取出的數據對象List也按照QueryKey存入緩存中,那么就可以大大提高響應速度,減輕數據服務器負擔,當然,你的Web Server的內存負擔也大大增加了。:-) 我們進一步考慮下面兩種情況: 1. 用戶自定義頁面記錄數 一般來說,用戶可以自定義自己的每頁顯示記錄個數,比如,有些用戶喜歡每頁20條,有的喜歡每頁10條。 假設用戶A翻到一個論壇的第一頁,顯示1 – 20條信息;用戶B翻到同一個論壇的第一頁,顯示1 – 10條信息。這個時候,緩存的命中率是很低的。用戶A和用戶B無法共享緩存信息。因為他們的range(的span)總是不同,QueryKey永遠不可能相同。 2. 記錄很多、每頁記錄數過少 假設一個論壇里面有1000條信息,每頁顯示10條,那么共有100頁。如果用戶一頁一頁的翻動,每次程序發(fā)出一個span大小為10的Query請求,取出10條記錄,根據QueryKey緩存起來。由于頁面記錄數過少,每次數據庫查詢的效率很低,緩存命中率也很低。 為了提高緩存命中率,并且順便實現(xiàn)數據預取功能,我們可以采取 同一定長Span的方案。比如,還是上面的例子,我們在程序中設定統(tǒng)一Span大小為100。 當用戶A請求1 – 10的記錄的時候,程序判斷這個落在 1 – 100的范圍內,那么用range (1, 100)獲取100條記錄,把前面的10條返回給用戶。當用戶A翻了一頁,請求11 – 20的記錄的時候,程序判斷還是落在 1 – 100的范圍內,而且已經存在于緩存中,那么直接把對應的11 – 20條返回給用戶A就可以。 當用戶B 請求1 – 20的記錄的時候,程序判斷這個落在 1 – 100的范圍內,而且已經存在于緩存中,那么直接把對應的1 – 20條返回給用戶B就可以。 可以看到,這種定長預取方案能夠大大提高數據庫查詢的效率和緩存的命中率。 [點擊此處收藏本文] 發(fā)表于 2005年01月08日 2:56 PM shaokun305 發(fā)表于2005-02-22 9:04 AM
Ping Back來自:blog.csdn.net buaawhl 發(fā)表于2005-05-31 8:37 AM
Ping Back來自:blog.csdn.net lovefanx 發(fā)表于2005-06-01 11:40 PM
你說的這個把sql串變成字符串數組來比較的方法,還是頭回聽說,確實很長見識:) buaawhl 發(fā)表于2005-06-02 11:41 AM
thanks. :-) 隨著對Hibernate, JDO的逐步深入了解,還有對象數據庫Cache‘ 和 Objectivity的了解,Lightor的緩存策略還在發(fā)展中。 (1)為了進一步提高命中率,減少不必要的緩存清空,或者減少緩存清空的粒度, (2) 為了讓相關的數據庫對象、頁面資源對象都放在一起,共同清空。 Lightor引入了多級緩存機制。 帶來的好處是,性能提高;Cluster支持良好;命中率高。 帶來的壞處是,Cache控制代碼對 業(yè)務邏輯代碼的侵入性高,編程復雜。所以,要控制好,一般把Cache 控制代碼局限在DAO層以內,不擴散到DAO層以外。當然,有的時候,為了更好的性能/命中率,Cache 控制代碼有可能需要進入業(yè)務邏輯,這個就一定要做好代碼的包裝:提供一個沒有緩存控制的業(yè)務方法;提供一個有緩存控制的包裝方法。對于一些通用的緩存控制,可以采用AOP實現(xiàn)。 |
|
|