ラージオブジェクト

Insideラージオブジェクト

SRA OSS, Inc. 日本支社
石井 達夫

ラージオブジェクトとは

たいていのDBにはBlob(Binary large object)などと呼ばれる、巨大オブジェクトを扱う仕組みがあります。これを使うと、画像や動画、あるいは巨大なテキストなどをDBで扱うことができるようになります。PostgreSQLの場合には、「ラージオブジェクト」(Large Object)と呼ばれており、PostgreSQLのラージオブジェクトは、今のところデフォルトで2GBまで、9.3以降では4TBまでを扱えるようになります(この改良は、安齋氏、長田氏、それに私によって行われました)。

普通のファイルに画像や動画をおいて管理する方法もありますが、ラージオブジェクトを使うメリットとしては、

  • PostgreSQLがアクセス手段を提供してくれるので、リモートにあるオブジェクトをアクセスする方法をアプリケーションが考えなくて済む
  • 普通のテーブルのデータと同じようにトランザクションの一部として扱われ、データの保全やバックアップについて特別な考慮が不要になる
  • データが自動圧縮される

などがあります。一方で、どうしてもデータベースサイズが巨大になるため、autovacuum の設定や、バックアップでそれなりの考慮が必要になる場合があります。

ラージオブジェクトの管理

pg_largeobject
図1 ラージオブジェクト1、2が
登録された pg_largeobject

ラージオブジェクトは、 pg_largeobject という特別なシステムカタログに格納されます。pg_largeobject は一つのデータベースに1個だけ存在し、すべてのラージオブジェクトはこのテーブルに格納されます。ラージオブジェクトが登録されると、ラージオブジェクトIDという番号が割り振られます。ラージオブジェクトIDは、通常OID(PostgreSQLが自動発番するオブジェクト識別用のユニークID)が割り当てられますが、ユーザが独自に管理する数字のIDを振ることも可能です。ラージオブジェクトは一個が2KB固定サイズの「ページ」に分割され、それぞれのページにはそのラージオブジェクト内で通番となるページ番号が割り振られます。ラージオブジェクトのデータ自体は、byteaとして格納されます。

PostgreSQLではすべてのテーブルがデフォルトで8KB固定サイズのブロックに分けて管理されます。したがって、一つのブロックには最大4つのページを格納することができます。ラージオブジェクトが一つのブロックに入りきらない場合は複数のブロックにまたがって登録されます。

図1は、5つのページからなる「ラージオブジェクト1」と、2つのページからなる「ラージオブジェクト2」が pg_largeobject に登録された様子を示します。

pg_largeobject にアクセスしてみる

では、実際にラージオブジェクトを登録し、 pg_largeobject にデータが登録されているかどうか確認しましょう。登録するのは、この原稿のテキストファイルです。

$ ls -l
:
:
-rw-r--r-- 1 t-ishii t-ishii  2934 12月 11 14:08 Lobj.txt

と、2934バイトのファイルなので、2ページで収まるはずです。

一方、 pg_largeobject ですが、以下のような構造になっています。

test=# \d pg_largeobject
Table "pg_catalog.pg_largeobject"
 Column |  Type   | Modifiers
--------+---------+-----------
 loid   | oid     | not null
 pageno | integer | not null
 data   | bytea   |
Indexes:
    "pg_largeobject_loid_pn_index" UNIQUE, btree (loid, pageno)

「loid」はラージオブジェクトID、「pageno」はページ番号です。「data」はラージオブジェクトのデータそのもの。では早速データを登録してloidとpagenoを見てみましょう。

test=# \lo_import 'Lobj.txt'
lo_import 1252797

test=# select loid,pageno from pg_largeobject where loid = 1252797;
  loid   | pageno
---------+--------
 1252797 |      0
 1252797 |      1
(2 rows)

確かに2ページに渡ってラージオブジェクトが登録されていますね。

ラージオブジェクトへのアクセス

以上でラージオブジェクトの格納法の概要がわかったと思います。原理的には、ラージオブジェクトIDが分かれば、byteaとして格納されているデータ本体を取り出すことが可能です。しかしこれではあまりに不便なので、C言語API(libpq)やJava用のラージオブジェクトへの標準的なアクセス方法が提供されています。

今回は更にその奥にあるプロトコルレベルのアクセスに踏み込んでみましょう。実はラージオブジェクトは、ここもかなり独特なのです。

PostgreSQLでは、ネットワークを通じてPostgreSQLにアクセスするためのプロトコルが定義されており、「フロントエンド/バックエンドプロトコル」と呼ばれています。たとえば、SELECT文で検索を行う際は、検索要求を表す'Q'に続き、データ長、SELECT文を送信する、という約束になっています(詳細は、PostgreSQLマニュアルの「フロントエンド/バックエンドプロトコル」をご覧ください)。

ラージオブジェクトには実は「ラージオブジェクトをアクセスする」という直接的なプロトコルは用意されておらず、「関数呼び出し」(Function Call)というプロトコルを代わりに使います。関数呼び出しプロトコルは、その名の通り、PostgreSQL上の関数を直接呼び出す仕組みで、PostgreSQL用のRPC(Remote Procedure Call)のようなものです。私の知る限りでは、ラージオブジェクト以外では使われておらず、一般にはほとんど知られていない機能だと思います。何となく危険な匂いのする機能ですが:-)、実際PostgreSQLの実装を良く知らないと使うことができないものです。

関数呼び出しの方法

関数呼び出しで呼び出す対象の関数はOIDで指定します(PostgreSQLでは、すべての関数はOIDで一意に識別できるようになっています)。また、引数の数、大きさも指定します。関数を呼び出すと、結果は別に定義されている「関数呼び出し結果」(Function Call Response)で受け取ることができます。

なお、引数や返り値の受け渡しに於いては、データの形式にはこのプロトコルはまったく感知しません(テキストかバイナリの区別があるだけ)。ネットワークバイトオーダーを含めて、バックエンドの関数がどのよう引数を解釈するのか、返り値を返すのかを呼び出す側が完全に理解しておかなければなりません。

ラージオブジェクトへのアクセス手順

関数呼び出しを使ってラージオブジェクトにアクセスする手順は以下のようになります。

  1. ラージオブジェクトのアクセスを請け負う組み込み関数用意されているので、そのOIDをシステムカタログ pg_proc を検索して知る
  2. OIDを指定してラージオブジェクト用の関数を呼び出す

用意されているラージオブジェクト用の関数は以下のものがあります。

関数名 機能
lo_open ラージオブジェクトを開く
lo_close ラージオブジェクトを閉じる
lo_read ラージオブジェクトを読み出す
lo_write ラージオブジェクトに書き込む
lo_seek ラージオブジェクトのオフセットを移動する
lo_seek64 lo_seekの64bit版(PostgreSQL 9.3以降)
lo_creat ラージオブジェクトを作る
lo_create ラージオブジェクトを作る(任意のOIDが指定可能)
lo_tell 現在のラージオブジェクトのオフセットを得る
lo_tell64 lo_tellの64bit版(PostgreSQL 9.3以降)
lo_unlink ラージオブジェクトを消去する

libpqやJDBCドライバでは、最初にこれらの関数のOIDを pg_proc から取得しています。APIにOIDを埋め込んでしまうのではなく、都度PostgreSQLに問い合わせることによって、PostgreSQL側に変更があって対応できるようになっているわけです。後は、ラージオブジェクトのAPIに対応したこれらの関数を、関数呼び出しプロトコル経由で呼び出すだけです。

ソースコードはどこに?

細かな実装に興味のある方のために、ラージオブジェクトのソースコードの構造について簡単にお話しておきましょう。

まず、フロントエンド側のlibpqのラージオブジェクトのコードは、src/interfaces/libpq/fe-lobj.c にあります。一方、バックエンドの方は、src/backend/libpq/be-fsstubs.c にあります。このファイルに上記の関数が定義されています。これらの関数は実は処理の入り口を受け持つだけで、処理の実体は、src/backend/storage/large_object/inv_api.c にあります。

libpqにおける関数呼び出し

libpqには、関数呼び出しを行うAPIが用意されています。PQFnという関数で、「近道」(fast-path) インタフェースと呼ばれます。実際、libpqのラージオブジェクトインターフェイスは、PQfnを呼び出すことで実装されています。

最後に

普段あまり知られることない、ラージオブジェクトの実装について説明しました。PostgreSQLのラージオブジェクトの実装はなかなか良く出来ていますが、一方で冒頭に書いたような課題もあります。この課題の解決方法として、pg_largeobject をパーティションニングできるようにするなどのアイデアもあります。この記事が、そうしたアイデアの実現に少しでもつながればと思います。


(2012年12月14日 公開)