---
title: ".NET 向け(極めて高速な！)ネイティブ TCP クライアントのご紹介"
date: "2026-10-19T02:50:20.213Z"
author: "Alex Soffronow Pagonidis"
category: "Engineering"
excerpt: "ClickHouse の新しい .NET 向けネイティブ TCP クライアントは、アロケーションを抑えつつ読み取りと挿入を高速化し、列指向 API や POCO サポート、HTTP からの移行手段を提供します。"
---

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

## TL;DR {#tldr}

- `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 クライアントの完全なドロップイン代替ではありません。

## 目的別の読み進め方 {#pick_your_path}

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

- すぐに試したい場合 → [クイックスタート](#user-content-quick_start)
- なぜ第 2 のクライアントを作ったのか？ → [TCP を採用した理由と相違点](#user-content-why_tcp_and_how_is_it_different)
- 移行すべきか？ → [メリットとデメリット](#user-content-pros_and_cons_it_is_not_all_roses_and_rainbows)
- 数値データを見たい場合 → [性能](#user-content-performance)
- 技術的な詳細を知りたい場合 → [実装の仕組み](#user-content-how_we_did_it)
- アプリケーションを HTTP から移行したい場合 → [HTTP クライアントからの移行](#user-content-migrating_from_the_http_client)

## クイックスタート {#quick_start}

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

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

ここからのステップ:

- [**TCP クライアントのドキュメント**](https://clickhouse.com/docs/ja/integrations/language-clients/csharp/tcp)では、設定、型マッピング、セッション、オブザーバビリティ、HTTP から TCP への詳細な移行対応表を解説しています。
- 読み取り、挿入、型、セッション、TLS、オブザーバビリティ、エラーハンドリングなど、一般的なユースケースを網羅した 30 個の[**サンプル**](https://github.com/ClickHouse/clickhouse-cs/tree/main/examples/Tcp)も用意しています。

## TCP を採用した理由と相違点 {#why_tcp_and_how_is_it_different}

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

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

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

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

### 2 つのプロトコルの違い

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

![](https://clickhouse.com/uploads/fig_reads_acf5c87d54.png)

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

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

![](https://clickhouse.com/uploads/fig_array_e9f1f67fe6.png)

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

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

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

## メリットとデメリット {#pros_and_cons_it_is_not_all_roses_and_rainbows}

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

**得られるメリット**

- **速度:**デコードなしの列指向読み取りが可能です。`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 を使い続けてください。

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

## 性能 {#performance}

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` [データセット](https://clickhouse.com/docs/ja/get-started/sample-datasets/anon-web-analytics-metrica)(スカラカラム 105 個)の 100 万行の読み取りと 10 万行の挿入、`Array`、`Map`、`Tuple` カラムを持つテーブル、および 1 つの `JSON` カラムを持つテーブル。
- **測定値:**各シナリオを 1 つのプロセス内で 5 回実行。ウォームアップ後の 4 回の中央値を報告。

![](https://clickhouse.com/uploads/fig_results_v3_143a1c059c.png)

特筆すべき点:

**カラム形式の読み取りでは所要時間が半分未満に短縮**(`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 軸に注目してください。

![](https://clickhouse.com/uploads/fig_alloc_c35731ed89.png)

## 実装の仕組み {#how_we_did_it}

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

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

これらの原則を実装するため、私たちは「デコード」する代わりにインプレースで「再解釈」する手法を取り、[`MemoryMarshal`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.memorymarshal?view=net-10.0) を多用しました。固定長カラムは、単一の読み取りで満たされるプールされた `byte[]` です。そしてその `Values` プロパティは何の変換も行わず、バイト列を `T` として単に再解釈します:

```csharp
// 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 回のコピーで済みます:

```csharp
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` を使用し、プールを使用するのは長い値のみに限定しています:

```csharp
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` カラムのツリーに含まれる内容と、各パーツの生成元を示しています:

![](https://clickhouse.com/uploads/fig_poco_5802a2a123.png)

値はスパンからプロパティセッターまで型付けされたまま維持されるため、ボクシングは発生せず、行バッチごとに値ごとではなくカラムごとに 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 つのバイトを順番に保持します。必要なバイトはすべて存在しますが、配置場所が異なります:

![](https://clickhouse.com/uploads/fig_uuid_50e2a177f7.png)

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

```csharp
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` ビットのみを読み取って、精度と引き換えに速度を向上させることができます。挿入時、クライアントはこの転置処理を行う必要があります:

![](https://clickhouse.com/uploads/fig_qbit_28d6ab5ed3.png)

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

```csharp
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 クライアントからの移行 {#migrating_from_the_http_client}

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

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

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

| HTTP クライアント | TCP クライアント | 備考 |
| --- | --- | --- |
| `ExecuteNonQueryAsync` | `ExecuteAsync` | DDL クエリ |
| `ExecuteScalarAsync` | `ExecuteScalarAsync` | 1 つの値、最初のカラムの先頭行 |
| `ExecuteReaderAsync` | `QueryAsync` / `QueryAsync<T>` / `StreamAsync` | 直接の対応物なし |
| `QueryAsync<T>` | `QueryAsync<T>` | POCO の読み取り |
| `InsertBinaryAsync` | `InsertRowsAsync<T>`、`InsertRowsAsync`、または `InsertAsync` | 挿入 |
| `PingAsync` | `PingAsync` | Ping |

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

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

```csharp
// 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 はテキストとして届きます。

詳細についてはドキュメントの[移行ガイド](https://clickhouse.com/docs/ja/integrations/language-clients/csharp/tcp#migrating-from-http)を参照してください。また、[`Tcp_004_MigratingFromHttp.cs`](https://github.com/ClickHouse/clickhouse-cs/blob/main/examples/Tcp/Core/Tcp_004_MigratingFromHttp.cs) では同じテーブルに対して両方のクライアントを実行するコードを確認できます。

### 今後の予定

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

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

---

## 今すぐ始める

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

[サインアップ](https://console.clickhouse.cloud/signUp?loc=blog-cta-2736&utm_blogctaid=2736)

---