SQL column sorting by value - sql

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.

Related

Using Subqueries to Define Column Alias

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

Convert rows to columns without using pivot

How can i convert this rows :
id id_name flag tag_name tag_type tag_value
1 1 163 XO c 10
1 1 163 X1 c 0
1 1 163 AM c 5
to this one row :
1 1 163 XO:c:10 X1:c:0 AM:c:5
Without using pivot ??
You're looking to do the equivalent of a group_concat, rather than a pivot
;with src as (select id, id_name, flag, tag_name+':'+tag_type+':'+CONVERT(varchar(5), tag_value) as tag from yourtable t1 )
SELECT distinct
src.id, src.id_name, src.flag,
tags = STUFF((
SELECT ' ' + s.tag
FROM src s
WHERE s.id=src.id
FOR XML PATH(''), TYPE).value('.', 'varchar(max)'), 1, 1, '')
FROM src
Here is one possible way to pivot your data like you've asked. Generally it would be better to perform this sort of string building/manipulation within your application rather then on the database directly. In this example Table2 can be swapped for a temporary table.
-- Create and Seed Table2
SELECT [id], [id_name], [flag], CAST(NULL as varchar(255)) AS [data]
INTO [Table2] FROM [Table1]
GROUP BY [id], [id_name], [flag];
-- Declare variables to hold row
DECLARE #id int, #id_name int, #flag int,
#tag_name varchar(2), #tag_type varchar(1), #tag_value int;
-- Declare the Cursor
DECLARE PivotCursor CURSOR FOR
SELECT [id], [id_name], [flag], [tag_name], [tag_type], [tag_value]
FROM [Table1] ORDER BY [id], [id_name], [flag], [tag_name]
;
-- Use the cursor to update Table2
OPEN PivotCursor;
FETCH NEXT FROM PivotCursor INTO #id, #id_name, #flag, #tag_name, #tag_type, #tag_value;
WHILE ##FETCH_STATUS = 0
BEGIN
UPDATE Table2 SET
[Data] = ISNULL([Data],'')+(CASE WHEN [Data] IS NULL THEN '' ELSE ' ' END)+#TagName+':'+#TagType+':'+CAST(#TagValue as varchar)
WHERE [id] = #id AND [id_name] = #id_name AND [flag] = #flag;
-- Get Next
FETCH NEXT FROM PivotCursor INTO #id, #id_name, #flag, #tag_name, #tag_type, #tag_value;
END
CLOSE PivotCursor;
DEALLOCATE PivotCursor;
The resulting table looks like this:
id id_name flag data
1 1 163 AM:c:5 X1:c:0 XO:c:10

TSQL Select all columns without first n from function/procedure

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.

SQL delete on group by conditions

A cursor is used, but it's slow and appears to be a big bottleneck in a SQL job. Basically, this is a cleanup effort to remove all but the top X accessories (ordered by sales rank) from a particular source that's previously grouped by a product id and account visibility.
The command is basically built in the each iteration of the cursor loop and exec'ed manually.
The vis column refers to multiple tenants that sort of acts like a bitmask e.g. two tenants could have the same product.
declare #prodid int
declare #cnt int
declare #vis bigint
declare #cmd varchar(600)
declare #clause varchar(600)
-- find records with more than X excess accessories
declare cur cursor for
select pa.prodid, 'cnt' = count(*), vis from [accessories] pa
group by prodid, vis
having count(*) > X -- e.g. 5
Sample output could look like
prodid cnt vis
123 6 128
234 8 260
345 10 512
In the case where X=5, the last 1 salesrank item for 123 would be removed, the last 3 for 234 and the last 5 for 345. Can this be done using a DELETE statement while including the groupings in some nested select?
open cur
fetch next from cur into #prodid, #cnt, #vis
while ##fetch_status = 0
begin
-- a clause that ends up looking like this:
-- 12345 and vis = 128 -- OR -- 23456 and vis is null
set #clause = convert(varchar(14), #prodid) + ' and vis ' + case
when #vis is null then ' is null '
else ' = ' + cast(#vis as varchar) end
-- delete all but the top X from source=2 and that match prodid and vis
set #cmd = 'delete from [accessories]
where source = 2 and prodid=' + #clause +
' and access_prodid in (select top ' + convert(varchar(5), #cnt - X) +
' access_prodid from [accessories] where prodid = '
+ #clause + ' and source = 2 order by salesrank)'
exec(#cmd)
fetch next from cur into #prodid, #cnt, #vis
end
close cur
deallocate cur
Try this:
WITH DupData AS
(
SELECT *,
ROW_NUMBER()
OVER(PARTITION BY pa.prodid, pa.vis ORDER BY salesrank) Position
FROM [accessories] pa
WHERE pa.source = 2
)
DELETE
FROM DupData
WHERE Position > 5
I would do this by using windows functions to identify the rows to be deleted:
with t as (select pa.*,
row_number() over (partition by prodid, vis order b salesrank) as sr
from [accessories] pa
)
delete from pa
from t
where pa.prodid = t.prodid and pa.vis = t.vis and pa.salesrank = t.salesrank
If there is a unique id in the pa table, then you can use that instead of the more complicated where statement. This assumes that salesrank is unique within each prodid/vis group.

Dynamic SQL Server 2005 Pivot

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