random technical thoughts from the Nominet technical team

How to build complicated Dynamic SQL within PL/SQL

1 Star2 Stars3 Stars4 Stars5 Stars (5 votes, average: 4.6 out of 5)
Loading ... Loading ...
Posted by chris on Mar 22nd, 2007

It is not unusual to be in a situation where a routine in PL/SQL takes a long series of arguments to use as parameters in a query. For example, suppose we have the following function:

FUNCTION Find_People_By_Address(street_address_in VARCHAR2,
                                locality_in VARCHAR2,
                                city_in VARCHAR2,
                                province_in VARCHAR2,
                                country_in VARCHAR2,
                                postcode_in VARCHAR2) RETURN PERSON_CURSOR;

We pass in parameters and expect to receive back a cursor which will give us people whose address matches those parameters. If we pass NULL, it means that we don’t want to consider that field. One way to do this is to open our cursor with a SQL statement that that looks something like this

SELECT ID, NAME
FROM PEOPLE_TABLE
WHERE (street_address_in IS NULL OR STREET_ADDRESS = street_address_in)
AND (locality_in IS NULL OR LOCALITY = locality_in)
AND (city_in IS NULL OR CITY = city_in)
...

This is fine for the simplest cases, but sometimes the optimizer gets a little confused because you have mentioned every column in the table. It is also a little hard to read and this becomes more and more of a problem as the query gets complicated. It would be better if passing in NULL for everything except postcode produced a query like this

SELECT ID, NAME
FROM PEOPLE_TABLE
WHERE POSTCODE = postcode_in

It is obvious to the optimizer what to do. Assuming postcode is indexed, the query will perform well. The logical way to do this is to build the SQL dynamically, but this can also open the door for the dreaded SQL Injection Attack. This happens if you use concatenation to add the arguments into the query like this

IF city_in IS NOT NULL THEN
   query := query || ' AND CITY = ''' || city_in || '''';
END IF;

Obviously, this is bad practice. So how do we get around this? The answer is of course bind variables, which Oracle supports for dynamic SQL. But it isn’t obvious how we use them here. Surely the number of bind variables we need will depend on the number of non-NULL arguments passed to our routine? Yes and no. You can work around this by keeping the number of bind variables fixed with one per argument, but by effectively throwing away the ones you are not interested in. You build the SQL like this:

query := 'SELECT ID, NAME FROM PEOPLE TABLE WHERE 1=1';

IF street_address IS NOT NULL THEN
   query := query || ' AND STREET_ADDRESS = :1';
ELSE
   query := query || ' AND (1=1 OR :1 IS NULL)';
END IF;

......

and then open the cursor like so

OPEN result_cursor
FOR query
USING street_address_in, locality_in, city_in, province_in, country_in, postcode_in;

So our query will end up with six bind variables in it every time, but the ones which are NULL will effectively be thrown away. Our query may not end up as readable as the postcode example given above, but it should be as fast and doesn’t have a vulnerability to SQL Injection.

Leave a Comment

Please note: Comment moderation is enabled and may delay your comment. There is no need to resubmit your comment.

Recent Posts

Highest Rated

Categories

Archives

Meta: