基於SQL詞法分析的多種數據庫自動分頁方案
(原文見 http://www.pwmis.cn/bbs/dispbbs.asp?boardID=8&ID=595 )
一、背景:
1,巨大的數據處理量;
2,快速分頁;
3,動態查詢(列表顯示的項目);
4,適應多種數據庫;
5,適應多表查詢;
6,適應無主鍵或主鍵無序的情況;
二、算法:
一般快速分頁都是採用基於有序主鍵採用二分查找排序的算法,可以應對大數據量的分頁要求。
但是實際運用中往往需要對某些非主鍵字段排序分頁,或者有些查詢沒有主鍵,或者主鍵是無序的。
本文采用兩種分法來處理這幾類情況:
(注意:下面的算法中均以降序爲準,1,2 方法考慮在SQL SERVER 及ACCESS 中的實現方法)
1,有主鍵,並且主鍵是有序的:
SELECT TOP @@PageSize * FROM
(SELECT TOP @@PageSize * FROM
(SELECT TOP @@Page_Size_Number [@@FieldList]
FROM (@@DataSourceTable) st
@@Where
ORDER BY @@PrimaryKey DESC
) t1 ORDER BY @@PrimaryKey ASC
) t2 ORDER BY @@PrimaryKey DESC
說明:在查詢中,使用主鍵排序比採用其它字段排序擁有極高的效率。
[@@DataSourceTable] 可能是一個單表的查詢,例如:
SELECT * FROM USERINFO WHERE USERCLASS='1' ORDER BY USERNAME DESC
也可能是一個複雜的多表查詢,例如:
SELECT A.ArticleID, A.ClassID, A.Title, A.CreateTime, A.Hints, A.Writer ,A.PaperDate,A.FromOffice,C.ClassName AS ClassName
FROM TB_OA_Article A
INNER JOIN TB_OA_Class C ON A.ClassID = C.ClassID
或者是一個視圖,例如:
SELECT * FROM VIEW1
[@@Where] 表示要多數據源的篩選條件。
注:把數據篩選條件放到這一層次可以避免 [@@DataSourceTable] 中有複雜的多表查詢的情況引起“字段不明確”的問題。
***以下如果沒有特殊說明,出現的相同單詞均以此處爲準。***
2,有主鍵,同時主鍵可能是無序的;或者沒有主鍵:
SELECT TOP @@PageSize * FROM
(SELECT TOP @@PageSize * FROM
(SELECT TOP @@Page_Size_Number [@@FieldList]
FROM (@@DataSourceTable) st
@@Where
ORDER BY @@OrderField DESC
) t1 ORDER BY @@OrderField ASC
) t2 ORDER BY @@OrderField DESC
說明:
該方式可以適應 各種分頁及排序的要求。
[@@OrderField] 可以不是主鍵,可以有多個,但是必須分別指明排序方式 [desc]/[asc]
3,Oracle 分頁算法:
select [@@FieldList] from (
select [@@FieldList],rownum rn from
(SELECT [@@FieldList] FROM ( @@DataSourceTable ) ST0 @@Where order by @@OrderField DESC ) st
where rownum <= @@Page_Size_Number
)
where rn >= (@@Page_Size_Number-@@PageSize)
說明:
在Oracle中,由於虛擬列 rownum 實在排序之後輸出的,所以必須採用嵌套的查詢來獲取制定範圍內的數據。
三、實現方案:
根據上面的算法,可以發現它們構造的SQL語句及其相似,算法1其實就是算法2的特例,在實現本文的分頁方案的時候,
只需要判斷是否提供了主鍵即可。
另外,爲了儘可能提高效率,在查詢第一頁和最後一頁的時候,需要分別處理(ORACLE 不需要下面的處理):
1,查詢第一頁:
SELECT TOP @@PageSize [@@FieldList]
FROM (@@DataSourceTable) st
@@Where
ORDER BY @@OrderField DESC
說明:對於第一頁,直接採用Top 方式,即可以擁有很高的效率。
2,查詢最後一頁:
SELECT * FROM
(SELECT Top @@LeftSize [@@FieldList]
FROM (@@DataSourceTable) st
@@Where
ORDER BY @@OrderField ASC
) T1 ORDER BY @@OrderField DESC
說明:對於最後一頁,只需要第一次取數據的時候倒序,第二次正序即可。注意這裏,@@LeftSize 的大小,
可能小於 @@PageSize,需要計算它的大小,這樣,在處理所有的分頁的時候,涉及到一個計算最後一頁大小的問題。
假設 符合條件的所有記錄數量爲 AllCount,每一頁的大小爲 PageSize,當前頁碼爲 PageNumber,
那麼計算最後一頁大小的方法如下:
if(PageSize*PageNumber>AllCount) //最後的頁 @@LeftSize
LeftSize=AllCount-PageSize*(PageNumber-1);
else
LeftSize=PageSize;
取得符合條件的記錄數的查詢:
SELECT COUNT(*) FROM (@@DataSourceTable) st
@@Where
說明:統計記錄數不再需要 Order by 子句。如果有主鍵,也可以採用下面的語句提高效率:
SELECT COUNT(@PrimaryKey) FROM (@@DataSourceTable) st
@@Where
-----------------------------------------------------------------
好了,上面已經分析了各種情況下處理分頁及排序的最佳算法,現在涉及到真正實現上面算法的時候了,
根據上面的算法,我們可以正確地寫出適應不同情況的分頁排序SQL 語句,但是做一次分頁排序需要寫
4條SQL語句,在一個項目中這樣的SQL語句將會多得嚇人(我們目前的一個方案就是採用每次查詢分頁手
寫四條SQL的方式),有沒有更好的辦法來解決這個問題呢?
[解決方案]
******************採用 SQL 詞法分析 ************************
一提到“詞法分析” 有點像編譯語言的詞法分析了,其實在每一個SQL語句中,不管查詢有多麼複雜,
都可以分爲以下4部分,而且是順序鐵定的4部分:
SELECT ... FROM ... [WHERE ...] [ORDER BY ...]
其中,Where 子句和 Order by 子句都是可選的,而 Select 子句中的某一個字段可以是一個子查詢,
From 子句 可以是一個或者多個表,甚至是一個( ) 中的子查詢。但是根據 SQL 語句中的這幾部分
特別的語法順序,我們自動分離出每一個子句是不成問題的,這裏只要注意 帶 ( ) 的子查詢就是了。
所以,在我們需要對一次查詢作分頁以及排序的時候,我們只需要寫出上面的標準SQL 語句即可,然後
程序可以取得我們要篩選的條件,待排序的字段及方式,自動地完成符合特定數據庫的分頁查詢語句。
在用戶給出的 一條查詢 SQL 中,我們可以簡單的對其進行字符串處理,就可以把各個子句提取出來,
然後根據上面的算法,構造出與具體數據庫無關分頁查詢語句。
**********************特別說明**********************************************
在最終實現的時候,用戶給的SQL 語句,其實就是上面的 “@@DataSourceTable”,之所以要這麼處理,
就是因爲用戶的這個SQL 語句,可能是一個簡單的單表查詢,可以能是一個複雜的多表連接查詢,甚至
是一個存儲過程(如果是存儲過程的話,那麼需要手工指明需要返回那些字段以及待排序的字段)
*****************************************************************************
ok,下面我們來進行第一步,“ SQL 詞法分析”
(2006.5.29 ,未完待續)
我們先來看幾個典型的查詢語句:
1,簡單的單表查詢:
SELECT UserID,UserName,UserClass FROM USERINFO WHERE USERCLASS='1' ORDER BY USERNAME DESC
分析:對該表進行簡單的單詞查找,很容易取得Select ,From ,Where ,Order by 子句。
經過轉換,將構造如下語句:
SELECT TOP @@PageSize * FROM
(SELECT TOP @@PageSize * FROM
(SELECT TOP @@Page_Size_Number UserID,UserName,UserClass
FROM USERINFO
WHERE USERCLASS='1'
ORDER BY USERNAME DESC
) t1 ORDER BY USERNAME ASC
) t2 ORDER BY USERNAME DESC
注意:爲了以後有可能將該查詢結果綁定到數據表格,然後通過模糊查找表格中顯示的文字內容(不限列),
有必要加上 @@Where 替換參數(指在運行時用文本方式替換重新生成一條查詢語句),上面的查詢將變成:
SELECT TOP @@PageSize * FROM
(SELECT TOP @@PageSize * FROM
(SELECT TOP @@Page_Size_Number * FROM
(SELECT UserID,UserName,UserClass
FROM USERINFO
WHERE USERCLASS='1'
) ST
@@Where ORDER BY USERNAME DESC
) t1 ORDER BY USERNAME ASC
) t2 ORDER BY USERNAME DESC
注意:@@Where ORDER BY USERNAME DESC 中的 Order by 子句,放在最內的一層原是查詢語句的外面了,
爲什麼要這樣做呢?在SQL SERVER 中,“除非指定了Top謂詞,否則在嵌套的查詢語句中不允許使用
Order by 子句”!所以對原始SQL 語句中的排序條件的處理就很重要了,對於單表的查詢還比較容易處理,
但是對於複雜的查詢就比較困難了,接着看下面的情況。
2,複雜的多表連接查詢:
select
B.BussinessID,B.ApplyCompany,B.Address,B.AddressNo,
M.MPID,M.Creator,M.CreateDate as PlanCreateDate,M.State,
(select Top 1 UserName from TB_Common_Uesr where UserID=M.Creator order by userid desc) as CreatorName
FROM TB_Flow_Bussiness B
Left join TB_Flow_MeasurePlan M on B.BussinessID=M.BussinessID
WHERE M.State='01'
ORDER BY PlanCreateDate DESC
注意:在子查詢
select Top 1 UserName from TB_Common_Uesr where UserID=M.Creator order by userid desc
中,使用了order by 謂詞,在主查詢中,也使用了order by 謂詞,所以不能夠簡單的查找
SELECT,FROM,WHERE,ORDER BY 謂詞的位置來確定相應的子句內容。
實際上,我們關心的只是最外層的 ORDER BY 子句,通過確定的查詢謂詞的順序,我們很容易找到該
ORDER BY 子句。經過轉換,將生成如下的語句:
SELECT Top 10 * from
(select B.BussinessID,B.ApplyCompany,B.Address,B.AddressNo,
M.MPID,M.Creator,M.CreateDate as PlanCreateDate,M.State,
(select Top 1 UserName from TB_Common_Uesr where UserID=M.Creator order by userid desc) as CreatorName
FROM TB_Flow_Bussiness B
Left join TB_Flow_MeasurePlan M on B.BussinessID=M.BussinessID
WHERE M.State='01'
) ST
where bussinessid like '%2006%' and CreatorName='XIAO'
ORDER BY PlanCreateDate DESC
如果最外層的Order by 子句中的 排序字段包含 “數據表.字段名” 這樣的限定標記,應該替換隻有字段名
的標記,例如上面如果還以 M.MPID 排序,那麼在外層的排序語句中,應該使用 MPID 的形式。
3,從另外的查詢結果集中進行的查詢:
指的是如下的查詢方式:
SELECT [@@FieldList]
FROM (@@DataSourceTable) st
Where @@WhereStr
ORDER BY @@OrderField ASC
其中,@@DataSourceTable 是另外一個查詢結果集,整個查詢的實例可能如下:
SELECT * from
(select B.BussinessID,B.ApplyCompany,B.Address,B.AddressNo,
M.MPID,M.Creator,M.CreateDate as PlanCreateDate,M.State,
(select Top 1 UserName from TB_Common_Uesr where UserID=M.Creator order by userid desc) as CreatorName
FROM TB_Flow_Bussiness B
Left join TB_Flow_MeasurePlan M on B.BussinessID=M.BussinessID
WHERE M.State='01'
) ST
where bussinessid like '%2006%' and CreatorName='XIAO'
ORDER BY PlanCreateDate DESC
這樣我們不管另外的查詢結果集有多麼複雜,都可以歸結到上面的公式中
4,從“存儲過程結果集”中進行的查詢:
在SQL2000以上,支持一種特殊的存儲過程--用戶自定義函數,它可以表的形式返回一個查詢結果集,
這樣我們就能 從“存儲過程結果集”中進行的查詢了,請看下面的例子:
--從函數中選擇結果集示例:
CREATE FUNCTION FUN_TEST1
(@STATE VARCHAR(10))
RETURNS TABLE --返回表類型
AS
RETURN
(SELECT B.BussinessID,B.ApplyCompany,B.Address,B.AddressNo,
M.MPID,M.Creator,M.CreateDate as PlanCreateDate,M.State
FROM TB_Flow_Bussiness B
Left join TB_Flow_MeasurePlan M on B.BussinessID=M.BussinessID
WHERE M.State=@STATE
)
GO
--從函數中選擇結果集
SELECT BussinessID,AddressNo,PlanCreateDate
FROM DBO.FUN_TEST1 ('01')
ORDER BY MPID
GO
看到了嗎?我們的From 子句中,可以包含一個用戶自定義函數,這樣我們就可以把很複雜的查詢寫在
SQLSERVER的用戶自定義函數中,在實際的使用的時候就很簡單了:)
由於從“存儲過程結果集”中進行的查詢SQL語句比較簡單,分析處理比較容易,用它構建分頁就很容易了.
(既然查詢都寫到了用戶函數中了,幹嗎不直接在存儲過程中分頁?我們這裏只是提供這麼一種可能.)
總結:
不管對於各種不同的查詢,只要我們能夠正確地提取Select ,From ,Where ,Order by 子句,我們就能夠
構建出本文所述的分頁排序語句來,從而將目前複雜的分頁問題減輕到最低程度.
到現在爲止,我們可以進行實戰了,"編程分析SQL語句"!
(2006.5.30 ,未完待續)
* 函數 MakeSQLStringByPage 在此基礎上實現了更爲複雜的分頁處理,這裏的複雜時說查詢
* 包含大量的子查詢或者連接查詢,因此評價查詢複雜與否採用下面的標準:
*
* 只包含一個 SELECT 謂詞;
* 沒有 INNER JOIN,RIGHT JOIN,LEFT JOIN 等表連接謂詞;
* 謂詞 FROM 後只能有一個表名;
*
* 否則,視爲該查詢爲一個複雜查詢,採用複雜查詢分頁方案;
** 約束:
使用該分頁方法要求 SQL語句本身必須滿足下列條件:
1,最外層的查詢不能含有 TOP 謂詞;
2,最外層查詢必須含有 ORDER BY 語句;
3,不能含有下列替換參數(區分大小寫):@@PageSize,@@Page_Size_Number,@@LeftSize,@@Where
4,SQL必須符合 SQL-92 以上標準,且 最外層ORDER BY 語句之後不能有其他語句,
Group by 等放在Order by 之前;
**
*/
/// <summary>
/// SQL SERVER 分頁處理,自動識別標準SQL語句並生成適合分頁的SQL語句
/// </summary>
public class SQLPage
{
public SQLPage()
{
//
// TODO: 在此處添加構造函數邏輯
//
}
/// <summary>
/// 生成SQL分頁語句,記錄總數爲0表示生成統計語句
/// </summary>
/// <param name="strSQLInfo">原始SQL語句</param>
/// <param name="strWhere">在分頁前要替換的字符串,用於分頁前的篩選</param>
/// <param name="PageSize">頁大小</param>
/// <param name="PageNumber">頁碼</param>
/// <param name="AllCount">記錄總數</param>
/// <returns>生成SQL分頁語句</returns>
public static string MakeSQLStringByPage(string strSQLInfo,string strWhere,int PageSize,int PageNumber,int AllCount)
{
string strSQLType=string.Empty ;
if(AllCount!=0)
{
if(PageNumber==1) //首頁
{
strSQLType="First";
}
else if(PageSize*PageNumber>AllCount) //最後的頁 @@LeftSize
{
PageSize=AllCount-PageSize*(PageNumber-1);
strSQLType="Last";
}
else //中間頁
{
strSQLType="Mid";
}
}
else //特殊處理
{
strSQLType="Count";
}
string SQL=MakeSQLStringByPage( strSQLInfo, strSQLType);
//執行分頁參數替換
SQL=SQL.Replace ("@@PageSize",PageSize.ToString ())
.Replace ("@@Page_Size_Number",Convert.ToString (PageSize * PageNumber))
.Replace ("@@LeftSize",PageSize.ToString ())
.Replace ("@@Where",strWhere);
return SQL;
}
private static string MakeSQLStringByPage(string strSQLInfo,string strSQLType)
{
#region SQL 複雜度分析
//SQL 複雜度分析 開始
bool SqlFlag=true;//簡單SQL標記
string TestSQL=strSQLInfo.ToUpper ();
int n=TestSQL.IndexOf ("SELECT ",0);
n=TestSQL.IndexOf ("SELECT ",n+7);
if(n==-1)
{
//可能是簡單的查詢,再次處理
n=TestSQL.IndexOf (" JOIN ",n+7);
if(n!=-1) SqlFlag=false;
else
{
//判斷From 謂詞情況
n=TestSQL.IndexOf("FROM ",9);
if(n==-1) return "";
//計算 WHERE 謂詞的位置
int m=TestSQL.IndexOf ("WHERE ",n+5);
// 如果沒有WHERE 謂詞
if(m==-1) m=TestSQL.IndexOf ("ORDER BY ",n+5);
//如果沒有ORDER BY 謂詞,那麼無法排序,退出;
if(m==-1) return "";
string strTableName=TestSQL.Substring (n,m-n);
//表名中有 , 號表示是多表查詢
if(strTableName.IndexOf (",")!=-1)
SqlFlag=false;
}
}
else
{
//有子查詢;
SqlFlag=false;
}
//SQL 複雜度分析 結束
#endregion
#region 排序語法分析
//排序語法分析 開始
int iOrderAt=strSQLInfo.ToLower ().LastIndexOf ("order by ");
//如果沒有ORDER BY 謂詞,那麼無法排序分頁,退出;
if(iOrderAt==-1) return "";
string strOrder=strSQLInfo.Substring (iOrderAt+9);
strSQLInfo=strSQLInfo.Substring(0,iOrderAt);
string[] strArrOrder=strOrder.Split (new char []{','});
for(int i=0;i<strArrOrder.Length ;i++)
{
string[] strArrTemp=(strArrOrder[i].Trim ()+" ").Split (new char[]{' '});
//壓縮多餘空格
for(int j=1;j<strArrTemp.Length ;j++)
{
if(strArrTemp[j].Trim ()=="")
{
continue;
}
else
{
strArrTemp[1]=strArrTemp[j];
if(j >1 ) strArrTemp[j]="";
break;
}
}
//判斷字段的排序類型
switch(strArrTemp[1].Trim ().ToUpper ())
{
case "DESC":
strArrTemp[1]="ASC";
break;
case "ASC":
strArrTemp[1]="DESC";
break;
default:
//未指定排序類型,默認爲降序
strArrTemp[1]="DESC";
break;
}
//消除排序字段對象限定符
if(strArrTemp[0].IndexOf (".")!=-1)
strArrTemp[0]=strArrTemp[0].Substring (strArrTemp[0].IndexOf (".")+1);
strArrOrder[i]=string.Join (" ",strArrTemp);
}
//生成反向排序語句
string strNewOrder=string.Join (",",strArrOrder).Trim ();
strOrder=strNewOrder.Replace ("ASC","ASC0").Replace ("DESC","ASC").Replace ("ASC0","DESC");
//排序語法分析結束
#endregion
#region 構造分頁查詢
string SQL=string.Empty ;
if(!SqlFlag)
{
//複雜查詢處理
switch(strSQLType.ToUpper ())
{
case "FIRST":
SQL="Select Top @@PageSize * FROM (/n" +strSQLInfo+
"/n) T0 @@Where ORDER BY "+strOrder;
break;
case "MID":
SQL=@"SELECT Top @@PageSize * FROM
(SELECT Top @@PageSize * FROM
(
SELECT Top @@Page_Size_Number * FROM (";
SQL+="/n"+strSQLInfo+" ) P_T0 @@Where ORDER BY "+strOrder+"/n";
SQL+=@") P_T1
ORDER BY "+ strNewOrder +") P_T2 /n"+
"ORDER BY "+strOrder;
break;
case "LAST":
SQL=@"SELECT * FROM (
Select Top @@LeftSize * FROM ("+"/n/r"+strSQLInfo+"/r";
SQL+=" ) P_T0 @@Where ORDER BY "+ strNewOrder+"/n/r"+
" ) P_T1 ORDER BY "+strOrder;
break;
case "COUNT":
SQL="Select COUNT(*) FROM ( " +strSQLInfo+" ) P_Count @@Where";
break;
default:
SQL=strSQLInfo+strOrder;//還原
break;
}
}
else
{
//簡單查詢處理
switch(strSQLType.ToUpper ())
{
case "FIRST":
SQL=strSQLInfo.ToUpper().Replace ("SELECT ","SELECT TOP @@PageSize ");
SQL+=" @@Where ORDER BY "+strOrder;
break;
case "MID":
string strRep=@"SELECT Top @@PageSize * FROM
(SELECT Top @@PageSize * FROM
(
SELECT Top @@Page_Size_Number ";
SQL=strSQLInfo.ToUpper().Replace ("SELECT ",strRep);
SQL+=" @@Where ORDER BY "+strOrder;
SQL+=" /r/n) P_T0 ORDER BY "+ strNewOrder+"/n/r"+
" ) P_T1 ORDER BY "+strOrder;
break;
case "LAST":
string strRep2=@"SELECT * FROM (
Select Top @@LeftSize ";
SQL=strSQLInfo.ToUpper().Replace ("SELECT ",strRep2);
SQL+=" @@Where ORDER BY "+ strNewOrder+"/n/r"+
" ) P_T1 ORDER BY "+strOrder;
break;
case "COUNT":
SQL="Select COUNT(*) FROM ( " +strSQLInfo+" ) P_Count @@Where";
break;
default:
SQL=strSQLInfo+strOrder;//還原
break;
}
}
return SQL;
#endregion
}
}Oracle分頁算法
Oracle :
基本的分頁原理利用Oracle內指的 rownum 僞列,它是一個遞增序列,但是它在Order by 之前生成,通常
採用下面的分頁語句:
select * from
(select rownum r_n,temptable.* from
( @@SourceSQL ) temptable
) temptable2 where r_n between @@RecStart and @@RecEnd
其中:
@@SourceSQL :當前任意複雜的SQL語句
@@RecStart:記錄開始的點,等於 ((tCurPage -1) * tPageSize +1)
@@RecEnd :記錄結束的點,等於 (tCurPage * tPageSize)
/// <summary>
/// Oracle 分頁SQL語句生成器
/// </summary>
/// <param name="strSQLInfo">原始SQL語句</param>
/// <param name="strWhere">在分頁前要替換的字符串,用於分頁前的篩選</param>
/// <param name="PageSize">頁大小</param>
/// <param name="PageNumber">頁碼</param>
/// <param name="AllCount">記錄總數</param>
/// <returns>生成SQL分頁語句</returns>
private static string MakePageSQLStringByOracle(string strSQLInfo,string strWhere,int PageSize,int PageNumber,int AllCount)
{
if(AllCount==0)
{
//生成統計語句
return "select count(*) from ("+strSQLInfo+") ";
}
//分頁摸板語句
string SqlTemplate=@"SELECT * FROM
(SELECT rownum r_n,temptable.* FROM
( @@SourceSQL ) temptable @@Where
) temptable2 WHERE r_n BETWEEN @@RecStart AND @@RecEnd";
int iRecStart= (PageNumber -1) * PageSize +1 ;
int iRecEnd = PageNumber * PageSize ;
//執行參數替換
string SQL=SqlTemplate.Replace ("@@SourceSQL",strSQLInfo)
.Replace ("@@Where",strWhere)
.Replace ("@@RecStart",iRecStart.ToString ())
.Replace ("@@RecEnd",iRecEnd.ToString ());
return strSQLInfo;
}