JSONモデリングの他のアプローチ
以下はClickHouseにおけるJSONのモデリングの代替手法です。これらは、JSON型の開発前に適用できたものであり、完全性のために文書化されています。そのため、多くのユースケースにおいては一般的には推奨されず、適用されません。
異なるテクニックは、同じスキーマ内の異なるオブジェクトに対して適用される場合があります。例えば、一部のオブジェクトはString
型を使用するのが最適であり、他のものはMap
型を使用するのが最適です。String
型が一度使用されると、それ以上のスキーマの決定を行う必要はありません。一方で、Map
のキー内にサブオブジェクトをネストすることも可能です - JSONを表すString
を含む形で、以下に示す通りです。
Stringを使用する
オブジェクトが非常に動的で、予測できない構造を持ち、任意のネストされたオブジェクトが含まれている場合、ユーザーはString
型を使用するべきです。値は、以下に示すようにJSON関数を使用してクエリ時に抽出できます。
上記のように構造化アプローチでデータを扱うことは、動的JSONを持つユーザーにとっては実行可能でないことがしばしばあります。これは、変更が加えられる可能性があるか、スキーマが十分に理解されていない場合です。絶対的な柔軟性のために、ユーザーは単にJSONをString
として保存し、必要に応じてフィールドを抽出するための関数を使用できます。これは、JSONを構造化されたオブジェクトとして扱うことの真逆を表しています。この柔軟性には、重要な欠点が伴い、主にクエリの構文の複雑さの増加やパフォーマンスの劣化をもたらします。
前述の通り、元の人オブジェクトについては、tags
カラムの構造を保証することはできません。元の行(company.labels
を含むが、ここでは無視します)を挿入し、Tags
カラムをString
として宣言します:
tags
カラムを選択すると、JSONが文字列として挿入されたことがわかります:
JSONExtract
関数を使用して、このJSONから値を取得できます。以下の簡単な例を考えてみましょう:
関数は、String
カラムtags
への参照と、抽出するためのJSON内のパスの両方を必要とすることに注意してください。ネストされたパスは、関数をネストさせる必要があります。例えば、JSONExtractUInt(JSONExtractString(tags, 'car'), 'year')
は、カラムtags.car.year
を抽出します。ネストされたパスの抽出は、関数JSON_QUERY
およびJSON_VALUE
を通じて簡素化できます。
arxiv
データセットの極端なケースを考えてみましょう。このデータセットでは、本文全体をString
として扱います。
このスキーマに挿入するには、JSONAsString
形式を使用する必要があります:
年ごとにリリースされた論文の数をカウントしたい場合、以下のクエリを考えてみましょう。単なる文字列を使用した場合と構造化バージョンのスキーマを対比させます:
ここでのXPath式の使用に注目してください。これはメソッドによってJSONをフィルタリングします。すなわち、JSON_VALUE(body, '$.versions[0].created')
です。
String関数は明らかに遅く(> 10倍)、インデックスを用いた明示的な型変換に比べてパフォーマンスが劣ります。上記のクエリは常に全表スキャンと各行の処理を要求します。このようなクエリは、このような小規模なデータセットでは依然として速いですが、大規模なデータセットではパフォーマンスが劣化します。
このアプローチの柔軟性は、明確なパフォーマンスと構文コストを伴い、スキーマ内の非常に動的なオブジェクトにのみ使用すべきです。
シンプルなJSON関数
上記の例では、JSON*ファミリーの関数が使用されています。これらは、simdjsonに基づくフルJSONパーサーを利用しており、厳密に解析され、異なるレベルでネストされた同じフィールドを区別します。これらの関数は、文法的には正しいが適切にフォーマットされていないJSON(例えば、キー間に二重スペースがある場合)も処理できます。
より高速で厳密な関数セットも利用可能です。これらのsimpleJSON*
関数は、主にJSONの構造とフォーマットについて厳密な仮定を行うことにより、潜在的に優れたパフォーマンスを提供します。具体的には:
-
フィールド名は定数でなければなりません
-
フィールド名の一貫したエンコーディング(例:
simpleJSONHas('{"abc":"def"}', 'abc') = 1
)が必要ですが、visitParamHas('{"\\u0061\\u0062\\u0063":"def"}', 'abc') = 0
は無効です -
フィールド名は、すべてのネストされた構造の間で一意である必要があります。ネストのレベル間での区別は行われず、マッチは無差別に行われます。複数のフィールドが一致する場合、最初に現れたものが使用されます。
-
文字列リテラル以外の特殊文字はありません。これにはスペースも含まれます。以下は無効であり、解析されません。
次の例は正しく解析されます:
上記のクエリは、simpleJSONExtractString
を使用してcreated
キーを抽出し、公開日の日付のみを必要とするという事実を利用しています。この場合、simpleJSON*
関数の制限は、パフォーマンスの向上を伴って許容されるものとなります。
Mapを使用する
任意のキーを格納するためにオブジェクトが使用される場合、主に1つのタイプの値を持つ場合は、Map
型を使用することを検討してください。理想的には、ユニークなキーの数は数百を超えないべきです。Map
型は、サブオブジェクトを持つオブジェクトにも使用できますが、その場合は一貫性が必要です。一般的に、Map
型はラベルやタグに使用することが推奨されます。例えば、ログデータのKubernetesポッドラベルなどです。
Map
はネストされた構造を表現する簡単な方法を提供しますが、いくつかの顕著な制限があります:
- フィールドはすべて同じ型でなければなりません。
- サブカラムにアクセスするには特別なマップ構文が必要です。なぜなら、フィールドはカラムとして存在しないからです。オブジェクト全体がカラムです。
- サブカラムにアクセスする際には、全ての兄弟とそれぞれの値を含む
Map
値をロードします。大きなマップの場合、これには重大なパフォーマンスペナルティが伴う可能性があります。
オブジェクトをMap
としてモデリングする際には、String
キーを使用してJSONキー名を格納します。したがって、マップは常にMap(String, T)
となり、ここでT
はデータに依存します。
プリミティブ値
Map
の最もシンプルな適用は、オブジェクトが同じプリミティブ型の値を含む場合です。ほとんどの場合、これはString
型の値T
を使用することを含みます。
先ほどの人物JSONを考えてみましょう。ここでは、company.labels
オブジェクトが動的であることが決定されていました。重要なことに、このオブジェクトには文字列型のキーと値のペアのみが追加されることが期待されています。したがって、これをMap(String, String)
として宣言できます:
元の完全なJSONオブジェクトを挿入します:
リクエストオブジェクト内のこれらのフィールドをクエリするには、マップ構文が必要です。例えば:
マップ関数の完全なセットが、クエリするために利用可能であり、こちらに説明されています。データが一貫した型でない場合、必要な型変換を実行するための関数が存在します。
オブジェクト値
Map
型は、サブオブジェクトがあるオブジェクトにも考慮できる場合がありますが、その場合は一貫性が必要です。
例えば、persons
オブジェクトのtags
キーが一貫した構造を要求する場合、各tag
のサブオブジェクトはname
とtime
カラムを持つ必要があります。このようなJSONドキュメントの簡易例は以下のようになります:
これは、Map(String, Tuple(name String, time DateTime))
を使用してモデル化できます。以下を参照してください:
この場合のマップの適用は通常稀であり、動的キー名を持つデータをサブオブジェクトなしで再設計する必要があることを示唆しています。たとえば、上記は次のように再設計され、Array(Tuple(key String, name String, time DateTime))
の使用を許可します。
Nestedを使用する
Nested型は、静的オブジェクトをモデル化するために使用できます。これらは変更されることが滅多にないため、Tuple
やArray(Tuple)
の代替手段を提供します。一般的に、このタイプをJSONに使用することは避けるべきです。なぜなら、その挙動はしばしば混乱を招くからです。Nested
の主な利点は、サブカラムをオーダリングキーで使用できることです。
以下に、静的オブジェクトをモデル化するためにNested型を使用する例を示します。以下は、JSONのシンプルなログエントリです:
request
キーをNested
として宣言できます。Tuple
と同様に、サブカラムを指定する必要があります。
flatten_nested
設定flatten_nested
は、ネストされた動作を制御します。
flatten_nested=1
値が1
(デフォルト)は、任意のレベルのネスティングをサポートしません。この値では、ネストされたデータ構造を長さが同じ複数のArrayカラムとして考えることが最も簡単です。フィールドmethod
、path
、およびversion
はすべて実質的に別々のArray(Type)
カラムであり、1つの重要な制約があります:method
、path
、およびversion
フィールドの長さは同じでなければなりません。 SHOW CREATE TABLE
を使用すると、以下のように示されます:
以下に、このテーブルに挿入します:
ここで注目すべき重要なポイントがいくつかあります:
-
JSONをネストされた構造として挿入するために、設定
input_format_import_nested_json
を使用する必要があります。これがないと、JSONをフラットにする必要があります。すなわち、 -
ネストされたフィールド
method
、path
、version
は、JSON配列として渡す必要があります。すなわち、
カラムはドット表記を使用してクエリできます:
サブカラム'のArray
を使用することにより、Array関数が活用できることに注意してください。特に、ARRAY JOIN句が役立つ場合があります - もしあなたのカラムに複数の値がある場合です。
flatten_nested=0
これは任意のレベルのネスティングを許可し、ネストされたカラムを単一のTuple
の配列として保持します - 事実的に、これらはArray(Tuple)
と同じになります。
これは好ましい方法であり、しばしばJSONをNested
に使用する最もシンプルな方法です。以下に示すように、すべてのオブジェクトをリストにするだけで済みます。
以下に、テーブルを再作成し、行を再挿入します:
ここで注意すべき重要なポイントがいくつかあります:
-
挿入には
input_format_import_nested_json
は必要ありません。 -
Nested
型はSHOW CREATE TABLE
で保持されます。この列は実質的にArray(Tuple(Nested(method LowCardinality(String), path String, version LowCardinality(String))))
です。 -
結果として、
request
を配列として挿入する必要があります。すなわち、
カラムは再びドット表記を用いてクエリできます:
例
上記のデータのより大きな例が、S3のパブリックバケットにあります: s3://datasets-documentation/http/
。
制約とJSONの入力形式に基づいて、このサンプルデータセットを挿入するには、以下のクエリを実行します。ここでは、flatten_nested=0
を設定します。
次の文は1000万行を挿入するため、実行には数分かかる場合があります。必要に応じてLIMIT
を適用してください:
このデータをクエリするには、要求フィールドにアクセスするために配列としてアクセスする必要があります。以下に、固定された時間範囲内でのエラーおよびHTTPメソッドを要約します。
ペアワイズ配列を使用する
ペアワイズ配列は、JSONを文字列として表現する柔軟性と、より構造化されたアプローチのパフォーマンスのバランスを提供します。このスキーマは柔軟性があり、新しいフィールドをルートに追加することができます。ただし、これはかなり複雑なクエリ構文を必要とし、ネストされた構造とは互換性がありません。
たとえば、次のようなテーブルを考えてみましょう:
このテーブルに挿入するには、JSONをキーと値のリストとして構造化する必要があります。以下のクエリは、JSONExtractKeysAndValues
を使用してこれを達成する例を示しています:
要求カラムは、文字列として表されたネストされた構造のままであることに注意してください。ルートに新しいキーを追加することができ、JSON自体にも任意の違いを持つことができます。ローカルテーブルに挿入するには、次のクエリを実行します:
この構造をクエリするには、indexOf
関数を使用して必要なキーのインデックスを特定する必要があります(これは値の順序と一致する必要があります)。これを使用して値配列カラムにアクセスできます。すなわち、values[indexOf(keys, 'status')]
。要求カラムに対しては、JSON解析メソッドが引き続き必要です。この場合、simpleJSONExtractString
ですが、