PHP Database Query Logging with PDO
February 26, 2009
One thing I really like about CakePHP is that when it’s in debug mode it prints out a list of all the database queries used to generate a page, along with the time it took to run each query. This makes it really easy to find problem queries that are slowing down your site. It’s such a great feature that I wanted something similar for projects that don’t use CakePHP.
What I came up with was a LoggedPDO class that extends PHP’s PDO class. If you’re already using PDO in your project you only need to change a single line of code to enable database query logging:
// Standard PDO... $db = new PDO($dsn, $username, $password); // ...should be changed to $db = new LoggedPDO($dsn, $username, $password);
And then all your database queries will be logged, timed and all information printed out at the end of each page. My code also includes a decorator for PDOStatements so any prepared statements that get executed also get logged. Below is a small sample of the code in use:
$db = new LoggedPDO("mysql:dbname=test;host=localhost", "root");
$results = $db->query("SHOW TABLES");
$tables = $results->fetchAll(PDO::FETCH_COLUMN);
foreach($tables AS $table) {
$statement = $db->prepare("DESCRIBE :table");
$statement->bindParam(":table", $table, PDO::PARAM_STR);
$results = $statement->execute();
$columns = $statement->fetchAll(PDO::FETCH_ASSOC);
}
Which would output:
| Query | Time (ms) |
|---|---|
| SHOW TABLES | 0.174 |
| [PS] DESCRIBE :table | 0.06 |
| [PS] DESCRIBE :table | 0.032 |
| 3 queries | 0.266 |
Full code for the both the LoggedPDO and LoggedPDOStatement classes is below:
/**
* Extends PDO and logs all queries that are executed and how long
* they take, including queries issued via prepared statements
*/
class LoggedPDO extends PDO
{
public static $log = array();
public function __construct($dsn, $username = null, $password = null) {
parent::__construct($dsn, $username, $password);
}
/**
* Print out the log when we're destructed. I'm assuming this will
* be at the end of the page. If not you might want to remove this
* destructor and manually call LoggedPDO::printLog();
*/
public function __destruct() {
self::printLog();
}
public function query($query) {
$start = microtime(true);
$result = parent::query($query);
$time = microtime(true) - $start;
LoggedPDO::$log[] = array('query' => $query,
'time' => round($time * 1000, 3));
return $result;
}
/**
* @return LoggedPDOStatement
*/
public function prepare($query) {
return new LoggedPDOStatement(parent::prepare($query));
}
public static function printLog() {
$totalTime = 0;
echo '<table border=1><tr><th>Query</th><th>Time (ms)</th></tr>';
foreach(self::$log as $entry) {
$totalTime += $entry['time'];
echo '<tr><td>' . $entry['query'] . '</td><td>' . $entry['time'] . '</td></tr>\n';
}
echo '<tr><th>' . count(self::$log) . ' queries</th><th>' . $totalTime . '</th></tr>\n';
echo '</table>';
}
}
/**
* PDOStatement decorator that logs when a PDOStatement is
* executed, and the time it took to run
* @see LoggedPDO
*/
class LoggedPDOStatement {
/**
* The PDOStatement we decorate
*/
private $statement;
public function __construct(PDOStatement $statement) {
$this->statement = $statement;
}
/**
* When execute is called record the time it takes and
* then log the query
* @return PDO result set
*/
public function execute() {
$start = microtime(true);
$result = $this->statement->execute();
$time = microtime(true) - $start;
LoggedPDO::$log[] = array('query' => '[PS] ' . $this->statement->queryString,
'time' => round($time * 1000, 3));
return $result;
}
/**
* Other than execute pass all other calls to the PDOStatement object
* @param string $function_name
* @param array $parameters arguments
*/
public function __call($function_name, $parameters) {
return call_user_func_array(array($this->statement, $function_name), $parameters);
}
}
GREAT!!!!
This helped me a lot.
Thanks again
Comment by ofer — March 19, 2009 @ 1:09 pm
This will return an error if parameter will be passed. So to prevent that, you need to add additional variable to execute() function.
Comment by Guy — May 3, 2009 @ 4:00 pm
Minor detail…
Carriage returned indicated as \n do now work within single-quoted strings.
Change the ends of those lines to: blablebla’.”\n”;
Hope that makes sense.
-Alister
Comment by Alister Cameron // Blogologist — August 8, 2009 @ 6:34 am
Hi Alister,
Good point, thanks. I changed the ” quotes in my code to ‘ for the blog.
Thanks, Ben
Comment by Ben — August 20, 2009 @ 10:53 pm
For the LoggedPDOStatement class you can use the PDO::ATTR_STATEMENT_CLASS option.
Usage:
PDO::ATTR_STATEMENT_CLASS => ‘LoggedPDOStatement’,
This must be passed while creating the PDO object or using the PDO->setAttribute function on the created PDO object.
Consult the documentation on that.
Thanks for sharing this code
Comment by Petsagourakis George — August 25, 2009 @ 11:12 am
Thx.
BTW, add
public function __get($name) {
return $this->statement->$name;
}
in LoggedPDOStatement.
Comment by Vlad — August 25, 2009 @ 10:35 pm
Petsagourakis George: Thanks for the tip!
Comment by Ben — August 28, 2009 @ 8:39 pm
Excellent.
Saved me time and worked a treat. What I loved most ? No bloat.
Sick of looking for quick simple solutions only to find I have to download a zip containing a readme, 3/4 sample files, classes, includes..etc and about 800 lines to do the job.
Comment by Tom Taylor — November 3, 2009 @ 9:29 am
This doesn’t actually print the queries that were executed, but instead the original statement with question marks or colons. Also, execute () accepts an array whereas this wrapper does not. It’s easy to fix, but it won’t fill in the bound parameters. Is there a way to get the actual query from PDO?
Comment by henk — November 9, 2009 @ 2:09 pm