Related
I am trying to use a multi-valued parameter in SSRS within a dynamic SQL query. In a static query I would use
SELECT myField
FROM myTable
WHERE myField IN (#myParameter)
Using answers to this question (TSQL Passing MultiValued Reporting Services Parameter into Dynamic SQL) I have tried
-- SSRS requires the output field names to be static
CREATE TABLE #temp
(
myField VARCHAR(100)
)
DECLARE #myQuery VARCHAR(5000) = 'SELECT myField
INTO #temp
FROM myTable
WHERE CHARINDEX('','' + myField + '','', '',''+''' + #myParameter + '''+'','') > 0'
EXEC (#myQuery)
This approach should work if the query understood #myParameter to be a string in a CSV format, but it doesn't seem to (as suggested by the link above). For example
SELECT #myParameter
won't work if there is more than one value selected.
I've also tried moving the parameter into a temporary table:
SELECT myField
INTO #tempParameter
FROM #myParameter
-- SSRS requires the output field names to be static
CREATE TABLE #temp
(
myField VARCHAR(100)
)
DECLARE #myQuery VARCHAR(5000) = 'SELECT myField
INTO #temp
FROM myTable
WHERE myField IN (SELECT myField FROM #tempParameter)'
EXEC (#myQuery)
I have SSRS 2012 and SQL Server 2012. NB: I need to use dynamic SQL for other reasons.
You don't need dynamic SQL for this. SSRS will (much to my dislike) inject multi value parameters when using a hard coded SQL statement in the report. Therefore you can just do something like the following:
SELECT *
FROM MyTable
WHERE MyColumn IN (#MyParameter)
AND OtherCol > 0;
Before running the query, SSRS will remove #MyParameter and inject a delimited list of parameters.
The best guess, if you need to use dynamic SQL, is to use a string splitter and an SP (I use DelimitedSplit8K_LEAD here). SSRS will then pass the value of the parameter (#MultiParam) as a delimited string, and you can then split that in the dynamic statement:
CREATE PROC dbo.YourProc #MultiParam varchar(8000), #TableName sysname AS
BEGIN
DECLARE #SQL nvarchar(MAX);
SET #SQL = N'SELECT * FROM dbo.' + QUOTENAME(#TableName) + N' MT CROSS APPLY dbo.DelimitedSplit8K_LEAD (#MultiParam,'','') DS WHERE MT.MyColumn = DS.item;';
EXEC sp_executesql #SQL, N'#MultiParam varchar(8000)', #MultiParam;
END;
GO
As I mentioned in the comments, your parameter is coming from SSRS as a single comma separated string as such:
#myParameter = 'FirstValue, Second Value Selected, Third Val'
When you try to use the parameter in the IN clause, it is read as such:
select *
from my table
where my column in ('FirstValue, Second Value Selected, Third Val')
This is invalid. The correct syntax would be like below, with quotes around each value.
select *
from my table
where my column in ('FirstValue', 'Second Value Selected', 'Third Val')
So, you need to find a way to quote each value, which is hard because you don't know how many values there will be. So, the best thing to do is split that parameter into a table, and JOIN to it. Since we use a table-valued function in this example, we use CROSS APPLY.
First, create the function that Jeff Moden made, and so many people use. Or, use STRING_SPLIT if you are on 2016 onward, or make your own. However, anything that uses a recursive CTE, WHILE loop, cursor, etc will be far slower than the one below.
CREATE FUNCTION [dbo].[DelimitedSplit8K]
--===== Define I/O parameters
(#pString VARCHAR(8000), #pDelimiter CHAR(1))
--WARNING!!! DO NOT USE MAX DATA-TYPES HERE! IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
-- enough to cover VARCHAR(8000)
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(#pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(#pString,t.N,1) = #pDelimiter
),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(#pDelimiter,#pString,s.N1),0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(#pString, l.N1, l.L1)
FROM cteLen l
;
Then, you simply call that with your function like so:
DB FIDDLE DEMO
create table mytable (Names varchar(64))
insert into mytable values ('Bob'),('Mary'),('Tom'),('Frank')
--this is your parameter from SSRS
declare #var varchar(4000) = 'Bob,Mary,Janice,Scarlett'
select distinct mytable.*
from mytable
cross apply dbo.DelimitedSplit8K(#var,',') spt
where spt.Item = mytable.Names
The split string solution didn't work for me either, and I tried to figure out what type the Multi parameter variable in SSRS actually is so I somehow could work with it. The multi parameter variable was #productIds and the type of the data field was UNIQUEIDENTIFIER. So I wrote the type to a log table
INSERT INTO DebugLog
SELECT CAST(SQL_VARIANT_PROPERTY(#productIds,'BaseType') AS VARCHAR(MAX))
When I selected one value the type was NVARCHAR, however when I selected two or more values I got an error
System.Data.SqlClient.SqlException: The sql_variant_property function requires 2 argument(s).
So I stopped trying to figure out what a multi parameter variable actually was, I already spent to much time.
Since I had the values in a table I could select the values from that table into a temp table and then join on that in the dynamic built query
SELECT p.Id
INTO #tempProdIds
FROM Products p WHERE p.Id IN #productIds
SET #query +='
....
JOIN #tempProdIds tpi on tl.Product = tpi.Id
....
'
EXEC(#query)
honestly, for several days, i am trying to learn about pivot table behavior. rightnow, i am able to display sum of row and column in pivot table. Here is the code that i am trying to set
DECLARE #cols AS NVARCHAR(MAX)
DECLARE #colswithNoNulls AS NVARCHAR(MAX)
DECLARE #query AS NVARCHAR(MAX)
DECLARE #tanggal_awal DATE
DECLARE #tanggal_akhir DATE
DECLARE #print NVARCHAR(MAX)
DECLARE #querycount AS NVARCHAR(MAX)
CREATE TABLE #datatable
(
product_id int,
product_date date,
product_ammount int
)
SET #tanggal_awal = convert(DATE,'02-01-2017')
SET #tanggal_akhir = convert(DATE,DATEADD(dd,-1,(DATEADD(mm,1,#tanggal_awal))))
--SELECT DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,GETDATE())+1,0))
INSERT INTO #datatable (product_id,product_date,product_ammount) VALUES
(1,GETDATE(),100),
(1,GETDATE(),900),
(2,DATEADD(DD,-1,GETDATE()),400),
(3,DATEADD(DD,4,GETDATE()),300),
(1,DATEADD(DD,4,GETDATE()),200),
(2,DATEADD(DD,2,GETDATE()),700),
(4,DATEADD(DD,-3,GETDATE()),1000),
(4,DATEADD(MM,1,GETDATE()),200),
(4,GETDATE(),750)
;WITH CTE (datelist,maxdate) AS
(
SELECT CONVERT(INT,(MIN(DATEPART(day,#tanggal_awal)))) datelist, CONVERT(INT,MAX(DATEPART(day,product_date))) maxdate
FROM #datatable
UNION ALL
SELECT CONVERT(INT,(DATEPART(day,datelist))), CONVERT(INT,(DATEPART(day,#tanggal_akhir)))
FROM cte
WHERE datelist < maxdate
) SELECT c.datelist
INTO #temp
FROM cte c
ORDER BY c.datelist
OPTION (maxrecursion 0)
SELECT #cols = STUFF((SELECT ',' + QUOTENAME(CONVERT(int, datelist))
FROM #temp
GROUP BY datelist
ORDER BY CONVERT(int, datelist)
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,''
)
SELECT #colswithNoNulls = STUFF((SELECT ',ISNULL(' + QUOTENAME(CONVERT(int, datelist)) +',''0'') '+ QUOTENAME(CONVERT(int, datelist))
FROM #temp
GROUP BY datelist
ORDER BY CONVERT(int, datelist)
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
SET #query =
'SELECT product_id, '+ #colswithNoNulls+', Total FROM
(
select
ISNULL((CAST(b.product_id as nvarchar(30))), ''Total'') product_id,
coalesce(b.product_ammount,0) as product_ammount,
DATEPART(dd,(convert(CHAR(10), product_date, 120))) PivotDate,
SUM(product_ammount) over (partition by b.product_id) as Total
FROM #datatable b
WHERE product_date between #tanggal_awal and #tanggal_akhir
GROUP BY product_ammount,product_date,product_id
WITH ROllup
) x
pivot
(
sum(product_ammount)
for PivotDate in (' +#cols+ ')
) p
ORDER BY CASE when (product_id = ''Total'') then 1 else 0 end, product_id'
EXECUTE sp_executesql #query ,N'#tanggal_awal DATE, #tanggal_akhir DATE', #tanggal_awal,#tanggal_akhir
IF(OBJECT_ID('tempdb.dbo.#temp','U') IS NOT NULL)
BEGIN
TRUNCATE TABLE #temp
TRUNCATE TABLE #datatable
DROP TABLE #temp
DROP TABLE #datatable
END
ELSE
BEGIN
SELECT '#temp is not created in this script' AS MESSAGE
END
as you can see , the result is show on the display. However, the total value at the very right bottom is strange since it is like doubled up exact total value like in this picture:
How to resolve this issue btw? since it was bit confusing for me. thank you for your help :)
Generally, I am not fully aware for RollUp functionality. From your PIVOT query. I have found some of the empty rows is coming up (basically subtotal rows from "With Rollup" option), so I have modified the Group by statement a little bit to achieve the expected result.
select
ISNULL((CAST(b.product_id as nvarchar(30))), 'Total') product_id,
coalesce(b.product_ammount,0) as product_ammount,
DATEPART(dd,(convert(CHAR(10), product_date, 120))) PivotDate,
SUM(product_ammount) over (partition by b.product_id) as Total
FROM #datatable b
WHERE product_date between #tanggal_awal and #tanggal_akhir
GROUP BY product_ammount,product_date,ROllup(product_id)
Kindly replace this query in PIVOT, then you will get the desired output.
Note: Sorry I am not fully aware of RollUp functionality, so I'm unable to give the right explanation.
I have the following TSQL that I have been trying, without luck, to do 2 things with:
convert Nulls to 0
output to a temporary table I can use for
other operations.
Part of the result is captured in the attached:
What changes do I need to make to both capture the result into a temp table with dynamic columns, and eliminate the nulls?
Thanks.
DECLARE #cols NVARCHAR(MAX), #stmt NVARCHAR(MAX), #ParmDefinition NVARCHAR(MAX), #PVT3 NVARCHAR(MAX);
--Get distinct values of the PIVOT Column
SELECT #cols = ISNULL(#cols+', ', '')+'['+T.Cat+']'
FROM
(
SELECT TOP 100 CONVERT( VARCHAR(2), IncomeCategoryID) AS Cat
FROM Payroll.lu_IncomeCategory
WHERE IsAllowance <> 0
AND IncomeCategoryID IN
(
SELECT DISTINCT
IncomeCategoryID
FROM Payroll.Income
WHERE IncomeCategoryID <> 0
)
ORDER BY IncomeCategoryID
) AS T;
SELECT #stmt = N'
SELECT *
FROM (SELECT EmployeeID,IncomeCategoryID,
SUM(RateAmount) [Amount]
FROM Payroll.Income
GROUP BY EmployeeID, IncomeCategoryID ) as PayData
pivot
(
SUM([Amount])
FOR IncomeCategoryID IN ('+#cols+')
) as IncomePivot';
EXEC sp_executesql
#stmt = #stmt;
Done.
For the first challenge, it worked after using the #NullToZeroCols example in one of the previous posts on this forum. For my second challenge, I eventually Selected Into a Non-Temporary table which I can search for and drop each time I execute my TSQL command. I learnt the scope of the previously created temporary table could not allow me to use it after the PIVOT command was executed.
I've applied a "borrowed" solution to splitting a delimited string into rows (I'm working in MSSQL 2008 R2), but the solution was specific to CTE/recursive queries. This works great, but only transforms one row of data. How would I adjust this to return all rows in my table (or, better yet, be able to include a where clause)?
I have struck out on researching this for two days, and as I'm not too experienced with CTE/recursive queries or XML...any expertise would be welcome! Thanks!!
DECLARE #RowData varchar(2000)
DECLARE #SplitOn varchar(1)
DECLARE #ObjectID int
SELECT
#ObjectID = ObjectID, #RowData = ObjectName, #SplitOn = ';' from Objects
declare #xml as xml
SET #XML = '<t><r>' + Replace(#RowData , #spliton, '</r><r>') + '</r></t>'
select #objectid as objectid, rtrim(ltrim(t.r.value('.', 'VARCHAR(8000)'))) as splitvalue
from #xml.nodes('/t/r') as t(r)
I would start by creating a table valued function that does the string splitting like the one found here: http://ole.michelsen.dk/blog/split-string-to-table-using-transact-sql.html
Then use an OUTER APPLY to use these against rows in a table. It is a query, so you can apply a where clause. Here is an example of the create for the function, a temp table that I filled with some test data and the select statement.
CREATE FUNCTION [dbo].[Split]
(
#String NVARCHAR(4000),
#Delimiter NCHAR(1)
)
RETURNS TABLE
AS
RETURN
(
WITH Split(stpos,endpos)
AS(
SELECT 0 AS stpos, CHARINDEX(#Delimiter,#String) AS endpos
UNION ALL
SELECT endpos+1, CHARINDEX(#Delimiter,#String,endpos+1)
FROM Split
WHERE endpos > 0
)
SELECT 'Id' = ROW_NUMBER() OVER (ORDER BY (SELECT 1)),
'Data' = SUBSTRING(#String,stpos,COALESCE(NULLIF(endpos,0),LEN(#String)+1)-stpos)
FROM Split
)
GO
IF ( OBJECT_ID(N'tmpdb..#Object') IS NOT NULL ) DROP TABLE [#Object];
CREATE TABLE [#Object]
(
[ObjectId] INT NOT NULL IDENTITY(1, 1)
, [Object] VARCHAR(1000) NOT NULL
);
INSERT INTO #Object
( [Object] )
VALUES ( 'brad;bill;jerry'), ('Scott;MATT;DEAN'), ('larry;bob;john')
GO
SELECT [tt].[ObjectId]
, [s].[Data]
FROM #Object AS [tt]
OUTER APPLY dbo.Split([tt].[Object], ';') AS s
WHERE tt.[ObjectId] < 3
Just replace the temp table stuff with your table(s). Hope that helps!
The reason why you are not getting more than one row is because you are thinking about sets in a programatic way instead of in a set-centric fashion.
SELECT #ObjectID .... gets one row ... the last row.
I suggest you use CROSS APPLY:
-- Data Setup
CREATE TABLE #Objects ( ObjectID INTEGER, ObjectName VARCHAR(2000) )
INSERT INTO #Objects ( ObjectID, ObjectName )
SELECT 1, 'foo;bar' UNION
SELECT 2, 'biz;baz;buz'
-- The Meat and Potatoes. You can add a WHERE to your inner or outer query
SELECT
ObjectID,
RTRIM( LTRIM( t.r.value( '.', 'VARCHAR(8000)' ) ) ) AS splitvalue
FROM
(
SELECT
ObjectID,
CONVERT( XML, '<t><r>' + REPLACE( ObjectName, ';', '</r><r>' ) + '</r></t>' ) AS xml_part
FROM
#Objects
) AS xml_part_builder
CROSS APPLY xml_part_builder.xml_part.nodes('/t/r') AS t(r)
-- CLEAN UP
DROP TABLE #Objects
Let me know if this helps.
Cheers!
I have a static pivot query that tracks historical pricing (see below); it is union all of one table of current data, one table of historical data, and one related table.
I would like to convert it to a dynamic SQL pivot query so that I don't manually have to update the column heading dates all of the time, but I am having problems doing so. The data comes out of an accounting system (Microsoft Dynamics) running on SQL Server, and due to system limitations, I am unable to use ansi nulls/ paddings/ warnings and also unable to use quoted identifiers. Is there another way to create a dynamic sql pivot query?
Here is my current query. I want to replace the month-end dates with dynamic sql. I have view access to the data only.
SELECT ACTDESCR AS "ACCT NAME", concat(right(actnumbr_2,2), actnumbr_3) as PLU, [3/31/2015], [2/28/2015], [1/31/2015], [12/31/2015], [11/30/2014], [10/31/2014], [9/30/2014], [8/31/2014], [7/31/2014], [6/30/2014], [5/31/2014], [4/30/2014], [3/31/2014], [2/28/2014], [1/31/2014]
FROM
(SELECT A.ACTDESCR, EOMONTH(DATEFROMPARTS(B.YEAR1,B.PERIODID,1),0) AS DATE1, A.actnumbr_2, a.actnumbr_3, B.PERDBLNC, A.ACTNUMBR_1
FROM TEST.dbo.table1 AS A
LEFT OUTER JOIN TEST.dbo.TABLE2 AS b
ON (a.ACTINDX = b.ACTINDX)
UNION ALL
SELECT A.ACTDESCR, EOMONTH(DATEFROMPARTS(C.YEAR1,C.PERIODID,1),0) AS DATE1, A.actnumbr_2, a.actnumbr_3, C.PERDBLNC, A.ACTNUMBR_1
FROM TEST.dbo.TABLE1 AS A
LEFT OUTER JOIN TEST.dbo.TABLE3 AS C
ON (a.ACTINDX = C.ACTINDX)
) AS ST
PIVOT
(
SUM(PERDBLNC)
FOR DATE1 IN ([3/31/2015], [2/28/2015], [1/31/2015], [12/31/2015], [11/30/2014], [10/31/2014], [9/30/2014], [8/31/2014], [7/31/2014], [6/30/2014], [5/31/2014], [4/30/2014], [3/31/2014], [2/28/2014], [1/31/2014])
) AS PVT
WHERE ACTNUMBR_1 = ?
You can use T-SQL with dynamic SQL for this:
--VARIABLE TO HOLD DATES--
DECLARE #DATES NVARCHAR(500)
--VARIABLE TO HOLD CODE--
DECLARE #SQL NVARCHAR(MAX)
--TEMP TABLE TO FIND DISTINCT DATES--
CREATE #DATES (COLUMNVALS NVARCHAR(10))
--INSERT DISTINCT DATES INTO TEMP TABLE--
INSERT INTO #DATES
SELECT DISTINCT DATE1 FROM (
SELECT EOMONTH(DATEFROMPARTS(YEAR1,PERIODID,1),0) AS DATE1
FROM TEST.DBO.TABLE2
UNION ALL
SELECT EOMONTH(DATEFROMPARTS(YEAR1,PERIODID,1),0) AS DATE1
FROM TEST.DBO.TABLE3)
--CONCAT DATES INTO SELECT LIST--
SET #DATES = COALESCE(#DATES+', ','') + '[' + DATE1 + ']' FROM #DATES
--CREATE THE SELECT STATEMENT--
SELECT #SQL = '
;WITH ST AS (
SELECT A.ACTDESCR, EOMONTH(DATEFROMPARTS(B.YEAR1,B.PERIODID,1),0) AS DATE1, A.ACTNUMBR_2, A.ACTNUMBR_3, B.PERDBLNC, A.ACTNUMBR_1
FROM TEST.DBO.TABLE1 AS A
LEFT OUTER JOIN TEST.DBO.TABLE2 AS B
ON A.ACTINDX = B.ACTINDX
UNION ALL
SELECT A.ACTDESCR, EOMONTH(DATEFROMPARTS(C.YEAR1,C.PERIODID,1),0) AS DATE1, A.ACTNUMBR_2, A.ACTNUMBR_3, C.PERDBLNC, A.ACTNUMBR_1
FROM TEST.DBO.TABLE1 AS A
LEFT OUTER JOIN TEST.DBO.TABLE3 AS C
ON A.ACTINDX = C.ACTINDX)
SELECT ACTDESCR AS "ACCT NAME", CONCAT(RIGHT(ACTNUMBR_2,2), ACTNUMBR_3) AS PLU, '+#DATES+'
FROM ST
PIVOT
(
SUM(PERDBLNC)
FOR DATE1 IN ('+#DATES+')
) AS PVT
WHERE ACTNUMBR_1 = ?'
--PRINT IT TO SEE WHAT IT'S DONE--
PRINT #SQL
--EXECUTE IT--
EXEC (#SQL)