How to COUNT rows according to specific complicated rules? - sql

I have the following table:
custid custname channelid channel dateViewed
--------------------------------------------------------------
1 A 1 ABSS 2016-01-09
2 B 2 STHHG 2016-01-19
3 C 4 XGGTS 2016-01-09
6 D 4 XGGTS 2016-01-09
2 B 2 STHHG 2016-01-26
2 B 2 STHHG 2016-01-28
1 A 3 SSJ 2016-01-28
1 A 1 ABSS 2016-01-28
2 B 2 STHHG 2016-02-02
2 B 7 UUJKS 2016-02-10
2 B 8 AKKDC 2016-02-10
2 B 9 GGSK 2016-02-10
2 B 9 GGSK 2016-02-11
2 B 7 UUJKS 2016-02-27
And I want the results to be:
custid custname month count
------------------------------
1 A 1 1
2 B 1 1
2 B 2 4
3 C 1 1
6 D 1 1
According to the following rules:
All channel views subscription is billed every 15 days. If the
customer viewed the same channel within the 15 days, he will only be
billed once for that channel. For instance, custid 2, custname B his billing cycle is 19 Jan - 3 Feb (one billing cycle), 4 Feb - 20 Feb (one billing cycle) and so on. Therefore, he is billed only 1 time in Jan since he watch the same channel throughout the billing cycle; and he is billed 4 times in Feb for watching (channelid 7, 8, 9) and channelid 7 watched on 27 Feb (since this falls in another billing cycle, customer B is also charged here). Customer B is not charged on 2 Feb for watching channel 2 since he was already billed in 19 jan - 3 Feb billing cycle.
An invoice is generated every month for each customer, therefore, the
results should show the 'Month' and the 'Count' of the channels
viewed for each customer.
Can this be done in SQL server?

;WITH cte AS (
SELECT custid,
custname,
channelid,
channel,
dateViewed,
CAST(DATEADD(day,15,dateViewed) as date) as dateEnd,
ROW_NUMBER() OVER (PARTITION BY custid, channelid ORDER BY dateViewed) AS rn
FROM (VALUES
(1, 'A', 1, 'ABSS', '2016-01-09'),(2, 'B', 2, 'STHHG', '2016-01-19'),
(3, 'C', 4, 'XGGTS', '2016-01-09'),(6, 'D', 4, 'XGGTS', '2016-01-09'),
(2, 'B', 2, 'STHHG', '2016-01-26'),(2, 'B', 2, 'STHHG', '2016-01-28'),
(1, 'A', 3, 'SSJ', '2016-01-28'),(1, 'A', 1, 'ABSS', '2016-01-28'),
(2, 'B', 2, 'STHHG', '2016-02-02'),(2, 'B', 7, 'UUJKS', '2016-02-10'),
(2, 'B', 8, 'AKKDC', '2016-02-10'),(2, 'B', 9, 'GGSK', '2016-02-10'),
(2, 'B', 9, 'GGSK', '2016-02-11'),(2, 'B', 7, 'UUJKS', '2016-02-27')
) as t(custid, custname, channelid, channel, dateViewed)
), res AS (
SELECT custid, channelid, dateViewed, dateEnd, 1 as Lev
FROM cte
WHERE rn = 1
UNION ALL
SELECT c.custid, c.channelid, c.dateViewed, c.dateEnd, lev + 1
FROM res r
INNER JOIN cte c ON c.dateViewed > r.dateEnd and c.custid = r.custid and c.channelid = r.channelid
), final AS (
SELECT * ,
ROW_NUMBER() OVER (PARTITION BY custid, channelid, lev ORDER BY dateViewed) rn,
DENSE_RANK() OVER (ORDER BY custid, channelid, dateEnd) dr
FROM res
)
SELECT b.custid,
b.custname,
MONTH(f.dateViewed) as [month],
COUNT(distinct dr) as [count]
FROM cte b
LEFT JOIN final f
ON b.channelid = f.channelid and b.custid = f.custid and b.dateViewed between f.dateViewed and f.dateEnd
WHERE f.rn = 1
GROUP BY b.custid,
b.custname,
MONTH(f.dateViewed)
Output:
custid custname month count
----------- -------- ----------- -----------
1 A 1 3
2 B 1 1
2 B 2 4
3 C 1 1
6 D 1 1
(5 row(s) affected)
I don't know why you get 1 in count field for customer A. He got:
ABSS 2016-01-09 +1 to count (+15 days = 2016-01-24)
SSJ 2016-01-28 +1 to count
ABSS 2016-01-28 +1 to count (28-01 > 24.01)
So in January there must be count = 3.

Whenever I am trying to count things with complex criteria, I use a sum and case statement. Something like below:
SELECT custid, custname,
SUM(CASE WHEN somecriteria
THEN 1
ELSE 0
END) As CriteriaCount
FROM whateverTable
GROUP BY custid, custname
You can make that somecriteria variable as complicated a statement as you like, so long as it returns a boolean. If it passes, this row returns a 1. If it fails, the row reutrns a 0, then we sum up the values returned to get the count.

Generally this is how you can get any number (10 in this example) of fixed 15 day intervals starting at the given date (#dd in this example).
DECLARE #dd date = CAST('2016-01-19 17:30' AS DATE);
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1),
E2(N) AS (SELECT 1 FROM E1 a, E1 b),
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10,000 rows max
tally(N) AS (SELECT TOP (10) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4)
SELECT
startd = DATEADD(D,(N-1)*15, #dd),
endd = DATEADD(D, N*15-1, #dd)
FROM tally
Adapt it to the rules defining how start date must be calculated for the user (and probably chanel).

#Sturgus what if I want to define it in the code? Any other
alternatives besides defining it in the table? How to write a query
that can be run every month to generate the monthly invoice. –
saturday 15 mins ago
Well, one way or another, you will have to save each customer's billing start date (minimally). If you want to do this entirely in SQL without 'editing the database', something like the following should work. The drawback to this approach is that you would need to manually edit the "INSERT INTO" statement every month to suit your needs. If you were allowed to edit the already existing customers table or create a new one, then it would reduce this manual effort.
DECLARE #CustomerBillingPeriodsTVP AS Table(
custID int UNIQUE,
BillingCycleID int,
BillingStartDate Date,
BillingEndDate Date
);
INSERT INTO #CustomerBillingPeriodsTVP (custID, BillingCycleID, BillingStartDate, BillingEndDate) VALUES
(1, 1, '2016-01-03', '2016-01-18'), (2, 1, '2016-01-18', '2016-02-03'), (3, 1, '2016-01-15', '2016-01-30'), (6, 1, '2016-01-14', '2016-01-29');
SELECT A.custid, A.custname, B.BillingCycleID AS [month], COUNT(DISTINCT A.channelid) AS [count]
FROM dbo.tblCustomerChannelViews AS A INNER JOIN #CustomerBillingPeriodsTVP AS B ON A.custid = B.CustID
GROUP BY A.custid, A.custname, B.BillingCycleID;
GO
Where are you getting your customers' billing start dates as it is?

I'm not sure how this solution will scale - but with some good index candidates and decent data housekeeping, it'll work..
You're going to need some extra info for starters, and to normalize your data. You will need to know the first charging period start date for each customer. So store that in a customer table.
Here are the tables I used:
create table #channelViews
(
custId int, channelId int, viewDate datetime
)
create table #channel
(
channelId int, channelName varchar(max)
)
create table #customer
(
custId int, custname varchar(max), chargingStartDate datetime
)
I'll populate some data. I won't get the same results as your sample output, because I don't have the appropriate start dates for each customer. Customer 2 will be OK though.
insert into #channel (channelId, channelName)
select 1, 'ABSS'
union select 2, 'STHHG'
union select 4, 'XGGTS'
union select 3, 'SSJ'
union select 7, 'UUJKS'
union select 8, 'AKKDC'
union select 9, 'GGSK'
insert into #customer (custId, custname, chargingStartDate)
select 1, 'A', '4 Jan 2016'
union select 2, 'B', '19 Jan 2016'
union select 3, 'C', '5 Jan 2016'
union select 6, 'D', '5 Jan 2016'
insert into #channelViews (custId, channelId, viewDate)
select 1,1,'2016-01-09'
union select 2,2,'2016-01-19'
union select 3,4,'2016-01-09'
union select 6,4,'2016-01-09'
union select 2,2,'2016-01-26'
union select 2,2,'2016-01-28'
union select 1,3,'2016-01-28'
union select 1,1,'2016-01-28'
union select 2,2,'2016-02-02'
union select 2,7,'2016-02-10'
union select 2,8,'2016-02-10'
union select 2,9,'2016-02-10'
union select 2,9,'2016-02-11'
union select 2,7,'2016-02-27'
And here is the somewhat unweildy query, in a single statement.
The two underlying sub-queries are actually the same data, so there may be more appropriate / efficient ways to generate these.
We need to exclude from billing any channel charged in the same charging period C for the previous Month. This is the essence of the join. I used a right-join so that I could exclude all such matches from the results (using old.custId is null).
select c.custId, c.[custname], [month], count(*) [count] from
(
select new.custId, new.channelId, new.month, new.chargingPeriod
from
(
select distinct cv.custId, cv.channelId, month(viewdate) [month], (convert(int, cv.viewDate) - convert(int, c.chargingStartDate))/15 chargingPeriod
from #channelViews cv join #customer c on cv.custId = c.custId
) old
right join
(
select distinct cv.custId, cv.channelId, month(viewdate) [month], (convert(int, cv.viewDate) - convert(int, c.chargingStartDate))/15 chargingPeriod
from #channelViews cv join #customer c on cv.custId = c.custId
) new
on old.custId = new.custId
and old.channelId = new.channelId
and old.month = new.Month -1
and old.chargingPeriod = new.chargingPeriod
where old.custId is null
group by new.custId, new.month, new.chargingPeriod, new.channelId
) filteredResults
join #customer c on c.custId = filteredResults.custId
group by c.custId, [month], c.custname
order by c.custId, [month], c.custname
And finally my results:
custId custname month count
1 A 1 3
2 B 1 1
2 B 2 4
3 C 1 1
6 D 1 1
This query does the same thing:
select c.custId, c.custname, [month], count(*) from
(
select cv.custId, min(month(viewdate)) [month], cv.channelId
from #channelViews cv join #customer c on cv.custId = c.custId
group by cv.custId, cv.channelId, (convert(int, cv.viewDate) - convert(int, c.chargingStartDate))/15
) x
join #customer c
on c.custId = x.custId
group by c.custId, c.custname, x.[month]
order by custId, [month]

Related

First 7 days sales

I want to check the sum of the amount for an item from its first day of sale next 7 days. Basically, I want to check the sum of sales for the first 7 days.
I am using the below query.
select item, sum(amt)
from table
where first_sale_dt = (first_sale_dt + 6).
When I run this query, I don't get any results.
Your code as it stands will give you no results, because you are looking at each row, and asking is the value first_sale_dt equal to a values it is not +6
You need to use a WINDOW function to look across many rows, OR self JOIN the table and filter the rows that are joined to give the result you want.
so with the CTE of data for testing:
WITH data as (
select * from values
(1, 2, '2022-03-01'::date),
(1, 4, '2022-03-04'::date),
(1, 200,'2022-04-01'::date),
(3, 20, '2022-03-01'::date)
t(item, amt, first_sale_dt)
)
this SQL show the filtered row that we are wanting to SUM, it is using a sub-select (which could be moved into a CTE) to find the "first first sale" to do the date range of.
select a.item, b.amt
from (
select
item,
min(first_sale_dt) as first_first_sale_dt
from data
group by 1
) as a
join data as b
on a.item = b.item and b.first_sale_dt <= (a.first_first_sale_dt + 6)
ITEM
AMT
1
2
1
4
3
20
and therefore with a SUM added:
select a.item, sum(b.amt)
from (
select
item,
min(first_sale_dt) as first_first_sale_dt
from data
group by 1
) as a
join data as b
on a.item = b.item and b.first_sale_dt <= (a.first_first_sale_dt + 6)
group by 1;
you get:
ITEM
SUM(B.AMT)
1
6
3
20
Sliding Window:
This is relying on dense data (1 row for every day), also the sliding WINDOW is doing work that is getting thrown away, which is a string sign this is not the performant solution and I would stick to the first solution.
WITH data as (
select * from values
(1, 2, '2022-03-01'::date),
(1, 2, '2022-03-02'::date),
(1, 2, '2022-03-03'::date),
(1, 2, '2022-03-04'::date),
(1, 2, '2022-03-05'::date),
(1, 2, '2022-03-06'::date),
(1, 2, '2022-03-07'::date),
(1, 2, '2022-03-08'::date)
t(item, amt, first_sale_dt)
)
select item,
first_sale_dt,
sum(amt) over(partition by item order by first_sale_dt rows BETWEEN current row and 6 following ) as s
,count(amt) over(partition by item order by first_sale_dt rows BETWEEN current row and 6 following ) as c
from data
order by 2;
ITEM
FIRST_SALE_DT
S
C
1
2022-03-01
14
7
1
2022-03-02
14
7
1
2022-03-03
12
6
1
2022-03-04
10
5
1
2022-03-05
8
4
1
2022-03-06
6
3
1
2022-03-07
4
2
1
2022-03-08
2
1
thus you need to then filter out some rows.
WITH data as (
select * from values
(1, 2, '2022-03-01'::date),
(1, 2, '2022-03-02'::date),
(1, 2, '2022-03-03'::date),
(1, 2, '2022-03-04'::date),
(1, 2, '2022-03-05'::date),
(1, 2, '2022-03-06'::date),
(1, 2, '2022-03-07'::date),
(1, 2, '2022-03-08'::date)
t(item, amt, first_sale_dt)
)
select item,
sum(amt) over(partition by item order by first_sale_dt rows BETWEEN current row and 6 following ) as s
from data
qualify row_number() over (partition by item order by first_sale_dt) = 1
gives:
ITEM
S
1
14
If you really want to use window function. Here is beginner friendly version
with cte as
(select *, min(sale_date) over (partition by item) as sale_start_date
from data) --thanks Simeon
select item, sum(amt) as amount
from cte
where sale_date <= sale_start_date + 6 --limit to first week
group by item;
On a side note, I suggest using dateadd instead of + on dates

Count the cycle, and count that already has been counted

I have my query:
SELECT UserGroupCode,COUNT(UserGroupCode) AS [CountofCycle]
FROM Users.GroupCycles
GROUP BY UserGroupCode;
Which shows me:
UserGroupCode CountofCycles
1 1
4 1
5 1
6 2 (gone into 2nd cycle)
7 1
8 1
9 1
10 1
11 1
12 1
13 1
14 1
15 1
16 1
17 1
18 1
19 1
When i try to count Total UserGroups where countofcycle=1
SELECT Count(t.CountOfCycle) AS 'totalgroups'
FROM
(SELECT CreateDate, COUNT(userGroupCode) AS [CountofCycle]
FROM Users.GroupCycles
GROUP BY CreateDate,UserGroupCode)t
WHERE CountofCycle=1
I get result = 18 which should be 16, if i delete CreateDate from both SELECT And GROUP BY statement i can get correct number of CountofCycles,
and when i change condition to CountofCycle=2 or >1 it shows me 0
What is the problem with showing UserGroups with cycle > 1 ???!??
Here is my query to filter out onCreateDate, in 2nd table that i UNION with 1st one, i cant't use CreateDate, as it disturbs my query results
SELECT Count(t.CountOfCycle) AS 'total groups'
FROM
(SELECT COUNT(userGroupCode) AS [CountofCycle], CreateDate
FROM users.GroupCycles GROUP BY userGroupCode,CreateDate)t
WHERE t.CountOfCycle=1 AND t.CreateDate Between '03/16/2017' AND '04/25/2017'
UNION ALL
SELECT Count(t.CountOfCycle) AS 'group on date2'
FROM
(SELECT COUNT(userGroupCode) AS [CountofCycle] FROM users.GroupCycles GROUP BY userGroupCode)t
WHERE t.CountOfCycle=2
Firstly to address why you are not getting the results you are expecting, and the simple reason is that you are comparing two different queries and expecting the results to be the same.
Consider this very simple example data
UserGroupCode | CreateDate
----------------+----------------
A | 2017-05-10
B | 2017-05-10
B | 2017-05-11
C | 2017-05-10
You have two records where the UserGroupCode is B, so if you run:
DECLARE #T TABLE (UserGroupCode CHAR(1), CreateDate DATE)
INSERT #T (userGroupCode, CreateDate)
VALUES ('A', '2017-05-10'), ('B', '2017-05-10'), ('B', '2017-05-11');
SELECT UserGroupCode, COUNT(*) AS [Count]
FROM #T
GROUP BY UserGroupCode
HAVING COUNT(*) = 2;
This returns:
UserGroupCode Count
-------------------------
B 2
However, if you were to add CreateDate to the grouping, "B" would be split into two groups, each with a count of 1:
DECLARE #T TABLE (UserGroupCode CHAR(1), CreateDate DATE)
INSERT #T (userGroupCode, CreateDate)
VALUES ('A', '2017-05-10'), ('B', '2017-05-10'), ('B', '2017-05-11');
SELECT UserGroupCode, CreateDate, COUNT(*) AS [Count]
FROM #T
WHERE UserGroupCode = 'B'
GROUP BY UserGroupCode, CreateDate;
This returns:
UserGroupCode CreateDate Count
---------------------------------------
B 2017-05-10 1
B 2017-05-11 1
Now, based you your queries you have posted, it looks like you want to know
The number of groups that only have one record in the date range 16th March 2017 to 25th April 2017.
The number of groups that have two records in total.
For this, consider a slightly larger data set:
UserGroupCode | CreateDate
----------------+----------------
A | 2017-04-10
B | 2017-04-10
B | 2017-05-11
C | 2017-01-01
C | 2017-01-02
D | 2017-04-01
D | 2017-04-02
E | 2017-01-02
So here.
Group A has one record in total, and it falls within the date range
Group B has two records in total, on in the date range, one not
Group C has two records, neither in the date range
Group D has two records, both in the date range.
Group E has one record, not in the date range
So for your first requirement:
The number of groups that only have one record in the date range 16th March 2017 to 25th April 2017.
We would expect 2 groups, A and B, because C and E have no records in the date range, and D has two.
and the second we would expect three groups, B, C and D, since A and E only have one record each.
You can do this with a single query by using a conditional aggregate.
DECLARE #T TABLE (UserGroupCode CHAR(1), CreateDate DATE)
INSERT #T (userGroupCode, CreateDate)
VALUES ('A', '2017-04-10'),
('B', '2017-04-10'), ('B', '2017-05-11'),
('C', '2017-01-01'), ('C', '2017-01-02'),
('D', '2017-04-01'), ('D', '2017-04-02'),
('E', '2017-01-02');
SELECT TotalGroups = COUNT(CASE WHEN RecordsInPeriod = 1 THEN 1 END),
GroupOnDate2 = COUNT(CASE WHEN TotalRecords = 2 THEN 1 END)
FROM ( SELECT UserGroupCode,
TotalRecords = COUNT(*),
RecordsInPeriod = COUNT(CASE WHEN CreateDate >= '20170316'
AND CreateDate <= '20170425' THEN 1 END)
FROM #T
GROUP BY UserGroupCode
) AS t;
Which gives:
TotalGroups GroupOnDate2
------------------------------
2 3
I'd expect to see a HAVING clause rather than a WHERE:
SELECT UserGroupCode, COUNT(UserGroupCode) [CountofCycle]
FROM [Users].[GroupCycles]
GROUP BY UserGroupCode
HAVING COUNT(UserGroupCode) > 1;
You could use HAVING, should work (and be more efficient)
select count(*)
from
(
SELECT CreateDate, COUNT(userGroupCode) AS [CountofCycle]
FROM Users.GroupCycles
GROUP BY CreateDate,UserGroupCode
having count(userGroupCode) > 1 -- here is HAVING clause
) x1

Running Sum that resets to 0 on each new cluster of consecutives

I have tried and failed to adapt several running sum methods (remember I have to use SQL Server 2008, so it's a bit trickier than in 2012).
The goal is to have a running sum of Amount ordered by Date. Any time Category field changes value during that list, the sum should restart.
Table structure:
[Date], [Category], [Amount]
Example:
[Date], [Category], [Amount], [RunSumReset]
-------------------------------------------
1-Jan, catA, 10, 10
2-Jan, catA, 5, 15
3-Jan, catA, 15, 30
15-Jan, catB, 3, 3
1-Feb, catB, 6, 9
11-Feb, catA, 10, 10
12-Feb, catC, 2, 2
1-Apr, catA, 5, 5
Thanks so much for any slick tips or tricks
Using Version 2008 makes things a bit trickier since the window version of SUM with ORDER BY clause is not available.
One way to do it is:
WITH CTE AS (
SELECT [Date], Category, Amount,
ROW_NUMBER() OVER (ORDER BY [Date]) -
ROW_NUMBER() OVER (PARTITION BY Category
ORDER BY [Date]) AS grp
FROM mytable
)
SELECT [Date], Category, Amount, Amount + COALESCE(t.s, 0) AS RunSumReset
FROM CTE AS c1
OUTER APPLY (
SELECT SUM(c2.Amount)
FROM CTE AS c2
WHERE c2.[Date] < c1.[Date] AND
c1.Category = c2.Category AND
c1.grp = c2.grp) AS t(s)
ORDER BY [Date]
The CTE is used to calculate field grp that identifies islands of consecutive records having the same Category. Once Category changes, grp value also changes. Using this CTE we can calculate the running total the way it is normally done in versions prior to SQL Server 2012, i.e. using OUTER APPLY.
Select sum of amounts in current row and up to first row that has different category. In your case you will need to replace NULL with some min date that SQL Server supports, like '17530101':
DECLARE #t TABLE
(
category INT ,
amount INT ,
ordering INT
)
INSERT INTO #t
VALUES ( 1, 1, 1 ),
( 1, 2, 2 ),
( 1, 3, 3 ),
( 2, 4, 4 ),
( 2, 5, 5 ),
( 3, 6, 6 ),
( 1, 7, 7 ),
( 1, 8, 8 ),
( 4, 9, 9 ),
( 1, 10, 10 )
SELECT category ,
amount ,
( SELECT SUM(amount)
FROM #t
WHERE category = t.category
AND ordering <= t.ordering
AND ordering > ( SELECT ISNULL(MAX(ordering), 0)
FROM #t
WHERE category <> t.category
AND ordering < t.ordering
)
) AS sum
FROM #t t
ORDER BY t.ordering
Output:
category amount sum
1 1 1
1 2 3
1 3 6
2 4 4
2 5 9
3 6 6
1 7 7
1 8 15
4 9 9
1 10 10

Calculating de-cumulatived values in TSQL?

Given a table of cars and their odometer reading at various dates (first of each month), how can I write TSQL (ideally, for use as a SQL Server view) to return the "incremental" values?
In other words, I want the reverse operation from Calculate a Running Total in SQL Server.
Example:
On this table:
CarId | Date | Mileage
---------------------------
1 1/1/2000 10000
1 2/1/2000 11000
1 3/1/2000 12000
2 1/1/2000 10000
2 2/1/2000 11001
2 3/1/2000 12001
3 1/1/2000 10000
(missing datapoint for (3, 2/1/2000))
3 3/1/2000 12000
We'd return something like (the details/edge cases are flexible):
CarId | Date | Delta
---------------------------
1 1/1/2000 10000
1 2/1/2000 1000
1 3/1/2000 1000
2 1/1/2000 10000
2 2/1/2000 1001
2 3/1/2000 1000
3 1/1/2000 10000
3 3/1/2000 2000
This should work for SQL 2005 or higher:
WITH cteData As
(
SELECT
CarId,
Date,
Mileage,
ROW_NUMBER() OVER (PARTITION BY CarId ORDER BY Date) As RowNumber
FROM
dbo.Cars
)
SELECT
C.CarId,
C.Date,
CASE
WHEN P.CarId Is Null THEN C.Mileage
ELSE C.Mileage - P.Mileage
END As Delta
FROM
cteData As C
LEFT JOIN cteData As P
ON P.CarId = C.CarId
And P.RowNumber = C.RowNumber - 1
ORDER BY
C.CarId,
C.Date
;
SQL Fiddle
NB: This assumes that "missing datapoint for (3, 2/1/2000)" means that there is no row in the table for car 3, February 2000.
Same approach as the one from #Richard Deeming, but this one regards possible null values as included in original question.
;with cte ( rn, id, date, mileage )
as
(
select
row_number() over ( partition by id order by id, date )
, id
, date
, mileage
from
cars
where
mileage is not null
)
select
"current".id
, "current".date
, delta = isnull( "current".mileage - predecessor.mileage, "current".mileage )
from
cte as "current"
left join cte as predecessor
on "current".id = predecessor.id
and "current".rn - 1 = predecessor.rn
See SQL-Fiddle.
Trying to do this without dependence on any 2012 functions, cursor, while loop, etc.
This works within some limitation -- namely, the null-entry for car#3's entry is a problem for it:
DECLARE #cars table ([id] int, [date] smalldatetime, [mileage] int)
INSERT INTO #cars ([id], [date], [mileage])
SELECT 1, '1/1/2000', 10000 UNION ALL
SELECT 1, '2/1/2000', 11000 UNION ALL
SELECT 1, '3/1/2000', 12000 UNION ALL
SELECT 2, '1/1/2000', 10000 UNION ALL
SELECT 2, '2/1/2000', 11000 UNION ALL
SELECT 2, '3/1/2000', 12000 UNION ALL
SELECT 3, '1/1/2000', 10000 UNION ALL
SELECT 3, '2/1/2000', NULL UNION ALL
SELECT 3, '3/1/2000', 12000
SELECT t1.id, t1.date, t1.mileage, t2.id, t2.date, t2.mileage, t1.mileage - t2.mileage as miles FROM #cars t1
LEFT JOIN #cars t2
ON t1.id = t2.id
AND t1.date = DATEADD(MONTH,1, t2.date)
Window functions are great. But SQL Server does not have the one you need until SQL Server 2012. There, you have the lag function:
select t.*,
(Milage - lag(Milage) over (partition by carId order by date)) as Delta
from t
For earlier versions, you can use a correlated subquery:
[trouble uploading query], alas.
select t.*, (Mileage - prevMileage) as Delta
from (select t.*,
(select top 1 Mileage from t t2
 where t2.carId = t.carId and t2.date < t.date order by desc
 ) as prevDelta
from t
) t

SQL amount consumed query?

How do you do a query to calculate how much of an item has been consumed (used up)?
We can find the qty of each item that we purchased in a purchases table with columns Id, ProductId, Qty (decimal), Date
Id, ProductId, Qty, Date
1, 1, 10, 1/1/11
2, 1, 5, 2/2/11
3, 1, 8, 3/3/11
And how do you then add a count of how many of each row in the purchase table have been consumed - assuming strict FIFO? So in the above example if we know that 14 have been consumed the output would be:
Id, ProductId, Qty, Date, Consumed
1, 1, 10, 1/1/11, 10
2, 1, 5, 2/2/11, 4
3, 1, 8, 3/3/11, 0
Hopefully that explains what I mean by an amount consumed query - we know 14 were consumed and that the first purchase was for 10, so all 10 have been consumed. The next purchase was for 5 so we know that 4 of those have been consumed.
Theres two places I can get the consumed data from - the ConsumedItems table: columns Id, ProductId, QtyUsed, Date), or from the ConsumedSummaryView with columns ProductId, QtyUsed (this is the sum of ConsumedItems.QtyUsed)
Sample table and view
create table purchases (Id int, ProductId int, Qty int, Date datetime)
insert purchases select 1, 1, 10, '1/1/11'
insert purchases select 2, 1, 5, '2/2/11'
insert purchases select 3, 1, 8, '3/3/11'
create view ConsumedSummaryView as select ProductID = 1, QtyUsed = 14
The query
;with p as (
select *, rn=ROW_NUMBER() over (partition by productid order by date, id)
from purchases)
, tmp(Id, ProductId, Qty, Date, rn, ToGo, Consumed) as (
select p.Id, p.ProductId, p.Qty, p.Date, cast(1 as bigint),
CAST(ISNULL(v.qtyused,0) - p.Qty as decimal(20,10)),
cast(case
when v.qtyused >= p.Qty Then p.Qty
when v.qtyused > 0 then v.qtyused
else 0 end as decimal(20,10))
from p
left join ConsumedSummaryView v on p.ProductId = v.productId
where rn=1
union all
select p.Id, p.ProductId, p.Qty, p.Date, cast(p.rn as bigint),
cast(ISNULL(tmp.toGo,0) - p.Qty as decimal(20,10)),
cast(case
when tmp.toGo >= p.Qty Then p.Qty
when tmp.toGo > 0 then tmp.toGo
else 0 end as decimal(20,10))
from tmp
--inner join p on p.rn=tmp.rn+1
inner join p on p.rn=tmp.rn+1 and p.productid = tmp.ProductId
)
select Id, ProductId, Qty, Date, Consumed
from tmp
order by rn
Output
Id ProductId Qty Date Consumed
----------- ----------- ----------- ----------------------- -----------
1 1 10 2011-01-01 00:00:00.000 10
2 1 5 2011-02-02 00:00:00.000 4
3 1 8 2011-03-03 00:00:00.000 0
A little different approach than Richard's, but I'm not sure which will perform better:
SELECT
Purchases.Id,
Purchases.ProductId,
Purchases.Qty,
Purchases.Date,
CASE
WHEN COALESCE (PreviousPurchases.PreviousUsed, 0) + Qty < ConsumedSummaryView.QtyUsed THEN Qty
ELSE
CASE
WHEN ConsumedSummaryView.QtyUsed - COALESCE (PreviousPurchases.PreviousUsed, 0) < 0 THEN 0
ELSE ConsumedSummaryView.QtyUsed - COALESCE (PreviousPurchases.PreviousUsed, 0)
END
END AS Used
FROM
Purchases
INNER JOIN ConsumedSummaryView ON Purchases.ProductId = ConsumedSummaryView.ProductId
LEFT OUTER JOIN (
SELECT
SUM(Purchases_2.Qty) AS PreviousUsed,
Purchases_1.Id
FROM
Purchases AS Purchases_2
INNER JOIN Purchases AS Purchases_1 ON Purchases_2.Id < Purchases_1.Id
AND Purchases_2.ProductId = Purchases_1.ProductId
GROUP BY
Purchases_1.Id
) AS PreviousPurchases ON Purchases.Id = PreviousPurchases.Id