Formatting SQL Server table data for reporting - sql

I have a table with the columns as below -
There are rows showing the allocation of various people under various project. The months (columns) can extend to Dec,20 and continue on from Jan,21 in the same pattern as above.
One Staff can be tagged to any number of projects in a given month.
Now to prepare a report on this I would like to format the data as below -
So basically for each project that a staff is assigned to, I would like to duplicate each of the 12 months for each year and show the designated allocation.
The name of the table containing the data is [Staff Allocation] and it has the following fields - [Staff ID],[Project ID],[Jan,20],[Feb,20],[Mar,20],[Apr,20] and so on as per the image above.
Is there any way to do this?
Any help on this is highly appreciated.
Adding in the sample data as below -
Staff ID
Project ID
Jan,20
Feb,20
Mar,20
Apr,20
May,20
Jun,20
Jul,20
1
20
0
0
0
100
80
10
0
1
30
0
0
0
0
20
90
100
2
20
100
100
100
0
0
0
0
3
50
80
100
0
0
0
0
0
3
60
15
0
0
0
20
0
0
3
70
5
0
100
100
80
0
0
create table test(StaffID int, ProjectID int, Jan20 int, Feb20 int, Mar20 int, Apr20 int, May20 int, Jun20 int, Jul20 int)
insert into test values
(1,20,0,0,0,100,80,10,0),
(1,30,0,0,0,0,20,90,100),
(2,20,100,100,100,0,0,0,0),
(3,50,80,100,0,0,0,0,0),
(3,60,15,0,0,0,20,0,0),
(3,70,5,0,100,100,80,0,0)
Select * from test

I get the sense (by your column names) that the source table will expand over time.
Here is an option that will dynamically unpivot your data without actually using Dynamic SQL
Example or dbFiddle
Select A.[Staff ID]
,A.[Project ID]
,[Month] = left([Key],3)
,[Year] = '20'+right([Key],2)
,Allocation = try_convert(int,B.[value])
From YourTable A
Cross Apply (
Select [Key]
,[Value]
From OpenJson( (Select A.* For JSON Path,Without_Array_Wrapper ) )
Where [Key] not in ('Staff ID','Project ID')
) B
Results

Here is an example of how you can unpivot your columns into rows by using cross apply and a values table constructor.
select StaffId, ProjectId, v.*
from t
cross apply(values
('Jan', 2020, Jan20),
('Feb', 2020, Feb20),
('Mar', 2020, Mar20),
('Apr', 2020, Apr20),
('May', 2020, May20),
('Jun', 2020, Jun20),
('Jul', 2020, Jul20)
)v([Month], [Year], Allocation);
Demo DB<>Fiddle

you need Unpivot and since your column names are in format of MMMYY.you can use Right and Left Functions as follows:
select [StaffID]
,[ProjectID]
,left(indicatorname,3) Month
,concat('20',right(indicatorname,2)) Year
,Allocation
from test
unpivot
(
Allocation
for indicatorname in ([Jan20]
,[Feb20]
,[Mar20]
,[Apr20]
,[May20]
,[Jun20]
,[Jul20]
)
) unpiv;
dbfiddle

Related

Using cross join with multiple variable in cte

I have some order numbers and want to check if any number has been skipped. I'll use left table method which is:
select * from
#CreatedCrossTable (which has all possibilities)
Left Join #MainTableWhichHaveRealSerialNo mt
where
mt is null
Order no structure is: "CodeType.Year.Month.SerialNo". For instance: "DP.21.07.001","DP.21.07.002".. or not DP, but "FB.21.07.001", "FB.21.07.002" etc.
I want to create a cross table schema for determine the skipped SerilNo values (CreatedCrossTable
above):
(Serial number is reset every month)
CodeType | Year | Month | SerialNo
DP 21 1 1
DP 21 1 2
DP 21 1 3
DP 21 1 4
...
(All SerialNos must increase max serial number of the original table's SerialNo (MainTableWhichHaveRealSerialNo) Also codeType,year and month values must match)
DP 21 2 1
DP 21 2 2
...
FB 21 1 1
FB 21 1 2
...
FB 21 1 1
FB 21 1 2
FB 21 1 3
...
Each Codes' and Month's serial number have a different Maximum Number
for creating CrossTable. I've written that code:
;WITH cteSerialNo AS
(
SELECT 1 AS ORDERNO
UNION ALL
SELECT (ORDERNO+1) AS ORDERNO FROM cteSerialNo WHERE ORDERNO < MAX_ORDER_NO
)
,cteMonthYear AS
(
SELECT CAST('2021.01.01' AS DATE) AS Dt
UNION ALL
SELECT DATEADD(MONTH , 1, Dt) AS Dt
FROM cteMonthYear
WHERE DATEADD (MONTH, 1, Dt) < GETDATE()
)
SELECT
*
FROM
(
SELECT
CODES.CODETYPE,
YEAR(Dts.Dt) AS 'YEAR',
MONTH(Dts.Dt) AS 'MONTH'
FROM
##KK_TBL_CODETYPES AS CODES
CROSS JOIN cteMonthYear AS Dts
) AS CROSSTABLE
CROSS JOIN cteSerialNo AS cSN
How can i enter (MAX_ORDER_NO) for each variable in this code?
Assuming that the max SerialNo value is based on the existing values in the SerialNo column, you would want to just find all possible combinations up to that SerialNo value and then remove those that have a match in the source data:
-- Define test data
declare #t table(CodeType varchar(2),[Year] int,[Month] int,SerialNo int);
insert into #t values
('DP',21,1,1)
,('DP',21,1,2)
,('DP',21,1,3)
--,('DP',21,1,4) -- Missing so should be in Output
,('DP',21,1,5)
,('DP',21,2,1)
,('DP',21,2,2)
,('FB',21,1,1)
,('FB',21,1,2)
,('FB',21,2,1)
,('FB',21,2,2)
--,('FB',21,2,3) -- Missing so should be in Output
,('FB',21,2,4)
;
with m as -- Get Max SerialNo for each grouping
(
select CodeType
,[Year]
,[Month]
,max(SerialNo) as MaxSerialNo
from #t
group by CodeType
,[Year]
,[Month]
)
,t as -- Create a table with 10 rows in
(
select t
from(values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) as t(t)
)
,n as -- Self join those 10 rows 5 times to generate a possible 10*10*10*10*10 = 100,000 incrementing numbers using row_number
(
select top(select max(MaxSerialNo) from m) row_number() over (order by (select null)) as n
from t,t t2,t t3,t t4,t t5
)
-- Join from the numbers table to the source data to generate all possible SerialNo values up to the Max
select m.CodeType
,m.[Year]
,m.[Month]
,n.n as SerialNo
from n
left join m
on n.n <= m.MaxSerialNo
except -- Then exclude any that are in the source data
select CodeType
,[Year]
,[Month]
,SerialNo
from #t
order by CodeType
,[Year]
,[Month]
,SerialNo
Output
CodeType
Year
Month
SerialNo
DP
21
1
4
FB
21
2
3

Accounting Calculate Debit credit in SQL(ssms)

I have an accounting calculation problem. I want to write it with SQL Query (in ssms).
I have two groups of documents related to one person (creditor and debtor)
Creditor documents cover debtor documents.
Consider the following example: (How can the result be achieved?)
USE [master]
GO
DROP TABLE IF EXISTS #credit/*creditor=0*/,#debit/*Debtor=1*/
SELECT *
INTO #debit
FROM (values
(88,'2/14',1,5,1),(88,'2/15',2,5,1)
)A (personID,DocDate,DocID,Fee,IsDebit)
SELECT *
INTO #credit
FROM (values
(88,'2/16',3,3,0),(88,'2/17',4,7,0)
)A (personID,DocDate,DocID,Fee,ISDeb)
SELECT * FROM #credit
SELECT * FROM #debit
--result:
;WITH res AS
(
SELECT 88 AS personID ,1 deb_DocID ,5 deb_Fee , 3 Cre_DocID ,3 Cre_Fee, 0 remain_Cre_Fee
UNION
SELECT 88 AS personID ,1 deb_DocID ,5 deb_Fee , 4 Cre_DocID ,7 Cre_Fee, 5 remain_Cre_Fee
UNION
SELECT 88 AS personID ,2 deb_DocID ,5 deb_Fee , 4 Cre_DocID ,7 Cre_Fee, 0 remain_Cre_Fee
)
SELECT *
FROM res
Sample data
Using an ISO date format to avoid any confusion.
The docdate and isdebit columns will not be used in the solution...
I ignored the docdate under the assumptions that the values are incremental and that it is allow to deposit a credit fee before any debit fee.
The isdebit flag seems redundant if you are going to store debit and credit transactions in separate tables anyway.
Updated sample data:
create table debit
(
personid int,
docdate date,
docid int,
fee int,
isdebit bit
);
insert into debit (personid, docdate, docid, fee, isdebit) values
(88, '2021-02-14', 1, 5, 1),
(88, '2021-02-15', 2, 5, 1);
create table credit
(
personid int,
docdate date,
docid int,
fee int,
isdebit bit
);
insert into credit (personid, docdate, docid, fee, isdebit) values
(88, '2021-02-16', 3, 3, 0),
(88, '2021-02-17', 4, 7, 0);
Solution
Couple steps here:
Construct a rolling sum for the debit fees. Done with a first common table expression (cte_debit).
Construct a rolling sum for the credit fees. Done with a second common table expression (cte_credit).
Take all debit info (select * from cte_debit)
Find the first credit info that applies to the current debit info. Done with a first cross apply (cc1). This contains the docid of the first document that applies to the debit document.
Find the last credit info that applies to the current debit info. Done with a second cross apply (cc2). This contains the docid of the last document that applies to the debit document.
Find all credit info that applies to the current debit info by selecting all documents between the first and last applicable document (join cte_credit cc on cc.docid >= cc1.docid and cc.docid <= cc2.docid).
Combine the rolling sum numbers to calculate the remaining credit fees (cc.credit_sum - cd.debit_sum). Use a case expression to filter out negative values.
Full solution:
with cte_debit as
(
select d.personid,
d.docid,
d.fee,
sum(d.fee) over(order by d.docid rows between unbounded preceding and current row) as debit_sum
from debit d
),
cte_credit as
(
select c.personid,
c.docid,
c.fee,
sum(c.fee) over(order by c.docid rows between unbounded preceding and current row) as credit_sum
from credit c
)
select cd.personid,
cd.docid as deb_docid,
cd.fee as deb_fee,
cc.docid as cre_docid,
cc.fee as cre_fee,
case
when cc.credit_sum - cd.debit_sum >= 0
then cc.credit_sum - cd.debit_sum
else 0
end as cre_fee_remaining
from cte_debit cd
cross apply ( select top 1 cc1.docid, cc1.credit_sum
from cte_credit cc1
where cc1.personid = cd.personid
and cc1.credit_sum <= cd.debit_sum
order by cc1.credit_sum desc ) cc1
cross apply ( select top 1 cc2.docid, cc2.credit_sum
from cte_credit cc2
where cc2.personid = cd.personid
and cc2.credit_sum >= cd.debit_sum
order by cc2.credit_sum desc ) cc2
join cte_credit cc
on cc.personid = cd.personid
and cc.docid >= cc1.docid
and cc.docid <= cc2.docid
order by cd.personid,
cd.docid,
cc.docid;
Result
personid deb_docid deb_fee cre_docid cre_fee cre_fee_remaining
-------- --------- ------- --------- ------- -----------------
88 1 5 3 3 0
88 1 5 4 7 5
88 2 5 4 7 0
Fiddle to see things in action. This also contains the intermediate CTE results and some commented helper columns that can be uncommented to help to further understand the solution.

SQL query to duplicate each row 12 times

I have a table which has columns site,year and sales . this table is unique on site+year eg
site year sales
-------------------
a 2012 50
b 2013 100
a 2006 35
Now what I want to do is make this table unique on site+year+month. Thus each row gets duplicated 12 times, a month column is added which is labelled from 1-12 and the sales values get divided by 12 thus
site year month sales
-------------------------
a 2012 1 50/12
a 2012 2 50/12
...
a 2012 12 50/12
...
b 2013 1 100/12
...
a 2006 12 35/12
I am doing this on python currently and it works like a charm, but I need to do this in SQL (ideally PostgreSQL since I will be using this as a datasource for tableau)
It would be very helpful if someone can provide the explanations with the solution as well, since I am a novice at this
You can use generate_series() for that
select t.site, t.year, g.month, t.sales / 12
from the_table t
cross join generate_series(1,12) as g (month)
order by t.site, t.year, g.month;
If the column sales is an integer, you should cast that to a numeric to avoid the integer division: t.sales::numeric / 12
Online example: http://rextester.com/GUWPI39685
Try this approach (For T-SQL - MS SQL) :
DECLARE #T TABLE
(
[site] VARCHAR(5),
[year] INT,
sales INT
)
INSERT INTO #T
VALUES('A',2012,50),('B',2013,100),('C',2006,35)
;WITH CTE
AS
(
SELECT
MonthSeq = 1
UNION ALL
SELECT
MonthSeq = MonthSeq+1
FROM CTE
WHERE MonthSeq <12
)
SELECT
T.[site],
T.[year],
[Month] = CTE.MonthSeq,
sales = T.[sales]/12
FROM CTE
CROSS JOIN #T T
ORDER BY T.[site],CTe.MonthSeq

Build table with previous months (cumulative)

I'm a bit lost with the following problem that I need to solve with an SQL query, no plsql. The idea is to build a cumulative column to calculate all previous months. The input table looks like
Month
1
2
3
..
24
I need build the following table :
Month Cum_Month
1 1
2 1
2 2
3 1
3 2
3 3
..
24 1
...
24 23
All this in SQL Server 2008, thanks in advance
You can do it like this:
DECLARE #tbl TABLE ([Month] INT)
INSERT #tbl VALUES
(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),
(11),(12),(13),(14),(15),(16),(17),(18),(19),(20),(21),(22),(23),(24)
SELECT Month
, ROW_NUMBER() OVER (PARTITION BY Month ORDER BY Month) num
FROM #tbl a
JOIN
(
SELECT *
FROM master..spt_values
WHERE type = 'P'
)
b ON b.number < a.Month
master..spt_values is used to generate numbers, after numbers are generated result of the subquery is joined on the #tbl to get the number of rows that corresponds to the month. After that ROW_NUMBER is used to create appropriate ordinal numbers for each month.
Here's a pretty cool trick not using any tables:
SELECT N.Number as Month, N2.Number as Cum_Month
FROM
(SELECT Number FROM master..spt_values WHERE Number BETWEEN 1 AND 24 AND Type = 'P') N
JOIN (SELECT Number FROM master..spt_values WHERE Number BETWEEN 1 AND 24 AND Type = 'P') N2 ON N.Number >= N2.Number
ORDER BY N.Number, N2.Number
And the Fiddle.
And if you really don't want the last 24 24 (why not), just change the second query to between 1 and 23).

Best way to interpolate values in SQL

I have a table with rate at certain date :
Rates
Id | Date | Rate
----+---------------+-------
1 | 01/01/2011 | 4.5
2 | 01/04/2011 | 3.2
3 | 04/06/2011 | 2.4
4 | 30/06/2011 | 5
I want to get the output rate base on a simple linear interpolation.
So if I enter 17/06/2011:
Date Rate
---------- -----
01/01/2011 4.5
01/04/2011 3.2
04/06/2011 2.4
17/06/2011
30/06/2011 5.0
the linear interpolation is (5 + 2,4) / 2 = 3,7
Is there a way to do a simple query (SQL Server 2005), or this kind of stuff need to be done in a programmatic way (C#...) ?
Something like this (corrected):
SELECT CASE WHEN next.Date IS NULL THEN prev.Rate
WHEN prev.Date IS NULL THEN next.Rate
WHEN next.Date = prev.Date THEN prev.Rate
ELSE ( DATEDIFF(d, prev.Date, #InputDate) * next.Rate
+ DATEDIFF(d, #InputDate, next.Date) * prev.Rate
) / DATEDIFF(d, prev.Date, next.Date)
END AS interpolationRate
FROM
( SELECT TOP 1
Date, Rate
FROM Rates
WHERE Date <= #InputDate
ORDER BY Date DESC
) AS prev
CROSS JOIN
( SELECT TOP 1
Date, Rate
FROM Rates
WHERE Date >= #InputDate
ORDER BY Date ASC
) AS next
As #Mark already pointed out, the CROSS JOIN has its limitations. As soon as the target value falls outside the range of defined values no records will be returned.
Also the above solution is limited to one result only. For my project I needed an interpolation for a whole list of x values and came up with the following solution. Maybe it is of interested to other readers too?
-- generate some grid data values in table #ddd:
CREATE TABLE #ddd (id int,x float,y float, PRIMARY KEY(id,x));
INSERT INTO #ddd VALUES (1,3,4),(1,4,5),(1,6,3),(1,10,2),
(2,1,4),(2,5,6),(2,6,5),(2,8,2);
SELECT * FROM #ddd;
-- target x-values in table #vals (results are to go into column yy):
CREATE TABLE #vals (xx float PRIMARY KEY,yy float null, itype int);
INSERT INTO #vals (xx) VALUES (1),(3),(4.3),(9),(12);
-- do the actual interpolation
WITH valstyp AS (
SELECT id ii,xx,
CASE WHEN min(x)<xx THEN CASE WHEN max(x)>xx THEN 1 ELSE 2 END ELSE 0 END flag,
min(x) xmi,max(x) xma
FROM #vals INNER JOIN #ddd ON id=1 GROUP BY xx,id
), ipol AS (
SELECT v.*,(b.x-xx)/(b.x-a.x) f,a.y ya,b.y yb
FROM valstyp v
INNER JOIN #ddd a ON a.id=ii AND a.x=(SELECT max(x) FROM #ddd WHERE id=ii
AND (flag=0 AND x=xmi OR flag=1 AND x<xx OR flag=2 AND x<xma))
INNER JOIN #ddd b ON b.id=ii AND b.x=(SELECT min(x) FROM #ddd WHERE id=ii
AND (flag=0 AND x>xmi OR flag=1 AND x>xx OR flag=2 AND x=xma))
)
UPDATE v SET yy=ROUND(f*ya+(1-f)*yb,8),itype=flag FROM #vals v INNER JOIN ipol i ON i.xx=v.xx;
-- list the interpolated results table:
SELECT * FROM #vals
When running the above script you will get the following data grid points in table #ddd
id x y
-- -- -
1 3 4
1 4 5
1 6 3
1 10 2
2 1 4
2 5 6
2 6 5
2 8 2
[[ The table contains grid points for two identities (id=1 and id=2). In my example I referenced only the 1-group by using where id=1 in the valstyp CTE. This can be changed to suit your requirements. ]]
and the results table #vals with the interpolated data in column yy:
xx yy itype
--- ---- -----
1 2 0
3 4 0
4.3 4.7 1
9 2.25 1
12 1.5 2
The last column itype indicates the type of interpolation/extrapolation that was used to calculate the value:
0: extrapolation to lower end
1: interpolation within given data range
2: extrapolation to higher end
This working example can be found here.
The trick with CROSS JOIN here is it wont return any records if either of the table does not have rows (1 * 0 = 0) and the query may break. Better way to do is use FULL OUTER JOIN with inequality condition (to avoid getting more than one row)
( SELECT TOP 1
Date, Rate
FROM Rates
WHERE Date <= #InputDate
ORDER BY Date DESC
) AS prev
FULL OUTER JOIN
( SELECT TOP 1
Date, Rate
FROM Rates
WHERE Date >= #InputDate
ORDER BY Date ASC
) AS next
ON (prev.Date <> next.Date) [or Rate depending on what is unique]