SQL Server - Using COALESCE across a Pivot with variable columns - sql

I am amending a SQL query that populates a UI table. I want to eliminate any nulls that result from the query and replace them with 0.00. Typically a COALESCE function on the final SELECT would give me what I need, but the columns in the pivot (#ListOfYears) might change (SELECT PV.* may give me 1 column or 10 columns depending on #ListofYears).
DECLARE #ListofYears varchar(6000)
DECLARE #SQL varchar(2000)
SET #ListofYears = '[2021],[2022]'
SET #SQL = '
SELECT PV.*
FROM
(SELECT
p.Pkey,
p.Code,
P.Name as Name,
Price,
GroupName
FROM
Catalogue C
LEFT JOIN CatalogueDetail CD on CD.CataloguePkey=C.Pkey
LEFT JOIN CatalogueYear CY on CY.Pkey=CD.CatalogueYearPkey where C.IsActive=1
) as D
PIVOT
(
COALESCE(Sum(D.Price), 0.00) for GroupName in ( ' + #ListofYears + ' )
) AS PV
ORDER BY Pkey'
PRINT #sql
EXEC (#sql)
Is there any way to use COALESCE or ISNULL so that my top SELECT statement does not result in any NULLS? Putting COALESCE in the pivot results in:
Msg 102, Level 15, State 1, Line 4
Incorrect syntax near ','.
Msg 156, Level 15, State 1, Line 22
Incorrect syntax near the keyword 'COALESCE'.

As I mentioned in the question, you need to wrap the COALESCE in the outer SELECT, not in the PIVOT. This means, in short, you need something like this:
SELECT {Other Columns},
COALESCE([2020],0) AS [2020],
COALESCE([2021],0) AS [2021]
...
FROM(SELECT p.Pkey,
p.Code,
P.Name as Name,
Price,
GroupName
FROM dbo.Catalogue C
LEFT JOIN dbo.CatalogueDetail CD on CD.CataloguePkey=C.Pkey
LEFT JOIN dbo.CatalogueYear CY on CY.Pkey=CD.CatalogueYearPkey
WHERE C.IsActive=1
) as D
PIVOT(SUM(D.Price)
FOR GroupName in ([2021],[2022])) AS PV
ORDER BY Pkey;
As you are using dynamic SQL, and injecting a list into you are, unsurprisingly, finding this difficult.
In truth, I would suggest switching to conditional aggregation, as this is much easier. Non-dynamic it would look like this:
SELECT {Other Columns}
SUM(CASE GroupName WHEN 2020 THEN D.Price ELSE 0 END) AS [2020],
SUM(CASE GroupName WHEN 2021 THEN D.Price ELSE 0 END) AS [2021]
FROM dbo.Catalogue C
LEFT JOIN dbo.CatalogueDetail CD on CD.CataloguePkey=C.Pkey
LEFT JOIN dbo.CatalogueYear CY on CY.Pkey=CD.CatalogueYearPkey
WHERE C.IsActive=1
GROUP BY {Other Columns};
As you want to do this dynamically, it would look a little like this (assuming you are using SQL Server 2017+):
DECLARE #Years table (Year int);
INSERT INTO #Years (Year)
VALUES (2020),(2021);
DECLARE #SQL nvarchar(MAX),
#CRLF nchar(2) = NCHAR(13) + NCHAR(10);
DECLARE #Delim nvarchar(20) = N',' + #CRLF;
SET #SQL = (SELECT N'SELECT {Other Columns}' + #CRLF +
STRING_AGG(N' SUM(CASE GroupName WHEN ' + QUOTENAME(Y.Year,'''') + N'THEN D.Price ELSE 0 END) AS ' + QUOTENAME(Y.Year),#Delim) WITHIN GROUP (ORDER BY Y.Year) + #CRLF +
N'FROM dbo.Catalogue C' + #CRLF +
N' LEFT JOIN dbo.CatalogueDetail CD on CD.CataloguePkey=C.Pkey' + #CRLF +
N' LEFT JOIN dbo.CatalogueYear CY on CY.Pkey=CD.CatalogueYearPkey' + #CRLF +
N'WHERE C.IsActive=1' + #CRLF +
N'GROUP BY {Other Columns};'
FROM #Years Y);
EXEC sys.sp_executesql #SQL;
If you are using a version of SQL Server that isn't fully supported, you'll need to use the "old" FOR XML PATH (and STUFF) method.
Obviously, you'll also need to replace the parts in braces with the appropriate SQL.

Related

Aggregate dynamic columns in SQL Server

I have a narrow table containing unique key and source data
Unique_Key
System
1
IT
1
ACCOUNTS
1
PAYROLL
2
IT
2
PAYROLL
3
IT
4
HR
5
PAYROLL
I want to be able to pick a system as a base - in this case IT - then create a dynamic SQL query where it counts:
distinct unique key in the chosen system
proportion of shared unique key with other systems. These systems could be dynamic and there are lot more than 4
I'm thinking of using dynamic SQL and PIVOT to first pick out all the system names outside of IT. Then using IT as a base, join to that table to get the information.
select distinct Unique_Key, System_Name
into #staging
from dbo.data
where System_Name <> 'IT'
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX);
SET #cols = STUFF((SELECT distinct ',' + QUOTENAME(System_Name)
FROM #staging
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT Unique_Key, ' + #cols + ' into dbo.temp from
(
select Unique_Key, System_Name
from #staging
) x
pivot
(
count(System_Name)
for System_Name in (' + #cols + ')
) p '
execute(#query)
select *
from
(
select distinct Unique_Key
from dbo.data
where System_Name = 'IT'
) a
left join dbo.temp b
on a.Unique_Key = b.Unique_Key
So the resulting table is:
Unique_Key
PAYROLL
ACCOUNTS
HR
1
1
1
0
2
1
0
0
3
0
0
0
What I want is one step further:
Distinct Count IT Key
PAYROLL
ACCOUNTS
HR
3
67%
33%
0%
I can do a simple join with specific case when/sum statement but wondering if there's a way to do it dynamically so I don't need to specify every system name.
Appreciate any tips/hints.
You can try to use dynamic SQL as below, I would use condition aggregate function get pivot value then we might add OUTER JOIN or EXISTS condition in dynamic SQL.
I would use sp_executesql instead of exec to avoid sql-injection.
DECLARE #System_Name NVARCHAR(50) = 'IT'
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX),
#parameter AS NVARCHAR(MAX);
SET #parameter = '#System_Name NVARCHAR(50)'
select DISTINCT System_Name
into #staging
from dbo.data t1
WHERE t1.System_Name <> #System_Name
SET #cols = STUFF((SELECT distinct ', SUM(IIF(System_Name = '''+ System_Name+''',1,0)) * 100.0 / SUM(IIF(System_Name = #System_Name,0,1)) ' + QUOTENAME(System_Name)
FROM #staging
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT SUM(IIF(System_Name = #System_Name,0,1)) [Distinct Count IT Key], ' + #cols + ' from dbo.data t1
WHERE EXISTS (
SELECT 1
FROM dbo.data tt
WHERE tt.Unique_Key = t1.Unique_Key
AND tt.System_Name = #System_Name
) '
EXECUTE sp_executesql #query, #parameter, #System_Name
sqlfiddle
When writing Dynamic Query, you start off with a non-dynamic query. Make sure you gets the result of the query is correct before you convert to dynamic query.
For the result that you required, the query will be
with cte as
(
select it.Unique_Key, ot.System_Name
from data it
left join data ot on it.Unique_Key = ot.Unique_Key
and ot.System_Name <> 'IT'
where it.System_Name = 'IT'
)
select [ITKey] = count(distinct Unique_Key),
[ACCOUNTS] = count(case when System_Name = 'ACCOUNTS' then Unique_Key end) * 100.0
/ count(distinct Unique_Key),
[HR] = count(case when System_Name = 'HR' then Unique_Key end) * 100.0
/ count(distinct Unique_Key),
[PAYROLL] = count(case when System_Name = 'PAYROLL' then Unique_Key end) * 100.0
/ count(distinct Unique_Key)
from cte;
Once you get the result correct, it is not that difficult to convert to dynamic query. Use string_agg() or for xml path for those repeated rows
declare #sql nvarchar(max);
; with cte as
(
select distinct System_Name
from data
where System_Name <> 'IT'
)
select #sql = string_agg(sql1 + ' / ' + sql2, ',' + char(13))
from cte
cross apply
(
select sql1 = char(9) + quotename(System_Name) + ' = '
+ 'count(case when System_Name = ''' + System_Name + ''' then Unique_Key end) * 100.0 ',
sql2 = 'count(distinct Unique_Key)'
) a
select #sql = 'with cte as' + char(13)
+ '(' + char(13)
+ ' select it.Unique_Key, ot.System_Name' + char(13)
+ ' from data it' + char(13)
+ ' left join data ot on it.Unique_Key = ot.Unique_Key' + char(13)
+ ' and ot.System_Name <> ''IT''' + char(13)
+ ' where it.System_Name = ''IT''' + char(13)
+ ')' + char(13)
+ 'select [ITKey] = count(distinct Unique_Key), ' + char(13)
+ #sql + char(13)
+ 'from cte;' + char(13)
print #sql;
exec sp_executesql #sql;
db<>fiddle demo
This solution changes the aggregation function of the PIVOT itself.
First, let's add a column [has_it] to #staging that keeps track of whether each Unique_Key has an IT row:
select Unique_Key, System_Name, case when exists(select 1 from data d2 where d2.Unique_Key=d1.Unique_Key and d2.System_Name='IT') then 1 else 0 end as has_it
into #staging
from data d1
where System_Name <> 'IT'
group by Unique_Key, System_Name
Now, the per-System aggregation (sum) of this column divided by the final total unique keys needed (example case=3) returns the requests numbers. Change the PIVOT to the following and it's ready as-is, without further queries:
set #query = ' select *
from
(
select System_Name,cnt as [Distinct Count IT Key],has_it*1.0/cnt as divcnt
from #staging
cross join
(
select count(distinct Unique_Key) as cnt
from dbo.data
where System_Name = ''IT''
)y
) x
pivot
(
sum(divcnt)
for System_Name in (' + #cols + ')
) p'

Split a single delimited string into a table for a given number of columns

I have a string that looks like this:
DECLARE #myString varchar(max) = '1,2,3,4,5,6'
I want to call a function (or in SOME way convert to a table) that takes as arguments the string, delimiter and an integer, and returns a table that splits the string into columns:
For instance,
SELECT * FROM somefunction(#mystring,',',2) should give me a table two columns:
1 2
3 4
5 6
AND
SELECT * FROM somefunction(#mystring,',',3) should give me three columns like so:
1 2 3
4 5 6
I want to reiterate firstly that this has a strong smell of a XY Problem and that very likely this isn't something you should be doing in the RDBMS. That isn't to say it can't be, but certainly SQL Server isn't the best place.
Secondly, as mentioned, this is impossible in a FUNCTION. A FUNCTION must be well defined, meaning the data in and the data out must be defined at the time the function is created. An object that needs to return a variable amount of columns isn't well defined, as you can't define the result until the object is called.
Also the only way to achieve what you are after is by using Dynamic SQL, which you cannot use inside a FUNCTION; as it requires using EXEC (which can only be used against a very few system objects and sp_executesql is not one of them).
This means we would need to use a Stored Procedure to achieve this, however, you won't be able to use syntax like SELECT * FROM dbo.MyProcedure(#mystring,',',2); You'll need to execute it (EXEC dbo.MyProcedure(#mystring,',',2);).
Before we get onto the end dynamic solution, we need to work out how we would do with a static value. That isn't too bad, you need to simply use a a string splitter that is ordinal position aware (STRING_SPLIT is not), so I am using DelimitedSplit8K_LEAD. Then you can use a bit of integer maths to assign the rows both a column and row group. Finally we can use those value to pivot the data, using a "Cross Tab".
For a non-dynamic approach, this gets us a result like this:
DECLARE #String varchar(8000),
#Columns tinyint;
SET #String = '1,2,3,4,5,6';
SET #Columns = 3;
WITH Groupings AS(
SELECT *,
(ROW_NUMBER() OVER (ORDER BY DS.ItemNumber) -1) / #Columns AS RowNo,
(ROW_NUMBER() OVER (ORDER BY DS.ItemNumber) -1) % #Columns +1 AS ColumnNo
FROM dbo.DelimitedSplit8K_LEAD(#String,',') DS)
SELECT MAX(CASE G.ColumnNo WHEN 1 THEN G.Item END) AS Col1,
MAX(CASE G.ColumnNo WHEN 2 THEN G.Item END) AS Col2,
MAX(CASE G.ColumnNo WHEN 3 THEN G.Item END) AS Col3
FROM Groupings G
GROUP BY G.RowNo;
Of course, if you change the value of #Columns the number of columns does not change, it's hard coded.
It's also important to note I have used a varchar(8000) not a varchar(MAX). DelimitedSplit8K_LEAD (and DelimitedSplitN4K_LEAD) do not support MAX lengths, and the article above, and the original iteration of the function (DelimitedSplit8K) explain why.
Moving on, now we need to get onto the dynamic value. I'm going to assume that you won't have a "silly" value for #Columns, and that it'll be between 1 and 100. I'm also assuming you are using a recent version of SQL Server, and thus have access to STRING_AGG; if not you'll need to use FOR XML PATH (and STUFF) to do the aggregation of the dynamic statement.
First we can use a tally with up to 100 rows, to get the right number of column groups, and then (like mentioned) STRING_AGG to aggregate the dynamic part. The rest of the statement is still static. With the variable we have before, we end up with something like this to create the dynamic statement:
DECLARE #Delimiter nvarchar(20) = N',' + #CRLF + N' ';
WITH Tally AS(
SELECT TOP (#Columns)
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL)) N1(N)
CROSS JOIN (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL)) N2(N))
SELECT #SQL = N'WITH Groupings AS(' + #CRLF +
N' SELECT *,' + #CRLF +
N' (ROW_NUMBER() OVER (ORDER BY DS.ItemNumber) -1) / #Columns AS RowNo,' + #CRLF +
N' (ROW_NUMBER() OVER (ORDER BY DS.ItemNumber) -1) % #Columns +1 AS ColumnNo' + #CRLF +
N' FROM dbo.DelimitedSplit8K_LEAD(#String,'','') DS)' + #CRLF +
N'SELECT ' +
(SELECT STRING_AGG(CONCAT(N'MAX(CASE G.ColumnNo WHEN ',T.I,N' THEN G.Item END) AS ',QUOTENAME(CONCAT(N'Col',T.I))),#Delimiter) WITHIN GROUP (ORDER BY T.I)
FROM Tally T) + #CRLF +
N'FROM Groupings G' + #CRLF +
N'GROUP BY G.RowNo;'
PRINT #SQL;
And the PRINT outputs the below (which is what we had before, excellent!):
WITH Groupings AS(
SELECT *,
(ROW_NUMBER() OVER (ORDER BY DS.ItemNumber) -1) / #Columns AS RowNo,
(ROW_NUMBER() OVER (ORDER BY DS.ItemNumber) -1) % #Columns +1 AS ColumnNo
FROM dbo.DelimitedSplit8K_LEAD(#String,',') DS)
SELECT MAX(CASE G.ColumnNo WHEN 1 THEN G.Item END) AS [Col1],
MAX(CASE G.ColumnNo WHEN 2 THEN G.Item END) AS [Col2],
MAX(CASE G.ColumnNo WHEN 3 THEN G.Item END) AS [Col3]
FROM Groupings G
GROUP BY G.RowNo;
Now we need to wrap this into a parametrised stored procedure, and also execute the dynamic statement. This gives us the following end result, using sys.sp_executesql to execute and parametrise the dynamic statement:
CREATE PROC dbo.DynamicPivot #String varchar(8000), #Delim char(1), #Columns tinyint, #SQL nvarchar(MAX) = NULL OUTPUT AS
BEGIN
IF #Columns > 100
THROW 72001, N'#Columns cannot have a value greater than 100.', 16;
DECLARE #CRLF nchar(2) = NCHAR(13) + NCHAR(10);
DECLARE #Delimiter nvarchar(20) = N',' + #CRLF + N' ';
WITH Tally AS(
SELECT TOP (#Columns)
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL)) N1(N)
CROSS JOIN (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL)) N2(N))
SELECT #SQL = N'WITH Groupings AS(' + #CRLF +
N' SELECT *,' + #CRLF +
N' (ROW_NUMBER() OVER (ORDER BY DS.ItemNumber) -1) / #Columns AS RowNo,' + #CRLF +
N' (ROW_NUMBER() OVER (ORDER BY DS.ItemNumber) -1) % #Columns +1 AS ColumnNo' + #CRLF +
N' FROM dbo.DelimitedSplit8K_LEAD(#String,#Delim) DS)' + #CRLF +
N'SELECT ' +
(SELECT STRING_AGG(CONCAT(N'MAX(CASE G.ColumnNo WHEN ',T.I,N' THEN G.Item END) AS ',QUOTENAME(CONCAT(N'Col',T.I))),#Delimiter) WITHIN GROUP (ORDER BY T.I)
FROM Tally T) + #CRLF +
N'FROM Groupings G' + #CRLF +
N'GROUP BY G.RowNo;'
--PRINT #SQL;
EXEC sys.sp_executesql #SQL, N'#String varchar(8000), #Delim char(1), #Columns tinyint', #String, #Delim, #Columns;
END;
GO
And then we can execute this like below:
EXEC dbo.DynamicPivot N'1,2,3,4,5,6',',',3; --Without #SQL
GO
DECLARE #SQL nvarchar(MAX)
EXEC dbo.DynamicPivot N'1 2 3 4 5 6 7 8 9 10',' ',5,#SQL OUTPUT; --With SQL to see statement run, and different delimiter
PRINT #SQL;
As I noted as well, and as you can see from the definition of the Procedure, if you try to pivot with more than 100 columns it will error:
--Will error, too many columns
EXEC dbo.DynamicPivot N'1,2,3,4,5,6',',',101; --Without #SQL
Which returns the error:
Msg 72001, Level 16, State 16, Procedure dbo.DynamicPivot, Line 5
#Columns cannot have a value greater than 100.
Edit: Noticed the value for the delimiter in the splitter needs to be parametrised too, so amended the SP's definition to add #Delim and added that the to dynamic SQL too and demonstrate in examples.

Passing parameter to stored procedure using dynamic SQL into pivot data

I am trying to pass a parameter into Stored procedure to filter data in my select statement but when i use the parameter it gives error Message: Invalid column name 'SessionId2075'. when I use static value in the where clause the procedure works fine. Can you please give me fix the issue. I checked all the previous answers and could not find the working solution.
Alter PROCEDURE [dbo].GetPivotFeeReport
(
#SessionId varchar(50)
)
as
begin
DECLARE #SQL as VARCHAR(MAX)
DECLARE #Columns as VARCHAR(MAX)
SET NOCOUNT ON;
SELECT #Columns =
COALESCE(#Columns + ', ','') + QUOTENAME(GroupHeaderValue)
FROM
(SELECT DISTINCT mgh.GroupHeaderValue
FROM StudentFeeDetail sf
INNER JOIN MasterGroupHeaderValue mgh
ON mgh.GroupHeaderValueId = sf.FeeForId
) AS B
ORDER BY B.GroupHeaderValue
SET #SQL = 'SELECT ClassName,' + #Columns + ',TOTAL
FROM
(
SELECT
distinct mc.className,
sf.FinalAmount,
mgh.GroupHeaderValue,
Sum (isnull(sf.FinalAmount,0)) over (partition by ClassName) AS TOTAL
--0 AS TOTAL
FROM StudentFeeDetail sf
INNER JOIN StudentAdmission sa
ON sa.AdmissionId = sf.AdmissionId
INNER JOIN MasterClass mc
ON mc.ClassId = sa.ClassId
INNER JOIN MasterGroupHeaderValue mgh
ON mgh.GroupHeaderValueId = sf.FeeForId
WHERE sa.SessionId = (' + #SessionId + ') -- this is where I am trying to use the parameter when used static value like this ''SessionId2075'' the procedure works fine
and sf.FeeAmt >0
GROUP BY className, FinalAmount, GroupHeaderValue
) as PivotData
PIVOT
(
sum(FinalAmount)
FOR GroupHeaderValue IN (' + #Columns + ')
) AS PivotResult
ORDER BY (ClassName)
'
EXEC ( #sql)
end

SQL Server stored procedure based on PIVOT and parameterized query not sorting properly

I am using a SQL Server stored procedure to generate a data table I use as a datasource for a WPF datagrid. The data are in two tables of parent [Sample] child [SampleConstituent] relationship and I am using a PIVOT to generate columns for the data records in the child table. The query uses parameters so that I can filter the records returned to the datagrid.
My problem is that I would like to return a TOP N result set from the query based on the most recent records. I have the following stored procedure and everything works except the data are always returned oldest records first. The TOP N filter is thus returning the oldest records and not the most recent. Sample.SampleDateTime is the column in the parent table I wish to sort by.
I have tried so many iterations that my novice brain is in knots!
ALTER PROCEDURE [dbo].[spSampleDisplayAllParams]
-- Add the parameters for the stored procedure here
#fromDate DATE = '2013-01-01',
#toDate DATE = '2100-01-01',
#ProductName NVARCHAR(50) = '%',
#SampleNumber NVARCHAR(50) = '%',
#numSamples NVARCHAR(50) = 200
AS
BEGIN
SET NOCOUNT ON;
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT distinct ',' + QUOTENAME(ConstituentName)
from SampleConstituent
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
PRINT #cols
set #query
= 'SELECT top (' + #numSamples + ') * from
(SELECT TOP 100 PERCENT s.SampleID, s.SampleNumber, s.SampleDateTime, s.ProductName, sc.ConstituentName, sc.ConstituentValue
FROM dbo.Sample s INNER JOIN
dbo.SampleConstituent sc ON s.SampleID = sc.SampleID
WHERE (s.Active = 1) AND
(s.ProductName Like ''' + #ProductName + ''') AND
(s.SampleNumber Like ''' + #SampleNumber + ''') AND
(s.SampleDateTime BETWEEN ''' + CONVERT(nvarchar(24), #FromDate, 121) +''' AND'''+ CONVERT(nvarchar(24), #ToDate, 121) +''')
ORDER BY s.SampleDateTime ) x
pivot
(
max(ConstituentValue)
for ConstituentName in (' + #cols + ')
) p '
execute(#query)
END
Try putting order by after the pivot :
) x
pivot
(
max(ConstituentValue)
for ConstituentName in (' + #cols + ')
) p ORDER BY SampleDateTime '
for more clarification your query should look something like this:
set #query
= 'SELECT top (' + #numSamples + ') * from
(SELECT s.SampleID, s.SampleNumber, s.SampleDateTime, s.ProductName, sc.ConstituentName, sc.ConstituentValue
FROM dbo.Sample s INNER JOIN
dbo.SampleConstituent sc ON s.SampleID = sc.SampleID
WHERE (s.Active = 1) AND
(s.ProductName Like ''' + #ProductName + ''') AND
(s.SampleNumber Like ''' + #SampleNumber + ''') AND
(s.SampleDateTime BETWEEN ''' + CONVERT(nvarchar(24), #FromDate, 121) +''' AND'''+ CONVERT(nvarchar(24), #ToDate, 121) +''')
) x
pivot
(
max(ConstituentValue)
for ConstituentName in (' + #cols + ')
) p ORDER BY SampleDateTime DESC'
--sort and order is placed after the pivot
Your order by is inside with the TOP 100 PERCENT. In this case, the ORDER BY is used to dictate which rows to include, but since you said you want all of them, SQL Server is clever and throws out both the TOP and the ORDER BY. Look at the plan, I bet there is no sort anywhere. This:
SELECT * FROM
(SELECT TOP 100 PERCENT something FROM somewhere ORDER BY something) AS x;
Is the exact same as:
SELECT * FROM
(SELECT something FROM somewhere) AS x;
In neither case is SQL Server told to order the end result by anything. So it doesn't, and instead chooses an ordering which is most efficient rather than trying to read your mind.
If you want to sort, you need to put it on the outermost query, as #Sonam has identified. Some background info here:
Bad habits to kick : relying on undocumented behavior
And FYI, you should do your best to guard yourself from SQL injection and use proper, parameterized queries. Again, not to take anything from #Sonam's answer, but this would be better as:
SELECT #sql = N'SELECT TOP (#numSamples) * FROM
(
SELECT s.SampleID, s.SampleNumber, s.SampleDateTime,
s.ProductName, sc.ConstituentName, sc.ConstituentValue
FROM dbo.Sample AS s
INNER JOIN dbo.SampleConstituent AS sc
ON s.SampleID = sc.SampleID
WHERE (s.Active = 1) AND
(s.ProductName LIKE #ProductName) AND
(s.SampleNumber LIKE #SampleNumber) AND
(s.SampleDateTime >= #FromDate AND s.SampleDateTime < DATEADD(DAY, 1, #ToDate)
) AS x
PIVOT
(
MAX(ConstituentValue) FOR ConstituentName IN (' + #cols + ')
) AS p
ORDER BY SampleDateTime DESC;';
DECLARE #params NVARCHAR(MAX) = N'#numSamples INT, #fromDate DATE, ' +
'#toDate DATE, #ProductName NVARCHAR(50), #SampleNumber NVARCHAR(50)';
EXEC sp_executesql #sql, #params, #numSamples, #fromDate, #toDate,
#ProductName, #SampleNumber;
Note that your creation of #cols may include constituent names that don't appear in the table in the date range you selected (or with the other parts of the where clause), so if you don't want a bunch of columns with NULL values for every date selected, you may want to add some of those criteria to that query as well.

Dynamic SQL to generate column names?

I have a query where I'm trying pivot row values into column names and currently I'm using SUM(Case...) As 'ColumnName' statements, like so:
SELECT
SKU1,
SUM(Case When Sku2=157 Then Quantity Else 0 End) As '157',
SUM(Case When Sku2=158 Then Quantity Else 0 End) As '158',
SUM(Case When Sku2=167 Then Quantity Else 0 End) As '167'
FROM
OrderDetailDeliveryReview
Group By
OrderShipToID,
DeliveryDate,
SKU1
The above query works great and gives me exactly what I need. However, I'm writing out the SUM(Case... statements by hand based on the results of the following query:
Select Distinct Sku2 From OrderDetailDeliveryReview
Is there a way, using T-SQL inside a stored procedure, that I can dynamically generate the SUM(Case... statements from the Select Distinct Sku2 From OrderDetailDeliveryReview query and then execute the resulting SQL code?
Having answered a lot of these over the years by generating dynamic pivot SQL from the metadata, have a look at these examples:
SQL Dynamic Pivot - how to order columns
SQL Server 2005 Pivot on Unknown Number of Columns
What SQL query or view will show "dynamic columns"
How do I Pivot on an XML column's attributes in T-SQL
How to apply the DRY principle to SQL Statements that Pivot Months
In your particular case (using the ANSI pivot instead of SQL Server 2005's PIVOT feature):
DECLARE #template AS varchar(max)
SET #template = 'SELECT
SKU1
{COLUMN_LIST}
FROM
OrderDetailDeliveryReview
Group By
OrderShipToID,
DeliveryDate,
SKU1
'
DECLARE #column_list AS varchar(max)
SELECT #column_list = COALESCE(#column_list, ',') + 'SUM(Case When Sku2=' + CONVERT(varchar, Sku2) + ' Then Quantity Else 0 End) As [' + CONVERT(varchar, Sku2) + '],'
FROM OrderDetailDeliveryReview
GROUP BY Sku2
ORDER BY Sku2
Set #column_list = Left(#column_list,Len(#column_list)-1)
SET #template = REPLACE(#template, '{COLUMN_LIST}', #column_list)
EXEC (#template)
I know that SO search engine is not perfect, but your question has been answered in SQL Server PIVOT Column Data.
Also see Creating cross tab queries and pivot tables in SQL.
Why do this using hard coded column names when you can pull all this dynamically from any table?
Using UNPIVOT and COALESCE, I can dynamically pull a list of columns from any table and associated column values for any record in a record listing and combine them in a list of column names with values by row. Here is the code. Just drop in your database and table name. The column/value table will be generated for you in SQL Server. Keep in mind, to get a shared column of values for the columns you want to convert to sql variant or text strings. But a great way to get a sample column list of values with matching column names and types with our while loops or cursors. Its pretty fast:
-- First get a list of all known columns in your database, dynamically...
DECLARE #COLUMNS nvarchar(max)
SELECT #COLUMNS =
CASE
WHEN A.DATA_TYPE = 'nvarchar' OR A.DATA_TYPE = 'ntext' THEN
COALESCE(#COLUMNS + ',','') + 'CAST(CONVERT(nvarchar(4000),['+A.[name]+']) AS sql_variant) AS ['+A.[name]+']'
WHEN A.DATA_TYPE = 'datetime' OR A.DATA_TYPE = 'smalldatetime' THEN
COALESCE(#COLUMNS + ',','') + 'CAST(CONVERT(nvarchar,['+A.[name]+'],101) AS sql_variant) AS ['+A.[name]+']'
ELSE
COALESCE(#COLUMNS + ',','') + 'CAST(['+A.[name]+'] AS sql_variant) AS ['+A.[name]+']'
END
FROM
(
SELECT
A.name,
C.DATA_TYPE
FROM YOURDATABASENAME.dbo.syscolumns A
INNER JOIN YOURDATABASENAME.dbo.sysobjects B ON B.id = A.id
LEFT JOIN
(
SELECT
COLUMN_NAME,
DATA_TYPE
FROM YOURDATABASENAME.INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'YOURTABLENAME'
) C ON C.COLUMN_NAME = A.name
WHERE B.name = 'YOURTABLENAME'
AND C.DATA_TYPE <> 'timestamp'
) A
-- Test that the formatted columns list is returned...
--SELECT #COLUMNS
-- This gets a second string list of all known columns in your database, dynamically...
DECLARE #COLUMNS2 nvarchar(max)
SELECT #COLUMNS2 = COALESCE(#COLUMNS2 + ',','') + '['+A.[name]+']'
FROM
(
SELECT
A.name,
C.DATA_TYPE
FROM YOURDATABASENAME.dbo.syscolumns A
INNER JOIN YOURDATABASENAME.dbo.sysobjects B ON B.id = A.id
LEFT JOIN
(
SELECT
COLUMN_NAME,
DATA_TYPE
FROM YOURDATABASENAME.INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'YOURTABLENAME'
) C ON C.COLUMN_NAME = A.name
WHERE B.name = 'YOURTABLENAME'
AND C.DATA_TYPE <> 'timestamp'
) A
-- Test that the formatted columns list is returned...
--SELECT #COLUMNS2
-- Now plug in the list of the dynamic columns list into an UNPIVOT to get a Column Name / Column Value list table...
DECLARE #sql nvarchar(max)
SET #sql =
'
SELECT
ColumnName,ColumnValue
FROM
(
SELECT
'+#COLUMNS+'
FROM YOURDATABASENAME.dbo.YOURTABLENAME
WHERE CHANGE_ID IN (SELECT ChangeId FROM YOURDATABASENAME.dbo.OperatorProcess WHERE OperatorProcessID = 3)
) AS SourceTable
UNPIVOT
(
ColumnValue FOR ColumnName IN ('+#COLUMNS2+')
) AS PivotTable
'
EXEC (#sql)
-- Darshankar Madhusudan i can do dynamic columnheading table easly...
--thanks
declare #incr int = 1,
#col int,
#str varchar(max),
#tblcrt varchar(max),
#insrt varchar(max),
set #tblcrt = 'DECLARE #Results table ('
set #str = ''
set #insrt = ''
select #col = max(column_id) From tempdb.sys.all_columns where object_id = object_id('tempdb.dbo.#aaa')
while #incr <= #col
BEGIN
SELECT #STR = #STR +case when #incr = 1 then '''' else ',''' end +rtrim(ltrim(NAME))+'''' FROM TEMPDB.SYS.ALL_COLUMNS WHERE OBJECT_ID = OBJECT_ID('TEMPDB.DBO.#AAA') and column_id = #incr
set #tblcrt = #tblcrt + case when #incr = 1 then '' else ',' end + 'Fld'+CAST(#incr as varchar(3)) +' varchar(50)'
set #insrt = #insrt + case when #incr = 1 then '' else ',' end + 'Fld'+CAST(#incr as varchar(3))
SET #INCR = #INCR + 1
END
set #tblcrt = #tblcrt + ')'
set #insrt = 'insert into #Results('+#insrt+') values (' + #STR +')'
set #tblcrt = #tblcrt+ ';' + #insrt + 'select * from #Results '
exec(#tblcrt)