Rows in pivot table not merging - sql-server

I have created the following table to illustrate what is happening
create table weather (
WDate varchar(10),
ItemCode varchar(8),
ItemValue int,
ItemUnits varchar(8))
insert into Weather values
('2020-02-10', 'MAXTEMP', 6, 'degC'),
('2020-02-10', 'MINTEMP', 2, 'degC'),
('2020-02-10', 'RAIN', 0, 'mm'),
('2020-02-11', 'MAXTEMP', 5, 'degC'),
('2020-02-11', 'RAIN', 20, 'mm'),
('2020-02-11', 'MINTEMP', 1, 'degC'),
('2020-02-12', 'RAIN', 5, 'mm'),
('2020-02-12', 'MAXTEMP', 8, 'degC'),
('2020-02-12', 'MINTEMP', 2, 'degC')
The data is not always in the same order because it can come from equipment that may not be time sync'ed. When I run the following query
SELECT
[wdate] as 'Date',
[MINTEMP] as 'Min Temp',
[MAXTEMP] as 'Max Temp',
[RAIN] as 'Rain'
FROM
(
SELECT
*
FROM
weather
) rawdata
PIVOT
(
min(ItemValue)
FOR ItemCode IN ([MINTEMP], [MAXTEMP], [RAIN])
) pitem
ORDER BY WDate
I get
WDate Min Temp Max Temp Rain
2020-02-10 2 6 NULL
2020-02-10 NULL NULL 0
2020-02-11 1 5 NULL
2020-02-11 NULL NULL 20
2020-02-12 2 8 NULL
2020-02-12 NULL NULL 5
I can't figure out why the Rain data doesn't end up on the same row as the Min and Max Temp. I was expecting
WDate Min Temp Max Temp Rain
2020-02-10 2 6 0
2020-02-11 1 5 20
2020-02-12 2 8 5

You must "FEED" the pivot with the minimum number of columns. Notice the ItemUnits is missing from the sub-select rawdata
Example
SELECT
[wdate] as 'Date',
[MINTEMP] as 'Min Temp',
[MAXTEMP] as 'Max Temp',
[RAIN] as 'Rain'
FROM
(
Select WDate
,ItemCode
,ItemValue
from Weather
) rawdata
PIVOT
(
min(ItemValue)
FOR ItemCode IN ([MINTEMP], [MAXTEMP], [RAIN])
) pitem
ORDER BY WDate
Returns
Date Min Temp Max Temp Rain
2020-02-10 2 6 0
2020-02-11 1 5 20
2020-02-12 2 8 5

Related

How to insert "empty" row extracting a month list?

I've this sp, which return a list of data, for each "month" (i.e. each row is a month). Somethings like that:
SELECT
*,
(CAST(t1.NumActivities AS DECIMAL) / t1.NumVisits) * 100 AS PercAccepted,
(CAST(t1.Accepted AS DECIMAL) / t1.Estimated) * 100 AS PercValue
FROM
(SELECT
MONTH(DateVisit) AS Month,
COUNT(*) AS NumVisits,
SUM(CASE WHEN DateActivity is not null THEN 1 ELSE 0 END) AS NumActivities,
SUM(Estimate) AS Estimated,
SUM(CASE WHEN DateActivity is not null THEN Estimate ELSE 0 END) AS Accepted
FROM [dbo].[Activities]
WHERE
DateVisit IS NOT NULL
AND (#year IS NULL OR YEAR(DateVisit) = #year)
AND (#clinicID IS NULL OR ClinicID = #clinicID)
GROUP BY MONTH(DateVisit)) t1
This is a result:
Month NumVisits NumActivities Estimated Accepted PercAccepted PercValue
1 5 1 13770.00 2520.00 20.00000000000 18.30065359477124
2 2 2 7900.00 7900.00 100.00000000000 100.00000000000000
3 1 0 2730.00 0.00 0.00000000000 0.00000000000000
8 1 1 3000.00 3000.00 100.00000000000 100.00000000000000
But as you can see, I could "miss" some Month (for example, here April "4" is missed).
Is it possible to insert, for the missing month/row, an empty (0) record? Such as:
Month NumVisits NumActivities Estimated Accepted PercAccepted PercValue
1 5 1 13770.00 2520.00 20.00000000000 18.30065359477124
2 2 2 7900.00 7900.00 100.00000000000 100.00000000000000
3 1 0 2730.00 0.00 0.00000000000 0.00000000000000
4 0 0 0 0 0 0
...
Here is a example with sample data:
CREATE TABLE #Report
(
Id INT,
Name nvarchar(max),
Percentage float
)
INSERT INTO #Report VALUES (1,'ONE',2.01)
INSERT INTO #Report VALUES (2,'TWO',3.01)
INSERT INTO #Report VALUES (5,'Five',5.01)
;WITH months(Month) AS
(
SELECT 1
UNION ALL
SELECT Month+1
FROM months
WHERE Month < 12
)
SELECT *
INTO #AllMonthsNumber
from months;
Your select query:
The left join will gives you the NULL for other months so just use ISNULL('ColumnName','String_to_replace')
\/\/\/\/
SELECT Month, ISNULL(Name,0), ISNULL(Percentage,0)
FROM AllMonthsNumber A
LEFT JOIN #Report B
ON A.Month = B.Id
EDIT:
Yes you can do it without creating AllMonthNumber Table:
You can use master..spt_values (found here) system table which contains the numbers so just with some where condition.
SELECT Number as Month, ISNULL(B.Name,0), ISNULL(Percentage,0)
FROM master..spt_values A
LEFT JOIN #Report B ON A.Number = B.Id
WHERE Type = 'P' AND number BETWEEN 1 AND 12

Distribute multiple payments to invoice lines

I'm having a problem allocating payments to invoice lines.
Data looks like this:
Invoice lines table (sales):
lineId invoiceId value
1 1 100
2 1 -50
3 1 40
4 2 500
Payments table (payments):
paymentId invoiceId amount
1 1 50
2 1 40
3 2 300
Now, I want to know for each invoice line the payment details. The payments shall be allocated first to the smallest values (i.e. line 2, -50)
The output should look like this:
lineId invoiceId value paymentId valuePaid valueUnpaid
2 1 -50 1 -50 0
3 1 40 1 40 0
1 1 100 1 60 40
1 1 100 2 40 0
4 2 500 3 300 200
The problem is solved in the post below, but the solution does not work if you have negative invoice values or if you have to split an invoice line in two payments.
https://dba.stackexchange.com/questions/58474/how-can-i-use-running-total-aggregates-in-a-query-to-output-financial-accumulati/219925?noredirect=1#comment431486_219925
This is what I've done so far based on the article above:
drop table dbo.#sales
drop table dbo.#payments
CREATE TABLE dbo.#sales
( lineId int primary key, -- unique line id
invoiceId int not null , -- InvoiceId foreign key
itemValue money not null ) -- value of invoice line.
CREATE TABLE dbo.#payments
( paymentId int primary key, -- Unique payment id
InvoiceId int not null, -- InvoiceId foreign key
PayAmount money not null
)
-- Example invoice, id #1, with 3 lines, total ammount = 90; id #2, with one line, value 500
INSERT dbo.#sales VALUES
(1, 1, 100),
(2, 1, -50),
(3, 1, 40),
(4, 2, 500) ;
-- Two payments paid towards invoice id#1, 50+40 = 90
-- One payment paid towards invoice id#2, 300
INSERT dbo.#Payments
VALUES (1, 1, 50),
(2, 1, 40),
(3, 2, 300);
-- Expected output should be as follows, for reporting purposes.
/* lineId, invoiceId, value, paymentId, valuePaid, valueUnpaid
2, 1, -50, 1, -50, 0
3, 1, 40, 1, 40, 0
1, 1, 100, 1, 60, 40
1, 1, 100, 2, 40, 0
4, 2, 500, 3, 300, 200 */
WITH inv AS
( SELECT lineId, invoiceId,
itemValue,
SumItemValue = SUM(itemValue) OVER
(PARTITION BY InvoiceId
ORDER BY ItemValue Asc
ROWS BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW)
FROM dbo.#Sales
)
, pay AS
( SELECT
PaymentId, InvoiceId, PayAmount as PayAmt,
SumPayAmt = SUM(PayAmount) OVER
(PARTITION BY InvoiceId
ORDER BY PaymentId
ROWS BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW)
FROM dbo.#payments
)
SELECT
inv.lineId,
inv.InvoiceId,
inv.itemValue,
pay.PaymentId,
PaymentAllocated =
CASE WHEN SumPayAmt <= SumItemValue - itemValue
OR SumItemValue <= SumPayAmt - PayAmt
THEN 0
ELSE
CASE WHEN SumPayAmt <= SumItemValue THEN SumPayAmt
ELSE SumItemValue END
- CASE WHEN SumPayAmt-PayAmt <= SumItemValue-itemValue
THEN SumItemValue-itemValue
ELSE SumPayAmt-PayAmt END
END
FROM inv JOIN pay
ON inv.InvoiceId = pay.InvoiceId
ORDER BY
inv.InvoiceId,
pay.PaymentId;
The current output is:
lineId InvoiceId itemValue PaymentId PaymentAllocated
2 1 -50.00 1 0.00
3 1 40.00 1 0.00
1 1 100.00 1 50.00
2 1 -50.00 2 0.00
3 1 40.00 2 0.00
1 1 100.00 2 40.00
4 2 500.00 3 300.00
Any direction will be appreciated. Thank you.
More info on the allocation rules:
Allocating first payment to the smallest sale (i.e. -50) was just a
convention to insure all sales lines get payments. If I’d allocate
arbitrary or with another rule, and line 1 (value 100) would get the
first payment, I’d use all the payment for this line and the rest of
the invoice would remain unallocated.
As I said, it’s just an convention. If someone else comes with a
different rule that works, it’s ok. Actually, the structure is
simplified compared with the production tables: payments also have a
payment date, type, … and a correct distribution should tell us what
invoice lines were paid at each payment time.
Payments are restricted by the logic of the system to be smaller then
the sum of the invoice lines. Well, it might be a case when payments
are greater: the total invoice is negative (ie: -100). In this case
we can insert in the payments table amounts in the range of -100: 0
and Total Payments are restricted to -100
In the end I found quite a simple and natural sollution - to allocate payments based on the percentage of each payment in the total value of the invoice.
drop table dbo.#sales
drop table dbo.#payments
CREATE TABLE dbo.#sales
( lineId int primary key, -- unique line id
invoiceId int not null , -- InvoiceId foreign key
itemValue money not null ) -- value of invoice line.
CREATE TABLE dbo.#payments
( paymentId int primary key, -- Unique payment id
InvoiceId int not null, -- InvoiceId foreign key
PayAmount money not null
)
-- Example invoice, id #1, with 3 lines, total ammount = 90; id #2, with one line, value 500
INSERT dbo.#sales VALUES
(1, 1, 100),
(2, 1, -50),
(3, 1, 40),
(4, 2, 500) ;
-- Two payments paid towards invoice id#1, 50+40 = 90
-- One payment paid towards invoice id#2, 300
INSERT dbo.#Payments
VALUES (1, 1, 50),
(2, 1, 40),
(3, 2, 300);
SELECT
s.lineId,
s.InvoiceId,
s.itemValue,
p.PayAmount,
p.PaymentId,
round(p.PayAmount / ts.SumItemValue,3) as PaymentPercent,
s.ItemValue * round(p.PayAmount / ts.SumItemValue,3) as AllocatedPayment
FROM dbo.#sales s
LEFT JOIN dbo.#payments p
ON s.InvoiceId = p.InvoiceId
LEFT JOIN (SELECT invoiceId, sum(itemValue) as SumItemValue FROM dbo.#sales GROUP BY invoiceId) ts
ON s.invoiceId = ts.invoiceId
ORDER BY
s.InvoiceId,
p.PaymentId;
And the resunt looks like this:
lineId InvoiceId itemValue PayAmount PaymentId PaymentPercent AllocatedPayment
1 1 100.00 50.00 1 0.556 55.60
2 1 -50.00 50.00 1 0.556 -27.80
3 1 40.00 50.00 1 0.556 22.24
3 1 40.00 40.00 2 0.444 17.76
2 1 -50.00 40.00 2 0.444 -22.20
1 1 100.00 40.00 2 0.444 44.40
4 2 500.00 300.00 3 0.60 300.00

How to use tally tables filling 0's in multi column query using T-SQL

I have used tally tables to fill in missing values (usualy 0's) in doing date queries and it works very well put I've never used them when querying "multiple tallies".
I have a hastily made example table and query
id Value Name Date
1 1 foo 2018-06-02
2 2 foo 2018-06-01
3 3 barr 2018-06-02
4 4 barr 2018-06-03
5 3 barr 2018-06-01
6 2 foo 2018-06-05
7 3 barr 2018-06-01
8 4 barr 2018-06-10
9 5 fum 2018-06-07
10 2 barr 2018-06-02
I can do a right outer join with a tally table where I hard code the name (barr) so I pull out data associated for one Name. (Note that the Numbers table is a big table of integers)
select 'barr' as Name,T2.Date,isnull(T1.Sum,0) as Sum
/* All records by date */
from
(
select Name,Date,Sum(Value) as Sum
from Test
where Name='barr'
group by Test.Date
) as T1
/* Tally table converted to date table. This gives a list of all dates */
right outer join
(
select convert(date,DateAdd(day,number,'2018-6-1')) as Date
from Numbers
where Number < 7
) as T2
on T1.Date = T2.Date
I get the expected output with 0's filled in:
barr 2018-06-01 6
barr 2018-06-02 5
barr 2018-06-03 4
barr 2018-06-04 0
barr 2018-06-05 0
barr 2018-06-06 0
barr 2018-06-07 0
This works, but I don't want to filter on a single name. I'd rather use a crossjoin (I'm guessing) that fills in all the empty days for all the names. Giving a result like this:
foo 2018-06-01 2
foo 2018-06-02 1
foo 2018-06-03 0
foo 2018-06-04 0
foo 2018-06-05 2
foo 2018-06-06 0
foo 2018-06-07 0
barr 2018-06-01 6
barr 2018-06-02 5
barr 2018-06-03 4
barr 2018-06-04 0
barr 2018-06-05 0
barr 2018-06-06 0
barr 2018-06-07 0
fum 2018-06-01 0
fum 2018-06-02 0
fum 2018-06-03 0
fum 2018-06-04 0
fum 2018-06-05 0
fum 2018-06-06 0
fum 2018-06-07 5
Do I need to iterate over the distinct names and do a union for each name or is there a way to use a full/crossjoin , or some other "clean" tsql construct?
I'd use a cross join.
If you have some other source of DistinctNames that is more efficient than calculating it from the values in Test then use that instead.
WITH Dates
AS (SELECT CONVERT(DATE, DATEADD(day, number, '2018-6-1')) AS Date
FROM Numbers
WHERE Number < 7),
DistinctNames
AS (SELECT DISTINCT Name
FROM Test),
Summed
AS (SELECT Name,
Date,
SUM(Value) AS Sum
FROM Test
GROUP BY Test.Date,
Test.Name)
SELECT dn.Name,
d.Date,
isnull(s.Sum, 0) AS Sum
FROM Dates d
CROSS JOIN DistinctNames dn
LEFT JOIN Summed s
ON s.Name = dn.Name
AND s.Date = d.Date
-- Sample data.
declare #StartDate as Date = '2018-06-01';
declare #EndDate as Date = DateAdd( day, 6, #StartDate );
select #StartDate as 'StartDate', #EndDate as 'EndDate';
declare #Samples as Table ( Id Int Identity, Val Int, Name VarChar(10), SampleDate Date );
insert into #Samples ( Val, Name, SampleDate ) values
( 1, 'foo', '2018-06-02' ),
( 2, 'foo', '2018-06-01' ),
( 3, 'barr', '2018-06-02' ),
( 4, 'barr', '2018-06-03' ),
( 3, 'barr', '2018-06-01' ),
( 2, 'foo', '2018-06-05' ),
( 3, 'barr', '2018-06-01' ),
( 4, 'barr', '2018-06-10' ),
( 5, 'fum', '2018-06-07' ),
( 2, 'barr', '2018-06-02' );
select * from #Samples;
-- The query.
with
-- All of the data summarized by Name and SampleDate .
GroupedData as (
select Name, SampleDate, Sum( Val ) as Total
from #Samples
where #StartDate <= SampleDate and SampleDate <= #EndDate
group by Name, SampleDate ),
-- The range of dates to be reported.
-- Use your existing Numbers table for better performance.
Dates as (
select #StartDate as ReportDate
union all
select DateAdd( day, 1, ReportDate )
from Dates
where ReportDate < #EndDate ),
-- The following assumes that you don't want to include names that have no data for the date range.
-- You may want to change it, e.g. to ignore only names without data in or prior to the date range.
DistinctNames as (
select distinct Name
from #Samples
where #StartDate <= SampleDate and SampleDate <= #EndDate )
-- Combine the data.
-- The cross join creates rows for every Name/Date pair.
-- The left outer join and coalesce merge the data together.
select Coalesce( GD.Name, DN.Name ) as Name,
Coalesce( GD.SampleDate, D.ReportDate ) as SampleDate,
Coalesce( Total, 0 ) as Total
from DistinctNames as DN cross join
Dates as D left outer join
GroupedData as GD on GD.Name = DN.Name and GD.SampleDate = D.ReportDate
order by Name, SampleDate;

T-SQL - 3 month moving sum - preceding null values

Using SQL Server 2016. I have the following data table (sample)
Target Date Total
-----------------
2018-01-24 1
2018-02-28 1
2018-03-02 1
2018-03-08 1
2018-03-15 1
2018-03-30 1
2018-04-16 1
2018-04-18 1
2018-04-30 1
I would like to get to get a 3 month moving sum (grouping is by month):
Target Date Total_Sum
-----------------------
2018-01-01 1
2018-02-01 2
2018-03-01 6
2018-04-01 8
Ok, this should get the answer you want. Firstly you need to total the value your months, then you can do a running total for the last 3 months:
CREATE TABLE SampleTable (TargetDate date, Total int);
GO
INSERT INTO SampleTable
VALUES ('20180124', 1),
('20180228', 1),
('20180302', 1),
('20180308', 1),
('20180315', 1),
('20180330', 1),
('20180416', 1),
('20180418', 1),
('20180430', 1);
GO
SELECT *
FROM SampleTable;
GO
WITH Months AS (
SELECT DATEADD(MONTH,DATEDIFF(MONTH, 0, TargetDate),0) AS TargetMonth, SUM(Total) AS MonthTotal
FROM SampleTable
GROUP BY DATEADD(MONTH,DATEDIFF(MONTH, 0, TargetDate),0))
SELECT TargetMonth,
SUM(MonthTotal) OVER (ORDER BY TargetMonth ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS Last3Months
FROM Months;
GO
DROP TABLE SampleTable;
GO
Pls try the below code
;WITH CTE(TargetDate,Total)
AS
(
SELECT '2018-01-24', 1 UNION ALL
SELECT '2018-02-28', 1 UNION ALL
SELECT '2018-03-02', 1 UNION ALL
SELECT '2018-03-08', 1 UNION ALL
SELECT '2018-03-15', 1 UNION ALL
SELECT '2018-03-30', 1 UNION ALL
SELECT '2018-04-16', 1 UNION ALL
SELECT '2018-04-18', 1 UNION ALL
SELECT '2018-04-30', 1
)
SELECT STUFF(TargetDate,9,2,'01') AS TargetDate
,Total_Sum
FROM
(
SELECT TargetDate,Total_Sum
,ROW_NUMBER()OVER(PARTITION BY Total_Sum ORDER BY TargetDate) AS Seq
FROM
(
SELECT TargetDate
,SUM(Total )OVER(ORDER BY MONTH(TargetDate) ) AS Total_Sum
FROM CTE
)dt
)fnl
WHERE Seq=1
Result
TargetDate Total_Sum
---------------------
2018-01-01 1
2018-02-01 2
2018-03-01 6
2018-04-01 9

SQL Server 2008 - merge rows when condition matched

I am running a stored procedure and getting the following value.
Name ID NewID Qty Rqty Total
Test1 1 5 4 9
Test2 10 1001 3 0 3
Test2 1001 4 2 6
Test3 15 1005 0 0 0
Test3 1005 3 4 7
If you look the Test3, where first id 15 has a new id 1005 But the Qty and Rqty is 0, next line I am getting ID 1005 and Qty, Rqty 3 and 4, total 7and this is correct.
What I am trying to do is when any ID got a new ID and Qty, Rqty both are 0, I need the following output in on row
Test3 15 1005 3 4 7
So, my final results will be -
Name ID NewID Qty Rqty Total
Test1 1 5 4 9
Test2 10 1001 3 0 3
Test2 1001 4 2 6
Test3 15 1005 3 4 7
My stored procedure is -
select Name, ID, NewID, Qty, RQty, SUM(Qty + RQty) as Total
from table1
Group By Name, ID, NewID, Qty, RQty
Order by Name
Could anyone help to solve this issue please.
Thanks
You will need to generate a psuedo "grouping" ID and NewID based on your conditions, group on those fields and select the MIN and MAX of the original ID and NewID along with aggregates of your other values. e.g.
CREATE TABLE dbo.Tests
(
Name varchar(10),
ID int,
[NewID] int NULL,
Qty int,
Rqty int
);
INSERT dbo.Tests (Name,ID,[NewID],Qty,Rqty)
--VALUES ('Test1',1,NULL,5,4)
-- ,('Test2',10,1001,3,0)
-- ,('Test2',1001,NULL,4,2)
-- ,('Test3',15,1005,0,0)
-- ,('Test3',1005,NULL,3,4);
VALUES ('test1',1,101,0,0)
,('test1',101,NULL,2,4)
,('test2',2,102,0,0)
,('test2',102,NULL,4,5)
,('test3',3,103,0,0)
,('test3',103,NULL,3,3)
,('test4',4,104,0,0)
,('test4',104,NULL,1,3)
,('test5',5,105,0,0)
,('test5',105,NULL,3,6);
SELECT t.Name,
min(t.ID) AS ID,
max(t.[NewID]) AS [NewID],
sum(t.Qty) AS Qty,
sum(t.Rqty) AS Rqty,
sum(t.Qty)+sum(t.Rqty) AS Total
FROM dbo.Tests AS t
CROSS APPLY (VALUES(CASE WHEN Qty = 0 AND Rqty = 0 AND t.[NewID] IS NOT NULL THEN [NewID] ELSE ID END,
CASE WHEN Qty = 0 AND Rqty = 0 AND t.[NewID] IS NOT NULL THEN -1 ELSE coalesce([NewID],-1) END)
) x(GroupingID, GroupingNewID)
GROUP BY Name,x.GroupingID,x.GroupingNewID
ORDER BY Name,max(t.ID);

Resources