OData / OData/AspNetCoreOData

AspNetCoreOData generates duplicate JOIN statement if expanding an IQueryable that already has a join

Open
#497 9 comments 0 reactions 3 assignees View on GitHub

@xuzhg is already working on this.

Since Feb 22, 2022.

investigating P2
Dominant language
C#
Stars
505
Forks
186
PR merge metrics
No merged PRs in 30d

Description

This issue was reported by an internal customer.

They have separate classes to represent DB entities and API-visible entities:

We are experiencing major performance regression in the Expand query when migrating our code to latest ASP.Net core with latest OData 8.0.6.

I’ve minimized my code to create a simple repro:

Defined 2 entities, Incident and Alert:

public class Incident
    {
        [Key]
        public long Id { get; set; }
 
        public IEnumerable<Alert> Alerts { get; set; }
    }
 
public class Alert
    {
        [Key]
        public string Id { get; set; }
    }

Defined their DB entities:

[Table("RawIncidents")]
    public class IncidentDbEntity
    {
        [Key]
        [Column]
        public long IncidentId { get; set; }
 
        public List<AlertDbEntity> Alerts { get; set; }
    }
 
 
[Table("AlertsForPublicApi")]
    public class AlertDbEntity
    {
        [Key]
        [Column(TypeName = "varchar")]
        public string AlertId { get; set; }
 
        [Column]
        [ForeignKey(nameof(Incident))]
        public long? IncidentId { get; set; }
 
        public IncidentDbEntity Incident { get; set; }
    }

Defined a query translation from DB entities to Public facing entities:

public IQueryable<Incident> GetIncidents()
       {
           var graphIncidents = m_dbContext.RawIncidents.Select(incident => new Incident()
           {
               Id = incident.IncidentId,
               Alerts = incident.Alerts.Select(a => new Alert()
               {
                   Id = a.AlertId
               })
           });

           return graphIncidents;
       }

And we return this IQueriable in the controller with [EnableQuery] attribute, to let OData do the magic and add the query options to the SQL query.

The problem is when we add $expand=alerts to the query - it adds additional left join that is not working (more than a minute of query time) on large SQL tables (more than ~1M rows)

This is the generated query, and this is the additional query for the $expand=alerts

SELECT [t].[IncidentId], [t0].[Id], [t1].[AlertId]
FROM (
    SELECT TOP(@__TypedProperty_3) [r].[IncidentId]
    FROM [WcdTestCloudEusStg].[RawIncidents] AS [r]
) AS [t]
LEFT JOIN (
    SELECT [a].[AlertId] AS [Id], [a].[IncidentId]
    FROM [WcdTestCloudEusStg].[AlertsForPublicApi] AS [a]
) AS [t0] ON [t].[IncidentId] = [t0].[IncidentId]
LEFT JOIN (
    SELECT [t2].[AlertId], [t2].[IncidentId]
    FROM (
        SELECT [a0].[AlertId], [a0].[IncidentId], ROW_NUMBER() OVER(PARTITION BY [a0].[IncidentId] ORDER BY [a0].[AlertId]) AS [row]
        FROM [WcdTestCloudEusStg].[AlertsForPublicApi] AS [a0]
    ) AS [t2]
    WHERE [t2].[row] <= @__TypedProperty_1
) AS [t1] ON [t].[IncidentId] = [t1].[IncidentId]
ORDER BY [t].[IncidentId], [t0].[Id], [t1].[IncidentId], [t1].[AlertId]

*I guess that it is doing it to enforce the page size we defined on each entity.
*But why 2 Join queries? And why order the rows in the 2nd query and not just simple TOP as in the Incident level?

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.