Transpose table and save as View - sql-server

I have a dynamic pivot/unpivot script that transposes a table. This is dynamic enough to return certain columns that I want and using dynamic columns.
What I am looking for is rather to convert this into either a UDF or a VIEW so that I can join it to other tables.
Please help.
ALTER PROC [dbo].[uspGetUserByValues]
(
#Select NVARCHAR(4000) = '*',
#Where NVARCHAR(4000) = NULL,
#OrderBy NVARCHAR(4000) = NULL
)
AS
BEGIN
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT distinct ',P.' + QUOTENAME(PropertyDescription)
from System_Properties
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT ' + #cols + ', M.Email, C.Company_Name, C.Company_Type_ID, U.UserName, ISNULL(SMS.SMSProfiles,0) SMSProfiles, U.UserID
from
(
select PropertyDescription, UP.UserID, PropertyValue
from User_Properties UP
JOIN System_Properties SP ON UP.PropertyID = SP.PropertyID
JOIN aspnet_Membership M ON UP.UserID = M.UserID
) X
pivot
(
min(PropertyValue)
for PropertyDescription in (' + REPLACE(#cols,'P.','') + ')
) P
JOIN aspnet_Membership M ON P.UserID = M.UserID
JOIN aspnet_Users U on P.UserID = U.UserID
JOIN Companies C ON C.Company_ID = P.Company_ID
LEFT JOIN (SELECT UserId, COUNT(Users_SMS_Profile_ID) SMSProfiles
FROM Users_SMS_Profile GROUP BY UserID ) SMS ON SMS.UserID = P.UserID
'
SET #query = 'SELECT ' + #Select + ' FROM ('+ #query +') A'
IF ISNULL(#Where,'NULL') != 'NULL'
BEGIN
SET #query = #query + ' WHERE ' + #Where
END
IF ISNULL(#OrderBy,'NULL') != 'NULL'
BEGIN
SET #query = #query + ' ORDER BY ' + #OrderBy
END
execute(#query)
--PRINT(#query)
END

OH wow I made it.
I know this is with "known" column names but actually I didn't have to know them.
Firstly, this is the query I used to create the View. I will need to drop the view at least every I add a new Property or I can actually write a job that checks if all the properties from System_Properties are represented in the view, if not then drop the view and run this code.
CREATE PROC [dbo].[uspCreateViewUsers]
AS
BEGIN
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT distinct ',P.' + QUOTENAME(PropertyDescription)
from System_Properties
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'CREATE VIEW vwUsers AS SELECT ' + #cols + ', M.Email, C.Company_Name, C.Company_Type_ID, U.UserName, ISNULL(SMS.SMSProfiles,0) SMSProfiles, U.UserID
from
(
select PropertyDescription, UP.UserID, PropertyValue
from User_Properties UP
JOIN System_Properties SP ON UP.PropertyID = SP.PropertyID
JOIN aspnet_Membership M ON UP.UserID = M.UserID
) X
pivot
(
min(PropertyValue)
for PropertyDescription in (' + REPLACE(#cols,'P.','') + ')
) P
JOIN aspnet_Membership M ON P.UserID = M.UserID
JOIN aspnet_Users U on P.UserID = U.UserID
JOIN Companies C ON C.Company_ID = P.Company_ID
LEFT JOIN (SELECT UserId, COUNT(Users_SMS_Profile_ID) SMSProfiles
FROM Users_SMS_Profile GROUP BY UserID ) SMS ON SMS.UserID = P.UserID
'
execute(#query)
END
Them the View, which can't be represented graphically by table joins looks like this:
SELECT P.[Company_ID], P.[Created_Date], P.[Created_User], P.[Cust_ID], P.[FirstName], P.[IPCheck], P.[JobTitle], P.[LastLogin], P.[LastModified_Date], P.[LastModified_User],
P.[LastName], P.[Newsletter_OptIn], P.[Password_Change], P.[SupAdmin], P.[SysAccess], P.[SysAdmin], P.[User_Cat_1], P.[User_Cat_10], P.[User_Cat_2],
P.[User_Cat_3], P.[User_Cat_4], P.[User_Cat_5], P.[User_Cat_6], P.[User_Cat_7], P.[User_Cat_8], P.[User_Cat_9], P.[UserClient_ID], M.Email, C.Company_Name,
C.Company_Type_ID, U.UserName, ISNULL(SMS.SMSProfiles, 0) SMSProfiles, U.UserID
FROM (SELECT PropertyDescription, UP.UserID, PropertyValue
FROM User_Properties UP JOIN
System_Properties SP ON UP.PropertyID = SP.PropertyID JOIN
aspnet_Membership M ON UP.UserID = M.UserID) X PIVOT (min(PropertyValue) FOR PropertyDescription IN ([Company_ID], [Created_Date], [Created_User],
[Cust_ID], [FirstName], [IPCheck], [JobTitle], [LastLogin], [LastModified_Date], [LastModified_User], [LastName], [Newsletter_OptIn], [Password_Change], [SupAdmin],
[SysAccess], [SysAdmin], [User_Cat_1], [User_Cat_10], [User_Cat_2], [User_Cat_3], [User_Cat_4], [User_Cat_5], [User_Cat_6], [User_Cat_7], [User_Cat_8],
[User_Cat_9], [UserClient_ID])) P JOIN
aspnet_Membership M ON P.UserID = M.UserID JOIN
aspnet_Users U ON P.UserID = U.UserID JOIN
Companies C ON C.Company_ID = P.Company_ID LEFT JOIN
(SELECT UserId, COUNT(Users_SMS_Profile_ID) SMSProfiles
FROM Users_SMS_Profile
GROUP BY UserID) SMS ON SMS.UserID = P.UserID
This now allows me to query the View as if it was a table.
I hope this helps someone else in the future.

Simply said: you cant
At least you can't do it using conventional TSQL programming. Which means you would have to use some hack. Let me explain.
Closest thing to your SP would be UDF. However, UDFs are rather restricted. One thing UDF expect is for data to stay the same while and after executing it. Of course that this means EXEC() is forbidden in that scope.
Another possibility would be a view. However, you have a number of parameters and view's schema depends on these parameters. Functionality to change view's schema based on input parameters doesn't exist in SQL server.
And now for the hacks.
One hack I can think of is:
create a CLR UDF
create new connection based on context connection (same server, same db)
exec your SP there
return result to your original pipe
But it may or may not work (it's a hack after all).
If the hack doesn't work, you can try playing it by the book. This means creating a CLR UDF, assemble select statement in there and execute it, which means that you will have to throw away your original SP. However, it is not a hack since SQL CLR UDF's are made for such (and other) situations. Only thing you will have to take care about is using SqlMetaData because UDF doesn't have a predefined resultset. See this.
In my previous answer I stated that it could be done using CLR UDF, but that was wrong. One thing I forgot is that Microsoft insists on providing a finite number of columns for UDF. This may not be obvious while developing in .NET - after all, you can return any number of columns to SqlPipe. See this (untested) code...
[SqlFunction(DataAccess = DataAccessKind.Read)]
public static void DynOutFunc(SqlString select, SqlString where, SqlString orderBy)
{
// 1: Create SQL query
string query = "select db_id(), db_name()";
// 2: Find out which colums and their types are part of the output
SqlMetaData[] metaData =
{
new SqlMetaData("ID", System.Data.SqlDbType.Int),
new SqlMetaData("Database", System.Data.SqlDbType.NVarChar, 256)
};
using (SqlConnection connection = new SqlConnection("context connection=true"))
{
connection.Open();
SqlCommand command = new SqlCommand(query, connection);
SqlDataReader reader = command.ExecuteReader();
using (reader)
{
while (reader.Read())
{
SqlDataRecord record = new SqlDataRecord(metaData);
SqlContext.Pipe.SendResultsStart(record);
for(int i = 0; i < metaData.Length; i++)
{
if(metaData[i].DbType == DbType.String)
record.SetString(i, reader.GetString(i));
else if(metaData[i].DbType == DbType.Int32)
record.SetInt32(i, reader.GetInt32(i));
// else if's should cover all supported data taypes
}
SqlContext.Pipe.SendResultsRow(record);
}
}
SqlContext.Pipe.SendResultsEnd();
}
}
Notice SqlMetaData collection that holds information about columns. What stops you from appending just another column to it?
But(!) when it comes to registering that function in the SQL Server itself, you HAVE to provide arguments, like:
CREATE FUNCTION DynOutFunc
#select [nvarchar](4000),
#where [nvarchar](4000),
#orderBy [nvarchar](4000)
RETURNS TABLE (p1 type1, p2 type2, ... pN typeN)
AS EXTERNAL NAME SqlClrTest.UserDefinedFunctions.DynOutFunc;
It turns out there are no hacks for this I can think of. Or there are just no hacks here at all.

Related

SQL Server - Select from a variable amount of tables depending on date range

I have a query that joins multiple Data Sources together, I need a query that will select from a variable amount of tables depending on the date range I send it.
Joining Query
SELECT I.SerialNumber as DataSource,Deployed,Removed
FROM InstrumentDeployments ID
INNER JOIN Instruments I On I.Id = ID.InstrumentId
INNER JOIN Points P On P.Id = ID.PointId
WHERE P.Id = 1
ORDER BY Deployed
Joining Query Result
So from the above query result, if I wanted to select all of the historical information, it would go through and get the data from the specific tables
(called DataSource in query above) dependant on the relevant date.
Final Query - Something like this but the variable tables from query result above.
SELECT * FROM (VariableTables) WHERE DateRange BETWEEN '2016-09-07' and '2018-07-28'
Thanks
Please note that this is completely untested as the sample data is an image (and I can't copy and paste text from an image). If this doesn't work, please provide your sample data as text.
Anyway, the only way you'll be able to achieve this is with Dynamic SQL. This also, like in the comments, assumes that every table has the exact same definition. if it doesn't you'll likely get a failure (perhaps a conversion error, or that for a UNION query all tables must have the same number of columns). If they don't, you'll need to explicitly define your columns.
Again, this is untested, however:
DECLARE #SQL nvarchar(MAX);
SET #SQL = STUFF((SELECT NCHAR(10) + N'UNION ALL' + NCHAR(10) +
N'SELECT *' + NCHAR(10) +
N'FROM ' + QUOTENAME(I.SerialNumber) + NCHAR(10) +
N'WHERE DateRange BETWEEN #dStart AND #dEnd'
FROM InstrumentDeployments ID
INNER JOIN Instruments I ON I.Id = ID.InstrumentId
INNER JOIN Points P ON P.Id = ID.PointId
WHERE P.Id = 1
ORDER BY Deployed
FOR XML PATH(N'')),1,11,N'') + N';';
PRINT #SQL; --Your best friend.
DECLARE #Start date, #End date;
SET #Start = '20160907';
SET #End = '20180728';
EXEC sp_executesql #SQL, N'#dStart date, #dEnd date', #dStart = #Start, #dEnd = #End;

passing multiple datatypes into dynamic sql-values not passing in parameterized query [duplicate]

This question already has answers here:
Dynamic SQL Not Converting VARCHAR To INT (shouldn't anyway)
(2 answers)
Closed 4 years ago.
I have a dynamic SQL query inside a stored procedure that works and gives me the correct results. But it is taking too long-because I have to compare as varchar instead of int. I believe #query variable in SQL server requires the statement to be a unicode.
Here is the dynamic sql part
ALTER PROCEDURE [dbo].[sp_GetRows]( #Id varchar(64))
AS
BEGIN
DECLARE #Query nvarchar(4000),
#Comp varchar(256)
SELECT #Comp
= STUFF((
SELECT DISTINCT ',' + char(39)+
tci.Component +char(39)
FROM TCI tci WITH(NOLOCK)
JOIN CDetail cd WITH(NOLOCK)
ON tci.ParentCId = cd.CIdentifier
WHERE tci.ParentCId = #Id
AND cd.ParentBranch IS NULL
FOR XML PATH('')),1,1,'')
SET #Query
= 'WITH CTE AS
(
SELECT '+#Id+' as ParentCId, CIdentifier as ChildCId,
a.Comp as Comp
from dbo.CD cd WITH(NOLOCK)
INNER JOIN
(SELECT DISTINCT ChildCId,Comp
FROM TCI tc WITH(NOLOCK)
WHERE ParentCId = '+ #Id + '
) a
ON cd.CIdentifier= a.ChildCId
);
EXEC (#Query)
END;
Here is the comparison-
SELECT CIdentifier FROM #tempTable temp WITH(NOLOCK)
WHERE temp.CIdentifier < '+#Id+'....
This compares as CIdentifier =1122233 instead of CIdentifier ='1122233' because dynamic SQL is not allowing me to pass it as an int. I keep getting the 'cannot convert varchar to int error'
So I used parameterized query - hoping that would enable me to pass int values.Here is the query part
SET #Query
= N';WITH CTE AS
(
......
(SELECT DISTINCT ChildCId,Comp
FROM TCI tc WITH(NOLOCK)
WHERE ParentCId = #Id
AND ChildCId + tc.Comp
NOT IN
(SELECT ChildId + Comp FROM dbo.TCI WITH(NOLOCK)
WHERE ParentId IN (SELECT CIdentifier FROM #tempTable WITH(NOLOCK)
WHERE temp.CIdentifier < #Idn
AND Comp IN ( #Comp))
)
)
)a
ON cd.CIdentifier= a.ChildId
)
SELECT * FROM CTE;'
EXEC sp_executeSQL #Query,'#Id VARCHAR(64),#Idn INT,#comp VARCHAR(256)',#Id=#Id,#Idn=#Idn,#comp =#comp
This gives me incorrect results and when I saw the execution using a trace - saw that values are not being passed onto the query. How can I get the query to pick up the variables?
Just change WHERE ParentCId = '+ #Id + ' to WHERE ParentCId = '+ cast(#Id as varchar(16)) + ' in the first query. The problem is SQL Server see's + as addition when the value is a numeric type, or date, and concatenation when it isn't. This is where you get the error from. However, when you do this, it will not make SQL Server compare it as a string literal so you don't have to worry about that. You can see this if you use PRINT (#Query) at the end instead of EXEC (#Query)
Note, this needs to be changed at the other locations you have any NUMERIC data type, like in the SELECT portion, SELECT '+ cast(#Id as varchar(16)) +'
Also, you code doesn't show where #Id value comes from, so be cautious of SQL injection here.

SQL Server 2012 - Manipulate string data for stored procedure

Can you give me some pointers (or point in the right direction on what search terms for google)? In a stored procedure I have a parameter #TAG (string). I receive '(14038314,14040071)' (for example) from another application that cannot be altered. In the stored procedure, I need to split apart '(14038314,14040071)' to put quotes around each string value, rebuild it, strip out the outer quotes,strip out the parens and pass it to #TAG in the query below so that it looks like the line commented out below?
SELECT
V.NAME AS VARIETY, TAGID
FROM
mfinv.dbo.onhand h
INNER JOIN
mfinv.dbo.onhand_tags t on h.onhand_id = t.onhand_id
INNER JOIN
mfinv.dbo.onhand_tag_details d on t.onhand_tag_id = d.onhand_tag_id
INNER JOIN
mfinv.dbo.FM_IC_PS_VARIETY V ON V.VARIETYIDX = d.VARIETYIDX
LEFT JOIN
mfinv.dbo.FM_IC_TAG TG ON TG.TAGIDX = t.TAGIDX
WHERE
h.onhand_id = (SELECT onhand_id FROM mfinv.dbo.onhand
WHERE onhand_id = IDENT_CURRENT('mfinv.dbo.onhand'))
AND TG.ID IN (#TAG)
--AND TG.ID IN ('14038314','14040071')
You can Use Dynamic SQL Like This
DECLARE #TAG Nvarchar(MAX)='14038314,14040071'
set #TAG=''''+REPLACE(#TAG,',',''',''')+''''
--select #TAG
DECLARE #SQL NVARCHAR(MAX)=N'
Select V.NAME AS VARIETY, TAGID
FROM mfinv.dbo.onhand h
INNER JOIN mfinv.dbo.onhand_tags t on h.onhand_id = t.onhand_id
INNER JOIN mfinv.dbo.onhand_tag_details d on t.onhand_tag_id = d.onhand_tag_id
INNER JOIN mfinv.dbo.FM_IC_PS_VARIETY V ON V.VARIETYIDX = d.VARIETYIDX
LEFT JOIN mfinv.dbo.FM_IC_TAG TG ON TG.TAGIDX = t.TAGIDX
WHERE h.onhand_id = (SELECT onhand_id FROM mfinv.dbo.onhand
WHERE onhand_id = IDENT_CURRENT (''mfinv.dbo.onhand''))
AND TG.ID IN ('+#TAG+')'
PRINT #SQL
EXEC (#SQL)
Here's what I did. Thank you all for responding. Thanks to dasblinkenlight for answering "How to replace first and last character of column in sql server?". Thanks to SQLMenace for answering "How Do I Split a Delimited String in SQL Server Without Creating a Function?".
Here's how I removed parenthesis:
#Tag nvarchar(256)
SET #Tag = SUBSTRING(#Tag, 2, LEN(#Tag)-2)
Here's how I split and rebuilt #Tag:
AND TG.ID in
(
SELECT SUBSTRING(',' + #Tag + ',', Number + 1,
CHARINDEX(',', ',' + #Tag + ',', Number + 1) - Number -1)AS VALUE
FROM master..spt_values
WHERE Type = 'P'
AND Number <= LEN(',' + #Tag + ',') - 1
AND SUBSTRING(',' + #Tag + ',', Number, 1) = ','
)

How to merge several records in one using COALESCE in MS SQL 2008

I'm guering three tables from the DataBase with the idea to extract information for a certain Client so I get single values from all columns except one.
My tables are :
Client :: (ClientId | ClientName)
Notifications :: (NotificationId | NotificiationText)
ClientsNotifications :: (ClientId | NotificationId)
A single client may have multiple notifications related to him, but I want to get them in a single row so after little research I decied that I should use COALESCE.
I made this query :
SELECT c.ClientName, (COALESCE(n.NotificiationText,'') + n.NotificiationText + ';')
FROM [MyDB].[dbo].[Client] AS c
LEFT JOIN [MyDB].[dbo].[ClientsNotifications] AS cn
ON c.ClientId = cn.ClientId
LEFT JOIN [MyDB].[dbo].[Notifications] AS n
ON c.ClientId = cn.ClientId
AND cn.NotificationId = n.NotificationId
WHERE c.ClientId = 1
For this particular user I have two notifications, the result I get is - two rows, on the first row I have the first notification concatenated for itself (I have two times the same string) on the second row I have the second notification concateneated for itself again.
So There are three things that I want but don't know how to do -
Right now for column name I get (No column name) so I want to give it one
I want the two notifications (or as many as they are) concatenated in a single row
I want to determine some delimeter so when I fetch the records I can perform split. In my example I use this - ';') which I think should act as delimeter but the concatenated strings that I have are not separeted by ; or anything.
You can give your column name an alias in the same way you do for a table, e.g.
SELECT <expression> AS ColumnAlias
However, for reasons detailed here I prefer using:
SELECT ColumnAlias = <expression>
Then to get multiple rows into columns you can use SQL Servers XML extensions to achieve this:
SELECT c.ClientName,
Notifications = STUFF(( SELECT ';' + n.NotificationText
FROM [MyDB].[dbo].[ClientsNotifications] AS cn
INNER JOIN [MyDB].[dbo].[Notifications] AS n
ON n.NotificationId = cn.NotificationId
WHERE c.ClientId = cn.ClientId
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
FROM [MyDB].[dbo].[Client] AS c
WHERE c.ClientId = 1;
Example on SQL Fiddle
An explanation of how this method works can be found in this answer so I shalln't repeat it here.
There's a trick to doing what you want to do. As it's written right now, you're just grabbing stuff off the same row. Also, multiple conditions on the second left join are unnecessary.
DECLARE #clientName VARCHAR(MAX) = '';
DECLARE #text VARCHAR(MAX) = '';
SELECT #clientName = c.ClientName
, #text = (CASE
WHEN n.NotificationText IS NOT NULL THEN #text + ';' + n.NotificationText
ELSE #text
END)
FROM [MyDB].[dbo].[Client] AS c
LEFT JOIN [MyDB].[dbo].[ClientsNotifications] AS cn
ON c.ClientId = cn.ClientId
LEFT JOIN [MyDB].[dbo].[Notifications] AS n
ON cn.NotificationId = n.NotificationId
WHERE c.ClientId = 1
SELECT #clientName AS ClientName, #text AS Notifications

Dynamically generate Sql for variable definition and assignment for a row in a table?

For example, I have a table
create table T (
A int,
B numeric(10,3),
C nvarchar(10),
D datetime,
E varbinary(8)
)
Update: This is just one of the example table. Any table can be used as input for generating the SQL string.
Is there an easy way to dynamically generate the following Sql for a row? (Any built-in function to make the Quotes, prefix easier?)
'declare
#A int = 1,
#B numeric(10,3) = 0.01,
#C nvarchar(10) = N''abcd'',
#D = ''10/1/2013'',
#E = 0x9123'
No, there isn't. The closest you might get, but which still will require manual changes, is by using SQL Server Management Studio. Expand the database and the table.
Right-click the table, then select Script table As, Insert To, and then selecting a new query window. This will generate output that will give you a starting point, but it's not generating variables. You'd have to either script that yourself, or edit the generated INSERT statement.
Example code:
INSERT INTO MyDB].[dbo].[Table1]
([A]
,[B]
,[C]
VALUES
(<A, int,>
,<B, float,>
,<C, nvarchar(10),>
)
GO
Not sure what specifically you are trying to achieve… There is no built in function for something like this but you can try to create one easily using query similar to this one…
select 'DECLARE #A int = ' + TableA.A +
', #B numeric(10,3) = ' + TableA.B +
', #C nvarchar(10) = N''' + TableA.C +
''', #D = ''' + TableA.D + ''''
from TableA
WHERE PrimaryKeyColumn = some_value
Just cleanup the query above and convert it into a function that returns nvarchar
If you want to dynamically generate table definitions too that’s possible too but you’ll have to use system views to create this for any given table.
Try something like this and work your way from here
select T.name, C.name, TY.name, C.column_id
from sys.tables T
inner join sys.columns C on T.object_id = C.object_id
inner join sys.types TY on TY.system_type_id = C.system_type_id
where T.name = 'TableName'
order by C.column_id asc

Resources