- DBのファイルを管理する - 読み取り・書き込みを独自にスケジューリングする(OSのファイルシステムに任せない) - ページを追跡する ## Page - ページは固定長 - 1ファイルの中に複数のページがあることがあることが一般的 - ページIDという、unique identifierが割り当てられている - 3つの「ページ」という言葉 - Hardware Page (4KB): ストレージのページ - Atomicな書き込みが保証される - OS Page (4KB, [[Huge page]] 2MB, 1GB) - Database Page (512B ~ 32KB) - 製品によって色々なサイズ - 4KB: SQLite, Oracle, DB2など - 8KB: Microsoft SQLServer, PostgreSQL, Ingresなど - 16KB: MySQL - どういうサイズが最適なのか? - Read heavy workloads: ページサイズを大きくする(読み取りスループット向上) - Write heavy workloads: ページサイズを小さくする(ページサイズ大きすぎると、小さなデータ変更でも大量の書き込みが必要になる) ### Page Header - 各ページの先頭には、ページの中身に関するメタデータを持つHeaderが付く - Page Size - Checksum - DBMS Version - Transaction Visibility情報([[Concurrency Control]]用) - Compression / Encoding Meta-data - Schema Information - Data Summary / Sketches - 一部のシステム(Oracleなど)は、ページ単体で自己完結(self-contained)していることを要求する ## Pageのアーキテクチャ - **Heap File Organization**: ファイルを、ページのUnordered Collectionとして管理する. これが一般的。 - インデックスとストレージが分離している。インデックスのリーフに、ページ番号とスロット番号が入っている。 - **Tree File Organization**: ページ群を[[B+ Tree]]などの木構造で組織化し、ノードをたどりながら必要なページにアクセス - つまりインデックスとストレージが一体化している?B Treeのリーフに、生のタプルが入っている状態。MySQL [[InnoDB]]はこれらしい - **Sequential / Sorted File Organization** ([[ISAM]]): キーの順序に沿ってページやレコードを並べて配置する - **Hashing File Organization**: レコードのハッシュ値で、どこのページに配置すべきか決定する - Page Directory: テーブルごと、該当するページがどのファイルに含まれているか記録しているグローバルな辞書(DB object 1つにつき1エントリ) - ページ$p$にアクセスする場合、まずそのページが含まれているファイルをPage Directoryから特定し、$p * \text{Page size}$でオフセットを計算してアクセスする - Page Directory自体もページとしてディスク上に保持され、データページと同期が取れている必要がある - DBMSはページの中身についてのメタデータも保持する - ページごとの空き容量 - 空き/未使用ページのリスト - ページの種類(データ or メタデータ) ## Page Layout Page Storage Architecture(Heap/Tree/Sequential/Hashing File Organization)が決まったら、次にページ**内部**でデータをどう並べるかを決める必要がある。以下3つのアプローチがある(今回はApproach 1のみ扱う)。 - Approach #1: Tuple-oriented Storage - Approach #2: Log-structured Storage - Approach #3: Index-organized Storage ### Tuple-oriented Storage: Slotted Pages - 素朴な案(Strawman): ページ内のタプル数を記録しておき、新規タプルは末尾に追記する - 問題点1: タプルを削除した場合、単純に詰めるとタプルの物理的な位置がズレて、既存のRecord IDが無効になってしまう - 問題点2: 可変長属性を持つ場合、タプルサイズが固定にならず扱いづらい - **Slotted Pages**: 上記の問題を解決する、最も一般的なページレイアウト手法 - ページの先頭からHeader・Slot Arrayを書いていき、末尾からタプル本体(固定長・可変長どちらも)を書いていく - Slot Arrayは、各スロット番号をタプルの開始位置オフセットにマッピングする - Headerは以下を追跡する - 使用中のスロット数 - 最後に使用したスロットの開始位置オフセット - タプルを削除する場合、タプル本体を消してSlot Arrayのエントリだけ無効化すればよく、他のタプルのRecord IDには影響しない - 空間の無駄が少なくなる - レコードIDは、ページIDとスロット番号で表現される - DBMSによってサイズが異なる(例: Postgres CTID 6-byte, SQLite ROWID 8-byte, Oracle ROWID 10-byte) - アプリケーション側は、Record IDの値そのものに意味があると期待してはいけない ## タプルのアーキテクチャ - ページ内に書くタプルは、`|ヘッダ|データ1|データ2|...|`と表現できる - タプルのバイト列をどう属性の型・値に解釈するかはDBMS次第で、DBMSのカタログにあるスキーマ情報を使って解釈する ### Tuple Header - 各タプルにもHeaderが付き、以下のようなメタデータを持つ(テーブルのスキーマ情報自体は持たない) - Visibility情報([[Concurrency Control]]用) - NULL値を表すBit Map ### Tuple Data - 属性は基本的にテーブル作成時に指定した順番で格納される(ソフトウェア工学的な単純さのため) - ただし、後述のword alignmentの都合でより効率的な順序に並べ替えることもある - 各データがワード幅(64bitなど)に収まらない場合、ワード境界をまたがるデータが出ると困る(CPUが余計な処理をせずアクセスできるようにするため)ので、以下のどれかをする - パディングをする(属性の後ろに空きビットを追加し、次のワードサイズまで切り上げる) - いい感じにリオーダーする(属性の物理的な並び順を変えてアラインさせる。それでも余った分はパディングが必要になることがある) ### データ型の表現 - `INTEGER` / `BIGINT` / `SMALLINT` / `TINYINT`: C/C++と同様の表現 - `FLOAT` / `REAL`(可変精度) vs `NUMERIC` / `DECIMAL`(固定精度) - Variable Precision Numbers: [[IEEE 754]]に従ったネイティブ表現。CPU側に専用命令・レジスタがあるため高速だが、正確な値を保証しない(例: `0.1 + 0.2 != 0.3`になりうる) - Fixed Precision Numbers: 丸め誤差が許容できない場合に使う、任意精度・スケールの数値型。可変長のバイナリ表現+メタデータとして実装されることが多い(例: PostgresのNUMERIC型は`ndigits`, `weight`, `scale`, `sign`, `digits`を持つ構造体) - `VARCHAR` / `VARBINARY` / `TEXT` / `BLOB`: 長さ情報を持つHeader + データバイト列、またはデータが格納された別ページ/オフセットへのポインタ - 照合順序(collation)・ソート順にも注意が必要 - `TIME` / `DATE` / `TIMESTAMP` / `INTERVAL`: Unixエポック(1970年1月1日)からの(マイクロ/ミリ)秒を表す32/64bit整数 ### NULL値の扱い - Choice #1: Null Column Bitmap Header(Tuple Headerに、どの属性がNULLかを示すビットマップを持つ): Row-storeで最も一般的 - Choice #2: Special Values(例: `INT32_MIN`のような、その型でNULLを表す特別な値を決め打ちする): Column-storeでよく使われる - Choice #3: Per Attribute Null Flag(属性ごとにNULLフラグを持つ): 1bitだけだとword alignmentが崩れるため単純な1bitでは済まず、非推奨(Don't Do This) ### 大きな値の扱い - 多くのDBMSでは、タプルが1ページのサイズを超えることを許可しない - ページサイズを超える値は、別途Overflowページに格納する - Postgres: TOAST(2KB超) - MySQL: Overflow(ページサイズの半分超) - SQL Server: Overflow(ページサイズ超) - 元のタプルの属性値部分には、サイズとOverflowページへのlocation(ポインタ)を格納する - 圧縮などの最適化の余地もある(例: German Strings) - External Value Storage: DB外部のファイルに大きな値を保存し、BLOB型として扱う方法もある(Oracle `BFILE`, Microsoft `FILESTREAM`など) - DBMSは外部ファイルの中身を直接操作できない - Durability保証もTransaction保証も効かない