Couchbase Serverにおけるインデックス設計パターン

### インデックス設計

ここで解説する内容を理解することにより、Couchbase Serverのクエリのパフォーマンスを向上させることができます。


#### カバリングインデックス

クエリがインデックス内のキーのみを参照している場合、クエリエンジンは、データノードからフェッチすることなく、インデックススキャン結果のみを利用してクエリに応答することができます。

検索に用いる必要のないキーをあえてインデックスに含めて定義することで、特定のクエリの性能を向上することができる一方、インデックスの維持に必要なリソース負担が増します。性能とリソースのバランスを考慮して最適化することになります。



#### パーシャル（部分）インデックス

`CREATE INDEX` DDLの定義に`WHERE`句を用いることで、本来のデータに対して、部分的にインデックスを作成することができます。

```SQL
CREATE INDEX travel_info ON `travel-sample`（name, id, icoo, iata）
WHERE type = 'airline';
```

上記例では、`type` フィールドの値として`airline` を持つ、ドキュメントに対してのみインデックスが作成されます。
このインデックスがクエリに利用されるためは、そのクエリのにも同じ条件が含まれている必要があります。

インデックスの`WHERE`句では、より複雑な述語を使用できます。

パーシャルインデックスを使用する目的としていかが考えられます。

- 必要な検索要件のみに特化したインデックス化範囲の限定による（更新・検索）性能の最適化
- 検索の選択肢が限定的な場合（性別や地域名など）に、複数のインデックスとして定義することによる性能の最適化（この場合、インデックス定義時のノード配置の指定と組み合わせて、分散環境の効果を活用することが考えられます）


#### ファンクション（関数）インデックス

関数を適用したフィールドをキーとして使用しインデックスを作成することができます。
インデックスには、関数適用後の値が格納されます。ただし、 複数ドキュメントに跨がる集計関数 (`SUM`、`AVG`、`COUNT` 等) は使用できません。


```SQL
CREATE INDEX travel_cxname ON `travel-sample`（LOWER（name））;
```

上の例は、大文字と小文字が混在している可能性のある`name` フィールドを検索するケースを想定しています。
大文字と小文字の組み合わせの全ての可能性を検索するために、`LOWER`　関数を使って値を小文字に統一しています。

このインデックスがクエリに利用されるためは、下のように、関数`LOWER`と関数へのパラメータ`name`の組み合わせ（`LOWER(name)`）が、インデックスの定義と一致している必要があります。

```SQL
SELECT ... FROM `travel-sample`
WHERE LOWER(name) = $name
```

この例では、パラメーター `$name` に、値が小文字で与えられていることを想定しています（条件式の右項に対して`LOWER` 関数を適用することも考えられますが、インデックスとの一致のためには必須ではありません）。

より複雑な式の組み合わせを使用することもできます。例えば以下のように、計算式を指定することも可能です。

```SQL
CREATE INDEX travel_cx1 ON `travel-sample`（LOWER（name）、length * width、round（salary））;
```



#### 重複インデックス

異なる名前で、名前以外は、共通した定義を持つインデックスを重複して定義することは許されています。

重複インデックスは、高可用性と負荷分散に役立ちます。

```SQL
CREATE INDEX i1 ON `travel-sample`（LOWER（name）、id、icoo）
WHERE type = 'airline' WITH {"nodes": ["192.0.1.0:8091"]} ;

CREATE INDEX i2 ON `travel-sample`（LOWER（name）、id、icoo）
WHERE type = 'airline' WITH {"nodes": ["192.0.2.0:8091"]} ;

CREATE INDEX i3 ON `travel-sample`（LOWER（name）、id、icoo）
WHERE type = 'airline' WITH {"nodes": ["192.0.3.0:8091"]} ;
```

`CREATE INDEX` ステートメントの`WITH` 句を使用して、物理的な場所を指定します。
上の例では、3つのインデックスはすべて、同じキーと同じWHERE句を持っています。唯一の違いは、これらのインデックスの名前です。

`CREATE INDEX` ステートメントの`WITH` 句を使用して、物理的な場所を指定します。(指定しない場合、ランダムに決定されます)


クエリの実行時、最適化の一貫として、重複インデックスはラウンドロビン方式で使用され、負荷が分散されます。

エンタープライズエディションでは、下記のように複数のノード指定を行うことができます。

```SQL
CREATE INDEX productName_index1 ON bucket_name(productName, ProductID)
WHERE type="product" USING GSI
WITH {"nodes":["node1:8091", "node2:8091", "node3:8091"]};
```

参考情報： `WITH`　句には、`node`　の他に、`num_replica `（インデックスレプリカの数）, そして真偽値をとる`defer_build`（インデックスの遅延作成）があります。


#### 配列インデックス

JSONは、スカラー、オブジェクト、または配列をフィールドとして持つことができます。各オブジェクトは、他のオブジェクトや配列をネストできます。各配列は他のオブジェクトや配列を持つことができます。

Couchbase Serverのインデックス定義DDLは、配列に対してインデックスを作成するための、特別な構文を備えています。

具体的に理解するため、次の配列について考えてみます。

```JSON
"schedule": [
        {
            "day": 0,
            "flight": "AF198",
            "utc": "10:13:00"
            },
        {
            "day": 0,
            "flight": "AF547",
            "utc": "19:14:00"
            },
        {
            "day": 0,
            "flight": "AF943",
            "utc": "01:31:00"
            },
        {
            "day": 1,
            "flight": "AF356",
            "utc": "12:40:00"
            },
        {
            "day": 1,
            "flight": "AF480",
            "utc": "08:58:00"
            },
        {
            "day": 1,
            "flight": "AF250",
            "utc": "12:59:00"
            }
    ]
```
`schedule` 配列に見られるような複雑な構造に対して、サブオブジェクト内の特定の配列またはフィールドにインデックスを付ける方法を次に示します。

```SQL
CREATE INDEX idx_sched
ON `travel-sample` ( DISTINCT ARRAY v.flight FOR v IN schedule END );
```


上のインデックス定義DDLは、以下の基本構文を元にしています。。

```SQL
ARRAY <インデックスキー> FOR v IN <配列名> END
```

`v`は、`schedule` 配列内の各要素/オブジェクトを参照するように暗黙的に宣言された変数です。`v.day` は、配列の各要素であるオブジェクトの`day`　フィールドを示しています。

これは一般化された式であるため、インデックスを作成する前にデータに追加のロジックと処理を適用する柔軟性を提供します。たとえば、各配列の要素に関数型インデックスを適用できます。

`DISTINCT` 修飾子により、インデックスからはキーの重複が除かれます。全てのキーを格納する場合は`ALL` 修飾子を利用します。

上記のインデックスにより、下記のように`schedule.flight`に対して検索を行うことが可能になります。

```SQL
SELECT * FROM `travel-sample`
WHERE ANY v IN schedule SATISFIES v.flight LIKE 'UA%' END;
```


`ARRAY <インデックスキー> FOR v IN <配列名> END`構文と合わせて利用することのできるキーワードを使って、複雑な条件指定を行うことができます。

**配列の選択された要素（WHEN句を使用）からの個々の属性の部分インデックス（WHERE句付き）**


インデックス：週の最初の4日間にスケジュールされたフライトIDのインデックスを作成します

```SQL
CREATE INDEX idx_flight_day
ON `travel-sample` ( ALL ARRAY v.flight FOR v IN schedule WHEN v.day < 4 END )
WHERE type = "route" ;
```

クエリ：1日目にスケジュールされた「UA」フライトのリストを検索します

```SQL
SELECT * FROM `travel-sample`
WHERE type = "route" ①
AND ANY v IN schedule SATISFIES (v.flight LIKE 'UA%') ②
AND (v.day=1) END; ③
```

上のインデックスは、次のような関係において、上のクエリの対象となります。

①クエリ述語中の`type="route"`の部分は、インデックスの`WHERE` 句に一致します。
②クエリ述語中`ANY-SATISFIES`句の`schedule`配列に関する検索のキーとして、インデックスキー`v.flight` が一致します。
③クエリ述語中`ANY-SATISFIES`句のもう一つの条件`v.day=1`　は、インデックス定義の`WHEN` 句の条件`v.day<4`に包含されます。



**配列の複数の要素を持つ配列インデックス**
インデックス：配列に含まれるオブジェクトの複数の属性でインデックスを作成

```SQL
CREATE INDEX idx_flight_day ON `travel-sample`
    ( DISTINCT ARRAY [v.flight, v.day] FOR v IN schedule END)
WHERE type = "route" ;
```

クエリ：2日目にスケジュールされた「US681」フライトのリストを検索します

```SQL
SELECT meta().id FROM `travel-sample`
WHERE type = "route"
AND ANY v in schedule SATISFIES [v.flight, v.day] = ["US681", 2] END;
```

**簡略化された構文を使用して配列内のすべての要素にインデックスを付ける**

インデックス：簡略化された配列インデックス構文を使用して、すべてのスケジュールでインデックスを作成

```SQL
CREATE INDEX idx_sched_simple
ON `travel-sample` (ALL schedule)
WHERE type = "route";
```

クエリ：特定のスケジュールに一致するすべてのルートドキュメントの詳細を検索します

```SQL
SELECT * FROM `travel-sample`
WHERE type = "route"
AND ANY v IN schedule
SATISFIES v = {"day":2, "flight": "US681", "utc": "19:20:00"} END; 
```
スケジュール配列の要素はオブジェクトであるため、述語条件の右側の値は同様に構造化されたオブジェクトである必要があります。


N1QLがもつ、配列操作のためのキーワード`UNNEST`を使って、上のクエリを下記のように書き換えることもできます。

```SQL
SELECT * FROM `travel-sample` t
UNNEST schedule sch
WHERE t.type = "route"
AND sch = {"day":2, "flight": "US681", "utc": "19:20:00"};
```


#### アダプティブ（Adaptive）インデックス

`DISTINCT PAIRS` 構文を利用して、ドキュメントの指定されたフィールドに対して、インデックスを付けることができます。指定されたフィールドの順序にかかわらず、全ての組み合わせを用いてクエリを実行することが可能になります。

これにより、フィールドのさまざまな組み合わせに対してさまざまな複合インデックスを作成しなくても、効率的なアドホッククエリが可能になります。

これは、特殊なタイプの配列インデックスです。

```SQL
CREATE INDEX `ai_airport_specific_fields` 
ON `travel-sample`(DISTINCT PAIRS({airportname, city, faa, type})) 
WHERE type = "airport";
```

さらに`self`キーワードを用いることにより、全てのフィールドを対象とすることができます。

```SQL
CREATE INDEX `ai_airport_all_fields` 
ON `travel-sample`(DISTINCT PAIRS(self)) 
WHERE type = "airport";
```

#### 性能最適化における考慮点

##### カーディナリティ

一般にデータベースにおけるインデックススキャンについては、カーディナリティが高い（データの種類が多い）項目の検索に向いているということが言えます。これは、カーディナリティが低い（データの種類が少ない）項目の検索では、実際上、フルスキャンに違い動きになる（フルスキャンが向いている）ことと裏返しの関係になります。

Couchbase Serverのインデックスにおいても、属性リストの先頭に高いカーディナリティの属性を配置することで、インデックスの検索性能を最適化することができます。ただし、ここで重要なのは、先行属性(leading attributes)の一致を妨げない限り、という条件が付くことです。

クエリに一致するインデックスの決定は、以下の順序で行われます。

①　キースペース(バケット名)マッチ
②　条件句マッチ
③　先行属性（leading attributes）マッチ

具体的には、クエリ中の下記の要素が対応します。

```SQL
SELECT c.name.first, c.name.last 
FROM retailSample c ①
WHERE c.userName = $userName　③
AND c.doc.type = "customer"　②
LIMIT 1;
```

```SQL
CREATE INDEX idx_cust ON retailSample(userName, userSex) 
WHERE doc.type ="customer"
```

上記のクエリとインデックスの例は、先行属性マッチとカーディナリティの関係が適切な例となります。

ただし、下記のようなクエリでは、上のインデックスを用いることができません。

```SQL
SELECT c.name.first, c.name.last 
FROM retailSample c
WHERE c.userSex = $userSex
AND c.doc.type = "customer"　
LIMIT 1;
```


####　複数ドキュメント取得時の最適化

クエリにより、多数の (かつ大きな) ドキュメントが返される場合は、クエリでドキュメントキーをクライアントに返し、クライアントからK-V API を使用してドキュメントをフェッチすることができます。 すべてのデータ転送を実現するために、クエリプロセスのメモリ領域がボトルネックとなる事がありません。また、1 つの大きなネットワーク伝送ではなく、多数の小さなネットワーク伝送を用いることが可能になります（ドキュメントキーによるデータ取得は、そのドキュメントを管理しているDataサービスとから直接行われます）。

```SQL
CREATE INDEX idx_order_pending ON retailSample (meta().id)
WHERE orderStatus = ‘Pending’
AND doc.type = ‘order’;
```

```SQL
SELECT meta().id as docKey
FROM retailSample
WHERE meta().id IS NOT MISSING
AND orderStatus = ‘Pending’ 
AND doc.type = ‘order’;
```

ここでは、`meta().id`　を検索の目的でなく、クエリ性能最適化のために利用しているため、カバリングインデックスの例ということができます。この場合、`meta().id IS NOT MISSING`という条件は、インデックスとの一致（先行属性マッチ）のために必要になります。