Return Multiple Result Sets with PHP and PostgreSQL Functions
04 Jan
A while back I discussed how to use the new sqlsrv extension for php to retrieve multiple result sets at a time from stored proceedures. This was indeed helpful when you need to retrieve related data and avoid multiple calls to stored proceedures. The down side is this was using a rather expensive (but good) database engine. Also, until recently, there was absolutely no support for the sqlsrv php extension on unix based servers. How can the open source world tap into such functionality?
PostgreSQL has the ability to return multiple cursors to result sets. Using this we can achieve the same effect with php with a little magic. Lets take a look at a simple function which returns three queries by way of refcursor type. Also note that I said “function”, not “procedure”, but never fear, 6 of one, half dozen of the other as far as this topic goes:
CREATE OR REPLACE FUNCTION test_get_users(userID integer) RETURNS SETOF refcursor AS $$ DECLARE ref1 refcursor; ref2 refcursor; ref3 refcursor; BEGIN OPEN ref1 FOR SELECT id, name, email FROM users; RETURN NEXT ref1; OPEN ref2 FOR SELECT id FROM users WHERE is_active=1; RETURN next ref2; OPEN ref3 FOR SELECT * FROM users WHERE id = userID; RETURN next ref3; RETURN; END; $$ LANGUAGE plpgsql;
So that is pretty simple, we run 3 select queries, and return the refcursor for those. Now the tricky part will be how to read each of these queries on the php end. The values returned from calling this function will resemble the image presented here. These merely represent references to the cursors to each query run, so we will have to use some pg magic to pull those result sets in. It boils down to we need to make one call to the function, then a separate call per result set to bring in those results (note the query has run at this point, its basically reading in the data, which should be quick). Lets look at the SQL which would achieve this:
BEGIN; SELECT test_get_users(2); FETCH ALL IN "<unnamed portal 1>"; FETCH ALL IN "<unnamed portal 2>"; FETCH ALL IN "<unnamed portal 3>"; END;
Now it should make a little more sense what has to happen. We call a fetch for each result set to return. So now we just need to process this with PHP:
$conn = new PDO("pgsql:host=localhost;port=5432;dbname=testing", "username", "password");
$userID = 2;
// begin transaction, this is all one process
$conn->beginTransaction();
// call the function
$stmt = $conn->prepare("select test_get_users(:userID)");
$stmt->bindParam('userID', $userID, PDO::PARAM_INT);
$stmt->execute();
$cursors = $stmt->fetchAll();
$stmt->closeCursor();
// get each result set
$results = array();
foreach($cursors as $k=>$v){
$stmt = $conn->query('FETCH ALL IN "'. $v[0] .'";');
$results[$k] = $stmt->fetchAll();
$stmt->closeCursor();
}
$conn->commit();
unset($stmt);
echo '<pre>';
print_r($results);echo "\n"; // all record sets
echo '</pre>';
Now first off, we call the select test_get_users(), which returns a list of refcursor names. So with this we know how many queries were returned. Just loop through this list and fetch each result set. It is essentially the same process used from the Sql Server example, except the nextRowset() is not yet implemented for the pdo pgsql extension, so we just manually call the fetch. No Biggie. You would then receive something similar to the following:
Array ( [0] => Array ( [0] => Array ( [id] => 2 [0] => 2 [name] => John Doe [1] => John Doe [email] => test2@test.com [2] => test2@test.com ) [1] => Array ( [id] => 3 [0] => 3 [name] => Jane Doe [1] => Jane Doe [email] => test3@test.com [2] => test3@test.com ) ...... ) [1] => Array ( [0] => Array ( [id] => 3 [0] => 3 ) [1] => Array ( [id] => 1 [0] => 1 ) ) [2] => Array ( [0] => Array ( [id] => 2 [0] => 2 [name] => John Doe [1] => John Doe [email] => test2@test.com [2] => test2@test.com [password] => zxcvxcvczxcv [3] => zxcvxcvczxcv [is_active] => 0 [4] => 0 ) ) )
If you’re still feeling frisky at this point, there are all sorts of ways to “fetch” the result sets, just take a look at the docs on cursors. The more I use PostgreSQL the more I am becoming a fan. It’s power and features go far beyond my skill set, but it feels good always knowing you have the best tool at hand for whatever task you have ahead of you. As always feel free to chime in and leave any knowledge you may have, especially on Postgres!



One Response
trent
Jan 04, 2012It may also be worth noting a use case for something like this. It is in fact very useful. Lets look at an example.
Suppose your site has a login area. Upon login you would like to present the user with a summary of their account. If it’s a blog you may show them a count of: how many blog posts, how many drafts, how many comments, how many pending comments etc. This would probably take about 4 queries/requests to the database at a minimum, unless you do some fancy conditioning, which will slow down the queries anyway. Now even though we must do separate FETCH requests, the heavy lifting has already been performed in the first query to the function. And I imagine the database engine will be able to optimize the function’s execution a little better than separate custom queries.
Also add on top of that you will get back a nice clean result set which can be cached and you’re looking a significant speed improvement. I believe this routine is worthy of wrapping up in a nice convenience method or a CakePHP Stored Procedure Behavior!