Cosmos DB for PostgreSQLのパーティションとカラムナーストレージ

Azure

公式ドキュメントにパーティショニングとカラムナーストレージのことは書いてあるのだけれど、実データを入れてテーブルがどのように見えるかについて、分散テーブルも含めてまとめて説明したものはないように思うので、書いておく。

テーブルの種類

パーティションとカラムナーストレージを正しく理解するには、Cosmos DB for PostgreSQLのテーブルの種類についてまず理解している必要がある。

Local Table

Cosmos DB for PostgreSQLのCoordinatorノードにpsqlで接続して、例えば以下のようにテーブルを作成した場合、これらのテーブルはCoordinatorノードに作成されている。

CREATE TABLE github_events(
    event_id bigint,
    event_type text,
    event_public boolean,
    repo_id bigint,
    payload jsonb,
    repo jsonb,
    user_id bigint,
    org jsonb,
    created_at timestamp
);
CREATE TABLE github_users(
    user_id bigint,
    url text,
    login text,
    avatar_url text,
    gravatar_id text,
    display_login text
);

確認してみよう。まずCoordinator。

$ psql "host=c-riohol.a35bbmlly7xdvy.postgres.cosmos.azure.com port=5432 dbname=citus user=citus sslmode=require"

citus=> \dt
           List of relations
 Schema |     Name      | Type  | Owner 
--------+---------------+-------+-------
 public | github_events | table | citus
 public | github_users  | table | citus
(2 rows)

citus=> SELECT * FROM citus_tables;
 table_name | citus_table_type | distribution_column | colocation_id | table_size | shard_count | table_owner | access_method 
------------+------------------+---------------------+---------------+------------+-------------+-------------+---------------
(0 rows)

念のため、Workerノードに接続して見てみる。

$ psql "host=w0-riohol.a35bbmlly7xdvy.postgres.cosmos.azure.com port=5432 dbname=citus user=citus sslmode=require"

citus=> \dt
Did not find any relations.

Cosmos DB for PostgreSQLで、Local Tableを用いることはほとんど無い。Local Tableは、この後に説明するDistributed / Reference Tableのメタ情報を管理するために主に用いられ、それらのメタ情報を直接編集することもほとんどない。ただし、viewとして見ることは無きにしも非ず。

Distributed Table

Distributed Tableを作成するには、Local Tableを作成後、create_distributed_table()を使う。上のテーブルの例であれば、以下の通り。

SELECT create_distributed_table('github_events', 'user_id');
SELECT create_distributed_table('github_users', 'user_id');

Coordinatorノードで見ると、\dtの結果は変わらないが、

citus=> \dt
           List of relations
 Schema |     Name      | Type  | Owner 
--------+---------------+-------+-------
 public | github_events | table | citus
 public | github_users  | table | citus
(2 rows)

citus_tablesを見ると、Distributed Tableになっていることが分かる。

citus=> SELECT * FROM citus_tables;
  table_name   | citus_table_type | distribution_column | colocation_id | table_size | shard_count | table_owner | access_method 
---------------+------------------+---------------------+---------------+------------+-------------+-------------+---------------
 github_events | distributed      | user_id             |             5 | 256 kB     |          32 | citus       | heap
 github_users  | distributed      | user_id             |             5 | 256 kB     |          32 | citus       | heap
(2 rows)

そして、pg_dist_shardから、シャードキーのハッシュ値をシャードに割り当てるための範囲が32(citus.shard_countのデフォルト値)に分割されていることが分かり、

citus=> SELECT * FROM pg_dist_shard;
 logicalrelid  | shardid | shardstorage | shardminvalue | shardmaxvalue 
---------------+---------+--------------+---------------+---------------
 github_events |  102297 | t            | -2147483648   | -2013265921
 github_events |  102298 | t            | -2013265920   | -1879048193
 github_events |  102299 | t            | -1879048192   | -1744830465
 github_events |  102300 | t            | -1744830464   | -1610612737
......

さらに、citus_shardsから、どのシャードが、どのWorkerノードに割り当てられているかが分かる。

citus=> SELECT * FROM citus_shards;
  table_name   | shardid |      shard_name      | citus_table_type | colocation_id |                          nodename                      
    | nodeport | shard_size 
---------------+---------+----------------------+------------------+---------------+--------------------------------------------------------
----+----------+------------
 github_events |  102297 | github_events_102297 | distributed      |             4 | private-w0-riohol.a35bbmlly7xdvy.postgres.cosmos.azure.
com |     5432 |       8192
 github_events |  102298 | github_events_102298 | distributed      |             4 | private-w1-riohol.a35bbmlly7xdvy.postgres.cosmos.azure.
com |     5432 |       8192
 github_events |  102299 | github_events_102299 | distributed      |             4 | private-w2-riohol.a35bbmlly7xdvy.postgres.cosmos.azure.
com |     5432 |       8192
......

つまり、「シャードカラムの値のハッシュ値が-2147483648から-2013265921の範囲の行は、102297というIDのシャードに格納され、そのシャードは、private-w0というWorkerノードに”github_events_102297″というテーブルとして割り当てられた」ことが分かる。

Reference Table

次に全てのWorkerノードに同一内容が複製されるReference Tableを作る。Local Tableを作成後、create_reference_table()を実行すれば良い。

CREATE TABLE geo_ips (
    addrs cidr NOT NULL PRIMARY KEY,
    latlon point NOT NULL
    CHECK (-90  <= latlon[0] AND latlon[0] <= 90 AND
        -180 <= latlon[1] AND latlon[1] <= 180)
);

SELECT create_reference_table('geo_ips');

citus_tablesを見てみる。

citus=> SELECT * FROM citus_tables;
  table_name   | citus_table_type | distribution_column | colocation_id | table_size | shard_count | table_owner | access_method 
---------------+------------------+---------------------+---------------+------------+-------------+-------------+---------------
 geo_ips       | reference        | <none>              |             6 | 80 kB      |           1 | citus       | heap
 github_events | distributed      | user_id             |             5 | 256 kB     |          32 | citus       | heap
 github_users  | distributed      | user_id             |             5 | 256 kB     |          32 | citus       | heap
(3 rows)

geo_ipsというReference Tableが追加されたことが分かる。さらに、pg_dist_shardとcitus_shardsを見る。

citus=> SELECT * FROM pg_dist_shard;
 logicalrelid  | shardid | shardstorage | shardminvalue | shardmaxvalue 
---------------+---------+--------------+---------------+---------------
......
 geo_ips       |  102425 | t            |               | 
(65 rows)

citus=> SELECT * FROM citus_shards;
  table_name   | shardid |      shard_name      | citus_table_type | colocation_id |                          nodename                      
    | nodeport | shard_size 
---------------+---------+----------------------+------------------+---------------+--------------------------------------------------------
----+----------+------------
 geo_ips       |  102425 | geo_ips_102425       | reference        |             6 | private-c-riohol.a35bbmlly7xdvy.postgres.cosmos.azure.c
om  |     5432 |      16384
 geo_ips       |  102425 | geo_ips_102425       | reference        |             6 | private-w1-riohol.a35bbmlly7xdvy.postgres.cosmos.azure.
com |     5432 |      16384
 geo_ips       |  102425 | geo_ips_102425       | reference        |             6 | private-w2-riohol.a35bbmlly7xdvy.postgres.cosmos.azure.
com |     5432 |      16384
 geo_ips       |  102425 | geo_ips_102425       | reference        |             6 | private-w3-riohol.a35bbmlly7xdvy.postgres.cosmos.azure.
com |     5432 |      16384
 geo_ips       |  102425 | geo_ips_102425       | reference        |             6 | private-w0-riohol.a35bbmlly7xdvy.postgres.cosmos.azure.
com |     5432 |      16384

つまり、Reference Tableは、「シャードキーの指定が無い=ハッシュ値の計算はされないがシャードIDが割り振られ、全てのWorkerノードにシャードが置かれる=複製されるDistributed Table」であることが分かる。

パーティションを作成する

パーティションの作成方法は、通常のPostgreSQLと全く同様に可能だ。

CREATE TABLE github_events(
    event_id bigint,
    event_type text,
    event_public boolean,
    repo_id bigint,
    payload jsonb,
    repo jsonb,
    user_id bigint,
    org jsonb,
    created_at timestamp
) PARTITION BY RANGE (created_at);

次に、Distributed Tableにすれば良い。

SELECT create_distributed_table('github_events', 'repo_id');

ただし、これだけでは実際にはパーティションは作成されていない。あくまで、パーティションキーが定義されただけなので、以下のようにパーティションテーブルを追加する。

CREATE TABLE github_events_201902 PARTITION OF github_events
    FOR VALUES FROM ('2019-02-01') TO ('2019-03-01');

citus_shardsを見てみよう。

citus=> SELECT * FROM citus_shards;
      table_name      | shardid |         shard_name          | citus_table_type | colocation_id |                          nodename        
                  | nodeport | shard_size 
----------------------+---------+-----------------------------+------------------+---------------+------------------------------------------
------------------+----------+------------
 github_events        |  102426 | github_events_102426        | distributed      |             7 | private-w0-riohol.a35bbmlly7xdvy.postgres
.cosmos.azure.com |     5432 |          0
......
 github_events_201902 |  102458 | github_events_201902_102458 | distributed      |             7 | private-w0-riohol.a35bbmlly7xdvy.postgres
.cosmos.azure.com |     5432 |       8192
......

github_eventsのパーティションとして、github_events_201902が作成され、そのシャード、github_events_201902_102458が作成されたことが分かる。

しかしこの方法には大きな問題がある。つまりパーティションを手動で作成しないとならない。

そこで、Cosmos for PostgreSQLではcreate_time_partition()が用意されている。つまり、

CREATE TABLE github_events(
    event_id bigint,
    event_type text,
    event_public boolean,
    repo_id bigint,
    payload jsonb,
    repo jsonb,
    user_id bigint,
    org jsonb,
    created_at timestamp
) PARTITION BY RANGE (created_at);

SELECT create_distributed_table('github_events', 'repo_id');

SELECT create_time_partitions(
  table_name         := 'github_events',
  partition_interval := '1 month',
  end_at             := now() + '12 months'
);

とすれば、パーティションを作成する期間を指定して、一気に作成することが可能だ。

citus=> SELECT partition
  FROM time_partitions
 WHERE parent_table = 'github_events'::regclass;
       partition        
------------------------
 github_events_p2023_11
 github_events_p2023_12
 github_events_p2024_01
 github_events_p2024_02
 github_events_p2024_03
 github_events_p2024_04
 github_events_p2024_05
 github_events_p2024_06
 github_events_p2024_07
 github_events_p2024_08
 github_events_p2024_09
 github_events_p2024_10
 github_events_p2024_11
(13 rows)

ここで、もう一度、citus_shardsを見てみよう。

citus=> select * from citus_shards;
       table_name       | shardid |          shard_name           | citus_table_type | colocation_id |                          nodename    
                      | nodeport | shard_size 
------------------------+---------+-------------------------------+------------------+---------------+--------------------------------------
----------------------+----------+------------
 github_events          |  102522 | github_events_102522          | distributed      |             9 | private-w0-riohol.a35bbmlly7xdvy.post
gres.cosmos.azure.com |     5432 |          0
......
 github_events_p2023_11 |  102554 | github_events_p2023_11_102554 | distributed      |             9 | private-w0-riohol.a35bbmlly7xdvy.post
gres.cosmos.azure.com |     5432 |       8192
......

つまり「各パーティションがDistributed Tableになっている」ことが分かる。

IoTのシナリオなどで、データをCosmos DB for PostgreSQLにストアする場合、このパーティションは非常に重要で、仮に上記のように月ごとに分割し、データが一定の時間間隔(例:毎秒)で必ず生成され取りこぼしが無い場合、各パーティションは最小28日、最大31日分をストアすることになり、データサイズがほぼ一定=インデックスサイズもほぼ一定=レスポンスが一定に保つことが期待できる(RDBMSなので確約はできない)。Distributed Tableにもなっているので、シャーディングの設計によっては、あるパーティションのシャードは、別のパーティションのシャードと同等のボリュームであることも期待できる。

カラムナーストレージ

執筆時点(2023年11月)のCosmos DB for PostgreSQLで利用可能なCitusはバージョン12.1なのだけれど、Citus 10からカラムナーストレージが利用可能になっている。要は列方向にデータを保持し圧縮(zstdが用いられている)する機能がサポートされている。上記のパーティションのうち、2023年11月のパーティションをカラムナーにしてみる。

CALL alter_old_partitions_set_access_method(
    'github_events',
    '2023-12-01 00:00:00' /* older_than */,
    'columnar'
);

time_partitionsを確認してみよう。

citus=> SELECT partition, access_method
    FROM time_partitions
    WHERE parent_table = 'github_events'::regclass;
       partition        | access_method 
------------------------+---------------
 github_events_p2023_11 | columnar
 github_events_p2023_12 | heap
 github_events_p2024_01 | heap
......
 github_events_p2024_10 | heap
 github_events_p2024_11 | heap
(13 rows)

新しいパーティションは通常のheapのままだが、日時の指定によって古いパーティションがcolumnarになっていることが分かる。

もちろん、テーブルの作成時にカラムナーにしてしまうことも可能。

citus=> CREATE TABLE github_events(
    event_id bigint,
    event_type text,
    event_public boolean,
    repo_id bigint,
    payload jsonb,
    repo jsonb,
    user_id bigint,
    org jsonb,
    created_at timestamp
) USING COLUMNAR;

データの中身によるが8分の1から40分の1程度の圧縮率が報告されている。

Distributed + Partitioning + Columnar Storageで、PostgreSQLをスケールアウトし、性能を一定に保ちつつ、ストレージの節約までを実現可能なことが理解されると幸いだ。