一. 關(guān)系數(shù)據(jù)庫系統(tǒng)的查詢處理過程
要做優(yōu)化, 首先理解查詢處理過程.
查詢處理: 把用戶提交給RDBMS的查詢語句轉(zhuǎn)換為高效的查詢執(zhí)行計劃.
經(jīng)過的步驟: 查詢分析, 查詢檢查, 查詢優(yōu)化(指的是DBMS完成的)和查詢執(zhí)行.
1)查詢分析
對查詢語句進(jìn)行掃描, 詞法分析(識別出保留字, 變量, 運(yùn)算符, 發(fā)現(xiàn)拼寫錯誤)和語法分析(判斷是否符合SQL的語法規(guī)則, 出現(xiàn)錯誤就報告SQL語法錯誤)
2)查詢檢查
有效性檢查: 對符合語法規(guī)則的查詢語句進(jìn)行語義檢查, 也就是根據(jù)數(shù)據(jù)字典中有關(guān)的模式定義去檢查語句中的數(shù)據(jù)庫對象, 比如表名, 屬性名是否都在存在和有效.
視圖轉(zhuǎn)換: 把對視圖的操作求解, 轉(zhuǎn)換成對基本表的操作
安全性: 還要檢查用戶權(quán)限
完整性檢查: 根據(jù)數(shù)據(jù)字典中存儲的完整性約束進(jìn)行檢查(主鍵約束, 外鍵約束, 用戶自定義約束非空, 取值范圍等).
如果都沒有問題, 那么就把SQL查詢語句轉(zhuǎn)換成內(nèi)部表示(等價的關(guān)系代數(shù)表達(dá)式), 構(gòu)建出一棵查詢的語法分析樹.
3) 查詢優(yōu)化
是數(shù)據(jù)庫技術(shù)的關(guān)鍵. 劃分為代數(shù)優(yōu)化(邏輯優(yōu)化), 物理優(yōu)化(存取路徑, 底層算法).
優(yōu)化的依據(jù): 基于規(guī)則, 基于代價, 基于語義
優(yōu)化器得出一個執(zhí)行策略, 并生成相應(yīng)的查詢計劃.
4) 查詢執(zhí)行
由code generator負(fù)責(zé)生成執(zhí)行能夠"查詢計劃"的實(shí)際代碼, 然后加以執(zhí)行并回送查詢結(jié)果.
一個典型的查詢過程可以參考這個, 里面寫的非常詳細(xì)!! http://www.jb51.net/article/33535.htm
二. 查詢優(yōu)化
1. 查詢優(yōu)化的意義
把最好的程序員的思維做在了DBMS中, 并且提供數(shù)據(jù)庫存儲的數(shù)據(jù)字典, 物理存儲信息供決策, 比普通應(yīng)用程序員自己寫的好很多. 因此選擇比較好的DBMS, 本身就是查詢優(yōu)化的基礎(chǔ)條件.
2. 選擇操作的算法
naive table scan: 全表掃描, 按照物理次序讀取Student表的M個block到內(nèi)存, 檢查是否滿足條件, 滿足則輸出.
index scan: 如果選擇條件中的屬性上有索引(比如學(xué)號, 姓名, age可能有B+), 那么可以用索引掃描方法, 通過索引先找到滿足條件的元組的指針, 然后通過指針在基本表中找到元組.
3. 連接操作的算法
連接操作在select中是最最最費(fèi)時間的, 因此有很多種算法.
做一個簡單的連接比如select * from student S inner join on SC where S.sno = SC.sno;
- naive nested loop join: O(N^2), 嵌套循環(huán)
- sort-merge join排序合并算法: 假設(shè)sno做連接, 那么兩個表都按照sno排序, 排好序了以后按照Student, 對SC進(jìn)行掃描, 注意由于排好序了, 因此兩張表都只要掃描一遍即可, 總的時間復(fù)雜度是O(NlgN + 2N) = O(NlgN)
如果已經(jīng)排好序了, 那就更快了. - index join: 假設(shè)SC表上已經(jīng)建立了屬性sno的索引, 那么對于Student中的每一個元組, 在SC中找到對應(yīng)的元組的時間就不是全表掃描O(N), 而是O(lgN).
- hash join: (神奇的hash大法!) 對記錄行數(shù)較少的那張表, 假設(shè)是Student表, 建立一個hashtable, 然后對大表SC, 用同樣的hash函數(shù), 對每個元組上的sno進(jìn)行hash, 如果在hashtable中有對應(yīng)的Student元組, 就連接.
整個時間開銷是O(N), 當(dāng)然了, 相比其他算法, 額外的空間開銷是O(N).
4. 代價模型
在大數(shù)據(jù)條件下, 代價模型不能只是簡單的CPU時間和內(nèi)存開銷. 要關(guān)注IO和網(wǎng)絡(luò)通信這些比較慢的瓶頸.
DBMS會根據(jù)其特有的代價模型來計算各種查詢執(zhí)行策略的代價, 然后選取代價最小的方案.
對集中式數(shù)據(jù)庫: 主要開銷是IO
對分布式數(shù)據(jù)庫: 主要開銷是IO + 網(wǎng)絡(luò)通信代價
5. 以一個實(shí)例分析連接操作的時間開銷
select Student.sname from Student, SC where Student.sno = SC.sno and SC.cno = 2;
naive: 先全體連接再逐個篩選
假設(shè)1000行Student記錄, 10000行SC選課記錄, 假設(shè)1個block能放10行Student, 1個block也能放100行SC.
那么naive算法IO開銷 = (假設(shè)內(nèi)存最多放6個block)
讀入行記錄: 1000/10 + (10000/100) * ( 1000/(105) ) = 2100 block
寫入中間結(jié)果到硬盤: 100010000 / 10 = 10^6 block
讀出中間結(jié)果, 進(jìn)行where條件篩選: 10^6 block
選擇列, 做輸出: 0
所以IO開銷 = x * 10^6
這其實(shí)也是mapreduce為什么這么慢的原因...大量的IO開銷.優(yōu)化: 先篩選再連接
對查詢的優(yōu)化, 應(yīng)當(dāng)關(guān)注如何降低參與連接的元組數(shù)目. 本語句中, 先進(jìn)行where篩選, 再連接才是正道.
讀入SC所有元組進(jìn)行篩選: 10000/100 = 100 block
由于篩選出來的元組很少, 比如只有50行 (1個block), 那么放在內(nèi)存中即可, 不用落盤.
讀入Student所有元組進(jìn)行連接: 1000/10 = 100 block
連接的時候, 把結(jié)果需要存盤: 100 * 5 = 500 block,
再對這500個block進(jìn)行連接條件的篩選, 需要讀入: 500 block
100 + 100 + 500*2 = 1200
所以IO開銷 = x * 10^3
降低到了千分之一的數(shù)量級別.
這種先做選擇, 再做連接的基本優(yōu)化, 叫做代數(shù)優(yōu)化. 因?yàn)槲覀儾]有建立或者利用任何物理結(jié)構(gòu), 因此代數(shù)優(yōu)化是算法上的優(yōu)化.
同樣地, 如果我們對SC表中sno建立了B+Tree index, 那么我們就不用讀入所有的塊, 只要讀取包含指定sno的塊,這樣也能提高速度, 這叫做物理優(yōu)化.
三. 代數(shù)優(yōu)化
1. 核心問題
關(guān)系代數(shù)表達(dá)式的等價變換規(guī)則
經(jīng)驗(yàn)性的優(yōu)化策略
2. 變換五大核心規(guī)則
總結(jié)起來就是: "(連接類的)交換律, 結(jié)合律; (投影和選擇類的)串接律, (這兩大類相互之間)分配率"
E1 X E2 = E2 X E1, (E1 X E2) X E3 = E1 X (E2 X E3)做笛卡爾積, 多個表做連接是滿足交換律和結(jié)合律的
投影和選擇的串接定律
多層的投影可以取小的那個
多層的選擇可以取交集(其實(shí)也是那個范圍比較小的), 這樣能夠把多次選擇多次表的掃描, 改成一次.選擇與投影交換律: 選擇和投影的順序可以隨意改變
選擇與笛卡爾積, 并, 自然連接, 差的分配律: 處在后面的選擇, 可以與處在前面的二目運(yùn)算順序進(jìn)行調(diào)整, 使得對相應(yīng)的表先實(shí)施選擇, 再實(shí)現(xiàn)連接等二目運(yùn)算. 這個非常重要, 是先選擇后進(jìn)行二目運(yùn)算的依據(jù), 又名"選擇提前".
選擇與笛卡爾積, 并的分配率: 可以先投影, 也可以先進(jìn)行二目運(yùn)算
還有幾個規(guī)則不是特別有用, 不敘述
3. 經(jīng)驗(yàn)性優(yōu)化五大策略
其實(shí)就是"選擇, 合并, 視圖"
選擇運(yùn)算盡可能先做, 這是最最最最重要和基本的, 這樣往往使得執(zhí)行代價減少了幾個數(shù)量級, 主要的原理就是選擇運(yùn)算能夠大大降低參與連接的元組的行數(shù), 使得連接生成的A?B結(jié)果也大大被縮小.
把選擇和投影運(yùn)算同時進(jìn)行, 如果有若干投影和選擇運(yùn)算, 并且他們都是針對同一個表, 那么可以在掃描這個表的時候同時完成這些所有的運(yùn)算, 以此避免重復(fù)掃描這張表.
把投影與其前或者后的雙目運(yùn)算(笛卡爾積, 等值連接, 并集, 差集)結(jié)合起來, 也就是說, 沒有必要為了選擇出幾個字段而單獨(dú)再重新掃描全表.
把某些選擇和在它前面要執(zhí)行的笛卡爾積結(jié)合起來成為一個連接運(yùn)算(比如變成等值連接), 這是因?yàn)檫B接運(yùn)算要比同樣情形下的笛卡爾積節(jié)省很多時間.
杜老師說: "笛卡爾積原則上是不能自己執(zhí)行的!!!太浪費(fèi)了!!!"
笛卡爾積先進(jìn)行了 O(N ? M)操作 生成了A?B大小的中間文件, 存盤, 然后讀盤, 然后再按照where選擇進(jìn)行篩選得到結(jié)果; 對比之下, 等值連接運(yùn)算也進(jìn)行了O(N?M)操作, 但是還進(jìn)行了篩選, 只生成比較小的中間文件, 存盤和讀盤的是一個非常小的結(jié)果集合. 大大減少了IO開銷.
- 找出公共子表達(dá)式(一次計算, 多次使用). 比如很多的查詢都基于某個公共部分, 那么可以定義一個公共子表達(dá)式, 然后先計算一次公共子表達(dá)式, 然后把它存盤, 供其他大量的表達(dá)式來使用. 我們定義視圖其實(shí)就是在實(shí)踐這種策略.
4. 舉例子
這是一棵查詢樹, 從葉子端開始執(zhí)行, 最后合并到頂部, 產(chǎn)生一個唯一的結(jié)果.
默認(rèn)情況下, select sname from student and SC where student.sno = SC.sno and SC.sno = '2';
就是這么構(gòu)建成查詢樹的.
四. 物理優(yōu)化
1. 基于經(jīng)驗(yàn)規(guī)則的優(yōu)化
對于小的表, 直接全表掃描, 即使列上有索引.
對于大的表, 如果是選擇條件涉及主鍵, 那么使用主鍵索引(MySQL等主流關(guān)系數(shù)據(jù)庫都會對主鍵建立索引);
如果不是涉及主鍵, 那么如果是等值查詢, 列上有索引, 就使用索引; 如果非等值查詢, 而是范圍值查詢, 那么范圍<=10%用索引, 范圍比較大的, 直接全表掃描.
And 和 OR: AND連接的, 優(yōu)先考慮使用索引; OR連接的, 優(yōu)先考慮使用順序掃描, 畢竟OR可能性非常多.
連接操作: 如果兩個表都按照連接屬性排序, 用sort-merge算法, 如果其中一個表在連接屬性上有索引,采用索引連接算法; 如果啥都沒有, 對小的表建立哈希表, 使用hash join方法; 或者使用基本的嵌套循環(huán), 不過外層循環(huán)(i循環(huán))使用小表, 這樣能稍微減小代價.
2. 基于代價的優(yōu)化
基于代價的優(yōu)化需要依賴數(shù)據(jù)庫表的各種"情報", 來計算出不同方案的代價, 從中選擇最優(yōu)的.
這些信息(meta info)包括:
對每個表來說:
表的元組行數(shù) N,
元組的長度, 即列的維度數(shù)目 P,
表占用的block數(shù)目 B,
表占用的溢出塊的塊數(shù) BO,對表中的每個列,
該列的不同值的個數(shù) k
列的最大值, 最小值max, min
列上有什么類型的索引 index: 按照實(shí)現(xiàn)方式有B+, Hash, Cluster / 按照類型來說有普通索引, 唯一索引, 主鍵索引, 聚集索引
列上的數(shù)值分布情況 (直方圖)
那么對于不同情況, 我們有如下的代價估算:
全表掃描 cost = B
有索引的掃描
如果選擇條件是key = value, 能適用唯一索引: L + 1
如果選擇條件是attr = value, 能使用普通索引: L + S (可能有S個元組滿足)
如果條件是范圍類的, 比如>, <, between A and B, 那么基本上就是接近全表掃描B;連接算法的代價
如果用嵌套循環(huán): 讀入Capacity-1塊B[a], 遍歷連接b表所有B[b], 再換下一批B[a], 直到a表結(jié)束. 所以, cost = B[a] + ( B[a]/(C-1) ) ? B[b] = a表IO次數(shù) + b表IO次數(shù), 假如要生成中間文件的話, 那么還得加上存下所有連接好的元組的磁盤IO開銷.
如果用sort-merge: cost = B[a] + B[b], 如果要把中間結(jié)果寫入外存, 那么還要加上存下所有連接好的元組的磁盤IO開銷, 這個開銷和上面嵌套循環(huán)的是一樣的.
假如表本身沒有排序, 那么排序的代價是B[a]logB[a] + B[b]logB[b]