ClickHouse/docs/ja/sql-reference/operators/in.md
2024-11-18 11:58:58 +09:00

15 KiB
Raw Blame History

slug
/ja/sql-reference/operators/in

IN 演算子

INNOT INGLOBAL INGLOBAL NOT IN 演算子は、その機能が非常に豊富であるため、個別に説明されます。

演算子の左側は単一のカラムまたはタプルです。

例:

SELECT UserID IN (123, 456) FROM ...
SELECT (CounterID, UserID) IN ((34, 123), (101500, 456)) FROM ...

左側がインデックスにある単一のカラムで、右側が定数の集合である場合、システムはクエリの処理にインデックスを使用します。

あまりにも多くの値を明示的にリストしないでください(例えば、数百万)。データセットが大きい場合は、一時テーブルに置きます(例えば、クエリ処理のための外部データのセクションを参照)、そしてサブクエリを使用します。

演算子の右側は、定数式の集合、定数式を持つタプルの集合(上記の例に示される)、またはデータベースのテーブル名や括弧内のSELECTサブクエリであることができます。

ClickHouseは、INサブクエリの左側と右側の型が異なることを許可します。この場合、左側の値を右側の型に変換します。accurateCastOrNull関数が適用されるかのようにです。つまり、データ型はNullableとなり、変換が実行できない場合はNULLを返します。

クエリ:

SELECT '1' IN (SELECT 1);

結果:

┌─in('1', _subquery49)─┐
│                    1 │
└──────────────────────┘

演算子の右側がテーブル名の場合(例えば、UserID IN users)、これはサブクエリ UserID IN (SELECT * FROM users) と同等です。クエリと一緒に送信される外部データを扱うときにこれを使用します。例えば、クエリは、ユーザーのIDセットを一時テーブルの「users」に読み込んでフィルタリングする必要がある場合に送信されます。

演算子の右側が、常にRAMに存在する準備済みデータセットを持つSetエンジンを持つテーブル名である場合、データセットは各クエリのために再作成されません。

サブクエリは、タプルをフィルタリングするために複数のカラムを指定することがあります。

例:

SELECT (CounterID, UserID) IN (SELECT CounterID, UserID FROM ...) FROM ...

IN演算子の左側と右側のカラムは同じ型を持つ必要があります。

IN演算子とサブクエリは、クエリの任意の部分(集約関数やラムダ関数も含む)に出現することができます。 例:

SELECT
    EventDate,
    avg(UserID IN
    (
        SELECT UserID
        FROM test.hits
        WHERE EventDate = toDate('2014-03-17')
    )) AS ratio
FROM test.hits
GROUP BY EventDate
ORDER BY EventDate ASC
┌──EventDate─┬────ratio─┐
│ 2014-03-17 │        1 │
│ 2014-03-18 │ 0.807696 │
│ 2014-03-19 │ 0.755406 │
│ 2014-03-20 │ 0.723218 │
│ 2014-03-21 │ 0.697021 │
│ 2014-03-22 │ 0.647851 │
│ 2014-03-23 │ 0.648416 │
└────────────┴──────────┘

3月17日以降の各日付について、その日にこのサイトを訪れたユーザーによるページビューの割合を計算します。 IN句内のサブクエリは、単一のサーバー上で一度だけ実行されます。依存サブクエリはありません。

NULL 処理

リクエスト処理中、IN演算子は、NULL との操作結果が常に 0 に等しいと仮定します。演算子の右側または左側に NULL があっても関連しません。NULL 値はデータセットに含まれず、相互に対応せず、transform_null_in = 0 の場合に比較されません。

t_null テーブルを使用した例を示します:

┌─x─┬────y─┐
│ 1 │ ᴺᵁᴸᴸ │
│ 2 │    3 │
└───┴──────┘

クエリ SELECT x FROM t_null WHERE y IN (NULL,3) を実行すると、次の結果が得られます:

┌─x─┐
│ 2 │
└───┘

y = NULL の行がクエリ結果から除外されていることがわかります。これは、ClickHouse が(NULL,3) 集合に NULL が含まれるかどうかを決定できず、操作の結果として 0 を返し、SELECT がこの行を最終出力から除外したためです。

SELECT y IN (NULL, 3)
FROM t_null
┌─in(y, tuple(NULL, 3))─┐
│                     0 │
│                     1 │
└───────────────────────┘

分散サブクエリ

IN演算子とサブクエリには、JOIN演算子に似た2つのオプションがあります。それは通常のIN / JOINGLOBAL IN / GLOBAL JOIN です。これらは、分散クエリ処理の方法が異なります。

:::note 以下に説明するアルゴリズムは、設定 distributed_product_mode に依存して、異なる動作をすることがあります。 :::

通常のINを使用すると、クエリはリモートサーバーに送信され、それぞれがINまたはJOIN句のサブクエリを実行します。

GLOBAL IN / GLOBAL JOIN を使用すると、まず全てのGLOBAL IN / GLOBAL JOIN用のサブクエリが実行され、その結果が一時テーブルに収集されます。その後、一時テーブルは各リモートサーバーに送信され、クエリはこの一時データを使用して実行されます。

分散クエリでない場合は、通常のIN / JOINを使用します。

分散クエリ処理のためにIN / JOIN句内のサブクエリを使用する際は注意が必要です。

例を見てみましょう。クラスター内の各サーバーには通常の local_table があると仮定します。各サーバーにもクラスター内の全サーバーを対象としたDistributed型の distributed_table テーブルがあります。

distributed_table に対するクエリの場合、クエリはすべてのリモートサーバーに送信され、それらで local_table を使用して実行されます。

例えば、クエリ

SELECT uniq(UserID) FROM distributed_table

はすべてのリモートサーバーに

SELECT uniq(UserID) FROM local_table

として送信され、それぞれで並行して実行され、中間結果が結合できるステージに達するまで実行されます。その後、中間結果は要求元サーバーに返され、そこでマージされ、クライアントに最終結果が送られます。

IN を使用したクエリを見てみましょう:

SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM local_table WHERE CounterID = 34)
  • 2つのサイトのオーディエンスの交差の計算。

このクエリはすべてのリモートサーバーに次のように送信されます

SELECT uniq(UserID) FROM local_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM local_table WHERE CounterID = 34)

つまり、IN句内のデータセットは、各サーバーに格納されているローカルデータのみを対象にして、各サーバーに独立して収集されます。

これは、クラスタサーバー全体でデータをユーザーIDごとに単一のサーバーに完全に存在するようにデータを分散した場合、正確かつ最適に機能します。この場合、必要なデータはすべてのサーバーでローカルに利用可能になります。そうでない場合、結果は不正確になります。このクエリの変形を「ローカルIN」と呼びます。

クラスタサーバー全体にデータがランダムに分散されている場合に、クエリの動作を修正するには、サブクエリ内に distributed_table を指定することができます。クエリは以下のようになります:

SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM distributed_table WHERE CounterID = 34)

このクエリはすべてのリモートサーバーに次のように送信されます

SELECT uniq(UserID) FROM local_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM distributed_table WHERE CounterID = 34)

サブクエリは各リモートサーバー上で開始されます。サブクエリが分散テーブルを使用しているため、各リモートサーバー上のサブクエリは、次のように各リモートサーバーに再送信されます:

SELECT UserID FROM local_table WHERE CounterID = 34

例えば、100台のサーバーのクラスターがある場合、クエリ全体を実行するには10,000の基本的なリクエストが必要であり、これは一般には許容されません。

このような場合、通常の IN の代わりに常に GLOBAL IN を使用すべきです。クエリの動作を見てみましょう:

SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID GLOBAL IN (SELECT UserID FROM distributed_table WHERE CounterID = 34)

要求元サーバーはサブクエリを実行します:

SELECT UserID FROM distributed_table WHERE CounterID = 34

その結果はRAMにある一時テーブルに格納されます。その後、クエリは各リモートサーバーに次のように送信されます:

SELECT uniq(UserID) FROM local_table WHERE CounterID = 101500 AND UserID GLOBAL IN _data1

一時テーブル _data1 はクエリと共に各リモートサーバーに送られます(一時テーブルの名前は実装定義です)。

これは通常の IN を使用するよりも最適ですが、以下の点に注意してください:

  1. 一時テーブルを作成する際には、データは一意にはされません。ネットワーク上で送信されるデータの量を減らすために、サブクエリでDISTINCTを指定してください。通常のINの場合はこれを行う必要はありません。)
  2. 一時テーブルはすべてのリモートサーバーに送られます。ネットワークトポロジは考慮されません。例えば、10台のリモートサーバーが要求元サーバーに対して非常に遠くにあるデータセンターにある場合、そのデータはリモートデータセンターへのチャネルを10回送信されます。GLOBAL INを使用する際は、大規模なデータセットを避けてください。
  3. リモートサーバーへのデータ転送時に、ネットワークの帯域幅制限は構成可能ではありません。ネットワークを過負荷にする可能性があります。
  4. GLOBAL INを定期的に使用する必要がないように、サーバー間でデータを分散してください。
  5. GLOBAL INを頻繁に使用する必要がある場合、ClickHouseクラスターの場所を計画し、単一のデータセンター内で単一のレプリカグループが存在し、迅速なネットワークが確保され、クエリが単一のデータセンター内で完全に処理できるようにします。

要求元サーバーにのみ利用可能なローカルテーブルがあり、それをリモートサーバーで使用する必要がある場合には、GLOBAL IN句内でローカルテーブルを指定するのも意味があります。

分散サブクエリと max_rows_in_set

max_rows_in_setmax_bytes_in_set を使用して、分散クエリ中に転送されるデータ量を制御することができます。

特に、GLOBAL IN クエリが大量のデータを返す場合が重要です。次のSQLを考えてみましょう:

select * from table1 where col1 global in (select col1 from table2 where <some_predicate>)

some_predicate が十分に選択的でない場合、大量のデータを返してパフォーマンスの問題を引き起こす可能性があります。このような場合、ネットワーク越しのデータ転送を制限することが賢明です。また、set_overflow_modethrow に設定されているため(デフォルト)、これらのしきい値に達した時に例外が発生することに注意してください。

分散サブクエリと max_parallel_replicas

max_parallel_replicas が1より大きい場合、分散クエリはさらに変換されます。

例を示します:

SELECT CounterID, count() FROM distributed_table_1 WHERE UserID IN (SELECT UserID FROM local_table_2 WHERE CounterID < 100)
SETTINGS max_parallel_replicas=3

このクエリは各サーバー上で以下のように変換されます:

SELECT CounterID, count() FROM local_table_1 WHERE UserID IN (SELECT UserID FROM local_table_2 WHERE CounterID < 100)
SETTINGS parallel_replicas_count=3, parallel_replicas_offset=M

ここで、M はローカルクエリが実行されているレプリカに応じて 1 から 3 の間です。

これらの設定はクエリ内のすべてのMergeTreeファミリーのテーブルに影響を与え、各テーブルにSAMPLE 1/3 OFFSET (M-1)/3を適用するのと同じ効果があります。

そのため、max_parallel_replicas設定を追加することで、両方のテーブルが同じレプリケーションスキームを持ち、UserIDまたはその部分キーによってサンプリングされる場合のみ、正しい結果が得られます。特に、local_table_2にサンプリングキーがない場合、不正な結果が生成されます。同じルールがJOINにも適用されます。

local_table_2が要件を満たさない場合の回避策として、GLOBAL INまたはGLOBAL JOINを使用することができます。

テーブルにサンプリングキーがない場合、parallel_replicas_custom_keyのより柔軟なオプションを使用することができます。これにより、異なる、より最適な動作を生成する可能性があります。