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.
I have this situation in a stored Procedure:
SET #DATE_RELEASE_START = '2015-01-01';
SET #DATE_RELEASE_END = '2015-05-31'
SELECT #statement = ' SELECT *
FROM (SELECT AFCDENTE, M.ID_MODIFICATION_CODE, COUNT(*) AS Conteggio--, CAST((COUNT(*) * 100/ 15032) AS decimal(10,7)) AS Percentage
FROM CIC_LOG_MODIFICHE AS L INNER JOIN ADM_MODIFICATION_CODE AS M ON L.CD_MODIFICATION_CODE = M.CD_MODIFICATION_CODE
INNER JOIN CIC_PRODUZIONE AS P ON P.CD_CIC_PRODUZIONE = L.CD_CIC_PRODUZIONE
WHERE AFDTMODI BETWEEN '+#DATE_RELEASE_START+' AND '+#DATE_RELEASE_END+' AND P.CD_PLANT = '+#CD_PLANT+' AND AFCDENTE IS NOT NULL
GROUP BY AFCDENTE, M.ID_MODIFICATION_CODE) as tbl
PIVOT (SUM(tbl.Conteggio) for tbl.ID_MODIFICATION_CODE in (' + #columns + ')) as pvt'
I get this error:
Conversion failed when converting date and/or time from character
string.
I tried casting those dates but no changes. I get the same error.
What should I do?
I'd suggest doing it like that:
SET #DATE_RELEASE_START = '2015-01-01';
SET #DATE_RELEASE_END = '2015-05-31'
SELECT #statement = ' SELECT *
FROM (SELECT AFCDENTE, M.ID_MODIFICATION_CODE, COUNT(*) AS Conteggio--, CAST((COUNT(*) * 100/ 15032) AS decimal(10,7)) AS Percentage
FROM CIC_LOG_MODIFICHE AS L INNER JOIN ADM_MODIFICATION_CODE AS M ON L.CD_MODIFICATION_CODE = M.CD_MODIFICATION_CODE
INNER JOIN CIC_PRODUZIONE AS P ON P.CD_CIC_PRODUZIONE = L.CD_CIC_PRODUZIONE
WHERE AFDTMODI BETWEEN #p0 AND #p1 AND P.CD_PLANT = #p2 AND AFCDENTE IS NOT NULL
GROUP BY AFCDENTE, M.ID_MODIFICATION_CODE) as tbl
PIVOT (SUM(tbl.Conteggio) for tbl.ID_MODIFICATION_CODE in (' + #columns + ')) as pvt'
EXECUTE sp_executesql #statement, N'#p0 DATETIME2, #p1 DATETIME2, #p0 NVARCHAR(1000)', #p0 = #DATE_RELEASE_START, #p1 = #DATE_RELEASE_END, #p2 = #CD_PLANT;
Instead casting them as VARCHAR, just pass them as variables and then use sp_executesql to do the right job.
You must quote the date strings in your SQL code properly:
WHERE AFDTMODI BETWEEN '''+#DATE_RELEASE_START+''' AND '''+#DATE_RELEASE_END+'''
Otherwise the SQL will read
WHERE AFDTMODI BETWEEN 2015-01-01 AND 2015-05-31
instead of
WHERE AFDTMODI BETWEEN '2015-01-01' AND '2015-05-31'
We've also had problems before with passing date/time values to stored procedures in Management Studio. We needed to give the date as 20150101 or 20150531.
[Edit] Due to time constraints I gave up on using a CTE and created a function that returns the concatenated string:
CREATE FUNCTION fn_GetCategoryNamesAsString
(
#lawID INT
)
RETURNS NVARCHAR(MAX)
AS
BEGIN
DECLARE #categoryNames NVARCHAR(MAX)
SET #categoryNames = ''
DECLARE #categoryID INT
DECLARE CUR CURSOR LOCAL FORWARD_ONLY READ_ONLY FOR
SELECT t1.LawCategoryID FROM [GWS].[dbo].[GWSMasterLawsLawCategories] t1 WHERE t1.LawID = #lawID
OPEN CUR
FETCH FROM CUR INTO #categoryID
WHILE ##FETCH_STATUS = 0
BEGIN
SET #categoryNames = #categoryNames + (SELECT t2.Name
FROM [GWS].[dbo].GWSMasterLawCategories t2
WHERE t2.LawCategoryID = #categoryID) + ', '
FETCH NEXT FROM CUR INTO #categoryID
END
CLOSE CUR
DEALLOCATE CUR
RETURN #categoryNames
END
GO
This does the job but I don't really like it. If anyone has a better solution I'd love to know.
[End edit]
I have seen several questions the deal roughly with the same topic but none cover the inclusion of null values.
I am writing a query that should return the full contents of one table with a couple of columns added with relevant data from other tables. These columns can include 0 - n values.
Null values need to be stored as an empty string and sets that do have the extra data should display it separated by commas.
Some approaches delivered all the names strung together, some only returned the values separately, some no values at all and, most often, the recursion went to deep (which means I fouled up as the dataset is small).
This is my current approach:
DECLARE #categoryNames NVARCHAR(MAX);
SET #categoryNames = '';
WITH sources (sourcesLawSourceID, sourcesName) AS (
SELECT DISTINCT [LawSourceID], [name]
FROM [GWS].[dbo].[GWSMasterLawSources]
),
categories AS(
SELECT GWSCategories.LawCategoryID AS categoryID, GWSLawCategories.LawID AS lawID,
categoryNames = #categoryNames
--CAST(LEFT( GWSCategories.name, CHARINDEX(',', GWSCategories.name + ',') -1) AS NVARCHAR(MAX)) categoryName,
--STUFF(GWSCategories.name, 1, CHARINDEX(',', GWSCategories.name + ','), '') categoryNames
FROM [GWS].[dbo].[GWSMasterLawCategories] GWSCategories
JOIN [GWS].[dbo].[GWSMasterLawsLawCategories] GWSLawCategories
ON GWSCategories.LawCategoryID = GWSLawCategories.LawCategoryID
UNION ALL
SELECT categories.categoryID, categories.lawID,
CAST(LEFT( #categoryNames, CHARINDEX(',', #categoryNames + ',') -1) AS NVARCHAR(MAX)) + GWSCategories.Name
FROM categories
JOIN [GWS].[dbo].[GWSMasterLawCategories] GWSCategories
ON categories.categoryID = GWSCategories.LawCategoryID
WHERE #categoryNames > ''
)
SELECT DISTINCT GWSMaster.[LawID]
,[Name]
,sources.sourcesName LawSourceName
,(SELECT STUFF((SELECT DISTINCT ', ' + RTRIM(LTRIM(categories.CategoryNames))
FROM categories
FOR XML PATH ('')), 1, 1, '')) Categories
FROM [GWS].[dbo].[GWSMasterLaws] GWSMaster
JOIN sources
ON sources.sourcesLawSourceID = GWSMaster.LawSourceID
JOIN categories
ON categories.lawID = GWSMaster.LawID
This leaves the category name field completely empty.
If I can give any more information or I have missed a question that answers my problem please let me know.
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.
Here the code from my function:
-- Get Country Names
DECLARE getCountriesName CURSOR FOR
SELECT c.Name
FROM VocabCountry c
RIGHT OUTER JOIN ProjectCountryRelations cr
ON cr.CountryId = c.Id
WHERE
c.Id = #project_id;
-- Finally Create list to return
OPEN getCountriesName;
FETCH NEXT FROM getCountriesName INTO #Name;
WHILE ##FETCH_STATUS = 0
BEGIN
SET #listOfItems = #listOfItems + #Name + ', '
FETCH NEXT FROM getCountriesName INTO #Name;
END;
CLOSE getCountriesName;
DEALLOCATE getCountriesName;
I am expecting a list of comma separated values, for example, like so:
Canada, United States of America,
I verified that the SELECT returns the countries expected.
Thanks for any help!
Eric
If you're on SQL Server 2008 or newer, you could easily do this with a single SELECT and some FOR XML PATH magic :-)
SELECT
STUFF(
(SELECT ',' + c.Name
FROM VocabCountry c
RIGHT OUTER JOIN ProjectCountryRelations cr
ON cr.CountryId = c.Id
FOR XML PATH('')
), 1, 1, '')
That should return a comma-separated list of those country names for you. No ugly and awful cursor needed!
Using the FOR XML PATH will concat the values with commas.
Using the STUFF Function will remove the first comma you don't want.
So use this:
SELECT
STUFF(
(SELECT ',' + c.Name
FROM VocabCountry c
RIGHT OUTER JOIN ProjectCountryRelations cr
ON cr.CountryId = c.Id
WHERE cr.Id = #project_id
FOR XML PATH('')
), 1, 1, '');
See this fiddle: http://sqlfiddle.com/#!3/fd648/21