2015-07-21 18 views
8

Ho una semplice query di paging LINQ contro una sola entità:Entity Framework genera SQL inefficiente per query di paging

var data = (from t in ctx.ObjectContext.Widgets 
      where t.CampaignId == campaignId && 
       t.CalendarEventId == calendarEventId 
       (t.RecurringEventId IS NULL OR t.RecurringEventId = recurringEventId) 
      select t); 

data = data.OrderBy(t => t.Id); 

if (page > 0) 
{ 
    data = data.Skip(rows * (page - 1)).Take(rows); 
} 

var l = data.ToList(); 

mi aspettavo di generare SQL simile a:

select top 50 * from Widgets w where CampaignId = xxx AND CalendarEventId = yyy AND (RecurringEventId IS NULL OR RecurringEventId = zzz) order by w.Id 

Quando eseguo quanto sopra query in SSMS, restituisce rapidamente (ha dovuto ricostruire i miei indici prima).

Tuttavia, l'SQL generato è diverso. Esso contiene una query nidificate come illustrato di seguito:

SELECT TOP (50) 
[Project1].[Id] AS [Id], 
[Project1].[CampaignId] AS [CampaignId] 
<redacted> 
FROM (SELECT [Project1].[Id] AS [Id], 
[Project1].[CampaignId] AS [CampaignId], 
<redacted>, 
row_number() OVER (ORDER BY [Project1].[Id] ASC) AS [row_number] 
    FROM (SELECT 
     [Extent1].[Id] AS [Id], 
     [Extent1].[CampaignId] AS [CampaignId], 
     <redacted> 
     FROM [dbo].[Widgets] AS [Extent1] 
     WHERE ([Extent1].[CampaignId] = @p__linq__0) AND ([Extent1].[CalendarEventId] = @p__linq__1) AND ([Extent1].[RecurringEventId] = @p__linq__2 OR [Extent1].[RecurringEventId] IS NULL) 
    ) AS [Project1] 
) AS [Project1] 
WHERE [Project1].[row_number] > 0 
ORDER BY [Project1].[Id] ASC 

La tabella Widget è enorme e la query interna restituisce 100000s di record, causando un timeout.

C'è qualcosa che posso fare per cambiare la generazione? Qualcosa che sto facendo male?

UPDATE

sono finalmente riuscito a refactoring il mio codice per restituire i risultati in tempi relativamente brevi:

var data = (from t in ctx.ObjectContext.Widgets 
      where t.CampaignId == campaignId && 
       t.CalendarEventId == calendarEventId 
       (t.RecurringEventId IS NULL OR t.RecurringEventId = recurringEventId) 
      select t)).AsEnumerable().Select((item, index) => new { Index = index, Item = item }); 

      data = data.OrderBy(t => t.Index); 

      if (page > 0) 
      { 
       data = data.Where(t => t.Index >= (rows * (page - 1))); 
      } 

      data = data.Take(rows); 

nota, la logica page > 0 è usato semplicemente per evitare un parametro non valido in uso; non fa ottimizzazione. Infatti, page > 1, se valido, non fornisce alcuna ottimizzazione evidente per la prima pagina; dal momento che il Where non è un'operazione lenta.

+2

si può mostrare la query Piano? Non vedo perché la query interna possa essere recuperata completamente qui. C'è qualcosa di sbagliato nel modo in cui viene eseguito SQL. –

+1

Quanto è veloce l'aggiunta di 'order by' alla query? cio 'seleziona' top 50 * da Widgets dove CampaignId = xxx AND CalendarEventId = yyy ordina per id' – Aducci

+1

Il tuo SQL veloce non ha ORDER BY. Cosa succede se lo aggiungi? – hvd

risposta

1

Prior SQL Server 2012, il codice SQL generato è il modo migliore per eseguire il paging. Sì, è terribile e molto inefficiente, ma è il meglio che puoi fare scrivendo a mano il tuo scritp SQL. Ci sono tonnellate di inchiostro digitale su questo nella rete. Basta google.

Nella prima pagina, questo può essere ottimizzato non facendo Skip e solo Take ma in qualsiasi altra pagina sei f ***** su.

Un workarround potrebbe essere quello di generare il proprio numero di riga in persistenza (un'identità automatica potrebbe funzionare) e fare semplicemente where(widget.number > (page*rows)).Take(rows) nel codice. Se c'è un buon indice nel tuo widget.number la query dovrebbe essere molto veloce. Ma, questo interrompe la dinamica orderBy.

Tuttavia, nel codice si può vedere che si sta ordinando per widget.id sempre; quindi, se la dinamica orderBy non è essenziale, potrebbe essere una soluzione alternativa valida.

Prenderai la tua medicina?

potresti chiedermelo.

No, non lo farò. Il modo migliore per gestire questo problema è avere un modello di lettura della persistenza in cui puoi persino avere una tabella per ordine di widget dal campo con il proprio widget.number. Il problema è che la modellazione di un sistema con un modello di lettura di persistenza solo per questo problema è troppo folle. Avere un modello di lettura fa parte del progetto generale del sistema e richiede di tenerlo in considerazione fin dall'inizio della progettazione e dello sviluppo di un sistema.

+0

Penso che l'identità automatica in LINQ sia la risposta. Pubblicherà l'aggiornamento se funziona. – Kev

1

La query generata è così complessa e nidificata perché è stato utilizzato il metodo Skip. In T-SQL Take è facilmente ottenibile utilizzando solo Top, ma non è il caso di Skip - per applicarlo è necessario row_number ed è per questo che esiste una query nidificata - inner restituisce le righe con row_number e outer le filtra per ottenere il corretto quantità di righe. La vostra query:

select top 50 * from Widgets w where CampaignId = xxx AND CalendarEventId = yyy AND (RecurringEventId IS NULL OR RecurringEventId = zzz) order by w.Id 

manca Salta righe iniziali. Per mantenere la query molto efficiente che sarebbe meglio, invece di usare Prendere e Skip per mantenere il paging per condizione in Id, perché si desidera ordinare le righe per il paging basandosi su quel campo:

var data = (from t in ctx.ObjectContext.Widgets 
     where t.CampaignId == campaignId && 
      t.CalendarEventId == calendarEventId 
      (t.RecurringEventId IS NULL OR t.RecurringEventId = recurringEventId) 
     select t); 

data = data 
    .OrderBy(t => t.Id); 
    .Where(t => t.Id >= rows * (page - 1) && t.Id < rows * page) 
    .ToList(); 
+0

Non penso che questo possa aiutare. Gli ID dei widget corrispondenti non inizieranno a 1 e potrebbero esserci degli spazi negli ID. – Kev

Problemi correlati