Как на самом деле работают скомпилированные запросы в Slick?

Я ищу подробное объяснение о выполнении скомпилированных запросов. Я не могу понять, как они просто скомпилируют один раз, и каковы преимущества их использования

2 ответа

Предполагая, что этот вопрос касается использования, а не внутренней реализации скомпилированных запросов, вот мой ответ:

Когда вы пишете запрос Slick, Slick фактически создает внутреннюю структуру данных для всех задействованных выражений - абстрактное синтаксическое дерево (AST). Когда вы хотите выполнить этот запрос, Slick берет структуру данных и переводит (или, другими словами, компилирует) ее в строку SQL. Это может быть довольно длительный процесс, занимающий больше времени, чем фактическое выполнение быстрых SQL-запросов к БД. Поэтому в идеале мы не должны делать этот перевод в SQL каждый раз, когда требуется выполнить запрос. Но как этого избежать? Кешируя переведенный / скомпилированный запрос SQL.

Slick может сделать что-то вроде только компиляции в первый раз и кеширования в следующий раз. Но это не так, потому что из-за этого пользователю сложнее рассуждать о времени выполнения Slick, потому что тот же код будет медленным в первый раз, но быстрее позже. (Также Slick должен был бы распознавать запросы, когда они выполняются во второй раз, и искать SQL в некотором внутреннем кеше, что усложнит реализацию).

Поэтому вместо этого Slick компилирует запрос каждый раз, если вы явно не кешируете его. Это делает поведение очень предсказуемым и в конечном итоге легче. Чтобы его кешировать, нужно использовать Compiled и сохраните результат в месте, которое НЕ будет пересчитано при следующем запросе. Таким образом, используя def лайк def q1 = Compiled(...) не имеет большого смысла, потому что он будет компилировать каждый раз. Это должно быть val или же lazy val, Также вы, вероятно, не хотите помещать этот val в класс, который вы создаете несколько раз. Хорошее место вместо этого val в синглтоне высшего уровня Scala object, который рассчитывается только один раз и сохраняется в течение времени жизни JVM.

Другими словами, Compiled не делает ничего волшебного. Это позволяет вам только явно запускать компиляцию Scala-to-SQL Slick и возвращать значение, содержащее SQL. Важно отметить, что это позволяет запускать компиляцию отдельно от фактического выполнения запроса, что позволяет компилировать один раз, но запускать его несколько раз.

Преимущество легко объяснить: компиляция запросов занимает много времени, как в Slick, так и на сервере базы данных. Если вы выполняете один и тот же запрос много раз, его быстрее скомпилировать только один раз.

Slick необходимо скомпилировать AST с операциями сбора в оператор SQL. (На самом деле, без скомпилированных запросов всегда нужно сначала создать AST, но по сравнению со временем компиляции это очень быстро.)

Сервер базы данных должен построить план выполнения для запроса. Это означает анализ запроса, перевод его в собственные операции с базой данных и поиск оптимизаций на основе макета данных (например, какой индекс использовать). Этой части можно избежать, даже если вы не используете скомпилированные запросы в Slick, просто используя переменные связывания, так что вы всегда получаете один и тот же код SQL для разных наборов параметров. Сервер базы данных хранит кэш недавно использованных / скомпилированных планов выполнения, поэтому, пока оператор SQL идентичен, план выполнения является только поиском по хешу и не требует повторного вычисления. Слик полагается на этот вид кеширования. Нет прямой связи между 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, они также могут быть вложенными кортежами, HLists или любыми другими типами. (Мы по-прежнему предоставляем абстракции для всех функций, потому что синтаксически удобнее написать, скажем, Function10 чем 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

"Упакованное" представление, построенное этим методом, может создать AST, содержащий QueryParameter узлы с правильным типом. Они обрабатываются так же, как и другие литералы во время компиляции, за исключением того, что фактические значения не известны. Экстрактор начинается как identity на верхнем уровне и уточняется для извлечения элементов записи по мере необходимости. Например, если у вас есть Tuple2 параметр, AST будет в конечном итоге с двумя QueryParameter узлы, которые знают, как извлечь первый и второй параметр кортежа в более поздней точке.

Это более поздний момент, когда применяется скомпилированный запрос. Выполнение такого AppliedCompiledFunction использует предварительно скомпилированный оператор SQL (или компилирует его на лету, когда вы используете его впервые) и заполняет параметры оператора, пропуская значение аргумента через экстракторы.

Другие вопросы по тегам