【MarkLogic Server】XQueryチューニングメモ～Element Range Index概説

はじめに
===
MarkLogicには、データをインポートした時点でUniversal Indexという索引が自動的に設定されます。これとは別に通常のデータベースと同じく追加的な索引を設定することができます。その最たるものがRange Indexです。Range Indexはパス・要素・属性などに対して設定できますが、今回は要素を対象にしたElement Range Indexを中心に説明していきます。

Range Indexの利点
===
Range Indexは利用される際にメモリに配置されます。このためRange Indexへの参照は通常のディスクアクセスと比べて極めて高速なメモリアクセスになります。

Element Range Indexの設定方法
===
ブラウザを開いてlocalhostの管理者画面(ポート8001番)にアクセスします。ここで左側のタブからConfigure→Databases→[追加したいデータベース]→Element Range Indexesを選択していきます。addタブを選択すると追加画面が現れました。試しに数値型で入れてみましょう。

| パラメータ | 説明 | 今回の設定値 |
|---|---|---|
| scalar type | データ型の指定 | decimal |
| localname | 対象要素名 | saleSum |
| range value positions | range value position indexの設定 | false |
| invalid values | データ型に合わない値の挿入・更新の拒否 | reject |

saleSum要素に対してdecimal型のRange Indexが設定されました。

同じようにshopNoという要素にstring型のRange Indexを設定します。string型にするとCollationという項目が出てきますが、今回は私のクエリコンソールの設定に合わせてhttp\://marklogic.com/collation/codepointとします。クエリコンソールの設定変更は管理者画面でGroups→Default→App Servers→App-Servicesより可能です。

Range Indexの効果
===
簡単な例で効果を見てみましょう。MarkLogicに以下のような売上額XMLファイルを10万個挿入しました。shopNoは1～200、jobNoは1～5、yearMonthは2017-01～2017-10、saleNoは1～10がいわゆる主キーです。

```
<root>
  <shopNo>100</shopNo>
  <jobNo>2</jobNo>
  <yearMonth>2017-02</yearMonth>
  <saleNo>10</saleNo>
  <saleSum>7761</saleSum>
</root>
```

shopNoごとにsaleSumを合計するXQueryを書いてみましょう。ここでは検索・計算部分の性能が特に重要なので戻り値はmap変数からshopNo=50を返すのみとしました。

☆XQuery①
1ファイルずつ開いて集計していくという素朴なものです。

```
let $map := map:map()
let $_ := 
  for $uri in cts:uris((), (), cts:directory-query("/root/", "infinity"))
    let $doc := fn:doc($uri)/root
    let $shopNo := $doc/shopNo/text()
    let $saleSum := xs:decimal($doc/saleSum/text())
    let $cur := map:get($map, $shopNo)
    return 
      if($cur) then map:put($map, $shopNo, ($cur + $saleSum))
      else map:put($map, $shopNo, $saleSum)
return map:get($map, "50")
```

☆XQuery②
こちらも1ファイルずつ見ていくものですが、今度はRange Index利用関数を使って、ループの手前で\<URI, shopNo\>と\<URI, saleSum\>の2項関係を取得しておきましょう。

```
let $map := map:map()
let $dirQuery := cts:diectory-query("/root/", "infinity")
let $uri_shopNo-map := cts:element-value-co-occurrences(
  xs:QName("xdmp:document"), xs:QName("shopNo"), ("map"), $dirQuery
)
let $uri_saleSum-map := cts:element-value-co-occurrences(
  xs:QName("xdmp:document"), xs:QName("saleSum"), ("map"), $dirQuery
)
let $_ := 
  for $uri in cts:uris((), (), $dirQuery)
    let $shopNo := map:get($uri_shopNo-map, $uri)
    let $saleSum := map:get($uri_saleSum-map, $uri)
    let $cur := map:get($map, $shopNo)
    return 
      if($cur) then map:put($map, $shopNo, ($cur + $saleSum))
      else map:put($map, $shopNo, $saleSum)
return map:get($map, "50")
```

☆XQuery③
shopNoを軸にループするように変更しましょう。これによりループ回数は10万回から200回になります。shopNoの一覧の取得はcts:element-valuesを使い、合計の計算はcts:sum-aggregateを使います。

```
let $map := map:map()
let $dirQuery := cts:diectory-query("/root/", "infinity")
let $shopNo-seq := cts:element-values(xs:QName("shopNo"), (), (), $dirQuery)
let $_ := 
  for $shopNo in $shopNo-seq
    let $total := 
      cts:sum-aggregate(
        cts:element-reference(xs:QName("saleSum")), (),
        cts:and-query((
          $dirQuery,
          cts:element-value-query(xs:QName("shopNo"), $shopNo)
        ))
      )
    return 
      map:put($map, $shopNo, $total)
return map:get($map, "50")
```

クエリコンソールで実行してみた処理時間結果は以下のような感じになりました。

| クエリ | 処理時間[秒] |
|---|---|
| XQuery① | 23 |
| XQuery② | 0.766 | 
| XQuery③ | 0.156 | 

XQuery①とXQuery②はRange Indexの大きな効果を示します。またXQuery②とXQuery③はそこからさらに工夫できる余地があることを示しています。
今回は3種類の関数を紹介しましたが、他にもいくつかありますので[こちら](https://docs.marklogic.com/guide/admin/range_index "こちら")もご参照ください。

他のRange Index
===
今回は要素のRange Indexを挙げましたが他にもありますので一覧で紹介します。

| 名称 | 対象 |
|---|---|
| Element Range Index | 要素 |
| Path Range Index | パス | 
| Attribute Range Index | 属性 | 
| Field Range Index | フィールド | 


Range Indexの設定のしどころ
===
概ね以下のような要素が設定を検討する対象となりそうです。
・結合条件となるもの
・ソート条件となるもの
・大小比較対象となるもの
・GroupBy条件となるもの
・集計などの計算対象となるもの


Range Index設定の注意点
===
一方でRange Indexのデメリットも説明しておきましょう。
・メモリ消費量が大きくなる。
・ロード時間やデータサイズに影響を与える。
・途中から追加すると再索引(リインデックス)が発生して、その間性能が低下する。
・繰り返し要素のように1つのXMLに複数の同名要素が存在する場合使いづらい。

MarkLogic社の[ブログ](https://developer.marklogic.com/blog/10000-range-indexes "ブログ")には多すぎるRange Indexの弊害が取り上げられていますが、100個程度を限度として提示しています。もちろんデータ内容にもよります。私の経験ではDBサイズ30GB程度のデータに65個設定したところ、32GBのメモリの空きがほぼなくなってしまうということもありました。
再索引も性能が低下するため要注意です。こればかりはディスク性能の高いSSDで開発を進めるぐらいしか解決策が見当たりません。

おわりに
===
これまでいくつか示してきたチューニング方法に比べてRange Indexの活用は劇的な効果を示します。ただし、設定するコストがあることも理解しておく必要があるようです。性能問題が起きたら、Range Index以外の方法も同時に考慮することをお勧めします。


```math
\def\textsmall#1{%
  {\rm\scriptsize #1}
}
```

------------------------------------------------------------------------

**免責事項**

​​​​​$\textsmall{当ユーザ会は本文書及びその内容に関して、いかなる保証もするものではありません。}$
​​​​​$\textsmall{万一、本文書の内容に誤りがあった場合でも当ユーザ会は一切責任を負いかねます。}$
​​​​​$\textsmall{また、本文書に記載されている事項は予告なしに変更または削除されることがありますので、予めご了承ください。}$