Creating a Table Valued Function with dynamic columns - sql-server

I received help previously on creating a query for listing total sales by month for a list of jobs. However, I cannot put this into a view as a variable needs declared.
I thought I could use a Table Valued Function (which I've never used before) but apparently I need to define the columns of the table which I cannot do if they are to be dynamic (the columns are yyyy-mm).
Can someone suggest how I could approach this problem please?
I am not familiar with creating anything other than views so this is completely new to me. Obviously if using the TVF isn't the best method please advise.
The query:
Declare #SQL varchar(max) = Stuff((Select Distinct ',' + QuoteName(YearMonth) From v_JobSalesByMonth Order by 1 For XML Path('')),1,1,'')
Select #SQL = '
Select [JobID], [TotalJobSales],' + #SQL + '
From (
Select JobID
,TotalJobSales = sum(SalesForMonth) over (Partition By JobID)
,YearMonth
,SalesForMonth
From v_JobSalesByMonth A) A
Pivot (sum(SalesForMonth) For [YearMonth] in (' + #SQL + ') ) p'
Exec(#SQL);
I started to create the function but am confused about how to define the dynamic columns.
CREATE FUNCTION db.tvfnJobSalesByMonthPivot (#JobID INT)
RETURNS #JobSalesByMonth TABLE
(
JobID int PRIMARY KEY NOT NULL,
TotalJobSales decimal(6,2) NULL,
2016-12 (ermmmmm?)
)
Many thanks in advance.
Paul

Related

Dynamically PIVOT on ONE AND ONLY ONE column from a single TABLE

I've read multiple threads on this topic, and there does seem to be a way to do this dynamically, but I'm getting a syntax/compile error on the code.
I am trying to dynamically pull multiple rows for a temp table that simply has one column. The definition of the temp table is one field called acct_name. The challenge seems to be the "dynamics" of the fact that there will be over 50+ rows in the temp table, which is subject to change at any time.
I followed a previous poster's example, but am still getting compile/runtime errors.
I'm a rather novice SQL person, so you'll have to excuse the crudity of my code or my question.
DECLARE #idList varchar(500)
SELECT #idList = COALESCE(#idList + ',', '') + acct_name
FROM ##tmp_accts
DECLARE #sqlToRun varchar(1000)
SET #sqlToRun = '
SELECT *
FROM (
SELECT acct_name FROM ##tmp_accts
) AS src
PIVOT (
MAX(acct_name) FOR acct_name IN ('+ #idList +')
) AS pvt'
EXEC (#sqlToRun)
Does anyone have an obvious suggestion, I think it's very close to working.....
FOR EXAMPLE,
Let's say for sake of example we have the following acct_names - '12345','23456','34567','45678'.
The desire result is to return one row with 4 columns each with the respective value of acct_name. HOWEVER, the acct name is dynamic and is not known in advance, nor is the count of acct_name known in advance. A temp table is generated on the fly which determines all of the relevant acct_names for that particular run. It will vary with each run, each day that the query is run.
Thank you.....
Thru an article available thru Microsoft, the following solution does the job apporpriately.....
DECLARE
#columns NVARCHAR(MAX) = '',
#sql NVARCHAR(MAX) = '';
-- select the category names
SELECT
#columns+=QUOTENAME(acct_name) + ','
FROM
##tmp_accts
GROUP BY
acct_name;
-- remove the last comma
SET #columns = LEFT(#columns, LEN(#columns) - 1);
-- construct dynamic SQL
SET #sql ='
SELECT * FROM
(
SELECT DISTINCT acct_name
FROM
##tmp_accts
) t
PIVOT(
COUNT(acct_name)
FOR acct_name IN ('+ #columns + ')
) AS piv;';
-- execute the dynamic SQL
EXECUTE sp_executesql #sql;

How can I create a SQL table variable using records in another table as column names

I've got the following SQL to create a table:
declare #FirstColumnSPResultTable table
(
[HotelId] int,
[HotelName] nvarchar(256),
[2016 - Period 1] decimal(10,1),
[2016 - Period 2] decimal(10,1),
[2016 - Period 3] decimal(10,1),
[2016 - Period 4] decimal(10,1),
[Overall] decimal(10,1),
[#Jobs] int,
[Rank] int,
[OverallRow] bit
)
Where I have the [2016 - Period 1] to [2016 - Period 4] columns I would like to generate these based on a comma separated list of Period IDs that are passed into the Stored Procedure.
The PeriodIDs link to a Period table which contains ID and PeriodName.
I thought about creating the table with only the HotelId and HotelName then looping through the periods and creating each column using a WHILE loop and ALTER TABLE statement, somehow using it's name retrieved in a SELECT statement, then adding the last 4 columns to the table.
I think it is possible to do it this way but can anyone tell me a better way?
EDIT:
Full Solution using dynamic sql as suggested by #ADyson:
DECLARE #Sql nvarchar(max) = '
DECLARE #FirstColumnSPResultTable table ([HotelId] int,
[HotelName] nvarchar(256),'
DECLARE #FirstPeriodIdCommaDelimListAsColumns nvarchar(max)
SET #FirstPeriodIdCommaDelimListAsColumns = STUFF
(
(
SELECT ',' + QUOTENAME(p.[PeriodName]) + ' decimal(10,1) '
FROM [dbo].[Period] p WITH(NOLOCK)
WHERE p.PeriodId IN (SELECT Value FROM dbo.fn_Split(',', #FirstPeriodIdCommaDelimListInt))
ORDER BY p.[Year], p.[PeriodName]
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'),1,1,''
) + ','
SET #Sql = #Sql + #FirstPeriodIdCommaDelimListAsColumns + '
[Overall] decimal(10,1),
[#Jobs] int,
[Rank] int,
[OverallRow] bit)'
so I create the first part of the table, then use FirstPeriodIdCommaDelimListAsColumns to store the next columns created from the periods as required. Then I finish off the table.
If you want to create a new table from the values in the other table, then you can use dynamic SQL to do this using the field names as variables, and build up a string to execute using the sp_executesql stored procedure.
What you're looking for is a pivot table. You can SELECT HotelId and HotelName (or whatever your key is) and PIVOT on PeriodID.

Dynamic Pivot with varying columns

I have a POA Code dynamic pivot that pulls data from a DX temp table and inserts the data into a temp POA table.
The issue I'm having is that there is a possibility of up to 35 different columns that can be returned. Depending on the month there could be 15 columns (POA1...POA15) or there could be all 35 columns (POA1...POA35). I join this dynamic pivot temp table on another patient table. My problem is, I need to show all 35 columns even if some of the columns do not exist in the temp POA table.
--Pivot DX POA Codes
DECLARE #POANAME VARCHAR(40)
SELECT #POAName = '##tmpPOA'
DECLARE #colsPOA NVARCHAR(2000)
SELECT #colsPOA = STUFF((SELECT DISTINCT TOP 100 PERCENT
'],[' + 'POA' + CAST(Dx.RowNum AS NVARCHAR)
FROM #tmpDX DX
ORDER BY '],[' + 'POA' + CAST(Dx.RowNum AS NVARCHAR)
FOR XML PATH ('')
),1,2,'') + ']'
DECLARE #queryPOA NVARCHAR(4000)
SET #queryPOA = 'N
SELECT
EncObjID,
'+
#colsPOA
+' INTO ' + POAName + '
FROM
(SELECT
Dx.EncObjID
,''POA'' + Dx.RowNum AS RowNum
,Dx.POAMne
FROM #tmpDx Dx
) p
PIVOT
(
MIN([POAMne])
FOR RowNum IN
( ' + #colsPOA + ' )
) AS pvt'
EXECUTE(#queryPOA)
I'm receiving an Invalid Column Name in my patient query because some of the columns don't exist in ##tmpPOA. I thought about creating a temp table called #tmpDxPOA and doing an insert (Insert Into #tmpDxPOA select * from ##tmpPOA), but that doesn't work (I receive a Column Name or number of supplied values does not match error).
Any thoughts on how to create all 35 columns even if there isn't any data? I don't care if they're null, I just need to have those place holders in the main patient query and it doesn't help that the number of columns returned varies every month.
With the help of #mxix I was able to come up with the following:
DECLARE #POASQL NVARCHAR(MAX)
SET #POASQL = N'INSERT INTO #tmpPOAFinal (EncObjID,'+#colsPOA+') SELECT * FROM ##tmpPOA'
EXECUTE(#POASQL)
I put this after the EXECUTE(#queryPOA) in my main query.
In order for this to work with Dynamic SQL the rows/colums need to exists more than zero times. Whether it be for one or more patient. I would try to fan out the number of POA possibilities right off the bat and then left outer join to get the actual values back.
IF OBJECT_ID('tempdb..#tmpPOA') IS NOT NULL DROP TABLE #tmpPOA
CREATE TABLE #tmpPOA (POA varchar(10))
IF OBJECT_ID('tempdb..#tmpPatient') IS NOT NULL DROP TABLE #tmpPatient
CREATE TABLE #tmpPatient (Patient varchar(15))
INSERT INTO #tmpPatient VALUES ('ABC123'),('ABC456'),('ABC789')
DECLARE #POAFlag as INT = 0
WHILE #POAFlag <36
BEGIN
INSERT INTO #tmpPOA
VALUES('POA' +CONVERT(varchar,#POAFlag))
SET #POAFlag = #POAFlag + 1
END
SELECT * FROM #tmpPOA
CROSS JOIN #tmpPatient
This should fan out all of the possibilities of the 35DXCodes for you to get their POA flag.

SQL SERVER Replacing the null value in dynamic PIVOT

Good day/night to all.
I'm new in stored procedure, i have lack of experience and understanding when it comes to stored procedure. I tried the other tutorial and answers but i don't know why my query wasnt working when using isnull(payment,'0') or coalesce(payment,'0').
declare #sql as nvarchar(max) = '[2013-04-01],[2013-04-02],[2013-04-03],[2013-04-04],[2013-04-05],[2013-04-06]';
declare #name as nvarchar(max) = 'Derichel'
set #sql =
'SELECT pid, [Fullname], ' + #sql + '
FROM
(SELECT pid, [Fullname], payment, dateregistration
from tbl_Personal
) AS s
PIVOT
(
min(payment)
FOR dateregistration IN (' + #sql + ')
) AS pvt
where [Fullname] = ''' + #name + '''
order by pid'
execute sp_executesql #sql;
Some answer and tutorials have fixed column inside IN ().
My #sql has been set to different date(it depends on user input from gui).
How can i replace the null value to 0?
the output of above code is
pid Fullname [2013-04-01] [2013-04-02] [2013-04-03] [2013-04-04] [2013-04-05] [2013-04-06]
6 Derichel NULL NULL NULL NULL NULL 0
i want to replace the null to 0.
You are getting the NULL values because there are no rows for the dates. When you try to include the ISNULL function on the root query (SELECT ... FROM tbl_Personal), there is nothing to modify (the row doesn't exist).
The NULL values appear as a result of the PIVOT operation, so you need to apply the ISNULL after the data is pivoted. Another way to look at it is to apply the ISNULL to the definition of the final results, which is the first SELECT clause.
Here's the SQL statement without the formatting for a dynamic pivot query.
SELECT pid, [Fullname],
ISNULL([2013-04-01], 0) AS [2013-04-01],
ISNULL([2013-04-02], 0) AS [2013-04-02],
ISNULL([2013-04-03], 0) AS [2013-04-03],
ISNULL([2013-04-04], 0) AS [2013-04-04],
ISNULL([2013-04-05], 0) AS [2013-04-05],
ISNULL([2013-04-06], 0) AS [2013-04-06]
FROM
(SELECT pid, [Fullname], payment, dateregistration
from tbl_Personal
) AS s
PIVOT
(
min(payment)
FOR dateregistration IN ([2013-04-01],[2013-04-02],[2013-04-03],[2013-04-04],[2013-04-05],[2013-04-06])
) AS pvt
where [Fullname] = 'Derichel'
order by pid
For the dynamic query, you won't be able to use the #SQL variable in both places that you use it now. The first instance would contain the ISNULL function calls, which are not allowed in the second instance (FOR dateregistration IN...).

Execute multiple dynamic T-SQL statements and obtain a limited number of unique values while preserving order

I have a SourceTable and a table variable #TQueries containing various T-SQL predicates that target SourceTable.
The expected result is to dynamically generate SELECT statements that return a list of Id's as specified by the predicates in #TQueries. Each dynamically generated SELECT statement also needs to execute in a particular order, and the final set of values needs to be unique and the ordering must be preserved.
Fortunately, there's a limit to how many values need to be retrieved and how many dynamic queries need to be generated. The Id list should contain at most 10 Ids, and we don't expect more than 7 queries.
The following is a sample of this setup, not the actual data/database:
-- Set up some test data, this is quick and dirty just to provide some data to test against
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[SourceTable]') AND type in (N'U'))
BEGIN
-- Create a numbers table, sorta
SELECT TOP 20
IDENTITY(INT,1,1) AS Id,
ABS(CHECKSUM(NewId())) % 100 AS [SomeValue]
INTO [SourceTable]
FROM sysobjects a
END
DECLARE #TQueries TABLE (
[Ordinal] INT,
[WherePredicate] NVARCHAR(MAX),
[OrderByPredicate] NVARCHAR(MAX)
);
-- Simulate SELECTs with different order by that get different data due to varying WHERE clauses and ORDER conditions
INSERT INTO #TQueries VALUES ( 1, N'[Id] IN (6,11,13,7,10,3,15)', '[SomeValue] ASC' ) -- Sort Asc
INSERT INTO #TQueries VALUES ( 2, N'[Id] IN (9,15,14,20,17)', '[SomeValue] DESC' ) -- Sort Desc
INSERT INTO #TQueries VALUES ( 3, N'[Id] IN (20,10,1,16,11,19,9,15,17,6,2,3,13)', 'NEWID()' ) -- Sort Random
My main issue has been avoiding the use of a CURSOR or iterating through the rows one by one. The closest I've come to a set operation that meets this criteria is using a table variable to store the results of each query or a massive CTE.
Suggestions and comments are welcome.
Here's a solution that builds a single statement both to run all the queries and to return the results.
It uses a similar approach as in your answer when iterating over the #TQueries table, i.e. it also uses {...} tokens where column values from #TQuery should go, and it puts the values there with nested REPLACE() calls.
Other than that, it heavily depends on ranking functions, and I'm not sure if doesn't really abuse them. You'd need to test this method before deciding if it's better or worse than the one you've got so far.
DECLARE #QueryTemplate nvarchar(max), #FinalSQL nvarchar(max);
SET #QueryTemplate =
N'SELECT
[Id],
QueryRank = {Ordinal},
RowRank = ROW_NUMBER() OVER (ORDER BY {OrderByPredicate})
FROM [dbo].[SourceTable]
WHERE {WherePredicate}
';
SET #FinalSQL =
N'WITH AllData AS (
' +
SUBSTRING(
(
SELECT
'UNION ALL ' +
REPLACE(REPLACE(REPLACE(#QueryTemplate,
'{Ordinal}' , [Ordinal] ),
'{OrderByPredicate}', [OrderByPredicate]),
'{WherePredicate}' , [WherePredicate] )
FROM #TQueries
ORDER BY [Ordinal]
FOR XML PATH (''), TYPE
).value('.', 'nvarchar(max)'),
11, -- starting just after the first 'UNION ALL '
CAST(0x7FFFFFFF AS int) -- max int; no need to specify the exact length
) +
'),
RankedData AS (
SELECT
[Id],
QueryRank,
RowRank,
ValueRank = ROW_NUMBER() OVER (PARTITION BY [Id] ORDER BY QueryRank)
FROM AllData
)SELECT TOP (#top)
[Id]
FROM RankedData
WHERE ValueRank = 1
ORDER BY
QueryRank,
RowRank
';
PRINT #FinalSQL;
EXECUTE sp_executesql #FinalSQL, N'#top int', 10;
Basically, every subquery gets these auxiliary columns:
QueryRank – a constant value (within the subquery's result set) derived from [Ordinal];
RowRank – a ranking assigned to a row based on the [OrderByPredicate].
The result sets are UNIONed and then every entry of every unique value is again ranked (ValueRank) based on the query ranking.
When pulling the final result set, duplicates are suppressed (by the condition ValueRank = 1), and QueryRank and RowRank are used in the ORDER BY clause to preserve the original row order.
I used EXECUTE sp_executesql #query instead of EXECUTE (#query), because the former allows you to add parameters to the query. In particular, I parametrised the number of results to return (the argument of TOP). But you could certainly concatenate that value into the dynamic script directly, just like other things, if you prefer EXECUTE () over EXECUTE sq_executesql.
If you like, you can try this query at SQL Fiddle. (Note: the SQL Fiddle version replaces the #TQueries table variable with the TQueries table.)
This is what I've managed to piece together cobbled from my original response and improved upon by comments from #AndriyM
DECLARE #sql_prefix NVARCHAR(MAX);
SET #sql_prefix =
N'DECLARE #TResults TABLE (
[Ordinal] INT IDENTITY(1,1),
[ContentItemId] INT
);
DECLARE #max INT, #top INT;
SELECT #max = 10;';
DECLARE #sql_insert_template NVARCHAR(MAX), #sql_body NVARCHAR(MAX);
SET #sql_insert_template =
N'SELECT #top = #max - COUNT(*) FROM #TResults;
INSERT INTO #TResults
SELECT TOP (#top) [Id]
FROM [dbo].[SourceTable]
WHERE
{WherePredicate}
AND NOT EXISTS (
SELECT 1
FROM #TResults AS [tr]
WHERE [tr].[ContentItemId] = [SourceTable].[Id]
)
ORDER BY {OrderByPredicate};';
WITH Query ([Ordinal],[SqlCommand]) AS (
SELECT
[Ordinal],
REPLACE(REPLACE(#sql_insert_template, '{WherePredicate}', [WherePredicate]), '{OrderByPredicate}', [OrderByPredicate])
FROM #TQueries
)
SELECT
#sql_body = #sql_prefix + (
SELECT [SqlCommand]
FROM Query
ORDER BY [Ordinal] ASC
FOR XML PATH(''),TYPE).value('.', 'varchar(max)') + CHAR(13)+CHAR(10)
+N' SELECT * FROM #TResults ORDER BY [Ordinal]';
EXEC(#sql_body);
The basic idea is to use a table variable to hold the results of each query. I create a template for the SQL and replace the values in the template based on what is stored in #TQueries.
Once the entire script is completed I run it with EXEC.

Resources