Converting SQL Rows into Columns - sql-server

I'm having some difficulty understanding the best approach to get the following result set.
I have a result set (thousands of rows) that I want to update from:
ID Question Answer
--- -------- --------
1 Business NULL
1 Job Other
1 Location UK
2 Business Legal
3 Location US
4 Location UK
To This:
ID Buisness Job Location
--- -------- --- --------
1 NULL Other UK
2 Legal NULL NULL
3 NULL NULL US
4 NULL NULL UK
I have been looking at SELF JOINS and PIVOT tables but wanted to understand the best method as I have not been able to achieve the desired output.
Thanks
Gary

If you want to use pivot, you can do it like this:
CREATE TABLE #Table1
([ID] int, [Question] varchar(8), [Answer] varchar(5))
;
INSERT INTO #Table1
([ID], [Question], [Answer])
VALUES
(1, 'Business', NULL),
(1, 'Job', 'Other'),
(1, 'Location', 'UK'),
(2, 'Business', 'Legal'),
(3, 'Location', 'US'),
(4, 'Location', 'UK')
;
select * from
(select * from #Table1) S
pivot (
max(Answer) for Question in (Business, Job, Location)
) P

select
id,
max(case when question='business' then answer end) 'business',
max(case when question='Job' then answer end) 'Job',
max(case when question='Location' then answer end) 'Location'
group by id

Related

Creating SQL Server (T-SQL) view that flattens nulls

I'm having a lot of difficulty trying to create a view that flattens the data without nulls. I've supplied the code that creates two basic tables and my view code so you can see what I've tried so far. Please note that the two tables do not have a matching primary or foreign key column, so the summary in the view is created by just joining on City. I can't use XML because my team of data analysts all have intermediate skills and won't be able to understand it. I considered using a recursive CTE, but I can't get it right. The result produces 6 lines but I want 3 lines.
Thanks for any ideas about a better way to achieve this.
CREATE TABLE A (
OrdID int,
Cat varchar(255),
Qty int,
City varchar(255),
Ctry varchar(255)
);
INSERT INTO A (OrdID, Cat, Qty, City, Ctry)
VALUES (1, 'TV', 5,'London', 'England');
INSERT INTO A (OrdID, Cat, Qty, City, Ctry)
VALUES (2, 'Laptop', 3,'London', 'England');
INSERT INTO A (OrdID, Cat, Qty, City, Ctry)
VALUES (3, 'Laptop', 4, 'Berlin', 'Germany');
CREATE TABLE Cust (
CustID int,
CustType varchar(255),
City varchar(255),
NumItems int,
);
INSERT INTO Cust (CustID, CustType, City, NumItems)
VALUES (1, 'New', 'London', 2);
INSERT INTO Cust (CustID, CustType, City, NumItems)
VALUES (2, 'Returning','London', 5);
INSERT INTO Cust (CustID, CustType, City, NumItems)
VALUES (3, 'Returning','Berlin', 2);
INSERT INTO Cust (CustID, CustType, City, NumItems)
VALUES (4, 'New','Berlin', 8);
alter view My_View
as
With CTE_FlattenNulls
as
(
Select
S.Cat
, S.Qty
, S.City
, S.Ctry
, case when C.CustType like 'New' then sum(C.NumItems) end as NewC
, case when C.CustType like 'Returning' then sum(C.NumItems) end as RetC
from A as S
left join Cust as C
on S.City = C.City
group by
S.Cat
, S.Qty
, S.City
, S.Ctry
, C.CustType
)
select
Cat
,Qty
,City
,Ctry
,NewC
,RetC
,SUM(IsNull(NewC, 0) + IsNull(RetC, 0)) as TotC
from CTE_FlattenNulls
group by
Cat
,Qty
,City
,Ctry
,NewC
,RetC
go
Just adding the output that I wanted:
Cat
Qty
City
Cntry
NewC
RetCust
TotC
Laptop
4
Berlin
Germany
8
2
10
Laptop
3
London
England
2
5
7
TV
5
London
England
2
5
7
You were very close.
See comments in code for explanation.
With CTE_FlattenNulls
as
(
Select S.Cat, S.Qty, S.City, S.Ctry,
-- To do conditional summation case expression needs to be inside the SUM function
sum( case when C.CustType like 'New' then C.NumItems else 0 end ) as NewC,
sum( case when C.CustType like 'Returning' then C.NumItems else 0 end ) as RetC
from A as S
left join Cust as C
on S.City = C.City
group by
S.Cat
, S.Qty
, S.City
, S.Ctry
-- then you do not need to group by this column and therefore you do not get extra rows
--, C.CustType
)
select Cat, Qty, City, Ctry, NewC, RetC,
-- As per your example, you would no longer need GROUP BY,
-- therefore SUM function should be removed
SUM(IsNull(NewC, 0) + IsNull(RetC, 0)) as TotC
from CTE_FlattenNulls
-- As per your example, you would no longer need GROUP BY
group by Cat, Qty, City, Ctry
-- ,NewC -- Definitely not needed anymore
-- ,RetC -- Definitely not needed anymore
Everything else stays the same
To get to your result, why can you not just do a simple group by with conditional sum ?
It has no need for a CTE
See this example, also in this DBFiddle
select A.Cat,
A.Qty,
A.City,
min(A.Ctry) as Country,
sum(case when C.CustType = 'New' then C.NumItems else 0 end) as NewC,
sum(case when C.CustType = 'Returning' then C.NumItems else 0 end) as RetCust,
sum(C.NumItems) as TotC
from A
join Cust C on A.City = C.City
group by A.Cat,
A.Qty,
A.City
order by A.Cat, A.City
it returns this
Cat
Qty
City
Country
NewC
RetCust
TotC
Laptop
4
Berlin
Germany
8
2
10
Laptop
3
London
England
2
5
7
TV
5
London
England
2
5
7

Conditionally move values between columns

I have the following record set output:
ID Name Pay_Type Paid_Amnt Interest_Amnt
1 John Smith Benefit 1075 0
1 John Smith Interest 1.23 0
2 Tom Ryder Benefit 1123 0
3 Mark Thompson Benefit 1211 0
3 Mark Thompson Interest 1.34 0
What I'd like is for values with the Pay_Type = Interest to be placed in the Interest column.
Desired output:
ID Name Pay_Type Pay_Type 2 Paid_Amnt Interest_Amnt
1 John Smith Benefit Interest 1075 1.23
2 Tom Ryder Benefit NULL 1123 0
3 Mark Thompson Benefit Interest 1211 1.34
I tried something like the following:
Select row_number()over(partition by id, case when pay_type = 'Interest' then interest_amnt = paid_amnt
when pay_type = 'Interest' then paid_amnt = 0 end) as new_interest
Does anyone know how to get the desired results?
Thank you
declare #t table(id int, pay_type varchar(25), name varchar(100), paid_amnt float, interest_amnt float)
insert into #t values(1, 'Benefit', 'John Smith', 1075, 0),
(1, 'Interest', 'John Smith',1.23, 0),
(2, 'Benefit', 'Tom Ryder', 1123, 0),
(3, 'Benefit', 'Mark Thompson', 1211, 0),
(4, 'Interest', 'Mark Thompson', 1.34, 0)
select * from #t
Just in case you can have more than 2 records per person, I believe this will give you what you want, it utilizes a couple of subqueries and group by,
subquery x groups your records so you get the interest sums and benefits sums in a row per user,
subquery y uses CASE expressions to place the summed amounts into their proper columns or zero in case of it being Benefit/Interest and adds the pay type columns of pay_type1 and pay_type2 with values of Benefit and Interest respectively,
outer query groups everything together into 1 row per user, and sums their interest and benefit columns respectively:
SELECT y.[id] AS [ID], y.[name] AS [Name],
y.[pay_type1] AS [Pay_Type], y.[Pay_Type2], SUM(y.[Paid_Amnt]) AS [Paid_Amnt],
SUM(y.[Interest_Amnt]) AS [Interest_Amnt]
FROM
(
SELECT id, name, 'Benefit' AS [pay_type1], 'Interest' AS [pay_type2],
CASE WHEN pay_type = 'Benefit' THEN x.Amount ELSE 0 END AS [Paid_Amnt],
CASE WHEN pay_type = 'Interest' THEN x.Amount ELSE 0 END AS [Interest_Amnt]
FROM
(
SELECT id, pay_type, name, SUM(paid_amnt) AS [Amount]
FROM table as t
GROUP BY id, pay_type, name
) AS x
) AS y
GROUP BY y.[id], y.[name], y.[pay_type1], y.[pay_type2]

How to select changed columns

The Problem
I'm trying to detect and react to changes in a table where each update is being recorded as a new row with some values being the same as the original, some changed (the ones I want to detect) and some NULL values (not considered changed).
For example, given the following table MyData, and assuming the OrderNumber is the common value,
ID OrderNumber CustomerName PartNumber Qty Price OrderDate
1 123 Acme Corp. WG301 4 15.02 2020-01-02
2 456 Base Inc. AL337 7 20.15 2020-02-03
3 123 NULL WG301b 5 19.57 2020-01-02
If I execute the query for OrderNumber = 123 I would like the following data returned:
Column OldValue NewValue
ID 1 3
PartNumber WG301 WG301b
Qty 4 5
Price 15.02 19.57
Or possibly a single row result with only the changes filled, like this (however, I would strongly prefer the former format):
ID OrderNumber CustomerName PartNumber Qty Price OrderDate
3 NULL NULL WG301b 5 19.57 NULL
My Solution
I have not had a chance to test this, but I was considering writing the query with the following approach (pseudo-code):
select
NewOrNull(last.ID, prev.ID) as ID,
NewOrNull(last.OrderNumber, prev.OrderNumber) as OrderNumber
NewOrNull(last.CustomerName, prev.CustomerName) as CustomerName,
...
from last row with OrderNumber = 123
join previous row where OrderNumber = 123
Where the function NewOrNull(lastVal, prevVal) returns NULL if the values are equal or lastVal value is NULL, otherwise the lastVal.
Why I'm Looking for an Answer
I'm afraid that the ugly join, the number of calls to the function, and the procedural approach may make this approach not scalable. Before I start down the rabbit hole, I was wondering...
The Question
...are there any other approaches I should try, or any best practices to solving this specific type of problem?
I came up with a solution for the second (less preferred) format:
The Data
Using the following data:
INSERT INTO MyData
([ID], [OrderNumber], [CustomerName], [PartNumber], [Qty], [Price], [OrderDate])
VALUES
(1, 123, 'Acme Corp.', 'WG301', '4', '15.02', '2020-01-02'),
(2, 456, 'Base Inc.', 'AL337', '7', '20.15', '2020-02-03'),
(3, 123, NULL, 'WG301b', '5', '19.57', '2020-01-02'),
(4, 123, 'ACME Corp.', 'WG301b', NULL, NULL, '2020-01-02'),
(6, 456, 'Base Inc.', NULL, '7', '20.15', '2020-02-05');
The Function
This function returns the updated value if it has changed, otherwise NULL:
CREATE FUNCTION dbo.NewOrNull
(
#newValue sql_variant,
#oldValue sql_variant
)
RETURNS sql_variant
AS
BEGIN
DECLARE #ret sql_variant
SELECT #ret = CASE
WHEN #newValue IS NULL THEN NULL
WHEN #oldValue IS NULL THEN #newValue
WHEN #newValue = #oldValue THEN NULL
ELSE #newValue
END
RETURN #ret
END;
The Query
This query returns the history of changes for the given order number:
select dbo.NewOrNull(new.ID, old.ID) as ID,
dbo.NewOrNull(new.OrderNumber, old.OrderNumber) as OrderNumber,
dbo.NewOrNull(new.CustomerName, old.CustomerName) as CustomerName,
dbo.NewOrNull(new.PartNumber, old.PartNumber) as PartNumber,
dbo.NewOrNull(new.Qty, old.Qty) as Qty,
dbo.NewOrNull(new.Price, old.Price) as Price,
dbo.NewOrNull(new.OrderDate, old.OrderDate) as OrderDate
from MyData new
left join MyData old
on old.ID = (
select top 1 ID
from MyData pre
where pre.OrderNumber = new.OrderNumber
and pre.ID < new.ID
order by pre.ID desc
)
where new.OrderNumber = 123
The Result
ID OrderNumber CustomerName PartNumber Qty Price OrderDate
1 123 Acme Corp. WG301 4 15.02 2020-01-02
3 (null) (null) WG301b 5 19.57 (null)
4 (null) ACME Corp. (null) (null) (null) (null)
The Fiddle
Here's the SQL Fiddle that shows the whole thing in action.
http://sqlfiddle.com/#!18/b720f/5/0

Total Number of Leaves of same type in a month

I have 2 tables name EmployeeInfo and Leave and I am storing the values that which employee have taken which type of leave in month and how many times.
I am trying to calculate the number of leaves of same type but I'm stuck at one point for long time.
IF EXISTS(SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('Leave'))
BEGIN;
DROP TABLE [Leave];
END;
GO
IF EXISTS(SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('EmployeeInfo'))
BEGIN;
DROP TABLE [EmployeeInfo];
END;
GO
CREATE TABLE [EmployeeInfo] (
[EmpID] INT NOT NULL PRIMARY KEY,
[EmployeeName] VARCHAR(255)
);
CREATE TABLE [Leave] (
[LeaveID] INT NOT NULL PRIMARY KEY,
[LeaveType] VARCHAR(255) NULL,
[DateFrom] VARCHAR(255),
[DateTo] VARCHAR(255),
[Approved] Binary,
[EmpID] INT FOREIGN KEY REFERENCES EmployeeInfo(EmpID)
);
GO
INSERT INTO EmployeeInfo([EmpID], [EmployeeName]) VALUES
(1, 'Marcia'),
(2, 'Lacey'),
(3, 'Fay'),
(4, 'Mohammad'),
(5, 'Mike')
INSERT INTO Leave([LeaveID],[LeaveType],[DateFrom],[DateTo], [Approved], [EmpID]) VALUES
(1, 'Annual Leave','2018-01-08 04:52:03','2018-01-10 20:30:53', 1, 1),
(2, 'Sick Leave','2018-02-10 03:34:41','2018-02-14 04:52:14', 0, 2),
(3, 'Casual Leave','2018-01-04 11:06:18','2018-01-05 04:11:00', 1, 3),
(4, 'Annual Leave','2018-01-17 17:09:34','2018-01-21 14:30:44', 0, 4),
(5, 'Casual Leave','2018-01-09 23:31:16','2018-01-12 15:11:17', 1, 3),
(6, 'Annual Leave','2018-02-16 18:01:03','2018-02-19 17:16:04', 1, 2)
My query which I have tried so far look something like this.
SELECT Info.EmployeeName, Leave.LeaveType, SUM(DATEDIFF(Day, Leave.DateFrom, Leave.DateTo)) [#OfLeaves], DatePart(MONTH, Leave.DateFrom)
FROM EmployeeInfo Info, Leave
WHERE Info.EmpID = Leave.EmpID AND Approved = 1
GROUP BY Info.EmployeeName, Leave.LeaveType, [Leave].[DateFrom], [Leave].[DateTo]
And the record like given below
EmployeeName LeaveType #OfLeaves MonthNumber
-------------- ----------------- ----------- -----------
Fay Casual Leave 1 1
Fay Casual Leave 3 1
Lacey Annual Leave 3 2
Marcia Annual Leave 2 1
I want the record to look like this
EmployeeName LeaveType #OfLeaves MonthNumber
-------------- ----------------- ----------- -----------
Fay Casual Leave 4 1
Lacey Annual Leave 3 2
Marcia Annual Leave 2 1
If you don't want to modify existing query due to some constraint, this might work:
Select iq.EmployeeName, iq.LeaveType, SUM(iq.#OfLeaves) as #OfLeaves, iq.MonthNumber
From (
SELECT Info.EmployeeName, Leave.LeaveType, SUM(DATEDIFF(Day, Leave.DateFrom, Leave.DateTo)) [#OfLeaves], DatePart(MONTH, Leave.DateFrom) as MonthNumber
FROM EmployeeInfo Info, Leave
WHERE Info.EmpID = Leave.EmpID AND Approved = 1
GROUP BY Info.EmployeeName, Leave.LeaveType, [Leave].[DateFrom], [Leave].[DateTo]
)iq
group by iq.EmployeeName, iq.LeaveType, iq.MonthNumber
This just need small adjustment with your query in the GROUP BY clause. Instead of grouping them by [Leave].[DateFrom] and [Leave].[DateTo] which causes the row to be separated, you need to group it with the calculated column that uses datepart.
SELECT Info.EmployeeName,
Leave.LeaveType,
SUM(DATEDIFF(Day, Leave.DateFrom, Leave.DateTo)) [#OfLeaves],
DatePart(MONTH, Leave.DateFrom)
FROM EmployeeInfo Info
INNER JOIN Leave
ON Info.EmpID = Leave.EmpID
WHERE Approved = 1
GROUP BY Info.EmployeeName,
Leave.LeaveType,
DatePart(MONTH, Leave.DateFrom) -- <<<< change only this part
Here's a Demo.
I have also modified the syntax into ANSI format.

Selecting newest entries with different types

I'm designing table which will contain properties of some objects which will change over time.
CREATE TABLE [dbo].[ObjectProperties]
(
[Id] INT NOT NULL PRIMARY KEY IDENTITY,
[ObjectType] SMALLINT NOT NULL,
[Width] SMALLINT NOT NULL,
[Height] SMALLINT NOT NULL,
[Weight] SMALLINT NOT NULL
)
Let's say I have this ObjectTypes:
1 = Chair
2 = Table
And Data for this table:
INSERT INTO [dbo].[ObjectProperties] ([Id], [ObjectType], [Width], [Height], [Weight]) VALUES (1, 1, 50, 50, 1000)
INSERT INTO [dbo].[ObjectProperties] ([Id], [ObjectType], [Width], [Height], [Weight]) VALUES (2, 2, 80, 40, 500)
INSERT INTO [dbo].[ObjectProperties] ([Id], [ObjectType], [Width], [Height], [Weight]) VALUES (3, 1, 50, 50, 2000)
So, as you can see I had Chair object which Weight was 1000 then I changed weight to 2000. And I'm storing something like modification history of objects properties.
Now I want to select newest data from this table for each object. I know how to select newest data for each object one by one:
SELECT TOP 1 * FROM [ObjectProperties] WHERE ObjectType = 1 ORDER BY Id DESC
But what if I want to select few objects with one query? Like
SELECT ... * FROM [ObjectProperties] WHERE ObjectType IN (1, 2) ...
And receive rows with ids 2 and 3 (because 3 has newer properties for Chair than 1)
You can use a CTE with ROW_NUMBER ranking function:
WITH CTE AS(
SELECT *,
RN=ROW_NUMBER()OVER(PARTITION BY ObjectType ORDER BY ID DESC)
FROM [ObjectProperties] op
)
SELECT * FROM CTE WHERE RN = 1
AND ObjectType IN (1, 2)
Demo
The ROW_NUMBER returns one row for every ObjectType-group order by ID DESC(so the record with the highest ID) .If you want to filter by certain ID's you just have to apply the appropriate WHERE clause, either in the CTE or in the outer SELECT.
Ranking Functions
A simple (admittedly crude) way is as follows:
select * from ObjectProperties where id in
(select max(id) from ObjectProperties group by objecttype)
This gives:
Id ObjectType Width Height Weight
----------- ---------- ------ ------ ------
2 2 80 40 500
3 1 50 50 2000

Resources