Do statistics referencing a column prevent that column from being dropped? - sql-server

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.

Related

Alter tables in a schema

I am trying to set a default value to a column(Inserted_time), but first i need to check if the column exists in the tables. If the column doesn't exist, I need to add that column and give it a default value.
I am working with Sql Server Management Studio.
So far I have written this code:
IF EXISTS ( select TABLE_NAME from INFORMATION_SCHEMA.COLUMNS where TABLE_CATALOG = 'DB_COPY' and COLUMN_NAME = 'Inserted_Time')
begin
ALTER TABLE table_name ADD CONSTRAINT [Inserted_Time_Def] SET DEFAULT (sysdatetimeoffset()) FOR [Inserted_Time]
end
else
ALTER TABLE table_name ADD COLUMN [Inserted_Time] CONSTRAINT [Inserted_Time_Def] DEFAULT (sysdatetimeoffset()) WITH VALUES
Once I retrieve the tables that has the column, I need to add that table_name to the Alter command. But I am not able to do that. Can someone please tell me how to use the table_names retrieved from select statement in the alter statement?
First, you want to put all the table names in a temporary table so you can loop through it.
After, you can use a cursor to execute a command for each table name.
In my example, I only printed the command I wanted to execute. That way you can be sure the code will do what you want first.
Example :
select TABLE_NAME As TableName INTO #TablesList from INFORMATION_SCHEMA.COLUMNS where TABLE_CATALOG = 'DB_COPY' and COLUMN_NAME = 'Inserted_Time'
DECLARE #TablesCursor as CURSOR;
DECLARE #TableName as NVARCHAR(max);
DECLARE #CommandToExecute as NVARCHAR(max);
SET #TablesCursor = CURSOR FOR SELECT TableName FROM #TablesList;
OPEN #TablesCursor;
FETCH NEXT FROM #TablesCursor INTO #TableName;
WHILE ##FETCH_STATUS = 0
BEGIN
SET #CommandToExecute = 'ALTER TABLE ' + #TableName + ' WHAT YOU WANNA DO '
PRINT #CommandToExecute
--EXEC(#CommandToExecute)
FETCH NEXT FROM #TablesCursor INTO #TableName;
END
CLOSE #TablesCursor;
DEALLOCATE #TablesCursor;
Assuming that every table is in a different schema, then you could do something like this:
DECLARE #SQL nvarchar(MAX);
SET #SQL = STUFF((SELECT NCHAR(13) + NCHAR(10) +
CASE WHEN EXISTS (SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS C
WHERE T.TABLE_SCHEMA = C.TABLE_SCHEMA
AND T.TABLE_NAME = C.TABLE_SCHEMA
AND C.COLUMN_NAME = N'Inserted_Time') THEN N'ALTER TABLE ' + QUOTENAME(T.TABLE_SCHEMA) + N'.' + QUOTENAME(T.TABLE_NAME) + N' ADD CONSTRAINT [Inserted_Time_Def] DEFAULT (sysdatetimeoffset()) FOR [Inserted_Time];'
ELSE N'ALTER TABLE ' + QUOTENAME(T.TABLE_SCHEMA) + N'.' + QUOTENAME(T.TABLE_NAME) + N' ADD COLUMN [Inserted_Time] CONSTRAINT [Inserted_Time_Def] DEFAULT (sysdatetimeoffset());'
END
FROM INFORMATION_SCHEMA.TABLES T
WHERE T.TABLE_CATALOG = N'DB_COPY'
FOR XML PATH(N''),TYPE).value('.','nvarchar(MAX)'),1,2,N'');
PRINT #SQL; --Your best friend. If more than 4,000 characters, use SELECT
EXECUTE sp_executesql #SQL;
This will very likely hugely out perform a CURSOR solution if you have a large number of schemas.

Update value in all tables that have a column name in SQL Server

I am writing a stored proc that needs to search a database for all tables that have a certain column name. Once I get a list of tables that have that column I need to update a value in that column. So first I get a list of tables that have a certain column.
SELECT c.name AS 'ColumnName'
,t.name AS 'TableName'
FROM sys.columns c
JOIN sys.tables t ON c.object_id = t.object_id
WHERE c.name LIKE 'company'
ORDER BY TableName
Now that I have a list of tables that need to be updated I need to run a query similar to the following to update the data in each table.
update table1 set company = #newValue where company = #oldvalue
I'm not sure how to go about writing this part. My first thought was to write a dynamic update statement inside of a cursor like:
Declare #newValue
Declare #oldValue
SET #companyCursor = CURSOR FOR
SELECT t.name AS 'TableName'
FROM sys.columns c
JOIN sys.tables t ON c.object_id = t.object_id
WHERE c.name LIKE 'company'
OPEN #companyCursor;
FETCH NEXT FROM #companyCursor INTO #tableName;
WHILE ##FETCH_STATUS = 0
BEGIN
update #tableName set company = #newValue where company = #oldValue
FETCH NEXT FROM #companyCursor INTO INTO #tableName;
END
Is this a good strategy?
I really dislike cursors so even in cases like this where a cursor is a viable solution I like to leverage the system views to avoid looping. You still have to use dynamic sql because object names cannot be parameterized.
Please note that I am guessing the datatype for company here and you can change this easily. Make sure you change the variable definition both in your script AND in the dynamic sql. You entire script could be shortened to something like this.
declare #SQL nvarchar(max) = ''
, #newValue varchar(10) = 'new'
, #oldValue varchar(10) = 'old'
select #SQL = #SQL + 'Update ' + quotename(object_name(c.object_id)) + ' set company = #newValue where company = #oldValue;'
from sys.columns c
where c.name = 'company'
select #SQL
--uncomment the line below when you are satisfied the dynamic sql is correct.
--This dynamic sql is parameterized as much as possible
--exec sp_executesql #SQL, N'#newValue varchar(10), #oldValue varchar(10)', #newValue = #newValue, #oldValue = #oldValue
No the update at the end will not work. You need to use exec (#sql) like this:
declare #sql varchar(4000)
begin
set #sql = 'update ' + #tableName + 'set company = ' + #newValue + 'where company = ' + #oldValue
exec (#sql)
fetch next ...
end
This assumes that #newvalue and #oldvalue are being assigned values somewhere.

How to use cursor value in Select Statement within Dynamic SQL (SQL Server)

I am trying to aggregate SQL Server Users through LinkdedServers, but am unable to select the database name in my query. I tried using db_name() as ''Database'' In my select statement, but it uses the current database context, and not the database that I am selecting from. I realize that this happens because I am using the "fully qualified" database name, so the database context never actually changes. I am also unable to pull in the cursor value as part of my select statement.
Does anyone have any ideas as to how I can get the database name of the database I am selecting from?
Here is my code:
DECLARE #DatabaseName VARCHAR(30)
DECLARE c1 CURSOR LOCAL FORWARD_ONLY STATIC READ_ONLY FOR
SELECT
Name
FROM
[LinkedServer].master.sys.databases
WHERE
Name NOT IN (
'master',
'model',
'tempdb',
'msdb',
'distribution',
)
OPEN c1
FETCH NEXT FROM c1
INTO #DatabaseName
WHILE ##FETCH_STATUS = 0
BEGIN
EXEC (
'
--INSERT INTO [Gather_Privileged_DBUsers_Sox]
SELECT distinct (dp.name) as ''DatabaseUser'',
s.name as ''ServerName'',
db_name() as ''Database'',
getdate() as ''date''
FROM [LinkedServer].[' + #DatabaseName + '].sys.database_role_members drm
JOIN [LinkedServer].[' + #DatabaseName + '].sys.database_principals dp
ON drm.member_principal_id = dp.principal_id
JOIN [LinkedServer].[' + #DatabaseName + '].sys.database_principals dp2
ON drm.role_principal_id = dp2.principal_id
JOIN [LinkedServer].[master].[sys].[servers] s on 1=1
WHERE
dp2.name in
(''db_owner'',''db_accessadmin'',''db_securityadmin'',''db_ddladmin'')
AND s.server_id = 0
AND dp.name not in (''dbo'')
AND dp.type != ''R''
')
FETCH NEXT FROM c1
INTO #DatabaseName
END
CLOSE c1;
DEALLOCATE c1;
GO
Attempt to use the variable value:
SELECT distinct (dp.name) as ''DatabaseUser'',
s.name as ''ServerName'',
' + #DatabaseName + ' as ''Database'',
getdate() as ''date''
When I do this, I get the following error: Msg 207, Level 16, State 1, Line 5
Invalid column name 'myTestDatabase'.
How can I turn the variable into a string in this situation?
You should get out of the habit of wrapping column aliases in single quotes. They are NOT string literals and it makes your code much harder to read. It also causes lots of anguish with dynamic sql.
Here is an example of how you would capture the value in your varaible and build this string without those extra single quotes.
declare #SQL nvarchar(max) =
'SELECT distinct (dp.name) as DatabaseUser,
s.name as ServerName,
''' + #DatabaseName + ''' as Database,
getdate() as [date]'

SQL Update Join Multiple Tables (if field exists)

This is my first post to StackOverflow. I've been using this amazing resource for a number of years to answer hundreds of SQL and PowerShell questions, however this one has me stumped for a number of days.
I am using SQL Server 2014 SP2 and I am trying to do an update to DATABASE1, FIELD1, then FIELD2 then FIELD3 from multiple other database.
FIELD1 may exist in one of multiple other databases.
FIELD1 may not exist in ALL databases - which is where I have the problem.
Database Design Link
I have the following (anonymised) query and it appears to be working:
EXEC sp_MSforeachdb 'IF ''?'' IN (''DATABASE2'',''DATABASE3'',''DATABASE4'')
BEGIN
UPDATE DATABASE1.PARAMETER
SET B.[VALUE] = A.[FIELD1]
FROM DATABASE1.TABLE1 B
INNER JOIN ?.dbo.[TABLE2] A
ON A.JOINVALUE = B.JOINVALUE
WHERE B.COLUMN2 = ''SOMETHING''
AND COLUMN3= ''PF.T.FIELD1''
END ;'
Until I get to say FIELD8, as it exists in DATABASE1 but not in DATABASE2, DATABASE3 or DATABASE4. I then get the following error:
Msg 207, Level 16, State 1, Line 30
Invalid column name 'FIELD8'.
From my Google and StackOverflow searches, I've tried to use (for the first time) a:
IF EXISTS (SELECT COLUMN1 FROM Database2.Table2 WHERE Column1='Field8')
EXEC .......
But that's where I started to really struggle.
Hope the above makes sense.
Any tips or assistance would be greatly appreciated.
N.B. I have about 3,000 fields in Database1 which require updating. I've so-far built all my UPDATE statements dynamically.
You can create stored proc, that will search for columns and tables in system tables:
ALTER PROCEDURE dbo.check_table_exists
-- Add the parameters for the stored procedure here
#table_name nvarchar(255),
#column_name nvarchar(255)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for procedure here
DECLARE #SQLString nvarchar(max),
#ParmDefinition nvarchar(500) = N'#table_name nvarchar(255), #column_name nvarchar(255)';
IF OBJECT_ID(N'tempdb..#check_column_exists') is not null DROP TABLE #check_column_exists
CREATE TABLE #check_column_exists (
db nvarchar(500) NULL,
column_exists bit NULL
)
SELECT #SQLString =
(
SELECT N'USE '+QUOTENAME([name]) +'; '+
'INSERT INTO #check_column_exists '+
'SELECT '''+[name]+''' as [db], '+
' COUNT(*) as column_exists ' +
'FROM sys.tables t ' +
'INNER JOIN sys.columns c ' +
' ON t.[object_id] = c.[object_id] ' +
'WHERE t.[name] = #table_name and c.[name] = #column_name; '
FROM sys.databases
WHERE [name] NOT IN (
'msdb',
'model',
'tempdb',
'master'
)
FOR XML PATH('')
) + 'SELECT [db] FROM #check_column_exists WHERE column_exists = 1; DROP TABLE #check_column_exists;'
EXEC sp_executesql #SQLString, #ParmDefinition, #table_name = #table_name, #column_name = #column_name
END
GO
You can change it to search only for columns and output the database and table name or whatever.
The output is:
db
-----------
DATABASE1
DATABASE4
...
etc
After that you can write this to table and use for dynamic SQL update query:
DECLARE #table_name nvarchar(255) = 'SomeTable',
#column_name nvarchar(255) = 'SomeField'
DECLARE #results TABLE (
db nvarchar(500)
)
INSERT INTO #results
EXEC dbo.check_table_exists #table_name, #column_name
--...Here goes building of dynamic SQL query to update data
First, sp_MSforeachdb is not reliable. For a working alternative, check here: Making a more reliable and flexible sp_MSforeachdb - Aaron Bertrand
Second, you can use system views to check if a column exists in a given table using sys.columns like so:
if exists (
select 1
from sys.columns c
where c.name = 'pilots_id' /* column name */
and c.object_id = object_id(N'pilots') /* table name */
)
begin
select 'Pilots_Id exists' /* do stuff */
end
rextester demo: http://rextester.com/UUXCB18567

Creating a "date updated" column in MS SQL?

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.

Resources