MIT 6.830 LAB3 Query Optimization查詢優化器
2021/04/12-2021/04/17
前言
課程地址:http://db.lcs.mit.edu/6.830/sched.php
代碼:https://github.com/MIT-DB-Class/simple-db-hw
講義:https://github.com/MIT-DB-Class/course-info-2018/
快要期末考了,所以趕着時間把Lab3寫完了。Lab3的代碼量其實並不是很多,但是難度會比前兩個Lab大,系統提供了整體的框架,所以主要在於理解。課程網站中的PPT十分有借鑒價值。
本次實驗主要內容:
- 實現TableStats類中的方法,使得可以估計過濾器的選擇率和遍歷的代價,使用直方圖或者你發明的其他方式展示結果。
- 實現JoinOptimizer類中的方法,使得可以估計cost和join的選擇率
- 實現JoinOptimizer類中的orderJoins方法。這個方法會根據前兩步計算出的數據,生成一個最佳的joins順序
CBO(cost-based optimizer)
在學習本次實驗之前,需要了解查詢優化器的一些相關知識
RBO & CBO
SQL優化的發展,則可以分為兩個階段,即RBO(Rule Base Optimization),和CBO(Cost Base Optimization)。
RBO,RBO主要是開發人員在使用SQL的過程中,有些發現有些通用的規則,可以顯著提高SQL執行的效率
例如,我們都知道join是非常耗時的一個操作,且性能與join雙方數據量大小呈線性關系(通常情況下)。那么很自然的一個優化,就是盡可能減少join左右雙方的數據量,於是就想到了先filter再join這樣一個rule。而非常多個類似的rule,就構成了RBO。
main idea of CBO
- 利用關於表的統計數據,來估計不同查詢計划的cost。通常來說,cost與join、selection的基數、filter的選擇率和join的謂詞有關。
- 使用數據來對join和select進行排序,並選擇最佳的實現方式
LAB3
exercise1
作為輔助類,提供方法來記錄table的數據,用於后期進行估算。
構造時候需要提供(buckets桶個數, min最小值, max最大值)
之后可以不斷往里面添加數據,然后調用
estimateSelectivity(Predicate.Op op, int v)
方法進行數據統計
實現
- IntHistogram.java
- StringHistogram.java
主要根據講義中的這個圖進行實現
這里我引入了內部類Bucket進行輔助實現
難點在於區間怎么定義,邊界條件要小心,必要時可以特判
private class Bucket {
private int left;
private int right;
private int count;
public Bucket(int left, int right) {
this.left = left;
this.right = right;
}
// getter and setter
}
核心方法實現,注意width計算,要從Bucket里獲得,而不是直接拿this.width,這個bug在exercise2才發現
/**
* Estimate the selectivity of a particular predicate and operand on this table.
* <p>
* For example, if "op" is "GREATER_THAN" and "v" is 5,
* return your estimate of the fraction of elements that are greater than 5.
*
* @param op Operator
* @param v Value
* @return Predicted selectivity of this particular operator and value
*/
public double estimateSelectivity(Predicate.Op op, int v) {
int index;
double sum;
Bucket bucket;
switch (op) {
case EQUALS:
index = getIndex(v);
if(index<0||index>=numBuckets){
return 0;
}else{
bucket = buckets.get(index);
return (1.0*bucket.getCount() / bucket.getWidth()) / ntup;
}
case GREATER_THAN:
index = getIndex(v);
if (index < 0) {
return 1;
} else if (index >= numBuckets) {
return 0;
} else {
bucket = buckets.get(index);
sum = 1.0*bucket.getCount() * (bucket.getRight() - v) / bucket.getWidth();
for (int i = index+1; i < numBuckets; i++) {
sum += buckets.get(i).getCount();
}
return sum / ntup;
}
case LESS_THAN:
index = getIndex(v);
if (index < 0) {
return 0;
} else if (index >= numBuckets) {
return 1;
} else {
bucket = buckets.get(index);
sum = 1.0*bucket.getCount() * (v - bucket.getLeft()) / bucket.getWidth();
for (int i = index - 1; i >= 0; i--) {
sum += buckets.get(i).getCount();
}
return sum / ntup;
}
case GREATER_THAN_OR_EQ:
return estimateSelectivity(Predicate.Op.GREATER_THAN,v-1);
case NOT_EQUALS:
return 1-estimateSelectivity(Predicate.Op.EQUALS,v);
case LESS_THAN_OR_EQ:
return estimateSelectivity(Predicate.Op.LESS_THAN,v+1);
default:
throw new UnsupportedOperationException();
}
}
exercise2
TableStats類用於統計某個Table的數據,包括選擇率、開銷等
實現
- TableStats.java中的
- 構造函數
estimateSelectivity(int field, Predicate.Op op, Field constant)
estimateScanCost()
estimateTableCardinality(double selectivityFactor)
在成員變量中引入
/**
* <FiledIndex,Histogram>
*/
private Map<Integer,StringHistogram> stringHistogramMap;
private Map<Integer,IntHistogram> integerIntHistogramMap;
為每一個field建立Histogram並調用即可
exercise3
計算JOIN的cost和cardinality,也就是join操作的開銷和join后的基數預估
實現
- JoinOptimizer.java
-
estimateJoinCost(LogicalJoinNode j, int card1, int card2, double cost1, double cost2)
-
estimateJoinCardinality(LogicalJoinNode j, int card1, int card2, boolean t1pkey, boolean t2pkey)
-
基本按照框架走就可
計算JoinCost使用講義中的公式
joincost(t1 join t2) = scancost(t1) + ntups(t1) x scancost(t2) //IO cost
+ ntups(t1) x ntups(t2) //CPU cost
計算基數,也是使用講義中簡化后的估算方法
-
對於equality joins
當一個屬性是primary key,由表連接產生的tuples數量不能大於non-primary key屬性的選擇數。
對於沒有primary key的equality joins,很難說連接輸出的大小是多少,可以是兩表被選擇數的乘積(如果兩表的所有tuples都有相同的值),或者也可以是0。
-
對於范圍scans,很難說清楚明確的數量。
輸出的數量應該與輸入的數量是成比例的,可以預估一個固定的分數代表range scans產生的向量叉積(cross-product),比如30%。總的來說,range join的開銷應該大於相同大小兩表的non-primary key equality join開銷。
exercise4
LAB中為我們實現了幾個輔助方法,我們只需要按照講義中的偽代碼將其串聯起來即可,雖然聽起來唬人,但需要動手寫的東西比較簡單,框架性的代碼已經給出了。
實現
- JoinOptimizer.java
Vector<LogicalJoinNode> orderJoins( HashMap<String, TableStats> stats, HashMap<String, Double> filterSelectivities, boolean explain)
核心就是翻譯這段偽代碼
j = set of join nodes
for (i in 1...|j|):
for s in {all length i subsets of j}
bestPlan = {}
for s' in {all length d-1 subsets of s}
subplan = optjoin(s')
plan = best way to join (s-s') to subplan
if (cost(plan) < cost(bestPlan))
bestPlan = plan
optjoin(s) = bestPlan
return optjoin(j)
這里使用到了一個十分巧妙的動態規划算法,課件上的描述如下:
直接將其翻譯即可實現
public Vector<LogicalJoinNode> orderJoins(
HashMap<String, TableStats> stats,
HashMap<String, Double> filterSelectivities, boolean explain)
throws ParsingException {
PlanCache pc = new PlanCache();
Set<Set<LogicalJoinNode>> nodeSets = new HashSet<>();
for (int i = 1; i <= joins.size(); i++) {
nodeSets = enumerateSubsets(joins,i);
for(Set<LogicalJoinNode> nodeSet:nodeSets){
double optCosts = Double.MAX_VALUE;
int optCards =0;
Vector<LogicalJoinNode> optJoins = null;
for(LogicalJoinNode n:nodeSet){
CostCard costCard = computeCostAndCardOfSubplan(stats,filterSelectivities,n,nodeSet,optCosts,pc);
if(costCard!=null){
optCosts = costCard.cost;
optJoins = costCard.plan;
optCards = costCard.card;
}
}
pc.addPlan(nodeSet,optCosts,optCards,optJoins);
}
}
Vector<LogicalJoinNode> res = null;
for(Set<LogicalJoinNode> nodes:nodeSets){
res = pc.getOrder(nodes);
}
if(explain){
printJoins(res,pc,stats,filterSelectivities);
}
return res;
}
優化點:Set<Set<T>> enumerateSubsets(Vector<T> v, int size)
方法
https://zhuanlan.zhihu.com/p/159688029這篇博文中提到了joinOrder運行慢的原因主要在於enumerateSubsets的方法,講義上也有這樣的描述:This method is not particularly efficient; you can earn extra credit by implementing a more efficient enumerator
於是,我們來優化一下這個方法
優化前:
@SuppressWarnings("unchecked")
public <T> Set<Set<T>> enumerateSubsets(Vector<T> v, int size) {
Set<Set<T>> els = new HashSet<Set<T>>();
els.add(new HashSet<T>());
for (int i = 0; i < size; i++) {
Set<Set<T>> newels = new HashSet<Set<T>>();
for (Set<T> s : els) {
for (T t : v) {
if(s.contains(t)){
continue;
}
Set<T> news = (Set<T>) (((HashSet<T>) s).clone());
if (news.add(t))
newels.add(news);
}
}
els = newels;
}
return els;
}
優化后:
@SuppressWarnings("unchecked")
public <T> Set<Set<T>> enumerateSubsets(Vector<T> v, int size) {
Set<Set<T>> els = new HashSet<Set<T>>();
Vector<Boolean> used = new Vector<>();
for (int i = 0; i < v.size(); i++) {
used.add(false);
}
enumerateSubsetsHelper(els,v,used,0,0,size);
}
private <T> void enumerateSubsetsHelper(Set<Set<T>> res,Vector<T> v,Vector<Boolean> used,int next,int count,int size){
if(count==size){
Set<T> tmp = new HashSet<>();
for (int i = 0; i < v.size(); i++) {
if(used.get(i)){
tmp.add(v.get(i));
}
}
res.add(tmp);
return;
}
for (int i = next; i <v.size()-(size-count-1); i++) {
used.set(i,true);
enumerateSubsetsHelper(res,v,used,i+1,count+1,size);
used.set(i,false);
}
}
reference
MIT 6.830 Database System 數據庫系統 Lab 3 實驗報告https://zhuanlan.zhihu.com/p/159688029
6.830 Lab 3: Query Optimizationhttps://blog.csdn.net/hjw199666/article/details/103639262