(T-SQL): How do I use result set A to calculate result set B? - sql

I have 2 queries, A and B, which I currently use independently from each other. They both return an employee ID and some metric values. I now want to use the employee IDs returned from result set A in query B.
A's query is structured like this:
select employee_id from employee where employee_team = 1 and employee_role = 1
B's query is structured like this:
declare #tester int = 123450 --plug in employee ID
select employee_id
,employee_name
,sum(case
when notes.note_author!=employee_id and logs.log_date<#today
then 1 else 0 end) as metric
from notes
inner join note_to_log_bridge as br on notes.note_id=br.note_id
inner join logs on br.log_id=logs.log_id
inner join employee on employee_id=#Tester
If I want to get B's metrics for 5 employees, I have to run query B 5 times, changing the #Tester variable each time. I'd like to instead find some way of automating that, so that I get the metrics for query B for every employee_id in result set A.
I tried stored result set A as a CTE and using a while loop to run through query B:
declare #line=1
with cte (employee_id) as <query_a>
while (#line<=count(cte.employee_id))
begin <query b>...
I never finished this query because I discovered that while cannot follow the creation of a CTE.
I tried using a table variable:
declare #set_a (employee_id int)
insert into #set_a <query a>
but when I try to use #set_a in query B, I get a message saying that I need to declare the scalar variable #set_a.
I tried using a temp table and got a "could not be bound" error.
I am out of ideas. Am I approaching this problem in anything resembling the right direction? Is this even possible?
Thank you!

Yes, you can use cursor and it will work perfectly fine.
But, if you have significantly more than 5 rows you may consider using CROSS APPLY to make it all in one query. It may work faster than cursor.
select
employee.employee_id
,CA.*
from
employee
CROSS APPLY
(
<put your query B here
and replace all references to parameter #Tester
with employee.employee_id>
) AS CA
where employee.employee_team = 1 and employee.employee_role = 1
You can think of this operator like this: for each row in the main outer query A CROSS APPLY runs the inner query B with the possibility to reference values of the row from the outer query A (in this case employee.employee_id).

Use Cursor ?
If I want to get B's metrics for 5 employees, I have to run query B 5
times, changing the #Tester variable each time.
DECLARE #empid int;
DECLARE vend_cursor CURSOR
FOR select employee_id from employee where employee_team = 1 and employee_role = 1
OPEN vend_cursor
FETCH NEXT FROM vend_cursor into #empid;
WHILE ##FETCH_STATUS = 0
BEGIN
// your query with #mpid
FETCH NEXT FROM db_cursor INTO #name
END
CLOSE vend_cursor
DEALLOCATE vend_cursor

Related

Assigne a string to a variable in a case statement in the THEN clause

Here's what I have. I have tried quotes instead of =, I have removed the variable and tried just the select statement wrapped in quotes. Loosing my mind trying to figure this out.
Yes, it is homework, I'm not asking for you to write it for me, just let me know why I can't seem to get it to work and what I'm doing wrong.
The Case is about 7 Whens long, But I can't get passed the first one.
Please HELP.
I know people don't like doing homework, so think of this as teaching me instead. Thank you,
CREATE PROC usr_spChooseReport(#ReportNum int)
AS
declare #sqlString nvarchar(500)
SELECT CASE(#ReportNum)
WHEN 1 THEN #sqlString =''select book.Title, Book_Copies.No_of_Copies, Library_Branch.BranchName from Book inner join Book_Copies on book.BookID=Book_Copies.BookID inner join Library_Branch on Book_Copies.BranchID=Library_Branch.BranchID where ONVERT(NVARCHAR(MAX),Library_Branch.BranchName) = 'Sharptown'
and CONVERT(NVARCHAR(MAX), Book.Title) ='The Lost Tribe'''
ELSE 'Unknown'
END
But if I try this,
CREATE PROC usr_spChooseReport(#ReportNum int)
AS
SELECT CASE(#ReportNum)
WHEN 1 THEN (select book.Title, Book_Copies.No_of_Copies, Library_Branch.BranchName from Book inner join Book_Copies on book.BookID=Book_Copies.BookID inner join Library_Branch on Book_Copies.BranchID=Library_Branch.BranchID where CONVERT(NVARCHAR(MAX),Library_Branch.BranchName) = 'Sharptown'
and CONVERT(NVARCHAR(MAX), Book.Title) ='The Lost Tribe')
ELSE 'Unknown'
END
I get this error:
Only one expression can be specified in the select list when the
subquery is not introduced with EXISTS.
After reading your question a second time I think I understand. They want you to use a case statement to set a variable #sqlString to a string which is the select statement. Then at some point they will EXEC (#sqlString)
You need to follow your first example for the remaining cases.
WHEN 2 THEN #sqlString = 'SELECT * FROM Somewhere'
WHEN 3 THEN #sqlString = 'SELECT * FROM SomewhereElse'
The difference between your first example and the second is that in the first they are assigning a string value to a variable #sqlString. In your second example you are running a query that returns multiple expressions. Two totally different things.
First off, this is a funky problem. If you follow this pattern, will you have a completely different query for each of the 7 numbers?
I would hope it's all the same query with simply different parameters passed. Then I would just create a lookup table like this:
DECLARE #LookUpTable TABLE (ID INT PRIMARY KEY, LibraryBranch VARCHAR(25), Book_Name VARCHAR(25))
INSERT INTO #LookUpTable
VALUES (1,'Sharptown','The Lost Tribe'),
(2,'Other Branch','Other book');
DECLARE #ReportNum INT = 1;
select book.Title, Book_Copies.No_of_Copies, Library_Branch.BranchName
FROM Book
inner join Book_Copies
on book.BookID=Book_Copies.BookID
inner join Library_Branch
on Book_Copies.BranchID=Library_Branch.BranchID
inner join #LookUpTable LT
on Library_Branch.BranchName = LT.LibraryBranch
AND Book.Title = LT.Book_Name
WHERE LT.ID = #ReportNum
BUT if you actually do have different queries for each case, then either try Jchao's method and EXEC(#sqlString) at the end OR you could use if statements. Note: ELSE IF means it will run only one of the queries so once it finds a match it will run that one query and then be done, but if NO match is found then the ELSE at the end will run
DECLARE #ReportNum INT = 1;
IF (#ReportNum = 1)
BEGIN
SELECT 1 --query 1
END
ELSE IF (#ReportNum = 2)
BEGIN
SELECT 2 --query 2
END
--etc to 7
ELSE
BEGIN
SELECT 'Unknown' --query 8 for unknown
END

Why my T-SQL (WHILE) does not work?

In my code, I need to test whether specified column is null and the most close to 0 as possible (it can holds numbers from 0 to 50) so I have tried the code below.
It should start from 0 and for each value test the query. When #Results gets null, it should return. However, it does not work. Still prints 0.
declare #hold int
declare #Result int
set #hold0
set #Result=0
WHILE (#Result!=null)
BEGIN
select #Result=(SELECT Hold from Numbers WHERE Name='Test' AND Hold=#hold)
set #hold=#hold+1
END
print #hold
First, you can't test equality of NULL. NULL means an unknown value, so you don't know whether or not it does (or does not) equal any specific value. Instead of #Result!=NULL use #result IS NOT NULL
Second, don't use this kind of sequential processing in SQL if you can at all help it. SQL is made to handle sets, not process things sequentially. You could do all of this work with one simple SQL command and it will most likely run faster anyway:
SELECT
MIN(hold) + 1
FROM
Numbers N1
WHERE
N1.name = 'Test' AND
NOT EXISTS
(
SELECT
*
FROM
Numbers N2
WHERE
N2.name = 'Test' AND
N2.hold = N1.hold + 1
)
The query above basically tells the SQL Server, "Give me the smallest hold value plus 1 (MIN(hold) + 1) in the table Numbers where the name is test (name = 'Test') and where the row with name of 'Test' and hold of one more that that does not exist (the whole "NOT EXISTS" part)". In the case of the following rows:
Name Hold
-------- ----
Test 1
Test 2
NotTest 3
Test 20
SQL Server finds all of the rows with name of "Test" (1, 2, 20) then finds which ones don't have a row with name = Test and hold = hold + 1. For 1 there is a row with Test, 2 that exists. For Test, 2 there is no Test, 3 so it's still in the potential results. For Test, 20 there is no Test, 21 so that leaves us with:
Name Hold
-------- ----
Test 2
Test 20
Now SQL Server looks for MIN(hold) and gets 2 then it adds 1, so you get 3.
SQL Server may not perform the operations exactly as I described. The SQL statement tells SQL Server what you're looking for, but not how to get it. SQL Server has the freedom to use whatever method it determines is the most efficient for getting the answer.
The key is to always think in terms of sets and how do those sets get put together (through JOINs), filtered (through WHERE conditions or ON conditions within a join, and when necessary, grouped and aggregated (MIN, MAX, AVG, etc.).
have you tried
WHILE (#Result is not null)
BEGIN
select #Result=(SELECT Hold from Numbers WHERE Name='Test' AND Hold=#hold)
set #hold=#hold+1
END
Here's a more advanced version of Tom H.'s query:
SELECT MIN(N1.hold) + 1
FROM Numbers N1
LEFT OUTER JOIN Numbers N2
ON N2.Name = N1.Name AND N2.hold = N1.hold + 1
WHERE N1.name = 'Test' AND N2.name IS NULL
It's not as intuitive if you're not familiar with SQL, but it uses identical logic. For those who are more familiar with SQL, it makes the relationship between N1 and N2 easier to see. It may also be easier for the query optimizer to handle, depending on your DBMS.
Try this:
declare #hold int
declare #Result int
set #hold=0
set #Result=0
declare #max int
SELECT #max=MAX(Hold) FROM Numbers
WHILE (#hold <= #max)
BEGIN
select #Result=(SELECT Hold from Numbers WHERE Name='Test' AND Hold=#hold)
set #hold=#hold+1
END
print #hold
While is tricky in T-SQL - you can use this for (foreach) looping through (temp) tables too - with:
-- Foreach with T-SQL while
DECLARE #tempTable TABLE (rownum int IDENTITY (1, 1) Primary key NOT NULL, Number int)
declare #RowCnt int
declare #MaxRows int
select #RowCnt = 1
select #MaxRows=count(*) from #tempTable
declare #number int
while #RowCnt <= #MaxRows
begin
-- Number from given RowNumber
SELECT #number=Number FROM #tempTable where rownum = #RowCnt
-- next row
Select #RowCnt = #RowCnt + 1
end

Optimize for speed a simple stored procedure

In SQL 2008 I've this easy-but-bad-write sp that works:
ALTER PROCEDURE [dbo].[paActualizaCapacidadesDeZonas]
AS
BEGIN
SET NOCOUNT ON;
DECLARE #IdArticulo AS INT
DECLARE #ZonaAct AS INT
DECLARE #Suma AS INT
UPDATE CapacidadesZonas SET Ocupado=0
DECLARE csrSumas CURSOR FOR
SELECT AT.IdArticulo, T.NumZona, SUM(AT.Cantidad)
FROM ArticulosTickets AT
INNER JOIN Tickets T ON AT.IdTicket = T.IdTicket
GROUP BY AT.IdArticulo, T.NumZona
OPEN csrSumas
FETCH NEXT FROM csrSumas INTO #IdArticulo, #ZonaAct, #Suma
WHILE ##FETCH_STATUS = 0
BEGIN
UPDATE CapacidadesZonas SET Ocupado = #Suma
WHERE NumZona = #ZonaAct AND IdArticulo = #IdArticulo
FETCH NEXT FROM csrSumas INTO #IdArticulo, #ZonaAct, #Suma
END
CLOSE csrSumas
DEALLOCATE csrSumas
END
I know: I must avoid cursors, so I'm pretty sure that it can be done in a much proper way.
I've tried with a single Update query:
UPDATE CapacidadesZonas SET Ocupado =
(SELECT SUM(AT.Cantidad)
FROM ArticulosTickets AT
INNER JOIN Tickets T ON AT.IdTicket = T.IdTicket
GROUP BY AT.IdArticulo, T.NumZona)
But this is really wrong, because the select returns more than one row.
I'm feeling bad with this, because it is supposed must be easy for me, but I can't find the equivalent query.
Any suggestions?
Thanks in advance.
There are many different solutions to this problem-- see this article for a few options. Here's one way: use a derived table.
UPDATE CapacidadesZonas SET Ocupado=0 WHERE Ocupado <> 0;
UPDATE CapacidadesZonas
SET Ocupado = SUM(s.Cantidad)
FROM CapacidadesZonas C INNER JOIN
(
SELECT T.NumZona, AT.IdArticulo, SUM(AT.Cantidad) as Ocupado
FROM ArticulosTickets AT
INNER JOIN Tickets T ON AT.IdTicket = T.IdTicket
GROUP BY AT.IdArticulo, T.NumZona
) s ON s.NumZona = C.NumZona AND s.IdArticulo = C.IdArticulo;
Caveats:
are you expecting that the CapacidadesZonas table is available to a live application while the update is happening? If so you may have a locking or perf issue since SQL will may lock the whole table for the update of every row. If this is the case, consider doing your update in batches (e.g. of 1,000 rows each). UPDATE TOP makes batching easy.
sometimes SQL picks a suboptimal plan for queries like this. it may be faster to load a temp table (like in astander's solution above, but using a temp table instead of a table var) than to try to do the update as a single query. If you do this, remember to make sure there's an index on (IDArticulo, NumZona) on the the temp table before you do your update.
Try:
UPDATE cz
SET Ocupado = SUM(AT.Cantidad)
FROM CapacidadesZonas as cz
INNER JOIN ArticulosTickets AT ON cz.numZona = at.numZona and cz.IDArticulo = at.IDArticulo
INNER JOIN Tickets T ON AT.IdTicket = T.IdTicket
GROUP BY AT.IdArticulo, T.NumZona

SQL - Counting Returned Records

I'm building a stored procedure. This stored procedure needs to insert a record if a record with a specific value does not exist. If the value does exist, I need to update the record. The problem I'm having is determining if a record with the given value exists or not. I am using the following code:
DECLARE #record1ID as char(36)
SET #record1ID = (SELECT TOP 1 ID FROM Person WHERE [Role]='Manager')
DECLARE #record2ID as char(36)
SET #record2ID = (SELECT TOP 1 d.ID FROM Department d WHERE d.[ManagerID]=#record1ID)
-- If #record2ID is set update record, otherwise add record
-- how do I setup this if/else statement?
Thank you!
If this were a SQL Server as it looks like, you could do a count like this:
declare #rec_counter as int
set #rec_counter = 0
select #rec_counter = count(*) FROM Department d WHERE d.[ManagerID]=#record1
if (#rec_counter > 0)
begin
-- do whatever here
end
IF (EXISTS YOUR_SELECT)
BEGIN ...
or
IF (#record2ID IS NULL)
BEGIN ...
or use select count(*) instead of selecting a value

Efficient SQL query for two updates and search queries

I have a query like this:
SELECT TOP 1 ID, DATA, OTHERINF FROM MYTABLE WHERE DATE = #DATE
and after reading the row data and using it I want to update that retrieved row and change one of it's columns (in another transaction).
But as you see here i searched for that row twice. Is there any way that I keep remember the row and do the update without searching again.
Thank you.
In the first query you retrieved the id. In the second query use that to find the row to update instead of using the date:
UPDATE MYTABLE
SET DATA = 'FooBar'
WHERE ID = 200
I know its out of vogue but you can also do positioned updates on cursors
e.g.
use Northwind
GO
DECLARE EMP_CURSOR CURSOR
FOR SELECT TOP 1 EmployeeID, LastName FROM EMPLOYEES WHERE HireDate = '1994-11-15'
FOR UPDATE OF LastName
OPEN EMP_CURSOR
FETCH NEXT FROM EMP_CURSOR
UPDATE EMPLOYEES
SET LastName = LastName + CAST(DatePart(ms,GetDate()) as char(3))
WHERE CURRENT OF EMP_CURSOR
CLOSE EMP_CURSOR
DEALLOCATE EMP_CURSOR
--Note, need to setup proper data types
DECLARE #Id INT
DECLARE #Data VARCHAR(MAX)
DECLARE #OtherInf VARCHAR(max)
SELECT TOP 1 #id = Id, #Data = Data, #OtherInf = OtherInf
FROM MyTable
WHERE Date = #Date
--Now you can do what you need, using the info above.
This should do it
You can combine the UPDATE with the SELECT into one statement, but not across two transactions. Therefore, if you need to update the value in another transaction than you select it (the reason for this is unclear to me), you need two statements.
I presume that the DATE column isn't indexed (if not, why not?) Your best bet, then, is to make sure you retrieve the primary key (isn't that ID?) and then use that as your condition in the update.