I need to retrieve change-data-capture rows for several tables, and I'm required (by company IT policy) to access the database via stored procedures. I would rather create a single stored procedure with the table name as a parameter, rather than one stored procedure for each table I'm monitoring. Where I get hung up is that CDC defines a separate table-valued function name for each table monitored, and I'm not sure how best to generalize around that.
Is it possible to modify the following example code so that it invokes cdc.fn_cdc_get_net_changes_dbo_ + #Table instead of cdc.fn_cdc_get_net_changes_dbo_TABLE ?
Is there another approach I should use?
create proc [dbo].GetChangesForTable
#Table varchar(50),
#BeginTime datetime,
#EndTime datetime
as
begin
DECLARE #begin_lsn binary(10), #end_lsn binary(10);
DECLARE #func nvarchar(128)
if #EndTime is null select #EndTime=GETDATE()
SELECT #begin_lsn = sys.fn_cdc_map_time_to_lsn('smallest greater than', #BeginTime);
SELECT #end_lsn = sys.fn_cdc_map_time_to_lsn('largest less than or equal', #EndTime);
-- HOW TO GET THE CORRECT FUNCTION CALLED HERE?
SELECT * FROM cdc.fn_cdc_get_net_changes_dbo_TABLE(#begin_lsn, #end_lsn, 'all')
end
GO
I think that's possible with sp_executesql like that :
DECLARE #sql nvarchar(4000)
SET #sql = N'SELECT * FROM cdc.fn_cdc_get_net_changes_dbo_'+cast(#TABLE as varchar)+'(#begin_lsn, #end_lsn, 'all')'
EXEC sp_executesql #sql, N'#Table varchar(50), #BeginTime datetime, #EndTime datetime',#Table,#BeginTime,#EndTime
Related
I have a bunch of simple expressions, such as:
c=a+b
c=a*b
...
I would like to pass them as parameter to a stored procedure, which is going to perform an update using them.
CREATE TABLE t(
a int,
b int,
c int
);
INSERT INTO t VALUES (1,2,3),(4,5,6);
CREATE PROCEDURE sp #left_member varchar(50), #right_member
AS
BEGIN
UPDATE t
SET #left_member = #right_member
END
EXEC sp 'c', 'a+b'
EXEC sp 'c', 'a*b'
Is there a way of doing something like that ? I would like to possibly avoid dynamic SQL. In my target design, the expressions will be stored in their own table (editable online).
I generally don't recommend doing this, but dynamic SQL is pretty much the solution:
CREATE PROCEDURE usp_exec_dangerous_update (
#left_member nvarchar(50),
#right_member nvarchar(50)
)
AS
BEGIN
DECLARE #sql NVARCHAR(MAX);
SET #sql = N'
UPDATE t
SET [left_member] = [right_member]
';
SET #sql = REPLACE(REPLACE(#sql, '[left_member]', #left_member), '[right_member]', #right_member);
EXEC sp_executesql #sql;
END;
Although such code can be useful in a thoughtful, well-designed system, in general it is not needed:
It exposes the system to SQL injection attacks. Running "generic" code is just dangerous.
It does not handle errors, which are easy to occur with this method.
I am writing a stored procedure which will compare two similar tables under two different database. Here I am using the keyword USE [dbname].
DECLARE
--INPUT
#BASE_DATABASE_NAME NVARCHAR(50),
#TARGET_DATABASE_NAME NVARCHAR(50),
#TARGET_PRODUCT_ID NVARCHAR(50),
#TARGET_PRODUCT_CODE NVARCHAR(50)
SET #BASE_DATABASE_NAME='USE [DB1]'
SET #TARGET_DATABASE_NAME='USE [DB2]'
SET #TARGET_PRODUCT_ID=4194
SET #TARGET_PRODUCT_CODE = #BASE_DATABASE_NAME ( SELECT PRODUCT_CODE FROM T_PRODUCT_MST WHERE PROD_ID = #TARGET_PRODUCT_ID)
print #TARGET_PRODUCT_CODE.
Error--
It's not working...
Can anybody help me out with this? I need to pass the database name dynamically to the sql query.
Thanks in advance..
You cannot use USE this way. USE sets the database against which all the statements are executed and cannot be used inside another query.
You can use dynamic SQL though to specify your query:
DECLARE
--INPUT
#BASE_DATABASE_NAME NVARCHAR(50),
#TARGET_PRODUCT_ID INT,
#TARGET_PRODUCT_CODE NVARCHAR(50)
SET #BASE_DATABASE_NAME='[DB1]'
SET #TARGET_PRODUCT_ID=1
DECLARE #SQL NVARCHAR(MAX) = N'SELECT #TARGET_PRODUCT_CODE = PRODUCT_CODE FROM '
+ #BASE_DATABASE_NAME
+ N'..T_PRODUCT_MST WHERE PROD_ID = #TARGET_PRODUCT_ID'
exec sp_executesql #SQL, N'#TARGET_PRODUCT_ID INT, #TARGET_PRODUCT_CODE NVARCHAR(50) OUTPUT',
#TARGET_PRODUCT_ID, #TARGET_PRODUCT_CODE OUTPUT
print #TARGET_PRODUCT_CODE
Another option to Szymon's answer is to use synonyms. First create your synonyms in the DB:
CREATE SYNONYM [dbo].[TargetProductCode] FOR [DB2].[dbo].[T_Product_MST]
And then your sql syntax becomes:
SET #TARGET_PRODUCT_CODE = SELECT PRODUCT_CODE
FROM dbo.TargetProductCod WHERE PROD_ID = #TARGET_PRODUCT_ID
If this doesn't need to be dynamic, this can be a good solution, and can also make for easier testing, if for some reason you need to point a test DB to a different target (can just update the synonym).
I don't get any errors when I do this, but it creates a table called "dbo.#tablename" in my database when i really want is for it to create the value that i am passing as the parameter in the exec procedure as the tablename. What am i doing wrong. Here is my update procedure script. Maybe i can change so so that it does create the value as the table name.
Here is what i have so far:
ALTER PROCEDURE [dbo].[Load_Negatives]
-- Add the parameters for the stored procedure here
#TABLENAME SYSNAME,
#AuditPeriodStartDate datetime,
#AuditPeriodEndDate datetime
AS
BEGIN
SET NOCOUNT ON;
Select
Location,
Customer,
Transaction_date
into
dbo.[#TABLENAME]
from dbo.CustomerHistory (nolock)
where
[Transaction_date] between #AuditPeriodStartDate and #AuditPeriodEndDate
END
Table names cannot be parametrised. Therefore, you need to build your SQL statement dynamically, incorporating the name into the dynamic script.
To minimise the risk of SQL injection, use the QUOTENAME system function with the #TABLENAME value and introduce parametrisation to your dynamic query to pass the other two parameters of the stored procedure:
ALTER PROCEDURE [dbo].[Load_Negatives]
#TABLENAME SYSNAME,
#AuditPeriodStartDate datetime,
#AuditPeriodEndDate datetime
AS
BEGIN
SET NOCOUNT ON;
DECLARE #sql nvarchar(max);
SET #sql = N'Select
Location,
Customer,
Transaction_date
into
dbo.' + QUOTENAME(#TABLENAME) + N'
from dbo.CustomerHistory (nolock)
where
[Transaction_date] between #AuditPeriodStartDate and #AuditPeriodEndDate'
;
EXECUTE sp_executesql
#sql,
N'#AuditPeriodStartDate datetime, #AuditPeriodEndDate datetime',
#AuditPeriodStartDate, #AuditPeriodEndDate
;
END
Basically, the dynamic query looks almost exactly the same as your current query. The only difference is that the table name is added as the result of QUOTENAME(#TABLENAME). The datetime parameters of the dynamic query happen to have same names as the corresponding parameters of the stored procedure but that is not mandatory.
The EXECUTE sp_executesql statement passes the datetime arguments to the dynamic query and then executes it.
One other note is about your use of the BETWEEN predicate with datetime values. If Transaction_date includes timestamps with non-zero time portions, it would be much better to specify the range in this form:
[Transaction_date] >= #AuditPeriodStartDate
and
[Transaction_date] < #AuditPeriodEndDate
That way you can be sure the results will include only relevant values. More information can be found in this blog article:
What do BETWEEN and the devil have in common?
Not sure how to implement this, but I need a way to get the current list of parameters for a stored procedure as well as their passed in values (this code will be executed in the stored procedure itself).
I know I can use sys.parameters to get the parameter names, but how to get the actual values?
What I need to do with this is to make a char string of the form
#param_name1=#value1,#param_name2=#value2,...,#param_namen=#valuen
I have tried to use dynamic sql, but not having much joy with that.
Any ideas??
Edit:
Currently I am just going through all the parameters one-by-one to build the string. However I want a "better" way to do it, since there are quite a few parameters. And incase parameters are added later on (but the code to generate the string is not updated).
I tried using dynamic sql but gave up, since the sp_executesql sp requires parameters be passed into it...
You state '(this code will be executed in the stored procedure itself).' so assuming you are in the procedure you will already know the parameter names as you have to declare them when creating your procedure. Just do a select and put the names inside text fields
ALTER PROCEDURE procname
(
#param1 NVARCHAR(255)
,#param2 INT
...
)
SELECT [Parameters] = '#param1=' + #param1
+ ',#param2=' + CONVERT(NVARCHAR(MAX),#param2)...
The CONVERT is there as an example for non-char datatypes.
update
You will need to create a linked server that points to itself to use the OPENQUERY function.
USE [master]
GO
/****** Object: LinkedServer [.] Script Date: 04/03/2013 16:22:13 ******/
EXEC master.dbo.sp_addlinkedserver #server = N'.', #srvproduct=N'', #provider=N'SQLNCLI', #datasrc=N'.', #provstr=N'Integrated Security=SSPI'
/* For security reasons the linked server remote logins password is changed with ######## */
EXEC master.dbo.sp_addlinkedsrvlogin #rmtsrvname=N'.',#useself=N'True',#locallogin=NULL,#rmtuser=NULL,#rmtpassword=NULL
GO
Now you can do something like this cursor to get each parameter name and then use dynamic sql in OPENQUERY to get the value:
DECLARE curParms CURSOR FOR
SELECT
name
FROM sys.parameters
WHERE OBJECT_ID = OBJECT_ID('schema.procedurename')
ORDER BY parameter_id
OPEN curParms
FETCH curParms INTO #parmName
WHILE ##FETCH_STATUS <> -1
BEGIN
SELECT #parmName + '=' + (SELECT * FROM OPENQUERY('linkedservername','SELECT ' + #parmName))
FETCH curParms INTO #parmName
END
CLOSE curParms
DEALLOCATE curParms
Since SQL Server 2014 we have sys.dm_exec_input_buffer, it is a table valued function with an output column event_info that gives the full execution statement (including parameters).
We can parse the param values from sys.dm_exec_input_buffer and get the param names from sys.parameters and join them together to get the string you want.
For example:
create procedure [dbo].[get_proc_params_demo]
(
#number1 int,
#string1 varchar(50),
#calendar datetime,
#number2 int,
#string2 nvarchar(max)
)
as
begin
-- get the full execution statement
declare #statement nvarchar(max)
select #statement = event_info
from sys.dm_exec_input_buffer(##spid, current_request_id())
-- parse param values from the statement
declare #proc_name varchar(128) = object_name(##procid)
declare #param_idx int = charindex(#proc_name, #statement) + len(#proc_name)
declare #param_len int = len(#statement) - #param_idx
declare #params nvarchar(max) = right(#statement, #param_len)
-- create param values table
select value, row_number() over (order by current_timestamp) seq
into #params
from string_split(#params, ',')
-- get final string
declare #final nvarchar(max)
select #final = isnull(#final + ',','') + p1.name + '=' + ltrim(p2.value)
from sys.parameters p1
left join #params p2 on p2.seq = parameter_id
where object_id = ##procid
select #final params
end
To test it:
exec get_proc_params_demo 42, 'is the answer', '2019-06-19', 123456789, 'another string'
Returns the string you want:
#number1=42,#string1='is the answer',#calendar='2019-06-19',#number2=123456789,#string2='another string'
I have something similar wrapped as a UDF. I use it for error logging in catch blocks.
There are bunch of databases to the SQL server I am connected.
How should I query the sysobjects in order to spot in what database a stored procedure with name 'myStoredProcedure' is located ?
The query should return the database name.
Thanks
I know you are not asking for this, but I'd really download RedGate's Sql Search add-in for SSMS and use that. It allows you to find any object (proc, table, view, column, etc) on any database easily.
And it's free!
I'd give this a try:
CREATE TABLE ##DatabaseList
(
DatabaseName varchar(50)
)
EXECUTE SP_MSForEachDB 'USE [?]; INSERT INTO ##DatabaseList SELECT DB_NAME() FROM [sys].[objects] WHERE name = "MyStoredProcedure" AND type_desc = "SQL_STORED_PROCEDURE"'
SELECT * FROM ##DatabaseList
DROP TABLE ##DatabaseList
That's using the undocumented/ unsupported system stored procedure SP_MSForEachDb and writing any hits to a global temp table, then outputting the contents to the Results window before dropping the table. If you just need to know which database (or databases - there may of course be more than one) has an appropriately named SP, this should do it. If you want to use the output elsewhere as a parameter, it may take a little more work.
By the way, I'm only learning this stuff myself over the last few months so if anyone can critique the above and suggest a better way to go at it I'm happy to receive feedback. Equally, I can answer any further questions posted here to the best of my ability.
Cheers
So out of curiosity I decided to try write this myself, especially since ADG mentioned his solution was using an unsupported, undocumented procedure. This could also be expanded to take a 2nd parameter so where it checks the type = P (stored Proc) you could probably change it to look for other things like views / tables etc.
My solution is a bit long but here goes:
CREATE PROCEDURE spFindProceduresInDatabases
(
#ProcedureName NVARCHAR(99)
)
AS
BEGIN
-- Get all the database names and put them into a table
DECLARE #Db TABLE (DatabaseName Varchar(99))
INSERT INTO #Db SELECT name FROM Sys.databases
-- Declare a table to hold our results
DECLARE #results TABLE (DatabaseName VARCHAR(99))
-- Make a Loop
-- Declare a variable to be incremented
DECLARE #count INT
SET #count = 0
-- Declare the end condition
DECLARE #endCount INT
SELECT #endCount = COUNT(*) FROM #Db
-- Loop through the databases
WHILE (#count < #endCount )
BEGIN
-- Get the database we are going to look into
DECLARE #dbWeAreChecking VARCHAR(99)
SELECT TOP 1 #dbWeAreChecking = DatabaseName FROM #Db
DELETE FROM #Db WHERE DatabaseName = #dbWeAreChecking
-- Create and execute our query
DECLARE #Query NVARCHAR(3000)
SET #Query = N'SELECT #outParam = COUNT(*) FROM '+#dbWeAreChecking+'.sys.sysobjects WHERE type = ''P'' and name = #ProcedureName'
Declare #outParam INT
print (#Query)
DECLARE #ParmDefinition NVARCHAR(500)
DECLARE #IntVariable INT
SET #ParmDefinition = N'#ProcedureName VARCHAR(99),#outParam INT OUTPUT'
SET #IntVariable = 35
EXECUTE sp_executesql
#Query ,
#ParmDefinition,
#ProcedureName,
#outParam = #outParam OUTPUT
-- If we have a result insert it into the results table
If (#outParam > 0)
BEGIN
INSERT INTO #results(DatabaseName) VALUES(#dbWeAreChecking)
END
-- Increment the counter
SET #count = (#count + 1)
END
-- SELECT ALL OF THE THINGS!!!
SELECT * FROM #results
END