PostgreSQLの内部構造
第2章ではPostgreSQLの内部構造を紐解きながら、基礎的な部分にフォーカスしていきます。
PostgreSQLの内部構造を知ることで、次章以降の事前知識にもなりますし、PostgreSQLの新機能がいかにして成長してきたかも見えてきます。
PostgreSQLのソースコードの場所
PostgreSQLのソースコードは全部で120万行以上(2018月11月27日現在)あります。これをすべて把握するのは難しいので、本章では重要なワードとその概要を押さえながら話を進めていきます。
もっと詳しく知りたいと思った人は、そのワードで公式ドキュメントを検索してください。それよりもさらに深く知りたいと思った人は、ソースコードリーディングをお勧めします。PostgreSQLのコード は、統率されたきれいなコードとなっています。C言語の初心者でも読みやすく、ミドルウェアのコードとしても学びが多いです。
プロセスの構造と役割
まずはプロセスについて説明します。
PostgreSQLは、マルチプロセスタイプのアーキテクチャです。よく比較対象に挙がるMySQLは、マルチスレッドタイプのアーキテクチャです。MySQLはコネクションが生成された場合、メインプロセスがスレッドを生成します。それに対してPostgreSQLはプロセスを生成します。
Linuxアーキテクチャに詳しい人からすると、PostgreSQLは無駄な実装に感じるかもしれません。たしかにスレッドとプロセスの生成ではスレッドのほうが軽量です。ですがRDBMSの性能はそこだけで決まるほど簡単なものではなく、たとえばロックの待ち時間に比べれば誤差と言えるでしょう。
このようにPostgreSQLはマルチプロセスですから、それぞれのプロセスを知ることは重要です。主なプロセスは表1 のとおりで、それらプロセスの流れは図1 のとおりです。図1に登場していない表1のプロセスは、スタートアッププロセスのように常駐しないものや、アーカイバやWALセンダのように設定した場合のみ登場するものです。
表1 主なプロセス
プロセス 内容
マスタプロセス 最初に起動される親プロセス
ライタ 共有バッファの内容をデータファイルに書き出す
WALライタ WALバッファの内容をWALファイルに書き出す
チェックポインタ すべてのダーティページをデータファイルに書き出す
自動VACUUMランチャ 設定にしたがって自動VACUUMワーカを起動する
自動VACUUMワーカ 自動VACUUMを実行する。複数起動することがある
統計情報コレクタ データベースの活動状況に関する統計情報を収集する
バックエンドプロセス クライアントの接続要求ごとに起動し、要求に対して処理する
スタートアッププロセス PostgreSQLの起動時などにクラッシュリカバリ処理を行う
ロガー PostgreSQLのログをファイルへ書き出す
アーカイバ WALログをアーカイブする
WALセンダ レプリケーション時にWALをスレーブサーバに転送する
WALレシーバ レプリケーション時にWALをマスタプロセスから受信する
バッググランドワーカ 汎用的なワーカ。バックグランドワーカをユーザーが定義した場合も起動する
ロジカルレプリケーションランチャ ロジカルレプリケーション時に受信側で1プロセスのみ起動するバックグランドワーカ
ロジカルレプリケーションワーカ ロジカルレプリケーション時に受信側で複数起動するバックグランドワーカ
パラレルワーカ パラレルスキャンを実行時に起動するバックグランドワーカ
図1 主なプロセスの流れ
PostgreSQLは、ライタがデータファイルやインデックスファイルをディスクに更新しています。ただし、その更新は、コミットに合わせてリアルタイムで行われているわけではありません。性能向上のため、チェックポイントと呼ばれる更新タイミングが発生するまでは、更新があっても共有バッファにデータを貯めておきます。この貯められたデータをダーティページと呼びます。そしてチェックポイントのタイミングで、チェックポインタがダーティページをディスクに書き込みます。
そのため、共有バッファに更新情報を貯めている間に障害が起きると、ダーティーページを失う可能性があります。それを防ぐために、共有バッファ中のデータに対してどのような更新を行ったかの情報を保存しているのがWALです。WALはコミットのタイミングでWALライタが記録しています。クラッシュリカバリが必要になったときは、WALの中にある記録を使用してデータ消失を防ぎます。共有バッファなどのメモリについては次節で説明します。
このようにPostgreSQLは、さまざまプロセスが役割分担をしながら処理しています。
メモリの構造と役割
RDBMSで重要なポイントと言えばメモリです。メモリには大きく分けて、共有メモリとプロセスメモリの2種類があります。
共有メモリは、すべてのプロセスから参照/更新される共有領域です。共有メモリは、PostgreSQLの起動時にOS経由で確保されます。
プロセスメモリは、バックエンドプロセスごとに確保される作業用領域です。メモリ領域を確保したプロセスのみが参照/更新できます。
共有メモリ、プロセスメモリは、それぞれいくつかの領域に分かれており、それぞれpostgresql.conf
の該当項目で設定します。
共有メモリ
共有メモリは、次の4つの領域に分かれています。
共有バッファ──プロセスが共通で使う
共有バッファは、テーブルやインデックスのデータをキャッシュする領域です。共有バッファはバックエンドプロセスや自動VACUUMワーカが参照/更新し、ライタがチェックポイントのタイミングで取り出してディスクに書き込みを行います。参照の際も、共有バッファにデータがある場合は高速に処理できます。後述する可視性マップと空き領域マップは、共有バッファを利用します。
共有バッファは、shared_buffersの項目で値を設定できます。デフォルト値は32MBです。割り当てとしては総メモリの25%程度を最初の目安にするのがよいでしょう。
可視性マップ──データファイルの参照可否を管理する
可視性マップは、テーブルのデータが参照できるか否かを管理する情報を扱う領域です。VACUUMの際に処理対象のページか判断する際に利用され、VACUUMの手助けをしています。PostgreSQL 9.2以降では、高速な検索方式のINDEX ONLY SCANの際にも利用されます。可視性マップは、VACUUM処理や各更新処理の際に更新されます。
空き領域マップ──データファイルの再利用を管理する
空き領域マップは、テーブル上の利用可能な領域を指し示す情報を扱う領域です。VACUUM処理の際にまったく参照されていない行を探し、空き領域として再利用できる状態にします。そのあと、追加や更新時に空き領域マップを探索し、空き領域を再利用します。
WALバッファ──WALで使う
WALバッファは、どのような更新を行ったかの記録であるWALをキャッシュする領域です。
WALバッファは、wal_buffersの項目で値を設定できます。PostgreSQL 9.1以降では自動調整されるため、デフォルト値のままで問題ありません。
プロセスメモリ
プロセスメモリは、次の3つの領域に分かれています。また、プロセスからメモリを経由してデータファイルを更新する流れが図2 となります。
図2 メモリの利用からデータファイルの更新までの流れ
作業メモリ──クエリ実行時の使う
作業メモリは、クエリの実行時にソートやハッシュテーブルの利用のために使われる領域です。
作業メモリは、work_memの項目で値を設定できます。大きなデータを扱うクエリが遅い場合、work_memの値を増やすと改善することがあります。ただし、work_memはバックエンドプロセスごとに確保されるため、work_memに大きな値を設定している場合に接続数が多いと、OSのメモリを圧迫することがあります。
また、サービス開始当初はwork_memの値が小さくても取り扱うデータも小さいため問題にならなかったものが、サービスの成長とともに取り扱うデータサイズが大きくなり、ソートの際にメモリからあふれることがあります。その際はスワップが発生するので、処理速度が突然遅くなります。
このようなことを防ぐため、work_memの値は適切にチューニングする必要がありますし、スワップを監視する必要があります。work_memの最大値は
(OSの実メモリ - shared_buffers) / max_connections
ですので、これを超えないように適切な値を設定しましょう。最初から大きな値を設定するよりも、最初は32MB程度を設定し、スワップを監視しながら適宜調整するのがよいです。もちろん、もっと小さな値から初めても問題ありません。
メンテナンス用作業メモリ──メンテナンス時に使う
メンテナンス用作業メモリは、VACUUM、インデックス作成、外部キー制約の追加などのデータベースに対するメンテナンス時に利用する領域です。
メンテナンス用作業メモリは、maintenance_work_memの項目で値を設定できます。たとえばサービスのメンテナンスやDB移行時などのメンテナンス作業でALTERを同時に複数回実行した際に、maintenance_work_memがデフォルトのままだと時間がかかることがあります。この作業でスワップが発生している場合は、maintenance_work_memを大きな値に変更すると改善します。ただし、値を大きくし過ぎるとOSのメモリを圧迫しますので、最初から大きい値を設定しないようにしましょう。
一時バッファ──一時テーブル領域の作成で使う
一時テーブルはCREATE TEMP TABLE文を実行すると作成されますが、その際に利用される領域が一時バッファです。
一時バッファは、temp_buffersの項目で値を設定できます。CREATE TEMP TABLE文を実行した際にスワップが発生するようであれば、値を増やしましょう。
PostgreSQLのファイルの役割
PostgreSQLのファイルの役割を知ることは、PostageSQL の特徴をつかむうえで重要です。PostgreSQLのファイルは、基本的にbaseとglobalディレクトリ配下に保存されます。それぞれのファイルはオブジェクト識別子(OID)という番号によって管理され、base/1/1247
のようなファイル名のみで判断できない形で保存されています。ただし、後述するWALファイルは、pg_walディレクトリ配下に保存されます。
PostgreSQLのファイルの中で、次の4つが重要です。
データファイル──テーブルの保存場所
データファイルは、テーブルのデータの実体が保存されるファイルです。データファイルは複数の8,192バイトのページ(Oracle DBで言うところのブロック)によって構成されます。
インデックスファイル──インデックスの保存場所
インデックスファイルは、インデックス情報が保存されるファイルです。データファイルと同様に複数の8,192バイトのページによって構成されます。
WALファイル──WALの保存場所
WALファイルは、WALの保存先です。データベースの永続性の保証を行うために重要なファイルで、16MBの固定サイズで作成されます。
TOASTファイル──大きなデータの保存場所
前述したように通常1つのページは8,192バイトです。TOASTファイルは、その1/4の約2KBを超えるサイズの行データを圧縮、格納する場合に作成されます。
TOASTは、長大な行データを保存するためのしくみです。PostgreSQLは複数ページにまたがるタプル(行データ)を許しません。そのため、ページの最大値である8KBを超えるデータを保存できません。そこで、TOASTが圧縮したり、分割したりして別の場所に保存してくれるのです。
ただし、TOASTも最大値が決まっており、1GBまでです。そのため、TOASTが利用できる型、たとえばTEXT型を利用していた場合に保存できる最大サイズは1GBです。
PostgreSQLの特徴的な機能のしくみ
ここからは、PostgreSQLの特徴的な機能のしくみを説明します。どれもPostageSQLを語るうえでは外せない機能ばかりです。
追記型アーキテクチャ──データ保存のしくみ
PostgreSQLがほかのデータベースと大きく違うところに、追記型アーキテクチャの採用があります。
追記型アーキテクチャとは、図3 上部のUPDATE文のように、更新の際に直接データファイルは更新せず、参照を外して別のデータを挿入するアーキテクチャです。これにより、MVCCと呼ばれるロックの制御やトランザクションの制御がシンプルに実装されます。
図3 追記型とVACUUMのアーキテクチャ
もちろんデメリットもあります。たとえば、更新の際に参照を外す処理と挿入の処理の2つが必要なので処理速度が遅いのではないか、参照されなくなったファイルはどうするのか、などです。
以降では、これらを解決する機能を紹介します。
HOT──更新の性能向上のキーマン
更新の際に処理が遅い問題の解決として、HOT(Heap Only Tuple )があります。HOTは更新の際にインデックスの更新量を大きく抑え、性能向上を行うしくみです。HOTについては、Let's Postgresの記事 に詳しく書いてあります。このようにPostgreSQLは、追記型のメリットを活かしながらデメリットに対しても改善を行っています。
VACUUM──データファイルの掃除屋
追記型アーキテクチャによって参照されなくなったデータファイルを、再度データを挿入できるように再利用可能状態にする処理がVACUUMです。ガベージコレクションのような処理と言うとわかりやすいでしょうか。VACUUMについては、「 PostgreSQL Conference Japan 2018【T4】今、改めて学ぶVACUUM — 佐藤 友章 」の動画が参考になります。
VACUUMは、定期的に自動VACUUMランチャから自動VACUUMワーカが起動され、図3下部のとおり処理します。デフォルトで自動VACUUMが実行されるので、ユーザーがVACUUMを意識する必要はほとんどありません。ときどき、過去のPostgreSQLの知識でVACUUMによる性能遅延が激しいためVACUUMを停止するという話を見かけますが、これは現在では誤ったテクニックです。
VACUUMの注意点
ただし、ロングトランザクションやインデックスを更新するような処理をかなりの高頻度で行うような場合は自動VACUUMが実行されなかったり、逆に高頻度でVACUUMが実行されて性能遅延することがあります。これらの場合のみ、ユーザーが任意のタイミングで計画的に実行する必要があります。VACUUMの処理を十分に理解したうえで適切なスケジュールを組みましょう。
そのほかの注意点として、VACUUMは更新や削除によって生まれた参照されないデータを再利用可能にするだけですので、一度大きくなったデータファイルが小さくなることはありせん。たとえば大きなデータファイルの1,000万件のレコードを削除し、VACUUMが実行された場合、1,000万件分再利用できるようになるだけです。対象のデータファイルが1,000万件相当のデータ量分、小さくなるわけではありません。
データファイルを小さくしたい場合は、VACUUM FULL
またはCLUSTER
コマンドを実行する必要があります。そして多くの場合、VACUUM FULL
よりもCLUSTER
コマンドを使うほうが適切です。CLUSTER
コマンドは一時的に対象のテーブルと同程度の空き領域がディスクに必要となるものの、テーブル再編成の処理が高速に行え、サービスへの影響を極小化できます。
参照も更新も邪魔をせずに同様のメンテナンスを行えるツールとして、PostgreSQLの拡張ツールのpg_repack があります。pg_repackはもともと同様の意図で開発されていたpg_reorgの後継ツールです。メンテナンスなどで大量のデータを削除した場合などは、pg_repackの利用を検討してください。日本語ドキュメント も公式で用意されています。
VACUUMのそのほかの大事な役割
VACUUMのそのほかの重要な役割として、不要になったトランザクションID(以下、XID)の回収と統計情報の更新があります。
統計情報の更新についてはご存じの人も多いと思いますので割愛します。
XIDの回収についてですが、PostgreSQLはテーブルやレコードなどをそれぞれ一つ一つのオブジェクトとして扱っており、それぞれにIDを割り振っています。これがXIDです。XIDには32ビットの符号なし整数の制約があり、約42億(2^32-1)でXIDは周回します。実際には約42億の半分の値である20億を再利用しており、それ超えるようなトランザクションが発生した場合、XIDを使い切り、XID周回問題が発生します。
XID周回問題は、残り1,000万トランザクションで警告され、残り100万トランザクションから新しいトランザクションはエラーになるため、基本的には発生しません。ですが、発生した場合は古いXIDが割り当てられていたレコードが突然参照できなくなるなどの問題が発生します。これを防ぐために、VACUUMで利用していないXIDを回収し、再利用できるようにしています。
可視性マップ──追記型アーキテクチャの問題改善
VACUUMは参照されていないデータファイルを再利用できるように処理するしくみと説明しました。それでは、VACUUMは参照されていないデータファイルをどうやって探しているのでしょうか。
そのために利用するのが、前述した共有メモリの可視性マップ(Visibility Map )です。可視性マップは、ページとして対象のページが参照できるかどうかのフラグをまとめたものです。未更新ですべて見える状態(all-visible)のときには1、更新されて見えないデータがある場合には0が設定されます。all-visibleなページはVACUUMする必要がないため、VACUUMがスキップされます[1] 。つまり、すべてのページがallvisibleな場合、VACUUMはデータファイルの更新をすべてスキップし、すぐさま終了します。
逆に可視性マップのすべてが0の場合、VACUUMはすべてのデータを更新する必要があるため、対象のテーブルをテーブルフルスキャンで処理します。
このように、PostgreSQLはVACUUMの処理改善のためのしくみを用意しています。
INDEX ONLY SCAN──最速の実行計画
可視性マップは、VACUUMの手助けのほかにも大事な役割があります。それがINDEX ONLY SCANのサポートです。INDEX ONLY SCANはとても高速に参照できるため、PostgreSQLを使ううえでぜひ覚えておきたい機能です。
INDEX ONLY SCANのしくみ
INDEX ONLY SCANはその名のとおり、インデックスのみを利用して検索結果を返すしくみです。
通常、インデックスを利用する際は、次の流れで検索結果を返します。
インデックスから該当のレコードを把握する
インデックスをもとに該当のレコードにアクセスし、データを取得する
検索結果を返す
INDEX ONLY SCANの場合、2.を省略して検索結果を返すことができます。手順が1つ減るわけですから、高速に返せるのは当然ですね。
INDEX ONLY SCANが使われる定番クエリに、SELECT COUNT(*) FROM table_name;
があります。これは、INDEX ONLY SCANが使える状態であれば主キーだけで検索結果を作ることができます。基本的にCOUNT()
は遅くなりやすい処理ですから、INDEXONLY SCANのことを覚えておいて損はありません。
ほかにも、SELECT indexed_col FROM table_name WHERE indexed_col in (1, 2, 3);
のように検索条件と検索結果、ともにインデックスのみで返せる場合はINDEX ONLY SCAN が使われます。さらにINCLUDE句を利用すると、インデックスに参照用の列を指定できます。
INCLUDE句はMySQLのカバリングインデックスの振る舞いとよく似ており、インデックスのみで検索、データの取得が達成できる場合はとても高速に結果を返せます。
INDEX ONLY SCANの制約
良いことずくめのINDEX ONLY SCANですが、利用するための制約があります。INDEX ONLY SCANを利用するためには、インデックスの情報が最新である必要があります。
実は、PostgreSQLはインデックスには更新情報の有無自体は持っていません。そのため、可視性マップをもとに更新情報の有無を判断しています。可視性マップを見れば、テーブルがall-visibleかどうかを判断できます。テーブルがすべてall-visibleな場合、更新されていないためインデックスの中の情報はどのトランザクションから見ても最新と言えます。つまり、allvisibleであればインデックスの値を返しても問題ないため、INDEX ONLY SCANを利用できます。
ここまで読んでお気付きかもしれませんが、INDEX ONLY SCANを利用するためには、VACUUMが実行されてから対象のテーブルに更新、削除、追加が実行されていないことが必須です。この制約は多くのテーブルでは実現が難しいものです。しかし、INDEX ONLY SCANができる場合の速度は大きなメリットがあるため、更新がされにくい都道府県マスタや更新頻度が少ないテーブルでは絶大な効果を発揮します。物理設計時にINDEX ONLY SCANの選択肢も覚えておくと、設計の選択肢が広がることでしょう。
まとめ
PostgreSQLの基本を知るというテーマで、内部構造を中心に紹介しました。PostgreSQLの内部構造はとても奥が深く、学び甲斐のあるミドルウェアです。
PostgreSQLの内部構造は日本語ドキュメントも豊富で、初学者から上級者まで全員に優しいことも特徴です。これを機に本格的に学びたい人に、PostgreSQL 10ベースですが『これからはじめる PostgreSQL入門 』( 注2 )と『[改訂新版]内部構造から学ぶPostgreSQL 設計・運用計画の鉄則 』( 注3 )がお勧めです。
特集1
イミュータブルデータモデルで始める
実践データモデリング
業務の複雑さをシンプルに表現!
特集2
いまはじめるFlutter
iOS/Android両対応アプリを開発してみよう
特集3
作って学ぶWeb3
ブロックチェーン、スマートコントラクト、NFT