テーブルモデルのベストプラクティス
このドキュメントでは、モデルの選択、テーブルスキーマの設計、モデルの制約という観点から、Doris のデータモデルを使用する際の注意点を説明します。適切なモデルを選び、一般的なパフォーマンス上の落とし穴を避けるために役立ちます。
データモデルを選択する際の推奨事項
データモデルはテーブルの作成時に決まり、後から変更できません。そのため、適切なデータモデルを選ぶことが重要です。
三つのモデルの比較
| データモデル | 適した用途 | 利点 | 注意点 |
|---|---|---|---|
| Duplicate | 任意のディメンションに対するアドホッククエリ | 集約モデルの制約を受けず、列指向ストレージの利点を十分に活用できる(必要な列だけを読み取る) | 事前集約は利用できない(ただし、非同期マテリアライズドビューを通じて利用可能) |
| Unique | 主キーの一意性とデータの更新が必要な場合 | 主キーの一意性を保証し、柔軟な更新をサポートする | 事前集約は利用できない(ただし、非同期マテリアライズドビューを通じて利用可能) |
| Aggregate | パターンが決まっているレポート用クエリ | 事前集約によって、クエリ時にスキャンおよび計算するデータ量を大幅に削減できる | count(*) クエリには不向き。宣言された集約タイプとは異なる集約クエリを実行する際は、結果の意味に注意すること |
列の部分更新が必要な場合
業務で列の部分更新が必要な場合は、以下のドキュメントで使用上の推奨事項を確認してください。
テーブル作成時の列型に関する推奨事項
テーブルを作成する際は、列型と列の順序について次の規則に従ってください。
- 列の順序: Key 列はすべての Value 列より前に配置する必要があります。
- 整数型を優先する: 整数の計算や検索は文字列よりはるかに効率的なため、可能な限り整数型を使用します。
- 必要十分な長さの整数型を選ぶ: 長さの異なる整数型から選ぶ際は、「必要な分だけ」という原則に従います。
- 必要十分な長さの可変長文字列を選ぶ: VARCHAR 型と STRING 型の長さについても、同じ「必要な分だけ」という原則に従います。
モデルによる Key 列の意味の違い
Duplicate、Aggregate、Unique の各モデルでは、テーブルの作成時に Key 列を指定しますが、その意味は異なります。
| モデル | Key 列の役割 |
|---|---|
| Duplicate | 「ソート列」としてのみ機能し、行を一意に識別するものではない |
| Aggregate、Unique | 「ソート列」と「行を一意に識別する列」の両方として機能し、本来の意味での Key 列となる |
Aggregate モデルの制約
Aggregate モデルは、最終的な集約済みデータを外部に公開します。つまり、まだ集約されていないデータ(たとえば、異なる二つのロードバッチに属するデータ)は、公開される前に何らかの仕組みによって整合性が確保される必要があります。
整合性保証の例
次のテーブルスキーマを想定します。
| 列名 | 型 | 集約タイプ | 説明 |
|---|---|---|---|
| user_id | LARGEINT | ユーザー ID | |
| date | DATE | データのロード日 | |
| cost | BIGINT | SUM | ユーザーの支出合計 |
ストレージエンジンには、すでに二つのロードバッチからのデータが格納されています。
バッチ 1
| user_id | date | cost |
|---|---|---|
| 10001 | 2017/11/20 | 50 |
| 10002 | 2017/11/21 | 39 |
バッチ 2
| user_id | date | cost |
|---|---|---|
| 10001 | 2017/11/20 | 1 |
| 10001 | 2017/11/21 | 5 |
| 10003 | 2017/11/22 | 22 |
このように、二つのロードバッチにまたがるユーザー 10001 のデータは、まだ集約されていません。ユーザーに次の最終的な集約済みデータだけが見えるようにするため、
| user_id | date | cost |
|---|---|---|
| 10001 | 2017/11/20 | 51 |
| 10001 | 2017/11/21 | 5 |
| 10002 | 2017/11/21 | 39 |
| 10003 | 2017/11/22 | 22 |
Doris はクエリエンジンに集約オペレーターを追加し、外部に公開するデータの整合性を保証します。
宣言と異なる集約クエリにおける意味上の落とし穴
Value 列に対して、宣言された集約タイプとは異なる集約クエリを実行する場合は、結果の意味に注意してください。たとえば、次のクエリを考えます。
SELECT MIN(cost) FROM table;
結果は 5 であり、1 ではありません。
count(*) クエリのオーバーヘッド
Aggregate モデルの整合性保証の仕組みは、一部のクエリの効率を大幅に低下させます。代表的な例が count(*) クエリです。
SELECT COUNT(*) FROM table;
ほかのデータベースでは、この種のクエリは非常に速く結果を返します。「ロード時に行数を数えて統計値として保存する」方法や、「クエリ時に一列だけをスキャンして行数を得る」方法なら、非常に小さいオーバーヘッドで結果を得られます。しかし、Doris の Aggregate モデルでは、このクエリのコストは非常に高くなります。
上記のデータを例にすると、最終的な集約結果は次のようになります。
| user_id | date | cost |
|---|---|---|
| 10001 | 2017/11/20 | 51 |
| 10001 | 2017/11/21 | 5 |
| 10002 | 2017/11/21 | 39 |
| 10003 | 2017/11/22 | 22 |
SELECT COUNT(*) FROM table; の正しい結果は 4 です。しかし、次のようになります。
user_id列だけをスキャンし、クエリ時に集約すると3(10001、10002、10003)になり、正しくありません。user_id列だけをスキャンし、クエリ時に集約しないと5(両方のバッチにある合計 5 行)になり、正しくありません。
正しい結果を得るには、user_id 列と date 列の両方を読み取り、クエリ時に集約して 4 を返す必要があります。つまり、count(*) クエリでは、Doris はすべての AGGREGATE KEY 列(ここでは user_id と date)をスキャンして集約し、意味的に正しい結果を得る必要があります。AGGREGATE KEY 列が多い場合、count(*) クエリは大量のデータをスキャンすることになります。
count(*) のパフォーマンス最適化
ワークロードに count(*) クエリが頻繁に含まれる場合は、値が常に 1 で、集約タイプが SUM の列を追加して count(*) を再現します。前の例のテーブルスキーマを次のように変更します。
| 列名 | 型 | 集約タイプ | 説明 |
|---|---|---|---|
| user_id | BIGINT | ユーザー ID | |
| date | DATE | データのロード日 | |
| cost | BIGINT | SUM | ユーザーの支出合計 |
| count | BIGINT | SUM | 行数の計算に使用 |
データをロードする際は、count 列の値を定数 1 に設定します。そのうえで、次のクエリを実行します。
SELECT COUNT(*) FROM table;
-- equivalent to
SELECT SUM(count) FROM table;
後者のほうが前者よりはるかに効率的です。
使用上の制約: AGGREGATE KEY 列が同じ行を複数回ロードしないようにする必要があります。そうしないと、SELECT SUM(count) FROM table; は元々ロードした行数のみを表し、SELECT COUNT(*) FROM table; と等価ではなくなります。
代替方法: 上記の count 列の集約タイプを REPLACE に変更し、値は引き続き定数 1 にします。この場合、SELECT SUM(count) FROM table; と SELECT COUNT(*) FROM table; は同じ結果になり、重複行のロードに関する制約はありません。
Unique モデルの Merge-on-Write 実装
Unique モデルの Merge-on-Write 実装には、上記の Aggregate モデルの制約がありません。先ほどと同じデータを使う場合、Merge-on-Write は各ロードの rowset に対応する削除ビットマップを追加して、上書きされたデータを示します。
最初のバッチをロードした後:
バッチ 1
| user_id | date | cost | 削除ビット |
|---|---|---|---|
| 10001 | 2017/11/20 | 50 | FALSE |
| 10002 | 2017/11/21 | 39 | FALSE |
二番目のバッチをロードした後、最初のバッチ内の重複行に削除済みの印が付きます。
バッチ 1
| user_id | date | cost | 削除ビット |
|---|---|---|---|
| 10001 | 2017/11/20 | 50 | TRUE |
| 10002 | 2017/11/21 | 39 | FALSE |
バッチ 2
| user_id | date | cost | 削除ビット |
|---|---|---|---|
| 10001 | 2017/11/20 | 1 | FALSE |
| 10001 | 2017/11/21 | 5 | FALSE |
| 10003 | 2017/11/22 | 22 | FALSE |
クエリ時には、削除ビットマップで削除済みとされたすべての行が除外され、データの集約は不要です。上記のデータには有効な行が 4 行あり、クエリ結果も 4 行です。そのため、先に述べた最もオーバーヘッドの小さい「一列だけをスキャンして行数を得る」方法を使用できます。
パフォーマンスデータ: テスト環境では、Unique モデルの Merge-on-Write 実装に対する count(*) クエリは、Aggregate モデルに対するクエリより10 倍以上高速です。
Duplicate モデル
Duplicate モデルには、上記の Aggregate モデルの制約がありません。このモデルには集約の意味がないため、count(*) クエリは任意の一列をスキャンして、意味的に正しい結果を得られます。