4

I just discovered that if you have a SELECT INTO that raises a TOO_MANY_ROWS exception, the variable still gets assigned the value from the first record the query retrieved. Is that expected behavior?

Here's my example:

for co in my_cursor loop
        l_sco_db_id := null;

    begin
      select db_id
        into l_sco_db_id
        from objects_tab
       where object_name  = co.object_name;

    exception
      when no_data_found then
        dbms_output.put_line('No objects_tab record found for Object ' || co.object_name);
      when too_many_rows then
        dbms_output.put_line('Multiple objects_tab records found for Object ' || co.object_name);
        l_sco_db_id := null;
    end;
end loop;

This is inside a loop, so I set the variable null at the beginning to ensure it's blank, but I had to explicitly do it again in the WHEN TOO_MANY_ROWS exception, which I didn't expect. None of my coworkers (at least, those in immediate earshot) expected the variable to have a value, either.

AndyDan
  • 749
  • 2
  • 13
  • 29
  • Somehow, it doesn't surprise me. Oracle is happy when the first row is processed. The error is raised when the second row is encountered. – Gordon Linoff Oct 19 '16 at 18:29
  • 2
    It does surprise me. One would expect (I think) that the query would be run, the rows would be counted, and only if there is exactly one row the value would be assigned. From what you describe the first row is read, a value is assigned to your variable, and then the runtime complains when it finds more rows. That does seem odd. –  Oct 19 '16 at 19:05
  • Yes, it did surprise me too @mathguy – pahariayogi Oct 19 '16 at 19:10
  • Sorry, I was confused (probably by Gordon's comment, too); the value printed when you find too many rows is not from the first row in the current query (with too many rows), but from the PRIOR query with a single row, right? I take back "It does surprise me" because I misunderstood what you were saying. I offered a longer explanation in an Answer now. –  Oct 19 '16 at 19:28
  • No, it's definitely from the current query. Notice that I set the variable to null before the select (well, before the begin, even). Also, the previous query would have returned a different db_id (it's the primary key for the table). – AndyDan Oct 19 '16 at 19:31
  • I added the cursor FOR loop to the code above to you can see what's going on. There's more stuff happening between the END and the END LOOP, but it's irrelevant for this question. – AndyDan Oct 19 '16 at 19:37
  • It seems to be excepted behavior even though Oracle documentation states otherwise(it says that the variable should be left undefined after exception is raised.) and it's understandable `Select into` statement what is it? It's an implicit cursor, when you open a cursor you have no idea how many rows it'll return you have to fetch from it-rows are not counted beforehand. So yes, the engine will fetch and assign rows while it can. THe same behavior you can observer when populating `varray` variable with bulk collect. Elements will be assigned up to the limit of `varray` then exception is raised. – Nick Krasnov Oct 19 '16 at 19:38
  • Interesting, after some testing using different data types, well different types of data types constrained and unconstrained it seems that a variable of constrained data types(`number(38)` for example ) behaves as expected - keep their initial value (null or some default value) while variable of unconstrained data type gets assigned values of first selected row when exception is raised. – Nick Krasnov Oct 19 '16 at 19:47
  • It doesn't surprise me, because the select into technically failed (it raised an exception), and you should not expect the variable to be populated or not populated. Do you really need to catch the exception just to output a warning statement, or should there be more input or logic that would limit to 1 row, or perhaps should you load multiple rows (bulk collect) into an array and process multiple values? – tbone Oct 19 '16 at 19:52
  • @NicholasKrasnov - Justin Cave pointed to the documentation which says in this case the value assigned to the variable is "undefined" - which means the implementation is free to do with it whatever it wants. Interesting to see what choices the implementers made, that is true. –  Oct 19 '16 at 19:52
  • 1
    @mathguy Yes, as I pointed it earlier the value is undefined. But I do not think that undefined means "implementation is free to do with it whatever it wants". I think it means that in this situation(in case of exception) the value isn't defined by the statement - previous value of the variable is unchanged by the statement - a variable of a constrained data type behaves as documentation states(it keeps its value, whether it's NULL or some default value), but a variable of an unconstrained data type gets assigned a value - value defined by the statement – Nick Krasnov Oct 19 '16 at 20:25

2 Answers2

7

It's expected behavior in that when you understand what is going on under the covers it makes some sense. But it's definitely a behavior that seems rather odd when you see it the first time. Technically, the behavior is documented to be undefined so it shouldn't be relied upon and could change in the future.

Under the covers, a select into is just syntactic sugar for

  • Open cursor
  • Fetch a row from the cursor into the target variable
  • Throw a no_data_found exception if no row was fetched
  • Attempt to fetch a second row from the cursor, throw a too_many_rows exception if that second fetch succeeded.

Given that, it sort of makes sense that the target variable would get written by the first fetch. The Oracle documentation for the select into statement, however, states that

PL/SQL raises the predefined exception TOO_MANY_ROWS and the values of the variables in the INTO clause are undefined.

So Oracle is free to either leave the value unchanged or to let the variable have the value of the first row fetched or the second row or, realistically, anything else. And you shouldn't write code that depends on any particular behavior.

As an example, if you look at this blog post from Jeff Kemp, the variable takes on the value from the first row that was fetched. But if you make a small tweak to Jeff's code so that you fetch into a local variable

CREATE or replace PROCEDURE proc2 (v OUT NUMBER) IS
  l_v integer;
BEGIN
   SELECT 1 INTO l_v FROM all_objects;
EXCEPTION
   WHEN TOO_MANY_ROWS THEN
      dbms_output.put_line
         ('TOO MANY ROWS: v='
          || l_v);
      v := l_v;
END;
/

then the behavior changes and the value doesn't appear to be overwritten.

DECLARE
   v NUMBER;
BEGIN
   proc2(v);
   dbms_output.put_line('AFTER: v=' || v);
END;
/
Justin Cave
  • 227,342
  • 24
  • 367
  • 384
1

Good observation. However, We can avoid exception handling (both no_data_found and two_many_rows) completely by following these tips:

  • Select aggregate functions. By definition, the aggregate functions (AVERAGE, COUNT, MIN, MAX, and SUM) return only one row as a result. Even if its WHERE clause has no matching rows, a COUNT of those rows will return one row — the answer "0." Selecting the MIN or MAX of a table is frequently done in this way to determine processing limits.

  • Limit the query using ROWNUM.

pahariayogi
  • 1,073
  • 1
  • 7
  • 18
  • True, but exception handling can be invaluable for providing feedback about data issues. – DCookie Oct 19 '16 at 19:18
  • 2
    I find this recommendation to be really bad. The code is specifically expected to work ONLY on tables with a single row. If more than one row is found, it SHOULD throw an exception, handled or otherwise; it should NOT take a max (or a random row) and **pretend** that nothing funny happened. I am sure a lot of wrong data in the world is due exactly to this kind of "exception avoidance". –  Oct 19 '16 at 19:21
  • 1
    Needless to mention the role of 'exception' handling here. Everything is there for a reason and useful/not useful in a given scenario. I disagree with comments like ‘recommendation to be really bad’. It's certainly best in a scenario when you know that two_may_rows may occur. – pahariayogi Oct 19 '16 at 19:37
  • I definitely need exception handling in this case. – AndyDan Oct 19 '16 at 19:38
  • "best in a scenario when you know that too_many_rows may occur"?? No, what is best in that scenario is to do whatever the requirement says. If the requirement says "in that case throw an exception", only second-rate programmers would instead choose the first row with `rownum=1` just to pretend an exception did not occur when in fact it has. –  Oct 19 '16 at 19:56