Upgrade to Pro — share decks privately, control downloads, hide ads and more …

JPUG勉強会 OSSデータベースの内部構造を理解しよう

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on.

JPUG勉強会 OSSデータベースの内部構造を理解しよう

PostgreSQLを題材に「OSSデータベースの内部構造を理解する第一歩」を ご紹介します

Avatar for Atsushi Ogawa

Atsushi Ogawa

February 12, 2026
Tweet

Other Decks in Programming

Transcript

  1. 自己紹介 OSSデータベースの内部構造を理解しよう 2 • 氏名:小川 淳 • PostgreSQLへの貢献 • PostgreSQL:

    regexp_replace • 高速化 • PostgreSQL: Cache last known per-tuple offsets to speed long tuple access • PostgreSQL: FunctionCallN improvement. • PostgreSQL: AllocSetReset improvement • github: https://github.com/oga5 • X: @AOga51748099
  2. 今日のゴール • PostgreSQLのソースコードの読むために必要な知識を習得 • PostgreSQLのアーキテクチャ • ソースコードの入手・ビルド • デバッガ(gdb)で動作を観察する方法 •

    システムカタログについて OSSデータベースの内部構造を理解しよう 3 ソースコード内部 SQL 日本語 リレーション (Relation) テーブル (Table) 表 タプル (Tuple) レコード (Record) 行 アトリビュート (Attribute) カラム (Column) 列 (*)資料中の用語について:以下は同じ意味で使用します
  3. デモの環境について • Windows11のWSL環境を利用します • PC環境 • Thinkad T14s • CPU:

    Intel Core Ultra 7 255H (16core) • Memory: 32GB • WSL環境: Ubuntu 24.04 • PostgreSQL: Version 18.1 WSLチートシート WSLの環境を構築して、PostgreSQLをビルド (Meson) 環境構築の参考資料(Zenn) OSSデータベースの内部構造を理解しよう 4
  4. アジェンダ 1. プロセスとデータ構造 2. ソースコードからビルド 3. SQL処理の流れ 4. システムカタログ 5.

    カタログキャッシュ 6. CATALOGマクロ OSSデータベースの内部構造を理解しよう 5
  5. 共有メモリ(Shared Memory): Server Process間で共有するメモリ領域 Shared Buffer データファイルのキャッシュ ページ単位(通常8KB)で管理 WAL Buffer

    ログバッファ Lock Table ロック管理 Server Process: Postmasterが各プロセスを起動 (fork) Backend Process 接続ごとのプロセス Postmaster (親プロセス) Checkpointer BackgroundWriter ディスク領域($PGDATA) WALWriter … fork fork … データファイル 固定長ページ(通常8KB) 設定ファイル 制御ファイル ログファイル … Backend Process PSQL JDBC Client プロセスとデータ構造 TABLEやINDEX毎に ファイルを作成 Client Process OSSデータベースの内部構造を理解しよう 7
  6. ファイルアクセス OSSデータベースの内部構造を理解しよう 8 Shared Buffer データファイルのキャッシュ ページ単位(通常8KB)で管理 Backend Process 接続ごとのプロセス

    データファイル 固定長ページ(通常8KB) ① ② ③ バックエンドプロセスがテーブルのブロックからレコードを取り出したいとき ①共有メモリのShared Bufferにあるか確認。あればそれを参照 ②共有メモリにない場合はファイルからブロックをRead ③共有メモリの空きページにコピーしてから利用する PostgreSQL 18では、②のファイルReadをI/O workerプロセスが実行する 場合もある(io_methodパラメータの設定次第) ・sync: バックエンドが自分でpreadシステムコールを実行 ・worker: バックエンドはI/O workerに読み取り要求する I/O workerプロセスがpread (PostgreSQL 18のデフォルト) ・io_uring: Linuxのio_uring APIを利用して非同期I/Oを実行 (効率が良いが、Linuxカーネル5.5以降が必要)
  7. ページ内部の構造 〜8KB ページの内部レイアウト〜 PageHeaderData (24 bytes) Line Pointer Array (ItemIdData[])

    lp[0] lp[1] lp[2] lp[3] Free Space Tuple 4 (newest) Tuple 3 Tuple 2 Tuple 1 (oldest) LP は上から伸び、Tuple は下から積む ー 両者が出会うとページ満杯 Line Pointer ページ内の各タプルのオフセット位置を持つ配列 固定サイズの配列なのでC言語の配列として扱える タプル本体:タプル毎に可変長 OSSデータベースの内部構造を理解しよう 9
  8. タプルの構造 〜1行のタプルがディスク上/メモリ上でどう並んでいるか〜 具体例:CREATE TABLE users (id int, name text, active

    bool, value int8) Header Null Bitmap pa d id int4 name varlen text active bool 固定長型 int4, bool, int8 → そのまま格納 (例: int4は4バイト, boolは1バイト) 可変長型 text, varchar → 先頭に長さ情報 (varlen)。その後ろにテキスト。NULL終端しない Null Bitmap 各カラムが NULL かどうかを 1bit で管理。NULLが存在しないタプルはNull Bitmapは省略 value int8 pa d pad 次のカラムをその型が要求する境界に揃えるために挿入される空きバイト int4なら4で割り切れるメモリ番地に配置する Header 固定長のヘッダ。トランザクション可視性(xmin/xmax)や各種フラグなどがある OSSデータベースの内部構造を理解しよう 10
  9. ソースコードの入手 ソース入手 (github) # ディレクトリ作成 mkdir –p $HOME/demo cd $HOME/demo

    # ソースコードを取得 git clone https://github.com/postgres/postgres.git cd postgres # tag一覧の確認 git tag # バージョンを指定してcheckout git checkout REL_18_1 gitの代わりに以下のURLからtarballを入手してもよいです https://www.postgresql.org/ftp/source/v18.1/ OSSデータベースの内部構造を理解しよう 12
  10. ソースコード:主なディレクトリ postgres/ src/ bin/ ← psql, pg_dumpなどのコマンド include/ ← includeファイル

    backend/ ← サーバ本体 tcop/ ← バックエンドのメイン(SQL受信、実行) access/ ← table, indexのアクセスメソッド parser/ optimizer/ executor/ catalog/ ← システムカタログ storage/ ← ファイルアクセス、ロック、共有メモリなど util/ adt/ ← データ型毎の実装 (int4, textなど) cache/ ← キャッシュ(カタログキャッシュなど) mmgr/ ← メモリマネージャ OSSデータベースの内部構造を理解しよう 13
  11. ビルド 〜 Ver.17 から meson/ninjaで高速ビルドが可能(従来の configure/make も利用可能)〜 meson/ninja(高速) cd $HOME/demo/postgres

    meson setup ../build ¥ --prefix=$HOME/demo/pgsql ¥ --buildtype=debug ¥ -Dcassert=true ¥ -Dc_args="-O0 -g3" cd ../build ninja ninja install 従来方式(configure/make) cd $HOME/demo/postgres CFLAGS="-O0 -g3" mkdir ../build cd ../build ../postgres/configure ¥ --prefix=$HOME/demo/pgsql ¥ --enable-debug --enable-cassert make -j$(nproc) make install $HOME/demo/postgres ソースコード $HOME/demo/build ビルド作業用 $HOME/demo/pgsql インストール先 ディレクトリ構成 OSSデータベースの内部構造を理解しよう 14
  12. 参考:meson vs configure/make(PG 18.1) OSSデータベースの内部構造を理解しよう 項目 meson + ninja Autotools

    (configure + make) 差分 user time 224.57 秒 404.42 秒 Meson が約 180 秒(55%)高速 system time 68.47 秒 50.41 秒 Meson のほうが多い (Ninja の小タスク並列化の影響) elapsed time (実時間) 29.62 秒 48.73 秒 Meson が約 19 秒(40%)高速 CPU 使用率 989% 933% Meson のほうが CPU を効率的に使用 最大常駐メモリ 715,760 KB 223,756 KB Meson のほうがメモリを多く使う (高速化のため) I/O outputs 4,400,200 1,597,504 Meson はビルドグラフが細かく I/O が多い pagefaults (minor) 13,654,538 11,234,547 Meson のほうが多い (Ninja の高速並列化の特徴) 15
  13. DB 初期化と起動 OSSデータベースの内部構造を理解しよう 〜デバッグビルドした PostgreSQL でクラスタを作成し起動する〜 DB クラスタ作成 export PATH=$HOME/demo/pgsql/bin:$PATH

    export PGDATA=$HOME/demo/pgsql/data initdb --no-locale --encoding=UTF8 起動 & DB作成 pg_ctl start -l $HOME/demo/pgsql/logfile createdb psqlで接続 psql 16
  14. 起動から SQL 実行までの関数フロー OSSデータベースの内部構造を理解しよう 18 〜main() から exec_simple_query() に至るまで〜 Postmaster(親プロセス)

    main() main.c エントリポイント PostmasterMain() postmaster.c 共有メモリ初期化、ポート LISTEN ServerLoop() postmaster.c 接続待ち受けループ (select/poll) BackendStartup() fork() Backend(子プロセス) BackendMain() backend_startup.c 子プロセスの初期化 ↓ PostgresMain() postgres.c SQL処理メインループ ReadCommand() postgres.c クライアントからの SQL 入力待ち ↓ exec_simple_query() postgres.c SQLを実行 ループして次のSQLを受け付ける postmaster.c 接続要求を受けて子プロセス生成 postmaster_child_launch() postmaster.c fork()で親/子に分岐 postmaster_child_launch() postmaster.c fork後、子プロセス側エントリポイント ↓ ループして次の接続を受け付ける ↓ ↓ ↓ ↓ ↓
  15. fork() 子プロセスを生成する // Postmasterの処理概要 for(;;) { // 接続待ち events =

    WaitEvent… … pid = fork(); // ここで子プロセスを生成。子プロセスもここから再開。 if( pid == 0 ) { // 子プロセス: バックエンドプロセスのメインルーチンを実行して、クライアント(psqlなど)の相手をする BackendMain(); } // 親プロセス: 次の接続を受け付けるためループ } CheckPointerなどのサーバプロセスもPostmasterがforkして起動 (例: fork -> CheckpoiterMain) postgres実行ファイル1つに、Postmaster, CheckPointer, Backendなどのメインルーチンが入っている OSSデータベースの内部構造を理解しよう 19
  16. SQL処理の流れ (exec_simple_query) OSSデータベースの内部構造を理解しよう 20 exec_simple_query("SELECT * FROM users WHERE id

    = 1") Parser SQL → RawStmt → Analyzer/ Rewriter RawStmt → Query RawStmtにカタログの 情報を付加 ViewなどのRewrite → Planner/ Optimizer Query → Plan 実行計画を生成 統計情報などのカタログ にアクセス → Executor Plan実行 → 結果出力 ポイント:SQLの実行はカタログなしでは動けない 実行計画を実行 SQLをRawStmtに変換 文法のチェック カタログアクセスなし pg_parse_query() pg_analyze_and_rewrite_ fixed_params() pg_plan_queries() PortalRun()
  17. デモ: デバッガでexec_simple_queryを観察 (1) 環境設定 export PATH=$HOME/demo/pgsql/bin:$PATH sudo sysctl kernel.yama.ptrace_scope=0 (*)

    ptrace_scope=0になっていないとgdbでアタッチできない 別端末で psqlを起動 psql # パラレルクエリを無効にする SET max_parallel_workers_per_gather = 0; # backend PIDを確認 select pg_backend_pid(); → 例: 12345 (*) パラレルクエリが有効の場合、1つのSQLを複数プロセスで処理するため動作がわかりにくくなる gdbでatatch gdb –p 12345 OSSデータベースの内部構造を理解しよう 21
  18. デモ: デバッガでexec_simple_queryを観察 (3) gdbの設定 break exec_simple_query continue psqlでSQL実行 select *

    from pg_class; SQL実行すると、exec_simple_queryで停止 OSSデータベースの内部構造を理解しよう 23
  19. FK: Foreign Key PK : Primary Key pg_class(テーブル) PK oid

    relname relkind relfilenode FK reltype pg_attribute(カラム) FK attrelid attname FK atttypid attnum pg_type(データ型) PK oid typname FK typinput FK typoutput pg_proc(関数) PK oid proname pronargs FK prorettype FK proargtypes pg_operator(演算子) PK oid oprname FK oprleft FK oprright FK oprresult FK oprcode システムカタログの参照関係 OSSデータベースの内部構造を理解しよう 26
  20. OSSデータベースの内部構造を理解しよう 27 システムカタログのデータ例 oid | typname | typlen | typinput

    | typoutput ------+---------+--------+-------------------+--------------------- 16 | bool | 1 | 1242 (boolin) | 1243 (boolout) 23 | int4 | 4 | 42 (int4in) | 43 (int4out) 1700 | numeric | -1 | 1701 (numeric_in) | 1702 (numeric_out) 2275 | cstring | -2 | 2292 (cstring_in) | 2293 (cstring_out) oid | proname | prolong | prosrc | pronargs | prorettype | proargtypes ------+------------+---------+------------+----------+----------------+-------------------- 42 | int4in | 12 | int4in | 1 | 23 (int4) | 2275 (cstring) 43 | int4out | 12 | int4out | 1 | 2275 (cstring) | 23 (int4) 65 | int4eq | 12 | int4eq | 2 | 16 (bool) | 23 (int4) 23 (int4) 1718 | numeric_eq | 12 | numeric_eq | 2 | 16 (bool) | 1700 (numeric) 1700 (numeric) oid | oprname | oprleft | oprright | oprresult | oprcode ------+---------+----------------+----------------+------------+-------------------- 96 | = | 23 (int4) | 23 (int4) | 16 (bool) | 65 (int4eq) 1752 | = | 1700 (numeric) | 1700 (numeric) | 16 (bool) | 1718 (numeric_eq) pg_type pg_proc pg_operator ・typinput 文字列からデータへの変換関数 ・typoutput データから文字列への変換関数 typlen: -1 (可変長), -2 (NULL終端文字列) prolang = 12 (internal function)の場合、prosrcの関数名でソースコードを検索すれば実装が見つかる PostgreSQL 18.1では「=」が63種類登録されている (左辺/右辺のデータ型によって実行される関数が変わる)
  21. OSSデータベースの内部構造を理解しよう 28 “=“の実装例 (integer, numeric) src/backend/utils/adt/int.c Datum int4eq(PG_FUNCTION_ARGS) { int32

    arg1 = PG_GETARG_INT32(0); int32 arg2 = PG_GETARG_INT32(1); PG_RETURN_BOOL(arg1 == arg2); } src/backend/utils/adt/numeric.c Datum numeric_eq(PG_FUNCTION_ARGS) { Numeric num1 = PG_GETARG_NUMERIC(0); Numeric num2 = PG_GETARG_NUMERIC(1); bool result; result = cmp_numerics(num1, num2) == 0; PG_FREE_IF_COPY(num1, 0); PG_FREE_IF_COPY(num2, 1); PG_RETURN_BOOL(result); } データ型によって”=“の複雑さが異なる mov eax, [arg1] ; arg1をeaxレジスタへ cmp eax, [arg2] ; arg2と比較 sete al ; equal → AL = 1, else 0 ret コンパイルしても数命令 (以下はx86の例) integer (int4) numeric cmp_numericsは数十行の関数
  22. システムカタログのデータ構造 OSSデータベースの内部構造を理解しよう 29 • システムカタログの実体はテーブルとインデックス • 通常のテーブルやインデックスと同じ処理で読み書きされる • システムカタログは主に固定長かつNOT NULLのカラムを使用

    • relnameなどの文字列もNAME型(64バイト固定長)にしている • 可変長やNULLABLEなカラムはテーブルの末尾に配置 • 固定長部分は同じメモリレイアウトにすることで、高速アクセスを実現 Header Null Bitmap pa d oid OID(4byte) name NAME(64byte) active bool value int8 pa d タプルの前方は固定長かつNOT NULLの データ型を利用することで、全てのタプルが 同じメモリレイアウトになる acl aclitem[] 可変長や NULLABLEは 後方に配置
  23. キャッシュ無効化 (Invalidation) OSSデータベースの内部構造を理解しよう 32 DDLを実行すると、他の Backend のキャッシュはどう更新される? 1 DDL 実行

    ALTER TABLE users ADD COLUMN email text; Backend A がカタログを更新 自身の カタログキャッシュ も更新 → 2 Commit時に Invalidation Messageを送信 共有メモリのShared Invalidation Queueに通知 を書き込む 共有メモリ上のキューを通じて 全 Backend に伝達 → 3 他 Backend が受信 次のSQL開始時に無効になっ たカタログキャッシュを削除 必要になった時点で カタログから再読み込み
  24. システムカタログの実装 OSSデータベースの内部構造を理解しよう 33 • ソースコード • backend/src/util/catcache.c • backend/src/util/syscache.c •

    SearchCatCacheInternal()でbreakして観察すると理解が深まる gdbの設定例: SearchCatCacheInternalを通過するときにカタログ名、検索条件を出力 break SearchCatCacheInternal command silent printf "SearchCatCacheInternal:%s,%d,%d,%d,%d¥n", cache->cc_relname, v1, v2, v3, v4 continue end continue
  25. .hファイル OSSデータベースの内部構造を理解しよう 35 例:/include/catalog/pg_class.h CATALOG(pg_class,1259,RelationRelationId) { NameData relname; Oid relnamespace;

    ... } FormData_pg_class; システムカタログの.hファイルが、C言語の構造体とシステムカタログのテーブル定義を兼ねている
  26. .dat ファイル — カタログ初期データの定義 OSSデータベースの内部構造を理解しよう 36 〜データ型・関数・演算子などの初期データは .dat に Perl

    ハッシュで記述されている〜 pg_type.dat(型の初期データ) # src/include/catalog/pg_type.dat [ { oid => '23', typname => 'int4', typlen => '4', typbyval => 't', typcategory => 'N', typinput => 'int4in', typoutput => 'int4out', }, ... ] 主要な .dat ファイル pg_proc.dat 組み込み関数 (2700+件) pg_operator.dat 組み込み演算子 (800+件) pg_type.dat 組み込み型 (100+件) pg_class.dat カタログ自身の定義 ビルドパイプライン .h + .dat → genbki.pl → postgres.bki
  27. CATALOG マクロから initdb まで 〜1つの カタログ定義 (.hファイル)が Backend コードとデータベース初期化の両方で使われる〜 pg_class.h

    CATALOG(pg_class, 1259, ...) コンパイル時 ビルド時 Backend のソースでは C 構造体として参照 // src/include/catalog/pg_class.h typedef struct FormData_pg_class { NameData relname; Oid relnamespace; ... } FormData_pg_class; + pg_class.dat (初期データ) genbki.pl で変換 postgres.bki (BKI コマンド列) initdb postgres --boot でカタログテーブル作成 構造体とディスク(メモリ)レイアウトが一致→ タプルを構造体にキャストして高速に読める OSSデータベースの内部構造を理解しよう 37
  28. 起動から SQL 実行までの関数フロー OSSデータベースの内部構造を理解しよう 42 〜main() から exec_simple_query() までデバッガ(gdb)で観察する〜 Postmaster(親プロセス)

    main() main.c エントリポイント PostmasterMain() postmaster.c 共有メモリ初期化、ポート LISTEN ServerLoop() postmaster.c 接続待ち受けループ (select/poll) BackendStartup() fork() Backend(子プロセス) BackendMain() backend_startup.c 子プロセスの初期化 ↓ PostgresMain() postgres.c SQL処理メインループ ReadCommand() postgres.c クライアントからの SQL 入力待ち ↓ exec_simple_query() postgres.c SQLを実行 ループして次のSQLを受け付ける postmaster.c 接続要求を受けて子プロセス生成 postmaster_child_launch() postmaster.c fork()で親/子に分岐 postmaster_child_launch() postmaster.c fork後、子プロセス側エントリ ↓ ループして次の接続を受け付ける ↓ ↓ ↓ ↓ ↓
  29. gdbコマンド集 OSSデータベースの内部構造を理解しよう 43 run : 実行開始 next (n) : step実行

    (次の行まで実行) step (s) : stepin (関数の中に入る) continue (c) : 実行再開 print <変数名> : 変数の値を表示 Enterキー : 前回のコマンドを実行 Ctrl+Cキー : 実行中のプログラムを一時停止 bt : スタックトレースを表示 break <関数名>: ブレークポイントの設定(関数名指定) 例: break BackendStartup break <ファイル名:行番号>: ブレークポイントの設定 (行番号指定) 例: break main.c:86 info breakpoint : ブレークポイントリストの表示 disable <ブレークポイント番号> : ブレークポイントを無効にする enable <ブレークポイント番号> : ブレークポイントを有効にする delete <ブレークポイント番号> : ブレークポイントを削除 set follow-fork-mode child|parent : fork後にデバッグしたいプロセスを指定 (childまたはparent) quit : gdb終了
  30. main()からデバッガで動作を観察 (1) OSSデータベースの内部構造を理解しよう 44 環境設定 export PATH=$HOME/demo/pgsql/bin:$PATH export PGDATA=$HOME/demo/pgsql/data sudo

    sysctl kernel.yama.ptrace_scope=0 (*) ptrace_scope=0になっていないとgdbでアタッチできない gdbで起動 cd $HOME/demo/pgsql/bin gdb postgres breakpointの 設定 break main break ServerLoop break BackendStartup handle SIGUSR1 nostop noprint pass (*) handleコマンドでプロセス同士の通信用シグナル(SIGUSR1)を無視する設定 実行開始 run
  31. main()からデバッガで動作を観察 (5) OSSデータベースの内部構造を理解しよう 48 子プロセスで 処理を進める nextでchild_process_kinds[child_type].main_fnまで進める break launch_backend.c:290してcontinueでもよい print

    child_process_kinds[child_type].main_fn 実行継続 next, step, continueなどでSQL待ち受けまで進める 子プロセスで breakpoint等 を設定 set follow-fork-mode parent break PostgresMain break exec_simple_query fork時に親プロセスをdebugする設定 fork後に実行する関数にBackendMainが設定されている
  32. main()からデバッガで動作を観察 (6) OSSデータベースの内部構造を理解しよう 49 別端末のpsqlで SQL実行 select * from pg_class

    gdbで確認 exec_simple_queryで停止 exec_simple_queryで停止できれば、parser, analyze, planner, executorなどを観察できる
  33. 付録:デバッガ用の設定 OSSデータベースの内部構造を理解しよう • PostgreSQLのソースディレクトリで、.vscode/launch.jsonを 作成 { "version": "0.2.0", "configurations": [

    { "name": "Attach to PostgreSQL (WSL)", "type": "cppdbg", "request": "attach", "program": "/home/pgsql/pgsql/bin/postgres", // postgres のパスを設定 "processId": "${command:pickProcess}", "MIMode": "gdb", "miDebuggerPath": "/usr/bin/gdb", "setupCommands": [ { "text": "-enable-pretty-printing" } ] } ] } 52