join/subquery in OUTPUT clause - sql

I'm updating a table and adding data on the changes using the TSQL OUTPUT clause. The problem is that the updated table only contains normalised IDs for the data I want to output and I need it in human readable form. Here's a simplified example to show what I mean
CREATE TABLE LOCATION
(locationid INT, name VARCHAR(50))
CREATE TABLE USER
(userid INT, username VARCHAR(50), locationid INT)
--insert some test data
UPDATE USER
SET locationid = #somevalue
WHERE userid = #someothervalue
I know I can add something like
OUTPUT inserted.username
+ ' location changed from ' + deleted.locationid
+ ' to ' + inserted.locationid
but that's no use to the end user who wants to know the location name, not its database ID.
I tried replacing deleted.locationid with (SELECT name from LOCATION WHERE locationid = deleted.locationid), and similarly for inserted.locationid and was met with the error Subqueries are not allowed in the OUTPUT clause
I've seen a couple of answers saying joins are allowed in OUTPUT clauses, but I haven't found a way to join separately on inserted and deleted. Is this possible in a single step in SQL Server 2008 R2, or will I need to do it via an intermediate step of storing the old and new values then joining in a second query for the output I need?

Following Microsoft article Output clearly describes the limitations of output clause in any DML operations like insert, update and delete.
It also describes that you can't use subquery with output clause
So here you can records the locationid from inserted and deleted row and in a table variable and can further get the location name using join with Location table
Example
INSERT INTO location VALUES(1,'Delhi')
INSERT INTO location VALUES(2,'Noida')
INSERT INTO [USER] VALUES(1,'Sandeep',1)
DECLARE #result table(UserName varchar(50), OldLocationId Int,NewLocaionId Int)
UPDATE [USER] SET locationid=2
OUTPUT INSERTED.UserName, DELETED.locationid,INSERTED.locationid INTO #result
WHERE userid=1
SELECT * FROM #result

Why not:
OUTPUT inserted.username
+ ' location changed from ' + deleted.name + ' to ' + inserted.name

Union your join and delete into a subquery and join on it.

Related

Enumerate the multiple rows in a multi-update Trigger

I have something like the table below:
CREATE TABLE updates (
id INT PRIMARY KEY IDENTITY (1, 1),
name VARCHAR (50) NOT NULL,
updated DATETIME
);
And I'm updating it like so:
INSERT INTO updates (name, updated)
VALUES
('fred', '2020-11-11),
('fred', '2020-11-11'),
...
('bert', '2020-11-11');
I need to write an after update Trigger and enumerate all the name(s) that were added and add each one to another table but can't work out how enumerate each one.
EDIT: - thanks to those who pointed me in the right direction, I know very little SQL.
What I need to do is something like this
foreach name in inserted
look it up in another table and
retrieve a count of the updates a 'name' has done
add 1 to the count
and update it back into the other table
I can't get to my laptop at the moment, but presumably I can do something like:
BEGIN
SET #count = (SELECT UCount from OTHERTAB WHERE name = ins.name)
SET #count = #count + 1
UPDATE OTHERTAB SET UCount = #count WHERE name = ins.name
SELECT ins.name
FROM inserted ins;
END
and that would work for each name in the update?
Obviously I'll have to read up on set based SQL processing.
Thanks all for the help and pointers.
Based on your edits you would do something like the following... set based is a mindset, so you don't need to compute the count in advance (in fact you can't). It's not clear whether you are counting in the same table or another table - but I'm sure you can work it out.
Points:
Use the Inserted table to determine what rows to update
Use a sub-query to calculate the new value if its a second table, taking into account the possibility of null
If you are really using the same table, then this should work
BEGIN
UPDATE OTHERTAB SET
UCount = COALESCE(UCount,0) + 1
WHERE [name] in (
SELECT I.[name]
FROM Inserted I
);
END;
If however you are using a second table then this should work:
BEGIN
UPDATE OTHERTAB SET
UCount = COALESCE((SELECT UCount+1 from OTHERTAB T2 WHERE T2.[name] = OTHERTAB.[name]),0)
WHERE [name] in (
SELECT I.[name]
FROM Inserted I
);
END;
Using inserted and set-based approach(no need for loop):
CREATE TRIGGER trg
ON updates
AFTER INSERT
AS
BEGIN
INSERT INTO tab2(name)
SELECT name
FROM inserted;
END

Need to log identifiers from select query recordset along with date/time stamp

I would like to log all records generated by a sql select statement in an SSRS report into a table for later reference at the time the report is run.
One idea is to append the unique ID to the reference table and then perform the full query with the additional data for the full record set, but i feel like there is probably a better way.
One way would be to use a STORED PROCEDURE for your SSRS report instead of hard coding the query in the RDL. Then, you could leverage the OUTPUT clause and log the query execution, and return the results. The proc would look something like this:
create procedure myProc (#param1 int)
as
begin
declare #resultTable table (id int, c1 char(4), c2 char(4))
insert into myloggingtable
output INSERTED.id, INSERTED.c2, INSERTED.c3 into #resultTable
select
id,
c1,
c2,
RunDateTime = getdate(),
from SomeTable
where id = #param1
select * from #resultTable
end
Here, I would have an auto-incremented key on the myloggingtable and the RunDateTime would be logged as the execution time.

How to change column names in the resultset by comparing with other resultset values

Can we store select query result in a variable and iterate every item of that variable.For example
select labelname from controlType
--gives me
labelname
----------
Employee Name
Employee Address
IsActive
DOB
----------
I have another table targetTable whose coulmns are
EmployeeName,
EmployeeAddress,
IsActvie,
DOB.
You can see these are the same columns that i got from the resultset except white spaces. I want to select all column of targetTable with formatted names as in the resultset. For eg should get 'Employee Name' instead of EmployeeName.
This is just an example. In my current scenario I dont know how many column are there and what is resultset. My idea was to store resultset in variable and do some logic while selecting columns of target table. How it can be done. Please assist.
Use a Table Variable to store the columns you need to select. Here's a nice tutorial on Table Variables. http://odetocode.com/articles/365.aspx
Then construct the SQL Statement Dynamically by looping through the column-names that are in the table variable.
Finally, use EXEC or sp_executesql to execute the dynamic sql you just generated. Here's an example: http://www.sommarskog.se/dynamic_sql.html
No need to have any variable you can just use following query and insert all record in #Temp variable and then loop through it
Select Id,labelname Into #Temp from controlType
Declare #Id int
While (Select Count(*) From #Temp) > 0
Begin
Select Top 1 #Id = Id From #Temp
--insert your logic here
Delete #Temp Where Id = #Id
End

Can't select from a successfully created #temp table

DECLARE #tmp_tab VARCHAR(20)
SELECT #TMP_TAB = '#TMP_TAB_BANK' + CAST(USER_ID(USER) AS NVARCHAR)
+ CAST(##SPID AS NVARCHAR)
EXEC('CREATE TABLE ' + #TMP_TAB + (ID INT NULL, NAME VARCHAR NULL)')
//Break point
EXEC('select * from ' + #TMP_TAB)
I'm working in SQL Server 2005. In the code above I decide what to name my temp table. Then I create the temp table. If I run just these codes I receive Commands completed successfully message.
However if I execute the last line of code to retrieve the data (well, I know it's empty) I get
invalid object name '#TMP_TAB_BANK157'
Why can't I fetch records from a just created table? If the temp table was not created then why don't I get any warning?
#TEMP tables are only available from the context they are created in. If you create #temp1 in one spid or connection, you can't access it from any other scope, except for child scopes.
If you create the #TEMP table in dynamic SQL, you need to select from it in the same scope.
Alternatives are:
##GLOBAL temp tables, which have their own risks.
Real tables

Is my stored procedure executing out of order?

Brief history:
I'm writing a stored procedure to support a legacy reporting system (using SQL Server Reporting Services 2000) on a legacy web application.
In keeping with the original implementation style, each report has a dedicated stored procedure in the database that performs all the querying necessary to return a "final" dataset that can be rendered simply by the report server.
Due to the business requirements of this report, the returned dataset has an unknown number of columns (it depends on the user who executes the report, but may have 4-30 columns).
Throughout the stored procedure, I keep a column UserID to track the user's ID to perform additional querying. At the end, however, I do something like this:
UPDATE #result
SET Name = ppl.LastName + ', ' + ppl.FirstName
FROM #result r
LEFT JOIN Users u ON u.id = r.userID
LEFT JOIN People ppl ON ppl.id = u.PersonID
ALTER TABLE #result
DROP COLUMN [UserID]
SELECT * FROM #result r ORDER BY Name
Effectively I set the Name varchar column (that was previously left NULL while I was performing some pivot logic) to the desired name format in plain text.
When finished, I want to drop the UserID column as the report user shouldn't see this.
Finally, the data set returned has one column for the username, and an arbitrary number of INT columns with performance totals. For this reason, I can't simply exclude the UserID column since SQL doesn't support "SELECT * EXCEPT [UserID]" or the like.
With this known (any style pointers are appreciated but not central to this problem), here's the problem:
When I execute this stored procedure, I get an execution error:
Invalid column name 'userID'.
However, if I comment out my DROP COLUMN statement and retain the UserID, the stored procedure performs correctly.
What's going on? It certainly looks like the statements are executing out of order and it's dropping the column before I can use it to set the name strings!
[Edit 1]
I defined UserID previously (the whole stored procedure is about 200 lies of mostly irrelevant logic, so I'll paste snippets:
CREATE TABLE #result ([Name] NVARCHAR(256), [UserID] INT);
Case sensitivity isn't the problem but did point me to the right line - there was one place in which I had userID instead of UserID. Now that I fixed the case, the error message complains about UserID.
My "broken" stored procedure also works properly in SQL Server 2008 - this is either a 2000 bug or I'm severely misunderstanding how SQL Server used to work.
Thanks everyone for chiming in!
For anyone searching this in the future, I've added an extremely crude workaround to be 2000-compatible until we update our production version:
DECLARE #workaroundTableName NVARCHAR(256), #workaroundQuery NVARCHAR(2000)
SET #workaroundQuery = 'SELECT [Name]';
DECLARE cur_workaround CURSOR FOR
SELECT COLUMN_NAME FROM [tempdb].INFORMATION_SCHEMA.Columns WHERE TABLE_NAME LIKE '#result%' AND COLUMN_NAME <> 'UserID'
OPEN cur_workaround;
FETCH NEXT FROM cur_workaround INTO #workaroundTableName
WHILE ##FETCH_STATUS = 0
BEGIN
SET #workaroundQuery = #workaroundQuery + ',[' + #workaroundTableName + ']'
FETCH NEXT FROM cur_workaround INTO #workaroundTableName
END
CLOSE cur_workaround;
DEALLOCATE cur_workaround;
SET #workaroundQuery = #workaroundQuery + ' FROM #result ORDER BY Name ASC'
EXEC(#workaroundQuery);
Thanks everyone!
A much easier solution would be to not drop the column, but don't return it in the final select.
There are all sorts of reasons why you shouldn't be returning select * from your procedure anyway.
EDIT: I see now that you have to do it this way because of an unknown number of columns.
Based on the error message, is the database case sensitive, and so there's a difference between userID and UserID?
This works for me:
CREATE TABLE #temp_t
(
myInt int,
myUser varchar(100)
)
INSERT INTO #temp_t(myInt, myUser) VALUES(1, 'Jon1')
INSERT INTO #temp_t(myInt, myUser) VALUES(2, 'Jon2')
INSERT INTO #temp_t(myInt, myUser) VALUES(3, 'Jon3')
INSERT INTO #temp_t(myInt, myUser) VALUES(4, 'Jon4')
ALTER TABLE #temp_t
DROP Column myUser
SELECT * FROM #temp_t
DROP TABLE #temp_t
It says invalid column for you. Did you check the spelling and ensure there even exists that column in your temp table.
You might try wrapping everything preceding the DROP COLUMN in a BEGIN...COMMIT transaction.
At compile time, SQL Server is probably expanding the * into the full list of columns. Thus, at run time, SQL Server executes "SELECT UserID, Name, LastName, FirstName, ..." instead of "SELECT *". Dynamically assembling the final SELECT into a string and then EXECing it at the end of the stored procedure may be the way to go.