序文
前回、このレポートを書くと言ってから、半年後、ようやくlab2を書くことにしました。
1. lab2 について
このラボ課題では、SimpleDB の一連の演算子を記述して、テーブルの変更 (レコードの挿入と削除など)、選択、結合、集計を実装します。これらは、ラボ 1 で作成した基盤の上に構築され、複数のテーブルに対して単純なクエリを実行できるデータベース システムを提供します。
さらに、ラボ 1 のバッファー プール管理の問題を無視しました。
データベースの存続期間中にメモリに収まりきらないページを参照するときに発生する問題に対処していません。ラボ 2 では、バッファ プールから古いページをフラッシュするエビクション ポリシーを設計します。
このラボでは、トランザクションやロックを実装する必要はありません。
2、lab2
1.演習1
演習 1 では、主に 2 つの演算子Filter
とJoin
- フィルター: この演算子は、コンストラクターの一部として指定された条件を満たすタプルのみを返します。
したがって、述語に一致しないタプルを除外します。
Join: この演算子は、コンストラクターの一部として渡された a に従って、2 つの子からのタプルを結合します。単純なネストされたループの結合のみが必要ですが、より興味深い結合の
実装を調べることができます。ラボの書き込みで実装について説明します。JoinPredicate
here は、Filter
たとえば、SQL ステートメント内の where の役割を指しますselect * from lists where id > 1
。重要な補助クラス here がありますPredicate
。これは、タプル内のフィールドを指定されたフィールドと比較して、単一のタプルでフィルタリング操作を実装するために使用されます。下の図では、タプル内のデータのどの列を比較するかを表すPredicate
3 つの重要なメンバー変数があることがわかります。これは、演算子を表す列挙型クラスであり、比較のロジックです。これは、比較される量を表します。比較した。fieldNum
op
EQUALS, GREATER_THAN, LESS_THAN, LESS_THAN_OR_EQ, GREATER_THAN_OR_EQ, LIKE, NOT_EQUALS
operand
Predicate
の関数を通じてfilter
、比較したいものを渡しTuple
、対応する列のデータを取得し、実装を通じて比較しますField
(例として取り上げます)。compare
IntField
//Predicate
public boolean filter(Tuple t) {
// some code goes here
boolean res = t.getField(this.fieldNum).compare(op, this.operand);
return res;
}
//IntField
public boolean compare(Predicate.Op op, Field val) {
IntField iVal = (IntField) val;
switch (op) {
case EQUALS:
case LIKE:
return value == iVal.value;
case NOT_EQUALS:
return value != iVal.value;
case GREATER_THAN:
return value > iVal.value;
case GREATER_THAN_OR_EQ:
return value >= iVal.value;
case LESS_THAN:
return value < iVal.value;
case LESS_THAN_OR_EQ:
return value <= iVal.value;
}
return false;
}
Predicate
単一の one をフィルタリングできるようになったのでTuple
、select * from lists where id > 1
テーブル全体を操作する必要がある場合、必要な反復子は 1 つだけです。Filter
イテレータがコンストラクタに渡され、各行を反復することがわかりますTuple
。
public Filter(Predicate p, OpIterator child) {
// some code goes here
this.predicate = p;
this.opIterator = child;
this.tupleDesc = child.getTupleDesc();
}
Filter
これは継承されOperator
、インターフェースをOperator
実装し、next() と hasNext() の両方で抽象メソッド fetchNext() を呼び出します。OpIterator
次に、fetchNext() を実装するだけです。
protected Tuple fetchNext() throws NoSuchElementException,
TransactionAbortedException, DbException {
// some code goes here
if (opIterator == null) {
throw new NoSuchElementException("Operator Iterator is empty");
}
while (opIterator.hasNext()) {
Tuple tuple = opIterator.next();
if (predicate.filter(tuple)) return tuple;
}
return null;
}
Filter
との実現が完了するとPredicate
、次Join
のことJoinPredicate
が容易になります。Join
私たちがここにいるのは内なるつながりであることは注目に値します。つまり、次のようなステートメントの機能を実現したいと考えていますSELECT * FROM a INNER JOIN b ON a.No=b.No
。内部結合と外部結合がわからない学生は、左右結合と内部結合の違いを読むことができます
の場合JoinPredicate
、およびPredicate
同様に、Join
2 つのタプルの特定のフィールドを比較する役割を持つヘルパー クラス。
//JoinPredicate
public boolean filter(Tuple t1, Tuple t2) {
// some code goes here
Field operand1 = t1.getField(fieldNum1);
Field operand2 = t2.getField(fieldNum2);
return operand1.compare(op, operand2);
}
また、次に fetchNext() を実装しますJoin
。ここでの考え方は、2 つのイテレータ child1 と child2 を使用して 2 つのテーブルのデータをループ処理することです。最初に child1 のタプルを取得して t に割り当て、次に t を child2 のタプルと比較し、条件を満たす t2 に接続します。接続条件 そして、接続された newTuple を返します。
protected Tuple fetchNext() throws TransactionAbortedException, DbException {
// some code goes here
while (this.it1.hasNext() || this.t != null) {
if (this.it1.hasNext() && this.t == null) {
t = it1.next();
}
while (it2.hasNext()) {
Tuple t2 = it2.next();
if (p.filter(t, t2)) {
TupleDesc td1 = t.getTupleDesc();
TupleDesc td2 = t2.getTupleDesc();
TupleDesc newTd = TupleDesc.merge(td1, td2);
Tuple newTuple = new Tuple(newTd);
newTuple.setRecordId(t.getRecordId());
int i = 0;
for (; i < td1.numFields(); ++i)
newTuple.setField(i, t.getField(i));
for (int j = 0; j < td2.numFields(); ++j)
newTuple.setField(i + j, t2.getField(j));
if (!it2.hasNext()) {
//child1匹配到的刚好是child2的最后一条记录,
//不重置的话取出child1的下一个tuple就不是与child2的第一个tuple进行比较了)
it2.rewind();
t = null;
}
return newTuple;
}
}
//child1的其中一个tuple与child2所有tuple都不匹配
//同样不重置的话取出child1的下一个tuple就不是与child2的第一个tuple进行比较
it2.rewind();
t = null;
}
return null;
}
2.演習2
演習 2 では、主に 5 つの型の整数と文字列 (GROUP BY、COUNT、SUM、AVG、MIN、MAX) でのグループ化をサポートする SQL 集計を実装できます。文字列型は COUNT のみを実装する必要があります。次のような SQL ステートメントを例にとると、select sum(money) as 总收入 from table GROUP BY xx
この実験では、1 つのフィールドに基づいてグループ化と集計を実装するだけで済みます。つまり、グループ化フィールドと集計フィールドは 1 つだけです。ここではグループ化フィールドxx
とmoney
集約フィールド
IntegerAggregator のメンバー変数は次のとおりです。
gbFieldIndex
: グループ化フィールドのシリアル番号
gbFieldType
: グループ化フィールドのタイプ
aggFieldIndex
: 集計フィールドのシリアル番号
gbHandler
:gbResult
集計結果を保存するために使用されるマップである抽象クラス、および分類が必須でない場合、そのキー値は分類されたフィールドでありNO_GROUPING_KEY
、その値は集計されたフィールドです。さまざまな実装の抽象メソッド handle() は、特定の操作を実装します. 実装されたGBHandler
クラスには、CountHandler、SumHandler、MaxHandler、MinHandler、および AvgHandler が含まれます。
private abstract class GBHandler{
ConcurrentHashMap<String,Integer> gbResult;
abstract void handle(String key,Field field);
private GBHandler(){
gbResult = new ConcurrentHashMap<>();
}
public Map<String,Integer> getGbResult(){
return gbResult;
}
}
private class CountHandler extends GBHandler {
@Override
public void handle(String key, Field field) {
if(gbResult.containsKey(key)){
gbResult.put(key,gbResult.get(key)+1);
}else{
gbResult.put(key,1);
}
}
}
private class SumHandler extends GBHandler{
@Override
public void handle(String key, Field field) {
if(gbResult.containsKey(key)){
gbResult.put(key,gbResult.get(key)+Integer.parseInt(field.toString()));
}else{
gbResult.put(key,Integer.parseInt(field.toString()));
}
}
}
private class MinHandler extends GBHandler{
@Override
void handle(String key, Field field) {
int tmp = Integer.parseInt(field.toString());
if(gbResult.containsKey(key)){
int res = gbResult.get(key)<tmp?gbResult.get(key):tmp;
gbResult.put(key, res);
}else{
gbResult.put(key,tmp);
}
}
}
private class MaxHandler extends GBHandler{
@Override
void handle(String key, Field field) {
int tmp = Integer.parseInt(field.toString());
if(gbResult.containsKey(key)){
int res = gbResult.get(key)>tmp?gbResult.get(key):tmp;
gbResult.put(key, res);
}else{
gbResult.put(key,tmp);
}
}
}
private class AvgHandler extends GBHandler{
ConcurrentHashMap<String,Integer> sum;
ConcurrentHashMap<String,Integer> count;
private AvgHandler(){
count = new ConcurrentHashMap<>();
sum = new ConcurrentHashMap<>();
}
@Override
public void handle(String key, Field field) {
int tmp = Integer.parseInt(field.toString());
if(gbResult.containsKey(key)){
count.put(key,count.get(key)+1);
sum.put(key,sum.get(key)+tmp);
}else{
count.put(key,1);
sum.put(key,tmp);
}
gbResult.put(key,sum.get(key)/count.get(key));
}
}
これでグループ化と集計の機能ができました.次にTupleを渡して呼び出すだけです.グループ化の型が不正な場合は例外がスローされます.正常な場合は通常のhandle()を呼び出すことができます.マップ操作。
public void mergeTupleIntoGroup(Tuple tup) {
// some code goes here
if(gbFieldType!=null&&(!tup.getField(gbFieldIndex).getType().equals(gbFieldType))){
throw new IllegalArgumentException("Given tuple has wrong type");
}
String key;
if (gbFieldIndex == NO_GROUPING) {
key = NO_GROUPING_KEY;
} else {
key = tup.getField(gbFieldIndex).toString();
}
gbHandler.handle(key,tup.getField(aggregateFieldIndex));
}
GBHandler
集計結果に対するイテレータを返します
public OpIterator iterator() {
// some code goes here
Map<String,Integer> results = gbHandler.getGbResult();
Type[] types;
String[] names;
TupleDesc tupleDesc;
List<Tuple> tuples = new ArrayList<>();
if(gbFieldIndex==NO_GROUPING){
types = new Type[]{
Type.INT_TYPE};
names = new String[]{
"aggregateVal"};
tupleDesc = new TupleDesc(types,names);
for(Integer value:results.values()){
Tuple tuple = new Tuple(tupleDesc);
tuple.setField(0,new IntField(value));
tuples.add(tuple);
}
}else{
types = new Type[]{
gbFieldType,Type.INT_TYPE};
names = new String[]{
"groupVal","aggregateVal"};
tupleDesc = new TupleDesc(types,names);
for(Map.Entry<String,Integer> entry:results.entrySet()){
Tuple tuple = new Tuple(tupleDesc);
if(gbFieldType==Type.INT_TYPE){
tuple.setField(0,new IntField(Integer.parseInt(entry.getKey())));
}else{
tuple.setField(0,new StringField(entry.getKey(),entry.getKey().length()));
}
tuple.setField(1,new IntField(entry.getValue()));
tuples.add(tuple);
}
}
return new TupleIterator(tupleDesc,tuples);
}
StringAggregator の実装は IntegerAggregator と同様であり、Handler 部分は COUNT を実装するだけでよいため、再度説明しません。次に、Aggregate の実装だけを残します. Aggregate クラスは、前の 2 つの Aggregators をカプセル化したものです. 集計フィールドの型に応じて、異なる Aggregators 内の Iterator() が呼び出され、結果セットの反復子が返されます.
3.演習3
演習 3 は、主に HeapPage、HeapFile、BufferPool でのタプルの挿入と削除を実装することです.ここでは、1 ページ内の HeapPage データの追加と削除を最初に実現し、HeapFile では各ページをトラバースするだけで済みますPage でメソッドを呼び出す BufferPool での実装 同様に、Database.getCatalog().getDatabaseFile(tableId) を呼び出すだけです。
まず HeapPage の挿入の実装を見てください. 最初に getNumEmptySlots() メソッドを呼び出してビット操作で空きスロットの数を取得します. 十分なスペースがない場合は例外がスローされます. 十分なスペースがある場合は,最初に遭遇した空のスロット スロット マークのマークが使用され、スロットの対応する位置にデータが挿入されます。タプルを削除するには、対応する位置を null に設定し、対応するスロットを未使用としてマークするだけです
public void insertTuple(Tuple t) throws DbException {
// some code goes here
// not necessary for lab1
if (getNumEmptySlots() == 0) throw new DbException("Not enough space to insert tuple");
if (!t.getTupleDesc().equals(this.td)) throw new DbException("Tuple's Description is not match for this page");
for (int i = 0; i < tuples.length; i++) {
if (!isSlotUsed(i)) {
markSlotUsed(i, true);
tuples[i] = t;
tuples[i].setRecordId(new RecordId(pid, i));
break;
}
}
}
public void deleteTuple(Tuple t) throws DbException {
// some code goes here
// not necessary for lab1
RecordId recordId = t.getRecordId();
int tupleIndex = recordId.getTupleNumber();
if (recordId != null && pid.equals(recordId.getPageId())) {
if (tupleIndex < getNumTuples() && isSlotUsed(tupleIndex)) {
tuples[tupleIndex] = null;
markSlotUsed(tupleIndex, false);
return;
}
throw new DbException("can't find tuple in the page");
}
throw new DbException("can't find tuple in the page");
}
public int getNumEmptySlots() {
// some code goes here
int res = 0;
for (int i = 0; i < numSlots; i ++ ) {
if ((header[i / 8] >> (i % 8) & 1) == 0) {
res ++;
}
}
return res;
}
/**
* Returns true if associated slot on this page is filled.
*/
public boolean isSlotUsed(int i) {
// some code goes here
int index = i / 8; //在bitmap的第几个字节中
int offset = i % 8; //在第几个字节中的偏移量
return (header[index] & (1 << offset)) != 0; //判断是否为1,即该slot是否填充
}
/**
* Abstraction to fill or clear a slot on this page.
*/
private void markSlotUsed(int i, boolean value) {
// some code goes here
// not necessary for lab1
int index = Math.floorDiv(i, 8);
byte b = header[index];
byte mask = (byte) (1 << (i % 8));
// change header's bitmap
if (value) header[index] = (byte) (b | mask);
else header[index] = (byte) (b & (~mask));
}
HeapPage の挿入と削除を実装し、空きスロットのあるページを見つけるためにすべてのページをトラバースする HeapFile を実装しました. すべてのページは BufferPool から取得されることに注意してください. BufferPool が存在しない場合は、それらを見つけるためにディスクに移動します. 現在のテーブルのすべてのページに残りのスペースがない場合は、新しいページを書き込む必要があります (つまり、writePage())。削除は簡単です。ページの削除を呼び出すだけです。しかし、挿入および削除するたびに変更されたページのリストを返さなければならないのはなぜでしょうか? これは、ダーティ ページを返し、バッファ プールでダーティ マークを付けるためです。
public List<Page> insertTuple(TransactionId tid, Tuple t)
throws DbException, IOException, TransactionAbortedException {
ArrayList<Page> list = new ArrayList<>();
BufferPool pool = Database.getBufferPool();
int tableid = getId();
for (int i = 0; i < numPages(); ++i) {
HeapPage page = (HeapPage) pool.getPage(tid, new HeapPageId(tableid, i), Permissions.READ_WRITE);
if (page.getNumEmptySlots() > 0) {
page.insertTuple(t);
page.markDirty(true, tid);
list.add(page);
return list;
}
}
HeapPage page = new HeapPage(new HeapPageId(tableid, numPages()), HeapPage.createEmptyPageData());
page.insertTuple(t);
writePage(page);
list.add(page);
return list;
}
public ArrayList<Page> deleteTuple(TransactionId tid, Tuple t) throws DbException,
TransactionAbortedException {
// some code goes here
ArrayList<Page> list = new ArrayList<>();
HeapPage page = (HeapPage) Database.getBufferPool().getPage(tid, t.getRecordId().getPageId(), Permissions.READ_WRITE);
page.deleteTuple(t);
list.add(page);
return list;
// not necessary for lab1
}
public void writePage(Page page) throws IOException {
// some code goes here
int pageNumber = page.getId().getPageNumber();
if (pageNumber > numPages()) {
throw new IllegalArgumentException("page is not in the heap file or page'id in wrong");
}
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
randomAccessFile.seek(pageNumber * BufferPool.getPageSize());
byte[] pageData = page.getPageData();
randomAccessFile.write(pageData);
randomAccessFile.close();
// not necessary for lab1
}
最後に BufferPool への挿入と削除を実装する.databaseFile.insertTuple(tid, t)
修正したダーティページを取得したら, markDirty を呼び出してマークダーティ操作を行う. これがlruCache.put(page.getId(), page)
次の実験で実装する LRU キャッシュの処理である. 削除操作は同じです。
public void insertTuple(TransactionId tid, int tableId, Tuple t)
throws DbException, IOException, TransactionAbortedException {
// some code goes here
// List<Page> pageList = Database.getCatalog().getDatabaseFile(tableId).insertTuple(tid, t);
// for (Page page : pageList) {
// addToBufferPool(page.getId(), page);
// }
// not necessary for lab1
DbFile databaseFile = Database.getCatalog().getDatabaseFile(tableId);
//System.out.println(tid.getId());
List<Page> pages = databaseFile.insertTuple(tid, t);
//System.out.println(tid.getId());
for (Page page : pages) {
//用脏页替换buffer中现有的页
page.markDirty(true, tid);
lruCache.put(page.getId(), page);
}
}
public void deleteTuple(TransactionId tid, Tuple t)
throws DbException, IOException, TransactionAbortedException {
// some code goes here
// DbFile dbFile = Database.getCatalog().getDatabaseFile(t.getRecordId().getPageId().getTableId());
// dbFile.deleteTuple(tid, t);
// not necessary for lab1
DbFile dbFile = Database.getCatalog().getDatabaseFile(t.getRecordId().getPageId().getTableId());
List<Page> pages = dbFile.deleteTuple(tid, t);
for (int i = 0; i < pages.size(); i++) {
pages.get(i).markDirty(true, tid);
}
}
4.演習4
演習 4 には Insert と Delete の 2 つの演算子が必要です. 演習 3 の挿入と削除により, ここでは非常に簡単です. 演習 1 と同様に, Insert は Operator を継承します. 実装する必要があるのは fetchNext() だけです. 返されるフィールドは数値です.ここでの状態は、呼び出しの繰り返しを避けるためです。Delete の実装も同様であるため、ここでは繰り返しません。
//Insert
protected Tuple fetchNext() throws TransactionAbortedException, DbException {
// some code goes here
if (state) return null;
Tuple t = new Tuple(this.td);
int cnt = 0;
BufferPool bufferPool = Database.getBufferPool();
while (child.hasNext()) {
try {
bufferPool.insertTuple(tid, this.tableID, child.next());
} catch (IOException e) {
throw new DbException("fail to insert tuple");
}
cnt++;
}
t.setField(0, new IntField(cnt));
state = true;
return t;
}
5.演習5
ページ削除戦略を実装する. インターネット上のこの実験に参加した全員が LRU を実装している. アイデアは非常に単純である. 両端リンクリストとマップで問題を解決できる.
public class LRUCache<K,V> {
class DLinkedNode {
K key;
V value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {
}
public DLinkedNode(K _key, V _value) {
key = _key; value = _value;}
}
private Map<K, DLinkedNode> cache = new ConcurrentHashMap<K, DLinkedNode>();
private int size;
private int capacity;
private DLinkedNode head, tail;
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
// 使用伪头部和伪尾部节点
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
head.prev = tail;
tail.prev = head;
tail.next = head;
}
public int getSize() {
return size;
}
public DLinkedNode getHead() {
return head;
}
public DLinkedNode getTail() {
return tail;
}
public Map<K, DLinkedNode> getCache() {
return cache;
}
//必须要加锁,不然多线程链表指针会成环无法结束循环。在这卡一天
public synchronized V get(K key) {
DLinkedNode node = cache.get(key);
if (node == null) {
return null;
}
// 如果 key 存在,先通过哈希表定位,再移到头部
moveToHead(node);
return node.value;
}
public synchronized void remove(DLinkedNode node){
node.prev.next = node.next;
node.next.prev = node.prev;
cache.remove(node.key);
size--;
}
public synchronized void discard(){
// 如果超出容量,删除双向链表的尾部节点
DLinkedNode tail = removeTail();
// 删除哈希表中对应的项
cache.remove(tail.key);
size--;
}
public synchronized void put(K key, V value) {
DLinkedNode node = cache.get(key);
if (node == null) {
// 如果 key 不存在,创建一个新的节点
DLinkedNode newNode = new DLinkedNode(key, value);
// 添加进哈希表
cache.put(key, newNode);
// 添加至双向链表的头部
addToHead(newNode);
++size;
}
else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
node.value = value;
moveToHead(node);
}
}
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
private DLinkedNode removeTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
}
要約する
今日、家庭教師から新年明けて登校させてくださいとの連絡があり、半年遅れていた実験レポートも再開されました. もちろん次のレポートがいつ書けるかは定かではありません. . 次のものを事前に発表して、lab4 と lab3 を書きます. 私はそれをしませんでした. なぜなら, コンテンツのこの部分は解析の最適化を達成するためのものです. 私はこの側面にあまり関与したくありません.この方向性を学んだ上でデータベース開発エンジニアになり、分散ストレージか何かに従事します。