Verschachteltes Grouping - LINQ

  • C#

Es gibt 5 Antworten in diesem Thema. Der letzte Beitrag () ist von LaMiy.

    Verschachteltes Grouping - LINQ

    Kann mir irgendein LINQ-Spezi folgende Anforderung in eine LINQ-Folge bringen? :)
    Selbst komme ich einfach nicht über die einfache Gruppierung hinaus.

    Ausgangsliste
    IDCategoryNamePrice
    11Test1
    11Test1
    12Test3
    21Abc5
    23Abc4

    Liste nach Umformung.
    IDCategoryNamePrice
    1
    Test5

    1Test2

    2Test3
    2
    Abc9

    1Abc4

    3Abc5


    Ich möchte also zunächst nach der ID gruppieren.
    Danach möchte ich nach der Kategorie gruppieren und dabei die Werte wie Preis o.ä zusammenrechnen.
    Außerdem soll in der ersten Gruppe der Preis aller Untergruppen auftauchen.

    Ich hoffe, dass das ungefähr klar geworden ist. Sonst einfach fragen :)

    Hier vielleicht noch die Klassen, so wie ich mir das vorstelle.
    Code

    C#-Quellcode

    1. public class Item
    2. {
    3. public int ID { get; set; }
    4. public int Category { get; set; }
    5. public string Name { get; set; }
    6. public float Price { get; set; }
    7. }
    8. public class GroupedItem
    9. {
    10. public int ID { get; set; }
    11. public string Name { get; set; }
    12. public float Price { get; set; } // Gesamtpreis
    13. public IEnumerable<Item> Childs { get; set; } // Alle gruppierten Kategoerien
    14. }

    C#-Quellcode

    1. // Demo Elemente
    2. IEnumerable<Item> items = new List<Item>()
    3. {
    4. new Item() {ID = 1, Category = 1, Name = "Test", Price = 1.0f},
    5. new Item() {ID = 1, Category = 1, Name = "Test", Price = 2.0f},
    6. new Item() {ID = 1, Category = 2, Name = "Test", Price = 2.0f},
    7. new Item() {ID = 1, Category = 2, Name = "Test", Price = 2.0f},
    8. new Item() {ID = 2, Category = 1, Name = "Abc", Price = 3.0f},
    9. new Item() {ID = 2, Category = 1, Name = "Abc", Price = 4.0f},
    10. new Item() {ID = 2, Category = 3, Name = "Abc", Price = 2.0f},
    11. };
    12. //TODO: Magic LINQ
    13. /**
    14. * Ergebnis
    15. GroupedItem
    16. ID = 1
    17. Name = Test
    18. Price = 7.0f
    19. Child = {
    20. {
    21. ID = 1
    22. Category = 1
    23. Name = Test
    24. Price = 3.0f
    25. }
    26. {
    27. ID = 1
    28. Category = 2
    29. Name = Test
    30. Price = 4.0f
    31. }
    32. }
    33. GroupItem
    34. ID = 2
    35. Name = Abc
    36. Price = 9.0f
    37. Child = {
    38. {
    39. ID = 2
    40. Category = 1
    41. Name = Abc
    42. Price = 7.0f
    43. }
    44. {
    45. ID = 2
    46. Category = 3
    47. Name = Abc
    48. Price = 2.0f
    49. }
    50. }
    51. */


    Ich habe auch mal ein Testprojekt mit einem Wunschergebnis angehangen.
    Dateien
    • ItemTest.7z

      (9,38 kB, 134 mal heruntergeladen, zuletzt: )
    Ich habe es geschafft das ganze in Zwei seperate Auflistungen zu bekommen.
    Weiß jemand wie ich das in meine beschriebene Klassenstruktur bekomme?

    C#-Quellcode

    1. ​var result = items.GroupBy(ac => new
    2. {
    3. ac.ID,
    4. ac.Category
    5. }).Select(ac => new Item
    6. {
    7. ID = ac.Key.ID,
    8. Category = ac.Key.Category ,
    9. Price = ac.Sum(acs => acs.Price),
    10. Name = ac.First().Name
    11. });
    12. var headers = result.GroupBy(ac => ac.ID).Select(ac => new Item()
    13. {
    14. ID = ac.First().ID,
    15. Price = ac.Sum(elem => elem.Price),
    16. Name = ac.First().Name
    17. });

    C#-Quellcode

    1. IEnumerable<Item> items = new List<Item>()
    2. {
    3. new Item() {ID = 1, Category = 1, Name = "Test", Price = 1.0f},
    4. new Item() {ID = 1, Category = 1, Name = "Test", Price = 2.0f},
    5. new Item() {ID = 1, Category = 2, Name = "Test", Price = 2.0f},
    6. new Item() {ID = 1, Category = 2, Name = "Test", Price = 2.0f},
    7. new Item() {ID = 2, Category = 1, Name = "Abc", Price = 3.0f},
    8. new Item() {ID = 2, Category = 1, Name = "Abc", Price = 4.0f},
    9. new Item() {ID = 2, Category = 3, Name = "Abc", Price = 2.0f},
    10. };
    11. var groups = new List<GroupedItem>();
    12. foreach (var grID in from itm in items group itm by itm.ID) {
    13. var cats = new List<Item>();
    14. foreach (var grCat in from itm in grID group itm by itm.Category) {
    15. var cat = grCat.First();
    16. cat.Price = grCat.Sum(c => c.Price);
    17. cats.Add(cat);
    18. }
    19. var gi = new GroupedItem() { ID = grID.Key, Name = grID.First().Name, Childs = cats, Price = cats.Sum(itm => itm.Price) };
    20. groups.Add(gi);
    21. }
    Ich musste leider eine Property aus einer anderen Datenquelle nehmen.
    Der entstandene Code ist nun extrem langsam.
    Die Daten aus der anderen Liste sind Anzahl an Verkäufen.

    Weiß da jemand wie man den noch optimieren könnte? Selbst kleine Verbesserungen würden schon Auswirkungen haben.
    Parallele Schleifen will ich eigentlich erstmal nicht, da ich damit irgendwie Probleme bei der Ausführung hatte.

    Spoiler anzeigen

    C#-Quellcode

    1. /// <summary>
    2. /// Wandelt eine Liste von Items in eine gruppierte Liste von Items und Kategoerien um.
    3. /// Ein GroupedItem representiert hier eine Zeile in der Tabelle mit den Produkten.
    4. /// </summary>
    5. /// <param name="items">Liste von Items</param>
    6. /// <returns>Die gruppierte Liste</returns>
    7. public List<GroupedItem> Convert(List<Item> items, List<Item> booking)
    8. {
    9. var groups = new List<GroupedItem>();
    10. // Hier die Daten aus einer anderen Quelle nehmen (Qty ..)
    11. var groupedBookings = booking.GroupBy(c => new { c.Asin, c.Category.ID });
    12. // Nach ID gruppieren
    13. foreach (var grID in from itm in items group itm by itm.Asin)
    14. {
    15. var cats = new List<Item>();
    16. // Nach Category ID gruppieren
    17. foreach (var grCat in from itm in grID group itm by itm.Category.ID)
    18. {
    19. // Hier die Zuordnung zwischen den beiden Listen finden
    20. var groupedWhere = groupedBookings.Where(c => c.Key.Asin == grID.Key).Where(c => c.Key.ID == grCat.Key);
    21. var firstCat = grCat.First();
    22. var cat = new Item()
    23. {
    24. Category = firstCat.Category,
    25. Asin = firstCat.Asin,
    26. Tag = firstCat.Tag,
    27. Title = firstCat.Title,
    28. Seller = firstCat.Seller
    29. };
    30. cat.Price = grCat.Sum(c => c.Price);
    31. cat.Qty = groupedWhere.Sum(elem => elem.Sum(itm => itm.Qty));
    32. //cat.Qty = grCat.Sum(c => c.Qty);
    33. cat.Rate = grCat.Average(c => c.Rate);
    34. cat.Earnings = grCat.Sum(c => c.Earnings);
    35. cat.AlertFlag = grCat.Any(c => c.AlertFlag); //
    36. cat.DQty = groupedWhere.Sum(elem => elem.Sum(itm => itm.DQty));
    37. cat.NQty = groupedWhere.Sum(elem => elem.Sum(itm => itm.NQty));
    38. cat.Clicks = groupedWhere.Sum(elem => elem.Sum(itm => itm.Clicks));
    39. //cat.DQty = grCat.Sum(c => c.DQty);
    40. //cat.NQty = grCat.Sum(c => c.NQty);
    41. cats.Add(cat);
    42. }
    43. var gi = new GroupedItem()
    44. {
    45. Asin = grID.Key,
    46. Tag = grID.First().Tag,
    47. Category = cats.Count > 1 ? Category.Empty : grID.First().Category, // Leere Kategorie anzeigen, wenn es mehrere Childs gibt
    48. Title = grID.First().Title,
    49. Seller = grID.First().Seller,
    50. Childs = cats.Count > 1 ? cats : new List<Item>(), // Prüfen ob mehr als eine Kategorie gefunden wurde
    51. Price = cats.Sum(itm => itm.Price),
    52. Earnings = cats.Sum(itm => itm.Earnings),
    53. Qty = cats.Sum(c => c.Qty),
    54. DQty = cats.Sum(c => c.DQty),
    55. NQty = cats.Sum(c => c.NQty),
    56. AlertFlag = cats.Any(c => c.AlertFlag),
    57. Clicks = cats.Sum(c => c.Clicks),
    58. Rate = cats.Average(c => c.Rate)
    59. };
    60. gi.Conversion = Math.Round((double)gi.Clicks/gi.DQty, 2); // Conversion berechnen
    61. groups.Add(gi);
    62. }
    63. return groups;
    64. }
    Ich musste jetzt leider performancebedingt auf SQL umsteigen. Hatte ich leider vorher so nicht kommen gesehen.
    Ich bin im Moment dabei die Gruppierung in SQL zu machen.
    Das gestaltet sich etwas schwierig, da ich SQL auch selbst nicht so gut kann.
    Ich weiß, dass das schwierig ist sich in den komplizierten LINQ Code (von oben) reinzudenken, aber ich versuche mal zu erklären wie das funktionieren soll.



    In der linken Tabelle (A) sind generelle Infos über Produkte. In der rechten Tabelle (B) sind Infos über die Verkäufe der Produkte.
    Leider kann man diese nicht 1:1 mappen und in eine Tabelle packen. (In dem Beispiel ginge das, aber in der Praxis nicht)

    Die Produkte aus A sollen gruppiert werden. (Das sind im Code die ersten beiden Schleifen)
    Das hab ich in SQL auch schon.

    Dann sollen die Produkte aus B auch gruppiert werden. (var groupedBookings = booking.GroupBy(c => new { c.Asin, c.Category.ID });)
    Und jetzt soll die Summe der Anzahlen aus Tabelle B genommen werden und schauen wo sie bei A hinpasst.
    In der Abbildung ist das genau dieses 2+3.

    Bisher habe ich diesen SQL Code.

    SQL-Abfrage

    1. SELECT rp.id, rp.asin, rp.category, SUM(brp.qty) FROM reports rp
    2. LEFT JOIN booking_reports brp ON brp.asin = rp.asin AND rp.category = brp.category
    3. GROUP BY rp.asin, rp.category


    Da ist allerdings auch die Gruppierung von Tabelle B nicht drinnen und die Anzahlen stimmen nicht. (Sind viel zu hoch).
    Hat da jemand eine Idee wie ich den Code vom letzten Post in SQL hinbekomme?


    Edit: Hab einen funktionierenden Code. Ich hoffe, dass der von der Performance her auch in Ordnung ist, aber scheint auf jeden Fall deutlich schneller als der alte in LINQ zu sein.

    SQL-Abfrage

    1. ​SELECT rp.id, rp.asin, rp.category, SUM(rp.price) AS price, rp.seller, rp.tag, AVG(rp.rate) AS rate, summe
    2. FROM reports rp,
    3. (SELECT *, SUM(qty) AS summe FROM booking_reports
    4. GROUP by asin, category) brp
    5. WHERE rp.asin = brp.asin AND rp.category = brp.category
    6. GROUP BY rp.asin, rp.category

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „LaMiy“ ()