SQL最佳化編寫規範

pentium發表於2007-02-02
書寫SQL語句一些較最佳化的方式[@more@]

  1.書寫格式
  
  示例程式碼:
  
  儲存過程SQL文書寫格式例
  select
  c.dealerCode,
  round(sum(c.submitSubletAmountDLR + c.submitPartsAmountDLR + c.submitLaborAmountDLR) / count(*), 2) as avg,
  decode(null, 'x', 'xx', 'CNY')
  from (
  select
  a.dealerCode,
  a.submitSubletAmountDLR,
  a.submitPartsAmountDLR,
  a.submitLaborAmountDLR
  from SRV_TWC_F a
  where (to_char(a.ORIGSUBMITTIME, 'yyyy/mm/dd') >= 'Date Range(start)'
  and to_char(a.ORIGSUBMITTIME, 'yyyy/mm/dd') <= 'Date Range(end)'
  and nvl(a.deleteflag, '0') <> '1')
  union all
  select
  b.dealerCode,
  b.submitSubletAmountDLR,
  b.submitPartsAmountDLR,
  b.submitLaborAmountDLR
  from SRV_TWCHistory_F b
  where (to_char(b.ORIGSUBMITTIME, 'yyyy/mm/dd') >= 'Date Range(start)'
  and to_char(b.ORIGSUBMITTIME,'yyyy/mm/dd') <= 'Date Range(end)'
  and nvl(b.deleteflag,'0') <> '1')
  ) c
  group by c.dealerCode
  order by avg desc;
  
  Java source裡的SQL字串書寫格式例
  strSQL = "insert into Snd_FinanceHistory_Tb "
  + "(DEALERCODE, "
  + "REQUESTSEQUECE, "
  + "HANDLETIME, "
  + "JOBFLAG, "
  + "FRAMENO, "
  + "INMONEY, "
  + "REMAINMONEY, "
  + "DELETEFLAG, "
  + "UPDATECOUNT, "
  + "CREUSER, "
  + "CREDATE, "
  + "HONORCHECKNO, "
  + "SEQ) "
  + "values ('" + draftInputDetail.dealerCode + "', "
  + "'" + draftInputDetail.requestsequece + "', "
  + "sysdate, "
  + "'07', "
  + "'" + frameNO + "', "
  + requestMoney + ", "
  + remainMoney + ", "
  + "'0', "
  + "0, "
  + "'" + draftStruct.employeeCode + "', "
  + "sysdate, "
  + "'" + draftInputDetail.honorCheckNo + "', "
  + index + ")";
  
  1).縮排
  對於儲存過程檔案,縮排為8個空格
  對於Java source裡的SQL字串,不可有縮排,即每一行字串不可以空格開頭
  
  2).換行
  1>.Select/From/Where/Order by/Group by等子句必須另其一行寫
  2>.Select子句內容如果只有一項,與Select同行寫
  3>.Select子句內容如果多於一項,每一項單獨佔一行,在對應Select的基礎上向右縮排8個空格(Java source無縮排)
  4>.From子句內容如果只有一項,與From同行寫
  5>.From子句內容如果多於一項,每一項單獨佔一行,在對應From的基礎上向右縮排8個空格(Java source無縮排)
  6>.Where子句的條件如果有多項,每一個條件佔一行,以AND開頭,且無縮排
  7>.(Update)Set子句內容每一項單獨佔一行,無縮排
  8>.Insert子句內容每個表欄位單獨佔一行,無縮排;values每一項單獨佔一行,無縮排
  9>.SQL文中間不允許出現空行
  10>.Java source裡單引號必須跟所屬的SQL子句處在同一行,連線符("+")必須在行首
  
  3).空格
  1>.SQL內算數運算子、邏輯運算子連線的兩個元素之間必須用空格分隔
  2>.逗號之後必須接一個空格
  3>.關鍵字、保留字和左括號之間必須有一個空格
  
  2.不等於統一使用"<>"
  
  Oracle認為"!="和"<>"是等價的,都代表不等於的意義。為了統一,不等於一律使用"<>"表示
  
  3.使用表的別名
  
  資料庫查詢,必須使用表的別名
  
  4.SQL文對錶欄位擴充套件的相容性
  
  在Java source裡使用Select *時,嚴禁透過getString(1)的形式得到查詢結果,必須使用getString("欄位名")的形式
  使用Insert時,必須指定插入的欄位名,嚴禁不指定欄位名直接插入values
  
  5.減少子查詢的使用
  
  子查詢除了可讀性差之外,還在一定程度上影響了SQL執行效率
  請儘量減少使用子查詢的使用,用其他效率更高、可讀性更好的方式替代
  
  6.適當新增索引以提高查詢效率
  
  適當新增索引可以大幅度的提高檢索速度
  請參看ORACLE SQL效能最佳化系列
  
  7.對資料庫表操作的特殊要求
  
  本專案對資料庫表的操作還有以下特殊要求:
  
  1).以邏輯刪除替代物理刪除
  
  注意:現在資料庫表中資料沒有物理刪除,只有邏輯刪除
  
  以deleteflag欄位作為刪除標誌,deleteflag='1'代表此記錄被邏輯刪除,因此在查詢資料時必須考慮deleteflag的因素
  
  deleteflag的標準查詢條件:NVL(deleteflag, '0') <> '1'
  
  2).增加記錄狀態欄位
  
  資料庫中的每張表基本都有以下欄位:DELETEFLAG、UPDATECOUNT、CREDATE、CREUSER、UPDATETIME、UPDATEUSER
  
  要注意在對標進行操作時必須考慮以下欄位
  
  插入一條記錄時要置DELETEFLAG='0', UPDATECOUNT=0, CREDATE=sysdate, CREUSER=登入User
  
  查詢一條記錄時要考慮DELETEFLAG,如果有可能對此記錄作更新時還要取得UPDATECOUNT作同步檢查
  
  修改一條記錄時要置UPDATETIME=sysdate, UPDATEUSER=登入User, UPDATECOUNT=(UPDATECOUNT+1) mod 1000,
  
  刪除一條記錄時要置DELETEFLAG='1'
  
  3).歷史表
  
  資料庫裡部分表還存在相應的歷史表,比如srv_twc_f和srv_twchistory_f
  
  在查詢資料時除了檢索所在表之外,還必須檢索相應的歷史表,對二者的結果做Union(或Union All)
  
  8.用執行計劃分析SQL效能
  
  EXPLAIN PLAN是一個很好的分析SQL語句的工具,它可以在不執行SQL的情況下分析語句
  
  透過分析,我們就可以知道ORACLE是怎樣連線表,使用什麼方式掃描表(索引掃描或全表掃描),以及使用到的索引名稱
  
  按照從裡到外,從上到下的次序解讀分析的結果
  
  EXPLAIN PLAN的分析結果是用縮排的格式排列的,最內部的操作將最先被解讀,如果兩個操作處於同一層中,帶有最小操作號的將首先被執行
  
  目前許多第三方的工具如PLSQL Developer和TOAD等都提供了極其方便的EXPLAIN PLAN工具
  
  PG需要將自己新增的查詢SQL文記入log,然後在EXPLAIN PLAN中進行分析,儘量減少全表掃描
  
  ORACLE SQL效能最佳化系列
  
  1.選擇最有效率的表名順序(只在基於規則的最佳化器中有效)
  
  ORACLE的解析器按照從右到左的順序處理FROM子句中的表名,因此FROM子句中寫在最後的表(基礎表driving table)將被最先處理
  
  在FROM子句中包含多個表的情況下,必須選擇記錄條數最少的表作為基礎表
  
  當ORACLE處理多個表時,會運用排序及合併的方式連線它們
  
  首先,掃描第一個表(FROM子句中最後的那個表)並對記錄進行排序;
  
  然後掃描第二個表(FROM子句中最後第二個表);
  
  最後將所有從第二個表中檢索出的記錄與第一個表中合適記錄進行合併
  
  例如:
  
  表 TAB1 16,384 條記錄
  
  表 TAB2 5 條記錄
  
  選擇TAB2作為基礎表 (最好的方法)
  
  select count(*) from tab1,tab2 執行時間0.96秒
  
  選擇TAB2作為基礎表 (不佳的方法)
  
  select count(*) from tab2,tab1 執行時間26.09秒
  
  如果有3個以上的表連線查詢,那就需要選擇交叉表(intersection table)作為基礎表,交叉表是指那個被其他表所引用的表
  
  例如:
  EMP表描述了LOCATION表和CATEGORY表的交集
  SELECT *
  FROM LOCATION L,
  CATEGORY C,
  EMP E
  WHERE E.EMP_NO BETWEEN 1000 AND 2000
  AND E.CAT_NO = C.CAT_NO
  AND E.LOCN = L.LOCN
  
  將比下列SQL更有效率
  SELECT *
  FROM EMP E ,
  LOCATION L ,
  CATEGORY C
  WHERE E.CAT_NO = C.CAT_NO
  AND E.LOCN = L.LOCN
  AND E.EMP_NO BETWEEN 1000 AND 2000
  
  2.WHERE子句中的連線順序
  
  ORACLE採用自下而上的順序解析WHERE子句
  
  根據這個原理,表之間的連線必須寫在其他WHERE條件之前,那些可以過濾掉最大數量記錄的條件必須寫在WHERE子句的末尾
  
  例如:
  (低效,執行時間156.3秒)
  SELECT *
  FROM EMP E
  WHERE SAL > 50000
  AND JOB = 'MANAGER'
  AND 25 < (SELECT COUNT(*) FROM EMP WHERE MGR=E.EMPNO);
  
  (高效,執行時間10.6秒)
  SELECT *
  FROM EMP E
  WHERE 25 < (SELECT COUNT(*) FROM EMP WHERE MGR=E.EMPNO)
  AND SAL > 50000
  AND JOB = 'MANAGER';
  
  3.SELECT子句中避免使用'*'
  
  當你想在SELECT子句中列出所有的COLUMN時,使用動態SQL列引用'*'是一個方便的方法,不幸的是,這是一個非常低效的方法
  
  實際上,ORACLE在解析的過程中,會將'*'依次轉換成所有的列名
  
  這個工作是透過查詢資料字典完成的,這意味著將耗費更多的時間
  
  4.減少訪問資料庫的次數
  
  當執行每條SQL語句時,ORACLE在內部執行了許多工作:解析SQL語句,估算索引的利用率,繫結變數,讀資料塊等等
  
  由此可見,減少訪問資料庫的次數,就能實際上減少ORACLE的工作量
  
  例如:
  
  以下有三種方法可以檢索出僱員號等於0342或0291的職員
  
  方法1 (最低效)
  SELECT EMP_NAME, SALARY, GRADE
  FROM EMP
  WHERE EMP_NO = 342;
  
  SELECT EMP_NAME, SALARY, GRADE
  FROM EMP
  WHERE EMP_NO = 291;
  
  方法2 (次低效)
  DECLARE
  CURSOR C1 (E_NO NUMBER) IS
  SELECT EMP_NAME,SALARY,GRADE
  FROM EMP
  WHERE EMP_NO = E_NO;
  BEGIN
  OPEN C1(342);
  FETCH C1 INTO …,…,…;
  …
  OPEN C1(291);
  FETCH C1 INTO …,…,…;
  …
  CLOSE C1;
  END;
  
  方法2 (高效)
  SELECT A.EMP_NAME, A.SALARY, A.GRADE,
  B.EMP_NAME, B.SALARY, B.GRADE
  FROM EMP A, EMP B
  WHERE A.EMP_NO = 342
  AND B.EMP_NO = 291;
  
  5.使用DECODE函式來減少處理時間
  
  使用DECODE函式可以避免重複掃描相同記錄或重複連線相同的表
  
  例如:
  SELECT COUNT(*), SUM(SAL)
  FROM EMP
  WHERE DEPT_NO = '0020'
  AND ENAME LIKE 'SMITH%';
  
  SELECT COUNT(*), SUM(SAL)
  FROM EMP
  WHERE DEPT_NO = '0030'
  AND ENAME LIKE 'SMITH%';
  
  你可以用DECODE函式高效地得到相同結果
  SELECT COUNT(DECODE(DEPT_NO, '0020', 'X', NULL)) D0020_COUNT,
  COUNT(DECODE(DEPT_NO, '0030', 'X', NULL)) D0030_COUNT,
  SUM(DECODE(DEPT_NO, '0020', SAL, NULL)) D0020_SAL,
  SUM(DECODE(DEPT_NO, 0030, SAL, NULL)) D0030_SAL
  FROM EMP
  WHERE ENAME LIKE 'SMITH%';
  
  'X'表示任何一個欄位
  類似的,DECODE函式也可以運用於GROUP BY和ORDER BY子句中
  
  6.用Where子句替換HAVING子句
  
  避免使用HAVING子句,HAVING只會在檢索出所有記錄之後才對結果集進行過濾,這個處理需要排序、統計等操作
  
  如果能透過WHERE子句限制記錄的數目,那就能減少這方面的開銷
  
  例如:
  低效
  SELECT REGION, AVG(LOG_SIZE)
  FROM LOCATION
  GROUP BY REGION
  HAVING REGION REGION != 'SYDNEY'
  AND REGION != 'PERTH'
  
  高效
  SELECT REGION, AVG(LOG_SIZE)
  FROM LOCATION
  WHERE REGION REGION != 'SYDNEY'
  AND REGION != 'PERTH'
  GROUP BY REGION
  
  7.減少對錶的查詢
  
  在含有子查詢的SQL語句中,要特別注意減少對錶的查詢
  
  例如:
  
  低效
  SELECT TAB_NAME
  FROM TABLES
  WHERE TAB_NAME = (SELECT TAB_NAME
  FROM TAB_COLUMNS
  WHERE VERSION = 604)
  AND DB_VER = (SELECT DB_VER
  FROM TAB_COLUMNS
  WHERE VERSION = 604)
  
  高效
  SELECT TAB_NAME
  FROM TABLES
  WHERE (TAB_NAME, DB_VER) = (SELECT TAB_NAME, DB_VER
  FROM TAB_COLUMNS
  WHERE VERSION = 604)
  
  Update多個Column例子:
  低效
  UPDATE EMP
  SET EMP_CAT = (SELECT MAX(CATEGORY)
  FROM EMP_CATEGORIES),
  SAL_RANGE = (SELECT MAX(SAL_RANGE)
  FROM EMP_CATEGORIES)
  WHERE EMP_DEPT = 0020;
  
  高效
  UPDATE EMP
  SET (EMP_CAT, SAL_RANGE) = (SELECT MAX(CATEGORY), MAX(SAL_RANGE)
  FROM EMP_CATEGORIES)
  WHERE EMP_DEPT = 0020;
  
  8.使用表的別名(Alias)
  
  當在SQL語句中連線多個表時,請使用表的別名並把別名字首於每個Column上
  
  這樣可以減少解析的時間並減少那些由Column歧義引起的語法錯誤
  
  9.用EXISTS替代IN
  
  在許多基於基礎表的查詢中,為了滿足一個條件,往往需要對另一個表進行聯接
  
  在這種情況下,使用EXISTS(或NOT EXISTS)通常將提高查詢的效率
  
  低效
  SELECT *
  FROM EMP (基礎表)
  WHERE EMPNO > 0
  AND DEPTNO IN (SELECT DEPTNO
  FROM DEPT
  WHERE LOC = 'MELB')
  
  高效
  SELECT *
  FROM EMP (基礎表)
  WHERE EMPNO > 0
  AND EXISTS (SELECT 'X'
  FROM DEPT
  WHERE DEPT.DEPTNO = EMP.DEPTNO
  AND LOC = 'MELB')
  
  10.用NOT EXISTS替代NOT IN
  
  在子查詢中,NOT IN子句將執行一個內部的排序和合並
  
  無論在哪種情況下,NOT IN都是最低效的,因為它對子查詢中的表執行了一個全表遍歷
  
  為了避免使用NOT IN,我們可以把它改寫成外連線(Outer Joins)或NOT EXISTS
  
  例如:
  SELECT …
  FROM EMP
  WHERE DEPT_NO NOT IN (SELECT DEPT_NO
  FROM DEPT
  WHERE DEPT_CAT = 'A');
  
  為了提高效率改寫為
  高效
  SELECT …
  FROM EMP A, DEPT B
  WHERE A.DEPT_NO = B.DEPT(+)
  AND B.DEPT_NO IS NULL
  AND B.DEPT_CAT(+) = 'A'
  
  最高效
  SELECT …
  FROM EMP E
  WHERE NOT EXISTS (SELECT 'X'
  FROM DEPT D
  WHERE D.DEPT_NO = E.DEPT_NO
  AND DEPT_CAT = 'A');
  
  11.用表連線替換EXISTS
  
  通常來說,採用表連線的方式比EXISTS更有效率
  
  例如:
  SELECT ENAME
  FROM EMP E
  WHERE EXISTS (SELECT 'X'
  FROM DEPT
  WHERE DEPT_NO = E.DEPT_NO
  AND DEPT_CAT = 'A');
  
  更高效
  SELECT ENAME
  FROM DEPT D, EMP E
  WHERE E.DEPT_NO = D.DEPT_NO
  AND DEPT_CAT = 'A';
  
  12.用EXISTS替換DISTINCT
  
  當提交一個包含多表資訊(比如部門表和僱員表)的查詢時,避免在SELECT子句中使用DISTINCT,一般可以考慮用EXIST替換
  
  例如:
  
  低效
  SELECT DISTINCT DEPT_NO, DEPT_NAME
  FROM DEPT D, EMP E
  WHERE D.DEPT_NO = E.DEPT_NO
  
  高效
  SELECT DEPT_NO, DEPT_NAME
  FROM DEPT D
  WHERE EXISTS (SELECT 'X'
  FROM EMP E
  WHERE E.DEPT_NO = D.DEPT_NO);
  
  EXISTS使查詢更為迅速,因為RDBMS核心模組將在子查詢的條件一旦滿足後,立刻返回結果
  
  13.用索引提高效率
  
  索引是表的一個概念部分,用來提高檢索資料的效率。實際上,ORACLE使用了一個複雜的自平衡B-tree結構
  
  通常,透過索引查詢資料比全表掃描要快。當ORACLE找出執行查詢和Update語句的最佳路徑時,ORACLE最佳化器將使用索引
  
  同樣,在聯結多個表時使用索引也可以提高效率。另一個使用索引的好處是,它提供了主鍵(primary key)的唯一性驗證
  
  除了那些LONG或LONG RAW資料型別,你可以索引幾乎所有的列
  
  通常在大型表中使用索引特別有效,當然,在掃描小表時,使用索引同樣能提高效率
  
  雖然使用索引能得到查詢效率的提高,但是我們也必須注意到它的代價
  
  索引需要空間來儲存,也需要定期維護,每當有記錄在表中增減或索引列被修改時,索引本身也會被修改
  
  這意味著每條記錄的INSERT、DELETE、UPDATE將為此多付出4、5次的磁碟I/O
  
  因為索引需要額外的儲存空間和處理,那些不必要的索引反而會使查詢反應時間變慢
  
  ORACLE對索引有兩種訪問模式:
  
  1).索引唯一掃描(INDEX UNIQUE SCAN)
  
  大多數情況下, 最佳化器透過WHERE子句訪問INDEX
  
  例如:
  表LODGING有兩個索引:建立在LODGING列上的唯一性索引LODGING_PK和建立在MANAGER列上的非唯一性索引LODGING$MANAGER
  SELECT *
  FROM LODGING
  WHERE LODGING = 'ROSE HILL';
  
  在內部,上述SQL將被分成兩步執行:
  
  首先,LODGING_PK索引將透過索引唯一掃描的方式被訪問,獲得相對應的ROWID;然後透過ROWID訪問表的方式執行下一步檢索
  
  如果被檢索返回的列包括在INDEX列中,ORACLE將不執行第二步的處理(透過ROWID訪問表)
  
  因為檢索資料儲存在索引中,單單訪問索引就可以完全滿足查詢結果
  
  2).索引範圍查詢(INDEX RANGE SCAN)
  
  適用於兩種情況:
  
  1>.基於唯一性索引的一個範圍的檢索
  
  2>.基於非唯一性索引的檢索
  
  例1
  SELECT LODGING
  FROM LODGING
  WHERE LODGING LIKE 'M%';
  
  WHERE子句條件包括一系列值,ORACLE將透過索引範圍查詢的方式查詢LODGING_PK
  
  由於索引範圍查詢將返回一組值,它的效率就要比索引唯一掃描低一些
  
  例2
  SELECT LODGING
  FROM LODGING
  WHERE MANAGER = 'BILL GATES';
  
  這個SQL的執行分兩步,LODGING$MANAGER的索引範圍查詢(得到所有符合條件記錄的ROWID),透過ROWID訪問表得到LODGING列的值
  
  由於LODGING$MANAGER是一個非唯一性的索引,資料庫不能對它執行索引唯一掃描
  
  WHERE子句中,如果索引列所對應的值的第一個字元由萬用字元(WILDCARD)開始,索引將不被採用
  SELECT LODGING
  FROM LODGING
  WHERE MANAGER LIKE '%HANMAN';
  
  在這種情況下,ORACLE將使用全表掃描
  
  14.避免在索引列上使用計算
  
  WHERE子句中,如果索引列是函式的一部分,最佳化器將不使用索引而使用全表掃描
  
  例如:
  
  低效
  SELECT …
  FROM DEPT
  WHERE SAL * 12 > 25000;
  
  高效
  SELECT …
  FROM DEPT
  WHERE SAL > 25000/12;
  
  請務必注意,檢索中不要對索引列進行處理,如:TRIM,TO_DATE,型別轉換等操作,破壞索引,使用全表掃描,影響SQL執行效率
  
  15.避免在索引列上使用IS NULL和IS NOT NULL
  
  避免在索引中使用任何可以為空的列,ORACLE將無法使用該索引
  
  對於單列索引,如果列包含空值,索引中將不存在此記錄;
  
  對於複合索引,如果每個列都為空,索引中同樣不存在此記錄。如果至少有一個列不為空,則記錄存在於索引中
  
  如果唯一性索引建立在表的A列和B列上,並且表中存在一條記錄的A,B值為(123,null),
  
  ORACLE將不接受下一條具有相同A,B值(123,null)的記錄插入
  
  如果所有的索引列都為空,ORACLE將認為整個鍵值為空,而空不可能等於空,因此你可以插入1000條具有相同鍵值的記錄,當然它們都是空!
  
  因為空值不存在於索引列中,所以WHERE子句中對索引列進行空值比較將使ORACLE停用該索引
  
  低效(索引失效)
  SELECT …
  FROM DEPARTMENT
  WHERE DEPT_CODE IS NOT NULL
  
  16.使用UNION-ALL和UNION
  
  當SQL語句需要UNION兩個查詢結果集合時,這兩個結果集合會以UNION-ALL的方式被合併,然後在輸出最終結果前進行排序
  
  如果用UNION ALL替代UNION,這樣排序就不是必要了,效率就會因此得到提高
  
  需要注意的是,UNION ALL將重複輸出兩個結果集合中相同記錄,因此還是要從業務需求分析使用UNION ALL的可行性
  
  關於索引下列經驗請參考:
  
  1).如果檢索資料量超過30%的表中記錄數,使用索引將沒有顯著的效率提高
  
  2).在特定情況下,使用索引也許會比全表掃描慢,但這是同一個數量級上的差距;而通常情況下,使用索引比全表掃描要快幾倍乃至幾千倍!
  
  其他具體內容請參考《ORACLE SQL效能最佳化系列》
  
  17.使用PrepareStatement
  
  在同一個方法中,當迴圈使用SQL文時,為了提高效能,
  
  請使用PreparedStatement。注意,
  
  僅限使用於少數的模組。
  
  方法如下:
  ? PreparedStatement stmt
  = conn.prepareStatement("select a from TABLE_A where b=? c=?");
  
  for(?? ){
  ???? stmt.setInt(1, varB);
  ???? stmt.setString(2, varC);
  ?   ResultSet rst = stmt.executeQuery();
  
  }

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/14377/viewspace-897080/,如需轉載,請註明出處,否則將追究法律責任。

相關文章