MIT 6.830 Lab1 實驗記錄


Lab1

啟動

先 Fork 后 Clone,直接用 IDEA 打開,然后可以看到 test 文件是綠色的,右鍵 test 文件夾可以看到 Run 'All Tests',這樣就可以運行測試用例了,至於用不用 Ant 就看情況了。

SimpleDB 架構

Lab1 實驗指導中指出,SimpleDB 包括如下幾個部分:

  • 域,元組,模式
  • 謂詞,條件
  • 讀取磁盤的方法,遍歷元組的方法
  • 操作類(select, join, insert, delete)
  • 緩沖池
  • 目錄(表,模式)

Database Class:可以從這個類中獲取數據庫狀態,主要包括 Catalog,Buffer Pool,Log File。

Exercies 1

實現 Tuple 和 TupleDesc,前者是元組 Tuple,后者是關於元組的描述即 Schema。

Tuple 里面需要存儲 Field,Schema 需要存儲 Type,暫且都使用 List 來存儲好了。List 具體實現采用 ArrayList,因為 ArrayList 隨機訪問比 LinkedList 快。對於一個數據庫來說,往 List 里面插入刪除(屬性)次數應該不多,更多的是設置獲取值。暫且不考慮線程安全的問題。

在 Tuple 中,存儲每個域的值,初始化為 null,其他的方法和 List 基本接口一致,直接調用就好。

在 TupleDesc 中,第一次寫 fieldNameToIndex failed 了,空指針異常,因為沒有判斷 name 是否為空。如果 name 為空,即使 TupleDes 中存在為空的 fieldName,也不能相等,直接拋出沒有這個元素即可。

難度:easy

Exercises 2

實現 Catalog,Catalog 負責存儲表。一個表的元數據有如下信息:Id,文件,表名,主鍵,這些表的元數據使用一個內部類存儲起來。Id 用 long 類型存儲,每次自增 1,需要一個靜態的變量來計數。使用 List 存儲,選擇 LinkedList,因為每次都要遍歷才能獲取到節點上的信息,除非用節點信息建立散列表。

  • 后面有個接口,getTableId 返回的類型是 int,所以 Id 還是用 int 來存儲吧
  • 需要仔細閱讀 DbFile 接口,DbFile 有獲取 id 的操作,並且可以唯一標識,getTableId 還是用這個吧
  • DbFile 內部還有 Page 的讀寫操作,元組的插入刪除,獲取 TupleDesc 等操作。
  • 有了 DbFile 的 getId 方法,我們應該在 CatalogItem 中存儲 id 嗎?這里考慮到 id 是 DbFile 決定的,而且需要用這個 id 去獲取 DbFile 內部信息,所以每次需要 id 的時候,調用 DbFile 的 getId 方法。
  • 之后幾個 for 循環遍歷來查信息,第一次 test 失敗了兩個:handleDuplicateIds,handleDuplicateNames
  • handleDuplicateIds,遇到一樣的 id,表明這個 DbFile 已經存在,不需要再次添加了。
  • handleDuplicateNames,這個好處理,用一個 Map 存儲表名計次,第一次是原名,后面出現一樣名字的時候,新名字為 tableName{Count}
  • 上面兩個方法都需要判重,使用兩個 HashMap 好了
  • 寫完后,發現還是通過不了。仔細看 handleDuplicateNames 的測試用例,發現獲取名字也是用的相同的名字,那相同的表名,我怎么確定呢?那應該是覆蓋了。后面加入的表,在表名相同的情況下,覆蓋前面的表。兩個方法都是都采用覆蓋的策略。
  • 要怎么覆蓋呢?一開始我的辦法是找到那個 item,然后修改里面的內容。但是轉念一下,有一種情況,名字和 id 同時都沖突了,這怎么處理?列表里有兩個節點需要覆蓋!因此,這里采用的方法是,找到那個 item 的下標,然后如果找得到就移除。最后在 add
  • 后面突然意識到,如果表明是空串呢?這里暫且不處理,因為即使是空串,也不影響它的行為。一方面,獲取表明,拿到空串是 ok。另一方面,如果插入的表名為空,前面已經有一個空串表名了,那我們就直接覆蓋好了。
  • BTW,使用內部類的時候,IDEA 建議使用靜態的內部類。因為內部類沒有用到外部的普通成員和方法,因此這種依賴是單向的,外部依賴內部,所以可以使用靜態內部類。內部類對象需要擁有外部對象的引用,因此理論上應該是比靜態內部類占用多幾個字節的。其他對比看一看:https://www.zhihu.com/question/263827779

難度:easy,要考慮的東西多了一點點

Exercises 3

實現 BufferPool 中的 getPage 方法,還有構造器。BufferPool 中 Page 的數量是固定的,直接用一個數組來存。后面可能還要實現頁面置換的算法,比如 LRU,最近最少使用。那么我們需要維護所有頁的下標,然后再置換。這樣看來選擇數組,應該不會導致實現頁面置換算法過程中,有什么不方便的地方。

getPage 的參數列表中,有三個:事務的 id,Page 的 id,權限。因此,還需要記錄 page 的權限,開一個和 pages 一樣大的數組。getPage 的時候,遍歷,然后判斷權限,如果請求的權限大於實際的權限,就拋出異常。

Exercises 4

這個部分主要完成 Heap 開頭的幾個文件。

HeapFile 是 DbFile 的一個具體實現,DbFile 代表了一個存儲在磁盤上的文件,一個 DbFile 存儲一個表(因此還有一個模式),需要提供一個唯一的 id 來標識,DbFile 上提供了 IO 相關的接口,比如按 Page 讀寫,從文件插入刪除元素。

HeapPage 是 Page 的一個具體實現,Page 接口表示 BufferPool 中存儲的一個 Page,因此可以說 Page 在內存中。Page 接口提供了一個 getPageData 方法,返回值是 byte[],其實 Page 就是一個 byte[] 數組的封裝。封裝了什么東西呢?有 id,dirty 等字段,還要能支持事務,用以恢復數據。

HeapPageId 是 PageId 的一個具體實現,PageId 接口,顧名思義就是 Page 的 Id,為什么需要封裝起來呢?我也不懂啊。封裝了這個 Page 歸屬的表的 Id,還有這個 Page 的序號。

HeapPageId.java

  • 留着 hashCode 方法沒寫,然后跑了測試,失敗了一個 testHashCode,看來 hashCode 這個方法需要實現
  • 於是查一查該如何實現 hashCode,一查發現 IDEA 中可以自動生成,於是生成了 equals 和 hashCode 方法。這個 equals 方法和我之前寫的略有區別。第一個是我寫的,后面那個是 IDEA 生成的。區別在於 instanceofgetClass() == o.getClass()。前者子類都會包括在內,因此可能子類都一起 equals 了,所以我的那個做法是錯誤的,不能使用 instanceof

原來的實現:

IDEA 生成的實現:

  • 那 hashCode 該如何實現呢?IDEA 生成的方法調用了 Objects 工具類中的 hash 方法,而 Objects.hash 實際上這些可變參數當成數組傳入 Arrays.hash 中。在 Arrays.hash 中,它是有個迭代公式 hash = hash * 31 + element.hashCode(),如果 element 為 null,那么是 0。element 如果是 Integer,那 hashCode 就是它的值。如果 element 是 Integer,那么使用字符串的一半字符和類似上面的公式來計算字符串的 hash 值,並且字符串中這個值需要被緩存下來,使用字符串來構造字符串的話,可以用之前的字符串的 hash 值。

Arrays.hash 方法

StringUTF16.hashCode 方法

RecordId.java

RecordId 是一條元組的 id,是一個實體類。寫好之后 equals failed 了,主要是因為判斷 pageId 相等用的是 ==,而不是調用它的 equals 方法🐷。

HeapPage.java

  • 自己來分析一遍一個 Page 可以存儲多少個元組。假設一個元組的長度是 len,那么需要 len * 8 bit 來存儲實際的數據,還需要在頭部放一個比特來表示這個元組是否有效(被刪除了,或者還沒有初始化)。那么實際上一個元組消耗的比特數是 len * 8 + 1 bit。Page 的長度是固定的,在 BufferPool 中有一個屬性確定了這個值,記作 pageSize。所以用 pageSize 去除一個元組的實際消耗長度就可以得到可以存儲多少個元組,結果向下取整即可。

\[numSlots = floor((pageSize * 8) / (len * 8 + 1)) \]

頭部的長度如何確定呢?一個字節可以存儲 8 個元組,因此 numSlots 除以 8,就可以得到至少需要這么多個字節,結果要向上取整。因為是至少需要

  • 后面需要獲取空的 slot 的數量,這里暫時假設空的 slot 存放的內容是 null。不不不,仔細看看代碼,這里應該需要用的是頭部的 bitmap,那就統計 bitmap 中 1 的個數。統計比特也沒有想到什么特別好的辦法,只好一個一個比特統計了。最多就開個多線程,或者使用 Java8 的流處理。

突然想到一個辦法,因為一個字節最多 256 中狀態,所以可以開一個數組來存儲每一種狀態中 1 的比特個數。之后處理的時候,就不需要每次都逐位統計了。可以設置成靜態的,然后用靜態代碼塊初始化。用下面的代碼來初始化。之后統計的時候,直接映射就可以得到用了幾個 slot。

  • 判斷一個 slot 是否被用過。首先定位到 header 中對應的字節,然后找到對應的 bit。
  • 哇。一跑,四個測試都 failed...
  • 仔細排查才發現錯誤在定位對應的比特那里,首先找到對應的字節,我用 i 除以 8,然后向上取整。這就出錯了,因為不足的部分應該對應於某個 bit,所以出錯了,改成向下取整。再跑一次測試,還有兩個測試沒有過,一個是關於迭代器的(忘了實現),一個是關於獲取空閑數目的。
  • getNumEmptySlots,這里面錯誤的地方,是因為忘記了 byte 其實是有符號整數,我直接遍歷 header 里面的字節然后用 bitCount 做映射,這樣就可能出現越界的問題。另一方面,還要注意到整數的表示方法,-1 對應的是全 1,所以需要做點小操作。

  • 把迭代器的代碼補上,再跑一次,ok

Exercises 5

實現 HeapFile,從磁盤中讀取數據。迭代器中迭代的 Page 需要從 BufferPool 中獲取。

  • readPage 方法從磁盤中讀寫 Page,可能讀寫的是任意一頁,因此這里使用 RandomAccessFile 來進行讀寫。getId 就按照注釋中建議的那樣,使用傳入的文件絕對路徑做散列來得到 hashCode。numPages 需要計算這個 HeapFile 的大小,直接文件長度除以每頁的長度即可,小學數學呵 ( •̀ ω •́ )✧
  • 迭代器,需要自己實現一個 DbFileIterator,這次實現的內部類不選擇靜態的,因為需要訪問外部的變量,比如 BufferPool 的實例對象。Iterator 中讀取 Tuple 時讀取頁面,要求只能從 BufferPool 中獲取頁面。寫到這里,突然意識到 BufferPool 是全局變量,要從 Database 那個類中獲取才行。另外,還要獲取總的頁數,這是外部類的一個普通方法。一開始采用的是普通內部類,經過這么一分析,還是選擇靜態內部類,因為 BufferPool 可以從 Database 獲取,總頁數傳入構造器即可。后來發現,要存儲兩個外部類的域,emmm,我還是選擇普通靜態類吧。
  • 跑一下測試,失敗了兩個,兩個都是關於 Iterator 的,失敗的原因是,測試用例中,會測試在不 open 的情況下調用 hasNext,這樣就觸發了空指針異常。修改成測試用例的預期行為之后,再跑一下,發現還是沒通過。仔細一看,是 BufferPool 中找不到這個頁面,所以是 HeapFile 讀取了頁面之后沒有放入到 BufferPool 中,但是放入其中又需要頁面置換算法,所以這兩個測試應該不是這個 exercise 要通過的。
  • 仔細往后看練習 6,就會發現需要一個迭代器,加入我們這個練習不通過那個迭代器的測試,那么后面就沒辦法繼續了。於是,還是得通過這兩個測試用例。在 BufferPool 中 getPage 如果找不到,那么可以 readPage,然后放入到 BufferPool 中。頁面置換的方法,就用最簡單的 FIFO。改完之后,還剩下一個測試沒有通過。這個測試用例說的是,如果 close,那么之后的 next,getNext 操作需要拋出異常。在 HeapFileIterator 中增加一個表示關閉的布爾變量,如果關閉,那就拋出異常。測試用例,通過。

Exercise 6

實現 SeqScan,它是 OpIterator 的具體實現。OpIterator 是所有操作符需要實現的 Iterator 接口,要求在 open 中調用成員中的 open,close 中調用成員的 close。剛剛想到如果 open 了兩次,或者 close 兩次,那么應該是怎么樣的行為呢?這里就讓 open 拋出異常好了,不然會產生奇怪的現象,要么不跑出異常,直接返回也可以。在完成前面的基礎上,實現一個掃描操作其實不難,只需要將前面實現的迭代器經過封裝一下就好了。測試一下,通過。Lab1 完。

小結

前面記錄的內容,都是邊做邊寫的,整個過程比較意識流,想到什么寫什么。繼續往后面看,后面給出了一個例子,將前面構建的元素全部組合到一起,實現一個簡單的查詢語句:select * from table。雖然是自己寫的工具,但我發現如果不看文檔,我還寫不好呢。基本的步驟是:需要自己手動組合每一列的類型,名字,然后新建一個 TupleDesc,再用這個 desc 去讀取本地文件,這樣就產生了一個有 schema,有數據的表,再將這個表放入到數據庫中。為了遍歷所有的元組,需要新建一個事務的 id,然后開始遍歷,遍歷前后記得 open,close。完成遍歷,還要設置事務已經完成。

總的來說,第一個實驗不算太難,基本上就是按部就班的做。做不出來就面向測試編程,總可以做對的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM