I have worked out how to pivot a row from a table using PIVOT in SQL Server 2005, however, I dont like the method as I have had to hard code the columns (Currency Codes) and I would like to have the Pivot select the columns dynamically.
As an Example, imagine you have the following table (called OrderCash):
OrderID CAD CHF EUR GBP JPY NOK USD
40 0 0 128.6 552.25 -9232 0 -4762
41 0 0 250.2 552.25 -9232 0 -4762
42 233.23 0 552.25 -9232 0 0 -4762
The hard-coded Pivot statement is:
SELECT OrderID,
CurrCode + 'GBP CURNCY' AS Ticker,
Cash AS Position
FROM
(
SELECT OrderID,
CAD,
CHF,
EUR,
GBP,
JPY,
NOK,
USD
FROM OrderCash
) p
UNPIVOT
(
Cash FOR CurrCode IN
(CAD, CHF, EUR, GBP, JPY, NOK, USD)
) AS unpvt
WHERE Cash != 0
And OrderID = 42
This would return the following required table:
OrderID Ticker Position
42 CADGBP CURNCY 233.23
42 EURGBP CURNCY 552.25
42 GBPGBP CURNCY -9232
42 USDGBP CURNCY -4762
The problem arrises further down the road when someone tells me I need to have AUD as a new currency in the table?
FYI, I have a table-valued function that returns all the column names as a table:
ALTER FUNCTION [dbo].[GetTableColumnNames]
(
#TableName NVARCHAR(250),
#StartFromColumnNum INT
)
RETURNS #ReturnTable TABLE
(
ColName NVARCHAR(250)
)
AS
BEGIN
INSERT INTO #ReturnTable
SELECT COLUMN_NAME from information_schema.columns
WHERE TABLE_NAME = #TableName
AND ORDINAL_POSITION >= #StartFromColumnNum
ORDER BY ORDINAL_POSITION
RETURN
END
So the easy bit has been done (SELECT * FROM dbo.GetTableColumnNames('OrderCash',2)), the problem I am having is inserting this 'dynamic' table with the column names into the Pivot?
Any help would be much appreciated.
Many thanks
Bertie.
I've done a few too many of these dynamic queries of late... (my columns shift by client by month). Here's one way to do it--no testing, no debugging, there might be a few bugs to iron out:
DECLARE
#Command nvarchar(max)
,#ColumnList nvarchar(max)
,#OrderId int
,#Debug bit
-- Build a comman-delimited list of the columns
SELECT #ColumnList = isnull(#ColumnLIst + ',', , '') + ColName
from dbo.GetTableColumnNames('OrderCash', 2)
-- Insert the list of columns in two places in your query
SET #Command = replace('
SELECT OrderID,
CurrCode + ‘‘GBP CURNCY’‘ AS Ticker,
Cash AS Position
FROM
(
SELECT OrderID, <#ColumnList>
FROM OrderCash
) p
UNPIVOT
(
Cash FOR CurrCode IN
(<#ColumnList>)
) AS unpvt
WHERE Cash != 0
And OrderID = #OrderId
', '<#ColumnList>', #ColumnList)
-- Always include something like this!
IF #Debug = 1
PRINT #Command
-- Using sp_executeSQL over EXECUTE (#Command) allows you execution
-- plan resuse with parameter passing (this is the part you may need
-- to debug on a bit, but it will work)
EXECUTE sp_executeSQL #Command, N'#OrderId int', #OrderId
Related
I am working with healthcare data that comes from a table that only has one column for procedure code billed, but multiple lines of transactions for each claim when multiple procedure codes are billed.
The specific issue I'm struggling with is I want to see up to three (possibly more, depending on how complicated the answer is) procedure codes for each grouped ClaimID, as well as a sum of the Amount on only one row per ClaimID. Is there a way to achieve this? I have attempted to use case statements in my select, as well as PIVOT and ROW_NUMBER functions without any luck.
When I run the following simple query, the results look like this:
select originalclaimid, procedurecode, sum(amount) as 'AR Outstanding'
from TABLE
group by ORIGINALCLAIMID, PROCEDURECODE
ClaimID
ProcedureCode
AROutstanding
1234
99599
20.00
1234
89898
0
1234
77878
10.00
2344
11112
0
2344
12223
5.00
3335
45454
0
The output I desire is:
ClaimID
ProcedureCode1
ProcedureCode2
ProcedureCode3
AROutstanding
1234
99599
89898
77878
30.00
2344
11112
12223
N/A (NULL is OK)
5.00
3335
45454
N/A
N/A
0
Ok, forget that last post. I think this will work for you.
--drop table testing
--drop table #tmpStud
create table testing (
ClaimID INT
, ProcedureCode INT
, AROutstanding INT
)
INSERT INTO testing (ClaimID, ProcedureCode, AROutstanding) VALUES
(1234,99599,20)
, (1234,89898,0)
, (2344,77878,10)
, (2344,12223,5)
, (3335,45454,0)
select
'Procedure' + CONVERT(NVARCHAR(150),ROW_NUMBER () OVER (PARTITION BY ClaimID ORDER BY ClaimID)) AS ClaimNo
, *
into #tmpStud
from testing
select * from #tmpStud
declare #distinct nvarchar(max) = ''
declare #max int = 33
, #loop int = 1
while (#loop < #max)
begin
if(#loop = 1) begin
set #distinct = #distinct + '[Procedure' + Convert(nvarchar(20),#loop) + ']'
set #loop = #loop + 1
end
else begin
set #distinct = #distinct + ',[Procedure' + Convert(nvarchar(20),#loop) + ']'
set #loop = #loop + 1
end
end
print(#distinct)
exec ('
select
*
from (
select
*
FROM #tmpStud
) AS s PIVOT
(
MAX(ClaimID)
FOR ClaimNo IN (' + #distinct + ')
) AS pvt
')
I have two tables which I have simplified below for clarity. One stores data values while the other defines the units and type of data. Some tests have one result, others may have more (My actual table has results 1-10):
Table 'Tests':
ID Result1 Result2 TestType(FK to TestTypes Type)
---------- ------------ ----------- -----------
1001 50 29 1
1002 90.9 NULL 2
1003 12.4 NULL 2
1004 20.2 30 1
Table 'TestTypes':
Type TestName Result1Name Result1Unit Result2Name Result2Unit ..........
------- --------- ------------ ----------- ------------ -----------
1 Temp Calib. Temperature F Variance %
2 Clarity Turbidity CU NULL NULL
I would like to use the ResultXName as the column alias when I join the two tables. In other words, if a user wants to see all Type 1 'Temp Calib' tests, the data would be formatted as follows:
Temperature Variance
------------ -----------
50 F 10.1%
20.2 F 4.4%
Or if they look at Type 2, which only uses 1 result and should ignore the NULL:
Turbidity
----------
90.9 CU
12.4 CU
I have had some success in combining the two columns of the tables:
SELECT CONCAT(Result1, ' ', ISNULL(Result1Unit, ''))
FROM Tests
INNER JOIN TestTypes ON Tests.TestType = TestTypes.Type
But I cannot figure out how to use the TestName as the new column alias. This is what I've been trying using a subquery, but it seems subqueries are not allowed in the AS clause:
SELECT CONCAT(Result1, ' ', ISNULL(Result1Unit, '')) AS (SELECT TOP(1) Result1Name FROM TestTypes WHERE Type = 1)
FROM Tests
INNER JOIN TestTypes ON Tests.TestType = TestTypes.Type
Is there a different method I can use? Or do I need to restructure my data to achieve this? I am using MSSQL.
Yes, this can be fully automated by constructing a dynamic SQL string carefully. The key points in this solution and references is listed as follows.
Count the Result variables (section 1.)
Get the new column name of ResultXName by using sp_executesql with the output definition (section 2-1)
Append the clause for the new column (section 2-2)
N.B.1. Although a dynamic table schema is usually considered a bad design, sometimes people are simply ordered to do that. Therefore I do not question the adequacy of this requirement.
N.B.2. Mind the security problem of arbitrary string execution. Additional string filters may be required depending on your use case.
Test Dataset
use [testdb];
GO
if OBJECT_ID('testdb..Tests') is not null
drop table testdb..Tests;
create table [Tests] (
[ID] int,
Result1 float,
Result2 float,
TestType int
)
insert into [Tests]([ID], Result1, Result2, TestType)
values (1001,50,29,1),
(1002,90.9,NULL,2),
(1003,12.4,NULL,2),
(1004,20.2,30,1);
if OBJECT_ID('testdb..TestTypes') is not null
drop table testdb..TestTypes;
create table [TestTypes] (
[Type] int,
TestName varchar(50),
Result1Name varchar(50),
Result1Unit varchar(50),
Result2Name varchar(50),
Result2Unit varchar(50)
)
insert into [TestTypes]([Type], TestName, Result1Name, Result1Unit, Result2Name, Result2Unit)
values (1,'Temp Calib.','Temperature','F','Variance','%'),
(2,'Clarity','Turbidity','CU',NULL,NULL);
--select * from [Tests];
--select * from [TestTypes];
Solution
/* Input Parameter */
declare #type_no int = 1;
/* 1. determine the number of Results */
declare #n int;
-- If there are hundreds of results please use the method as of (2-1)
select #n = LEN(COALESCE(LEFT(Result1Name,1),''))
+ LEN(COALESCE(LEFT(Result2Name,1),''))
FROM [TestTypes]
where [Type] = #type_no;
/* 2. build dynamic query string */
-- cast type number as string
declare #s_type varchar(10) = cast(#type_no as varchar(10));
-- sql query string
declare #sql nvarchar(max) = '';
declare #sql_colname nvarchar(max) = '';
-- loop variables
declare #i int = 1; -- loop index
declare #s varchar(10); -- stringified #i
declare #colname varchar(max); -- new column name
set #sql += '
select
L.[ID]';
-- add columns one by one
while #i <= #n begin
set #s = cast(#i as varchar(10));
-- (2-1) find the new column name
SET #sql_colname = N'select #colname = Result' + #s + 'Name
from [TestTypes]
where [Type] = ' + #s_type;
EXEC SP_EXECUTESQL
#Query = #sql_colname,
#Params = N'#colname varchar(max) OUTPUT',
#colname = #colname OUTPUT;
-- (2-2) sql clause of the new column
set #sql += ',
cast(L.Result' + #s + ' as varchar(10)) + '' '' + R.Result' + #s + 'Unit as [' + #colname + ']'
-- next Result
set #i += 1
end
set #sql += '
into [ans]
from [Tests] as L
inner join [TestTypes] as R
on L.TestType = R.Type
where R.[Type] = ' + #s_type;
/* execute */
print #sql; -- check the query string
if OBJECT_ID('testdb..ans') is not null
drop table testdb..ans;
exec sp_sqlexec #sql;
/* show */
select * from [ans];
Result (type = 1)
| ID | Temperature | Variance |
|------|-------------|----------|
| 1001 | 50 F | 29 % |
| 1004 | 20.2 F | 30 % |
/* the query string */
select
L.[ID],
cast(L.Result1 as varchar(10)) + ' ' + R.Result1Unit as [Temperature],
cast(L.Result2 as varchar(10)) + ' ' + R.Result2Unit as [Variance]
into [ans]
from [Tests] as L
inner join [TestTypes] as R
on L.TestType = R.Type
where R.[Type] = 1
Tested on SQL Server 2017 (linux docker image, latest version) on debian 10
This question already has an answer here:
How to replace a string with values from columns in a table in SQL
(1 answer)
Closed 9 years ago.
Exp Major Start
__________________________________________________________
| |
'My names are W.Major and W.Start' | Hal | Bark
___________________________________|________|_________________
'W.Major is a doctor' | Mark | Slope
___________________________________|________|_______________
Hi All suppose I have the table above in SQL server management studio
and for any text in the Exp column I want to replace W.Major with the value in the Major column and wherever there is a W.Start I want to replace it with the value in the Start column.
Do you know what type of SP I have to write to get this accomplished?
Well you can use a Dynamic SQL and UNPIVOT to get table with all unit replacements and then you run a while loop to replace expressions one by one till you get your result.
I am sure there can be better techniques but here is my solution.
Please mark it as answer for my effort :)
IF OBJECT_ID ('tempdb.dbo.#temptable') IS NOT NULL DROP TABLE #temptable
CREATE TABLE #tempTable ( ID INT IDENTITY(1, 1), [Exp] nvarchar(4000) not NULL, replacementWord nvarchar(50) not null, Wordvalue nvarchar(50) not null, flag bit null, ReplacedExp nvarchar(4000) null)
DECLARE #query VARCHAR(4000)
DECLARE #queryRWords VARCHAR(2000)
SELECT #queryRWords =
STUFF((select DISTINCT '],['+ LTRIM(C.name) from sys.Columns C INNER JOIN sys.objects O on O.object_id=C.object_id where O.name='yourtable' and O.type_desc='USER_TABLE' and C.name not like 'Exp' ORDER BY '],['+LTRIM(C.name) FOR XML PATH('') ),1,2,'') + ']'
SET #query='INSERT INTO #tempTable(Exp, replacementWord, WordValue) select [Exp], [replacementWord],[WordValue] FROM (SELECT [Exp], [major],[start] FROM [yourtable]) p
UNPIVOT([Wordvalue] FOR [replacementWord] IN ('+#queryRWords+'))AS unpvt'
EXECUTE(#query)
UPDATE #tempTable SET ReplacedExp=[Exp]
DECLARE #ExpCount INT
SELECT #ExpCount= COUNT(*) FROM #tempTable
WHILE #ExpCount >0
BEGIN
IF((SELECT [Exp] From #tempTable where Id=#ExpCount)<>(SELECT [Exp] from #tempTable where Id=(#ExpCount-1)))
BEGIN
UPDATE #tempTable SET FLAG=1, ReplacedExp= REPLACE(ReplacedExp, CAST('W.'+replacementWord AS VARCHAR), WordValue) FROM #tempTable WHERE Id=#ExpCount
END
ELSE
BEGIN
UPDATE #tempTable SET ReplacedExp= REPLACE(ReplacedExp, CAST('W.'+replacementWord AS VARCHAR), WordValue) FROM #tempTable WHERE Id=#ExpCount
UPDATE #tempTable SET ReplacedExp= (SELECT ReplacedExp FROM #temptable where Id=#ExpCount) WHERE Id=(#ExpCount-1)
END
SET #ExpCount=#ExpCount-1
END
UPDATE #tempTable
SET flag=1 where Id=1
SELECT ReplacedExp FROM #tempTable where flag=1
Here is the sample TSQL where you can have a good start
SELECT Exp, Major, Start
, Replace(Replace(Exp, 'W.Major', Major), 'W.Start', Start) As Result
FROM [Your Table Name]
Using T-SQL I'm creating a temp table grid. I need to reorder the columns based on the total of the column starting with the largest.
For example
---- DO MO BC NI SC
Total 22 44 53 57 24
Prod A 0 24 0 24 0
Prod B 0 0 0 20 7
Prod C 0 20 0 13 13
Would become:
---- NI BC MO SC DO
Total 57 53 44 24 22
Prod A 24 0 24 0 0
Prod B 20 0 0 7 0
Prod C 13 0 20 13 0
First of, ---- if a terrible column name but I could think of no better for this so I kept it.
You can build the query dynamically where you sort the columns when you build the query string.
declare #SQL nvarchar(max)
set #SQL = '
select [----]'+
(
select ', '+T2.N.value('local-name(.)', 'nvarchar(128)')
from (
select DO, MO, BC, NI, SC
from T
where [----] = 'Total'
for xml path(''), type
) as T1(X)
cross apply T1.X.nodes('*') as T2(N)
order by T2.N.value('.', 'int') desc
for xml path('')
)+'
from T'
exec (#SQL)
SQL Fiddle
Update
If you think the XML version of building the dynamic query is a bit complicated and unintuitive you can use this instead, totally void of XML stuff.
declare #SQL nvarchar(max)
declare #Col nvarchar(128)
declare C cursor local fast_forward for
select U.Col
from (
select DO, MO, BC, NI, SC
from T
where [----] = 'Total'
) as T
unpivot(Val for Col in (DO, MO, BC, NI, SC)) as U
order by U.Val desc
set #SQL = 'select [----]'
open C
fetch next from C into #Col
while ##FETCH_STATUS = 0
begin
set #SQL = #SQL + ',' + #Col
fetch next from C into #Col
end
close C
deallocate C
set #SQL = #SQL + ' from T'
exec (#SQL)
SQL Fiddle
If is is a temp table you could:
create it
calculate the column order
create another table with the correct order
insert into that table (in the correct order)
But I still have to ask why?
You do know you can order the columns in a select statement?
Do do know how to sum the columns?
select sum(col1), sum(col2)
from #temp
Unfortunately you cannot reorder columns in SQL.
I know this may sounds silly but I would like to create function that will process data from tables with different size.
Lets say I have first table like so:
ID IRR M0 M1
----------------------
1 0 -10 5
2 0 -20 10
3 0 -100 100
4 0 -10 0
And second table like so:
ID IRR M0 M1 M2
----------------------------
1 0 -10 5 60
2 0 -20 10 0
3 0 -100 100 400
4 0 -10 0 10
I would like to create function that will be able to process data from both tables.
I know that first column contains ID, second IRR, rest of columns will hold cash flow for specific month.
Function should be able to process all columns instead of first 2 and store result in second column.
I know that I can get all columns from specific table with:
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.columns
WHERE TABLE_NAME = 'First_Table'
Problems begin when I would like to create function that will return those columns as rows.
I can create function like so:
CREATE FUNCTION UnpivotRow (#TableName varchar(50), #FromWhichColumn int, #Row int)
RETURNS #values TABLE
(
id INT IDENTITY(0, 1),
value DECIMAL(30, 10)
)
...
But how this function should look?
I think that ideal for this kind of processing table should look like so:
ProjectID TimePeriod Value
--------------------------------
1 0 -10
1 1 5
2 0 -20
2 1 10
3 0 -100
3 1 100
4 0 -10
4 1 0
I need to unpivot whole table without knowing number of columns.
EDIT:
If this can't be done inside function, then maybe inside a procedure?
This can be done using dynamic SQL to perform the UNPIVOT:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX),
#colTP as NVARCHAR(MAX)
select #cols = stuff((select ','+quotename(C.name)
from sys.columns as C
where C.object_id = object_id('table1') and
C.name like 'M%'
for xml path('')), 1, 1, '')
set #query = 'SELECT id,
replace(timeperiod, ''M'', '''') timeperiod,
value
from table1
unpivot
(
value
for timeperiod in (' + #cols + ')
) u '
exec(#query)
See SQL Fiddle with Demo.
This solution would have to be placed in a stored procedure.