TSQL: How to add automatically select columns - sql

I have a problem and can't solve it. Furthermore I can't find an answer anywhere in the internet.
Simplified I have a big table with coloumns, where values to products with an ID are stored by year:
year
id
value
In my stored procedure the attributes for getting information are:
#year
#id
If you want to get information about more than one product, you can use a comma-seperated list of product-ids like ('654654,543543,987987').
My TSQL should be like this:
select year,
sum(case when id = #id[1] then value),
sum(case when id = #id[2] then value),
[...]
from table myTable
where year = #year
group by year
order by year
What I want to do is iterate throught the comma-seperated ids and for each id, I want to add a new select attribut like this (sum(case when id = #id[x] then value).
Can you help me with this problems? Any suggestions to solve it?!
Thanks for your help!

PIVOT operation could simplify the query.
But, anyway, it seems that the only way to construct such a query is to use dynamic SQL.
DECLARE
#Ids NVARCHAR(MAX),
#stmt NVARCHAR(MAX)
SET #Ids = '1,2'
-- Transform Ids into the format PIVOT understands - with square brackets.
-- Primitive way, to not overcomplicate sample.
SET #Ids = '[' + REPLACE(#Ids, ',', '], [') + ']'
PRINT #Ids -- [1], [2]
SET #Stmt = '
SELECT *
FROM Products as p
PIVOT
(
SUM(p.Value)
FOR p.Id IN (' + #Ids + ')
) AS t
ORDER BY Year'
EXEC sp_executesql #Stmt
If you need more accurate way of splitting a comma separated list into an array (table), please see this article for details.
This example is available on SQL Fiddle

As you are using stored procedure, you can use sp_executesql to executed dynimically build SQL statement.
So, you have to iterate over the CSV like this:
DECLARE #List NVARCHAR(MAX) = N'1001,dada,1002,1003'
DECLARE #ProductsID TABLE ( [ID] BIGINT )
DECLARE #XML xml = N'<r><![CDATA[' + REPLACE(#List, ',', ']]></r><r><![CDATA[') + ']]></r>'
INSERT INTO #ProductsID ([ID])
SELECT DISTINCT CAST(Tbl.Col.value('.', 'float') AS bigint)
FROM #xml.nodes('//r') Tbl(Col)
WHERE ISNUMERIC(Tbl.Col.value('.', 'varchar(max)')) = 1
SELECT [ID] FROM #ProductsID
Then, having a table with the ID to dynamically build you SQL statement and execute it.

Related

SQL Sort / Order By pivoted fields while COALESCE function

I have some rates for resources for all countries
The rows will be Resource IDs
Columns should be Country Codes
Challenge here, I cannot sort the Country Codes in ASC
It would be so grateful if you could help me on this.
When I query, I get the list of country codes, but not sorted. i.e., USA,BRA,ARG etc. But the expected result should be ARG,BRA,USA in columns of the pivot.
Here is my code:
DECLARE #idList nvarchar(MAX)
SELECT
#idList = COALESCE(#idList + ',', '') + CountryCodeISO3
FROM
(
SELECT
DISTINCT CountryCodeISO3
FROM
Published.RateCardsValues
WHERE
CardID = 55
) AS SRC
DECLARE #sqlToRun nvarchar(MAX)
SET
#sqlToRun = '
SELECT *
FROM (
SELECT
[ResourceCode]
,[TITLES]
,[MostRepresentativeTitle]
,[ABBR_RES_DESC]
,[TypicalJobGrade]
,[BidGridResourceCode]
,[OpUnit]
,[PSResType]
,[JobGradeORResCat]
,[CountryCodeISO3]
--,[CurrencyCode]
,[RateValue]
FROM
[Published].[RateCardsValues] rc
WHERE
CardID = 55) As src
PIVOT (
MAX(RateValue) FOR [CountryCodeISO3] IN (' + #idList + ')
) AS pvt'
EXEC (#sqlToRun)
As you have discovered, PIVOT in T-SQL requires you to know at development time what the values will be that you will be pivoting on.
This is limiting, because if you want something like "retrieve data for all the countries where Condition X is true, then pivot on their IDs!", you have to resort to dynamic SQL to do it.
If Condition X is constant -- I'm guessing that belonging to CardID = 55 doesn't change often -- you can look up the values, and hardcode them in your code.
If the CardID you're looking up is always 55 and you have relatively few countries in that category, I'd actually advise doing that.
But if your conditions for picking countries can change, or the number of columns you want can vary -- something like "all the countries where there were sales of product Y, for month Z!" -- then you can't predict them, which means that the T-SQL PIVOT can't be set up (without dynamic SQL.)
In that case, I'd strongly suggest that you have whatever app you plan to use the data in do the pivoting, not T-SQL. (SSRS and Excel can both do it themselves, and code can be written to do it in .NET langauges.) T-SQL, as you have seen, does not lend itself to dynamic pivoting.
What you have will "work" in the sense that it will execute without errors, but there's another downside, in the next stage of your app: not only will the number of columns potentially change over time, the names of the columns will change, as countries move in and out of Card ID 55. That may cause problems for whatever app or destination you have in mind for this data.
So, my two suggestions would be: either hard-code your country codes, or have the next stage in your app (whatever executes the query) do the actual pivoting.
You need to sort the columns while creating the dynamic SQL
Also:
Do not use variable coalescing, use STRING_AGG or FOR XML instead
Use QUOTENAME to escape the column names
sp_executesql allows you to pass parameters to the dynamic query
DECLARE #idList nvarchar(MAX)
SELECT
#idList = STRING_AGG(QUOTENAME(CountryCodeISO3), ',') WITHIN GROUP (ORDER BY CountryCodeISO3)
FROM
(
SELECT
DISTINCT CountryCodeISO3
FROM
Published.RateCardsValues
WHERE
CardID = 55
) AS SRC;
DECLARE #sqlToRun nvarchar(MAX);
SET
#sqlToRun = '
SELECT *
FROM (
SELECT
[ResourceCode]
,[TITLES]
,[MostRepresentativeTitle]
,[ABBR_RES_DESC]
,[TypicalJobGrade]
,[BidGridResourceCode]
,[OpUnit]
,[PSResType]
,[JobGradeORResCat]
,[CountryCodeISO3]
--,[CurrencyCode]
,[RateValue]
FROM
[Published].[RateCardsValues] rc
WHERE
CardID = 55) As src
PIVOT (
MAX(RateValue) FOR [CountryCodeISO3] IN (' + #idList + ')
) AS pvt'
EXEC sp_executesql #sqlToRun;
On earlier versions of SQL Server, you cannot use STRING_AGG. You need to hack it with FOR XML. You need to also use STUFF to strip off the first separator.
DECLARE #idList nvarchar(MAX)
DECLARE #separator nvarchar(20) = ',';
SET #idList =
STUFF(
(
SELECT
#sep + QUOTENAME(CountryCodeISO3)
FROM
Published.RateCardsValues
WHERE
CardID = 55
GROUP BY
CountryCodeISO3
ORDER BY
CountryCodeISO3
FOR XML PATH(''), TYPE
).value('text()[1]','nvarchar(max)'),
1, LEN(#separator), '')
;
DECLARE #sqlToRun nvarchar(MAX);
SET
#sqlToRun = '
SELECT *
FROM (
SELECT
[ResourceCode]
,[TITLES]
,[MostRepresentativeTitle]
,[ABBR_RES_DESC]
,[TypicalJobGrade]
,[BidGridResourceCode]
,[OpUnit]
,[PSResType]
,[JobGradeORResCat]
,[CountryCodeISO3]
--,[CurrencyCode]
,[RateValue]
FROM
[Published].[RateCardsValues] rc
WHERE
CardID = 55) As src
PIVOT (
MAX(RateValue) FOR [CountryCodeISO3] IN (' + #idList + ')
) AS pvt'
EXEC sp_executesql #sqlToRun;

The argument 1 of the XML data type method “value” must be a string literal

If i pass #count variable i am getting this error
Below is my query
DECLARE #Error_Description NVARCHAR(Max)
DECLARE #Count VARCHAR(20)
DECLARE #x NVARCHAR(Max)
SELECT #Error_Description = 'The external columns for Excel Source are out of synchronization with the data source columns.
The column "szReferencceNumber" needs to be added to the external columns.
The column "SMSa" needs to be added to the external columns.
The column "as" needs to be added to the external columns.'
SELECT #Count = (LEN(#Error_Description) - LEN(REPLACE(#Error_Description, '"', ''))) / LEN('"')
SELECT #Count
SELECT COALESCE(LTRIM(CAST(('<X>' + REPLACE(#Error_Description, '"', '</X><X>') + '</X>') AS XML).value('(/X)[' + #Count + ']', 'varchar(128)')), '')
The first parameter to value must be a string literal. To select the nodes with a dynamic index you can do the following
SELECT
n.value('.', 'varchar(128)') as Result
from (SELECT CAST(('<X>' + REPLACE(#Error_Description, '"', '</X><X>') + '</X>') AS XML)) ca(x)
CROSS APPLY x.nodes('(/X)') n(n)
WHERE n.value('for $l in . return count(../*[. << $l]) + 1', 'int') %2 = 0
This returns the value for every second node. So achieves your desired results of getting the values enclosed in quotes.
Result
---------------------
szReferencceNumber
SMSa
as
if you're using 2012+, and you can use nvarchar(4000) (not MAX), you could get a copy of DelimitedSplitN4K_LEAD and grab rows where the value of ItemNumber is even:
DECLARE #Error_Description nvarchar(4000);
SELECT #Error_Description = N'The external columns for Excel Source are out of synchronization with the data source columns.
The column "szReferencceNumber" needs to be added to the external columns.
The column "SMSa" needs to be added to the external columns.
The column "as" needs to be added to the external columns.';
SELECT DS.Item
FROM dbo.DelimitedSplitN4K_LEAD(#Error_Description,'"') DS
WHERE DS.ItemNumber % 2 = 0;
If you're on SQL server 2016+, then you could use some JSON manipulation (which supports MAX values):
SELECT OJ.value
FROM (VALUES(#Error_Description))V(Error_Description)
CROSS APPLY (VALUES('["' + REPLACE(REPLACE(REPLACE(V.Error_Description,'"','","'),NCHAR(13),''),NCHAR(10),'')+ '"]'))R(JSON)
CROSS APPLY OPENJSON(R.JSON) OJ
WHERE OJ.[Key] % 2 = 1;
You can use your #Count within the XQuery predicate, but not via concatenation. There is sql:variable():
TheXml.value('(/X)[sql:variable("#Count") cast as xs:int?][1]', 'varchar(128)')
It would help to declare the variable #Count as INT in order to avoid the XQuery cast.
Hint: You need the final [1] to enforce the singleton .value() demands for.
this is all based on the #Shnugo answer above, thanks a lot Shnugo
I have a long script saved in to a temp table
select * from #Radhe
I want to print the whole script.
DECLARE #SQL NVARCHAR(MAX)
DECLARE #XML3 XML
--load the script to XML
SELECT #XML3 = (SELECT #Radhe.Item AS x FROM #Radhe FOR XML PATH(''))
--print line by line
declare #i int = 1
select #sql = 'radhe'
while #sql is not null
begin
SELECT #sql = #xml3.value('(/x/text())[sql:variable("#i")
cast as xs:int?][1]', 'varchar(max)')
print #sql
select #i = #i + 1
if #i > 10000 --limit it to 10000 lines
set #sql = null
end
and it works.
It took me a long time to get this done.
Hope I can help a fellow DBA or developer.

Assistance with a SQL query parsing JSON

I have a database table called QueueItems that contains the following fields:
SpecificData contains JSON data so we have used the well known parseJSON SQL function that is about on the internet.
So an example of the JSON is below:
{"DynamicProperties":{"IdentificationIndex":"CK","PaymentMethod":"C","Variants1":"010 ZERO BAL,716 ZERO BAL,717 ZERO BAL","Variants2":"CHECK_010,CHECK_716,CHECK_717","CustomerCode":"NO","FreeSelectionField":"NO","FreeSelectionValue":"NO","Variants1Line":"RFFOAVIS","Variants2Line":"ZRFFOUS_C","VendorFrom":"1","VendorTo":"999999999","DueDateCheck":"Yes","PaymentMethodSel":"Yes","PaymentMethodSelOnFail":"No","LineItemsOfPayDocs":"Yes","StartImmediately":"Yes","CreatePaymentMedium":"No","ExportFormat":"HTML Format","ExcludeValues":"Yes"}}
When I run the SQL function parseJSON it returns me the data in this format:
Now, the query I am trying to write is:
SELECT QueueItemID, QueueItemStatus, StartProcessing, EndProcessing, [THEN append each column from parseJSON but this needs to be transposed first]
So far I have managed to transpose the JSON into columns with a single row using:
DECLARE
#Cols AS VARCHAR(MAX) = ''
,#Query AS NVARCHAR(MAX) = ''
,#ParamDef AS NVARCHAR(MAX)
,#Json AS VARCHAR(MAX) = '{"DynamicProperties":{"IdentificationIndex":"CK","PaymentMethod":"C","Variants1":"010 ZERO BAL,716 ZERO BAL,717 ZERO BAL","Variants2":"CHECK_010,CHECK_716,CHECK_717","CustomerCode":"NO","FreeSelectionField":"NO","FreeSelectionValue":"NO","Variants1Line":"RFFOAVIS","Variants2Line":"ZRFFOUS_C","VendorFrom":"1","VendorTo":"999999999","DueDateCheck":"Yes","PaymentMethodSel":"Yes","PaymentMethodSelOnFail":"No","LineItemsOfPayDocs":"Yes","StartImmediately":"Yes","CreatePaymentMedium":"No","ExportFormat":"HTML Format","ExcludeValues":"Yes"}}'
SELECT
#Cols += ',' + Name
FROM
parseJson(#Json)
WHERE
Name NOT IN ('DynamicProperties', '-')
SET #Cols = SUBSTRING(#Cols,2,LEN(#Cols))
SET #Query = N'SELECT *
FROM
(
SELECT [StringValue], [Name]
FROM parseJson(''' + #Json + ''')
) [d]
PIVOT
(
MAX([StringValue])
FOR [Name] IN (' + #Cols + ')
) [piv]'
EXECUTE (#Query)
As you can see the JSON may contain ANY data, so I am dynamically inserting the column names into the PIVOT statement FOR IN.
Now to Join this data as additional columns onto my main query SELECT QueueItemId.. FROM QueueItems I was originally going to put this code in a UDF and call it as part of my main query / stored procedure but I have two issues:
1) The sp_executesql or EXEC statement - not allowed in a UDF
2) To return a table from a UDF I need to define the fields... The fields are dynamic... I could get this working by passing back XML to my stored procedure but I still have issue number 1.
So the question is:
1) Is there any better way or writing this then using a UDF?
2) Is there any other way or transposing the output from parseJSON?
3) Is there any other way of using the PIVOT but not having to specify the columns?
Any help would be much appreciated.
EDIT IN RESPONSE TO M ALI
I know the syntax is wrong and it doesn't work but the query would look something like this:
DECLARE #Cols AS VARCHAR(MAX) = ''
,#Query AS NVARCHAR(MAX) = ''
SELECT
QI.QueueItemID,
QI.QueueItemStatus,
QI.StartProcessing,
QI.EndProcessing,
SD.*
FROM QueueItems AS QI
LEFT JOIN
(
SELECT
#Cols += ',' + Name
FROM
parseJson(QI.SpecificData)
WHERE
Name NOT IN ('DynamicProperties', '-')
SET #Cols = SUBSTRING(#Cols,2,LEN(#Cols))
SET #Query = N'SELECT *
FROM
(
SELECT [StringValue], [Name]
FROM parseJson(''' + QI.SpecificData + ''')
) [d]
PIVOT
(
MAX([StringValue])
FOR [Name] IN (' + #Cols + ')
) [piv]'
EXECUTE (#Query)
) AS SD
And the results would be the columns from QueueItems, joined on the left by columns from the parsed JSON string.
QueueItemID, StartProcessing, EndProcessing, IndentificationIndex, PaymentMethod etc...

SQL Pivot Table

Hey guys I have tried to convert this query into a pivot table and I have searched everywhere but can't seem to apply it. The following is my code.
SELECT ClientPartner, ClientManager, WIPAmount
FROM tblTranWIP
WHERE (WIPDate>={ts '2013-04-01 00:00:00'}
AND WIPDate<{ts '2014-03-31 00:00:01'})AND TransTypeIndex=1 AND ContIndex<900000
The following screenshot shows my output. I basically want to have Managers and Partners as the First Column and Row and then have WipAmount be in the middle as ClientPartners and Managers match id's The screen shot shows it easier than I explain. The link is a screenshot, the system won't let me add embed images
There is at least one way I know of to get the information you are looking for, and that is to use a PIVOT:
SELECT pvt.*
FROM
(
SELECT ClientPartner, ClientManager, WIPAmount FROM tblTranWIP
) AS tranwip
PIVOT
(
SUM(WIPAmount) FOR ClientPartner IN ([46], [58], [177], [207])
) AS pvt
Here is a SQL Fiddle.
this was a great help, upon reading many tutorials. I came up with the following code for what I needed, which was a dynamic pivot as I don't know every single column code.
DECLARE #sql varchar(MAX)
DECLARE #columns TABLE (COL varchar(50))
declare #columnscsv varchar(MAX)
insert into #columns
select distinct ClientPartner from tblTranWip
select #columnscsv = COALESCE(#columnscsv + '],[','') + COL from #columns
set #columnscsv = '[' + #columnscsv + ']'
SET #sql = 'Select ClientManager ' + #columnscsv + ' FROM(SELECT ClientPartner, ClientManager, WipAmount FROM tblTranWip) a
PIVOT (MAX(WipAmount) for ClientPartner in (' + #columnscsv + ')) AS PVT ORDER by ClientManager'
EXEC (#sql)

Can SQL Server Pivot without knowing the resulting column names?

I have a table that looks like this:
Month Site Val
2009-12 Microsoft 10
2009-11 Microsoft 12
2009-10 Microsoft 13
2009-12 Google 20
2009-11 Google 21
2009-10 Google 22
And I want to get a 2-dimension table that gives me the "Val" for each site's month, like:
Month Microsoft Google
2009-12 10 20
2009-11 12 21
2009-10 13 22
But the catch is, I don't know all the possible values that can be in "Site". If a new site appears, I want to automatically get a new column in my resulting table.
All the code samples I saw that could do this required me to hardcode "Microsoft and Google" in the query text.
I saw one that didn't, but it was basically faking it by listing the Sites and generating a query on the fly (concatting a string) that had those column names in it.
Isn't there a way to get SQL Server 2008 to do this without a hack like that?
NOTE: I need to be able to run this as a query that I send from ASP.Net, I can't do stored procedures or other stuff like that.
Thanks!
Daniel
The example you linked to uses dynamic SQL. Unfortunately, there is no other built-in method for pivoting in SQL Server when the output columns are not known in advance.
If the data is not too large, it's probably easiest to simply run a normal row query from ASP.NET and perform your pivot in the application code. If the data is very large, then you'll have to generate the SQL dynamically after first querying for the possible column values.
Note that you don't actually need to write a SQL statement that generates dynamic SQL; you can simply generating the SQL in ASP.NET, and that will most likely be much easier. Just don't forget to escape the distinct Site values before chucking them in a generated query, and don't forget to parameterize whatever parts of the SQL statement that you normally would without the pivot.
It's been more than 10 years, and the same problem came to me.
Is there any way to pivot without knowing column names?
Then I searched something and found the below solution. We can achieve this by using dynamic query. I am adding this so it will help someone.
CREATE TABLE TEMP
(
[Month] varchar(50),
[Site] varchar(50),
Val int
)
INSERT INTO TEMP
VALUES ('2009-12', 'Microsoft', 10),
('2009-11', 'Microsoft', 12),
('2009-10', 'Microsoft', 15),
('2009-12', 'Google', 20),
('2009-11', 'Google', 8),
('2009-10', 'Google', 11),
('2009-12', 'Facebook', 13),
('2009-11', 'Facebook', 12),
('2009-10', 'Facebook', 5)
DECLARE #Columns as VARCHAR(MAX)
SELECT #Columns = COALESCE(#Columns + ', ','') + QUOTENAME([Site])
FROM
(SELECT DISTINCT [Site] FROM TEMP) AS B
ORDER BY B.[Site]
DECLARE #SQL as VARCHAR(MAX)
SET #SQL = 'SELECT Month, ' + #Columns + '
FROM
(
select Month,[Site],Val from TEMP
) as PivotData
PIVOT
(
Sum(Val)
FOR [Site] IN (' + #Columns + ')
) AS PivotResult
ORDER BY Month'
EXEC(#SQL);
As you can see I took the column values into a string and then dynamically use that to pivot.
Here is the result:
If we take the answer of marc_s and put it into a procedure, we have this:
create procedure spPivot (
#DataSource varchar(max),
#Column1 varchar(100),
#PivotColumn varchar(100),
#AggregateColumn varchar(100),
#AgregateFunction varchar(20),
#Debug bit = 0) as
declare #SQL varchar(max) =
'DECLARE #Columns as VARCHAR(MAX)
SELECT #Columns = COALESCE(#Columns + '', '','''') + QUOTENAME({PivotColumn})
FROM (SELECT DISTINCT {PivotColumn} FROM {DataSourceA} ds) c
ORDER BY {PivotColumn}
DECLARE #SQL as VARCHAR(MAX)
SET #SQL = ''SELECT {Column1}, '' + #Columns + ''
FROM {DataSourceB} as PivotData
PIVOT (
{AgregateFunction}({AggregateColumn})
FOR {PivotColumn} IN ('' + #Columns + '')
) AS PivotResult
ORDER BY {Column1}''
EXEC(#SQL)'
if #DataSource like 'select %' begin
set #SQL = replace(#SQL, '{DataSourceA}', '(' + #DataSource + ')')
set #SQL = replace(#SQL, '{DataSourceB}', '(' + replace(#DataSource, '''', '''''') + ')')
end else begin
set #SQL = replace(#SQL, '{DataSourceA}', #DataSource)
set #SQL = replace(#SQL, '{DataSourceB}', #DataSource)
end
set #SQL = replace(#SQL, '{Column1}', #Column1)
set #SQL = replace(#SQL, '{PivotColumn}', #PivotColumn)
set #SQL = replace(#SQL, '{AggregateColumn}', #AggregateColumn)
set #SQL = replace(#SQL, '{AgregateFunction}', #AgregateFunction)
if #Debug = 1
print #SQL
else
exec(#SQL)
And an example of its usage:
spPivot
'select ''Bucket'' Category, ''Large'' SubCategory, 1 Amount union all
select ''Bucket'' Category, ''Large'' SubCategory, 2 Amount union all
select ''Shovel'' Category, ''Large'' SubCategory, 4 Amount union all
select ''Shovel'' Category, ''Small'' SubCategory, 8 Amount',
'Category', 'SubCategory', 'Amount', 'sum'
The example works, but note that it's probably more efficient to send the procedure the name of a [temp] table because it's queried twice within. So using marc_s' temp table, the call would be
spPivot 'TEMP', '[Month]', 'Site', 'Val', 'SUM'
Also note you have a #debug parameter that you can use to figure out why your call is not working as you expect.
select
month,
min(case site when 'microsoft'then val end) microsoft,
min(case site when 'google'then val end) google
from
withoutpivot
group by
month
select
main.month,
m.val as microsoft,
g.val as google
from
withoutpivot main
inner join
withoutpivot m on m.month = main.month
inner join
withoutpivot g on g.month = main.month
where
m.site = 'microsoft'
and g.site = 'google'