Материализации
Вычисляемое свойство можно материализовать. В этом случае свойство будет храниться в базе данных постоянно и автоматически обновляться при изменении данных, от которых это свойство зависит. При этом при чтении значений такого материализованного свойства в сессии изменений, не изменившей данные, от которых оно зависит, эти значения будут читаться непосредственно из базы, как если бы свойство было первичным (а не вычисляться каждый раз). Соответственно, все первичные свойства являются материализованными по определению.
Свойство можно материализовать тогда и только тогда, когда для него существует конечное число наборов объектов, для которых значение этого свойства не NULL (то есть операция итерации по всем его не NULL значениям будет корректной).
Не материализованное свойство вычисляется внутри каждого запроса, который его читает: свойство, созданное операцией с множествами — группировкой, разбиением, рекурсией, — становится подзапросом такого запроса. Не материализованное абстрактное свойство вычисляется так же - из определений реализаций, которые могут подойти, вместе с выбором между ними, поэтому читающий его запрос растёт с их числом и размером, например при группировке, реализованной отдельно для каждого из нескольких классов-потомков. Если такое свойство читается внутри другой операции с множествами, его подзапрос включается в текст внешнего запроса - по разу на каждое место и каждый вид, в котором оно там читается (например, отдельно его новое и предыдущее значения при вычислении изменений), - поэтому текст одного запроса растёт с числом таких мест и размером вложенных определений. То же верно для запросов, которые обновляют хранимые значения материализованного свойства при изменении данных, от которых оно зависит: определения читаемых им не материализованных свойств — для материализованной группировки в том числе её ключей группировки — разворачиваются в текст запроса обновления, который поэтому растёт с размером этих определений и может превысить ограничение ниже, даже когда изменение затрагивает несколько наборов объектов. Длина текста одного запроса ограничена рабочим параметром queryLengthLimit (по умолчанию 2000000 символов): более длинный запрос сервер приложений отклоняет ещё до выполнения (ошибка TOO LONG QUERY), независимо от того, сколько строк он вернул бы, и не повторяет его. Будут ли изменения промежуточных свойств, которые читает такое обновление, сначала записаны во временные таблицы (что может сократить запрос обновления), решается при построении запроса — по сложности определений и статистике изменённых данных, — поэтому одно и то же изменение может превысить ограничение на одном сервере и уложиться на другом либо на том же сервере в другое время. Ограничение проверяется при выполнении запроса, а не при загрузке модулей: даже если модули загрузились без ошибок, в том числе при проверке с dryRun, применение изменения данных, от которых зависит материализованное свойство, может завершиться ошибкой TOO LONG QUERY. Материализация внутреннего свойства заменяет его подзапрос чтением хранимых значений. Если внешний запрос читает такую группировку лишь для части групп — например, для ключей, записанных в локальное свойство при импорте, — платформа переносит это ограничение внутрь подзапроса, когда по статистике ограниченный подзапрос оценивается дешевле. Малое число различных ограничивающих значений может сделать такой перенос выгодным, но само соотношение числа этих значений и числа групп выбор плана не определяет. При заполнении локального свойства из памяти, в том числе при импорте, платформа подсчитывает фактическое число строк и число различных значений каждого столбца ключей и свойств, а затем округляет эти величины для планирования запросов. Если ограничение внутрь не перенесено, оно применяется к результату группировки, а само по себе предварительное чтение в локальное свойство не избавляет от вычисления группировки для всех групп. Абстрактное свойство можно материализовать, как и любое другое вычисляемое: его значения хранятся в одном поле его таблицы, а если ключ этой таблицы - абстрактный класс, поле хранит значения для объектов всех его потомков.
Запрос компилируется - его текст строится из определений читаемых им свойств - когда в кэше скомпилированных запросов (см. рабочие параметры) для него нет запроса, как при первом выполнении после запуска сервера. При большом определении заметное время может занимать сама компиляция: первое чтение после запуска может быть медленным, а повторные чтения используют скомпилированный запрос из кэша. Для материализованного свойства запрос читает хранимое поле вместо определения (в сессии, не изменившей данные, от которых оно зависит, как сказано выше), и определение в этот запрос не разворачивается.
Хранимые значения материализованного свойства можно пересчитать — заново вычислить из определения свойства. Это нужно, когда эти значения разошлись с определением: после изменения определения — для абстрактного свойства после изменения или добавления одной из его реализаций — или после прямого исправления данных в базе. Запуск сервера не пересчитывает значения свойства только из-за того, что его определение изменилось: при запуске платформа пересчитывает свойства, материализованные впервые, — поле которых только что создано, — и материализованные свойства, зависящие от статических объектов, добавленных при этом запуске, причём последние целиком, вместе с уже имеющимися хранимыми значениями. Поэтому до пересчёта хранимые значения остаются несогласованными с изменённым определением: изменение данных, от которых свойство зависит, обновляет только затронутые наборы объектов, а агрегирование может обновляться от своего хранимого значения — например, сумма прибавлением изменения к нему, — так что и эти значения не обязательно приводятся к новому определению.
Пересчет
Пересчет материализованного свойства сверяет сохраненные значения с его определением и записывает обратно те, что разошлись. То, как именно это делается, задают несколько рабочих параметров.
useRecalculateClassesInsteadOfInconsisentExpr (по умолчанию true) сначала пересчитывает классы таблицы, в которой хранится свойство, и только потом сверяет значения - так сверка может пользоваться обычным выражением свойства. При выключенном параметре классы не трогаются, а сверка идет по выражению, которое терпит несогласованные данные о классах, что добавляет соединений и требует больше памяти. Из двух вариантов пересчет классов грубее: он переписывает данные о классах всей таблицы. Если пересчет запрошен без классов, параметр не действует.
recalculateMaterializationsMixedSerializable (по умолчанию false) меняет то, как пересчет, идущий в собственной транзакции, удерживает уровень изоляции: расходящиеся строки сначала собираются вне транзакции, и на строгом уровне выполняется только их обратная запись. Это стоит лишней временной таблицы и второго запроса и ничего не меняет для пересчета, который администратор попросил выполнить в одной общей транзакции.
maxRecalculateTime (300000 миллисекунд) - не таймаут, по нему ничего не прерывается. Операция, занявшая больше этого времени, добавляется в отчет, который возвращает сервисное действие, - так видно медленные таблицы и свойства. 0 выводит в отчет практически каждую операцию, так как сравнение строгое.
groupByTables (по умолчанию true) группирует пересчитываемые свойства по таблице, в которой они хранятся: при многопоточном пересчете свойства одной таблицы выполняются одно за другим, а не одновременно, и поэтому не ждут блокировок друг друга. Он же обновляет - при старте сервера - статистику по колонкам, которые синхронизация схемы базы только что создала или перенесла. При выключенном параметре это обновление статистики пропускается совсем, и запросы по таким колонкам сохраняют плохие планы, пока статистика не будет пересчитана иначе, поэтому его лучше оставить включенным.
Язык
Для материализации свойства используется опция MATERIALIZED в опциях свойств. Для пересчета хранимых значений материализованного свойства используется оператор RECALCULATE.
Примеры
sum = GROUP SUM sum(OrderDetail od) BY order(od) MATERIALIZED;
date(OrderDetail od) = date(order(od)) MATERIALIZED;
// такое свойство нельзя материализовать, так как оно не NULL для бесконечного числа дат
lastDate(Customer customer, DATE date) = GROUP LAST date(Order order) IF customer(order) = customer AND date(order) < date ORDER order;