2016-04-22 10 views
9

Ho una lista enorme di articoli e ho bisogno di raggrupparli per una proprietà. Quindi dovrebbe essere selezionato il più vecchio di ogni gruppo.Entity Framework GroupPer prendere il più vecchio con mySQL

Esempio semplificato: selezionare l'utente più vecchio di ogni FirstName.

using (ED.NWEntities ctx = new ED.NWEntities()) 
{ 
    IQueryable<ED.User> Result = ctx.User.GroupBy(x => x.FirstName) 
            .Select(y => y.OrderBy(z => z.BirthDate) 
            .FirstOrDefault()) 
            .AsQueryable(); 
} 

Classe User:

public partial class User 
{ 
    public int UserID { get; set; } 
    public string FirstName { get; set; } 
    public string LastName { get; set; } 
    public Nullable<System.DateTime> BirthDate { get; set; } 
} 

Mi chiedevo perché questa dichiarazione ha preso così a lungo fino a quando ho impostato un punto di interruzione Result e guardai in istruzione SQL generata:

{SELECT 
`Apply1`.`UserID`, 
`Apply1`.`FIRSTNAME1` AS `FirstName`, 
`Apply1`.`LastName`, 
`Apply1`.`BirthDate` 
FROM (SELECT 
`Distinct1`.`FirstName`, 
(SELECT 
`Project2`.`UserID` 
FROM `User` AS `Project2` 
WHERE (`Distinct1`.`FirstName` = `Project2`.`FirstName`) OR ((`Distinct1`.`FirstName` IS NULL) AND (`Project2`.`FirstName` IS NULL)) 
ORDER BY 
`Project2`.`BirthDate` ASC LIMIT 1) AS `UserID`, 
(SELECT 
`Project2`.`FirstName` 
FROM `User` AS `Project2` 
WHERE (`Distinct1`.`FirstName` = `Project2`.`FirstName`) OR ((`Distinct1`.`FirstName` IS NULL) AND (`Project2`.`FirstName` IS NULL)) 
ORDER BY 
`Project2`.`BirthDate` ASC LIMIT 1) AS `FIRSTNAME1`, 
(SELECT 
`Project2`.`LastName` 
FROM `User` AS `Project2` 
WHERE (`Distinct1`.`FirstName` = `Project2`.`FirstName`) OR ((`Distinct1`.`FirstName` IS NULL) AND (`Project2`.`FirstName` IS NULL)) 
ORDER BY 
`Project2`.`BirthDate` ASC LIMIT 1) AS `LastName`, 
(SELECT 
`Project2`.`BirthDate` 
FROM `User` AS `Project2` 
WHERE (`Distinct1`.`FirstName` = `Project2`.`FirstName`) OR ((`Distinct1`.`FirstName` IS NULL) AND (`Project2`.`FirstName` IS NULL)) 
ORDER BY 
`Project2`.`BirthDate` ASC LIMIT 1) AS `BirthDate` 
FROM (SELECT DISTINCT 
`Extent1`.`FirstName` 
FROM `User` AS `Extent1`) AS `Distinct1`) AS `Apply1`} 

Domanda: C'è un modo per risolvere il suo più efficiente? I sottoselezionamenti sono costosi e EF ne genera uno per colonna. Uso mySQL .NET Connector versione 6.9.5.0

+0

Non può riprodurre questo per qualche motivo, per me è tutto fatto in una query –

+0

@AlexanderDerck stai usando MySQL? Utilizzo .NET Connector versione 6.9.5.0 – fubo

+1

Un altro caso di un framework che aggiunge complessità?

risposta

3

Usando del answer sulla distinta Jon Skeet ..

public static IEnumerable<TSource> DistinctBy<TSource, TKey> 
    (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) 
{ 
    HashSet<TKey> seenKeys = new HashSet<TKey>(); 
    foreach (TSource element in source) 
    { 
     if (seenKeys.Add(keySelector(element))) 
     { 
      yield return element; 
     } 
    } 
} 

si può provare:

using (ED.NWEntities ctx = new ED.NWEntities()) 
{ 
    IQueryable<ED.User> Result = ctx.User.OrderBy(y => y.BirthDate) 
            .DistinctBy(z => z.FirstName) 
            .AsQueryable(); 
} 
+3

ma questo recupera l'intera tabella dal database e tratta successivamente i dati? – fubo

+0

Recuperare prima i dati flat e quindi eseguire il raggruppamento in memoria potrebbe essere un approccio valido per superare la tendenza di MySqls a soffocare le sotto-query. –

1

Si stanno prima raggruppando e quindi ordinando ogni subquery. Certo, sarebbe lento.

Provare ad ordinare la tabella prima, quindi è necessario farlo una sola volta. E poi raggrupparli e prendere il primo.

IQueryable<ED.User> Result = ctx.User 
    .OrderBy(x => x.BirthDate) 
    .GroupBy(x => x.FirstName, (k,g) => g.FirstOrDefault()) 
    .AsQueryable(); 
+0

Il tuo approccio si traduce anche in 4 sottoselezioni - nessun miglioramento – fubo

2

si può provare a fare qualcosa di più si avvicina il modo in cui si farebbe in SQL (senza una funzione "row_number like") ... e vedere cosa viene generato.

var maxAges = ctx.User.GroupBy(x => x.FirstName) 
         .Select(g => new { 
         firstName = g.Key, 
         maxAge = g.Min(x => x.BirthDate) 
         }); 
var result = from u in ctx.User 
      join a in maxAges on new{f = u.FirstName, b =u.BirthDate} equals new{f = a.firstName, b =a.maxAge} 
      select u; 

(miscelazione sintassi fluente e di query, come trovo sintassi di query più chiara per join, ma ... questo è solo un punto di vista personale)

+0

C'è un modo per far partecipare l'ID? Ciò eviterebbe valori non validi nel caso in cui 'BirthDate' +' FirstName' sono duplicati – fubo

+0

Sì, è possibile includere * ID *, potremmo fare un groupby per * FirstName + Birthdate + ID * – bit

+0

@fubo Ne prenderesti uno a caso in questo caso ? Puoi fare la tua prima query nel risultato enumerato (dato che non dovresti avere troppi risultati di db), ma non è bello (ma ... è l'ottimizzazione) –

0

Guardando a questo, i tuoi previous e alcune altre questioni (come this), sembra che usare EF con MySQL sia un dolore.

Si può finalmente provare questo query LINQ

var query = db.User.Where(user => !db.User.Any(
    u => u.UserID != user.UserID && u.FirstName == user.FirstName && 
    (u.BirthDate < user.BirthDate || (u.BirthDate == user.BirthDate && u.UserID < user.UserID)))); 

che genera questa semplice query SQL

SELECT 
`Extent1`.`UserID`, 
`Extent1`.`FirstName`, 
`Extent1`.`LastName`, 
`Extent1`.`BirthDate` 
FROM `Users` AS `Extent1` 
WHERE NOT EXISTS(SELECT 
1 AS `C1` 
FROM `Users` AS `Extent2` 
WHERE ((`Extent2`.`UserID` != `Extent1`.`UserID`) AND (`Extent2`.`FirstName` = `Extent1`.`FirstName`)) AND ((`Extent2`.`BirthDate` < `Extent1`.`BirthDate`) OR ((`Extent2`.`BirthDate` = `Extent1`.`BirthDate`) AND (`Extent2`.`UserID` < `Extent1`.`UserID`)))) 

anche se non sono sicuro di quello che sarà l'impatto sulle prestazioni.

+0

questo è lento come la query dal mio quesiton. probabilmente ho bisogno di aggiungere alcuni indici – fubo

+0

abbastanza giusto. Ho avuto questa sensazione. Richiedere 'FirstName' e creare indici su di esso potrebbe essere d'aiuto. Oltre a questo, non vedo alcun costrutto di query fattibile, probabilmente dovresti prendere in considerazione gli approcci alla memoria. –

0

Avrete bisogno di indici e questo non garantisce le migliori prestazioni perché la query generata da EF sarà molto probabilmente una sottoquery nidificata di grandi dimensioni.

se la prestazione è ancora un problema, è possibile restituire l'ID utente del più vecchio per ogni gruppo ed eseguire un'altra query per ottenere l'oggetto Utente.

Caso peggiore, utilizzare inline sql, una vista o stored proc.

poiché non uso Mysql e non so quali indici avete, lascerò questo compito per voi.

var oldestUsers = (from u in users 
         group u by u.FirstName into grp 
         select new { 
          grp.Key, 
          oldestUser = (from u in grp 
             orderby u.BirthDate descending 
             select u).First() 
         }).ToList(); 

    foreach (var u in oldestUsers) 
    { 
     Console.WriteLine("{0} {1:D}", u.oldestUser.FirstName, u.oldestUser.BirthDate); 
    } 
1

Sono abbastanza sicuro che quando si utilizza MySQL si è in grado di creare una clausola GROUP BY che si differenzia per la sua dichiarazione SELECT. Con altre parole, le righe che stai selezionando non devono far parte della funzione di aggregazione. Quindi una query come questa dovrebbe funzionare:

SELECT 
     FirstName 
     ,LastName 
     ,BirthDate 
    FROM Users 
    GROUP BY FirstName 
    ORDER BY BirthDate 

Si prega di provare questo nel browser di query mySQL. Che è possibile utilizzare questa query direttamente con voi entità contesto quadro come questo:

string query = ".."; // the query above 

var res = context.Database.SqlQuery<Users>(query).ToList(); 
Problemi correlati