Oracle 分頁方法研究

来源:http://www.cnblogs.com/hanzongze/archive/2017/06/26/oracle-paging-1.html
-Advertisement-
Play Games

" 1、Oracle 中的三大分頁方法 " "1.1、通過分析函數分頁" "1.2、通過 ROWNUM 分頁" "1.3、通過 ROWID 分頁" " 2、Oracle 分頁解決方案淺析 " "2.1、純後端代碼完成分頁" "2.2、通過存儲過程來分頁" "2.3、兩個通用的分頁存儲過程" " 3、 ...


1、Oracle 中的三大分頁方法

本人最近總結了一下 Oracle 中的分頁寫法,從純粹的 SQL 寫法上來看,所謂分頁就是嵌套子查詢,無非就是不同的分頁方法嵌套的子查詢層數不同而已。Oracle 中一共有三種分頁寫法,分別是:嵌套一層子查詢的分析函數分頁、嵌套兩層子查詢的 ROWNUM 分頁和嵌套三層子查詢的 ROWID 分頁。

1.1、通過分析函數分頁

按員工年齡排序,每頁顯示 3 個員工,取第 1 頁的數據。只嵌套一層子查詢,寫法簡潔,容易理解,但一般沒人用這種方法。只需要在子查詢中的分析函數內部排序即可實現排序功能。

SELECT t2.staff_name,t2.birthday FROM(
  SELECT t1.staff_name,t1.birthday,ROW_NUMBER() OVER(ORDER BY t1.birthday) rn
  FROM demo.t_staff t1
) t2 WHERE t2.rn >= ((1-1)*3+1) AND t2.rn <= (1*3);

1.2、通過 ROWNUM 分頁

按員工年齡排序,每頁顯示 3 個員工,取第 1 頁的數據。嵌套兩層子查詢,寫法比較靈活,一般都是用這種方法。只需要在子查詢內部排序即可實現排序功能。

SELECT t3.staff_name,t3.birthday FROM(
  SELECT t2.*,ROWNUM rn FROM(
    SELECT t1.staff_name,t1.birthday FROM demo.t_staff t1 ORDER BY t1.birthday
  ) t2 WHERE ROWNUM <= (1*3)
) t3 WHERE t3.rn >= ((1-1)*3+1);

通過 ROWNUM 分頁的一種變通寫法(相對來說更好理解):

SELECT t3.staff_name,t3.birthday FROM(
  SELECT t2.*,ROWNUM rn FROM(
    SELECT t1.staff_name,t1.birthday FROM demo.t_staff t1 ORDER BY t1.birthday
  ) t2
) t3 WHERE t3.rn >= ((1-1)*3+1) AND t3.rn <= (1*3);

1.3、通過 ROWID 分頁

按員工年齡排序,每頁顯示 3 個員工,取第 1 頁的數據。寫法複雜,不太靈活,不易理解,很少有人用這種方法。必須在最內層子查詢和最外層查詢中都排序才可實現排序功能。

SELECT t4.staff_name,t4.birthday
FROM demo.t_staff t4
WHERE t4.ROWID IN(
  SELECT t3.rid FROM(
    SELECT t2.rid,ROWNUM rn FROM(
      SELECT t1.ROWID rid FROM demo.t_staff t1 ORDER BY t1.birthday
    ) t2 WHERE ROWNUM <= (1*3)
  ) t3 WHERE t3.rn >= ((1-1)*3+1)
) ORDER BY t4.birthday;

2、Oracle 分頁解決方案淺析

Oracle 中的三大分頁方法應用最廣泛的還是第二種,也就是基於 ROWNUM 的分頁方法。由於實現分頁的語法是固定的,所以一般項目中都是會提供一個公用的分頁模版方法,然後其它需要分頁的業務方法再調用這個方法來完成分頁功能的。

分頁的實現過程就是拼接 SQL 語句的過程,但選擇在那個地方來完成拼接也是有講究的。一般來說在服務端拼接是一個比較好的選擇,這種方案主要好處就是靈活、簡單、易維護。另一種比較常見的做法是通過存儲過程來分頁,然後在服務端調用存儲過程,這種方案理論上分頁效率比較高,但實現過程相對複雜,也沒有純服務端代碼那麼好維護。

2.1、純後端代碼完成分頁

純後端代碼完成分頁在定義、調用、性能、理解、維護等方面有不少小的技巧值得推敲。前幾天我結合自己這些年來的分頁經驗和一個在公司幹了十多年的技術專家交流了這個問題,最終我們一致認為還是傳遞整個內層子查詢的方式最好(主要是可以規避掉一大堆小的坑)。拼接格式如下:

SELECT t3.* FROM(
  SELECT t2.*,ROWNUM rn FROM(
    :subquery
  ) t2 WHERE ROWNUM <= (:pageIndex*:pageSize)
) t3 WHERE t3.rn >= ((:pageIndex-1)*:pageSize+1)

我們以前都有嘗試過將子查詢分拆成多個部分,然後分別傳遞的方式,不過一旦項目深入之後問題總比想象的要多得多。譬如參數過多導致調用難度增加,為了實現分頁不得不將寫好的整條語句拆成幾個部分多餘浪費時間,出問題時調試的複雜度也增加了,多表分頁也相對難以處理,經驗不足的程式員常常沒耐心看懂現有代碼進而又捏造了一個所謂的改進版(事實上這種情況還很多)……

不過即便是整個子查詢傳進來,也仍然會有不同的處理方式。譬如我上文提到的那個專家說他們就曾嘗試過把傳遞進來子查詢切分成多個部分再重新組合,但後來發現複雜的子查詢極難寫對,徒增了團隊里新人的挫敗感……

外層查詢中的那個星號是比較關鍵的一點,儘管我們都知道查詢中出現星號往往是不好的,但分頁時依然拘泥這一點的話,必然會到導致複雜的拼接。複雜的拼接往往不好寫,調用時也容易出錯,時不時還得回頭去看內部的實現再推導出該如何調用,這個過程顯然是比較浪費時間的。

2.2、通過存儲過程來分頁

我本人大部分時候還是通過存儲過程來實現分頁的,不過對很多人來說寫存儲過程甚至調用存儲過程都是比較難的,我覺得主要原因還是因為相關知識點不熟、寫的少。下麵列出了寫分頁存儲過程和調用存儲過程的相關參考連接:

下麵是一個調用 Oracle 分頁存儲過程的 C# 方法:

/// <summary>
/// 調用存儲過程,執行分頁
/// </summary>
/// <param name="tableName">表名</param>
/// <param name="queryFields">查詢(欄位)列表</param>
/// <param name="queryWhere">查詢條件</param>
/// <param name="orderBy">排序子句</param>
/// <param name="pageIndex">頁索引(頁碼)</param>
/// <param name="pageSize">頁大小(每頁數據條數)</param>
/// <param name="pageCount">總頁數</param>
/// <param name="rowCount">總行數</param>
/// <param name="resultSet">結果集</param>
public void ExecutePaging(
    string tableName, string queryFields, string queryWhere, string orderBy,
    int pageIndex, int pageSize,
    ref int pageCount, ref int rowCount, ref DataTable resultSet)
{
    OracleParameter[] ps = {
        new OracleParameter(":tableName", OracleDbType.Varchar2, 1000),
        new OracleParameter(":queryFields", OracleDbType.Varchar2, 1000),
        new OracleParameter(":queryWhere", OracleDbType.Varchar2, 2000),
        new OracleParameter(":orderBy", OracleDbType.Varchar2, 200),
        new OracleParameter(":pageIndex", OracleDbType.Int32),
        new OracleParameter(":pageSize", OracleDbType.Int32),
        new OracleParameter(":pageCount", OracleDbType.Int32),
        new OracleParameter(":rowCount", OracleDbType.Int32),
        new OracleParameter(":resultSet", OracleDbType.RefCursor)
    };

    ps[0].Value = tableName;
    ps[1].Value = queryFields;
    ps[2].Value = queryWhere;
    ps[3].Value = orderBy;
    ps[4].Value = pageIndex;
    ps[5].Value = pageSize;
    ps[6].Direction = ParameterDirection.Output;
    ps[7].Direction = ParameterDirection.Output;
    ps[8].Direction = ParameterDirection.Output;

    resultSet = OracleHelper.ProcQuery("sp_dynamic_paging", ps); // 調用存儲過程
    pageCount = Verifier.VerifyInt(ps[6].Value);
    rowCount = Verifier.VerifyInt(ps[7].Value);
}

2.3、兩個通用的分頁存儲過程

下麵這個存儲過程是從我曾負責過的一個項目中抽取出來的,也是我第一次嘗試寫存儲過程分頁,100%原創,中間改版過幾次,為方便閱讀註釋內容已被我去掉,現在的這個版本中的i_queryFields參數是不接受星號的:

CREATE OR REPLACE PROCEDURE sp_paging(
  i_tableName VARCHAR2, -- 表名
  i_queryFields VARCHAR2, -- 查詢(欄位)列表
  i_queryWhere VARCHAR2, -- 查詢條件
  i_orderBy VARCHAR2, -- 排序子句
  i_pageIndex NUMBER, -- 當前頁索引
  i_pageSize NUMBER, -- 頁大小
  o_rowCount OUT NUMBER, -- 總行數
  o_pageCount OUT NUMBER, -- 總頁數
  o_resultSet OUT SYS_REFCURSOR -- 結果集
) IS
  v_count_sql VARCHAR2(2000);
  v_select_sql VARCHAR2(4000);
  BEGIN
    -- 拼接查詢總行數的語句
    v_count_sql := 'SELECT COUNT(1) FROM '||i_tableName;

    -- 拼接查詢條件
    IF i_queryWhere IS NOT NULL THEN
      v_count_sql := v_count_sql||' WHERE 1=1 '||i_queryWhere;
    END IF;

    -- 計算總行數
    EXECUTE IMMEDIATE v_count_sql INTO o_rowCount;
    --DBMS_OUTPUT.PUT_LINE(v_count_sql||';');

    -- 計算總頁數(CEIL 向上取整)
    o_pageCount := CEIL(o_rowCount / i_pageSize);

    -- 如果有記錄,且當前頁索引合法,則繼續查詢
    IF o_rowCount >= 1 AND i_pageIndex >= 1 AND i_pageIndex <= o_pageCount THEN

      -- 當記錄總數小於或等於頁大小時,查詢所有記錄
      IF o_rowCount <= i_pageSize THEN
        v_select_sql := 'SELECT '||i_queryFields||' FROM('||i_tableName||')';

        IF i_queryWhere IS NOT NULL THEN
          v_select_sql := v_select_sql||' WHERE 1=1 '||i_queryWhere;
        END IF;

        IF i_orderBy IS NOT NULL THEN
          v_select_sql := v_select_sql||' order by '||i_orderBy;
        END IF;

      -- 查詢第一頁
      ELSIF i_pageIndex = 1 THEN
        v_select_sql := 'SELECT '||i_queryFields||' FROM('||i_tableName||')';

        IF i_queryWhere IS NOT NULL THEN
          v_select_sql := v_select_sql||' WHERE 1=1 '||i_queryWhere;
        END IF;

        IF i_orderBy IS NOT NULL THEN
          v_select_sql := v_select_sql||' order by '||i_orderBy;
        END IF;

        v_select_sql := 'SELECT '||i_queryFields||' FROM('||v_select_sql||') WHERE ROWNUM<='||i_pageSize;

      -- 查詢指定頁
      ELSE
        v_select_sql := 'SELECT '||i_queryFields||' FROM('||i_tableName||')';

        IF i_queryWhere IS NOT NULL THEN
          v_select_sql := v_select_sql||' WHERE 1=1 '||i_queryWhere;
        END IF;

        IF i_orderBy IS NOT NULL THEN
          v_select_sql := v_select_sql||' order by '||i_orderBy;
        END IF;

        v_select_sql := 'SELECT '||i_queryFields||' FROM(SELECT ROWNUM rn,'||i_queryFields||' FROM('||v_select_sql
          ||')) WHERE rn>'||((i_pageIndex-1)*i_pageSize)||' AND rn<='||(i_pageIndex*i_pageSize);
      END IF;

      --DBMS_OUTPUT.PUT_LINE(v_select_sql||';');
      OPEN o_resultSet FOR v_select_sql;
    ELSE
      OPEN o_resultSet FOR 'SELECT * FROM '||i_tableName||' WHERE 1!=1';
    END IF;
END;

下麵這個存儲過程摘自《劍破冰山——Oracle開發藝術》一書,有刪改:

CREATE OR REPLACE PROCEDURE sp_dynamic_paging(
  i_tableName VARCHAR2, -- 表名
  i_queryFields VARCHAR2, -- 查詢列表
  i_queryWhere VARCHAR2, -- 查詢條件
  i_orderBy VARCHAR2, -- 排序
  i_pageSize NUMBER, -- 頁大小
  i_pageIndex NUMBER, -- 頁索引
  o_rowCount OUT NUMBER, -- 返回總條數
  o_pageCount OUT NUMBER, -- 返回總頁數
  o_resultSet OUT SYS_REFCURSOR -- 返回分頁結果集
)
IS
  v_startRows INT; -- 開始行
  v_endRows INT; -- 結束行 
  v_pageSize INT;
  v_pageIndex INT;
  v_queryFields VARCHAR2(2000);
  v_queryWhere VARCHAR2(2000);
  v_orderBy VARCHAR2(200);
  v_count_sql VARCHAR2(1000); -- 接收統計數據條數的 SQL 語句
  v_select_sql VARCHAR2(4000); -- 接收查詢分頁數據的 SQL 語句
BEGIN
  -- 如果沒有表名,則直接返回異常消息
  -- 如果沒有欄位,則表示查詢全部欄位
  IF i_queryFields IS NOT NULL THEN
    v_queryFields:=i_queryFields;
  ELSE
    v_queryFields:=' * ';
  END IF;

  -- 可以沒有查詢條件
  IF i_queryWhere IS NOT NULL THEN
    v_queryWhere := ' WHERE 1=1 AND'||i_queryWhere||' ';
  ELSE
    v_queryWhere := ' WHERE 1=1 ';
  END IF;

  -- 可以沒有排序條件
  IF i_orderBy IS NULL THEN
    v_orderBy:=' ';
  ELSE
    v_orderBy:='ORDER BY '||i_orderBy;
  END IF;

  -- 如果未指定查詢頁,則預設為首頁
  IF i_pageIndex IS NULL OR i_pageIndex<1 THEN
    v_pageIndex:=1;
  ELSE
    v_pageIndex:=i_pageIndex;
  END IF;

  -- 如果未指定每頁記錄數,則預設為 10 條
  IF i_pageSize IS NULL THEN
    v_pageSize:=10;
  ELSE
    v_pageSize:=i_pageSize;
  END IF;

  -- 構造查詢總條數的語句
  v_count_sql:='SELECT COUNT(1) FROM '||i_tableName||v_queryWhere;
  --DBMS_OUTPUT.PUT_LINE(v_count_sql||';');

  -- 構造查詢數據的語句
  v_select_sql:='(SELECT '||v_queryFields||' FROM '||i_tableName||v_queryWhere||v_orderBy||') t2';

  -- 查詢總條數
  EXECUTE IMMEDIATE v_count_sql INTO o_rowCount;

  -- 得到總頁數
  IF MOD(o_rowCount,i_pageSize)=0 THEN
    o_pageCount:=o_rowCount/i_pageSize;
  ELSE
    o_pageCount:=FLOOR(o_rowCount/i_pageSize)+1;
  END IF;

  -- 如果當前頁大於最大頁數,則取最大頁數
  IF i_pageIndex>o_pageCount THEN
    v_pageIndex:=o_pageCount;
  END IF;

  -- 設置開始結束的記錄數
  v_startRows:=(v_pageIndex-1)*v_pageSize+1;
  v_endRows:=v_pageIndex*v_pageSize;

  -- 進行完成的動態 SQL 語句拼接
  v_select_sql:='SELECT t3.* FROM'||'(SELECT t2.*,ROWNUM rn FROM'||v_select_sql
    ||' WHERE ROWNUM<='||v_endRows||') t3 WHERE t3.rn>='||v_startRows;
  --DBMS_OUTPUT.PUT_LINE(v_select_sql||';');
  OPEN o_resultSet FOR v_select_sql;
END;

下麵這段 PL/SQL 代碼用於測試上面兩個存儲過程:

DECLARE
  v_tableName VARCHAR2(1000);
  v_queryFields VARCHAR2(1000);
  v_queryWhere VARCHAR2(1000);
  v_orderBy VARCHAR2(200);
  v_pageSize INT := 3;
  v_pageIndex INT;
  v_rowCount INT := 0;
  v_pageCount INT := 0;
  v_resultSet SYS_REFCURSOR;
BEGIN
  v_tableName:='t_staff';
  v_queryFields:='staff_name,birthday';
  v_orderBy:='birthday';
  v_pageIndex:=1;

  sp_dynamic_paging(
    i_tableName => v_tableName,
    i_queryFields => v_queryFields,
    i_queryWhere => v_queryWhere,
    i_orderBy => v_orderBy,
    i_pageSize => v_pageSize,
    i_pageIndex => v_pageIndex,
    o_rowCount => v_rowCount,
    o_pageCount => v_pageCount,
    o_resultSet => v_resultSet
  );
END;

3、總結

本文主要講述了 Oracle 中的三種分頁方法和常見的兩種分頁解決方案,並給出了兩個通用的分頁存儲過程源碼。主要是對我個人所掌握的 Oracle 分頁方法和技術做了個全面的回顧。

本文鏈接http://www.cnblogs.com/hanzongze/p/oracle-paging-1.html
版權聲明:本文為博客園博主 韓宗澤 原創,作者保留署名權!歡迎通過轉載、演繹或其它傳播方式來使用本文,但必須在明顯位置給出作者署名和本文鏈接!個人博客,能力有限,若有不當之處,敬請批評指正,謝謝!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 蘋果所需: 1、 郵箱密碼 2、 聯繫人姓名,電話 3、 公司英文名字 4、 官網地址 鄧白氏所需: 1.公司中文名:(註:中文名稱一定要和營業執照上一致) 公司英文名: 2. 公司辦公中文地址:(註:具體到詳細地址) 公司辦公英文地址: 3.公司郵編:(註:具體到詳細郵編) 4.公司主營業務 5. ...
  • 原理 FPS的計算 CoreAnimation有一個很好用的類CADisplayLink,這個類會在每一幀繪製之前調用,並且可以獲取時間戳。於是,我們只要統計出,在1s內的幀數即可。 記憶體和CPU信息的獲取 CPU和記憶體的獲取採用了mach頭文件中的方法,調用了底層API,採用C方式來獲取。 LHP ...
  • 一,工程圖。 二,代碼。 #import "ViewController.h" #import "Base64CodeByteFunc.h" @interface ViewController () @end @implementation ViewController - (void)viewDi ...
  • 系統管理賬戶登錄系統會大大降低系統的安全性,所以為了更安全的使用電腦,通常我們都會創建一個新的用戶來登錄系統,MySQL也不例外。MySQL中創建用戶與授權的實現方法,對於剛開始接觸mysql的朋友可以參考下! 創建 Mysql創建用戶的方法分成三種:INSERTUSER表的方法、CREATEUS ...
  • 1.查詢dblink語句 col owner for a20col db_link for a30col username for a20col host for a30set linesize 120set pages 60select * from dba_db_links order by o ...
  • 官方文檔鏈接 http://docs.oracle.com/cd/E11882_01/server.112/e41134/protection.htm SBYDB02000 最大可用模式(Maximum Availability) 這種保護模式在 不影響主庫可用性 的前提下提供最高水平的數據保護 在 ...
  • 第一次寫博客,哈哈,內心有種無法掩蓋的小激動。 我有個問題,如何才能成為一名好程式員?是代碼風騷?還是會不間斷的學習?歡迎大家給出自己的建議。 好了,廢話不多說了,進入今天的正題。 我不知道你們在用windows版的redis的時候有沒有遇到這麼幾個坑: 1.# Warning: no config ...
  • 存儲數據是為了查找數據,存儲結構影響數據查找的性能。對無序數據進行查找,最快的查找演算法是哈希查找;對有序數據進行查找,最快的查找演算法是平衡樹查找。在傳統的關係型資料庫中,聚集索引和非聚集索引都是平衡樹(B-Tree)類型的存儲結構,用於順序存儲數據,便於實現數據的快速查找。除了提升數據查找的性能之外 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...