Split List by grouped Data - c#

I have a list of product records that I am displaying on a dataTable, that are grouped by department name / id. From the image below my returned data does not include the highlighted rows. I am trying to achieve whats shown in the image in that - for each set of products already grouped by department (deserts, stationery, household) get the totals, and name of department (and other columns which I removed to show less clutter) to append to the list
I am trying to achieve the outcome above
This is my current code/query which returns this data without the highlighted rows
List<SaleDetailViewModel> list = new List<SaleDetailViewModel>();
NpgsqlCommand query = new NpgsqlCommand
("SELECT q_department.q_name, q_saledetail.q_code, q_saledetail.q_description, " +
"SUM(q_quantity) AS qtysum, " + //running total of q_quantity for the product in that department
"FROM q_saledetail " +
"LEFT JOIN q_department ON (q_saledetail.q_departmentid = q_department.q_code ) " +
"GROUP BY " +
"q_saledetail.q_departmentid, q_department.q_name, q_saledetail.q_description, q_saledetail.q_code " +
"ORDER BY q_saledetail.q_departmentid "
, connectToDB);
NpgsqlDataReader read = query.ExecuteReader();
while(read.Read())
{
var reportData = new SaleDetailViewModel();
reportData.departmentName = read["q_name"].ToString(); //department name
reportData.q_description = read["q_description"].ToString(); //product DESCRIPTION
reportData.q_code = read["q_code"].ToString(); //product BAR CODE
reportData.sumOfQuantity = Convert.ToDecimal(read["qtysum"]); //sum of quantity sold for that product
list.Add(reportData);
}
connectToDB.Close();
return list;
My challenge now is adding the grouped department row data for each set of products, and append to the list that will be returned i.e
//--
foreach(grouped dataset by department)
{
//get the totals and heading for each set of data
reportData.q_code = //insert department id
reportData.q_description = //insert department name
reportData.sumOfQuantity = // insert total for that department
list.Add(reportData) //add that to the list that will be shown on view
}
//--
I can get department id and code for the grouped data using read.GetString().
string deptName = read.GetString(0);
How do I continue to loop through the dataReader and add totals for each overall set like the quantity columns and have that as a row of its own to be added to the list?... to get the result from the image shown.
Attempt to explain a bit better:
Within my while loop how do I aggregate the set for each department group of products and create a row to add to the list. Or there's a better way of achieving this..

Option 1: Sql and union.
SELECT
q_department.q_name,
q_saledetail.q_code,
q_saledetail.q_description,
SUM(q_quantity) AS qtysum, --running total of q_quantity for the product in that department
FROM q_saledetail
LEFT JOIN q_department ON (q_saledetail.q_departmentid = q_department.q_code )
GROUP BY
q_saledetail.q_departmentid, q_department.q_name, q_saledetail.q_description, q_saledetail.q_code
UNION
SELECT
q_department.q_name,
'',
'',
sum(q_quantity)
FROM q_saledetail
LEFT JOIN q_department ON (q_saledetail.q_departmentid = q_department.q_code )
GROUP BY
q_saledetail.q_departmentid, q_department.q_name
ORDER BY q_saledetail.q_departmentid
By Removing some of the fields that identify different q_saledetail from the group by list, the sum function will sum for all departments instead of breaking the sums up.
Sorry but I can't remember exactly how ordering works with union statements off hand, but I know you can only order
in one place, either the last statement, or the first, or with an outer query that selects from the union.
What you'll have in your data now, is rows that are the 'total' rows for each department, that you know are
'total' rows because the q_code or q_description fields are blank. You can then order the data set by department and then by code (asc or desc) so that the empty code field is at the end of the group of that department and display it like that.
Option 2: Linq and a little manual code work
List<SaleDetailViewModel> list = new List<SaleDetailViewModel>();
while(read.Read())
{
... //Your mapping code
list.Add(reportData);
}
var groupedList = list.GroupBy(x => x.departmentName);
Which will give you a IEnumerable<IGrouping<string, SaleDetailViewModel>.
What that is a your data now grouped by the name of each department which acts as a key
You'll now loop through groupedList and add the values of that IGrouping up.
foreach(var group in groupedList)
{
var total = 0;
foreach(var saledetail in group)
{
total += saledetail.sumOfQuantity;
}
}
Then you just need to add the logic to add it to a list/your datatable in the order of those groups.
Hope this helps.

Related

New to C#, how to update data table (or join table)?

I am new to C# (has experience in other languages before, C++, SQL AutoIT). I have a datatable with 10 columns
Name, MemberNoA, MemberNoB, DriverLicense, MobileNo, Address1, Address2, Address3, ProgramJoinned, Remark
The datatable has around 17,000 rows, what I want to do is, if the same person's records appear more than 2 times in the datatable, put a description in a remark field.
4 criteria to define "same person", any one criteria match will treat as "same person"
i Name + MemberNoA
ii Name + MemberNoB
iii Name + DriverLicense
iv Name + MobileNo
i.e. if there are 3 records with same Name and same MemberNoA, need to put description into remark field of these 3 records.
I work out result set from the above 4 criteria like this:
var resultCriteria1 = from gpr in dt.AsEnumerable()
group gpr by new {
Fld1 = gpr.Field < string > ("Name"),
Fld2 = gpr.Field < string > ("MemberNoA")
}
into grpp
where grpp.Count() > 2
select new {
Name = grpp.Key.Fld1,
MemA = grpp.Key.Fld2,
Cnt = grpp.Count()
};
after that, I loop thru all rows in dt and for each row, loop thru all result set in 4 criteria:
for (int i = 1; i < dt.Rows.Count; i++) {
foreach(var item in resultCriteria1) {
if ((item.Nam == s trName) && (item.MemA == M emberNoA)) {
dt.Rows[i].ItemArray[9] = d t.Rows[i].ItemArray[9] + "Criteria 1 match\r\n";
}
}
}
The program work but run very slow! Is there any method like simple sql statement:
update table1 where
table1.name = table2.name and
table1.MemberNoA = table2.MemberNo2
set table1.Remark = "Criteria 1 match\r\n"
Is there any way to do this in C# or any way to optimize it ? Thanks.
Regds
LAM Chi-fung
The problem is that you are making a cartesian join between the grouped results and the original datatable, without using any performant data structures such as a dictionary or hashset.
But you don't actually need to join it at all, the grouped results can actually hold the relevant data rows directly.
The following code should be performant enough
var grouped =
from gpr in dt.Rows.Cast<DataRow>()
group gpr by (
Fld1: (string)gpr["Name"],
Fld2: (string)gpr["MemberNoA"]
)
into grpp
where grpp.Count() > 2
select grpp;
foreach (var grp in grouped)
foreach (var row in grp)
row["Remark"] += "Criteria 1 match\r\n";
What you can do is to use hashtable, order your data, and then iterate comparing current row data with previous using a cursor. This should give you Log(n) time complexity.

MySQL IN clause: How can I get multiple rows when I use a same value multiple times?

I'm programming a C# Windows Forms Application in Visual Studio and I'm trying to get data about prices of products and the amount a user has added a product to its shopping list from my local MySQL-database into a List(int).
What I do is following:
If a user has added a product 4 times to their shopping list, I'm adding the barcode of the product 4 times to my List(int).
This is working but when I'm reading out all items of the List with the String.Join()-method into the IN-clause of my query and execute it, it only returns a row one time altough the IN-operator has the same barcode multiple times.
The following is how I'm adding barcodes to my List(int)
int count = 0;
List<int> barcodes = new List<int>();
MySqlCommand cmd = new MySqlCommand("SELECT product_barcode, amount FROM shopping_list_items WHERE shopping_list_id = " + current_shoppingListID + ";", db.connection);
cmd.ExecuteNonQuery();
var reader = cmd.ExecuteReader();
while (reader.Read())
{
do
{
barcodes.Add(Int32.Parse(reader["product_barcode"].ToString()));
count++;
} while (count < Int32.Parse(reader["amount"].ToString()));
}
reader.Close();
This is how I'm executing my query and assign the values to variables:
MySqlCommand cmdSum = new MySqlCommand("SELECT sum(price) AS 'total', supermarket_id FROM prices WHERE barcode IN (" + String.Join(", ", barcodes) + ") GROUP BY supermarket_id;", db.connection);
cmdSum.ExecuteNonQuery();
var readerSum = cmdSum.ExecuteReader();
while (readerSum.Read())
{
switch (double.Parse(readerSum["supermarket_id"].ToString()))
{
case 1:
sumSupermarket1 = double.Parse(readerSum["total"].ToString());
break;
case 2:
sumSupermarket2 = double.Parse(readerSum["total"].ToString());
break;
case 3:
sumSupermarket3 = double.Parse(readerSum["total"].ToString());
break;
}
}
A simplified query just to make it simple may look like this:
SELECT name FROM products WHERE barcode IN (13495, 13495, 13495);
If the above one is my query then I want it to return 3 the same rows.
So my question now is, how can I get multiple rows altough I use a same value multiple times in the IN-clause of a MySQL-query?
Q: how can I get multiple rows altough I use a same value multiple times in the IN-clause of a MySQL-query?
A: We don't. That's not how IN () works.
Note that
WHERE foo IN ('fee','fi','fi','fi')`
Is shorthand for
WHERE ( foo = 'fee'
OR foo = 'fi'
OR foo = 'fi'
OR foo = 'fi'
)
Understand what's happening here. MySQL is going to examine each row, and for each row it checks to see if this condition returns TRUE or not. If the row satisfies the condition, the row gets returned. Otherwise the row is not returned.
It doesn't matter that a row with foo value of 'fi' satisfies multiple conditions. All MySQL cares about is that the condition inside the parens ultimately evaluates to TRUE.
As an illustration, consider:
WHERE ( t.picked_by = 'peter piper'
OR t.picked_amount = 'peck'
OR t.name LIKE '%pickled%'
OR t.name LIKE '%pepper%'
)
There could be a row that satisfies every one of these conditions. But the WHERE clause is only asking if the entire condition evaluates to TRUE. If it does, return the row. If it doesn't, then exclude the row. We don't get four copies of a row because more than one of the conditions is satisfied.
So how do we get a set with multiple copies of a row?
As one possible option, we could use separate SELECT statements and combine the results with UNION ALL set operator. Something like this:
SELECT p1.name FROM product p1 WHERE p1.barcode IN (13495)
UNION ALL
SELECT p2.name FROM product p2 WHERE p2.barcode IN (13495)
UNION ALL
SELECT p3.name FROM product p3 WHERE p3.barcode IN (13495)
Note that the result from this query is significantly different than the result from the original query.
There are other query patterns that can return an equivalent set.
FOLLOWUP
Without an understanding of the use case, the specification, I'm just guessing at what we are attempting to achieve. Based on the two queries shown in the code (which follows a common pattern we see in code that is vulnerable to SQL Injection),
The shopping list:
SELECT i.product_barcode
, i.amount
FROM shopping_list_item i
WHERE i.shopping_list_id = :id
What is amount? Is that the quantity ordered? We want two cans of this, or three pounds of that? Seems like we would want to multiply the unit price by the quantity ordered to get the cost. (Two cans is going to cost twice as much as one can.)
If what we are after is the total cost of the items on the shopping list from multiple stores, we could do something like this:
SELECT SUM(p.price * s.amount) AS `total`
, p.supermarket_id
FROM ( SELECT i.product_barcode
, i.amount
FROM shopping_list_item i
WHERE i.shopping_list_id = :id
) s
JOIN price p
ON p.barcode = s.product_barcode
GROUP
BY p.supermarket_id
Note that if a particular product_barcode is not available for particular supermarket_id, that item on the list will be excluded from the total, i.e. we could get a lower total for a supermarket that doesn't have everything on our list.
For performance, we can eliminate the inline view, and write the query like this:
SELECT SUM(p.price * i.amount) AS `total`
, p.supermarket_id
FROM shopping_list_item i
JOIN price p
ON p.barcode = i.product_barcode
WHERE i.shopping_list_id = :id
GROUP
BY p.supermarket_id
If we absolutely have to rip through the shopping list query, and then use the rows from that to create a second query, we could form a query that looks something like this:
SELECT SUM(p.price * i.amount) AS `total`
, p.supermarket_id
FROM ( -- shopping_list here
SELECT '13495' AS product_barcode, '1'+0 AS amount
UNION ALL SELECT '13495', '1'+0
UNION ALL SELECT '13495', '1'+0
UNION ALL SELECT '12222', '2'+0
UNION ALL SELECT '15555', '5'+0
-- end shopping_list
) i
JOIN price p
ON p.barcode = i.product_barcode
WHERE i.shopping_list_id = :id
GROUP
BY p.supermarket_id
You would probably be better off investigating LINQ to SQL rather than using direct SQL and injection.
You can use an inline table join to accomplish what you want:
"SELECT sum(price) AS 'total', supermarket_id
FROM (select "+barcodes[0]+"as bc union all select "+String.Join(" union all select ", barcodes.Skip(1).ToArray())+") w
JOIN prices p ON p.barcode = w.bc
GROUP BY supermarket_id;"
Note: If you can name the column with the inline table alias (I couldn't test that) you could simplify the inline table generation.

How to add order by column name after define the query in linq, when your linq have two data to be selected?

I intend to add order by column name after define the query, but in the query set have two dataset to be retrieve, the below is my queryset
var query = (from data in entities.transaction_table
join data2 in entities.vendor_table
on data.vendor_id equals data2.user_id
where data.user_id == user.user_id
select new { data, data2 });
//SORT
if (!(string.IsNullOrEmpty(sortColumn) && string.IsNullOrEmpty(sortColumnDir)))
{
query = query.OrderBy("" +sortColumn + " " + sortColumnDir);
}
when i try to execute query, the sort column does not found because i select data and data2.
{"No property or field 'date_created' exists in type '<>f__AnonymousType1`2'"}
how can i execute this query with specific order by column
if i use select data instead of select new data, data2, the query was a success, however i need to retrieve data on both side

Filtering detail table structure query language

Query Detail Table
Main Table
1 .PK
Detail Table
1. PK
2. Detail description.
If mysql i can group the main table PK for duplicate issue but how it implement in sql server ?Since sql server required something like aggregate
E.g
(vehicle) Main Table
Car
bikes
van
(vehicleItem) Detail Table
Car->item a,item b
bikes-> item a,item c..
van->item b ,item c
I want to filter in a query such as i want to filter item a only?Seem quite inefficient to call in sub loop.Even put all detail item it in a column consider as incorrect but when search item id(number) might be same number unless the item id are guid and distinct different.
** the main purpose is to output list not to sum crosstab :) ya.. grouping cool if you want to sum up.
var sql = "select * from vehicle ";
var command = new SqlCommand(connection,sql);
try {
var reader = command.ExecuteReader();
if (reader.HasRows) {
while (reader.Read())
if (!string.IsNullOrEmpty(Request.QueryString["ItemIdValue[]"])){
var d = Request.QueryString.GetValues("ItemIdValue[]");
if (d != null){
if (d[0].Contains("all")){
if (GetItemExist(" AND vehicleItem.itemId in (SELECT itemId from item ) ")){ }
}else{
if (GetItemExist(" AND vehicleItem.itemId IN ( " + itemFilter.Remove(itemFilter.Length - 1) + " ) ")){ }
}
}
}
}
}
}
At last thinking back, why not select main table and in subquery..
var sql = "select * from vehicle ";
if (!string.IsNullOrEmpty(Request.QueryString["itemIdValue[]"])){
var d = Request.QueryString.GetValues("itemIdValue[]");
if (d != null){
if (d[0].Contains("all")){
sql = sql + #"
AND vehicleId IN (
SELECT vehicleId
FROM vehicle
JOIN vehicleItem
ON vehicle.vehicleId = vehicleItem.vehicleId
WHERE vehicleItem.itemId in
(SELECT itemId from item)
) ";
} else{
for (var e = 0; e < d.Length; e++){
itemFilter += d[e] + ",";
_fieldVariable.Add("itemIdValue[]");
_valueVariable.Add(d[e]);
}
sql = sql + #"
AND vehicleId IN (
SELECT vehicleId
FROM vehicle
JOIN vehicleItem
ON vehicle.vehicleId = vehicleItem.vehicleId
WHERE vehicleItem.itemId in ( " + itemFilter.Remove(itemFilter.Length - 1) + " ) ) ";
}
}
}
When joining two tables t1 and t2, a result set containing all matching rows from table t1 and table t2 is created. When you only want one resulting row per t1's row, it is up to you to filter the join accordingly.
There are several possible ways to do that:
adapt the join's ON clause
group accordingly
specify a WHERE clause
As you wanted to use grouping in your request, you could use a query like this:
SELECT m.PK, min(d.Detail)
FROM Main m
LEFT JOIN Detail d on d.PK=m.pk
WHERE d.Detail = 'item a'
GROUP by m.PK
That way you will get one row per PK with the minimum value in detail. You can use other aggregate functions too. See https://msdn.microsoft.com/en-us/library/ms173454.aspx.
Edit: Added WHERE clause to filter by "item a".

How to get the count of occurrences in one table and update another table to that value?

I want to write an SQL statement that will:
Count the number of rows in the table booking that are open and where the booking.postcode is "MK"
Take a note of the plot (in booking.plot_id), and then update the table plot.jobs with the value count
For example running the SQL query when booking table has the following rows:
Would see the following highlighted values in plot.jobs being updated to 1:
This my code so far (note I am using Connector/Net):
public int CountBooking()
{
string query = "SELECT Count(*) FROM booking WHERE postcode=MK AND status=open";
int count = -1;
// ExecuteScalar will return one value
var cmd = new MySqlCommand(query, _connection);
count = int.Parse(cmd.ExecuteScalar() + "");
// Close
CloseConnection();
return count;
}
If there were 3 rows with a plot_id of 4, and 6 rows with a plot_id of 6, then the highlighted values will be updated to 3 and 6 respectively.
How would I go about achieving it?
You are close on the first part. If you want the number of openings for each plot_id, you'll need to use that in your GROUP BY statement like this:
SELECT plot_id, COUNT(*) AS numOpenings
FROM bookings
WHERE postcode = 'MK' AND status = 'open'
GROUP BY plot_id;
You can use that as a subquery in your UPDATE statement by joining it to the plot table and updating the matching rows, like this:
UPDATE plot p
JOIN(
SELECT plot_id, COUNT(*) AS numOpenings
FROM bookings
WHERE postcode = 'MK' AND status = 'open'
GROUP BY plot_id) temp ON temp.plot_id = p.plot_id
SET p.jobs = temp.numOpenings;
This worked out in SQL Fiddle. Let me know if you have more problems.
Never used Connector/Net but I would do something like this:
Run this query to retrieve the plot_id and JobCount:
select plot_id, count(*) JobCount from booking where postcode = 'mk' and status = 'open' group by plot_id;
Then scroll through this result set and issue update commands like this:
Update Jobs set jobs = [JobCount] where plot_ID = [plot_id]
Note the values of [JobCount] and [plot_id] will come from your first query.

Categories