🛞

PostgreSQLがドキュメントDBに?Microsoft DocumentDBを動かしてみる

2025/02/24に公開

今回発表されたDocumentDBとは

DocumentDBは2025年1月にMicrosoftからオープンソースとして発表されたデータベースで、いわゆるNoSQLのワークロードを対象としながらも、以下のような興味深い特徴を持っています。

  • PostgreSQL上に構築されたドキュメント型のデータベース
  • Azure Cosmos DB for MongoDBのエンジンとして使われている
  • 今回の発表ではpg_documentdb_coreとpg_documentdbという2つのモジュールが公開された

リレーショナルデータベースと異なり、NoSQL(ここではドキュメント型)では事実上の標準としてMongoDBがありますが、彼らは2018年にSSPLという新たなライセンスを発表し、オープンソース界隈で大きな議論を巻き起こしました。

そうした動きに対して、今回のDocumentDBではNoSQLの標準となることを目標にオープンソース(MITライセンス)で公開されています。

後に出てくるように、現状のDocumentDBだけでMongoDBのワークロードを置き換えることはできませんが、他のOSSと連携したり、Cosmos DB for MongoDBの内部コンポーネントを更に公開するなどで対応が進むことが予想されます。

ネーミングの問題

今回発表されたのは、MicrosoftのOSSとしてのDocumentDBです。

しかし、検索すればすぐ分かる通り、AWSにもAmazon DocumentDBが存在します。こちらは2019年に発表されたMongoDB互換のドキュメント型データベースです。こちらはAWSだけで利用できるマネージドサービスで、ソースも公開されていません。

なぜ後発のMicrosoftが同じ名前にしたのかは理解に苦しむ所ですが、好意的に解釈すると、「NoSQLの標準」としてこのネーミングは譲れなかったということなのかも知れません。

DocumentDBを動かしてみる

ここからはDocumentDBを実機で動かしていきます。

ただ、上にあるようにDocumentDBの名前だけだと何を指すか分からないため、以下では適宜pg_documentdbという名称を使います。

DocumentDBのアナウンス時にあったコンポーネント名は、 pg_documentdb_corepg_documentdb_api の2つです。しかし、現在github上で記載されているPostgreSQLの拡張は、 pg_documentdb_corepg_documentdb です。

名称は今後も変わる可能性があり、モジュールも追加が予想されるため、DocumentDBに関わるコンポーネントをまとめたもの、拡張の総称としてpg_documentdbと呼ぶことにします。

インストール

DocumentDBの公式リポジトリで紹介されているのは、コンテナ内でのモジュールをビルドする方法です。もちろんこれでも良いのですが、ビルドにそれなりのマシンパワーが必要でそれが準備できないケースもあるでしょう。

そんな環境向けにはFerretDBという所でDocumentDBのDockerイメージを提供しています。こちらのインストール方法に従って導入するのが簡単です。

中身を見てみる

さて、インストールが完了したら中に何があるのかを確認していきましょう。

まずはExtension(PostgreSQLの拡張機能)を確認します。先ほど説明したように、documentdbとdocumentdb_coreという2つの拡張が確認できます。

postgres=# \dx
                                    List of installed extensions
      Name       | Version |   Schema   |                        Description                         
-----------------+---------+------------+------------------------------------------------------------
 documentdb      | 0.102-0 | public     | API surface for DocumentDB for PostgreSQL
 documentdb_core | 0.102-0 | public     | Core API surface for DocumentDB on PostgreSQL
 pg_cron         | 1.6     | pg_catalog | Job scheduler for PostgreSQL
 plpgsql         | 1.0     | pg_catalog | PL/pgSQL procedural language
 postgis         | 3.4.3   | public     | PostGIS geometry and geography spatial types and functions
 rum             | 1.3     | public     | RUM index access method
 tsm_system_rows | 1.0     | public     | TABLESAMPLE method which accepts number of rows as a limit
 vector          | 0.8.0   | public     | vector data type and ivfflat and hnsw access methods
(8 rows)

続いてスキーマを確認してみると、documentdbユーザを所有者としたいくつかのものが作成されていることが分かります。documentdb_dataというスキーマもあり、ここにデータが入るのか?などと言う予想も付きます。

postgres=# \dn
               List of schemas
          Name           |       Owner       
-------------------------+-------------------
 cron                    | documentdb
 documentdb_api          | documentdb
 documentdb_api_catalog  | documentdb
 documentdb_api_internal | documentdb
 documentdb_core         | documentdb
 documentdb_data         | documentdb
 public                  | pg_database_owner
(7 rows)

更に細かい話として、AccessMethod(AM)も確認しておきます。

postgres=# select * from pg_am;
  oid  |     amname     |                  amhandler                  | amtype
-------+----------------+---------------------------------------------+--------
     2 | heap           | heap_tableam_handler                        | t
   403 | btree          | bthandler                                   | i
   405 | hash           | hashhandler                                 | i
   783 | gist           | gisthandler                                 | i
  2742 | gin            | ginhandler                                  | i
  4000 | spgist         | spghandler                                  | i
  3580 | brin           | brinhandler                                 | i
 16588 | ivfflat        | ivfflathandler                              | i
 16590 | hnsw           | hnswhandler                                 | i
 17927 | rum            | rumhandler                                  | i
 18763 | documentdb_rum | documentdb_api_catalog.documentdbrumhandler | i
(11 rows)

AccessMethodとはPostgreSQL v12以降でサポートされた、独自のデータ格納方式をプラガブルに開発・提供できるもので、PostgreSQLに新たなデータを格納するような大きな拡張で良く使われる方式です(Citus、pg_duckdbなど)。

しかし、pg_documentdbではテーブルのAccessMethod(上記表でamtypeがtのもの)は追加されず、インデックスのAMのみが提供されています。
これは後で出てくるように、データ格納自体は通常のPostgreSQLのテーブル(heap)に新たなデータ型(BSON)で行うもので、特別なAMは必要ないということになります。

※PostgreSQLのTable Access Methodの詳細を知りたい方はこちらが参考になります。

JSONをあれこれしてみる

さて、ここからはpg_documentdbを使ってドキュメント型DB的にデータを追加・検索してみます。

コレクションの作成

コレクションは(誤解を恐れずに言えば)RDBのテーブルに相当するもので、作成にはdocumentdb_api.create_collectionを使います。

postgres=# SELECT documentdb_api.create_collection('documentdb','patient');
NOTICE:  creating collection
 create_collection 
-------------------
 t
(1 row)

(SELECTでDDL的なことを行う気持ち悪さは置いておいて)create_collectionで何が起きたのかを確認してみましょう。

先ほどスキーマの確認をしていましたが、documentdb_api_catalogというスキーマにcollectionsというテーブルが作成されています。この中に新たに作成したコレクションがレコードとして登録されます。この辺りはpg_documentdbのメタデータということになります。

postgres=# \d
                                List of relations
         Schema         |              Name               |   Type   |   Owner    
------------------------+---------------------------------+----------+------------
 documentdb_api_catalog | collection_indexes              | table    | documentdb
 documentdb_api_catalog | collection_indexes_index_id_seq | sequence | documentdb
 documentdb_api_catalog | collections                     | table    | documentdb
 documentdb_api_catalog | collections_collection_id_seq   | sequence | documentdb
 documentdb_api_catalog | documentdb_index_queue          | table    | documentdb
(5 rows)

postgres=# select * from collections;
 database_name |  collection_name  | collection_id | shard_key |           collection_uuid            | view_definition | validator | validation_level | validat
ion_action 
---------------+-------------------+---------------+-----------+--------------------------------------+-----------------+-----------+------------------+--------
-----------
 documentdb    | system.dbSentinel |             1 |           | 84643c0a-20b3-4571-ad80-b1f66831c299 |                 |           |                  | 
 documentdb    | patient           |             2 |           | 52423f5f-312d-40ef-83bf-52065f2ceb24 |                 |           |                  | 
(2 rows)

ドキュメント(データ)を操作してみる

コレクションにドキュメント(レコードに相当)を追加する際には、documentdb_api.insert_one等を使います。
※これまたINSERTですが、SELECT文です。

select documentdb_api.insert_one('documentdb','patient', '{ "patient_id": "P001", "name": "Alice Smith", "age": 30, "phone_number": "555-0123", "registration_year": "2023","conditions": ["Diabetes", "Hypertension"]}');
select documentdb_api.insert_one('documentdb','patient', '{ "patient_id": "P002", "name": "Bob Johnson", "age": 45, "phone_number": "555-0456", "registration_year": "2023", "conditions": ["Asthma"]}');
select documentdb_api.insert_one('documentdb','patient', '{ "patient_id": "P003", "name": "Charlie Brown", "age": 29, "phone_number": "555-0789", "registration_year": "2024", "conditions": ["Allergy", "Anemia"]}');
select documentdb_api.insert_one('documentdb','patient', '{ "patient_id": "P004", "name": "Diana Prince", "age": 40, "phone_number": "555-0987", "registration_year": "2024", "conditions": ["Migraine"]}');
select documentdb_api.insert_one('documentdb','patient', '{ "patient_id": "P005", "name": "Edward Norton", "age": 55, "phone_number": "555-1111", "registration_year": "2025", "conditions": ["Hypertension", "Heart Disease"]}');

格納されたドキュメントを検索するには、documentdb_api.collectionを使います。当然、フィルタなどの様々な検索方法も提供されています。

postgres=# SELECT document FROM documentdb_api.collection('documentdb','patient');
                                                                                                                       document                                                                                                                       
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 { "_id" : { "$oid" : "67b4ad49b0b106e08c0bb120" }, "patient_id" : "P001", "name" : "Alice Smith", "age" : { "$numberInt" : "30" }, "phone_number" : "555-0123", "registration_year" : "2023", "conditions" : [ "Diabetes", "Hypertension" ] }
 { "_id" : { "$oid" : "67b4ad49b0b106e08c0bb121" }, "patient_id" : "P002", "name" : "Bob Johnson", "age" : { "$numberInt" : "45" }, "phone_number" : "555-0456", "registration_year" : "2023", "conditions" : [ "Asthma" ] }
 { "_id" : { "$oid" : "67b4ad49b0b106e08c0bb122" }, "patient_id" : "P003", "name" : "Charlie Brown", "age" : { "$numberInt" : "29" }, "phone_number" : "555-0789", "registration_year" : "2024", "conditions" : [ "Allergy", "Anemia" ] }
 { "_id" : { "$oid" : "67b4ad49b0b106e08c0bb123" }, "patient_id" : "P004", "name" : "Diana Prince", "age" : { "$numberInt" : "40" }, "phone_number" : "555-0987", "registration_year" : "2024", "conditions" : [ "Migraine" ] }
 { "_id" : { "$oid" : "67b4ad4bb0b106e08c0bb124" }, "patient_id" : "P005", "name" : "Edward Norton", "age" : { "$numberInt" : "55" }, "phone_number" : "555-1111", "registration_year" : "2025", "conditions" : [ "Hypertension", "Heart Disease" ] }
(5 rows)

実際にデータはどこに格納されている?

ここまででコレクションはメタデータとして管理されており、実際にはJSONドキュメントをPostgreSQLのレコードとして何処かのテーブルに格納しているのでは?と予想が建てられます。

確認のために、先ほどのdocumentdb_api.collectionで検索した際の実行計画を見てみましょう。

postgres=# explain SELECT document FROM documentdb_api.collection('documentdb','patient');
                             QUERY PLAN
---------------------------------------------------------------------
 Bitmap Heap Scan on documents_2  (cost=4.18..12.64 rows=4 width=32)
   Recheck Cond: (shard_key_value = '2'::bigint)
   ->  Bitmap Index Scan on _id_  (cost=0.00..4.18 rows=4 width=0)
         Index Cond: (shard_key_value = '2'::bigint)
(4 rows)

結果として非常にシンプルな実行計画が返ってきて、documents_2というテーブル(heap)を検索していることが分かります。

今度はdocumentdb_dataのスキーマの中を確認して、documents_2というテーブルがそこに作成されていることを確認します。
すると、documentというdocumentdb_core.bsonのデータ型のカラムがあり、ここに本体のデータが入っていると考えられます。

postgres=# set search_path = documentdb_data;
SET

postgres=# \d
                       List of relations
     Schema      |    Name     | Type  |         Owner         
-----------------+-------------+-------+-----------------------
 documentdb_data | documents_1 | table | documentdb_admin_role
 documentdb_data | documents_2 | table | documentdb_admin_role
 documentdb_data | retry_1     | table | documentdb_admin_role
 documentdb_data | retry_2     | table | documentdb_admin_role
(4 rows)

postgres=# \d documents_2 
                     Table "documentdb_data.documents_2"
     Column      |           Type           | Collation | Nullable | Default 
-----------------+--------------------------+-----------+----------+---------
 shard_key_value | bigint                   |           | not null | 
 object_id       | documentdb_core.bson     |           | not null | 
 document        | documentdb_core.bson     |           | not null | 
 creation_time   | timestamp with time zone |           | not null | now()
Indexes:
    "collection_pk_2" PRIMARY KEY, btree (shard_key_value, object_id)
Check constraints:
    "shard_key_value_check" CHECK (shard_key_value = '2'::bigint)

では、実際にdocuments_2を直接検索してみます。

postgres=# select * from documents_2;
 shard_key_value |                    object_id                     |                                                                                                                       document                                                                  
                                                     |         creation_time         
-----------------+--------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-----------------------------------------------------+-------------------------------
               2 | { "" : { "$oid" : "67b4ad49b0b106e08c0bb120" } } | { "_id" : { "$oid" : "67b4ad49b0b106e08c0bb120" }, "patient_id" : "P001", "name" : "Alice Smith", "age" : { "$numberInt" : "30" }, "phone_number" : "555-0123", "registration_year" : "2023", "c
onditions" : [ "Diabetes", "Hypertension" ] }        | 2025-02-18 15:54:49.384579+00
               2 | { "" : { "$oid" : "67b4ad49b0b106e08c0bb121" } } | { "_id" : { "$oid" : "67b4ad49b0b106e08c0bb121" }, "patient_id" : "P002", "name" : "Bob Johnson", "age" : { "$numberInt" : "45" }, "phone_number" : "555-0456", "registration_year" : "2023", "c
onditions" : [ "Asthma" ] }                          | 2025-02-18 15:54:49.387933+00
               2 | { "" : { "$oid" : "67b4ad49b0b106e08c0bb122" } } | { "_id" : { "$oid" : "67b4ad49b0b106e08c0bb122" }, "patient_id" : "P003", "name" : "Charlie Brown", "age" : { "$numberInt" : "29" }, "phone_number" : "555-0789", "registration_year" : "2024", 
"conditions" : [ "Allergy", "Anemia" ] }             | 2025-02-18 15:54:49.389876+00
               2 | { "" : { "$oid" : "67b4ad49b0b106e08c0bb123" } } | { "_id" : { "$oid" : "67b4ad49b0b106e08c0bb123" }, "patient_id" : "P004", "name" : "Diana Prince", "age" : { "$numberInt" : "40" }, "phone_number" : "555-0987", "registration_year" : "2024", "
conditions" : [ "Migraine" ] }                       | 2025-02-18 15:54:49.391631+00
               2 | { "" : { "$oid" : "67b4ad4bb0b106e08c0bb124" } } | { "_id" : { "$oid" : "67b4ad4bb0b106e08c0bb124" }, "patient_id" : "P005", "name" : "Edward Norton", "age" : { "$numberInt" : "55" }, "phone_number" : "555-1111", "registration_year" : "2025", 
"conditions" : [ "Hypertension", "Heart Disease" ] } | 2025-02-18 15:54:51.112564+00
(5 rows)

このように先ほどdocumentdb_api.insert_oneで作成したドキュメントが全て返ってきました。

documents_1というテーブルもあり、実行計画でshard_key_valueが使われていたことから、複数のPostgreSQL上のテーブルを使うことで、MongoDBのシャーディングを実装していることが予想されます。

おそらく、コレクションとドキュメントの紐づけはobject_idでされていると思われますが、そちらを管理するテーブルは現時点では分かっていません。

ここまでの感想

今回はPostgreSQLのpsqlクライアントを利用して、pg_documentdbのコレクションとドキュメントを作成してみました。

ただ、ここまでを見て疑問に思う方も多いと思います。

「psqlから扱うなら、PostgreSQLのJSONBのレコードを作成するのと何が違うの?」

細かな違いはあるのでしょうが、PostgreSQLにJSONデータを格納してSQLから検索するだけなら大差はないのでしょう。

しかし、DocumentDB全体としては今回の発表で完成形ではないはずです。

待たれるMongoDBワイヤプロトコル互換

MongoDBを利用しているアプリケーションからの移行を考える場合、PostgreSQLに格納したコレクションードキュメントをMongoDBのプロトコルで接続して扱う必要があります。

以下の図は、OSSのMongoDB互換PostgreSQLを提供しているFerretDBのサイトに存在するものですが、図中のMongoDB Protocolを喋れるコンポーネントがMicrosoftのDocumentDBにはないため、ここの対応が今後必須になると考えられます。

FerretDBが既に公開しているようにFerretDBとDocumentDBを組み合わせて役割をそれぞれ分担する方法は、その1つの解決策です。

もう1つは冒頭で上げたように、Cosmos DB for MongoDBの内部コンポーネントをOSSとして追加で公開する方法が考えられるでしょう。

いずれにしても、MongoDBの新バージョンとの互換性を追求するのか、それともNoSQL標準として分岐した形式を独自に作り上げていくのかを選択することになるでしょう。

まとめ

ということで、今回はMicrosoftがOSSとして公開したDocumentDBを使ってみて、その内部の動きや今後のロードマップなどを予想してみました。

PostgreSQLをストレージエンジンとしてJSONやVectorなど様々な形式のデータを扱う構成は、今後さらに発展していくと考えられます。

DocumentDBについても、今回の予想が合っているのか、引き続き情報を追っていきたいと思います。

Discussion