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;
Related
I have the following problem: from the table of pays and dues, I need to find the date of the last overdue. Here is the table and data for example:
create table t (
Id int
, [date] date
, Customer varchar(6)
, Deal varchar(6)
, Currency varchar(3)
, [Sum] int
);
insert into t values
(1, '2017-12-12', '1110', '111111', 'USD', 12000)
, (2, '2017-12-25', '1110', '111111', 'USD', 5000)
, (3, '2017-12-13', '1110', '122222', 'USD', 10000)
, (4, '2018-01-13', '1110', '111111', 'USD', -10100)
, (5, '2017-11-20', '2200', '222221', 'USD', 25000)
, (6, '2017-12-20', '2200', '222221', 'USD', 20000)
, (7, '2017-12-31', '2201', '222221', 'USD', -10000)
, (8, '2017-12-29', '1110', '122222', 'USD', -10000)
, (9, '2017-11-28', '2201', '222221', 'USD', -30000);
If the value of "Sum" is positive - it means overdue has begun; if "Sum" is negative - it means someone paid on this Deal.
In the example above on Deal '122222' overdue starts at 2017-12-13 and ends on 2017-12-29, so it shouldn't be in the result.
And for the Deal '222221' the first overdue of 25000 started at 2017-11-20 was completly paid at 2017-11-28, so the last date of current overdue (we are interested in) is 2017-12-31
I've made this selection to sum up all the payments, and stuck here :(
WITH cte AS (
SELECT *,
SUM([Sum]) OVER(PARTITION BY Deal ORDER BY [Date]) AS Debt_balance
FROM t
)
Apparently i need to find (for each Deal) minimum of Dates if there is no 0 or negative Debt_balance and the next date after the last 0 balance otherwise..
Will be gratefull for any tips and ideas on the subject.
Thanks!
UPDATE
My version of solution:
WITH cte AS (
SELECT ROW_NUMBER() OVER (ORDER BY Deal, [Date]) id,
Deal, [Date], [Sum],
SUM([Sum]) OVER(PARTITION BY Deal ORDER BY [Date]) AS Debt_balance
FROM t
)
SELECT a.Deal,
SUM(a.Sum) AS NET_Debt,
isnull(max(b.date), min(a.date)),
datediff(day, isnull(max(b.date), min(a.date)), getdate())
FROM cte as a
LEFT OUTER JOIN cte AS b
ON a.Deal = b.Deal AND a.Debt_balance <= 0 AND b.Id=a.Id+1
GROUP BY a.Deal
HAVING SUM(a.Sum) > 0
I believe you are trying to use running sum and keep track of when it changes to positive, and it can change to positive multiple times and you want the last date at which it became positive. You need LAG() in addition to running sum:
WITH cte1 AS (
-- running balance column
SELECT *
, SUM([Sum]) OVER (PARTITION BY Deal ORDER BY [Date], Id) AS RunningBalance
FROM t
), cte2 AS (
-- overdue begun column - set whenever running balance changes from l.t.e. zero to g.t. zero
SELECT *
, CASE WHEN LAG(RunningBalance, 1, 0) OVER (PARTITION BY Deal ORDER BY [Date], Id) <= 0 AND RunningBalance > 0 THEN 1 END AS OverdueBegun
FROM cte1
)
-- eliminate groups that are paid i.e. sum = 0
SELECT Deal, MAX(CASE WHEN OverdueBegun = 1 THEN [Date] END) AS RecentOverdueDate
FROM cte2
GROUP BY Deal
HAVING SUM([Sum]) <> 0
Demo on db<>fiddle
You can use window functions. These can calculate intermediate values:
Last day when the sum is negative (i.e. last "good" record).
Last sum
Then you can combine these:
select deal, min(date) as last_overdue_start_date
from (select t.*,
first_value(sum) over (partition by deal order by date desc) as last_sum,
max(case when sum < 0 then date end) over (partition by deal order by date) as max_date_neg
from t
) t
where last_sum > 0 and date > max_date_neg
group by deal;
Actually, the value on the last date is not necessary. So this simplifies to:
select deal, min(date) as last_overdue_start_date
from (select t.*,
max(case when sum < 0 then date end) over (partition by deal order by date) as max_date_neg
from t
) t
where date > max_date_neg
group by deal;
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
I have a table with exam scores for different weeks. I wanted to create an extra column with the score difference, like if score decreased by 0-5 then 1, 5-9 then 2, 10+ then 3 and if score increases then 4. Here is the sample data that I have with me in the table.
--DROP TABLE #Scores
CREATE TABLE #Scores (
NAME varchar(10),
Grade varchar(10),
Subject varchar(25),
Exam_Date datetime,
Score int
)
INSERT INTO #Scores
VALUES ('Sam', 'XI', 'Maths', '2016-08-01 15:47:29.533', 38),
('Sam', 'XI', 'Maths', '2016-07-25 15:47:29.533', 50),
('Mike', 'XI', 'Maths', '2016-08-01 15:47:29.533', 50),
('Mike', 'XI', 'Maths', '2016-07-25 15:47:29.533', 45)
SELECT * FROM #Scores
Thanks in adavance
You would use lag() and case:
select s.*,
(case when score - prev_score < 0 then 4
when score - prev_score <= 5 then 1
when score - prev_score <= 9 then 2
else 3
end) as score_diff
from (select s.*,
lag(score) over (partition by name, subject order by exam_date) as prev_score
from #scores s
) s;
Thanks to #Gordon Linoff, I change the code a little bit. The logic is right, just change the math a little.
select s.*,
(case when score - prev_score > 0 then 4
when score - prev_score between -5 and 0 then 1
when score - prev_score between -9 and -5 then 2
else 3
end) as score_diff
from (select s.*,
lag(score) over (partition by name, subject order by exam_date) as prev_score
from #scores s
) s;
Result is captured and shown below:
Consider a further step of normalization. Keep the scores in a separate table. Relate the student to the scores table.
You have to decide how you are going to reference the previous score to compare to the current. If you create an additional field to either store the change from last score then you can have a calculated field that shows the current score, or, store the previous score in a field along side the new score, then have a calculated field show the change between the two.
What I'm trying to do in come up with a single query that can give the percentage of repeats within 30 days of an initial event, but only count any events within 30 days as a single repeat. Here's a sample data set for a single person:
Person Date
══════════════
A 3/1/14
A 3/21/14
A 3/29/14
A 4/14/14
A 4/17/14
In this case, 3/21 would be the repeat event, and 3/29 wouldn't be counted as a second. 4/14 would be the start of the next window, with 4/17 being the second repeat.
To calculate the percentage of repeats here, the numerator would be the distinct count of people who had an initial event in the month and also had a subsequent event within 30 days. The denominator is a distinct count of people with events in that month. In the case of crossing months, the repeat is counted within the month of the initial event.
I know I could come up with something that uses a loop/cursor or temp table, but as the data set grows, it's going to take forever. Does anyone have any thoughts on how to do this as a single query? It's probably going to involve a couple of CTE's. Everything I've come up with so far has failed.
Nice one... try this:
create table #t (Person varchar(10), EventDate date);
insert #t (Person, EventDate)
values
('A', '3/1/14'),
('A', '3/21/14'),
('A', '3/29/14'),
('A', '4/14/14'),
('A', '4/17/14'),
('A', '8/3/14'),
('B', '3/25/14'),
('B', '4/2/14'),
('B', '4/20/14'),
('B', '6/14/14'),
('B', '8/17/14'),
('B', '8/26/14');
;WITH OrderedEvents AS (
SELECT Person, EventDate, ROW_NUMBER() OVER (PARTITION BY Person ORDER BY EventDate) AS Ord
FROM #t
)
, RepeatedEvents AS (
SELECT Person, EventDate, Ord, EventDate AS InitialDate
FROM OrderedEvents
WHERE Ord = 1
UNION ALL
SELECT o.Person, o.EventDate, o.Ord
, CASE WHEN DATEDIFF(DAY, r.InitialDate, o.EventDate) > 30 THEN o.EventDate ELSE r.InitialDate END
FROM OrderedEvents o
JOIN RepeatedEvents r ON o.Person = r.Person AND o.Ord = r.Ord + 1
)
, GroupedEvents AS (
SELECT Person, MONTH(InitialDate) AS Mth, YEAR(InitialDate) AS Yr
, IsRepeat = CASE WHEN COUNT(*) > 1 THEN 1 ELSE 0 END
FROM RepeatedEvents
GROUP BY Person, MONTH(InitialDate), YEAR(InitialDate)
)
SELECT Mth, Yr, CAST(SUM(IsRepeat) AS NUMERIC) / CAST(COUNT(DISTINCT person) AS NUMERIC) AS Pct
FROM GroupedEvents
GROUP BY Mth, Yr;
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;