У меня есть служба элементов, которая вызывается из контроллера API ядра Asp.Net. Запрос получает все элементы в определенной категории для отображения на веб-сайте электронной коммерции (reactjs). Контроллер возвращает только json:
public async Task<IEnumerable<EcommerceItemDto>> GetAllItemsAsync(string customerNumber, string category = "All", int page = 0, int pageSize = 9999)
{
IQueryable<Category> categories;
if (category == "All")
{
categories = _context.Categories
.Include(c => c.Children)
.Include(p => p.Parent)
.AsNoTrackingWithIdentityResolution();
}
else
{
categories = _context.Categories
.Where(n => n.Name == category)
.Include(c => c.Children)
.Include(p => p.Parent)
.AsNoTrackingWithIdentityResolution();
}
var items = await _context.EcommerceItems
.FromSqlInterpolated($"SELECT * FROM [cp].[GetEcommerceItemsView] WHERE [CustomerNumber] = {customerNumber}")
.Include(x => x.Category)
.Include(i => i.Images.OrderByDescending(d => d.Default))
.OrderBy(i => i.ItemNumber)
.Where(c => categories.Any(x => x.Children.Contains(c.Category)) || categories.Contains(c.Category))
.Skip(page * pageSize)
.Take(pageSize)
.AsNoTracking()
.ToListAsync();
var dto = _mapper.Map<IEnumerable<EcommerceItemDto>>(items);
return dto;
}
Я могу показать свой контроллер Action
, [cp].[GetEcommerceItemsView]
Вид, EcommerceItem
Учебный класс, EcommerceItemDto
Учебный класс, Category
Класс и Images
Класс, если кто думает, что поможет. Я сбежал Tuning
в моей базе данных Sql и применил рекомендации. Я отметил это, используя _mapper.Map
а также .ProjectTo
и mapper.Map быстрее на волосок. В моей тестовой базе данных всего около 65 элементов с изображениями. Но этот запрос занимает от 1000 до 1500 мс.
ОБНОВИТЬ
По запросу @ iSR5 …
public class EcommerceItem : INotifyPropertyChanged
{
[Key]
public string ItemNumber { get; set; }
public string ItemDescription { get; set; }
[Column(TypeName = "text")]
public string ExtendedDesc { get; set; }
public bool Featured { get; set; }
public string CategoryName { get; set; }
public Category Category { get; set; }
[Column(TypeName = "numeric(19, 5)")]
public decimal Price { get; set; }
[Column(TypeName = "numeric(19, 5)")]
public decimal QtyOnHand { get; set; }
public bool? Metadata1Active { get; set; }
[StringLength(50)]
public string Metadata1Name { get; set; }
[StringLength(50)]
public string Metadata1 { get; set; }
... // I have Twenty Matadata Groups
public bool? Metadata20Active { get; set; }
[StringLength(50)]
public string Metadata20Name { get; set; }
[StringLength(50)]
public string Metadata20 { get; set; }
public ICollection<EcommerceItemImages> Images { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Мой EcommerceItemDto:
public class EcommerceItemDto
{
[Key]
public string ItemNumber { get; set; }
public string ItemDescription { get; set; }
[Column(TypeName = "text")]
public string ExtendedDesc { get; set; }
public bool Featured { get; set; }
public string CategoryName { get; set; }
[Column(TypeName = "numeric(19, 5)")]
public decimal Price { get; set; }
[Column(TypeName = "numeric(19, 5)")]
public decimal QtyOnHand { get; set; }
public bool? Metadata1Active { get; set; }
[StringLength(50)]
public string Metadata1Name { get; set; }
[StringLength(50)]
public string Metadata1 { get; set; }
... // Same as EcommerceItem I have twenty Metadata groups
public bool? Metadata20Active { get; set; }
[StringLength(50)]
public string Metadata20Name { get; set; }
[StringLength(50)]
public string Metadata20 { get; set; }
public ICollection<EcommerceItemImagesDto> Images { get; set; }
}
Моя модель категории:
[Table("bma_ec_categories")]
public class Category : INotifyPropertyChanged
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Column("category_id")]
public int CategoryId { get; set; }
[Column("parent_category_id")]
public int? ParentId { get; set; }
[Required]
[Column("category_name")]
[StringLength(50)]
public string Name { get; set; }
public Category Parent { get; set; }
public ICollection<Category> Children { get; set; }
[Column("title")]
[StringLength(150)]
public string Title { get; set; }
[Column("description")]
[StringLength(250)]
public string Description { get; set; }
[Column("keywords")]
[StringLength(250)]
public string Keywords { get; set; }
[Column("page_content", TypeName = "text")]
public string PageContent { get; set; }
[Column("banner_group_id")]
public int? BannerGroupId { get; set; }
[Column("inactive")]
public byte? Inactive { get; set; }
[Column("issitecategory")]
public byte Issitecategory { get; set; }
[Column("metadata1_active")]
public byte Metadata1Active { get; set; }
[Column("metadata1_name")]
[StringLength(50)]
public string Metadata1Name { get; set; }
...
[Column("metadata20_active")]
public byte? Metadata20Active { get; set; }
[Column("metadata20_name")]
[StringLength(50)]
public string Metadata20Name { get; set; }
[Column("sort_order")]
public int? SortOrder { get; set; }
public ICollection<EcommerceItem> Items { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Просто чтобы быть полным, мой View
, он получает рассчитанную цену для вошедшего в систему пользователя и выравнивает метаданные категории и элемента:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER VIEW [cp].[GetEcommerceItemsView]
AS
SELECT
RTRIM(LTRIM(IV.ITEMNMBR)) AS ItemNumber
,RTRIM(LTRIM(IM.ITEMDESC)) AS ItemDescription
,ecItems.[extended_desc] AS ExtendedDesc
,CAST(ecItems.Featured AS BIT) AS Featured
,cat.category_name AS CategoryName
,CASE IM.PRICMTHD
WHEN 1 THEN IV.UOMPRICE
WHEN 2 THEN IV.UOMPRICE * IC.LISTPRCE / 100
WHEN 3 THEN (IM.CURRCOST) * (1 + (IV.UOMPRICE / 100))
WHEN 4 THEN (IM.STNDCOST) * (1 + (IV.UOMPRICE / 100))
WHEN 5 THEN (IM.CURRCOST) / (1 - (IV.UOMPRICE / 100))
WHEN 6 THEN (IM.STNDCOST) / (1 - (IV.UOMPRICE / 100))
ELSE 0
END AS Price
,IQ.QTYONHND AS QtyOnHand
,CAST(cat.[metadata1_active] AS BIT) AS [Metadata1Active]
,cat.[metadata1_name] AS [Metadata1Name]
,ecItems.[metadata1] AS [Metadata1]
...
,CAST(cat.[metadata20_active] AS BIT) AS [Metadata20Active]
,cat.[metadata20_name] AS [Metadata20Name]
,ecItems.[Metadata20] AS [Metadata20]
,C.CUSTNMBR AS CustomerNumber
FROM dbo.RM00101 AS C
LEFT OUTER JOIN dbo.IV00108 AS IV
ON C.PRCLEVEL = IV.PRCLEVEL
LEFT OUTER JOIN dbo.IV00101 AS IM
ON IM.ITEMNMBR = IV.ITEMNMBR
LEFT OUTER JOIN dbo.IV00102 AS IQ
ON IQ.ITEMNMBR = IV.ITEMNMBR
AND IQ.RCRDTYPE = 1
LEFT OUTER JOIN dbo.IV00105 AS IC
ON IC.ITEMNMBR = IV.ITEMNMBR
AND IV.CURNCYID = IC.CURNCYID
LEFT OUTER JOIN dbo.bma_ec_items AS ecItems
ON ecItems.ITEMNMBR = IV.ITEMNMBR
LEFT OUTER JOIN dbo.bma_ec_item_category AS icat
ON icat.ITEMNMBR = IV.ITEMNMBR
LEFT OUTER JOIN dbo.bma_ec_categories AS cat
ON cat.category_id = icat.category_id
WHERE (IM.ITEMNMBR IN (SELECT
ITEMNMBR
FROM dbo.bma_ec_items
WHERE (display_on_ecommerce = 1))
)
GO
Специальное примечание: Я использую сериализатор Newtonsoft Json в своем контроллере. Я знаю, что существуют более быстрые сериализаторы.
ОБНОВЛЕНИЕ 2
В соответствии с запросом, следующие результаты теста:
BenchmarkDotNet = v0.13.0, ОС = Windows 10.0.19043.1110 (21H1 / May2021Update) Intel Core i7-7700 CPU 3,60 ГГц (Kaby Lake), 1 процессор, 8 логических и 4 физических ядра .NET SDK = 5.0.302
[Host] : .NET 5.0.8 (5.0.821.31504), X64 RyuJIT [AttachedDebugger]
DefaultJob: .NET 5.0.8 (5.0.821.31504), X64 RyuJIT
Метод | IterationCount | Иметь в виду | Ошибка | StdDev | Мин. | Максимум | Соотношение | Соотношение SD |
---|---|---|---|---|---|---|---|---|
GetItems | 50 | 228,2 мс | 8,15 мс | 23,65 мс | 119,7 мс | 265,6 мс | 1,00 | 0,00 |
GetItemsWithChanges | 50 | 231,0 мс | 4,60 мс | 10,66 мс | 211,3 мс | 253,1 мс | 1.04 | 0,22 |
GetItems | 100 | 455,8 мс | 9,04 мс | 24,29 мс | 414,9 мс | 522,0 мс | 1,00 | 0,00 |
GetItemsWithChanges | 100 | 453,8 мс | 9.00 мс | 20,32 мс | 421,0 мс | 502,2 мс | 1,00 | 0,08 |
ОБНОВЛЕНИЕ 3
Тест без категорий:
Метод | IterationCount | Иметь в виду | Ошибка | StdDev | Мин. | Максимум | Соотношение | Соотношение SD |
---|---|---|---|---|---|---|---|---|
GetItems | 50 | 231,1 мс | 6.06 мс | 17,88 мс | 140,4 мс | 259,7 мс | 1,00 | 0,00 |
GetItemsWithChangesWithoutCategories | 50 | 230,9 мс | 4,61 мс | 10,69 мс | 214,0 мс | 255,4 мс | 1.01 | 0,15 |
GetItems | 100 | 450,2 мс | 8,99 мс | 22,40 мс | 416,6 мс | 510,1 мс | 1,00 | 0,00 |
GetItemsWithChangesWithoutCategories | 100 | 452,5 мс | 9.01 мс | 23,10 мс | 417,3 мс | 515,9 мс | 1.01 | 0,08 |
1 ответ
Я выполнил все рекомендации из RedGate Sql Prompt (изменил свое мнение на одно рекомендуемое изменение):
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER VIEW [cp].[GetEcommerceItemsView]
AS
SELECT
RTRIM(LTRIM(IV.ITEMNMBR)) AS ItemNumber
,RTRIM(LTRIM(IM.ITEMDESC)) AS ItemDescription
,ecItems.[extended_desc] AS ExtendedDesc
,CAST(ecItems.Featured AS BIT) AS Featured
,cat.category_name AS CategoryName
,CASE IM.PRICMTHD
WHEN 1 THEN IV.UOMPRICE
WHEN 2 THEN IV.UOMPRICE * IC.LISTPRCE / 100
WHEN 3 THEN (IM.CURRCOST) * (1 + (IV.UOMPRICE / 100))
WHEN 4 THEN (IM.STNDCOST) * (1 + (IV.UOMPRICE / 100))
WHEN 5 THEN (IM.CURRCOST) / (1 - (IV.UOMPRICE / 100))
WHEN 6 THEN (IM.STNDCOST) / (1 - (IV.UOMPRICE / 100))
ELSE 0
END AS Price
,IQ.QTYONHND AS QtyOnHand
,CAST(cat.[metadata1_active] AS BIT) AS [Metadata1Active]
,cat.[metadata1_name] AS [Metadata1Name]
,ecItems.[metadata1] AS [Metadata1]
...
,CAST(cat.[metadata20_active] AS BIT) AS [Metadata20Active]
,cat.[metadata20_name] AS [Metadata20Name]
,ecItems.[Metadata20] AS [Metadata20]
,C.CUSTNMBR AS CustomerNumber
FROM dbo.RM00101 AS C
LEFT OUTER JOIN dbo.IV00108 AS IV
ON C.PRCLEVEL = IV.PRCLEVEL
LEFT OUTER JOIN dbo.IV00101 AS IM
ON IM.ITEMNMBR = IV.ITEMNMBR
LEFT OUTER JOIN dbo.IV00102 AS IQ
ON IQ.ITEMNMBR = IV.ITEMNMBR
AND IQ.RCRDTYPE = 1
LEFT OUTER JOIN dbo.IV00105 AS IC
ON IC.ITEMNMBR = IV.ITEMNMBR
AND IV.CURNCYID = IC.CURNCYID
LEFT OUTER JOIN dbo.bma_ec_items AS ecItems
ON ecItems.ITEMNMBR = IV.ITEMNMBR
AND ecItems.display_on_ecommerce = 1
LEFT OUTER JOIN dbo.bma_ec_item_category AS icat
ON icat.ITEMNMBR = IV.ITEMNMBR
LEFT OUTER JOIN dbo.bma_ec_categories AS cat
ON cat.category_id = icat.category_id
GO
Повторно запустите советник по настройке и примените созданные индексы. Затем я переключился на System.Text.Json из Newtonsoft.Json в качестве сериализатора в проекте Asp.net Core. Проведите повторный тест производительности:
Метод | IterationCount | Иметь в виду | Ошибка | StdDev | Мин. | Максимум | Соотношение | Соотношение SD |
---|---|---|---|---|---|---|---|---|
GetItems | 50 | 225,5 мс | 4,89 мс | 14,26 мс | 134,77 мс | 250,5 мс | 1,00 | 0,00 |
GetItemsWithChangesWithoutCategories | 50 | 225,4 мс | 6,86 мс | 20,23 мс | 98,31 мс | 254,2 мс | 1.01 | 0,13 |
GetItems | 100 | 434,6 мс | 7,65 мс | 11,21 мс | 415,40 мс | 460,2 мс | 1,00 | 0,00 |
GetItemsWithChangesWithoutCategories | 100 | 442,1 мс | 8,78 мс | 16,50 мс | 418,91 мс | 478,6 мс | 1.02 | 0,05 |
Я предполагаю, что это оптимизировано по мере возможности, если я не перейду на Dapper.
ОБНОВИТЬ
Честно говоря, я обнаружил, что в моем имени пользователя была опечатка, которую я использовал для получения токена авторизации. Я поправил и повторно запустил тест:
Метод | IterationCount | Иметь в виду | Ошибка | StdDev | Мин. | Максимум | Соотношение |
---|---|---|---|---|---|---|---|
GetItems | 50 | 6.260 с | 0,1213 с | 0,1490 с | 6.012 с | 6.598 с | 1,00 |
GetItemsWithChangesWithoutCategories | 50 | 2,036 с | 0,0407 с | 0,0543 с | 1.948 с | 2,162 с | 0,32 |
GetItems | 100 | 12,521 с | 0,2439 с | 0,2281 с | 12.048 с | 12.932 с | 1,00 |
GetItemsWithChangesWithoutCategories | 100 | 4.135 с | 0,0790 с | 0,0739 с | 4.031 с | 4.242 с | 0,33 |
Не думаю, что щегольство решит проблему. Вам необходимо повторно оценить представление вне вашего кода (используя SSMS или любую IDE СУБД), а затем применить к нему тот же запрос, пока
Execution Plan
горит. затем просмотрите план выполнения и посмотрите, что можно улучшить. Что касается обновленного теста (с исправленным именем пользователя), он показывает, что он даже становится медленнее, чем раньше. (возможно, из-за получения дополнительных данных после того, как вы исправили имя пользователя).— iSR5