views:

69

answers:

4

I am trying to do the following update in Oracle 10gR2:

update
  (select voyage_port_id, voyage_id, arrival_date, port_seq,
    row_number() over (partition by voyage_id order by arrival_date) as new_seq
   from voyage_port) t
set t.port_seq = t.new_seq

Voyage_port_id is the primary key, voyage_id is a foreign key. I'm trying to assign a sequence number based on the dates within each voyage.

However, the above fails with ORA-01732: data manipulation operation not legal on this view

What is the problem and how can I avoid it ?

A: 

Don't think you can update a derived table, I'd rewrite as:

update voyage_port
set port_seq = t.new_seq
from
voyage_port p
inner join
  (select voyage_port_id, voyage_id, arrival_date, port_seq,
   row_number() over (partition by voyage_id order by arrival_date) as new_seq
   from voyage_port) t
on p.voyage_port_id = t.voyage_port_id
Codewerks
Don't think Oracle supports `update ... from`
Andomar
Could be, but whatever it supports he'll have to put the actual table to update the update clause, not the derived table
Codewerks
Actually, Oracle does support this syntax. See http://www.psoug.org/reference/update.html under "Update the results of a SELECT statement"
ObiWanKenobi
A: 

The first token after the UPDATE should be the name of the table to update, then your columns-to-update. I'm not sure what you are trying to achieve with the select statement where it is, but you can' update the result set from the select legally.
A version of the sql, guessing what you have in mind, might look like...

update voyage_port t
set t.port_seq = (<select statement that generates new value of port_seq>)

NOTE: to use a select statement to set a value like this you must make sure only 1 row will be returned from the select !

EDIT : modified statement above to reflect what I was trying to explain. The question has been answered very nicely by Andomar above

Steve De Caux
Somewhere in there you have to join the result of the subquery to main table, other the db won't know which 'new_seq' to assign to which parent row.
Codewerks
Actually, Oracle does support this syntax. See http://www.psoug.org/reference/update.html under "Update the results of a SELECT statement"
ObiWanKenobi
Codewerks has a point, I've edited my answer to more accurately reflect what I was trying to say. FWIW
Steve De Caux
+2  A: 

You can update some views, but there are restrictions and one is that the view must not contain analytic functions. See SQL Language Reference on UPDATE and search for first occurence of "analytic".

This will work, provided no voyage visits more than one port on the same day (or the dates include a time component that makes them unique):

update voyage_port vp
set vp.port_seq =
( select count(*)
  from voyage_port vp2
  where vp2.voyage_id = vp.voyage_id
  and vp2.arrival_date <= vp.arrival_date
)

I think this handles the case where a voyage visits more than 1 port per day and there is no time component (though the sequence of ports visited on the same day is then arbitrary):

update voyage_port vp
set vp.port_seq =
( select count(*)
  from voyage_port vp2
  where vp2.voyage_id = vp.voyage_id
  and (vp2.arrival_date <= vp.arrival_date)
  or (   vp2.arrival_date = vp.arrival_date 
     and vp2.voyage_port_id <= vp.voyage_port_id
     )
)
Tony Andrews
OK, that is probably the problem then. Any idea as to what I can do to rewrite the update statement ?
ObiWanKenobi
@Tony: "in your original SQL, the sequence of ports visited on the same day is arbitrary" - no, the arrival_date includes both date and time, and the order by in the analytic function makes sure it's not arbitrary
ObiWanKenobi
OK, in that case my simpler code will work for you too. I was assuming the dates had no time component.
Tony Andrews
+4  A: 

Since you can't update subqueries with row_number, you'll have to calculate the row number in the set part of the update. At first I tried this:

update voyage_port a
set a.port_seq = (
  select 
    row_number() over (partition by voyage_id order by arrival_date)
  from voyage_port b
  where b.voyage_port_id = a.voyage_port_id
)

But that doesn't work, because the subquery only selects one row, and then the row_number() is always 1. Using another subquery allows a meaningful result:

update voyage_port a
set a.port_seq = (
  select c.rn
  from (
      select 
        voyage_port_id
      , row_number() over (partition by voyage_id 
            order by arrival_date) as rn
      from voyage_port b
   ) c
  where c.voyage_port_id = a.voyage_port_id
)

It works, but more complex than I'd expect for this task.

Andomar
The second statement does indeed work! And I agree, it looks a bit complex compared to my original attempt/syntax.
ObiWanKenobi