SQL-Server Trigger on update for Audit - sql-server

I can't find an easy/generic way to register to an audit table the columns changed on some tables.
I tried to do it using a Trigger on after update in this way:
First of all the Audit Table definition:
CREATE TABLE [Audit](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Date] [datetime] NOT NULL default GETDATE(),
[IdTypeAudit] [int] NOT NULL, --2 for Modify
[UserName] [varchar](50) NULL,
[TableName] [varchar](50) NOT NULL,
[ColumnName] [varchar](50) NULL,
[OldData] [varchar](50) NULL,
[NewData] [varchar](50) NULL )
Next a trigger on AFTER UPDATE in any table:
DECLARE
#sql varchar(8000),
#col int,
#colcount int
select #colcount = count(*) from INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'MyTable'
set #col = 1
while(#col < #colcount )
begin
set #sql=
'INSERT INTO Audit
SELECT 2, UserNameLastModif, ''MyTable'', COL_NAME(Object_id(''MyTable''), '+ convert(varchar,#col) +'), Deleted.'
+ COL_NAME(Object_id('MyTable'), #col) + ', Inserted.' + COL_NAME(Object_id('MyTable'), #col) + '
FROM Inserted LEFT JOIN Deleted ON Inserted.[MyTableId] = Deleted.[MyTableId]
WHERE COALESCE(Deleted.' + COL_NAME(Object_id('MyTable'), #col) + ', '''') <> COALESCE(Inserted.' + COL_NAME(Object_id('MyTable'), #col) + ', '''')'
--UserNameLastModif is an optional column on MyTable
exec(#sql)
set #col = #col + 1
end
The problems
Inserted and Deleted lost the context when I use the exec function
Seems that colnumber it isn't always a correlative number, seems if you create a table with 20 columns and you delete one and create another, the last one have a number > #colcount
I was looking for a solution for all over the net but I couln't figure out
Any Idea?
Thanks!

This highlights a greater problem with structural choice. Try to write a set-based solution. Remove the loop and dynamic SQL and write a single statement that inserts the Audit rows. It is possible but to make it easier consider a different table layout, like keeping all columns on 1 row instead of splitting them.
In SQL 2000 use syscolumns. In SQL 2005+ use sys.columns. i.e.
SELECT column_id FROM sys.columns WHERE object_id = OBJECT_ID(DB_NAME()+'.dbo.Table');

#Santiago : If you still want to write it in dynamic SQL, you should prepare all of the statements first then execute them.
8000 characters may not be enough for all the statements. A good solution is to use a table to store them.
IF NOT OBJECT_ID('tempdb..#stmt') IS NULL
DROP TABLE #stmt;
CREATE TABLE #stmt (ID int NOT NULL IDENTITY(1,1), SQL varchar(8000) NOT NULL);
Then replace the line exec(#sql) with INSERT INTO #stmt (SQL) VALUES (#sql);
Then exec each row.
WHILE EXISTS (SELECT TOP 1 * FROM #stmt)
BEGIN
BEGIN TRANSACTION;
EXEC (SELECT TOP 1 SQL FROM #stmt ORDER BY ID);
DELETE FROM #stmt WHERE ID = (SELECT MIN(ID) FROM #stmt);
COMMIT TRANSACTION;
END
Remember to use sys.columns for the column loop (I shall assume you use SQL 2005/2008).
SET #col = 0;
WHILE EXISTS (SELECT TOP 1 * FROM sys.columns WHERE object_id = OBJECT_ID('MyTable') AND column_id > #col)
BEGIN
SELECT TOP 1 #col = column_id FROM sys.columns
WHERE object_id = OBJECT_ID('MyTable') AND column_id > #col ORDER BY column_id ASC;
SET #sql ....
INSERT INTO #stmt ....
END
Remove line 4 #colcount int and the proceeding comma. Remove Information schema select.

DO not ever use any kind of looping a trigger. Do not use dynamic SQl or call a stored proc or send an email.All of these things are exretemly inappropriate in a trigger.
If tyou want to use dynamic sql use it to create the script to create the trigger. And create an audit table for every table you want audited (we actually have two for every table) or you will have performance problems due to locking on the "one table to rule them all".

Related

Select on a table with 2 possible structures

I'm trying to write a query that will select data from a table. due to different versions of the database, there are 2 possible structures for the source table, where the newer version has 2 more fields than the old one.
I've tried identifying the older structure and replacing the columns with NULL and also tried writing 2 separate queries with and IF statement directing to the correct one. Neither of these solutions work and in both cases it seems that the SQL engine is failing on validating these 2 columns.
Examples of my attempted solutions:
IF NOT EXISTS (SELECT *
FROM sys.objects
WHERE object_id = Object_id(N'[dbo].[Test2]')
AND type IN ( N'U' ))
BEGIN
CREATE TABLE [dbo].[test2]
(
[id] [INT] IDENTITY(1, 1) NOT NULL,
[statusid] [INT] NULL
)
END
go
DECLARE #Flag INT = 0
IF EXISTS(SELECT 1
FROM sys.columns
WHERE NAME = N'TestId'
AND object_id = Object_id(N'dbo.Test2'))
SET #Flag = 1
--Solution #1
IF #Flag = 1
SELECT id,
statusid,
testid
FROM dbo.test2
ELSE
SELECT id,
statusid
FROM dbo.test2
--Solution #2
SELECT id,
statusid,
CASE
WHEN #Flag = 1 THEN testid
ELSE NULL
END AS TestId
FROM dbo.test2
you can use Dynamic SQL and generate the query accordingly depends on value of #flag
declare #sql nvarchar(max)
select #sql = N'select id, statusid, '
+ case when #flag = 1 then 'testid' else 'NULL' end + ' as testid'
+ ' from dbo.test2'
print #sql
exec sp_executesql #sql
But it will not be that easy to code and maintain Dynamic Query if you have a complex query.

SQL Server 2005 IF...ELSE incorrect execution

I am using SQL Server 2005 - 9.00.5266.00 (X64)
I am not sure what did I miss in below SQL. Can somebody find out what is the problem?
Here it goes -
create table my_temp
(
col1 varchar(10)
)
--===================== the first block is executed
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'my_temp') and Name = 'col2')
begin
select 'alter table my_temp add col2 bit'
--alter table my_temp add col2 bit
end
else
begin
select 'update my_temp set col2 = null'
--update my_temp set col2 = null
end
--====================== the second block is executed
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'my_temp') and Name = 'col2')
begin
--select 'alter table my_temp add col2 bit'
alter table my_temp add col2 bit
end
else
begin
--select 'update my_temp set col2 = null'
update my_temp set col2 = null
end
I am confused that the same IF statement executes different code blocks.
the problem related with batch
if you executed your code like that, where you seperate batch with use of GO statement
create table my_temp
(
col1 varchar(10)
)
GO
-- rest of script
then you will run your query each time ELSE part will executed because table is created
if you executed whole script first time with out GO statement then IF part will executed because at that time table will not create
Once you separate the QUERY table creation and other code then always ELSE part will executed

Generate CREATE TABLE statement from arbitrary SQL query

Is there script/procedure/function that can generate a CREATE TABLE statement from an arbitrary SQL query?
When building procedures, I'd like to have a quick way to generate a temporary table, rather than having to review the table definitions of all of the tables referenced in the query.
Simple example:
SELECT p.pat_id, pat_name,
enc_id, admsn_time, disch_time
FROM patient p
INNER JOIN encounter e ON p.pat_id=e.pat_id
WHERE admsn_time >= '01/01/2014'
Would generate (columns' data definition is retrieved from the system table):
-- randomly-generated table name
CREATE TABLE #random_name (
PAT_ID VARCHAR(18) NOT NULL,
PAT_NAME VARCHAR(200),
ENC_ID NUMERIC(18,0) NOT NULL,
ADMSN_TIME DATE,
DISCH_TIME DATE
)
SSMS workflow:
select the text
right click, select Generate CREATE TABLE statement (function/script/proc called; result placed on clipboard)
place cursor in desired location
paste
This is a bit of a hack, but you could try selecting into a table (see line #3):
SELECT p.pat_id, pat_name,
enc_id, admsn_time, disch_time
INTO delete_me
FROM patient p
INNER JOIN encounter e ON p.pat_id=e.pat_id
WHERE admsn_time >= '01/01/2014'
Then you can highlight the delete_me table in SSMS, right-click, and generate the CREATE TABLE script.
Finally, you'd want to DROP TABLE delete_me to clean up.
use
SELECT p.pat_id, pat_name,
enc_id, admsn_time, disch_time
into #randomtable
FROM patient p
INNER JOIN encounter e ON p.pat_id=e.pat_id
WHERE admsn_time >= '01/01/2014'
and your table will be created when you execute the statement.
First of all, I would make a stored procedure of the query. (It keeps me from forgetting it later)
Secondly, I would write a query to generate the table for me:
DECLARE #CREATE_TABLE_QUERY NVARCHAR(MAX) = N'';
SELECT
#CREATE_TABLE_QUERY += ', ' + name + ' ' + UPPER(system_type_name) + CHAR(13) + CHAR(10) + CHAR(9)
FROM
sys.dm_exec_describe_first_result_set('YOUR_PROCEDURE_NAME_HERE', NULL, 1);
SELECT
#CREATE_TABLE_QUERY = N'CREATE TABLE TABLE_NAME_HERE(' + CHAR(13) + CHAR(10) + CHAR(9) + STUFF(#CREATE_TABLE_QUERY, 1, 1, N'') + ');';
PRINT #CREATE_TABLE_QUERY;
Note: Replace 'YOUR_PROCEDURE_NAME_HERE' with the name of your own stored procedure.
Note: Replace TABLE_NAME_HERE with the table name of your choice.
The above will generate something like this:
CREATE TABLE TABLE_NAME_HERE(
WeekName VARCHAR(40)
, Line Name VARCHAR(50)
, TheDate DATETIME
, ReceivedAll INT
, Answered INT
, Abandoned INT
, Call Length INT
, WaitTimeAnswer INT
, WaitTimeAbandon INT
, PeriodName VARCHAR(10)
, Week SMALLINT
, Period SMALLINT
, Year SMALLINT
, WeekInPeriod SMALLINT
, NumWeeksInPeriod SMALLINT
, WeekendDate DATETIME
, CRCOperative VARCHAR(100)
, CallType VARCHAR(20)
, Charge Time INT
, SourceNumber VARCHAR(80)
, DestinationNumber VARCHAR(80)
, CallStart DATETIME
, Out of Hours VARCHAR(12)
, IsWorkingDay BIT
);

query/script/stored procedure which will return a single table

I have the following query. can someone help rapping this in a cursor.
IF OBJECT_ID('tempdb.[dbo].[#Results]') IS NOT NULL
DROP TABLE [dbo].[#Results]
GO
CREATE TABLE [dbo].[#Results] (
[DatabaseName] VARCHAR(128) NULL,
[DatabaseVersion] VARCHAR(128) NULL,
[DateChangedOn] DATETIME NULL)
EXEC sp_msForEachDb '
IF EXISTS (SELECT * FROM [?].sys.objects WHERE NAME = ''stdVersions'')
BEGIN
INSERT INTO #Results
SELECT
''?'' AS [DatabaseName],
v.[DatabaseVersion],
l.[DateChangedOn]
FROM [?].dbo.stdVersions v
CROSS JOIN [?].dbo.stdChangeLog l
END'
SELECT * FROM #Results ORDER BY DateChangedOn
the query should return list of all database names in the server and within the databases return the databaseversion column and DateChangeOn Column. All the databases in the server contain tables named stdVersions and stdChangeLog of which the stdVersion table have a single row of DatabaseVersion comes and stdChangeLog table have a single row of DateChangedOn.
Below is an example of creating a cursor on the #Results table.
DECLARE #DatabaseName sysname
, #DatabaseVersion sysname
, #DateChangedOn datetime;
DECLARE DatabaseList CURSOR LOCAL FAST_FORWARD
FOR
SELECT DatabaseName
, DatabaseVersion
, DateChangedOn
FROM #Results
ORDER BY DateChangedOn;
OPEN DatabaseList;
WHILE 1 = 1
BEGIN
FETCH NEXT FROM DatabaseList INTO
#DatabaseName
, #DatabaseVersion
, #DateChangedOn;
IF ##FETCH_STATUS = -1 BREAK;
--process row here
END;
CLOSE DatabaseList;
DEALLOCATE DatabaseList;
You mention that the "std" tables exist in all databases. If these tables does not include system databases, change the IF statement in the script below to exclude those:
IF EXISTS (SELECT * FROM [?].sys.objects WHERE NAME = ''stdVersions'')
AND [?] NOT IN(N'master', N'model', N'tempdb', N'msdb', N'SSISDB')

How to get a table storage information

SQL Server Edition: SQL Server 2005 w/ SP3 and 2008
Is there a built-in SQL Server stored procedures that will retrieve following information?
Or a DMV (Dynamic Management View) would be great as well.
I am interested mainly on how to find out FILEGROUP data of a table specifically.
But it'd be better if there was a sproc that will return all of following result.
By the way, is there any documents that shows one-to-one matching of how to retrieve data that SQL Server UI displays?
The system stored procedure sp_help could be a good starting point.
For example:
exec sp_help 'schema.TableName'
This will show you all kinds of goodness:
-- Script to analyze table space usage using the
-- output from the sp_spaceused stored procedure
-- Works with SQL 7.0, 2000, and 2005
set nocount on
print 'Show Size, Space Used, Unused Space, Type, and Name of all database files'
select
[FileSizeMB] =
convert(numeric(10,2),sum(round(a.size/128.,2))),
[UsedSpaceMB] =
convert(numeric(10,2),sum(round(fileproperty( a.name,'SpaceUsed')/128.,2))) ,
[UnusedSpaceMB] =
convert(numeric(10,2),sum(round((a.size-fileproperty( a.name,'SpaceUsed'))/128.,2))) ,
[Type] =
case when a.groupid is null then '' when a.groupid = 0 then 'Log' else 'Data' end,
[DBFileName] = isnull(a.name,'*** Total for all files ***')
from
sysfiles a
group by
groupid,
a.name
with rollup
having
a.groupid is null or
a.name is not null
order by
case when a.groupid is null then 99 when a.groupid = 0 then 0 else 1 end,
a.groupid,
case when a.name is null then 99 else 0 end,
a.name
create table #TABLE_SPACE_WORK
(
TABLE_NAME sysname not null ,
TABLE_ROWS numeric(18,0) not null ,
RESERVED varchar(50) not null ,
DATA varchar(50) not null ,
INDEX_SIZE varchar(50) not null ,
UNUSED varchar(50) not null ,
)
create table #TABLE_SPACE_USED
(
Seq int not null
identity(1,1) primary key clustered,
TABLE_NAME sysname not null ,
TABLE_ROWS numeric(18,0) not null ,
RESERVED varchar(50) not null ,
DATA varchar(50) not null ,
INDEX_SIZE varchar(50) not null ,
UNUSED varchar(50) not null ,
)
create table #TABLE_SPACE
(
Seq int not null
identity(1,1) primary key clustered,
TABLE_NAME SYSNAME not null ,
TABLE_ROWS int not null ,
RESERVED int not null ,
DATA int not null ,
INDEX_SIZE int not null ,
UNUSED int not null ,
USED_MB numeric(18,4) not null,
USED_GB numeric(18,4) not null,
AVERAGE_BYTES_PER_ROW numeric(18,5) null,
AVERAGE_DATA_BYTES_PER_ROW numeric(18,5) null,
AVERAGE_INDEX_BYTES_PER_ROW numeric(18,5) null,
AVERAGE_UNUSED_BYTES_PER_ROW numeric(18,5) null,
)
declare #fetch_status int
declare #proc varchar(200)
select #proc = rtrim(db_name())+'.dbo.sp_spaceused'
declare Cur_Cursor cursor local
for
select
TABLE_NAME =
rtrim(TABLE_SCHEMA)+'.'+rtrim(TABLE_NAME)
from
INFORMATION_SCHEMA.TABLES
where
TABLE_TYPE = 'BASE TABLE'
order by
1
open Cur_Cursor
declare #TABLE_NAME varchar(200)
select #fetch_status = 0
while #fetch_status = 0
begin
fetch next from Cur_Cursor
into
#TABLE_NAME
select #fetch_status = ##fetch_status
if #fetch_status <> 0
begin
continue
end
truncate table #TABLE_SPACE_WORK
insert into #TABLE_SPACE_WORK
(
TABLE_NAME,
TABLE_ROWS,
RESERVED,
DATA,
INDEX_SIZE,
UNUSED
)
exec #proc #objname =
#TABLE_NAME ,#updateusage = 'true'
-- Needed to work with SQL 7
update #TABLE_SPACE_WORK
set
TABLE_NAME = #TABLE_NAME
insert into #TABLE_SPACE_USED
(
TABLE_NAME,
TABLE_ROWS,
RESERVED,
DATA,
INDEX_SIZE,
UNUSED
)
select
TABLE_NAME,
TABLE_ROWS,
RESERVED,
DATA,
INDEX_SIZE,
UNUSED
from
#TABLE_SPACE_WORK
end --While end
close Cur_Cursor
deallocate Cur_Cursor
insert into #TABLE_SPACE
(
TABLE_NAME,
TABLE_ROWS,
RESERVED,
DATA,
INDEX_SIZE,
UNUSED,
USED_MB,
USED_GB,
AVERAGE_BYTES_PER_ROW,
AVERAGE_DATA_BYTES_PER_ROW,
AVERAGE_INDEX_BYTES_PER_ROW,
AVERAGE_UNUSED_BYTES_PER_ROW
)
select
TABLE_NAME,
TABLE_ROWS,
RESERVED,
DATA,
INDEX_SIZE,
UNUSED,
USED_MB =
round(convert(numeric(25,10),RESERVED)/
convert(numeric(25,10),1024),4),
USED_GB =
round(convert(numeric(25,10),RESERVED)/
convert(numeric(25,10),1024*1024),4),
AVERAGE_BYTES_PER_ROW =
case
when TABLE_ROWS <> 0
then round(
(1024.000000*convert(numeric(25,10),RESERVED))/
convert(numeric(25,10),TABLE_ROWS),5)
else null
end,
AVERAGE_DATA_BYTES_PER_ROW =
case
when TABLE_ROWS <> 0
then round(
(1024.000000*convert(numeric(25,10),DATA))/
convert(numeric(25,10),TABLE_ROWS),5)
else null
end,
AVERAGE_INDEX_BYTES_PER_ROW =
case
when TABLE_ROWS <> 0
then round(
(1024.000000*convert(numeric(25,10),INDEX_SIZE))/
convert(numeric(25,10),TABLE_ROWS),5)
else null
end,
AVERAGE_UNUSED_BYTES_PER_ROW =
case
when TABLE_ROWS <> 0
then round(
(1024.000000*convert(numeric(25,10),UNUSED))/
convert(numeric(25,10),TABLE_ROWS),5)
else null
end
from
(
select
TABLE_NAME,
TABLE_ROWS,
RESERVED =
convert(int,rtrim(replace(RESERVED,'KB',''))),
DATA =
convert(int,rtrim(replace(DATA,'KB',''))),
INDEX_SIZE =
convert(int,rtrim(replace(INDEX_SIZE,'KB',''))),
UNUSED =
convert(int,rtrim(replace(UNUSED,'KB','')))
from
#TABLE_SPACE_USED aa
) a
order by
TABLE_NAME
print 'Show results in descending order by size in MB'
select * from #TABLE_SPACE order by USED_MB desc
go
drop table #TABLE_SPACE_WORK
drop table #TABLE_SPACE_USED
drop table #TABLE_SPACE
Found a solution.
It seems like it takes longer to type this out than using UI to find out table FILEGROUP information.
Found through List tables in filegroups:
declare #objectid bigint
set #objectid = object_id('table_name')
exec sp_objectfilegroup #objectid
I became too lazy to type those three lines so ended up creating another stored procedure that takes table name instead.
create procedure spTableFileGroup
#TableName sysname
as
begin
if exists( select 1
from INFORMATION_SCHEMA.TABLES T
where T.TABLE_NAME = #TableName) begin
declare #objectid bigint
set #objectid = object_id(#TableName)
exec sp_objectfilegroup #objectid
end
else begin
print 'There is no table named "' + #TableName + '"'
end
end
GO
Usage
exec spTableFileGroup 'table_name'
GO
Have a look at DBCC showfilestats or sp_spaceused for filegroups.
Found a script at a blog. That lists tables and their sizes.
For a more user friendly (administrative view, you can generate reports using right mouse on the db).
The tables FILEGROUP is determined by it's clustered index. You can use this query to find the filegroup:
SELECT *
FROM
sys.tables AS tbl
INNER JOIN sys.indexes AS idx ON idx.object_id = tbl.object_id and idx.index_id < 2
LEFT OUTER JOIN sys.data_spaces AS dsidx ON dsidx.data_space_id = idx.data_space_id
In regard to your second question, I don't think there's any documentation, however, you can use SQL profiler when you view the details in SSMS. This will show you the exact queries.
This might do the trick ->
use Your_database_name
GO
SELECT FG.*, MF.* FROM sys.filegroups FG INNER JOIN sys.master_files
MF on MF.data_space_id = FG.data_space_id WHERE database_id = db_id()

Resources