2

Ok, say I have a query:

SELECT * FROM TABLE_AWESOME WHERE YEAR = :AMAZINGYEAR;

Which works very nicely. But say I want to be able to return either just those results or all results based on a drop down. (e.g., the drop down would have 2008, 2009, ALL YEARS)

I decided to tackle said problem with PL/SQL with the following format:

DECLARE
  the_year VARCHAR(20) := &AMAZINGYEAR;
BEGIN
  IF the_year = 'ALL' THEN
      SELECT * FROM TABLE_AWESOME;
  ELSE
      SELECT * FROM TABLE_AWESOME WHERE YEAR = the_year;
  END IF;
END;

Unfortunately, this fails. I get errors like "an INTO clause is expected in this SELECT statement".

I'm completely new to PL/SQL so I think I'm just expecting too much of it. I have looked over the documentation but haven't found any reason why this wouldn't work the way I have it. The query I'm actually using is much much more complicated than this but I want to keep this simple so I'll get answer quickly.

Thanks in advance :)

clifgriffin
  • 2,008
  • 3
  • 21
  • 41
  • You have to do something with the result of the query. What do you want to do next? – Stephanie Page Nov 16 '10 at 18:46
  • I just want it to return the results. I'm binding to the results with a SqlDataSource in ASP.NET. I don't intend to create this as a procedure, I just want to execute the block and bind to the results. Is that possible? – clifgriffin Nov 16 '10 at 18:53

5 Answers5

4

There is a real danger in the queries offered by Jim and Alex.

Assumption, you have 20 years of data in there, so a query on YEAR = return 5% of the blocks. I say blocks and not rows because I assume the data is being added on that date so the clustering factor is high.

If you want 1 year, you want the optimizer to use an index on year to find those 5% of rows.

If you want all years, you want the optimizer to use a full table scan to get every row.

Are we good so far?

Once you put this into production, the first time Oracle loads the query it peaks at the bind variable and formulates a plan based on that.

SO let's say the first load is 'All'.

Great, the plan is a Full table scan (FTS) and that plan is cached and you get all the rows back in 5 minutes. No big deal.

The next run you say 1999. But the plan is cached and so it uses a FTS to get just 5% of the rows and it takes 5 minutes. "Hmmm... the user says, that was many fewer rows and the same time." But that's fine... it's just a 5 minute report... life is a little slow when it doesn't have to be but no one is yelling.

That night the batch jobs blow that query out of the cache and in the morning the first user asks for 2001. Oracle checks the cache, not there, peeks at the variable, 2001. Ah, the best plan for that is an index scan. and THAT plan is cached. The results come back in 10 seconds and blows the user away. The next person, who is normally first, does the morning "ALL" report and the query never returns.

WHY?

Because it's getting every single row by looking through the index.... horrible nested loops. The 5 minute report is now at 30 and counting.

Your original post has the best answer. Two queries, that way both will ALWAYS get the best plan, bind variable peeking won't kill you.

The problem you're having is just a fundamental Oracle issue. You run a query from a tool and get the results back INTO the tool. If you put a select statement into a pl/sql block you have to do something with it. You have to load it into a cursor, or array, or variable. It's nothing to do with you being wrong and them being right... it's just a lack of pl/sql skills.

Stephanie Page
  • 3,875
  • 1
  • 18
  • 22
  • +1 good advice, but not always a problem. For all we know, the table has a UNIQUE constraint on year. e.g. it might be a summary table. – Jeffrey Kemp Nov 17 '10 at 00:15
  • Agreed, I didn't word my caveat strongly enough *8-) – Alex Poole Nov 17 '10 at 09:09
  • This is why I love this site...I'm always learning stuff. I don't think this will be problematic in my case. The query I'm using is returning all of the classes a student has taken for all terms they've been a student. So you're looking at like 50 rows max. And, about 6 rows average. – clifgriffin Nov 17 '10 at 13:39
  • @Jeffrey... yeh, true, which is really the problem with most questions here. Scale is never truly understood. But even if it is unique, there could be 500,000 years of ice core samples. – Stephanie Page Nov 17 '10 at 15:06
  • @Alex, If you read Kerry Osborne's blog he's adamant that bind variable peeking is a bug, not a feature because of these situations. – Stephanie Page Nov 17 '10 at 15:07
  • Of course I should also mention that 11g fixes some of this by allowing the optimizer to categorize variables as having this kind of impact. Even in 10g the optimizer has an OR plan built in but I've never seen it work. – Stephanie Page Nov 17 '10 at 15:08
  • 1
    @Jeffrey, the last thing I'd add is that, of course you're right, it's not always a problem... but that 1/some OR ALL type SQL construct will always *potentially* suffer from that issue. Using an IF and 2 SQL will *never* suffer from that. In general I preach against things which could go wrong because people get into habits, if it works fine here, it will obviously work fine everywhere, and then they get eliminated from the gene pool. If the habit you develop can't have an issue, you're better off propagating those habits. – Stephanie Page Nov 17 '10 at 15:14
  • @Stephanie: I agree about peeking, I've battled that before and have had to create workarounds as if it's a bug. I guess I was assuming it was low volume and meant to suggest it needed analysis to check it was suitable for this case. I'm not disagreeing with anything you've said *8-) – Alex Poole Nov 17 '10 at 15:17
  • @Alex, ditto I voted both answers up to show it's a matter of analysis not a matter of right and wrong. As stated, I lean toward the always safe and verbose method then the tight but potentially dangerous. – Stephanie Page Nov 17 '10 at 15:29
  • 1
    Wait, 50 rows max in the table? Pretty small school then eh? or are you talking about the results and if you don't have 1 table per student then you also have a WHERE Student_ID = :1 in the query and that really changes our answers a lot. I think you worked an example to make it simple but didn't realize you were leaving out key details. – Stephanie Page Nov 17 '10 at 15:32
  • The reason it changes the answers a lot is because student_id is far more selective than year. That selectivity means you've indexes that for sure, or will be after you read this comment. Then either year is part of the index or not. But in either case you'll do an index scan to get 1 or all years, the only question is whether the year predicate will be a filter or part of the index scan. – Stephanie Page Nov 17 '10 at 15:37
  • Voted up because I didn't realize my solution could be that big of a problem. (This is what happens when you let developers pretend to be DBAs.) – Jim Davis Nov 17 '10 at 20:03
  • @Jim, you my friend, are an awesome developer. I can say that because anyone willing to admit that, knows enough to know what they don't know. That makes you a damn near expert. – Stephanie Page Nov 17 '10 at 21:51
3

You could do it with one query, something like:

SELECT * FROM TABLE_AWESOME WHERE (? = 'ALL' OR YEAR = ?)

and pass it the argument twice.

Jim Davis
  • 5,241
  • 1
  • 26
  • 22
  • Don't do this unless your total number of rows is close to the number of rows for 1 year. If you have 20 years or more in there... don't use this. You'll get performance issues. You still have to explain what you're trying to do. – Stephanie Page Nov 16 '10 at 22:48
3

In PL/SQL you have to SELECT ... INTO something, which you need to be able to return to the client; that could be a ref cursor as tanging demonstrates. This can complicate the client.

You can do this in SQL instead with something like:

SELECT * FROM TABLE_AWESOME WHERE :AMAZING_YEAR = 'ALL' OR YEAR = :AMAZINGYEAR;

... although you may need to take care about indexes; I'd look at the execution plan with both argument types to check it isn't doing something unexpected.

Alex Poole
  • 183,384
  • 11
  • 179
  • 318
1

Not sure about using a SqlDataSource, but you can definately do this via the system.data.oracle or the oracle clients.

You would do this via an anonymous block in asp.net

VAR SYS1 REFCURSOR;
VAR SYS2 REFCURSOR;

DECLARE
  FUNCTION CURSORCHOICE(ITEM IN VARCHAR2) RETURN SYS_REFCURSOR IS
      L_REFCUR SYS_REFCURSOR;
    returnNum VARCHAR2(50);
    BEGIN
        IF upper(item) = 'ALL' THEN
            OPEN L_REFCUR FOR
            SELECT  level  FROM DUAL 
            CONNECT BY LEVEL < 15 ;
        ELSE
            OPEN L_REFCUR FOR
            SELECT   'NONE'  FROM DUAL ;  
        END IF;
        RETURN L_REFCUR;
    END ;
BEGIN
:SYS1 := CURSORCHOICE('ALL');
:SYS2 := CURSORCHOICE('NOT ALL');
end ;
/
PRINT :SYS1 ;
PRINT :SYS2 ;

whereas you would simply create an output param (of type refcursor) -- instead of the var sys# refcursors) and pretty much just amend the above code.

I answered a similar question about getting an anonymous block refcuror here How to return a RefCursor from Oracle function?

Community
  • 1
  • 1
Harrison
  • 8,970
  • 1
  • 32
  • 28
0

This kind of parameter shall be processed from within your code so that your OracleCommand object only executes either queries.

using (var connection = new OracleConnection(connString)) {
    connection.Open();

    string sql = "select * from table_awesome";
    sql = string.Concat(sql, theYear.Equals(@"ALL") ? string.Empty : " where year = :pYear")

    using (var command = connection.CreateCommand()) {
        command.CommancText = sql;
        command.CommandType = CommandType.Text;
        var parameter = command.CreateParameter();
        parameter.Name = @":yearParam";
        parameter.Direction = ParameterDirection.Input;
        parameter.Value = theYear;

        var reader = command.ExecuteQuery();

        if (!reader.HasRows) return;

        while (reader.Read()) {
            // Extract your data from the OracleDataReader instance here.
        }
    }
}
Will Marcouiller
  • 23,773
  • 22
  • 96
  • 162