TSQL Force Cast all columns in a view definition - sql-server

I have about 500 simple Sql Views that each reference 1 or 2 underlying tables.
The views are used to interface to another system.
How do I force cast all columns to reflect the current datatypes
I want to do this so that if underlying table columns change, the output of the Views remain unaffected.
SELECT
CAST (T1.ColumnA AS NUMERIC(38, 0)),
CAST (T1.ColumnB AS DATE),
CAST (T1.ColumnC AS VARCHAR(900)),
CAST (T2.ColumnA AS TIME(7)),
CAST (T2.ColumnB AS VARCHAR(4))
FROM
Table1 T1
LEFT JOIN
Table2 T2 ON T1.ID = T2.ID
All suggestions welcome.

You do not need to do anything. Even if the table columns types are changed, these in the view(s) are not. If you want to change the columns types in the view, you need to explicitly do this by dropping/recreating the view or using sp_refreshview. So, there is nothing to worry about.
Here is a code, demonstrating this:
DROP TABLE IF EXISTS [dbo].[StackOverflow];
CREATE TABLE [dbo].[StackOverflow]
(
[ColumnA] INT
,[ColumnB] NVARCHAR(25)
,[ColumnC] DATETIME2(0)
);
GO
DROP VIEW IF EXISTS [dbo].[vw_StackOverflow];
GO
CREATE VIEW [dbo].[vw_StackOverflow]
AS
SELECT [ColumnA]
,[ColumnB]
,[ColumnC]
FROM [dbo].[StackOverflow];
GO
SELECT C.[name]
,T.[name]
,C.[max_length]
,c.[precision]
,c.[scale]
FROM [sys].[columns] C
INNER JOIN [sys].[types] T
ON C.[system_type_id] = T.[system_type_id]
WHERE [object_id] = OBJECT_ID('[dbo].[vw_StackOverflow]');
GO
ALTER TABLE [dbo].[StackOverflow]
ALTER COLUMN [ColumnA] BIGINT;
ALTER TABLE [dbo].[StackOverflow]
ALTER COLUMN [ColumnB] NVARCHAR(12);
ALTER TABLE [dbo].[StackOverflow]
ALTER COLUMN [ColumnC] DATETIME2(7);
GO
SELECT C.[name]
,T.[name]
,C.[max_length]
,c.[precision]
,c.[scale]
FROM [sys].[columns] C
INNER JOIN [sys].[types] T
ON C.[system_type_id] = T.[system_type_id]
WHERE [object_id] = OBJECT_ID('[dbo].[vw_StackOverflow]');
GO
EXEC sp_refreshview N'[dbo].[vw_StackOverflow]';
GO
SELECT C.[name]
,T.[name]
,C.[max_length]
,c.[precision]
,c.[scale]
FROM [sys].[columns] C
INNER JOIN [sys].[types] T
ON C.[system_type_id] = T.[system_type_id]
WHERE [object_id] = OBJECT_ID('[dbo].[vw_StackOverflow]');
GO

Related

Efficient query to filter for list of values across columns/distinct rows

SQL Server version is 2016+/Azure SqlDb (flexible if additive, compatible with both, forward-compatible).
Use case is API users sending a single-column list of values to filter some target table. The target table has 2-n columns whose values are unique across rows (maybe columns, doesn't matter) for the table/range being queried. (So far n <= 5, but that's a detail/not guaranteed.)
Here's a good-enough sample table DDL:
IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'SomeTable')
BEGIN
CREATE TABLE dbo.SomeTable (
ID int IDENTITY(1, 1) not null PRIMARY KEY CLUSTERED
, NaturalKey1 nvarchar(10) not null UNIQUE NONCLUSTERED
, NaturalKey2 nvarchar(10) not null UNIQUE NONCLUSTERED
, NaturalKey3 nvarchar(10) not null UNIQUE NONCLUSTERED
);
END
IF NOT EXISTS (SELECT 1 FROM dbo.SomeTable)
BEGIN
INSERT INTO dbo.SomeTable VALUES
('A', 'AA', 'ZZZZZ')
,('B', 'B', 'YYYYY')
,('C', 'CC', 'XXX')
,('D', 'DDD', 'WWWWW')
,('E', 'EEEE', 'V')
,('F', 'FF', 'UUUUUUUUU')
,('G', 'GGGGGGGG', 's')
-- lots more
;
END
SELECT * FROM dbo.SomeTable;
-- DROP TABLE dbo.SomeTable;
Assumptions are that all NaturalKey columns are of same type (probably nvarchar); filtering happens db-side; and in as few steps as possible, ideally one execution, in a stored procedure. Parameter will be string list or TVP, doesn't matter really. Result will include all data in any row of SomeTable that matches any value on any column. Target table is of unknown size.
Here's an example parameter for our pal above:
DECLARE #filterValues nvarchar(1000) = 'DDD,XXX,E,HH,ok,whatever,YYYYY';
SELECT * FROM string_split(#filterValues, ',');
I know a couple ways to do this, and can imagine several more, so it's not that kind of stuck. I'll bet someone knows a better trick than either of the two I'll illustrate.
Approach 1 Build a temp table updated for existence and join on it (concise and nice to audit, that's about it for pros)
DECLARE #filterValues nvarchar(1000) = 'DDD,XXX,E,HH,ok,whatever,YYYYY';
SELECT value AS InValue, CONVERT(int, null) AS IDMatch
INTO #filters
FROM string_split(#filterValues, ',');
UPDATE f
SET f.IDMatch = st.ID
FROM #filters AS f
INNER JOIN dbo.SomeTable AS st ON f.InValue IN (st.NaturalKey1, st.NaturalKey2, st.NaturalKey3);
SELECT * FROM #filters; -- Audit
SELECT st.* FROM #filters AS f INNER JOIN dbo.SomeTable AS st ON f.IDMatch = st.ID;
IF OBJECT_ID('tempdb..#filters') IS NOT NULL DROP TABLE #filters;
Approach 2 Unpivot SomeTable (I like the nifty cross apply trick) and just join (at scale there be ogres methinks)
SELECT
st.*
FROM
dbo.SomeTable AS st
CROSS APPLY (VALUES (st.NaturalKey1)
, (st.NaturalKey2)
, (st.NaturalKey3)
) AS nk(Val)
INNER JOIN #filters AS f ON nk.Val = f.InValue;
IF OBJECT_ID('tempdb..#filters') IS NOT NULL DROP TABLE #filters;
Is there a question in our future
Works is better than doesn't work, but looking for better/more efficient/more scalable methods from the T-SQL gurus. Unknown dimensions in columns and rows, response time is an SLA, filter size may or may not be capped. Bonus points if this ports neatly from SomeTable to SomeTableVersionN. (No dynamic SQL in an API.)
Could be dupe question, couldn't find it, pointing that out is just fine thank you.

SQL Server - How to enforce a field value from a combination of fields not repeated with another field value

I know I haven't framed the question very well, to be honest I found it difficult to explain without an example.
I have a table with SalesPersonID and SalesPersonSSN fields.
My requirement is a SalesPersonID should only exist with one SalesPersonSSN and vice versa.
If you see the table (sample data), the record with SalesPersonID 2003 is invalid because SalesPersonSSN 3344556677 already exists with SalesPersonID 2001. Similarly SalesPersonID 2001 should never exist with other than 3344556677.
I don't know how to enforce this rule in the table.
Also is there a simple query to find out if the rule is violated.
You want unique constraint :
alter table t
add constraint ssn unique(SalesPersonSSN);
If you want the data that violates the rules you can use exists :
select t.*
from table t
where exists (select 1
from table t1
where t1.SalesPersonSSN = t.SalesPersonSSN and
t1.SalesPersonID <> t.SalesPersonID
);
To find out if your rule is violated you could use the follwowing
Table
DECLARE #t TABLE (SalesPersonId INT, SalesPersonSSN VARCHAR(255))
INSERT INTO #t VALUES (2001,'3344556677'), (2002,'7755330099'), (2003,'3344556677')
Query
SELECT t.*
FROM #t t
INNER JOIN (SELECT SalesPersonSSN
FROM #t
GROUP BY SalesPersonSSN
HAVING COUNT(*) > 1
) a
ON a.SalesPersonSSN = t.SalesPersonSSN
You need to write the complete logic for it:
declare #ssn as varchar(255)='7755330099' --INPUT
declare #pid as int=201 --INPUT
declare #ssn1 as varchar(255)--local variable
declare #pid1 as int --local variable
select #pid1=SalesPersonId from #t where SalesPersonSSN=#ssn;
select #ssn1=SalesPersonSSN from #t where SalesPersonId=#pid;
if(#pid1 is not null and #pid1<>#pid)
begin
print('failed: as person '+cast(#pid1 as varchar(255))+' already asigned to ssn#'+#ssn)
end
if(#ssn1 is not null and #ssn1<>#ssn)
begin
print('failed: as ssn#'+#ssn1 +' already asigned to pid# '+cast(#pid as varchar(255)))
end
Table definition:
create TABLE #t(SalesPersonId INT, SalesPersonSSN VARCHAR(255))
INSERT INTO #t VALUES (2001,'3344556677'), (2002,'7755330099'), (2003,'3344556677')

Alternative to replace Union and subqueries to create Indexed view

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 to know the data type of a field in trigger?

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())

sql script, if and go statements

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.

Resources