Query logic issue: Using parameters and variables in a single query - sql

I'm trying to combine 2 different select statements (each are being put into 2 variables) and then using 2 parameters to show my results, but I'm not 100% confident with my logic.
I'm using 2 tables (Salaries and Department)
I first created the procedure with the 2 parameters:
CREATE PROCEDURE DepartmentPercentage
(
#Dept VARCHAR(20)
#DeptPercent int OUTPUT
)
Then I declared and set my 2 variables, using an inner join in one of them to connect the Salaries and Department tables:
AS
DECLARE #Sal int
DECLARE #DeptRate int
SET #Sal = (SELECT SUM(AN_RATE) FROM Salaries) --Total Annual Rate for the entire table
SET #DeptRate = (SELECT SUM(S.AN_RATE) -- Calculates the Total Annual Rate for a given Department
FROM Salaries as S
INNER JOIN Department as D
ON D.DEPT_ID = S.DEPT_ID
WHERE DESCRIPTION = #Dept)
Then I put my out parameter to equal a division between the 2 declared variables:
#DeptPercent = (#DeptRate/#Sal)
My execution statement:
EXEC DepartmentPercentage #Dept = 'Fire Department', #DeptPercent;
Any help with my logic would be much appreciated. I don't know too much about using multiple parameters and variables in a single query.

This should get you Close to the results you want without the need for all the parameters and variables:
SELECT SUM(S.AN_RATE) AS total,
SUM(CASE DESCRIPTION WHEN #Dept THEN S.AN_RATE ELSE 0 END) AS dept,
SUM(CASE DESCRIPTION WHEN #Dept THEN S.AN_RATE ELSE 0 END) / SUM(S.AN_RATE) AS deptPercent
FROM Salaries as S
INNER JOIN Department as D
ON D.DEPT_ID = S.DEPT_ID
I know you probably only want the last column but thought the other 2 might help other people. If I get chance in the morning I'll setup a SQLfiddle with test data.
SQLFiddle seems to be having some problems at the minute so here is a worked example:
--Test Data Setup
DECLARE #Salaries AS TABLE (SAL_ID int, DEPT_ID int, AN_RATE decimal(7,2))
DECLARE #Department AS TABLE (DEPT_ID int, DEPT_DESCRIPTION VARCHAR(20))
INSERT INTO #Department VALUES (1, 'Fire Department')
INSERT INTO #Department VALUES (2, 'Earth Department')
INSERT INTO #Department VALUES (3, 'Wind Department')
INSERT INTO #Salaries VALUES (1, 1, 10000.00)
INSERT INTO #Salaries VALUES (2, 1, 15000.00)
INSERT INTO #Salaries VALUES (3, 1, 20000.00)
INSERT INTO #Salaries VALUES (4, 3, 25000.00)
INSERT INTO #Salaries VALUES (5, 2, 22000.00)
INSERT INTO #Salaries VALUES (6, 2, 21000.00)
--Parameter
DECLARE #Dept VARCHAR(20)
SET #Dept = 'Fire Department'
--Query
SELECT SUM(S.AN_RATE) AS total,
SUM(CASE DEPT_DESCRIPTION WHEN #Dept THEN S.AN_RATE ELSE 0 END) AS dept,
SUM(CASE DEPT_DESCRIPTION WHEN #Dept THEN S.AN_RATE ELSE 0 END) / SUM(S.AN_RATE) * 100 AS deptPercent
FROM #Salaries as S
INNER JOIN #Department as D
ON D.DEPT_ID = S.DEPT_ID

Related

Insert a random string from a list into a table

I'm trying to insert a random department name into an SQL Server table. Currently I have the code below. I want any of the four department values listed (comp sci, biology, psychology,chemistry) to be inserted randomly when I populate my table with sample data. Any help would be appreciated
Declare #SID int
Set #SID = 1
/* Create temporary table to insert random department name
declare #myList table (Dept varchar(50))
insert into #myList values ('Computer Science'), ('Biology'), ('Psychology'), ('Chemistry')*/
While #SID <= 12000
Begin
Insert Into Student values ('Student', CAST(#SID as nvarchar(10)), 'Department' + CAST(#SID as nvarchar(10)), '50')
Print #SID
Set #SID = #SID + 1
End
Use a select (rather than values) to select from the list you have created and insert the top 1 ordered by newid().
--insert into dbo.Student (Name, id, Department, Position)
select top 1 'Student', CAST(#SID as nvarchar(10)), Dept, '50'
from #myList
order by newid();
Note: Its best practice to list all columns being inserted into.
Since you required 12,000 of random data, use #myList to CROSS JOIN itself. And use row_number() to generate the sequential number. The random part is handle by NEWID()
with stud as
(
select SID = row_number() over (order by newid()),
l1.Dept
from #myList l1
cross join #myList l2
cross join #myList l3
cross join #myList l4
cross join #myList l5
cross join #myList l6
cross join #myList l7
)
select *
from stud
where SID <= 12000
For TSQL, you can generate a random number, then use the first digit to determine which value to use, eg
declare #ran int = (select rand() * 10)
declare #subject varchar(100)=''
if #ran <= 3
set #subject = 'computer science'
else if #ran <= 6
set #subject = 'Biology'
else if #ran <= 8
set #subject = 'Psychology'
else
set #subject = 'Chemistry'
insert into table ...... etc etc

CTE Recursive Queries

I have a table with records of employees that shows a relationship of employees and who they report to:
From_ID position TO_ID position
----------------------------------------
1 Lowest_employee 3 employee
3 employee 4 employee
4 employee 5 BOSS
2 Lowest_employee 6 employee
6 employee 3 employee
10 Lowest_employee 50 BOSS2
I would like to show results that look like this, with the employee / boss IDs:
EmployeeID BossID
--------------------
1 5
2 5
10 50
This means employees 1 and 2 report to ID 5 and employee 10 reports to another boss with ID 50.
I know I need to use CTE and Recursive Queries, but cannot understand how it can be done, I'm newer to CTE Recursive Queries.
I read this article but it doesn't make any sense to me MS link
Any help with the query required to achieve this would be useful.
This includes setting up test data, however I think this is what you want:
Test Data:
DECLARE #Table TABLE
(
From_ID int,
TO_ID int
)
INSERT INTO #Table VALUES(1,3)
INSERT INTO #Table VALUES(3,4)
INSERT INTO #Table VALUES(4,5)
INSERT INTO #Table VALUES(2,6)
INSERT INTO #Table VALUES(6,3)
INSERT INTO #Table VALUES(10,50)
Query to get answer:
;WITH Hierarchy (Employee, Superior, QueryLevel)
AS
(
--root is all employees that have no subordinates
SELECT E.From_ID, E.TO_ID, 1
FROM #Table E
LEFT
JOIN #Table S
ON S.TO_ID = E.From_ID
WHERE S.TO_ID IS NULL
--recurse up tree to final superior
UNION ALL
SELECT H.Employee, S.TO_ID, H.QueryLevel + 1
FROM Hierarchy H
JOIN #Table S
ON S.From_ID = H.Superior
)
SELECT Employee, Superior
FROM
(
SELECT *, ROW_NUMBER() OVER(PARTITION BY Employee ORDER BY QueryLevel DESC) AS RowNumber
FROM Hierarchy
) H
WHERE RowNumber = 1
Essentially, this works by :
1) get all employees with no reportees (the root)
2) recurses up through the bosses, recording the 'level'
3) use over/partition to select only the 'final' boss
WITH q (employee, boss) AS
(
SELECT fromId, toId
FROM mytable
WHERE fromId NOT IN
(
SELECT toId
FROM mytable
)
UNION ALL
SELECT employee, toId
FROM q
JOIN mytable t
ON t.fromId = boss
)
SELECT *
FROM q
WHERE boss NOT IN
(
SELECT fromId
FROM mytable
)
You could try something like this?
DECLARE #Employees TABLE (
EmployeeId INT,
PositionName VARCHAR(50),
ReportsToId INT);
INSERT INTO #Employees VALUES (1, 'Driver', 3);
INSERT INTO #Employees VALUES (3, 'Head of Driving Pool', 4);
INSERT INTO #Employees VALUES (4, 'Corporate Flunky', 5);
INSERT INTO #Employees VALUES (2, 'Window Cleaner', 6);
INSERT INTO #Employees VALUES (6, 'Head of Office Services', 3);
INSERT INTO #Employees VALUES (10, 'Minion', 50);
INSERT INTO #Employees VALUES (5, 'BOSS', NULL);
INSERT INTO #Employees VALUES (50, 'BOSS2', NULL);
WITH Employees AS (
SELECT
EmployeeId,
1 AS [Level],
EmployeeID AS [Path],
ISNULL(ReportsToId, EmployeeId) AS ReportsToId
FROM
#Employees
WHERE
ReportsToId IS NULL
UNION ALL
SELECT
e.EmployeeID,
x.[Level] + 1 AS [Level],
x.[Path] + e.EmployeeID AS [Path],
x.ReportsToId
FROM
#Employees e
INNER JOIN Employees x ON x.EmployeeID = e.ReportsToId)
SELECT
ec.EmployeeId,
e.PositionName,
ec.[Level],
CASE WHEN ec.ReportsToId = ec.EmployeeId THEN NULL ELSE ec.ReportsToId END AS ReportsToId --Can't really report to yourself
FROM
Employees ec
INNER JOIN #Employees e ON e.EmployeeId = ec.EmployeeId
ORDER BY
ec.[Path];

comparing two colums in sqlserver and returing the remaining data

I have two tables. First one is student table where he can select two optional courses and other table is current semester's optional courses list.
When ever the student selects a course, row is inserted with basic details such as roll number, inserted time, selected course and status as "1". When ever a selected course is de-selected the status is set as "0" for that row.
Suppose the student has select course id 1 and 2.
Now using this query
select SselectedCourse AS [text()] FROM Sample.dbo.Tbl_student_details where var_rollnumber = '020803009' and status = 1 order by var_courseselectedtime desc FOR XML PATH('')
This will give me the result as "12" where 1 is physics and 2 is social.
the second table holds the value from 1-9
For e.g course id
1 = physics
2 = social
3 = chemistry
4 = geography
5 = computer
6 = Spoken Hindi
7 = Spoken English
8 = B.EEE
9 = B.ECE
now the current student has selected 1 and 2. So on first column, i get "12" and second column i need to get "3456789"(remaining courses).
How to write a query for this?
This is not in single query but is simple.
DECLARE #STUDENT AS TABLE(ID INT, COURSEID INT)
DECLARE #SEM AS TABLE (COURSEID INT, COURSE VARCHAR(100))
INSERT INTO #STUDENT VALUES(1, 1)
INSERT INTO #STUDENT VALUES(1, 2)
INSERT INTO #SEM VALUES(1, 'physics')
INSERT INTO #SEM VALUES(2, 'social')
INSERT INTO #SEM VALUES(3, 'chemistry')
INSERT INTO #SEM VALUES(4, 'geography')
INSERT INTO #SEM VALUES(5, 'computer')
INSERT INTO #SEM VALUES(6, 'Spoken Hindi')
INSERT INTO #SEM VALUES(7, 'Spoken English')
INSERT INTO #SEM VALUES(8, 'B.EEE')
INSERT INTO #SEM VALUES(9, 'B.ECE')
DECLARE #COURSEIDS_STUDENT VARCHAR(100), #COURSEIDS_SEM VARCHAR(100)
SELECT #COURSEIDS_STUDENT = COALESCE(#COURSEIDS_STUDENT, '') + CONVERT(VARCHAR(10), COURSEID) + ' ' FROM #STUDENT
SELECT #COURSEIDS_SEM = COALESCE(#COURSEIDS_SEM , '') + CONVERT(VARCHAR(10), COURSEID) + ' ' FROM #SEM WHERE COURSEID NOT IN (SELECT COURSEID FROM #STUDENT)
SELECT #COURSEIDS_STUDENT COURSEIDS_STUDENT, #COURSEIDS_SEM COURSEIDS_SEM
try this:
;WITH CTE as (select ROW_NUMBER() over (order by (select 0)) as rn,* from Sample.dbo.Tbl_student_details)
,CTE1 As(
select rn,SselectedCourse ,replace(stuff((select ''+courseid from course_details for xml path('')),1,1,''),SselectedCourse,'') as rem from CTE a
where rn = 1
union all
select c2.rn,c2.SselectedCourse,replace(rem,c2.SselectedCourse,'') as rem
from CTE1 c1 inner join CTE c2
on c2.rn=c1.rn+1
)
select STUFF((select ''+SselectedCourse from CTE1 for xml path('')),1,0,''),(select top 1 rem from CTE1 order by rn desc)

project a sparse result at some level

I don't really know what to call this but it's not that hard to explain
Basically what I have is a result like this
Similarity ColumnA ColumnB ColumnC
1 SomeValue NULL SomeValue
2 NULL SomeB NULL
3 SomeValue NULL SomeC
4 SomeA NULL NULL
This result is created by matching a set of strings against another table. Each string also contains some values for these ColumnA..C which are the values I wan't to aggregate in some way.
Something like min/max works very well but I can't figure out how to get it to account for the highest similarity not just the min/max value. I don't really want the min/max, I want the first non-null value with the highest similarity.
Ideally the result would look like this
ColumnA ColumnB ColumnC
SomeA SomeB SomeC
I'd like be able to efficiently join in the temporary result to compute the rest and I've been exploring different options. Something which I've been considering is creating a SQL Server CLR aggregate the yields the "first" non-null value but I'm unsure if there's even such a thing as a first or last when running an aggregate on a result.
Okay, so I figured it out, I originally had trouble with the UPDATE FROM and JOIN not playing well together. I was counting on that the UPDATE would just occur multiple times and that would give me the correct results, however, there's no such guarantee from SQL Server (it's actually undefined behavior and alltough it appeared to work we'll have none of that) but since you can run UPDATE against a CTE I combined that with the OUTER APPLY to select the exactly 1 row to complement a missing value if possible.
Here's the whole thing with test data as well.
DECLARE #cost TABLE (
make nvarchar(100) not null,
model nvarchar(100),
a numeric(18,2),
b numeric(18,2)
);
INSERT #cost VALUES ('a%', null, 100, 2);
INSERT #cost VALUES ('a%', 'a%', 149, null);
INSERT #cost VALUES ('a%', 'ab', 349, null);
INSERT #cost VALUES ('b', null, null, 2.5);
INSERT #cost VALUES ('b', 'b%', 249, null);
INSERT #cost VALUES ('b', 'b', null, 3);
DECLARE #unit TABLE (
id int,
make nvarchar(100) not null,
model nvarchar(100)
);
INSERT #unit VALUES (1, 'a', null);
INSERT #unit VALUES (2, 'a', 'a');
INSERT #unit VALUES (3, 'a', 'ab');
INSERT #unit VALUES (4, 'b', null);
INSERT #unit VALUES (5, 'b', 'b');
DECLARE #tmp TABLE (
id int,
specificity int,
a numeric(18,2),
b numeric(18,2),
primary key(id, specificity)
);
INSERT #tmp
OUTPUT inserted.* --FOR DEBUGGING
SELECT
unit.id
, ROW_NUMBER() OVER (
PARTITION BY unit.id
ORDER BY cost.make DESC, cost.model DESC
) AS specificity
, cost.a
, cost.b
FROM #unit unit
INNER JOIN #cost cost ON unit.make LIKE cost.make
AND (cost.model IS NULL OR unit.model LIKE cost.model)
;
--fix the holes
WITH tmp AS (
SELECT *
FROM #tmp
WHERE specificity = 1
AND (a IS NULL OR b IS NULL) --where necessary
)
UPDATE tmp
SET
tmp.a = COALESCE(tmp.a, a.a)
, tmp.b = COALESCE(tmp.b, b.b)
OUTPUT inserted.* --FOR DEBUGGING
FROM tmp
OUTER APPLY (
SELECT TOP 1 a
FROM #tmp a
WHERE a.id = tmp.id
AND a.specificity > 1
AND a.a IS NOT NULL
ORDER BY a.specificity
) a
OUTER APPLY (
SELECT TOP 1 b
FROM #tmp b
WHERE b.id = tmp.id
AND b.specificity > 1
AND b.b IS NOT NULL
ORDER BY b.specificity
) b
;

SQL: How do I loop through the results of a SELECT statement?

How do I loop through the results of a SELECT statement in SQL? My SELECT statement will return just 1 column but n results.
I have created a fictional scenario below complete with the Pseudo code of what I'm trying to do.
Scenario:
Students are registering for their classes. They submit a form with multiple course selections (ie. select 3 different courses at once). When they submit their registration I need to ensure there is still room left int the courses they have selected (note I will do a similar check before presenting them with course selection UI but I need to verify afterwards in case somebody else has gone in and swipped up the remaining spots).
Pseudo Code:
DECLARE #StudentId = 1
DECLARE #Capacity = 20
-- Classes will be the result of a Select statement which returns a list of ints
#Classes = SELECT classId FROM Student.CourseSelections
WHERE Student.CourseSelections = #StudentId
BEGIN TRANSACTION
DECLARE #ClassId int
foreach (#classId in #Classes)
{
SET #SeatsTaken = fnSeatsTaken #classId
if (#SeatsTaken > #Capacity)
{
ROLLBACK; -- I'll revert all their selections up to this point
RETURN -1;
}
else
{
-- set some flag so that this student is confirmed for the class
}
}
COMMIT
RETURN 0
My real problem is a similar "ticketing" problem. So if this approach seems very wrong please feel free to recommend something more practical.
EDIT:
Attempting to implement the solution below. At this point it doesn't work. Always returns "reserved".
DECLARE #Students TABLE
(
StudentId int
,StudentName nvarchar(max)
)
INSERT INTO #Students
(StudentId ,StudentName)
VALUES
(1, 'John Smith')
,(2, 'Jane Doe')
,(3, 'Jack Johnson')
,(4, 'Billy Preston')
-- Courses
DECLARE #Courses TABLE
(
CourseId int
,Capacity int
,CourseName nvarchar(max)
)
INSERT INTO #Courses
(CourseId, Capacity, CourseName)
VALUES
(1, 2, 'English Literature'),
(2, 10, 'Physical Education'),
(3, 2, 'Photography')
-- Linking Table
DECLARE #Courses_Students TABLE
(
Course_Student_Id int
,CourseId int
,StudentId int
)
INSERT INTO #Courses_Students
(Course_Student_Id, StudentId, CourseId)
VALUES
(1, 1, 1),
(2, 1, 3),
(3, 2, 1),
(4, 2, 2),
(5, 3, 2),
(6, 4, 1),
(7, 4, 2)
SELECT Students.StudentName, Courses.CourseName FROM #Students Students INNER JOIN
#Courses_Students Courses_Students ON Courses_Students.StudentId = Students.StudentId INNER JOIN
#Courses Courses ON Courses.CourseId = Courses_Students.CourseId
DECLARE #StudentId int = 4
-- Ideally the Capacity would be database driven
-- ie. come from the Courses.Capcity.
-- But I didn't want to complicate the HAVING statement since it doesn't seem to work already.
DECLARE #Capacity int = 1
IF EXISTS (Select *
FROM
#Courses Courses INNER JOIN
#Courses_Students Courses_Students ON Courses_Students.CourseId = Courses.CourseId
WHERE
Courses_Students.StudentId = #StudentId
GROUP BY
Courses.CourseId
HAVING
COUNT(*) > #Capacity)
BEGIN
SELECT 'full' as Status
END
ELSE BEGIN
SELECT 'reserved' as Status
END
No loop needed. You're looking at a standard aggregate with COUNT and GROUP.
Of course, some details are needed but the principle is this...
DECLARE #StudentId = 1
DECLARE #Capacity = 20
-- Classes will be the result of a Select statement which returns a list of ints
IF EXISTS (SELECT *
FROM
Student.CourseSelections CS
JOIN
---this is where you find out course allocations somehow
ClassTable C ON CS.classId = C.classId
WHERE
Student.CourseSelections = #StudentId
GROUP BY --change this, it depends on where you find out course allocations
ClassID
HAVING
COUNT(*) > #Capacity)
'no'
ELSE
'yes'
Edit:
I've changed the link table. Course_Student_ID is usually not needed in link tables.
The JOIN now
gets the courses for that student
then looks at all students on this course and compares to capacity
Cut down version of above:
...
-- Linking Table
DECLARE #Courses_Students TABLE (
,CourseId int
,StudentId int)
INSERT INTO #Courses_Students
(StudentId, CourseId)
VALUES (1, 1), (1, 3), (2, 1), (2, 2), (3, 2), (4, 1), (4, 2)
DECLARE #StudentId int = 4
--straight list
SELECT
C.CourseName, C.Capacity, COUNT(*)
FROM
#Courses_Students CSThis
JOIN
#Courses C ON CSThis.CourseId = C.CourseId
JOIN
#Courses_Students CSOthers ON CSOthers.CourseId = C.CourseId
WHERE
CSThis.StudentId = #StudentId
GROUP BY
C.CourseName, C.Capacity
--oversubscribed list
SELECT
C.CourseName, C.Capacity, COUNT(*)
FROM
#Courses_Students CSThis
JOIN
#Courses C ON CSThis.CourseId = C.CourseId
JOIN
#Courses_Students CSOthers ON CSOthers.CourseId = C.CourseId
WHERE
CSThis.StudentId = #StudentId
GROUP BY
C.CourseName, C.Capacity
HAVING
COUNT(*) > C.Capacity
Avoid looping through result sets in SQL as much as you can. If you really can't (if you really are a standard programmer but profession leads you into SQL) use cursors. They don't smell nice, but are unavoidable at times.
Another option would be to implement a CHECK Constraint on your table that contains the Course information. The check constraint could call your existing function to check that there are free seats.
Wrap all of your Inserts/Updates in to one transaction. If any of the Inserts/Updates fails then the entire transaction will be rolled back.