UNPIVOT/PIVOT to combine multiple records into one - sql

Currently, I have a table that tracks product inventory locations using the following table:
ProductID(PK) Location(PK) BIN1 BIN2
1000 EAST XRZY CCAB
1000 WEST AAAA NULL
I'm attempting to UNPIVOT the data into the following:
ProductID EAST_BIN1 EAST_BIN2 WEST_BIN1 WEST_BIN2
1000 XRZY CCAB AAAA NULL
Note that the location column has been PIVOTed into part of the BIN value field.
However, I've found that if I pivot the data, I'm unable to combine it with the BIN fields. PIVOT simply aggregates (using MAX) the BIN values into one field, while UNPIVOT just transforms the BIN* fields into rows.
What am I missing in terms of transforming the data above?
Any help would be greatly appreciated!

You can do it "by hand" as follows:
SELECT ProductID,
MAX(CASE WHEN Location='EAST' THEN BIN1 ELSE NULL END) AS EAST_BIN1,
MAX(CASE WHEN Location='EAST' THEN BIN2 ELSE NULL END) AS EAST_BIN2,
MAX(CASE WHEN Location='WEST' THEN BIN1 ELSE NULL END) AS WEST_BIN1,
MAX(CASE WHEN Location='WEST' THEN BIN2 ELSE NULL END) AS WEST_BIN2
FROM YOURTABLE
GROUP BY ProductID
This creates multiple rows (as your source table) with the results in the correct column, then smashes them down to one row with a group by. The correct value is taken using the aggregate function MAX.

To pivot you'll need to get the data into a single BIN column to pivot on. Consider this...
declare #t table (ProductId int, Location varchar(20), BIN1 varchar(4), BIN2 varchar(4));
insert into #t values(1000, 'EAST', 'XRZY', 'CCAB'), (1000, 'WEST', 'AAAA', null);
with cte as (
select ProductId, Col = Location + '_BIN1', BINVal = Bin1 from #t
union all
select ProductId, Col = Location + '_BIN2', BINVal = Bin2 from #t
)
select
*
from
cte
pivot (
max(BINVal)
for Col in ([EAST_BIN1], [EAST_BIN2], [WEST_BIN1], [WEST_BIN2])
) p

Related

Compare two rows (both with different ID) & check if their column values are exactly the same. All rows & columns are in the same table

I have a table named "ROSTER" and in this table I have 22 columns.
I want to query and compare any 2 rows of that particular table with the purpose to check if each column's values of that 2 rows are exactly the same. ID column always has different values in each row so I will not include ID column for the comparing. I will just use it to refer to what rows will be used for the comparison.
If all column values are the same: Either just display nothing (I prefer this one) or just return the 2 rows as it is.
If there are some column values not the same: Either display those column names only or display both the column name and its value (I prefer this one).
Example:
ROSTER Table:
ID
NAME
TIME
1
N1
0900
2
N1
0801
Output:
ID
TIME
1
0900
2
0801
OR
Display "TIME"
Note: Actually I'm okay with whatever result or way of output as long as I can know in any way that the 2 rows are not the same.
What are the possible ways to do this in SQL Server?
I am using Microsoft SQL Server Management Studio 18, Microsoft SQL Server 2019-15.0.2080.9
Please try the following solution based on the ideas of John Cappelletti. All credit goes to him.
SQL
-- DDL and sample data population, start
DECLARE #roster TABLE (ID INT PRIMARY KEY, NAME VARCHAR(10), TIME CHAR(4));
INSERT INTO #roster (ID, NAME, TIME) VALUES
(1,'N1','0900'),
(2,'N1','0801')
-- DDL and sample data population, end
DECLARE #source INT = 1
, #target INT = 2;
SELECT id AS source_id, #target AS target_id
,[key] AS [column]
,source_Value = MAX( CASE WHEN Src=1 THEN Value END)
,target_Value = MAX( CASE WHEN Src=2 THEN Value END)
FROM (
SELECT Src=1
,id
,B.*
FROM #roster AS A
CROSS APPLY ( SELECT [Key]
,Value
FROM OpenJson( (SELECT A.* For JSON Path,Without_Array_Wrapper,INCLUDE_NULL_VALUES))
) AS B
WHERE id=#source
UNION ALL
SELECT Src=2
,id = #source
,B.*
FROM #roster AS A
CROSS APPLY ( SELECT [Key]
,Value
FROM OpenJson( (SELECT A.* For JSON Path,Without_Array_Wrapper,INCLUDE_NULL_VALUES))
) AS B
WHERE id=#target
) AS A
GROUP BY id, [key]
HAVING MAX(CASE WHEN Src=1 THEN Value END)
<> MAX(CASE WHEN Src=2 THEN Value END)
AND [key] <> 'ID' -- exclude this PK column
ORDER BY id, [key];
Output
+-----------+-----------+--------+--------------+--------------+
| source_id | target_id | column | source_Value | target_Value |
+-----------+-----------+--------+--------------+--------------+
| 1 | 2 | TIME | 0900 | 0801 |
+-----------+-----------+--------+--------------+--------------+
A general approach here might be to just aggregate over the entire table and report the state of the counts:
SELECT
CASE WHEN COUNT(DISTINCT ID) = COUNT(*) THEN 'Yes' ELSE 'No' END AS [ID same],
CASE WHEN COUNT(DISTINCT NAME) = COUNT(*) THEN 'Yes' ELSE 'No' END AS [NAME same],
CASE WHEN COUNT(DISTINCT TIME) = COUNT(*) THEN 'Yes' ELSE 'No' END AS [TIME same]
FROM yourTable;

Use SSRS to move child rows to repeating columns for exporting?

I'm trying to take an ugly SQL output and use SSRS to make it suitable for export to a mail house.
What would be the right approach to group data from this:
order_no
type
name
item
price
1
header
sally
NULL
NULL
1
data
NULL
book
12.50
1
data
NULL
dvd
39.00
2
header
bob
NULL
NULL
2
data
NULL
shirt
50.00
2
data
NULL
shorts
65.00
Into this?
order_no
type
name
item_1
price_1
item_2
price_2
1
header
sally
book
12.50
dvd
39.00
2
header
bob
shirt
50.00
shorts
65.00
Should this be a Matrix? I'm having trouble getting making progress.
There may be a much cleaner way of doing this but this is the approach I took...
First I replicated your sample data
DECLARE #t TABLE(order_no int, [type] varchar(20), [name] varchar(20), [item] varchar(20), price decimal (10,2))
INSERT INTO #t VALUES
(1,'header', 'sally' , NULL, NULL ),
(1,'data', NULL , 'book', 12.50),
(1,'data', NULL , 'dvd', 39.00),
(2,'header', 'bob' , NULL, NULL ),
(2,'data', NULL , 'shirt', 50.00),
(2,'data', NULL , 'shorts', 65.00)
;
WITH o (order_no, [type], [name], [item], [price], [ItemNumber]) AS
(
SELECT *, ROW_NUMBER() OVER(PARTITION BY order_no ORDER BY item) AS ItemNumber FROM #t WHERE [type] != 'header'
)
SELECT
h.order_no, h.[type], h.name
, d.ItemNumber, d.ItemCaption, d.ItemValue
FROM (SELECT DISTINCT order_no, [Type], [name] FROM #t WHERE [type] = 'header') h
JOIN
(
SELECT order_no, ItemNumber, 'Item_' + CAST(ItemNumber as varchar(10)) as ItemCaption, Item as ItemValue from o
UNION
SELECT order_no, ItemNumber, 'Price_' + CAST(ItemNumber as varchar(10)) as ItemCaption, CAST(Price as varchar(20)) as ItemValue from o
) d ON h.order_no = d.order_no
I created a CTE just to clean up the query a little and included an row_number for each item, we'll use this to created column captions which we can use in the matrix.
This gives us the following output
We now have everything in place for a simple matrix.
Note: As we had to convert everything to strings, the prices are no longer numbers so bear this in mind if you plan on doing anything else with the data later - they would have to be converted back
So, create a new report, add a new dataset and use the above query as the dataset query.
Add a matrix control, drag order_no to the row placeholder, ItemCaption to the column placeholder and ItemValue to the data placeholder.
Next, right-click the order_no column and choose "insert column - inside group right", the set new column value to your type field. Repeat for the header field.
Your design will look like this.
Finally In the column group sort properties, sort by ItemNumber then ItemCaption
The final report looks like this...

SQL split totals

Currently I have the following result in SQL:
qty
description
volume
weight
4
Flowers
3,4
4
This is in an XML format. So what I want to do, and I think I need to user "FOR XML PATH" in a certain way, but I am not sure how to achieve the following result:
qty
description
volume
weight
1
Flowers
0,85
1
1
Flowers
0,85
1
1
Flowers
0,85
1
1
Flowers
0,85
1
So I need to divide the XML path based on the total qty (4). For each (4) products, I need to create a new row. Then divide the volume and the weight (/qty).
Can anyone help me to push me into the right direction?
Edit:
The first result, qty of 4, is a result in a temp table.
I extract the data from the temp table into XML format. Here is a snippet
(SELECT "qty" = value('(#col24)[1]', 'varchar(50)'), "weight" = value('(#col28)[1]', 'varchar(50)'), "volume" = value('(#col26)[1]', 'decimal(16,2)') FOR XML PATH('product'), ROOT('products'), TYPE)
The qty, weight and volume represents the totals.
This is what I want to devide to create a "product" for each "qty".
You can use a recursive CTE to split the rows (you might need to up the recursion limit if your quantity can be higher than 100).
declare #Test table (qty int, [description] varchar(64), volume decimal(9,2), [weight] decimal(9,2))
insert into #Test (qty, [description], volume, [weight]) values (4, 'Flowers', 3.4, 4);
with cte as (
select qty, [description], volume, [weight], 1 as rn
from #Test
union all
select qty, [description], volume, [weight], rn + 1
from cte
where rn < qty
)
select 1 qty, [description], cast(volume / qty as decimal(9,2)) volume, cast([weight] / qty as decimal(9,2)) [weight]
from cte
for xml path('product'), root('products'), type;
-- option (maxrecursion 200); -- If you need to increase it above the default of 100
Note: If you setup the DDL+DML, as I shown, in your questions you make it much easier for people to reply.
One way is by recursive CTE:
WITH cte AS (
SELECT *, qty AS cc, 1 org FROM #sale
UNION ALL
SELECT cte.qty /qty
, cte.description
, cte.volume /qty
, cte.weight /qty
, cte.cc - 1 AS cc
, 0 org
FROM cte
WHERE cc > 1
)
SELECT * FROM cte
where org = 0
FOR XML path
However if you have a tally table it will faster and simpler:
SELECT qty /qty
, description
, volume /qty
, weight /qty
FROM table
JOIN numbertables < -- number tables from 1 to max
on numbertables.values < qty
FOR XML path

SQL query to separate a column into separate columns

I would like to have separate columns for H and T's prices, with 'period' as the common index. Any suggestions as to how I should go about this?
This is what my SQL query produces at the moment:
You can use GROUP BY and a conditional, like this:
SELECT
period
, SUM(CASE NAME WHEN 'H' THEN price ELSE 0 END) as HPrice
, SUM(CASE NAME WHEN 'T' THEN price ELSE 0 END) as TPrice
FROM MyTable
GROUP BY period
You can do the following:
SELECT period,
max(CASE WHEN name = 'H' THEN price END) as h_price,
max(CASE WHEN name = 'T' THEN price END) as t_price
FROM myTable
GROUP by period
If you mean to recreate the table?
1) Create a new table with columns: period, price_h & price_t.
2) Copy all (distinct) from period into new table's period.
3) Copy all price where name = H to new table's price_h joining the period column
4) repeat 3 for price_t....
good luck!
A little late to the game on this but you could also pivot the data.
Lets create a sample table.
CREATE TABLE myData(period int, price decimal(12,4), name varchar(10))
GO
-- Inserting Data into Table
INSERT INTO myData
(period, price, name)
VALUES
(1, 53.0450, 'H'),
(1, 55.7445, 'T'),
(2, 61.2827, 'H'),
(2, 66.0544, 'T'),
(3, 61.3405, 'H'),
(3, 66.0327, 'T');
Now the select with the pivot performed.
SELECT period, H, T
FROM (
SELECT period, price, name
FROM myData) d
PIVOT (SUM(price) FOR name IN (H, T)) AS pvt
ORDER BY period
I've used this technique when I needed to build a dynamic sql script that took in the columns in which would be displayed on the header of the table. No need for case statements.
Im not sure about the performance of the case and pivot. Maybe someone with a little more experience could add some comments on which would give better performance.

How to select info from row above?

I want to add a column to my table that is like the following:
This is just an example of how the table is structured, the real table is more than 10.000 rows.
No_ Name Account_Type Subgroup (New_Column)
100 Sales 3
200 Underwear 0 250 *100
300 Bikes 0 250 *100
400 Profit 3
500 Cash 0 450 *400
So for every time there is a value in 'Subgroup' I want the (New_Column) to get the value [No_] from the row above
No_ Name Account_Type Subgroup (New_Column)
100 Sales 3
150 TotalSales 3
200 Underwear 0 250 *150
300 Bikes 0 250 *150
400 Profit 3
500 Cash 0 450 *400
There are cases where the table is like the above, where two "Headers" are above. And in that case I also want the first above row (150) in this case.
Is this a case for a cursor or what do you recommend?
The data is ordered by No_
--EDIT--
Starting from the first line and then running through the whole table:
Is there a way I can store the value for [No_] where [Subgroup] is ''?
And following that insert this [No_] value in the (New_Column) in each row below having value in the [Subgroup] row.
And when the [Subgroup] row is empty the process will keep going, inserting the next [No_] value in (New_Column), that is if the next line has a value in [Subgroup]
Here is a better image for what I´m trying to do:
SQL Server 2012 suggests using Window Offset Functions.
In this case : LAG
Something like this:
SELECT [No_]
,[Name]
,[Account_Type]
,[Subgroup]
,LAG([No_]) OVER(PARTITION BY [Subgroup]
ORDER BY [No_]) as [PrevValue]
FROM table
Here is an example from MS:
http://technet.microsoft.com/en-us/library/hh231256.aspx
The ROW_NUMBER function will allow you to find out what number the row is, but because it is a windowed function, you will have to use a common table expression (CTE) to join the table with itself.
WITH cte AS
(
SELECT [No_], Name, Account_Type, Subgroup, [Row] = ROW_NUMBER() OVER (ORDER BY [No_])
FROM table
)
SELECT t1.*, t2.[No_]
FROM cte t1
LEFT JOIN cte t2 ON t1.Row = t2.Row - 1
Hope this helps.
Next query will return Name of the parent row instead of the row itself, i.e. Sales for both Sales, Underwear, Bikes; and Profit for Profit, Cash:
select ISNULL(t2.Name, t1.Name)
from table t1
left join table t2 on t1.NewColumn = t2.No
So in SQL Server 2008 i created test table with 3 values in it:
create table #ttable
(
id int primary key identity,
number int,
number_prev int
)
Go
Insert Into #ttable (number)
Output inserted.id
Values (10), (20), (30);
Insert in table, that does what you need (at least if understood correctly) looks like this:
declare #new_value int;
set #new_value = 13; -- NEW value
Insert Into #ttable (number, number_prev)
Values (#new_value,
(Select Max(number) From #ttable t Where t.number < #new_value))
[This part added] And to work with subgroup- just modify the inner select to filter out it:
Select Max(number) From #ttable t
Where t.number < #new_value And Subgroup != #Subgroup
SELECT
No_
, Name
, Account_Type
, Subgroup
, ( SELECT MAX(above.No_)
FROM TableX AS above
WHERE above.No_ < a.No_
AND above.Account_Type = 3
AND a.Account_Type <> 3
) AS NewColumn
FROM
TableX AS a