На старт
Все мы используем в разработке такой объект как регистры сведений. Мы все о них знаем, все умеем. Среди регистров - это самый простой, понятный объект, в котором нет практически ничего лишнего. По крайней мере, так кажется до поры до времени.
В нескольких статьях будут представлены основные сведения о внутреннем устройстве регистров сведений, о SQL-запросах платформы при работе с ними и их изменение в зависимости от настроек регистра. Рассмотрим некоторые особенности с выходом платформы 8.3 и совсем немного об оптимизации работы с ними.
Материалы ниже не являются всеобъемлющей инструкцией или руководством. А на Инфостарт есть более полная статья с описанием множества нюансов этого типа объекта. Это публикация от Сергея Носкова - "Регистры сведений 1С. Как это устроено.". Здесь же Вы найдете самую общую информацию, а некоторые темы совсем не будут раскрыты. Однако, это может быть отличным началом к исследованию внутренних механизмов платформы.
Структура хранения
Поговорим о регистрах сведений. Но не о на настройках и их правильном использовании, а о скрытой от разработчиков стороне СУБД. Рассмотрим? как регистры сведений хранятся в базе данных.
Что там в базе
Структура таблиц, используемая для хранения данных и настроек регистра сведений, меняется в зависимости от настроек объекта метаданных в конфигураторе. Следующие настройки влияют на то, как будет платформ 1С:Предприятие 8.x хранить данные в базе:
Рассмотрим влияние этих настроек с простого примера. В тестовой базе у нас есть непериодический регистр сведений "Настройки":
Единственное измерение "СтаутсТовара" ссылается на перечисление, все ресурсы имеют примитивные типы. Структура хранения такого регистра ограничивается только одной таблицей в базе данных:
Как видно из рисунка, структура хранения такого регистра достаточно примитивна. Каждому полю регистра в дереве метаданных конфигурации соответствует поле таблицы на стороне СУБД.
Рассмотрим еще один простой пример.
Структура метаданных этого регистра также достаточно простая: измерение "Товар", ссылающееся на справочник "Товары", и ресурс "Статус", ссылающееся на перечисление "СтатусыТоваров". Отличие настроек этого регистра от предыдущего кроется в параметре "Периодичность", которая теперь установлена в значение "В пределах дня". Структура таблицы в базе для этого регистра будет следующей:
Кроме полей измерений и ресурсов в таблицу добавлено поле "Period", в котором хранится значение даты и времени записи. Вне зависимости от значения периодичности структура таблицы регистра не изменится. Если же обратно поменять периодичность в значение "Непериодический", то из структуры таблицы будет удалено поле "Period".
Если для регистра поставить настройку "Режим записи" в "Подчиненный регистратору", то в таблицу дополнительно добавится поле "RecorderRRef", в котором будет хранится ссылка на документ-регистратор, а также поле "LineNo" (Номер строки) и "Active" (Активность). Отдельно этот пример рассматривать не будем. Давайте лучше посмотрим на структуру периодического регистра сведений с включенной опцией хранения итогов среза последних:
Регистр с такими настройками использует две таблицы в базе данных. Первая - это основная таблица регистра:
Как раньше и было сказано, в таблице содержатся поля измерений и ресурсов регистра. Т.к. регистр подчинен регистратору, то в состав полей добавлено поле "RecorderRRef", в котором хранится ссылка на документ-регистратор. Периодичность у регистра установлена в "По позиции регистратора", но поле "Period" в таблице все равно осталось. При такой настройке в этом поле хранится дата и время документа.
Вторая таблица - это таблица итогов среза последних записей:
В ней хранятся последние записи регистра в разрезе периодов. В структуру таблицы входят измерения и ресурсы регистра, период, регистратор (если регистр подчинен регистратору) и реквизиты регистра (в нашем примере их нет).
Также бы выглядела и таблица итого среза первых, только хранила бы она срез первых записей в разрезе периодов. О плюсах использования таблиц итого регистров сведений мы поговорим позже.
Отдельно стоит упомянуть о таблице настроек хранения итогов регистра сведений. Для последнего примера эта таблица выглядит так (см. след. скриншот).
В таблице только одно поле, хранящее флаг включения итогов. Изменить эту настройку можно в режиме предприятия в управлении итогами:
Таблица настроек хранения итогов добавляется для регистра сведений, если значение периодичности регистра отличается от значения "Непериодический".
Рассмотрим пример формирования платформой таблиц итогов среза первых и среза последних.
Пример формирования таблиц итогов
Например, таблица движений регистра "Цены номенклатуры", который мы рассматривали в предыдущем примере, содержит следующие записи:
Тогда таблица итогов среза последних записей будет выглядеть так:
То есть в таблицу попали последние по периоду записи в разрезе измерений регистра. По такому же принципу формируется таблица итогов среза первых, только берутся самые первые записи в таблице движений:
Таким образом, таблицы итогов позволяют значительно ускорить выполнения запросов получения срезов последних / первых записей регистров. Подробнее об этом мы поговорим в следующей части статьи.
Пойдем дальше
Мы рассмотрели варианты хранения регистров сведений на стороне СУБД в зависимости от их настроек, а также познакомились со служебными таблицами регистра для хранения итогов и настроек хранения итогов. Стоит отметить, что возможность хранения итогов для регистров сведений появилась только в версии 8.3.
Далее мы отловим SQL Profiler'ом запросы, которые формирует платформа 1С:Предприятие к СУБД при работе с регистрами сведений.
Запросы платформы
Все запросы, которые формирует платформа для регистров сведений, можно разделить на два типа: для периодических регистров и непериодических. Начнем с простого примера запроса для непериодических регистров.
Непериодический регистр
В тестовой конфигурации у нас есть простой непериодический регистр "Настройки":
Если мы сделаем запрос к таблице регистра с отбором по полю "УчитыватьВДокументахПоступления", то получаем простейший SQL-запрос:
SELECT
T1._Fld33RRef,
T1._Fld34,
T1._Fld35,
T1._Fld36
FROM dbo._InfoRg32 T1
WHERE (T1._Fld34 = 0x00)
В запросе выбираются поля регистра, а в секции WHERE устанавливается отбор по полю. Рассмотрим примеры с периодическим регистром.
Периодический регистр
Тестовая база содержит периодический регистр:
Как было сказано ранее, такие регистры могут иметь на стороне СУБД несколько таблиц:
- Основная таблица регистра
- Таблицы итогов (одна или две, в зависимости от настроек итогов для регистра: итоги для среза первых и итоги для среза последних).
SQL-запрос к основной таблице итогов ничем не будет отличаться от запроса к таблице непериодического регистра. Другое дело запрос для получения среза последних/первых записей периодического регистра. Вот так, например, выглядит SQL-запрос для получения среза последних записей без установки параметра "Период":
Этот запрос используется платформой для получения среза последних записей. Принцип его работы прост: получаем макс. значение периода для записей в разрезе всех измерений, а дальше к этой таблице присоединяем записи из основной таблицы регистра. В результате мы получаем срез последних записей.
Аналогично выглядит запрос для получения среза первых, только в первом подзапросе получают не максимальное значение периода, а минимальное.
Из-за того, что в запросе присутствует несколько подзапросов и соединение с ними, оптимизатор СУБД не всегда может подобрать оптимальный план запроса, поэтому гарантировать стабильность выполнения этого запроса нельзя.
В версии 8.3 появились новые настройки, позволяющие избежать проблем с производительностью.
Особенности платформы 8.3
Версия 8.3 позволяет включить использование итогов среза первых и среза последних. Давайте рассмотрим какой запрос будет сформирован платформой для получения среза последних по итоговой таблице регистра сведений:
Как мы видим, для получения среза последних используется таблица итого. Вычисление макс./мин. значения периода для записей не выполняется. Такой запрос будет выполняться стабильней.
В примере, к подзапросу присоединяется левым соединением таблица справочника "Товары" для получения представления товара (Наименования).
Стоит заметить, что если срез последних получается в запросе без установки отбора по периоду (то есть текущий срез), то используется запрос выше. Если же поставить параметр "Период" для таблицы среза последних, то платформа будет использовать запрос аналогично запросы платформы 8.2. То есть таблица итогов не будет использоваться.
Далее будет рассмотрен пример написания собственного запроса получения среза последних / первых. Его можно использовать для ситуаций, когда нужно повысить стабильность выполнения запросов.
Свой запрос для среза последних
Написание собственного запроса для получения среза последних записей для 1С:Предприятия.
О чем идет речь
Ранее мы рассмотрели SQL-запросы, которые формирует платформа 1С:Предприятие при работе с регистрами сведений в зависимости от их настроек. Особый интерес вызывает запрос для получения среза первых / последних записей без использования таблицы итогов.
Как мы видим, в запросе используется соединение с подзапросом, что может стать причиной проблемы с производительностью из-за не оптимального плана запроса, который выберет оптимизатор СУБД. Это будет происходить не всегда, но 100% гарантии стабильности дать нельзя (подробнее о причинах неоптимальной работы с подзапросами будет идти речь в одной из следующих статей).
Поскольку мы не можем вмешиваться во внутренние механизмы платформы, то для решения этой проблемы самым простым ее вариантом будет написание собственного запроса на языке запросов платформы с использованием временных таблиц.
Пример запроса среза последних
Для получения среза последних записей напишем следующий запрос:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЦеныНоменклатуры.Товар КАК Товар,
| МАКСИМУМ(ЦеныНоменклатуры.Период) КАК Период
|ПОМЕСТИТЬ ПоследниеЗаписи
|ИЗ
| РегистрСведений.ЦеныНоменклатуры КАК ЦеныНоменклатуры
|СГРУППИРОВАТЬ ПО
| ЦеныНоменклатуры.Товар
|ИНДЕКСИРОВАТЬ ПО
| Товар,
| Период
|;
|ВЫБРАТЬ
| ПоследниеЗаписи.Товар,
| ЦеныНоменклатуры.Цена
|ИЗ
| ПоследниеЗаписи КАК ПоследниеЗаписи
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры КАК ЦеныНоменклатуры
| ПО ПоследниеЗаписи.Товар = ЦеныНоменклатуры.Товар
| И ПоследниеЗаписи.Период = ЦеныНоменклатуры.Период";
Во временную таблицу "ПоследниеЗаписи" мы получаем максимальные периоды с группировкой по необходимым измерениям (в нашем случае по изменению "Товар"). Во втором запросе, используя полученную таблицу максимальных периодов, мы находим записи в основной таблице движений по заданному измерению и периоду.
Если нужно поставить отбор, например, по товару, то запрос будет такой:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЦеныНоменклатуры.Товар КАК Товар,
| МАКСИМУМ(ЦеныНоменклатуры.Период) КАК Период
|ПОМЕСТИТЬ ПоследниеЗаписи
|ИЗ
| РегистрСведений.ЦеныНоменклатуры КАК ЦеныНоменклатуры
// Устанавливаем отборы по периоду среза
// последних записей и по товару
|ГДЕ
| ЦеныНоменклатуры.Товар = &Товар
| И ЦеныНоменклатуры.Период <= &Период
|...";
Отборы устанавливаются в той части запроса, где идет получение максимальных периодов по разрезам регистра сведений. Если мы посмотрим на SQL-запрос платформы в этом случае, то соединений с подзапросами мы не увидим:
1. Запрос получения макс. периодов
INSERT INTO #tt2 WITH(TABLOCK) (_Q_000_F_000RRef, _Q_000_F_001)
SELECT
T1._Fld27RRef,
MAX(T1._Period)
FROM dbo._InfoRg26 T1
WHERE (T1._Fld27RRef = @P1) AND (T1._Period <= @P2)
GROUP BY T1._Fld27RRef
2. Получение значений ресурсов регистра для найденных периодов и измерений с использованием сформированной ранее временной таблицы
SELECT
T1._Q_000_F_000RRef,
T2._Fld37
FROM #tt2 T1 WITH(NOLOCK)
LEFT OUTER JOIN dbo._InfoRg26 T2
ON ((T1._Q_000_F_000RRef = T2._Fld27RRef)
AND (T1._Q_000_F_001 = T2._Period))
В результат мы получили запрос без использования подзапросов, вместо которых создается временная таблица. Конечно, на ее создание тратятся дополнительные ресурсы, но стабильность выполнения запроса гарантирована.
Срез первых записей
Отдельно рассматривать ситуацию с получением среза первых записей смысла нет, так как запрос будет практически один в один с предыдущим. Единственное отличие - получать нужно не максимальное значение периода в первом запросе, а минимальное значение.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЦеныНоменклатуры.Товар КАК Товар,
| МИНИМУМ(ЦеныНоменклатуры.Период) КАК Период
|ПОМЕСТИТЬ ПоследниеЗаписи
|...";
Попробуйте самостоятельно написать такой запрос и поэкспериментировать с результатом.
Финиш
На этом небольшая заметка по внутреннему устройству регистров сведений подошла к концу. За бортом осталось множество вопросов:
- Устройство индексов регистра и их зависимость от настроек
- Как происходит запись в таблицы регистров
- Как работают управляемые блокировки для этого типа объектов
- Проблемы производительности при использовании итогов для среза первых / последних записей
- и другое.
Официальная документация, публикация от Сергея Носкова и Ваши собственные эксперименты всегда помогут ответить на все возникшие вопросы.
Спасибо за внимание!