6

I'm using Oracle PL/SQL.

I have a timestamped table T, and I want to set a row's value for column A to be the same as that of the previous row, if they're sorted by columns B and Timestamp, provided that the timestamps are not different by more than 45 seconds.

In pseudocode, it's something like:

UPDATE T t_curr
  SET A =
    (SELECT A
      FROM T t_prev
      INNER JOIN t_curr
        ON (t_prev is the row right before t_curr, when you sort by B and Timestamp)
          AND t_curr.Timestamp - t_prev.Timestamp < 45
    )

I tried this:

UPDATE T t_curr
  SET A =
    (SELECT A
      FROM T t_prev
      INNER JOIN t_curr
        ON RANK (t_curr)
          OVER (B, Timestamp)
          = 1 + RANK (t_prev)
          OVER (B, Timestmap)
          AND t_curr.Timestamp - t_prev.Timestamp < 45
    )

But I got:

Error(38,16): PL/SQL: ORA-00934: group function is not allowed here

pointing at the first instance of RANK.

What did I do wrong, and how do I get this right?

Isaac Moses
  • 1,589
  • 6
  • 26
  • 44
  • 1
    I was about to suggest using `lag` or `lead` but that might not work either... or you could try `update T set a = select Q1.A from( (select A, rownum r1 from T)Q1 left outer join (select A, rownum r2 from T) Q2 on Q1.r1 = Q2.r2-1)` – FrustratedWithFormsDesigner Nov 11 '10 at 15:33
  • @FrustratedWithFormsDesigner - You're right that lag and lead get me the same problem. I got something based on your other suggestion to compile, though, so thanks! If you want to copy it into an answer for me to accept, that'd be fine. – Isaac Moses Nov 11 '10 at 15:46
  • Done! (I posted as comment at first because I've never tried this for an update and wasn't sure it would work) ;) – FrustratedWithFormsDesigner Nov 11 '10 at 15:53

5 Answers5

4

Try using a merge statement. Not sure it quite does what you want but it should work. Unfortunately the insert clause is necessary) but shouldn't ever be called.

merge into t a
using (
  select 
    A, 
    B, 
    timestamp, 
    lag(A) over (order by id, timestamp) as prior_A,
    lag(timestamp) over (order by B, timestamp) as prior_timestamp
  from t) b
on  (a.B = b.B)
when matched then 
  update set a.a = case when b.timestamp-b.prior_timestamp <= 45 
    then b.prior_A else b.A end
when not matched then insert (B) values (null)
Mike Meyers
  • 2,885
  • 1
  • 20
  • 26
  • Thanks! I got something based on this to compile, and it makes sense to me that it ought to work. – Isaac Moses Nov 11 '10 at 21:08
  • 1
    I still think you might need to look at your requirements. What happens when you have several transactions all within 45 seconds of each other? For example, three rows all 40 seconds apart? – Mike Meyers Nov 11 '10 at 22:28
1

Can you try something like this:

update x 
set x = y.A
from T x
join T y
where x.B = (select MAX(B) from T where B < y.B)
and x.Timestamp = (select MAX(Timestamp) from T where Timestamp < y.Timestamp)
and y.Timestamp - x.Timestamp < 45
Fosco
  • 38,138
  • 7
  • 87
  • 101
  • I feel like that would cause performance problems, wouldn't it? I'm dealing with tens of thousands of rows. – Isaac Moses Nov 11 '10 at 15:37
  • 1
    @MOE37x3 I wouldn't assume anything... Depending on who you talk to, tens of thousands of rows really isn't that much. – Fosco Nov 11 '10 at 15:49
1

And another option... doesn't quite do what do want because it ignores the requirement to sort on B but it might give you something to think about.... Without table definitions and things it was a little hard to get a handle on exactly what was required.

Edit: on reading the question again, it looks like your syntax is wrong. Group functions (lead/lag/rank etc) can only appear in the select list or the order by clause. They are evaluated after the joins, where, group by and having clauses. So something like what is shown below should work.

update T a
set A = (select 
  new_A
  from (
  select 
    B, 
    A, 
    timestamp, 
    first_value(A) 
      over (order by timestamp range between 45 preceding and current row) as new_A
  from mike_temp_1
) b where b.id = a.id)
Mike Meyers
  • 2,885
  • 1
  • 20
  • 26
0

What you can do is.

update t
set colToUpdate = nextValue
from  (
select A
      ,B
      ,C
      ,(LEAD(B, 1, null) over (order by A)) as nextValue
  FROM db.schema.table
  ) as t
    where colToUpdate is null

This requires that the column you want to update is null, unless you want to update all of them.

0

You could try (might need some tweaking to get it right, but the idea is two identical ordered subqueries joined by offset rownumbers)

update T set a = (select A1
                 from (
                       select S1.A A1, rownum r1
                       from (select * from T order by B, timestamp) S1
                       left outer join
                       select S2.A A2, rownum r2
                       from (select * from T order by B, timestamp) S2
                       on r1 = r2-1
                      )
                  )
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
FrustratedWithFormsDesigner
  • 26,726
  • 31
  • 139
  • 202
  • It looks like rownum doesn't take order by into account. It just does by the order the rows were accessed. http://www.dbforums.com/oracle/988716-rownum-order.html – Isaac Moses Nov 11 '10 at 16:00
  • @MOE37x3: I know, my first one didn't have any ordering. I addded ordering on B and the timestamp field. Does that work for you? – FrustratedWithFormsDesigner Nov 11 '10 at 16:06
  • According to what I've read, rownum gets applied before order by, so the numbers will be there, and the rows will be in the specified order, but the numbers could be in some other order. – Isaac Moses Nov 11 '10 at 16:09
  • @MOE37x3: Hmm you might be right - I can't test anything because the network connections to the db server are down right now. :( I tried putting the ordering into a separate subquery, that might help. – FrustratedWithFormsDesigner Nov 11 '10 at 16:16