├── chapter1 ├── 1-1.jpg ├── 1-2.jpg ├── 1-3.jpg └── README.md ├── chapter2 ├── 2-1.jpg └── README.md ├── chapter4 ├── 4_1.jpg ├── 4_2.jpg ├── 4_3.jpg ├── 4_4.jpg ├── 4_5.jpg ├── 4_6.jpg ├── 4_7.jpg ├── 4_8.jpg ├── 4_9.jpg └── README.md ├── chapter5 ├── 5_1.jpg └── README.md ├── chapter6 ├── 6_1.jpg ├── 6_2.png ├── 6_3.jpg └── README.md ├── chapter7 ├── 7_1.jpg ├── 7_2.jpg └── README.md ├── chapter8 ├── 8_1.jpg ├── 8_2.jpg ├── 8_3.jpg ├── 8_4.jpg ├── 8_5.jpg └── README.md ├── layouts └── footer.html ├── book.json ├── refs └── README.md ├── README.md ├── chapter9 └── README.md ├── SUMMARY.md └── chapter3 └── README.md /chapter1/1-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter1/1-1.jpg -------------------------------------------------------------------------------- /chapter1/1-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter1/1-2.jpg -------------------------------------------------------------------------------- /chapter1/1-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter1/1-3.jpg -------------------------------------------------------------------------------- /chapter2/2-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter2/2-1.jpg -------------------------------------------------------------------------------- /chapter4/4_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter4/4_1.jpg -------------------------------------------------------------------------------- /chapter4/4_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter4/4_2.jpg -------------------------------------------------------------------------------- /chapter4/4_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter4/4_3.jpg -------------------------------------------------------------------------------- /chapter4/4_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter4/4_4.jpg -------------------------------------------------------------------------------- /chapter4/4_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter4/4_5.jpg -------------------------------------------------------------------------------- /chapter4/4_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter4/4_6.jpg -------------------------------------------------------------------------------- /chapter4/4_7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter4/4_7.jpg -------------------------------------------------------------------------------- /chapter4/4_8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter4/4_8.jpg -------------------------------------------------------------------------------- /chapter4/4_9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter4/4_9.jpg -------------------------------------------------------------------------------- /chapter5/5_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter5/5_1.jpg -------------------------------------------------------------------------------- /chapter6/6_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter6/6_1.jpg -------------------------------------------------------------------------------- /chapter6/6_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter6/6_2.png -------------------------------------------------------------------------------- /chapter6/6_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter6/6_3.jpg -------------------------------------------------------------------------------- /chapter7/7_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter7/7_1.jpg -------------------------------------------------------------------------------- /chapter7/7_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter7/7_2.jpg -------------------------------------------------------------------------------- /chapter8/8_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter8/8_1.jpg -------------------------------------------------------------------------------- /chapter8/8_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter8/8_2.jpg -------------------------------------------------------------------------------- /chapter8/8_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter8/8_3.jpg -------------------------------------------------------------------------------- /chapter8/8_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter8/8_4.jpg -------------------------------------------------------------------------------- /chapter8/8_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snaga/postgresqlinternals/HEAD/chapter8/8_5.jpg -------------------------------------------------------------------------------- /layouts/footer.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 5 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["footer"], 3 | "pluginsConfig": { 4 | "layout": { 5 | "footerPath" : "layouts/footer.html" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /refs/README.md: -------------------------------------------------------------------------------- 1 | # 参考文献 2 | 3 | * トランザクション処理 概念と技法(上・下)、ジム・グレイ、アンドレアス・ロイター、日経BP社 4 | * リレーショナル・データベースシステム RDBMS技術解説、滝沢誠、ソフトウェア・リサーチ・センター 5 | * PostgreSQL全機能バイブル、鈴木啓修、技術評論社 6 | * Database System Implementation, Hector Garcia-Molina, Jeffrey D. Ullman, Jennifer Widom, Prentice Hall 7 | * 問合せ最適化インサイド、板垣貴裕、「とことん分かるPostgreSQLインサイド」講演資料 8 | * Explaining EXPLAIN 第2回、中西剛紀、第20回しくみ+アプリケーション勉強会 講演資料 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # はじめに 2 | 3 | ## 本コンテンツについて 4 | 5 | 本コンテンツは、2014年1月30~31日に筑波大学で開講された「情報システム特別講義D」における講義「Inside PostgreSQL Kernel」の内容を再構成、加筆・修正したものです。 6 | 7 | 作成者:永安悟史 [@snaga](https://twitter.com/snaga/) 8 | 9 | ## 本コンテンツへのフィードバックについて 10 | 11 | 本コンテンツへのフィードバックはGithubで受け付けています。 12 | 13 | [https://github.com/snaga/postgresqlinternals](https://github.com/snaga/postgresqlinternals) のIssuesをご利用ください。 14 | 15 | -------------------------------------------------------------------------------- /chapter1/README.md: -------------------------------------------------------------------------------- 1 | # アーキテクチャ概要 2 | 3 | ## PostgreSQLの構成要素 4 | 5 | ![PostgreSQLの構成要素](1-1.jpg "PostgreSQLの構成要素") 6 | 7 | PostgreSQLの構成要素としては、大きくプロセスとメモリとファイルがあります。上記の図のように複数のプロセスがさまざまなメモリの領域を使って、ファイルのデータにアクセスするという構造になっています。 8 | 9 | プロセスには、クライアントからの接続を受け付けるリスナプロセス、SQLの処理を実際に行うバックグラウンドプロセス、それ以外にもバックグラウンドライタやログを出力するロガープロセス、あとWALライタなどのプロセスがあります。 10 | 11 | メモリには、共有バッファやWALバッファなどを始めとした、さまざまなメモリの領域があります。 12 | 13 | ファイルは、実際のユーザデータを保持しているテーブルファイルやインデックスファイルがあります。また、トランザクションログファイル、アーカイブログファイルなどもあります。 14 | 15 | PostgreSQLの内部では、これらが関連して動作しています。 16 | 17 | ## PostgreSQLの基本的なアーキテクチャ 18 | 19 | ![PostgreSQLの基本的なアーキテクチャ](1-2.jpg "PostgreSQLの基本的なアーキテクチャ") 20 | 21 | 図の一番右側に共有バッファと呼ばれるメモリの空間がありますが、PostgreSQLの基本的なアーキテクチャというのは、共有バッファを中心として複数のプロセッサに連携しながら処理を行うというものです。 22 | 23 | 図の右下にあるテーブルファイルやインデックスファイルから実際のデータを読み込んで、メモリ空間である共有バッファにデータを貯めておき、そのメモリ上のデータをサーバプロセスと呼ばれる図の中央にあるいくつか複数のプロセスが共有しながら読み書きするという形になってます。 24 | 25 | 全体的なデータの流れですが、クライアントから送られたデータは、サーバプロセスによって一旦共有バッファに書き出されます。共有バッファに書かれたデータは、バックグラウンドライタを通してテーブルファイルやインデックスファイルに書き戻されることになります。ユーザによってデータベースに送信されたデータは、こういったサイクルで処理されています。 26 | 27 | 別のデータの書き出しの流れとして、WALライタがあります。WALライタは、共有バッファ中のデータに対してどのような更新をしたかという情報を「トランザクションログ」と呼ばれるログファイルに記録しておき、クラッシュリカバリの時に使用します。 28 | 29 | 以上がPostgreSQLの基本的なアーキテクチャになります。 30 | 31 | ## SQL文の処理される流れ 32 | 33 | ![SQL文の処理される流れ](1-3.jpg "SQL文の処理される流れ") 34 | 35 | SQL文がPostgreSQL内部で処理される流れは上図の通りです。 36 | 37 | ユーザが送信したクエリは、サーバプロセスによって受信された後、まずは構文解析されます。 38 | 39 | 次に書き換えが行われます。VIEWやRULEにもとづいて定型的な書き換えを実行します。 40 | 41 | そして、実行計画(実行プラン、クエリプランとも呼ばれる)を作成し、最適化を行います。 42 | 43 | 最後にエグゼキュータで実行して結果をクライアントに返す、という流れで処理を行っています。 44 | -------------------------------------------------------------------------------- /chapter9/README.md: -------------------------------------------------------------------------------- 1 | # PostgreSQLの拡張 2 | 3 | ## PostgreSQLを拡張する方法 4 | 5 | PostgreSQLの特徴として拡張のしやすさが挙げられます。 6 | 7 | * UDF (ユーザ定義関数) 8 | * インデックス拡張(AND GiST) 9 | * Hook 10 | * カスタムバックグラウンドワーカー 11 | * EXTENSION 12 | 13 | PostgreSQLの拡張の基本は、UDFと呼ばれるユーザ定義関数を定義する方法です。これはCでも書けますしそれ以外のLL系の言語でも書くことができます。 14 | 15 | また、インデックスを拡張するというのも、PostgreSQLでは比較的多く見られる拡張です。 16 | 17 | 最近、PostgreSQLを拡張する方法として「Hook」呼ばれる機能が入りました。これは、PostgreSQLの内部の機能に対してユーザが作った関数を割り当てることで、内部のオプティマイザやエグゼキュータの処理を一部ユーザ側で乗っ取ることができるような仕組みです。これを使って内部の動きを変えるというのも1つの方法としてあります。 18 | 19 | それ以外に、カスタムバックグラウンドライトという拡張も可能です。バックグラウンドワーカーというのは、PostgreSQLの裏側に常駐してバックグラウンドで動くプロセスのことで、カスタムバックグラウンドワーカーはこれをユーザ自身が作成できる機能です。 20 | 21 | EXTENSIONというのは、こういったユーザが作成した関数などをモジュール化する機能です。複数の関数を一括してインストールしたりアンインストールしたりする機能を提供しています。 22 | 23 | 24 | ## Hookによる拡張 25 | 26 | * PostgreSQLは動的にモジュール(共有ライブラリ)をロード可能 27 | * shared_preload_libraries 28 | * local_preload_libraries, LOADコマンド 29 | * 内部には、関数ポインタを用いた Hook が多数存在する 30 | * ParserSetupHook 31 | * ExecutorStart_hook 32 | * ProcessUtility_hook 33 | * などなど 34 | * この Hook に独自の関数を設定することで、内部のデータにアクセスしつつ、処理を拡張することができる 35 | 36 | Hookによる拡張は、Sharedオブジェクトを作成して、それをpreloadするという形で実装します。 37 | 38 | 使用できるHookとしては、ParserSetupHook、ExecutorStart_hook、ProcessUtility_hookなどがあり、これらにユーザの定義した関数を設定することで、こういったコマンドなどの内部処理を則ることができます。 39 | 40 | ## GiSTによるインデックスの拡張 41 | 42 | * 汎用検索ツリー(GiST:Generalized Search Tree) 43 | * 任意のインデックスを実装する枠組み(フレームワーク)を提供 44 | * 7つのメソッドを実装することによって新しいインデックスを実装できる 45 | * same, consistent, union 46 | * penalty, picksplit 47 | * compress, decompress 48 | 49 | ## 参考文献・参考資料 50 | 51 | * C言語によるユーザ定義関数の作り方 http://www.slideshare.net/snaga/how-towritepgfunc 52 | * 関数の変動性分類についておさらいしてみる http://www.slideshare.net/toshiharada/lt-41040251 53 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # PostgreSQL Internals 2 | 3 | 本コンテンツは、2014年1月30~31日に筑波大学で開講された「情報システム特別講義D」における講義「Inside PostgreSQL Kernel」の内容を再構成、加筆・修正したものです。 4 | 5 | * [はじめに](README.md) 6 | * 本コンテンツについて 7 | * 本コンテンツへのフィードバックについて 8 | * [1. アーキテクチャ概要](chapter1/README.md) 9 | * PostgreSQLの構成要素 10 | * PostgreSQLの基本的なアーキテクチャ 11 | * SQL文の処理される流れ 12 | * [2. トランザクション管理](chapter2/README.md) 13 | * トランザクション処理におけるACID特性 14 | * 各レコードの可視性の管理 15 | * Atomicity(原子性)の実装 16 | * Consistency(一貫性)の実装 17 | * Isolation(分離性)の実装 18 | * トランザクション分離レベルの定義 19 | * Durability(永続性)の実装 20 | * チェックポイント 21 | * [3. メタデータ管理](chapter3/README.md) 22 | * pg_controlファイル 23 | * OID/XID/TID 24 | * システムカタログ 25 | * [4. MVCCとストレージ構造](chapter4/README.md) 26 | * テーブルファイル 27 | * テーブルのページレイアウト 28 | * データ型とデータサイズ 29 | * インデックス(B-Tree)ファイル 30 | * B-Tree(リーフ)のページレイアウト 31 | * VACUUM処理 32 | * インデックスとタプルの可視性 33 | * TOASTテーブル 34 | * FreeSpace Map (FSM) 35 | * FSMの物理レイアウト 36 | * Visibility Map (VM) 37 | * Visibility Mapの構造 38 | * [5. メモリ管理](chapter5/README.md) 39 | * 共有メモリとローカルヒープ 40 | * Memory Context 41 | * 共有バッファと管理アルゴリズム 42 | * [6. ロック制御](chapter6/README.md) 43 | * ロック 44 | * 2-Phase Locking (2PL) 45 | * デッドロックの検出と解消 46 | * SpinLockとLWL 47 | * [7. パース、リライト、オプティマイズ](chapter7/README.md) 48 | * パース、リライト 49 | * オプティマイザ統計情報 50 | * よく利用されるオプティマイザ統計情報 51 | * 実行コストの計算 52 | * Seq scan/Index scan実行コスト例 53 | * GEQO(遺伝的問い合わせ最適化) 54 | * 参考資料 55 | * [8. エグゼキュータ](chapter8/README.md) 56 | * エグゼキュータの役割 57 | * 結合処理 58 | * Nested Loop Join 59 | * Merge Join 60 | * Hash Join 61 | * インデックスの種類 62 | * インデックスのアクセスメソッド 63 | * B-Treeインデックスのタプル取得呼び出し 64 | * B-Treeインデックスのアクセスメソッド 65 | * B-Treeインデックスの構造 66 | * B-Treeデモ 67 | * GINインデックス 68 | * [9. PostgreSQLの拡張](chapter9/README.md) 69 | * PostgreSQLを拡張する方法 70 | * Hookによる拡張 71 | * GiSTによるインデックスの拡張 72 | * 参考文献・参考資料 73 | * [参考文献](refs/README.md) 74 | -------------------------------------------------------------------------------- /chapter6/README.md: -------------------------------------------------------------------------------- 1 | # ロック制御 2 | 3 | ## ロック 4 | 5 | * 並行して処理を行っているRDBMSでは、データの整合性のためにリソースに対する排他制御が必須 6 | * データの読み取り、書き込み、その他 7 | * ロックには、ロックレベルと粒度の概念がある 8 | * ロックのレベル(モード)はPostgreSQLでは8種類 9 | * 粒度は、テーブル、レコード、トランザクションなど 10 | 11 | !["ロック"](6_1.jpg "ロック") 12 | 13 | ロックとのいうは、当然データを操作する上で非常に重要にな要素です。特に並行して処理を行っているデータベースではデータの整合性を担保するためにリソースに対する排他制御は必須の機能になります。ロックすべき処理には、データの読み取りや書き込み、その他にもいろいろあります。 14 | 15 | ロックには「レベル」と「粒度」という概念があります。 16 | 17 | 例えば、ロックのレベル、PostgreSQLではモードとも呼ばれますが、この種類は8種類くらいあります。 18 | 19 | ACCESS SHAREという共有ロックから始まって、行の排他ロックや、その他にも、いろんなロックのモードがあります。 20 | 21 | 粒度というのは、レコード単位のロックなのか、テーブル単位のロックなのか、など、いろんな粒度のリソースを対象にロックすることができますので、この辺りの条件を組み合わせて、何に対してどのようなロックをするかということを考えていくのがPostgreSQLのロックの仕組みになります。 22 | 23 | ## 2-Phase Locking (2PL) 24 | 25 | * ロックの獲得期、解放期の2フェーズに分類して管理 26 | * ロックの獲得・解放を繰り返すと、デッドロックを起こしやすくなるため 27 | 28 | !["2-Phase Locking (2PL)"](6_2.png "2-Phase Locking (2PL)") 29 | 30 | 出典: http://www.cubrid.org/files/attach/images/220547/566/507/two-phase-lock.png 31 | 32 | 33 | * PostgreSQLでは、トランザクションの終了までロックを保持 34 | * COMMIT/ROLLBACK時に一括解放 35 | * CommitTransaction(), AbortTransaction() @ xact.c 36 | * ResourceOwnerRelease() @ resowner.c 37 | 38 | PostgreSQLでは、Two-phase lockingという仕組みを実装していて、ロックの獲得期と解放期の2フェーズに分類してロックを管理します。 39 | 40 | なぜかというと、ロックの獲得と解放を細かく繰り返すとデットロックを起こしやすくなるためです。これは、理論的にそうなることが明らかなので、PostgreSQLではこのような形でシンプルに管理する形になっています。 41 | 42 | PostgreSQLでは、トランザクション終了までロックを保持しておいて、最後にトランザクションをCOMMITまたはROLLBACKするときに一括してロックを解放します。よって、例えばテーブルロックを取るための「LOCK」というコマンドはありますが、逆の「UNLOCK」というコマンドはありません。一旦獲得したロックを解放するためには、トランザクションをコミットするかロールバックするかしなければなりません。 43 | 44 | 45 | ## デッドロックの検出と解消 46 | 47 | * デッドロック 48 | * 複数のリソースのロックを獲得しようとする複数のセッション 49 | * それ以上継続することは(論理的に)できない 50 | 51 | !["デッドロックの検出と解消"](6_3.jpg "デッドロックの検出と解消") 52 | 53 | * ロック待機が1秒(デフォルト)を越えるとデッドロック検出ロジックが動作 54 | * デッドロックが見つかったら、どれかのセッションをABORTさせる 55 | * 残ったセッションは継続できる 56 | 57 | デットロックとはどういうものかというと、トランザクション1とトランザクション2がある時、それぞれ違うリソースに対するロックを確保した後、相手がロックを確保しているリソースのロックを取ろうとすると、それぞれがお互いのロック解放を待ちあうデットロックとなってしまい、いつまでたっても論理的に先に進めない、という状態になります。このように、複数のセッションが複数のリソースに対するロックを獲得しようとする時に発生するものです。 58 | 59 | PostgreSQLの場合は、ロック待機が1秒を超えるとデットロックが起こってるんじゃないか考えて、デッドロックの有無を確認するチェックが実行されます。このチェックのロジックが走った結果、デットロックが見つかると、どれかのセッションがABORTされ、残ったセッションは継続できる、ということになります。つまり、このデットロックの輪の確認が一秒に1回チェックが走り、問題があればデッドロックを破る、という処理が行われています。 60 | 61 | そのため、ロック待機やデットロックが多く起こるような状態を引き起こすアプリケーションやトランザクションを走らせると、このデットロックのチェックのロジックのコストが高くなる、という現実があり、パフォーマンスチューニングが必要になる場合があります。 62 | 63 | 64 | ## SpinLockとLWLock 65 | 66 | * Heavyweight Lock 67 | * ユーザー(DBA)から見えるロック 68 | * テーブルロックなどに使用。pg_locksビューから確認可能 69 | * Lightweight Lock 70 | * ユーザー(DBA)から見えないロック 71 | * バッファのロックなど、内部的なリソースのロックに使用 72 | * Spinlock 73 | * ユーザー(DBA)から見えないロック 74 | * CPUやメモリなどの超短時間だけ使用するロック 75 | * CPUやメモリ操作のAtomicityやConsistencyを担保するロック 76 | 77 | PostgreSQLには、Heavyweight Lock、Lightweight Lock、Spinlockというように、いろんなロックのレベルがあります。 78 | 79 | Heavyweight Lockは、ユーザやデータベースのエンジニアが明示的にとるロックで、LOCK TABLEやSELECT FOR UPDATEなどのロックがこのHeavyweight Lockに該当します。 80 | 81 | その次がLightweight Lockで、ユーザやDBAからは見えないロックですが、共有バッファのロックなど内部的なリソースのロックに使用されるものです。 82 | 83 | 一番下のSpinlockというのは、ユーザから見えないロックですが、CPUやメモリなどを占有する際に超短時間だけ使用するロックです。CPUやメモリ操作のAtomicity(原子性)やConsistency(一貫性)を担保するための一番低いレベルのロックになります。 84 | -------------------------------------------------------------------------------- /chapter3/README.md: -------------------------------------------------------------------------------- 1 | # メタデータ管理 2 | 3 | ## pg_controlファイル 4 | 5 | * PostgreSQLが起動した時に最初に読み込まれるファイル 6 | * PostgreSQLインスタンスの状態は各種IDの値などが保存される(変更があると更新される) 7 | * 前回は綺麗にシャットダウンされたのか? 8 | * クラッシュしていたらリカバリ(REDO)を実行する 9 | * 次に使うべき、XIDやOID(後述)は何か? 10 | * 前回のチェックポイントはいつだったか? 11 | * など。。。 12 | 13 | pg_controlファイルは、PostgreSQLを起動した時に一番最初に読み込まれるファイルで、PostgreSQLのインスタンスの状態や、前回シャットダウンしたときの各種IDの値、といった内部の情報が保存されているファイルになります。 14 | 15 | つまり、前回は正しくシャットダウンされたかどうか、もしクラッシュしてたらリカバリしないといけないので、どこからリカバリを行うか、前回のチェックポイントはいつだったか、次に使うべき内部のID(XIDやOID)は何か、といった情報が保存されているファイルになります。 16 | 17 | 以下はpg_controlファイルのサンプルです。 18 | 19 | ``` 20 | $ pg_controldata /var/lib/pgsql/9.3/data 21 | pg_control version number: 937 22 | Catalog version number: 201306121 23 | Database system identifier: 5922567728546267133 24 | Database cluster state: in production 25 | pg_control last modified: Sat 21 Dec 2013 04:20:24 PM JST 26 | Latest checkpoint location: 185/944492D8 27 | Prior checkpoint location: 185/94447E70 28 | Latest checkpoint's REDO location: 185/944492D8 29 | Latest checkpoint's REDO WAL file: 000000010000018500000094 30 | Latest checkpoint's TimeLineID: 1 31 | Latest checkpoint's PrevTimeLineID: 1 32 | Latest checkpoint's full_page_writes: on 33 | Latest checkpoint's NextXID: 0/76141 34 | Latest checkpoint's NextOID: 90863 35 | Latest checkpoint's NextMultiXactId: 1 36 | Latest checkpoint's NextMultiOffset: 0 37 | Latest checkpoint's oldestXID: 1799 38 | Latest checkpoint's oldestXID's DB: 1 39 | Latest checkpoint's oldestActiveXID: 0 40 | Latest checkpoint's oldestMultiXid: 1 41 | Latest checkpoint's oldestMulti's DB: 1 42 | Time of latest checkpoint: Sat 21 Dec 2013 04:20:23 PM JST 43 | Fake LSN counter for unlogged rels: 0/1 44 | Minimum recovery ending location: 0/0 45 | Min recovery ending loc's timeline: 0 46 | Backup start location: 0/0 47 | Backup end location: 0/0 48 | End-of-backup record required: no 49 | (略) 50 | ``` 51 | 52 | 上記のサンプルを見ると「Latestなんとか」という情報がいくつもあるのが分かります。つまり、前回シャットダウンしたときの最後の状態をここに保存しておいて、次に起動するときに、またこの情報を使って立ち上げるというしくみになっています。 53 | 54 | 例えば、「Latest checkpoint's REDO location」とか「Latest checkpoint's REDO WAL file」という項目がありますが、これは、前回チェックポイント時のトランザクションログの位置、あるいはその時のWALファイルのファイル名、といった情報を示しています。 55 | 56 | > pg_controlファイル補足。 57 | > 58 | > pg_controlファイルはチェックポイントの際に書き出されています。 59 | > 60 | > ファイルサイズは512バイト程度で一回のディスクオペレーションで書き出せるサイズになっています。(部分破損がないように)。また、データそのものはテキスト形式ではなくバイナリ形式となっています。上記のサンプルはpg_controldataコマンドを使って人間が読める形で出力した情報になります。 61 | 62 | 63 | ## OID/XID/TID 64 | 65 | データベースのオブジェクトやとトランザクションなどを管理するために、PostgreSQLの内部にはOID、XID、TIDといったIDが何種類か存在しています。 66 | 67 | * OID – オブジェクトID 68 | * データベース内部で作成されるオブジェクト(テーブル、インデックス、関数、データ型など)に付与される一意な値 69 | * 昔は、レコード単位で付与されていた(今は違う) 70 | * unsigned int(32bit)、単調増加 71 | * XID – トランザクションID 72 | * 参照トランザクションも含め、トランザクションごとに付与される一意な値 73 | * この値を見ることによって、レコードの新旧を判断する(前述) 74 | * unsigned int(32bit)、単調増加(3~) 75 | * 周回を防止するためのVACUUMとFrozenTransactionId(2) 76 | * TID – タプルID 77 | * ブロックIDと(ブロック内)オフセット番号 78 | * レコードの物理的な位置を表す(インデックスなどで使用) 79 | * unsigned short(16bit) + unsigned short(16bit) + unsigned short(16bit) 80 | 81 | OIDというのは「オブジェクトID」とも呼ばれていて、データベース内部で作成されるオブジェクト、テーブルやインデックスや関数などに一意に付与される値になります。昔はレコード単位で付与されていたんですが、今は変わってきています。unsigned intで、単調増加する値です。 82 | 83 | 2つ目はXID、「トランザクションID」とも呼ばれるものです。「XID」というキーワードはPostgreSQLでよく出てくるのですが、参照トランザクションも含めて、トランザクションを始めるごとに1個ずつ付与されていくという数字になります。beginを実行すると、そのタイミングでXIDを1つ払いだして、もう1回beginってやると、また次のXIDが払い出される、という仕組みになります。 84 | 85 | PostgreSQLの内部では、このXIDの値を見ることによって、トランザクションやレコードの新旧、つまりどちらのトランザクションが先に始まったのか、どちらのトランザクションの方が遅いものか、といった前後関係を判断しています。 86 | 87 | PostgreSQLでは、タプルのヘッダにINSERTしたトランザクションIDとか、削除したトランザクションIDとかを保持していることを2章で解説しましたが、つまり、各レコードのヘッダにこのXIDを保持しているということになります。 88 | 89 | ただし、トランザクションIDは32bitのunsigned intなので、これをずっと使い続けると、どこかでwrap around(周回)します。そのため、PostgreSQLの内部ではXIDの周回をうまくハンドリングするような仕組みが実装されています。 90 | 91 | 最後のTIDは、「タプルID」と呼ばれるIDで、特定のタプル(行)がどこのブロックにあって、そのブロックの中の何番目のレコードとして存在しているのか、という情報を保持しています。内部的にはブロックIDとブロック内のオフセット番号で表現されるデータです。 92 | 93 | また、TIDはインデックスアクセスを理解する時に非常に重要な値です。 94 | 95 | 96 | ## システムカタログ 97 | 98 | PostgreSQLには、システムカタログとよばれる特殊なテーブルやビューがあります。 99 | 100 | * データベースオブジェクト(テーブル、インデックス、関数等)を管理するためのシステムテーブル(&ビュー) 101 | * 97のシステムテーブル&ビュー (PostgreSQL 9.3) 102 | * initdb実行時にpg_catalogスキーマに作成される 103 | * これが壊れると、データベースオブジェクトを正しく見つけられなくなる 104 | 105 | これはデータベース内のオブジェクト(テーブルやインデックス、関数など)を管理するためのテーブルやビューで、PostgreSQL 9.3だと97個くらいのシステムテーブルやシステムビューから構成されています。 106 | 107 | このシステムカタログの中で、実際にどのようなテーブルがあるか、どのようなユーザがいるか、どのようなデータ型があるか、といった情報を管理しています。 108 | 109 | よって、このシステムカタログが壊れると、データベースオブジェクトを正しく見つけられなくなってしまいます。システムカタログというのは、簡単に言えば「データを管理するためのデータ」ということであり、それほどこのシステムカタログというのは重要である、ということになります。 110 | 111 | 以下はシステムカタログの例です。 112 | 113 | ``` 114 | postgres=# \dS 115 | List of relations 116 | Schema | Name | Type | Owner 117 | ------------+---------------------------------+-------+---------- 118 | pg_catalog | pg_aggregate | table | postgres 119 | pg_catalog | pg_am | table | postgres 120 | pg_catalog | pg_amop | table | postgres 121 | (略) 122 | pg_catalog | pg_database | table | postgres 123 | (略) 124 | (97 rows) 125 | 126 | postgres=# \d pg_database 127 | Table "pg_catalog.pg_database" 128 | Column | Type | Modifiers 129 | ---------------+-----------+----------- 130 | datname | name | not null 131 | datdba | oid | not null 132 | encoding | integer | not null 133 | datcollate | name | not null 134 | datctype | name | not null 135 | datistemplate | boolean | not null 136 | datallowconn | boolean | not null 137 | datconnlimit | integer | not null 138 | datlastsysoid | oid | not null 139 | datfrozenxid | xid | not null 140 | datminmxid | xid | not null 141 | dattablespace | oid | not null 142 | datacl | aclitem[] | 143 | Indexes: 144 | "pg_database_datname_index" UNIQUE, btree (datname), tablespace "pg_global" 145 | "pg_database_oid_index" UNIQUE, btree (oid), tablespace "pg_global" 146 | Tablespace: "pg_global" 147 | 148 | postgres=# 149 | ``` 150 | 151 | 「¥ds」を実行するとシステムカタログの一覧を見ることができます。 152 | 153 | pg_aggregateというテーブルが一番上にありますが、pg_aggregateというのは集約関数の情報を保持しているテーブルです。その次に、pg_amというテーブルがあります。「am」はアクセスメソッドの略ですが、いろいろなインデックスへのアクセスメソッドが一覧で管理されています。 154 | 155 | また、pg_databaseには、このPostgreSQL内部でどういったデータベースが作成・管理されているのか、といった情報が管理されています。 156 | 157 | さらに、このpg_datebaseシステムテーブルを詳細に見てみると、データベースの名前、所有者のオブジェクトIDやデータベースエンコーディングなど、その他諸々、さまざまな情報が維持・管理されていることが分かります。 158 | 159 | このようなさまざまな管理情報を保持しているのがPostgreSQLのシステムカタログです。 160 | -------------------------------------------------------------------------------- /chapter5/README.md: -------------------------------------------------------------------------------- 1 | # メモリ管理 2 | 3 | ## 共有メモリとローカルヒープ 4 | 5 | * 共有メモリ 6 | * 複数のバックエンドで共有されるデータを保持する 7 | * セッション(バックエンド)をまたいで共有すべきデータ 8 | * 共有バッファ、トランザクション情報、ロック情報、等 9 | * サービス起動時に固定サイズを確保する 10 | * CreateSharedMemoryAndSemaphores() @ ipci.c 11 | * ローカルヒープ 12 | * 個別のセッション(バックエンド)で使用するメモリ 13 | * ソート、演算処理などを実行する時に使用するメモリスペース 14 | * 必要となる度に確保、不要になったら開放 15 | * palloc(), pfree() @ mcxt.c 16 | 17 | PostgreSQLのメモリ管理について、PostgreSQLの中で使われるメモリには大きく分けて「共有メモリ」と「ローカルヒープ」という領域があります。 18 | 19 | 第1章で「共有バッファ」というキーワードがでてきましたが、アーキテクチャの図の一番右の方にあったメモリが「共有メモリ」と呼ばれるメモリ空間です。複数のバックエンドで共有させてデータを保持します。つまり、セッション間をまたいで共有すべきデータを保持しているということです。 20 | 21 | 共有バッファやトランザクションの情報、誰がどういうロックを保持しているのか、というようなロックの情報もここの領域で保持しています。 22 | 23 | この共有メモリは、PostgreSQLのサービスを起動する時に固定のサイズを確保するようになっており、内部では CreateSharedMemoryAndSemaphores() という関数で、共有メモリの確保が行われています。 24 | 25 | 一方で、ローカルヒープというのは、個別のセッションでバックエンドが使用するメモリです。例えばソートや演算処理に使うメモリ空間は他のバックエンドと共有する必要はないので、このローカルヒープを使って実行します。必要となるたびに確保して、不要になったら開放します。PostgreSQLの内部で使うためのインターフェースとして、palloc()やpfree()といった、malloc()/free()と同じような実装がされています。 26 | 27 | 28 | ## Memory Context 29 | 30 | * フラットなメモリ空間に構造を導入する 31 | * メモリコンテキスト単位で確保・破棄する(メモリリークを防止) 32 | * コンテキストの中から palloc() で確保して使用する 33 | * メモリのセグメンテーションを防止する 34 | * メモリコンテキストは用途によって切り替えて使う 35 | * TopMemoryContext (すべてのコンテキストの親) 36 | * TopTransactionContext (トランザクションのコンテキスト。COMMIT/ROLLBACKまで生存)、等 37 | * 親コンテキストを破棄すると、子コンテキストも再帰的に破棄される 38 | * メモリコンテキスト操作 @ mcxt.c 39 | * MemoryContextInit() 40 | * MemoryContextCreate() 41 | * MemoryContextSwitchTo() 42 | * MemoryContextAlloc() 43 | * MemoryContextDelete() 44 | 45 | もう1つのPostgreSQLの特徴的なメモリの管理として、「メモリコンテキスト(Memory Context)」という機能があります。 46 | 47 | Unixの場合、メモリ空間はフラットな空間になっていますが、そのフラットなメモリ空間の中に構造を導入するのが、このメモリコンテキストと呼ばれる機能です。 48 | 49 | メモリコンテキストが実現する特徴として、メモリコンテキスト単位で確保したり破棄したりすることでメモリリークを防止する、という機能があります。つまり、先ほどのpalloc()やpfree()をする際に、対象となるメモリコンテキストを決めるわけですが、そのコンテキストを破棄することによって、pfree()を呼ばなくても強制的にメモリを解放することができる、というのがメモリコンテキストの特徴の一点目になります。 50 | 51 | また、コンテキストの中でpallocで確保して使用することになりますので、メモリのセグメンテーションを防止する、ある程度局所化するということを実現することができるようになります。 52 | 53 | メモリコンテキストはその用途によって切り替えて使うことになります。TopMemoryContextやTopTransactionContextといったコンテキストが用途によって何種類かありますが、これらをうまく切り替えてpalloc()しながら使う、そして不要になったら全部まとめてコンテキストごと破棄する、というような使い方をすることになります。 54 | 55 | メモリコンテキストを操作する関数としては、mcxt.cというファイルの中で、MemoryContextInit()、MemoryContextCreate()、MemoryContextSwitchTo()や、MemoryContextAlloc()という関数があります。これらの関数を見ると、実際にメモリコンテキストをどのように管理しているか、ということが分かります。 56 | 57 | 以下は、メモリコンテキストに関連するMemoryContext構造体とそのアクセスメソッドMemoryContextMethods構造体の定義です。 58 | 59 | MemoryContext構造体の中には、parentっていう要素があり、これがそのコンテキストの親コンテキストを示しています。その下にfirstchildとかnextchildとかというツリー構造を実現するメンバ変数があって、これらを使って複数のコンテキストの構造を管理する、というのがこのメモリコンテキストの基本的なしくみになります。 60 | 61 | ``` 62 | typedef struct MemoryContextData 63 | { 64 | NodeTag type; /* identifies exact kind of context */ 65 | MemoryContextMethods *methods; /* virtual function table */ 66 | MemoryContext parent; /* NULL if no parent (toplevel context) */ 67 | MemoryContext firstchild; /* head of linked list of children */ 68 | MemoryContext nextchild; /* next child of same parent */ 69 | char *name; /* context name (just for debugging) */ 70 | bool isReset; /* T = no space alloced since last reset */ 71 | } MemoryContextData; 72 | 73 | typedef struct MemoryContextMethods 74 | { 75 | void *(*alloc) (MemoryContext context, Size size); 76 | /* call this free_p in case someone #define's free() */ 77 | void (*free_p) (MemoryContext context, void *pointer); 78 | void *(*realloc) (MemoryContext context, void *pointer, Size size); 79 | void (*init) (MemoryContext context); 80 | void (*reset) (MemoryContext context); 81 | void (*delete_context) (MemoryContext context); 82 | Size (*get_chunk_space) (MemoryContext context, void *pointer); 83 | bool (*is_empty) (MemoryContext context); 84 | void (*stats) (MemoryContext context, int level); 85 | #ifdef MEMORY_CONTEXT_CHECKING 86 | void (*check) (MemoryContext context); 87 | #endif 88 | } MemoryContextMethods; 89 | ``` 90 | 91 | ## 共有バッファと管理アルゴリズム 92 | 93 | 次に、共有バッファが実際にどのように使われているのかを解説します。 94 | 95 | * 共有バッファは、ディスク上のブロックをキャッシュする共有メモリ領域 96 | * ディスク上のブロックのうち、アクセスするものだけを読み込む 97 | * ディスクI/Oを抑えて読み書きを高速化 98 | * すべてのサーバプロセスで共有 99 | * すべてのブロックはバッファに乗らないので入れ替えが発生 100 | * 「どのバッファを捨てるか、どのバッファをキープするか」が性能に影響 101 | * Buffer replacement algorithmの変遷:LRU → ARC → 2Q → Clocksweep 102 | * BufferAlloc() @ bufmgr.c 103 | 104 | 共有バッファはディスク上のブロックをキャッシュする共有メモリ領域のことです。 105 | 106 | データベースでは、通常、物理メモリのサイズよりもデータそのもののサイズの方が大きくなっています。そのため、そういう状況において、限られたサイズの物理メモリをうまく使って、いかにI/O量を減らすかというのが共有バッファの持つ役割になります。 107 | 108 | ![共有バッファと管理アルゴリズム](5_1.jpg "共有バッファと管理アルゴリズム") 109 | 110 | 上図の例ですと、右側のテーブルのブロックの9番目とか17番目、5番目とか14番目とか、実際にアクセスする部分だけを読み込もうとしているわけですが、当然ながらすべてのブロックは物理メモリ上には載らないので、新しいブロックを読み込むときに共有バッファがもういっぱいになっていたら、読み込み済みの他のブロックを捨てないといけない、といった処理が発生します。 111 | 112 | そのどれを捨てるかを判断するアルゴリズムのことを Buffer replacement algorithm とここでは表現していますが、LRUやARC、2Qなど、昔からいくつかアルゴリズムは変遷してきました。現在は Clocksweep と呼ばれる、時計が針を刻むようにカウントしていって捨てるものを決める、というようなアルゴリズムが使われています。このアルゴリズムの詳細は BufferAlloc() というコードの実装を見ると分かります。 113 | 114 | なお、各バッファページに割り当てられるデータ構造は BufferDesc という構造体として定義されています。 115 | 116 | ``` 117 | typedef struct sbufdesc 118 | { 119 | BufferTag tag; /* ID of page contained in buffer */ 120 | BufFlags flags; /* see bit definitions above */ 121 | uint16 usage_count; /* usage counter for clock sweep code */ 122 | unsigned refcount; /* # of backends holding pins on buffer */ 123 | int wait_backend_pid; /* backend PID of pin-count waiter */ 124 | 125 | slock_t buf_hdr_lock; /* protects the above fields */ 126 | 127 | int buf_id; /* buffer's index number (from 0) */ 128 | int freeNext; /* link in freelist chain */ 129 | 130 | LWLockId io_in_progress_lock; /* to wait for I/O to complete */ 131 | LWLockId content_lock; /* to lock access to buffer contents */ 132 | } BufferDesc; 133 | ``` 134 | 135 | > バッファ管理アルゴリズムの補足。 136 | > 137 | > 典型的なRDBのバッファ管理のアルゴリズム(LRUなど)を単純に実装すると、大きなテーブルをスキャンすると、バッファの中身がすべて溢れてしまうことがあります。 138 | > 139 | > 現在のPostgreSQLでは、大きなスキャンを行う場合には別のメモリグループを確保して、メインの共有バッファが溢れないように(中身が消えないように)うまく工夫する実装が行われています。 140 | > 141 | > バッファ管理アルゴリズムが変遷してきたのは、いわゆるバッファ管理のトラディッショナルな問題をどのように解決していくか、その工夫の跡を示していると言えます。 142 | -------------------------------------------------------------------------------- /chapter7/README.md: -------------------------------------------------------------------------------- 1 | # パース、リライト、オプティマイズ 2 | 3 | ## パース、リライト 4 | 5 | * SQLのシンタックスに基づいて構文解析を行う 6 | * 構文は gram.y (yacc) で定義 7 | * パーサはSQL文を受け取り、内部構造としてのクエリツリー(Query構造体)を作成する 8 | * parse_analyze() @ postgres.c 9 | * transformSelectStmt() @ analyze.c 10 | * set debug_print_parse = true でクエリツリーをログ出力 11 | * ビューについてはクエリツリーのリライト(書き換え)行う 12 | * pg_rewrite_query() @ postgres.c 13 | * set debug_print_rewritten = true でリライト後のクエリツリーをログ出力 14 | * プランナーは、クエリツリーを受け取って実行プラン(PlannedStmt構造体)を作成する 15 | * pg_plan_queries() @ postgres.c 16 | 17 | パースというのは、SQLの文法(シンタックス)に基づいて構文解析を行う処理です。 18 | 19 | パーサはSQL文を解析した上で、内部構造としてQuery構造体(PostgreSQLではクエリツリーと呼ばれます)を作成します。 20 | 21 | PostgreSQLにはVIEWという機能があります。これはデータの実態を持つテーブルの参照方法を定義する機能ですが、PostgreSQLではクエリツリーを書き換えることによってこの機能を実現しています。VIEWというのは、特定のテーブルに対する別名の定義、別の構造の定義のような形になりますので、そこは1回作成したクエリツリーを一定のルール(VIEWの定義)に基づいて書き換えることで実現することができるわけです。 22 | 23 | プランナーはクエリツリーを受け取って実行プランを作成します。 24 | 25 | プランナーはクエリツリーを受け取って実行プランを作成するとありますが、これが実際にどのように行われるのか、いかに最適な実行プランを作成するかというのが、このオプティマイザと呼ばれるモジュールの役割になります。 26 | 27 | ## オプティマイザ統計情報 28 | 29 | * テーブルやインデックスなどのカラムの値の統計 30 | * オプティマイザは、オプティマイザ統計情報を見て実行プランを作成 31 | * 統計情報が間違っていると、妥当な実行プランができない 32 | * ANALYZEまたはVACUUMコマンドで更新 33 | 34 | !["オプティマイザ統計情報"](7_1.jpg "オプティマイザ統計情報") 35 | 36 | オプティマイザはオプティマイザ統計情報という情報を見て実行計画を作ります。 37 | 38 | オプティマイザ統計情報は、テーブルやインデックスなどのカラムの値の統計で、これはpg_statisticというシステムテーブルに統保持されてます。これを見ると特定のカラムにおけるNULL値の割合や、非NULL項目の平均保存幅、データのサイズ、データの偏りなどの情報を取得することができます。 39 | 40 | つまり、どのテーブルからどの順番で結合するとパフォーマンスがいいのか、あるいはより効率よく絞り込めるのか、など、そういった予測をオプティマイザ統計情報を使って計算します。その上で、その中で一番効率の良い実行プランを選ぶ、というのがオプティマイザの役割になります。 41 | 42 | そのため、この統計情報が間違っていると適切な実行プランの作成、選択ができなくなります。よって、オプティマイザ統計情報というのは、SQLのパフォーマンスを考える上で非常に重要なデータになります。 43 | 44 | > オプティマイザ統計情報の補足。 45 | > 46 | > データベース内に保持されているデータの状況は日々変わっていきますので、オプティマイザ統計情報も更新する必要があります。 47 | > 48 | > PostgreSQLでは、メンテナンスのためにVACUUMコマンドが自動的に実行されていますが(自動VACUUM機能)、オプティマイザ統計情報もその時同時に更新されています。VACUUMコマンドは、テーブルの(ほぼ)全スキャンを行うことになるため、その時にテーブルの情報を収集することができるためです。VACUUMコマンドとは別に、オプティマイザ統計情報を更新するためだけのANALYZEというコマンドもあります。 49 | > 50 | > オプティマイザ統計情報に実データとの相違が発生して、正しい実行プランを作成できなくなるケースとしては、新しいデータを大量に投入するケース(レコード件数0のテーブルに100万行追加するなど)、あるいは大量にデータを削除するケース(100万行あったレコードを1万行に減らすなど)などがあります。 51 | > 52 | > そのような大きいデータの変動があるとオプティマイザ統計情報の更新が追いつかないため、実行プランがおかしくなってクエリの実行時間が異様に長くなる、というケースがありますので、その時にはANZLYZEコマンドをかけて、きちんとオプティマイザ統計情報を更新する必要があります。 53 | 54 | ## よく利用されるオプティマイザ統計情報 55 | 56 | * オプティマイザ統計情報は、主にselectivityと、その結果のコストを計算するために使用される 57 | * 非nullの値の最頻値とその割合 58 | * STATISTIC_KIND_MCV @ pg_statistic.h 59 | * ある値を指定した時に、それがどれくらい存在するのかを予測する 60 | * カラムの値のヒストグラム 61 | * STATISTIC_KIND_HISTOGRAM @ pg_statistic.h 62 | * ある値を指定した時に、それ以上/それ以下の値がどれくらい存在するかを予測する 63 | * 物理的な並びと論理的な並びの相関係数 64 | * STATISTIC_KIND_CORRELATION @ pg_statistic.h 65 | * 論理的にソートをする場合のコストを予測する 66 | 67 | オプティマイザの統計情報は、主にselectivity(レコードの選択率)と実行コストを計算するために使われます。 68 | 69 | まずは、非NULL値の最頻値とその割合です。MCV(Most Common Value)と呼ばれますが、ある値を指定した際に、その値がテーブルの中にどれくらい存在しているのかというのを予測するためにこの統計情報を使います。 70 | 71 | 次は、カラムの値のヒストグラムです。ある値を指定した時に、それ以上、あるいはそれ以下の値がどのくらい存在するかとういうのを予測するために、このヒストグラムの統計情報が使われます。 72 | 73 | 最後は、物理的な並びと論理的な並びの相関係数です。データベース内でソートをする場合に、論理的なデータの順番と物理的なデータの配置が一致しているとパフォーマンスが非常に良くなります。逆に、論理的なデータの並びとブロックの並びがきちんと相関していないと、ランダムアクセスのような状況が発生してしまいますので、実行コストが高くなり、パフォーマンスが悪化することになります。そのため、実行コストを予測する際には、そういった情報も参考にしたりします。 74 | 75 | このようなさまざまな統計情報を使って最適な実行プランを作成するというのがPostgreSQLのオプティマイザの特徴になります。 76 | 77 | ## 実行コストの計算 78 | 79 | * 各種コストパラメータを用いて算出する論理的な値 80 | * 実行コストは、ディスクI/OコストとCPUコストで構成される 81 | * シーケンシャルスキャン時のブロック読み込みコスト 1.0 82 | * ランダムアクセス時のブロック読み込みコスト 4.0 83 | * タプル1行を読み込むCPUコスト 0.01 84 | * インデックス1エントリを読み込むCPUコスト 0.0025 85 | * 各種オペレーションのコスト計算 @ costsize.c 86 | * cost_seqscan() 87 | * cost_index() 88 | * cost_bitmap_heap_scan() 89 | * cost_tidscan() 90 | * cost_sort() 91 | * cost_agg() 92 | * … 93 | 94 | 実行コストは、ディスクI/OとCPUコストで構成されます。 95 | 96 | コストを算出する基礎値として、シーケンシャルスキャンのときのブロックの読み込みコストを1.0、ランダムアクセスするときのブロックの読み込みコストを4.0、タプル1行を読み込むCPUコストを0.01、インデックス1エントリを読み込むCPUコストを0.0025、のように定義されています。 97 | 98 | オプティマイザは、テーブルからデータを取り出すときにどれくらいのブロックにどのようにアクセスするのかとか、どのくらいのレコードを取り出すのか、というのを見積もった後に、この基礎値を使って実行コストを算出して、いくつか候補として計算した中でもっとも実行コストが良い実行プランを選ぶ、というのがPostgreSQLのコストベースの実装になります。 99 | 100 | > 実行コストの補足。 101 | > 102 | > オプティマイザが実行コスト算出の際に使用する基礎値は、postgresql.confの中のプランナコスト定数のパラメータとして定義されており、上記の値はデフォルト値です。そのため、自分で変更することも可能です。 103 | > 104 | > 例えば、ストレージをフラッシュデバイスにした場合に、ランダムアクセスでもコストは1.0で良いのではないか、という考え方もあります。 105 | 106 | 以下は、シーケンシャルのコストを計算する部分のコードです。 107 | 108 | ``` 109 | void cost_seqscan(Path *path, PlannerInfo *root, 110 | RelOptInfo *baserel, ParamPathInfo *param_info) 111 | { 112 | Cost startup_cost = 0; 113 | Cost run_cost = 0; 114 | double spc_seq_page_cost; 115 | QualCost qpqual_cost; 116 | Cost cpu_per_tuple; 117 | 118 | /* 予測されるテーブルの行数を取得する */ 119 | if (param_info) 120 | path->rows = param_info->ppi_rows; 121 | else 122 | path->rows = baserel->rows; 123 | 124 | if (!enable_seqscan) 125 | startup_cost += disable_cost; 126 | 127 | /* シーケンシャルスキャン時の1ページ読み込みコストを取得 */ 128 | get_tablespace_page_costs(baserel->reltablespace, NULL, &spc_seq_page_cost); 129 | 130 | /* ディスクI/Oコスト = シーケンシャルスキャンコスト * ページ数 */ 131 | run_cost += spc_seq_page_cost * baserel->pages; 132 | 133 | /* CPUコスト = タプル読み込みコスト * 取り出しタプル数 */ 134 | get_restriction_qual_cost(root, baserel, param_info, &qpqual_cost); 135 | 136 | startup_cost += qpqual_cost.startup; 137 | cpu_per_tuple = cpu_tuple_cost + qpqual_cost.per_tuple; 138 | run_cost += cpu_per_tuple * baserel->tuples; 139 | 140 | path->startup_cost = startup_cost; 141 | path->total_cost = startup_cost + run_cost; 142 | } 143 | ``` 144 | 145 | コスト計算の手順としては、最初に予測されるページの行数を取得し、そのときにシーケンシャルスキャンの1ページの読み込むコストを取得し、ディスクのI/Oのコストを出ます。次に、CPUコストを計算して、それを全部足しあわせて、最後に合計のコストを取得する、という処理になっています。 146 | 147 | これは cost_seqscan() という関数ですので、シーケンシャルスキャンのコストを計算しているというロジックであることが分かります。 148 | 149 | ## Seq scan/Index scan実行コスト例 150 | 151 | 以下は、インデックススキャンとテーブルスキャンのコストがどのように違ってくるかを理解できる例です。 152 | 153 | * select count(*) from orders where o_orderkey < $i; 154 | 155 | ![Seq scan/Index scan実行コスト例](7_2.jpg "Seq scan/Index scan実行コスト例") 156 | 157 | 上の図は、600万行あるテーブルの中からレコードを取り出す時に、少しずつレコード数を増やしていくと実行コストがどのように変わるかを図にしてものです。横軸が取り出すレコード数、縦軸が実行コストを表しています。 158 | 159 | シーケンシャルスキャンの場合は、取り出すレコードが何件かに関わらずテーブルをすべてスキャンするという処理ですので、何行取り出してもほとんど変わりません。 160 | 161 | 一方で、インデックスアクセスの場合は、取り出すレコード数が少ない時(=selectivityが高い時)は実行コストが低くなります。ただし、取り出す行数が増えていく(=selectivityが下がってくる)と実行コストが上がっていって、ある時点でシーケンシャルスキャンとインデックススキャンの実行コストが逆転します。 162 | 163 | よって、ここの交差するポイントを超えると、インデックススキャンよりもシーケンシャルスキャンを使った方が実行コストが低いのである、ということをオプティマイザが判断して、インデックススキャンではなくシーケンシャルスキャンを使う実行プランを作成する、というのがオプティマイザの役割になります。 164 | 165 | 166 | ## GEQO(遺伝的問い合わせ最適化) 167 | 168 | * 結合するテーブルが増えると、組み合わせのパターンが増加する 169 | * テーブルがnの場合の組み合わせのパターン:n! 170 | * 結合処理を行うテーブルが多くなった場合、PostgreSQLではGEQOというオプティマイザが実行される 171 | * 遺伝的アルゴリズムを用いた最適化処理 172 | 173 | PostgreSQLには、CEQO(遺伝的問い合わせ最適化)というオプティマイザの機能があります。 174 | 175 | SQLでJOINの処理を行う場合、使用するテーブルが増えていくと、実行プランを作成する際に試さなければならない組み合わせのパターンが爆発的に増えていきます。そうなると、オプティマイザの処理がどんどん重くなります。 176 | 177 | そのため、結合処理を行うテーブルを多くなった時には、PostgreSQLではGEQOというオプティマイザが実行されます。 178 | 179 | これは遺伝的アルゴリズム(GA)を使ってJOINの組み合わせ爆発の問題を避けるテクニックで、このような工夫を用いて、計算量を抑えながらより良い実行プランを早く見つける、ということをPostgreSQLのオプティマイザは行っています。 180 | 181 | ## 参考資料 182 | 183 | * 問合せ最適化インサイド http://www.slideshare.net/ItagakiTakahiro/ss-4656848 184 | * より深く知るオプティマイザとそのチューニング http://www.slideshare.net/hayamiz/ss-42415350 185 | * PostgreSQL:行数推定を読み解く/row-estimation https://speakerdeck.com/kyabatalian/row-estimation 186 | * Beyond EXPLAIN: Query Optimization From Theory To Code http://pt.slideshare.net/hayamiz/beyond-explain-query-optimization-from-theory-to-code 187 | -------------------------------------------------------------------------------- /chapter8/README.md: -------------------------------------------------------------------------------- 1 | # エグゼキュータ 2 | 3 | ## エグゼキュータの役割 4 | 5 | * テーブルスキャン 6 | * インデックススキャン 7 | * 結合 8 | 9 | エグゼキュータは、SQLを実行する最後の段階で、実際にブロックアクセスなどによってデータを読み込んだり演算して返却したりする処理です。 10 | 11 | エグゼキュータの中では、テーブルスキャン、インデックススキャン、結合というような処理が行われていますが、ここでは結合、JOINに絞って見ていきます。 12 | 13 | 14 | ## 結合処理 15 | 16 | * Nested Loop Join 17 | * Merge Join 18 | * Hash Join 19 | 20 | PostgreSQLでは、大きく分けて3つ結合処理、Nested Loop Join、Merge Join、Hash Joinが実装されています。 21 | 22 | ## Nested Loop Join 23 | 24 | * Nested Loop Join 25 | * Outer tableを一行読んで、Inner table のカラムに該当する値を探す 26 | * Outer tableが少ないのが望ましい 27 | * Inner tableのカラムにはインデックスがあるのが望ましい 28 | * スタートアップコストなし 29 | * 計算量 O(NM) 30 | 31 | !["Nested Loop Join"](8_1.jpg "Nested Loop Join") 32 | 33 | 出典: ["Explaining EXPLAIN 第2回"](http://www.postgresql.jp/wg/shikumi/study20_materials/explain8aac660e8cc76599-7b2c2056de3057304f307f52c95f374f1a.pdf/view) 34 | 35 | Nested Loop Joinは、外側(Outer)テーブルのスキャンしながら、毎回InnerをスキャンしながらOuterに結合できる行を探す、という処理になります。 36 | 37 | そのため、最大の実行コストは「Outerの行数×Innerの行数」ということになります。 38 | 39 | ## Merge Join 40 | 41 | * Merge Join 42 | * 結合に使うカラムでOuterテーブルとInnerテーブルをあらかじめソートしておく(スタートアップコスト) 43 | * 結合するカラムにインデックスがあることが望ましい 44 | * 計算量 O(NlogN+MlogM) 45 | 46 | !["Merge Join"](8_2.jpg "Merge Join") 47 | 48 | 出典: ["Explaining EXPLAIN 第2回"](http://www.postgresql.jp/wg/shikumi/study20_materials/explain8aac660e8cc76599-7b2c2056de3057304f307f52c95f374f1a.pdf/view) 49 | 50 | 51 | ## Hash Join 52 | 53 | * Hash Join 54 | * 結合に使用するOuterテーブルのカラムでハッシュ表を作成する(スタートアップコスト) 55 | * ハッシュ表がメモリに乗るサイズになることが望ましい 56 | * 計算量 O(N+M) 57 | 58 | !["Hash Join"](8_3.jpg "Hash Join") 59 | 60 | 出典:["Explaining EXPLAIN 第2回"](http://www.postgresql.jp/wg/shikumi/study20_materials/explain8aac660e8cc76599-7b2c2056de3057304f307f52c95f374f1a.pdf/view) 61 | 62 | 63 | ## インデックスの種類 64 | 65 | * B-Treeインデックス 66 | * ツリー構造のインデックス 67 | * 一致だけでなく、指定した範囲を探す(インデックススキャン) 68 | * =, <, > などの演算をサポート 69 | 70 | * Hashインデックス 71 | * “=“ の演算をサポート 72 | 73 | * GiSTインデックス 74 | * 汎用検索ツリー(Generalized Search Tree) 75 | * http://db.cs.berkeley.edu/papers/vldb95-gist.pdf 76 | * 他のツリー型インデックスの実装のテンプレートにできる 77 | 78 | * GINインデックス 79 | * 汎用転置インデックス(Generalized Inverted Index) 80 | * 全文検索のインデックスとして使用される(分かち書きと併用) 81 | 82 | PostgreSQLでは、大きく4つのインデックスが使えます。B-Treeインデックス、Hashインデックス、GiSTインデックスとGINインデックスです。 83 | 84 | それぞれ使われ方が異なっており、よく使われるのはB-Treeです。最近では全文検索などの用途でGINインデックスなどが使われるケースが増えてきました。 85 | 86 | PostgreSQL特有のものという観点では、例えば最近できたGiSTインデックスは「汎用検索ツリー」と呼ばれるもので、これは他のツリー型のインデックスを実装するときのテンプレートとして使えるものです。内部でAPIがいくつか定義されており、それを拡張することで新しいインデックスを作れるようになります。 87 | 88 | GINインデックスは、汎用転置インデックスと呼ばれるもので、フルテキスト検索をしたい場合に、分かち書きなどで元の文章を細かく分割して、その単語が文書のどこにあるのか、ブロックのどこにあるのか、といった情報を持つためのインデックスです。特に日本語の全文検索などではよく使われるものです。 89 | 90 | 91 | ## インデックスのアクセスメソッド 92 | 93 | * PostgreSQLの各種インデックスは、アクセスメソッドのセット 94 | * pg_amテーブルから一覧を取得可能 95 | * 新しいインデックス定義する場合は、アクセスメソッドを拡張する 96 | 97 | ``` 98 | postgres=> select amname,aminsert,ambeginscan,amgettuple,amrescan,amendscan,ambuild from pg_am; 99 | amname | aminsert | ambeginscan | amgettuple | amrescan | amendscan | ambuild 100 | --------+------------+---------------+--------------+------------+-------------+----------- 101 | btree | btinsert | btbeginscan | btgettuple | btrescan | btendscan | btbuild 102 | hash | hashinsert | hashbeginscan | hashgettuple | hashrescan | hashendscan | hashbuild 103 | gist | gistinsert | gistbeginscan | gistgettuple | gistrescan | gistendscan | gistbuild 104 | gin | gininsert | ginbeginscan | - | ginrescan | ginendscan | ginbuild 105 | spgist | spginsert | spgbeginscan | spggettuple | spgrescan | spgendscan | spgbuild 106 | (5 rows) 107 | 108 | postgres=> 109 | ``` 110 | 111 | PostgreSQLのインデックスは、pg_amと呼ばれるシステムカタログの中で定義されていて、上記の例ではbtree、hash、gist、gin、spgistというのがあります。 112 | 113 | その中にINSERTのオペレーションをするメソッド(関数)や、スキャンを開始するときのメソッドなど、それぞれのインデックスのオペレーションをサポートするメソッドが定義されています。 114 | 115 | これを拡張することによって、新しいインデックスを作ったりすることもできるわけです。 116 | 117 | 118 | ## B-Treeインデックスのタプル取得呼び出し 119 | 120 | * ブロック番号とブロック内位置を取得 121 | 122 | 以下は、B-Treeのインデックスのタプルの呼び出しのコードです。 123 | 124 | ``` 125 | ItemPointer 126 | index_getnext_tid(IndexScanDesc scan, ScanDirection direction) 127 | { 128 | FmgrInfo *procedure; 129 | bool found; 130 | 131 | GET_SCAN_PROCEDURE(amgettuple); 132 | 133 | /* 134 | * The AM's amgettuple proc finds the next index entry matching the scan 135 | * keys, and puts the TID into scan->xs_ctup.t_self. It should also set 136 | * scan->xs_recheck and possibly scan->xs_itup, though we pay no attention 137 | * to those fields here. 138 | */ 139 | found = DatumGetBool(FunctionCall2(procedure, PointerGetDatum(scan), Int32GetDatum(direction))); 140 | 141 | /* If we're out of index entries, we're done */ 142 | if (!found) 143 | { 144 | /* ... but first, release any held pin on a heap page */ 145 | ... 146 | } 147 | 148 | /* Return the TID of the tuple we found. */ 149 | return &scan->xs_ctup.t_self; 150 | } 151 | ``` 152 | 153 | コードブロックの中ほどで、DatumGetBool()という呼び出しがありますが、さらにFunctionCall2()という関数を呼び出していることが分かります。ここでは、先ほど見ていたインデックスのアクセスメソッドのうちamgettupleメソッドを呼んでいます。 154 | 155 | その際に、引数としてインデックススキャンをするためのデータの構造体や、スキャンの方向の情報などを渡して、スキャンを開始します。 156 | 157 | このコードブロック自体は index_getnext_tid() という関数なので、次のインデックスタプルを取ってくる、というオペレーションはこのようなコードで実装されているわけです。 158 | 159 | 160 | ## B-Treeインデックスのアクセスメソッド 161 | 162 | * B-Treeインデックスへのインターフェース @ nbtree.c 163 | * テーブルからインデックス作成 btbuild() 164 | * btbuildempty() 165 | * btinsert() 166 | * インデックスのスキャン開始 btbeginscan() 167 | * インデックスから次のタプルを取得する btgettuple() 168 | * インデックスのスキャンを終了 btendscan() 169 | 170 | 171 | ## B-Treeインデックスの構造 172 | 173 | * ユーザIDのカラムにインデックスを作成した場合 174 | 175 | !["B-Treeインデックスの構造"](8_4.jpg "B-Treeインデックスの構造") 176 | 177 | PostgreSQLのB-Treeインデックスの内部の構造は、まずはメタページという8kbのブロックがB-Treeのファイルの中にありまして、それからルートノード、インターナルノード、リーフノードがあります。 178 | 179 | リーフノードの中にはインデックスのキーの値を保持していて、リーフノード同士で横につながっているという構造になっています。 180 | 181 | ここで、1つのブロックの中に1から200とか、201から400というようにユーザIDのキー値が入っているとします。そのうち1つのキーを取り出そうとすると、まずはインデックスのタプルデータを取得することになります。 182 | 183 | インデックスのタプルデータには、実レコードの格納されているテーブルブロックの情報、つまり該当するデータがテーブルファイルのどこのブロックにあるのか、そしてそのブロックの中の何番目のレコードなのか、といったような値が記録されており、その情報を使うことで、インデックスからテーブルのレコードを探すことが可能になるわけです。 184 | 185 | このような構造を使って、インデックスとテーブルのレコード、タプルをつなげるというのが、PostgreSQLの実装になっています。 186 | 187 | 188 | ## B-Treeデモ 189 | 190 | 以下はB-Treeインデックスのデモです。 191 | 192 | ``` 193 | snaga=# create table t1 as select generate_series(1,1000)::integer as c; 194 | SELECT 1000 195 | snaga=# select max(c),min(c) from t1; 196 | max | min 197 | ------+----- 198 | 1000 | 1 199 | (1 row) 200 | snaga=# create index t1_c_idx on t1(c); 201 | CREATE INDEX 202 | snaga=# select * from bt_metap('t1_c_idx'); 203 | magic | version | root | level | fastroot | fastlevel 204 | --------+---------+------+-------+----------+----------- 205 | 340322 | 2 | 3 | 1 | 3 | 1 206 | (1 row) 207 | snaga=# select * from bt_page_items('t1_c_idx',1); 208 | itemoffset | ctid | itemlen | nulls | vars | data 209 | ------------+---------+---------+-------+------+------------------------- 210 | 1 | (1,141) | 16 | f | f | 6f 01 00 00 00 00 00 00 211 | 2 | (0,1) | 16 | f | f | 01 00 00 00 00 00 00 00 212 | 3 | (0,2) | 16 | f | f | 02 00 00 00 00 00 00 00 213 | (...snip...) 214 | 366 | (1,139) | 16 | f | f | 6d 01 00 00 00 00 00 00 215 | 367 | (1,140) | 16 | f | f | 6e 01 00 00 00 00 00 00 216 | (367 rows) 217 | 218 | snaga=# select * from pgstatindex('t1_c_idx'); 219 | -[ RECORD 1 ]------+------ 220 | version | 2 221 | tree_level | 1 222 | index_size | 32768 223 | root_block_no | 3 224 | internal_pages | 0 225 | leaf_pages | 3 226 | empty_pages | 0 227 | deleted_pages | 0 228 | avg_leaf_density | 81.99 229 | leaf_fragmentation | 0 230 | 231 | snaga=# 232 | ``` 233 | 234 | 上記の例では、最初にテーブルt1を作成して、その中に値を1から1000まで入れINSERTしています。そのため、maxとminを取り出すと、1と1000になっています。 235 | 236 | それに対してインデックス作成して、そのインデックスの中身、まずはメタページを見てみます。 237 | 238 | メタページを見ると、ルートが3番目のブロックになっていてツリーのレベルは1、つまりB-Treeの段数は2段である、というようなことが分かります。 239 | 240 | また、それのキーの値やタプルIDを見てみると、キーの値(dataカラム)が1、2から始まって少しずつ増えていってるのが分かるかと思いますし、タプルID(ctidカラム)にはレコードを保持しているテーブルのブロック番号とブロック内のオフセット値のペアが保存されていることが分かります。 241 | 242 | ## GINインデックス 243 | 244 | * 汎用転置インデックスのフレームワーク 245 | 246 | !["GINインデックス"](8_5.jpg "GINインデックス") 247 | -------------------------------------------------------------------------------- /chapter4/README.md: -------------------------------------------------------------------------------- 1 | # MVCCとストレージ構造 2 | 3 | ## テーブルファイル 4 | 5 | まず、ユーザのデータを保存するテーブルファイルとその構造について解説します。 6 | 7 | * 8kB単位のブロック単位で構成される 8 | * 各ブロックの中に実データのレコード(タプル)を配置 9 | * 基本的に追記のみ 10 | * 削除したら削除マークを付加する(VACUUMで回収) 11 | * レコード更新時は「削除+追記」を行う。 12 | 13 | ![テーブルファイル](4_1.jpg "テーブルファイル") 14 | 15 | PostgreSQLのテーブルファイルは、基本的に8kB単位のブロックで構成されています。図にありますが、全体をひとつのテーブルファイルだと考えると、その中にブロック1、ブロック2、ブロック3…というように、8kB単位で作成されています。 16 | 17 | 各ブロックの中に実データのレコード(タプル)が入っているという構造になっています。第3章でサンプルを見ながら解説しましたが、基本的には追記されるだけで、updateされる時には違うレコードとして追記していく、というしくみになっております。 18 | 19 | 右側にはテーブルの統計情報が表示されています。 20 | 21 | tuple_countというのが行(レコード)の数で、dead_tauple_countというのは、その中で削除された行の数がどれくらいあるかという統計情報です。テーブルの中が物理的にどういう構造・状態になっているのか、ということを知るための統計情報を取ることができます。 22 | 23 | ## テーブルのページレイアウト 24 | 25 | 8kbのブロックの中がさらにどうなっているのかというのが、以下の図です。 26 | 27 | * テーブルファイルのページブロックは、ページヘッダ、アイテムポインタ、タプルヘッダ、およびタプルデータで構成される。 28 | 29 | ![テーブルのページレイアウト](4_2.jpg "テーブルのページレイアウト") 30 | 31 | この8kbのブロックの中にページヘッダと呼ばれる管理情報を持つ領域があります。 32 | 33 | 次にアイテムポインタと呼ばれる、実際のユーザのデータがブロック内のどこに存在しているのかを表す「ラインポインタ」を持っています。そして、実際のユーザのデータは、ブロックの空き領域を後ろの方から使って保存されていきます。 34 | 35 | つまり、このブロックの中からユーザのデータを読むには、(1)このブロックを読み込んで、(2)アイテムポインタを見て、(3)アイテムポインタの指している先のタプルを読み込んで、(4)さらにタプルのヘッダを読んで、(5)最後にタプルデータを読むという処理によって、実際のユーザのデータを取り出せるという流れになります。 36 | 37 | ページヘッダを除く空きスペースを、アイテムポインタが前方から、レコードのデータが後方から使用します。ページヘッダが28バイト、アイテムポインタが4バイト、といったサイズで定義されていますので、レコードを保存するのに使えるブロック内の領域は限られてきます。 38 | 39 | また、当然ながらタプルのデータ、つまりユーザのデータというのは、使っているデータ型に依存して、実際にどのようなテーブル定義をしてどのようなデータを入れるかによって、実際のタプルのサイズは変わってきます。 40 | 41 | ## データ型とデータサイズ 42 | 43 | ![データ型とデータサイズ](4_3.jpg "データ型とデータサイズ") 44 | 45 | 上記はマニュアルからの抜粋ですが、データの型とサイズについては、smallintは2バイト、integerは4バイト、bigintを使うと8バイトというように、テーブルの定義、カラムに使うデータ型によって、レコードのサイズが規定されます。 46 | 47 | 48 | ## インデックス(B-Tree)ファイル 49 | 50 | * 8kB単位のブロック単位で構成される 51 | * ブロック(8kB単位)をノードとする論理的なツリー構造を持つ 52 | * ルート、インターナル、リーフの各ノードから構成 53 | * ルートノードから辿っていく 54 | * リーフノードは、インデックスのキーとレコードへのポインタを持つ 55 | 56 | ![インデックス(B-Tree)ファイル](4_4.jpg "インデックス(B-Tree)ファイル") 57 | 58 | PostgreSQLのB-Treeインデックスも一般的なB-Treeの構造を取っており、「ルート、インターナル、リーフ」がある、という構造です。各ノードは8kb単位のブロックで構成されています。 59 | 60 | 一番最後であるリーフノードにはインデックスのエントリがあり、キーの値とレコードへのポイントを持っています。 61 | 62 | ## B-Tree(リーフ)のページレイアウト 63 | 64 | * B-Treeインデックスのリーフページブロックは、ページヘッダ、アイテムポインタ、インデックスタプルで構成される。 65 | 66 | ![B-Tree(リーフ)のページレイアウト](4_5.jpg "B-Tree(リーフ)のページレイアウト") 67 | 68 | B-Treeのページのレイアウトもテーブルのブロックと同じように、ページヘッダがあり、空き領域の後ろの方からデータが保存され、前の方はアイテムポインタが使うという構造になっています。 69 | 70 | 71 | ## VACUUM処理 72 | 73 | レコードをUPDATEをすると、論理的には1行なんだけれど、PostgreSQLの内部では物理的には複数行ある、という話をしてきました。 74 | 75 | ![VACUUM処理](4_6.jpg "VACUUM処理") 76 | 77 | 上記の図の左上は、「レコード2」を「レコード2’」として更新したときに、物理的には前のレコードが残っていて、かつ新しいレコードがある、という状態を示しています。 78 | 79 | PostgreSQLの中では、自動的にVACUUMという処理が走っていて、その処理が走ると、昔削除した古いレコードで、もう不要な領域が空き領域として管理されるようになります。 80 | 81 | その空き領域として管理されるようになると、その領域が再度使えるようになり、異なるレコード(例えばレコード5)を追記するときには、その空き領域を使えます。 82 | 83 | 逆に、VACUUMによって空き領域として開放されていないと、まだ古いレコードで埋まっているので、空き領域を使えずにファイルの末尾に追記する必要があり、追記に伴ってファイルサイズが大きくなってしまうことになります。 84 | 85 | ですので、PostgreSQLがこの追記型のストレージの構造を持っているというのは、ある意味において「アーキテクチャしてのトレードオフ」をそこに設定しているということになります。そのため、不要になった後にVACUUMで領域を回収することでパフォーマンスを維持する、それ以外にもVACUUMの問題をどうにか回避しよう、という工夫が、PostgreSQL内部にはいろいろと実装されています。 86 | 87 | ## インデックスとタプルの可視性 88 | 89 | 1. PostgreSQLでは、可視性情報をレコードタプルに持つ 90 | 1. インデックスエントリは、可視性情報を持たない 91 | 1. インデックスエントリが存在しても、レコード本体が「削除済」になっている可能性がある 92 | 1. よって、インデックス経由でレコードを取り出す場合、「インデックスエントリがある→レコード本体が生きてる」という確認処理が行われる 93 | 1. つまり、インデックスのブロックと、テーブルのブロックの両方に必ずアクセスが発生する 94 | 95 | PostgreSQLでは、レコードごとに可視性を判断する情報を持っているという解説をしてきました。あるトランザクションに対してレコードを見せるべきか見せないべきか、あるいはVACUUMをする時に生きてるか死んでるかを判断するための情報はテーブル内のレコードに持っています。 96 | 97 | 逆に、インデックスの側にはそういった情報を持っていません。 98 | 99 | そのため、例えば「ユーザID = 100」というインデックスのエントリが存在していたとしても、テーブルの方のレコードを見てみると、「ユーザID = 100」というレコードはすでに削除された後である、という事象が発生し得ます。 100 | 101 | そのため、インデックス経由でレコードを読む場合には、(1)インデックスエントリがあることを確認した後に、(2)該当するレコード本体がまだ生きているかどうかを確認する、という流れになります。 102 | 103 | よって、インデックスのブロックにアクセスして、その後テーブルのブロックにアクセスしなければならなくなりますので、この仕組みはオーバーヘッドになるのではないか、という議論もあります。そのため、この問題を克服するために、PostgreSQLでは内部でいくつか工夫が実装されています。 104 | 105 | 106 | ## TOASTテーブル 107 | 108 | PostgreSQLでは、8kbのブロックに収まりきらないような大きなデータを管理するための手法として「TOAST」という呼ばれる機能が実装されています。 109 | 110 | 1. The Oversized-Attribute Storage Technique 111 | 1. 長い値(約2kb以上)を、通常のテーブルのブロックではなく、専用の外部テーブルに持たせる機能 112 | 1. toast_save_datum() @ tuptoaster.c 113 | 1. TOAST対象の値にOIDを付与 114 | 1. 約2kbのチャンクに分割して、チャンク番号を付与 115 | 1. 1ブロックに最大4チャンクを格納 116 | 1. 例えば、7kbのテキストをTOASTする場合 117 | 1. テキストの値に OID (例えば12345)を付与 118 | 1. 2kB, 2kB, 2kB, 1kBの4チャンクに分割 119 | 1. チャンク番号を0, 1, 2, 3と付与する 120 | 1. TOASTテーブルに書き込む 121 | 1. 元テーブルのフィールドに位置情報を保存 122 | 1.varatt_external型 123 | 124 | ![TOASTテーブル](4_7.jpg "TOASTテーブル") 125 | 126 | TOASTでは、大きなデータ(通常は2kb以上)を通常のテーブルのブロックではなく、専用の外部テーブルに格納する方式を取ります。その際、データにOIDを付与して2kbにチャンクに分割、それぞれのチャンクにチャンク番号を付与して、1ブロックに最大4チャンクを保存する、という処理を行っています。 127 | 128 | 例えば7kbのテキストをテーブルに入れようとした場合には、そのテキストの値にまずOID、例えば12345のような値を付与して、その後に2kb、2kb、2kb、1kbの4つチャンクに分割します。それぞれのチャンクにチャンク番号0、1、2、3と付与して、それを2kbずつTOASTテーブルに書き込みます。 129 | 130 | このようにして、長いデータ(text、byteaやblobなど)はこのような仕組みで管理されているのがPostgreSQLの特徴です。 131 | 132 | 以下はTOAST対象となるデータを内部で持つ時のデータ構造です。 133 | 134 | ``` 135 | struct varatt_external 136 | { 137 | int32 va_rawsize; /* Original data size (includes header) */ 138 | int32 va_extsize; /* External saved size (doesn't) */ 139 | Oid va_valueid; /* Unique ID of value within TOAST table */ 140 | Oid va_toastrelid; /* RelID of TOAST table containing it */ 141 | }; 142 | ``` 143 | 144 | 可変長のデータは上記のように、もともとのサイズ(va_rawsize)と外部のサイズ(va_extsize)、TOASTテーブル(外部テーブル)内でのOID(va_valueid)と、TOASTテーブルそのもののOID(va_toastrelid)が設定されて、元のテーブルの方に、このように「外部のどのテーブルのどこに保存されたのか、どれくらいのサイズなのか」という情報が保存されるようになっています。 145 | 146 | 147 | ## FreeSpace Map (FSM) 148 | 149 | データベース内ではブロックがたくさん作成されるわけですが、ではそのブロックにどれくらいの空き領域があるのか、今どこのブロックにどれくらい書き込めるのか、といった情報を内部で管理しておく必要があります。 150 | 151 | 例えば、400bytesのデータをINSERTしたい時に、その400bytesのデータを実際にどこのブロックに書き込めますか、というような空き領域の管理をしているのがFreeSpace Mapという機能です。 152 | 153 | 1. 各ページの空き領域情報を管理するためのファイル 154 | 1. タプルを格納する空き領域のあるページを見つける 155 | 1. 空き領域のあるページがもう無いことを確認する 156 | 1. 各テーブル/インデックスファイルごとに存在 157 | 1. 拡張子 “_fsm” のファイル(リレーションの “fork” と呼ばれる) 158 | 1. ブロックごとに1バイトを使って、空き領域を BLCKSZ/256 で割った値を保持 159 | 1. FSM value = Free space / ( BLCKSZ / 256 ) 160 | 1. 例)1024bytesの空き領域がある場合、1024/ (8192/256) = 32 を記録 161 | 1. Free space = FSM value * ( BLCKSZ / 256 ) 162 | 1. FSM value 0~255 で空き領域 0~8160 bytes を表現可能 163 | 1. 各ページ内に8158個のノードを持つバイナリツリー構造 164 | 1. 4095のNonLeafと、4063のLeaf (topとleafを含めて13段のツリー) 165 | 166 | これはタプルを格納する空き領域があるページを見つけるための機能で、必要とすサイズの空き領域のあるページを見つけるか、あるいは空き領域があるページがもうない、つまりどのページにも書き込めないので新しくブロックを1個増やさないといけない、などといった判断をするために使われます。 167 | 168 | 各テーブルファイルやインデックスファイルごとに存在していて、<拡張子_fsm> という名前のついたファイルになります。 169 | 170 | FSMでは、それぞれのブロックごとに上記の計算式でFSMの値を出して、それを格納しています。空き領域、Freespaceを「ブロックサイズ÷256」という値を「FSMの値」として持っています。逆にその値を使えば、そのブロックにどれくらいの空き領域があるかを逆算できます。 171 | 172 | 173 | ## FSMの物理レイアウト 174 | 175 | 実際のFSMはdumpを取ると以下のようなの物理レイアウトになっています。 176 | 177 | ![FSMの物理レイアウト](4_8.jpg "FSMの物理レイアウト") 178 | 179 | グレーアウトされている部分はページヘッダです。 180 | 181 | それ以降は「6f」というデータがありますが、全体は2分木で構成されていて、直下の2つのリーフのうち、大きい空き領域のある方を記憶している、という構造になっています。 182 | 183 | ツリー構造は全部で13段あり、ツリーの下の方でも「6f」という値がFSMの値として保持されています。 184 | 185 | 他のところはほとんど0になっていますが、これはこのテーブルに1ブロックしか存在していなかった時の状況です。 186 | 187 | ## Visibility Map (VM) 188 | 189 | PostgreSQLには、Visibility Mapという機能があり、このVisibility Mapは、VACUUMするときに本当にVACUUMする必要があるかどうかを判断するために、あるいはインデックスを読んだ際にテーブルのレコード本体を読みに行く必要があるかどうか、といった判断をする際に参照されるデータです。 190 | 191 | 1. 各ブロックのレコードの可視性状態を保持するファイル 192 | 1. 「削除された行があるかどうか」をビットマップで保持 193 | 1. 拡張子 “_vm” のファイル(リレーションの “fork” と呼ばれる) 194 | 1. そのブロックをVACUUMする必要があるかどうかの判断 195 | 1. Index-Only Scan(レコード本体を見ない)できるかどうかの判断 196 | 1. ビットが立っていると、ブロック内の全タプルが全トランザクションに可視 197 | 1. つまり、VACUUMする必要がない 198 | 1. 加えて、テーブルファイルにアクセスしなくても、タプルがすべて生きていることが分かる(Index-Only Scan) 199 | 1. visibilitymap_test() @ visibilitymap.c 200 | 1. タプルを操作した際に、ビットを操作する 201 | 1. visibilitymap_clear() @ visibilitymap.c 202 | 1. visibilitymap_set() @ visibilitymap.c 203 | 204 | 各ブロックのレコードの可視状態がどうなっているかというのを保持するファイルで、ブロックの中に削除されたレコードがあるかどうかの情報をビットマップで保持しています。つまり、0/1でこのブロックの状態を管理しています。 205 | 206 | Visibility Mapは、そのブロックをVACUUMをする必要があるかどうかの判断をするために参照する、あるいは(Index-Only Scanと呼ばれる処理ですが)インデックスアクセスの際にテーブル本体のレコードの可視性を確認しなくて済むかどうかを判断するために参照する、そのためのフラグとしてビットマップを持っています。 207 | 208 | 209 | ## Visibility Mapの構造 210 | 211 | 以下はVisibility Mapの構造です。 212 | 213 | 1. テーブルのブロックとVMの関係 214 | 1. ブロック1は全レコード可視、ブロック2はレコード6が削除(更新)済み 215 | 1. VACUUM時に、ブロック1の処理は飛ばす 216 | 1. Index-Only Scanの時に、ブロック1のタプルはすべて存在と分かる 217 | 218 | ![VMの構造](4_9.jpg "VMの構造") 219 | 220 | 以下で、テーブルのブロックとVisibilityの関係を見てみます。 221 | 222 | 例えば、ブロック1とブロック2がある場合に、ブロック1の方はレコードがすべて生きていて、ブロック2の方は1行だけ削除されている状態を考えてみます。 223 | 224 | ブロック1の中に「1、3、4、2’」というレコードが存在していて、どれもまだ削除されてないとします。また、ブロック2の方は、「5、6、7、8、9」というレコードがありますが、レコード6というのが6'として更新されていて、古いレコード「6」は削除済み、という状態を考えてみます。 225 | 226 | そのときにVisibility Mapがどういうデータをもっているかというと、ブロック1の可視性について、すべてのレコードが可視の場合には1を持ちます。このブロックの中のレコードが全部生きている、という情報を持っています。 227 | 228 | こちらの2つ目のブロックは、すでに1レコードが削除されてるので、可視性のフラグが0になっていて、このブロックの中には削除された行がある、というステータスがここで保持されています。 229 | -------------------------------------------------------------------------------- /chapter2/README.md: -------------------------------------------------------------------------------- 1 | # トランザクション管理 2 | 3 | ## トランザクション処理におけるACID特性 4 | 5 | ここではPostgreSQLのトランザクション管理について解説します。 6 | 7 | トランザクション処理には「ACID特性、ACID属性」と呼ばれる要件があります。ACIDは、それぞれ「Atomicity、Consistency、Isolation、Durability」の頭文字を取ったもので、それぞれに備えているべき特徴があります。 8 | 9 | * Atomicity (原子性) 10 | * それ以上分解できない単位の操作である 11 | * 「変更された」か「変更されていないか」のどちらか 12 | * Consistency (一貫性、整合性) 13 | * 予め定められたルールに則った(整合性の取れた)状態である 14 | * 正の値しかとらない、など。 15 | * Isolation (分離性、独立性) 16 | * 実行中のトランザクションが他のトランザクションに影響を与えない 17 | * 実行中のトランザクションの状態を参照・変更することができない 18 | * Durability (永続性) 19 | * 一度コミットされたトランザクションは、何があっても残される 20 | * 障害が発生しても、コミットされたトランザクションの結果は残る 21 | 22 | 23 | 一つ目の「Atomicity、原子性」は、「実行されたか、実行されていないか」の状態でそれ以上分解できない単位であるということ、つまり中途半端に処理されている状態を防ぐことができる、という性質です。 24 | 25 | その次が「Consistency、一貫性」で、あらかじめ定められたルールや制約にのっとった整合性のとれた状態である必要があるというのがこのトランザクション処理の2つ目の性質です。 26 | 27 | 3つ目の「Isolation、分離性」というのは、実行するトランザクションが他のトランザクションに影響を与えない、 影響を与えてはいけない、という性質です。 28 | 29 | 最後の「Durability、永続性」というのは、例えばコミットしたトランザクションで金額を変更した場合、その変更内容が失われては困るので、その変更はきちんと残っていなければならない、という性質です。 30 | 31 | > Durabilityについての補足。 32 | > 33 | > 永続化をするといっても、現実には技術的な限界もあります。コミットしたトランザクションのログデータというのは、変更データがどのようなデバイスに存在するのか、例えばそれがローカルディスクにあるのか、ネットワークを介して大阪にあるのか、シンガポールにあるのか、いずれにせよ消失してしまう可能性があります。つまり、「何があっても残される」というのは保証できません。 34 | > 35 | > そのため、実際のシステムでは、このトランザクションログを2重化するか3重化するか、あるはどのようなデバイスに書くかということが重要な問題で、あとはどのようにバックアップを取るかと、といったことが現場では必要になる技術的な知識になります。 36 | 37 | ## 各レコードの可視性の管理 38 | 39 | ここで、トランザクションのACIDを実現する上で重要な要素となる、各レコードの可視性管理について説明します。 40 | 41 | * PostgreSQLのテーブルファイルの中には、複数バージョンのタプルが存在する 42 | * 複数のトランザクションにうまくデータを「見せる」ため 43 | * 詳細は、ACID属性のIsolationで 44 | * 最初は一行、更新される度に、新しい行が追加 45 | * 各行は、「作成したトランザクション」と「削除(更新)したトランザクション」の情報を持つ 46 | 47 | PostgreSQLテーブルファイルの中には複数のバージョンのタプル(行、レコード)が存在します。 48 | 49 | PostgreSQLの中では同じ行を更新したときに、違う行として書き込むという特徴があり、これがPostgreSQLでレコードをうまく管理するための1つの方法、実装になっております。 50 | 51 | このあたりは、MySQLとかOracleとかと違う実装になっています。 52 | 53 | なぜこのような実装になっているかというと、複数のトランザクションに対してうまく適切なタプルを見せるためです。最初はタプルが1行あるところから始まりますが、そのレコードが更新されるたびに新しいタプルが追加されるという形になります。そのため、各タプルは「作成したトランザクション」と「削除されたトランザクション」の情報を持ちます。 54 | 55 | 実際の例で見てみます。 56 | 57 | まず最初に一行Insertをして、その後にselectとするとInsertしたレコードが見えます。 58 | 59 | ``` 60 | testdb=# INSERT INTO t1 VALUES ( 101, 'insert 1' ); 61 | INSERT 0 1 62 | testdb=# SELECT * FROM t1; 63 | uid | uname 64 | -----+---------- 65 | 101 | insert 1 66 | (1 row) 67 | ``` 68 | 69 | この時、実際のテーブルの中のデータを細かく見てみると、ブロックの中の状態としては、まずは1行存在していることが分かります、 70 | 71 | ``` 72 | testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0)); 73 | lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax 74 | ----+--------+----------+--------+--------+-------- 75 | 1 | 8152 | 1 | 37 | 1859 | 0 76 | (1 row) 77 | ``` 78 | 79 | 次にupdateすると、論理的には1行のままではあるものの、ブロックの中を見ると実際には2行ある、という状態になります。 80 | 81 | ``` 82 | testdb=# UPDATE t1 SET uname = 'update 1' WHERE uid = 101; 83 | UPDATE 1 84 | testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0)); 85 | lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax 86 | ----+--------+----------+--------+--------+-------- 87 | 1 | 8152 | 1 | 37 | 1859 | 1860 88 | 2 | 8112 | 1 | 37 | 1860 | 0 89 | (2 rows) 90 | ``` 91 | 92 | 複数のタプルを管理するために、各タプルのt_max/t_minというフラグを使って、これは削除されたタプルである、あるいはまだ削除されていないタプルである、という情報を管理しています。 93 | 94 | ``` 95 | testdb=# UPDATE t1 SET uname = 'update 2' WHERE uid = 101; 96 | UPDATE 1 97 | testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0)); 98 | lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax 99 | ----+--------+----------+--------+--------+-------- 100 | 1 | 8152 | 1 | 37 | 1859 | 1860 101 | 2 | 8112 | 1 | 37 | 1860 | 1861 102 | 3 | 8072 | 1 | 37 | 1861 | 0 103 | (3 rows) 104 | 105 | testdb=# 106 | ``` 107 | 108 | このように、updateするごとに1行、1行、物理的に増えていくというのがPostgreSQLの特徴になっています。 109 | 110 | 111 | ## Atomicity(原子性)の実装 112 | 113 | * PostgreSQLにおけるAtomicityの実装 114 | * コミットされたデータのみを処理対象とすることで原子性を実現 115 | * アボートされたデータもディスク上には存在するが無視される 116 | * 各タプルヘッダの情報によって「コミットされた状態かどうか」を判断 117 | * 重要なのは以下の二つ 118 | * 各タプルごとの可視性情報 119 | * HeapTupleFields 構造体 の t_xmin, t_xmax メンバー 120 | * HeapTupleHeader 構造体の t_infomask メンバー 121 | * コミットログによるトランザクションの状態情報 122 | * 4つの状態定義:IN_PROGRESS, COMITTED, ABORTED, SUB_COMMITTED @ clog.h 123 | * TransactionIdGetCommitLSN() → TransactionIdGetStatus() 124 | 125 | 次に、PostgreSQLにおけるAtomicityの実装について説明します。 126 | 127 | PostgreSQLでは、コミットされたデータのみを処理対象とすることで原子性を実現しています。アボートされたトランザクションのデータもディスク上には存在しますが、これらは無視されます。 128 | 129 | 各レコード(タプル)のヘッダの部分でコミットされた状態かどうかという情報を持っています。 130 | 131 | ここで重要なのは「各タプルごとに可視性の情報を持っている」ということと「コミットログによるトランザクションの状態情報」という点です。 132 | 133 | 先ほど可視性の説明の際にも触れましたが、各レコードのヘッダ部分に、この行は生きてますよ、とか、もう削除されましたよ、という情報を保持しています。 134 | 135 | また、PostgreSQLではコミットログと呼ばれるログを持っており、このログではトランザクションがまだ実行中なのか、それともコミットされたのか、あるいはアボートしたのか、といった4つの状態の定義を保持しています。 136 | 137 | これらの情報によって、テーブルから読みだした行が、本当にコミットされたトランザクションのものなのか、あるいはアボートされたトランザクションのものなのか、といった状態を読み取りつつ、この行は本当にユーザに返すべき(=見せるべき)行なのか、あるいは無視すべき(=見せてはならない)行なのか、といった判断がPostgreSQLの内部では行われています。 138 | 139 | このようなしくみを使って、データベースを使うユーザに対して論理的な整合性を保ちつつデータを提供しています。 140 | 141 | 以下は、各タプルのヘッダの構造体です。 142 | 143 | ``` 144 | typedef struct HeapTupleFields 145 | { 146 | TransactionId t_xmin; /* inserting xact ID */ 147 | TransactionId t_xmax; /* deleting or locking xact ID */ 148 | 149 | union 150 | { 151 | CommandId t_cid; /* inserting or deleting command ID, or both */ 152 | TransactionId t_xvac; /* old-style VACUUM FULL xact ID */ 153 | } t_field3; 154 | } HeapTupleFields; 155 | 156 | 157 | struct HeapTupleHeaderData 158 | { 159 | union 160 | { 161 | HeapTupleFields t_heap; 162 | DatumTupleFields t_datum; 163 | } t_choice; 164 | 165 | ItemPointerData t_ctid; /* current TID of this or newer tuple */ 166 | 167 | /* Fields below here must match MinimalTupleData! */ 168 | uint16 t_infomask2; /* number of attributes + various flags */ 169 | uint16 t_infomask; /* various flag bits, see below */ 170 | uint8 t_hoff; /* sizeof header incl. bitmap, padding */ 171 | 172 | /* ^ - 23 bytes - ^ */ 173 | bits8 t_bits[1]; /* bitmap of NULLs -- VARIABLE LENGTH */ 174 | 175 | /* MORE DATA FOLLOWS AT END OF STRUCT */ 176 | }; 177 | ``` 178 | 179 | 180 | ## Consistency(一貫性)の実装 181 | 182 | * PostgreSQLにおけるConsistencyの実装 183 | * ステートメント実行時に各種制約がチェックされる 184 | * コミット時まで遅延される制約もあり、コミット時にチェックされる 185 | * SET CONSTRAINTS { DEFERRED | IMMEDIATE } で変更可能 186 | * いずれにせよコミット完了時には制約に整合していることを保証 187 | * 制約の評価の遅延実行 188 | * 遅延設定 AfterTriggerSetState() @ utility.c 189 | * 遅延実行 AfterTriggerFireDeferred() @ xact.c 190 | 191 | 次は、Consistency、一貫性の実装についてです。 192 | 193 | データベースでは、各データがどういった論理の状態であるか、例えば、これとこれを足したらこういう状態であるべきであるとか、このカラムはこの値しか入ってはいけない、あるいは、このテーブルのこの値はこっちのテーブルのこの値とイコールである必要がある、など、そのような論理的な制約の実現して、その一貫性をもつ必要があります。 194 | 195 | PostgreSQLでは制約の評価という形で実現しており、データが特定の制約を満たす必要がある場合、(遅くとも)コミットするときにそういった論理的な制約をチェックしてコミット処理をしています。 196 | 197 | コミット時に論理的な制約を取れないような操作を行うトランザクションは、そのコミット自体ができないのでアボートされることになります。つまり、一貫性を崩すような変更はコミットできない、ということになります。 198 | 199 | PostgreSQLでは、このようにしてデータベースの一貫性が保たれるというような実装になっています。 200 | 201 | 興味がある方は、関連するソースコードの関数やヘッダファイルを参照してください。 202 | 203 | ## Isolation(分離性)の実装 204 | 205 | * PostgreSQLにおけるIsolationの実装 206 | * MVCC (Multi-Version Concurrency Control) 207 | * Snapshotによるトランザクションの世代管理 (snapshot.h) 208 | * 実装はXIDとCommandIdによるトランザクションの世代管理 209 | * “Snapshot” とは 210 | * トランザクションごとに生成 211 | * 何が見えて何が見えないのかという可視性の管理情報 212 | * Visibilityはトランザクションの分離レベルによっても変わる 213 | * PostgreSQLでの実装は3つのレベル 214 | * Read Committed、Repeatable Read、Serializable 215 | 216 | PostgreSQLにおけるIsolationの実装については、MVCCを実現していますということと、Snapshotによるトランザクションの世代管理をしている、という特徴があります。 217 | 218 | Snapshotというのは内部でトランザクションの管理に使うデータ構造で、トランザクションを開始する時に作成され、どのタイミングでトランザクションが始まったかという情報を保持しています。 219 | 220 | このSnapshotを使うことによって、そのトランザクションでどの行が見えるべきか、あるいはどの行が見えてはいけないのかという可視性の判断を実現することができることになります。 221 | 222 | ## トランザクション分離レベルの定義 223 | 224 | * Read Uncommitted 225 | * 他のトランザクションがコミットしていない内容が見える (Dirty Read) 226 | * Read Committed 227 | * 他のトランザクションがコミットしていない内容は見えない 228 | * 他のトランザクションがコミットした変更が途中から見える (Unrepeatable Read) 229 | * Repeatable Read 230 | * 他のトランザクションがコミットしていない内容は見えない 231 | * 他のトランザクションがコミットした変更は見えない 232 | * 他のトランザクションがコミットした追加・削除が見える(Phantom Read) 233 | * 但し、PostgreSQLの実装では発生しない 234 | * Serializable 235 | * 他のトランザクションがコミットしていない内容は見えない 236 | * 他のトランザクションがコミットした変更は見えない 237 | * 他のトランザクションがコミットした追加・削除が見えない 238 | 239 | 行の可視性、その行が見えるべきかどうかというトランザクションの分離レベルによっても少しずつ変わってきます。トランザクションの分離レベルには Read Committed、Repeatable Read、Serializabelなどがあります。 240 | 241 | 分離レベルには、他のトランザクションがコミットしたデータだったら見えてもいいとか、他のトランザクションがコミットしたもの見えてはならない(Serializable)、といった、いくつかのレベルがあります。 242 | 243 | なぜ複数のレベルがあるかというと、厳密にすべてのトランザクションをSerialozable(直列化)するとパフォーマンスへの影響が非常に大きくなります。 244 | 245 | そのため、トランザクション処理のパフォーマンスを改善するために、ところどころは同時に実行してもいいよとか、コミットしたデータだったら見えても良い、といったように、少しずつ制約を緩くしてる、つまり他のトランザクションへの影響を許容しているのがこのトランザクションの分離レベルの定義になります。 246 | 247 | > 分離レベルの補足。 248 | > 249 | > Read CommittedのトランザクションとSerializableのトランザクションが同時に実行されて、両方が同じレコードを更新しようとした場合、どちらが先に実行されるかによっても動作が変わりますが、片方のトランザクションが他方のトランザクションがコミットされるのを待つ、といった状態が発生することがあります。 250 | > 251 | > 興味のある方は実際に試してみてください。 252 | 253 | 以下は、Snapshotのデータの構造体です。トランザクション関連のxmaxとかxminのような時間やタイミングを表すデータがその内部に含まれています。 254 | 255 | ``` 256 | typedef struct SnapshotData 257 | { 258 | SnapshotSatisfiesFunc satisfies; /* tuple test function */ 259 | 260 | /* 261 | * The remaining fields are used only for MVCC snapshots, and are normally 262 | * just zeroes in special snapshots. (But xmin and xmax are used 263 | * specially by HeapTupleSatisfiesDirty.) 264 | * 265 | * An MVCC snapshot can never see the effects of XIDs >= xmax. It can see 266 | * the effects of all older XIDs except those listed in the snapshot. xmin 267 | * is stored as an optimization to avoid needing to search the XID arrays 268 | * for most tuples. 269 | */ 270 | TransactionId xmin; /* all XID < xmin are visible to me */ 271 | TransactionId xmax; /* all XID >= xmax are invisible to me */ 272 | TransactionId *xip; /* array of xact IDs in progress */ 273 | uint32 xcnt; /* # of xact ids in xip[] */ 274 | /* note: all ids in xip[] satisfy xmin <= xip[i] < xmax */ 275 | int32 subxcnt; /* # of xact ids in subxip[] */ 276 | TransactionId *subxip; /* array of subxact IDs in progress */ 277 | bool suboverflowed; /* has the subxip array overflowed? */ 278 | bool takenDuringRecovery; /* recovery-shaped snapshot? */ 279 | bool copied; /* false if it's a static snapshot */ 280 | 281 | /* 282 | * note: all ids in subxip[] are >= xmin, but we don't bother filtering 283 | * out any that are >= xmax 284 | */ 285 | CommandId curcid; /* in my xact, CID < curcid are visible */ 286 | uint32 active_count; /* refcount on ActiveSnapshot stack */ 287 | uint32 regd_count; /* refcount on RegisteredSnapshotList */ 288 | } SnapshotData; 289 | ``` 290 | 291 | 292 | ## Durability(永続性)の実装 293 | 294 | * PostgreSQLにおけるDurabilityの実装 295 | * チェックポイントにおけるデータファイルへの更新 296 | * コミットにおけるWAL(トランザクションログ)への同期書き込み 297 | 298 | * チェックポイント 299 | * 共有メモリ上のデータをディスクに一括して反映する処理 300 | * CreateCheckpoint() @ xlog.c 301 | * WAL同期書き込み 302 | * WALファイルは同期書き込みモードでオープン 303 | * XLogFlush() @ xlog.c 304 | 305 | 最後にDurability、永続性の実装について。 306 | 307 | PostgreSQLでは、「チェックポイント」と「コミット時におけるトランザクションログへの書き込み」という機能が実装されており、この2つによって永続性が実現されています。 308 | 309 | チェックポイントというのは後述しますが、共有メモリのデータをディスクに一括して反映する処理で、RDBMSによって非常に重要な処理です。 310 | 311 | > 永続性についての補足。 312 | > 313 | > MySQLのInnodbでは、[doublewrite](https://dev.mysql.com/doc/refman/5.5/en/innodb-disk-io.html) という方法を使って永続性を担保しています。 314 | > 315 | > これは、テーブルなどのデータブロックを変更する前に、一旦 [doublewrite buffer](https://dev.mysql.com/doc/refman/5.5/en/glossary.html#glos_doublewrite_buffer) と呼ばれるテーブルスペース内の領域にデータを(時系列/continuousに)書き出し、その後に改めてテーブルのデータを変更する処理です。 316 | > 317 | > これは、PostgreSQLのトランザクションログ相当の処理をInnodb内で完結させているのと同等の処理と考えることができるでしょう。 318 | 319 | ## チェックポイント 320 | 321 | * チェックポイントとは 322 | * 共有バッファの内容がディスクに反映されていることを保証する地点。 323 | * クラッシュリカバリの開始点として使われる。 324 | * チェックポイントにおける処理 325 | * 共有バッファ内の変更されているページ(dirtyページ)をディスクに一括して書き戻す。 326 | * ディスクI/Oとしてはランダム(テーブルやインデックスの必要なブロックのみ)かつ同期書き込みなので時間がかかる。 327 | 328 | ![チェックポイント](2-1.jpg "チェックポイント") 329 | 330 | 共有バッファの中身を一定のタイミングでディスクに書き出すのがチェックポイントの処理になります。 331 | 332 | 先に説明した通り、通常は共有バッファ上のデータを読み書きしているわけですが、メモリの内容というのはクラッシュすると消えてしまいます。そのため、データの永続性を実現するために、チェックポイントの処理によって一定の周期でディスクに書き出す、ということをしています。 333 | 334 | 例えば、上記の図の一番右端のタイミングでクラッシュした場合を考えてみます。 335 | 336 | チェックポイントのしくみ上、クラッシュした直前のチェックポイントまではディスクにデータが保存されていることが保証されています。 337 | 338 | そのため、直前のチェックポイントまで戻った上で、そのチェックポイント以降のトランザクションログを取り出してきて最新の状態までリカバリを実行することで、データのリカバリを実施します。(これをロールフォワードリカバリと言います) 339 | 340 | > チェックポイントの補足。 341 | > 342 | > PostgreSQLでは、チェックポイントが実行されると、その直後にページブロックを変更する際に(更新レコードだけではなく)ページブロック全体をトランザクションログに書き出します。これが full page write と呼ばれる処理です。 343 | > 344 | > このことによって、万一クラッシュなどが発生した場合に、ページブロック全体をトランザクションログから復旧できるようにしています。 345 | > 346 | > 但し、このようにページブロック全体を書き出すとトランザクションログが大きくなるため、(一貫性や永続性を実現するための技術的なトレードオフではあるものの)解決すべき課題として開発者たちには認識されています。 347 | --------------------------------------------------------------------------------