忍者ブログ
IT関係の小作人労働の日々の日記です。 最近データベースが好きです。 インフラ構築、DB構築、アプリケーション開発・・・何でも屋です。 何でもできそうで、何にもできない。

【db.c】MacとC言語でデータベースをゼロからビルドしてみた完全実録


データベースの内部構造を理解するために、SQLiteの構造をモデルにした「db.c」をMac(Apple Silicon)環境で動かしてみました。環境構築から、誰もが抱く「あの疑問」の解決まで、省略なしの全行程を記録します。

1. 開発環境の準備:Xcode Command Line Tools

【 現場の感触 】 MacでC言語をコンパイルするには、Apple純正のコンパイラ(Clang)が必要です。まずはターミナルから、開発の「儀式」とも言えるツールのインストールから始めます。

# ターミナル(Terminal.app)を起動し、以下を入力
xcode-select --install

※すでにインストール済みの場合は「already installed」と表示されます。未導入ならポップアップに従ってインストールを完了させてください。

2. ソースコードの取得とディレクトリ作成

【 現場の感触 】 作業用フォルダを作成し、GitHubから最新の db.c をダウンロードします。curlコマンドを使って、GitHubのマスターリポジトリから直接保存します。

# 作業フォルダの作成と移動
mkdir ~/db-study && cd ~/db-study

# ソースコードのダウンロード
curl -O https://raw.githubusercontent.com/cstack/db_tutorial/master/db.c

3. コンパイルの実行

【 現場の感触 】 Mac標準のコンパイラ(Clang)を使用して、ソースコードを実行可能なバイナリファイルに変換します。エラーが出なければ、同じフォルダ内に「db」という実行ファイルが作成されます。

# clangでビルド。-o は出力ファイル名の指定。
clang db.c -o db

# 実行ファイルができたか確認(lsコマンド)
ls -F
→ db* と表示されれば準備完了です。

4. データベースの起動と操作(実践)

【 現場の感触 】 実際にプログラムを動かし、データを入力してみます。ここで驚くのが、「CREATE TABLE」を一度も叩かずに insert ができてしまうことです。

# 起動。引数にデータベースファイル名(mydb.db)を指定。
./db mydb.db

db > insert 1 user1 person1@example.com
Executed.
db > insert 2 user2 person2@example.com
Executed.

db > select
(1, user1, person1@example.com)
(2, user2, person2@example.com)

db > .exit

5. 【重要解説】なぜ「表」を作らなくても入るのか?

【 仕組み 】 通常のDBではありえないこの挙動の理由は、ソースコードの中にあります。この `db.c` は、「ID、Username、Email」という3つの列を持つ1つのテーブルしか扱えないように、最初から「構造」がプログラムに直接書き込まれている(ハードコードされている)からです。

スキーマとプログラムの合体
・通常の汎用DB:何が来るか不明なため、まず「表の定義(メタデータ)」を作る必要がある。
・自作 db.c:最初から「ID(4byte)、名前(32byte)、メルアド(255byte)」という1行291byteの固定データが来ると知っている。

メリット
1行のサイズが常に一定(固定長)なので、n行目を探すには「291 × n」バイト目を見に行くだけで済みます。この割り切りが、自作DBの学習をシンプルにしています。

6. 本質的な「検証」:データの永続性確認

【 現場の感触 】 最後に、プロセスを終了してもデータが消えないことを確認します。これこそが「データベースを極める」ための第一歩です。

[ 再起動テスト ]
① 再度実行: ./db mydb.db
② データの確認: select と入力

[ 結果 ]
先ほど入れたデータがそのまま表示されれば、「ファイルへのページ書き出し(Persistence)」の仕組みが正常に動いている証拠です。

7. まとめ:やってみて分かったこと

「CREATE TABLEがいらない」のではなく、「最初から1つだけCREATE済みの表がある状態で起動している」という設計。この仕組みを理解することで、DBの心臓部である「ファイルI/O」や「ページ管理」がいかに動いているか、その一端を鮮明に体感することができました。


巨大なRDBMSも、突き詰めればこうした「ファイルの読み書き」の積み重ね。自作してみることで、トラブル時にも「今はディスクと対話中かな?」といった具体的な想像力が働くようになります。


PR

【データベースの知識】SQLパフォーマンスチューニングの即効ヒント


同じ結果を得るSQLでも、書き方ひとつでデータベース内部の「仕事量」は劇的に変わります。今回は、明日からの開発でそのまま使える、実行速度を劇的に改善するための6つのTipsを紹介します。

1. 実行効率を高める書き方の基本

【 基本 】 データベースエンジンにとって、無駄なスキャンや重複排除(ソート)は最大の敵です。まずは、より効率的な演算子や関数を選ぶことから始めましょう。

COUNTよりEXISTS:1件見つかった瞬間に検索を終了するため、全件カウントするより圧倒的に高速です。
UNIONよりUNION ALL:UNIONは重複を消すために内部で「ソート」が発生します。重複がないと分かっているならALL一択です。
ORよりIN:ORを多用するとインデックスが効かなくなる場合があります。複数の値を指定するならIN演算子の方が最適化されやすいです。

2. インデックスとデータアクセスの最適化

【 ポイント 】 インデックスはただ作るだけでなく、「使われるように書く」のがコツです。また、読み込むデータの範囲をいかに絞り込むかが勝負を分けます。

インデックスの作成:WHERE句やJOINの結合キーには必ずインデックスを検討しましょう。
COUNT(*)より主キー参照:製品によっては、主キーを指定することでインデックススキャンのみで処理が完結し、高速化する場合があります。
範囲指定はBETWEEN:`col > 10 AND col < 20` と書くより、`BETWEEN` を使う方がオプティマイザが範囲を正しく認識しやすく、読みやすいSQLになります。

3. まとめ:チューニングの優先順位

闇雲に修正するのではなく、まずは以下の表を参考に、コストの高い部分から手を入れていきましょう。

改善項目修正前推奨(修正後)効果の理由
存在確認 COUNT(*) > 0 EXISTS 見つかったら即終了
集合演算 UNION UNION ALL ソート処理をスキップ
複数指定 OR ... OR ... IN ( ... ) 可読性と解析効率の向上


パフォーマンスチューニングに「銀の弾丸」はありませんが、これらの定石を組み合わせることで、システムのレスポンスは確実に見違えるはずです。

【PostgreSQL】論理レプリケーションを自前で動かしてみた実録


実際に手を動かすのが一番。ということで、PostgreSQLの論理レプリケーションをローカル環境で構築してみました。パブリッシャー(出す側)とサブスクライバー(受ける側)のやり取りを、実際にコマンドを叩きながら追いかけます。

1. 設定の落とし穴:wal_levelの変更

【 現場の感触 】 最初のハードルは設定変更です。デフォルトでは論理レプリケーション用のログが出ないので、`postgresql.conf` を書き換えます。再起動が必要なので、本番環境なら「ちょっと待って」となるところですね。

# wal_level を logical にして再起動!
show wal_level;

[ 結果 ]
wal_level
-----------
logical ← これで準備完了。

2. パブリケーションの作成と権限の「儀式」

【 現場の感触 】 テーブルを作ってデータを放り込みます。ここで大事なのは「主キー(PK)」があること。論理レプリケーションでは「どの行を更新するか」を特定するためにPKが必須です。あとは接続用の専用ユーザーを作って、権限を付与します。この「運び屋」を作る作業がレプリケーションっぽさを感じさせます。

-- 全テーブルを対象にパブリケーション作成
CREATE PUBLICATION pub FOR ALL TABLES;

-- レプリケーション専用の「運び屋」ユーザーを作る
CREATE ROLE repluser LOGIN REPLICATION PASSWORD 'repluser';
GRANT pg_read_all_data TO repluser;

3. 同期開始!サブスクリプションの威力

【 現場の感触 】 サブスクライバー側で接続情報を指定してサブスクリプションを作成。実行した瞬間に、既存のデータが「バッ」と流れてくるのは見ていて気持ちいいものです。試しにデータを1行追加すると、即座に反映されるのが確認できました。まさにリアルタイム!

-- サブスクライバー側で実行(ここで同期が始まる)
CREATE SUBSCRIPTION sub CONNECTION 'host=... user=repluser...' PUBLICATION pub;

-- パブリッシャー側で insert
insert into sample_table values(3, 'ccc');

-- サブスクライバー側で確認
select * from sample_table; → ちゃんと 3 | ccc が反映されている!

4. 応用:同期を止めても「裏で溜まっている」

【 現場の感触 】 運用でよくある「一時停止(DISABLE)」も試しました。停止中にパブリッシャー側でガシガシ更新('ddd'を追加)しても、サブスクライバー側は静かなまま。でも、再度「ENABLE」にした瞬間、溜まっていた更新が追い付いてくる。この確実性が論理レプリの頼もしいところです。

検証のまとめ
1. DISABLEにすると、サブスクライバー側はピタッと止まる。
2. パブリッシャー側の変更は破棄されず、裏(スロット)に保持される。
3. ENABLEに戻すと、未反映分が高速に流し込まれる。


実際にやってみると、コマンド一発で同期が制御できる手軽さと、内部でWALがしっかり管理されている安心感がよく分かりました。バージョン間のデータ移行や、特定のデータ集約には最高に便利そうです。

【データベースの知識】SQLが実行されるまでの内部ステップ


私たちがSQLを1行実行したとき、データベースの内部では瞬時にいくつかの工程が実行されています。この「実行ステップ」を知ることで、なぜ同じ結果を返すクエリでも速度が変わるのか、その理由が見えてきます。

1. SQL実行の4つの主要ステップ

【 基本 】 送信されたSQL文は、データベースエンジンによって以下の順序で処理されます。特に「実行計画」の作成は、クエリの成否を分ける心臓部です。

[ 処理の流れ ]
1. 構文解析(Parsing):SQLの文法に誤りがないかチェックし、意味を解析します。
2. 実行計画の作成(Optimization):インデックスを使うか、どの結合法を選ぶかなど、最も効率的なルートを計画します。
3. コンパイル:作成された計画を、データベースが実行可能な形式に変換します。
4. SQLの実行:実際にデータへアクセスし、結果を返します。

2. 各フェーズで何が行われているか

各ステップの詳細を見てみましょう。エンジニアが意識すべきは、特に「2」のプロセスです。

構文解析の壁:キーワードの綴りミスや、存在しないテーブルの指定などはここでエラーとなります。
オプティマイザの判断:統計情報を元に「実行計画(実行の設計図)」を作ります。ここでの判断ミスがスロークエリの原因になります。
キャッシュの活用:一度解析したSQLは「共有プール」に保存され、2回目以降はステップ1〜3を省略して高速化(ソフトパース)される仕組みがあります。

3. まとめ:実行ステップを意識するメリット

「なぜバインド変数を使うべきなのか」「なぜ統計情報の更新が必要なのか」といった疑問の答えは、すべてこの実行ステップの中にあります。

効率化のコツ
・バインド変数を使うことで、再解析(ハードパース)のコストを減らせる。
・最新の統計情報を保つことで、最適な実行計画を立てさせることができる。
・複雑なクエリは、実行計画を確認(EXPLAIN)してボトルネックを特定する。


SQLをただの「命令文」としてではなく、DB内部の「プロセス」として捉えることが、脱・初心者への近道です。


【データベースの知識】結合(JOIN)アルゴリズムの選択と最適化


データベースを扱う上で避けて通れないのが「テーブル結合(JOIN)」です。SQLを書けば結果は同じに見えますが、内部のアルゴリズム選択ひとつで、処理速度は100倍、1000倍と劇的に変わります。今回は実務で役立つ3つの主要アルゴリズムを解説します。

1. 主要な3つの結合アルゴリズム

【 基本 】 データベースが2つのテーブルを繋ぐ際、主に以下の3つの手法から最適なものを選択します。それぞれ「得意なデータ量」と「リソースの使い方」に明確な違いがあります。

Nested Loop Join(入れ子ループ法)
外側の表から1行ずつ取り出し、内側の表を走査して一致を探します。少量のデータに強く、インデックスの活用が前提となります。
Sort Merge Join(ソートマージ法)
両方の表を結合キーでソートし、端から順に突き合わせます。大量データに強く、不等号結合(<, >)でも利用可能です。
Hash Join(ハッシュ結合)
片方の表からメモリ上にハッシュテーブルを作り、もう片方と突き合わせます。大量データ同士の等価結合(=)において非常に強力です。

2. 特徴比較表:データ量とインデックス

【 ポイント 】 実行計画を確認する際、現在のデータ量に対して適切な方法が選ばれているかを判断する基準を持っておくことが重要です。

アルゴリズム得意なデータ量インデックス備考
Nested Loop 1件〜少量 必須 オンライン処理の基本
Sort Merge 大量データ あれば尚可 ソートのCPU負荷あり
Hash Join 大量データ 不要 メモリ(ワークエリア)を消費

3. パフォーマンス改善:DBへの介入術

通常、DBは統計情報を元に自動で判断しますが、判断を誤ることもあります。その際、エンジニアが特定のアルゴリズムを強制・誘導する方法は製品ごとに異なります。

[ 製品別の介入方法 ]
Oracle(ヒント句):/*+ USE_NL(a b) */ のように、SQL内に直接指示を書き込みます。
PostgreSQL(パラメータ):SET enable_mergejoin = off; 等で特定の機能を無効化し誘導します。
MySQL(インデックスヒント):USE INDEX を指定することで、間接的にNLJなどへ誘導します。


結合アルゴリズムの仕組みを知ることは、スロークエリの根本原因を特定し、最適なパフォーマンスを引き出す第一歩となります。