SQL: order by calculated rate between two tables - sql

I might be confusing syntax from other languages with SQL and that's why this isn't working, my subquery is definitely incorrect.
Having the following 2 tables:
TEST_RESULTS
Student_ID Test_ID Test_Result
A1 234 90
B2 234 80
C3 345 85
D4 234 95
A1 345 95
C3 456 95
TEST_DESCRIPTION
Test_ID Test_Description Passing_Score
234 Test A 85
345 Test B 90
456 Test C 95
I want to calculate the rate of passing for each test and sort by it.
The output I am looking for:
Test_ID Test_Description students_taking students_passing rate
456 Test C 1 1 1
234 Test A 3 2 0.666666667
345 Test B 2 1 0.5
This is my query
SELECT td.Test_ID, td.Test_Description, COUNT(tr.Student_ID) as
students_taking, students_passing, students_passing/students_taking as rate
FROM
(SELECT td.Test_ID, td.Test_Description, COUNT(tr.Student_ID) as
students_passing
FROM TEST_RESULTS tr
JOIN TEST_DESCRIPTION td
on tr.Test_ID = td.Test_ID
WHERE tr.Test_Result > td.)
GROUP BY td.Test_ID, td.Test_Description
ORDER BY rate DESC, td.Test_ID, td.Test_Description
My select from select is wrong, because I am getting no results for this query.

I'm using CTE, LEFT JOIN for getting the desired result.
Try this query --
;WITH CTE
AS
(
SELECT
TD.TEST_ID,
TEST_DESCRIPTION,
COUNT(TR.STUDENT_ID) AS STUDENTS_TAKING,
COUNT(CASE WHEN TR.TEST_RESULT >= TD.PASSING_SCORES THEN
TR.STUDENT_ID END) AS STUDENTS_PASSING
FROM TEST_DESCRIPTION TD
LEFT JOIN TEST_RESULTS TR
ON TD.TEST_ID = TR.TEST_ID
GROUP BY TD.TEST_ID,
TEST_DESCRIPTION
)
SELECT
TEST_ID,
TEST_DESCRIPTION,
STUDENTS_TAKING,
STUDENTS_PASSING,
STUDENTS_PASSING / CONVERT (DECIMAL(4,2),STUDENTS_TAKING) AS RATE
FROM CTE
ORDER BY TEST_DESCRIPTION

Please check below query-
SELECT TD.TEST_ID,
TD.TEST_DESCRIPTION,
STUDENT_TAKING,
STUDENT_PASSING,
RATE
FROM TEST_DESCRIPTION TD,
(SELECT TR.TEST_ID,COUNT(TR.STUDENT_ID) "STUDENT_TAKING",
COUNT(CASE WHEN TEST_RESULT>=PASSING_SCORE THEN STUDENT_ID END) STUDENT_PASSING,
TO_NUMBER(TO_CHAR(COUNT(CASE WHEN TEST_RESULT>=PASSING_SCORE THEN STUDENT_ID END)/COUNT(TR.STUDENT_ID),'9999.99')) RATE
FROM TEST_RESULTS TR,TEST_DESCRIPTION TD
WHERE TR.TEST_ID=TD.TEST_ID
GROUP BY TR.TEST_ID)SUB
WHERE SUB.TEST_ID=TD.TEST_ID ORDER BY RATE DESC;

SELECT td.Test_ID, td.Test_Description,
students_taking = counts.students_taking,
students_passing = counts.students_passing,
rate = counts.rate
FROM TEST_DESCRIPTION td
OUTER APPLY (
SELECT
students_taking = COUNT(1),
students_passing = COUNT(CASE WHEN tr.Test_Result > td.Passing_score THEN 1 ELSE NULL END),
rate = IIF(COUNT(1) <> 0, COUNT(CASE WHEN tr.Test_Result > td.Passing_score THEN 1 ELSE NULL END) / CAST(COUNT(1) AS FLOAT), 0)
FROM TEST_RESULTS tr
WHERE tr.Test_ID = td.Test_ID
) counts
ORDER BY counts.rate DESC, td.Test_ID

Related

To select data from multiple records in SQL Server having a common ID

I need to select/concat data from 2 tables in SQL Server I'm using Left Join, but the data is returned as multiple records.
Below are the sample tables
Table1
Id Name Age
1 Sk 20
2 Rb 30
Table2
ID Bike Price Table1Id
1 RX 200 1
2 CD 250 1
3 FZ 300 1
4 R1 400 2
The desired output is
ID Name Age Bike1 Price1 Bike2 Price2 Bike3 Price3
1 Sk 20 RX 200 CD 250 FZ 300
2 Rb 30 R1 400 NULL NULL NULL NULL
A sample format of the query I'm using
SELECT A.ID, A.Name, B.Bike, B.Price FROM Table1 A LEFT JOIN Table2 B ON
A.id = B.Table1Id order by A.id
The output I'm getting from the above query is
ID Name Age Bike Price
1 Sk 20 RX 200
1 Sk 20 CD 250
1 Sk 20 FZ 300
2 Rb 30 R1 400
I need the data as one record for a particular ID and not multiple records (As seen in the desired output). Tired using offset, but offset will return only limited result not the entire records.
Any suggestions on how this can be achieved?
If you know the maximum number of bikes per person, you can use conditional aggregation:
SELECT ID, Name,
MAX(CASE WHEN seqnm = 1 THEN Bike END) as bike_1,
MAX(CASE WHEN seqnm = 1 THEN Price END) as price_1,
MAX(CASE WHEN seqnm = 2 THEN Bike END) as bike_2,
MAX(CASE WHEN seqnm = 2 THEN Price END) as price_2,
MAX(CASE WHEN seqnm = 3 THEN Bike END) as bike_3,
MAX(CASE WHEN seqnm = 3 THEN Price END) as price_3
FROM (SELECT A.ID, A.Name, B.Bike, B.Price,
ROW_NUMBER() OVER (PARTITION BY A.id ORDER BY B.Price) as seqnum
FROM Table1 A LEFT JOIN
Table2 B
ON A.id = B.Table1Id
) ab
GROUP BY ID, Name,
ORDER BY id

Counting number of positive value in a query

I'm working on the following query and table
SELECT dd.actual_date, dd.week_number_overall, sf.branch_id, AVG(sf.overtarget_qnt) AS targetreach
FROM sales_fact sf, date_dim dd
WHERE dd.date_id = sf.date_id
AND dd.week_number_overall BETWEEN 88-2 AND 88
AND sf.branch_id = 1
GROUP BY dd.actual_date, branch_id, dd.week_number_overall
ORDER BY dd.actual_date ASC;
ACTUAL_DATE WEEK_NUMBER_OVERALL BRANCH_ID TARGETREACH
----------- ------------------- ---------- -----------
13/08/14 86 1 -11
14/08/14 86 1 12
15/08/14 86 1 11.8
16/08/14 86 1 1.4
17/08/14 86 1 -0.2
19/08/14 86 1 7.2
20/08/14 87 1 16.6
21/08/14 87 1 -1.4
22/08/14 87 1 14.4
23/08/14 87 1 2.8
24/08/14 87 1 18
26/08/14 87 1 13.4
27/08/14 88 1 -1.8
28/08/14 88 1 10.6
29/08/14 88 1 7.2
30/08/14 88 1 14
31/08/14 88 1 9.6
02/09/14 88 1 -3.2
the "TargetReach" column shows whether target has been reach or not.
A negative value means target wasn't reached on that day.
How can I get calculate the number of ROW with positive value for this query?
that will show something like:
TOTAL_POSITIVE_TARGET_REACH WEEK_NUMBER_OVERALL
--------------------------- ------------------
13 88
I have tried to use CASE but still not working right.
Thanks a lot.
You want to use conditional aggregation:
with t as (
<your query here>
)
select week_number_overall, sum(case when targetreach > 0 then 1 else 0 end)
from t
group by week_number_overall;
However, I would rewrite your original query to use proper join syntax. Then the query would look like:
SELECT week_number_overall,
SUM(CASE WHEN targetreach > 0 THEN 1 ELSE 0 END)
FROM (SELECT dd.actual_date, dd.week_number_overall, sf.branch_id, AVG(sf.overtarget_qnt) AS targetreach
FROM sales_fact sf JOIN
date_dim dd
ON dd.date_id = sf.date_id
WHERE dd.week_number_overall BETWEEN 88-2 AND 88 AND sf.branch_id = 1
GROUP BY dd.actual_date, branch_id, dd.week_number_overall
) t
GROUP BY week_number_overall
ORDER BY week_number_overall;
THe difference between a CTE (the first solution) and a subquery is (in this case) just a matter of preference.
SELECT WEEK_NUMBER_OVERALL, COUNT(*) TOTAL_POSITIVE_TARGET_REACH
FROM (your original query)
WHERE TARGETREACH >= 0
GROUP BY WEEK_NUMBER_OVERALL
select sum( decode( sign( TARGETREACH ) , -1 , 0 , 0 , 0 , 1 , 1 ) )
from ( "your query here" );
Use HAVING Clause
SELECT dd.actual_date, dd.week_number_overall, sf.branch_id, AVG(sf.overtarget_qnt) AS targetreach
FROM sales_fact sf, date_dim dd
WHERE dd.date_id = sf.date_id
AND dd.week_number_overall BETWEEN 88-2 AND 88
AND sf.branch_id = 1
GROUP BY dd.actual_date, branch_id, dd.week_number_overall
HAVING AVG(sf.overtarget_qnt)>0
ORDER BY dd.actual_date ASC;
Using decode(), sign() get both positive count & negative count.
drop table test;
create table test (
key number(5),
value number(5));
insert into test values ( 1, -9 );
insert into test values ( 2, -8 );
insert into test values ( 3, 10 );
insert into test values ( 4, 12 );
insert into test values ( 5, -9 );
insert into test values ( 6, 8 );
insert into test values ( 7, 51 );
commit;
select sig , count ( sig ) from
(
select key, ( (decode( sign( value ) , -1 , '-ve' , 0 , 'zero' , 1 , '+ve' ) ) ) sig
from test
)
group by sig
SIG COUNT(SIG)
---- ----------------------
+ve 4
-ve 3

How to count for each ID, how many of each Type of outcomes(1,2,3) has that ID diagnosised?

I have tried many times but i have no idea how to group the answer for each technician for each diagnosis outcome (Only three types 1,2,3) that they have? (For example, how many diagnosis outcome 1 did technicianID 54 have .)
I try to put in the count function but it is not works.Could someone give me some idea in SQL?
It is what i found in the database.
TechnicianID FirstName
------------ -------------
54 Wayne
TechnicianID DiagnosisOutcomeID
------------ ------------------
54 1
54 2
54 1
54 3
The Result should be
TechnicianID FirstName DiagnosisOutcome=1 DiagnosisOutcome=2 DiagnosisOutcome=3
------------ -------------
54 Wayne
ThankYou
Since you have not mentioned any RDBMS, the query below will work on many RDBMS,
SELECT a.TechnicianID,
a.FirstName,
SUM(CASE WHEN b.DiagnosisOutcomeID = 1 THEN 1 ELSE 0 END) DiagnosisOutcomeID1,
SUM(CASE WHEN b.DiagnosisOutcomeID = 2 THEN 1 ELSE 0 END) DiagnosisOutcomeID2,
SUM(CASE WHEN b.DiagnosisOutcomeID = 3 THEN 1 ELSE 0 END) DiagnosisOutcomeID3
FROM Table1 a
INNER JOIN Table2 b
ON a.TechnicianID = b.TechnicianID
GROUP BY a.TechnicianID, a.FirstName
SQLFiddle Demo
If you are using RDBMS that supports PIVOT function,
SELECT TechnicianID,
FirstName,
DiagnosisOutcomeID1,
DiagnosisOutcomeID2,
DiagnosisOutcomeID3
FROM
(
SELECT a.TechnicianID,
a.FirstName,
'DiagnosisOutcomeID' + CAST(b.DiagnosisOutcomeID AS VARCHAR(5)) AS DiagnosisOutcomeID
FROM Table1 a
INNER JOIN Table2 b
ON a.TechnicianID = b.TechnicianID
) data
PIVOT
(
COUNT(DiagnosisOutcomeID)
FOR DiagnosisOutcomeID IN (DiagnosisOutcomeID1,DiagnosisOutcomeID2,DiagnosisOutcomeID3)
) pvt
ORDER BY TechnicianID
SQLFiddle Demo
The CASE statement is what you need.
Have a look at this SQL Fiddle
SELECT T.FirstName
,SUM(CASE WHEN DiagnosisOutcomeID = 1 THEN 1 ELSE 0 END) AS OUTCOME1
,SUM(CASE WHEN DiagnosisOutcomeID = 2 THEN 1 ELSE 0 END) AS OUTCOME2
,SUM(CASE WHEN DiagnosisOutcomeID = 3 THEN 1 ELSE 0 END) AS OUTCOME3
FROM Tech T
INNER JOIN
Diagnosis D
ON T.TechnicianID = D.TechnicianID
GROUP BY
T.FirstName

How can I get the first result for each account in this SQL query?

I'm trying to write a query that follows this logic:
Find the first following status code of an account that had a previous status code of X.
So if I have a table of:
id account_num status_code
64 1 X
82 1 Y
72 2 Y
87 1 Z
91 2 X
103 2 Z
The results would be:
id account_num status_code
82 1 Y
103 2 Z
I've come up with a couple of solutions but I'm not all that great with SQL and so they've been pretty inelegeant thus far. I was hoping that someone here might be able to point me in the right direction.
View:
SELECT account_number, id
FROM table
WHERE status_code = 'X'
Query:
SELECT account_number, min(id)
FROM table
INNER JOIN view
ON table.account_number = view.account_number
WHERE table.id > view.id
At this point I have the id that I need but I'd have to write ANOTHER query that uses the id tolook up the status_code.
Edit: To add some context, I'm trying to find calls that have a status_code of X. If a call has a status_code of X we want to dial it a different way the next time we make an attempt. The aim of this query is to provide a report that will show the results of the second dial if the first dial resulted an X status code.
Here's a SQL Server solution.
UPDATE
The idea is to avoid a number of NESTED LOOP joins as proposed by Olaf because they roughly have O(N * M) complexity and thus extremely bad for your performance. MERGED JOINS complexity is O(NLog(N) + MLog(M)) which is much better for real world scenarios.
The query below works as follows:
RankedCTE is a subquery that assigns a row number to each id partioned by account and sorted by id which represents the time. So for the data below the output of this
SELECT
id,
account_num,
status_code,
ROW_NUMBER() OVER (PARTITION BY account_num ORDER BY id DESC) AS item_rank
FROM dbo.Test
would be:
id account_num status_code item_rank
----------- ----------- ----------- ----------
87 1 Z 1
82 1 Y 2
64 1 X 3
103 2 Z 1
91 2 X 2
72 2 Y 3
Once we have them numbered we join the result on itself like this:
WITH RankedCTE AS
(
SELECT
id,
account_num,
status_code,
ROW_NUMBER() OVER (PARTITION BY account_num ORDER BY id DESC) AS item_rank
FROM dbo.Test
)
SELECT
*
FROM
RankedCTE A
INNER JOIN RankedCTE B ON
A.account_num = B.account_num
AND A.item_rank = B.item_rank - 1
which will give us an event and a preceding event in the same table
id account_num status_code item_rank id account_num status_code item_rank
----------- ----------- ----------- ----------- ----------- ----------- ----------- -----------
87 1 Z 1 82 1 Y 2
82 1 Y 2 64 1 X 3
103 2 Z 1 91 2 X 2
91 2 X 2 72 2 Y 3
Finally, we just have to take the preceding event with code "X" and the event with code not "X":
WITH RankedCTE AS
(
SELECT
id,
account_num,
status_code,
ROW_NUMBER() OVER (PARTITION BY account_num ORDER BY id DESC) AS item_rank
FROM dbo.Test
)
SELECT
A.id,
A.account_num,
A.status_code
FROM
RankedCTE A
INNER JOIN RankedCTE B ON
A.account_num = B.account_num
AND A.item_rank = B.item_rank - 1
AND A.status_code <> 'X'
AND B.status_code = 'X'
Query plans for this query and #Olaf Dietsche solution (one of the versions) are below.
Data setup script
CREATE TABLE dbo.Test
(
id int not null PRIMARY KEY,
account_num int not null,
status_code nchar(1)
)
GO
INSERT dbo.Test (id, account_num, status_code)
SELECT 64 , 1, 'X' UNION ALL
SELECT 82 , 1, 'Y' UNION ALL
SELECT 72 , 2, 'Y' UNION ALL
SELECT 87 , 1, 'Z' UNION ALL
SELECT 91 , 2, 'X' UNION ALL
SELECT 103, 2, 'Z'
SQL Fiddle with subselect
select id, account_num, status_code
from mytable
where id in (select min(t1.id)
from mytable t1
join mytable t2 on t1.account_num = t2.account_num
and t1.id > t2.id
and t2.status_code = 'X'
group by t1.account_num)
and SQL Fiddle with join, both for MS SQL Server 2012, both returning the same result.
select id, account_num, status_code
from mytable
join (select min(t1.id) as min_id
from mytable t1
join mytable t2 on t1.account_num = t2.account_num
and t1.id > t2.id
and t2.status_code = 'X'
group by t1.account_num) t on id = min_id
SELECT MIN(ID), ACCOUNT_NUM, STATUS_CODE FROM (
SELECT ID, ACCOUNT_NUM, STATUS_CODE
FROM ACCOUNT A1
WHERE EXISTS
(SELECT 1
FROM ACCOUNT A2
WHERE A1.ACCOUNT_NUM = A2.ACCOUNT_NUM
AND A2.STATUS_CODE = 'X'
AND A2.ID < A1.ID)
) SUB
GROUP BY ACCOUNT_NUM
Here's an SQLFIDDLE
Here's query, with your data, checked under PostgreSQL:
SELECT t0.*
FROM so13594339 t0 JOIN
(SELECT min(t1.id), t1.account_num
FROM so13594339 t1, so13594339 t2
WHERE t1.account_num = t2.account_num AND t1.id > t2.id AND t2.status_code = 'X'
GROUP BY t1.account_num
) z
ON t0.id = z.min AND t0.account_num = z.account_num;

How to compute the diff between records?

My table records is like below
ym cnt
200901 57
200902 62
200903 67
...
201001 84
201002 75
201003 75
...
201101 79
201102 77
201103 80
...
I want to computer the diff between current month and per month .
the result would like below ...
ym cnt diff
200901 57 57
200902 62 5 (62 - 57)
200903 67 5 (67 - 62)
...
201001 84 ...
201002 75
201003 75
...
201101 79
201102 77
201103 80
...
Can anyone told me how to wrote a sql to got the result and with a good performance ?
UPDATE:
sorry for simple words
my solution is
step1: input the currentmonth data into temp table1
step2: input the permonth data into temp table2
step3: left join 2 tables to compute the result
Temp_Table1
SELECT (ym - 1) as ym , COUNT( item_cnt ) as cnt
FROM _table
GROUP BY (ym - 1 )
order by ym
Temp_Table2
SELECT ym , COUNT( item_cnt ) as cnt
FROM _table
GROUP BY ym
order by ym
select ym , (b.cnt - a.cnt) as diff from Temp_Table2 a
left join Temp_Table1 b
on a.ym = b.ym
*If i want to compare the diff between the month in this year and last year
I can only change the ym - 1 to ym - 100*
but , actually , the group by key is not only ym
there is max 15 keys and max 100 millions records
so , I wonder a good solution can easy to manager the source
and good performance.
For MSSQL, this has one reference to the table, so potentially it can be faster (maybe not) than left join which has two references to the table:
-- ================
-- sample data
-- ================
declare #t table
(
ym varchar(6),
cnt int
)
insert into #t values ('200901', 57)
insert into #t values ('200902', 62)
insert into #t values ('200903', 67)
insert into #t values ('201001', 84)
insert into #t values ('201002', 75)
insert into #t values ('201003', 75)
-- ===========================
-- solution
-- ===========================
select
ym2,
diff = case when cnt1 is null then cnt2
when cnt2 is null then cnt1
else cnt2 - cnt1
end
from
(
select
ym1 = max(case when k = 2 then ym end),
cnt1 = max(case when k = 2 then cnt end),
ym2 = max(case when k = 1 then ym end),
cnt2 = max(case when k = 1 then cnt end)
from
(
select
*,
rn = row_number() over(order by ym)
from #t
) t1
cross join
(
select k = 1 union all select k = 2
) t2
group by rn + k
) t
where ym2 is not null
Can anyone told me how to wrote a sql to got the result
Absolutely. Simply get the row with the next highest date, and subtract.
and with a good performance ?
No. Relational databases are not really meant to be traversed linearly, and even using indexes appropriately would require a virtual linear traversal.