HierarchyID: Get all descendants for a list of parents - sql-server

I have a list of parent ids like this 100, 110, 120, 130 which is dynamic and can change. I want to get all descendants for specified parents in a single set. To get children for a single parent I used such query:
WITH parent AS (
SELECT PersonHierarchyID FROM PersonHierarchy
WHERE PersonID = 100
)
SELECT * FROM PersonHierarchy
WHERE PersonHierarchyID.IsDescendantOf((SELECT * FROM parent)) = 1
Have no idea how to do that for multiple parents. My first try was to write something like several unions, however I'm sure that there should be smarter way of doing this.
SELECT * FROM PersonHierarchy
WHERE PersonHierarchyID.IsDescendantOf(
(SELECT PersonHierarchyID FROM PersonHierarchy WHERE PersonID = 100)
) = 1
UNION ALL
SELECT * FROM PersonHierarchy
WHERE PersonHierarchyID.IsDescendantOf(
(SELECT PersonHierarchyID FROM PersonHierarchy WHERE PersonID = 110)
) = 1
UNION ALL ...
P.S. Also I found such query to select list of ids which might be helpful:
SELECT * FROM (VALUES (100), (110), (120), (130)) AS Parent(ParentID)
To summarize, my goal is to write query which accepts array of parent IDs as a parameter and returns all their descendants in a single set.

You're thinking too hard.
WITH parent AS (
SELECT PersonHierarchyID FROM PersonHierarchy
WHERE PersonID in (<list of parents>)
)
SELECT * FROM PersonHierarchy
WHERE PersonHierarchyID.IsDescendantOf((SELECT * FROM parent)) = 1
I'd write it like this, though:
select child.*
from PersonHierarchy as parent
inner join PersonHierarchy as child
on child.PersonHierarchyID.IsDescendantOf(
parent.PersonHierarchyId
) = 1
where Parent.PersonId in (<list of parents>)
Note: in both cases, this could be slow as it has to evaluate IsDescendantOf for n*m entries (with n being the cardinality of the list of parents and m being the cardinality of the table).
I recently had a similar problem and I solved it by writing a table-valued function that, given a hierarchyId would return all of the parents. Let's look at a solution to your problem using that approach. First, the function:
CREATE FUNCTION [dbo].[GetAllAncestors] (#h HierarchyId, #IncludeSelf bit)
RETURNS TABLE
AS RETURN
WITH cte AS (
SELECT #h AS h, 1 AS IncludeSelf
)
SELECT #h.GetAncestor(n.NumberId) AS Hierarchy
FROM ref.Number AS n
WHERE n.NumberId <= #h.GetLevel()
AND n.NumberId >= 1
UNION ALL
SELECT h
FROM cte
WHERE IncludeSelf = #IncludeSelf
It assumes that you have a Numbers table. They're immensely useful. If you don't have one, look at the accepted answer here. Let's talk about that function for a second. In essence, it says "For the passed in hierarchyId, get the current level. Then get call GetAncestor until you're at the top of the hierarchy.". Note that it optionally returns the passed in hierarchyId. In my case, I wanted to consider a record an ancestor of itself. You may or may not want to.
Moving onto a solution that uses this, we get something like:
select child.*
from PersonHierarchy as child
cross apply [dbo].[GetAllAncestors](child.PersonHierarchyId, 0) as ancestors
inner join PersonHierarchy as parent
on parent.PersonHierarchyId = ancestors.Hierarchy
where parent.PersonId in (<list of parents>)
It may or may not work for you. Try it out and see!

It might be useful for someone. I found way of doing this by self-joining query:
SELECT p2.* FROM PersonHierarchy p1
LEFT JOIN PersonHierarchy p2
ON p2.PersonHierarchyID.IsDescendantOf(p1.PersonHierarchyID) = 1
WHERE
p1.PersonID IN (100, 110, 120, 130)

You can use this query
Select
child.*,
child.[PersonHierarchyID].GetLevel(),
child.[PersonHierarchyID].GetAncestor(1)
From
PersonHierarchy as parents
Inner Join PersonHierarchy as child
On child.[PersonHierarchyID].IsDescendantOf(parents.[PersonHierarchyID] ) = 1
Where
parents.[PersonHierarchyID] = 0x68

Please check, this should work for you. I havent tried to modify your script but just put the query in loop. Hope it helps.
DECLARE #String VARCHAR(MAX) = '100, 110, 120, 130'
DECLARE #SQL VARCHAR(MAX)
SET #String = REPLACE(#String, CHAR(32), '') + ','
WHILE CHARINDEX(',', #String) > 0
BEGIN
DECLARE #ToString INT
DECLARE #StringLength INT
DECLARE #WorkingString VARCHAR(MAX)
DECLARE #WorkingLength INT
SET #ToString = CHARINDEX(',', #String)
SET #StringLength = LEN(#String)
SET #WorkingString = SUBSTRING(#String, 1, #ToString - 1)
SET #String = SUBSTRING(#String, #ToString + 1, #StringLength)
SET #WorkingString = 'SELECT * FROM PersonHierarchy ' + CHAR(13) + CHAR(10)
+ 'WHERE PersonHierarchyID.IsDescendantOf((SELECT PersonHierarchyID FROM PersonHierarchy WHERE PersonID = '
+ #WorkingString + ')) = 1' + CHAR(13) + CHAR(10)
+ CASE WHEN CHARINDEX(',', #String) > 0 THEN 'UNION ALL'+ CHAR(13) + CHAR(10) ELSE '' END
SET #SQL = ISNULL(#SQL,'') + #WorkingString
END
PRINT #SQL
EXEC (#SQL)

Related

Paginating a parent in SQL Server on a parent/child query

(SQL Server 2012 - Web Edition)
I have a parent/child (one to many) relationship in a query like so:
SELECT a.a, a.b, b.c
FROM tablea INNER JOIN
tableb ON b.pk = a.fk
I have a huge pagination query that encompasses this using the standard (psuedo-code):
WITH C as (SELECT top(#perpage*#pagenum) rowID = row_number() OVER (somefield)),
SELECT c.* FROM C (query) WHERE DT_RowId > (#pagenum-1)*#perpage
The question I have is in this scenario is it possible to paginate off the parent table (a), instead of the entire query? Can I modify my pagination query (not the sql that pulls the query itself) so that when I ask for 10 rows, it gives me 10 rows from the parent, with 'x' number of children attached?
I know I'm not giving the bigger picture here, but the bigger picture is ugly. If need be, we can go there, but it's out there. Here's a small taste of where we're going with this:
IF UPPER(LEFT(#rSQL, 6)) = 'SELECT'
BEGIN
SET #rSQL = 'SELECT * FROM (' + #rSQL + ')' + ' as rTBL';
SET #rSQL = RIGHT(#rSQL, LEN(#rSQL)-7);
IF (LEN(LTRIM(#search)) > 0)
BEGIN
SET #rPaging =
'IF (#schemaonly=1) SET FMTONLY ON;
SELECT #ttlrows = COUNT(*) FROM (SELECT ' + #rSQL + #rWhere + ') AS TBL;
WITH C as (select top(#perpage*#pagenum) DT_RowId = ROW_NUMBER() OVER (' + #rOrder + '), ';
SET #rPaging = #rPaging + #rSQL + #rWhere + ')
SELECT C.*' + #rcols + ', (#perpage-1) * #pagenum as pagenum, #ttlrows as ct, CEILING(#ttlrows / CAST(#perpage AS FLOAT)) as pages
FROM C '+ #query + ' WHERE DT_RowId > (#pagenum-1) * #perpage ';
END
ELSE
BEGIN
SET #rPaging =
'IF (#schemaonly=1) SET FMTONLY ON;
SELECT #ttlrows = COUNT(*) FROM (' + #oSQL + ') AS SUBQUERY;
WITH C as (select top(#perpage*#pagenum) DT_RowId = ROW_NUMBER() OVER (' + #rOrder + '), ';
SET #rPaging = #rPaging + #rSQL + ')
SELECT C.*' + #rcols + ',(#perpage-1) * #pagenum as pagenum, #ttlrows as ct, CEILING(#ttlrows / CAST(#perpage AS FLOAT)) as pages
FROM C '+ #query + ' WHERE DT_RowId > (#pagenum-1) * #perpage ';
END
PRINT #rPaging;
EXECUTE SP_EXECUTESQL #rPaging, #parms, #ttlrows out, #schemaonly, #perpage, #pagenum, #fksiteID, #filter1, #filter2, #filter3, #filter4, #intfilter1, #intfilter2, #intfilter3, #intfilter4, #datefilter1, #datefilter2, #search;
SET FMTONLY OFF;
END
ELSE
BEGIN
SET #rSQL = LTRIM(REPLACE(UPPER(#rSQL), 'EXEC',''));
EXECUTE SP_EXECUTESQL #rSQL, #parms, #ttlrows out, #schemaonly, #perpage, #pagenum, #fksiteID, #filter1, #filter2, #filter3, #filter4, #intfilter1, #intfilter2, #intfilter3, #intfilter4, #datefilter1, #datefilter2;
END
You could do the pagination in a CTE that only gets the parent rows, and then join the child rows in a subsequent CTE or in the main query.
Due to the dynamic way you are using this, this might have to involve building your pagination query from the same building blocks you use to build #query. Without seeing the code that builds #query I can't be much more specific than that.
You could add
,DENSE_RANK() OVER (ORDER BY table_a.primary_key)
This would indirectly provide the same result as
,ROW_NUMBER() OVER(ORDER BY table_a.primary_key)
but the former would be on the final result set instead going back to table a for the latter code snippet.
But please be aware of the disadvantage: any additional ranking function will force an additional sort operation on the result set! This might significantly influence the query performance. If this is the case in your scenario, I'd recommend to follow Tab Allemans solution and use a cte.

T-SQL - Update first letter in each word of a string that are not 'or', 'of' or 'and' to uppercase. Lowercase 'or', 'of' or 'and' if found

Given the below table and data:
IF OBJECT_ID('tempdb..#Temp') IS NOT NULL
DROP TABLE #Temp
CREATE TABLE #Temp
(
ID INT,
Code INT,
PDescription VARCHAR(2000)
)
INSERT INTO #Temp
(ID,
Code,
PDescription)
VALUES (1,0001,'c and d, together'),
(2,0002,'equals or Exceeds $27.00'),
(3,0003,'Fruit Evaporating Or preserving'),
(4,0004,'Domestics And domestic Maintenance'),
(5,0005,'Bakeries and cracker')
SELECT *
FROM #Temp
DROP TABLE #Temp
Output:
ID Code PDescription
1 1 c and d, together
2 2 equals or Exceeds $27.00
3 3 Fruit Evaporating Or preserving
4 4 Domestics And domestic Maintenance
5 5 Bakeries and cracker
I need a way to achieve the below update to the description field:
ID Code PDescription
1 1 C and D, Together
2 2 Equals or Exceeds $27.00
3 3 Fruit Evaporating or Preserving
4 4 Domestics and Domestic Maintenance
5 5 Bakeries and Cracker
If you fancied going the SQL CLR route the function could look something like
using System.Data.SqlTypes;
using System.Text.RegularExpressions;
public partial class UserDefinedFunctions
{
//One or more "word characters" or apostrophes
private static readonly Regex _regex = new Regex("[\\w']+");
[Microsoft.SqlServer.Server.SqlFunction]
public static SqlString ProperCase(SqlString subjectString)
{
string resultString = null;
if (!subjectString.IsNull)
{
resultString = _regex.Replace(subjectString.ToString().ToLowerInvariant(),
(Match match) =>
{
var word = match.Value;
switch (word)
{
case "or":
case "of":
case "and":
return word;
default:
return char.ToUpper(word[0]) + word.Substring(1);
}
});
}
return new SqlString(resultString);
}
}
Doubtless there may be Globalization issues in the above but it should do the job for English text.
You could also investigate TextInfo.ToTitleCase but that still leaves you needing to handle your exceptions.
The following function is not the most elegant of solutions but should do what you want.
ALTER FUNCTION [dbo].[ToProperCase](#textValue AS NVARCHAR(2000))
RETURNS NVARCHAR(2000)
AS
BEGIN
DECLARE #reset BIT;
DECLARE #properCase NVARCHAR(2000);
DECLARE #index INT;
DECLARE #character NCHAR(1);
SELECT #reset = 1, #index=1, #properCase = '';
WHILE (#index <= len(#textValue))
BEGIN
SELECT #character= substring(#textValue,#index,1),
#properCase = #properCase + CASE WHEN #reset=1 THEN UPPER(#character) ELSE LOWER(#character) END,
#reset = CASE WHEN #character LIKE N'[a-zA-Z\'']' THEN 0 ELSE 1 END,
#index = #index +1
END
SET #properCase = N' ' + #properCase + N' ';
SET #properCase = REPLACE(#properCase, N' And ', N' and ');
SET #properCase = REPLACE(#properCase, N' Or ', N' or ');
SET #properCase = REPLACE(#properCase, N' Of ', N' of ');
RETURN RTRIM(LTRIM(#properCase))
END
Example use:
IF OBJECT_ID('tempdb..#Temp') IS NOT NULL
DROP TABLE #Temp
CREATE TABLE #Temp
(
ID INT,
Code INT,
PDescription VARCHAR(2000)
)
INSERT INTO #Temp
(ID,
Code,
PDescription)
VALUES (1,0001, N'c and d, together and'),
(2,0002, N'equals or Exceeds $27.00'),
(3,0003, N'Fruit Evaporating Or preserving'),
(4,0004, N'Domestics And domestic Maintenance'),
(5,0005, N'Bakeries and cracker')
SELECT ID, Code, dbo.ToProperCase(PDescription) AS [Desc]
FROM #Temp
DROP TABLE #Temp
If you want to convert your text to proper case before inserting into table, then simply call function as follow:
INSERT INTO #Temp
(ID,
Code,
PDescription)
VALUES (1,0001, dbo.ToProperCase( N'c and d, together and')),
(2,0002, dbo.ToProperCase( N'equals or Exceeds $27.00')),
(3,0003, dbo.ToProperCase( N'Fruit Evaporating Or preserving')),
(4,0004, dbo.ToProperCase( N'Domestics And domestic Maintenance')),
(5,0005, dbo.ToProperCase( N'Bakeries and cracker'))
This is a dramatically modified version of my Proper UDF. The good news is you may be able to process the entire data-set in ONE SHOT rather than linear.
Take note of #OverR (override)
Declare #Table table (ID int,Code int,PDescription varchar(150))
Insert into #Table values
(1,1,'c and d, together'),
(2,2,'equals or Exceeds $27.00'),
(3,3,'Fruit Evaporating Or preserving'),
(4,4,'Domestics And domestic Maintenance'),
(5,5,'Bakeries and cracker')
-- Generate Base Mapping Table - Can be an Actual Table
Declare #Pattn table (Key_Value varchar(25));Insert into #Pattn values (' '),('-'),('_'),(','),('.'),('&'),('#'),(' Mc'),(' O''') -- ,(' Mac')
Declare #Alpha table (Key_Value varchar(25));Insert Into #Alpha values ('A'),('B'),('C'),('D'),('E'),('F'),('G'),('H'),('I'),('J'),('K'),('L'),('M'),('N'),('O'),('P'),('Q'),('R'),('S'),('T'),('U'),('V'),('W'),('X'),('Y'),('X')
Declare #OverR table (Key_Value varchar(25));Insert Into #OverR values (' and '),(' or '),(' of ')
Declare #Map Table (MapSeq int,MapFrom varchar(25),MapTo varchar(25))
Insert Into #Map
Select MapSeq=1,MapFrom=A.Key_Value+B.Key_Value,MapTo=A.Key_Value+B.Key_Value From #Pattn A Join #Alpha B on 1=1
Union All
Select MapSeq=99,MapFrom=A.Key_Value,MapTo=A.Key_Value From #OverR A
-- Convert Base Data Into XML
Declare #XML xml
Set #XML = (Select KeyID=ID,String=+' '+lower(PDescription)+' ' from #Table For XML RAW)
-- Convert XML to varchar(max) for Global Search & Replace
Declare #String varchar(max)
Select #String = cast(#XML as varchar(max))
Select #String = Replace(#String,MapFrom,MapTo) From #Map Order by MapSeq
-- Convert Back to XML
Select #XML = cast(#String as XML)
-- Generate Final Results
Select KeyID = t.col.value('#KeyID', 'int')
,NewString = ltrim(rtrim(t.col.value('#String', 'varchar(150)')))
From #XML.nodes('/row') AS t (col)
Order By 1
Returns
KeyID NewString
1 C and D, Together
2 Equals or Exceeds $27.00
3 Fruit Evaporating or Preserving
4 Domestics and Domestic Maintenance
5 Bakeries and Cracker
You don't even need functions and temporary objects. Take a look at this query:
WITH Processor AS
(
SELECT ID, Code, 1 step,
CONVERT(nvarchar(MAX),'') done,
LEFT(PDescription, CHARINDEX(' ', PDescription, 0)-1) process,
SUBSTRING(PDescription, CHARINDEX(' ', PDescription, 0)+1, LEN(PDescription)) waiting
FROM #temp
UNION ALL
SELECT ID, Code, step+1,
done+' '+CASE WHEN process IN ('and', 'or', 'of') THEN LOWER(process) ELSE UPPER(LEFT(process, 1))+LOWER(SUBSTRING(process, 2, LEN(process))) END,
CASE WHEN CHARINDEX(' ', waiting, 0)>0 THEN LEFT(waiting, CHARINDEX(' ', waiting, 0)-1) ELSE waiting END,
CASE WHEN CHARINDEX(' ', waiting, 0)>0 THEN SUBSTRING(waiting, CHARINDEX(' ', waiting, 0)+1, LEN(waiting)) ELSE NULL END FROM Processor
WHERE process IS NOT NULL
)
SELECT ID, Code, done PSDescription FROM
(
SELECT *, ROW_NUMBER() OVER (PARTITION BY ID ORDER BY step DESC) RowNum FROM Processor
) Ordered
WHERE RowNum=1
ORDER BY ID
It produces desired result as well. You can SELECT * FROM Processor to see all steps executed.

SQL Server 2012 Concatenating Strings from Multiple Rows

[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.

How to compare two comma-separated strings, and return TRUE if there is at least 1 match

I have to variables that contain comma-separated strings:
#v1 = 'hello, world, one, two'
#v2 = 'jump, down, yes, one'
I need a function that will return TRUE if there is at least one match. So in the above example, it would return TRUE since the value 'one' is in both strings.
Is this possible in SQL?
Use a split function (many examples here - CLR is going to be your best option in most cases back before SQL Server 2016 - now you should use STRING_SPLIT()).
Once you have a split function, the rest is quite easy. The model would be something like this:
DECLARE #v1 VARCHAR(MAX) = 'hello, world, one, two',
#v2 VARCHAR(MAX) = 'jump, down, yes, one';
SELECT CASE WHEN EXISTS
(
SELECT 1
FROM dbo.Split(#v1) AS a
INNER JOIN dbo.Split(#v2) AS b
ON a.Item = b.Item
)
THEN 1 ELSE 0 END;
You can even reduce this to only call the function once:
SELECT CASE WHEN EXISTS
(
SELECT 1 FROM dbo.Split(#v1)
WHERE ', ' + LTRIM(#v2) + ','
LIKE '%, ' + LTRIM(Item) + ',%'
) THEN 1 ELSE 0 END;
On 2016+:
SELECT CASE WHEN EXISTS
(
SELECT 1 FROM STRING_SPLIT(#v1, ',')
WHERE ', ' + LTRIM(#v2) + ','
LIKE '%, ' + LTRIM([Value]) + ',%'
) THEN 1 ELSE 0 END;
You can use CTEs to split your string into xml nodes, then insert the words into table variables. Joining the table variables will reveal any matches
DECLARE #v1 VARCHAR(200) = 'hello, world, one, two'
DECLARE #v2 VARCHAR(200) = 'jump, down, yes, one'
DECLARE #v1Words TABLE (word VARCHAR(100))
DECLARE #v2Words TABLE (word VARCHAR(100))
;WITH cteSplitV1 AS(
SELECT CAST('<word>' + REPLACE(#v1,', ','</word><word>') + '</word>' AS XML) AS words)
INSERT INTO #v1Words(word)
SELECT word.x.value('.','VARCHAR(100)') AS [word]
FROM cteSplitV1
CROSS APPLY words.nodes('/word') AS word(x)
;WITH cteSplitV2 AS(
SELECT CAST('<word>' + REPLACE(#v2,', ','</word><word>') + '</word>' AS XML) AS words)
INSERT INTO #v2Words(word)
SELECT word.x.value('.','VARCHAR(100)') AS [word]
FROM cteSplitV2
CROSS APPLY words.nodes('/word') AS word(x)
SELECT *
FROM #v1Words v1
JOIN #v2Words v2
ON v1.word = v2.word

SQL Server 2005 - column names based on variable passed in pivot query

I have following query which takes 2 parameters.
YearNumber
MonthNumber
In my pivot query, I am trying to select columns based on #Year_Rtl variable. I need to select data for the year passed, last year and last last year. Since the data being displayed on UI is table format divided by #Year_Rtl, I decided to write a pivot query for that as below.
In the query, it works fine if I hard code [#Year_Rtl], [#Year_Rtl - 1], [#Year_Rtl - 2] to [2012], [2011], [2010]. But since the year passed can be anything, I want columns to be named dynamically.
DECLARE #Month_Rtl int
DECLARE #Year_Rtl int
SET #Year_Rtl = 2012
SET #Month_Rtl = 1
SELECT
'Data 1', [#Year_Rtl], [#Year_Rtl - 1], [#Year_Rtl - 2]
FROM
(SELECT [Yr_No], Qty
FROM dbo.Table1 t
WHERE (t.Col1 = 10) AND
(t.Col2 = '673') AND
((t.Mth_No = #Month_Rtl AND t.Yr_No = #Year_Rtl) OR
(t.Mth_No = 12 AND t.Yr_No IN (#Year_Rtl - 1, #Year_Rtl - 2)))
) p PIVOT (SUM(Qty)
FOR [Yr_No] IN ([#Year_Rtl], [#Year_Rtl-1], [#Year_Rtl-2])
) AS pvt
Above query throws following errors:
Error converting data type nvarchar to smallint.
The incorrect value "#Year_Rtl" is supplied in the PIVOT operator.
Invalid column name '#Year_Rtl - 1'.
Invalid column name '#Year_Rtl - 2'.
Since you can use dynamic SQL, I'd go with a macro-replacement approach. You're identifying areas of the query that must be dynamically replaced with placeholders (e.g. $$Year_Rtl) and then calculating their replacement values below. I find that it keeps the SQL statement easy to follow.
DECLARE #SQL NVarChar(2000);
SELECT #SQL = N'
SELECT
''Data 1'', [$$Year_Rtl], [$$Year_RtlM1], [$$Year_RtlM2]
FROM
(SELECT [Yr_No], Qty
FROM dbo.Table1 t
WHERE (t.Col1 = 10) AND
(t.Col2 = ''673'') AND
((t.Mth_No = $$Month_Rtl AND t.Yr_No = $$Year_Rtl) OR
(t.Mth_No = 12 AND t.Yr_No IN ($$Year_RtlM1, $$Year_RtlM2)))
) p PIVOT (SUM(Qty)
FOR [Yr_No] IN ([$$Year_Rtl], [$$Year_RtlM1], [$$Year_RtlM2])
) AS pvt';
SELECT #SQL = REPLACE(#SQL, '$$Year_RtlM2', #Year_Rtl - 2);
SELECT #SQL = REPLACE(#SQL, '$$Year_RtlM1', #Year_Rtl - 1);
SELECT #SQL = REPLACE(#SQL, '$$Year_Rtl', #Year_Rtl);
SELECT #SQL = REPLACE(#SQL, '$$Month_Rtl', #Month_Rtl);
PRINT #SQL;
-- Uncomment the next line to allow the built query to execute...
--EXECUTE sp_ExecuteSQL #SQL;
Since consuming code will also have to be flaky under this scheme (e.g. selecting columns based on "position" rather than name) - why not normalize the columns by performing a DATEDIFF(year,Yr_No,#Year_Rtl), and work from there? Those columns will always be 0, -1 and -2...
You need to look into Dynamic SQL Pivoting.
I recommend reading Itzik Ben-Gan's T-SQL Fundamentals where he goes over how to do this.
Alternatively try this article if you don't want to buy the book.
Maybe this will help:
First getting the columns with a tally function like this:
DECLARE #Month_Rtl int,
#Year_Rtl int,
#Year_Rtl_Start INT,
#cols VARCHAR(MAX),
#values VARCHAR(MAX)
SET #Year_Rtl = 2012
SET #Month_Rtl = 1
SET #Year_Rtl_Start=2009
;WITH Years ( n ) AS (
SELECT #Year_Rtl_Start UNION ALL
SELECT 1 + n FROM Years WHERE n < #Year_Rtl )
SELECT
#cols = COALESCE(#cols + ','+QUOTENAME(n),
QUOTENAME(n)),
#values = COALESCE(#values + ','+CAST(n AS VARCHAR(100)),
CAST(n AS VARCHAR(100)))
FROM
Years
ORDER BY n DESC
The variable #cols contains the columns that is in the pivot and the variable #values contains the years for the IN. The #Year_Rtl is the end year and the #Year_Rtl_Start is the start for you range.
Then declaring and executing the dynamic pivot like this:
DECLARE #query NVARCHAR(4000)=
N'SELECT
''Data 1'', '+#cols+'
FROM
(
SELECT
[Yr_No], Qty
FROM
dbo.Table1 t
WHERE
t.Col1 = 10
AND t.Col2 = ''673''
AND
(
(
t.Mth_No = '+CAST(#Month_Rtl AS VARCHAR(10))+'
AND t.Yr_No = '+CAST(#Year_Rtl AS VARCHAR(10))+'
)
OR
(
t.Mth_No = 12
AND t.Yr_No IN ('+#values+'))
)
) p
PIVOT
(
SUM(Qty)
FOR [Yr_No] IN ('+#cols+')
) AS pvt'
EXECUTE(#query)

Resources