Или некоторые соображения по скорости работы EF в реальных приложениях. Довольно часто мы не задумываемся сколько «стоит» удобство использования ORM и всякого «сахара». На днях сел писать программу для импорта данных из sqlite в MS SQL. Структура приведена ниже на рисунках.
Следующие поля в исходной базе строковые:
· Bssid
· Ssid
· Capabilities
· Type
Импортируемые данные
Схема таблиц для вставки данных
Хранить фичи в виде строки мне показалось плохой идеей поэтому распарсил строки и завел новую табличку для возможностей. Получившаяся схема приведена на рисунке «Схема таблиц для вставки данных»
Дальше использовал EF code first для чтения данных и для записи. Результаты не утешали, на обработку одной записи уходило 170- 210 ms. На 50 000 записях это 142 минуты. Ого!!!!!!! При том что чтение этих данных в редакторе заняло 2 – 3 секунды. Немножко про архитектуру - все в принципе стандартно:
public class KvnSiteContext : DbContext{
public DbSet<Node> Nodes { get; set; }
public DbSet<Feature> Features { get; set; }
public DbSet<UserProfile> UserProfiles { get; set; }
public DbSet<Location> Locations { get; set; }
}
Дальше создается базовый репозиторий для доступа к коллекциям и репозиторий под каждую коллекцию. Кода минимум, все красиво, но тормознуто. Начинаем оптимизировать. Для начала создал класс для логгирования временных промежутков.
sealed internal class Duration{
public Duration()
{
_start = DateTime.Now;
}
private readonly DateTime _start;
public TimeSpan GetDuration()
{
return DateTime.Now - _start;
}
}
Это позволило точно определить виновника по времени.
Итак первоначальный алгоритм:
· Считать записи из таблицы network
· Распарсить фичи
· Каждую фичу вставить в коллекцию и сохранить если новая
· Вставить элемент Node
· Сохранить контекст
· Считать записи из таблицы location
· Найти по полю bssid сущность Node
· Вставить сущность Location
· Сохранить контекст
Результаты были неутешительны, как я уже писал 170 – 210 ms.
Будем что то менять. Переносим сохранение коллекции фич на момент сохранения ноды.
Время уменьшается, до 140-160ms. Дальнейшее расследование показало что слишком много запросов даже при простом поиске, хотя данные уже у нас и зачем искать в БД?
Было применено локальное кэширование всего чего возможно, результаты неутешительные 110-120ms. И тогда я понял что время тяжёлой артиллерии. TSQL!!!!!!!!!!!!
Были созданы 3 хранимые процедуры и одна функция найдена в интернете. В результате время выполнения импорта снизилось с 142 мин до 7. Вот вам один из примеров того как влияют фреймворки на скорость.
Код хранимок:
USE [kvnwifi]
GO
/****** Object: StoredProcedure [dbo].[insertNode] Script Date: 05/26/2013 20:46:50 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[insertNode]
@Id uniqueidentifier OUT,
@bssid nvarchar(MAX),
@ssid nvarchar (MAX),
@frequency int,
@capabilities nvarchar(MAX),
@lasttime bigint,
@lastlat float,
@lastlon float ,
@type int,
@user_id int
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
set @id = NEWID();
-- Insert statements for procedure here
INSERT into [dbo].[Nodes]
(
[Id],
[BsSid],
[SSid],
[Frequency],
[LastTime],
[Lat],
[Lon],
[Type],
[User_UserId]
)
VALUES(
@id,
@bssid,
@ssid,
@frequency,
@lasttime,
@lastlat,
@lastlon,
@type,
@user_id
);
exec dbo.InsertCapabilities @Nodeid = @id, @Capabilities = @capabilities
END
USE [kvnwifi]
GO
/****** Object: StoredProcedure [dbo].[InsertCapabilities] Script Date: 05/26/2013 20:47:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[InsertCapabilities]
@NodeId uniqueidentifier,
@Capabilities nvarchar(MAX)
AS
BEGIN
SET NOCOUNT ON
DECLARE @i int
DECLARE @numrows int
DECLARE @feature_name nvarchar(100)
DECLARE @numfeature int
DECLARE @fid int
DECLARE @fn_table TABLE(
idx smallint Primary Key IDENTITY(1,1),
[Name] nvarchar(100)
)
INSERT @fn_table
select [Name] from [dbo].[splitstring](@Capabilities)
SET @i = 1
SET @numrows = (SELECT COUNT(*) FROM @fn_table)
IF @numrows > 0
WHILE (@i <= (SELECT MAX(idx) FROM @fn_table))
BEGIN
-- get the next employee primary key
SET @feature_name = (SELECT [Name] FROM @fn_table WHERE idx = @i)
SET @numfeature = (SELECT COUNT(*) from [dbo].[Features] WHERE [FeatureName] = @feature_name)
IF @numfeature != 0
BEGIN
SET @fid = (SELECT [Id] FROM Features WHERE [FeatureName] = @feature_name)
END
ELSE
BEGIN
INSERT INTO dbo.Features ([FeatureName]) VALUES (@feature_name)
SET @fid = @@IDENTITY
END
INSERT INTO dbo.FeatureNodes ([Feature_Id],[Node_Id]) VALUES (@fid,@NodeId)
-- increment counter for next employee
SET @i = @i + 1
END
END
USE [kvnwifi]
GO
/****** Object: StoredProcedure [dbo].[insertLocation] Script Date: 05/26/2013 20:47:33 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author: <Author,,Name>
-- Create date: <Create Date,,>
-- Description: <Description,,>
-- =============================================
ALTER PROCEDURE [dbo].[insertLocation]
@Id uniqueidentifier OUT,
@Bssid nvarchar(MAX),
@Level bigint,
@Lat float,
@Lon float,
@Altitude float,
@Accuracy float,
@LastDate bigint
AS
BEGIN
DECLARE @bsguid uniqueidentifier
DECLARE @return uniqueidentifier
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
SET @bsguid = (SELECT [Id] FROM [dbo].[Nodes] WHERE [BsSid] = @Bssid)
SET @Id = NEWID()
INSERT INTO [dbo].[Locations]
(
[Id],
[Bssid],
[Level],
[Lat],
[Lon],
[Altitude],
[Accuracy],
[LastDate]
)
VALUES
(
@Id,
@bsguid,
@Level,
@Lat,
@Lon,
@Altitude,
@Accuracy,
@LastDate
)
END
start: 26.05.2013 18:38:24 end: 26.05.2013 18:45:01
start: 26.05.2013 18:38:24 end: 26.05.2013 18:38:24 item processed: 43301 last time ms: 10,0006