Complicated SQL Server query - c#

I am trying to write an SQL (Server) query which will return all events on a current day, and for all events where the column recurring= 1, I want it to return this event on the day it is being held and for the subsequent 52 weeks following the event.
My tables are structured as followed :
Event
{
event_id (PK)
title,
description,
event_start DATETIME,
event_end DATETIME,
group_id,
recurring
}
Users
{
UserID (PK)
Username
}
Groups
{
GroupID (PK)
GroupName
}
Membership
{
UserID (FK)
GroupID (FK)
}
The code I have thus far is as follows :
var db = Database.Open("mPlan");
string username = HttpContext.Current.Request.Cookies.Get("mpUsername").Value;
var listOfGroups = db.Query("SELECT GroupID FROM Membership WHERE UserID = (SELECT UserID from Users WHERE Username = #0 )", username);
foreach(var groupID in listOfGroups)
{
int newGroupID = groupID.GroupID;
var result = db.Query(
#"SELECT e.event_id, e.title, e.description, e.event_start, e.event_end, e.group_id, e.recurring
FROM event e
JOIN Membership m ON m.GroupID = e.group_id
WHERE e.recurring = 0
AND m.GroupID = #0
AND e.event_start >= #1
AND e.event_end <= #2
UNION ALL
SELECT e.event_id, e.title, e.description, DATEADD(week, w.weeks, e.event_start), DATEADD(week, w.weeks, e.event_end), e.group_id, e.recurring
FROM event e
JOIN Membership m ON m.GroupID = e.group_id
CROSS JOIN
( SELECT row_number() OVER (ORDER BY Object_ID) AS weeks
FROM SYS.OBJECTS
) AS w
WHERE e.recurring = 1
AND m.GroupID = #3
AND DATEADD(WEEK, w.Weeks, e.event_start) >= #4
AND DATEADD(WEEK, w.Weeks, e.event_end) <= #5", newGroupID, start, end, newGroupID, start, end
);
This results in when one queries for the date of the event stored in the database, this event and 52 weeks of events are returned. When one queries for the event the week after this one, nothing is returned.

The simplest solution would be to alter the following 2 lines
AND e.event_start >= #4
AND e.event_end <= #5"
to
AND DATEADD(WEEK, w.Weeks, e.event_start) >= #4
AND DATEADD(WEEK, w.Weeks, e.event_end) <= #5"
However, I'd advise putting all this SQL into a stored procedure, SQL-Server will cache the execution plans and it will result in (slightly) better performance.
CREATE PROCEDURE dbo.GetEvents #UserName VARCHAR(50), #StartDate DATETIME, #EndDate DATETIME
AS
BEGIN
-- DEFINE A CTE TO GET ALL GROUPS ASSOCIATED WITH THE CURRENT USER
;WITH Groups AS
( SELECT GroupID
FROM Membership m
INNER JOIN Users u
ON m.UserID = u.UserID
WHERE Username = #UserName
GROUP BY GroupID
),
-- DEFINE A CTE TO GET ALL EVENTS FOR THE GROUPS DEFINED ABOVE
AllEvents AS
( SELECT e.*
FROM event e
INNER JOIN Groups m
ON m.GroupID = e.group_id
UNION ALL
SELECT e.event_id, e.title, e.description, DATEADD(WEEK, w.weeks, e.event_start), DATEADD(WEEK, w.weeks, e.event_end), e.group_id, e.recurring
FROM event e
INNER JOIN Groups m
ON m.GroupID = e.group_id
CROSS JOIN
( SELECT ROW_NUMBER() OVER (ORDER BY Object_ID) AS weeks
FROM SYS.OBJECTS
) AS w
WHERE e.recurring = 1
)
-- GET ALL EVENTS WHERE THE EVENTS FALL IN THE PERIOD DEFINED
SELECT *
FROM AllEvents
WHERE Event_Start >= #StartDate
AND Event_End <= #EndDate
END
Then you can call this with
var result = db.Query("EXEC dbo.GetEvents #0, #1, #2", username, start, end);
This elimates the need to iterate over groups in your code behind. If this is actually a requirement then you could modify the stored procedure to take #GroupID as a parameter, and change the select statements/where clauses as necessary.
I have assumed knowledge of Common Table Expressions. They are not required to make the query work, they just make things slightly more legible in my opinion. I can rewrite this without them if required.

I would check my parameters one at a time against some trivial SQL, just to rule them out as possible culprits. Something like this:
var result = db.Query("select r=cast(#0 as varchar(80))",username);
var result = db.Query("select r=cast(#0 as int)",newGroupID);
var result = db.Query("select r=cast(#0 as datetime)",start);
var result = db.Query("select r=cast(#0 as datetime)",end);

Related

Multiple AND conditions on the same column [Servicestack.OrmLite]

I was wondering if it's possible to have multiple AND conditions on the same column using Servicestack.OrmLite. This is the SELECT statement I printed out, but It always returns 0. I should get the product count from the products having both specifications with id 1016 and 17.
SELECT COUNT(DISTINCT "Product"."Id")
FROM "Product"
INNER JOIN "ProductManufacturer"
ON ("Product"."Id" = "ProductManufacturer"."ProductId")
INNER JOIN "ProductSpecificationAttribute"
ON ("Product"."Id" = "ProductSpecificationAttribute"."ProductId")
WHERE ("ProductManufacturer"."ManufacturerId" = 6)
AND ("ProductSpecificationAttribute"."SpecificationAttributeOptionId" = 1016)
AND ("ProductSpecificationAttribute"."SpecificationAttributeOptionId" = 17)
A single column value can't possibly have two values at the same time.
What you want is either:
AND
(
ProductSpecificationAttribute.SpecificationAttributeOptionId = 1016
OR
ProductSpecificationAttribute.SpecificationAttributeOptionId = 17
)
Or, more succinctly:
AND
(
ProductSpecificationAttribute.SpecificationAttributeOptionId
IN (1016, 17)
)
And turn off whatever option is forcing your tool to "inject" "double" "quotes" "around" "every" "entity" "name" because it makes the query text unmanageable. You might also consider using aliases and schema prefixes, like INNER JOIN dbo.ProductSpecificationAttribute AS psa...
After further clarification... the goal is to find products where they have both of those attributes on different rows, which isn't clear from the description or the code ORMLite barfed out. Here's what you want in that case (there are several ways to do this, but converting everything to EXISTS also allows you to remove the DISTINCT from the COUNT, which is never free):
SELECT COUNT(Product.Id) FROM dbo.Product AS p
WHERE EXISTS
(
SELECT 1 FROM dbo.ProductManufacturer AS pm
WHERE pm.ProductId = p.Id AND pm.ManufacturerId = 6
)
AND EXISTS
(
SELECT 1 FROM dbo.ProductSpecificationAttribute AS psa
WHERE psa.ProductId = p.Id
AND psa.SpecificationAttributeOptionId = 1016
)
AND EXISTS
(
SELECT 1 FROM dbo.ProductSpecificationAttribute AS psa
WHERE psa.ProductId = p.Id
AND psa.SpecificationAttributeOptionId = 17
);
If ProductSpecificationAttribute is poorly index and this leads to two scans, you could change that by saying something like this (untested, but I'm happy to test it out if you can produce a db<>fiddle:
SELECT COUNT(Product.Id) FROM dbo.Product AS p
WHERE EXISTS
(
SELECT 1 FROM dbo.ProductManufacturer AS pm
WHERE pm.ProductId = p.Id
AND pm.ManufacturerId = 6
)
AND EXISTS
(
SELECT 1 FROM dbo.ProductSpecificationAttribute AS psa
WHERE psa.ProductId = p.Id
AND psa.SpecificationAttributeOptionId IN (17, 1016)
GROUP BY psa.ProductId, psa.SpecificationAttributeOptionId
HAVING COUNT(DISTINCT psa.SpecificationAttributeOptionId) > 1
);
It's also really weird that the table ProductManufacturer has a list of ProductIDs in it that point back to Product - usually Product would have a ManufacturerID that points in the other direction.
Anyway, you might consider using stored procedures that your ORM can call if it has problems creating queries beyond basic CRUD (which is unfortunately a limitation of all ORMs to some degree - they're great at the basics, covering 80% of the use case, but they're terrible at the other 20% - unfortunately most of us end up needing that other 20% before too long).
You can get all the product ids that you want if you group by product and set the conditions in the HAVING clause:
SELECT p.Id
FROM Product p
INNER JOIN ProductManufacturer pm ON p.Id = pm.ProductId
INNER JOIN ProductSpecificationAttribute psa ON p.Id = psa.ProductId
WHERE pm.ManufacturerId = 6 AND psa.SpecificationAttributeOptionId IN (17, 1016)
GROUP BY p.Id
HAVING COUNT(DISTINCT psa.SpecificationAttributeOptionId) = 2; -- both specifications must exist
If you want to count these products you could either use the above query as a subquery or a cte and count the rows:
WITH cte AS (
SELECT p.Id
FROM Product p
INNER JOIN ProductManufacturer pm ON p.Id = pm.ProductId
INNER JOIN ProductSpecificationAttribute psa ON p.Id = psa.ProductId
WHERE pm.ManufacturerId = 6 AND psa.SpecificationAttributeOptionId IN (17, 1016)
GROUP BY p.Id
HAVING COUNT(DISTINCT psa.SpecificationAttributeOptionId) = 2;
)
SELECT COUNT(*) FROM cte;
or, use COUNT() window function:
SELECT DISTINCT COUNT(*) OVER ()
FROM Product p
INNER JOIN ProductManufacturer pm ON p.Id = pm.ProductId
INNER JOIN ProductSpecificationAttribute psa ON p.Id = psa.ProductId
WHERE pm.ManufacturerId = 6 AND psa.SpecificationAttributeOptionId IN (17, 1016)
GROUP BY p.Id
HAVING COUNT(DISTINCT psa.SpecificationAttributeOptionId) = 2;

Basic misunderstanding of LINQ to SQL and foreign keys

I am working on a much larger project, and can't seem to get LINQ to SQL working the way I expect it to. I created a simple subset of the project so I can use LinqPad to try to make sure I have a basic understanding of how this should work.
Clearly, I don't: I've created two very simple tables - customer and job. The customer table has an ID (auto-increment) and a Name, the Job table has an ID (ai), a Name, and a CustomerID (foreign key to ID in the customer table).
When I run the following code against an initially empty database:
void Main()
{
string custName = "James";
string[] jobNames = new string[] {"Home Depot", "Menards", "Sam's Club" };
var cust = customer.FirstOrDefault(c => c.Name == custName);
if (cust == null)
{
cust = new customer
{
Name = custName
};
customer.InsertOnSubmit(cust);
}
foreach(var jn in jobNames)
{
if (!job.Any(j => j.Customer.Name == cust.Name && j.Name == jn))
job.InsertOnSubmit(new job {
Name = jn,
Customer = cust
});
}
SubmitChanges();
customer.Dump();
job.Dump();
}
I would expect to end up with 1 customer and 3 jobs in the database - that's all good. But the generated SQL and the setting of the customer IDs are not at all what I expect:
SQL --
SELECT t0.ID, t0.Name
FROM customer AS t0
WHERE (t0.Name = #p0)
LIMIT 0, 1
-- p0 = [James]
SELECT COUNT(*) AS value
FROM job AS t0
LEFT OUTER JOIN customer AS t1
ON (t1.ID = t0.CustomerID)
WHERE ((t1.Name = #p0) AND (t0.Name = #p1))
-- p0 = [James]
-- p1 = [Home Depot]
SELECT COUNT(*) AS value
FROM job AS t0
LEFT OUTER JOIN customer AS t1
ON (t1.ID = t0.CustomerID)
WHERE ((t1.Name = #p0) AND (t0.Name = #p1))
-- p0 = [James]
-- p1 = [Menards]
SELECT COUNT(*) AS value
FROM job AS t0
LEFT OUTER JOIN customer AS t1
ON (t1.ID = t0.CustomerID)
WHERE ((t1.Name = #p0) AND (t0.Name = #p1))
-- p0 = [James]
-- p1 = [Sam's Club]
INSERT INTO job(CustomerID, ID, Name)
VALUES (NULL, 0, #p0)
-- p0 = [Home Depot]
INSERT INTO job(CustomerID, ID, Name)
VALUES (NULL, 0, #p0)
-- p0 = [Menards]
INSERT INTO job(CustomerID, ID, Name)
VALUES (NULL, 0, #p0)
-- p0 = [Sam's Club]
INSERT INTO customer(ID, Name)
VALUES (0, #p0)
-- p0 = [James]
SELECT t0.ID, t0.Name
FROM customer AS t0
SELECT t0.CustomerID, t0.ID, t0.Name
FROM job AS t0
Results in LinqPad:
I thought that the beauty of LINQ to SQL was that I don't have to manage to set the foreign keys and that I should be able to do what I'm trying to do here with a single hit to the database. What am I missing?
EDIT: So I guess I understand the customer IDs being set to NULL because the generated SQL is calling INSERT on the jobs before the INSERT on the customer, hence there is no ID yet. Why would it do that? Also, if I run the same query again, I get three more rows in the jobs table, but the CustomerIDs are all still set to NULL.
Linq query have no issues.
check DB table relations

LINQ - select statement in the selected column

i am intend to convert the following query into linQ
SELECT TOP 100 S.TxID,
ToEmail,
[Subject],
ProcessedDate,
[Status] = (CASE WHEN EXISTS (SELECT TxID FROM TxBounceTracking
WHERE TxID = S.TxID)
THEN 'Bounced'
WHEN EXISTS (SELECT TxID FROM TxOpenTracking
WHERE TxID = S.TxID)
THEN 'Opened'
ELSE 'Sent' END)
FROM TxSubmissions S
WHERE S.UserID = #UserID
AND ProcessedDate BETWEEN #StartDate AND #EndDate
ORDER BY ProcessedDate DESC
The following code is the linq that i converted.
v = (from a in dc.TxSubmissions
where a.ProcessedDate >= datefrom && a.ProcessedDate <= dateto && a.UserID == userId
let bounce = (from up in dc.TxBounceTrackings where up.TxID == a.TxID select up)
let track = (from up in dc.TxOpenTrackings where up.TxID == a.TxID select up)
select new { a.TxID, a.ToEmail, a.Subject,
Status = bounce.Count() > 0 ? "Bounced" : track.Count() > 0 ? "Opened" : "Sent",
a.ProcessedDate });
However this linq is too slow because the bounce and track table, how should i change the linq query to select one row only to match the SQL query above >>
SELECT TxID FROM TxOpenTracking WHERE TxID = S.TxID
in my selected column, so it can execute faster.
Note that the record contained one million records, thats why it lag
As you don't care about readability because you will end up generating the query via EF you can try to join with those two tables. (it looks that TxID is a FK or a PK/FK)
More about JOIN vs SUB-QUERY here: Join vs. sub-query
Basically your SQL looks like this:
SELECT TOP 100 S.TxID, ToEmail, [Subject], ProcessedDate,
[Status] = (CASE WHEN BT.TxID IS NOT NULL
THEN 'Bounced'
WHEN OP.TxID IS NOT NULL
THEN 'Opened'
ELSE 'Sent' END)
FROM TxSubmissions S
LEFT JOIN TxBounceTracking BT ON S.TxID = BT.TxID
LEFT JOIN TxOpenTracking OP ON S.TxID = OP.TxID
WHERE S.UserID = #UserID
AND ProcessedDate BETWEEN #StartDate AND #EndDate
ORDER BY ProcessedDate DESC
And then, you can try to convert it to LINQ something like:
v = (from subs in dc.TxSubmissions.Where(sub => sub.ProcessedDate >= datefrom && sub.ProcessedDate <= dateto && sub.UserID == userId)
from bts in dc.TxBounceTrackings.Where(bt => bt.TxID == subs.TxID).DefaultIfEmpty()
from ots in dc.TxOpenTrackings.Where(ot => ot.TxID == subs.TxID).DefaultIfEmpty()
select new { });
More about left join in linq here: LEFT JOIN in LINQ to entities?
Also if you remove default if empty you'll get a inner join.
Also you need to take a look at generated SQL in both cases.

Merging SQL query for results that are alike

I have a system right now that stores products and tags that are associated with each product.
Example
Product: Microphone
Tags: Music, Electronics, Audio
There is a Tag table, Product Table, and TagProductMapping Table. The third table obviously maps the product to the tags for a one to many relation. When I query the Microphone product using a LEFT JOIN I get 3 records that are almost duplicate except the "TagName" column where obviously it has all 3 different tags. How can I merge this result? Its frustrating because if I try to query exactly 10 products, it will only limit to 10 results, which wouldnt really be 10 products.
Anyone have a good idea for this?
EDIT:
Here is the result on my query, notice how the only thing different between the 3 JobId's are the CategoryName, which are the tags.
Here are what my tables look like
-Tagmapping table:
-Tag Table
-"products table" (in this case, its my job table)
Here is my stored procedure:
ALTER PROCEDURE [dbo].[JobPostingSelectAll]
(
#StartRowIndex INT,
#PageSize INT,
#OrderBy VARCHAR(50),
#OrderByDirection VARCHAR(4),
#CurrentUserId INT,
#CategoryId INT
)
AS
SET NOCOUNT ON
SELECT
JobId,
Title,
Answers,
UserId,
UserName,
ProfileImageName,
CategoryId,
CategoryName,
Fees,
DESCRIPTION,
DateCreated,
UniqueTitle,
IsSecured
FROM (
SELECT J.JobId,
J.Title,
(SELECT COUNT(ja2.JobId) FROM JobApplication ja2 left join Deliverable d2 ON d2.DeliverableId = ja2.DeliverableId
WHERE ja2.JobId=j.JobId and (d2.PurchaseCount>0 OR d2.IsFrozen=0)) AS Answers,
J.UserId,
U.UserName,
U.ImageName as ProfileImageName,
J.CategoryId,
C.CategoryName,
J.Fees,
J.Description,
J.DateCreated,
J.UniqueTitle,
J.IsSecured,
ROW_NUMBER() OVER(
ORDER BY
CASE
WHEN #OrderByDirection = 'asc' AND #OrderBy = 'Answers'
THEN (SELECT COUNT(ja2.JobId) FROM JobApplication ja2 left join Deliverable d2 ON d2.DeliverableId = ja2.DeliverableId
WHERE ja2.JobId=j.JobId and (d2.PurchaseCount>0 OR d2.IsFrozen=0)) END ASC,
CASE
WHEN #OrderByDirection = 'asc' AND #OrderBy = 'Answers'
THEN J.DateCreated END DESC,
CASE
WHEN #OrderByDirection = 'asc' AND #OrderBy = 'Answers'
THEN J.Title END ASC,
CASE
WHEN #OrderByDirection = 'desc' AND #OrderBy = 'Answers'
THEN (SELECT COUNT(ja2.JobId) FROM JobApplication ja2 left join Deliverable d2 ON d2.DeliverableId = ja2.DeliverableId
WHERE ja2.JobId=j.JobId and (d2.PurchaseCount>0 OR d2.IsFrozen=0)) END DESC,
CASE
WHEN #OrderByDirection = 'desc' AND #OrderBy = 'Answers'
THEN J.DateCreated END DESC,
CASE
WHEN #OrderByDirection = 'desc' AND #OrderBy = 'Answers'
THEN J.Title END ASC,
CASE WHEN #OrderByDirection = 'asc' AND #OrderBy = 'Fees'
THEN J.Fees END ASC,
CASE WHEN #OrderByDirection = 'asc' AND #OrderBy = 'Fees'
THEN J.DateCreated END DESC,
CASE WHEN #OrderByDirection = 'desc' AND #OrderBy = 'Fees'
THEN J.Fees END DESC,
CASE WHEN #OrderByDirection = 'desc' AND #OrderBy = 'Fees'
THEN J.DateCreated END DESC,
CASE WHEN #OrderByDirection = 'asc' AND #OrderBy = 'DateCreated'
THEN J.DateCreated END ASC,
CASE WHEN #OrderByDirection = 'desc' AND #OrderBy = 'DateCreated'
THEN J.DateCreated END DESC
) AS RowIndex
FROM [JobPosting] J
LEFT JOIN TagMapping TM ON J.JobId = TM.QuestionId
LEFT JOIN Categories C ON TM.TagId = C.CategoryID
Left Join [User] U ON J.UserId = U.UserID
WHERE J.IsLocked = 0 AND j.IsDeleted = 0
AND (#CategoryId IS NULL OR J.CategoryId = #CategoryId)
) AS JobPostingList
WHERE RowIndex BETWEEN #StartRowIndex AND (#StartRowIndex + #PageSize) - 1
SELECT COUNT(J.JobID) AS TotalRecords
FROM JobPosting J
WHERE J.IsLocked = 0 AND J.IsDeleted = 0
AND (#CategoryId IS NULL OR J.CategoryId = #CategoryId)
-- select all filecount grouped by Type Of File for specific Job
SELECT J.JobId, F.MimeType, COUNT(F.FileId) AS FileCount
FROM
JobPosting J
Left Join Files F ON F.JobPostingId = J.JobId
WHERE J.IsLocked = 0 AND J.IsDeleted = 0
AND (#CategoryId IS NULL OR J.CategoryId = #CategoryId)
GROUP BY
F.MimeType,J.JobId
Having COUNT(F.FileId) > 0
The issue you're having occurs because your database structure normalizes tag data for each product (I've found this page to be a good reference).
When you SELECT from your Products table and JOIN over to your Tags table, it's crucial to remember that you're not getting a list of Products; rather, you're getting a listing of Product-Tag combinations.
If you want to get a list of the top 10 products along with their tag information, I'd suggest using a subquery:
select * from
(select top 10 * from ProductsTable) TopProducts
inner join Tagmapping on TopProducts.ProductID = Tagmapping.ProductID
inner join Tags on Tagmapping.TagID = Tags.TagID
Even though this solves your initial selection issue, this will still produce the kind of multiple listing you show above where only the tag information differs from row to row.
It's possible to format the output to have multiple tag entries (maybe separated by commas) as described here, but that's going to be something you want to do as a last step before you present the data to the user, either through SQL or whatever software layer you're using as a go-between.
I believe that if you are not interested in the tag name, you can select all columns except the tag name and use "select distinct" to show just the products without its tags. I can be wrong though =(

How can I optimise this Linq query to remove the unnecessary SELECT Count(*)

I have three tables, Entity, Period and Result. There is a 1:1 mapping between Entity and Period and a 1:Many between Period and Result.
This is the linq query:
int id = 100;
DateTime start = DateTime.Now;
from p in db.Periods
where p.Entity.ObjectId == id && p.Start == start
select new { Period = p, Results = p.Results })
This is relevant parts of the generated SQL:
SELECT [t0].[EntityId], [t2].[PeriodId], [t2].[Value], (
SELECT COUNT(*)
FROM [dbo].[Result] AS [t3]
WHERE [t3].[PeriodId] = [t0].[Id]
) AS [value2]
FROM [dbo].[Period] AS [t0]
INNER JOIN [dbo].[Entity] AS [t1] ON [t1].[Id] = [t0].[EntityId]
LEFT OUTER JOIN [dbo].[Result] AS [t2] ON [t2].[PeriodId] = [t0].[Id]
WHERE ([t1].[ObjectId] = 100) AND ([t0].[Start] = '2010-02-01 00:00:00')
Where is the SELECT Count(*) coming from and how can I get rid of it? I don't need a count of the "Results" for each "Period" and it slows the query down by an order of magnitude.
Consider using the Context.LoadOptions and specifying for Period to LoadWith(p => p.Results) to eager load the period with results without needing to project into an anonymous type.

Categories

Resources