How to invert rows and columns using a T-SQL Pivot Table - sql

I have a query that returns one row. However, I want to invert the rows and columns, meaning show the rows as columns and columns as rows. I think the best way to do this is to use a pivot table, which I am no expert in.
Here is my simple query:
SELECT Period1, Period2, Period3
FROM GL.Actuals
WHERE Year = 2009 AND Account = '001-4000-50031'
Results (with headers):
Period1, Period2, Period3
612.58, 681.36, 676.42
I would like for the results to look like this:
Desired Results:
Period, Amount
Jan, 612.58
Feb, 681.36
Mar, 676.42
This is a simple example, but what I'm really after is a bit more comlex than this. I realize I could produce theses results by using several SELECT commands instead. I'm just hoping someone can shine some light on how to accomplish this with a Pivot Table or if there is yet a better way.

For a fixed set of fields, you can use a UNION of select statements, each statement fetching one of the fields. Standard SQL does not provide a way to transpose the result for a variable number of fields, e.g. select * from table.
If MSSQL has an extension which helps (like pivot), I don't know about it.

Try something like this (tested on MSSQL2008):
DECLARE #Data TABLE(Period1 Decimal(5, 2), Period2 Decimal(5, 2), Period3 Decimal(5, 2))
INSERT #Data
VALUES (612.58, 681.36, 676.42)
SELECT Period, Amount
FROM (SELECT Period1 AS Jan, Period2 AS Feb, Period3 AS Mar FROM #Data) AS D
UNPIVOT (Amount FOR Period IN (Jan, Feb, Mar)) AS U
Update
Based on Jeff's comment, how about this:
DECLARE #Actuals TABLE(Account INT, [Year] INT, Period1 Decimal(5, 2), Period2 Decimal(5, 2), Period3 Decimal(5, 2))
INSERT #Actuals VALUES (1, 2010, 612.58, 681.36, 676.42)
INSERT #Actuals VALUES (1, 2009, 512.58, 581.36, 576.42)
SELECT Account, Period, Amount
FROM
(
SELECT a.Account, a.Period1 AS Jan, a.Period2 AS Feb, a.Period3 AS Mar, a1.Period1 AS Jan1, a1.Period2 AS Feb1, a1.Period3 AS Mar1
FROM #Actuals AS a
LEFT OUTER JOIN #Actuals AS a1 ON a.Account = a1.Account AND a1.[Year] = a.[Year] - 1
WHERE a.[Year] = 2010
) AS d
UNPIVOT (Amount FOR Period IN (Jan, Feb, Mar, Jan1, Feb1, Mar1)) AS u
or, with an explicit year column:
DECLARE #Actuals TABLE(Account INT, [Year] INT, Period1 Decimal(5, 2), Period2 Decimal(5, 2), Period3 Decimal(5, 2))
INSERT #Actuals VALUES (1, 2010, 612.58, 681.36, 676.42)
INSERT #Actuals VALUES (1, 2009, 512.58, 581.36, 576.42)
SELECT Account, [Year], Period, Amount
FROM
(
SELECT a.Account, a.[Year], a.Period1 AS Jan, a.Period2 AS Feb, a.Period3 AS Mar
FROM #Actuals AS a WHERE a.[Year] IN (2009, 2010)
) AS d
UNPIVOT (Amount FOR Period IN (Jan, Feb, Mar)) AS u

Check the unnest function, its quite amazing with little overhead. Just add a select query on top of the nested select query and unnest at that point:
SELECT id,
unnest(array['a', 'b', 'c']) AS colname,
unnest(array[a, b, c]) AS thing
FROM foo
ORDER BY id;

Related

Classic banking task

I think this is a usual task in banking area.
I need to fill 'Income' column by previous values from 'Outcome'. But every 'Outcome' value calculated like Outcome = Income + Debit - Credit from current row (each rows).
I guess I should use lag() for 'Income'. But this creates cyclicality in calculating.
I hope this can help:
create table account(acc_date date,income int, debit int, credit int, outcome int);
insert into account values('2021-01-01', 100,800,500,400),
('2021-02-01', null,900,1500,null),
('2021-03-01', null,1700,2000,null),
('2021-04-01', null,2100,2800,null),
('2021-05-01', null,3500,4000,null);
select * from account;
Untested, but by using a sum() over() and coalesce in concert with the lag() over()
with cte as (
Select *
,OutCome = sum( isnull(Income,0)+Debit-Credit ) over (order by date)
From YourTable
)
Select Date
,Income = coalesce(Income,lag(outcome,1) over (order by date))
,Credit
,Debit
,OutCome
From cte

SQL Server - How can I query to return products only when their sales exceeds a certain percentage?

The basic requirement is this: We capture sales by day of week and product. If more than half* of the day's sales came from one product, we want to capture that. Else we show "none".
So image we sell shoes, pants and shirts. On Monday, we sold $100 of each. So it was a three way split, and each category accounted for 33.3% of sales. We show "none". On Tuesday though, half of our sales came from shoes, and on Wednesday, 80% from shirts. So we want to see that.
The query below returns the desired result, but I'm not a fan of a queries within queries within queries. They can be inefficient and hard to read, and I feel like there's a cleaner way. Can this be improved upon?
*The requirement for half will be a parameter (#threshold here). In some cases, we might want to show only when it's 75% or more of sales. Obviously that parameter has to be >= 50%.
declare #sales as table (day_of_week varchar(16), product varchar(8), sales_amt int)
insert into #sales values ('monday', 'shoes', 100)
insert into #sales values ('monday', 'pants', 100)
insert into #sales values ('monday', 'shirts', 100)
insert into #sales values ('tuesday', 'shoes', 500)
insert into #sales values ('tuesday', 'pants', 300)
insert into #sales values ('tuesday', 'shirts', 200)
insert into #sales values ('wednesday', 'shoes', 100)
insert into #sales values ('wednesday', 'pants', 100)
insert into #sales values ('wednesday', 'shirts', 800)
declare #threshold as decimal(3,2) = 0.5
select day_of_week, case when pct_of_day >= #threshold then product else 'none' end half_of_sales from (
select day_of_week, product, pct_of_day, row_number() over (partition by day_of_week order by pct_of_day desc) _rn
from (
select day_of_week, product, sum(sales_amt) * 1.0 / sum(sum(sales_amt)) over (partition by day_of_week) pct_of_day
from #sales
group by day_of_week, product
) x
) z
where _rn = 1
maybe a little easier to read?
DECLARE #threshold AS decimal(3, 2) = 0.5;
WITH ssum
AS (SELECT
day_of_week,
SUM(sales_amt) sa
FROM #sales
GROUP BY day_of_week)
SELECT
s.day_of_week,
MAX(CASE WHEN s.sales_amt * 1.0 / ssum.sa >= #threshold THEN s.product ELSE 'none' END) threshold
FROM ssum
INNER JOIN #sales AS s
ON ssum.day_of_week = s.day_of_week
GROUP BY s.day_of_week
Firstly, you can place the nested queries in CTEs, which can make them easier to read. It won't make them more efficient, but then nested queries are not necessarily inefficient in themselves, not sure why you think so
Second, the query could be optimized, because the row-numbering is equally valid on the non-percentaged sum(sales_amt) value, so it can be on the same level as the windowed sum over
declare #threshold as decimal(3,2) = 0.5;
with GroupedSales as (
select
day_of_week,
product,
sum(sales_amt) * 1.0 / sum(sum(sales_amt)) over (partition by day_of_week) pct_of_day,
row_number() over (partition by day_of_week order by sum(sales_amt) desc) _rn
from #sales
group by
day_of_week,
product
)
select
day_of_week,
case when pct_of_day >= #threshold
then product
else 'none'
end half_of_sales
from GroupedSales
where _rn = 1;

Reset running total when value from reference table is zero

I created a view where its query is a running total of another view. As we all know, running total will continue to do sum regardless if the reference table/view is equal to zero. In my case, I would like to reset the running total when the reference view has a value of zero. I'm a bit new to SQL Server so I don't know how to approach this..
To better understand my question, here's my reference view (for this example, I created it as a table) where I will compute the running total:
Create table Net_Cash(
ID int IDENTITY primary key,
MO int,
YR int,
LC decimal(6,2)
);
insert into Net_Cash values
(1, 2011, 56.23),
(2, 2011, 881.4),
(3, 2011, 195.09),
(4, 2011, 522.9),
(5, 2011, 0),
(6, 2011, 355.66),
(7, 2011, 0),
(8, 2011, 344.86);
Here's my running total query:
SELECT
MO,
YR, (sum(LC) OVER (ORDER BY YR, MO)) AS LC
FROM
Net_Cash
Result:
Expected result:
I want the LC column to reset to zero (reset running total) if the LC column of Net_Cash is zero.
Thanks for the help!
You need to assign groups to the data. You can do so by counting the number of 0s before each row:
SELECT MO, YR,
SUM(LC) OVER (PARTITION BY grp ORDER BY YR, MO) AS LC
FROM (SELECT nc.*,
SUM(CASE WHEN LC = 0 THEN 1 ELSE 0 END) OVER (ORDER BY YR, MO) as grp
FROM Net_Cash nc
) nc;

How to divide results into separate rows based on year?

I have a query that looks at profits and operations costs of different stores based on the fiscal year, and currently the fiscal years and variables are sorted into single, respective columns such as:
FiscalYear Metric Store Amount
2017 Profit A 220
2017 Cost A 180
2018 Profit B 200
2018 Cost B 300
...
I need to cross tab the rows so that for each store, I can compare the 2017 profit against the 2018 profit, and 2017 cost against the 2018 cost.
I broke out profits and costs by creating CASE WHEN statements for the ProfitLossTable, but I don't know how to make it create a "2017 Profit" and "2018 Profit" column, respectively, for each Store.
WITH [Profits, Cost] AS
(
SELECT ID, StoreID, Number, FYYearID,
CASE WHEN ID = 333 then Number END AS Profit
CASE WHEN ID = 555 then Number END AS Cost
FROM ProfitLossTable
),
Location AS
(
Select StoreID, StoreName
FROM StoreTable
),
FiscalMonth AS
(
SELECT FYYearID, FYYear
FROM FiscalMonthTable
)
SELECT A.Profit, A.Cost
FROM [Profits, Cost] A
JOIN Location B
ON A.StoreID = B.StoreID
JOIN FiscalMonth C
ON A.FYYearID = C.FYYearID
The code above shows this, and I feel like I am close to creating columns based on year, but I don't know what to do next.
FiscalYear Store Profit Cost
2017 A 220 100
2017 A 180 100
2018 B 200 100
2018 B 300 100
As a working (on my machine anyway ;-p) example using your data:
create table #temp(
FiscalYear int not null,
Metric nvarchar(50) not null,
Store nvarchar(10) not null,
Amount int not null
)
insert into #temp
values
(2017, N'Profit', N'A', 220),
(2017, N'Cost', N'A', 180),
(2018, N'Profit', N'B', 200),
(2018, N'Cost', N'B', 300)
select * from #temp
select Metric,
[2017] as [2017],
[2018] as [2018]
from (select FiscalYear, Amount, Metric from #temp) base_data
PIVOT
(SUM(Amount) FOR FiscalYear in ([2017], [2018])
) as pvt
order by pvt.Metric
drop table #temp

how to view sql query horizontally using pivot from 1 table

table.
rows of my table. for january
rows for february
My sample query
select id2,
(select budget from tblmonth where id2='1' and month='January' and groups='CCARE') as jbud1,
(select actual from tblmonth where id2='1' and month='January' and groups='CCARE') as jact1,
(select variance from tblmonth where id2='1' and month='January' and groups='CCARE') as jvar1,
(select [percent] from tblmonth where id2='1' and month='January' and groups='CCARE') as jper1,
(select budget from tblmonth where id2='2' and month='February' and groups='CCARE') as fbud2,
(select actual from tblmonth where id2='2' and month='February' and groups='CCARE') as fact2,
(select variance from tblmonth where id2='2' and month='February' and groups='CCARE') as fvar2,
(select [percent] from tblmonth where id2='2' and month='February' and groups='CCARE') as fper2
from tblmonth where groups='CCARE' and id2='1' and month='January'
my problem with this query is it returns duplicate values.
i want to achieve is.
the columns budget, actual, variance, percent from each month will be this display in a single result. like this in the image.
thanks in advance :) im only new to sql and im using a sql server 2014.
At first I create temp table, similar to yours:
CREATE TABLE #tblmonth (
id2 smallint,
[year] varchar(50),
groups varchar(100),
element varchar(100),
[month] varchar(50),
budget decimal(18,2),
actual decimal(18,2),
variance decimal(18,2),
[percent] decimal(18,2),
)
INSERT INTO #tblmonth VALUES
(1, 2016, 'CCARE', 'Basic', 'January', 52.28, 43.00, 43.98, 0.00),
(2, 2016, 'CCARE', 'Bonuses', 'January', 1.77, 17.10, -46.12, 0.00),
(3, 2016, 'CCARE', 'Overtime', 'January', 2.34, 20.20, 7.98, 0.00),
(4, 2016, 'CCARE', 'Comminication', 'January', 19.01, 27.34, -81.98, 0.00),
(5, 2016, 'CCARE', 'HDMF', 'January', 0.98, 22.17, -22.98, 0.00),
(1, 2016, 'CCARE', 'Basic', 'February', 152.28, 3.00, 4.98, 0.00),
(2, 2016, 'CCARE', 'Bonuses', 'February', 12.77, 1.10, -4.12, 0.00),
(3, 2016, 'CCARE', 'Overtime', 'February', 23.34, 3.20, 0.98, 0.00),
(4, 2016, 'CCARE', 'Comminication', 'February', 191.01, 2.34, -1.98, 0.00),
(5, 2016, 'CCARE', 'HDMF', 'February', 10.98, 2.17, -2.98, 0.00)
Then I ran dynamic SQL + UNPIVOT + PIVOT (about pivoting you can read on MSDN and here):
DECLARE #sql nvarchar(max),
#columns nvarchar(max)
--here we get column names for pivoting, so we dont need to write them by hand
SELECT #columns = STUFF((
SELECT ',budget'+[MONTH] + ',actual'+[MONTH]+ ',variance'+[MONTH]+',percent'+[MONTH]
FROM #tblmonth
GROUP BY [MONTH]
FOR XML PATH('')),1,1,'')
-- Main query, at first we UNPIVOT so we make column to rows,
-- then we pivot them back in a way we need.
SELECT #sql = '
SELECT *
FROM (
SELECT [year],
groups,
element,
[rows] + [month] as [rowname],
[values]
FROM (
SELECT id2,
[year],
groups,
element,
[month],
CAST(budget as varchar(50)) as budget,
CAST(actual as varchar(50)) as actual,
CAST(variance as varchar(50)) as variance,
CAST([percent] as varchar(50)) as [percent]
FROM #tblmonth) as p
UNPIVOT (
[values] FOR [rows]
IN (budget, actual, variance, [percent])
) as unpvt
) as p
PIVOT(
MAX([values]) FOR [rowname] IN ('+#columns+'
)
) as pvt'
EXEC sp_executesql #sql
Output:
year groups element budgetFebruary actualFebruary varianceFebruary percentFebruary budgetJanuary actualJanuary varianceJanuary percentJanuary
2016 CCARE Basic 152.28 3.00 4.98 0.00 52.28 43.00 43.98 0.00
2016 CCARE Bonuses 12.77 1.10 -4.12 0.00 1.77 17.10 -46.12 .00
2016 CCARE Comminication 191.01 2.34 -1.98 0.00 19.01 27.34 -81.98 0.00
2016 CCARE HDMF 10.98 2.17 -2.98 0.00 0.98 22.17 -22.98 0.00
2016 CCARE Overtime 23.34 3.20 0.98 0.00 2.34 20.20 7.98 0.00
Try this:
select t1.id2, t1.budget,t1.actual,t1.variance,t1[percent]
,t2.budget,t2.actual,t2.variance,t2.[percent]
from tblmonth t1 inner join
(select * from tblmonth where groups='CCARE' and month='February' ) t2
on t1.id2 = t2.id2 and t1.year = t2.year
where t1.groups='CCARE' and t1.month='January'
It should be something like this :
select
q.id2, q.year, q.groups, q.element,
jan.month as jMonth, jan.budget as jBudget, jan.actual as jActual, jan.variance as jVariance, jan.[percent] as jPercent, jan.date_update as jDateUpdate,
feb.month as fMonth, feb.budget as fBudget, feb.actual as fActual, feb.variance as fVariance, feb.[percent] as fPercent, feb.date_update as fDateUpdate
from (select groups, year, id2, element from tblmonth
where groups='CCARE' and year=2016 group by groups, year, id2, element) q
left join (select * from tblmonth
where groups='CCARE' and year=2016 and month='January') jan
on (q.groups = jan.groups and q.year = jan.year and
q.id2 = jan.id2 and q.element = jan.element)
left join (select * from tblmonth
where groups='CCARE' and year=2016 and month='February') feb
on (q.groups = feb.groups and q.year = feb.year and
q.id2 = feb.id2 and q.element = feb.element);
Simply joining subqueries on the common fields.
The first subquery q isn't really crucial.
Because you could use one of the months instead to join other months to it.
But the SQL looks better this way if one would add extra months.
Although it would make the SQL smaller. For example:
select
jan.id2, jan.year, jan.groups, jan.element,
jan.month as jMonth, jan.budget as jBudget, jan.actual as jActual, jan.variance as jVariance, jan.[percent] as jPercent, jan.date_update as jDateUpdate,
feb.month as fMonth, feb.budget as fBudget, feb.actual as fActual, feb.variance as fVariance, feb.[percent] as fPercent, feb.date_update as fDateUpdate
from tblmonth jan
left join tblmonth feb on (jan.groups = feb.groups and jan.year = feb.year and jan.id2 = feb.id2
and jan.element = feb.element and feb.month = 'February')
where jan.groups='CCARE' and jan.year=2016 and jan.month='January';