Use table name as parameter in SQL function (bis) - sql-server

Update: I Solved this. Obviously I reinvented the wheel, but I did not immediately find the answer where I searched.
Given there is another question exactly the same as mine, but that does not answer my question, I will try to be very clear. My question is not answered because the answers do not indicate how to accomplish my task. I don't really care how I accomplish it, but there has to be a way.
I want the ability to count occurrences by level of any two discrete columns in
an arbitrary table. I want to store the results for later reference because the query takes a long time to run.
The table name and two column names should be definable.
Based on a lot of research, it appears that a function, not a procedure should be used, but I am more interested in what happens, not how it happens.
DROP FUNCTION IF EXISTS O_E_1
GO
DROP TABLE IF EXISTS TestTable
GO
CREATE FUNCTION O_E_1
(#feature NVARCHAR(128), #table NVARCHAR(128))
RETURNS TABLE
AS
RETURN
(SELECT
COUNT(DISTINCT [PersonID]) AS count_person,
#feature AS feature, [HasT2DM] AS target
FROM
dbo.#table
GROUP BY
[#feature], [HasT2DM]);
GO
SELECT *
INTO TestTable
FROM O_E_1('Diagnosis', 'PatientDiagnoses')
go
I hope that with a little bit of work, I can accomplish this.
I have a version that does this in a procedure using dynamic SQL but
unfortunately, I don't see how to save that result to a table. If someone wants to tell me how to save the results of a dynamic SELECT to a table in my schema, that would accomplish what I need.
Here is the procedure version with dynamic SQL. Also included is how I am trying to store the results into a table.
BEGIN
SET NOCOUNT ON;
DECLARE #cmd NVARCHAR(max)
set #cmd = '
(SELECT
COUNT(DISTINCT [PersonID]) AS count_person,
[' + #feature + '] AS feature, [HasT2DM] AS target
FROM
dbo.[' + #table + ']
GROUP BY
[' + #feature + '], [HasT2DM])
'
EXEC sp_executesql #cmd
END
GO
O_E_1 #feature = 'Diagnosis', #table = 'PatientDiagnoses'
SELECT *
INTO TestTable
FROM (O_E_1 #feature = 'Diagnosis', #table = 'PatientDiagnoses')
GO
I was able to code the answer I need. Here it is.
DROP PROCEDURE IF EXISTS O_E_1
GO
DROP TABLE IF EXISTS TestTable
GO
CREATE PROCEDUre O_E_1
#feature NVARCHAR(128),
#table NVARCHAR(128)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #cmd NVARCHAR(max)
set #cmd = '
(SELECT
COUNT(DISTINCT [PersonID]) AS count_person,
[' + #feature + '] AS feature, [HasT2DM] AS target
FROM
dbo.[' + #table + ']
GROUP BY
[' + #feature + '], [HasT2DM])
'
EXEC sp_executesql #cmd
END
GO
DROP TABLe IF EXISTS RESULT
CREATE TABLE Result
(count_person numeric,
feature varchar(128),
target varchar(128)
)
INSERT Result EXEC O_E_1 #feature = 'Diagnosis', #table = 'PatientDiagnoses'
Select TOP 100 * FROM RESULT

Related

Calling dynamic SQL or stored procedure

I'm using a search object query (found on the internet, wish I could give credit to the developer) to search database for the columns needed when I write queries. The output search object query allows me to enter the type of table to look in (partial name) as well as the column name (partial name) I'm trying to find. I've been attempting to modify the search object query so it returns the 1st value (Top 1) it finds. This would help me to easily see at a glance if the column has the particular type of data I'm looking for.
I've attempted to write it both as a stored procedure that I could pass two parameters (partial table and partial column name) and I've also tried using dynamic SQL (my first attempt at using it, so I'm a novice when it comes to use it). I had moderate success with the use of dynamic SQL, but can only get it to produce one result rather than be called multiple times for all the results in my search object output. The code I used is shown here:
-- This is the search object query found on internet
Use masterdb
Select a.name, b.name
From sysobjects a
Inner Join syscolumns b On a.id = b.id
Where b.name like '%Result%'
And a.name like '%Lab%'
Order By a.name, b.name
-- This is a separate query I used to test calling the data with dynamic SQL
DECLARE #value VARCHAR(100), #tablename VARCHAR(100)
SET #value = 'Result'
SET #tablename = 'LabSpecimen'
DECLARE #sqlText NVARCHAR(1000);
SET #sqlText = N'SELECT Top 1 ' + #value + ' FROM testndb.dbo.' + #tablename
EXEC (#sqlText)
If I use the search object query and search for tables that have lab and column names that have result, I might get output like this:
LabMain,ResultID
LabSpecimen,ResultCategory
LabSpecimen,ResultDate
LabSpecimen,Results
I would like to have the search object query pull data from the table in the first column and the column name in the 2nd column and return the first value it finds to give me a sample output for the given column name/table. Output would look like this:
LabMain,ResultID,E201812310001
LabSpecimen,ResultCategory,ExampleCategory
LabSpecimen,ResultDate,20181231
LabSpecimen,Results,34.20
Okay, I really didn't want to have to post an answer to this, but here goes.
So, the first, really-really-huge thing is: SQL Injection. SQL Injection is the #1 security vulnerability for something like a dozen years running, per OWASP. Basically, SQL Injection is where you use dynamic SQL that has any fragment of the sql command being populated by a user. So in the OP's case, this section here:
SET #value = 'Result'
SET #tablename = 'LabSpecimen'
DECLARE #sqlText NVARCHAR(1000);
SET #sqlText = N'SELECT Top 1 ' + #value + ' FROM testndb.dbo.' + #tablename
EXEC (#sqlText)
... if the end incarnation would be that #tableName and #value are populated by the user as part of their search? Then the user can do a 'search' that ends up injecting sql statements that the server runs directly; for a cheap example, imagine this for #value:
3' ; drop table #tableName --
... which would go ahead and drop every table that matches the #tablename you passed in.
Anyway, so, as we go through this problem, we're going to keep SQL Injection in mind at every step.
Problem #1: How to get the tables/columns that match.
You pretty much already nailed this. The only thing missing is to put it into a temp table so that you can loop through it (and limit it down to U-types, since otherwise you'll get stored procs and system tables.) I went ahead and had it also hit the Schema information - that way, if you have tables in different schemas, it'll still be able to get the results.
declare #tableNameFragment varchar(100) -- note: these top 4 lines will eventually
declare #columnNameFragment varchar(100) -- be changed to stored proc args
set #tableNameFragment = 'Performance' -- and populated by the user calling
set #columnNameFragment = 'status' -- the proc (instead of hard-coded.)
declare #entityMatches TABLE (TableName varchar(200), ColName varchar(128))
insert into #entityMatches
Select sch.TABLE_SCHEMA + '.' + sysobj.name as TableName, syscol.name as ColName
From sysobjects sysobj
Join syscolumns syscol On sysobj.id = syscol.id
Join INFORMATION_SCHEMA.TABLES sch on sch.TABLE_NAME = sysobj.name
where sysobj.xtype = 'U'
and (sysobj.name like '%' + isnull(#tableNameFragment,'') + '%')
and (syscol.name like '%' + isnull(#columnNameFragment,'') + '%')
Now, notice that while #tableNameFragment and #columnNameFragment are used, they're not used in a dynamic query. It doesn't matter if the user puts in something malicious into those values
Problem #2 - How to loop through your table
Basically, you're going to need a cursor. I hate cursors, but sometimes (like this one), they're necessary.
Problem #3 - How to actually do a dynamic query and get a result back
This is actually trickier than it looks. You can't do a raw EXEC() for a return value, nor can you simply have the cmd you're executing populating a variable - because EXEC (and SP_ExecuteSql operate in a different context, so they can't populate variables outside in your script.)
You need to use SP_ExecuteSQL, but specify a return variable getting populated by the interior sql command. For example:
declare #sqlCmd nvarchar(max)
declare #dynamicReturn varchar(max)
set #sqlCmd = 'select #retVal=1'
EXEC Sp_executesql #sqlCmd,
N'#retVal varchar(max) output',
#dynamicReturn output
select #dynamicReturn
Problem #4 - How to write your Dynamic command
Here's where things get dicey, since it's where we're using a dynamic SQL command. The important thing here is: you cannot use anything the user provided as an input. Which means, you can't use the variables #tableNameFragment or #columnNameFragment. You can use the values in the #entityMatches table, though. Why? Because the user didn't populate them. They got populated by the data in the sys tables - it doesn't matter if the user puts something nefarious in the input variables, that #entityMatches data simply holds the existing table/column names that match.
Also important: When you're working on code that could be a problem if a future dev down the line tweaks or copies/pastes - you should put comment warnings to illuminate the issue.
So, putting it all together? You'll have something that looks like this:
declare #tableNameFragment varchar(100) -- note: these top 4 lines will eventually
declare #columnNameFragment varchar(100) -- be changed to stored proc args
set #tableNameFragment = 'Performance' -- and populated by the user calling
set #columnNameFragment = 'status' -- the proc (instead of hard-coded.)
declare #entityMatches TABLE (TableName varchar(200), ColName varchar(128))
insert into #entityMatches
Select sch.TABLE_SCHEMA + '.' + sysobj.name as TableName, syscol.name as ColName
From sysobjects sysobj
Join syscolumns syscol On sysobj.id = syscol.id
Join INFORMATION_SCHEMA.TABLES sch on sch.TABLE_NAME = sysobj.name
where sysobj.xtype = 'U'
and (sysobj.name like '%' + isnull(#tableNameFragment,'') + '%')
and (syscol.name like '%' + isnull(#columnNameFragment,'') + '%')
declare #returnResults TABLE (TableName varchar(200), ColName varchar(128), FirstValue varchar(max))
declare Cur Cursor For select TableName,ColName from #entityMatches
declare #cursorTable varchar(200), #cursorColumn varchar(128)
open Cur
fetch Next from cur into #cursorTable,#cursorColumn
while ##FETCH_STATUS = 0
begin
-- Note: the variables #cursorTable, #cursorColumn are NOT user populated
-- but instead are populated from the Sys tables. Because of this,
-- this dynamic sql below is not SQL-Injection vulnerable (the entries
-- are not populated from user entry of any sort.)
-- Be very careful modifying the lines below to make sure you don't
-- introduce a vulnerability.
declare #sqlCmd nvarchar(max)
declare #dynamicReturn varchar(max)
set #sqlCmd = 'select top 1 #retVal=[' + #cursorColumn + '] from ' + #cursorTable
EXEC Sp_executesql #sqlCmd,
N'#retVal varchar(max) output',
#dynamicReturn output
insert into #returnResults values (#cursorTable, #cursorColumn, #dynamicReturn)
fetch Next from cur into #cursorTable,#cursorColumn
End
close cur
deallocate cur
select * from #returnResults
Create a stored procedure like below mention stored procedure.
Get the table and column name from sysobject & syscolumn and add it in hash table on the base of parameter of stored procedure. After that declare a cursor and in loop of cursor create a dynamic query of column and table name and get first row of current column from table of cursor loop. After that execute the query and update the result in the hash table. At the end of lookup select the Record from hash table. Check the below stored procedure. I hope that its helpful for you.
Create procedure Sp_GetSampleData
#TName varchar(200) = ''
as
Select
a.name TableName, b.name ColumnName,
CAST('' as varchar(max)) as SampleValue
into
#Tbl
from
sysobjects a
inner join
syscolumns b on a.id = b.id
where
(#TName='' or a.name = #TName)
order ny
a.name, b.name
declare #TableName varchar(200), #ColumnName varchar(200),
#sqlText nvarchar(max), #Val varchar(max)
declare Cur Cursor For
select TableName, ColumnName
from #Tbl
open Cur
fetch Next from cur into #TableName,#ColumnName
while ##FETCH_STATUS =0
begin
set #sqlText=''
set #Val=''
SET #sqlText = N'SELECT Top 1 #Val=[' + #ColumnName + '] FROM testndb.dbo.' + #TableName
EXEC Sp_executesql
#sqlText,
N'#Val varchar(max) output',
#Val output
print #sqlText
update #Tbl set SampleValue=#Val where TableName=#TableName and ColumnName =#ColumnName
fetch Next from cur into #TableName,#ColumnName
End
close cur
deallocate cur
select * from #Tbl

Deadlock MS-SQL when Inserting Into a Table

I am using the following query to insert in the respective historical table changes occurred to a given table. I am executing the same query simultaneously for multiple tables in python (changing the table name and database). None of the historical tables have foreign keys. But some of the executions end up in deadlock. Each table have assign a unique historical table. I am not sure how to solve the issue. Is it because I use a variable table with the same name in all the procedures?
declare #name_tab table (name_column varchar(200),
dtype varchar(200))
declare #columns varchar(max)
declare #query varchar(max)
declare #database varchar(200)
declare #table_name varchar(200)
set #database = '%s'
set #table_name = '%s'
insert into #name_tab
select c.name as name_column,
t.name as dtype
from sys.all_columns c
INNER JOIN sys.types t
ON t.system_type_id = c.system_type_id
where OBJECT_NAME(c.object_id) = #table_name
set #columns= stuff((select ','+name_column from #name_tab FOR XML PATH('')),1, 1, '')
set #query= 'insert into ' +#database+'..'+'HISTORY_'+#table_name+' select super_q.* from' +
'(select cast (GETDATE() as smalldatetime) as TIME_MODIFIED, new_info.* from '+
'(SELECT ' + #columns + ' From '+#database+'..'+#table_name +
' except ' +
'SELECT ' + #columns + ' From '+#database+'..'+'HISTORY_'+#table_name + ') new_info) as super_q'
execute(#query)
I got this sample from system_health
It appears that some concurrent process is altering or creating a table at the same time. The deadlock XML should contain additional details about what's going on.
But whatever the actual cause, the solution is simple. Use your scripting above to generate the trigger bodies in static SQL so you don't have to query the catalog for every insert.
Create a procedure in your database called, say, admin.GenerateHistoryTables and one called admin.GenerateHistoryTriggers and run those ahead of time to install the history tables and wire up the triggers.
Or stop re-inventing the wheel and use Change Data Capture or Temporal Tables.

procedure to rollback a transaction from audit table in SQL Server

I have created an audit table that is populated by an audit Trail (triggers after every update, delete, and insert) on different tables in my database. I am now asked to create a stored procedure (script) to rollback the data change using the audit id. How do I go about do so. I wrote a script which seems good. The command is accepted by SQL Server (command completed Successfully). Unfortunately when I test it by passing the Audit_id, the command is completed but the data is not rolled back. This is the Procedure I developed. Any help will be greatly appreciated.
create PROCEDURE [dbo].[spAudit_Rollback_2]
#AUDIT_ID NVARCHAR(MAX)
AS
SET Nocount on
BEGIN
DECLARE
#TABLE_NAME VARCHAR(100),
#COLUMN VARCHAR(100),
#OLD_VALUE VARCHAR(200),
#ID varchar(50)
SELECT #TABLE_NAME = TABLE_NAME FROM AUDIT;
SELECT #COLUMN = [COLUMN] FROM AUDIT;
SELECT #AUDIT_ID = AUDIT_ID FROM AUDIT;
SELECT #OLD_VALUE = OLD_VALUE FROM AUDIT
SELECT #ID = ROW_DESCRIPTION FROM AUDIT;
update [Production].[UnitMeasure]
set #COLUMN = #OLD_VALUE
WHERE [Production].[UnitMeasure].[UnitMeasureCode] = #ID
END
[dbo].[spAudit_Rollback_2]'130F0598-EB89-44E5-A64A-ABDFF56809B5
This is the same script but using adventureworks2017 database and data.
If possible I would even prefer to use a variable to retrieve that table name from Audit and use that in the procedure. That too is giving me another error.
Any help with this procedure will be awesome.
This needs to be dynamic SQL because you're updating a column that's defined in a variable. Do the following in place of your current UPDATE statement.
DECLARE #sql VARCHAR(1000) = ''
SET #sql = 'UPDATE [Production].[UnitMeasure] ' +
'SET ' + #COLUMN + ' = ''' + #OLD_VALUE + '''' +
'WHERE [Production].[UnitMeasure].[UnitMeasureCode] = ''' + #ID + ''''
EXEC(#sql)

Renaming multiple tables

In SQL Server, I have a database abc. In this database I have hundreds of tables. Each of these tables is called xyz.table
I want to change all the tables to be called abc.table.
Do we have a way by which I can change all the names from xyz.table to abc.table in database abc?
I am able to manually change the name by changing the schema for each table to abc
You could have a cursor run over all your tables in the xyz schema and move all of those into the abc schema:
DECLARE TableCursor CURSOR FAST_FORWARD
FOR
-- get the table names for all tables in the 'xyz' schema
SELECT t.Name
FROM sys.tables t
WHERE schema_id = SCHEMA_ID('xyz')
DECLARE #TableName sysname
OPEN TableCursor
FETCH NEXT FROM TableCursor INTO #TableName
-- iterate over all tables found
WHILE ##FETCH_STATUS = 0
BEGIN
DECLARE #Stmt NVARCHAR(999)
-- construct T-SQL statement to move table to 'abc' schema
SET #Stmt = 'ALTER SCHEMA abc TRANSFER xyz.' + #TableName
EXEC (#Stmt)
FETCH NEXT FROM TableCursor INTO #TableName
END
CLOSE TableCursor
DEALLOCATE TableCursor
You can use Alter Schema with an undocumented Stored Procedure exec sp_MSforeachtable which basically iterates through all the tables .
exec sp_MSforeachtable "ALTER SCHEMA new_schema TRANSFER ? PRINT '? modified' "
change the new_schema keyword with your new Schema .
For details please go through the link
sp_MSforeachtable
Alter Schema for all the tables
As others have pointed out that the SP is deprecated so There is another way to do this by getting the names of the table from sys.tables
Declare #value int
Set #value=1
declare #sql varchar(max), #table varchar(50), #old varchar(50), #new varchar(50)
set #old = 'dbo'
set #new = 'abc'
while exists(select * from sys.tables where schema_name(schema_id) = #old)
begin
;With CTE as
(
Select *,row_number() over(order by object_id) rowNumber from sys.tables
where schema_name(schema_id) = #old
)
select #table= name from CTE where #value=rowNumber
Set #value=#value+1
set #sql = 'alter schema ' + #new + ' transfer ' + #old + '.' + #table
exec(#sql)
end
I'm assuming You've already created the schema abc in the database.
If not you can refer here
http://www.youtube.com/watch?v=_DDgv8uek6M
http://www.quackit.com/sql_server/sql_server_2008/tutorial/sql_server_database_schemas.cfm
To change the schema of all the tables in database you can use following system created msforeachtable stored procedure to rename schema of each table with alter schema.
exec sp_MSforeachtable "ALTER SCHEMA abc TRANSFER ? PRINT '? modified' "
Without using the undocumented/unsupported sp_MSforeachtable procedure, here's a somewhat concise way to select and/or run all of the necessary ALTER statements for every table on the given schema:
declare #oldSchema nvarchar(50) = 'abc' -- usually 'dbo'
declare #newSchema nvarchar(50) = 'xyz' -- use your new schema name
declare #sql nvarchar(max) =
(select
(select N'alter schema [' + #newSchema + '] transfer [' + #oldSchema + '].[' + name + ']
' as 'data()'
from sys.tables
where schema_name(schema_id) = #oldSchema for xml path(''), type)
.value('text()[1]','nvarchar(max)'))
-- You can select out the results for scrutiny
select #sql
-- Or you can execute the results directly
exec (#sql)
This avoids using a cursor, and uses brackets to escape table names that may conflict with SQL keywords.

How to get a database object based on an OBJECT_ID?

In SQL Server 2008, is there a way to access an object based on the OBJECT_ID?
[Edit] The example described below was solved using Andrew's suggestion in the comment, but I'm still curious about the general case. Can an object itself be retrieved using OBJECT_ID, or can it only be accessed indirectly by using the object name via sp_executesql?
My specific case is a stored procedure that uses several temporary tables. At the end of the procedure I want to dump the data from the temporary tables into actual tables for analysis (only if a debug switch is enabled).
The code for dumping the data is similar to this:
IF OBJECT_ID('tempdb..#MyTempTable', 'U') IS NOT NULL
BEGIN
IF OBJECT_ID('Debug_MyTempTable', 'U') IS NOT NULL
DROP TABLE Debug_MyTempTable
SELECT * INTO Debug_MyTempTable FROM #MyTempTable
END
This code block is repeated for each temporary table, so I would prefer to put it in a procedure and call it with a table name:
EXEC [dbo].[CreateDebugTable]
#tableName = 'MyTempTable'
I imagine the procedure would look something like:
CREATE PROCEDURE [dbo].[CreateDebugTable]
#tableName VARCHAR(50)
AS
BEGIN
IF OBJECT_ID('tempdb..#' + #tableName, 'U') IS NOT NULL
BEGIN
IF OBJECT_ID('dbo.Debug_' + #tableName, 'U') IS NOT NULL
DROP TABLE <Debug_TempTable>
SELECT * INTO <Debug_TempTable> FROM <#TempTable>
END
END
The procedure depends on being able to translate the OBJECT_ID of DebugTempTable and #TempTable into the actual tables (shown with <> in the code above). Is this possible?
[Edit]
This is the altered procedure using sp_executesql instead of explicit tables.
CREATE PROCEDURE [dbo].[CreateDebugTable]
#tableName VARCHAR(50)
AS
BEGIN
DECLARE #tmpTable VARCHAR(50) = '#' + #tableName
DECLARE #dboTable VARCHAR(50) = 'Debug_' + #tableName
DECLARE #sql NVARCHAR(100)
IF OBJECT_ID('tempdb..' + #tmpTable, 'U') IS NOT NULL
BEGIN
IF OBJECT_ID('dbo.' + #dboTable, 'U') IS NOT NULL
BEGIN
SET #sql = 'DROP TABLE ' + #dboTable
EXECUTE sp_executesql #sql
END
SET #sql = 'SELECT * INTO ' + #dboTable + ' FROM ' + #tmpTable
EXECUTE sp_executesql #sql
END
END
The object_id is just used as a key in the various metadata views. There is no TSQL syntax to SELECT from (or otherwise manipulate) objects based on their object_id.
If you have an object_id then in general you could use
SELECT QUOTENAME(OBJECT_SCHEMA_NAME(#object_id[,database_id])) +
'.' +
QUOTENAME(OBJECT_NAME(#object_id[,database_id]) )
To get the 2 part name of the object but for #temp tables this returns the long internal name rather than the short one that you can actually use in queries.

Resources