top
Loading...
DataGrid基于Access的快速分頁法
DataGrid是一個功能非常強大的ASP.NET Web服務器端控件,它除了能夠方便地按各種方式格式化顯示表格中的數據,還可以對表格中的數據進行動態的排序、編輯和分頁。使Web開發人員從繁瑣的代碼中解放。實現DataGrid的分頁功能一直是很多初學ASP.NET的人感到棘手的問題,特別是自定義分頁功能,實現方法多種多樣,非常靈活。本文將向大家介紹一種DataGird控件在Access數據庫下的快速分頁法,幫助初學者掌握DataGrid的分頁技術。

目前的分頁方法

DataGrid內建的分頁方法是使用諸如“SELECT * FROM <TABLE>”的SQL語句從數據庫表中取出所有的記錄到DataSet中,DataGrid控件綁定到該DataSet之后,它的自動分頁功能會幫你從該DataSet中篩選出當前分頁的數據并顯示出來,其他沒有用的數據將被丟棄。

還有一種方法是使用自定義分頁功能,先將DataGrid的AllowCustomPaging屬性設置為True,再利用DataAdapter的Fill方法將數據的篩選工作提前到填充DataSet時,而不是讓DataGrid幫你篩選:

public int Fill (
DataSet dataSet, //要填充的 DataSet。
int startRecord, //從其開始的從零開始的記錄號。
int maxRecords, //要檢索的最大記錄數。
string srcTable //用于表映射的源表的名稱。
);

該方法首先將來自查詢處的結果填充到DataSet中,再將不需要顯示的數據丟棄。當然,自定義分頁功能需要完成的事情還不止這些,本文將在后面詳細介紹。

以上兩種方法的工作原理都是先從數據庫中取出所有的記錄,然后篩選出有用的數據顯示出來。可見,兩種方法的效率基本上是一致的,因為它們在數據訪問階段并沒有采取有效的措施來減少Access對磁盤的訪問次數。對于小數量的記錄,這種開銷可能是比較小的,如果針對大量數據的分頁,開銷將會非常巨大,從而導致分頁的速度非常的慢。換句話說,就算每個DataGrid分頁面要顯示的數據只是一個擁有幾萬條記錄的數據庫表的其中10條,每次DataGrid進行分頁時還是要從該表中取出所有的記錄。

很多人已經意識到了這個問題,并提出了解決方法:用自定義分頁,每次只從數據庫中取出要顯示的數據。這樣,我們需要在SQL語句上下功夫了。由于Access不支持真正的存儲過程,在編寫分頁算法上就沒有SQL Server那么自由了。SQL Server可以在存儲過程中利用臨時表來實現高效率的分頁算法,受到了廣泛的采用。而對于Access,我們必須想辦法在一條SQL語句內實現最高效的算法。

用一條SQL語句取得某段數據的方法有好幾種。算法不同,效率也就不同。我經過粗略的測試,發現效率最差的SQL語句執行時耗費的時間大概是效率最高的SQL語句的3倍!而且這個數值會隨著記錄總數的增加而增加。下面將介紹其中兩條常用的SQL語句。

為了方便接下來的討論,我們先約定如下:

變量說明變量說明
@PageSize每頁顯示的記錄總數@MiddleIndex中間頁的索引
@PageCount分頁總數@LastIndex最后一頁的索引
@RecordCount數據表的記錄總數@TableName數據庫表名稱
@PageIndex當前頁的索引@PrimaryKey主鍵字段名稱
@FirstIndex第一頁的索引@QueryFields要查詢的字段集

變量
定義
@PageCount(int)Math.Ceiling((double)@RecordCount / @PageSize)
@FirstIndex0
@LastIndex@PageCount – 1
@MiddleIndex(int)Math.Ceiling((double)@PageCount / 2) – 1

先讓我們看看效率最差的SQL語句:

SELECT TOP @PageSize * FROM @TableName
WHERE @PrimaryKey NOT IN (
SELECT TOP @PageSize*@PageIndex @PrimaryKey FROM @TableName
ORDER BY @PrimaryKey ASC
) ORDER BY @PrimaryKey ASC

這條SQL語句慢就慢在NOT IN這里,主SELECT語句遍歷的每個@PrimaryKey的值都要跟子SELECT語句的結果集中的每一個@PrimaryKey的值進行比較,這樣時間復雜度非常大。這里不得不提醒一下大家,平時編寫SQL語句時應該盡量避免使用NOT IN語句,因為它往往會增加整個SQL語句的時間復雜度。

另一種是使用了兩個TOP和三個ORDER BY的SQL語句,如下所示:

SELECT * FROM (
SELECT TOP @PageSize * FROM (
SELECT TOP @PageSize*(@PageIndex+1) * FROM @TableName
ORDER BY @PrimaryKey ASC
) TableA ORDER BY @PrimaryKey DESC
) TableB ORDER BY @PrimaryKey ASC


這條SQL語句空間復雜度比較大。如果要顯示的分頁面剛好是最后一頁,那么它的效率比直接SELECT出所有的記錄還要低。因此,對于分頁算法,我們還應該具體情況具體分析,不能一概而論。下面將簡單介紹一下相關概念,如果您對主鍵和索引非常熟悉,可以直接跳過。

有關主鍵和索引的概念

在 ACCESS中,一個表的主鍵(PRIMARY KEY,又稱主索引)必然是唯一索引(UNIQUE INDEX),它的值是不會重復的。除此之外,索引依據索引列的值進行排序,每個索引記錄包含著一個指向它所引用的數據行的指針,這對ORDER BY的執行非常有幫助。我們可以利用主鍵這兩個特點來實現對某條記錄的定位,從而快速地取出某個分頁上要顯示的記錄。

舉個例子,假設主鍵字段為INTEGER型,在數據庫表中,記錄的索引已經按主鍵字段的值升序排好(默認情況下),那么主鍵字段值為“11”的記錄的索引,肯定剛好在值為“12”的記錄的索引前面(假設數據庫表中存在主鍵的值為“12”的記錄)。如果主鍵字段不具備UNIQUE約束,數據庫表中將有可能存在兩個或兩個以上主鍵字段的值為“11”的記錄,這樣就無法確定這些記錄之間的前后位置了。

下面就讓我們看看如何利用主鍵來進行數據的分段查詢吧。

快速分頁法的原理

其實該分頁法是從其他方法衍生而來的。本人對原來的方法認真地分析,發現通過優化和改進可以非常有效地提高它的效率。原算法本身效率很高,但缺乏對具體問題的具體分析。同一個分頁算法,可能在取第一頁的數據時效率非常高,但是在取最后一頁的數據時可能反而效率更低。

經過分析,我們可以把分頁算法的效率狀態分為四種情況:

(1)@PageIndex <= @FirstIndex

(2)@FirstIndex < @PageIndex <= @MiddleIndex

(3)@MiddleIndex < @PageIndex < @LastIndex

(4)@PageIndex >= @LastIndex

狀態(1)和(4)分別表示第一頁和最后一頁。它們屬于特殊情況,我們不必對其使用特殊算法,直接用TOP就可以解決了,不然會把問題復雜化,反而降低了效率。對于剩下的兩種狀態,如果分頁總數為偶數,我們可以看作是從數據庫表中刪掉第一頁和最后一頁的記錄,再把剩下的按前后位置平分為兩部分,即前面的一部分,也就是狀態(2),后面的為另一部分,也就是狀態(3);如果分頁總數為奇數,則屬于中間頁面的記錄歸于前面的部分。這四種狀態分別對應著四組SQL語句,每組SQL語句由升序和降序兩條SQL語句組成。

下面是一個數據庫表,左邊第一列是虛擬的,不屬于該數據庫表結構的一部分,它表示相應記錄所在的分頁索引。該表將用于接下來的SQL語句的舉例中:

PageIndexItemIdProductIdPrice
00010011$12
0020011$13
10030012$13
0040012$11
20050013$14
0060013$12
30070011$13
0080012$15
40090013$12
0100013$11

由表可得:@PageSize = 2,@RecordCount = 10,@PageCount = 5

升序的SQL語句

(1)@PageIndex <= @FirstIndex

取第一頁的數據是再簡單不過了,我們只要用TOP @PageSize就可以取出第一頁要顯示的記錄了。

SELECT TOP @PageSize @QueryFields FROM @TableName WHERE @Condition ORDER BY @PrimaryKey ASC

(2)@FirstIndex < @PageIndex <= @MiddleIndex

把取數據表前半部分記錄和取后半部分記錄的SQL語句分開寫,可以有效地改善性能。后面我再詳細解釋這個原因。現在看看取前半部分記錄的SQL語句。先取出當前頁之前的所有記錄的主鍵值,再從中選出最大值,然后取出主鍵值大于該最大值的前@PageSize條記錄。這里@PrimaryKey的數據類型可以不是INTEGER類型,CHAR、VARCHAR等其他類型照樣可以。

SELECT TOP @PageSize @QueryFields

FROM @TableName

WHERE @PrimaryKey > (

SELECT MAX(@PrimaryKey) FROM (

SELECT TOP @PageSize*@PageIndex @PrimaryKey

FROM @TableName

WHERE @Condition

ORDER BY @PrimaryKey ASC

) TableA

) WHERE @Condition

ORDER BY @PrimaryKey ASC

例如:@PageIndex=1,紅-->黃-->藍


(3)@MiddleIndex < @PageIndex < @LastIndex

接下來看看取數據庫表中后半部分記錄的SQL語句。該語句跟前面的語句算法的原理是一樣的,只是方法稍微不同。先取出當前頁之后的所有記錄的主鍵值,再從中選出最小值,然后取出主鍵值小于該最小值的前@PageSize條記錄。

SELECT * FROM (

SELECT TOP @PageSize @QueryFields

FROM @TableName

WHERE @PrimaryKey < (

SELECT MIN(@PrimaryKey) FROM (

SELECT TOP (@RecordCount-@PageSize*(@PageIndex+1)) @PrimaryKey

FROM @TableName

WHERE @Condition

ORDER BY @PrimaryKey DESC

) TableA

) WHERE @Condition

ORDER BY @PrimaryKey DESC

) TableB

ORDER BY @PrimaryKey ASC

之所以把取數據表前半部分記錄和取后半部分記錄的SQL語句分開寫,是因為使用取前半部分記錄的SQL語句時,當前頁前面的記錄數目隨頁數遞增,而我們還要從這些記錄中取出它們的主鍵字段的值再從中選出最大值。這樣一來,分頁速度將隨著頁數的增加而減慢。因此我沒有這樣做,而是在當前頁索引大于中間頁索引時(@MiddleIndex < @PageIndex)選用了分頁速度隨著頁數的增加而加快的算法。由此可見,假設把所有分頁面劃分為前面、中間和后面三部分,則最前面和最后面的分頁速度最快,最中間的分頁速度最慢。

例如:@PageIndex=3,紅 --> 黃 --> 藍


(4)@PageIndex >= @LastIndex

取最后一頁的記錄可以簡單地使用類似狀態(1)的做法:

SELECT * FROM (

SELECT TOP @PageSize @QueryFields

FROM @TableName

WHERE @Condition

ORDER BY @PrimaryKey DESC

) TableA ORDER BY @PrimaryKey ASC

不過,這樣產生的最后一頁不一定是實際意義上的最后一頁。因為最后一頁的記錄數未必剛好跟@PageSize相等,而上面的SQL語句是直接取得倒數的@PageSize條記錄。如果想要精確地取得最后一頁的記錄,應該在先計算出該頁的記錄數,作為TOP語句的條件:

SELECT * FROM (

SELECT TOP (@RecordCount-@PageSize*@LastIndex) @QueryFields

FROM @TableName WHERE @Condition

ORDER BY @PrimaryKey DESC

) TableA ORDER BY @PrimaryKey ASC

降序的SQL語句

降序的SQL語句跟升序的大同小異,這里就不在羅嗦了J

(1)@PageIndex <= @FirstIndex

SELECT TOP @PageSize @QueryFields FROM @TableName WHERE @Condition ORDER BY @PrimaryKey DESC

(2)@FirstIndex < @PageIndex <= @MiddleIndex

SELECT TOP @PageSize @QueryFields

FROM @TableName

WHERE @PrimaryKey < (

SELECT MIN(@PrimaryKey) FROM (

SELECT TOP @PageSize*@PageIndex @PrimaryKey

FROM @TableName

WHERE @Condition

ORDER BY @PrimaryKey DESC

) TableA

) WHERE @Condition

ORDER BY @PrimaryKey DESC

(3)@MiddleIndex < @PageIndex < @LastIndex

SELECT * FROM (

SELECT TOP @PageSize @QueryFields

FROM @TableName

WHERE @PrimaryKey > (

SELECT MAX(@PrimaryKey) FROM (

SELECT TOP (@RecordCount-@PageSize*(@PageIndex+1)) @PrimaryKey

FROM @TableName

WHERE @Condition

ORDER BY @PrimaryKey ASC

) TableA

) WHERE @Condition

ORDER BY @PrimaryKey ASC

) TableB ORDER BY @PrimaryKey DESC

(4)@PageIndex >= @LastIndex

SELECT * FROM ( SELECT TOP (@RecordCount-@PageSize*@LastIndex) @QueryFields
FROM @TableName WHERE @Condition ORDER BY @PrimaryKey ASC
) TableA ORDER BY @PrimaryKey DESC

如何動態產生上述的SQL語句?

看了上面的SQL語句之后,相信大家已經基本明白該分頁法的原理了。下面,我們將要設計一個動態生成SQL語句的類FastPaging。該類有一個公有靜態方法,它根據您給出的條件動態生成SQL語句,作為方法的返回值。

// 產生根據指定字段排序并分頁查詢的 SELECT 語句。

public static String Paging(

int pageSize, //每頁要顯示的記錄的數目。

int pageIndex, //要顯示的頁的索引。

int recordCount, //數據表中的記錄總數。

String tableName, //要查詢的數據表。

String queryFields, //要查詢的字段。

String primaryKey, //主鍵字段。

bool ascending, //是否為升序排列。

String condition //查詢的篩選條件。

) {

StringBuilder sb = new StringBuilder();

int pageCount = GetPageCount(recordCount,pageSize); //分頁的總數

int middleIndex = GetMidPageIndex(pageCount); //中間頁的索引

int firstIndex = 0; //第一頁的索引

int lastIndex = pageCount - 1; //最后一頁的索引

if (pageIndex <= firstIndex) {

// 代碼略

} else if (pageIndex > firstIndex && pageIndex <= middleIndex) {

sb.Append("SELECT TOP ").Append(pageSize).Append(" ")

.Append(queryFields).Append(" FROM ").Append(tableName)

.Append(" WHERE ").Append(primaryKey);

if (ascending)

sb.Append(" > (").Append(" SELECT MAX(");

else

sb.Append(" < (").Append(" SELECT MIN(");

sb.Append(primaryKey).Append(") FROM ( SELECT TOP ")

.Append(pageSize*pageIndex).Append(" ").Append(primaryKey)

.Append(" FROM ").Append(tableName);

if (condition != String.Empty)

sb.Append(" WHERE ").Append(condition);

sb.Append(" ORDER BY ").Append(primaryKey).Append(" ")

.Append(GetSortType(ascending)).Append(" ) TableA )");

if (condition != String.Empty)

sb.Append(" AND ").Append(condition);

sb.Append(" ORDER BY ").Append(primaryKey).Append(" ")

.Append(GetSortType(ascending));

}

else if (pageIndex > middleIndex && pageIndex < lastIndex) {

// 代碼略

} else if (pageIndex >= lastIndex) {

// 代碼略

}

return sb.ToString();

}

除了Paging方法還有另外幾個方法:

// 根據記錄總數和分頁大小計算分頁數。

public static int GetPageCount(int recordCount, int pageSize) {

return (int)Math.Ceiling((double)recordCount/pageSize);

}

// 計算中間頁的頁索引。

public static int GetMidPageIndex(int pageCount) {

return (int)Math.Ceiling((double)pageCount/2) - 1;

}

// 獲取排序的方式("ASC"表示升序,"DESC"表示降序)

public static String GetSortType(bool ascending) {

return (ascending ? "ASC" : "DESC");

}

// 獲取一個布爾值,該值指示排序的方式是否為升序。

public static bool IsAscending(String orderType) {

return ((orderType.ToUpper() == "DESC") ? false : true);

}

讓DataGrid工作起來

有了上面的類,實現分頁的工作就簡單多了。首先,我們要將DataGrid的AllowPaging屬性和AllowCustomPaging屬性為True,除此之外,為了體現出升序和降序的功能,還需要將AllowSorting屬性也設置為True。然后在每次分頁時,我們需要產生一個OleDbDataReader對象或DataView對象綁定到DataGrid,作為DataGrid的數據源。這里需要用FastPaging類的Paging方法根據條件產生一個SQL語句,并賦給OleDbCommand對象的CommandText屬性:

cmd.CommandText = FastPaging.Paging(

DataGrid1.PageSize,

(int)ViewState["CurrentPageIndex"],

DataGrid1.VirtualItemCount,

"Items",

"ItemId, ProductId, Price",

"ItemId",

FastPaging.IsAscending(OrderType),

""

);

在上面的程序段中,ViewState["CurrentPageIndex"]的值在DataGrid的Page事件處理程序中被更新為e.NewPageIndex。為了方便處理ViewState的空值,最好把對ViewState["CurrentPageIndex"]的存取操作和空值判斷封裝在一個屬性里。DataGrid1. VirtualItemCount應該設置為數據庫表中的記錄總數。DataGrid通過它和PageSize屬性可以虛擬出DataGrid的分頁數。VirtualItemCount的值是在Page的Load事件處理程序中被設置的,而該值的大小需要經過一次數據庫訪問才能得到。為了提高性能,可以只在第一次加載頁面的時候設置該值。

總結

DataGrid基于Access的快速分頁法到這里就介紹完了。當然,這種方法并不能“包治百病”,可能對于您的要實現的功能,還有其它更好的方法。這就需要大家在平時工作和學習中不斷總結經驗,在解決實際問題時盡可能找到最有效的方法。這也是本文的方法中所貫穿的思想。

作者:http://www.zhujiangroad.com
來源:http://www.zhujiangroad.com
北斗有巢氏 有巢氏北斗