(このブログは2017年に掲載されたものの日本語訳です)XQueryコードを最適化するためのわかりやすい手順があったらいいと思いませんか。今回はそれをご紹介します。ここでは、XQueryを使用する際に参考となるチェックリストをご紹介します。
MarkLogicは、 queryとupdateの2つのいずれかのモードでトランザクションを実行します。主な違いとして、queryトランザクションは読み取り専用(read-only)で更新ができないのに対し、updateトランザクションでは更新ができます。
これが私のリストのNo.1となります。というのも、私自身が誤ってコードをupdateモードで実行してしまうことが多く、そのためにパフォーマンスが大幅に低下することがあるからです。しかしこれから紹介するように、これはとても簡単にチェックできます。
その話に入る前に、データベースのデータを更新するには、update(更新)ステートメントが必要なことを知っておいてください。これは絶対に使いたいものですし、使う必要があります。
しかし、場合によってはコードを実行しても何も更新されないこともあります。このような「クエリのみ」のコードは、queryモードで実行すべきです。ここでは、必要がないのにクエリを誤ってupdateモードで実行してしまっている場合を考えてみます。
updateモードで実行した場合、MarkLogicは適宜「読み取りロック」と「書き込みロック」を適用します。間違ってトランザクションをupdateモードで実行してしまうミスはよくあります。これによって、パフォーマンスが大幅に低下する可能性があります。例えば、以下のクエリを見てみましょう。
for $doc in fn:doc() where $doc/root/selected = true() return $doc
fn:doc()は、パラメータが与えられない場合、データベース内のすべてを返します。このコードがupdateモードで実行されると、「ドキュメントを更新したい」のだとMarkLogicは想定し、fn:doc()で返されるすべてのドキュメントをロックします。また、 書き込みロックは排他的なため、マルチスレッドのリクエストはすべて、ロックの奪い合いをします。ロックの競合は正常であり、当たり前のように発生しますが、これによりパフォーマンスが低下する可能性があります。これは単純な例ですが、言いたいことはわかってもらえたかと思います。本当は必要ないのに、間違ってupdateモードでコードを実行してしまうと、パフォーマンスに問題が生じてしまうのです。
幸いなことに、本当はqueryモードで実行すべきなのに間違ってupdateモードで実行している場合にエラーを発生させる、うまい方法があります。
let $assert-query-mode as xs:unsignedLong := xdmp:request-timestamp() for $doc in fn:doc() where $doc/root/selected = true() return $doc
ここで重要なのは、letステートメントです。
let $assert-query-mode as xs:unsignedLong := xdmp:request-timestamp()
xdmp:request-timestは、queryモードで実行するとxs:unsignedLong
を返します。しかし、updateモードで実行するとempty-sequence()
を返します。このletステートメントは、updateトランザクションから実行すると、empty-sequence()
からxs:unsignedLong
へのキャストに失敗するため、例外が発生します。
クエリを実行して例外が発生した場合は、updateモードで実行していることがわかります。この場合、原因を突き止めて、このコードをqueryモードで実行できるように修正しなければなりません。
間違ってupdateモードになってしまう可能性のあるシナリオをいくつかご紹介します。
以下は、updateモードとなるコードの例です。
let $assert-query-mode as xs:unsignedLong := xdmp:request-timestamp() return if (fn:false()) then xdmp:document-insert("/test.xml", <test>This never gets called</test>) else "query mode stuff"
ご覧のとおり、実際にはdocument-insertは呼び出されませんが、MarkLogicはコードパスの中でこの関数を認識しているため、updateモードで実行されます。
MarkLogicのマニュアルには、トランザクションに特化した章があります。これを最低1年に1回は読むようにお薦めします。
私のリストのステップ2は見落とされがちですが、幸いなことにすぐに簡単に修正できます。
MarkLogicの最も有名なAPIの1つに、cts:searchがあります。MarkLogicを開発したことがある人は皆、どこかで使ったことがあると思います。もしこれを使った際に検索が遅いと感じた場合、間違って「フィルタリングあり検索」を実行していないかどうかを確認してください。
それでは「フィルタリングあり検索」とは何でしょうか。
スコット・パーネル氏は、2016年11月のブログ『A goal without a plan is just a wish(計画のない目標はただの願い事 )』で、これをうまく説明しています。
デフォルトでは、
cts:search
は2つのフェーズでクエリを解決します。第1段階では、Dノードのインデックス解決を行います。この最初の結果には、インデックスの構成やクエリによっては偽陽性(false positive:本当はそうでないのにそうだと判断されたもの)を含む可能性があります。第2段階では、Eノードで結果のフィルタリングを行い、マッチしたドキュメントを検証して偽陽性を除去します。インデックスだけでクエリを完全に解決できるのであれば、フィルタリングは不要です。
「インデックスが適切に設定されているのでフィルタリングなしで実行したい」という前提で、 cts:search
をチェックしていきます。
cts:searchのマニュアルを見ると、この関数のシグネチャがわかります。
cts:search( $expression as node()*, $query as cts:query?, [$options as (cts:order|xs:string)*], [$quality-weight as xs:double?], [$forest-ids as xs:unsignedLong*] ) as node()*
cts:searchをフィルタリングなしで実行するには、3つめのパラメータのオプションとして「unfiltered」を指定する必要があります。
cts:search(fn:doc(), $query, ("unfiltered"))
ここで確認しておきたいのは、cts:searchのデフォルトがフィルタリングあり(filtered)であることです。unilteredパラメータを指定しないかぎり、デフォルトである「フィルタリングあり」の挙動となります。
コードが期待通りに動作しない場合は、 必ずプロファイリングを行ってください。プロファイリングをせずに最適化しないでください。間違ったものを修正して時間を無駄にしてしまう可能性が高いからです。
幸いなことに、MarkLogicではこれを簡単に行うことができます。
MarkLogicマニュアルによると:
クエリコンソールはインタラクティブなwebベースのクエリ作成ツールです。クエリをアドホックにXQuery/サーバーサイドJavaScript/SQL/SPARQLの形式で記述・実行できます。クエリコンソールを使うと、コードの一部のテスト、デバッグ、クエリのプロファイリング、管理用のXQueryスクリプトの実行などができます。
QConsoleはhttp://yourserver:8000/qconsole/にあります。
プロファイル用のボタンを使うと、クエリで最も時間がかかっている場所がわかるので、簡単にクエリを実行できる場合には朗報です。
実行したいコードを入力し、Profileタブを選択し、Runボタンをクリックするだけです。
表の結果は、実行時間の長いものから短いものの順に並べられています(Shallow %)。実行に異常に時間がかかっているステートメントを探し、それをどのように最適化できるかを考えます。
何らかの理由で、QConsoleでコードを実行できない場合があるとします。ご心配なく。以下の関数を使うことができます。
以下のようにコードをラップするだけでOKです。
prof:enable(xdmp:request()), for $x in (1 to 10) let $y := $x + 1 return $y, prof:disable(xdmp:request()), prof:report(xdmp:request())
prof:report()の出力は、XML版のプロファイルレポートです。コードが他のコードの中に深く埋もれている場合は、xdmp:logでログを記録するか、xdmp:document-insert()でドキュメントに挿入して後から見ることができます。
凝ったことが好きな人は、関数を作ることで、再利用可能なモジュールを作成して、それをログに記録させることもできます。
(: name this file something like /path/to/profiler.xqy :) xquery version "1.0-ml"; module namespace profiler = "http://marklogic.com/profiler"; declare function profiler:profile($func) { let $req := xdmp:request() (: start profiling :) let $_ := prof:enable($req) (: call the code to profile :) let $result := $func() (: stop profiling :) let $_ := prof:disable($req) (: create the profile report :) let $report := prof:report($req) (: save report somewhere :) let $_ := xdmp:document-insert("/profile-output/" || fn:string($req) || ".xml", $report) (: log it too :) let $_ := xdmp:log(xdmp:describe($report, (), ())) return (: return the output, if any, from your profiled code :) $result };
これはかなり凝っていますね。これはどのように使うのでしょうか。
import module namespace profiler = "http://marklogic.com/profiler" at "/path/to/profiler.xqy"; profiler:profile(function() { (: put your code in here :) for $x in (1 to 10) let $y := $x + 1 return $y })
このコードを実行すると、プロファイラレポートがErrorLogに記録され、/profile-output/${requestid}.xmlに挿入されます。QConsoleの出力のように読みやすくはありませんが、同じ情報がすべて含まれています。
MarkLogicでは、2つのフェーズでクエリを実行します。第1段階は高速で、インデックスを使って検索結果を絞り込みます。この最初の結果には、インデックスの構成やクエリによっては偽陽性が含まれる可能性があります。第2段階では、結果をフィルタリングします。この第2段階では、ドキュメントをディスクから取り出し、それがクエリにマッチするかどうかを検証します。マッチしなかったドキュメント(偽陽性)は結果セットから削除されます。ドキュメントを取得する際にディスクI/Oを使用するので、これがクエリの実行速度を低下させる原因となることが多いです。
多くの場合、インデックスのみに頼ることで、第2段階を回避できます。MarkLogicでは、まさにこれを実行するためにcts
名前空間およびxdmp
名前空間内の多くの関数を公開しています。
よくある例として、クエリにマッチするドキュメントの件数を把握するというものがあります。
fn:count(/customer[@paid=fn:false()])
これは、paid=”false”属性にマッチするすべての顧客をディスクから取得する必要があるため、パフォーマンスが低下します。これを高速化するには、代わりにインデックスを使用します。
xdmp:estimate(/customer[@paid=fn:false()])
ここでxdmp:estimateを使用していることに注意してください。これが速いのは、フィルタリングなしで実行しているためです。つまり、クエリ解決の第2段階を実行しません。このため名前が「estimate(想定)」なのです。これはインデックスに基づいて件数を算出しています。結果が正しい値となるようにするには、インデックスを適切に設定する必要があります。
上記の1~4の手順の後に、パフォーマンスのボトルネックがcts:searchのコードにあることがわかった場合は、インデックスの最適化を検討することになります。この方法については、2016年11月のスコット・パーネル氏のブログ記事『A goal without a plan is just a wish』に詳しく書かれていますので参照してください。
このチェックリストは、MarkLogicの開発者がパフォーマンスの問題を発見して修正する方法の一部を示しています。この記事で取り上げたトピックについてもっと知りたい場合は、以下を参照してください。
Like what you just read, here are a few more articles for you to check out or you can visit our blog overview page to see more.
In this post, we dive into building a full five-card draw poker game with a configurable number of players. Written in XQuery 1.0, along with MarkLogic extensions to the language, this game provides examples of some great programming capabilities, including usage of maps, recursions, random numbers, and side effects. Hopefully, we will show those new to XQuery a look at the language that they may not get to see in other tutorials or examples.
If you are getting involved in a project using ml-gradle, this tip should come in handy if you are not allowed to put passwords (especially the admin password!) in plain text. Without this restriction, you may have multiple passwords in your gradle.properties file if there are multiple MarkLogic users that you need to configure. Instead of storing these passwords in gradle.properties, you can retrieve them from a location where they’re encrypted using a Gradle credentials plugin.
Apache NiFi introduces a code-free approach of migrating content directly from a relational database system into MarkLogic. Here we walk you through getting started with migrating data from a relational database into MarkLogic
Don’t waste time stitching together components. MarkLogic combines the power of a multi-model database, search, and semantic AI technology in a single platform with mastering, metadata management, government-grade security and more.
Request a Demo