I need to surround the following sql script with an if statment that checks the existence of one table. There's a lot more fields to the statement but the snippet below should be enough to get the idea.
If I surround this whole batch with an if statement it doesn't like that i have GOs between if statments. If i take out the GOs it complains about TMP_FIELD being an invalid column.
What are some ways to do this the right way? All i'm doing is taking a bunch of fields and changing from varchar to datetime. This is part of a setup.exe file so I just need it to run once and not for future upgrades. The way I determine that is if a certain table exists then don't run the script.
IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = object_id(N'MY_TABLE') and OBJECTPROPERTY(id, N'IsUserTable') = 1)
ALTER TABLE MY_TABLE ADD TMP_FIELD datetime
GO
IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = object_id(N'MY_TABLE') and OBJECTPROPERTY(id, N'IsUserTable') = 1)
UPDATE MY_TABLE SET TMP_FIELD = modifiedDate
GO
IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = object_id(N'MY_TABLE') and OBJECTPROPERTY(id, N'IsUserTable') = 1)
ALTER TABLE MY_TABLE DROP COLUMN modifiedDate
GO
IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = object_id(N'MY_TABLE') and OBJECTPROPERTY(id, N'IsUserTable') = 1)
ALTER TABLE MY_TABLE ADD modifiedDate datetime
GO
IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = object_id(N'MY_TABLE') and OBJECTPROPERTY(id, N'IsUserTable') = 1)
UPDATE MY_TABLE SET modifiedDate = TMP_FIELD
GO
IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = object_id(N'MY_TABLE') and OBJECTPROPERTY(id, N'IsUserTable') = 1)
ALTER TABLE MY_TABLE DROP COLUMN TMP_FIELD
GO
You don't really need to do all that gymnastic for changing the type of a column, do you?
create table MY_TABLE (
modifiedDate varchar(20)
)
go
insert MY_TABLE (modifiedDate) values ('2012-10-20 17:50:41')
go
select * from MY_TABLE
go
alter table MY_TABLE alter column modifiedDate datetime
go
select * from MY_TABLE
go
drop table MY_TABLE
go
So, I would write your statement like this:
if exists (select table_name from INFORMATION_SCHEMA.TABLES where TABLE_NAME = 'MY_TABLE')
begin
alter table MY_TABLE alter column modifiedDate datetime
end
The GOs mark the end of a batch of TSQL statements. You can't mix DDL (data definition language) statements like ALTER TABLE with DML (data manipulation language) statements like UPDATE TABLE in the same batch.
Each batch is compiled on it's own. So when the ALTER TABLE and UPDATE TABLE statements are in the same batch, SQL Server can't compile the update statement because the column modifiedData hasn't actually been created yet.
If you want to get around 'Invalid column ..' errors, one option is to use dynamic SQL.
e.g. :
create table dbo.t1 (id int primary key, cola varchar(20))
go
insert dbo.t1 values (1, 'one')
insert dbo.t1 values (2, 'two')
go
if not exists(select * from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = 't1' and COLUMN_NAME = 'colb')
BEGIN
-- add new column 'colb', and set its value initially to existing values in 'cola'
ALTER TABLE dbo.t1 ADD colb varchar(20)
DECLARE #v NVARCHAR(500)
SET #v = N'UPDATE dbo.t1 SET colb = cola'
EXEC (#v) -- use dynamic SQL, otherwise will get Invalid column name colb error.
END
GO
Note that dynamic SQL should be considered a last resort. David Brabant's answer seems to be the way to go for your original problem.
Related
I am adding a trigger to an existing database table, which is used by an application I did not code, and cannot change the code in.
I want to insert some information from TableA into TableB on INSERT into TableA.
Application runs INSERT INTO TableA <-- this updates ##identity
An ON INSERT trigger that runs on TableA then inserts data into TableB <-- this also updates the ##identity with a new value
Application reads ##identity <-- which is from TableB not from TableA as the application is expecting
Is there any way to not update the ##identity from within the trigger?
....since ##identity has no scope you could create your own scope which carries the ##identity value at the end of the trigger
create table tableA(idA int identity(100, 1), colA int)
go
create table tableB(idB int identity(1000, 1), colB int)
go
create trigger triggerA on tableA
for insert
as
begin
if not exists(select * from inserted)
begin
return;
end
declare #tableA##identity int = ##identity;
select ##identity as [##identity_triggerA_in];
--add rows to tableB
insert into tableB(colB)
select object_id
from sys.all_objects
select ##identity as [##identity_after_insert_in_tableB];
if #tableA##identity is not null
begin
declare #sql varchar(100) = concat('create table #t(id int identity(', #tableA##identity, ',1)); insert into #t default values');
exec (#sql);
end
select ##identity as [##identity_triggerA_out];
end
go
insert into tableA(colA) values (10);
select ##identity;
go
insert into tableA(colA)
select top (200) 1
from sys.all_objects;
select ##identity;
go
insert into tableA(colA)
select 1
where 1=2;
select ##identity;
go
drop table tableA;
go
drop table tableB;
go
I'm trying to create a trigger to insert all the value that I delete from a table in a "Backup" table,
Ex:
Table 1: NomePilota, ModelloVettura, NomeScuderia
BackupTable1 (Table 2): NomePilota, ModelloVettura, NomeScuderia
What I want from the trigger to do: Insert into 'Table 2' deleted values from table 1.
I tried like this:
CREATE TRIGGER Backup ON dbo.Table1 AFTER (i can only use after) DELETE AS
BEGIN
DECLARE #Pilota VARCHAR(20) = (SELECT NomePilota FROM deleted)
DECLARE #Vettura VARCHAR(50) = (SELECT ModelloVettura FROM deleted)
DECLARE #Scuderia VARCHAR(20) = (SELECT NomeScuderia FROM deleted)
INSERT INTO Table2 (NomePilota, ModelloVettura, NomeScuderia) VALUES (#Pilota, #Vettura, #Scuderia)
But it send a error:
Cannot insert multiple records in #Pilota, #Vet, #Scud
How can I fix that? Does the deleted table already have a default ID column to use like in a for? Can I use something like vectors? (like #Nome[] = SELECT * FROM Tabella, Insert into Tabella2 (Nome) VALUES #Nome[#Numero (numero is like the record number of nome]).
Why not make life simple?
INSERT INTO Table2 (NomePilota, ModelloVettura, NomeScuderia)
SELECT NomePilota, ModelloVettura, NomeScuderia FROM deleted
The insert statment can work on the results of a select -- here we leverage this to simplify the task at hand.
I am looking to create a SQL Server trigger that moves a record from one table to an identical replica table if the record matches a specific condition.
Questions: do I need to specify each column, or can I use a wildcard?
Can I use something like:
SET #RecID = (SELECT [RecoID] FROM Inserted)
IF NULLIF(#RecID, '') IS NOT NULL
(then insert....)
THANKS!
There's a lot of stuff you "CAN" do in a trigger, but that doesn't mean you should. I'd would urge to to avoid setting scalar variables within a trigger at all costs. Even if you 100% sure your table will never have more that 1 row inserted per transaction because that's how the app is designed... You'll be in for very rude awakening when you find out that not all transactions come through the application.
Below is a quick demonstration of both types of triggers...
USE tempdb;
GO
IF OBJECT_ID('tempdb.dbo.PrimaryTable', 'U') IS NOT NULL
DROP TABLE dbo.PrimaryTable;
GO
IF OBJECT_ID('tempdb.dbo.TriggerScalarLog', 'U') IS NOT NULL
DROP TABLE dbo.TriggerScalarLog;
GO
IF OBJECT_ID('tempdb.dbo.TriggerMultiRowLog', 'U') IS NOT NULL
DROP TABLE dbo.TriggerMultiRowLog;
GO
CREATE TABLE dbo.PrimaryTable (
Pt_ID INT NOT NULL IDENTITY (1,1) PRIMARY KEY CLUSTERED,
Col_1 INT NULL,
Col_2 DATE NOT NULL
CONSTRAINT df_Col2 DEFAULT (GETDATE())
);
GO
CREATE TABLE dbo.TriggerScalarLog (
Pt_ID INT,
Col1_Old INT,
Col1_New INT,
Col2_Old DATE,
Col2_New DATE
);
GO
CREATE TABLE dbo.TriggerMultiRowLog (
Pt_ID INT,
Col1_Old INT,
Col1_New INT,
Col2_Old DATE,
Col2_New DATE
);
GO
--=======================================================
CREATE TRIGGER dbo.PrimaryCrudScalar ON dbo.PrimaryTable
AFTER INSERT, UPDATE, DELETE
AS
SET NOCOUNT ON;
DECLARE
#Pt_ID INT,
#Col1_Old INT,
#Col1_New INT,
#Col2_Old DATE,
#Col2_New DATE;
SELECT
#Pt_ID = ISNULL(i.Pt_ID, d.Pt_ID),
#Col1_Old = d.Col_1,
#Col1_New = i.Col_1,
#Col2_Old = d.Col_2,
#Col2_New = i.Col_2
FROM
Inserted i
FULL JOIN Deleted d
ON i.Pt_ID = d.Pt_ID;
INSERT dbo.TriggerScalarLog (Pt_ID, Col1_Old, Col1_New, Col2_Old, Col2_New)
VALUES (#Pt_ID, #Col1_Old, #Col1_New, #Col2_Old, #Col2_New);
GO -- DROP TRIGGER dbo.PrimaryCrudScalar;
CREATE TRIGGER PrimaryCrudMultiRow ON dbo.PrimaryTable
AFTER INSERT, UPDATE, DELETE
AS
SET NOCOUNT ON;
INSERT dbo.TriggerMultiRowLog (Pt_ID, Col1_Old, Col1_New, Col2_Old, Col2_New)
SELECT
ISNULL(i.Pt_ID, d.Pt_ID),
d.Col_1,
i.Col_1,
d.Col_2,
i.Col_2
FROM
Inserted i
FULL JOIN Deleted d
ON i.Pt_ID = d.Pt_ID;
GO -- DROP TRIGGER dbo.TriggerMultiRowLog;
--=======================================================
--=======================================================
-- --insert test...
INSERT dbo.PrimaryTable (Col_1)
SELECT TOP 100
o.object_id
FROM
sys.objects o;
SELECT 'INSERT Scarar results';
SELECT * FROM dbo.TriggerScalarLog tsl;
SELECT 'INSERT Multi-Row results';
SELECT * FROM dbo.TriggerMultiRowLog tmrl;
UPDATE pt SET
pt.Col_1 = pt.Col_1 + rv.RandomVal,
pt.Col_2 = DATEADD(DAY, rv.RandomVal, pt.Col_2)
FROM
dbo.PrimaryTable pt
CROSS APPLY ( VALUES (ABS(CHECKSUM(NEWID())) % 10000 + 1) ) rv (RandomVal);
SELECT 'UPDATE Scarar results';
SELECT * FROM dbo.TriggerScalarLog tsl;
SELECT 'UPDATE Multi-Row results';
SELECT * FROM dbo.TriggerMultiRowLog tmrl;
DELETE pt
FROM
dbo.PrimaryTable pt;
SELECT 'DELETE Scarar results';
SELECT * FROM dbo.TriggerScalarLog tsl;
SELECT 'DELETE Multi-Row results';
SELECT * FROM dbo.TriggerMultiRowLog tmrl;
You could, but I'd recommend against it. If your source table changed things would start failing.
Also, in your example if you were to ever have more than one row inserted at a time you would get thrown an error (or have unpredictable results). I'd recommend a more set based approach:
INSERT table2 ( user_id ,
user_name ,
RecoID
)
SELECT user_id ,
user_name ,
RecoID
FROM inserted i
LEFT JOIN table2 t ON i.RecoID = t.RecoID
WHERE t.RecoID IS NULL;
EDIT:
If you want to stop the insert happening on your original table then you'll need to do something along the lines of:
CREATE TRIGGER trigger_name
ON table_orig
INSTEAD OF INSERT
AS
BEGIN
-- make sure we aren't triggering from ourselves from another trigger
IF TRIGGER_NESTLEVEL() <= 1
return;
-- insert into the table_copy if the inserted row is already in table_orig (not null)
INSERT table_copy ( user_id ,
user_name ,
RecoID
)
SELECT user_id ,
user_name ,
RecoID
FROM inserted i
LEFT JOIN table_orig c ON i.RecoID = c.RecoID
WHERE t.RecoID IS NOT NULL;
-- insert into table_orig if the inserted row is not already in table_orig (null)
INSERT table_orig ( user_id ,
user_name ,
RecoID
)
SELECT user_id ,
user_name ,
RecoID
FROM inserted i
LEFT JOIN table_orig c ON i.RecoID = c.RecoID
WHERE t.RecoID IS NULL;
END;
The instead of will stop the insert if you don't want it to actually be inserted, so you'll need to do that yourself (the second insert statement).
Please note I changed some nulls to not nulls and the table we are left joining to in some cases.
When I go to run this query on SQL Server to create an indexed view, an error occurs to remove subqueries and Union
CREATE VIEW [dbo].[view_ToolGroup]
WITH SCHEMABINDING
AS
SELECT
toolGroup.ToolGroupId,toolGroupToTool.ToolId, toolGroupApp.AppId as TGAppId,
purposeToToolGroup.PurposeId as TGPurposeId, TGRole.RoleId as TGRoleId
FROM
[dbo].[toolGroup], [dbo].[purposeToTG], [dbo].[toolGroupToTool],
[dbo].[toolGroupToeApp] as toolGroupApp,
[dbo].[toolGroupToeAppToeRole] as toolGroupAppRole,
[dbo].[eRole] as TGRole
WHERE
toolGroup.ToolGroupId = purposeToToolGroup.ToolGroupId
and toolGroup.ToolGroupId = toolGroupToTool.ToolGroupId
and toolGroup.ToolGroupId = toolGroupApp.ToolGroupId
and toolGroupApp.toolGroupToeApplicationID=toolGroupAppRole.toolGroupToeApplicationID
and toolGroupAppRole.ToolgroupToeApplicationID in
(select ToolgroupToeApplicationID
from [dbo].[ToolgroupToeApplication])
and toolGroupAppRole.RoleId = TGRole.RoleId
UNION
SELECT
toolGroup.ToolGroupId, toolGroup.ToolGroupName,
null, null, null, null, null, null, null, null
FROM
[dbo].[toolGroup], [dbo].[toolGroupToeApplication]
WHERE
toolGroup.ToolGroupId = toolGroupToeApplication.ToolGroupId
and toolGroup.ToolGroupId not in
(select PurposeToToolGroup.ToolGroupId from [dbo].[PurposeToToolGroup])
and toolGroup.ToolGroupId in (select distinct ToolGroupId
from [dbo]. [toolGroupToeApplication] )'
GO
CREATE UNIQUE CLUSTERED INDEX IDX_view_ToolGroup
ON view_ToolGroup(ToolGroupId, ToolId, TGPurposeId, TGRoleId)
GO
Can anybody suggest an alternative solution to replace UNION and subqueries?
As per all the suggestion above, there is no direct way of doing this. However we can cheat. You could do the following
Break up the statements into two views
Add index to each view
Replace IN with INNER JOIN
User NOT IN while calling the views
As Damien suggested, it would be foolish of me to try and suggest or attempt to judge the logic at this time since we have no idea what the tables represent. I have however restructured the code as per the above which you could use as a template to structure your actual query.
Hope this helps
--Drop Index If Already Aresent
IF EXISTS (SELECT * FROM sys.views WHERE object_id = OBJECT_ID(N'[dbo].[vw_ToolGroup_One]'))
DROP VIEW dbo.vw_ToolGroup_One
GO
IF EXISTS (SELECT * FROM sys.views WHERE object_id = OBJECT_ID(N'[dbo].[vw_ToolGroup_Two]'))
DROP VIEW dbo.vw_ToolGroup_Two
GO
--Drop Dependant Tables
IF OBJECT_ID(N'toolGroup_tmp')>0
BEGIN
DROP TABLE toolGroup_tmp
END
CREATE TABLE toolGroup_tmp (ToolGroupId INT,ToolGroupName VARCHAR(100))
INSERT INTO toolGroup_tmp
SELECT 1,'ONE'
GO
IF OBJECT_ID(N'purposeToTG_tmp')>0
BEGIN
DROP TABLE purposeToTG_tmp
END
CREATE TABLE purposeToTG_tmp (ToolGroupId INT,PurposeId int)
INSERT INTO purposeToTG_tmp
SELECT 1,1
GO
IF OBJECT_ID(N'toolGroupToTool_tmp')>0
BEGIN
DROP TABLE toolGroupToTool_tmp
END
CREATE TABLE toolGroupToTool_tmp (ToolGroupId INT,ToolId INT)
INSERT INTO toolGroupToTool_tmp
SELECT 1,1
GO
IF OBJECT_ID(N'toolGroupToeApp_tmp')>0
BEGIN
DROP TABLE toolGroupToeApp_tmp
END
CREATE TABLE toolGroupToeApp_tmp (ToolGroupId INT,AppId INT,toolGroupToeApplicationID INT)
INSERT INTO toolGroupToeApp_tmp
SELECT 1,1,1
GO
IF OBJECT_ID(N'toolGroupToeAppToeRole_tmp')>0
BEGIN
DROP TABLE toolGroupToeAppToeRole_tmp
END
CREATE TABLE toolGroupToeAppToeRole_tmp (ToolGroupId INT,RoleId INT,toolGroupToeApplicationID INT)
INSERT INTO toolGroupToeAppToeRole_tmp
SELECT 1,1,1
GO
IF OBJECT_ID(N'ToolgroupToeApplication_tmp')>0
BEGIN
DROP TABLE ToolgroupToeApplication_tmp
END
CREATE TABLE ToolgroupToeApplication_tmp (ToolGroupId INT,ToolgroupToeApplicationID INT)
INSERT INTO ToolgroupToeApplication_tmp
SELECT 1,1
GO
IF OBJECT_ID(N'PurposeToToolGroup_tmp')>0
BEGIN
DROP TABLE PurposeToToolGroup_tmp
END
CREATE TABLE PurposeToToolGroup_tmp (ToolGroupId INT)
INSERT INTO PurposeToToolGroup_tmp
SELECT 1
GO
IF OBJECT_ID(N'eRole_tmp')>0
BEGIN
DROP TABLE eRole_tmp
END
CREATE TABLE eRole_tmp (RoleId INT)
INSERT INTO eRole_tmp
SELECT 1
GO
--Create Views
DECLARE #SQL NVARCHAR(MAX)
SET #SQL = '
CREATE VIEW dbo.vw_ToolGroup_One WITH SCHEMABINDING
AS
SELECT
tg.ToolGroupId,
tg.ToolGroupName,
tgtt.ToolId,
tga.AppId AS TGAppId,
pttg.PurposeId AS TGPurposeId,
tgr.RoleId AS TGRoleId
FROM dbo.toolGroup_tmp tg
INNER JOIN dbo.purposeToTG_tmp pttg
ON tg.ToolGroupId = pttg.ToolGroupId
INNER JOIN dbo.toolGroupToTool_tmp tgtt
ON tg.ToolGroupId = tgtt.ToolGroupId
INNER JOIN dbo.toolGroupToeApp_tmp tga
ON tg.ToolGroupId = tga.ToolGroupId
INNER JOIN dbo.toolGroupToeAppToeRole_tmp tgar
ON tga.toolGroupToeApplicationID = tgar.toolGroupToeApplicationID
INNER JOIN dbo.ToolgroupToeApplication_tmp tgta
ON tgta.ToolgroupToeApplicationID = tgar.ToolgroupToeApplicationID
INNER JOIN dbo.eRole_tmp tgr
ON tgar.RoleId = tgr.RoleId
'
EXEC SP_EXECUTESQL #SQL
SET #SQL = '
CREATE VIEW dbo.vw_ToolGroup_Two WITH SCHEMABINDING
AS
SELECT tg.ToolGroupId,
tg.ToolGroupName,
NULL AS ToolId,
NULL AS TGAppId,
NULL AS TGPurposeId,
NULL AS TGRoleId
FROM dbo.toolGroup_tmp tg
INNER JOIN dbo.ToolgroupToeApplication_tmp tgtea
ON tg.ToolGroupId = tgtea.ToolGroupId
'
EXEC SP_EXECUTESQL #SQL
-- Create Indexes
CREATE UNIQUE CLUSTERED INDEX IDX_view_ToolGroup_One
ON vw_ToolGroup_One(ToolGroupId, ToolGroupName, ToolId, TGPurposeId, TGRoleId);
CREATE UNIQUE CLUSTERED INDEX IDX_view_ToolGroup_Two
ON vw_ToolGroup_Two(ToolGroupId, ToolGroupName);
GO
-- Invoke Query
SELECT * FROM vw_ToolGroup_One
UNION ALL
SELECT * FROM vw_ToolGroup_Two tgt
WHERE NOT EXISTS ( SELECT 1
FROM dbo.PurposeToToolGroup_tmp pttg
WHERE pttg.ToolGroupId = tgt.ToolGroupId)
How can i know the data type of a field inside a trigger. I am able to get the field name and it's value inside a trigger after insert as follows:
DECLARE #AfterInserted XML
SELECT #AfterInserted = (
SELECT *
FROM INSERTED
WHERE User_Key = User_Key
FOR XML RAW, ROOT
);
CREATE TABLE #XML(
FieldName nvarchar(250),
Value nvarchar(250));
Insert Into #XML(FieldName, Value)
select T.N.value('local-name(.)', 'nvarchar(100)'),
T.N.value('.', 'nvarchar(250)')
from #AfterInserted.nodes('/root/row/#*') as T(N)
I also need data type of the field too. something like T.N.value('Data-type')?
Thanks
Not exactly sure if this will work for your purpose, but:
SELECT SQL_VARIANT_PROPERTY(your_column, 'BaseType')
FROM your_table
Will return a column's field type as NVARCHAR.
You can also use Precision, Scale, MaxLength, Collation and TotalBytes as the 2nd parameter for SQL_VARIANT_PROPERTY.
There is no need to use XML to get meta-data related to the table that the Trigger is on. You can query the system catalog to get the info directly.
The Trigger is an object with an object_id, so the ##PROCID system variable will have the object_id of the Trigger itself, within the context of the Trigger.
Using the value of ##PROCID, you can look in sys.objects at the row for that specific object_id and the parent_object_id field will be the object_id of the Table that the Trigger is on.
Using the value of parent_object_id, you can query sys.tables / sys.objects to get table-level info, or query sys.columns to get column-level info.
The example below illustrates the above info:
SET ANSI_NULLS ON
SET QUOTED_IDENTIFIER ON
SET NOCOUNT ON
GO
-- DROP TABLE dbo.TriggerTest
IF (OBJECT_ID('dbo.TriggerTest') IS NULL)
BEGIN
PRINT 'Creating TriggerTest...'
CREATE TABLE dbo.TriggerTest (Col1 INT, Col2 VARCHAR(30), Col3 DATETIME)
END
--
IF (OBJECT_ID('dbo.TR_TriggerTest_IU') IS NOT NULL)
BEGIN
PRINT 'Dropping TR_TriggerTest_IU...'
DROP TRIGGER dbo.TR_TriggerTest_IU
END
GO
PRINT 'Creating TR_TriggerTest_IU...'
GO
CREATE TRIGGER dbo.TR_TriggerTest_IU
ON dbo.TriggerTest
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON
-- get Table info
SELECT *
FROM sys.tables st
WHERE st.[object_id] = ( -- get parent_object_id of this Trigger
-- which will be the table that the
-- Trigger is on
SELECT so.parent_object_id
FROM sys.objects so
WHERE so.[object_id] = ##PROCID
)
-- get Column info
SELECT *
FROM sys.columns sc
-- Custom types will repeat values of system_type_id and produce a
-- cartesian product, so filter using "is_user_defined = 0"
INNER JOIN sys.types st
ON st.system_type_id = sc.system_type_id
AND st.is_user_defined = 0
WHERE sc.[object_id] = ( -- get parent_object_id of this Trigger
-- which will be the table that the
-- Trigger is on
SELECT so.parent_object_id
FROM sys.objects so
WHERE so.[object_id] = ##PROCID
)
END
GO
INSERT INTO dbo.TriggerTest (Col1, Col2, Col3) VALUES (1, 'a', GETDATE())