SQL - Dynamically selecting information into a variable from different databases - sql

i have 3 databases, db 1 and db 2 are for jobs and db 3 is for workflows.
both db 1 and 2 have a process to create workflows directly into db 3.
i have a trigger in db 3 that activates when there is a workflow creation because i need to add more information into the workflow from the jobs.
so what i did was create a variable that i set with a normal select from db 1 (select job from db1.dbo.job)
i want to know if there is a way to do the select statement dynamically
for example:
if db1 then #db = db1
if db2 then #db = db2
then in the #variable = select job from #db
right now what i have is the following query but i am simple adding all my logic to another IF and then changing the selects statements... i dont want to do that becuase i will be adding more data bases to integrate with db3
Declare
#flag int,
#variable varchar(10)
set #flag = 1
if #flag = 1
begin
set #variable = (select job from db1.dbo.job)
end
else if #flag = 2
begin
set #variable = (select job from db2.dbo.job)
end
update db3 set job = #variable
is it even possible to do what im trying to do????

You need dynamic SQL for this.
Note:
Always use QUOTENAME to correctly escape names
Use sp_executesql to pass data in parameters all the way through. You can use OUTPUT parameters too
Declare
#flag int
set #flag = 1
DECLARE #sql nvarchar(max) = N'
UPDATE db3
SET job = (
SELECT TOP 1 job
FROM ' + QUOTENAME(CASE WHEN #flag = 1 THEN 'db1' ELSE 'db2' END) + '.dbo.job
);
';
EXEC sp_executesql #sql;

This kind of thing is possible using techniques like dynamic sql and sp_executesql, or by creating synonyms on the fly.
HOWEVER, you have a more fundamental issue with the approach.
When a trigger is called, you cannot make the assumption that only a single row will be in the inserted table, because it is possible to insert multiple rows into a table all at once.
To demonstrate the problem, here is a "solution" to your problem which "works" if only one row is inserted into table T. But think about what would happen if more than one row was inserted. What would the "correct" flag value be if multiple rows are inserted with different flag values? How can we update the row in T based on the value of the #id variable if multiple rows are affected?
You could use this pattern and iterate over the rows in the inserted table using a cursor, but it would be a bit ugly.
create table T(id int, flag int, extraData nvarchar(128));
create or alter trigger T_ins on T for insert as
begin
declare #sql nvarchar(max) = 'update T set extraData = (select top 1 name from {otherDB}.sys.tables) where id = #id'
declare #db sysname, #id int;
-- this select doesn't make any sense if more than one row exists in the inserted table!
select #id = id,
#db = iif(inserted.flag = 1, 'master', 'msdb')
from inserted;
set #sql = replace(#sql, '{otherDB}', #db);
exec sp_executesql #sql, N'#id int', #id;
end
A way to do it without a cursor is to left join onto all of the possible sources of additional data in the other databases, including the mutually exclusive flag value in the join condition, and coalesce the columns into the update statement. I think the code is also much cleaner:
create or alter trigger jobs_ins on jobs for insert as
begin
update j
set extraData = coalesce(j1.somedata, j2.somedata)
from jobs j
join inserted ins on ins.id = j.id
left join otherDB1.dbo.jobdata j1 on i.flag = 1 and j1.id = ins.id
left join otherDB2.dbo.jobdata j2 on i.flag = 2 and j2.id = ins.id;
end

Related

SQL Query against multiple databases

I'm trying to run a query against multiple databases on the same server.
I need to pull all the values from 2 tables in a database based on a criteria from a 3rd table in the database, if the database was created after a certain date.
I have a query to find when the database was created:
SELECT *
FROM sys.databases
WHERE STATE = 0 --ignores offline databases
AND database_id > 4 --does not include master, model, msdb, tempdb
AND create_date > CONVERT(datetime, '2021-01-01')
And the query that I need run on each database is generally as follows:
SELECT *
FROM Table1
INNER JOIN Table3 ON Table3.Column6=Table1.Column2
AND Table3.Column3='Value1'
AND Table3.Column4='Value2'
INNER JOIN Table2 ON Table3.Column6=Table2.Column2
I did find this question, which is essentially would I would like to do, but when I look at the INFORMATION_SCHEMA.TABLES the TABLE_CATALOG column does not have the table names I would like to query against. I thought I could try pulling the names from the sys.databases table like above, so I tried modifying it to:
DECLARE #cmd VARCHAR(max) = N''
SELECT #cmd += COALESCE(#cmd + ' UNION ALL ', '') + 'SELECT *
FROM [' + name + '].dbo.Table1
INNER JOIN [' + name + '].dbo.Table3 on Table3.Column6=Table1.Column2
AND Table3.Column3= ''Value1''
AND Table3.Column4=''Value2''
INNER JOIN [' + name + '].dbo.Table2 on Table3.Column6=Table2.Column2'
FROM sys.databases
WHERE STATE = 0
AND database_id>4
AND create_date>CONVERT(datetime,'2021-08-26')
SET #cmd = STUFF(#cmd, CHARINDEX('UNION ALL', #cmd), 10, '')
PRINT #cmd
EXEC(#cmd)
But when I run it with a date earlier than 2021-08-26 (which grabs more than 5 tables), I get a memory error. I need to run this at least to the beginning of April (preferably up to the beginning of the year) which will grab around 500 tables.
What is the recommended way to run a query against multiple databases in SQL?
My recommendation would be instead of trying to build one massive UNION ALL dynamic SQL statement, that you build a #temp table to hold the results of each output, and then it's much easier to send the same string to each database:
CREATE TABLE #hold(dbname sysname, Column1 {data type}, ...);
DECLARE #sql nvarchar(max), #exec nvarchar(1024);
SET #sql = N'SELECT DB_NAME(), *
FROM dbo.Table1
INNER JOIN dbo.Table3
ON Table3.Column6 = Table1.Column2
AND Table3.Column3 = ''Value1''
AND Table3.Column4 = ''Value2''
INNER JOIN dbo.Table2
ON Table3.Column6 = Table2.Column2;';
DECLARE #dbname sysname, #c cursor;
SET #c = CURSOR FORWARD_ONLY STATIC READ_ONLY FOR
SELECT name FROM sys.databases
WHERE state = 0 -- ignores offline databases
AND database_id > 4 -- does not include master, model, msdb, tempdb
AND create_date > CONVERT(datetime, '20210101');
OPEN #c;
FETCH NEXT FROM #c INTO #dbname;
WHILE ##FETCH_STATUS = 0
BEGIN
SET #exec = QUOTENAME(#dbname) + N'.sys.sp_executesql';
INSERT #hold EXEC #exec #sql;
FETCH NEXT FROM #c INTO #dbname;
END;
SELECT * FROM #hold;
You might also consider investing in sp_ineachdb, a procedure I wrote to help simplify running the same command in the context of each database.
Execute a Command in the Context of Each Database in SQL Server using sp_ineachdb
Execute a Command in the Context of Each Database in SQL Server - Part 2

how I can hide displaying result of exec command

WHILE #nxt < #rc
BEGIN
set #sql = (select Rec from DmsRec where row=#nxt)
use TestDB1
exec (#sql)
select #rc_child = ##ROWCOUNT
use MyTempDB
if #rc_child = 0
begin
insert into DMSResults
select DOCID from DmsRec where row=#nxt
end
set #nxt = #nxt + 1
END
My Question is how I can hide displaying result of exec command.
(using Sql Server 2008r2) Above query is small part of complete procedure.
Accessing multiple conditions,value and multiple databases I generate a select statement and stored in Rec Field into table DmsRec
After generating require Select.... statement I stored it into table DmsRec.
e.g. SELECT ACCD FROM M_TABLE WHERE 1=1 and ACCD='0034422'.
This select statement which is stored as a value in table DmsRec
want to execute in different database.
So I used use command and change the database name, access the recordset.
If the executed sql (return 0 row) then run the insert command which is executed in different database.
With this I came to know wether Inserted docid into DMSResults is those tranaction whose master is not avalialbel.
It works perfectly but can not hide displaying records of exec command.
I run exec(#sql) is only to check whether it is returning any row or not.
and stored rowcount value into variable.
But do not want to display records in grid (at the same time result {effected rows} should stored in variable).
Without changing options in SSMS manually i.e.
Tools/Options, Query Results/SQL Server/Results to XX, check "Discard results after query executes"
EDIT - based on comments below
You can use sp_executesql and pass parameters to it, even OUTPUT parameters.
Here's an example - you will need to amend the SQL query you're storing slightly
DECLARE #SQL NVARCHAR(MAX), #Rows INT, #nxt INT = 1
CREATE TABLE DmsRec (row INT, rec NVARCHAR(10))
INSERT INTO DmsRec(row, rec)
VALUES(1, 1)
SET #SQL = 'IF EXISTS ( SELECT rec FROM dbo.DmsRec WHERE row = #nxt) SET #Rows = 1 ELSE SET #Rows = 0' --+'; SELECT #Rows = ##ROWCOUNT'
EXEC sys.sp_executesql #sql, N'#nxt INT, #Rows INT OUTPUT', #nxt, #Rows OUTPUT;
SELECT #Rows
SET #nxt = 0 -- record doesn't exist
SET #SQL = 'IF EXISTS ( SELECT rec FROM dbo.DmsRec WHERE row = #nxt) SET #Rows = 1 ELSE SET #Rows = 0'
EXEC sys.sp_executesql #sql, N'#nxt INT, #Rows INT OUTPUT', #nxt, #Rows OUTPUT;
SELECT #Rows
You can then use #Rows variable to determine whether you need to insert a new record or not.
If #Rows = 1
BEGIN
--INSERT record
END

How do I write dynamic SQL from a temp table to update each employee record?

I have the following temp table (#updates):
userid newvalue
------------------
031233 A
467763 B
656532 C
I need to update the user table for each recordset:
update tbl.users set foo = 'A' where id = '031233';
update tbl.users set foo = 'B' where id = '467763';
update tbl.users set foo = 'C' where id = '656532';
AFAIK, I need dynamic SQL to read the #updates table and perform the updates:
declare #cnt int;
declare #id int = 1;
select #cnt = count(1) from #updates; -- 3
while #id <= #cnt
begin
select #id;
select #sql = N'update tbl.users set foo = ' + ?? + 'where id = ' + ??;
exec sp_executesql #sql;
select #id = #id + 1;
end
;
Obviously, this is not working but even after a few hours of googling and trying, this is the best I can do.
Can anyone help me and tell me how I properly loop through the temp table?
In order to loop through a table you need a CURSOR. You can find documentation for them here. In your example your loop would look something like
Declare c CURSOR Local Fast_Forward For
Select userid, newvalue from #updates
Open c
Declare #userid varchar(10), #newvalue varchar(5)
Fetch Next From c Into #userid, #newvalue
While ##FETCH_STATUS = 0
Begin
select #sql = N'update tbl.users set foo = ' + #newvalue + 'where id = ' + #userid;
exec sp_executesql #sql;
Fetch Next From c Into #userid, #newvalue
End
Close c
Deallocate c
As you can see, the setup for a cursor is quite verbose and ugly. Cursors are typically frowned upon as well and you should only use them if you really need to. In your case you don't. You can just join your temp table to your base table and update it that way
Update u Set
foo = t.newvalue
From tbl.Users u
Join #updates t On t.userid = u.id
This is much more performant and easier to read
Not sure why you want to loop, a query is fine.
Since no one proposed a MERGE I do:
MERGE INTO tbl.users
USING #updates
ON tbl.users.id = #updates.userid
WHEN MATCHED THEN
UPDATE
SET foo = #updates.newvalue
SQL server version should be >= 2008 I think
You don't need a loop. This can be done with a join.
update u
set foo = upd.newvalue
from tbl.users u
join #updates upd on u.id = upd.userid

SQL Server - Convert SQL to Stored Procedure

Suppose I have the following structure to a set of tables in my SQL Server (2012) DB:
StartDate: Col1: Col2: .... Coln:
And, the way the DBA set up the database (have no control over that - I only have query access), all the tables with this structure that I'd want to query have, say, names beginning with MyTbl....
So, I would like to create a query that queries ALL these tables at once to get data for a specific StartDate and I've done it using the following SQL:
declare #t table(tablename varchar(50))
declare #sql varchar(max)
set #sql = ''
insert into #t
SELECT t.name AS table_name FROM sys.tables AS t
WHERE t.name LIKE 'MyTbl%'
select #sql = #sql + 'Select ''' + tablename + ''' as Table_Name, t.* From ' + tablename +
' t where StartDate = ''2015-01-01'' +
' union ' from #t
Select #sql = substring(#sql, 1, len(#sql) - 6)
exec(#sql)
In other words:
Find all tables in my DB with names beginning with MyTbl
Query each table for any data with StartDate = '2015-01-01`
Union all those queries together to get one big dataset result
The SQL works perfectly, but I'm getting quite stuck in creating a stored procedure from this query that can take in a parameter for StartDate and I don't know enough about stored procedures to do this correctly.
How could I convert this into a stored procedure that takes a date in for StartDate (to replace the ''2015-01-01'' in the query)?
Any help / guidance would be GREATLY appreciated!!!
THANKS!!!
I noticed you were not looping through each table .. here is something I had put together
CREATE PROCEDURE get_tabledata (#date DATE)
AS
BEGIN
DECLARE #t TABLE (
id INT IDENTITY(1, 1)
,tablename VARCHAR(50)
)
DECLARE #id INT
DECLARE #tablename VARCHAR(max)
DECLARE #sql VARCHAR(max)
SET #sql = ''
INSERT INTO #t
SELECT t.NAME AS table_name
FROM sys.tables AS t
WHERE t.NAME LIKE 'MyTbl%'
SET #id = ##ROWCOUNT
IF (#id > 0)
BEGIN
WHILE (#id > 0)
BEGIN
SET #tablename = (
SELECT tablename
FROM #t
WHERE id = #id
)
SELECT #sql = #sql + 'Select ' + #tablename + ''' as Table_Name, t.* From ' + #tablename + ' t where StartDate = ' + '' + convert(VARCHAR, #date) + ''
SET #sql = #sql + ' union'
Set #id = #id -1;
END
SELECT #sql = substring(#sql, 1, len(#sql) - 6)
END
EXEC (#sql)
END
While it can be a little dense if you're not used to the styling Microsoft uses on these pages, the best place to start would be the Create Procedure documentation on MSDN
https://msdn.microsoft.com/en-us/library/ms187926.aspx
That said, creating a stored procedure is pretty straight forward. Here's a really simple procedure that takes a #startDate parameter and then just returns it back. This is just to illustrate how and where you define your parameters
create procedure dbo.MyProcedure
-- put your input parameters here
#StartDate date
as
--put the body of your procedure (i.e. everything you've written in your OP) here
select #StartDate
go
YOu'll notice however that if you run this twice in a row, you get an error, because it tries to build the same procedure again. Another thing which can come in handy is adding some code before your procedure which will basically check to see if it already exists, and if it does, alter the procedure rather than just blindly re-create it.
This is a snippet from a template I use quite often which handles all of that logic for you. The simplest way to use this is press CTRL-SHIFT-M, which brings up a dialogue to replace all those tags with values you provide.
use [<Database Name, sysname,>]
go
if not exists (select 1
from sys.procedures with(nolock)
where name = '<Procedure Name, sysname,>'
and [schema_id] = schema_id('<Schema, sysname,dbo>')
and type = 'P'
)
exec ('create procedure [<Schema, sysname,dbo>].[<Procedure Name, sysname,>]
as
select ''Procedure not defined.'' as ErrorDescription
return')
--Executed as dynamic SQL since SQL Server Management Studio considures the straight SQL code a syntax error for some reason on the create procedure statement
GO
alter procedure [<Schema, sysname,dbo>].[<Procedure Name, sysname,>]
<Parm 1 Name, sysname,include [#]> <Parm 1 Datatype, sysname,><Parm 1 Default, sql_variant,include [=] if used>,
<Parm 2 Name, sysname,include [#]> <Parm 2 Datatype, sysname,><Parm 2 Default, sql_variant,include [=] if used>
as
/*******************************************************************************************************
********************************************************************************************************/
---------------------------------------------
-- declare variables
---------------------------------------------
---------------------------------------------
-- create temp tables
---------------------------------------------
---------------------------------------------
-- set session variables
---------------------------------------------
set nocount on
---------------------------------------------
-- body of stored procedure
---------------------------------------------
return

Replace empty cells with NULL values in large number of columns

I have SQL table that has a large number of columns. For some reason, some columns have empty cells instead of NULL cells. I would like to make all empty cells in all the columns to be NULL.
I know that the way to go for a single column is:
UPDATE your_table SET column = NULL WHERE column = ''
However, I am not sure how to execute a similar logic efficiently for all columns without having to write the column names one by one.
Thanks,
Run the following query:
SELECT 'UPDATE yourtable SET ' + name + ' = NULL WHERE ' + name + ' = '''';'
FROM syscolumns
WHERE id = object_id('yourtable')
AND isnullable = 1;
The output of this query will be a chunk of SQL script like this:
UPDATE yourtable SET column1 = NULL WHERE column1 = '';
UPDATE yourtable SET column2 = NULL WHERE column2 = '';
UPDATE yourtable SET column3 = NULL WHERE column3 = '';
-- etc...
Copy and paste that SQL script into a new query and run it to update all your columns.
You could do a query on syscolumns to get a list of columns, and use the results to construct your query.
select quotename(name) + ' = nullif (' + quotename(name)+ ','''')'
from syscolumns
where id = object_id('yourtable')
Additionally, if you write your query as
update yourtable
set
yourcolumn=nullif(yourcolumn, ''),
yourcolumn2=nullif(yourcolumn2, ''),
...
then you can do it in a single query without a where clause
I actually use Robert N's answer above daily when I'm importing flat file data sets, so I put it into a stored procedure that I could pass a table name to. It just populates a temp table with the update statements, then executes each row in the table.
USE [master]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author: LikeableBias
-- Create date: 2016-06-27
-- Description: Finds and NULLs all blank values in table where column allows nulls
-- =============================================
CREATE PROCEDURE [dbo].[sproc_NullBlanks]
#tablename NVARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON;
--------Insert update statements to temp table for execution
DECLARE #statements TABLE (statement NVARCHAR(MAX))
INSERT INTO #statements
( statement )
SELECT ('UPDATE '+#tablename+' SET [' + name + '] = NULL WHERE ' + name + ' = '''';')
FROM syscolumns
WHERE id = OBJECT_ID(#tablename)
AND isnullable = 1;
--------Open cursor, execute statements, then close cursor
DECLARE #statement NVARCHAR(MAX)
DECLARE cur CURSOR LOCAL FOR
SELECT statement FROM #statements
OPEN cur
FETCH NEXT FROM cur INTO #statement
WHILE ##FETCH_STATUS = 0 BEGIN
EXEC sys.sp_executesql #statement
FETCH NEXT FROM cur INTO #statement
END
CLOSE cur
DEALLOCATE cur
END
GO