SQL query error: The multi-part identifier "sys.schemas" could not be bound [closed] - sql

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 4 years ago.
Improve this question
I'm getting the error
The multi-part identifier sys.schemas could not be bound. The
multi-part identifier sys.tables could not be bound.
With the following code:
SET NOCOUNT ON
DECLARE #Statement NVARCHAR(MAX) = ''
DECLARE #Statement2 NVARCHAR(MAX) = ''
DECLARE #FinalStatement NVARCHAR(MAX) = ''
DECLARE #TABLE_SCHEMA SYSNAME = sys.schemas
DECLARE #TABLE_NAME SYSNAME = sys.tables
SELECT
#Statement = #Statement + 'SUM(CASE WHEN ' + COLUMN_NAME + ' IS NULL THEN 1 ELSE 0 END) AS ' + COLUMN_NAME + ',' + CHAR(13) ,
#Statement2 = #Statement2 + COLUMN_NAME + '*100 / OverallCount AS ' + COLUMN_NAME + ',' + CHAR(13) FROM
INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = #TABLE_NAME AND TABLE_SCHEMA = #TABLE_SCHEMA
IF ##ROWCOUNT = 0
RAISERROR('TABLE OR VIEW with schema "%s" and name "%s" does not exists or you do not have appropriate permissions.',16,1, #TABLE_SCHEMA, #TABLE_NAME)
ELSE
BEGIN
SELECT #FinalStatement =
'SELECT ' + LEFT(#Statement2, LEN(#Statement2) -2) + ' FROM (SELECT ' + LEFT(#Statement, LEN(#Statement) -2) +
', COUNT(*) AS OverallCount FROM ' + #TABLE_SCHEMA + '.' + #TABLE_NAME + ') SubQuery'
EXEC(#FinalStatement)
END
Where is my code wrong?

sysname is some character variant. You can't do
DECLARE #TABLE_SCHEMA SYSNAME = sys.schemas
DECLARE #TABLE_NAME SYSNAME = sys.tables
you need to use quotes as with char, varchar, etc.. Use
DECLARE #TABLE_SCHEMA SYSNAME = 'sys.schemas'
DECLARE #TABLE_NAME SYSNAME = 'sys.tables'
to fix the error.
Edit:
And since you apparently want to use these values to filter on the schema name and table name separately (
WHERE TABLE_NAME = #TABLE_NAME AND TABLE_SCHEMA = #TABLE_SCHEMA
) you should only assign the right part to each variable:
DECLARE #TABLE_SCHEMA SYSNAME = 'some_schema_name'
DECLARE #TABLE_NAME SYSNAME = 'some_table_name'
A schema with the name sys.schemas is unlikely to exist. As well as a table named sys.tables (I'm talking about just the name. Of course this is otherwise a qualified identifier including the schema.).
E.g.:
DECLARE #TABLE_SCHEMA SYSNAME = 'sys'
DECLARE #TABLE_NAME SYSNAME = 'tables'
(But sys.tables doesn't seem to bee listed in information_schema.columns. But I guess your not after system but "regular" tables anyway.)

I think what you are looking for has similarities to what this person achieved here:
recursively go through each table and find the total number of NULL values.
But do bear in mind that this can take a while depending on the size of your database.
<!-- language: lang-SQL -->
DECLARE #schemaName AS sysname;
DECLARE #tableName AS sysname;
DECLARE #columnName AS sysname;
DECLARE #schema_id AS int;
DECLARE #object_id AS int;
DECLARE #column_id AS int;
DECLARE #isNullable AS bit;
DECLARE #lastSchema_id AS int;
DECLARE #lastTable_id AS int;
DECLARE #recordCount AS bigint;
DECLARE #nullCnt AS bigint;
DECLARE #SQL as nvarchar(max);
DECLARE #paramDefinition NVARCHAR(max);
if exists(select name from tempdb..sysobjects where name LIKE'#Columns%')
DROP TABLE #Columns;
CREATE TABLE #Columns (
schema_id int,
object_id int,
column_id int,
schemaName sysname,
tableName sysname,
columnName sysname,
recordCnt bigint,
nullCnt bigint,
nullPct numeric(38,35) );
-- Set to the #lastSchema_id and #lastTable_id to NULL so that the first
-- loop through the cursor the record count is generated.
SET #lastSchema_id = NULL;
SET #lastTable_id = NULL;
-- List of all the user schemas.tables.columns
-- in the database
DECLARE c_Cursor CURSOR FOR
SELECT schemas.schema_id
, all_objects.object_id
, all_columns.column_id
, schemas.name AS schemaName
, all_objects.name AS tableName
, all_columns.name AS columnName
, all_columns.is_nullable
FROM sys.schemas
INNER JOIN sys.all_objects
ON schemas.schema_id = all_objects.schema_id
AND all_objects.type = 'U'
INNER JOIN sys.all_columns
ON all_objects.object_id = all_columns.object_id
ORDER BY schemas.schema_id
, all_objects.object_id
, all_columns.column_id;
OPEN c_Cursor;
FETCH NEXT FROM c_Cursor
INTO #schema_id
, #object_id
, #column_id
, #schemaName
, #tableName
, #columnName
, #isNullable;
-- Loop through the cursor
WHILE ##FETCH_STATUS = 0
BEGIN
-- Get the record count for the table we are currently working on if this is
-- the first time we are encountering the table.
IF ( ( #schema_id <> #lastSchema_id ) OR ( #object_id <> #lastTable_id )
OR ( #lastSchema_id IS NULL ) OR ( #lastTable_id IS NULL ) )
BEGIN
SET #SQL = N'SELECT #recordCount = COUNT(1) FROM ' + QUOTENAME(#schemaName) + N'.' + QUOTENAME(#tableName) + ';';
SET #paramDefinition = N'#recordCount bigint OUTPUT';
exec sp_executesql #SQL,
#paramDefinition,
#recordCount = #recordCount OUTPUT;
END
-- If the column is NOT NULL, there is no reason to do a count
-- Set the nullCnt and nullPct to 0
IF ( #isNullable = 0 )
BEGIN
INSERT INTO #Columns
( [schema_id]
, [object_id]
, column_id
, schemaName
, tableName
, columnName
, recordCnt
, nullCnt
, nullPct )
VALUES
( #schema_id
, #object_id
, #column_id
, #schemaName
, #tableName
, #columnName
, #recordCount
, 0
, 0.0 );
END
-- If the column is NULL, count the number of NULL fields in the table.
ELSE
BEGIN
SET #SQL = N'SELECT #nullCnt = COUNT(1) FROM ' + QUOTENAME(#schemaName) + N'.' + QUOTENAME(#tableName) +
N' WHERE ' + QUOTENAME(#columnName) + N' IS NULL;';
SET #paramDefinition = N'#nullCnt bigint OUTPUT';
PRINT #SQL;
exec sp_executesql #SQL,
#paramDefinition,
#nullCnt = #nullCnt OUTPUT;
INSERT INTO #Columns
( [schema_id]
, [object_id]
, column_id
, schemaName
, tableName
, columnName
, recordCnt
, nullCnt
, nullPct )
VALUES
( #schema_id
, #object_id
, #column_id
, #schemaName
, #tableName
, #columnName
, #recordCount
, #nullCnt
-- USE NULLIF in case there are no recods in the table
, ISNULL( #nullCnt * 1.0 / NULLIF( #recordCount, 0) * 100.0, 0 ) );
END
-- Set the #lastSchema_id and #lastTable_id so that on
-- the next loop, if it's the same table there is no
-- need to recount the columns for the table.
SET #lastSchema_id = #schema_id;
SET #lastTable_id = #object_id;
FETCH NEXT FROM c_Cursor
INTO #schema_id
, #object_id
, #column_id
, #schemaName
, #tableName
, #columnName
, #isNullable;
END;
CLOSE c_Cursor;
DEALLOCATE c_Cursor;
SELECT *
FROM #Columns;
https://social.msdn.microsoft.com/Forums/sqlserver/en-US/3e097cad-f355-4f7e-b265-f7e6137670bf/challenge-how-to-count-nulls-for-each-column-in-every-table-in-vldb-etc?forum=transactsql

Related

Removing empty resultsets from looping through multiple tables at once

My goal is to remove empty resultsets from a script I wrote to find matching values within multiple tables at once. I am currently returning empty resultsets where I've explicitly written to remove any row where the AssociatedId is empty. How can I write this to remove the empty resultsets?
Here is my SQL:
Select * From #Agreement
Declare #TableName nvarchar(256)
Set #TableName = ''
While #TableName IS NOT NULL
Begin
Set #TableName = (
Select top 1 MIN(QUOTENAME(TABLE_SCHEMA) + '.' + QUOTENAME(TABLE_NAME))
From INFORMATION_SCHEMA.TABLES
Where TABLE_TYPE = 'BASE TABLE'
AND QUOTENAME(TABLE_SCHEMA) + '.' + QUOTENAME(TABLE_NAME) > #TableName
AND TABLE_NAME LIKE '%Association%'
AND QUOTENAME(TABLE_NAME) NOT IN ('[AssociationPrivilege]'
, '[AssociationMultiplicityAssociation]'
, '[AssociationPrivilegeAssociation]'
, '[AssociationTypeAssociation]'
, '[AssociationType]'
, '[AsyncOperationResultsAssociation]'
, '[AttributeGroupRoleMappingAssociation]'
, '[EntityAttributeDataTypeAssociation]'
, '[AutoDelegationAssociation]'
, '[EntityAssociation]'
, '[AssociationMultiplicity]'
, '[RoleAssociationMapping]')
Group BY TABLE_SCHEMA, TABLE_NAME
)
Declare #SQLQuery nvarchar(500)
Set #SQLQuery = 'SELECT AssociationName, AssociatedId FROM ' + #TableName + ' WHERE AssociatedId
IN (Select AgreementSysID From #Agreement) AND CAST(AssociatedId AS NVARCHAR(255)) NOT IN ('''')
GROUP BY AssociationName, AssociatedId'
Exec sp_executesql #SqlQuery
End
GO
Create a second query to see how many records are going to be returned, and only run the select if there are results available.
Declare #SQLQueryCount nvarchar(max), #SQLQueryResults nvarchar(max), #RecordCount int = 0, #WhereClause nvarchar(max);
Set #WhereClause = ' WHERE AssociatedId
IN (Select AgreementSysID From #Agreement) AND CAST(AssociatedId AS NVARCHAR(255)) NOT IN ('''')
GROUP BY AssociationName, AssociatedId';
Set #SQLQueryCount = 'SELECT #RecordCount = count(*) FROM ' + #TableName + #WhereClause;
Set #SQLQueryResults = 'SELECT AssociationName, AssociatedId FROM ' + #TableName + #WhereClause;
Exec sp_executesql #SqlQueryCount, N'#RecordCount int out', #RecordCount out;
If #RecordCount > 0 Begin
Exec sp_executesql #SqlQueryResult;
End;
Note: For dynamic SQL I recommend always using nvarchar(max) because adding a length could cause very undesired results if you modified the query to make it longer than you can store.

Dynamic FROM source based on query results

I have a SQL Server instance that has several databases. Each database has a users table. I'm attempting to write a query that will count user record counts for each database. I've gotten to a point where I can select all database_name, schema_name, and table_name's into a temp table. I need to be able to select as from the results of that query though but I don't know how to go about doing that.
DECLARE #sql nvarchar(max);
SELECT #sql =
(SELECT ' UNION ALL
SELECT ' + + quotename(name,'''') + ' as database_name,
s.name COLLATE DATABASE_DEFAULT
AS schema_name,
t.name COLLATE DATABASE_DEFAULT as table_name
FROM '+ quotename(name) + '.sys.tables t
JOIN '+ quotename(name) + '.sys.schemas s
ON s.schema_id = t.schema_id'
FROM sys.databases
WHERE state = 0
ORDER BY [name] FOR XML PATH(''), type).value('.', 'nvarchar(max)');
SET #sql = stuff(#sql, 1, 12, '') + ' order by database_name,
schema_name,
table_name';
DECLARE #allTables TABLE
(
database_name NVARCHAR(MAX),
schema_name NVARCHAR(MAX),
table_name NVARCHAR(MAX)
)
INSERT INTO #allTables
EXEC (#sql)
SELECT
*
-- ,(select count(*) from [database_name].[schema_name].[table_name] where isadmin = 0) as 'Users'
-- ,(select count(*) from [database_name].[schema_name].[table_name] where isadmin = 1) as 'Admins'
FROM
#allTables
WHERE
table_name = 'C2_SEC_USERS'
This results in the following result set:
I have commented out the part of the select that I need to actually count the users because it's not valid SQL. Is it possible to select FROM based on the result set? If so, how?
You are almost there. Now you need to do 2 things -
Firstly Add an identity field in your Table variable #allTables like following
DECLARE #allTables TABLE
(
id int identity(1,1),
database_name NVARCHAR(MAX),
schema_name NVARCHAR(MAX),
table_name NVARCHAR(MAX)
)
Secondly build dynamic query and execute
DECLARE
#CurrentId AS INT = 0,
#CurrentDb AS NVARCHAR(MAX),
#CurrentSchema AS NVARCHAR(MAX);
SET #sql = '';
WHILE (EXISTS(SELECT id FROM #allTables WHERE table_name = 'C2_SEC_USERS' AND id > #CurrentId))
BEGIN
SELECT TOP 1 #CurrentId = id, #CurrentDb = [database_name], #CurrentSchema = [schema_name] FROM #allTables WHERE table_name = 'C2_SEC_USERS' AND id > #CurrentId
SET #sql += CONCAT('SELECT ''', #CurrentDb, ''' ''database'', COUNT(*) ''User Count'' FROM ', #CurrentDb,'.',#CurrentSchema,'.C2_SEC_USERS');
SET #sql += '
';
END;
EXEC (#sql)
I ended up figuring it out by using a cursor
declare #sql nvarchar(max);
declare #allTables TABLE (database_name NVARCHAR(MAX), schema_name NVARCHAR(MAX), table_name NVARCHAR(MAX))
select #sql =
(select ' UNION ALL
SELECT ' + quotename(name,'''') + ' as database_name,
s.name COLLATE DATABASE_DEFAULT
AS schema_name,
t.name COLLATE DATABASE_DEFAULT as table_name
FROM '+ quotename(name) + '.sys.tables t
JOIN '+ quotename(name) + '.sys.schemas s
on s.schema_id = t.schema_id'
from sys.databases
where state=0
order by [name]
for xml path(''), type).value('.', 'nvarchar(max)');
set #sql = stuff(#sql, 1, 12, '') + ' order by database_name, schema_name, table_name';
insert #allTables
exec (#sql)
declare #userCounts TABLE (database_name NVARCHAR(MAX),
users int,
admins int)
DECLARE #dbName VARCHAR(MAX)
DECLARE #schemaName VARCHAR(MAX)
DECLARE #tableName VARCHAR(MAX)
DECLARE #countSql NVARCHAR(MAX)
DECLARE tables_cursor CURSOR FOR SELECT database_name, schema_name, table_name
FROM #allTables
where table_name = 'C2_SEC_USERS' and database_name != 'master'
OPEN tables_cursor
FETCH NEXT FROM tables_cursor INTO #dbName, #schemaName, #tableName
WHILE ##FETCH_STATUS = 0 BEGIN
set #countSql = 'Select ''' + #dbName + ''' as database_name, (select count(*) from [' + #dbName + '].dbo.C2_SEC_USERS where isadmin = 0 and isinactive = 0) as users, (select count(*) from [' + #dbName + '].dbo.C2_SEC_USERS where isadmin = 1 and isinactive = 0) as admins'
INSERT INTO #userCounts
exec (#countSql)
FETCH NEXT FROM tables_cursor INTO #dbName, #schemaName, #tableName;
END;
CLOSE tables_cursor;
DEALLOCATE tables_cursor;
select * from #userCounts;

T-SQL stored procedure - select just a few columns [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 7 years ago.
Improve this question
I have a stored procedure which selects all the columns from the table. But I need to select only few of them.
How do I assign a variable to select few columns from a table?
Existing stored procedure code:
DECLARE #DatabaseName NVARCHAR(255) = N’AdventureWorksDW’
DECLARE #SchemaName NVARCHAR(255) = N’dbo’
DECLARE #TableName NVARCHAR(255) = N’DimProduct’
BEGIN
SET NOCOUNT ON
-- Declare the parameters internal to query
DECLARE #SQLString NVARCHAR(MAX) = N”
DECLARE #ParamDefinition NVARCHAR(MAX) = N”
DECLARE #ColumnList TABLE (
ColumnId INT IDENTITY(1,1)
, ColumnName NVARCHAR(255)
, ColumnMaxLength INT
, ColumnMinLength INT
)
DECLARE #ColumnMaxLength INT
DECLARE #ColumnMinLength INT
DECLARE #ColumnCount INT = 0
DECLARE #LoopCounter INT = 1
DECLARE #ColumnName NVARCHAR(255)
SET #SQLString =
N’SELECT COLUMN_NAME FROM ‘
#DatabaseName + N’.’ + N’INFORMATION_SCHEMA.COLUMNS
WHERE
DATA_TYPE IN (”CHAR”, ”NCHAR”, ”VARCHAR”, ”NVARCHAR”)
AND TABLE_SCHEMA = #SchemaName
AND TABLE_NAME = #TableName’
SET #ParamDefinition = N’#SchemaName NVARCHAR(255), #TableName NVARCHAR(255)’
INSERT INTO #ColumnList (ColumnName)
EXECUTE sp_executesql #SQLString, #ParamDefinition, #SchemaName, #TableName
SELECT #ColumnCount = COUNT(*) FROM #ColumnList
WHILE (#LoopCounter <= #ColumnCount)
BEGIN
SELECT #ColumnName = ColumnName
FROM #ColumnList
WHERE ColumnId = #LoopCounter
SET #SQLString =
N’SELECT ‘
‘#ColumnMinLength = MIN(LEN(‘ + #ColumnName + ‘))’
‘, #ColumnMaxLength = MAX(LEN(‘ + #ColumnName + ‘))’
‘ FROM ‘
#DatabaseName + N’.’ + #SchemaName + N’.’ + #TableName
‘ WITH (NOLOCK) ‘
SET #ParamDefinition = N’#ColumnMinLength INT OUTPUT, #ColumnMaxLength INT OUTPUT’
EXECUTE sp_executesql #SQLString, #ParamDefinition, #ColumnMinLength OUTPUT, #ColumnMaxLength OUTPUT
UPDATE #ColumnList
SET
ColumnMinLength = #ColumnMinLength
, ColumnMaxLength = #ColumnMaxLength
WHERE ColumnId = #LoopCounter
SET #LoopCounter += 1
END
SELECT
ColumnName AS [Column Name]
, ColumnMinLength AS [Column Minimum Length]
, ColumnMaxLength AS [Column Maximum Length]
FROM #ColumnList
ORDER BY [Column Name]
END
You can build dynamic SQL query and execute it as follows
create procedure SELECT_MyTable (
#fieldList nvarchar(max)
)
as
declare #select nvarchar(max)
set #select = 'SELECT ' + #fieldList + ' FROM MyTable'
exec sp_executesql #select
GO
declare #fieldList nvarchar(max) = 'UserName,region'
exec SELECT_MyTable #fieldList
Can you select from the table directly instead of using a stored procedure?
If so, you can use the syntax
SELECT Column1, Column2, ColumnN
FROM MyTable

Search all tables in database and return a value?

I'm using SQL Server 2008 and working off of the follow example HERE.
Below is the code I have so far. (The only difference is a turned it into a query from a stored procedure and removed schema name which wasn't necessary.)
What I'd like to do is add another column to the result and give the first column's data value. In other words if each table starts off with a Primary Key, my result will include the Primary Key of the row where the data was found. Is this even possible? Thanks.
DECLARE #DataToFind nvarchar(MAX) = ''
DECLARE #ExactMatch BIT = 0
DECLARE #Temp TABLE(RowId INT IDENTITY(1,1), SchemaName sysname, TableName sysname, ColumnName SysName, DataType VARCHAR(100), DataFound BIT)
INSERT INTO #Temp(TableName,SchemaName, ColumnName, DataType)
SELECT C.Table_Name,C.TABLE_SCHEMA, C.Column_Name, C.Data_Type
FROM Information_Schema.Columns AS C
INNER Join Information_Schema.Tables AS T
ON C.Table_Name = T.Table_Name
AND C.TABLE_SCHEMA = T.TABLE_SCHEMA
WHERE Table_Type = 'Base Table'
And Data_Type In ('ntext','text','nvarchar','nchar','varchar','char')
DECLARE #i INT
DECLARE #MAX INT
DECLARE #TableName sysname
DECLARE #ColumnName sysname
DECLARE #SchemaName sysname
DECLARE #SQL NVARCHAR(4000)
DECLARE #PARAMETERS NVARCHAR(4000)
DECLARE #DataExists BIT
DECLARE #SQLTemplate NVARCHAR(4000)
SELECT #SQLTemplate = CASE WHEN #ExactMatch = 1
THEN 'If Exists(Select *
From ReplaceTableName
Where Convert(nVarChar(4000), [ReplaceColumnName])
= ''' + #DataToFind + '''
)
Set #DataExists = 1
Else
Set #DataExists = 0'
ELSE 'If Exists(Select *
From ReplaceTableName
Where Convert(nVarChar(4000), [ReplaceColumnName])
Like ''%' + #DataToFind + '%''
)
Set #DataExists = 1
Else
Set #DataExists = 0'
END,
#PARAMETERS = '#DataExists Bit OUTPUT',
#i = 1
SELECT #i = 1, #MAX = MAX(RowId)
FROM #Temp
WHILE #i <= #MAX
BEGIN
SELECT #SQL = REPLACE(REPLACE(#SQLTemplate, 'ReplaceTableName', QUOTENAME(SchemaName) + '.' + QUOTENAME(TableName)), 'ReplaceColumnName', ColumnName)
FROM #Temp
WHERE RowId = #i
PRINT #SQL
EXEC SP_EXECUTESQL #SQL, #PARAMETERS, #DataExists = #DataExists OUTPUT
IF #DataExists =1
UPDATE #Temp SET DataFound = 1 WHERE RowId = #i
SET #i = #i + 1
END
SELECT TableName, ColumnName
FROM #Temp
WHERE DataFound = 1
Returning the primary key is absolutely possible. You will need to:
Determine the name if the primary key column or columns in each table. If you can stick to a single column, it will make your life much easier.
Add a variable for the content of the Primary Key value, say #PrimaryKeyValue
Modify #SQLTemplate to return the value of this column into #PrimaryKeyValue, something like this:
SELECT #SQLTemplate = CASE WHEN #ExactMatch = 1
THEN 'select #PrimaryKeyValue = min(' + #PrimaryKeyColumnName + ')
from ReplaceTableName
where Convert(nVarChar(4000), [ReplaceColumnName]) = ''' + #DataToFind + ''')'
ELSE 'select #PrimaryKeyValue = min(' + #PrimaryKeyColumnName + ')
from ReplaceTableName
where Convert(nVarChar(4000), [ReplaceColumnName]) Like ''%' + #DataToFind + '%'')'
END,
#PARAMETERS = '#PrimaryKeyValue nvarchar(4000) OUTPUT',
#i = 1

SQL Server Update Trigger, Get Only modified fields

I am aware of COLUMNS_UPDATED, well I need some quick shortcut (if anyone has made, I am already making one, but if anyone can save my time, I will appriciate it)
I need basicaly an XML of only updated column values, I need this for replication purpose.
SELECT * FROM inserted gives me each column, but I need only updated ones.
something like following...
CREATE TRIGGER DBCustomers_Insert
ON DBCustomers
AFTER UPDATE
AS
BEGIN
DECLARE #sql as NVARCHAR(1024);
SET #sql = 'SELECT ';
I NEED HELP FOR FOLLOWING LINE ...., I can manually write every column, but I need
an automated routin which can work regardless of column specification
for each column, if its modified append $sql = ',' + columnname...
SET #sql = $sql + ' FROM inserted FOR XML RAW';
DECLARE #x as XML;
SET #x = CAST(EXEC(#sql) AS XML);
.. use #x
END
I've another completely different solution that doesn't use COLUMNS_UPDATED at all, nor does it rely on building dynamic SQL at runtime. (You might want to use dynamic SQL at design time but thats another story.)
Basically you start with the inserted and deleted tables, unpivot each of them so you are just left with the unique key, field value and field name columns for each. Then you join the two and filter for anything that's changed.
Here is a full working example, including some test calls to show what is logged.
-- -------------------- Setup tables and some initial data --------------------
CREATE TABLE dbo.Sample_Table (ContactID int, Forename varchar(100), Surname varchar(100), Extn varchar(16), Email varchar(100), Age int );
INSERT INTO Sample_Table VALUES (1,'Bob','Smith','2295','bs#example.com',24);
INSERT INTO Sample_Table VALUES (2,'Alice','Brown','2255','ab#example.com',32);
INSERT INTO Sample_Table VALUES (3,'Reg','Jones','2280','rj#example.com',19);
INSERT INTO Sample_Table VALUES (4,'Mary','Doe','2216','md#example.com',28);
INSERT INTO Sample_Table VALUES (5,'Peter','Nash','2214','pn#example.com',25);
CREATE TABLE dbo.Sample_Table_Changes (ContactID int, FieldName sysname, FieldValueWas sql_variant, FieldValueIs sql_variant, modified datetime default (GETDATE()));
GO
-- -------------------- Create trigger --------------------
CREATE TRIGGER TriggerName ON dbo.Sample_Table FOR DELETE, INSERT, UPDATE AS
BEGIN
SET NOCOUNT ON;
--Unpivot deleted
WITH deleted_unpvt AS (
SELECT ContactID, FieldName, FieldValue
FROM
(SELECT ContactID
, cast(Forename as sql_variant) Forename
, cast(Surname as sql_variant) Surname
, cast(Extn as sql_variant) Extn
, cast(Email as sql_variant) Email
, cast(Age as sql_variant) Age
FROM deleted) p
UNPIVOT
(FieldValue FOR FieldName IN
(Forename, Surname, Extn, Email, Age)
) AS deleted_unpvt
),
--Unpivot inserted
inserted_unpvt AS (
SELECT ContactID, FieldName, FieldValue
FROM
(SELECT ContactID
, cast(Forename as sql_variant) Forename
, cast(Surname as sql_variant) Surname
, cast(Extn as sql_variant) Extn
, cast(Email as sql_variant) Email
, cast(Age as sql_variant) Age
FROM inserted) p
UNPIVOT
(FieldValue FOR FieldName IN
(Forename, Surname, Extn, Email, Age)
) AS inserted_unpvt
)
--Join them together and show what's changed
INSERT INTO Sample_Table_Changes (ContactID, FieldName, FieldValueWas, FieldValueIs)
SELECT Coalesce (D.ContactID, I.ContactID) ContactID
, Coalesce (D.FieldName, I.FieldName) FieldName
, D.FieldValue as FieldValueWas
, I.FieldValue AS FieldValueIs
FROM
deleted_unpvt d
FULL OUTER JOIN
inserted_unpvt i
on D.ContactID = I.ContactID
AND D.FieldName = I.FieldName
WHERE
D.FieldValue <> I.FieldValue --Changes
OR (D.FieldValue IS NOT NULL AND I.FieldValue IS NULL) -- Deletions
OR (D.FieldValue IS NULL AND I.FieldValue IS NOT NULL) -- Insertions
END
GO
-- -------------------- Try some changes --------------------
UPDATE Sample_Table SET age = age+1;
UPDATE Sample_Table SET Extn = '5'+Extn where Extn Like '221_';
DELETE FROM Sample_Table WHERE ContactID = 3;
INSERT INTO Sample_Table VALUES (6,'Stephen','Turner','2299','st#example.com',25);
UPDATE Sample_Table SET ContactID = 7 where ContactID = 4; --this will be shown as a delete and an insert
-- -------------------- See the results --------------------
SELECT *, SQL_VARIANT_PROPERTY(FieldValueWas, 'BaseType') FieldBaseType, SQL_VARIANT_PROPERTY(FieldValueWas, 'MaxLength') FieldMaxLength from Sample_Table_Changes;
-- -------------------- Cleanup --------------------
DROP TABLE dbo.Sample_Table; DROP TABLE dbo.Sample_Table_Changes;
So no messing around with bigint bitfields and arth overflow problems. If you know the columns you want to compare at design time then you don't need any dynamic SQL.
On the downside the output is in a different format and all the field values are converted to sql_variant, the first could be fixed by pivoting the output again, and the second could be fixed by recasting back to the required types based on your knowledge of the design of the table, but both of these would require some complex dynamic sql. Both of these might not be an issue in your XML output. This question does something similar to getting the output back in the same format.
Edit: Reviewing the comments below, if you have a natural primary key that could change then you can still use this method. You just need to add a column that is populated by default with a GUID using the NEWID() function. You then use this column in place of the primary key.
You may want to add an index to this field, but as the deleted and inserted tables in a trigger are in memory it might not get used and may have a negative effect on performance.
Inside the trigger, you can use COLUMNS_UPDATED() like this in order to get updated value
-- Get the table id of the trigger
--
DECLARE #idTable INT
SELECT #idTable = T.id
FROM sysobjects P JOIN sysobjects T ON P.parent_obj = T.id
WHERE P.id = ##procid
-- Get COLUMNS_UPDATED if update
--
DECLARE #Columns_Updated VARCHAR(50)
SELECT #Columns_Updated = ISNULL(#Columns_Updated + ', ', '') + name
FROM syscolumns
WHERE id = #idTable
AND CONVERT(VARBINARY,REVERSE(COLUMNS_UPDATED())) & POWER(CONVERT(BIGINT, 2), colorder - 1) > 0
But this snipet of code fails when you have a table with more than 62 columns.. Arth.Overflow...
Here is the final version which handles more than 62 columns but give only the number of the updated columns. It's easy to link with 'syscolumns' to get the name
DECLARE #Columns_Updated VARCHAR(100)
SET #Columns_Updated = ''
DECLARE #maxByteCU INT
DECLARE #curByteCU INT
SELECT #maxByteCU = DATALENGTH(COLUMNS_UPDATED()),
#curByteCU = 1
WHILE #curByteCU <= #maxByteCU BEGIN
DECLARE #cByte INT
SET #cByte = SUBSTRING(COLUMNS_UPDATED(), #curByteCU, 1)
DECLARE #curBit INT
DECLARE #maxBit INT
SELECT #curBit = 1,
#maxBit = 8
WHILE #curBit <= #maxBit BEGIN
IF CONVERT(BIT, #cByte & POWER(2,#curBit - 1)) <> 0
SET #Columns_Updated = #Columns_Updated + '[' + CONVERT(VARCHAR, 8 * (#curByteCU - 1) + #curBit) + ']'
SET #curBit = #curBit + 1
END
SET #curByteCU = #curByteCU + 1
END
I've done it as simple "one-liner". Without using, pivot, loops, many variables etc. that makes it looking like procedural programming. SQL should be used to process data sets :-), the solution is:
DECLARE #sql as NVARCHAR(1024);
select #sql = coalesce(#sql + ',' + quotename(column_name), quotename(column_name))
from INFORMATION_SCHEMA.COLUMNS
where substring(columns_updated(), columnproperty(object_id(table_schema + '.' + table_name, 'U'), column_name, 'columnId') / 8 + 1, 1) & power(2, -1 + columnproperty(object_id(table_schema + '.' + table_name, 'U'), column_name, 'columnId') % 8 ) > 0
and table_name = 'DBCustomers'
-- and column_name in ('c1', 'c2') -- limit to specific columns
-- and column_name not in ('c3', 'c4') -- or exclude specific columns
SET #sql = 'SELECT ' + #sql + ' FROM inserted FOR XML RAW';
DECLARE #x as XML;
SET #x = CAST(EXEC(#sql) AS XML);
It uses COLUMNS_UPDATED, takes care of more than eight columns - it handles as many columns as you want.
It takes care on proper columns order which should be get using COLUMNPROPERTY.
It is based on view COLUMNS so it may include or exclude only specific columns.
The below code works for over 64 columns and logs only the updated columns. Follow the instruction in the comments and all should be well.
/*******************************************************************************************
* Add the below table to your database to track data changes using the trigger *
* below. Remember to change the variables in the trigger to match the table that *
* will be firing the trigger *
*******************************************************************************************/
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
CREATE TABLE [dbo].[AuditDataChanges]
(
[RecordId] [INT] IDENTITY(1, 1)
NOT NULL ,
[TableName] [VARCHAR](50) NOT NULL ,
[RecordPK] [VARCHAR](50) NOT NULL ,
[ColumnName] [VARCHAR](50) NOT NULL ,
[OldValue] [VARCHAR](50) NULL ,
[NewValue] [VARCHAR](50) NULL ,
[ChangeDate] [DATETIME2](7) NOT NULL ,
[UpdatedBy] [VARCHAR](50) NOT NULL ,
CONSTRAINT [PK_AuditDataChanges] PRIMARY KEY CLUSTERED
( [RecordId] ASC )
WITH ( PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON ) ON [PRIMARY]
)
ON [PRIMARY];
GO
ALTER TABLE [dbo].[AuditDataChanges] ADD CONSTRAINT [DF_AuditDataChanges_ChangeDate] DEFAULT (GETDATE()) FOR [ChangeDate];
GO
/************************************************************************************************
* Add the below trigger to any table you want to audit data changes on. Changes will be saved *
* in the AuditChangesTable. *
************************************************************************************************/
ALTER TRIGGER trg_Survey_Identify_Updated_Columns ON Survey --Change to match your table name
FOR INSERT, UPDATE
AS
SET NOCOUNT ON;
DECLARE #sql VARCHAR(5000) ,
#sqlInserted NVARCHAR(500) ,
#sqlDeleted NVARCHAR(500) ,
#NewValue NVARCHAR(100) ,
#OldValue NVARCHAR(100) ,
#UpdatedBy VARCHAR(50) ,
#ParmDefinitionD NVARCHAR(500) ,
#ParmDefinitionI NVARCHAR(500) ,
#TABLE_NAME VARCHAR(100) ,
#COLUMN_NAME VARCHAR(100) ,
#modifiedColumnsList NVARCHAR(4000) ,
#ColumnListItem NVARCHAR(500) ,
#Pos INT ,
#RecordPk VARCHAR(50) ,
#RecordPkName VARCHAR(50);
SELECT *
INTO #deleted
FROM deleted;
SELECT *
INTO #Inserted
FROM inserted;
SET #TABLE_NAME = 'Survey'; ---Change to your table name
SELECT #UpdatedBy = UpdatedBy --Change to your column name for the user update field
FROM inserted;
SELECT #RecordPk = SurveyId --Change to the table primary key field
FROM inserted;
SET #RecordPkName = 'SurveyId';
SET #modifiedColumnsList = STUFF(( SELECT ',' + name
FROM sys.columns
WHERE object_id = OBJECT_ID(#TABLE_NAME)
AND SUBSTRING(COLUMNS_UPDATED(),
( ( column_id
- 1 ) / 8 + 1 ),
1) & ( POWER(2,
( ( column_id
- 1 ) % 8 + 1 )
- 1) ) = POWER(2,
( column_id - 1 )
% 8)
FOR
XML PATH('')
), 1, 1, '');
WHILE LEN(#modifiedColumnsList) > 0
BEGIN
SET #Pos = CHARINDEX(',', #modifiedColumnsList);
IF #Pos = 0
BEGIN
SET #ColumnListItem = #modifiedColumnsList;
END;
ELSE
BEGIN
SET #ColumnListItem = SUBSTRING(#modifiedColumnsList, 1,
#Pos - 1);
END;
SET #COLUMN_NAME = #ColumnListItem;
SET #ParmDefinitionD = N'#OldValueOut NVARCHAR(100) OUTPUT';
SET #ParmDefinitionI = N'#NewValueOut NVARCHAR(100) OUTPUT';
SET #sqlDeleted = N'SELECT #OldValueOut=' + #COLUMN_NAME
+ ' FROM #deleted where ' + #RecordPkName + '='
+ CONVERT(VARCHAR(50), #RecordPk);
SET #sqlInserted = N'SELECT #NewValueOut=' + #COLUMN_NAME
+ ' FROM #Inserted where ' + #RecordPkName + '='
+ CONVERT(VARCHAR(50), #RecordPk);
EXECUTE sp_executesql #sqlDeleted, #ParmDefinitionD,
#OldValueOut = #OldValue OUTPUT;
EXECUTE sp_executesql #sqlInserted, #ParmDefinitionI,
#NewValueOut = #NewValue OUTPUT;
IF ( LTRIM(RTRIM(#NewValue)) != LTRIM(RTRIM(#OldValue)) )
BEGIN
SET #sql = 'INSERT INTO [dbo].[AuditDataChanges]
([TableName]
,[RecordPK]
,[ColumnName]
,[OldValue]
,[NewValue]
,[UpdatedBy])
VALUES
(' + QUOTENAME(#TABLE_NAME, '''') + '
,' + QUOTENAME(#RecordPk, '''') + '
,' + QUOTENAME(#COLUMN_NAME, '''') + '
,' + QUOTENAME(#OldValue, '''') + '
,' + QUOTENAME(#NewValue, '''') + '
,' + QUOTENAME(#UpdatedBy, '''') + ')';
EXEC (#sql);
END;
SET #COLUMN_NAME = '';
SET #NewValue = '';
SET #OldValue = '';
IF #Pos = 0
BEGIN
SET #modifiedColumnsList = '';
END;
ELSE
BEGIN
-- start substring at the character after the first comma
SET #modifiedColumnsList = SUBSTRING(#modifiedColumnsList,
#Pos + 1,
LEN(#modifiedColumnsList)
- #Pos);
END;
END;
DROP TABLE #Inserted;
DROP TABLE #deleted;
GO
I transformed the accepted answer to get list of column names separated by comma (according to author's recommendation). Output - "Columns_Updated" as 'Column1,Column2,Column5'
-- get names of updated columns
DECLARE #idTable INT
declare #ColumnName nvarchar(300)
declare #ColId int
SELECT #idTable = T.id
FROM sysobjects P JOIN sysobjects T ON P.parent_obj = T.id
WHERE P.id = ##procid
DECLARE #changedProperties nvarchar(max) = ''
DECLARE #Columns_Updated VARCHAR(2000) = ''
DECLARE #maxByteCU INT
DECLARE #curByteCU INT
SELECT #maxByteCU = DATALENGTH(COLUMNS_UPDATED()),
#curByteCU = 1
WHILE #curByteCU <= #maxByteCU BEGIN
DECLARE #cByte INT
SET #cByte = SUBSTRING(COLUMNS_UPDATED(), #curByteCU, 1)
DECLARE #curBit INT
DECLARE #maxBit INT
SELECT #curBit = 1,
#maxBit = 8
WHILE #curBit <= #maxBit BEGIN
IF CONVERT(BIT, #cByte & POWER(2, #curBit - 1)) <> 0 BEGIN
SET #ColId = cast( CONVERT(VARCHAR, 8 * (#curByteCU - 1) + #curBit) as int)
select #ColumnName = [Name]
FROM syscolumns
WHERE id = #idTable and colid = #ColId
SET #Columns_Updated = #Columns_Updated + ',' + #ColumnName
END
SET #curBit = #curBit + 1
END
SET #curByteCU = #curByteCU + 1
END
The only way that occurs to me that you could accomplish this without hard coding column names would be to drop the contents of the deleted table to a temp table, then build a query based on the table definition to to compare the contents of your temp table and the actual table, and return a delimited column list based on whether they do or do not match. Admittedly, the below is elaborate.
Declare #sql nvarchar(4000)
DECLARE #ParmDefinition nvarchar(500)
Declare #OutString varchar(8000)
Declare #tbl sysname
Set #OutString = ''
Set #tbl = 'SomeTable' --The table we are interested in
--Store the contents of deleted in temp table
Select * into #tempDelete from deleted
--Build sql string based on definition
--of table
--to retrieve the column name
--or empty string
--based on comparison between
--target table and temp table
set #sql = ''
Select #sql = #sql + 'Case when IsNull(i.[' + Column_Name +
'],0) = IsNull(d.[' + Column_name + '],0) then ''''
else ' + quotename(Column_Name, char(39)) + ' + '',''' + ' end +'
from information_schema.columns
where table_name = #tbl
--Define output parameter
set #ParmDefinition = '#OutString varchar(8000) OUTPUT'
--Format sql
set #sql = 'Select #OutString = '
+ Substring(#sql,1 , len(#sql) -1) +
' From SomeTable i ' --Will need to be updated for target schema
+ ' inner join #tempDelete d on
i.PK = d.PK ' --Will need to be updated for target schema
--Execute sql and retrieve desired column list in output parameter
exec sp_executesql #sql, #ParmDefinition, #OutString OUT
drop table #tempDelete
--strip trailing column if a non-zero length string
--was returned
if Len(#Outstring) > 0
Set #OutString = Substring(#OutString, 1, Len(#Outstring) -1)
--return comma delimited list of changed columns.
Select #OutString
End
The sample code provided by Rick lack handling for multiple rows update.
Please let me enhance Rick's version as below:
USE [AFC]
GO
/****** Object: Trigger [dbo].[trg_Survey_Identify_Updated_Columns] Script Date: 27/7/2018 14:08:49 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER TRIGGER [dbo].[trg_Survey_Identify_Updated_Columns] ON [dbo].[Sample_Table] --Change to match your table name
FOR INSERT
,UPDATE
AS
SET NOCOUNT ON;
DECLARE #sql VARCHAR(5000)
,#sqlInserted NVARCHAR(500)
,#sqlDeleted NVARCHAR(500)
,#NewValue NVARCHAR(100)
,#OldValue NVARCHAR(100)
,#UpdatedBy VARCHAR(50)
,#ParmDefinitionD NVARCHAR(500)
,#ParmDefinitionI NVARCHAR(500)
,#TABLE_NAME VARCHAR(100)
,#COLUMN_NAME VARCHAR(100)
,#modifiedColumnsList NVARCHAR(4000)
,#ColumnListItem NVARCHAR(500)
,#Pos INT
,#RecordPk VARCHAR(50)
,#RecordPkName VARCHAR(50);
SELECT *
INTO #deleted
FROM deleted;
SELECT *
INTO #Inserted
FROM inserted;
SET #TABLE_NAME = 'Sample_Table';---Change to your table name
DECLARE t_cursor CURSOR
FOR
SELECT ContactID
FROM inserted
OPEN t_cursor
FETCH NEXT
FROM t_cursor
INTO #RecordPk
WHILE ##FETCH_STATUS = 0
BEGIN
--SELECT #UpdatedBy = Surname --Change to your column name for the user update field
--FROM inserted;
--SELECT #RecordPk = ContactID --Change to the table primary key field
--FROM inserted;
SET #RecordPkName = 'ContactID';
SET #modifiedColumnsList = STUFF((
SELECT ',' + name
FROM sys.columns
WHERE object_id = OBJECT_ID(#TABLE_NAME)
AND SUBSTRING(COLUMNS_UPDATED(), ((column_id - 1) / 8 + 1), 1) & (POWER(2, ((column_id - 1) % 8 + 1) - 1)) = POWER(2, (column_id - 1) % 8)
FOR XML PATH('')
), 1, 1, '');
WHILE LEN(#modifiedColumnsList) > 0
BEGIN
SET #Pos = CHARINDEX(',', #modifiedColumnsList);
IF #Pos = 0
BEGIN
SET #ColumnListItem = #modifiedColumnsList;
END;
ELSE
BEGIN
SET #ColumnListItem = SUBSTRING(#modifiedColumnsList, 1, #Pos - 1);
END;
SET #COLUMN_NAME = #ColumnListItem;
SET #ParmDefinitionD = N'#OldValueOut NVARCHAR(100) OUTPUT';
SET #ParmDefinitionI = N'#NewValueOut NVARCHAR(100) OUTPUT';
SET #sqlDeleted = N'SELECT #OldValueOut=' + #COLUMN_NAME + ' FROM #deleted where ' + #RecordPkName + '=' + CONVERT(VARCHAR(50), #RecordPk);
SET #sqlInserted = N'SELECT #NewValueOut=' + #COLUMN_NAME + ' FROM #Inserted where ' + #RecordPkName + '=' + CONVERT(VARCHAR(50), #RecordPk);
EXECUTE sp_executesql #sqlDeleted
,#ParmDefinitionD
,#OldValueOut = #OldValue OUTPUT;
EXECUTE sp_executesql #sqlInserted
,#ParmDefinitionI
,#NewValueOut = #NewValue OUTPUT;
--PRINT #newvalue
--PRINT #oldvalue
IF (LTRIM(RTRIM(#NewValue)) != LTRIM(RTRIM(#OldValue)))
BEGIN
SET #sql = 'INSERT INTO [dbo].[AuditDataChanges]
([TableName]
,[RecordPK]
,[ColumnName]
,[OldValue]
,[NewValue] )
VALUES
(' + QUOTENAME(#TABLE_NAME, '''') + '
,' + QUOTENAME(#RecordPk, '''') + '
,' + QUOTENAME(#COLUMN_NAME, '''') + '
,' + QUOTENAME(#OldValue, '''') + '
,' + QUOTENAME(#NewValue, '''') + '
' + ')';
EXEC (#sql);
END;
SET #COLUMN_NAME = '';
SET #NewValue = '';
SET #OldValue = '';
IF #Pos = 0
BEGIN
SET #modifiedColumnsList = '';
END;
ELSE
BEGIN
-- start substring at the character after the first comma
SET #modifiedColumnsList = SUBSTRING(#modifiedColumnsList, #Pos + 1, LEN(#modifiedColumnsList) - #Pos);
END;
END;
FETCH NEXT
FROM t_cursor
INTO #RecordPk
END
DROP TABLE #Inserted;
DROP TABLE #deleted;
CLOSE t_cursor;
DEALLOCATE t_cursor;
This is perfect example for track log of updated columnwise value with unique records and UpdatedBy user.
IF NOT EXISTS
(SELECT * FROM sysobjects WHERE id = OBJECT_ID(N'[dbo].[ColumnAuditLogs]')
AND OBJECTPROPERTY(id, N'IsUserTable') = 1)
CREATE TABLE ColumnAuditLogs
(Type CHAR(1),
TableName VARCHAR(128),
PK VARCHAR(1000),
FieldName VARCHAR(128),
OldValue VARCHAR(1000),
NewValue VARCHAR(1000),
UpdateDate datetime,
UserName VARCHAR(128),
UniqueId uniqueidentifier,
UpdatedBy int
)
GO
create TRIGGER TR_ABCTable_AUDIT ON ABCTable FOR UPDATE
AS
DECLARE #bit INT ,
#field INT ,
#maxfield INT ,
#char INT ,
#fieldname VARCHAR(128) ,
#TableName VARCHAR(128) ,
#PKCols VARCHAR(1000) ,
#sql VARCHAR(2000),
#UpdateDate VARCHAR(21) ,
#UserName VARCHAR(128) ,
#Type CHAR(1) ,
#PKSelect VARCHAR(1000),
#UniqueId varchar(100),
#UpdatedBy VARCHAR(50)
--You will need to change #TableName to match the table to be audited.
-- Here we made ABCTable for your example.
SELECT #TableName = 'ABCTable' -- change table name accoring your table name
-- use for table unique records for everytime updation.
set #UniqueId = CONVERT(varchar(100),newID())
-- date and user
SELECT #UserName = SYSTEM_USER ,
#UpdateDate = CONVERT (NVARCHAR(30),GETDATE(),126)
SELECT #UpdatedBy = ModifiedBy --Change to your column name for the user update field
FROM inserted;
-- Action
IF EXISTS (SELECT * FROM inserted)
IF EXISTS (SELECT * FROM deleted)
SELECT #Type = 'U'
ELSE
SELECT #Type = 'I'
ELSE
SELECT #Type = 'D'
-- get list of columns
SELECT * INTO #ins FROM inserted
SELECT * INTO #del FROM deleted
-- Get primary key columns for full outer join
SELECT #PKCols = COALESCE(#PKCols + ' and', ' on')
+ ' i.' + c.COLUMN_NAME + ' = d.' + c.COLUMN_NAME
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ,
INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
WHERE pk.TABLE_NAME = #TableName
AND CONSTRAINT_TYPE = 'PRIMARY KEY'
AND c.TABLE_NAME = pk.TABLE_NAME
AND c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME
-- Get primary key select for insert
SELECT #PKSelect = COALESCE(#PKSelect+'+','')
+ 'convert(varchar(100),
coalesce(i.' + COLUMN_NAME +',d.' + COLUMN_NAME + '))'
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS pk ,
INFORMATION_SCHEMA.KEY_COLUMN_USAGE c
WHERE pk.TABLE_NAME = #TableName
AND CONSTRAINT_TYPE = 'PRIMARY KEY'
AND c.TABLE_NAME = pk.TABLE_NAME
AND c.CONSTRAINT_NAME = pk.CONSTRAINT_NAME
IF #PKCols IS NULL
BEGIN
RAISERROR('no PK on table %s', 16, -1, #TableName)
RETURN
END
SELECT #field = 0,
#maxfield = MAX(ORDINAL_POSITION)
FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = #TableName
WHILE #field < #maxfield
BEGIN
SELECT #field = MIN(ORDINAL_POSITION)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = #TableName
AND ORDINAL_POSITION > #field
SELECT #bit = (#field - 1 )% 8 + 1
SELECT #bit = POWER(2,#bit - 1)
SELECT #char = ((#field - 1) / 8) + 1
IF SUBSTRING(COLUMNS_UPDATED(),#char, 1) & #bit > 0
OR #Type IN ('I','D')
BEGIN
SELECT #fieldname = COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = #TableName
AND ORDINAL_POSITION = #field
SELECT #sql = '
insert ColumnAuditLogs ( Type,
TableName,
PK,
FieldName,
OldValue,
NewValue,
UpdateDate,
UserName,
UniqueId,
[UpdatedBy])
select ''' + #Type + ''','''
+ #TableName + ''',' + #PKSelect
+ ',''' + #fieldname + ''''
+ ',convert(varchar(1000),d.' + #fieldname + ')'
+ ',convert(varchar(1000),i.' + #fieldname + ')'
+ ',''' + #UpdateDate + ''''
+ ',''' + #UserName + ''''
+ ',''' + #UniqueId + ''''
+ ',' + QUOTENAME(#UpdatedBy, '''')
+ ' from #ins i full outer join #del d'
+ #PKCols
+ ' where i.' + #fieldname + ' <> d.' + #fieldname
+ ' or (i.' + #fieldname + ' is null and d.'
+ #fieldname
+ ' is not null)'
+ ' or (i.' + #fieldname + ' is not null and d.'
+ #fieldname
+ ' is null)'
EXEC (#sql)
END
END
GO