SQL Server select query dynamic column output - sql

I want to accomplish the following in SQL Server 2008
I have an article table like follows
| ArticleId | Description |
|-----------+-------------|
| 1 | Test |
|-----------+-------------|
And a order forecast table like this.
| ArticleId | Week | Order | Amount |
|-----------+--------------+--------+
| 1 | 51 | 1 | 0 |
| 1 | 52 | 2 | 150 |
| 1 | 1 | 3 | 0 |
| 1 | 2 | 4 | 200 |
| 1 | 3 | 5 | 0 |
|-----------+------+-------+--------+
Is there a way to create a query the produces a column for each record in the forecast table in the order of the order column. If it's possible how could I do that?
| ArticleId | Description | Week51 | Week52 | Week1 | Week2 | Week3 |
|-----------+-------------+-----------------+-------+-------+-------+
| 1 | Test | 0 | 150 | 0 | 200 | 0 |
|-----------+-------------+--------+--------+-------+-------+-------+

Provided the WEEK numbers and Order numbers are consistent, it is a small matter to maintain the column sequence.
You may notice I used #forecast and #article because I did not know your actual table names.
Example
Declare #SQL varchar(max) = '
Select *
From (
Select A.ArticleID
,D.Description
,B.*
From #forecast A
Join #article D on A.ArticleID=D.ArticleID
Cross Apply (values (''Week''+left(Week,4),Amount) ) B(Item,Value)
) A
Pivot (max([Value])
For [Item] in (' + Stuff((Select ','+QuoteName('Week'+left(Week,4))
From (Select Distinct top 100 [Order],Week From #forecast Order by [Order] ) A
For XML Path('')),1,1,'') + ') ) p'
Exec(#SQL);
--Print #SQL
Returns
ArticleID Description Week51 Week52 Week1 Week2 Week3
1 Test 0 150 0 200 0

In the general case, the SQL language strictly requires the number and data types of result columns be known at query compile time. What you're asking for here can't be known until after the execution plan is prepared and you start looking in the data.
Therefore, the best you'll be able to do generally is run this as three separate steps:
Run a query to tell you about the columns you'll want to use.
Use the results from #1 to compose a new query on the fly.
Execute the new query and return the results
Even then, this kind of pivot is typically better handled in your client code or report tool. The only good news is it's still possible to accomplish all this from most platforms with a single long SQL string.
For this specific situation, where you're clearly looking at week numbers, you can work around the issue by assuming all 53 possible weeks up front (not 52, because of partial weeks at the end of the year!), and writing a large SQL statement which manually accounts for all 55 columns (53 weeks + Article and Description).

You can try the following query using Pivot for your desired result.
Create Table Article (ArticleId Int, [Description] Varchar(10))
Insert Into Article Values (1, 'Test')
Create Table OrderForecast(ArticleId Int, [Week] Int, [Order] Int, Amount Int)
Insert Into OrderForecast Values (1, 51, 1, 0),(1, 52, 2, 150), (1, 1, 3, 0),(1, 2, 4, 200), (1, 3, 5,0)
Select ArticleId, [Description], Week51, Week52, Week1, Week2, Week3
from
(
select ArticleId, [Description], Amount, [Week]
from
(
SELECT OrderForecast.ArticleId, 'Week' + Convert(Varchar(10), OrderForecast.[Week]) as [Week], [Order], Amount,
Article.[Description] as [Description] FROM OrderForecast
Inner Join Article On OrderForecast.ArticleId = Article.ArticleId
)a
) d
pivot
(
max(Amount)
for [Week] in (Week51, Week52, Week1, Week2, Week3)
) piv;
The result will be as shown below
ArticleId Description Week51 Week52 Week1 Week2 Week3
-------------------------------------------------------------
1 Test 0 150 0 200 0
Here I have used query as table because week was in numbers like 1, 2 but you want the result in the Week1, Week2, etc. So I have concatenated word Week in the number and used it in the Pivot query.
You can find the live demo Live Demo Here

Related

SQL sort by closest string match in multiple columns

I have a search parameter that I am trying to search on multiple columns using the "Like % InputParam %" pattern matching which gives me the following result. ie - Matching OrderID and Ref no to the input parameter.
Consider I have the following table -
OrderId | Name | Ref No |
12345 | XYZ | 120545 |
1205 | ABC | 451003 |
00120505 | CDE | 000174 |
Here OrderID, Ref no are strings and the input query = '1205'. I want the result to be sorted from the most matched to the least matched.
Where most matched is the most accurate match like 1205 = 1205 here
and Least matched is a substring like 00120505 = 1205.
Output -
OrderId | Name | Ref no |
1205 | ABC | 451003 |
12345 | XYZ | 120545 |
00120505 | CDE | 000174 |
You can do it by defining a computed column e.g. MATCH_SCORE with a value that depends on comparisons between OrderId and the value you are looking for; and then use ORDER BY MATCH_SCORE.
Working example:
DECLARE #tbl TABLE (OrderId VARCHAR(10), Name VARCHAR(10), RefNo VARCHAR(10))
INSERT INTO #tbl VALUES ('12345','XYZ','120545'), ('1205','ABC','451003'), ('00120505','CDE','000174')
DECLARE #v VARCHAR(10) = '1205'
;WITH data AS
(SELECT *,
CASE WHEN OrderId = #v THEN 1
WHEN OrderId LIKE #v + '%' THEN 2
WHEN OrderId LIKE '%' + #v + '%' THEN 3
ELSE 4
END AS MATCH_SCORE
FROM #tbl
)
SELECT * FROM data ORDER BY MATCH_SCORE
Output:
OrderId
Name
RefNo
MATCH_SCORE
1205
ABC
451003
1
00120505
CDE
000174
3
12345
XYZ
120545
4
I used a Common Table Expression to construct data, which defines a temporary named result set that is used by the SELECT that follows it.
You need to define what you mean by the distance between two strings. I'll use #peter-b 's definition in the example below. Once you know how to measure how close a string is to the search parameter, you can transpose the columns to rows with cross apply (LATERAL is the standard name), and use the min distance to order the rows.
with t (orderid, refno) as (
select '12345','120545'
union all
select '1205','451003'
)
select t.orderid, t.refno
, min(case when u.s = '1205' then 1
when u.s like '1205'+'%' then 2
when u.s like '%' + u.s + '%' then 3
else 4
end) as distance
from t
cross apply (
select t.orderid
union all
select t.refno
) as u (s)
group by t.orderid, t.refno
order by 3
;
orderid refno distance
1205 451003 1
12345 120545 2
Fiddle

Generate random numbers in a specific range without duplicate values

SELECT CEILING (RAND(CAST(NEWID() AS varbinary)) *275) AS RandomNumber
This creates random numbers. However, it spits out duplicates
Generate a numbers table with the range of your desire. In my case, I do it via recursive cte. Then order the numbers table using the newid function.
with numbers as (
select 0 as val union all
select val + 1 from numbers where val < 275
)
select ord = row_number() over(order by ap.nid),
val
into #rands
from numbers n
cross apply (select nid = newid()) ap
order by ord
option (maxrecursion 1000);
One run of the code above results in a table of 276 values that begins and ends as follows:
| ord | val |
+-----+-----+
| 1 | 102 |
| 2 | 4 |
| 3 | 127 |
| ... | ... |
| 276 | 194 |
Non duplicating ordering of random numbers.
You can select from it a variety of ways, but one way could be:
-- initiate these to begin with
declare #ord int = 1;
declare #val int;
declare #rand int;
-- do this on every incremental need for a random number
select #val = val,
#ord = #ord + 1
from #rands
where ord = #ord;
print #val;
In the comments to my other answer, you write:
The table I'm working with has an ID , Name , and I want to generate a 3rd column that assigns a unique random number between 1-275 (as there are 275 rows) with no duplicates.
In the future, please include details like this in your original question. With this information, we can help you out better.
First, let's make a sample of your problem. We'll simplify it to just 5 rows, not 275:
create table #data (
id int,
name varchar(10)
);
insert #data values
(101, 'Amanda'),
(102, 'Beatrice'),
(103, 'Courtney'),
(104, 'Denise'),
(105, 'Elvia');
Let's now add the third column you want:
alter table #data add rando int;
Finally, let's update the table by creating a subquery that orders the rows randomly using row_number(), and applying the output the the column we just created:
update reordered
set rando = rowNum
from (
select *,
rowNum = row_number() over(order by newid())
from #data
) reordered;
Here's the result I get, but of course it will be different every time it is run:
select *
from #data
| id | name | rando |
+-----+----------+-------+
| 101 | Amanda | 3 |
| 102 | Beatrice | 1 |
| 103 | Courtney | 4 |
| 104 | Denise | 5 |
| 105 | Elvia | 2 |

How to SUM column 1 and select column 2 by condition?

I've stuck with how to sum column A and select column B with a condition if column B >= 50 select this row id.
Example Table Like this
+----+-----------+---------+
| ID | PRICE | PERCENT |
+----+-----------+---------+
| 1 | 5 | 5 |
| 2 | 18 | 20 |
| 3 | 7 | 50 |
| 4 | 16 | 56 |
| 5 | 50 | 87 |
| 6 | 17 | 95 |
| 7 | 40 | 107 |
+----+-----------+---------+
SELECT ID, SUM(PRICE) AS PRICE, PERCENT FROM Table
Column ID and PERCENT, I want to select from a row with PERCENT >= 50
The result should be
Any suggestions?
Try below query:
declare #tbl table(ID int, PRICE int, [PERCENT] int);
insert into #tbl values
(1, 5, 5),
(2, 18, 20),
(3, 7, 50),
(4, 16, 56),
(5, 50, 87),
(6, 17, 95),
(7, 40, 107);
select top 1 ID,
(select sum(PRICE) from #tbl) PRICE,
[PERCENT]
from #tbl
where [PERCENT] > 50
You could include the total in a subquery in the SELECT clause of your query like this:
SELECT
[ID],
(SELECT SUM([PRICE]) FROM T) AS [PRICE],
[PERCENT]
FROM
T
WHERE
[PRICE] >= 50
However, it remains unclear which of the five valid records should be picked. You indicated it should be the record where PERCENT has value 56, but IMHO value 50 would be possible too, just like 87, 95, and 107 (?). It is unclear why you pick value 56 as the correct one. If it doesn't matter, you could use TOP (1) in the SELECT clause, but if it does matter, you should extend the WHERE clause with appropriate conditions/filters.
Mixing aggregate data from groups back with individual elements/records like this is often fuzzy. I consider it to be a "code smell" and here in your question on StackOverflow, it might indicate an XY-problem. Anyway, these query results might get misinterpreted quite easily if you are not careful. Always remember that such aggregated data in the result (in this case the PRICE field) has practically nothing to do with the detail data in the result (in this case the ID and PERCENT fields). Unless you want to combine your aggregate data with your detail data (in a calculation for example), but you do not indicate you want anything like that in your question...
you can do this Trick to have a result of 2 queries in 1 query:
select ID as ID,T.[PERCENT] AS B, 0 as sumA
from Table_1 as T
where T.[PERCENT]>=50
union All
select 0 as ID,0 AS B, sum(t.[PRICE]) as sumA
from Table_1 as T
Am not sure why you need this but certainly, You can Archive Above Output using below query
Sample Data
declare #data table
(Id int, Price int, [Percent] int)
insert #data
VALUES (1,5,5),
(2,18,20),
(3,7,50),
(4,16,56),
(5,50,87),
(6,17,95),
(7,40,107)
Query
select top 1 ID, (select sum(price) from #data) as Price, [Percent ]
from #data
where [Percent ] >50
You can try the following code:
SELECT TOP (1) [ID], SUM(PRICE) OVER (), [PERCENT]
FROM #tbl
ORDER BY CASE WHEN [PERCENT] > 50 THEN 0 ELSE 1 END, [ID];
I am using OVER clause in order to extract/read data from the table only once - one table scan.

How to combine two records?

I have a table that looks like this
ID | Value | Type
-----------------------
1 | 50 | Travel
1 | 25 | Non-Travel
1 | 25 | Non-Travel
1 | 25 | Non-Travel
1 | 50 | Travel
1 | 75 | Non-Travel
How can I query this to make the output rearrange to this?
ID | Travel | Non-Travel
------------------------
1 | 100 | 150
The query to actually get the first table I posted has many joins and a BIT column in one of the tables where 0 or NULL is non-travel and 1 is travel. So I have something like this:
SELECT
[ID]
,CASE WHEN [IsTravel] IN (0,NULL) THEN ISNULL(SUM([VALUE]),0) END AS 'NonTravel'
,CASE WHEN [IsTravel] = 1 THEN ISNULL(SUM([VALUE]),0) END AS 'Travel'
FROM
...
However the result ends up showing this
ID | Travel | Non-Travel
------------------------
1 | 100 | NULL
1 | NULL | 150
How can I edit my query to combine the rows to show this result?
ID | Travel | Non-Travel
------------------------
1 | 100 | 150
Thanks in advance.
select ID,
SUM(CASE WHEN Type = 'Travel' THEn value ELSE 0 END) [Travel],
SUM(CASE WHEN Type = 'NonTravel' THEn value ELSE 0 END) [NonTravel]
from #Table1
GROUP BY ID
You need to wrap each of your conditionals in aggregations such as MAX(), and GROUP BY other columns to roll up the values and remove the NULL. Something like this:
SELECT
[ID]
,MAX(CASE WHEN [IsTravel] IN (0,NULL) THEN ISNULL(SUM([VALUE]),0) END) AS 'NonTravel'
,MAX(CASE WHEN [IsTravel] = 1 THEN ISNULL(SUM([VALUE]),0) END) AS 'Travel'
FROM
...
GROUP BY [ID]
If the logic gets too cluttered or confusing (don't know without seeing your whole current query) then drop those results into a temp table or CTE and do the simple MAX() and GROUP BY from there.
You can use pivot as below:
Select * from (
Select Id, [Value], [Type] from yourtable ) a
pivot (sum([Value]) for [Type] in ([Travel],[Non-Travel]) ) p
Output as below:
+----+------------+--------+
| Id | Non-Travel | Travel |
+----+------------+--------+
| 1 | 150 | 100 |
+----+------------+--------+
For dynamic list of Travel types you can do dynamic query as below:
Declare #cols1 varchar(max)
Declare #query nvarchar(max)
Select #cols1 = stuff((select Distinct ','+QuoteName([Type]) from #traveldata for xml path('')),1,1,'')
Set #query = ' Select * from (
Select Id, [Value], [Type] from #traveldata ) a
pivot (sum([Value]) for [Type] in (' + #cols1 + ') ) p '
Exec sp_executesql #query

Count by unique ID, group by another

I've inherited some scripts that count the number of people in a team by department; the current scripts create a table for each individual department and the previous user would copy/paste the data into Excel. I've been tasked to pull this report into SSRS so I need one table for all the departments by team.
Current Table
+-------+-----------+---------+
| Dept | DataMatch | Team |
+-------+-----------+---------+
| 01 | 4687Joe | Dodgers |
| 01 | 3498Cindy | RedSox |
| 01 | 1057Bob | Yankees |
| 01 | 0497Lucy | Dodgers |
| 02 | 7934Jean | Yankees |
| 02 | 4584Tom | Dodgers |
+-------+-----------+---------+
Desired Results
+-------+---------+--------+---------+
| Dept | Dodgers | RedSox | Yankees |
+-------+---------+--------+---------+
| 01 | 2 | 1 | 1 |
| 02 | 1 | 0 | 1 |
+-------+---------+--------+---------+
The DataMatch field is the unique identifier I will be counting. I started by wrapping each department in a CTE however this results in the Dept as the Column which would not work for my report, so I need to transpose my results and I haven't been able to figure that out. There are 60 departments and my query was getting very long.
Current query
SELECT Dept, DataMatch, Team INTO #temp_Team
FROM TeamDatabase
WHERE Status = 14
AND Team <> 'Missing'
;WITH A_cte (Team, Dept01)
AS
(
SELECT Team
, COUNT(DISTINCT datamatch) AS 'Dept01'
FROM #temp_Team
WHERE Dept = '01'
GROUP BY Team
),
B_cte (Team, Dept02) AS
(
SELECT Team
, COUNT(DISTINCT datamatch) AS 'Dept02'
FROM #temp_Team
WHERE Dept = '02'
GROUP BY Team
)
SELECT A_cte.Team
, A_cte.Dept01
, B_cte.Dept02
FROM A_cte
INNER JOIN B_cte
ON A_cte.Team=B_cte.Team
Which results in:
+----------------------------+-------+-------+
| Team | Prg01 | Prg02 |
+----------------------------+-------+-------+
| RedSox | 144 | 141 |
| Yankees | 63 | 236 |
| Dodgers | 298 | 196 |
+----------------------------+-------+-------+
I feel that using a pivot on my already very long query would be excessive and impact performance, 60 departments with over 30,000 rows.
What, mostly likely basic, step am I missing?
TL;DR - How do I count people by team and list by department?
I would replace the whole query with a dynamic pivot instead of adding a pivot to your CTEs.
You can add your Status/Team conditions to the SELECT inside the dynamic query at the bottom. They would be WHERE STATUS=14 AND TEAM !=''MISSING'' - note that is two single quotes to nest it within the string.
IF OBJECT_ID('tempdb..#data') IS NOT NULL DROP TABLE #data
CREATE TABLE #data (Dept VARCHAR(50), DataMatch NVARCHAR(50), Team VARCHAR(50))
INSERT INTO #data (Dept, DataMatch, Team)
VALUES ('01', '4687Joe','Dodgers'),
('01', '3498Cindy','RedSox'),
('01', '1057Bob','Yankees'),
('01', '0497Lucy','Dodgers'),
('02', '7934Jean','Yankees'),
('02', '4584Tom','Dodgers')
DECLARE #cols AS NVARCHAR(MAX),
#sql AS NVARCHAR(MAX)
SET #cols = STUFF(
(SELECT N',' + QUOTENAME(y) AS [text()]
FROM (SELECT DISTINCT Team AS y FROM #data) AS Y
ORDER BY y
FOR XML PATH('')),
1, 1, N'');
SET #sql = 'SELECT Dept, '+#cols+'
FROM (SELECT Dept, DataMatch, Team
FROM #data D) SUB
PIVOT (COUNT([DataMatch]) FOR Team IN ('+#cols+')) AS P'
PRINT #SQL
EXEC (#SQL)
In case you don't want to use a dynamic pivot, here is just a stand-alone query... again, add your conditions as you need.
SELECT Dept, Dodgers, RedSox, Yankees
FROM (SELECT Dept, DataMatch, Team
FROM #data D) SUB
PIVOT (COUNT([DataMatch]) FOR Team IN ([Dodgers], [RedSox], [Yankees])) AS P
I'm not sure I follow what relevance your existing query has, but to get from your current table to your desired results is a pretty straightforward usage of PIVOT:
SELECT *
FROM Table1
PIVOT(COUNT(DataMatch) FOR Team IN (Dodgers,RedSox,Yankees))pvt
Demo: SQL Fiddle
And this of course could be done dynamically if the teams list isn't static.