Lab 2
這個實驗需要完成:增刪查改,頁面置換算法。
Exercise 1
實現 Filter 和 Join 操作,文檔中提到已經提供了 Project 和 OrderBy 的實現。用 IDEA 查看 Operator 的實現類,我們可以發現有 8 個實現,這些實現類對應着一個具體的操作:聚合,刪除,過濾,散列版本的連接,插入,連接,排序,投影。Operator 實現了 OpIterator 的部分方法,
- Predicate 和 JoinPredicate 直接按要求寫即可,測試直接過
- 實現 Filter 之前,先看一看 Operator 和 Project。為了實現 Filter,我們需要知道數據從哪里來,又要去到哪兒。數據來源是從一個 OpIterator 中拿,如果是一元操作符,那么需要一個 OpIterator。數據的去向,是 fetchNext 中返回的 Tuple。在 fetchNext 中,需要執行具體的操作。比如,Project 中的 fetchNext,它就篩選了屬性。Filter 中的 fetchNext 需要篩選元組。寫完一次過測試。值得一提的是,open 和 close,這兩個和構造析構道理類似。先進行父類的構造,然后再進行子類的。先進行子類的析構,然后再執行父類的。
- 接下來實現 Join,這是一個二元操作符,那么需要有兩個 OpIterator。對於 Join,最普通的方法就是進行兩遍掃描,然后返回匹配的元組,並且注釋中提到,我們需要將兩個元組合並到一起。那么 Join 有沒有辦法優化呢?
- Database System Concepts 這本書有個網站上有配套的 PPT,可以看第 15 章的 PPT,就有 Join 的實現方法。PPT 中列舉了 5 種方法,嵌套循環,分塊嵌套循環(因為內存放不下),建索引的嵌套循環,排序然后再連接,基於散列的連接。要搞懂還需要點時間,還是先做一個嵌套循環熟悉一下這個 SimpleDB 中能做什么,不能做什么先吧。
- 雖然思路是最簡單的嵌套循環,但是測試一跑,只有直接獲取屬性的那個通過了,干啊。報了兩個空指針異常。仔細查一查,錯誤出現在合並 Tuple 的操作中。之前設計 Tuple 的時候,初始化的每個屬性都是空,我在合並 Tuple 的時候,直接用的 addAll,這意味着並沒有清楚前面的空值,所以后面就出現了空指針異常。ok,這波之后,rewind 通過了。
- 還有兩個測試沒有通過啊。最后經過一番波折,在 fetchNext 那里找到了問題,因為沒有重置 child2 的指針!在適當的時機重置后,兩個測試都通過了。
Exercise 1 補充: Join 的實現方法
雖然通過了測試用例,但是不應該滿足於此,應該嘗試優化一下 Join。還有把 Join 的幾種實現方法都學一學。看完 PPT 后,除了嵌套循環,沒有一個我能做。🙃
Nested-Loop Join
嵌套循環,判斷是否滿足條件,如果滿足條件就加入到結果集。
Block Nested-Loop Join
這個 PPT 里面提到了一個情況,那就是如果表太多了,無法一次性全部讀進來的情況下,需要將表分塊讀進來。如果分塊讀取,還繼續使用第一個算法的話,性能會特別糟糕。假設 n 表示表的長度,b 表示分塊數目。如果連接表 r 和表 s,那么需要讀取次數為:$n_r \times b_s + b_r $。因為表是很長的,所以 \(n_r\) 肯定不會小,那么讀取磁盤的次數就大大增加了。因此,為了改進這個缺點,有如下的算法,兩兩分塊之間進行一次嵌套循環。總的時間復雜度是一樣的,但是 IO 操作大大減小了。這啟發我們以后考慮大規模的數據的時候,不僅僅要考慮理論的復雜度,還要考慮實際運行時候的硬件限定,比如內存有限,需要進行 IO。
Indexed Nested-Loop Join
嵌套循環中第二個循環,本質上是在尋找滿足條件的元組,那么可以在第二個表上建立索引,第二個循環就可以替換成索引了。PPT 中提到存在一個約束,要是等值連接或者自然連接才可以用索引替代掃描。假設操作用的是等值連接,在 SimpleDB 中,我們可以用需要判斷的域來建立散列表,用 Objects.hash 來獲取散列值。
Merge-Join
這個思路比較有意思,首先給兩個表排序,然后用雙指針來遍歷。
Hash-Join
使用兩次散列,第一次散列用來分塊,第二次散列用來匹配。
Exercise 2
實現聚合操作(count,sum,avg,max,min),聚合操作中有一個 group 分組要實現。只要在一個屬性上實現聚合操作。
Aggregate 這個類和之前的 Join 一樣,都是 Operator 的具體實現。Aggregate 中將不同類型的聚合操作抽離出來,比如對於整數型的聚合操作,需要專門寫一個整數型的 Aggregator 來處理。Aggregate 中,每次得到數據源 child 的時候,我們就需要初始化一次 Aggregator 來獲得聚合操作后的數據。
- 首先,我先把 Aggregate 的內容填寫了一下,然后開始看每個具體的 Aggregator。Aggregator 內部需要維護分組數據,維護 (groupValue, aggregateValue),這個好做,用散列表來做就好了。先實現 StringAggregator,按部就班,寫好之后測試就可以通過了。順便一提,散列表一開始用 Integer 映射到 Integer,但是后面需要返回一個 OpIterator 的時候,里面的 next 方法返回的 Tuple 需要設置 AggregateValue,因此將散列表的 key 改成了 Field 類型。
- 接下來實現 IntegerAggregator。實現 count,sum,max,min 都只需要維護一個值就好了,但是實現 avg 還需要維護多一個數。因此,建立散列表的時候,value 的類型需要存儲兩個整數。寫完,測試仨下就通過了,因為 isClosed 沒有設置初始值 failed 兩次,布爾值默認是 false。
- 接下來就是 Aggregate 了。把 Aggregate 填好空之后,跑一下測試用例,有一個沒有通過,sumStringGroupBy。
- 之后面向測試用例編程,單步調試看看錯誤在哪里,發現是返回的 Tuple 中 groupValue 的類型不一致,因為沒有用上構造器傳進來的 gFieldType,用上了就好了。在跑一下,AggregateTest 就通過了。試試看 systemtest 吧。
- systemtest 失敗了一個呢,testAverageNoGroup。是一個越界的問題,訪問了 list 中 -1 位置的 元素,這個 -1 很有可能來自於 gField,應該是前面有些地方沒有判斷導致的。仔細跟蹤到 mergeTupleIntoGroup,StringAggregator 中已經處理好了,但是 IntegerAggregator 中沒有做同樣的處理。疏忽大意。改好之后,Exercise 2 也就順利 pass 了。
Exercise 3
實現在內存中插入刪除數據。
- 首先完成的 HeapPage.java 上面的插入和刪除。對於插入,需要找到空位並且標記 bitmap,刪除只需要標記 bitmap 就好。寫好之后,一測發現只過了一個,原來是很多異常的情況沒有處理好。注意看 insertTuple 和 deleteTuple 中的注釋,需要拋出異常。設置好條件,拋出異常,之后測試(HeapPageWriteTest)就順利通過了。
- 接下來寫 HeapFile.java。HeapFile 中的插入元組,刪除元組方法,統一使用 BufferPool 中緩存的 Page,如果那個 Page 不在內存中,那么由 BufferPool 去讀取。文檔中有那么一句話:"Note that it is important that the HeapFile.insertTuple() and HeapFile.deleteTuple() methods access pages using the BufferPool.getPage() method",寫好了 HeapFile.java 中的 insertTuple 之后,測試沒有通過。仔細看了一遍測試代碼,測試中向一個空的 HeapFile 寫入元組。這個元組是通過 Utility.java 中的 getHeapTuple 獲取的,這個方法沒有創建一個表,所以執行到后面的時候,報錯了,說找不到這個表。
- 一開始覺得是測試的錯誤,認為他沒有設置好 RecordId。仔細看文檔,才會發現,需要我們自己去設置 RecordId。這個 RecordId 依賴於一個 PageId,PageId 需要去找到文件上可以填入元組的空閑頁,如果沒有,需要創建新的頁。對於創建的新頁面,需要寫入到磁盤,然后通過 BufferPool 讀取進來。
- 之后還需要填寫 BufferPool 中 insert 和 delete 的部分,這個調用 PageFile 中相應的部分即可。第一次測試沒有通過,原因在於 Tuple 中的 equals 方法沒有正確實現。這個 equals,需要元組的 schema 和屬性值相等即可。寫好 equals,測試就通過了。
Exercise 4
- Insert 和 Delete 隨便寫了一寫,Insert 的單元測試可以通過了,但是系統測試還不行。主要問題是在 DeleteTest 中沒有通過。
- 最后定位到問題,在 HeapPage 的迭代器中。DeleteTest 中一共創建了 1000+ 個元組,但是實際上只刪除了 550 個。經過排查,發現從最底層的 Operator 返回的元組,只有 550 個。因此,認為問題應該出現在比較底層的位置。最終找到了 HeapPage 的 Iterator,發現 hasNext 的判斷方法是
i < (numSlots - getNumEmptySlots())
,這么判斷是存在問題的。因為刪除的元組,可能是迭代器前面的元組,接着使用getNumEmptySlots()
得到的數量減少了,實際上不應該減少。 - 修改了 HeapPage 中 Iterator 的邏輯之后,重新測試,原來失敗的判斷,現在成功了。但是,后續的斷言仍然有問題。出現問題的地方是,刪除了元組之后,使用 SeqScan 仍然掃描到了元組。我很快就發現了問題所在的地方,在 HeapFile 中的 hasNext 判斷。如果元組橫跨了幾個頁,那么應該跳到那個頁。不應該像代碼中寫的那樣,獲取下一頁,然后返回 true。這么做是錯誤的,因為下一頁可能什么都沒有。
- 寫好之后,邏輯稍微有點問題,經過調整之后,測試通過了。但是修改了 HeapFile 這部分的邏輯之后,前面的一個測試失敗了。修改了 BufferPool 中的 insertTuple 之后,才能通過測試。markDirty 之后將內容寫入到磁盤。感覺這樣實在是有點奇怪。不知道應該如何處理,暫且為了通過測試,那么寫吧。
Exercise 5
在實現前面的基礎上,不做修改即可通過測試。之前的策略是先進先出。
總結
所有的測試都通過了。距離 Lab1 已經相隔了兩個月,這星期重新撿起來的時候,還把以前寫的代碼看了看。整體難度不算特別大,只要耐心一點,仔細調試下去,總是可以把測試通過的。
這一個實驗的有五個練習。練習一和二,實現各種操作符。練習三和四,完善文件、緩存相關的操作,最后實現插入和刪除。練習五,實現頁面置換算法,這里就選擇了最簡單的先入先出。