Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Iterative query without using CTE (or cursors!)

I have some data in a table that looks similar to this:

Item    Date         Price
A       8/29/2012    $3
B       8/29/2012    $23
C       8/29/2012    $10
A       8/30/2012    $4
B       8/30/2012    $25
C       8/30/2012    $11
A       8/31/2012    $3
B       8/31/2012    $22
C       8/31/2012    $8
A       9/01/2012    $3
B       9/01/2012    $26
C       9/01/2012    $9
A       9/02/2012    $3
B       9/02/2012    $24
C       9/02/2012    $9

I need to write a query that identifies that price for A hasn't changed since 8/30/2012, price for item C hasn't changed since 9/01/2012, and to return the number of days elapsed for both (we're looking for items with non-moving prices). I can't use CTE, or cursors, or separately created temp tables (select into, etc) because of the limitations of the web report tool this sql needs to run in. I can only use a 'basic' single pass select query (subqueries will work though). Does anyone have any cunning ideas on how to achieve this??

My first attempt was to group by Item and Price, where price is the same as the latest price, having count > 2, identifying the min date and doing a datediff between min date and getdate. However this just identifies the first instance of that price, it doesn't take into account any subsequent rows that might have a different price. Hope that makes sense.

like image 747
Luke Avatar asked Sep 24 '12 18:09

Luke


2 Answers

I've given both types of ages and account for items that exist only once in the table (they don't have an old date). Please let us know how close this is.

UPDATE: I had to correct the date calculation in "PriceAgeToNow", and I've also tried to filter out records that are have only had a new price for 1 day. Here is the SQL Fiddle.

-- Get the age of the current price
select *
    , datediff(d, c.OldDate, getdate()) as PriceAgeToNow
    , datediff(d, c.OldDate, c.NewDate) as PriceAgeToNewestDate
from (
    select *
        -- Get max date of former price
        , isnull(
            (select max(Date) from PricingTable where Item = b.Item and Date < b.NewDate and Price != b.NewPrice), b.NewDate
        ) as OldDate
    from (
        -- Get current price
        select *
            , (select Price from PricingTable where Item = a.Item and Date = a.NewDate) as NewPrice
        from (
            -- Get current date
            select Item
                , max(Date) as NewDate
            from PricingTable
            group by Item
        ) a
    ) b
) c
-- Attempt to filter out price changes that have only lasted 1 day
where datediff(d, c.OldDate, c.NewDate) > 1
like image 53
Tim Lehner Avatar answered Oct 18 '22 20:10

Tim Lehner


I have a preference for using windows functions where they are most appropriate. And, in this case, there is an interesting trick for finding sequences of things that are the same. Enumerate them (with row_number) over all the data, partitioned by item and ordered by date. And then enumerate them, over all the data, partitioned by item and price and ordered by date.

For A, you would get

A 8/29 $3 1 1
A 8/30 $4 2 1
A 8/31 $3 3 2
A 9/1  $3 4 3
A 9/3  $3 5 4

The difference between the last two columns is constant, for any sequence of prices that remain constant in the data. We can then use this to find when that sequence begins. The following query takes this approach:

select item, price, (id_seqnum - ipd_seqnum) as groupid, min(date) as mindate
from (select p.*,
             row_number() over (partition by item order by date) id_seqnum,
             row_number() over (partition by item, price order by date) as ipd_seqnum,
             max(date) over (partition by item) as maxdate
      from prices p
     ) t
group by item, price, (id_seqnum - ipd_seqnum)
having max(date) = max(maxdate)

It also find the maximum date for each item, and then chooses the grouping that has the maximum date.

like image 45
Gordon Linoff Avatar answered Oct 18 '22 20:10

Gordon Linoff