はじめに

先日、PlanetScaleからNekiがPlatform Previewとして発表されました。Nekiは、複数のPostgreSQL shardの前段にrouterを置き、PostgreSQLを水平分割して扱うための仕組みのようです。

発表では、各shardについて “Every shard is real Postgres” と説明し、さらに “Neki handles routing, resharding, online schema changes, and version upgrades, all with a single connection string” としています。

大まかに訳すと、各shardは実際のPostgreSQLのまま使いつつ、routingやReshardなどの分散DBとして必要な仕事は、接続先を一つに見せたNekiが受け持つ、ということです。何となく良さそうな感じは伝わってきますが、これだけではSQLがどこで実行されるのか、既存のPostgreSQLの知識がどこまで通用するのかは分かりません。

そこで今回は、Platform PreviewのNekiを実際に1 shardと4 shardで動かし、標準psqlからの接続、Nekiのdistributed plan、Routeを代表するshardのPostgreSQL plan、data topologyを見てみました。性能順位を決めるためのbenchmarkではなく、分散処理の仕事がどの層に現れるかを確かめるための小規模な検証です。

NekiはPlatform Previewです。以下で扱うのは2026年9月中旬の検証時点での挙動であり、将来の仕様変更、制約の変更、破壊的変更があっても不思議ではありません。一般的なPostgreSQLの性質やNekiの将来の挙動まで断定するものではありません。

Nekiとは何か

Nekiをざっくり言うと、複数のPostgreSQLをshardとして束ね、applicationには一つのPostgreSQL接続先として見せるための分散layerです。applicationが各shardへ直接接続するのではなく、まずNeki routerへ接続します。

Application / PostgreSQL driver / ORM
                 |
                 | PostgreSQL wire protocol
                 v
            Neki router
           /     |      \
          v      v       v
       sidecar sidecar sidecar
          |      |       |
     PostgreSQL PostgreSQL PostgreSQL
       shard 1    shard 2    shard 3 ...

これはqueryの経路を簡略化した論理図です。公式の構成では、routerは各PostgreSQL instanceに付属するNeki sidecarへ仕事を送り、sidecarがPostgreSQLへの接続を扱うようです。primaryとreplicaの構成は、この図では省略しています。

公式の説明を整理すると、主な構成要素は次のとおりです。

  • Neki router: PostgreSQL wire protocolを受け、SQLをparseしてdistributed planを作り、実行するshardを選び、複数shardの結果を一つの結果として返すようです。
  • shard: 実データを保持するphysical PostgreSQLの単位です。公式説明では、primary 1台と少なくとも2台のreplicaから構成されるようです。
  • shard group: tableを載せるshardの集合と、そのkey rangeの対応を表す論理的な単位です。
  • data topology: logical tableをphysical shardへどう対応付けるかを表すJSON設定です。shard key、hash方法、shard group、key rangeを定義します。routerはこれを使ってqueryの行き先を決めます。
  • control plane: nodeのhealthを追跡し、failover、Reshard、schema change、version upgradeといったworkflowを調整します。

言い換えると、PostgreSQLが各shard内で「どう実行するか」を決め、Neki routerとdata topologyが「どのshardで実行するか」を決めます。この2つの判断が分かれていることが、この記事でplanを二段に分けて読む理由です。

この説明は公式アナウンスrouterの解説をもとにしています。以下では、この構成が実際のEXPLAINにどう現れるかを確かめます。

Neki planとPostgreSQL plan

この記事でいうNeki planは、routerが作る分散実行計画です。EXPLAINすると、まずRoute [EqualUnique]Route [Scatter]のように、どのshardへ送るか、何台へ送るか、複数の結果をrouterでどうまとめるかが現れます。

一方、Neki planの中にあるPostgresPlanは、Routeを代表する1 shardから得たPostgreSQLの通常の実行計画です。ここにはIndex ScanSeq ScanHash Joinなどが現れます。Scatter先のすべてのshardで同じplanになることまでは、この出力だけでは分かりません。

Neki plan:      Route [Scatter]      # どのshardで実行するか
PostgresPlan:   Hash Join             # shard内でどう実行するか

そのため、Scatterになっているなら「なぜ送信先を絞れないのか」をデータ配置定義やquery条件から考えます。Index ScanではなくSeq Scanなら、通常のPostgreSQLと同じようにindexや統計情報を検討します。以後、router側の判断をNeki plan、shard側の判断をPostgreSQL planと呼び分けます。

この記事で使う言葉

  • shard: 分割されたPostgreSQLの単位です。この記事では原語のままshardと呼びます。
  • データ配置定義(data topology): logical tableとphysical shardの対応を定義するJSON設定です。
  • 分散キー(shard key): rowの配置先を決めるcolumnまたは決定的な式です。この記事ではcolumnを使いました。queryにこの条件があれば、routerは送信先を絞り込める場合があります。
  • Scatter: 送信先を一つに絞れないため、対象tableのshard groupに属する全shardへqueryを送るNeki plan上の実行方式です。
  • コロケーション(co-location): JOINしたい関連rowを、同じshard groupとshard indexで同じrouting keyへ対応させる設計です。いわゆる同居配置です。
  • 処理のshard内実行(pushdown): routerで処理せず、選ばれたshard内のPostgreSQLへ処理を任せることです。
  • 対象shard数(fanout): 1 queryが届くshardの数です。少ないほど、routerが扱う中間結果も小さくなりやすくなります。

先に結論

今回のplanを見る限り、Nekiはqueryを送るshardの選択、複数shardへの送信、結果の結合といった機械的な分散処理を引き受けるようです。一方で、分散キーとコロケーションは、利用者側のdata modelに残るように見えます。

今回の4 shard環境では、WHERE customer_id = ?のように送信先を決める値が分かるqueryは1 shardへ送るplanになりました。分散キーが分からないqueryは対象shard groupの4 shardへScatterするplanになりました。JOINでは、関連rowを同じkeyで配置すると代表shard内でJOINするplanになり、配置が異なる場合はrouterが両表を集めて結合するplanになりました。

つまり、NekiはPostgreSQLを水平分割して扱うための実行基盤を提供する製品のようです。ただし、その基盤を効率よく使えるかは、関連データをどのように同じshardへ集めるかという、application側の設計にも依存しそうです。

検証時期と環境

検証時期は2026年9月中旬で、NekiはPlatform Previewでした。AWS us-east-1に、ARM PS-10(1/8 vCPU、1 GiB RAM)のshardを用意しました。各shardはprimary 1台とreplica 2台、routerはNKR-1で、storageは各shard 10 GiBに固定しています。PostgreSQL serverは18.6 (Neki)、pscale CLIは0.332.0、psql clientは13.2でした。

今回の記事では、短命の別databaseで行った実験をまとめています。分散キーやschemaが同じ一つの4 shard環境で、すべてのplanを取ったわけではありません。

実験環境 shard数 主な用途 分散キー・schema
互換性確認環境 1 psql、DDL、transaction、EXPLAIN 分散なし
routing環境 4 1 shard routing、Scatter、集約 customers.id
non-colocated JOIN環境 4 router側JOINのplan customers.idorders.id
colocated JOIN環境 4 コロケーションしたJOINのplan 両tableのcustomer_id

この規模から「Nekiは速い」「PostgreSQLより何倍速い」といった結論は出せません。以下では、planとroutingがどう変化したかだけを扱います。

普通のPostgreSQLとして何が使えたか

最初に見たいのは、既存のPostgreSQL clientやSQLをどこまで持ち込めるかです。接続できるだけでなく、transactionやEXPLAINまで普段どおりに扱えるかを確かめます。

短期roleを作り、credentialはprocess environmentだけに置いて、標準psqlからrouterへ接続しました。今回確認できた範囲では、DDL/DML、prepared statement、transactionとsavepoint、session setting、primary key index、PostgreSQLのEXPLAIN、system catalog参照が動きました。

serverはPostgreSQL 18.6 ... (Neki)を返し、shard内のplanには通常のPostgreSQLと同じIndex ScanSeq Scanが現れました。既存のquery tuning知識を持ち込めそうだ、という意味では実質的な価値がありそうです。

今回のpsql sessionの出力は次のとおりでした。DDL、INSERT、prepared statement、transaction、savepoint、EXPLAINを同じ接続で実行しています。

PostgreSQL 18.6 (Debian 18.6-1.pgdg13+2) (Neki)
CREATE TABLE
INSERT 0 2
PREPARE
BEGIN
SAVEPOINT
ROLLBACK
COMMIT

Index Scan using compatibility_probe_pkey on compatibility_probe
  Index Cond: (id = 1)

ただし、wire protocolが同じことと、すべての意味論が同じことは別です。DDLを実行すると、次のnoticeが返りました。

DDL is committed and the change is visible on this router;
other routers may not see it yet.
To wait until it is visible on every router, use __neki.wait_for_ddl(...)

別の実行では、新しいtableが見えずrelation does not existになりました。保存した操作記録だけから接続先routerまで特定はできませんが、noticeが説明するrouter間のDDL可視化遅延だった可能性があります。同一psql session内ではDDLとINSERTが成功し、後続処理の前に返されたrevisionを__neki.wait_for_ddlで待ちました。PostgreSQLのDDLは使えそうですが、複数routerをまたぐ可視化を意識する必要がありそうな点は、通常の単一PostgreSQLとは異なります。

端的に言うと、今回の範囲ではpsqlと普段のPostgreSQLの道具立てを持ち込めそうでした。ただし、DDLの可視化にはrouterをまたぐときの追加の考慮がありそうです。

データ配置定義はtableをどこへ置くかを記述する

次に知りたいのは、rowの置き場所を誰が、どの情報から決めるのかです。Nekiでは、これをデータ配置定義に明示します。

コロケーションしたJOIN環境では、xxhash_customer_idのkey spaceを4つの範囲に分けました。次は実際のtopologyからphysical shard UIDを置き換えて簡略化した例です。次節のrouting環境はcustomers.idを分散キーにした別環境です。

{
"shard_indexes": {
"xxhash_customer_id": {"type": "xxhash", "columns": ["customer_id"]}
},
"shard_groups": [{
"uid": "main_shards",
"default_shard_index": "xxhash_customer_id",
"key_ranges": [
{"shard_uid": "shard-1", "end": "40"},
{"shard_uid": "shard-2", "start": "40", "end": "80"},
{"shard_uid": "shard-3", "start": "80", "end": "c0"},
{"shard_uid": "shard-4", "start": "c0"}
]
}]
}

Nekiはcustomer_idをXXH3-64で送信先を決める値へ変換し、対応するkey rangeのshardへrowとqueryを送ります。tableとshard group、shard index、key rangeの関係をデータ配置定義として利用者が定義します。

ここでの要点は、rowの置き場所が暗黙に決まるのではなく、利用者が定義したkeyと範囲から決まるように見えることです。

分散キーが分かれば1 shard、分からなければScatter

同じSELECTでも、なぜ1 shardで済む場合と全shardへ行く場合があるのでしょうか。分散キーの条件を持つqueryと持たないqueryを比べると、この違いが見えてきそうです。

routing環境で、customers.idを分散キーにした状態から、次のqueryをEXPLAINしました。

SELECT id, name FROM customers WHERE id = 1;

Neki planは次のようになりました。

Route [EqualUnique]
  Query: SELECT customers.id, customers.name
         FROM public.customers WHERE id = $1
  ShardGroup: main_shards
  Values: $1
  PostgresPlan: Index Scan using customers_pkey on customers

EqualUniqueは、Neki layerが1 shardを選ぶplanであることを示します。代表shardのPostgreSQL planではprimary key indexが使われる予定になっています。送信先選択とrow探索は別の仕事です。

一方、分散キーではないnameで絞り込むと、次の形になりました。

Collapse
  Route [Scatter]
      Query: SELECT customers.id, customers.name
             FROM public.customers WHERE name = $1
      ShardGroup: main_shards
      PostgresPlan: Seq Scan on customers
          Filter: (name = 'a'::text)

このqueryでは分散planを生成できました。ただし、planを生成できることと安価に実行できることは別です。Scatterのplanでは対象shard groupの全shardへqueryを送り、結果をCollapseする予定になっています。集約でも、代表shardのPostgreSQL planにはHashAggregateがあり、router側ではAggregate [Ordered]Sort [Merge]で結果を統合するplanになっていました。

Aggregate [Ordered]
  Sort [Merge]
    Route [Scatter]
      PostgresPlan: Sort
        HashAggregate
          Group Key: category
          Seq Scan on orders

端的に言うと、分散キーで送信先を特定できるqueryは1 shardに絞るplanになり、特定できないqueryは対象shard groupの全shardへ送るplanになりました。後者ではrouter側の統合作業も増えそうです。

JOINでデータ配置定義の意味がはっきりする

関連するrowを同じshardへ置くと、JOINのplanはどう変わるのでしょうか。ここでは、同じ関係を表す2つのschemaでplanを比較します。コロケーションには共通のcustomer_idが必要だったため、これは配置だけを変えた純粋な対照実験ではありません。

まず、customersをcustomer ID、ordersをorder自身のIDで分散させました。customerのrowと、そのcustomerのorderが同じshardにある保証はありません。

SELECT c.id, c.name, o.total
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.id;

この場合のplanは、両表をScatterしてrouterでJoin [Hash]するものでした。保存した証拠はEXPLAINの結果であり、JOIN本体の結果検証までは行っていません。ただし、両側の中間結果をrouterへ集めて結合するplanが生成されることは確認できました。

Collapse
  Join [Hash]
    Route [Scatter]
      PostgresPlan: Seq Scan on customers c
    Route [Scatter]
      PostgresPlan: Seq Scan on orders o

次に、別の4 shard環境でcustomersordersの両方にcustomer_idを持たせ、同じshard groupとxxhash_customer_idを使いました。これがコロケーションです。predicateなしのJOINは4 shardへのScatterのままでしたが、代表shardのPostgreSQL planはHash Joinになりました。router側にJoin operatorがなく、代表shard内でJOINするplanになっていることが読み取れます。

OperatorType: Collapse
  OperatorType: Route
  Variant: Scatter
  RemoteCalls: 4
  PostgresPlan:
    Node Type: Hash Join
    Hash Cond: (o.customer_id = c.customer_id)

さらにWHERE c.customer_id = 7を加えると、planはRoute [EqualUnique]で、plan上のRemoteCallsは1になりました。代表shardのPostgreSQL planではNested Loopcustomersにはprimary keyのIndex Scanが現れました。

OperatorType: Route
Variant: EqualUnique
RemoteCalls: 1
PostgresPlan:
  Node Type: Nested Loop
    Node Type: Index Scan
    Index Name: customers_pkey
    Index Cond: (customer_id = 7)

今回の2表JOINでは、コロケーションしたschemaでも対象shard数を1に絞るplanにはなりませんでした。代表shard内でJOINするplanにはコロケーションが関わり、1 shardまで絞るplanには分散キーを条件に含める必要もありました。

この結果から、コロケーションはJOINをshard内へ寄せるplanに関わる設計であり、対象shard数を減らすにはquery側の条件も必要になりそうだと読めます。

ordinary PostgreSQL shardの価値

では、各shardがordinary PostgreSQLであることは、実務上どんな助けになるのでしょうか。Neki planとPostgreSQL planを分けて読めることに、手がかりがありそうです。
今回の価値は、単にCRUDが動いたことではありません。Neki planの中にPostgreSQL planがあり、問題をlayerごとに分けて考えられました。

Application SQL
  -> Neki parser / planner
  -> Route [EqualUnique] or Route [Scatter]
  -> SQL sent to shards
  -> PostgreSQL planner
  -> Index Scan / Seq Scan / Hash Join / Aggregate

対象shard数が大きければデータ配置定義やapplication queryを疑い、Neki plan内のshard側planにSeq Scanが現れれば通常のPostgreSQLと同じようにindexや統計情報を検討できそうです。既存のPostgreSQL運用知識を、分散の問題と混ぜずに使えることが、この構成の重要な利点になりそうだと感じました。

一方、extensionには二つの確認経路がありました。pg_extensionをSQLで照会すると、実際に導入済みのneki_xxhashpg_pscale_utilsplpgsqlを確認できました。これはshard内で有効なextensionの観測です。

別にconfiguration profileのextension catalogを見ると、vectorvectorscaleは利用候補として表示されつつ無効でした。こちらはplatform側の設定候補であり、pg_extensionの照会結果とは別です。extensionの作成・有効化までは試していません。「通常のPostgreSQL」という言葉を、任意のnative extensionを自由に導入できる意味に広げることはできなさそうです。

name             enabled  internal
pg_pscale_utils  true     true
pgextwlist       true     true
vector           false    false
vectorscale      false    false

端的には、shard内のSQL実行を調べるときは既存のPostgreSQL知識を使えそうです。ただしextensionなどmanaged serviceとしての制約まで消えるわけではなさそうです。

今回見えた責務分担

ここまでの観測を、誰が何を決めるかでまとめると次のようになります。これはNekiに何かが欠けている、という意味ではありません。関連dataをどう置くかはapplicationの意味に依存するため、製品側だけで自動的に決めにくい判断にも見えます。

領域 Nekiが受け持つように見えたこと shard上のPostgreSQLが受け持つこと 実装者・運用者に残りそうなこと
queryの実行 送信先shardの選択、複数shardへの送信、結果の統合 選ばれたshard内でSQLを実行 分散キー、コロケーション、対象shard数を抑えられるquery条件の設計
shard内の実行 SQLを選ばれたshardへ転送 index、統計情報を使った実行計画の選択 index、統計情報、extensionの利用可否の確認
問題の切り分け Neki planとPostgreSQL planを分けて表示 代表shardの実行計画を返す 送信先の問題か、shard内SQLの問題かを読み分けること

端的には、Nekiは分散実行の機械的な部分を受け持ち、実装者はdata modelとqueryの局所性を設計する、という分担になりそうです。

まとめ

今回見る限り、NekiはPostgreSQLを捨てずに、分散DBとして実行できる範囲を増やすもののようです。psql、SQL、index、EXPLAIN、PostgreSQL plannerの読み方は、今回の範囲ではそのまま使えました。

ただし、分散queryをうまく実行するためのkeyは、PostgreSQLの内側ではなくデータ配置定義にありそうです。どのtableをどのkeyで分散させるか、関連rowを同じshardに置くかは、利用者側の設計判断になりそうです。Nekiの送信先選択のlayerとordinary PostgreSQL shardの組み合わせを活かせるかどうかは、この設計に大きく依存しそうです。

繰り返しますが、これはPlatform Preview時点の小規模検証です。採用判断では、利用する時点の公式ドキュメントと制約を確認し、実際のschemaとworkloadでplanを取り直した方がよさそうです。

次回は、1 shardにあるtableを別のshard groupへ移すReshard workflowを追います。初回コピー、変更追従、整合性確認、read/writeの宛先切り替えを、運用面から見ていきます。

参照資料