コンパイルされたクエリの実行に関する詳細な説明を探しています。それらが一度だけコンパイルされる方法と、それらを使用する利点が理解できません
2 に答える
この質問がコンパイル済みクエリの内部実装ではなく、使用法に関するものであると仮定すると、私の答えは次のとおりです。
Slick クエリを作成すると、Slick は実際に、関連するすべての式のデータ構造 (抽象構文木 (AST)) を内部的に作成します。このクエリを実行したい場合、Slick はデータ構造を取得し、それを SQL 文字列に変換 (つまり、コンパイル) します。これは、DB で実際に高速な SQL クエリを実行するよりも時間がかかる、かなり時間のかかるプロセスになる可能性があります。したがって、理想的には、クエリを実行する必要があるたびに、この SQL への変換を行うべきではありません。しかし、それを回避する方法は?変換/コンパイルされた SQL クエリをキャッシュする。
Slick は、初回のみコンパイルして、次回はキャッシュするようなことを行うことができます。しかし、そうではありません。ユーザーが Slick の実行時間を推測するのが難しくなるためです。同じコードでも最初は遅くなりますが、後で速くなるからです。(また、Slick は、クエリが 2 回目に実行されたときにクエリを認識し、内部キャッシュで SQL を検索する必要があり、実装が複雑になります)。
代わりに、明示的にキャッシュしない限り、Slick は毎回クエリをコンパイルします。これにより、動作が非常に予測可能になり、最終的には簡単になります。キャッシュするには、Compiled
次にクエリが必要になったときに再計算されない場所に結果を使用して保存する必要があります。def
そのため、 likeを使用しdef q1 = Compiled(...)
ても意味がありません。毎回コンパイルされるからです。val
またはである必要がありlazy val
ます。また、複数回インスタンス化するクラスにその値を入れたくないでしょう。代わりval
に、最上位の Scala singleton 内の aが適切な場所ですobject
。これは、一度だけ計算され、JVM のライブ タイムの間保持されます。
つまり、Compiled
魔法のようなことは何もしません。Slick の Scala-to-SQL コンパイルを明示的にトリガーし、SQL を含む値を返すことのみを許可します。重要なことに、これにより、実際のクエリの実行とは別にコンパイルをトリガーできるため、1 回のコンパイルで複数回実行できます。
利点は簡単に説明できます。Slick とデータベース サーバーの両方で、クエリのコンパイルに時間がかかります。同じクエリを何度も実行する場合は、コンパイルは 1 回だけの方が高速です。
Slick は、コレクション操作を含む AST を SQL ステートメントにコンパイルする必要があります。(実際には、コンパイルされたクエリがなければ、常に最初に AST をビルドする必要がありますが、コンパイル時間と比較すると、これは非常に高速です。)
データベース サーバーは、クエリの実行計画を作成する必要があります。これは、クエリを解析し、それをネイティブ データベース操作に変換し、データ レイアウト (使用するインデックスなど) に基づいて最適化を見つけることを意味します。この部分は、Slick でコンパイルされたクエリを使用しない場合でも、バインド変数を使用するだけで回避できるため、異なるパラメーター セットに対して常に同じ SQL コードを取得できます。データベース サーバーは、最近使用された/コンパイルされた実行計画のキャッシュを保持するため、SQL ステートメントが同一である限り、実行計画はハッシュ ルックアップのみであり、再度計算する必要はありません。Slick はこの種のキャッシングに依存しています。古いクエリを再利用するために Slick からデータベース サーバーに直接通信することはありません。
それらがどのように実装されるかについては、ストリーミング/非ストリーミングおよびコンパイル済み/適用済み/アドホック クエリを同じ方法で処理するための追加の複雑さがありますが、興味深いエントリ ポイントは次のCompiled
とおりです。
implicit def function1IsCompilable[A , B <: Rep[_], P, U](implicit ashape: Shape[ColumnsShapeLevel, A, P, A], pshape: Shape[ColumnsShapeLevel, P, P, _], bexe: Executable[B, U]): Compilable[A => B, CompiledFunction[A => B, A , P, B, U]] = new Compilable[A => B, CompiledFunction[A => B, A, P, B, U]] {
def compiled(raw: A => B, profile: BasicProfile) =
new CompiledFunction[A => B, A, P, B, U](raw, identity[A => B], pshape.asInstanceOf[Shape[ColumnsShapeLevel, P, P, A]], profile)
}
これにより、すべての暗黙的Compilable
なオブジェクトが得られますFunction
。アリティ 2 ~ 22 の同様のメソッドが自動生成されます。個々のパラメーターは のみを必要とするため、Shape
ネストされたタプル、HList、または任意のカスタム型にすることもできます。(引数としてaを取る aFunction10
よりも、たとえば a を書く方が構文的に便利なので、すべての関数アリティに抽象化を提供します。)Function1
Tuple10
Shape
コンパイルされた関数をサポートするためだけに存在するメソッドがあります。
/** Build a packed representation containing QueryParameters that can extract
* data from the unpacked representation later.
* This method is not available for shapes where Mixed and Unpacked are
* different types. */
def buildParams(extract: Any => Unpacked): Packed
このメソッドによって構築された「パックされた」表現QueryParameter
は、正しいタイプのノードを含む AST を生成できます。実際の値が不明であることを除いて、これらはコンパイル中に他のリテラルと同じように扱われます。エクストラクタidentity
は最上位レベルで開始され、必要に応じてレコード要素を抽出するように調整されます。たとえば、Tuple2
パラメーターがある場合、AST はQueryParameter
、後でタプルの最初と 2 番目のパラメーターを抽出する方法を知っている 2 つのノードになります。
この後のポイントは、コンパイルされたクエリが適用されるときです。このような を実行AppliedCompiledFunction
すると、事前にコンパイルされた SQL ステートメントが使用され (または、初めて使用するときにオンザフライでコンパイルされます)、エクストラクタを介して引数値をスレッド化することにより、ステートメント パラメータが入力されます。