gifts2017

Оптимизация расчета себестоимости выпуска продукции (УПП 1.3, Партионный учет)

Опубликовал Сергей Линков (linkov) в раздел Программирование - Практика программирования

В результате выполненных доработок удалось уменьшить время проведения Регламентной операции "Рассчитать себестоимость (БУ, НУ)" в два и более раза, а в отдельных операциях (Распределение продукции и затрат по переделам) удалось достичь 7-ми кратного уменьшения времени проведения!

До начала оптимизации дело обстояло так:

В отдельных месяцах проведение Регламентной операции "Рассчитать себестоимость (БУ, НУ)" удавалось осуществить только в выходные дни - проведение длилось более суток. При этом происходил лавинообразный рост служебной базы tempdb до астрономических 360 Гб.

Конфигурация:

  • Управление производственным предприятием, редакция 1.3 (1.3.79.2)
  • 1С:Предприятие 8.3 (8.3.8.1784)
  • Microsoft SQL Server 2012
  • Используется партионный учет
  • Расчет себестоимости ведется по переделам
  • Размер базы (.mdf) 38 Гб

Решение:

1. В борьбе с разрастанием служебной базы tempdb кардинально решить вопрос удалось следующим образом:

В процедурах:

  • ПроцедурыРасчетаСебестоимостиВыпуска.СформироватьТекстЗапросаЗаполнениеКорректировкиВстречногоВыпускаПродукции();
  • ПроцедурыРасчетаСебестоимостиВыпуска.СформироватьТекстЗапросаПоВыпускуПродукцииИЗатратамНаВыпуск();
  • ПроцедурыРасчетаСебестоимостиВыпуска.СформироватьТекстЗапросаПоЗатратамНаВыпуск();
  • ПроцедурыРасчетаСебестоимостиВыпуска.СформироватьТекстЗапросаБазаРаспределенияЗатратНаПродукцию();
  • РасчетСебестоимостиВыпускаРаспределениеПоПеределам.СформироватьТекстЗапросаВыпускБезПрямыхРасходов();
  • РасчетСебестоимостиВыпускаРаспределениеПоПеределам.СформироватьТекстЗапросаВыпускНаРаспределяемыеРасходы();
  • РасчетСебестоимостиВыпускаРаспределениеПоПеределам.СформироватьТекстЗапросаМатериальныеПроизводственныеРасходы();
  • РасчетСебестоимостиВыпускаРаспределениеПоПеределам.СформироватьТекстЗапросаНематериальныеПроизводственныеРасходы();
  • РасчетСебестоимостиВыпускаРаспределениеПоПеределам.СформироватьТекстЗапросаНематериальныеПроизводственныеРасходыНУ();

1.1. В текстах запросов конструкции языка связанные с обращением к реквизиту через точку заменяем на явное соединение с соответствующей таблицей. Особенно это актуально для справочника Номенклатура (реквизит ВестиУчетПоСериямВНЗП).

Пример: РасчетСебестоимостиВыпускаРаспределениеПоПеределам.СформироватьТекстЗапросаМатериальныеПроизводственныеРасходы()

//+    - добавленные строки

ВЫБРАТЬ РАЗЛИЧНЫЕ
	...
	//ВЫБОР КОГДА ЗатратыНаВыпуск.Продукция.ВестиУчетПоСериямВНЗП ТОГДА
	ВЫБОР КОГДА ЕСТЬNULL(ЗатратыНаВыпускПродукция.ВестиУчетПоСериямВНЗП, ЛОЖЬ) ТОГДА //+
	...
	//ВЫБОР КОГДА ЗатратыНаВыпуск.Затрата.ВестиУчетПоСериямВНЗП ТОГДА
	ВЫБОР КОГДА ЕСТЬNULL(ЗатратыНаВыпускЗатрата.ВестиУчетПоСериямВНЗП, ЛОЖЬ) ТОГДА //+
	...
ИЗ
	РегистрНакопления.ЗатратыНаВыпускПродукции%СуффиксУчета% КАК ЗатратыНаВыпуск
	...

	ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Номенклатура КАК ЗатратыНаВыпускПродукция //+
	ПО ЗатратыНаВыпускПродукция.Ссылка = ЗатратыНаВыпуск.Продукция //+

	ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Номенклатура КАК ЗатратыНаВыпускЗатрата //+
	ПО ЗатратыНаВыпускЗатрата.Ссылка = ЗатратыНаВыпуск.Затрата //+

1.2. В текстах запросов условия следующего вида (имеет смысл для вложенных запросов с объемистой выборкой):

ГДЕ
	ВыпускПродукции.Продукция В (
		ВЫБРАТЬ РАЗЛИЧНЫЕ
			БазаРаспределенияЗатрат.Продукция
		ИЗ
			РегистрСведений.БазаРаспределенияЗатрат%СуффиксУчета% КАК БазаРаспределенияЗатрат
		ГДЕ
			БазаРаспределенияЗатрат.Период МЕЖДУ &НачДата И &КонДата
			И БазаРаспределенияЗатрат.РаспределениеКосвенныхЗатрат = &РаспределениеКосвенныхЗатрат
			И БазаРаспределенияЗатрат.РасчетСебестоимостиВыпуска
		)

заменяем на внутреннее соединение следующего вида:

	ВНУТРЕННЕЕ СОЕДИНЕНИЕ (
		ВЫБРАТЬ РАЗЛИЧНЫЕ
			БазаРаспределенияЗатрат.Продукция
		ИЗ
			РегистрСведений.БазаРаспределенияЗатрат%СуффиксУчета% КАК БазаРаспределенияЗатрат
		ГДЕ
			БазаРаспределенияЗатрат.Период МЕЖДУ &НачДата И &КонДата
			И БазаРаспределенияЗатрат.РаспределениеКосвенныхЗатрат = &РаспределениеКосвенныхЗатрат
			И БазаРаспределенияЗатрат.РасчетСебестоимостиВыпуска
	) КАК БазаРаспределенияЗатрат
	ПО БазаРаспределенияЗатрат.Продукция = ВыпускПродукции.Продукция

В результате этих двух доработок (1.1 и 1.2) максимальный рост tempdb в итоге составил 1.8 Гб, так же существенно увеличилась скорость выполнения запросов.

2. Поиск из результата запроса заменяем на поиск строк из таблицы значений методом НайтиСтроки (в местах где происходит поиск в цикле с многотысячным количеством итераций)

Пример: ПроцедурыРасчетаСебестоимостиВыпуска.КорректировкаДвиженийПоВыпускуПродукции()

//+		- добавленные строки

//ВыборкаПоВыпуску = РезультатЗапросаПоВыпускуПродукции.Выбрать();
ТаблицаПоВыпуску = РезультатЗапросаПоВыпускуПродукции.Выгрузить(); //+
...

ОбходПоЗатратам = РезультатЗапросаПоЗатратамНаВыпускПродукции.Выбрать();
Пока ОбходПоЗатратам.Следующий() Цикл

	СтруктураПоискаВыпуск = ПолучитьСтруктуруПоискаСтрокВыпускаПродукции(
		СтруктураШапкиДокумента,
		ОбходПоЗатратам
		);
	...

	//ВыборкаПоВыпуску.Сбросить();
	//Пока ВыборкаПоВыпуску.НайтиСледующий(СтруктураПоискаВыпуск) Цикл // На этой строчке замер производительности показывал многочасовые ожидания
	МассивСтрокПоВыпуску = ТаблицаПоВыпуску.НайтиСтроки(СтруктураПоискаВыпуск); //+
	Для каждого ВыборкаПоВыпуску Из МассивСтрокПоВыпуску Цикл //+
		...

	КонецЦикла;
	...

КонецЦикла;

3. Перебор строк с нулевым переделом в отсортированной таблице заменяем на перебор элементов массива полученного из таблицы методом НайтиСтроки, то есть исключаем затратную операцию сортировки.

Пример: РасчетСебестоимостиВыпускаРаспределениеПоПеределам.СоздатьТабПеределов() - здесь удалось достичь 7-ми кратного выигрыша по времени !!!

//+    - добавленные строки

//МаксИндекс = ТабПеределов.Количество() - 1;
...

Пока ПроставленПередел Цикл
	...

	//ТабПеределов.Сортировать("НомерПередела Убыв");	// На этой строчке замер производительности показывал многочасовые ожидания (в нашей таблице до 6 млн. записей)
	//ТекСтрока = ТабПеределов.Найти(0, "НомерПередела");
	СтрокиСПустымиПеределами = ТабПеределов.НайтиСтроки(Новый Структура("НомерПередела", 0)); //+

	//Если ТекСтрока = Неопределено Тогда
	Если СтрокиСПустымиПеределами.Количество() = 0 Тогда //+
		Прервать;
	КонецЕсли;

	//Индекс = ТабПеределов.Индекс(ТекСтрока);
	...

	//ИндексСтроки = Индекс;
	//Пока ИндексСтроки <= МаксИндекс Цикл
	МаксИндекс = СтрокиСПустымиПеределами.ВГраница(); //+
	Для Сч=0 По МаксИндекс Цикл //+
		ИндексСтроки = МаксИндекс - Сч; //+

		//СтрокаТаблицы = ТабПеределов[ИндексСтроки];
		СтрокаТаблицы = СтрокиСПустымиПеределами[ИндексСтроки]; //+
		...

		СтрокаТаблицы.НомерПередела = 1;
		СтрокиСПустымиПеределами.Удалить(ИндексСтроки); //+ // Удаление текущей строки с проставленным переделом
		...

		//ИндексСтроки = ИндексСтроки + 1;

	КонецЦикла;
	...

КонецЦикла;

4. Так же был применён метод асинхронной записи регистров. По этому методу выигрыш по времени в наших условиях составил до 30%

Суть метода:

В документе Расчет себестоимости в оригинальном алгоритме реализована запись движений регистров порциями по 1000 строк по мере расчета.

	Если НаборЗаписейБазаРаспределенияЗатрат.Количество() = 1000 Тогда
		НаборЗаписейБазаРаспределенияЗатрат.Записать(Ложь);
	КонецЕсли;

Здесь метод Записать(Ложь); дописывает записи к уже существующим в информационной базе, а затем записи текущего набора очищает. При этом, в наших условиях, время расчета очередной порции и время записи строк этого набора в базу примерно сопоставимы.

Выносим процесс записи в отдельную процедуру и запускаем ее в фоновом задании передавая записи набора в виде таблицы значений через параметры. В этом случае расчет очередной порции строк начнется не дожидаясь завершения записи предыдущей.

Графически эти два варианта выглядят так:

1. Последовательные Расчет и Запись
Основной поток Расчет 1 Запись 1 Расчет 2 Запись 2 Расчет 3 Запись 3 Расчет 4 Запись 4 Расчет 5 Запись 5
2. Асинхронные Расчет и Запись
Основной поток Расчет 1 Расчет 2 Расчет 3 Расчет 4 Расчет 5 Ожидание
Фоновое задание 1 Запись 1
Фоновое задание 2 Запись 2
Фоновое задание 3 Запись 3
Фоновое задание 4 Запись 4
Фоновое задание 5 Запись 5

Замечания и ограничения применимости данного метода:

  • При стандартном проведении запись в регистры из фонового задания невозможна из-за конфликта блокировок (режим управления блокировкой данных - автоматический), метод можно реализовать только при так называемом "Проведении вне транзакции".
  • Как следствие из предыдущего, в коде необходимо предусмотреть признаки по которым при стандартном проведении должен выполняться последовательный алгоритм, а при проведении вне транзакции алгоритм с асинхронной записью.
  • Перед внедрением данного метода необходимо выполнить замер производительности. Может оказаться, что в Ваших условиях программно-аппаратной реализации, запись в информационную базу происходит достаточно быстро по сравнению с расчетом или наоборот. В этих случаях затраты времени на выгрузку записей набора в таблицу значений для передачи в фоновое задание и запуск фонового задания могут оказаться выше ожидаемого выигрыша.
  • Фоновые задания по каждому виду регистра необходимо выстраивать в стек, в котором каждое последующее задание должно ожидать завершение предыдущего.
  • В местах записи последней порции набора записей (до условия: Если НаборЗаписейБазаРаспределенияЗатрат.Модифицированность() Тогда) необходимо выполнить ожидание завершения фоновых заданий, запущенных в контексте по данному регистру.

Реализация метода:

Задачи.РегламентныеОперацииЗакрытияМесяца.ФормаЗадачи - на командную панель формы добавляем кнопку "Провести вне транзакции" и назначаем следующий обработчик:

Процедура КоманднаяПанельДокументыРОПровестиВнеТранзакции(Кнопка)
	
	Ответ = Вопрос("Внимание! Проведение документов вне транзакции можно выполнять только, 
	  |если одновременно не вводятся первичные документы в периоде, предшествующем проводимому документу.
  	  |Провести документы вне транзакции?", РежимДиалогаВопрос.ДаНет, 100, КодВозвратаДиалога.Нет);
	
	Если Ответ <> КодВозвратаДиалога.Да Тогда
		Возврат;
	КонецЕсли;	
	
	//Перед проведением упорядочим сформированные документы: сначала УУ, потом БУ, потом НУ
	//Для документа РасчетСебестоимости важно чтобы сначала провелся документ БУ, а лишь затем НУ
	СформированныеДокументы.Сортировать("ОтражатьВУправленческомУчете убыв, ОтражатьВБухгалтерскомУчете убыв, ОтражатьВНалоговомУчете убыв");
	
	МассивДокументы = ПолучитьВыбранныеДокументы(Истина);
	
	Для каждого Строка из МассивДокументы Цикл
		ДокументОбъект = Строка.Документ.ПолучитьОбъект();
		Если ДокументОбъект <> Неопределено И НЕ ДокументОбъект.ПометкаУдаления Тогда
			
			Попытка
				ДокументОбъект.Заблокировать();
			Исключение
				ВызватьИсключение "Не удалось заблокировать документ " + Строка.Документ + ", " + ОписаниеОшибки();
			КонецПопытки;
	
			Попытка
				
				Если ТипЗнч(Строка.Документ) = Тип("ДокументСсылка.РасчетСебестоимостиВыпуска") Тогда	
					
					Если ДокументОбъект.Проведен Тогда
						ДокументОбъект.Проведен = Ложь;
						ДокументОбъект.Записать(РежимЗаписиДокумента.Запись);
					КонецЕсли;
				
					// Также у непроведенного документа могут остаться движения, если предыдущее проведение вне транзакции завершилось внештатно (аварийно). Такие движения необходимо очистить.
					ЛУ_КлиентСервер.ОчиститьДвиженияДокумента(ДокументОбъект.Движения, ДокументОбъект.Ссылка);

					ДокументОбъект.мУдалятьДвижения = Ложь;
					Отказ = Ложь;
					
					глЗначениеПеременнойУстановить("НаборыДвиженийЗаписыватьАсинхронно", Истина, Истина); // Взводим флаг асинхронной записи движений

					ДокументОбъект.ОбработкаПроведения(Отказ, РежимПроведенияДокумента.Неоперативный);
					
					глЗначениеПеременнойУстановить("НаборыДвиженийЗаписыватьАсинхронно", Ложь, Истина); // Отключаем асинхронную запись
					
					Если Не Отказ Тогда
						
						Для Каждого ТекущееДвижение Из ДокументОбъект.Движения Цикл
							Если ТекущееДвижение.Модифицированность() Тогда
								ТекущееДвижение.Записать();
							КонецЕсли;
						КонецЦикла;	
						
						ДокументОбъект.Проведен = Истина;
						ДокументОбъект.Записать(РежимЗаписиДокумента.Запись);
						
					КонецЕсли;
					
				Иначе // Типовой функционал проведения
					
					ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
					
				КонецЕсли;
				
			Исключение
				глЗначениеПеременнойУстановить("НаборыДвиженийЗаписыватьАсинхронно", Ложь, Истина);
				ВызватьИсключение "Не удалось провести документ " + Строка.Документ + ", " + ОписаниеОшибки();
			КонецПопытки;
			
			ДокументОбъект.Разблокировать();
			
			Строка.ДокументПроведен = ДокументОбъект.Проведен;
		
		КонецЕсли;
		
	КонецЦикла;
	
КонецПроцедуры

В местах записи очередной порции движений добавляем вызов функции: НаборЗаписейЗаписатьАсинхронно(НаборЗаписей)

Если НаборЗаписейЗатратыНаВыпуск.Количество() = 1000 Тогда
	Если НЕ ЛУ_КлиентСервер.НаборЗаписейЗаписатьАсинхронно(НаборЗаписейЗатратыНаВыпуск) Тогда //+
		НаборЗаписейЗатратыНаВыпуск.Записать(Ложь);
	КонецЕсли; //+
КонецЕсли;

НаборЗаписейЗаписатьАсинхронно - функция общего модуля ЛУ_КлиентСервер (Сервер, Клиент обычное приложение) со следующим листингом:

Функция НаборЗаписейЗаписатьАсинхронно(НаборЗаписей) Экспорт
	
	// Проверка активности режима асинхронной записи
	НаборыДвиженийЗаписыватьАсинхронно = Неопределено;
	Если НЕ РаботаСОбщимиПеременными.ПолучитьИзКэшаКонфигурации("НаборыДвиженийЗаписыватьАсинхронно", НаборыДвиженийЗаписыватьАсинхронно, Неопределено, Ложь) 
		или НЕ НаборыДвиженийЗаписыватьАсинхронно Тогда
		Возврат Ложь;
	КонецЕсли;
	
	// Передаем набор записей для записи в фоновое задание,
	// которое выстраивается в очередь в виде стека с другими фоновыми заданиями по данному регистру,
	// очищаем набор записей в текущем контексте и передаем управление назад в процедуру проведения документа,
	// тоесть исключаем ожидание записи регистра для основного потока
	
	ТипРегистра = Неопределено;
	НаборЗаписейМетаданные = НаборЗаписей.Метаданные();
	Если Метаданные.РегистрыНакопления.Содержит(НаборЗаписейМетаданные) Тогда
		ТипРегистра = "РегистрНакопления";
	ИначеЕсли Метаданные.РегистрыБухгалтерии.Содержит(НаборЗаписейМетаданные) Тогда
		ТипРегистра = "РегистрБухгалтерии";
	ИначеЕсли Метаданные.РегистрыСведений.Содержит(НаборЗаписейМетаданные) Тогда
		ТипРегистра = "РегистрСведений";
	ИначеЕсли Метаданные.РегистрыРасчета.Содержит(НаборЗаписейМетаданные) Тогда
		ТипРегистра = "РегистрРасчета";
	Иначе
		Возврат Ложь;
	КонецЕсли;
	
	ИмяРегистра = НаборЗаписей.Метаданные().Имя;
	Замещение = Ложь;
	
	АктивныеФоновыеЗадания = ФоновыеЗадания.ПолучитьФоновыеЗадания(Новый Структура("Состояние, Наименование", СостояниеФоновогоЗадания.Активно, ИмяРегистра));
	Для Сч = 0 По АктивныеФоновыеЗадания.ВГраница() Цикл
		АктивныеФоновыеЗадания[Сч] = АктивныеФоновыеЗадания[Сч].УникальныйИдентификатор;
	КонецЦикла;
	
	ПараметрыЗадания = Новый Массив;
	ПараметрыЗадания.Добавить(ТипРегистра);
	ПараметрыЗадания.Добавить(ИмяРегистра);
	ПараметрыЗадания.Добавить(НаборЗаписей.Отбор.Регистратор.Значение);
	ПараметрыЗадания.Добавить(НаборЗаписей.Выгрузить());
	ПараметрыЗадания.Добавить(АктивныеФоновыеЗадания);
	ПараметрыЗадания.Добавить(Замещение);
	ФоновыеЗадания.Выполнить("ЛУ_ВызовСервера.ФоновоеЗаданиеЗаписатьНаборЗаписей", ПараметрыЗадания,, ИмяРегистра);
	
	НаборЗаписей.Очистить();
	
	Возврат Истина;
	
КонецФункции

ФоновоеЗаданиеЗаписатьНаборЗаписей - функция общего модуля ЛУ_ВызовСервера (Сервер, Вызов сервера, Привилегированный) со следующим листингом:

Процедура ФоновоеЗаданиеЗаписатьНаборЗаписей(ТипРегистра, ИмяРегистра, Регистратор, ТаблицаДвижений, АктивныеФоновыеЗадания, Замещение) Экспорт
	
	Если ТипРегистра = "РегистрНакопления" Тогда
		НаборЗаписейЗадания = РегистрыНакопления[ИмяРегистра].СоздатьНаборЗаписей();
		
		Если ТаблицаДвижений <> Неопределено Тогда
			НаборЗаписейЗадания.мТаблицаДвижений = ТаблицаДвижений;
			ОбщегоНазначения.ВыполнитьДвижениеПоРегистру(НаборЗаписейЗадания);		
		КонецЕсли;
		
	Иначе
		Если ТипРегистра = "РегистрБухгалтерии" Тогда
			НаборЗаписейЗадания = РегистрыБухгалтерии[ИмяРегистра].СоздатьНаборЗаписей();
		ИначеЕсли ТипРегистра = "РегистрСведений" Тогда
			НаборЗаписейЗадания = РегистрыСведений[ИмяРегистра].СоздатьНаборЗаписей();
		ИначеЕсли ТипРегистра = "РегистрРасчета" Тогда
			НаборЗаписейЗадания = РегистрыРасчета[ИмяРегистра].СоздатьНаборЗаписей();
		КонецЕсли; 
		
		Если ТаблицаДвижений <> Неопределено Тогда
			НаборЗаписейЗадания.Загрузить(ТаблицаДвижений);
		КонецЕсли;
	КонецЕсли; 
	
	НаборЗаписейЗадания.Отбор.Регистратор.Установить(Регистратор);
	
	// Проверим завершение предыдущих заданий по данному регистру
	Для каждого ФоновоеЗаданиеУИД Из АктивныеФоновыеЗадания Цикл
		ФоновоеЗадание = ФоновыеЗадания.НайтиПоУникальномуИдентификатору(ФоновоеЗаданиеУИД);
		Если НЕ ФоновоеЗадание = Неопределено
			и ФоновоеЗадание.Состояние = СостояниеФоновогоЗадания.Активно 
			Тогда
			ФоновоеЗадание.ОжидатьЗавершения();
		КонецЕсли;
	КонецЦикла;
	
	НаборЗаписейЗадания.Записать(Замещение);
	
КонецПроцедуры

В местах записи последней порции набора записей выполняем ожидание завершения фоновых заданий, запущенных в контексте по данному регистру.

ЛУ_КлиентСервер.ОжиданиеЗавершенияФоновыхЗаданий(СтруктураДвижений.ДвиженияЗатратыНаВыпуск); //+
	
Если СтруктураДвижений.ДвиженияЗатратыНаВыпуск.Модифицированность() Тогда
	СтруктураДвижений.ДвиженияЗатратыНаВыпуск.Записать(Ложь);
КонецЕсли;

ОжиданиеЗавершенияФоновыхЗаданий - функция общего модуля ЛУ_КлиентСервер (Сервер, Клиент обычное приложение) со следующим листингом:

Процедура ОжиданиеЗавершенияФоновыхЗаданий(НаборЗаписей) Экспорт
	
	// Проверка активности режима асинхронной записи
	НаборыДвиженийЗаписыватьАсинхронно = Неопределено;
	Если НЕ РаботаСОбщимиПеременными.ПолучитьИзКэшаКонфигурации("НаборыДвиженийЗаписыватьАсинхронно", НаборыДвиженийЗаписыватьАсинхронно, Неопределено, Ложь) 
		или НЕ НаборыДвиженийЗаписыватьАсинхронно Тогда
		Возврат;
	КонецЕсли;
	
	ИмяРегистра = НаборЗаписей.Метаданные().Имя;
	
	АктивныеФоновыеЗадания = ФоновыеЗадания.ПолучитьФоновыеЗадания(Новый Структура("Состояние, Наименование", СостояниеФоновогоЗадания.Активно, ИмяРегистра));
	Если АктивныеФоновыеЗадания.Количество() > 0 Тогда
		ФоновыеЗадания.ОжидатьЗавершения(АктивныеФоновыеЗадания);
	КонецЕсли;
	
КонецПроцедуры

Более подробные листинги смотри во вложении

Вложенный файл ПроцедурыРасчетаСебестоимостиВыпуска.zip содержит модули подвергшиеся оптимизации согласно описанным в статье методам и предназначены для конфигурации Управление производсвенным предприятем 1.3 (1.3.79.2). Возможна установка на более ранние или старшие релизы. Клиент - Серверный вариант. Обычные формы.

Модули предназначены для самостоятельного внедрения. Код всех модулей полностью открыт. Возможна доработка собственными силами. Дополнительное лицензирование не требуется.

Обновление модулей, связанные с изменением типовой конфигурации, не планируется. Так как данный функционал в типовой конфигурации более не развивается.

Состав файла:

1. ПроцедурыРасчетаСебестоимостиВыпуска.cf
Помечаем для объединения только следующие общие модули:
- ПроцедурыРасчетаБазыРаспределенияЗатрат
- ПроцедурыРасчетаСебестоимостиВыпуска
- РасчетСебестоимостиВыпускаРаспределениеПоПеределам
- ЛУ_КлиентСервер
- ЛУ_ВызовСервера

2. Задачи.РегламентныеОперацииЗакрытияМесяца.ФормаЗадачи.txt
- На форме задачи в командной панели необходимо добавить кнопку "Провести вне транзакции" и назначить обработчик из этого файла

Скачать файлы

Наименование Файл Версия Размер
ПроцедурыРасчетаСебестоимостиВыпуска.zip 1
.zip 175,16Kb
27.10.16
1
.zip 3 175,16Kb Скачать

См. также

Подписаться Добавить вознаграждение

Комментарии

1. Юрий Михеев (DatiniFM) 23.08.16 17:13
Вопрос только один - А что программеры 1с в УПП тогда делают -усиленно штампуют неоптимальные алгоритмы?
2. Сергей Линков (linkov) 23.08.16 17:43
(1) DatiniFM,

Да нет, не штампуют, все в пределах методологии.
Только вот на наших объемах и программно - аппаратной реализации более оптимальными оказались представленные алгоритмы.
Хотя, по п.3 типовой алгоритм по любому косяковый.
3. Михаил Ражиков (tango) 23.08.16 22:41
4. Константин (Mkonst) 24.08.16 12:29
некоторые вещи из публикации надо будет попробовать на своей упп...
5. Владимир Гусев (adhocprog) 24.08.16 16:06
6. Эльдар Гамзаев (lunjio) 25.08.16 10:10
Хорошая статья, спасибо, единственное, что при создании фоновых задач можно управлять стеком указывая ключ задания, который в данном случае будет являться именем регистра. 1С при запуске смотрит, нет ли задач с таким же ключем и только тогда запускает, а так же что это можно было оформить в конфигурацию в качестве модулей с такими же наименованиями, а не текстовыми файликами, а в целом мне очень актуально, больше всего занимает процесс записи у нас при расчете. Спасибо за то, что поделились.
7. Сергей Линков (linkov) 25.08.16 10:54
(6) lunjio,

единственное, что при создании фоновых задач можно управлять стеком указывая ключ задания, который в данном случае будет являться именем регистра. 1С при запуске смотрит, нет ли задач с таким же ключем и только тогда запускает

В этом случае сразу вывалится исключение что задание с таким ключём уже существует.
Если же предварительно запускать ожидание завершения фонового задания с этим ключем, тогда будет тормозиться основной поток - теряется смысл асинхронной записи.
Более того, даже после завершения, фоновое задание продолжает некоторое время висеть в памяти и соответственно не позволит запустить другое с таким же ключем.

Если вы внимательно просмотрите код, то увидите, что у меня все задания по одному регистру запускаются с именем этого регистра в наименовании, и уже внутри фонового задания происходит ожидание завершения предыдущих с таким же наименованием.
(Ключ задания - должен быть уникальным, Наименование задания - может быть любым)

а так же что это можно было оформить в конфигурацию в качестве модулей с такими же наименованиями, а не текстовыми файликами,

Ок, во вложенный архив добавлен cf
8. Александр Ваньков (anama_agro) 26.08.16 16:30
Большое спасибо Вам за идею! Хотелось бы задать пару вопросов: не совсем понимаю зачем мы предварительно выгружаем набор записей в таблицу значений ПараметрыЗадания.Добавить(НаборЗаписей.Выгрузить()) (на это же тратится дополнительное время)? Почему нельзя просто передать НаборЗаписей в фоновое задание и там выполнить метод НаборЗаписейЗадания.Записать(Замещение)? Так же не совсем понимаю почему для регистра накопления используется вызов промежуточного метода: ОбщегоНазначения.ВыполнитьДвижениеПоРегистру(НаборЗаписейЗадания) ? Разве строки НаборЗаписейЗадания.Загрузить(ТаблицаДвижений); будет недостаточно? Заранее благодарю за ответ!
9. Сергей Линков (linkov) 26.08.16 17:24
(8) anama_agro,

не совсем понимаю зачем мы предварительно выгружаем набор записей в таблицу значений ПараметрыЗадания.Добавить(НаборЗаписей.Выгрузить()) (на это же тратится дополнительное время)? Почему нельзя просто передать НаборЗаписей в фоновое задание и там выполнить метод НаборЗаписейЗадания.Записать(Замещение)?

Пробовал, надеялся, но ругается на мутабельность значения.

Так же не совсем понимаю почему для регистра накопления используется вызов промежуточного метода: ОбщегоНазначения.ВыполнитьДвижениеПоРегистру(НаборЗаписейЗадания) ? Разве строки НаборЗаписейЗадания.Загрузить(ТаблицаДвижений); будет недостаточно?

Данный участок кода позаимствован из типового. В этом методе при копировании ТаблицыЗначений в НаборЗаписей отсекаются пустые ссылки и пустые значения в случае, если реквизит составного типа. Что благотворно сказывается на самочувствии регистра накопления.
10. Евгений Багаев (sarun) 31.08.16 13:01
{ОбщийМодуль.ПроцедурыРасчетаБазыРаспределенияЗатрат.Модуль(2200)}: Ошибка при вызове метода контекста (Выполнить)
РезультатЗапроса = Запрос.Выполнить();
по причине:
{(103, 1)}: Синтаксическая ошибка
11. Евгений Багаев (sarun) 31.08.16 13:02
Добавил все процедуры. Закоментировал участки где идет асинхронная запись в регистры.
12. Сергей Линков (linkov) 31.08.16 13:38
(10) sarun,

{ОбщийМодуль.ПроцедурыРасчетаБазыРаспределенияЗатрат.Модуль(2200)}: Ошибка при вызове метода контекста (Выполнить)
РезультатЗапроса = Запрос.Выполнить();
по причине:
{(103, 1)}: Синтаксическая ошибка

Действительно, есть ошибочка, выложил исправленный cf
(у нас не задействован управленческий учет, поэтому ошибка не проявлялась)
13. Эльдар Гамзаев (lunjio) 31.08.16 19:37
(7) linkov,
Вынужден с вами не согласиться, лично для интереса проверил следующим образом - создал регламентное задание которое в качестве параметра принимает ссылку на некий справочник, далее в цикле с 10 тыс. итерациями в специально созданный регистр сведений записываю данную ссылку с измерением равным счетчику цикла, для тестов достаточно. Создаю подряд два регламентных задания, с одинаковым ключом, в оба передаю разные значения ссылок, записываю их подряд, выполнится должны почти сразу после записи, ставлю точку останова в процедуре выполнения фонового задания, в другом сеансе вижу, что одно задание выполняется, второе ещё не выполнялось и не запускалось, как только отпускаю точку останова, попадаю туда сразу второй раз, в консоли регламентных заданий вижу что теперь второе задание исполняется, первое выполнено.
А чтобы регламентные задания не висели впустую, в фоновом задании их можно просто удалять.
У себя ещё не внедрил, но деньги за это думаю просите обосновано, грамотно проделанная оптимизация, ещё раз спасибо за публикацию.
14. Евгений Багаев (sarun) 01.09.16 06:12
при обычной (не асинхронной записи) в регистры выигрыша в скорости не заметил . УПП 1.3.81.2, по переделам, партионный учет, база 50 Гб.
15. Евгений Багаев (sarun) 01.09.16 06:15
16. Сергей Линков (linkov) 01.09.16 09:45
(13) lunjio,

Вынужден с вами не согласиться, лично для интереса проверил следующим образом - создал регламентное задание которое в качестве параметра принимает ссылку на некий справочник, далее в цикле с 10 тыс. итерациями в специально созданный регистр сведений записываю данную ссылку с измерением равным счетчику цикла, для тестов достаточно. Создаю подряд два регламентных задания, с одинаковым ключом, в оба передаю разные значения ссылок, записываю их подряд, выполнится должны почти сразу после записи, ставлю точку останова в процедуре выполнения фонового задания, в другом сеансе вижу, что одно задание выполняется, второе ещё не выполнялось и не запускалось, как только отпускаю точку останова, попадаю туда сразу второй раз, в консоли регламентных заданий вижу что теперь второе задание исполняется, первое выполнено.
А чтобы регламентные задания не висели впустую, в фоновом задании их можно просто удалять.


Верно. Для Регламентного задания уникальность ключа не требуется. По Вашему предложению тоже можно организовать выполнение Фоновых заданий в стеке. Но все же получается сложновато - в этом случае для запуска Фоновых заданий дополнительно придется создавать, записывать в базу и удалять объекты Регламентных заданий. К тому же основное назначение Регламентных заданий - выполнение Фоновых задач по расписанию. Спасибо за подсказку, возможно, где нибудь пригодится.
17. Сергей Линков (linkov) 01.09.16 10:54
(14) sarun,

при обычной (не асинхронной записи) в регистры выигрыша в скорости не заметил. УПП 1.3.81.2, по переделам, партионный учет, база 50 Гб.

Эффект ускорения проведения возникает не при большом размере базы, а при большом количестве выпускаемой номенклатуры в месяц и большом количестве затрат.
У нас недавно был апгрейд железа. После чего время проведения стало следующим: без доработок - 8 часов, с доработками не асинхронно - 5 часов, асинхронно - 3,5 часа.
Ну и без доработок размер tempdb по прежнему зашкаливает.

Количество записей в регистрах нашего документа (один месяц):
РегистрНакопления.ВыпускПродукцииБухгалтерскийУчет: 46 136
РегистрНакопления.ВыпускПродукцииНалоговыйУчет: 46 136
РегистрНакопления.НезавершенноеПроизводствоБухгалтерскийУчет: 324 521
РегистрНакопления.НезавершенноеПроизводствоНалоговыйУчет: 324 445
РегистрНакопления.ЗатратыНаВыпускПродукцииБухгалтерскийУчет: 2 646 298
РегистрНакопления.ЗатратыНаВыпускПродукцииНалоговыйУчет: 2 646 297
18. Эльдар Гамзаев (lunjio) 02.09.16 11:58
(16) linkov,
Да согласен, в вашем варианте более оптимально, как-то сразу не подумал, притом что если организовывать через регламентные, встает проблема с ожиданием завершения всех текущих заданий, в голове рисуется что ожидание будет не очень красивым в плане кода и логики ,в важем случае все четко, НО внесу свои две копеек всё-таки в оптимизацию кода
Текст где в процедуре фонового задания мы ожидаем завершения предыдущих в стеке, нужно перенести в начало процедуры, не совсем оптимально по отношению к памяти, создаем набор записи, переносим в него таблицу, а потом ждём.
19. Сергей Линков (linkov) 03.09.16 10:19
(18) lunjio,

НО внесу свои две копеек всё-таки в оптимизацию кода
Текст где в процедуре фонового задания мы ожидаем завершения предыдущих в стеке, нужно перенести в начало процедуры, не совсем оптимально по отношению к памяти, создаем набор записи, переносим в него таблицу, а потом ждём.

Зато оптимально в плане скорости, после окончания ожидания уже всё готово, остается только записать набор записей. Спорный момент. Зависит от приоритетов в конкретной реализации.
20. Александр Крынецкий (echo77) 26.11.16 07:59
(17) Сколько записей в журнале проводок(РегистрБухгалтерии.Хозрасчетный)?
(0) Кстати, процесс можно еще немного ускорить, если добавить правильный индекс в таблицу, в которой выполняется поиск
Для написания сообщения необходимо авторизоваться
Прикрепить файл
Дополнительные параметры ответа