Slick中的编译查询实际上是如何工作的?

13

我需要一份关于编译查询执行的详细解释。我无法理解它们如何只需要编译一次以及使用它们的好处。


1
不是它们只执行一次,而是只编译一次。 - pedrofurla
为什么需要编译超过一次?你是否熟悉预处理语句? - pedrofurla
实际上,我正在尝试理解一个漂亮的例子http://slick.typesafe.com/doc/2.1.0/queries.html#compiled-queries,作为一个初学者,我无法理解使用编译查询的优势。 - Im89
未编译的查询需要花费更长时间来执行,因为它们需要从 Slick 结构翻译成实际的 SQL。在已编译的查询中,编译只发生一次,并且您可以在每次执行中重用结果。 - pedrofurla
当您在Slick中构建查询时,实际上会构建一个表示您查询或查询部分(如条件、连接等)的数据结构。这个结构需要在某个时刻编译成SQL。 - pedrofurla
显示剩余2条评论
2个回答

18
假设这个问题是关于使用方式而不是编译查询的内部实现,那么这就是我的答案:
当您编写Slick查询时,Slick实际上会为所有涉及的表达式创建一个数据结构 - 抽象语法树(AST)。当您想要运行此查询时,Slick将获取该数据结构并将其翻译(或换句话说,编译)成SQL字符串。这可能是一个相当耗时的过程,比在数据库上执行快速SQL查询所需的时间还要长。因此,理想情况下,我们不应每次需要执行查询时都进行这种翻译为SQL的过程。但是如何避免呢?通过缓存翻译/编译后的SQL查询。
Slick可以做一些事情,例如仅在第一次编译它并为下一次缓存它。但是它没有这样做,因为这使得用户难以推断Slick的执行时间,因为相同的代码第一次会很慢,但之后会更快。 (此外,Slick需要在第二次运行查询时识别查询并在某些内部缓存中查找SQL,这会使实现变得复杂)。
因此,除非您明确地将其缓存起来,否则Slick每次都会编译查询。这使得行为非常可预测,最终更容易。要缓存它,您需要使用Compiled并将结果存储在下次需要查询时不会重新计算的位置。因此,像def q1 = Compiled(...)这样的def没有太多意义,因为它会每次都编译。它应该是一个vallazy val。而且您可能不希望将那个val放入您多次实例化的类中。相反,一个好的位置是顶级Scala单例object中的val,它只计算一次并保持JVM的生命周期。
因此,换句话说,Compiled没有任何神奇之处。它只允许您显式触发Slick的Scala-to-SQL编译,并返回包含SQL的值。重要的是,这允许您从实际执行查询中分开触发编译,从而使您可以编译一次,但运行多次。

10
< p >这个< em >优点很容易解释:在Slick和数据库服务器中,查询编译需要时间。如果您多次执行相同的查询,则编译一次会更快。

Slick需要将包含集合操作的AST编译成SQL语句。(实际上,如果没有编译查询,则始终必须先< em >构建 AST,但与编译时间相比,这非常快速。)

数据库服务器必须为查询构建执行计划。这意味着解析查询,将其转换为本地数据库操作,并基于数据布局(例如使用哪个索引)查找优化。即使您不使用Slick中的编译查询,也可以通过使用绑定变量来避免这部分工作,以便在不同参数集的情况下始终获得相同的SQL代码。数据库服务器保留最近使用/编译的执行计划缓存,因此只要SQL语句相同,执行计划只需进行哈希查找,无需重新计算。Slick依赖于这种缓存。没有直接从Slick到数据库服务器的通信来重用旧查询。

至于它们如何实现,在处理流式/非流式以及编译/应用/特定场景查询时有一些额外的复杂性,但有趣的入口点在< code >编译中:

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)
}
每个Function都会隐式地生成一个Compilable对象。对于2到22参数的函数,类似的方法会自动生成。由于每个参数只需要一个Shape,因此它们也可以是嵌套的元组、HList或任何自定义类型。(我们仍然为所有函数参数提供抽象,因为在语法上更方便编写,例如,写一个接受Tuple10作为其参数的Function1可能不如写一个Function10方便。) 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节点,它们知道稍后如何提取元组的第一个和第二个参数。

稍后的时间点是在应用编译后的查询时。执行此类AppliedCompiledFunction会使用预编译的SQL语句(或在首次使用时即时编译)并通过提取器将参数值传递填充到语句参数中。


1
非常感谢您的回答。现在,我理解了编译查询的用途,但我无法在示例中尝试它们。您能给我一个具体的例子吗?谢谢。 - Im89
1
您可以在此处找到大量示例:https://github.com/slick/slick/blob/master/slick-testkit/src/main/scala/com/typesafe/slick/testkit/tests/TemplateTest.scala - szeiger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接