How to increate the performance when using group in LINQ - c#

Database
Cars | CarDetails | Owners
----------------------------------
Id | CarDetailId | Id
Name | CarId | CarId
Type | CarId | OwnerName
| | PhoneNumber
LINQ Code
var intiQuery = from c in Cars
join cd in CarDetails
join o in Owners
select new { c,cd,o}
var results = from qry in intiQuery
group new { qry.c, qry.cd, qry.o} by qry.c.Id into g
select new
select new { CarId= g.Key,
Name = g.Select(g=>g.c.Name).FirstOrDefault(),
Type = g.Select(g=>g.c.Type).FirstOrDefault(),
Price= g.Select(g=>g.cd.Price).FirstOrDefault(),
OwnerName= g.Select(g=>g.o.OwnerName).FirstOrDefault(),
PhoneNumber= g.Select(g=>g.o.PhoneNumber).FirstOrDefault(),
}
My question is simply how to increase the performance when calling this query as you can see for each field, I need to.Select().FirstOrDefault() to get the corresponding data. If let's says I got 100 data I will need to get the data one by one 500 times it will take ages to display the data.
Extra Info in case someone not clear.
Cars
Id |Name |Type
-----------------------------------
1 |Toyota |Family
CarDetails
CarDetailId | CarId | Price
-----------------------------------
1 | 1 | 200000
Owners
Id| CarId | OwnerName | PhoneNumber
-----------------------------------
1 | 1 | Mitch | 48774800
2 | 1 | Camilo | 87404078
The result I wanted to get is something like this hope some of you can have a clearer picture
CarId| Name | Type | Price |OwnerName |PhoneNumber
----------------------------------------------------------------------
1 | Toyota | Family | 200000 | Mitch,Camilo | 48774800,87404078

This is absolutely not the best solution, but it will certainly be easy to understand and if you are not filtering the data then it may be good enough.
As you state, the performance problem is because your hitting your database hundreds of time, and so we can easily avoid that by simply pulling the information from the database in three simple queries.
var allCars = Cars.ToList();
var allCarDetails = CarDetails.ToList();
var allOwners= Owners.ToList();
Once you have all this information in memory, you can manipulate the in-memory objects to produce the results you need.
var results = (from car in allCars
let owners = allOwners.Where(a => a.CarID == car.Id)
select new
{
CarID = car.Id,
car.Name,
car.Type,
Price = allCarDetails.Where(a => a.CarID == car.Id).Select(a => a.Price).SingleOrDefault(),
OwnerName = String.Join(',', owners.Select(a => a.Name)),
PhoneNumber = String.Join(',', owners.Select(a => a.PhoneNumber))
}
);
If your tables have a lot of extra fields which you have not mentioned, then we may want to change the initial three queries to just pull the information required.

Related

Column getting lost in LINQ with Method Syntax after group by

I'm pretty new to LINQ and trying to figure it out. I have the following statement:
Context.dataset1
.Join(
Context.dataset2,
r => r.ID, o => o.ID,
(r, o) => new { PartID = r.PartID, Quantity = r.Quantity1 - r.Quantity2, Date = o.Date })
.GroupBy(
column => new { column.Date },
(key, group) => new {Date = key.Date, Quantity = group.Sum(g => g.Quantity) })
.Where(x => x.Quantity > 0);
the return data set looks like this
| Date | Quantity |
| ------------- | ---------|
| 2022-01-01 | 333 |
| 2022-01-02 | 444 |
| 2022-03-03 | 444 |
what i want it to look like is
| PartID | Date | Quantity |
|--------| ------------- | ---------|
|1 | 2022-01-01 | 333 |
|1 | 2022-01-02 | 444 |
|2 | 2022-03-03 | 444 |
Basically it seems that when I do the groupby I lose access to the PartId column since i'm no specifying it inside the groupby. I'm not sure how to make it appear without grouping by it which I don't want to do.
Any help would be great. Thanks.
What if two different part ids exist for the same date? What part id would it show? If you really want the part id, then you need to include the part id in your group by. For example:
column => new { column.PartID, column.Date }
This will mean that if you have multiple part ids for the same date, you will have as many rows for that date as you have distinct part ids. Based on your comments, this seems like what you're after.

Filter LINQ To Entities (EF Core) query by List of HashSet of String and Enum

I have this Linq to Entities (EF Core) query which looks like below
var query = (from p in db.Samples
join q in db.Items on p.Id equals q.SampleId
Where p.active = IsActive and p.Id = GivenId
group new
{
p.Name,
p.Address,
p.Marks,
p.LocationId,
q.EmailId,
q.Grade
}
by new
{ q.Grade }
into data
select new DataSummary()
{
UserName = data.Name,
Grade = data.Min(x => x.Grade),
Email = data.Min(x => x.Email,
Total = data.Sum(x => x.Marks)
}.ToList()
Now I have a constant List of Hashset of Grades and Location that looks like this:
public List<(HashSet<string> Grades, HashSet<Location> Loctions)> LocationGrades => new()
{
(new() { "A", "B" }, new()), // Includes all location
(new() { "C"}, new(){
Location.Boston, //Location is Enum
Location.Maine
}
}
I want to get the data where if the student has grade A or B include all location and if the student has grade C only include Boston and Maine.
Is it possible to integrate this within the LINQ to Entities query?
Sample Table
| ID | Name | Address | Marks | LocationId |
|-----|-------|---------|-------|-------------|
| 234 | Test | 123 St | 240 | 3 (Maine) |
| 122 | Test1 | 234 St | 300 | 5 (Texas) |
| 142 | Test1 | 234 St | 390 | 1 (Boston) |
Items Table
| ID | SampelId | Grade | Email |
|----|----------|-------|-------|
| 12 | 234 | A | a.com |
| 13 | 122 | C | b.com |
| 14 | 142 | C | c.com |
So, In the table above I shouldn't get Texas row but get Boston row as they both have Grade C but Texas does not exist in the HashSet combo.
Okay, now I got it. You have to add dynamic ORed constraints to the query based on a given list of elements. This is a little tricky, because AND can be done with using multiple .Where() statements, but OR not. I did something similar recently against CosmosDB by using LinqKit and the same should also work against EF.
In your case you probably of to do something like this:
...
into data
.WhereAny(grades, (item, grade) => item.Grade == grade)
select new DataSummary()
...
I think the given example doesn't match your exact case, but it allows you to define multiple ORed constraints from a given list and I think this is the missing part you're searching. Take care to use within the lambda method only definitions which are also supported by EF core. The given inner enumeration (in this example grades) will be iterated on the client side and can be dynamically build with everything available in C#.

How to select 'product' rows instead of 'sale' rows

In my LINQ query below I want to select the 'product' rows and add the 'sale' rows data into it but the opposite is happening, it's selecting the 'sale' rows and adding the 'product' rows
var query = (from product in SampleProductTable
from sale in SampleSalesTable
where (sale.ProductId == product.Id)
select new
{
Id = product.Id,
TotalSales = product.TotalSales + ((product.Id == sale.ProductId) ? sale.Amount : 0)
})
Sample Product Table
+-------+------------+---------+-----------------+-------+------------+
| Id | CategoryId | BrandId | Name | Price | TotalSales |
+-------+------------+---------+-----------------+-------+------------+
| mlk3 | MLK | BRND1 | Creamy Milk | 5 | 10 |
| snck2 | SNCK | BRND2 | Chocolate Snack | 2 | 24 |
+-------+------------+---------+-----------------+-------+------------+
Sample Sales Table
+-----+-----------+--------+
| Id | ProductId | Amount |
+-----+-----------+--------+
| 120 | mlk3 | 55 |
| 121 | mlk3 | 15 |
| 122 | snck2 | 12 |
| 123 | mlk3 | 5 |
| 124 | mlk3 | 10 |
| 125 | snck2 | 2 |
| 126 | mlk3 | 115 |
| 127 | snck2 | 6 |
| 128 | snck2 | 34 |
+-----+-----------+--------+
Desired Output
+-------+------------+
| Id | TotalSales |
+-------+------------+
| mlk3 | 210 |
| snck2 | 78 |
+-------+------------+
var answer = (from product in SampleProductTable
join sale in SampleSalesTable on product.Id == sale.ProductId into subSales
from subSale in subSales.DefaultIfEmpty()
group subSale by new { product.Id, product.TotalSales } into gr
select new
{
gr.Key.Id,
TotalSales = gr.Sum(x => x == null ? 0 : x.Amount) + gr.Key.TotalSales
}).ToList();
Approximate T-SQL:
select p.Id, p.TotalSales + sum(coalesce(s.Amount, 0)) TotalSales
from SampleProductTable p
left outer join SampleSalesTable s on p.Id = s.ProductId
group by p.Id, p.TotalSales
In your example you join two collections the way that the result will have as many rows as there are distinct child records (sales in this case) and create a new object for each record (similar to INNER JOIN). That's why the result is "sales-based".
If I understand your intent correctly, I would approach it like:
SampleProductTable.Select(p => new
{
Id = p.Id,
TotalSales = p.Sales.Sum(s => s.Amount)
}
please note that for this approach you will need to map "Sales" collection on a product.
First of all it would be better to use the join statement instead, and then, it seems you need to group your Sales tables based on the ProductId:
var query = (from product in SampleProductTable
join sale in SampleSalesTable.GroupBy(c => c.ProductId)
on product.Id equals sale.Key
select new
{
Id = product.Id,
TotalSales = product.TotalSales + sale.Sum(c=>c.Amount)
}).ToList();
Also please note: since you used a where statement in your code, you don't need to use this condition (product.Id == sale.ProductId) ? in your select anymore. Same as mine, because I used the join statement with on keyword, there is no need to use the condition in the select area.
You can see your desired result in the following link:
https://dotnetfiddle.net/RFTtrv
In general LINQ terms the query shape you are looking for is called grouped join:
The group join is useful for producing hierarchical data structures. It pairs each element from the first collection with a set of correlated elements from the second collection.
In your case, it will produce a collection of correlated Sales for each Product. Then all you need is to apply aggregate (Sum) inside the final projection (select):
var query =
from product in SampleProductTable
join sale in SampleSalesTable on product.Id equals sale.ProductId into productSales
select new
{
Id = product.Id,
TotalSales = product.TotalSales + productSales.Sum(sale => sale.Amount)
};
But since in some of the comments you mentioned converting to SQL, most likely you are using some ORM like LinqToSQL, EF or EF Core. In such case the things are even simpler. These ORMs support a so called navigation properties which represent the relationships, and when used inside queries are translated to SQL with all the necessary joins, so you don't need to be bothered with such details and can concentrate on the logic needed to produce the desired result.
If that's the case, the Product class would normally have something like
public ICollection<Sale> Sales { get; set; }
and the query in question would be simple Select like this:
var query = db.Products
.Select(product => new
{
Id = product.Id,
TotalSales = product.TotalSales + product.Sales.Sum(sale => sale.Amount)
});
LEFT JOIN with grouping looks like
var query =
from product in SampleProductTable
join sale in SampleSalesTable.GroupBy(c => c.ProductId)
on product.Id equals sale.Key into join1
from lj in join1.DefaultIfEmpty() // left join
select new
{
Id = product.Id,
TotalSales = product.TotalSales + (lj == null ? 0 : lj.Sum(c => c.Amount))
};
Left join may return null, so check the potential group, lj before trying to sum it. For later c# versions null check could be abbreviated to
TotalSales = product.TotalSales + (lj?.Sum(c => c.Amount) ?? 0)
Fiddle
In query syntax, Slava's solution should return with the result you're looking for i.e.
var querySyntax = (from product in SampleProductTable
join sale in SampleSalesTable on product.Id equals sale.ProductId into sales
from subSales in sales.DefaultIfEmpty()
group subSales by new { product.Id, product.TotalSales }
into grp
select new
{
grp.Key.Id,
TotalSales = grp.Sum(s => s.Amount) + grp.Key.TotalSales
}).ToList();
If you have a burning desire to use method syntax for whatever reason, this equivalent LINQ query will also work:
var methodSyntax = (SampleProductTable
.GroupJoin(SampleSalesTable, product => product.Id, sale => sale.ProductId,
(product, sales) => new {product, sales})
.SelectMany(s => s.sales.DefaultIfEmpty(), (s, subSales) => new {s, subSales})
.GroupBy(ss => new {ss.s.product.Id, ss.s.product.TotalSales}, ss => ss.subSales)
.Select(grp => new {grp.Key.Id, TotalSales = grp.Sum(s => s.Amount) + grp.Key.TotalSales})).ToList();

LINQ grouping by nullable child and parent

everyone!
I've just faced a problem with timing out in my LINQ query.
I have 3 tables: Work, Projects and Subprojects.
Projects:
+--------+
| Id |<--+<--+
| Name | | |
+--------+ | |
SubProjects: | |
+--------+ | |
+->| Id | | |
| | Name | | |
| | ProjId |---+ |
| +--------+ |
| Work: |
| +------------+ |
| | Id | |
| | Type | |
| | ProjId |---+
+--| SubProjId | (nullable)
+------------+
I need to create a report based on Subprojects:
Group by subproject Id,
if subproject Id is null -> group by project Id
I've solved it by making two queries and then merging them, but when sometimes it times out.
I was doing it with
result1.AddRange(result2);
because
var temp = result1.Concat(result2);
is throwing an Exception:
Internal .NET Framework Data Provider error 1004, 0, Unresolvable Var used in Command: VarType=Computed, Id=2090.
Can somebody help me with creating it in one query?
I'm not sure what your code looks like so this might not be perfect but you could try something like this:
var result = from work in works
group work by work.SubProjId ?? work.ProjId into groupedWorks
select groupedWorks.ToList();
or
var result = works.GroupBy(work => work.SubProjId ?? work.ProjId).ToList();
try this query
var itemlist =contex.Work.where(x=>x.SubProjId !=null).Groupby(x=>x.SubProjId).Concat(Contex.Work.where(x=>x.SubProjId ==null).Groupby(x=>x.ProjId)).ToList();
I'm guessing this is what you need:
var groups = from work in ctx.Works // the work table
group work // we want to group whole work "rows"
// we are grouping by project id and subproject id
by new { ProjId = work.ProjId, SubProjId = work.SubProjId }
into g // and we are calling the grouping 'g'
select g; // select the group
// example of doing something with the groupings
foreach (var group in groups)
{
var key = group.Key; // gets a { ProjId, SubProjId } tuple
foreach (var work in group)
{
// each work is a row in the Work-table
}
}

How to use LINQ to select from 5 tables?

I have 5 tables:
course_id | course_name (course)
------------------------------
1 | Basic1
2 | Basic2
3 | Basic3
4 | Basic4
5 | Basic5
course_id | trainer_id (course_trainer)
-----------------------------
1 | 1
1 | 2
2 | 2
3 | 2
4 | 3
4 | 2
5 | 3
course_id | topic_id (course_topic)
-----------------------------
1 | 1
1 | 2
2 | 2
3 | 2
4 | 3
4 | 2
5 | 3
trainer_id| trainer_name (trainer)
-----------------------------
1 | Tom
2 | Thomas
3 | Sue
tropic_id | topic_name (topic)
-----------------------------
1 | Skill 1
2 | Skill 2
3 | Skill 3
How can I use LINQ to select with result as below
Course_name | Trainer_name | Topic_name
----------------------------------------------
Basic 1 | Tom, Thomas | Skill 1, Skill 2
Basic 2 | Thomas | Skill 2
Basic 3 | Thomas | Skill 2
Basic 4 | Sue, Thomas | Skill 3, Skill 2
Basic 5 | Sue | Skill 3
That is my code in C#, but the result isn't correct. Please help me, many thanks !
public class course_datatable
{
public string course_name {get; set;}
public string trainer_name {get; set;}
public string topic_name {get; set;}
}
IQueryable<course_datatable> coursequery =
from c in db.course
join ct in db.course_trainer on c.course_id equals ct.course_id
join t in db.trainers on ct.trainer_id equals t.trainer_id
join ctopic in db.course_topic on c.course_id equals ctopic.course_id
join topic in db.topic on ctopic.topic_id equals topic.topic_id
select new course_datatable()
{
course_name = c.course_name,
trainer = t.trainer_name,
topic = topic.topic_name
};
Get your data from your database:
var result = context.Courses.Select(c =>
new { Course = c, Trainers = c.Trainers, Skills = c.Skills }).ToList();
and then flatten the Trainers and Skills objects using String.Join:
result.Select(r => new
{
Course = r.Course.Course_Name,
Trainer = String.Join(",", r.Trainers.Select(t => t.TrainerName).ToArray()),
Skill = String.Join(",", r.Skills.Select(S => S.SkillName).ToArray())
});
edit
Using your schema, I'll rename so that it should work.
var result = db.course.Select(c => new
{
Course = c,
Trainers = c.course_trainer.trainers,
Skills = c.course_topic.topic
}).ToList();
result.Select(r => new
{
Course = r.Course.course_Name,
Trainer = String.Join(",", r.Trainers.Select(t => t.trainer_name).ToArray()),
Skill = String.Join(",", r.Skills.Select(S => S.topic_name).ToArray())
});
You can do this all in one statement but I've structured it this way so that it's hopefully clearer for you.
Because you seem unable to use my initial answer (which is preferred because doesn't require redundant join conditions), I'll work with your existing code and show you how to group and project.
Starting with this:
var coursequery =
from c in db.course
join ct in db.course_trainer on c.course_id equals ct.course_id
join t in db.trainers on ct.trainer_id equals t.trainer_id
join ctopic in db.course_topic on c.course_id equals ctopic.course_id
join topic in db.topic on ctopic.topic_id equals topic.topic_id
select new course_datatable()
{
course_name = c.course_name,
trainer = t.trainer_name,
topic = topic.topic_name
};
You then want to GroupBy the course_name
var groups = coursequery.GroupBy(item => item.course_name);
and then each group needs to project into your new result type
var result = groups.Select(group =>
new course_datatable
{
course_name = group.Key,
trainer_name = String.Join(",", group.Select(i=> i.trainer_name).ToArray()),
topic_name = String.Join(",", group.Select(i => i.topic_name).ToArray()),
}).ToList();
or if you want to try something else (for fun) use LINQs Aggregate method, rarely used:
var result = groups.Select(group =>
group.Aggregate((initial, next) =>
{
initial.topic_name += String.Format(", {0}", next.topic_name);
initial.trainer_name += String.Format(", {0}", next.trainer_name);
return initial;
})).ToList();

Categories

Resources