Как было уже замечено при съемках нашего сериала, принятая вендором парадигма сплошного повторного использования кусков кода приводит к тому, что эти куски сплошь и рядом вызываются в цикле по результату запроса. Ничего не имею против облегчения труда по контролю кода. Но в результате мы сильно проседаем в производительности, если этот функционал у пользователя оказывается сериозно загружен.
И это - не беда, кому надо, купит на hh.ru грамотного 1сника, и он вынесет функционал этих кусков в основной запрос. Тогда результат запроса не надо уже обрабатывать, но можно будет без накладных расходов использовать по назначению. Был бы запрос хороший.
Проблемы, как оказалось, есть с расчетом значений по индивидуальным формулам. Их из цикла так просто не вынесешь.
**
Неожиданно, эти формулы, совершенно проходная вещь для 1сника (со времен 1С 6.0, по крайней мере, DOS-овские варианты платформы автор не застал) оказывается предметом научной публикации.
Встречайте: Юргинский технологический институт (филиал) ФГАОУ ВО "Национального
исследовательского Томского политехнического университета", г. Юрга. "Подход к настройке рассчитываемых показателей в пользовательском режиме платформы "1С:Предприятие 8"
Abstract
The author describes a method to create the parameters and the parameter
calculation formulas in user mode without the need to edit code in Designer.
Для примера, приведенного на рис. 3, порядок преобразования исходной символьной
формулы в итоговое расчетное значение показателя будет следующим:
• изначально формула представлена как "Д-Р";
• формула "Д-Р" преобразуется в вид "Доходы-Расходы", т.е. краткие обозначения
переменных формулы меняются на показатели, участвующие в расчете;
• показатели формулы "Доходы-Расходы" преобразуются в значения этих
показателей, и формула из рассматриваемого примера принимает вид "85000-80000";
• полученный на 3-м шаге программный код передается в оператор "Выполнить", и
расчетное значение показателя "Прибыль до налогообложения" получится как 5000.
**
И вот как этот "подход" выглядит со стороны Designer'а в УТ 11.5.16.97:
А что так много? В конфигураторе-то? В научной статье - три строчки: условные значки преобразуются к значениям из базы, значения подставляются в формулу, формула передается в оператор "Выполнить", успех!
За кулисами научной статьи осталось то обстоятельство, что формула, которую пользователь пишет в строковое поле:
а) связана с данными в базе через условное обозначение, в научной статье - через код элемента справочника, в "Видах цен" УТ 1.5 - через уникальный в пределах справочника набор букв в специальном реквизите "Идентификатор". И эту связь надо преобразовать в связь со ссылкой элемента (с конкретной строкой в таблице базы данных);
б) По ссылке надо найти числовое значение для подстановки в формулу и подставить на место условного обозначения.
в) И, наконец, получив выражение в строке, которую поймет интерпретатор языка 1С, вычислить конечный результат.
Для того, чтобы все это сработало, программисту 1С в конфигураторе приходится писать фактически еще один интерпретатор - из строки, которую придумает пользователь, в строку, которую поймет "система", т.е. платформа 1С. Всем этим и заняты процедуры в пяти (!) общих модулях на рисунке выше. (upt: Транслятор! Вспомнил, машинка, которая переводит с одного птичьего языка на другой, называется транслятор).
Наша проблема, в рамках проекта "Ценовая власть", заключается в том, что вынести эту функцию - целого интерпретатора, на минуточку! - из цикла по элементам справочника (по Валютам - при вычислении курса, по Видам цен и по Номенклатуре - при вычислении цены, по элементам ресурсной спецификации - на производстве) в один запрос. Как бы этому ни противились уважаемые мэтры, требующие замеров.
Строка формулы здесь, по условиям задачи, индивидуальна по определению, и существует в базе данных в виде литеральной строки, которую запрос и вернет в виде литеральной же строки. И то обстоятельство, что это - формула, которую надо бы вычислить, ни языку запросов 1С, ни даже T-SQL-ю никак не объяснишь.
Но... Но, гг присяжные заседатели! Вендор предоставил нам такую замечательную фичу, как Система Компоновки Данных.
**
Итак, в пустышке создаем справочник Валюты.
Заполняем. Две базовые, без формул. И сотню зависимых, с формулами = первая базовая, другая, их сумма, и, для уникальности формулы, довесок в виде порядкового номера после запятой. Как-то так:
&НаСервереБезКонтекста
Процедура ЗаполнитьНаСервере()
НовыйЭлемент = Справочники.Валюты.СоздатьЭлемент();
НовыйЭлемент.Наименование = "База1";
НовыйЭлемент.Записать();
НовыйЭлемент = Справочники.Валюты.СоздатьЭлемент();
НовыйЭлемент.Наименование = "База2";
НовыйЭлемент.Записать();
ГСЧ = Новый ГенераторСлучайныхЧисел();
Для Счетчик = 1 По 100 Цикл
НовыйЭлемент = Справочники.Валюты.СоздатьЭлемент();
НовыйЭлемент.Наименование = "Валюта"+Формат(Счетчик,"ЧЦ=3; ЧВН=");
Случайное = ГСЧ.СлучайноеЧисло(1,10) * 2;
ПослеЗапятой = Формат(Счетчик/1000,"ЧРД=.");
НомерФормулы = ГСЧ.СлучайноеЧисло(1,3);
Если НомерФормулы = 1 Тогда
Формула = "База1 + " + ПослеЗапятой;
ИначеЕсли НомерФормулы = 2 Тогда
Формула = "База2 + " + ПослеЗапятой;
Иначе
Формула = "База1 + База2 + " + ПослеЗапятой;
КонецЕсли;
НовыйЭлемент.Формула = Формула;
НовыйЭлемент.Записать();
КонецЦикла;
КонецПроцедуры
&НаКлиенте
Процедура Заполнить(Команда)
ЗаполнитьНаСервере();
КонецПроцедуры
Идем теперь в регистр сведений, и заполняем там курсы базовых валют, так:
&НаСервереБезКонтекста
Процедура ЗаполнитьБазуНаСервере()
Период = Дата(2014,1,1);
База1 = Справочники.Валюты.НайтиПоНаименованию("База1");
База2 = Справочники.Валюты.НайтиПоНаименованию("База2");
НаборЗаписей = РегистрыСведений.КурсыВалют.СоздатьНаборЗаписей();
ГСЧ = Новый ГенераторСлучайныхЧисел;
Для Счетчик = 1 По 100 Цикл
Курс = ГСЧ.СлучайноеЧисло(1,2);
Запись = НаборЗаписей.Добавить();
Запись.Период = Период;
Запись.Валюта = База1;
Запись.КурсВЦикле = Курс;
Запись = НаборЗаписей.Добавить();
Запись.Период = Период;
Запись.Валюта = База2;
Запись.КурсВЦикле = Курс + 1;
Период = КонецДня(Период)+1;
КонецЦикла;
НаборЗаписей.Записать(Истина);
КонецПроцедуры
&НаКлиенте
Процедура ЗаполнитьБазу(Команда)
ЗаполнитьБазуНаСервере();
КонецПроцедуры
Итак, тестовые данные подготовлены.
Теперь будем заполнять курсы расчетных валют, с расчетом в цикле - колонку "Курс в цикле", и расчетом в запросе СКД - колонку "Курс запросом СКД".
**
Первый вариант заполнения - предельно честный, то есть воспроизводящий логику повторных кусков кода, как она есть сами знаете где.
При этом запись результата вычисления формул в замере времени не участвует, поставлен комментарий. Сама запись была нужна для того, чтобы убедиться в корректности вычислений.
Если вам это захочется увидеть своими глазами - просто уберите "//".
Итак, вот такой код:
&НаСервереБезКонтекста
Процедура ЗаполнитьРасчетныеВЦиклеСПолучениемДанныхВЦиклеНаСервере()
НачалоЧтения = ТекущаяУниверсальнаяДатаВМиллисекундах();
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Валюты.Ссылка КАК Ссылка,
| Валюты.Формула КАК Формула
|ИЗ
| Справочник.Валюты КАК Валюты
|ГДЕ
| (ВЫРАЗИТЬ(Валюты.Наименование КАК СТРОКА(1))) = ""В""
|
|УПОРЯДОЧИТЬ ПО
| Валюты.Наименование";
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписиРасчетныеВалюты = РезультатЗапроса.Выбрать();
Прочитали = ТекущаяУниверсальнаяДатаВМиллисекундах();
КонечнаяДата = Дата(2014,4,10);
Пока ВыборкаДетальныеЗаписиРасчетныеВалюты.Следующий() Цикл
Период = Дата(2013,12,31);
//НаборЗаписей = РегистрыСведений.КурсыВалют.СоздатьНаборЗаписей();
Пока ИСТИНА Цикл
Период = КонецДня(Период)+1;
Если Период > КонечнаяДата Тогда
Прервать
КонецЕсли;
Запрос.Текст =
"ВЫБРАТЬ
| КурсыВалют.Валюта.Наименование КАК Валюта,
| КурсыВалют.КурсВЦикле КАК Курс
|ИЗ
| РегистрСведений.КурсыВалют КАК КурсыВалют
|ГДЕ
| (ВЫРАЗИТЬ(КурсыВалют.Валюта.Наименование КАК СТРОКА(1))) = ""Б""
| И КурсыВалют.Период = &Период
|
|УПОРЯДОЧИТЬ ПО
| КурсыВалют.Валюта.Наименование";
Запрос.УстановитьПараметр("Период", Период);
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписиБаза = РезультатЗапроса.Выбрать();
ВыборкаДетальныеЗаписиБаза.Следующий();
База1 = ВыборкаДетальныеЗаписиБаза.Курс;
ВыборкаДетальныеЗаписиБаза.Следующий();
База2 = ВыборкаДетальныеЗаписиБаза.Курс;
Результат = Вычислить(ВыборкаДетальныеЗаписиРасчетныеВалюты.Формула);
//Запись = НаборЗаписей.Добавить();
//Запись.Период = Период;
//Запись.Валюта = ВыборкаДетальныеЗаписиРасчетныеВалюты.Ссылка;
//Запись.КурсВЦикле = Результат;
КонецЦикла;
//НаборЗаписей.Записать(Ложь);
КонецЦикла;
Вычислили = ТекущаяУниверсальнаяДатаВМиллисекундах();
Сообщить("Читали за " + (Прочитали-НачалоЧтения) + ", вычисляли формулы "+(Вычислили - Прочитали)+", всего "+(Вычислили-НачалоЧтения)+" миллисекунд");
КонецПроцедуры
&НаКлиенте
Процедура ЗаполнитьРасчетныеВЦиклеСПолучениемДанныхВЦикле(Команда)
ЗаполнитьРасчетныеВЦиклеСПолучениемДанныхВЦиклеНаСервере();
КонецПроцедуры
... выполнен за:
... четыре с половиной секунды. Без записи на диск, повторю. Запись - это отдельная большая история.
С записью: "Читали за 1, вычисляли формулы 6 651, всего 6 652 миллисекунд", - 6,6 секунды.
**
В следующем, улучшенном в соответствии с продвигаемой идеей, варианте получение данных уже вынесено из цикла, на месте остается только вычисление формул.
&НаСервереБезКонтекста
Процедура ЗаполнитьРасчетныеВЦиклеНаСервере()
НачалоЧтения = ТекущаяУниверсальнаяДатаВМиллисекундах();
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Валюты.Ссылка КАК Ссылка,
| Валюты.Формула КАК Формула
|ПОМЕСТИТЬ СпрВ
|ИЗ
| Справочник.Валюты КАК Валюты
|ГДЕ
| (ВЫРАЗИТЬ(Валюты.Наименование КАК СТРОКА(1))) = ""В""
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Валюты.Ссылка КАК Ссылка,
| Валюты.Наименование КАК Наименование
|ПОМЕСТИТЬ СпрБ
|ИЗ
| Справочник.Валюты КАК Валюты
|ГДЕ
| (ВЫРАЗИТЬ(Валюты.Наименование КАК СТРОКА(1))) = ""Б""
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| СпрБ.Наименование КАК Валюта,
| КурсыВалют.КурсВЦикле КАК Курс,
| КурсыВалют.Период КАК Период
|ПОМЕСТИТЬ База
|ИЗ
| РегистрСведений.КурсыВалют КАК КурсыВалют
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ СпрБ КАК СпрБ
| ПО КурсыВалют.Валюта = СпрБ.Ссылка
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| База.Период КАК Период,
| СпрВ.Ссылка КАК Ссылка,
| База.Валюта КАК Валюта,
| СпрВ.Формула КАК Формула,
| База.Курс КАК Курс
|ИЗ
| СпрВ КАК СпрВ
| ЛЕВОЕ СОЕДИНЕНИЕ База КАК База
| ПО (ИСТИНА)
|
|УПОРЯДОЧИТЬ ПО
| Период,
| Ссылка,
| Валюта";
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписиРасчетныеВалюты = РезультатЗапроса.Выбрать();
Прочитали = ТекущаяУниверсальнаяДатаВМиллисекундах();
НаборЗаписей = РегистрыСведений.КурсыВалют.СоздатьНаборЗаписей();
Пока ВыборкаДетальныеЗаписиРасчетныеВалюты.Следующий() Цикл
База1 = ВыборкаДетальныеЗаписиРасчетныеВалюты.Курс;
ВыборкаДетальныеЗаписиРасчетныеВалюты.Следующий();
База2 = ВыборкаДетальныеЗаписиРасчетныеВалюты.Курс;
Результат = Вычислить(ВыборкаДетальныеЗаписиРасчетныеВалюты.Формула);
Запись = НаборЗаписей.Добавить();
Запись.Период = ВыборкаДетальныеЗаписиРасчетныеВалюты.Период;
Запись.Валюта = ВыборкаДетальныеЗаписиРасчетныеВалюты.Ссылка;
Запись.КурсВЦикле = Результат;
КонецЦикла;
НаборЗаписей.Записать(ЛОЖЬ);
Вычислили = ТекущаяУниверсальнаяДатаВМиллисекундах();
Сообщить("Читали за " + (Прочитали-НачалоЧтения) + ", вычисляли формулы "+(Вычислили - Прочитали)+", всего "+(Вычислили-НачалоЧтения)+" миллисекунд");
КонецПроцедуры
&НаКлиенте
Процедура ЗаполнитьРасчетныеВЦикле(Команда)
ЗаполнитьРасчетныеВЦиклеНаСервере();
КонецПроцедуры
С записью, в десять раз быстрее:
Читали за 60, вычисляли формулы 602, всего 662 миллисекунд
Читали за 59, вычисляли формулы 565, всего 624 миллисекунд
Без записи:
Читали за 62, вычисляли формулы 190, всего 252 миллисекунд
Успех? Успех. Хотя чтение дольше благодаря более сложному начальному запросу.
**
Теперь вынесем из цикла и вычисление пользовательских формул. Ну, как из цикла. Автор - невеликий знаток СКД, но что-то ему подсказывает, что вычисляемые поля - это то, что машинка СКД вычисляет как раз в цикле по полученным данным.
То есть, мы выполним основной запрос, где СКД получит всё, кроме вычисления пользовательских формул. Затем в цикле по таблице выполнит вычисление формул, а уж потом отдаст ее нам, чтобы мы могли ее записать.
Вот этот алгоритм:
&НаСервере
Процедура ЗаполнитьПоСКДНаСервере()
НачалоЧтения = ТекущаяУниверсальнаяДатаВМиллисекундах();
СхемаКомпоновкиДанных = РегистрыСведений.КурсыВалют.ПолучитьМакет("СхемаКД");
КомпоновщикНастроек = Новый КомпоновщикНастроекКомпоновкиДанных();
КомпоновщикНастроек.Инициализировать(Новый ИсточникДоступныхНастроекКомпоновкиДанных(СхемаКомпоновкиДанных));
КомпоновщикНастроек.ЗагрузитьНастройки(СхемаКомпоновкиДанных.НастройкиПоУмолчанию);
Настройки = КомпоновщикНастроек.Настройки;
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, Настройки,,,Тип("ГенераторМакетаКомпоновкиДанныхДляКоллекцииЗначений"));
ПроцессорКомпоновкиДанных = Новый ПроцессорКомпоновкиДанных;
ПроцессорКомпоновкиДанных.Инициализировать(МакетКомпоновки,,,Истина);
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений;
ДанныеТЗ = Новый ТаблицаЗначений;
ПроцессорВывода.УстановитьОбъект(ДанныеТЗ);
ПроцессорВывода.Вывести(ПроцессорКомпоновкиДанных);
Прочитали = ТекущаяУниверсальнаяДатаВМиллисекундах();
НаборЗаписей = РегистрыСведений.КурсыВалют.СоздатьНаборЗаписей();
НаборЗаписей.Загрузить(ДанныеТЗ);
НаборЗаписей.Записать(Ложь);
Записали = ТекущаяУниверсальнаяДатаВМиллисекундах();
Сообщить("Читали и вычисляли за " + (Прочитали-НачалоЧтения) + ", писали "+(Записали - Прочитали)+", всего "+(Записали-НачалоЧтения)+" миллисекунд");
КонецПроцедуры
&НаКлиенте
Процедура ЗаполнитьПоСКД(Команда)
ЗаполнитьПоСКДНаСервере();
КонецПроцедуры
Читали и вычисляли за 496, писали 201, всего 697 миллисекунд
Без записи:
**
Итого, миллисекунд
Вариант алгоритма | Данные и формулы внутри цикла | Формулы внутри цикла | Всё вне цикла |
---|---|---|---|
с записью результатов | 6650 | 630 | 680 |
без записи результатов | 4520 | 250 | 480 |
Что означают эти цифры? Во-первых, они означают, что получение данных и выполнение пользовательских формул внутри цикла по результату основного запроса - однозначное зло.
Во-вторых, запись результатов, полученных из СКД, однозначно рулит. Просто загрузи таблицу!
А вот выполнение пользовательских функций внутри цикла основного запроса выиграло у схемы СКД.
Тут еще дело может быть в том, что у меня не получилось вычислить эти формулы средствами самого СКД, пришлось как обычно обращаться к функции общего модуля.
И еще немаловажен тот факт, что я работаю на домашней машине, и серверной лицензии у меня нет, как нет и никакого SQL. Так что оценить выигрыш, который получится за счет сокращения не "серверных" вызовов, а вызовов именно к SQL-базе, у меня не выйдет от слова никак не могу.
В целом, я считаю опыт вполне успешным. К сожалению, применить это именно к функциональному блоку ценообразования в обозримом будущем не выйдет. Причин для сомнений много. Это и, мягко говоря, невысокий интерес к теме со стороны коллег здесь, на ИС, такой мягкий индикатор. И такое толстое обстоятельство, что ценообразованием занимается (если занимается) топ-менеджмент, а они в этот ваш 1с как-то не очень. По большей части привычки у них ексельные. Плюс ко всему, ценообразование с такими широкими возможностями, которые предоставлены последней версией (это где-то два с половиной года?) еще просто не завоевала широкого признания. Рассчитывать тут можно только на "молодую кровь", которая раньше в этой области не работала.
Но! Но, господа присяжные заседатели! Пользовательские формулы - это не только ценообразование. Поставьте отбор по "Формул" в конфигурации 1С:ERP Управление предприятием 2 - там полно этой радости.
Поэтому коллеги, за небольшой бонус (10 стартманей - цена часа работы), я отдаю вам, практически даром, замечательную базу, в которой вы можете потрогать это своими руками. На волшебную палочку не рассчитывайте. На самом деле здесь был весьма узкий интерес - только сами пользовательские формулы. А в реальности основной запрос будет ОЧЕНЬ сложным, вытаскивать из-под цикла придется очень много. Но вам придется этим заняться, если (не если, а когда) склад, производство (и продажники!) встанут и пойдут курить.
Проверено на следующих конфигурациях и релизах:
- Управление торговлей, редакция 11, релизы 11.5.15.49