SQL: selecting rows where column value changed last time - sql

I need to get the latest date where number is changed I have this SQL statement
Select
a.group, a.date a.number
From
xx.dbo.list a
Where
a.group in ('10, '10NC', '210')
And a.date >= '2018-06-01'
And a.number > 0
And a. number <> (Select Top 1 b.number
From xxx.dbo.list b
Where b.group = a.group
And b.date >= '2018-06-01'
And b.number > 0
And b.date < a.date
Order by b.date desc)
order by a.date desc
I have a table that looks like this
Group date Number
--------------------------
10 2018-02-06 4
10 2018-04-06 4
10 2018-06-12 4
10NC 2018-02-06 68
10NC 2018-04-06 35
10NC 2018-06-11 35
10NC 2018-06-12 68
10NC 2018-06-13 35
210 2018-06-02 94
210 2018-06-04 100
210 2018-06-06 100
210 2018-06-07 93
I get this output now, but I only want to get the rows with X
Group date Number
------------------------------
10NC 2018-06-12 68
10NC 2018-06-13 35 X
210 2018-06-04 100
210 2018-06-07 93 X
Can anyone help?

You would use lag():
select a.*
from (select a.group, a.date, a.number, lag(a.number) over (partition by group order by date) as prev_number
From xx.dbo.list a
where a.group in ('10', '10NC', '210') And
a.date >= '2018-06-01' And
a.number > 0
) a
where prev_number <> number;

Is this what is Expected?
DECLARE #List TABLE ([Group] VARCHAR(100), [Date] DATE, Number INT)
INSERT INTO #List
SELECT '10','2018-02-06',4
UNION ALL
SELECT '10','2018-04-06',4
UNION ALL
SELECT '10','2018-06-12',4
UNION ALL
SELECT '10NC','2018-02-06',68
UNION ALL
SELECT '10NC','2018-04-06',35
UNION ALL
SELECT '10NC','2018-06-11',35
UNION ALL
SELECT '10NC','2018-06-12',68
UNION ALL
SELECT '10NC','2018-06-13',35
UNION ALL
SELECT '210','2018-06-02',94
UNION ALL
SELECT '210','2018-06-04',100
UNION ALL
SELECT '210','2018-06-06',100
UNION ALL
SELECT '210','2018-06-07',93
;WITH CTE AS
(
SELECT
*
,RN = ROW_NUMBER() OVER (Partition by [Group] ORDER BY [DATE] DESC)
FROM #List
WHERE
[Date] >= '2018-06-01'
AND [Group] in ('10', '10NC', '210')
And Number > 0
)
SELECT * FROM CTE WHERE RN = 1
Note: I am posting it directly in answer as i don't have enough reputation to ask questions in comments.

Related

How to calculate new column with sum of moving time window within a group in snwoflake SQL?

I have a table like this:
date
ID
count
2021-01-01
A
24
2021-01-02
A
10
2021-01-03
A
5
2021-01-04
A
1
2021-01-01
B
5
2021-01-02
B
10
2021-01-03
B
1
2021-01-04
B
10
2021-01-01
C
5
2021-01-03
C
10
2021-01-04
C
1
2021-01-05
C
10
and I want to calculate a new column that sums the count value for the two days before the date within each ID. There might be missing dates (days) in between, which is why a simple lag function propably will not work (See example ID C). So I want to sum the values in between a certain date range within each ID.
So the resulting table should look like
date
ID
count
sum_two_days_before
2021-01-01
A
24
Null
2021-01-02
A
10
Null
2021-01-03
A
5
34
2021-01-04
A
1
15
2021-01-01
B
5
Null
2021-01-02
B
10
Null
2021-01-03
B
1
15
2021-01-04
B
10
11
2021-01-01
C
5
Null
2021-01-03
C
10
5
2021-01-04
C
1
10
2021-01-05
C
10
11
Would be glad about help!
A correlated sub-query might work.
But can't verify.
SELECT *
, ( SELECT SUM(t2.count)
FROM your_table t2
WHERE t2.ID = t.ID
AND t2.date IN (DATEADD(day,-1,t.date), DATEADD(day,-2,t.date))
HAVING COUNT(t2.date) = 2
) AS sum_two_days_before
FROM your_table t
ORDER BY t.ID, t.date;
And if that doesn't work.
Maybe adding 2 LAG will work.
But it just looks for previous days, not today-1 & today-2
SELECT *
, LAG(t.count, 1) OVER (PARTITION BY t.ID ORDER BY t.date) +
LAG(t.count, 2) OVER (PARTITION BY t.ID ORDER BY t.date) AS sum_two_dates_before
FROM your_table t
ORDER BY t.ID, t.date;
But if snowlake isn't having the HAVING, then maybe this will work.
SELECT *
, ( SELECT
SUM(CASE WHEN t.date = DATEADD(day,-2,t.date) THEN t2.count END)
+ SUM(CASE WHEN t.date = DATEADD(day,-1,t.date) THEN t2.count END)
FROM your_table t2
WHERE t2.ID = t.ID
AND t2.date IN (DATEADD(day,-1,t.date), DATEADD(day,-2,t.date))
) AS sum_two_days_before
FROM your_table t
ORDER BY t.ID, t.date;
So correct "as described" results can be gotten from a verbose version:
WITH data AS (
SELECT * FROM values
('2021-01-01','A', 24, null),
('2021-01-02','A', 10, null),
('2021-01-03','A', 5, 34),
('2021-01-04','A', 1, 15),
('2021-01-01','B', 5, null),
('2021-01-02','B', 10, null),
('2021-01-03','B', 1, 15),
('2021-01-04','B', 10, 11),
('2021-01-01','C', 5, null),
('2021-01-03','C', 10, 5),
('2021-01-04','C', 1, 10),
('2021-01-05','C', 10, 11)
v(date, id, count, expected )
)
SELECT
date,
id,
count,
expected,
sum_2_days
FROM (
SELECT
date,
id,
count,
expected,
LAG(date,2)over(partition by id order by date) as d2,
LAG(date,1)over(partition by id order by date) as d1,
lag(count,2)over(partition by id order by date) as c2,
lag(count,1)over(partition by id order by date) as c1,
dateadd(day,-2,date)::date AS dm2,
d2 is not null OR d1 = dm2 as X1,
iff(d2 >= dm2, c2, 0) as x1_v2,
iff(d1 >= dm2, c1, 0) as x1_v1,
x1_v2 + x1_v1 as x1_r,
iff(X1, x1_r, null) as sum_2_days
FROM data
)
order by 2,1;
giving:
DATE
ID
COUNT
EXPECTED
SUM_2_DAYS
2021-01-01
A
24
2021-01-02
A
10
2021-01-03
A
5
34
34
2021-01-04
A
1
15
15
2021-01-01
B
5
2021-01-02
B
10
2021-01-03
B
1
15
15
2021-01-04
B
10
11
11
2021-01-01
C
5
2021-01-03
C
10
5
5
2021-01-04
C
1
10
10
2021-01-05
C
10
11
11
or the compressed version:
SELECT
date,
id,
count,
expected,
iff( LAG(date,2)over(partition by id order by date) is not null
OR LAG(date,1)over(partition by id order by date) = dateadd(day,-2,date)
, iff( LAG(date,2)over(partition by id order by date) >= dateadd(day,-2,date)
,lag(count,2)over(partition by id order by date)
,0)
+ iff( LAG(date,1)over(partition by id order by date) >= dateadd(day,-2,date)
,lag(count,1)over(partition by id order by date)
,0)
,null) as sum_2_days
FROM data
order by 2,1;
AND if you really want to do it with a double join you can via some pre-conditioning:
WITH date_a AS (
SELECT date, id, count, expected,
dateadd(day,-2,date) as dm2,
first_value(date)over(partition by id order by date) as fd
FROM data
)
select
a.date,
a.id,
a.count,
a.expected,
sum(iff(a.fd <= a.dm2, b.count, null)) as sum_2_days
FROM date_a a
LEFT JOIN data b
ON a.id = b.id AND a.date > b.date AND b.date >= a.dm2
GROUP BY 1,2,3,4
order by 2,1;

SQL - Find if column dates include at least partially a date range

I need to create a report and I am struggling with the SQL script.
The table I want to query is a company_status_history table which has entries like the following (the ones that I can't figure out)
Table company_status_history
Columns:
| id | company_id | status_id | effective_date |
Data:
| 1 | 10 | 1 | 2016-12-30 00:00:00.000 |
| 2 | 10 | 5 | 2017-02-04 00:00:00.000 |
| 3 | 11 | 5 | 2017-06-05 00:00:00.000 |
| 4 | 11 | 1 | 2018-04-30 00:00:00.000 |
I want to answer to the question "Get all companies that have been at least for some point in status 1 inside the time period 01/01/2017 - 31/12/2017"
Above are the cases that I don't know how to handle since I need to add some logic of type :
"If this row is status 1 and it's date is before the date range check the next row if it has a date inside the date range."
"If this row is status 1 and it's date is after the date range check the row before if it has a date inside the date range."
I think this can be handled as a gaps and islands problem. Consider the following input data: (same as sample data of OP plus two additional rows)
id company_id status_id effective_date
-------------------------------------------
1 10 1 2016-12-15
2 10 1 2016-12-30
3 10 5 2017-02-04
4 10 4 2017-02-08
5 11 5 2017-06-05
6 11 1 2018-04-30
You can use the following query:
SELECT t.id, t.company_id, t.status_id, t.effective_date, x.cnt
FROM company_status_history AS t
OUTER APPLY
(
SELECT COUNT(*) AS cnt
FROM company_status_history AS c
WHERE c.status_id = 1
AND c.company_id = t.company_id
AND c.effective_date < t.effective_date
) AS x
ORDER BY company_id, effective_date
to get:
id company_id status_id effective_date grp
-----------------------------------------------
1 10 1 2016-12-15 0
2 10 1 2016-12-30 1
3 10 5 2017-02-04 2
4 10 4 2017-02-08 2
5 11 5 2017-06-05 0
6 11 1 2018-04-30 0
Now you can identify status = 1 islands using:
;WITH CTE AS
(
SELECT t.id, t.company_id, t.status_id, t.effective_date, x.cnt
FROM company_status_history AS t
OUTER APPLY
(
SELECT COUNT(*) AS cnt
FROM company_status_history AS c
WHERE c.status_id = 1
AND c.company_id = t.company_id
AND c.effective_date < t.effective_date
) AS x
)
SELECT id, company_id, status_id, effective_date,
ROW_NUMBER() OVER (PARTITION BY company_id ORDER BY effective_date) -
cnt AS grp
FROM CTE
Output:
id company_id status_id effective_date grp
-----------------------------------------------
1 10 1 2016-12-15 1
2 10 1 2016-12-30 1
3 10 5 2017-02-04 1
4 10 4 2017-02-08 2
5 11 5 2017-06-05 1
6 11 1 2018-04-30 2
Calculated field grp will help us identify those islands:
;WITH CTE AS
(
SELECT t.id, t.company_id, t.status_id, t.effective_date, x.cnt
FROM company_status_history AS t
OUTER APPLY
(
SELECT COUNT(*) AS cnt
FROM company_status_history AS c
WHERE c.status_id = 1
AND c.company_id = t.company_id
AND c.effective_date < t.effective_date
) AS x
), CTE2 AS
(
SELECT id, company_id, status_id, effective_date,
ROW_NUMBER() OVER (PARTITION BY company_id ORDER BY effective_date) -
cnt AS grp
FROM CTE
)
SELECT company_id,
MIN(effective_date) AS start_date,
CASE
WHEN COUNT(*) > 1 THEN DATEADD(DAY, -1, MAX(effective_date))
ELSE MIN(effective_date)
END AS end_date
FROM CTE2
GROUP BY company_id, grp
HAVING COUNT(CASE WHEN status_id = 1 THEN 1 END) > 0
Output:
company_id start_date end_date
-----------------------------------
10 2016-12-15 2017-02-03
11 2018-04-30 2018-04-30
All you want know is those records from above that overlap with the specified interval.
Demo here with somewhat more complicated use case.
Maybe this is what you are looking for? For these kind of questions, you need to join two instance of your table, in this case I am just joining with next record by Id, which probably is not totally correct. To do it better, you can create a new Id using a windowed function like row_number, ordering the table by your requirement criteria
If this row is status 1 and it's date is before the date range check
the next row if it has a date inside the date range
declare #range_st date = '2017-01-01'
declare #range_en date = '2017-12-31'
select
case
when csh1.status_id=1 and csh1.effective_date<#range_st
then
case
when csh2.effective_date between #range_st and #range_en then true
else false
end
else NULL
end
from company_status_history csh1
left join company_status_history csh2
on csh1.id=csh2.id+1
Implementing second criteria:
"If this row is status 1 and it's date is after the date range check
the row before if it has a date inside the date range."
declare #range_st date = '2017-01-01'
declare #range_en date = '2017-12-31'
select
case
when csh1.status_id=1 and csh1.effective_date<#range_st
then
case
when csh2.effective_date between #range_st and #range_en then true
else false
end
when csh1.status_id=1 and csh1.effective_date>#range_en
then
case
when csh3.effective_date between #range_st and #range_en then true
else false
end
else null -- ¿?
end
from company_status_history csh1
left join company_status_history csh2
on csh1.id=csh2.id+1
left join company_status_history csh3
on csh1.id=csh3.id-1
I would suggest the use of a cte and the window functions ROW_NUMBER. With this you can find the desired records. An example:
DECLARE #t TABLE(
id INT
,company_id INT
,status_id INT
,effective_date DATETIME
)
INSERT INTO #t VALUES
(1, 10, 1, '2016-12-30 00:00:00.000')
,(2, 10, 5, '2017-02-04 00:00:00.000')
,(3, 11, 5, '2017-06-05 00:00:00.000')
,(4, 11, 1, '2018-04-30 00:00:00.000')
DECLARE #StartDate DATETIME = '2017-01-01';
DECLARE #EndDate DATETIME = '2017-12-31';
WITH cte AS(
SELECT *
,ROW_NUMBER() OVER (PARTITION BY company_id ORDER BY effective_date) AS rn
FROM #t
),
cteLeadLag AS(
SELECT c.*, ISNULL(c2.effective_date, c.effective_date) LagEffective, ISNULL(c3.effective_date, c.effective_date)LeadEffective
FROM cte c
LEFT JOIN cte c2 ON c2.company_id = c.company_id AND c2.rn = c.rn-1
LEFT JOIN cte c3 ON c3.company_id = c.company_id AND c3.rn = c.rn+1
)
SELECT 'Included' AS RangeStatus, *
FROM cteLeadLag
WHERE status_id = 1
AND effective_date BETWEEN #StartDate AND #EndDate
UNION ALL
SELECT 'Following' AS RangeStatus, *
FROM cteLeadLag
WHERE status_id = 1
AND effective_date > #EndDate
AND LagEffective BETWEEN #StartDate AND #EndDate
UNION ALL
SELECT 'Trailing' AS RangeStatus, *
FROM cteLeadLag
WHERE status_id = 1
AND effective_date < #EndDate
AND LeadEffective BETWEEN #StartDate AND #EndDate
I first select all records with their leading and lagging Dates and then I perform your checks on the inclusion in the desired timespan.
Try with this, self-explanatory. Responds to this part of your question:
I want to answer to the question "Get all companies that have been at
least for some point in status 1 inside the time period 01/01/2017 -
31/12/2017"
Case that you want to find those id's that have been in any moment in status 1 and have records in the period requested:
SELECT *
FROM company_status_history
WHERE id IN
( SELECT Id
FROM company_status_history
WHERE status_id=1 )
AND effective_date BETWEEN '2017-01-01' AND '2017-12-31'
Case that you want to find id's in status 1 and inside the period:
SELECT *
FROM company_status_history
WHERE status_id=1
AND effective_date BETWEEN '2017-01-01' AND '2017-12-31'

MS SQL get aggregate datetime difference by status

I have below table in sql.
======================================================
UnitID Status DateTime Value
======================================================
101 A 01/12/2017 00:02:10 10
101 A 01/12/2017 00:02:40 25
101 A 01/12/2017 00:03:20 18
101 B 01/12/2017 00:03:55 30
101 B 01/12/2017 00:04:05 10
101 B 01/12/2017 00:04:30 20
101 B 01/12/2017 00:04:50 10
101 A 01/12/2017 00:05:00 28
101 A 01/12/2017 00:05:50 18
101 A 01/12/2017 00:06:20 18
102 A 01/12/2017 00:02:10 10
102 A 01/12/2017 00:02:40 25
102 A 01/12/2017 00:03:20 18
102 B 01/12/2017 00:03:55 30
102 B 01/12/2017 00:04:05 10
102 B 01/12/2017 00:04:30 20
102 B 01/12/2017 00:04:50 10
102 A 01/12/2017 00:05:00 28
102 A 01/12/2017 00:05:50 18
102 A 01/12/2017 00:06:20 18
From this table i need below mention output.
===========================================
UnitID StatusA StatusB MaxValue
===========================================
101 02:30 00:55 30
102 02:30 00:55 30
what i need is the total time difference by status. so how could i achieve this in mssql query. so here 02:30 is time duration for status "A" in the table.
Thank you in advanced.
As far as I know you cannot have status in different columns, only by row.
SELECT [UnitID], [Status], MAX([DateTime]) - MIN([DateTime]), MAX([Value])
FROM [theTable]
GROUP BY [UnitID], [Status]
Output would be like
101 A 02:30 30
101 B 00:55 30
102 A 02:30 30
102 B 00:55 30
If you have fixed states of A and B you can go messy and do this:
SELECT UnitID, A, B, MaxValue
FROM
(
SELECT [UnitID], MAX([DateTime]) - MIN([DateTime]) AS A, null AS B, MAX([Value]) AS MaxValue
FROM [theTable]
WHERE Status = 'A'
GROUP BY [UnitID]
UNION ALL
SELECT [UnitID], null, MAX([DateTime]) - MIN([DateTime]), MAX([Value])
FROM [theTable]
WHERE Status = 'B'
GROUP BY [UnitID]
) x
You can do what you need with the following query. I tried to separate each step on different CTE's so you can see step by step how to get to your result. LAG will retrieve the previous row value (spliting by the PARTITION BY columns and ordering by the ORDER BY).
;WITH LaggedValues AS
(
SELECT
M.UnitID,
M.Status,
M.DateTime,
LaggedDateTime = LAG(M.DateTime) OVER (PARTITION BY M.UnitID ORDER BY M.DateTime ASC),
LaggedStatus = LAG(M.Status) OVER (PARTITION BY M.UnitID ORDER BY M.DateTime ASC)
FROM
Measures AS M
),
TimeDifferences AS
(
SELECT
T.*,
SecondDifference = CASE
WHEN T.Status = T.LaggedStatus THEN DATEDIFF(SECOND, T.LaggedDateTime, T.DateTime) END
FROM
LaggedValues AS T
),
TotalsByUnitAndStatus AS
(
SELECT
T.UnitID,
T.Status,
SecondDifference = SUM(T.SecondDifference)
FROM
TimeDifferences AS T
GROUP BY
T.UnitID,
T.Status
),
TotalsByUnit AS -- Conditional aggregation (alternative to PIVOT)
(
SELECT
T.UnitID,
StatusA = MAX(CASE WHEN T.Status = 'A' THEN T.SecondDifference END),
StatusB = MAX(CASE WHEN T.Status = 'B' THEN T.SecondDifference END)
FROM
TotalsByUnitAndStatus AS T
GROUP BY
T.UnitID
)
SELECT
T.UnitID,
StatusA = CONVERT(VARCHAR(10), T.StatusA / 60) + ':' + CONVERT(VARCHAR(10), T.StatusA % 60),
StatusB = CONVERT(VARCHAR(10), T.StatusB / 60) + ':' + CONVERT(VARCHAR(10), T.StatusB % 60)
FROM
TotalsByUnit AS T
You can get the difference for each group:
select unitid, status, min(datetime) as mindt, max(datetime) as maxdt, max(value) as maxvalue
from (select t.*,
row_number() over (partition by unitid order by datetime) as seqnum,
row_number() over (partition by unitid, status order by datetime) as seqnum_s
from t
) t
group by unitid, status, (seqnum - seqnum_s);
This solves the "groups-and-islands" problem. Now you can get the information you want using conditional aggregation:
with t as (
select unitid, status, min(datetime) as mindt, max(datetime) as maxdt, max(value) as maxvalue
from (select t.*,
row_number() over (partition by unitid order by datetime) as seqnum,
row_number() over (partition by unitid, status order by datetime) as seqnum_s
from t
) t
group by unitid, status, (seqnum - seqnum_s)
)
select unitid,
sum(case when status = 'A' then datediff(minute, mindt, maxdt) end) as a_minutes,
sum(case when status = 'b' then datediff(minute, mindt, maxdt) end) as a_minutes,
max(maxvalue)
from t
group by unitid;
I'll leave it up to you to convert the minutes back to times.

TSQL OVER (PARTITION BY ... )

For my next trick, I would like to select only the most recent event for each client. Instead of four events for 000017 I want one.
OK c_id e_date e_ser e_att e_recip Age c_cm e_staff rn
--> 000017 2013-04-02 00:00:00.000 122 1 1 36 90510 90510 15
--> 000017 2013-02-26 00:00:00.000 122 1 1 36 90510 90510 20
--> 000017 2013-02-12 00:00:00.000 122 1 1 36 90510 90510 24
--> 000017 2013-01-29 00:00:00.000 122 1 1 36 90510 90510 27
--> 000188 2012-11-02 00:00:00.000 160 1 1 31 1289 1289 44
--> 001713 2013-10-01 00:00:00.000 142 1 1 26 2539 2539 1
--> 002531 2013-07-12 00:00:00.000 190 1 1 61 1689 1689 21
--> 002531 2013-06-14 00:00:00.000 190 1 1 61 1689 1689 30
--> 002531 2013-06-07 00:00:00.000 190 1 1 61 1689 1689 31
--> 002531 2013-05-28 00:00:00.000 122 1 1 61 1689 1689 33
Here is the query that got me to this stage (perhaps you have some suggestions to improve this as well, the extra nested query creating t2 table is probably excessive.) Thank you all!!!
SELECT TOP(10)*
FROM (
SELECT *
FROM (
SELECT (SELECT CASE WHEN
(e_att IN (1,2)
AND e_date > DATEADD(month, -12, getdate())
AND e_ser NOT IN (100,115)
AND e_recip NOT IN ('2','7')
AND (( (e_recip = '3') AND (DATEDIFF(Year, c_bd, GetDate())>10) ) OR (e_recip <> '3') )
AND c_cm = e_staff)
THEN '-->'
WHEN 1=1 THEN ''
END
) AS 'OK'
,c_id, e_date, e_ser, e_att, e_recip, DATEDIFF(Year, c_bd, GetDate()) AS 'Age', c_cm, e_staff
,row_number() OVER (PARTITION BY c_id ORDER BY e_date DESC) rn
FROM events INNER JOIN client ON e_case_no = c_id
LEFT OUTER JOIN doc ON doc.doc_dbid = client.c_id
WHERE client.c_id IN ( /* confidential query */ )
AND e_date > DATEADD(month, -12, getdate())
AND e_ser BETWEEN 11 AND 1000
GROUP BY c_id, e_date, e_ser, e_att, e_recip, c_bd, c_cm, e_staff
) t1
) t2
WHERE OK = '-->'
ORDER BY c_id, e_date DESC
It looks like the following produces the row number, sorted by date, per client:
,row_number() OVER (PARTITION BY c_id ORDER BY e_date DESC) rn
So adding where rn=1 should yield the most recent event per client:
) t1
WHERE rn = 1
) t2
Here is some improvements to your original query:
SELECT TOP(10) *
FROM (
SELECT '-->' AS 'OK' -- always this see where.
,c_id, e_date, e_ser, e_att, e_recip, DATEDIFF(Year, c_bd, GetDate()) AS 'Age', c_cm, e_staff
,row_number() OVER (PARTITION BY c_id ORDER BY e_date DESC) rn
FROM events INNER JOIN client ON e_case_no = c_id
LEFT OUTER JOIN doc ON doc.doc_dbid = client.c_id
WHERE client.c_id IN ( /* confidential query */ )
-- this part was in case and then filtered for later, if we put it in where now more efficient
(e_att IN (1,2) AND e_date > DATEADD(month, -12, getdate())
AND e_ser NOT IN (100,115)
AND (( (e_recip = '3') AND DATEDIFF(Year, c_bd, GetDate()>10) ) OR e_recip NOT IN ('2', '3', '7') )
AND c_cm = e_staff)
AND e_date > DATEADD(month, -12, getdate())
AND e_ser BETWEEN 11 AND 1000
GROUP BY c_id, e_date, e_ser, e_att, e_recip, c_bd, c_cm, e_staff
) t2
ORDER BY c_id, e_date DESC
Besides removing some un-needed parens, if you move the stuff from the CASE statement to a where you don't need to filter on it in the outer query and this makes it simpler.
Add in the row_number statement from McGarnagle's answer and you should get the results you want.

SQL - Count number of changes in an ordered list

Say I've got a table with two columns (date and price). If I select over a range of dates, then is there a way to count the number of price changes over time?
For instance:
Date | Price
22-Oct-11 | 3.20
23-Oct-11 | 3.40
24-Oct-11 | 3.40
25-Oct-11 | 3.50
26-Oct-11 | 3.40
27-Oct-11 | 3.20
28-Oct-11 | 3.20
In this case, I would like it to return a count of 4 price changes.
Thanks in advance.
You can use the analytic functions LEAD and LAG to access to prior and next row of a result set and then use that to see if there are changes.
SQL> ed
Wrote file afiedt.buf
1 with t as (
2 select date '2011-10-22' dt, 3.2 price from dual union all
3 select date '2011-10-23', 3.4 from dual union all
4 select date '2011-10-24', 3.4 from dual union all
5 select date '2011-10-25', 3.5 from dual union all
6 select date '2011-10-26', 3.4 from dual union all
7 select date '2011-10-27', 3.2 from dual union all
8 select date '2011-10-28', 3.2 from dual
9 )
10 select sum(is_change)
11 from (
12 select dt,
13 price,
14 lag(price) over (order by dt) prior_price,
15 (case when lag(price) over (order by dt) != price
16 then 1
17 else 0
18 end) is_change
19* from t)
SQL> /
SUM(IS_CHANGE)
--------------
4
Try this
select count(*)
from
(select date,price from table where date between X and Y
group by date,price )
Depending on the Oracle version use either analytical functions (see answer from Justin Cave) or this
SELECT
SUM (CASE WHEN PREVPRICE != PRICE THEN 1 ELSE 0 END) CNTCHANGES
FROM
(
SELECT
C.DATE,
C.PRICE,
MAX ( D.PRICE ) PREVPRICE
FROM
(
SELECT
A.Date,
A.Price,
(SELECT MAX (B.DATE) FROM MyTable B WHERE B.DATE < A.DATE) PrevDate
FROM MyTable A
WHERE A.DATE BETWEEN YourStartDate AND YourEndDate
) C
INNER JOIN MyTable D ON D.DATE = C.PREVDATE
GROUP BY C.DATE, C.PRICE
)