はじめに
ここでは、Couchbase Liteデータベースの検索のために利用するクエリについて、トラブルシューティングなどの目的のために調査を行う方法について説明します。
Couchbase Liteは、内部的にSQLiteを利用しており、ここでの解説は、SQLiteのEXPLAIN QUERY PLAN
コマンドに関係してきます。
SQLiteのEXPLAIN QUERY PLAN
の詳細については、以下を参照してください。
https://www.sqlite.org/eqp.html
調査方法には、APIを利用する方法と、cbliteコマンドを利用する方法があります。これらのいずれの方法を用いた場合でも、出力内容は同じフォーマットとなります。
出力内容は、クエリのパフォーマンスの問題を診断したり、クエリを最適化したりするときに役立つ洞察を提供します。
使用法
cbliteでの利用例
ターミナル上で、データベースを指定して、インタラクティブモードで、cbliteコマンドを実行します。
cblite <your-database-name>.cblite2
--explain
オプションを指定します。
次の例では、select
サブコマンドを使用してN1QLクエリとして入力しています。
(cblite) select --explain domains group by country order by country, name
また、クエリはquery
サブコマンドを使用してJSON文字列として入力することもできます。
(cblite) query --explain {"GROUP_BY":[[".country"]],"ORDER_BY":[[".country"],[".name"]],"WHAT":[[".domains"]]}
APIでの利用
ここではKotlinでのコード例を示します(後にJavaのサンプルも提示します)。
CBLQuery *query =
[CBLQueryBuilder
select:@[[CBLQuerySelectResult all]]
from:[CBLQueryDataSource database:database]
where:[[CBLQueryExpression property:@"type"]
equalTo:[CBLQueryExpression string:@"university"]]
groupBy:@[[CBLQueryExpression property:@"country"]]
orderBy:@[[[CBLQueryOrdering property:@"title"] descending]]
]; ①
NSLog(@"%@", [query explain:&error]); ②
①通常どおりクエリを作成します
②クエリのexplainメソッドを呼び出します。ここでは、結果を、ログとして出力しています。
出力
APIと、cbliteコマンドとで、出力内容は同じフォーマットとになります。
SELECT fl_result(fl_value(_doc.body, 'domains')) FROM kv_default AS _doc WHERE (_doc.flags & 1 = 0) GROUP BY fl_value(_doc.body, 'country') ORDER BY fl_value(_doc.body, 'country'), fl_value(_doc.body, 'name') ①
7|0|0| SCAN TABLE kv_default AS _doc ②
12|0|0| USE TEMP B-TREE FOR GROUP BY
52|0|0| USE TEMP B-TREE FOR ORDER BY
{"GROUP_BY":[[".country"]],"ORDER_BY":[[".country"],[".name"]],"WHAT":[[".domains"]]} ③
この出力は、次の3つの主要な要素で構成されています。
① SQLクエリに翻訳された文字列(ただし、必ずしも何らかの実行可能なコードに対応しているわけではなく、診断用の情報)。
② クエリプランは、SQLiteのSQLクエリを実装する方法の高レベルのビューを提供します。潜在的な問題を特定し、問題のあるクエリの最適化に役立てることができます。
③ JSON文字列形式のクエリ。cbliteツールで直接実行することができます。
クエリプランセクション
出力中のクエリプランセクションには、クエリの実行プランが表形式で表示されます。
データがどのように取得されるか、また必要に応じて、どのように加工されるかを示しています。
7|0|0| SCAN TABLE kv_default AS _doc ①
12|0|0| USE TEMP B-TREE FOR GROUP BY ②
52|0|0| USE TEMP B-TREE FOR ORDER BY ③
①検索方法(Retrieval method): この行は、クエリに使用されている検索方法を示しています。ここでは、データベースの順次読み取り(SCAN)です。最適化を検討できる可能性があります。詳細については、以下を参照してください。
②グループ化方法(Grouping method): この行は、クエリでGroup By句が使用されており、データを並べ替える必要があり、bツリーが一時ストレージとして使用されることを示しています。
③順序付け方法(Ordering method): この行は、クエリでOrder By句が使用されており、データを並べ替える必要があり、bツリーが一時ストレージとして使用されることを示しています。
検索方法(Retrieval method)
クエリオプティマイザは、要求されたデータアイテムを可能な限り効率的に取得しようとします。これは通常、1つ以上の使用可能なインデックスを使用することによって行われます。
取得方法(Retrieval Method)毎の説明を以下に記します。
Search
クエリはキーを使用して必要なデータに直接アクセスできます。Searchモードを使用したクエリが最速です。
Scan Index
クエリはインデックスの全部または一部をスキャンすることでデータを取得できます(たとえば、範囲内の値を一致させようとする場合)。このタイプのクエリはSearchよりも低速ですが、少なくとも、コンパクトで順序付けられた形式のインデックスの恩恵を受けます。
Scan Table
クエリはデータベーステーブルをスキャンして、必要なデータを取得する必要があります。これはこれらの方法の中で最も遅く、最適化を検討することが考えられます。
ベストプラクティス
インデックスを作成すると、クエリのパフォーマンスを高速化できます。
以下、インデックスに対して適切なアプローチをとることの重要性について説明します。
ワイルドカードの扱い
LIKE検索では、(利用できるインデックスがあり、かつ)次の条件が満たされている場合のみインデックスを使用できます。
- 検索文字列がワイルドカードで始まらない
- 検索文字列は実行時に既知の定数である(クエリの処理中に導出された値ではない)
以下のクエリ例では、ワイルドカードを接頭辞と接尾辞に使用しています。
Query query = QueryBuilder
.select(SelectResult.all())
.from(DataSource.database(database))
.where(Expression.property("type").like(Expression.string("%hotel%")));
Log.i(query.explain());
インデックス付きプロパティtype
に対する条件句は、ワイルドカードプレフィックスが接頭辞として使われているため、そのインデックスを使用できません。
結果のクエリプランでは、取得方法として「Scan Table」に決定していることがわかります。
2|0|0| SCAN TABLE kv_default AS _doc
接頭辞として使われているワイルドカードプレフィックス%を削除すると、クエリプランの取得方法が変更されてインデックス検索になることがわかります。このような単純な変更により、クエリのパフォーマンスに大きな違いが生じる可能性があります。
Query query = QueryBuilder
.select(SelectResult.all())
.from(DataSource.database(database))
.where(Expression.property("type").like(Expression.string("hotel%"))
.and(Expression.property("name").like(Expression.string("%royal%"))));
Log.i(query.explain());
結果のクエリプランから、クエリオプティマイザがtypeIndex
にアクセスできるようになり、検索がより効率的になっていることがわかります。
3|0|0| SEARCH TABLE kv_default AS _doc USING INDEX typeIndex (<expr>>? AND <expr><?)
関数を適切に利用する
関数はクエリを作成するのに非常に便利なツールですが、クエリオプティマイザーがインデックスを使用できるかどうかに影響を与える可能性があることに注意する必要があります。
たとえば、インデックスが設定されているプロパティに対して、lower()
関数を使用した例について見てみます。
Query query = QueryBuilder
.select(SelectResult.all())
.from(DataSource.database(database))
.where(Function.lower(Expression.property("type")).equalTo(Expression.string("hotel")));
Log.i(query.explain());
ここでは、Where式で、プロパティtype
にlower()
関数を使用しています
クエリプランは、以下の通りです。
2|0|0| SCAN TABLE kv_default AS _doc
lower()
関数を削除すると、状況が変わります。
Query query = QueryBuilder
.select(SelectResult.all())
.from(DataSource.database(database))
.where(Expression.property("type").equalTo(Expression.string("hotel")));
Log.i(query.explain());
以下のようにクエリプランが変化します。
3|0|0| SEARCH TABLE kv_default AS _doc USING INDEX typeIndex (<expr>=?)
このことを踏まえると、インデックスを作成する方法を検討できます。たとえば、クエリ実行時ではなく、インデックスを作成するときにlower()
関数を使用することが考えられます。
インデックス更新
クエリの2回目の実行、データベースを再度開いた後、またはインデックスを削除して再作成した後など、それ以前と比べてクエリの実行速度が速くなる場合があります。これは、SQLクエリオプティマイザーがクエリを最適化するため、以前よりも効果的な統計情報を収集したことを意味します。
統計は、次のような特定のイベントの後に収集されます。
- インデックス作成後
- データベースクローズ後
- データベース圧縮後
クエリの調査を行う際に、データベースの圧縮を検討することが考えられるでしょう。
関連情報