when executing the following stored procedure I get Invalid Object Name dbo.Approved. The object dbo.Approved does exist, so presumably this is something to do with the way i pass the table name in as the parameter?
I should also add that i get the error either by executing the procedure via .NET, or from within SMSS.
#tableName as nvarchar(100)
AS
BEGIN
EXEC('
UPDATE T1
SET T1.NPTid = dbo.Locations.NPT_ID
FROM ' + '[' + #tableName + '] As T1
INNER JOIN dbo.Locations ON T1.Where_Committed = dbo.Locations.Location_Name
')
END
Edit after receiving help from Joe and JNK the sproc is now this but i get the error
Msg 102, Level 15, State 1, Procedure sp_Updater, Line 14
Incorrect syntax near 'QUOTENAME'.
new sproc
#tableName as nvarchar(100),
#schemaName as nvarchar(20)
AS
BEGIN
EXEC('
--Update NPT
UPDATE T1
SET T1.NPTid = dbo.Locations.NPT_ID
FROM ' + QUOTENAME(#schemaName) + '.' + QUOTENAME(#tableName) + ' As T1
INNER JOIN dbo.Locations ON T1.Where_Committed = dbo.Locations.Location_Name
')
END
With the square brackets in you string, your table reference turns into [dbo.Approved] which is not valid. The reference should be [dbo].[Approved] instead.
You might want to consider passing schema name and table name as two separate parameters.
It would also be better to use the QUOTENAME function instead of hard coding the square brackets.
declare #sql nvarchar(1000)
set #sql = N'UPDATE T1
SET T1.NPTid = dbo.Locations.NPT_ID
FROM ' + QUOTENAME(#schemaName) + N'.' + QUOTENAME(#tableName) + N' As T1
INNER JOIN dbo.Locations ON T1.Where_Committed = dbo.Locations.Location_Name
'
EXEC (#sql)
If you use brackets for the three-part-name, you need to have brackets around each section but not the period, i.e.:
[dbo].[Approved]
If you pass dbo.Approved as your parameter, your Dynamic SQL is reading it as [dbo.Approved] which would only work if you had a table named that (i.e. the dbo. is part of the table name not the schema).
Change it to:
'...[dbo].[' + #tablename + ']...
And just pass Approved as the parameter.
Your wrapping the id too early so '[' + #tableName + '] is getting translated to [dbo.approved] when it should be [dbo].[Approved]
Table names and column names are actually sysname (which is, as I recall an NVARCHAR(128) or NVARCHAR(256) - off the top of my head I don't quite remember)
Also, You are vulnerable to a SQL Injection Attack. You should validate that #tableName is a real table by checking it against INFORMATION_SCHEMA.TABLES
Finally, just to be absolutely sure, in case the real table has some odd characters in it, you should use QUOTENAME(#tableName) to fully escape the table name.
Related
I'm trying to fetch the data in a specific table name by passing tableName as a parameter to the stored procedure.
CREATE PROCEDURE schemaName.spDynamicTableName
#tableName NVARCHAR(100)
AS
BEGIN
DECLARE #sql nvarchar(max)
SET #sql = 'SELECT * FROM ' + #tableName
EXECUTE sp_executesql #sql
END;
--> EXEC schemaName.spDynamicTableName 'Employee';
Now, how can I pass list of table names to a procedure so that procedure will iterate over the list of table names and fetch the data from all the tables?
Ok, let's start off with the problems you have in your current set up. Firstly it sounds like you have a design flaw here. Most likely you are using a table's name to infer information that should be in a column. For example perhaps you have different tables for each client. In such a scenario the client's name should be a column in a singular table. This makes querying your data significantly easier and allows for good use for key constraints as well.
Next, your procedure. This is a huge security hole. The value of your dynamic object is not sanitised nor validated meaning that someone (malicious) has almost 100 characters to mess with your instance and inject SQL into it. There are many articles out there that explain how to inject securely (including by myself), and I'm going to cover a couple of processes here.
Note that, as per my original paragraph, you likely really have a design flaw, and so that is the real solution here. We can't address that in the answers here though, as we have no details of the data you are dealing with.
Fixing the injection
Injecting Securely
The basic's of injecting a dynamic object name is to make it secure. You do that by using QUOTENAME; it both delimit identifies the object name and escapes any needed characters. For example QUOTENAME(N'MyTable') would return an nvarchar with the value [MyTable] and QUOTENAME(N'My Alias"; SELECT * FROM sys.tables','"') would return the nvarchar value "My Alias""; SELECT U FROM sys.tables".
Validating the value
You can easily validate a value by checking that the object actually exists. I prefer to do this with the sys objects, so something like this would work:
SELECT #SchemaName = s.[name],
#TableName = t.[name]
FROM sys.schemas s
JOIN sys.tables t ON s.schema_id = t.schema_id
WHERE s.[name] = #Schema --This is a parameter
AND t.[name] = #Table; --This is a parameter
As a result, if the FROM returns no values, then the 2 variables in the SELECT won't have a value assigned and no SQL will be run (as {String} + NULL = NULL).
The Solution
Table Type Parameter
So, to allow for multiple tables, we need a table type parameter. I would create one with both the schema and table name in the columns, but we can default the schema name.
CREATE TYPE dbo.Objects AS table (SchemaName sysname DEFAULT N'dbo',
TableName sysname); --sysname is a sysnonym for nvarchar(128) NOT NULL
And you can DECLARE and INSERT into the TYPE as follows:
DECLARE #Objects dbo.Objects;
INSERT INTO #Objects (TableName)
VALUES(N'test');
Creating the dynamic statement
Assuming you are using a supported version of SQL Server, you'll have access to STRING_AGG; this removes any kind of looping from the procedure, which is great for performance. If you're using a version only in extended support, then use the "old" FOR XML PATH method.
This means you can take the values and create a dynamic statement along the lines of the below:
SET #SQL = (SELECT STRING_AGG(N'SELECT * FROM ' + QUOTENAME(s.[name]) + N'.' + QUOTENAME(t.[name]) + N';',' ')
FROM sys.schemas s
JOIN sys.tables t ON s.schema_id = t.schema_id
JOIN #Objects O ON s.name = O.SchemaName
AND t.name = O.TableName);
The Stored Proecure
Putting all this together, this will give you a procedure that would look like this:
CREATE PROC schemaName.spDynamicTableName #Objects dbo.Objects AS
BEGIN
DECLARE #SQL nvarchar(MAX),
#CRLF nchar(2) = NCHAR(13) + NCHAR(10);
SET #SQL = (SELECT STRING_AGG(N'SELECT N' + QUOTENAME(t.[name],'''') + N',* FROM ' + QUOTENAME(s.[name]) + N'.' + QUOTENAME(t.[name]) + N';',#CRLF) --I also inject the table's name as a column
FROM sys.schemas s
JOIN sys.tables t ON s.schema_id = t.schema_id
JOIN #Objects O ON s.name = O.SchemaName
AND t.name = O.TableName);
EXEC sys.sp_executesql #SQL;
END;
And then you would execute it along the lines of:
DECLARE #Objects dbo.Objects;
INSERT INTO #Objects (SchemaName,TableName)
VALUES(N'dbo',N'MyTable'),
(N'dbo',N'AnotherTable');
EXEC schemaName.spDynamicTableName #Objects;
This one accepts a comma delimited list of tables and guards against SQL injection with a simple QUOTENAME escape (not sure if this is quite enough though):
IF OBJECT_ID('dbo.spDynamicTableName') IS NOT NULL DROP PROC dbo.spDynamicTableName
GO
/*
EXEC dbo.spDynamicTableName 'Students,Robert--
DROP TABLE Students'
*/
CREATE PROC dbo.spDynamicTableName
#tableName NVARCHAR(100)
AS
BEGIN
DECLARE #sql nvarchar(max)
SELECT #sql = STRING_AGG('SELECT * FROM ' + QUOTENAME(value), ';')
FROM STRING_SPLIT(#tableName, ',')
--PRINT #sql
EXEC dbo.sp_executesql #sql
END;
GO
There are two ways you can do this: use a string that contains the names you want and are separated by a special character as:
Table1, Table2, Table3
and split it in the stored procedure (check this)
The second method: make a typo as follows:
CREATE TYPE [dbo].[StringList] AS TABLE
(
[TableName] [NVARCHAR(50)] NULL
)
Add a parameter for your stored procedure as StringList:
CREATE PROCEDURE schemaName.spDynamicTableName
#TableNames [dbo].[StringList] READONLY,
AS
BEGIN
END;
Then measure its length using the following code and make a repeat loop::
DECLARE #Counter INT
DECLARE #TableCount INT
SELECT #TableCount = Count(*), #Counter = 0 FROM #TableNames
WHILE #Counter < #TableCount
BEGIN
SELECT #TableName = Name
FROM #TableNames
ORDER BY Name
OFFSET #Counter ROWS FETCH NEXT 1 ROWS ONLY
SET #sql = 'SELECT * FROM ' + #TableName
EXECUTE sp_executesql #sql
SET #Counter = #Counter + 1
END
I am trying to dynamically insert data into a temp table passing like data as a variable:
DECLARE #data NVARCHAR(MAX)
SET #data = 'INSERT INTO #coco ' + '([' + #val + '])' + ' SELECT [USER_ID] FROM [dbo].[Sheet1$] WHERE [Standard_Name] LIKE ' + #val
EXEC sp_executesql #data
#val is a column name selected from table Sheet1$ and few column name has space between them. While executing, I am getting error, like for column name "Acrobat Reader":
Incorrect syntax near 'Acrobat'.
Also if I am adding data using hardcoded one by one in a column its adding data to one column while other column its adding NULL.
Any suggestion how I can overcome this?
Parametrise your SQL, and this problem "goes away":
DECLARE #data;
SET #data = N'INSERT INTO #Coco (' QUOTENAME(#val) + N')' + NCHAR(10) +
N'SELECT [USER_ID]' + NCHAR(10) +
N'FROM dbo.[Sheet1$]' + NCHAR(10) +
N'WHERE [Standard_Name] = #val;'; --As this doesn't contain a %, there's no need for LIKE
EXEC sp_executesql #data,
N'#val = sysname', --guessed datatype
#val = #val;
Note the comments I made in the SQL though.
Afraid, I've no idea what your second statement means. You'll need to explain further.
I'm trying a very simple drop column statement:
alter table MyTable drop column MyColumn
and receiving several errors along the lines of
Msg 5074, Level 16, State 1, Line 1
The statistics '_dta_stat_1268251623_3_2' is dependent on column 'MyColumn'.
followed ultimately by
Msg 4922, Level 16, State 9, Line 1
ALTER TABLE DROP COLUMN MyColumn failed because one or more objects access this column.
I didn't think statistics prevent a column from being dropped. Do they? If so, since these are apparently auto-created statistics I can't depend on the names being the same across multiple copies of the same database, so how could I drop all such statistics in an upgrade script to be executed on a different database?
The code proposed in JNK answer does not work, but the idea is good. If you want to delete all user created statistics this my tested solution :
DECLARE #sql NVARCHAR(MAX)
DECLARE statCursor CURSOR FOR
SELECT
'DROP STATISTICS ' + QUOTENAME(SCHEMA_NAME(t.schema_id))
+ '.' + QUOTENAME(t.name)
+ '.' + QUOTENAME(st.name) AS sql
FROM
sys.stats AS st
INNER JOIN sys.tables AS t
ON st.object_id = t.object_id
WHERE
st.user_created = 1
ORDER BY 1;
OPEN statCursor;
FETCH NEXT FROM statCursor INTO #sql
WHILE ##FETCH_STATUS = 0
BEGIN
PRINT #sql
EXEC sp_executesql #sql
FETCH NEXT FROM statCursor INTO #sql
END
CLOSE statCursor
DEALLOCATE statCursor
Auto-generated statistics that I have seen all either have the name of the index they represent OR start with something like WA_Sys_.
Are you 100% sure this is not a set of custom stats someone set up?
Check this:
select *
FROM sys.stats WHERE name = '_dta_stat_1268251623_3_2'
...and see what the user_created field indicates.
Per comment:
This is untested but you could try something like:
exec sp_MSforeachdb '
use ?
DECLARE #SQL varchar(max) = ''''
select #SQL = #SQL + ''DROP STATISTICS '' + OBJECT_NAME(c.object_id) + ''.'' + s.name + CHAR(10) + CHAR(13)
from sys.stats s
INNER JOIN sys.stats_columns sc
ON sc.stats_id = s.stats_id
INNER JOIN sys.columns c
ON c.column_id = sc.column_id
WHERE c.name = ''ClaimNbr''
--and s.user_created = 1
PRINT #SQL'
Change the PRINT to an EXEC if it looks good.
sp_msforeachdb is a cursor in the background but the rest of the logic you can do as a set.
Is there any simple way to create a column in MS SQL that will track the last time a record was updated?
I would like to have two fields. One to track when the record was created. That one is simple. Create a datetime field and set its default to getdate(). However the second field seams to be a bit more tricky. I want it to have the latest date (and time) the record was modified.
My options are:
Include getdate() in every update statement - not an option, these tables will be accessed from MS Access
Allow updates only through an SP. - not an option, these tables will be accessed from MS Access
Create triggers for each table - the DB is recreated on many machines and I am afraid it will conflict or be forgotten or get out of synch.
Are there any other options?
Triggers are pretty much your only option here. What is to stop anyone from updating tables with SSMS, those updates would not update the date updated column in that case
Option 4:
Create a stored procedure that automatically creates triggers for all the tables in your database. In SQL 2005, optionally run this trigger any time any table is created (using a DDL trigger).
CREATE PROC UpdateTriggersCreate
AS
DECLARE
#TableSchema sysname,
#TableName sysname,
#PrimaryKeys nvarchar(4000),
#ObjectName nvarchar(4000)
#TriggerName nvarchar(4000),
#SQL nvarchar(4000);
SET #TableName = '';
SET #TableSchema = '';
WHILE 1 = 1 BEGIN
SELECT TOP 1
#TableSchema = TABLE_SCHEMA,
#TableName = TABLE_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE
COLUMN_NAME = 'LastUpdatedDate'
AND (
TABLE_SCHEMA > #TableSchema
OR (
TABLE_SCHEMA = #TableSchema
AND TABLE_NAME > #TableName
)
)
ORDER BY TABLE_SCHEMA, TABLE_NAME;
IF ##RowCount = 0 BREAK;
IF NOT EXISTS (
SELECT *
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS C
WHERE
C.CONSTRAINT_TYPE = 'PRIMARY KEY'
AND C.TABLE_SCHEMA = #TableSchema
AND C.TABLE_NAME = #TableName
) BEGIN
PRINT '-- Not processing table ''' + #TableSchema + '.' + #TableName + ''' because automatic last updated triggers cannot be used on tables with no primary key.';
CONTINUE;
END;
SET #PrimaryKeys = NULL;
SELECT #PrimaryKeys = Coalesce(#PrimaryKeys + ' AND T.', 'T.') + QuoteName(Y.COLUMN_NAME) + ' = I.' + QuoteName(Y.COLUMN_NAME)
FROM
INFORMATION_SCHEMA.TABLE_CONSTRAINTS T
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE Y
ON T.CONSTRAINT_CATALOG = Y.CONSTRAINT_CATALOG
AND T.CONSTRAINT_SCHEMA = Y.CONSTRAINT_SCHEMA
AND T.CONSTRAINT_NAME = Y.CONSTRAINT_NAME
WHERE
T.CONSTRAINT_TYPE = 'PRIMARY KEY'
AND T.TABLE_SCHEMA = #TableSchema
AND T.TABLE_NAME = #TableName;
-- order is not important which is good because ORDER BY is unreliable in this case
SET #ObjectName = #TableSchema + '.' + #TableName;
SET #TriggerName = 'TR_' + Replace(#ObjectName, '.', '_') + '_U_TimeUpdated';
SET #SQL = 'IF Object_ID(''' + #TriggerName + ''', ''TR'') IS NOT NULL DROP TRIGGER ' + #TriggerName;
EXEC sp_executesql #SQL;
SET #SQL = 'CREATE TRIGGER ' + #TriggerName + ' ON ' + #ObjectName + ' FOR INSERT
AS
SET NOCOUNT ON
UPDATE T
SET T.LastUpdatedDate = GetDate()
FROM
' + #ObjectName + ' T
INNER JOIN Inserted I ON ' + #PrimaryKeys;
EXEC sp_executesql #SQL;
END;
Once you have a stored procedure like this, schedule it to run once a day or (in sql 2005 and up) in response to the DDL creation of tables.
Update 1
The code now handles schema properly, and looks up the primary keys. It also reports on and skips tables that have no primary key.
I'm not sure if I worked out all the syntax errors--I adapted it from code I've done this in before and didn't actually test it. I'm sure you can figure it out.
Stored procs are an option with Access, but you have to intercept the event in VBA and call a stored proc instead, followed by Me.Undo.
I've done it but many moons ago and don't have sample code to show you, sorry.
Otherwise triggers are the usual way, either before or after ones.
I'm trying to look for a value in my Microsoft SQL Server 2008 database but I don't know what column or table to look in. I'm trying to craft a query which will just look in all tables and all columns for my value.
You probably could do it using dynamic sql using sys.cols & sys.tables you should be able to create the query.
This will, in all likelyhood, be an extremely long running query.
I rethought my answer and if you run the query below it will generate a number of sql statements, if you run those statements you will find out which column has the value you want. Just replace [your value here] with the appropriate value. This is assuming your value is a varchar.
SELECT 'SELECT ''' + TABLE_NAME + '.' + column_name +
''' FROM ' + TABLE_NAME + ' WHERE ' +
column_name + ' = ''[your value here]'''
FROM INFORMATION_SCHEMA.COLUMNS
WHERE DATA_TYPE = 'varchar';
You can't do it in a single query. You are going to have to cycle through the sys.tables and sys.columns info views and construct multiple queries (a single one for each table) which will look in all the fields for your value in a very long OR construct (one for each field).
I wrote this a while back, not exactly sure what it was for anymore. I remember it was before I knew about sp_msForEachTable though! You might need to adjust the variable sizes (may as well make them all MAX if you are on 2005 +)
create proc SearchForValues (#search varchar(100))
as
Begin
declare #i int
declare #tbl varchar(50)
declare #col varchar(50)
declare #sql varchar(500)
create table #TEMP (id int identity (1,1), colname varchar(50), tblname varchar(50))
insert into #TEMP
select a.name, b.name from dbo.syscolumns a inner join
(
select * from dbo.sysobjects where xtype = 'U'
) b
on a.ID = b.ID
create table #SEARCHRESULT (TblName varchar(50), ColName varchar(50))
If isnumeric(#search) = 0 and #search is not null
begin
set #search = '''' + #search + ''''
end
set #i = 1
While #i <= (select max(id) from #TEMP)
Begin
select #tbl = tblname from #temp where ID = #i
select #col = colname from #temp where ID = #i
set #sql = 'If Exists(select *
from [' + #tbl + ']
where convert(varchar(500), [' + #col + ']) = ' + #search + '
)
Insert Into #SEARCHRESULT (TblName, ColName) Values(''' + #tbl + ''',''' + #col + ''')'
execute (#sql)
set #i = #i + 1
End
drop table #TEMP
select * from #SEARCHRESULT
drop table #SEARCHRESULT
end
You can't with plain SQL. Unless you use a tool that does that (such a PL/SQL developer for Oracle).
I'm risking to be downvoted on this nice 1st april day, but I think it will easier to grep the datafile in this case.
The stored procedure sp_msForEachTable executes a query for each table. This is the simple part. Looking into all columns of every table should be the much harder part. At first, they probably have different data types. So you will probably be only able to perform a string comparison.
But I am quit sure that this is posible by using information from the system tables and some system stored procedures. I would try finding a solution to access a single column on a single table where table name and column name are only given as string parameters. At this point Dynamic SQL comes to mind. If you solved that, it should become relativly simple to get all table names with all column names from the system tables and join every together or put it into a stored procedutre. I would like to see the result if you find a solution.