自己動手寫數據庫: 視圖元數據管理- 統計元數據管理

在數據庫中,除了數據表外,還有一個重要對象叫視圖。視圖是由 SQL 語句將不同字段從不同表中抽取或者構造後形成的新表,跟數據庫表不同在於,它不存儲在磁盤上,而是在使用時臨時構建出來。

跟數據庫表一樣,視圖同樣需要進行元數據管理。跟上節相同我們定義一個 ViewManager 來創建視圖,同時創建一個 viewcat 數據庫表來存儲視圖的元數據,這個表有兩個字段分別是 ViewName, 他是字符串類型,還有一個叫 ViewDef,他是一個二進制數據類型,具體細節在後面的實現中會清楚說明。

我們看看代碼實現,首先在 metadata_management 目錄下創建文件 view_manager.go 文件,然後輸入代碼如下:

package metadata_manager

import (
    rm "record_manager"
    "tx"
)

const (
    /*
        視圖元數據對應數據結構大小,通常視圖元數據的定義需要幾千個字節,我們這裏出於學習和實驗目的,只把使用
        100字節來表示
    */
    MAX_VIEWDEF = 100
)

type ViewManager struct {
    tblMgr *TableManager
}

func NewViewManager(isNew bool, tblMgr *TableManager, tx *rm.Transation) *ViewManager {
    viewMgr := &ViewManager{
        tblMgr: tblMgr,
    }

    if isNew {
        //使用表管理器創建元數據表viewcat
        sch := rm.NewSchema()
        sch.AddStringField("viewname", MAX_NAME)
        sch.AddStringField("viewdef", MAX_VIEWDEF)
        tblMgr.CreateTable("viewcat", sch, tx)
    }

    return viewMgr
}

func (v *ViewManager) CreateView(vname string, vdef string, tx *rm.Transation) {
    //每創建一個視圖對象,就在viewcat表中插入一條對該視圖對象元數據的記錄
    layout := v.tblMgr.getLayout("viewcat", tx)
    ts := rm.NewTableScan(tx, "viewcat", layout)
    ts.Insert()
    ts.SetString("viewname", vname)
    ts.SetString("viewdef", vdef)
    ts.Close()
}

func (v *ViewManager) GetViewDef(vname string, tx *tx.Transation) string {
    result := ""
    layout := v.tblMgr.GetLayout("viewcat", tx)
    //獲取視圖的表結構
    ts := rm.NewTableScan(tx, "viewcat", layout)
    for ts.Next() {
        if ts.GetString("viewcat") == vname {
            result = ts.GetString("viewdef")
            break
        }
    }

    ts.Close()
    return result
}

另外還需要考慮的元數據是統計信息。統計信息一般包含當前有多少條記錄,字段在磁盤中的分佈信息等,這些數據在引擎執行查詢時用於估計成本,統計信息處理的好能大大加快查詢速度。如果是商用數據庫,這些信息將會多如牛毛,我們這裏簡單起見就保持三種數據即可,他們分別是每個表使用了多少區塊,每個表包含了多少條記錄,對於某個表中的某個字段,它有多少個不重複的值。

不難看到維護這些統計信息需要付出一定的性能代價,因爲當數據庫表有插入,刪除,更新等操作時,我們都得對統計信息進行更新,爲了處理這個問題我們不再像前面那樣使用元數據表來存放統計數據,而是把統計信息全部保留在內存裏,當數據庫系統啓動時,它掃描一次所有數據庫表,構造出統計信息寄存在內存中。同時每過一段時間系統就掃描數據庫表然後更新統計數據。這種做法的問題在於在某個時刻統計信息跟實際情況有所不符,但問題不大,因爲這些信息主要用來估算查詢成本,它不是很準確問題也不大。我們看看統計元數據的實現,在當前目錄增加一個文件名爲 stat_manager.go,實現代碼如下:

package metadata_manager

import (
    rm "record_manager"
    "sync"
    "tx"
)

const (
    //數據庫表發生變化100次後更新統計數據
    REFRESH_STAT_INFO_COUNT = 100
)

type StatInfo struct {
    numBlocks int //數據庫表的區塊數
    numRecs   int //數據庫表包含的記錄數
}

func newStatInfo(numBlocks int, numRecs int) *StateInfo {
    return &StatInfo{
        numBlocks: numBlocks,
        numRecs:   numRecs,
    }
}

func (s *StatInfo) BlocksAccessed() int {
    return s.numBlocks
}

func (s *StatInfo) RecordsOutput() int {
    return s.numRecs
}

func (s *StatInfo) DistincValues(fldName string) int {
    //字段包含多少不同的值
    return 1 + (s.numRecs / 3) //初步認爲三分之一,後面再修改
}

type StatManager struct {
    tblMgr     *TableManager
    tableStats map[string]*StateInfo
    numCalls   int
    lock       sync.Mutex
}

func NewStatManager(tblMgr *TableManager, tx *tx.Transation) *StatManager {
    statMgr := &StatManager{
        tblMgr:   tblMgr,
        numCalls: 0,
    }
    //更新統計數據
    statMgr.refreshStatistics(tx)
    return statMgr
}

func (s *StatManager) GetStatInfo(tblName string, layout *rm.Layout, tx *tx.Transation) *StatInfo {
    s.lock.Lock()
    defer s.lock.Unlock()
    s.numCalls += 1
    if s.numCalls > REFRESH_STAT_INFO_COUNT {
        s.refreshStatistic(tx)
    }

    si := s.tableStats[talName]
    if si == nil {
        //爲新數據庫表創建統計對象
        si = s.calcTableStats(tblName, layout, tx)
        s.tableStats[tblName] = si
    }

    return si
}

func (s *StatManager) refreshStatistics(tx *tx.Transation) {
    s.tableStats = make(map[string]*StatInfo)
    s.numCalls = 0
    tcatLayout := s.tblMgr.GetLayout("tblcat", tx)
    tcat := rm.NewTableScan(tx, "tblcat", tcatLayout)
    for tcat.Next() {
        tblName := tcat.GetString("tblname")
        layout := s.tblMgr.GetLayout(tblName, tx)
        si := s.calcTableStats(tblName, layout, tx)
        s.tableStats[tblName] = si
    }

    tcat.Close()
}

func (s *StatManager) calcTableStats(tblName string, layout *rm.Layout, tx *tx.Transation) *StatInfo {
    numRecs := 0
    numBlocks := 0
    ts := rm.NewTableScan(tx, tblName, layout)
    for ts.Next() {
        numRecs += 1
        numBlocks = ts.GetRid().BlockNumber() + 1
    }
    ts.Close()
    return newStatInfo(numRecs, numBlocks)
}

在上面代碼中,我們使用對象 StatInfo 來包含表的統計信息,其中包括表的記錄數,區塊數還有給定字段擁有的不同值的數量。StatManager 用於獲取統計元數據,它只在系統啓動時創建,在創建時它調用自己的 refreshStatistics 接口創建統計數據並存儲在內存中,這個接口會繼續調用 calcTableStats 來獲取每個表的相關數據,後者會從通過 TableManage 獲取所有數據庫表,然後獲得每個表的相關數據,然後創建 StatInfo 對象,並把表的統計數據存儲在其中。

最後我們使用一個名爲 MetaDataManager 的對象將前面實現的所有 Manager 統一管理起來,在目錄中創建 meta_manager.go 實現代碼如下:

package metadata_manager

import (
    rm "record_manager"
    "tx"
)

type MetaDataManager struct {
    tblMgr  *TableManager
    viewMgr *ViewManager
    statMgr *StatManager
    //索引管理器以後再處理
    //idxMgr *IndexManager
}

func NewMetaDataManager(isNew bool, tx *tx.Transation) *MetaDataManager {
    metaDataMgr := &MetaDataManager{
        tblMgr: NewTableManager(isNew, tx),
    }

    metaDataMgr.viewMgr = NewViewManager(isNew, metaDataMgr.tblMgr, tx)
    metaDataMgr.statMgr = NewStatManager(metaDataMgr.tblMgr, tx)

    return metaDataMgr
}

func (m *MetaDataManager) CreateTable(tblName string, sch *rm.Schema, tx *tx.Transation) {
    m.tblMgr.CreateTable(tblName, sch, tx)
}

func (m *MetaDataManager) CreateView(viewName string, viewDef string, tx *tx.Transation) {
    m.viewMgr.CreateView(viewName, viewDef, tx)
}

func (m *MetaDataManager) GetLayout(tblName string, tx *tx.Transation) *rm.Layout {
    return m.tblMgr.GetLayout(tblName, tx)
}

func (m *MetaDataManager) GetViewDef(viewName string, tx *tx.Transation) string {
    return m.viewMgr.GetViewDef(viewName, tx)
}

func (m *MetaDataManager) GetStatInfo(tblName string, layout *rm.Layout, tx *tx.Transation) *StatInfo {
    return m.statMgr.GetStatInfo(tblName, layout, tx)
}

最後我們在 main 函數中調用 MetaDataManager 看看效果:

package main

import (
    bmg "buffer_manager"
    fm "file_manager"
    "fmt"
    lm "log_manager"
    "math/rand"
    mm "metadata_management"
    record_mgr "record_manager"
    "tx"
)

func main() {
    file_manager, _ := fm.NewFileManager("recordtest", 400)
    log_manager, _ := lm.NewLogManager(file_manager, "logfile.log")
    buffer_manager := bmg.NewBufferManager(file_manager, log_manager, 3)

    tx := tx.NewTransation(file_manager, log_manager, buffer_manager)
    sch := record_mgr.NewSchema()
    sch.AddIntField("A")
    sch.AddStringField("B", 9)

    mdm := mm.NewMetaDataManager(true, tx)
    mdm.CreateTable("MyTable", sch, tx)
    layout := mdm.GetLayout("MyTable", tx)
    size := layout.SlotSize()
    fmt.Printf("MyTable has slot size: %d\n", size)
    sch2 := layout.Schema()
    fmt.Println("Its fields are: ")
    for _, fldName := range sch2.Fields() {
        fldType := ""
        if sch2.Type(fldName) == record_mgr.INTEGER {
            fldType = "int"
        } else {
            strlen := sch2.Length(fldName)
            fldType = fmt.Sprintf("varchar ( %d )", strlen)
        }

        fmt.Printf("%s :  %s\n", fldName, fldType)
    }

    ts := record_mgr.NewTableScan(tx, "MyTable", layout)
    //測試統計元數據
    for i := 0; i < 50; i++ {
        ts.Insert()
        n := rand.Intn(50)
        ts.SetInt("A", n)
        strField := fmt.Sprintf("rec%d", n)
        ts.SetString("B", strField)
    }
    si := mdm.GetStatInfo("MyTable", layout, tx)
    fmt.Printf("blocks for MyTable is %d\n", si.BlocksAccessed())
    fmt.Printf("records for MyTable is :%d\n", si.RecordsOutput())
    fmt.Printf("Distinc values for field A is %d\n", si.DistinctValues("A"))
    fmt.Printf("Distinc values for field B is %d\n", si.DistinctValues("B"))

    //統計視圖信息
    viewDef := "select B from MyTable where A = 1"
    mdm.CreateView("viewA", viewDef, tx)
    v := mdm.GetViewDef("viewA", tx)
    fmt.Printf("View def = %s\n", v)
    tx.Commit()
}

上面代碼運行後輸出結果如下:

MyTable has slot size: 33
Its fields are: 
A :  int
B :  varchar ( 9 )
blocks for MyTable is 5
records for MyTable is :50
Distinc values for field A is 17
Distinc values for field B is 17
View def = select B from MyTable where A = 1
transation 1  committed

具體的邏輯請在 B 站搜索 Coding 迪斯尼查看調試演示和邏輯講解。代碼下載路徑:https://github.com/wycl16514/database_system_meta_data_manager

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/B5RzugfLr738iaNBkntriA