Thursday, August 5, 2010

Dynamic column ordering and paging in PL/SQL

Update 09/30/2010: There was a problem with how I was doing the dynamic ordering.  I have resolved this and updated the post.

Update 10/08/2013: Oracle 12c finally has support for sane paging without jumping through all of these hoops. Until I get a chance to update this post you can read more at http://www.oracle-base.com/articles/12c/row-limiting-clause-for-top-n-queries-12cr1.php

The following demonstrates how a PL/SQL function or procedure can take in parameters, and dynamically sort based on those parameters.
Let's start with an example.  This example accepts two parameters, a VARCHAR2 that will be the column we want to sort on, and a REF CURSOR.  The REF CURSOR is an out variables that the calling program can use to iterate through to get the result set.

TYPE cursortype IS REF CURSOR;
PROCEDURE sort_example
(
  sort_column VARCHAR2,
  result_set OUT cursortype
) AS

BEGIN

  OPEN result_set FOR
    SELECT column_one,
      column_two
      FROM example_table
      ORDER BY DECODE
      (
        UPPER(sort_column),
        'COLUMN_ONE',
        column_one,
        'COLUMN_TWO',
        column_two,
        column_one
      );

EXCEPTION
  WHEN OTHERS THEN
    NULL;//Real error handling should go in this exception block.


END;

The DECODE statement examines the sort_column passed in and matches it to an actual column in the table(s) that we are selecting from.  If the variable passed in is not in our list of columns then it will default, in this example, to column_one.
Using this technique we have to define, up front, all of the columns that we are going to allow the calling procedure to order by.  If we want to allow ordering by any column, we must go through and list them out one-by-one.
It is also a good idea to add some extra validation, beyond the DECODE clause, to make sure that sort_column is an actual column in the table.  In our real packages that do this, we have an out variable named message.  If an invalid column name is passed in we notify the calling program by sticking a message to that effect in our message variable.
To make our procedure even more robust, it is nice to have the ability to decide if the column should be sorted in ascending or descending order.
This gets a little tricker, and to accomplish we need to understand how ROWNUM works in PL/SQL.  When a query is executed, Oracle assigns each returned result a pseudo-column nmaed ROWNUM, with the first returned row having a ROWNUM of 1, the second 2, etc.
Through a subquery, and ROWNUM, we can dynamically sort in either ascending or descending order.

TYPE cursortype IS REF CURSOR;
PROCEDURE sort_example
  (
    sort_column VARCHAR2,
    sort_direction VARCHAR2,
    result_set OUT cursortype
  ) AS


BEGIN

  OPEN result_set FOR
  SELECT *
    FROM
    (
      SELECT column_one,
        column_two
        FROM example_table


        ORDER BY DECODE
        (
          UPPER(sort_column),
          'COLUMN_ONE',
          column_one,
          'COLUMN_TWO',
          column_two,
          column_one
        )
    )
    ORDER BY
      CASE
        WHEN UPPER(sort_direction) = 'DESC' THEN
          ROWNUM * -1
        ELSE
          ROWNUM
      END;

EXCEPTION
  WHEN OTHERS THEN
    NULL;//Real error handling should go in this exception block.

END;

Here we have our original query, initially sorting on the passed in column, that is subqueried and then sorted again, either on the rownum, or the opposite of rownum.  If the calling program passed in 'DESC' for the sort_direction, then we sort descending, if anything else is passed in we sort ascending.

We can take this maddening query a step further, utilizing rownum again, to handle paging.  We use this in our applications to push paging out of our interface code, and onto the database.  It is much quicker for our database to only return ten or so results we want to show on each page, then to return the whole set of data, and have our PHP application parse through the data.
So here is our final procedure, with dynamic sorting and paging.

TYPE cursortype IS REF CURSOR;
PROCEDURE sort_example
  (
    sort_column VARCHAR2,
    sort_direction VARCHAR2,
    start_row NUMBER,
    end_row NUMBER,
    result_set OUT cursortype
  ) AS

BEGIN

  IF start_row < 1 THEN
    start_row = 1
  END IF;
  IF start_row > end_row THEN
    end_row = start_row;
  ELSIF end_row IS NULL THEN
    end_row = start_row + 10;
  END IF;


  OPEN result_set FOR
  SELECT *
    FROM
    (
      SELECT ROWNUM rnum,
        column_one,
        column_two
        FROM
        (
          SELECT *
            FROM
            (
              SELECT column_one,
                column_two
                FROM example_table


                ORDER BY DECODE
                (
                  UPPER(sort_column),
                  'COLUMN_ONE',
                  column_one,
                  'COLUMN_TWO',
                  column_two,
                  column_one
                )
            )
            ORDER BY
              CASE
                WHEN UPPER(sort_direction) = 'DESC' THEN
                  ROWNUM * -1
                ELSE
                  ROWNUM
              END
        )
    )
    WHERE rnum >= start_result
      AND rnum <= end_result
    ORDER BY rnum;

EXCEPTION
  WHEN OTHERS THEN
    NULL;//Real error handling should go in this exception block.

END;

Yes, this one is a beast.  Starting at the child-most query, we have our SELECT statement, ordering the data by the column passed in.  In the parent query we then use ROWNUM, and the sort_direction argument to sort either ascending or descending.  We subquery these results, capturing the ROWNUM into an aliased column named rnum, so that in it's parent query we can refer to it's ROWNUM, and not some other ROWNUM from any other part of the query.  In the parent-most query we use our new rnum column to get only the data from the rows asked for.
Before you do any querying, we check the validity of the start_result and end_result arguments to make sure they actually make sense.  If they don't then we do some manual patching.  You may want to handle this differently, such as throwing an exception.  It is really up to you.  We have a DEFAULT_PAGING environmental variable we store in our database.  When end_result is NULL, then we set it to start_result plus our DEFAULT_PAGING variable.
You may look at this and think it is complicated, and probably performs very poorly.  In our testing, as long as your inner-most query has the proper indexes and constraints, the performance is actually pretty good.  In our environment, it is definitely much faster than having our PHP parse through all the results, sort and order the whole thing, and then cut out only the rows we actually want, especially when we have result sets that are well over 100,000 rows long.
There is one final step we take, so that our interface application knows how many results there are in the whole query, so that it knows how many pages it can expect there to be.  This example won't show it, but we actually check for a cached rowcount first, and we have all sorts of logic to try to guarantee integrity of those results, but I'm not going to get into all that here.







TYPE cursortype IS REF CURSOR;
PROCEDURE sort_example
  (
    sort_column VARCHAR2,
    sort_direction VARCHAR2,
    start_row NUMBER,
    end_row NUMBER,
    result_set OUT cursortype,
    rowcount OUT NUMBER
  ) AS

BEGIN

  IF start_row < 1 THEN
    start_row = 1
  END IF;
  IF start_row > end_row THEN
    end_row = start_row;
  ELSIF end_row IS NULL THEN
    end_row = start_row + 10;
  END IF;

  OPEN result_set FOR
  SELECT *
    FROM
    (
      SELECT ROWNUM rnum,
        column_one,
        column_two
        FROM
        (
          SELECT *
            FROM
            (
              SELECT column_one,
                column_two
                FROM example_table


                ORDER BY DECODE
                (
                  UPPER(sort_column),
                  'COLUMN_ONE',
                  column_one,
                  'COLUMN_TWO',
                  column_two,
                  column_one
                )
            )
            ORDER BY
              CASE
                WHEN UPPER(sort_direction) = 'DESC' THEN
                  ROWNUM * -1
                ELSE
                  ROWNUM
              END
        )
    )
    WHERE rnum >= start_result
      AND rnum <= end_result
    ORDER BY rnum;

  SELECT COUNT(*)
    INTO rowcount
    FROM example_table;

EXCEPTION
  WHEN OTHERS THEN
    NULL;//Real error handling should go in this exception block.

END;

With any luck this will help someone out.  With better luck, someone will drop by and offer some tips on how to do this even better.

Friday, July 9, 2010

iDrive vs. Android Engine

I remember when Apple first announced the release of iDrive, the first car manufactured by Apple. I was sitting in the doctor's office when the news shifted from reports of civil rights abuses in Russia to the iDrive. The car sounded amazing. I didn't get it immediately, but all the Youtube videos showing off the great features had me putting my money down six months after the release.
The thing was slick. It had all the features that futurists have been saying a car would have one day for the last fifty years. When I approached the car it used biometrics to recognize me and automatic unlock and open the driver's door. When my wife approached the passenger door it did the same for her. I decided I'd visit my fellow grease-head friend to show off my latest purchase.
I simply spoke his name and it started driving itself to his house. The ride was real smooth. You could barely feel the turns or bumps when it went over bumpy roads. A couple miles down the road I grabbed the steering wheel, and put it in to manual mode. Once I took control I knew that this was what driving was always meant to be. I was in heaven.
I drove for a few more miles and then put it back in automatic. I wanted to see how it performed parking at my buddies house. To my amazement it pulled right up to his driveway and parallel parked, making sure not to be in the yellow-painted no-parking zone. It was simply amazing.
So I pulled my friend out of his house and showed off my new iDrive. We went for a spin around the neighborhood and he admitted the thing was as sweet as he had heard.
I asked him if he was planning on getting one. He kind of looked at me blankly and then asked me to pop the hood. When I did I was pretty taken back by what I saw. There was a huge metal covering over the engine. I thought I would just pop it off, but when I looked for a way to do it, I couldn't find any nut or bolts, or anything so I could remove it. I peeked on the underside and it was the same story. He pointed at the cover and said, "I can't bring myself to buy something with those ridiculous restrictions." I thought he was overreacting and brushed it off.
I figured that sooner or later I'd find out how to get that thing off and start digging around at the engine. Like my buddy, I'm a grease-head. I got my first car when I was 14, not even old enough to drive, and by the time I had a license in my hand, I had gone through every inch of that machine and had some sort of custom modification. I've done the same to every car I've owned ever since. I wasn't going to let some silly cover get in my way of finding out how this thing works and improving upon it.
When I got home I was on a mission. I was going to get that cover off by nightfall. I pulled the iDrive into my shop and pulled out all of my tools. I spent two hours without getting anywhere.
Frustrated, I pulled out my laptop and did a little Googling. First hit for "iDrive engine case" had instruction on how to get the thing open. The directions where poorly written, and the process was a little convoluted, but it seemed like it should work. There was one pretty big problem. At the top of the instruction the site anounced:
Warning: Cracking your iDrive could potentially void your car's warranty and insurance.
This had to be a joke. I went back to the Apple dealership and asked them about opening up the engine cover. They explained that removing the cover would not only void both my warranty and insurance, but it was illegal. I laughed in his face and drove home a little confused.
Some more research on the Internet confirmed the Apple representatives claims. In fact, it seemed to be a pretty big discussion on the Internet. Trolls and flame wars abounded in various chat rooms. When I asked about it on the iDrive IRC channel, the simply asking of the questions seemed to warrant some pretty emotionally charged statements from some people. I didn't think simply asking the question would get me attacked.
It turned out to be even worse. Not only could I not get to my engine, but nobody else was allowed to either, unless they where a certified Apple technician. I am a grease head, but I also have a mechanic who has been working on my cars for the last fifteen years. Between my personal and professional life I don't always have the time to fix stuff when it breaks. My mechanic has shown me that he can be trusted and does excellent work. Ever since I've found him I've never taken my car to anyone else. The idea that I would be forced to go to some stranger was absurd.
Online there seemed to be a lot of apologizers out there, supporting Apples policies. They argued that Apple was simply ensuring a quality experience. The average person doesn't care how their car work, they just want it to run. By only allowing repairs from certified technicians Apple could guarantee their customers didn't just take it to Joe mechanic and get screwed over. (Incidentally, my mechanics name is Joe.)
As if this wasn't enough, I also could only accessorize my car with products that Apple has licensed. The cigarette lighter was even shaped differently, so none of my devices that would plug into a cigarette lighter would work in my car. The gas intake was different too. I noticed that many of the gas stations advertised that they sold 'Apple Go', which simply meant they had pumps that would fit in an iDrive. It seemed that none of my standard car stuff worked in my iDrive. The only way to even get music was synchronize it with your iTunes library.
Of course, the Apple stores carried all the accessories that did work, but I wasn't really looking forward to replacing all the stuff I already owned.
Even if your iDrive broke down, and you where going to take it to an Apple certified technician for repairs, you also had to get an Apple certified wrecker to come tow it.
The outrage seemed obvious to me. Here I was with an otherwise fantastic machine, but it had all of these artificial barriers placed on it. The stated line was to "ensure the customer experience", but it seemed to me that it was just a way to funnel all the money through Apple. Other people got it, but it seemed limited to other car geeks like me. And even within our group there was a pretty strong fan-based who seemed to not care that their freedoms had been all but removed from their car.
Stuck with the car I figured I would make the most of it. I did purchase some extra accessories from Apple, and a few other certified vendors. They where all pretty amazing. One shaved my face for me on the way to work. Another would show me meta-data about my surroundings, overlaying 3D information about points of interest on the windows. Google came out with an application that would detect traffic problems and reroute you, but Apple rejected it.
A year and a half later, back at the doctor's office, the news reported on Google's new Android engine, just after discussing reports of improvements in the rights for Russian citizens. It was an engine built collaboratively with other manufacturers as a competitor to the iDrive. The computer in the engine would be based off the Linux kernel. A year after that Volvo released the first vehicle with the Android engine. The reviews online gave it high marks, but many where saying it still didn't compare in quality to the iDrive.
Finally a car called the Droid-Z was released. A soon as I test drove the thing I knew it was time to ditch the iDrive. I sold it on e-bay and immediately picked up my Droid-Z. The default features that is shipped with where absolutely on par, or better, than the iDrive.
Pumped, I drove to my buddies house again. The first thing I did when he came out is pop open my hood. The engine was still covered, but was being held down by eight nuts, which easily came off. We immediately started hunting down all of the pieces and parts, figuring out what did what. We drove it around again and fell in love with the experience of the Droid-Z.
When I got home I went back out to the garage and dusted off all my old accessories. Everything in the Droid-Z was standard and all my old gear had a use again. I was in heaven.
When I talk to others about why they should get and Android powered vehicle they don't usually understand what I'm getting out. It seems the average person doesn't see the benefit of being able to buy any standard part, or take their vehicle to any mechanic they choose. They don't see the outrage in being legally prohibited to getting to their engine. They don't realize what a sad future the iDrive, now the iDrive 3, represents for the automotive industry. And the worst part is seeing my fellow grease-head friends fall into the trap.