SQL loop, condition queries - sql

I have table 'db' which has these columns:
sname(student name)
cname (course name)
year ( the year when student take)
qtr ( the quarter when student take) : W , S, F (W>S>F)
grade
What I want to do is that list the students that gpa has increased every quarter (previous grade < current gpa). Students may have gaps between quarters.
SELECT a.sname
FROM db a
WHERE EXISTS (SELECT *
FROM db b
WHERE a.sname = b.sname
AND a.year = b.year
AND a.qtr < b.qtr
AND a.grade > b.grade)
UNION
After union, I will do the case when a.year > b.year.
This is what I am doing right now - is it correct way to do it this way?
I have no idea how I can loop in SQL queries...
Any advise will be appreciate. Thank you

If you're using the current version of sqlite (3.25.X), this is really easy to do using the lag() window function. In the below, I'm using numeric values for grades instead of letters because it's much easier to work with, and assuming the following table definition (Based on your description) and sample rows in it:
CREATE TABLE IF NOT EXISTS grades(
sname TEXT NOT NULL
, year INTEGER NOT NULL
, qtr INTEGER NOT NULL
, cname TEXT NOT NULL
, grade NUMERIC
, PRIMARY KEY (sname, year, qtr, cname)
) WITHOUT ROWID;
INSERT INTO grades(sname, cname, year, qtr, grade) VALUES
('Bob', 'Math', 2017, 3, 2.0), ('Bob', 'Math', 2017, 4, 2.5),
('Bob', 'Math', 2018, 1, 3.0), ('Amy', 'Math', 2017, 3, 4.0),
('Amy', 'Math', 2017, 4, 3.5), ('Amy', 'Math', 2018, 1, 4.0),
('Bob', 'History', 2017, 3, 3.5), ('Bob', 'History', 2017, 4, 3.0),
('Bob', 'History', 2018, 1, 3.5), ('Amy', 'History', 2017, 3, 2.5),
('Amy', 'History', 2017, 4, 3.5), ('Amy', 'History', 2018, 1, 4.0);
I assume that when you said previous grade < current gpa you meant previous gpa to match with gpa has increased every quartely[sic].
First, a query that calculates each student's GPA for each quarter and includes the previous GPA as well in each row (Thanks to lag()):
SELECT sname, year, qtr
, avg(grade) AS gpa
, lag(avg(grade), 1, 0.0)
OVER (PARTITION BY sname ORDER BY year, qtr) AS prev_gpa
FROM grades
GROUP BY sname, year, qtr
ORDER BY sname, year, qtr;
This produces:
sname year qtr gpa prev_gpa
---------- ---------- ---------- ---------- ----------
Amy 2017 3 3.25 0.0
Amy 2017 4 3.5 3.25
Amy 2018 1 4.0 3.5
Bob 2017 3 2.75 0.0
Bob 2017 4 2.75 2.75
Bob 2018 1 3.25 2.75
As you can see, with this sample data, Amy has an always-increasing GPA, while Bob doesn't. So, the issue is how to filter the results to just her? The answer to that lies in using HAVING with a GROUP BY. It's complicated a bit because values computed by window functions can only appear in the select column list and ORDER BY clauses, so I shove the above query into a CTE to work around that restriction:
WITH gpas AS (
SELECT sname, year, qtr
, avg(grade) AS gpa
, lag(avg(grade), 1, 0.0)
OVER (PARTITION BY sname ORDER BY year, qtr) AS prev_gpa
FROM grades
GROUP BY sname, year, qtr)
SELECT sname
FROM gpas
GROUP BY sname
HAVING sum(CASE WHEN gpa > prev_gpa THEN 1 ELSE 0 END) = count(gpa)
ORDER BY sname;
which produces
sname
----------
Amy
The GROUP BY sname HAVING ... part filters out the students where at least one of their rows doesn't have an increased GPA compared to their previous one. It's worth spending some time reading up on grouping, as it's probably the most difficult basic concept to grasp and also very useful and powerful.

Related

Calculate Date difference between multiple rows SQL

I need to calculate the date difference between multiple rows. The scenario is I have a vehicle that can do inspections throughout the month as well as when the vehicle is assigned to a different project. I want to calculate that how many days that a vehicle is assigned to the project per month or previous month. I have tried multiple ways and I can't get even closer. I am relatively new to stack overflow. Apologies if anything is missing. Please let me know if this can be done. Thank you.
All the columns are in one single table if that helps. Please let me know the query on how to achieve this
I am using SQL server 2017.
Original Data
Expected Output
I am not proud of this solution, but I think it works for you. My approach was to create a table of days and then look at which project the vehicle was assigned to each day. Finally, aggregate by month and year to get the results. I had to do this as a script since you can use aggregate functions in the definitions of recursive CTEs, but you may find a way to do this without needing a recursive CTE.
I created a table variable to import your data so I could write this. Note, I added an extra assignment to test assignments that spanned months.
DECLARE #Vehicles AS TABLE
(
[VehicleID] INT NOT NULL,
[Project] CHAR(2) NOT NULL,
[InspectionDate] DATE NOT NULL
);
INSERT INTO #Vehicles
(
[VehicleID],
[Project],
[InspectionDate]
)
VALUES
(1, 'P1', '2021-08-20'),
(1, 'P1', '2021-09-05'),
(1, 'P2', '2021-09-15'),
(1, 'P3', '2021-09-20'),
(1, 'P2', '2021-10-10'),
(1, 'P1', '2021-10-20'),
(1, 'P3', '2021-10-21'),
(1, 'P2', '2021-10-22'),
(1, 'P4', '2021-11-15'),
(1, 'P4', '2021-11-25'),
(1, 'P4', '2021-11-30'),
(1, 'P1', '2022-02-05');
DECLARE #StartDate AS DATE, #EndDate AS DATE;
SELECT #StartDate = MIN([InspectionDate]), #EndDate = MAX([InspectionDate])
FROM #Vehicles;
;WITH [seq]([n])
AS (SELECT 0 AS [n]
UNION ALL
SELECT [n] + 1
FROM [seq]
WHERE [n] < DATEDIFF(DAY, #StartDate, #EndDate)),
[days]
AS (SELECT DATEADD(DAY, [n], #StartDate) AS [d]
FROM [seq]),
[inspections]
AS (SELECT [VehicleID],
[Project],
[InspectionDate],
LEAD([InspectionDate], 1) OVER (PARTITION BY [VehicleID]
ORDER BY [InspectionDate]
) AS [NextInspectionDate]
FROM #Vehicles),
[assignmentsByDay]
AS (SELECT [d].[d], [i].[VehicleID], [i].[Project]
FROM [days] AS [d]
INNER JOIN [inspections] AS [i]
ON [d].[d] >= [i].[InspectionDate]
AND [d] < [i].[NextInspectionDate])
SELECT [assignmentsByDay].[VehicleID],
[assignmentsByDay].[Project],
MONTH([assignmentsByDay].[d]) AS [month],
YEAR([assignmentsByDay].[d]) AS [year],
COUNT(*) AS [daysAssigned]
FROM [assignmentsByDay]
GROUP BY [assignmentsByDay].[VehicleID],
[assignmentsByDay].[Project],
MONTH([assignmentsByDay].[d]),
YEAR([assignmentsByDay].[d])
ORDER BY [year], [month], [assignmentsByDay].[VehicleID], [assignmentsByDay].[Project]
OPTION(MAXRECURSION 0);
And the output is:
VehicleID
Project
month
year
daysAssigned
1
P1
8
2021
12
1
P1
9
2021
14
1
P2
9
2021
5
1
P3
9
2021
11
1
P1
10
2021
1
1
P2
10
2021
20
1
P3
10
2021
10
1
P2
11
2021
14
1
P4
11
2021
16
1
P4
12
2021
31
1
P4
1
2022
31
1
P4
2
2022
4
I think you are looking for this:
select vehicleId
, Project
, month(inspectiondate) month
, year(inspectiondate) year
, datediff(day , min(inspectiondate), case when max(inspectiondate) = min(inspectiondate) then eomonth(min(inspectiondate)) else max(inspectiondate) end) days
from Vehicles
group by vehicleId, Project , month(inspectiondate), year(inspectiondate)
This query in for each month/year for each specific vehicle in a project in that month/year , you get the max and min inspection date and calculate the difference.
db<>fiddle here

setting a flag for score change in SQL

I have a table with exam scores for different weeks. I wanted to create an extra column with the score difference, like if score decreased by 0-5 then 1, 5-9 then 2, 10+ then 3 and if score increases then 4. Here is the sample data that I have with me in the table.
--DROP TABLE #Scores
CREATE TABLE #Scores (
NAME varchar(10),
Grade varchar(10),
Subject varchar(25),
Exam_Date datetime,
Score int
)
INSERT INTO #Scores
VALUES ('Sam', 'XI', 'Maths', '2016-08-01 15:47:29.533', 38),
('Sam', 'XI', 'Maths', '2016-07-25 15:47:29.533', 50),
('Mike', 'XI', 'Maths', '2016-08-01 15:47:29.533', 50),
('Mike', 'XI', 'Maths', '2016-07-25 15:47:29.533', 45)
SELECT * FROM #Scores
Thanks in adavance
You would use lag() and case:
select s.*,
(case when score - prev_score < 0 then 4
when score - prev_score <= 5 then 1
when score - prev_score <= 9 then 2
else 3
end) as score_diff
from (select s.*,
lag(score) over (partition by name, subject order by exam_date) as prev_score
from #scores s
) s;
Thanks to #Gordon Linoff, I change the code a little bit. The logic is right, just change the math a little.
select s.*,
(case when score - prev_score > 0 then 4
when score - prev_score between -5 and 0 then 1
when score - prev_score between -9 and -5 then 2
else 3
end) as score_diff
from (select s.*,
lag(score) over (partition by name, subject order by exam_date) as prev_score
from #scores s
) s;
Result is captured and shown below:
Consider a further step of normalization. Keep the scores in a separate table. Relate the student to the scores table.
You have to decide how you are going to reference the previous score to compare to the current. If you create an additional field to either store the change from last score then you can have a calculated field that shows the current score, or, store the previous score in a field along side the new score, then have a calculated field show the change between the two.

Calculate number of leaves taken by Employee in a month in SQL Server

How can I calculate number of leaves taken by Employee in a month in SQL Server?
Empid Leaveid Fromdate Todate No of days
100 L1 2008-05-10 2008-05-13 3
100 L2 2008-05-20 2008-05-21 1
100 L3 2008-05-25 2008-06-05 12
100 L4 2009-01-20 2009-01-22 2
100 L5 2009-02-14 2009-02-20 6
100 L6 2009-02-28 2009-02-28 1
Use SUM and GROUP BY.
SELECT Empid, SUM([No of Days]) AS Days
FROM leavetable
GROUP BY Empid
Read more here GROUP BY and here SUM.
This would give you:
Empid Days
100 25
Or if you mean count the amount of times an employee has been off use Count.
SELECT Empid, Count(Leaveid) AS LeaveTotal
FROM leavetable
GROUP BY Empid
Read more here COUNT.
This would give you:
Empid LeaveTotal
100 6
CREATE TABLE #EmpLeave (EmpId INT, LeaveID VARCHAR(5), FromDate Date, Todate Date, NoOfDays INT)
INSERT INTO #EmpLeave VALUES
(100, 'L1', '2008-05-10', '2008-05-13', 4),
(100, 'L2', '2008-05-20', '2008-05-21', 2),
(100, 'L3', '2008-05-25', '2008-06-05', 12),
(100, 'L4', '2009-01-20', '2009-01-22', 3),
(100, 'L5', '2009-02-14', '2009-02-20', 7),
(100, 'L6', '2009-02-28', '2009-02-28', 1)
;With CTE_leave AS (
select * from #EmpLeave where Month(FromDate) <> Month(ToDate) )
SELECT a.EMPID,DATENAME(MONTH,FromDate ) Month ,SUM(a.LeaveCount) LeaveTaken
FROM
(
SELECT C.EMPID,C.LeaveID, C.FromDate, DATEDIFF(DD,C.FromDate,EOMonth(FromDate) ) + 1 LeaveCount
FROM CTE_Leave C
UNION
SELECT C.EMPID, C.LeaveID, DATEADD(DD,1, EOMonth(FromDate)) fromDate, DATEDIFF(DD,DATEADD(DD,1, EOMonth(FromDate)), C.ToDate ) + 1 LeaveCount
FROM CTE_Leave C
UNION
SELECT EMPID, LeaveID, FromDate, DATEDIFF(DD, FromDate, Todate) + 1 LeaveCount FROM #EmpLeave where Month(FromDate) = Month(ToDate)
) a
group by a.EMPID, DATENAME(MONTH,FromDate )
#Anchalose find above possible solution. Let us know solves your query.
Please do post schema details along with the desired result also while posting your query, it will help us understand the problem better.
#Matt, please refrain from using cuss words and try to post the solution as per the requirement.

How do I fetch data grouped by month and year including the period with no data?

Relatively new to SQL and I am stumped on this little issue. This doesn't seem to be very difficult to do, but I just can't seem to figure it out.
I am trying to get a count of transactions from a table, but I can't seem to get sql to get me to show all of the months instead of only the months and the year that the transactions occured in.
Here is the query:
SELECT YEAR(dbo.countproject.trans_date) AS [TransYear]
, MONTH (dbo.countproject.trans_date) AS [TransMonth]
, COUNT(Id) AS TransNum
FROM dbo.countproject
WHERE dbo.countproject.make_name = 'Honda'
AND dbo.countproject.model_name = 'Civic'
AND dbo.countproject.type = 'Sale'
AND dbo.countproject.trans_type LIKE '%%EU'
AND dbo.countproject.mfr = '2000'
GROUP BY YEAR(dbo.countproject.trans_date)
, MONTH(dbo.countproject.trans_date)
ORDER BY YEAR(dbo.countproject.trans_date)
The query returns the following result set:
| TransYear | TransMonth | TransNum |
|-----------|------------|----------|
| 2004 | 1 | 5 |
| 2004 | 3 | 1 |
| 2005 | 4 | 2 |
and so forth....
I am trying to get it to show all the months and years even if the value is NULL.
I tried creating a new table which will have the year and the month as columns to get it to join somehow, but I am lost.
Any help would be appreciated.
If you are using SQL Server 2005 or above, you could use Common Table Expressions (CTE) to get the desired result. Below example shows how you can fetch the results as you had described in the question.
Click here to view the demo in SQL Fiddle.
Description:
Create and insert statements create the table and populates with some sample data. I have created the table based on the query provided in the question.
The statement within the WITH clause is executing a recursive expression. In this case the SELECT above the UNION ALL fetches the minimum and maximum dates available in the table dbo.countproject
Once the minimum date is fetched, the second SELECT statement after the UNION ALL increments the date in 1 month intervals until the recursive expression reaches the maximum date available in the table.
The recursive CTE has produced all the available dates possible. This output is available in the table named alltransactions.
We have to join this CTE output alltransactions with the actual table countproject using LEFT OUTER JOIN since we want to show all years and months even if there are no transactions.
The tables alltransactions and countproject are joined on the year and month parts of the date. The query then applies the necessary filters in the WHERE clause and then groups the data by year and month before ordering it by year and month.
You can notice from the sample data that the earliest date in the table is 2004-07-01 and the latest date is 2005-12-01. Hence the output shows from year 2004 / month 07 till year 2005 / month 12.
Hope that helps.
Script:
CREATE TABLE dbo.countproject
(
id INT NOT NULL IDENTITY
, trans_date DATETIME NOT NULL
, make_name VARCHAR(20) NOT NULL
, model_name VARCHAR(20) NOT NULL
, type VARCHAR(20) NOT NULL
, trans_type VARCHAR(20) NOT NULL
, mfr INT NOT NULL
);
INSERT INTO dbo.countproject (trans_date, make_name, model_name, type, trans_type, mfr) VALUES
('1900-01-01', 'Honda', 'Civic', 'Sale', 'EU', 2000),
('1900-01-01', 'Toyota', 'Corolla', 'Sale', 'EU', 2000),
('2004-07-01', 'Nissan', 'Altima', 'Sale', 'EU', 2000),
('2005-12-01', 'Toyota', 'Camry', 'Sale', 'EU', 2000),
('2004-04-01', 'Ford', 'Focus', 'Sale', 'EU', 2000),
('2005-08-01', 'Honda', 'Civic', 'Sale', 'EU', 2000),
('2005-11-01', 'Toyota', 'Camry', 'Sale', 'EU', 2000),
('2004-08-01', 'Toyota', 'Corolla', 'Sale', 'EU', 2000),
('2005-12-01', 'Honda', 'Civic', 'Sale', 'EU', 2000),
('2004-07-01', 'Honda', 'Civic', 'Sale', 'EU', 2000),
('2004-11-01', 'Honda', 'Civic', 'Sale', 'EU', 2000),
('2005-08-01', 'Honda', 'Civic', 'Sale', 'EU', 2000);
;WITH alltransactions
AS
(
SELECT MIN(trans_date) AS continuousdate
, MAX(trans_date) AS maximumdate
FROM dbo.countproject
WHERE trans_date <> '1900-01-01'
UNION ALL
SELECT DATEADD(MONTH, 1, continuousdate) AS continuousdate
, maximumdate
FROM alltransactions
WHERE DATEADD(MONTH, 1, continuousdate) <= maximumdate
)
SELECT YEAR(at.continuousdate) AS [Year]
, MONTH(at.continuousdate) AS [Month]
, COUNT(cp.trans_date) AS [Count]
FROM alltransactions at
LEFT OUTER JOIN countproject cp
ON YEAR(at.continuousdate) = YEAR(cp.trans_date)
AND MONTH(at.continuousdate) = MONTH(cp.trans_date)
AND cp.make_name = 'Honda'
and cp.model_name = 'Civic'
and cp.type = 'Sale'
and cp.trans_type LIKE '%EU'
and cp.mfr = '2000'
GROUP BY YEAR(at.continuousdate)
, MONTH(at.continuousdate)
ORDER BY [Year]
, [Month];
Output:
Year Month Count
----- ------ -----
2004 4 0
2004 5 0
2004 6 0
2004 7 1
2004 8 0
2004 9 0
2004 10 0
2004 11 1
2004 12 0
2005 1 0
2005 2 0
2005 3 0
2005 4 1
2005 5 0
2005 6 0
2005 7 0
2005 8 2
2005 9 0
2005 10 0
2005 11 0
2005 12 1
You have to use an LEFT or RIGHT OUTER JOIN!
Here is an easy sample: http://www.w3schools.com/sql/sql_join_left.asp
You should get it done by yourself.
Greetings
Alas, the SQL statement can only return the data in the table. If you want all months, you need either a table with the year/month combinations you are intererested in or, preferably, a calendar table with all days and information about them.
With a calendar table, your query have a from clause that looked like:
from
(
select distinct year(date) as yr, month(date) as mon
from calendar c
where date between <earliest> and <latest>
) c
left outer join CountTable ct
on c.yr = year(ct.trans_date)
and c.mon = month(ct.trans_date)

SQL Grouping By

Using ORACLE SQL.
I have a table 'Employees' with one of the attributes 'hire_date' . My task (book exercise) is to write a SELECT that will show me how many employees were hired in 1995, 1996, 1997 and 1998 .
Something like:
TOTAL 1995 1996 1997 1998
-----------------------------------------
20 4 5 29 2
Individually is easy to count the number of employees for every year, eg:
SELECT
COUNT(*),
FROM
employees e
WHERE
e.hire_date like '%95'
But I am having difficulties when I have to 'aggregate' the data in the needed format .
Any suggestions ?
I'm assuming your hire_date is a varchar2, since you are doing a "like" clause in your example.
Will a simple table with one row per year do?
If so, try this in Oracle:
select case grouping(hire_date)
when 0 then hire_date
else 'TOTAL'
end hire_date,
count(hire_date) as count_hire_date
from employees
group by rollup(hire_date);
That should give something like:
hire_date count_hire_date
1995 10
1996 20
1997 30
TOTAL 60
If you do need to pivot your results into something like you've shown in your question, then you can do the following if you know the distinct set of years prior to running the query. So for example, if you knew that you only had 1995, 1996 and 1997 in your table, then you could pivot the results using this:
SELECT
MAX(CASE WHEN hire_date = 'TOTAL' THEN ilv.count_hire_date END) total,
MAX(CASE WHEN hire_date = '1995' THEN ilv.count_hire_date END) count_1995,
MAX(CASE WHEN hire_date = '1996' THEN ilv.count_hire_date END) count_1996,
MAX(CASE WHEN hire_date = '1997' THEN ilv.count_hire_date END) count_1997
from (
select case grouping(hire_date)
when 0 then hire_date
else 'TOTAL'
end hire_date,
count(hire_date) as count_hire_date
from employees
group by rollup(hire_date)
) ilv;
This has the obvious disadvantage of you needing to add a new clause into the main select statement for each possible year.
The syntax is not intuitive. This leverages cut'n'paste coding:
SQL> select
2 sum(case when to_char(hiredate, 'YYYY') = '1980' then 1 else 0 end) as "1980"
3 , sum(case when to_char(hiredate, 'YYYY') = '1981' then 1 else 0 end) as "1981"
4 , sum(case when to_char(hiredate, 'YYYY') = '1982' then 1 else 0 end) as "1982"
5 , sum(case when to_char(hiredate, 'YYYY') = '1983' then 1 else 0 end) as "1983"
6 , sum(case when to_char(hiredate, 'YYYY') = '1987' then 1 else 0 end) as "1987"
7 , count(*) as total
8 from emp
9 /
1980 1981 1982 1983 1987 TOTAL
---------- ---------- ---------- ---------- ---------- ----------
1 10 1 0 2 20
Elapsed: 00:00:00.00
SQL>
Here's how I'd do it in MySQL, don't know if this applies to Oracle too:
SELECT
YEAR(hire_date), COUNT(*)
FROM
employees
GROUP BY
YEAR(hire_date)
SELECT NVL(hire_date,'Total'), count(hire_date)
FROM Employees GROUP BY rollup(hire_date);
If you need to PIVOT the data see A_M's answer. If you have years for which you have no data, yet still want the year to show up with a zero count you could do something like the following:
SELECT NVL(a.Year,b.Year), NVL2(a.Year,a.Count,0) FROM
(
SELECT NVL(hire_date,'Total') Year, count(hire_date) Count
FROM Employees GROUP BY rollup(hire_date)
) a
FULL JOIN
(
SELECT to_char(2000 + rownum,'FM0000') Year FROM dual CONNECT BY rownum<=9
) b ON a.Year = b.Year;
Here is some test data.
create table Employees (hire_date Varchar2(4));
insert into Employees values ('2005');
insert into Employees values ('2004');
insert into Employees values ('2006');
insert into Employees values ('2009');
insert into Employees values ('2009');
insert into Employees values ('2005');
insert into Employees values ('2004');
insert into Employees values ('2006');
insert into Employees values ('2006');
insert into Employees values ('2006');
Here's how I would do it in MS SQL - it will be similar in Oracle, but I don't want to try to give you Oracle code because I don't usually write it. This is just to get you a basic skeleton.
Select
Year(e.hire_date),
Count(1)
From
employees e
Group By
Year(e.hire_date)
I realize this is 6 years ago, but I also found another unique way of doing this using the DECODE function in Oracle.
select
count(decode(to_char(hire_date, 'yyyy') , '2005', hire_date, null)) hired_in_2005,
count(decode(to_char(hire_date, 'yyyy') , '2006', hire_date, null)) hired_in_2006,
count(decode(to_char(hire_date, 'yyyy') , '2007', hire_date, null)) hired_in_2007,
count(*) total_emp
from employees
where to_char(hire_date,'yyyy') IN ('2005','2006','2007')