Inefficient LINQ - Know What SQL Should Be - Can't get there - c#

I know the SQL I want to produce:
SELECT qdt, pbn, cid, pid, SUM(Amount) AS TotalAmount
FROM SomeDb.SomeTable
WHERE Status = 'Open'
GROUP BY cid, pid, qdt, pbn
ORDER BY qdt
I have LINQ, that I was hoping would produce something as clean as the above SQL:
var query = (
from someTable in SomeTable
where someTable.Status == "Open"
group someTable by new { someTable.cid, someTable.pid } into grouped
select new
{
lid = grouped.FirstOrDefault().lid,
qdt = grouped.FirstOrDefault().qdt,
pbn = grouped.FirstOrDefault().pbn,
cid = grouped.FirstOrDefault().cid,
cn = grouped.FirstOrDefault().cn,
pid = grouped.FirstOrDefault().pid,
amount = grouped.Sum(o => o.Amount),
Status = grouped.FirstOrDefault().Status
});
But that produces many lines of nasty SQL. Each grouped value ends up getting its own SELECT statement within the query, like this, for qdt:
SELECT [t5].[qdt]
FROM (
SELECT TOP (1) [t4].[qdt]
FROM [SomeDb].[SomeTable] AS [t4]
WHERE ([t1].[cid] = [t4].[cid]) AND ([t1].[pid] = [t4].[pid]) AND ([t4].[Status] = #p0)
) AS [t5]
) AS [qdt]
Is there a way to change the LINQ to produce the simpler SQL?

There are two issues I see with your LINQ attempt. For starters, you are not grouping by the same fields in the SQL query as in your LINQ. Then, you are using FirstOrDefault on the results of the group instead of selecting the group's key data.
Your query should look like this:
var query =
(
from someTable in SomeTable
where someTable.Status == "Open"
group someTable by new
{
someTable.lid,
someTable.qdt,
someTable.pbn,
someTable.cid,
someTable.cn,
someTable.pid,
someTable.Status,
} into grouped
select new
{
lid = grouped.Key.lid,
qdt = grouped.Key.qdt,
pbn = grouped.Key.pbn,
cid = grouped.Key.cid,
cn = grouped.Key.cn,
pid = grouped.Key.pid,
amount = grouped.Sum(o => o.Amount),
Status = grouped.Key.Status
}
);
I cannot test right now whether it will generate the exact same SQL though.

You don't seem to have all the grouped by columns in the desired sql in your linq;
var query = (
from someTable in SomeTable
where someTable.Status == "Open"
group someTable by new { someTable.cid, someTable.pid, someTable.qdt,someTable.pbn, someTable.Status } into grouped
select new
{
qdt = grouped.FirstOrDefault().qdt,
pbn = grouped.FirstOrDefault().pbn,
cid = grouped.FirstOrDefault().cid,
pid = grouped.FirstOrDefault().pid,
Status = grouped.FirstOrDefault().Status,
amount = grouped.Sum(o => o.Amount)
});
I can't really test it but that looks a little more like the sql you are trying to replicate in linq.

Related

How to compare result of two subqueries in where clause SQLKata

I try to compare results of two subqueries in where clause in SQLKata.
In SQL it should be like this:
WHERE (SELECT count(id) FROM main.someTable) = (SELECT count(id) FROM main.anotherTable)
In SQLKata I can compare result of subquery with scalar value:
var mainSubquery = new Query("main.someTable")
.SelectRaw("count(id)");
var anotherSubquery = new Query("main.anotherTable")
.SelectRaw("count(id)");
query
.WhereSub(mainSubquery, "=", 0)
But I can't compare results of two subqueries this way:
query
.WhereSub(mainSubquery, "=", anotherSubquery),
How can I fix it? Maybe I should execute both of the subqueries and only then compare their results?
No overload accepts queries on both the left and right sides together.
But you can always compile the queries and use the WhereRaw. One gotcha is to be aware of the bindings orders.
Take a look on this example:
using SqlKata;
using SqlKata.Compilers;
var sub1 = new Query("A").SelectRaw("count(1)");
var sub2 = new Query("B").SelectRaw("count(1)");
var compiler = new SqlServerCompiler();
var c1 = compiler.Compile(sub1);
var c2 = compiler.Compile(sub2);
var bindings = c1.Bindings;
bindings.AddRange(c2.Bindings); // order (c1, c2) is important here
var query = new Query("Table").WhereRaw($"({c1.Sql}) = ({c2.Sql})", bindings);
var result = compiler.Compile(query);
Console.WriteLine(result.ToString());
This will output the following
SELECT * FROM [Table] WHERE (SELECT count(1) FROM [A]) = (SELECT count(1) FROM [B])

How to write Update logic using LEFT JOIN in LINQ Method Syntax?

In my .Net application, EntityFramework 6 is used and I am writing a logic in C# to update one of the column values of the ORDERTABLE to Yes or No.
In ORDERTABLE, for a Single TranID there will be one or more OrderNumber. I need to update the last OrderNumber (i.e., MAX(OrderNumber)) value of a TranID to 'NO' and for remaining OrderNumber value will be 'YES'.
Below SQL query is giving me the expected result but I am not sure of converting this into LINQ to Entities code logic.
UPDATE SC
SET ORDERVALUE = CASE WHEN SC1.ORDERNUMBER IS NULL THEN 'YES' ELSE 'NO' END
FROM ORDERTABLE SC
LEFT JOIN (SELECT MAX(ORDERNUMBER) ORDERNUMBER, SID FROM ORDERTABLE WHERE STATUS ='A' GROUP BY SID) SC1
ON SC.ORDERNUMBER = SC1.ORDERNUMBER AND SC.SID = SC1.SID
WHERE SC.STATUS ='A' AND SC.SID IN (SELECT ID FROM ORDERMASTER(NOLOCK) WHERE MID = variablename)
Select Query:
SELECT
ORDERVALUE = CASE WHEN SC1.ORDERNUMBER IS NULL THEN 'YES' ELSE 'NO' END,
SC.*
FROM ORDERTABLE SC
LEFT JOIN (SELECT MAX(ORDERNUMBER) ORDERNUMBER, SID FROM ORDERTABLE WHERE STATUS ='A' GROUP BY SID) SC1
ON SC.ORDERNUMBER = SC1.ORDERNUMBER AND SC.SID = SC1.SID
WHERE SC.STATUS ='A' AND SC.SID IN (SELECT ID FROM ORDERMASTER(NOLOCK) WHERE MID = variablename)
In C#, LINQ to Entities code should looks like somewhat similar to below first format (LINQ Method) but not like the second one(LINQ query).
//1. LINQ Method
using (var context = new ProductDBEntities())
{
dbContextTransaction = context.Database.BeginTransaction();
ORDERDETAILS od = context.ORDERDETAILS.Single(G => G.ID == 1);
od.OrderNumber = OrdNumber;
od.LastModifiedBy = createdBy;
od.LastModifiedTS = DateTime.UtcNow;
context.SaveChanges();
}
//2. LINQ Query
using (var context = new ProductDBEntities())
{
var query = from st in context.ORDERTABLE
where ...
select st;
var ORDERTABLE = query.FirstOrDefault<ORDERTABLE >();
}

SQL Unions with table counts using EntityFramework LINQ query

I am trying replicate the SQL below using LINQ and Entity Framework and cannot figure out how this should be written.
My simplistic LINQ version does a query per table
public IActionResult Index()
{
dynamic view = new ExpandoObject();
view.AppUsers = Context.AppUsers.Count();
view.CustomerShops = Context.CustomerShops.Count();
view.FavouriteOrders = Context.FavouriteOrders.Count();
view.Items = Context.Items.Count();
view.ItemVariations = Context.ItemVariations.Count();
view.MenuCategories = Context.MenuCategories.Count();
view.MenuCategoryProducts = Context.MenuCategoryProducts.Count();
view.Orders = Context.Orders.Count();
view.Products = Context.Products.Count();
view.ProductVariations = Context.ProductVariations.Count();
view.Shops = Context.Shops.Count();
view.Staffs = Context.Staffs.Count();
return View(view);
}
I use this pattern from time to time to for reporting on my column counts and thought this should be easy to do in LINQ, but no luck so far.
This pure SQL UNION would only generate 1 SQL request, instead of a request per table.
select * from (
select 'asp_net_roles' as type, count(*) from asp_net_roles
union
select 'asp_net_user_roles' as type, count(*) from asp_net_user_roles
union
select 'asp_net_users' as type, count(*) from asp_net_users
union
select 'app_users' as type, count(*) from app_users
union
select 'shops' as type, count(*) from shops
union
select 'staffs' as type, count(*) from shops
union
select 'items' as type, count(*) from items
union
select 'item_variations' as type, count(*) from item_variations
union
select 'products' as type, count(*) from products
union
select 'product_variations' as type, count(*) from product_variations
union
select 'menu_categories' as type, count(*) from menu_categories
) as counters
order by 1;
I saw a partial implementation [linq-group-by-multiple-tables] (https://stackoverflow.com/a/3435503/473923) but this is based of grouping data.
FYI: I'm new to C#/Linq, so sorry if this seams obvious.
Use the this code from my answer
And fill ExpandoObject with result:
var tablesinfo = Context.GetTablesInfo();
var expando = new ExpandoObject();
if (tablesinfo != null)
{
var dic = (IDictionary<string, object>)expando;
foreach(var info in tablesinfo)
{
dic.Add(info.TableName, info.RecordCount);
}
}
Idea is that you can UNION counts if you group entities by constant.
Schematically function builds the following IQueryable Expression:
var tablesinfo =
Context.AppUsers.GroupBy(x => 1).Select(g => new TableInfo{ TableName = "asp_net_roles", RecordCount = g.Count() })
.Concat(Context.MenuCategories.GroupBy(x => 1).Select(g => new TableInfo{ TableName = "menu_categories", RecordCount = g.Count() }))
.Concat(Context.Items.GroupBy(x => 1).Select(g => new TableInfo{ TableName = "items", RecordCount = g.Count() }))
....
There is nothing wrong with your LINQ query. It's very acceptable approach. However it's not the most efficient.
There is no need to fetch count from individual tables one by one. You can get the counts from all the tables at once using the System tables Sys.Objects and Sys.Partitions. Just try running this query in your database.
SELECT A.Name AS TableName, SUM(B.rows) AS RecordCount
FROM sys.objects A INNER JOIN sys.partitions B
ON A.object_id = B.object_id
WHERE A.type = 'U' AND B.index_id IN (0, 1)
GROUP BY A.Name
For quick response and cleaner code, you can store this SQL query in a string variable, and run the LINQ
var result = dataContext.ExecuteQuery<YOUR_MODEL_CLASS>
(your_string_query);
I would put something like this:
Dictionary<string, int> view = new() {
new() {'asp_net_roles', Context.AppUsers.Count() },
...
}
return View(view);
maybe not the most pure way, but does the job (unless I misunderstood what you try to accomplish)

2 parameterized where clauses in a single query (not 'where _ and _')

Can parameterizing 2 different where clauses (non contiguous) in a single query be done in dapper?
Sample SQL query as C# property
private string GetSalesRepToCustomer => #"
WITH SALES_REP_FILTERED AS (
SELECT
SALES_REP_ID
FROM
SALES_REP
/**where**/ /* 1st where clause. */
)
SELECT
A.SALES_REP_ID,
B.CUSTOMER_ID
FROM
SALES_REP_FILTERED A LEFT JOIN
CUSTOMER B ON A.SALES_REP_ID = B.SALES_REP_ID
/**where**/ /* 2nd where clause */
";
C# parameterization not working, illustration purposes.
SqlTemplate queryTemplate = new SqlBuilder()
.where("SALES_REP_ID = :SALES_REP_ID")
// this 2nd 'where' is illustrative.
// In actual an 'and' clause is created.
// My goal is to have a secondary/non contiguous 'where' clause in the same query.
.where("CUSTOMER_ID IN :CUSTOMER_ID")
.AddTemplate(GetSalesRepToCustomer);
var conn = new OracleConnection();
var queryResults = conn.Query(
queryTemplate.RawSql,
new { SALES_REP_ID = 2021, CUSTOMER_ID = new int[] {11, 22, 33}}
);
Context and reasoning of what I want to achieve:
Read online (Cant recall where. Probably on stackoverflow) the incentive(s) for dapper query parameterization and would like to stick/follow to this recommended practice.
I could split the query up into 2 and make separate round trips to the DB. But there are performance and IO payoffs/benefits when executed as a single query.
My last option is to in line the where clauses in SQL. But this probably would negate the benefits of dapper parameterization? I.e.
private string GetSalesRepToCustomer => #"
WITH SALES_REP_FILTERED AS (
SELECT
SALES_REP_ID
FROM
SALES_REP
where SALES_REP_ID = :SALES_REP_ID
)
SELECT
A.SALES_REP_ID,
B.CUSTOMER_ID
FROM
SALES_REP_FILTERED A LEFT JOIN
CUSTOMER B ON A.SALES_REP_ID = B.SALES_REP_ID
WHERE CUSTOMER_ID IN :CUSTOMER_ID
";
And the where parameterization is taken out.
SqlTemplate queryTemplate = new SqlBuilder()
// No parameterized where clauses. So also no dapper performance improvements?
.AddTemplate(GetSalesRepToCustomer);
var conn = new OracleConnection();
var queryResults = conn.Query(
queryTemplate.RawSql,
new { SALES_REP_ID = 2021, CUSTOMER_ID = new int[] {11, 22, 33}}
);
As I understand it, this should be fine:
var conn = new OracleConnection();
var sql = #"
WITH SALES_REP_FILTERED AS (
SELECT
SALES_REP_ID
FROM
SALES_REP
where SALES_REP_ID = :SALES_REP_ID
)
SELECT
A.SALES_REP_ID,
B.CUSTOMER_ID
FROM
SALES_REP_FILTERED A LEFT JOIN
CUSTOMER B ON A.SALES_REP_ID = B.SALES_REP_ID
WHERE CUSTOMER_ID IN :CUSTOMER_ID
";
var queryResults = conn.Query<SOME_TYPE>(
sql,
new { SALES_REP_ID = 2021, CUSTOMER_ID = new int[] {11, 22, 33}}
);
But your SQL could also be written as:
SELECT
A.SALES_REP_ID,
B.CUSTOMER_ID
FROM
SALES_REP SR
LEFT JOIN CUSTOMER C ON SR.SALES_REP_ID = C.SALES_REP_ID
WHERE
C.CUSTOMER_ID IN :CUSTOMER_ID AND
SR.SALES_REP_ID = :SALES_REP_ID
If you pass 3 customer IDs, then dapper will rewrite your WHERE clause to be:
WHERE C.CUSTOMER_ID IN (:CUSTOMER_ID1, :CUSTOMER_ID2, :CUSTOMER_ID3) AND
and it will populate the command's parameters collection with the three parameters populated with the values supplied

Linq equivalent of aggregate function on multiple tables in one database trip

I have a table function which returns table names and number of entries within that table :
CREATE FUNCTION [dbo].[ufnGetLookups] ()
RETURNS
#lookupsWithItemCounts TABLE
(
[Name] VARCHAR(100),
[EntryCount] INT
)
AS
BEGIN
INSERT INTO #lookupsWithItemCounts([Name],[EntryCount])
VALUES
('Table1', (SELECT COUNT(*) FROM Table1)),
('Table2', (SELECT COUNT(*) FROM Table2)),
('Table3', (SELECT COUNT(*) FROM Table))
RETURN;
END
What would be the Linq equivalent of above simple function? Notice that I want to get the result in one single shot and the speed of the operation is quite important for me. If I realise that the converted linq to sql results in a massive bulky sql with performance hit, I would rather stick to my existing user defined function and forget about the linq equivilant.
You can do that with a UNION query. EG
var q = db.Books.GroupBy(g => "Books").Select(g => new { Name = g.Key, EntryCount = g.Count() })
.Union(db.Authors.GroupBy(g => "Authors").Select(g => new { Name = g.Key, EntryCount = g.Count() }));
var r = q.ToList();
Not an EF guy, and not sure if this would be more performant.
Select TableName = o.name
,RowCnt = sum(p.Rows)
From sys.objects as o
Join sys.partitions as p on o.object_id = p.object_id
Where o.type = 'U'
and o.is_ms_shipped = 0x0
and index_id < 2 -- 0:Heap, 1:Clustered
--and o.name in ('Table1','Table2','Table3' ) -- Include (or not) your own filter
Group By o.schema_id,o.name
Note: Wish I could recall the source of this, but I've used it in my discovery process.

Categories

Resources