Нумерация строк таблицы (счетчик строк)
Подсчет нарастающего итога по нужным группировкам
Использование временных таблиц для получения нужных индексов в соединениях
Обновление таблицы по собственным данным
Использование хранимых процедур и функций
Нумерация строк таблицы (счетчик строк)
Допустим, есть потребность пронумеровать таблицу, в которой есть документ, номер строки, сотрудник, день и какое-то количество (1 и 2)
Создаем временную таблицу и помещаем результат выборки в нее для дальнейшей обработки.
RecordSet.ВыполнитьИнструкцию("If object_id('tempdb..#tempt') is not null drop table #tempt"); RecordSet.ВыполнитьИнструкцию("Create table #tempt (id int, iddoc char(9), lineno int, emp char(9), day char(11) , num1 numeric(10,2), num2 numeric(10,2)"); ТекстЗапроса = " |Insert into #tempt |Select 0 Ид |Запрос.Документ ... |From ( ... |) as Запрос |"; RecordSet.ВыполнитьИнструкцию(ТекстЗапроса);
Пронумеровываем строки в служебной колонке id.
ТекстЗапроса = " |Declare @i int |Set @i = 0 |Update #tempt Set id = @i, @i = @i + 1 |"; RecordSet.ВыполнитьИнструкцию(ТекстЗапроса);
Подсчет нарастающего итога по нужным группировкам <в начало>
Теперь нам необходимо подсчитать нарастающий итог по количеству 2 в разрезе сотрудника и дня. Результат помещаем во временную таблицу #temptsum.
ТекстЗапроса = " |Select a.id |, a.iddoc |, a.emp |, a.day |, a.num1 |, Sum(isnull(b.num2, 0)) + max(a.num2) sumnum2 into #temptsum |From #tempt a (nolock) |Left join #tempt b (nolock) on b.id < a.id and b.emp = a.emp and b.day = a.day |Group by a.id, a.iddoc, a.emp, a.day, a.day, a.num1 |"; RecordSet.ВыполнитьИнструкцию(ТекстЗапроса);
Использование курсоров <в начало>
Использование курсоров не приветствуется, но иногда без них обойтись трудно. Например, в случаях списания остатков партий со сложными условиями и расчетами, сложных распределений. При использовании курсоров отмечается падение производительности и повышенная нагрузка на сервер. Но если предлагается выбрать между перетягиванием недообработанных данных на клиента с последующей возней в таблице значений и обработкой данных на сервере с помощью курсора - я выберу последний вариант. Хорошо, что есть возможность использования полноценного T-SQL в 1С 7.7, один из примеров которого вы сможете увидеть ниже.
Теоретические основы можно посмотреть по следующим ссылкам (ссылки даны для SQL 2005, можно переключить):
Курсоры (ядро СУБД)
Курсоры (Transact-SQL)
Мне понравилась статья, где достаточно просто и с примерами рассказывается о курсорах в SQL Server.
Приведу практический пример, пусть с изъянами и не вылизанный, который писался несколько лет назад.
Краткие пояснения: физически существует одна заявка, которую необходимо собирать на складе. Нам необходимо организовать деление ее на зоны сборки адресного склада и части для конвейерной сборки. К тому же, при наличии более 10 позиций в одной части такую часть необходимо поделить еще на 2 части (группы стеллажей). Приведенный код выполняется в модуле проведения документа "Заявка".
#tempra - это таблица движений регистра, которая содержит помимо товара, заявки, адреса и других измерений еще зону сборки и часть заявки (реквизиты регистра). После обработки таблицы (а именно заполнения реквизита "Часть заявки", поле в запросе именуется PZ) останется сделать апдейт таблицы движений и документ будет готов для обработки складом.
TransporterRack - служебная таблица, в которой указано какие стеллажи (Rack) и ячейки (Cell) к какой зоне относятся (ZoneId) и к какой части (первой или второй) заявки будут принадлежать (Part). Гравитация (место сборки больших количеств товара) имеет номер стеллажа '00' и обрабатывается отдельно, делится по ячейкам, а не по стеллажам.
ТекстЗапроса = " |DECLARE @MaxPart TINYINT |DECLARE @CountLine INT |DECLARE @Zone char(9) | |if object_id('tempdb..#tempzone') is not null drop table #tempzone | |SELECT Zone, NumZone, Count(*) CountLine INTO #tempzone |FROM #tempra |GROUP BY Zone, NumZone |ORDER BY NumZone | |DECLARE Cur Cursor FOR SELECT Zone, CountLine FROM #tempzone |OPEN Cur |FETCH NEXT FROM Cur INTO @Zone, @CountLine |WHILE (@@fetch_status <> -1) |BEGIN | |IF @CountLine > 10 |BEGIN | |SET @MaxPart = (SELECT MAX(Pz) FROM #tempra (NOLOCK)) | |UPDATE #tempra |SET PZ = @MaxPart + 1 |FROM #tempra T |LEFT JOIN dbo.TransporterRack TR (NOLOCK) ON T.Zone = TR.ZoneId AND T.Rack = TR.Rack |WHERE T.Zone = @Zone AND TR.Rack <> '00' AND TR.Part = 1 | |UPDATE #tempra |SET PZ = @MaxPart + 1 |FROM #tempra T |LEFT JOIN dbo.TransporterRack TR (NOLOCK) ON T.Zone = TR.ZoneId AND T.Rack = TR.Rack AND T.Cell = TR.Cell |WHERE T.Zone = @Zone AND TR.Rack = '00' AND TR.Part = 1 | |SET @MaxPart = (SELECT MAX(Pz) FROM #tempra (NOLOCK)) | |UPDATE #tempra |SET PZ = @MaxPart + 1 |FROM #tempra T |LEFT JOIN dbo.TransporterRack TR (NOLOCK) ON T.Zone = TR.ZoneId AND T.Rack = TR.Rack |WHERE T.Zone = @Zone AND TR.Rack <> '00' AND TR.Part = 2 | |UPDATE #tempra |SET PZ = @MaxPart + 1 |FROM #tempra T |LEFT JOIN dbo.TransporterRack TR (NOLOCK) ON T.Zone = TR.ZoneId AND T.Rack = TR.Rack AND T.Cell = TR.Cell |WHERE T.Zone = @Zone AND TR.Rack = '00' AND TR.Part = 2 | |END |ELSE |BEGIN | |SET @MaxPart = (SELECT MAX(Pz) FROM #tempra (NOLOCK)) | |UPDATE #tempra |SET PZ = @MaxPart + 1 |WHERE Zone = @Zone AND Rack IS NOT NULL | |END | |SET @MaxPart = (SELECT MAX(Pz) FROM #tempra (NOLOCK)) | |UPDATE #tempra |SET PZ = @MaxPart + 1 |WHERE PZ = 0 AND Zone = @Zone | |FETCH NEXT FROM Cur INTO @Zone, @CountLine |END |CLOSE Cur |DEALLOCATE cur |"; RecordSet.ВыполнитьИнструкцию(ТекстЗапроса);
Использование временных таблиц для получения нужных индексов в соединениях <в начало>
Если необходимо соединяться с большой таблицей, не имеющей нужных индексов, то быстрее будет поместить нужный блок данных в свою временную таблицу для дальнейшей обработки. Ниже приведен практический пример апдейта "матрицы" по данным регистра продаж.
//Сбор сведений о продажах, создаем временную таблицу и помещаем в нее данные о продажах за период ТекстЗапроса = " |if exists (select * from tempdb..sysobjects where id = object_id('tempdb..#TempSale')) |drop table #TempSale |create table #TempSale (tov char(9), apt char(9), kol numeric(14,3) primary key clustered (tov, apt)) |"; RecordSet.ВыполнитьИнструкцию(ТекстЗапроса); ТекстЗапроса = " |INSERT INTO #TempSale |SELECT РозницаУпрОбороты.Товар |, РозницаУпрОбороты.Подразделение |, РозницаУпрОбороты.КоличествоРасход |FROM $РегистрОбороты.РозницаУпр(:ДатаН, |:ДатаК~,,, |(Движение IN ($Перечисление.ДвиженияВРознице.Продажа, $Перечисление.ДвиженияВРознице.ПродажаБН)), |(Товар, Подразделение), |Количество) AS РозницаУпрОбороты |"; ДатаК = ТекущаяДата() - 1; ДатаН = ДобавитьМесяц(ДатаК, -3); RecordSet.УстановитьТекстовыйПараметр("ДатаН", ДатаН); RecordSet.УстановитьТекстовыйПараметр("ДатаК", ДатаК); RecordSet.ВыполнитьИнструкцию(ТекстЗапроса); //Обновление данных таблицы ("матрицы") ТекстЗапроса = " |UPDATE MatrixCosm |SET Sale = CASE WHEN TS.Kol > 0 THEN 1 ELSE 0 END |FROM MatrixCosm MC |LEFT JOIN #TempSale AS TS With (NOLOCK) ON TS.Tov = MC.Tov AND TS.Apt = MC.Apt |"; RecordSet.ВыполнитьИнструкцию(ТекстЗапроса);
Обновление таблицы по собственным данным <в начало>
У Вас не получится обновить таблицу соединив ее с самой собой. На помощь снова придет временная таблица.
Ниже приведен пример апдейта таблицы справочника (в частности, запись сведений о родителе) с использованием данных самого справочника и таблицы, сформированной из внешнего источника данных (Category). Эта таблица повторяет иерархию справочника и содержит доп. данные для обновления.
ТекстЗапроса = " |if object_id('tempdb..#tempcat') is not null drop table #tempcat |"; RecordSet.ВыполнитьИнструкцию(ТекстЗапроса); ТекстЗапроса = " |SELECT SC.Id SCId |, ISNULL(SCP.Id, ' 0 ') ParentId |, Cat.IsFolder IsFolder |, Cat.Descr Descr |, Cat.IdMenu IdMenu into #tempcat |FROM $Справочник.КатегорииНоменклатуры SC (NOLOCK) |LEFT JOIN Category Cat (NOLOCK) ON $SC.ИД = Cat.Id |LEFT JOIN Category CatP (NOLOCK) ON Cat.Parent = CatP.Id |LEFT JOIN $Справочник.КатегорииНоменклатуры SCP (NOLOCK) ON CatP.Id = $SCP.ИД |"; RecordSet.ВыполнитьИнструкцию(ТекстЗапроса); ТекстЗапроса = " |UPDATE $Справочник.КатегорииНоменклатуры |SET ParentId = t.ParentId |, IsFolder = t.IsFolder |, Descr = t.Descr |, $Справочник.КатегорииНоменклатуры.ИДМеню = t.IdMenu |FROM $Справочник.КатегорииНоменклатуры SC |LEFT JOIN #tempcat t ON SC.Id = t.SCId |"; RecordSet.ВыполнитьИнструкцию(ТекстЗапроса); ТекстЗапроса = " |if object_id('tempdb..#tempcat') is not null drop table #tempcat |"; RecordSet.ВыполнитьИнструкцию(ТекстЗапроса);
Использование хранимых процедур и функций <в начало>
Иногда возникает необходимость в запросах использовать функции, а для обработки, вывода данных - процедуры. Теоретические основы можно почитать, например, по ссылке.
Рассмотрим классический пример - перевод из 36-ной системы счисления в 10-ную. Допустим, нам нужно записывать ID новых элементов, хранимых в базе 1С. Для записи нового элемента его ID нужно увеличить на 1.
Тексты запросов проверки существования и создания функции:
Запрос.ВыполнитьИнструкцию("IF EXISTS(SELECT name FROM sysobjects WHERE name = 'Convert36To10') DROP FUNCTION Convert36To10"); ТекстЗапроса = " |CREATE FUNCTION Convert36To10(@Res36 CHAR(9)) RETURNS int |AS |begin |DECLARE @j INT |DECLARE @Deci INT |DECLARE @Arr36 CHAR(36) |SELECT @Arr36 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' |SELECT @Deci = 0 |SELECT @j = 1 |while @j <= LEN(LTRIM(RTRIM(@Res36))) |begin |if @j <> 1 |SELECT @Deci = @Deci*36 |SELECT @Deci = @Deci + CHARINDEX(SUBSTRING(LTRIM(RTRIM(@Res36)), @j,1),@Arr36) -1 |SELECT @j = @j+1 |end |RETURN(@Deci); |end |"; Запрос.ВыполнитьИнструкцию(ТекстЗапроса);
Пример использования созданной функции:
ТекстЗапроса = " |declare @rank int, |@id char(9) |declare rank_cursor cursor |for select id |from " + БазаИсточник + ".dbo." + ТаблицаИсточник + " as t |where t.id not in ( |select tid |from " + ВремТаблица + ") |order by id |open rank_cursor |fetch next from rank_cursor into @id |set @rank = (select maxid from " + БазаПриемник + ".dbo._1suidctl where TYPEID = :ИД) |if @rank is null select @rank = 0 |set @rank = dbo.Convert36To10(@rank) |while (@@fetch_status <> -1) |begin |select @rank, @id |set @rank = @rank + 1 |insert into " + ВремТаблица + " values (dbo.sp_tohex(@rank, 6), @id, 1) |fetch next from rank_cursor into @id |end |close rank_cursor |deallocate rank_cursor |"; Запрос.УстановитьТекстовыйПараметр("ИД", СокрЛП(СпрО.Приемник.ИД)); Запрос.ВыполнитьИнструкцию(ТекстЗапроса);
Использование триггеров <в начало>
Можно ли в своей базе 1С "поселить" триггеры? Да, конечно можно. Главное - помнить о том, что они существуют.
Теоретические основы с примерами по триггерам можно почитать, например, тут и тут.
Рассмотрим пример изменения данных по остатку в таблице исходя из данных по плановым и фактическим данным (находятся в той же таблице).
ТекстЗапроса = " |IF EXISTS (SELECT * FROM sys.triggers WHERE object_id = OBJECT_ID(N'[dbo].[Plan_goodsUpdate]')) |SELECT 1 |ELSE |SELECT 0 |"; ТЗ = RecordSet.ВыполнитьИнструкцию(ТекстЗапроса); Существует = ТЗ.ПолучитьЗначение(1, 1); Если Существует = 0 Тогда ТекстЗапроса = " |CREATE TRIGGER [Plan_goodsUpdate] ON [dbo].[Plan_goods] |AFTER UPDATE |AS |BEGIN |UPDATE Plan_Goods with (UPDLOCK, HOLDLOCK) |SET Rest_kol = CASE WHEN PlanG.Plan_kol - Ins.Fact_kol > 0 THEN PlanG.Plan_kol - Ins.Fact_kol ELSE 0 END |FROM Plan_Goods AS PlanG |INNER JOIN Inserted AS Ins ON PlanG.Idd = Ins.Idd |END |"; RecordSet.ВыполнитьИнструкцию(ТекстЗапроса); КонецЕсли;
Рассмотрим более сложный пример: изменение данных в таблице планов в зависимости от изменения данных по продажам товаров. Планы составляются раз в месяц и актуальны за этот период, поэтому в начале каждого месяца просто уничтожаем триггер и создаем заново, очищаем и подготавливаем таблицу по планам.
НачалоМесяца = Строка(ДатаГод(НачМесяца(ТекущаяДата()))) + "-" + Прав("0"+Строка(ДатаМесяц(НачМесяца(ТекущаяДата()))),2) + "-" + Прав("0"+Строка(ДатаЧисло(НачМесяца(ТекущаяДата()))),2); RecordSet.ВыполнитьИнструкцию("IF EXISTS (SELECT * FROM sys.triggers WHERE object_id = OBJECT_ID(N'[dbo].[SaleDelete]')) DROP TRIGGER [dbo].[SaleDelete]"); ТекстЗапроса = " |CREATE TRIGGER [SaleDelete] ON [dbo].[Sale] |AFTER DELETE |AS |BEGIN |UPDATE Plan_Goods with (UPDLOCK, HOLDLOCK) |SET Fact_kol = Fact_kol - Del.Kol |FROM Plan_Goods AS PlanG |INNER JOIN ( |SELECT SUM(Kol) Kol |, Buyer |, Tov |FROM Deleted |GROUP BY Buyer |, Tov |) AS Del ON PlanG.Customer_id = Del.Buyer AND PlanG.Goods_id = Del.Tov |WHERE PERIOD = '" + НачалоМесяца + "' |END |"; RecordSet.ВыполнитьИнструкцию(ТекстЗапроса);
Аналогичным образом создаем триггер "AFTER INSERT", а для "AFTER UPDATE" делаем UPDATE дважды в одном запросе: "FROM Deleted" и "FROM Inserted".
Продолжение следует...
Другие статьи по прямым запросам:
Ускоряем регистрацию объектов в МОД (для SQL)
Проверка дублей строк с помощью 1С++
Аналог ON DUPLICATE KEY UPDATE в MS SQL
Как использовать УРБД в отличающихся конфигурациях
Как написать прямой запрос в 1С (DBF, 1sqlite)
Запросы в 1С к двум базам одновременно (DBF, OLE DB)
Как написать прямой запрос в 1С (SQL) с помощью 1С++
Онлайн резервирование товаров на складе (online reservation)