Skip to content

.NET 向け(極めて高速な!)ネイティブ TCP クライアントのご紹介

image 512x512 9
2026年10月16日 · 23分で読む

TL;DR

  • ClickHouse.Driver パッケージに、新しい実験的な ClickHouse クライアントをリリースします。ネイティブ TCP プロトコルと Native 列指向フォーマットを使用します。
  • ゼロアロケーション、ゼロコピー、可能な限りの一括処理など、性能を最優先にゼロから設計しました。一般的なユースケースでは HTTP クライアントと比べて 1.5 〜 2 倍高速です。GC アロケーションを 99% 以上削減するケースもあります。
  • ClickHouse Cloud に対するストリーミングクエリ 1 本で、毎秒最大 110 万行/1 億 1,500 万値を読み取ります。16 並列の挿入では、毎秒最大 87 万 7,000 行(POCO からは 83 万 2,000 行)に達します。控えめなハードウェア上でも毎秒 3,000 クエリ以上を処理できます。
  • HTTP クライアントの完全なドロップイン代替ではありません。

目的別の読み進め方

2 分のクイックスタートから JIT 逆アセンブリまで、幅広い内容を扱っています。関心に合わせて読み進めてください:

クイックスタート

TCP クライアントは、NuGet の通常の ClickHouse.Driver パッケージに含まれています。dotnet add package ClickHouse.Driver でインストールしてください。実験的機能であるため、その型を使用するとコンパイラ警告 CHTCP0001 が発生します。プロジェクトファイルでこれを明示的に許可してください:

<PropertyGroup>
  <NoWarn>$(NoWarn);CHTCP0001</NoWarn>
</PropertyGroup>

ここからのステップ:

  • TCP クライアントのドキュメントでは、設定、型マッピング、セッション、オブザーバビリティ、HTTP から TCP への詳細な移行対応表を解説しています。
  • 読み取り、挿入、型、セッション、TLS、オブザーバビリティ、エラーハンドリングなど、一般的なユースケースを網羅した 30 個のサンプルも用意しています。

TCP を採用した理由と相違点

なぜネイティブクライアントなのか

ClickHouse はデータをカラム単位で保存・処理します。これまで公式の .NET クライアントは、行指向フォーマットである RowBinary を用いた HTTP のみをサポートしていました。そのため、すべての結果が 2 回変換されていました。サーバーがカラムブロックを行ストリームに変換し、クライアントがそのストリームを 1 値ずつデコードします。挿入では同じ 2 ステップが逆順で実行されます。クライアントが行を書き込み、サーバーがそれをカラムへとパースし直します。

ネイティブプロトコルは、これら両方の変換を排除します。これは clickhouse-client や、ClickHouse サーバー同士が分散クエリで利用しているプロトコルです。データはカラムのブロックとして、双方向で Native フォーマットのまま転送されます。これにより両端での処理が軽減されます。サーバーはフォーマット変換を行う必要がなく、クライアントは 1 値ずつではなく一括処理でカラム全体を読み取れます。

ネイティブプロトコルはデータ以外の情報も伝送します。クエリ実行中、サーバーはデータブロックの合間に個別のパケットとして、進捗状況、プロファイルイベント、合計値と極値、さらには要求に応じてサーバー自身のログメッセージを送信します。クライアントはこれらをコールバックに渡します。また、プロトコルがステートフルであるため、1 つのセッションは 1 つのソケットに対応します。ロードバランサーの背後であっても、一時テーブルや SET 文は単一の接続(つまり単一のレプリカ)上に保持されます。

2 つのプロトコルの違い

根本的な違いは、ネットワーク上を流れるデータの形式です。RowBinary はカラムを行ごとにインターリーブします。各行の値が連続して並び、その後に次の行が続きます。Native はカラムごとにまとめます。1 つのブロックにすべての id、次にすべての name、その後にすべての score が格納され、各カラムの先頭にはカラム名と型が付与されます。

HTTP クライアントはストリームを値ごとに走査します。TCP クライアントはブロックごとにカラムあたり 1 回の呼び出しを行います。固定長型の場合、その呼び出しはプールされたバッファへの単一の一括読み取りとなり、取得されるカラムは ReadOnlySpan<T> になります。通常、ブロックには数千行が含まれるため、呼び出し回数を大幅に削減できます。

複合型も同じ規則に従います。Array(Float64) カラムは、すべての行オフセットの後に、全行の全要素が 1 つの連続した領域として並びます:

そのため、クライアントはブロック全体の要素を一度に読み取ることができ、各行は 2 つのオフセット間のスライスになります。このレイアウトは挿入時にも効果を発揮します。InsertAsync にフラットな値とオフセットを渡せば(ClickHouseTcpColumn.CreateArray、または SELECT から直接取得したカラム)、全要素が 1 回のコピーで送信されます。ジャグ配列 double[][] も利用可能で、行あたり 1 回のコピーで処理されます。

列指向の形式をどの程度保持するかは選択可能です。TCP クライアントは 3 つの階層をサポートしています:

階層読み取り挿入コスト
カラムStreamAsync(借用ブロック)InsertAsync行ごとのアロケーションなし。固定長カラムは一括で読み書きされます。
POCOQueryAsync<T>InsertRowsAsync<T>1 行につき 1 オブジェクト。マッピングはカラム単位でコンパイルされ、ボクシングは発生しません。
object[]QueryAsyncInsertRowsAsync1 行につき 1 配列、1 値につき 1 つのボクシング。挿入時は行からカラムへの転置が行われます。

メリットとデメリット

ネイティブクライアントは高速ですが、トレードオフなしの無償のアップグレードではありません。得られるメリットと失われるデメリットを整理します。

得られるメリット

  • **速度:**デコードなしの列指向読み取りが可能です。StreamAsync はブロックを返し、各カラムの .Values はソケットが読み取ったバッファ上のスパンになります。また、列指向の挿入は実質的に memcpy です。
  • **設定不要の POCO:**マッピングは初回使用時にコンパイルされ、カラムごとに 1 つのループが生成され、ボクシングは発生しません。
  • **クエリメタデータ:**クエリ実行中に、進捗状況、プロファイルイベント、サーバーログ、合計値および極値のコールバックを受け取れます。
  • **サーバーに届くキャンセル:**キャンセル操作を行うと Cancel パケットが送信されるため、サーバー側でクエリを停止できます。
  • **セッション:**セッションが 1 つのソケットに対応するため、ロードバランサー配下でも session_id なしで一時テーブル、SET、SET ROLE が動作します。
  • W3C トレースコンテキストを ClickHouse に伝播します。

失われるデメリット

  • **実験的機能であること:**安定版になる前に API が変更される可能性があります。
  • **JSON はテキストのみ:**JSON カラムは文字列として届くため、独自にパースする必要があります(HTTP では JsonObject や POCO が得られます)。サーバー側での追加の文字列シリアライズ処理は比較的遅いため、JSON を多用する場合は高い性能は期待できません。ただし、バイナリ JSON フォーマットのサポートも近日中に実装する予定です。
  • **object[] 階層では性能上のメリットの多くが失われる:**1 行ごとの配列、値ごとのボクシング、挿入時の行からカラムへの転置により、列指向フォーマットの性能上の利点はすべて失われます。そのため、オブジェクトの読み書きに通常 object[] アプローチを使用する ORM では、顕著な改善は見られません。
  • **DateTime が数値として返される:**カラムおよび object[] の行では生の uint/long が返されるため、ゼロコピーかつ完全な精度での読み取りが維持されます。変換には ReadAs<DateTimeOffset> を使用するか、適切な型を持つ POCO プロパティを使用して自動変換してください。
  • **ブロックが借用される:**アロケーションを回避するため、ブロックはループの 1 回の反復の間のみ有効であり、その後にバッファが再利用されます。ブロックのデータをループ外で保持したい場合は、手動でコピーする必要があります。
  • 未対応の機能:
    • ADO.NET 非対応(そのため Dapper や Entity Framework Core には非対応)。
    • CSV、Parquet、JSONEachRow などの代替フォーマットへの非対応。
    • JWT 認証への非対応。
    • {name:Type} 形式のパラメータのみ対応(型なしの @name には非対応)。
  • .NET 8 以降のみ対応。

どちらを選ぶべきか

  • 分析的な読み取り、大量の一括挿入、性能重視のワークロードの場合:TCP をお試しください。
  • ORM、別ファイル形式、トークン認証、JSON のヘビーな利用の場合:HTTP を使い続けてください。

両クライアントは同じパッケージに含まれているため、用途に応じて使い分けることができます。

性能

TCP の 2 つのアプローチ(ブロックと POCO)を HTTP クライアントと比較ベンチマークしました。

  • **構成:**クライアントは AWS m7i.8xlarge、サーバーは ClickHouse Cloud(26.4)、実際のネットワーク経由で接続。
  • **測定対象:**HTTP クライアント(QueryAsync<T> / InsertBinaryAsync<T>)、カラム形式の TCP(StreamAsync / InsertAsync)、POCO 形式の TCP(QueryAsync<T> / InsertRowsAsync<T>)。
  • データ:hits データセット(スカラカラム 105 個)の 100 万行の読み取りと 10 万行の挿入、Array、Map、Tuple カラムを持つテーブル、および 1 つの JSON カラムを持つテーブル。
  • **測定値:**各シナリオを 1 つのプロセス内で 5 回実行。ウォームアップ後の 4 回の中央値を報告。

特筆すべき点:

カラム形式の読み取りでは所要時間が半分未満に短縮(hits で 0.44 倍、コレクションで 0.45 倍)。同等条件の比較となる POCO でも約 3 分の 2 に短縮されました。

挿入は全体的に高速化。 hits の挿入時間は、カラム形式で 0.61 倍、POCO で 0.66 倍になりました。16 並列挿入では、どちらのスタイルでも 0.46 倍になりました。

JSON は予想通り低速。 JSON は文字列との相互シリアライズ/デシリアライズが必要なため、性能は低下します。

スループットの観点では、単一ストリームから毎秒 1 億 1,500 万値以上を読み取れます。32 vCPU クライアントでの結果(各構成のウォーム実行の最良値):

TCP カラム形式(最大)TCP POCO(最大)HTTP(最大)
hits の単一ストリーミング読み取り(105 カラム)毎秒 110 万行毎秒 76 万 4,000 行毎秒 48 万 6,000 行
16 並列の hits 挿入毎秒 87 万 7,000 行毎秒 83 万 2,000 行毎秒 40 万 5,000 行
SELECT 1(16 インフライト)毎秒 3,100 クエリ毎秒 3,100 クエリ毎秒 1,800 クエリ

メモリ

最大の改善点はメモリにあります。hits の 100 万行読み取りにおいて、HTTP では 1.6 GB がアロケーションされますが、TCP のカラム形式ではわずか 6 MB です。以下のグラフの対数 X 軸に注目してください。

実装の仕組み

TCP クライアントのすべての設計決定は、3 つの原則に基づいて行われました:

  • **ゼロコピー:**データはソケットからカラム独自のバッファへと 1 回だけコピーされます。そのコピーより上位のレイヤーはすべて、そのバッファ上のビューになります。送信時は、すでに保持しているカラムが配列から送信バッファへと 1 回のコピーで送られます(デフォルトで有効な圧縮を使用すると、解凍パスと追加の 1 コピーが発生します。詳細は後述の「ゼロコピーが不可能なケース」を参照してください)。
  • **GC ゼロアロケーション:**厳密に必要な場合を除き、列指向階層で行ごとのアロケーションは発生しません。カラムバッファはプールから取得されてプールへ返却され、残るのはブロックごとのわずかな小さなオブジェクトのみです。これにより、hits の 100 万行読み取りでのアロケーションが 1.6 GB ではなく 6 MB で済むのです。
  • **エンコードとデコードを最小化:**ほとんどの固定長型において、ClickHouse の通信レイアウトと .NET のメモリレイアウトは同一です。そのため、Int64 や Float64 は一切デコードされません。明示的に変換を要求しない限り、DateTime は数値のままであり、文字列は UTF-8 のまま保持されます。

これらの原則を実装するため、私たちは「デコード」する代わりにインプレースで「再解釈」する手法を取り、MemoryMarshal を多用しました。固定長カラムは、単一の読み取りで満たされるプールされた byte[] です。そしてその Values プロパティは何の変換も行わず、バイト列を T として単に再解釈します:

// Reading a fixed-width column (error handling trimmed)
// Borrow the needed space from a pool
int byteCount = checked(rowCount * Unsafe.SizeOf<T>());
byte[] rented = ArrayPool<byte>.Shared.Rent(byteCount);

// A single read copies the entire column from the socket
await reader.ReadBytesAsync(rented.AsMemory(0, byteCount), cancellationToken);

// The column is built around that data
return new PrimitiveColumn<T>(columnName, columnType, rented, byteCount, pooled: true);

// PrimitiveColumn<T>: accessing the values as type T by reinterpreting in place
public ReadOnlySpan<T> Values => MemoryMarshal.Cast<byte, T>(buffer.AsSpan(byteOffset, length));

これは、すべての整数型と浮動小数点型、Bool、生の DateTime カウント値、.NET の Int128、そしてメモリ配置が通信フォーマットと一致するように 4 つのリトルエンディアン ulong として [StructLayout(LayoutKind.Sequential)] 宣言された独自実装の Int256/UInt256 に対応しています。クライアントのコードベースに unsafe コードは一切存在せず、プロジェクト設定でも有効化していません。Span<T>、MemoryMarshal、ArrayPool<T> がすべての処理を担っています。

**一括読み取りによるバッファのスキップ:**パケットヘッダーや長さなどの小さな読み取りは、16 KiB のバッファを経由します。カラムデータは経由しません。ReadBytesAsync はバッファにすでにあるデータをコピーし、残りのカラムデータをソケットからプールされた配列へと直接読み取ります。200 MB のカラムに必要なのは 200 MB の格納先であり、200 MB の読み取りバッファではありません。

**値ではなくビュー:**他の型も通信時の形状をそのまま維持します。Nullable カラムは、生の null マップとゼロコピーの内部カラムで構成されるため、明示的に要求されない限り T? 配列は作成されません。String カラムは、プールされた 1 つのブロブとオフセットで構成されます。GetBytes(row) はコピーなしで UTF-8 バイト列を返し、.NET 文字列は要求されたときにのみ生成されます。LowCardinality(String) は、辞書エントリを行ごとではなくブロックごとに 1 回デコードします。

**書き込み時における逆方向の同じテクニック:**連続した配列で裏付けられたカラムは MemoryMarshal.AsBytes によって送信され、スライス全体に対して 1 回のコピーで済みます:

if (typed is ISpanColumn<T> contiguous)
{
    writer.WriteBytes(MemoryMarshal.AsBytes(contiguous.Span.Slice(start, length)));
    return;
}

複合カラムも同様に動作します。SELECT から読み取ったカラムをそのまま InsertAsync に渡せば、null マップ、オフセット、LowCardinality 辞書が再構築されることなくそのまま送信されます。

スタック上の短寿命バッファ:.NET の string を UTF-8 として送信する場合など、エンコードが必要な箇所では、短い値には stackalloc を使用し、プールを使用するのは長い値のみに限定しています:

int maxBytes = Encoding.UTF8.GetMaxByteCount(value.Length);
if (maxBytes <= 256)
{
    Span<byte> scratch = stackalloc byte[256];
    WriteEncodedString(value, scratch);
}
else
{
    // Rent a buffer from ArrayPool<byte>.Shared, encode, return it.
}

メモリの借用:Block の背後にあるバッファはクライアントが所有します。await foreach が次の反復に進むと、バッファはプールに返却され、次のブロックで再利用されます。これが列指向階層をアロケーションフリーにしている理由であり、ブロックが 1 回の反復の間のみ有効である理由でもあります。ReadOnlySpan<T> は await をまたいで生存できないため、コンパイラも安全性の確保に寄与します。カラムオブジェクトを保持し続けた場合、ループが次に進んだ後に読み取ろうとすると、別のブロックのデータを暗黙的に返すのではなく例外がスローされます。

**カラム優先、その上に行:**借用ルールは最下層に存在する必要があるため、アロケーションフリーな列指向 API を最初に構築し、その上に POCO と object[] の行を構築しました。所有権の移動は正確に 2 回だけ発生します。ソケットからブロックへ(クライアントが所有)、そしてブロックから行またはオブジェクトへ(利用者が所有)です。

リフレクションやボクシングのない POCO

POCO との相互の読み書きにおいて、行ごとのリフレクションは使用しません。クライアントは特定のカラムセットを持つ POCO 型を初めて認識した際、カラムごとに式ツリーを構築してデリゲートにコンパイルし、そのプランをキャッシュします。

読み取り時には、各カラムに対してコンパイル済みのループが 1 つ生成され、行全体の範囲にわたって 1 つのプロパティを設定します。以下は、DateTimeOffset プロパティにマッピングされた DateTime64 カラムのツリーに含まれる内容と、各パーツの生成元を示しています:

値はスパンからプロパティセッターまで型付けされたまま維持されるため、ボクシングは発生せず、行バッチごとに値ごとではなくカラムごとに 1 回のデリゲート呼び出しが行われます。

型変換はループ内で行われます。プロパティの型がカラムの型と異なる場合、カラムのコーデックが変換用の式を提供します。DateTime64 カラムはその生のカウント値をカラムのタイムゾーンにおける DateTimeOffset に変換し、Enum8 は指定の enum になり、Nullable カラムは T? を設定します。

挿入はその鏡像です。各カラムには、全行から 1 つのプロパティをプールされた型付きバッファに読み取るコンパイル済みループが用意されます(destination[slot] = rows[start + slot].Id)。固定長型の場合、コーデックはそのバッファを 1 回のコピーで書き込みます。

注意点として、NativeAOT 環境では式ツリーがコンパイルされずに解釈実行され、解釈実行されるツリーは ReadOnlySpan<T> を保持できません。そのため、NativeAOT では値ごとに 1 回の呼び出しを伴うカラムのインデクサー経由での読み取りプランにフォールバックします。

SIMD

クライアントの大部分では SIMD を必要としません。通信レイアウトが .NET のメモリレイアウトと一致している場合は、単純に 1 回の一括コピーを行うだけです。ベクトルコードを使用するのは、両者のレイアウトが一致せず、さらなる最適化の余地がある場合のみです。

UUID:正しいバイト列と異なる順序

ClickHouse は UUID を 2 つのリトルエンディアン 64 ビット値として保存します。.NET の Guid は、1 つの int、2 つの short、そしてそれに続く 8 つのバイトを順番に保持します。必要なバイトはすべて存在しますが、配置場所が異なります:

定数制御ベクトルを用いた Vector128.Shuffle は、x86 では単一の vpshufb(Arm64 では TBL)にコンパイルされます。読み取り時には、カラム全体をプールされた Guid[] に直接シャッフルします:

ref byte src = ref MemoryMarshal.GetReference(source);
ref byte dst = ref MemoryMarshal.GetReference(destination);
for (int i = 0; i < count; i++)
{
    nuint offset = (nuint)i * UuidSize;
    Vector128.Shuffle(Vector128.LoadUnsafe(ref src, offset), control).StoreUnsafe(ref dst, offset);
}

マイクロベンチマークにおいて、これはバイトを 1 つずつ移動するよりも約 9 倍高速であり、アロケーションも一切発生しません。ベクトル命令をサポートしていないマシンでは、単純なスカラループにフォールバックします。

これを自作する際に知っておくべき点として、.NET 8 および 9 では、JIT が制御ベクトルを定数として認識できる場合にのみ Shuffle() が vpshufb に変換されます。このループは制御ベクトルをパラメータとして受け取りますが、static readonly フィールドが見えている呼び出し元に JIT がインライン展開するため、依然として vpshufb が適用されます。インライン展開されないメソッドに通常のパラメータとして渡すと、代わりにソフトウェアフォールバックへの呼び出しが発生します。.NET 10 では両方のケースで高速化されます。自作コードの挙動は DOTNET_JitDisasm で確認できます。

QBit:横向きに格納されたベクトル

ClickHouse の QBit(T, N) 型は、近似ベクトル検索を高速化するために存在します。N 個の浮動小数点値を受け取り、最上位ビットから順にビットごとに 1 つのプレーン(float32 の場合は 32 プレーン)として格納します。これにより、L2DistanceTransposed(vec, ref, p) などの距離関数が各要素の先頭 p ビットのみを読み取って、精度と引き換えに速度を向上させることができます。挿入時、クライアントはこの転置処理を行う必要があります:

1 ビットずつ処理すると、要素あたり 32 回の演算が必要になります。Vector256 を使用すると、ExtractMostSignificantBits によって 8 つの浮動小数点数の最上位ビットを 1 バイトに集約できます。これは通信時のビット順序と一致した、まさにプレーンの 1 バイト分になります。すべてのレーンを左に 1 ビットシフトすれば、次のビットが最上位に来ます:

Vector256<uint> lanes = Vector256.LoadUnsafe(ref source, (nuint)(group << 3));
int slot = rowBase + QBitLayout.ByteOfGroup(group, BytesPerRow);
for (int wireIndex = 0; wireIndex < BitWidth; wireIndex++)
{
    scratch[(wireIndex * planeStride) + slot] = (byte)lanes.ExtractMostSignificantBits();
    lanes <<= 1;
}

JIT はこのループ本体を 1 つの vmovmskps と 1 つの vpslld に変換します。これにより、書き込みパスは 1,024 次元で 11.7 倍、768 次元で 16.5 倍高速化されました。

ゼロコピーが不可能なケース

ゼロコピーが基本原則ですが、通信フォーマットと .NET のメモリレイアウトが一致しない場合(またはオブジェクトをインスタンス化する必要がある場合)は不可能です。

最も顕著な例は文字列です。ClickHouse は生のバイト列(通常は UTF-8 ですが、任意のエンコーディングの可能性があります)を送信し、.NET の string は UTF-16 であるため、要求された文字列はすべてデコードおよびアロケーションされます。UTF-8 を直接扱える場合は、IStringColumn.GetBytes で生のバイト配列を直接取得できます。文字列は 1 回のコピーで書き込むこともできません。ネットワーク上のすべての値に独自の長さプレフィックスが付与されているため、クライアントはそれらを 1 つずつ書き込みます(FixedString にはプレフィックスがないため、一括書き込みが可能です)。

一部の型は読み取り時に形状が変わります。IP アドレスは行あたり 1 つの IPAddress オブジェクトになり、Date は DateOnly になり、10 進数は decimal または独自の ClickHouseTcpDecimal に変換されます。これらはすべてブロックが到着した直後に行われます。また挿入時、Nullable 値やタプルフィールドはデータ内で連続した領域を持たないため、1 回のコピーではなく値ごとに書き込まれます。

HTTP クライアントからの移行

どちらか一方だけに絞る必要はありません。両方のクライアントが ClickHouse.Driver に同梱されているため、クリティカルパスを TCP に移行しつつ、ネイティブクライアントがまだ対応していない ORM、ファイル形式、トークン認証などには HTTP を維持することができます。チェックリストは以下の通りです:

**ネイティブポートへ接続:**ポートは 9000、または UseTls=true の場合は 9440 です。サーバー設定値は文字列です。接続文字列内では set_ プレフィックスを付けて指定します(set_max_threads=4)。依存性注入(DI)を使用する場合、services.AddClickHouseTcpDataSource(...) によってプールされたクライアントが 1 つ登録され、IClickHouseTcpClient が提供されます。

**メソッドのマッピング:**ほとんどの呼び出しに直接対応するメソッドがあります:

HTTP クライアントTCP クライアント備考
ExecuteNonQueryAsyncExecuteAsyncDDL クエリ
ExecuteScalarAsyncExecuteScalarAsync1 つの値、最初のカラムの先頭行
ExecuteReaderAsyncQueryAsync / QueryAsync<T> / StreamAsync直接の対応物なし
QueryAsync<T>QueryAsync<T>POCO の読み取り
InsertBinaryAsyncInsertRowsAsync<T>、InsertRowsAsync、または InsertAsync挿入
PingAsyncPingAsyncPing

パラメータの書き換え:@name プレースホルダーは {name:Type} に変更し、値は ClickHouseTcpQueryOptions.Parameters に移動します。日時データには {t:DateTime64(3, 'UTC')} のようにタイムゾーンを指定してください。

まずは POCO から始め、クリティカルなループをカラム形式に移行:QueryAsync<T> と InsertRowsAsync<T> は、現在のコードに最も近い形式です:

// HTTP
using var reader = await http.ExecuteReaderAsync("SELECT id, name FROM events");
while (reader.Read())
{
    Console.WriteLine($"{reader.GetFieldValue<ulong>(0)}: {reader.GetString(1)}");
}

// TCP
await foreach (Event e in tcp.QueryAsync<Event>("SELECT id, name FROM events"))
{
    Console.WriteLine($"{e.Id}: {e.Name}");
}

それが動作したら、重要なループを StreamAsync と InsertAsync に切り替えます。

最後に、動作上の破壊的変更を確認:

  • DateTime カラムは、カラム形式および object[] 階層では生の数値として返されます。POCO プロパティは自動的に変換されます。
  • 接続をプールに返却するため、すべての列挙(enumeration)を完了させるか破棄(dispose)してください。
  • 結果の読み取りを途中で中断したりキャンセルしたりすると、接続は再利用されずに閉じられます。
  • MaxPoolSize(デフォルトは 20)によって、同時に実行できる操作数が制限されます。
  • ReadTimeout はクエリの合計時間ではなく、読み取り間の無通信時間を計測します。
  • 自動リトライ機能はありません。再試行する可能性のある挿入には、DeduplicationToken を設定してください。
  • 通常の呼び出しはプール内の任意の接続に送られる可能性があるため、一時テーブルや SET を使用する場合は OpenSessionAsync が必要です。
  • JSON はテキストとして届きます。

詳細についてはドキュメントの移行ガイドを参照してください。また、Tcp_004_MigratingFromHttp.cs では同じテーブルに対して両方のクライアントを実行するコードを確認できます。

今後の予定

最初に取り組むのは ADO.NET です。ネイティブプロトコル上で動作する DbConnection を提供し、Dapper、EF Core、linq2db でも利用できるようにします。その後、バイナリ JSON に対応し、JSON カラムが余分なシリアライズおよびデシリアライズの処理を経由しなくて済むようにする予定です。

ぜひフィードバックをお寄せください。実際のワークロードでクライアントを試していただき、動作しなかった点、使いづらい点、不足している機能などを教えてください。GitHub issue からご連絡いただけます。API は現在まだ試験運用版(experimental)であるため、皆様からのフィードバックを設計に反映させるには今が最適なタイミングです。

今すぐ始める

実際のデータで ClickHouse を試してみませんか? ClickHouse Cloud をわずか数分で開始でき、300 ドル分の無料クレジットも獲得できます。

サインアップ

この記事をシェア

  • Y Combinator icon
  • X icon
  • Bluesky icon
  • Facebook icon
  • LinkedIn icon

Subscribe to our newsletter

Stay informed on feature releases, product roadmap, support, and cloud offerings!

Follow us

XBlueskySlackGithubTelegramMeetupRSS