How to optimize SELECT statement with multiple sub-queries - sql

I have a query that I'm trying to optimize but haven't had much success. There are two tables, one with the main data and one with timestamps of when specific events occurred. The tables are relational using a common key of adID. I am trying to perform a query that pulls in all of the timestamps and other data from the main table. I have it working but I am trying to optimize so it runs faster.
SELECT a.ID,a.repID,a.artistID,
(
SELECT TOP 1 c.timestamp
FROM Tracking AS c
WHERE statusID = 4
AND c.ID = a.ID
ORDER BY c.timestamp ASC
)
AS created,
(
SELECT TOP 1 d.timestamp
FROM Tracking AS d
WHERE statusID = 5
AND d.ID = a.ID
ORDER BY d.timestamp ASC
)
AS claimed,
(
SELECT TOP 1 p.timestamp
FROM Tracking AS p
WHERE statusID = 6
AND p.ID = a.ID
ORDER BY p.timestamp ASC
)
AS proof,
(
SELECT TOP 1 v.timestamp
FROM Tracking AS v
WHERE statusID = 8
AND v.ID = a.ID
ORDER BY v.timestamp ASC
)
AS approved,
(
SELECT count(ID)
FROM Tracking AS t
WHERE statusID = 6
AND t.ID = a.ID
)
AS proofcount
FROM Advertising AS a
WHERE a.statusID = 8
Any help on this is appreciated. I'm not too familiar with SQL Server so I am not too well versed in optimizing queries such as these.

You should be able to use the following:
SELECT a.ID,
a.repID,
a.artistID,
min(case when t.statusID = 4 then t.timestamp end) created,
min(case when t.statusID = 5 then t.timestamp end) claimed,
min(case when t.statusID = 6 then t.timestamp end) proof,
min(case when t.statusID = 8 then t.timestamp end) approved,
count(case when t.statusID = 6 then id end) proofcount
FROM Advertising AS a
LEFT JOIN Tracking t
on a.id = t.id
WHERE a.statusID = 8
GROUP BY a.ID, a.repID, a.artistID;

I think the following query gets to what you want:
select id, repid, artistid,
max(case when statusId = 4 and seqnum = 1 then timestamp end),
max(case when statusId = 5 and seqnum = 1 then timestamp end),
max(case when statusId = 6 and seqnum = 1 then timestamp end),
max(case when statusId = 8 and seqnum = 1 then timestamp end),
sum(case when statusId = 6 then 1 else 0 end)
from (select a.ID, a.repID, a.artistID, t.statusId, t.timestamp
row_number() over (partition by a.id, t.statusId order by timestamp) as seqnum
from advertising a left outer join
tracking t
on a.id = t.id
) t
where seqnum = 1
group by id, repid, artistid
It joins the tables together and identifies the earliest record using row_number(). It then selects only these records and groups by the other fields.
Your query also filters only those records that have statusid = 8. I'm not sure if this is intentional. If so, then you want this having clause at the end of the query:
having sum(case when statusId = 8 then 1 else 0 end) > 0

Related

In a nested query, check all values for a condition

How can I get rid of nested queries (agree, dis_agreed)? How to rewrite to join - I can not think of it. Maybe there are other optimal solutions?
select *
from (
select
(select count(id) from Agreement a where a.ConclusionCardFile = f.id and a.Status = 1) agreed,
(select count(id) from Agreement a where a.ConclusionCardFile = f.id and (a.Status <> 1 or a.Status is null)) dis_agreed
from ConclusionCard_Files f
) t
where t.agreed > 0 and dis_agreed = 0
You can write the conditions as a where clause:
select *
from conclusionCard_Files
where exists (
select *
from agreement
where agreement.conclusionCardFile = conclusionCard_Files.id
having sum(case when status = 1 then 1 else 0 end) > 0
and sum(case when status = 1 then 0 else 1 end) = 0
)
Maybe you just using sub-queries only to filter? What about to move them to WHERE clause?
SELECT
*
FROM ConclusionCard_Files f
WHERE
EXISTS(select * from Agreement a where (a.ConclusionCardFile = f.id) and a.Status =1)
AND NOT EXISTS(select * from Agreement a where (a.ConclusionCardFile = f.id) and (a.Status != 1 or a.Status is null))
It's performance friendly because SqlServer do not count all Counts
If I understand correctly, you can try to use JOIN with HAVING condition aggregate function.
SELECT COUNT(CASE WHEN a.Status = 1 THEN ID END) agreed,
COUNT(CASE WHEN a.Status <> 1 or a.Status is null THEN ID END) dis_agreed
FROM Agreement a
INNER JOIN ConclusionCard_Files f
ON a.ConclusionCardFile = f.id
HAVING
COUNT(CASE WHEN a.Status = 1 THEN ID END) > 0
AND
COUNT(CASE WHEN a.Status <> 1 or a.Status is null THEN ID END) = 0
EDIT
if you want to get data from ConclusionCard_Files based on your condition. you can try to let condition aggregate function in subquery each ConclusionCardFile from table Agreement then do JOIN
SELECT f.*
FROM (
SELECT COUNT(CASE WHEN a.Status = 1 THEN ID END) agreed,
COUNT(CASE WHEN a.Status <> 1 or a.Status is null THEN ID END) dis_agreed,
a.ConclusionCardFile
FROM Agreement a
GROUP BY a.ConclusionCardFile
) a
INNER JOIN ConclusionCard_Files f
ON a.ConclusionCardFile = f.id
WHERE a.agreed > 0 AND a.dis_agreed = 0

Query for count and distinct

I should make a report in T-SQL from several table.
I can join all the table needed but after I don't know excatly how to get my information.
Explanation :
I've got the following table :
Tbl_User (UserId, Username)
Tbl_Customer (CustomeriD, CustomerName)
Tbl_DocA (DocId, CustomerID, DateCreate, DateAdd, UseriD)
Tbl_DocB (DocId, CustomerID, DateCreate, DateAdd, UseriD)
Tbl_DocC (DocId, CustomerID, DateCreate, DateAdd, UseriD)
I am trying to get a report like this :
After I can get this, the idea is to have a filter with the date in SQL reporting.
You can union all the document tables together and join users and customers on it.
SELECT Customer.CustomerID
,Customer.CustomerName
,COUNT(CASE WHEN DocType = 'A' THEN 1 END) AS doc_a_total
,COUNT(CASE WHEN DocType = 'B' THEN 1 END) AS doc_b_total
,COUNT(CASE WHEN DocType = 'C' THEN 1 END) AS doc_c_total
,COUNT(CASE WHEN DocType = 'A' AND user.username ='azerty' THEN 1 END) AS doc_a_made_by_azerty
,COUNT(CASE WHEN DocType = 'B' AND user.username ='azerty' THEN 1 END) AS doc_b_made_by_azerty
,COUNT(CASE WHEN DocType = 'C' AND user.username ='azerty' THEN 1 END) AS doc_c_made_by_azerty
FROM (
(SELECT 'A' AS DocType, * FROM Tbl_DocA)
UNION ALL
(SELECT 'B' AS DocType, * FROM Tbl_DocB)
UNION ALL
(SELECT 'C' AS DocType, * FROM Tbl_DocC)
) AS docs
JOIN Tbl_User AS user ON user.UserId = docs.UseriD
JOIN Tbl_Customer AS Customer ON Customer.CustomeriD = docs.CustomeriD
GROUP BY Customer.CustomerID , Customer.CustomerName
You can use common table expressions to get the count for each report type per customer, with conditional aggregation for reports made by a specific user, and join them to the customers table.
Something like this should get you the desired results:
DECLARE #UserId int = 1; -- or whatever the id of the user you need
WITH CTEDocA AS
(
SELECT CustomerID
, COUNT(DocId) As NumberOfReports
, COUNT(CASE WHEN UserId = #UserId THEN 1 END) As NumberOfReportsByUserAzerty
FROM Tbl_DocA
GROUP BY CustomerID
), CTEDocB AS
(
SELECT CustomerID
, COUNT(DocId) As NumberOfReports
, COUNT(CASE WHEN UserId = #UserId THEN 1 END) As NumberOfReportsByUserAzerty
FROM Tbl_DocB
GROUP BY CustomerID
), CTEDocC AS
(
SELECT CustomerID
, COUNT(DocId) As NumberOfReports
, COUNT(CASE WHEN UserId = #UserId THEN 1 END) As NumberOfReportsByUserAzerty
FROM Tbl_DocC
GROUP BY CustomerID
)
SELECT cust.CustomeriD
,cust.CustomerName
,ISNULL(a.NumberOfReports, 0) As NumberOfDocA
,ISNULL(a.NumberOfReportsByUserAzerty, 0) As NumberOfDocAByAzerty
,ISNULL(b.NumberOfReports, 0) As NumberOfDocB
,ISNULL(b.NumberOfReportsByUserAzerty, 0) As NumberOfDocBByAzerty
,ISNULL(c.NumberOfReports, 0) As NumberOfDocC
,ISNULL(c.NumberOfReportsByUserAzerty, 0) As NumberOfDocCByAzerty
FROM Tbl_Customer cust
LEFT JOIN CTEDocA As a
ON cust.CustomeriD = a.CustomerID
LEFT JOIN CTEDocA As b
ON cust.CustomeriD = b.CustomerID
LEFT JOIN CTEDocA As c
ON cust.CustomeriD = c.CustomerID
To filter by date you can add a where clause to each common table expresstion.
BTW, The fact that you have three identical tables for three document types suggest a bad database design.
If these tables are identical you should consider replacing them with a single table and add a column to that table describing the document type.
There are several ways to do this. One key feature needed is to count a particular user apart from the others. This is done with conditional aggregation. E.g.:
select
customerid,
count(*),
count(case when userid = <particular user ID here> then 1 end)
from tbl_doca
group by customerid;
Here is one possible query using a cross join to get the user in question once and cross apply to get the numbers.
select
c.customerid,
c.customername,
doca.total as doc_a_total,
doca.az as doc_a_by_azerty,
docb.total as doc_b_total,
docb.az as doc_b_by_azerty,
docc.total as doc_c_total,
docc.az as doc_c_by_azerty
from tbl_customer c
cross join
(
select userid from tbl_user where username = 'Azerty'
) azerty
cross apply
(
select
count(*) as total,
count(case when da.userid = azerty.userid then 1 end)n as az
from tbl_doca da
where da.customerid = c.customerid
) doca
cross apply
(
select
count(*) as total,
count(case when db.userid = azerty.userid then 1 end)n as az
from tbl_docb db
where db.customerid = c.customerid
) docb
cross apply
(
select
count(*) as total,
count(case when dc.userid = azerty.userid then 1 end)n as az
from tbl_docc dc
where dc.customerid = c.customerid
) docc
order by c.customerid;
Other options would be to replace the cross apply with left outer join and non-correlated subqueries or to put subqueries into the select clause.
Combining the totals for the documents is another method.
Then use conditional aggregation for the counts.
untested notepad scribble:
;WITH SPECIFICUSER AS
(
SELECT UseriD
FROM Tbl_User
WHERE UserName = 'azerty'
),
DOCTOTALS (
SELECT CustomeriD, UseriD, 'DocA' AS Src, COUNT(DocId) AS Total
FROM Tbl_DocA
GROUP BY CustomeriD, UseriD
UNION ALL
SELECT CustomeriD, UseriD, 'DocB', COUNT(DocId)
FROM Tbl_DocB
GROUP BY CustomeriD, UseriD
UNION ALL
SELECT CustomeriD, UseriD, 'DocC', COUNT(DocId)
FROM Tbl_DocC
GROUP BY CustomeriD, UseriD
)
SELECT
docs.CustomeriD,
cust.CustomerName,
SUM(CASE WHEN usrX.UseriD is not null AND docs.Src = 'DocA' THEN docs.Total ELSE 0 END) AS Total_DocA_userX,
SUM(CASE WHEN Src = 'DocA' THEN docs.Total ELSE 0 END) AS Total_DocA,
SUM(CASE WHEN usrX.UseriD is not null AND docs.Src = 'DocB' THEN docs.Total ELSE 0 END) AS Total_DocB_userX,
SUM(CASE WHEN Src = 'DocB' THEN docs.Total ELSE 0 END) AS Total_DocB,
SUM(CASE WHEN usrX.UseriD is not null AND docs.Src = 'DocC' THEN docs.Total ELSE 0 END) AS Total_DocC_userX,
SUM(CASE WHEN Src = 'DocC' THEN docs.Total ELSE 0 END) AS Total_DocC
FROM DOCTOTALS docs
LEFT JOIN Tbl_Customer cust ON cust.CustomeriD = docs.CustomeriD
LEFT JOIN Tbl_User usr ON usr.UseriD = docs.UseriD
LEFT JOIN SPECIFICUSER usrX ON usrX.UseriD = docs.UseriD
GROUP BY docs.CustomeriD, cust.CustomerName
ORDER BY docs.CustomeriD
Those long column names could be set on the report side

SQL Select TOP 1 for each group in subquery

Good morning,
I want to alter my query in such a way, that only the top 1, filtered from h.started asc is selected.
select h.started, * from wshhistory h
join asset a on h.assetid = a.uid
inner join
(
select Count(*) as TotalLatest, a.uid, a.deleted from asset a
join wshhistory h on a.uid = h.assetid
where h.latest = 1
group by a.uid, a.deleted
having Count(*) > 1
) X
on X.uid = h.assetid
where X.deleted = 0 and h.latest = 1
order by h.assetid desc
I searched all over, and found in most posts, to use:
ROW_NUMBER() OVER (PARTITION BY a.uid ORDER BY h.started asc) as rn
But I can't seem to use this since I need use group by, and this results in the error message:
Column 'wshhistory.started' is invalid in the select list because it
is not contained in either an aggregate function or the GROUP BY
clause.
To give some extra info about my query:
I need to search where I have duplicates of Latest = 1 (table: wshhistory), of the same assetid. And then I need to set the them all on 0 except the latest one.
I think you want something like this:
with toupdate as (
select h.*,
row_number() over (partition by h.assetid order by h.started desc) as seqnum
from wshhistory h
where h.latest = 1
)
update toupdate
set latest = 0
where seqnum > 1 and
exists (select 1
from asset a
where a.uid = toupdate.assetid and a.deleted = 0
);
Sample data and desired results are much easier to work with than non-working queries.

HAVING clause on SUM column

I want to have a condition on my score column that I get from sum, but HAVING score =< 1 is not working if I put it after group by. That would have to show me projects that have good score. I am using hsqldb, what's going wrong? I get 'user lacks privelege or object not found: SCORE'
SELECT p.id, p.project_name, SUM(CASE r.type_code
WHEN 'GOOD' THEN 1
WHEN 'VERY_GOOD' THEN 1
WHEN 'BAD' THEN -1
WHEN 'VERY_BAD' THEN -1
ELSE 0 END) AS score
FROM record_project AS rp
JOIN project AS p ON p.id = rp.project_id
JOIN record AS r ON r.id = rp.record_id
GROUP BY p.id, p.project_name
HAVING score =< 1 <<<---- wrong?!
ORDER BY score DESC LIMIT 1
You should be using the whole calculated column,
SELECT p.id, p.project_name,
SUM(CASE WHEN r.type_code IN ('GOOD','VERY_GOOD') THEN 1
WHEN r.type_code IN ('BAD','VERY_BAD') THEN -1
ELSE 0 END) score
FROM record_project AS rp
JOIN project AS p ON p.id = rp.project_id
JOIN record AS r ON r.id = rp.record_id
GROUP BY p.id, p.project_name
HAVING SUM(CASE WHEN r.type_code IN ('GOOD','VERY_GOOD') THEN 1
WHEN r.type_code IN ('BAD','VERY_BAD') THEN -1
ELSE 0 END) <= 1
ORDER BY score DESC
-- LIMIT 1
You can incorporate the HAVING as a WHERE over a subquery:
SELECT * FROM (
SELECT p.id, p.project_name, SUM(CASE r.type_code
WHEN 'GOOD' THEN 1
WHEN 'VERY_GOOD' THEN 1
WHEN 'BAD' THEN -1
WHEN 'VERY_BAD' THEN -1
ELSE 0 END) AS score
FROM record_project AS rp
JOIN project AS p ON p.id = rp.project_id
JOIN record AS r ON r.id = rp.record_id
GROUP BY p.id, p.project_name) x
WHERE score =< 1
ORDER BY score DESC
LIMIT 1

distinct records with inner join in oracle joins

I am using following query to return a list of records:
Select DISTINCT A.Id Questionid,
A.Created_Date Questiondate,
Row_Number() Over ( Order By A.Created_Date Desc) Row_Id,
B.Id Answerid,
COUNT(*) OVER (partition by a.id) QCOUNT,
B.Created_Date Answerdate
From Question_Table A
Inner Join Answers_Table B
ON A.Id = B.Question_Id
AND A.Is_Active = 1
AND A.Is_Delete = 0
And B.Is_Active = 1
And B.Is_Delete = 0
And A.Category_Id = 1318221772
Answers_Table.question_id is mapped to question_table.id
Hence, for a particular question id, there can be more than one rows on answers_table.
The Output now displaying is:
QUESTIONID QUESTIONDATE ROW_ID ANSWERID QCOUNT ANSWERDATE
52776 08-DEC-12 1 31383 2 09-DEC-12
52776 08-DEC-12 2 31482 2 10-DEC-12
52719 07-DEC-12 3 31321 1 07-DEC-12
But my requirement to display output as:
QUESTIONID QUESTIONDATE ROW_ID ANSWERID QCOUNT ANSWERDATE
52776 08-DEC-12 1 31383 2 09-DEC-12
52719 07-DEC-12 3 31321 1 07-DEC-12
How can I display unique questionid (like 52776,52719) on to the output?
Depending on what you are trying to do you may be able to rewrite this with a Group By statement. However, if you want to keep your existing query, try changing this line:
Row_Number() Over (Order By A.Created_Date Desc) Row_Id,
to this:
Row_Number() Over ( partition by a.id Order By B.Created_Date ASC) Row_Id,
And then make the whole thing a subquery selecting where row_id = 1. This should just return one row per Question and only the earliest Answer for that Question.
Select * From (
Select DISTINCT A.Id Questionid,
A.Created_Date Questiondate,
Row_Number() Over ( partition by a.id Order By B.Created_Date ASC) Row_Id,
B.Id Answerid,
COUNT(*) OVER (partition by a.id) QCOUNT,
B.Created_Date Answerdate
From Question_Table A
Inner Join Answers_Table B
ON A.Id = B.Question_Id
AND A.Is_Active = 1
AND A.Is_Delete = 0
And B.Is_Active = 1
And B.Is_Delete = 0
And A.Category_Id = 1318221772
) Where ROW_ID = 1