I'm currently trying to write a default procedure template for reporting from a T-SQL Datawarehouse.
The idea is to wrap each query in a procedure, so that permissions and logging can be managed easily.
Since this will be done by the DBAs, I would like to have this solution work by only pasting some standard code before and after the main query. I'd prefer if the DBA didn't have to modify any part of the logging-code.
I've solved this for most parts, however, I need to log which parameters the user has submitted to the procedure.
The obvious solution would be hardcode the parameters into the logging. However, the procedures can have a varying amount of parameters, and I'd therefore like a catch-all solution.
My understanding is that there is no easy way iterating through all parameters.
I can however access the parameter-names from the table sys.parameters.
The closest to a solution I've come, is this minimal example:
CREATE TABLE #loggingTable (
[ProcedureID] INT
, [paramName] NVARCHAR(128)
, [paramValue] NVARCHAR(128)
)
;
go
CREATE PROCEDURE dbo.[ThisIsMyTestProc] (
#param1 TINYINT = NULL
, #Param2 NVARCHAR(64) = null
)
AS
BEGIN
-- Do some logging here
DECLARE #query NVARCHAR(128)
DECLARE #paramName NVARCHAR(128)
DECLARE #paramValue nvarchar(128)
DECLARE db_cursor CURSOR FOR
SELECT [name] FROM [sys].[parameters] WHERE object_id = ##PROCID
OPEN db_cursor
FETCH NEXT FROM db_cursor INTO #paramName
WHILE ##FETCH_STATUS = 0
BEGIN
SET #query = 'SELECT #paramValue = cast(' + #paramName + ' as nvarchar(128))';
SELECT #query;
-- Following line doesn't work due to scope out of bounds, and is prone to SQL-Injections.
--EXEC SP_EXECUTESQL #query; -- Uncomment for error
insert into #loggingTable(ProcedureID, paramName, paramValue)
values(##PROCID, #paramName, #paramValue)
FETCH NEXT FROM db_cursor INTO #paramName
END
CLOSE db_cursor
DEALLOCATE db_cursor
-- Run the main query here (Dummy statement)
SELECT #param1 AS [column1], #Param2 AS [column2]
-- Do more logging after statement has run
END
GO
-- test
EXEC dbo.[ThisIsMyTestProc] 1, 'val 2';
select * from #loggingTable;
-- Cleanup
DROP PROCEDURE dbo.[ThisIsMyTestProc];
DROP table #loggingTable;
However, this does have to major drawbacks.
It doesn't work due to variable scopes
It is prone to SQL-Injections, which is unacceptable
Is there any way to solve this issue?
The values of the parameters are not availiable in a generic approach. You can either create some code generator, which will use sys.parameters to create a chunk of code you'd have to copy into each of your SPs, or you might read this or this about tracing and XEvents. The SQL-Server-Profiler works this way to show you statements together with the parameter values...
If you don't want to get into tracing or XEvents you might try something along this:
--Create a dummy proc
CREATE PROCEDURE dbo.[ThisIsMyTestProc] (
#param1 TINYINT = NULL
, #Param2 NVARCHAR(64) = null
)
AS
BEGIN
SELECT ##PROCID;
END
GO
--call it to see the value of ##PROCID
EXEC dbo.ThisIsMyTestProc; --See the proc-id
GO
--Now this is the magic part. It will create a command, which you can copy and paste into your SP:
SELECT CONCAT('INSERT INTO YourLoggingTable(LogType,ObjectName,ObjectId,Parameters) SELECT ''ProcedureCall'', ''',o.[name],''',',o.object_id,','
,'(SELECT'
,STUFF((
SELECT CONCAT(',''',p.[name],''' AS [parameter/#name],',p.[name],' AS [parameter/#value],''''')
FROM sys.parameters p
WHERE p.object_id=o.object_id
FOR XML PATH('')
),1,1,'')
,' FOR XML PATH(''''),ROOT(''parameters''),TYPE)'
)
FROM [sys].[objects] o
WHERE o.object_id = 525244926; --<-- Use the proc-id here
--Now we can copy the string into our procedure
--I out-commented the INSERT part, the SELECT is enough to show the effect
ALTER PROCEDURE dbo.[ThisIsMyTestProc] (
#param1 TINYINT = NULL
, #Param2 NVARCHAR(64) = null
)
AS
BEGIN
--The generated code comes in one single line
--INSERT INTO YourLoggingTable(LogType,ObjectName,ObjectId,Parameters)
SELECT 'ProcedureCall'
,'ThisIsMyTestProc'
,525244926
,(SELECT'#param1' AS [parameter/#name],#param1 AS [parameter/#value],''
,'#Param2' AS [parameter/#name],#Param2 AS [parameter/#value],''
FOR XML PATH(''),ROOT('parameters'),TYPE)
END
GO
Hint: We need the empty element (,'') at the end of each line to allow multiple elements with the same name.
--Now we can call the SP with some param values
EXEC dbo.ThisIsMyTestProc 1,'hello';
GO
As a result, your Log-Table will get an entry like this
ProcedureCall ThisIsMyTestProc 525244926 <parameters>
<parameter name="#param1" value="1" />
<parameter name="#Param2" value="hello" />
</parameters>
Just add typical logging data like UserID, DateTime, whatever you need...
Scope is the killer issue for this approach. I don't think there's a way to reference the values of parameters by anything but their variable names. If there was a way to retrieve variable values from a collection or by declared ordinal position, it could work on the fly.
I understand wanting to keep the overhead for the DBAs low and eliminating opportunities for error, but I think the best solution is to generate the required code and supply it to the DBAs or give them a tool that generates the needed blocks of code. That's about as lightweight as we can make it for the DBA, but I think it has the added benefit of eliminating processing load in the procedure by turning it into a static statement with some conditional checking for validity and concatenation work. Cursors and looping things should be avoided as much as possible.
Write a SQL script that generates your pre- and post- query blocks. Generate them in mass with a comment at the top of each set of blocks with the stored procedure name and hand it to the DBAs to copy/paste into the respective procs. Alternatively, give them the script and let them run it as needed to generate the pre- and post- blocks themselves.
I would include some checks in the generated script to help make sure it works during execution. This will detect mismatches in the generated code due to subsequent modifications to the procedure itself. We could go the extra mile and include the names of the parameters when the code is generated and verify them against sys.parameters to make sure the parameter names hard-coded into the generated code haven't changed since code generation.
-- Log execution details pre-execution
IF object_name(##PROCID) = 'ThisIsMyTestProc' AND (SELECT COUNT(*) FROM [sys].[parameters] WHERE object_id = ##PROCID) = 2
BEGIN
EXEC LogProcPreExecution #Params = CONCAT('parm1: ', #param1, ' parm2: ', #Param2), #ProcName = 'ThisIsMyTestProc', #ExecutionTime = getdate() #ExecutionUser = system_user
END
ELSE
BEGIN
--Do error logging for proc name and parameter mismatch
END
--Log procedure would look like this
CREATE PROCEDURE
LogProcPreExecution
#Parameters varchar(max),
#ProcName nvarchar(128),
#ExecutionTime datetime,
#ExecutionUser nvarchar(50)
AS
BEGIN
--Do the logging
END
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.
If you’re writing dynamic SQL, always add a Debug mode. It doesn’t have to be anything fancy at first, just something like:
IF #Debug = 1 BEGIN PRINT #MySQLInjectionGift END;
How can we use the above script in our Stored procedure
It can be used as a parameter to inject code.
DROP PROC IF EXISTS dbo.usp_myproc
GO
CREATE PROC dbo.usp_myproc (#Debug bit = 0)
AS
BEGIN
DECLARE #MySQLInjectionGift varchar(max) ='a=''HI THERE'','
DECLARE #SQL varchar(max) =
'
SELECT TOP 3'+IIF(#Debug=1,#MySQLInjectionGift,'')+'* FROM SYS.TABLES
'
EXEC (#SQL)
END
GO
EXEC usp_myproc
GO
EXEC usp_myproc #Debug = 1
You can do anything with it like create WHERE clauses on the fly, create columns that do calculations that are not part of the actual query but helpful in debugging. Typical injection include arbitrary SQL select statements that show progress from one SQL to another as when the stored procedure is large, the decision execution path may not be clear especially when there are lots of conditional statement. The debug mode can also run start-up code that can prepare the data or execute a unit test at the end of the query.
I have just been asked to test 200+ stored procedures (after a DML change and recompile) to make sure that they work in a database that has no unit testing implemented.
As I don't know what "appropriate" input would be for all of these stored procedures, I was wondering if there's a log of procedures that have been run along with the parameters used to run them.
If not, is there a way of testing a bunch of procedures without knowing what valid inputs for their parameters would be?
Or am I about to learn why systems should incorporate testing from the start of the system?
Thanks
I don't think you can get this information from somewhere like sys tables or another place. One thing you can do is to use SQL Server Profiler or Extended Events. This way you can catch all procedure calls with parameters you need.
It turns out I wasn't able to get the information that I had hoped to get from the database.
So, instead, I created an execution listing of all the stored procedures and functions I could find with their parameters using the following:
DECLARE #NAME NVARCHAR(100) =NULL, #SPECIFIC NVARCHAR(100), #PARAMETER NVARCHAR(500)
DECLARE #LISTING NVARCHAR(MAX) =''
DECLARE PARAM_CURSOR INSENSITIVE CURSOR FOR
SELECT SPECIFIC_NAME, PARAMETER_NAME
FROM INFORMATION_SCHEMA.PARAMETERS
where SPECIFIC_NAME in (
select OBJECT_NAME(object_id) name
from sys.objects
where (OBJECT_DEFINITION(object_id) LIKE 'whatever you''re querying'
)
)
OPEN PARAM_CURSOR
FETCH NEXT FROM PARAM_CURSOR INTO #SPECIFIC, #PARAMETER
WHILE ##FETCH_STATUS = 0
BEGIN
IF #NAME = #SPECIFIC
SET #LISTING = #LISTING + ',' + #PARAMETER
ELSE
BEGIN
SET #NAME = #SPECIFIC
SET #LISTING = #LISTING + CHAR(13)+CHAR(10)+ 'EXEC '+#NAME+' '+#PARAMETER
END
IF LEN(#LISTING) > 3000
begin
PRINT #LISTING
set #listing = ''
end
FETCH NEXT FROM PARAM_CURSOR INTO #SPECIFIC, #PARAMETER
END
PRINT #LISTING
close PARAM_CURSOR
DEALLOCATE PARAM_CURSOR
Then I created a list of all the common parameters, and figured out what appropriate values for each of them would be.
Obviously that wasn't ideal, but since I couldn't find another way, I had to do some things manually.
Hope this helps someone else in the future!
Thanks
I have a database with multiple schemas. In every schema I got table called [Logs], so my database tables looks like:
[s1].[Logs]
[s2].[Logs]
[s3].[Logs]
...
[sN].[Logs]
Every day I would like to run stored procedure, which will do same operations on every above table. Is there a way to pass schema name into stored procedure? I am using SQL on Azure.
No, it is not - unless the SP Uses then dynamic SQL to execute some SQL String you constructed in the SP.
This happens via the sp_executesql stored procedure
http://technet.microsoft.com/en-us/library/ms188001.aspx
has more information.
Microsoft has a few undocumented procedures that perform "foreach" operations on tables (sp_msforeachtable) and databases (sp_msforeachdb). Both of these rely on another undocumented proc called sp_msforeachworker which you might be able to exploit to create a foreachschema type of routine. Theres an article (reg required) here that demonstrates this approach.
That said, its unlikely Azure supports anything of these, so you might have to fashion your own using a crude loop:
declare #schemas table (i int identity(1,1), name sysname);
insert into #schemas
select name from sys.schemas where name like 's[0-9]%';
declare #i int, #name sysname, #cmd nvarchar(max);
select #i = min(i) from #schemas;
while #i is not null
begin
select #name = name from #schemas where i = #i;
set #cmd = replace(N'select count(*) from [{0}].[Logs];', '{0}', #name);
print #cmd;
--exec(#cmd);
select #i = min(i) from #schemas where i > #i;
end
I have a stored procedure that alters user data in a certain way. I pass it user_id and it does it's thing. I want to run a query on a table and then for each user_id I find run the stored procedure once on that user_id
How would I write query for this?
use a cursor
ADDENDUM: [MS SQL cursor example]
declare #field1 int
declare #field2 int
declare cur CURSOR LOCAL for
select field1, field2 from sometable where someotherfield is null
open cur
fetch next from cur into #field1, #field2
while ##FETCH_STATUS = 0 BEGIN
--execute your sproc on each row
exec uspYourSproc #field1, #field2
fetch next from cur into #field1, #field2
END
close cur
deallocate cur
in MS SQL, here's an example article
note that cursors are slower than set-based operations, but faster than manual while-loops; more details in this SO question
ADDENDUM 2: if you will be processing more than just a few records, pull them into a temp table first and run the cursor over the temp table; this will prevent SQL from escalating into table-locks and speed up operation
ADDENDUM 3: and of course, if you can inline whatever your stored procedure is doing to each user ID and run the whole thing as a single SQL update statement, that would be optimal
try to change your method if you need to loop!
within the parent stored procedure, create a #temp table that contains the data that you need to process. Call the child stored procedure, the #temp table will be visible and you can process it, hopefully working with the entire set of data and without a cursor or loop.
this really depends on what this child stored procedure is doing. If you are UPDATE-ing, you can "update from" joining in the #temp table and do all the work in one statement without a loop. The same can be done for INSERT and DELETEs. If you need to do multiple updates with IFs you can convert those to multiple UPDATE FROM with the #temp table and use CASE statements or WHERE conditions.
When working in a database try to lose the mindset of looping, it is a real performance drain, will cause locking/blocking and slow down the processing. If you loop everywhere, your system will not scale very well, and will be very hard to speed up when users start complaining about slow refreshes.
Post the content of this procedure you want call in a loop, and I'll bet 9 out of 10 times, you could write it to work on a set of rows.
You can do it with a dynamic query.
declare #cadena varchar(max) = ''
select #cadena = #cadena + 'exec spAPI ' + ltrim(id) + ';'
from sysobjects;
exec(#cadena);
Something like this substitutions will be needed for your tables and field names.
Declare #TableUsers Table (User_ID, MyRowCount Int Identity(1,1)
Declare #i Int, #MaxI Int, #UserID nVarchar(50)
Insert into #TableUser
Select User_ID
From Users
Where (My Criteria)
Select #MaxI = ##RowCount, #i = 1
While #i <= #MaxI
Begin
Select #UserID = UserID from #TableUsers Where MyRowCount = #i
Exec prMyStoredProc #UserID
Select
#i = #i + 1, #UserID = null
End
Use a table variable or a temporary table.
As has been mentioned before, a cursor is a last resort. Mostly because it uses lots of resources, issues locks and might be a sign you're just not understanding how to use SQL properly.
Side note: I once came across a solution that used cursors to update
rows in a table. After some scrutiny, it turned out the whole thing
could be replaced with a single UPDATE command. However, in this case,
where a stored procedure should be executed, a single SQL-command
won't work.
Create a table variable like this (if you're working with lots of data or are short on memory, use a temporary table instead):
DECLARE #menus AS TABLE (
id INT IDENTITY(1,1),
parent NVARCHAR(128),
child NVARCHAR(128));
The id is important.
Replace parent and child with some good data, e.g. relevant identifiers or the whole set of data to be operated on.
Insert data in the table, e.g.:
INSERT INTO #menus (parent, child)
VALUES ('Some name', 'Child name');
...
INSERT INTO #menus (parent,child)
VALUES ('Some other name', 'Some other child name');
Declare some variables:
DECLARE #id INT = 1;
DECLARE #parentName NVARCHAR(128);
DECLARE #childName NVARCHAR(128);
And finally, create a while loop over the data in the table:
WHILE #id IS NOT NULL
BEGIN
SELECT #parentName = parent,
#childName = child
FROM #menus WHERE id = #id;
EXEC myProcedure #parent=#parentName, #child=#childName;
SELECT #id = MIN(id) FROM #menus WHERE id > #id;
END
The first select fetches data from the temporary table. The second select updates the #id. MIN returns null if no rows were selected.
An alternative approach is to loop while the table has rows, SELECT TOP 1 and remove the selected row from the temp table:
WHILE EXISTS(SELECT 1 FROM #menuIDs)
BEGIN
SELECT TOP 1 #menuID = menuID FROM #menuIDs;
EXEC myProcedure #menuID=#menuID;
DELETE FROM #menuIDs WHERE menuID = #menuID;
END;
Can this not be done with a user-defined function to replicate whatever your stored procedure is doing?
SELECT udfMyFunction(user_id), someOtherField, etc FROM MyTable WHERE WhateverCondition
where udfMyFunction is a function you make that takes in the user ID and does whatever you need to do with it.
See http://www.sqlteam.com/article/user-defined-functions for a bit more background
I agree that cursors really ought to be avoided where possible. And it usually is possible!
(of course, my answer presupposes that you're only interested in getting the output from the SP and that you're not changing the actual data. I find "alters user data in a certain way" a little ambiguous from the original question, so thought I'd offer this as a possible solution. Utterly depends on what you're doing!)
I like the dynamic query way of Dave Rincon as it does not use cursors and is small and easy. Thank you Dave for sharing.
But for my needs on Azure SQL and with a "distinct" in the query, i had to modify the code like this:
Declare #SQL nvarchar(max);
-- Set SQL Variable
-- Prepare exec command for each distinctive tenantid found in Machines
SELECT #SQL = (Select distinct 'exec dbo.sp_S2_Laser_to_cache ' +
convert(varchar(8),tenantid) + ';'
from Dim_Machine
where iscurrent = 1
FOR XML PATH(''))
--for debugging print the sql
print #SQL;
--execute the generated sql script
exec sp_executesql #SQL;
I hope this helps someone...