Search code examples
c#indexingentity-framework-coreazure-sql-databasesql-server-profiler

EF Core query is extremely slow. How do I improve it?


This query uses a lot of CPU and takes about 30 seconds to execute. What can I do in order to improve it?

The reason it uses joins is because there weren't proper relationships. I just added ones and started to replace all repositories since this is using EF Core and EF Core is already based on repositories and Unit of work.

var employeeLocations = await _dbContext.Set<LocationEmployee>()
    .Include(x => x.Employee)
    .Where(x => x.Employee.Email == _userEmail && x.IsActive && (x.CanBuy || x.CanSell))
    .ToListAsync(cancellationToken);

What else? Ideas?

I have a feeling that the Union is what's causing the slowness.

Original code

public class GetAllReviewAndReleaseQuery : IRequest<ListDto<ReviewAndReleaseItemDto>>
{
    public ReviewAndReleaseFilterDto Filter { get; set; }
}

public sealed class GetAllReviewAndReleaseQueryHandler : IRequestHandler<GetAllReviewAndReleaseQuery, ListDto<ReviewAndReleaseItemDto>>
{
    readonly MarketTransactionRepository _marketTransaction;
    readonly ProductRepository _productRepository;
    readonly CommodityRepository _commodityRepository;
    readonly ILogger<GetAllReviewAndReleaseQueryHandler> _logger;
    readonly OfferMonitoringRepository _offerMonitoringRepository;
    readonly OfferRepository _offerRepository;
    readonly EmployeeRepository _employeeRepository;
    readonly HedgeAccountRepository _hedgeAccountRepository;
    readonly string _userEmail;
    readonly EmployeeLocationRepository _employeeLocationRepository;
    readonly ContractRepository _contractRepository;
    readonly CustomerRepository _customerRepository;

    public GetAllReviewAndReleaseQueryHandler(MarketTransactionRepository marketTransaction,
        ProductRepository productRepository,
        CommodityRepository commodityRepository,
        OfferMonitoringRepository offerMonitoringRepository,
        OfferRepository offerRepository,
        EmployeeRepository employeeRepository,
        HedgeAccountRepository hedgeAccountRepository,
        ILogger<GetAllReviewAndReleaseQueryHandler> logger,
        CurrentUser userEmail, EmployeeLocationRepository employeeLocationRepository, ContractRepository contractRepository, CustomerRepository customerRepository)
    {
        _marketTransaction = marketTransaction;
        _logger = logger;
        _productRepository = productRepository;
        _commodityRepository = commodityRepository;
        _offerMonitoringRepository = offerMonitoringRepository;
        _offerRepository = offerRepository;
        _employeeRepository = employeeRepository;
        _hedgeAccountRepository = hedgeAccountRepository;
        _userEmail = userEmail?.Email ?? CurrentUser.InternalUser;
        _employeeLocationRepository = employeeLocationRepository;
        _contractRepository = contractRepository;
        _customerRepository = customerRepository;
    }

    public async Task<ListDto<ReviewAndReleaseItemDto>> Handle(GetAllReviewAndReleaseQuery request, CancellationToken cancellationToken)
    {
        if (!request.Filter.ValidatePagingParametersAreGreaterThanZero())
        {
            throw new PaginationNotValidException("Pagination parameters are not valid, please review your parameters");
        }
        
        var employeeLocations = from el in _employeeLocationRepository.GetDbSet()
            join emp in _employeeRepository.GetDbSet() on el.EmployeeId equals emp.Id
            where emp.Email == _userEmail && el.IsActive && (el.CanBuy || el.CanSell)
            select el;

        var dataSet1 = from f in _marketTransaction.GetDbSet()
            join c in _commodityRepository.GetDbSet() on f.CommodityId equals c.Id
            join p in _productRepository.GetDbSet() on c.ProductId equals p.Id
            join h in _hedgeAccountRepository.GetDbSet() on f.MarketAccount equals h.Account into gha
            from account in gha.DefaultIfEmpty()
            join con in _contractRepository.GetDbSet() on f.ContractId equals con.Id into contract
            from ct in contract.DefaultIfEmpty()
            join off in _offerRepository.GetDbSet() on f.OfferId equals off.Id into offer
            from of in offer.DefaultIfEmpty()
            join cusForCon in _customerRepository.GetDbSet() on ct.CustomerId equals cusForCon.Id into customerForContract
            from customerForCon in customerForContract.DefaultIfEmpty()
            join cusForOff in _customerRepository.GetDbSet() on of.CustomerId equals cusForOff.Id into customerForOffer
            from customerForOff in customerForOffer.DefaultIfEmpty()
            where f.IsActive == true && (f.Source == EMarketTransactionSource.ExternalFill || employeeLocations.Any(em => em.LocationId == ct.DeliveryLocationId || em.LocationId == of.DeliveryLocationId))
            select new ReviewAndReleaseDto
            {
                Id = f.Id,
                CustomerFirstName = customerForCon == null ? (customerForCon.FirstName ?? customerForOff.FirstName) : string.Empty,
                CustomerLastName = customerForCon == null ? (customerForCon.LastName ?? customerForOff.LastName) : string.Empty,
                CustomerNumber = customerForCon == null ? (customerForCon.Number ?? customerForOff.Number) : string.Empty,
                CustomerId = customerForCon == null ? customerForOff.Id : customerForCon.Id,
                CreationDate = f.CreatedOn,
                UpdatedOn = f.UpdatedOn,
                IsReal = true,
                FuturesPrice = f.FuturesPrice,
                IsSell = f.IsSell,
                MarketAccount = f.MarketAccount,
                WasAcknowledge = f.WasAcknowledge,
                PassFill = f.PassFill,
                IsGtc = f.IsGtc,
                Expiration = f.Expiration,
                Lots = f.Lots,
                WorkingLots = f.WorkingLots,
                ProductId = c.ProductId,
                InstrumentCode = p.Code,
                MarketPrice = f.MarketPrice,
                FuturesMonth = f.FuturesMonth,
                MarketId = Convert.ToString(f.MarketId),
                AccountName = Convert.ToString(account.Name),
                InternalCode = Convert.ToString(f.InternalCode),
                Source = f.Source == EMarketTransactionSource.Contract ? EMarketTransactionSource.Contract :
                    f.Source == EMarketTransactionSource.Accumulation ? EMarketTransactionSource.Accumulation :
                    f.Source == EMarketTransactionSource.Offer ? EMarketTransactionSource.Offer : EMarketTransactionSource.ExternalFill,
                Type = f.Type == EMarketTransactionType.Market ? EMarketTransactionType.Market : EMarketTransactionType.Limit,
                Event = f.Event == ETransactionEvent.Creation ? ETransactionEvent.Creation :
                    f.Event == ETransactionEvent.Edition ? ETransactionEvent.Edition :
                    f.Event == ETransactionEvent.Cancelation ? ETransactionEvent.Cancelation :
                    f.Event == ETransactionEvent.Roll ? ETransactionEvent.Roll :
                    f.Event == ETransactionEvent.Book ? ETransactionEvent.Book :
                    f.Event == ETransactionEvent.RollBack ? ETransactionEvent.RollBack :
                    f.Event == ETransactionEvent.PartiallyFilled ? ETransactionEvent.PartiallyFilled :
                    f.Event == ETransactionEvent.Filled ? ETransactionEvent.Filled : ETransactionEvent.Completed,
                State = f.State.Value == EMarketTransactionState.Ready ? EMarketTransactionState.Ready :
                    f.State.Value == EMarketTransactionState.Denied ? EMarketTransactionState.Denied :
                    f.State.Value == EMarketTransactionState.Pending ? EMarketTransactionState.Pending :
                    f.State.Value == EMarketTransactionState.Working ? EMarketTransactionState.Working :
                    f.State.Value == EMarketTransactionState.PartiallyFilled ? EMarketTransactionState.PartiallyFilled :
                    f.State.Value == EMarketTransactionState.Filled ? EMarketTransactionState.Filled :
                    f.State.Value == EMarketTransactionState.Canceled ? EMarketTransactionState.Canceled :
                    f.State.Value == EMarketTransactionState.Rejected ? EMarketTransactionState.Rejected :
                    EMarketTransactionState.Expired
            };

        var dataSet2 = from f in _offerMonitoringRepository.GetDbSet()
            join o in _offerRepository.GetDbSet() on f.OfferId equals o.Id
            join c in _commodityRepository.GetDbSet() on o.CommodityId equals c.Id
            join p in _productRepository.GetDbSet() on c.ProductId equals p.Id
            join cusForOf in _customerRepository.GetDbSet() on o.CustomerId equals cusForOf.Id
            where f.Action.Value == EOfferMonitorAction.Add && employeeLocations.Any(el => o.LocationId == el.LocationId)
            select new ReviewAndReleaseDto
            {
                Id = f.Id,
                CustomerFirstName = cusForOf.FirstName,
                CustomerLastName = cusForOf.LastName,
                CustomerNumber = cusForOf.Number,
                CustomerId = cusForOf.Id,
                CreationDate = f.CreatedOn,
                UpdatedOn = f.UpdatedOn,
                IsReal = false,
                FuturesPrice = o.FuturesPrice ?? 0,
                IsSell = !o.IsSell,
                MarketAccount = 0,
                WasAcknowledge = false,
                PassFill = false,
                IsGtc = o.Gtc,
                Expiration = o.Expiration,
                Lots = (int)(o.Quantity / c.LotFactor),
                WorkingLots = 0,
                ProductId = c.ProductId,
                InstrumentCode = p.Code,
                MarketPrice = o.FreightPrice,
                FuturesMonth = o.FuturesMonth,
                MarketId = Convert.ToString(null),
                AccountName = Convert.ToString(null),
                InternalCode = Convert.ToString(o.InternalCode),
                Source = EMarketTransactionSource.Offer,
                Type = EMarketTransactionType.Limit,
                Event = f.Action.Value == EOfferMonitorAction.Cancel ? ETransactionEvent.Cancelation :
                    f.Action.Value == EOfferMonitorAction.Add ? ETransactionEvent.Creation : ETransactionEvent.Edition,
                State = EMarketTransactionState.Pending
            };

        var query = dataSet1.Union(dataSet2);

        query = ApplyFilters(request, query);

        var tag50CurrentUser = await (from e in await _employeeRepository.GetAllEntities() where e.Email == _userEmail select e.Tag50).FirstOrDefaultAsync(cancellationToken: cancellationToken);
        var data = new ListDto<ReviewAndReleaseItemDto> { Total = await query.CountAsync(cancellationToken: cancellationToken) };
        if (data.Total > 0)
        {
            query = query.OrderByDescending(x => x.UpdatedOn ?? x.CreationDate);
            query = query.GetPagedQuery(request.Filter.Start.Value, request.Filter.Limit.Value);
            foreach (var reviewAndReleaseDto in 
                     from item in query.ToList()
                     let customerFullName = $"{item.CustomerFirstName} {item.CustomerLastName}"
                     select new ReviewAndReleaseItemDto
                     {
                         Id = item.Id,
                         OrderNumber = item.MarketId,
                         Type = Helper.MapType(item.Type),
                         TypeName = item.Type.ToString(),
                         CreationDate = item.CreationDate,
                         UpdateDate = item.UpdatedOn,
                         Price = item.Type == EMarketTransactionType.Market ? null : item.FuturesPrice,
                         IsSell = item.IsSell,
                         Side = Helper.MapSide(item.IsSell),
                         Quantity = item.Lots,
                         WorkingQuantity = item.WorkingLots,
                         Status = Helper.MapStatusGridToString(item.State, item.Event),
                         AccountNumber = item.MarketAccount,
                         AccountName = item.AccountName,
                         AvgFillPrice = item.IsReal ? item.MarketPrice : 0,
                         WasAcknowledge = item.WasAcknowledge,
                         PassFill = item.PassFill,
                         GTC = new()
                         {
                             IsGTC = item.IsGtc,
                             Expiration = item.Expiration
                         },
                         FuturesMonth = item.FuturesMonth,
                         InternalCode = item.InternalCode,
                         Customer = new()
                         {
                             Id = item.CustomerId,
                             Name = $"{item.CustomerFirstName} {item.CustomerLastName}".Trim(),
                             Number = item.CustomerNumber,
                         },
                         Source = Helper.MapSource(item.Source),
                         Symbol = new() { Code = item.InstrumentCode },
                         Restrictions = new()
                         {
                             CanBeApproved = item.IsReal && item.State == EMarketTransactionState.Ready && tag50CurrentUser,
                             CanBeDenied = item.IsReal && item.Type == EMarketTransactionType.Market && tag50CurrentUser &&
                                           (item.State == EMarketTransactionState.Ready || (item.State == EMarketTransactionState.Pending && item.Event == ETransactionEvent.Creation && !item.WasAcknowledge)),
                             CanBeResent = item.IsReal && item.State == EMarketTransactionState.Rejected && item.Type == EMarketTransactionType.Market && tag50CurrentUser,
                             CanBeAcknowledged = item.IsReal && item.Type == EMarketTransactionType.Market && item.State == EMarketTransactionState.Rejected && !item.WasAcknowledge && tag50CurrentUser,
                             CanBeEdited = item.Source == EMarketTransactionSource.ExternalFill && item.State != EMarketTransactionState.Canceled && tag50CurrentUser,
                             CanBeCanceled = item.Source == EMarketTransactionSource.ExternalFill && item.State != EMarketTransactionState.Canceled && tag50CurrentUser
                         }
                     })
            {
                data.List.Add(reviewAndReleaseDto);
            }
        }

        _logger.ReviewAndReleaseAll();
        return data;
    }

    private static IQueryable<ReviewAndReleaseDto> ApplyFilters(GetAllReviewAndReleaseQuery request, IQueryable<ReviewAndReleaseDto> query)
    {
        var endDate = request.Filter.EndDate ?? DateTime.Now.Date;

        query = request.Filter.StartDate != null
            ? query.Where(x => x.UpdatedOn == null ? x.CreationDate.Date >= request.Filter.StartDate && x.CreationDate.Date <= endDate : x.UpdatedOn.Value.Date >= request.Filter.StartDate && x.UpdatedOn.Value.Date <= endDate)
            : query.Where(x => x.UpdatedOn == null ? x.CreationDate.Date <= endDate : x.UpdatedOn.Value.Date <= endDate);

        query = query.WhereIf(!string.IsNullOrEmpty(request.Filter.Number), x => x.MarketId.ToUpper().Contains(request.Filter.Number.ToUpper())
                                                                                     || x.InternalCode.ToUpper().Contains(request.Filter.Number.ToUpper()));
        query = query.WhereIf(!string.IsNullOrEmpty(request.Filter.AccountName), x => x.AccountName.ToUpper().Contains(request.Filter.AccountName.ToUpper()));

        query = query.WhereIf(request.Filter.FuturesMonth is { Count: > 0 }, x => request.Filter.FuturesMonth.Contains(x.FuturesMonth));
        query = query.WhereIf(request.Filter.Symbol is { Count: > 0 }, x => request.Filter.Symbol.Contains(x.ProductId));
        query = query.WhereIf(request.Filter.CustomerId is { Count: > 0 }, x => request.Filter.CustomerId.Contains(x.CustomerId));
        query = query.WhereIf(!string.IsNullOrEmpty(request.Filter.ContractOrCustomer), x => x.InternalCode.Contains(request.Filter.ContractOrCustomer) ||
                                                                              x.CustomerFirstName.Contains(request.Filter.ContractOrCustomer) ||
                                                                              x.CustomerLastName.Contains(request.Filter.ContractOrCustomer) ||
                                                                              x.CustomerNumber.Contains(request.Filter.ContractOrCustomer));
        if (request.Filter.Status is { Count: > 0 })
        {
            var listStatusFilter = request.Filter.Status.Select(Helper.MapStatusToEnum).ToList();
            query = query.Where(x => listStatusFilter.Contains(x.State));
        }

        return query;
    }
}

public class MarketTransactionConfiguration : IEntityTypeConfiguration<MarketTransaction>
{
    public void Configure(EntityTypeBuilder<MarketTransaction> builder)
    {
        builder.ConfigureBase();
        builder.ConfigureAuditable();
        builder.ToTable("MarketTransaction", SchemaName.Transaction);
        builder.OwnsOne(o => o.State)
            .Property(p => p.Value)
            .HasColumnName("State")
            .HasConversion(
                v => v.ToString(),
                v => (EMarketTransactionState)Enum.Parse(typeof(EMarketTransactionState), v)
            ).HasMaxLength(15).IsConcurrencyToken();

        builder.Property(x => x.Type).HasMaxLength(6).HasConversion(
            v => v.ToString(),
            v => (EMarketTransactionType)Enum.Parse(typeof(EMarketTransactionType), v)
        );
        builder.Property(x => x.Event).HasMaxLength(11).HasConversion(
            v => v.ToString(),
            v => (ETransactionEvent)Enum.Parse(typeof(ETransactionEvent), v)
        );
        builder.Property(x => x.Source).HasMaxLength(18).HasConversion(
            v => v.ToString(),
            v => (EMarketTransactionSource)Enum.Parse(typeof(EMarketTransactionSource), v)
        );
        builder.Property(x => x.MarketAccount).HasMaxLength(500);
        builder.Property(x => x.MarketId).HasMaxLength(500);
        builder.Property(x => x.MarketInformation);
        builder.Property(x => x.MarketPrice).HasColumnType(ColumnType.Decimal4).IsRequired();
        builder.Property(x => x.ContractPrice).HasColumnType(ColumnType.Decimal4).IsRequired();
        builder.Property(x => x.FuturesPrice).HasColumnType(ColumnType.Decimal4).IsRequired();
        builder.Property(x => x.OldFuturesPrice).HasColumnType(ColumnType.Decimal4).IsRequired();
        builder.Property(x => x.IsSell).IsRequired();
        builder.Property(x => x.Quantity).IsRequired().HasColumnType(ColumnType.Decimal2);
        builder.Property(x => x.OldQuantity).IsRequired().HasColumnType(ColumnType.Decimal2);
        builder.Property(x => x.Lots).IsRequired();
        builder.Property(x => x.WorkingLots).IsRequired();
        builder.Property(x => x.ThresholdUsed).IsRequired();
        builder.Property(x => x.CommodityId).IsRequired();
        builder.Property(x => x.CommodityName).IsRequired();
        builder.Property(x => x.Comments);
        builder.Property(x => x.Instrument).IsRequired();
        builder.Property(x => x.ContractId);
        builder.Property(x => x.OfferId);
        builder.Property(x => x.FuturesMonth).HasMaxLength(24).IsRequired();
        builder.Property(x => x.ClientNumber).HasMaxLength(30).IsRequired();
        builder.Property(x => x.InternalCode).HasMaxLength(22);
        builder.Property(x => x.InternalNumber).HasMaxLength(30).IsRequired();
        builder.Property(x => x.IsGtc).IsRequired();
        builder.Property(x => x.CropYear).IsRequired();
        builder.Property(x => x.WasAcknowledge).IsRequired();
        builder.Property(x => x.WasAlreadySent).IsRequired();
        builder.Property(x => x.Expiration);
        builder.Property(x => x.PassFill);

        builder.HasOne(x => x.Commodity)
            .WithMany()
            .HasForeignKey(x => x.CommodityId);
        
        builder.HasOne(x => x.Contract)
            .WithMany()
            .HasForeignKey(x => x.ContractId)
            .OnDelete(DeleteBehavior.SetNull);
        
        builder.HasOne(x => x.Offer)
            .WithMany()
            .HasForeignKey(x => x.OfferId)
            .OnDelete(DeleteBehavior.SetNull);
        
        builder.HasMany(x => x.OrderFills)
            .WithOne()
            .IsRequired()
            .OnDelete(DeleteBehavior.Cascade);
    }
}

Solution

  • I suggest you to try the following query:

    var employeeLocations = await _dbContext.Set<LocationEmployee>()
         .Where(x => x.Employee.Email == _userEmail &&
          x.IsActive && (x.CanBuy || x.CanSell))
          .Select(x => new
           {
                LocationEmployee = x,
                Employee = x.Employee
           })
           .ToListAsync(cancellationToken);
    

    The above query does not try not to load all the LocationEmployee tables data. Instead it pass a sub set of data via the Select.

    Furthermore, make sure that you have indexes added to the properties Employee.Email,IsActive,CanBuy and CanSell columns or attributes on the database side.