Здравствуйте!
Есть хранимая процедура, предназначенная для загрузки товаров в базу интернет магазина. В нее может передаваться от десятков до сотен тысяч товаров. При небольшом количестве товаров работает быстро, однако база выросла до 400 000 товаров [Product], для каждого из которых несколько товарных предложений (поставщиков) [Offer] и для каждого предложения несколько типов цен (Закуп, опт, розница) [OfferPrice].
Загрузка может происходить одновременно в разных потоках (клиент ищет, делается запрос по API). Либо может загружаться одновременно несколько прайсов от поставщиков.
В результате имею две проблемы:
1. Блокировки. Возникают ошибки:
- Транзакция (идентификатор процесса 66) вызвала взаимоблокировку ресурсов блокировка с другим процессом и стала жертвой взаимоблокировки. Запустите транзакцию повторно.
- Счетчик транзакций после выполнения EXECUTE показывает несовпадение числа инструкций BEGIN и COMMIT. Предыдущее число = 0, текущее число = 1.
Нефиксируемая транзакция обнаружена в конце пакета. Был выполнен откат транзакции.
Пытался разобраться с помощью профайлера, но не получилось.
В коде делал блокировку, чтобы несколько потоков одновременно не выполняли эту хранимую процедуру. Не помогло:
2. Низкая производительность.
Иногда сотни товаров грузятся доли секунды. Но обычно 2-5 секунд. Периодически тупняки секунд по 10-30.
Если приходит прайс размером 200к позиций - вешается минут на 5
Таблиц очень много. Индексы вроде все расставил.
Код процедуры:
Есть хранимая процедура, предназначенная для загрузки товаров в базу интернет магазина. В нее может передаваться от десятков до сотен тысяч товаров. При небольшом количестве товаров работает быстро, однако база выросла до 400 000 товаров [Product], для каждого из которых несколько товарных предложений (поставщиков) [Offer] и для каждого предложения несколько типов цен (Закуп, опт, розница) [OfferPrice].
Загрузка может происходить одновременно в разных потоках (клиент ищет, делается запрос по API). Либо может загружаться одновременно несколько прайсов от поставщиков.
В результате имею две проблемы:
1. Блокировки. Возникают ошибки:
- Транзакция (идентификатор процесса 66) вызвала взаимоблокировку ресурсов блокировка с другим процессом и стала жертвой взаимоблокировки. Запустите транзакцию повторно.
- Счетчик транзакций после выполнения EXECUTE показывает несовпадение числа инструкций BEGIN и COMMIT. Предыдущее число = 0, текущее число = 1.
Нефиксируемая транзакция обнаружена в конце пакета. Был выполнен откат транзакции.
Пытался разобраться с помощью профайлера, но не получилось.
В коде делал блокировку, чтобы несколько потоков одновременно не выполняли эту хранимую процедуру. Не помогло:
+ |
lock (locker) { using (var db = new SQLDataAccess()) { db.cn.ConnectionString = Connection.GetConnectionString(); db.cnOpen(); db.cmd.CommandText = "[Catalog].[sp_ImportOutsideProductTVP]"; db.cmd.CommandType = CommandType.StoredProcedure; db.cmd.Parameters.Clear(); db.cmd.Parameters.Add(new SqlParameter("@VendorId", vendorId)); db.cmd.Parameters.Add(new SqlParameter("@mainCategoryId", categoryId)); db.cmd.Parameters.Add(new SqlParameter("@searchedArtNo", artNo ?? (object)DBNull.Value)); db.cmd.Parameters.Add(new SqlParameter("@dissableLastOffers", dissableLastOffers)); db.cmd.Parameters.Add(new SqlParameter("@addCrosses", addCrosses)); SqlParameter productTable = new SqlParameter(); productTable.ParameterName = "@ProductImportTable"; productTable.TypeName = "dbo.OutsideProductImportTable"; productTable.SqlDbType = SqlDbType.Structured; productTable.Value = new OutsideProductDataRecord(outsideProducts); db.cmd.Parameters.Add(productTable); SqlParameter priceTable = new SqlParameter(); priceTable.ParameterName = "@PriceImportTable"; priceTable.TypeName = "dbo.OutsideProductPricesImportTable"; priceTable.SqlDbType = SqlDbType.Structured; priceTable.Value = new OutsideProductPriceDataRecord(outsideProductPrices); db.cmd.Parameters.Add(priceTable); db.cmd.CommandTimeout = 60 * 10; db.cmd.ExecuteNonQuery(); db.cn.Close(); sw.Stop(); ApsCore.WriteLogTime("[sp_ImportOutsideProductTVP] " + sw.Elapsed); } } |
2. Низкая производительность.
Иногда сотни товаров грузятся доли секунды. Но обычно 2-5 секунд. Периодически тупняки секунд по 10-30.
Если приходит прайс размером 200к позиций - вешается минут на 5
Таблиц очень много. Индексы вроде все расставил.
Код процедуры:
+ |
/****** Object: StoredProcedure [Catalog].[sp_ImportOutsideProductTVP] Script Date: 19.10.2016 15:39:03 ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO ALTER PROCEDURE [Catalog].[sp_ImportOutsideProductTVP] @ProductImportTable dbo.OutsideProductImportTable READONLY, @PriceImportTable dbo.OutsideProductPricesImportTable READONLY, @VendorId INT, @mainCategoryId INT, @searchedArtNo NVARCHAR(100), @dissableLastOffers BIT, @addCrosses BIT AS BEGIN SET NOCOUNT ON; DECLARE @ProductId INT; DECLARE @errorText NVARCHAR(200); BEGIN TRY DECLARE @t INT; DECLARE @outsideProduct TABLE ( [ProductId] INT NOT NULL, [ArtNo] NVARCHAR (100) NOT NULL, [ArtNoClear] NVARCHAR (100) NOT NULL, [Article] NVARCHAR (100) NOT NULL, [BrandNameClear] NVARCHAR (100) NOT NULL, [BrandName] NVARCHAR (100) NOT NULL, [Name] NVARCHAR (255) NOT NULL, [Description] NVARCHAR (255) NULL, [Url] NVARCHAR (100) NOT NULL, [Amount] FLOAT (53) NOT NULL, [RowNumber] INT NOT NULL, [PeriodFrom] INT NOT NULL, [PeriodTo] INT NOT NULL, [Multiplicity] INT NOT NULL, [BrandId] INT NOT NULL, [Unit] NVARCHAR (50) NOT NULL, [Information] NVARCHAR (1000) NOT NULL, UNIQUE([ArtNo],[ProductId],[RowNumber]) ); DECLARE @outsideProductPrices TABLE ( [RowNumber] INT NOT NULL, [PriceTypeId] INT NOT NULL, [Price] FLOAT NOT NULL, UNIQUE([PriceTypeId],[RowNumber]) ); DECLARE @relatedProducts TABLE ( [Id] INT IDENTITY (1, 1) NOT NULL, [ArtNo] NVARCHAR (100) NOT NULL, [ArtNoClear] NVARCHAR (100) NOT NULL, [BrandNameClear] NVARCHAR (100) NOT NULL, [LinkType] INT NOT NULL UNIQUE([ArtNo],[Id]) ); DECLARE @productIds TABLE (productId INT, UNIQUE(productId)); SET @ProductId = (SELECT TOP 1 ProductId FROM [Catalog].[Product] p WHERE p.ArtNo = @searchedArtNo); INSERT INTO @outsideProduct SELECT * FROM @ProductImportTable; INSERT INTO @outsideProductPrices SELECT * FROM @PriceImportTable; IF (@searchedArtNo IS NOT NULL) DELETE FROM @outsideProduct WHERE [ArtNo] IN (SELECT [NotAnalogUid] FROM [Catalog].[NotAnalog] WHERE [Uid]=@searchedArtNo); BEGIN TRANSACTION; SET @t = @@trancount; /*Проставляем ProductId для товаров, которые уже есть в базе*/ UPDATE @outsideProduct SET [@outsideProduct].ProductId = p.ProductId FROM [Catalog].[Product] p WHERE [@outsideProduct].ArtNo = p.ArtNo /* Помечаем старые Offer */ IF (@dissableLastOffers = 1) UPDATE [Catalog].[Offer] SET [IsOldOffer]=1 WHERE [VendorId] = @VendorId ELSE UPDATE [Catalog].[Offer] SET [IsOldOffer]=1 WHERE [VendorId] = @VendorId AND ProductId IN (SELECT DISTINCT ProductId FROM @outsideProduct) SET @errorText = 'Добавление товаров'; /*Добавление товаров*/ ;with rawsource as (SELECT ArtNo, ArtNoClear, BrandNameClear, Article, Name, [Description], Unit, BrandId, Url, row_number() over(partition by ArtNo order by ArtNo) as n FROM @outsideProduct) , source as (SELECT ArtNo, ArtNoClear, BrandNameClear, Article, Name, [Description], Unit, BrandId, Url FROM rawsource where n = 1) MERGE Catalog.Product AS target USING source ON (target.ArtNo = source.ArtNo) WHEN MATCHED THEN UPDATE SET target.[DateModified] = SYSDATETIME(), target.[DateOffersUpdate] = SYSDATETIME() WHEN NOT MATCHED THEN INSERT ([ArtNo],[Article],[Name],[BriefDescription], [Enabled], [DateAdded], [DateModified], [BrandID],[UrlPath],[CategoryEnabled], [Unit], [HasMultiOffer],[ArtNoClear],[Ratio],[AddManually], [Discount]) VALUES (source.ArtNo,source.Article,source.[Name], source.[Description], 1,SYSDATETIME(),SYSDATETIME(),source.BrandId,source.Url,1,source.Unit,1,source.ArtNoClear,0,1, 0); UPDATE @outsideProduct SET [@outsideProduct].[ProductId] = p.ProductId FROM [Catalog].[Product] p WHERE [@outsideProduct].[ArtNo] = p.ArtNo AND [@outsideProduct].ProductId=-1; INSERT INTO @productIds SELECT DISTINCT [ProductId] FROM @outsideProduct; /*Добавление товаров в категорию по умолчанию*/ SET @errorText = 'Добавление товаров в категорию по умолчанию'; MERGE [Catalog].[ProductCategories] AS target USING (SELECT DISTINCT ProductID FROM @outsideProduct) AS source --ON (target.ProductId = source.ProductId) ON (target.ProductId = source.ProductId AND target.CategoryID=@mainCategoryId) --AND (SELECT COUNT(ProductID) FROM [Catalog].[ProductCategories] pc WHERE pc.ProductId = source.ProductID) = 0) WHEN NOT MATCHED THEN INSERT (CategoryID, ProductID, SortOrder, Main) VALUES (@mainCategoryId, source.ProductId, 0, 0); /*Добавление товарных предложений*/ SET @errorText = 'Добавление товарных предложений'; ;with source as (SELECT ProductId, ArtNo + '-' + CAST(@VendorId AS VARCHAR) + '-' + CAST(RowNumber AS VARCHAR) as ArtNo, ArtNoClear, BrandNameClear, Article, Name, RowNumber, Amount, BrandName, PeriodFrom, PeriodTo, Multiplicity, Information, Unit FROM @outsideProduct) MERGE [Catalog].[Offer] AS target USING source ON (target.ArtNo = source.ArtNo) WHEN MATCHED THEN UPDATE SET target.[Article] = source.Article, target.[Amount] = source.Amount, target.[Main] = 0, target.[PeriodFrom] = source.PeriodFrom, target.[PeriodTo] = source.PeriodTo, target.[ProductName] = source.Name, target.[BrandName] = source.BrandName, target.[Information] = source.[Information] WHEN NOT MATCHED THEN INSERT ([ProductID],[VendorID],[ArtNo],[Article],[Amount],[Main],[PeriodFrom],[PeriodTo],[Multiplicity],[ProductName],[BrandName],[Information],[Unit],[DateUpdate],[RowNumber],[IsOldOffer]) VALUES (source.ProductId, @VendorId, source.ArtNo ,source.Article,source.[Amount],0, source.PeriodFrom,source.PeriodTo,source.[Multiplicity], source.Name,source.BrandName,source.[Information], source.[Unit], SYSDATETIME(), source.RowNumber, 0); /*Загрузка цен*/ SET @errorText = 'Загрузка цен'; ;with source as (SELECT o.OfferID, op.PriceTypeId, op.Price FROM @outsideProductPrices op JOIN [Catalog].[Offer] o ON o.RowNumber = op.RowNumber AND o.VendorID = @VendorId ) MERGE [Catalog].[OfferPrice] AS target USING source ON target.OfferId = source.OfferId WHEN NOT MATCHED THEN INSERT (OfferID, PriceTypeId, Price) VALUES (source.OfferID, source.PriceTypeId, source.Price); /*Добавление кроссов */ SET @errorText = 'Добавление кроссов'; IF @addCrosses = 1 BEGIN INSERT INTO @relatedProducts ([ArtNo], [ArtNoClear], [BrandNameClear], [LinkType]) SELECT [ArtNo], [ArtNoClear], [BrandNameClear], 1 FROM @outsideProduct WHERE [ArtNo] NOT IN (SELECT [NotAnalogUid] FROM [Catalog].[NotAnalog] WHERE [Uid]=@searchedArtNo); WITH rawsource as (SELECT DISTINCT [ArtNo], [LinkType] FROM @relatedProducts) , source as (SELECT p.[ProductId], r.[LinkType] as LinkType FROM [Catalog].[Product] p JOIN rawsource r ON r.[ArtNo] = p.ArtNo) MERGE [Catalog].[RelatedProducts] AS target USING source ON (target.ProductId = @ProductId AND target.[LinkedProductID]=source.ProductID) WHEN NOT MATCHED THEN INSERT ([ProductID],[LinkedProductID],[RelatedType]) VALUES (@ProductId, source.[ProductId], source.[LinkType]); END /*Реиндекс товарных предложений*/ SET @errorText = 'Реиндекс товарных предложений'; UPDATE [Catalog].[Offer] SET [Main]=0 WHERE [ProductID] IN (SELECT ProductId FROM @productIds) AND [IsOldOffer] = 0 ;WITH allOffers AS ( SELECT o.ProductId, o.OfferId, row_number() OVER(PARTITION BY o.ProductId ORDER BY o.PeriodFrom, op.Price) as n FROM [Catalog].[Offer] o JOIN [Catalog].[OfferPrice] op ON op.OfferId = o.OfferID JOIN [Catalog].[Vendor] v ON o.VendorID=v.VendorId WHERE [ProductID] IN (SELECT ProductId FROM @productIds) AND o.IsOldOffer=0 AND v.IsOutsideApi=0 AND o.Amount>0 AND op.Price > 0) MERGE [Catalog].[Offer] as target USING (SELECT ProductId, OfferId FROM allOffers WHERE n=1) AS source ON target.OfferId=source.OfferId WHEN MATCHED THEN UPDATE SET Main=1; SET @errorText = 'Обновление наименований'; ;with offerNames AS (SELECT DISTINCT ProductId, ProductName FROM [Catalog].[Offer] o WHERE o.ProductID IN (SELECT ProductId FROM @productIds) AND o.[IsOldOffer]=0 AND o.[Main]=1) MERGE [Catalog].[Product] AS target USING offerNames AS source ON target.ProductId=source.ProductId WHEN MATCHED THEN UPDATE SET Name=source.ProductName; COMMIT TRANSACTION SET @errorText = 'Создание [##productIds]'; /*Загрузка ProductID для последующего запуска [Catalog].[sp_PreCalcProductParamsMassAps]*/ SET @errorText = 'Загрузка ProductID для последующего запуска [Catalog].[sp_PreCalcProductParamsMassAps]'; IF OBJECT_ID(N'tempdb..##productIds', N'U') IS NOT NULL DROP TABLE [##productIds]; CREATE TABLE [##productIds] ( [ProductId] INT NOT NULL ); INSERT INTO [##productIds] (ProductId) SELECT ProductId FROM @productIds; SET @errorText = 'Вызов [sp_PreCalcProductParamsMassAps]'; EXEC [Catalog].[sp_PreCalcProductParamsMassAps]; IF OBJECT_ID(N'tempdb..##productIds', N'U') IS NOT NULL DROP TABLE [##productIds]; END TRY BEGIN CATCH IF @t = 0 and @@trancount > 0 rollback transaction; RAISERROR (@errorText,10,1) ; END CATCH; END |