制、常見錯(cuò)誤與優(yōu)化方案)
先問一個(gè)看起來很簡(jiǎn)單的問題在 Oracle 數(shù)據(jù)庫里怎么查出表中第一行數(shù)據(jù)這個(gè)問題我拿來面試過不少人也經(jīng)常在技術(shù)社群里看到有人問。有意思的是能一次答對(duì)的不到一半。有的人脫口而出WHERE ROWNUM 1有的人上來就寫LIMIT 1明顯是寫 MySQL 寫慣了還有人直接ORDER BY 1 FETCH FIRST 1 ROW ONLY——語法看著沒錯(cuò)但放到 11g 的生產(chǎn)庫上直接報(bào)錯(cuò)?!叭〉谝恍小边@個(gè)需求做開發(fā)的十有八九都寫過但它背后藏著的 Oracle 底層邏輯和那些反直覺的坑才是真正值得掰開揉碎講清楚的東西。這篇文章就專門圍繞這個(gè)問題展開先說最常見的錯(cuò)誤寫法為什么錯(cuò)再講 ROWNUM 的底層機(jī)制然后給出不同 Oracle 版本下的正確姿勢(shì)和性能優(yōu)化思路最后聊聊隨機(jī)取行、分組取首行這些進(jìn)階場(chǎng)景以及我實(shí)際踩過的兩個(gè)隱藏比較深的坑。1. “取第一行”看起來簡(jiǎn)單第一個(gè)坑就翻車先說個(gè)典型場(chǎng)景。一張訂單表orders里面有幾百萬行數(shù)據(jù)業(yè)務(wù)方說“給我查一條訂單看看字段長(zhǎng)什么樣”。這種需求其實(shí)很常見——不是為了精確取哪一條而是快速看一眼表里的數(shù)據(jù)形態(tài)。很多人的第一反應(yīng)是SELECT * FROM orders WHERE ROWNUM 1;這條 SQL 能跑也能返回一行數(shù)據(jù)但它有一個(gè)致命的問題你根本不知道返回的是哪一行。這不叫“取第一行”這叫“取任意一行”——Oracle 從表中讀到哪一行ROWNUM 就給哪一行發(fā)號(hào)先被讀到的就先拿到 1 號(hào)。而“先被讀到的”取決于執(zhí)行計(jì)劃怎么掃數(shù)據(jù)。可能是全表掃描的第一行可能是索引掃描命中的第一行沒有任何業(yè)務(wù)上的確定性。另一個(gè)更隱蔽的坑是下面這種寫法SELECT * FROM orders WHERE ROWNUM 1 ORDER BY create_time DESC;很多人寫這段代碼的本意是“先按時(shí)間倒序排好再拿第一條”但實(shí)際執(zhí)行過程完全不是這樣。SQL 的語義順序里WHERE的過濾發(fā)生在ORDER BY排序之前。所以這條 SQL 的真實(shí)邏輯是先隨便抓一行抓住的那行編號(hào)為 1返回然后排序——排序排的是已經(jīng)被截?cái)嗪蟮哪且恍信帕说扔跊]排。這個(gè)坑我親眼見過有人踩。當(dāng)時(shí)一個(gè)同事要查“最近創(chuàng)建的一筆訂單”寫了類似上面的 SQL結(jié)果返回的是表里最早的一條記錄。排查了半天最后發(fā)現(xiàn)根本不是數(shù)據(jù)問題是邏輯順序搞反了。再往下說還有一個(gè)寫法連語法都過不去SELECT * FROM orders ORDER BY create_time DESC WHERE ROWNUM 1;這個(gè)直接在 Oracle 上報(bào) ORA-00933因?yàn)镺RDER BY必須放在WHERE之后。有 MySQL 習(xí)慣的人特別容易踩這個(gè)畢竟 MySQL 里L(fēng)IMIT是放在最后的導(dǎo)致一些朋友誤以為 Oracle 也能把條件寫后面。所以你看光是“取第一行”四個(gè)字就能拆出三種完全不同的需求需求描述真實(shí)意圖常見錯(cuò)誤隨便拿一條看看取任意一行以為 ROWNUM1 是確定性的把所有行排完序后取第一條取排序后的首行WHERE 和 ORDER BY 順序搞反取物理存儲(chǔ)上的第一行按塊掃描順序取首行意識(shí)到物理順序不可控這里給新手一個(gè)最基本的建議寫“取第一行”之前先搞清楚你要的是哪種“第一行”。沒有排序邏輯的第一行在 Oracle 里沒有任何確定性依賴它就是給自己埋雷。2. ROWNUM的偽列機(jī)制為什么必須嵌套子查詢才行要徹底理解上面那些坑就得從 ROWNUM 的底層機(jī)制說起。ROWNUM 是 Oracle 提供的一個(gè)偽列它不是一個(gè)真實(shí)存儲(chǔ)在表中的列而是查詢結(jié)果集生成過程中Oracle 給每一行臨時(shí)分配的序號(hào)。聽起來很抽象打個(gè)比方你就懂了。想象一下你去銀行柜臺(tái)辦事。取號(hào)機(jī)上出的號(hào)就是 ROWNUM。但有個(gè)特殊規(guī)則只有你已經(jīng)坐到柜臺(tái)前的椅子上叫號(hào)器才會(huì)給你發(fā)號(hào)。如果你排在第 2 位但第一位辦完走了、第二位又沒來那叫號(hào)器會(huì)一直叫 1 號(hào)永遠(yuǎn)不會(huì)叫 2 號(hào)。在這個(gè)規(guī)則下2 號(hào)永遠(yuǎn)不可能被叫到——除非 1 號(hào)先被處理完。Oracle 的 ROWNUM 就是這套“坐著才發(fā)號(hào)”的邏輯Oracle 讀取結(jié)果集第一行給它標(biāo)號(hào) ROWNUM 1然后檢查 WHERE 條件如果條件不成立這一行被丟掉繼續(xù)讀下一行下一行重新標(biāo)號(hào) ROWNUM 1再檢查條件以此類推。所以WHERE ROWNUM 1能返回?cái)?shù)據(jù)是因?yàn)榈谝恍袡z查時(shí)條件成立。但WHERE ROWNUM 2永遠(yuǎn)查不到數(shù)據(jù)因?yàn)槊恳恍斜蛔x到的時(shí)候都先被編號(hào)為 1壓根等不到編號(hào) 2 就被條件過濾掉了。同理WHERE ROWNUM 1也永遠(yuǎn)返回空。這也就解釋了為什么必須先嵌套一層子查詢SELECT * FROM ( SELECT * FROM orders ORDER BY create_time DESC ) WHERE ROWNUM 1;執(zhí)行順序是內(nèi)層子查詢先把所有行排序生成一個(gè)完整的有序結(jié)果集外層查詢?cè)谶@個(gè)有序結(jié)果集上從頭取第一行。這個(gè)結(jié)果集是“已經(jīng)排序完的實(shí)體”所以第一行確定就是你要的那條。注意一個(gè)細(xì)節(jié)很多人聽說嵌套子查詢后會(huì)寫成這樣SELECT * FROM ( SELECT * FROM orders WHERE ROWNUM 1 ORDER BY create_time DESC );把這個(gè)寫法和正確寫法對(duì)比一下差別就在于ROWNUM 截?cái)喟l(fā)生在子查詢內(nèi)部還是外部。上面這種把 ROWNUM 放在內(nèi)層子查詢里的寫法又是“先取任意一行再排序”完全失去了嵌套的意義。理解了這個(gè)機(jī)制很多相關(guān)的坑都能一眼看出來。比如有的同學(xué)問“為什么我加了 ROWNUM 1 之后查詢變快了”——因?yàn)?Oracle 讀到第一行滿足條件的行后就直接停止繼續(xù)掃描了。這在全表掃描時(shí)確實(shí)能大幅減少 IO屬于物理上的短路優(yōu)化。順便說一句ROWNUM 和 ROWID 是兩個(gè)很容易混淆的概念。ROWID 是行的物理地址表示這行數(shù)據(jù)存在哪個(gè)文件的哪個(gè)塊的第幾行ROWNUM 是邏輯序號(hào)表示這行數(shù)據(jù)在當(dāng)前查詢結(jié)果集中的位置。一個(gè)對(duì)應(yīng)物理位置一個(gè)對(duì)應(yīng)邏輯順序用途完全不同排查問題時(shí)別搞混。3. 按排序取首行的完整寫法與Oracle版本差異理解了 ROWNUM 機(jī)制后下面把“排序后取首行”的各種寫法完整梳理一遍。日常開發(fā)里90% 以上的“取第一行”都是這個(gè)意思——按某個(gè)業(yè)務(wù)字段排序取最前的那條。3.1 嵌套子查詢 ROWNUM12c 之前的標(biāo)準(zhǔn)答案SELECT * FROM ( SELECT * FROM orders ORDER BY create_time DESC ) WHERE ROWNUM 1;這是 11g 及更早版本里的標(biāo)準(zhǔn)寫法也是面試?yán)镒钕M愦鸪鰜淼哪莻€(gè)。注意兩個(gè)細(xì)節(jié)第一ROWNUM 1和ROWNUM 1在這里等價(jià)工程上更推薦 1因?yàn)檎Z義上更明確是“取一條”不容易被誤讀。第二內(nèi)層子查詢里建議加上完整的排序條件。比如按時(shí)間排序時(shí)create_time可能出現(xiàn)相同值這時(shí)候最好追加一個(gè)唯一鍵做二級(jí)排序SELECT * FROM ( SELECT * FROM orders ORDER BY create_time DESC, order_id DESC ) WHERE ROWNUM 1;否則 create_time 相同的情況下返回哪一條又變成不確定的了。3.2 FETCH FIRST ROW ONLY12c 及以后的官方推薦Oracle 從 12c 開始引入了 ANSI 標(biāo)準(zhǔn)的FETCH FIRST子句完全就是為了簡(jiǎn)化這種“取前 N 條”的語義而生的SELECT * FROM orders ORDER BY create_time DESC FETCH FIRST 1 ROW ONLY;這個(gè)寫法和嵌套子查詢 ROWNUM 在大多數(shù)場(chǎng)景下性能相當(dāng)?shù)勺x性好太多——SQL 從前往后讀先明確排序再明確取幾條非常符合直覺。如果你需要取前 5 條寫法是FETCH FIRST 5 ROW ONLY。如果要取百分之一寫作FETCH FIRST 1 PERCENT ROW ONLY。如果要取第 2 條到第 3 條配合 OFFSETSELECT * FROM orders ORDER BY create_time DESC OFFSET 1 ROWS FETCH NEXT 2 ROWS ONLY;這就是 Oracle 分頁的另一種實(shí)現(xiàn)方式。提到分頁做開發(fā)的朋友應(yīng)該馬上會(huì)聯(lián)想到ROWNUM三層嵌套分頁——那個(gè)經(jīng)典寫法其實(shí)是本篇文章討論內(nèi)容的直接延伸外層固定總行數(shù)中間層算頁碼偏移最內(nèi)層排序。這里提醒一句如果你的生產(chǎn)庫還在 11gFETCH FIRST會(huì)直接報(bào) ORA-00933。遷移老項(xiàng)目代碼時(shí)這個(gè)坑很常見從 12c 代碼庫往 11g 環(huán)境回遷必須把 FETCH FIRST 改回嵌套子查詢寫法。3.3 ROW_NUMBER() 窗口函數(shù)還有一種寫法用分析函數(shù)SELECT * FROM ( SELECT t.*, ROW_NUMBER() OVER (ORDER BY create_time DESC) rn FROM orders t ) WHERE rn 1;這個(gè)寫法的通用性最強(qiáng)因?yàn)镽OW_NUMBER()不僅能取整體第一行還能配合PARTITION BY取分組內(nèi)的第一行。但代價(jià)是它需要對(duì)所有行計(jì)算序號(hào)然后才能過濾執(zhí)行計(jì)劃里通常多一個(gè) WINDOW SORT 步驟性能相比 ROWNUM 截?cái)嘁?。三種寫法放在一起對(duì)比一下寫法版本要求可讀性性能特征推薦場(chǎng)景嵌套子查詢 ROWNUM全版本中掃描到首行即停老版本兼容、追求性能FETCH FIRST12c最好同樣支持首行截?cái)嘈马?xiàng)目首選ROW_NUMBER()全版本中必須全量計(jì)算序號(hào)分組取首行等復(fù)雜場(chǎng)景從我個(gè)人的使用習(xí)慣來說12c 以上的新庫無腦用 FETCH FIRST老庫用嵌套子查詢 ROWNUMROW_NUMBER() 只在需要分組或者需要行號(hào)做二次處理時(shí)才用。4. 千萬行大表取首行執(zhí)行計(jì)劃與索引提案前面講的都是寫法層面的問題實(shí)際生產(chǎn)環(huán)境里另一個(gè)折磨人的問題就是性能。尤其是“幾千萬行大表”這種場(chǎng)景下取首行SQL 寫對(duì)了也可能會(huì)跑出讓人崩潰的執(zhí)行時(shí)間和 IO 消耗。先說一個(gè)結(jié)論如果只是“隨便取一行”WHERE ROWNUM 1加上FIRST_ROWS(n)之類的提示在大表上也能很快返回因?yàn)?Oracle 讀到第一行就停了。真正的性能坑集中在“按非索引列排序取首行”這個(gè)場(chǎng)景——它必須先把整張表的數(shù)據(jù)讀完、排完序才能找出第一條。舉個(gè)實(shí)際例子。某張流水表account_flow有 3000 萬行業(yè)務(wù)要查“金額最大的一筆流水”直接寫法SELECT * FROM ( SELECT * FROM account_flow ORDER BY amount DESC ) WHERE ROWNUM 1;我在測(cè)試環(huán)境跑過全表掃描加排序耗時(shí)接近 40 秒。這個(gè)結(jié)果不意外——排序本身要把 3000 萬行的 amount 字段全部讀出來放到臨時(shí)表空間排序性能瓶頸在 IO 和排序空間上。優(yōu)化思路有兩個(gè)方向。方向一把排序字段做成索引。如果amount上有索引Oracle 可以直接走索引的有序掃描從頭讀第一個(gè)索引條目就能拿到最大值。雖然還是 INDEX FULL SCAN但掃描到第一條就停了不會(huì)讀完整個(gè)索引CREATE INDEX idx_account_flow_amount ON account_flow(amount DESC); SELECT * FROM ( SELECT * FROM account_flow ORDER BY amount DESC ) WHERE ROWNUM 1;建了降序索引之后執(zhí)行計(jì)劃會(huì)變成 INDEX FULL SCAN (MIN/MAX) 類型的路徑執(zhí)行時(shí)間從 40 秒縮短到幾十毫秒級(jí)別。這里提醒一句索引建立后別忘了收集統(tǒng)計(jì)信息EXEC DBMS_STATS.GATHER_INDEX_STATS(USER, IDX_ACCOUNT_FLOW_AMOUNT);方向二如果業(yè)務(wù)只關(guān)心“最大/最小的某個(gè)字段值”而不是完整的一行數(shù)據(jù)直接用聚合函數(shù)更高效。比如只要最大金額是多少SELECT MAX(amount) FROM account_flow;Oracle 對(duì)MAX/MIN有專門的優(yōu)化路徑在普通 B 樹索引上做 MIN/MAX 掃描只需要讀兩個(gè)索引塊就能拿到結(jié)果連表數(shù)據(jù)都不用碰。這在高并發(fā)場(chǎng)景下是性價(jià)比最高的方案。再說一個(gè)容易忽略的執(zhí)行計(jì)劃細(xì)節(jié)。很多人以為嵌套子查詢里寫了ORDER BY子查詢就會(huì)把全部數(shù)據(jù)排完序再交給外層。實(shí)際上優(yōu)化器在特定條件下可以做排序消除sort elimination——如果排序字段本身就是索引的有序鍵優(yōu)化器會(huì)直接把排序操作省掉改用索引掃描的有序輸出。這也是為什么取首行時(shí)執(zhí)行計(jì)劃里到底有沒有SORT ORDER BY這一步很重要。用 EXPLAIN PLAN 看一眼EXPLAIN PLAN FOR SELECT * FROM ( SELECT * FROM account_flow ORDER BY amount DESC ) WHERE ROWNUM 1; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);如果執(zhí)行計(jì)劃里出現(xiàn)了SORT ORDER BY說明優(yōu)化器老老實(shí)實(shí)排了序如果顯示的是INDEX FULL SCAN (MIN/MAX)或者直接從索引取數(shù)說明走了優(yōu)化路徑。養(yǎng)成看執(zhí)行計(jì)劃的習(xí)慣比死記硬背優(yōu)化規(guī)則靠譜得多。另外涉及大表取首行時(shí)還要警惕熱塊競(jìng)爭(zhēng)。如果業(yè)務(wù)上高頻執(zhí)行“取最新一條”這類查詢所有人都去搶索引最右端那個(gè)葉塊就會(huì)產(chǎn)生 buffer busy wait。解決辦法通常是反向索引或者減少查詢頻率這屬于另一層級(jí)的優(yōu)化話題在這里先提一句供遇到性能問題的朋友排查時(shí)參考。5. 隨機(jī)行、分組首行、空表判斷幾個(gè)特殊場(chǎng)景前面講的都是“按規(guī)則取第一行”但實(shí)際開發(fā)中“取第一行”還有幾個(gè)容易被人問起、又容易寫錯(cuò)的變體我集中放到這一節(jié)講。5.1 隨機(jī)取一行如果業(yè)務(wù)需求是“從表里隨機(jī)抽一條”很多人會(huì)寫出SELECT * FROM orders ORDER BY DBMS_RANDOM.VALUE FETCH FIRST 1 ROW ONLY;這個(gè)寫法語義上完全沒問題大表上的性能就是另一回事了。DBMS_RANDOM.VALUE會(huì)給每一行生成一個(gè)隨機(jī)數(shù)然后全量排序代價(jià)極大。3000 萬行的大表跑一次這種查詢幾秒鐘是少不了的。大表隨機(jī)取行的優(yōu)化思路通常是先估算表行數(shù)隨機(jī)一個(gè)偏移量然后從中間位置取。但這是另一個(gè)話題了這里只想表達(dá)一個(gè)觀點(diǎn)ORDER BY 隨機(jī)函數(shù)的寫法只適合小表大表面試時(shí)答這個(gè)會(huì)被直接追問性能。5.2 分組取每組的第一行這是數(shù)據(jù)分析和報(bào)表里非常常見的需求——“每個(gè)客戶取最新一筆訂單”“每個(gè)商品分類取價(jià)格最低的一款”。很多人會(huì)用三層嵌套子查詢寫代碼又長(zhǎng)又難調(diào)試。最優(yōu)雅的做法是前面提到的 ROW_NUMBER()SELECT * FROM ( SELECT t.*, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY create_time DESC) rn FROM orders t ) WHERE rn 1;PARTITION BY把數(shù)據(jù)按客戶分組組內(nèi)按時(shí)間倒序編號(hào)最后取每組編號(hào)為 1 的行。這個(gè)寫法配合ORDER BY create_time DESC的復(fù)合索引customer_id, create_time DESC能跑出不錯(cuò)的性能。順便對(duì)比一下老開發(fā)可能習(xí)慣用NOT EXISTS實(shí)現(xiàn)同樣的需求SELECT * FROM orders a WHERE NOT EXISTS ( SELECT 1 FROM orders b WHERE b.customer_id a.customer_id AND b.create_time a.create_time );語義是“找一張表里不存在比我更新的訂單”——也就是每組最新的一條。這個(gè)寫法在客戶數(shù)少、每人訂單多的情況下性能可以但如果客戶數(shù)量大關(guān)聯(lián)查詢的代價(jià)會(huì)成倍上漲。我從實(shí)測(cè)來看ROW_NUMBER() 復(fù)合索引是更穩(wěn)妥的選擇。5.3 判斷空表和取首行時(shí)的邊界處理還有一個(gè)經(jīng)常被忽略的細(xì)節(jié)表里沒有數(shù)據(jù)時(shí)“取第一行”會(huì)返回什么用FETCH FIRST 1 ROW ONLY查詢空表結(jié)果集是空的程序代碼里做fetch()會(huì)返回NO_DATA_FOUND。如果你是在 PL/SQL 里處理就要考慮這個(gè)異常分支BEGIN SELECT create_time INTO v_create_time FROM orders ORDER BY create_time DESC FETCH FIRST 1 ROW ONLY; EXCEPTION WHEN NO_DATA_FOUND THEN v_create_time : NULL; END;這種寫法在取最新時(shí)間戳?xí)r很常用但它有一個(gè)隱患如果create_time本身允許 NULL排序后第一條可能恰恰是 NULL。換句話說你取到了“存在但為 NULL”的時(shí)間跟“表里沒有數(shù)據(jù)”在 PL/SQL 里表現(xiàn)完全不一樣。寫代碼判斷空表時(shí)COUNT(*)或者EXISTS更直接SELECT COUNT(*) INTO v_cnt FROM orders WHERE ...; IF v_cnt 0 THEN -- 空表邏輯 END IF;如果你是在存儲(chǔ)過程里動(dòng)態(tài)拼 SQL 取首行記住動(dòng)態(tài) SQL 和靜態(tài) SQL 的 ROWNUM 行為一致但綁定變量和字面量的執(zhí)行計(jì)劃可能不同這在 11g 的綁定變量窺探bind peeking機(jī)制下尤其要注意。6. 我用ROWNUM踩過的兩個(gè)真實(shí)坑與驗(yàn)證方法理論講完說實(shí)踐。最后分享兩個(gè)我實(shí)際踩過的坑都跟“取第一行/取前幾行”有關(guān)希望能幫各位少走彎路。6.1 坑一PL/SQL 游標(biāo)里 ROWNUM 放錯(cuò)了層當(dāng)時(shí)要寫一個(gè)報(bào)表存儲(chǔ)過程邏輯是“從子表里取最近三條記錄然后循環(huán)處理”。我一開始寫的代碼是FOR rec IN ( SELECT * FROM child_table WHERE parent_id p_parent_id AND ROWNUM 3 ORDER BY create_time DESC ) LOOP ... END LOOP;一眼看過去覺得沒問題又是過濾又是排序。但實(shí)際跑出來的結(jié)果完全不對(duì)——取到的三條根本不是最新的三條。原因就是前面講的那個(gè)機(jī)制WHERE ROWNUM 3在ORDER BY之前執(zhí)行先把物理掃描的前三條抓走了然后才排序。正確寫法還是那招——嵌套子查詢FOR rec IN ( SELECT * FROM ( SELECT * FROM child_table WHERE parent_id p_parent_id ORDER BY create_time DESC ) WHERE ROWNUM 3 ) LOOP ... END LOOP;這個(gè)坑的問題在于它不像語法報(bào)錯(cuò)那樣直接暴露而是數(shù)據(jù)結(jié)果不對(duì)。數(shù)據(jù)量的變化也可能讓問題時(shí)隱時(shí)現(xiàn)——小表掃描順序碰巧和排序一致時(shí)結(jié)果是對(duì)的數(shù)據(jù)一多物理順序變了就出現(xiàn)偶發(fā)錯(cuò)誤。這類“偶爾錯(cuò)、偶爾對(duì)”的問題在排查時(shí)最難定位。6.2 坑二ORDER BY 的列沒進(jìn) SELECT 列表排序被優(yōu)化器陰了一把第二個(gè)坑更隱蔽。當(dāng)時(shí)有個(gè)分頁查詢外層是 ROWNUM 控制頁大小內(nèi)層子查詢排序。為了“精簡(jiǎn)結(jié)果集”內(nèi)層 SELECT 只選了業(yè)務(wù)要展示的字段排序字段在子查詢里沒出現(xiàn)在 SELECT 列表中SELECT * FROM ( SELECT order_id, order_amount, status FROM orders ORDER BY create_time DESC ) WHERE ROWNUM 20;Oracle 文檔里明確說明ORDER BY的列需要出現(xiàn)在 SELECT 列表中否則不保證排序結(jié)果。但實(shí)際執(zhí)行時(shí)它也不一定報(bào)錯(cuò)——優(yōu)化器可能會(huì)自行處理在某些執(zhí)行路徑下排序結(jié)果符合預(yù)期換了一種執(zhí)行計(jì)劃后結(jié)果就變了。我在 19c 上測(cè)試這種寫法在某些索引組合下會(huì)出現(xiàn)返回行亂序的情況排查了很久才定位到是排序字段被優(yōu)化器“優(yōu)化”掉了。解決辦法有兩個(gè)一是把create_time也放進(jìn)子查詢的 SELECT 列表二是設(shè)計(jì)復(fù)合索引(create_time DESC, order_id)讓排序完全走索引。6.3 驗(yàn)證寫法正確性的通用方法最后分享一個(gè)通用的驗(yàn)證思路。不管用哪種寫法寫完后先問三個(gè)問題SQL 的語義執(zhí)行順序是什么WHERE 過濾、排序、行數(shù)截?cái)嗄膫€(gè)先哪個(gè)后Oracle 里 WHERE 一定在 ORDER BY 之前窗口函數(shù)在 ORDER BY 之后別搞反。執(zhí)行計(jì)劃里有沒有多余的全表排序EXPLAIN PLAN 看有沒有 SORT ORDER BY有就說明排序躲不掉如果業(yè)務(wù)能接受沒排序的“第一行”直接 ROWNUM1 完事性能差著兩個(gè)數(shù)量級(jí)。結(jié)果是不是確定性的連續(xù)跑三次如果三次返回一樣的行才算穩(wěn)定。如果業(yè)務(wù)對(duì)“第一行”的定義模糊干脆把結(jié)果設(shè)為任意行并讓產(chǎn)品接受這個(gè)現(xiàn)實(shí)。這三個(gè)問題想明白基本上“取第一行”相關(guān)的 SQL 就不會(huì)再翻車了。最后再順手分享一個(gè)小筆記Oracle 的OFFSET ... FETCH語法其實(shí)從 12c 開始一直沿用到現(xiàn)在如果你開發(fā)環(huán)境是 21c 或 23ai可以放心用如果你要兼容 11g 的老生產(chǎn)環(huán)境嵌套子查詢 ROWNUM 才是那塊壓艙石。碰到取首行需求先問清楚業(yè)務(wù)意圖再選對(duì)應(yīng)的 SQL 形態(tài)這樣寫出來的查詢既高效又經(jīng)得起推敲。