This is a follow-up to Lars' comment about the PEAR book. In the MDB2 chapter I showed an example how you can create custom debug handlers in MDB2 and then gave a suggestion about a useful application of this functionality for performance tuning. Basically the idea is that your custom debug handler collects all queries that are executed during the life of a given script. Then, once the script finishes execution, the debug handler reports the stats that it has collected. In the book, the example is how you count the number of times each distinct query is executed, this way you can spot problems caused by the OO abstraction. For example, say you have a come class Users
that has a method loadUser()
, which abstracts the database work. While debugging with the custom error handler, you might figure out that without noticing, you're calling this method in a few places and it makes the same repeating query(queries) over and over again. So you can now optimize/cache results and so on.
The suggestion I made in the book is that in addition to counting, you might want to try executing all SELECTs again, just to see how much time they take and you can execute them once again, prepending them with EXPLAIN to get some details on possible room for improvement.
Now here's one solution to this suggestion. What you can see in this script is:
- Setting up MDB2
- Declaring a custom debug handler class
- "Attaching" it to the MDB2 instance
- Registering it for execution at the end of each script
- Testing it (creating a DB, table, some queries)
I hope you like it and try it out.
Here's the result of executing this script, you can see what you get back.
Room for improvement
Obviously, the method dumpInfo()
can be improved. First, it can print out a nice table, instead of lazy print_r()
. Then, it can include some logic, my idea is for it to "understand" the EXPLAIN results and to give you a hint by using colors, for exampe green background for queries that are OK, yellow for warnings and red for queries that definitelly need some work. Could be nice, no?
Test script
Kinda longish, but I hope I added enough comments. I also hope I didn't introduce any syntax errors while formatting it for posting here, chopping long lines, etc.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
// PEAR error handling setup require_once 'PEAR.php'; function pearError ($e) { echo '<pre>'; echo $e->getMessage().': '.$e->getUserinfo(); echo '</pre>'; } PEAR::setErrorHandling( PEAR_ERROR_CALLBACK, 'pearError' ); // creating MDB2 instance require_once 'MDB2.php'; $dsn = 'mysql://root:test@localhost'; $mdb2 =& MDB2::factory($dsn); $mdb2->setFetchMode(MDB2_FETCHMODE_ASSOC); // The custom error handler // // It will collect all the queries being executed // in the script, the collection is done by the // collectInfo() method. // Once the script finishes executing, we'll call // the method executeAndExplain() which will // execute all unique SELECTs once again // in order to give us an info of how much time // each query takes. // Then executeAndExplain() will execute again // all SELECTs, this time prepending an EXPLAIN // so that we can get valuable // optimization-related information // Not only that but instead of simple EXPLAIN, // we can use EXPLAIN EXTENDED and after that // we can call SHOW WARNINGS - // this will give us even more optimization hints // // http://dev.mysql.com/doc/refman/5.1/en/explain.html // http://dev.mysql.com/doc/refman/5.1/en/show-warnings.html // class Explain_Queries { // how many queries were executed var $query_count = 0; // which queries and their count var $queries = array(); // results of EXPLAIN-ed SELECTs var $explains = array(); // the MDB2 instance var $db = false; // constructor that accepts MDB2 reference function Explain_Queries(&$db) { $this->db = $db; } // this method is called on every query function collectInfo( &$db, $scope, $message, $is_manip = null) { // increment the total number of queries $this->query_count++; // the SQL is a key in the queries array // the value will be the count of how // many times each query was executed @$this->queries[$message]++; } // print the debug information function dumpInfo() { echo '<h3>Queries on this page</h3>'; echo '<pre>'; print_r($this->queries); echo '</pre>'; echo '<h3>EXPLAIN-ed SELECTs</h3>'; echo '<pre>'; print_r($this->explains); echo '</pre>'; } // the method that will execute all SELECTs // with and without an EXPLAIN and will // create $this->explains array of debug // information // SHOW WARNINGS will be called after each // EXPLAIN for more information function executeAndExplain() { // at this point, stop debugging $this->db->setOption('debug', 0); $this->db->loadModule('Extended'); // take the SQL for all the unique queries $queries = array_keys($this->queries); foreach ($queries AS $sql) { // for all SELECTs… $sql = trim($sql); if (stristr($sql,"SELECT") !== false){ // note the start time $start_time = array_sum( explode(" ", microtime()) ); // execute query $this->db->query($sql); // note the end time $end_time = array_sum( explode(" ", microtime()) ); // the time the query took $total_time = $end_time - $start_time; // now execute the same query with // EXPLAIN EXTENDED prepended $explain = $this->db->getAll( 'EXPLAIN EXTENDED ' . $sql ); $this->explains[$sql] = array(); // update the debug array with the // new data from // EXPLAIN and SHOW WARNINGS if (!PEAR::isError($explain)) { $this->explains[$sql]['explain'] = $explain; $this->explains[$sql]['warnings'] = $this->db->getAll('SHOW WARNINGS'); } // update the debug array with the // count and time $this->explains[$sql]['time'] = $total_time; } } } } // instance of the custom debug handler $my_debug_handler = new Explain_Queries($mdb2); // set debug option $mdb2->setOption('debug', 1); // set debug handler to the method that // collects all queries $mdb2->setOption( 'debug_handler', array($my_debug_handler, 'collectInfo') ); // register functions to be executed on shut down // after the script has finished execution. // Now that the show's over, it's the time to // report what happened in this script db-access-wise // First shutdown function executes the // SELECTs again, the other one prints the results register_shutdown_function( array($my_debug_handler, 'executeAndExplain') ); register_shutdown_function( array($my_debug_handler, 'dumpInfo') ); // // // At this point all MDB2 setup is done, // time for the actual script to do something // // // load the DB manager module $mdb2->loadModule('Manager'); // drop database if it exists // temporarily change the PEAR error handling PEAR::pushErrorHandling(PEAR_ERROR_RETURN); $mdb2->dropDatabase('test_db_explain'); PEAR::popErrorHandling(); // create and set a new database $mdb2->createDatabase('test_db_explain'); $mdb2->setDatabase('test_db_explain'); // create table "events" from a definition array // the table has event ID, name and date/time $definition = array ( 'id' => array ( 'type' => 'integer', 'unsigned' => 1, 'notnull' => 1, 'default' => 0, ), 'name' => array ( 'type' => 'text', 'length' => 255 ), 'datetime' => array ( 'type' => 'timestamp' ) ); $mdb2->createTable('events', $definition); // create a primary key - the ID field $definition = array ( 'primary' => true, 'fields' => array ( 'id' => array() ) ); $mdb2->createConstraint( 'events', 'myprimekey', $definition ); // load the class that has some static helper // functions to work with MDB2's cross-RDBMS // date format MDB2::loadFile('Date'); // INSERT // some data to insert into the events table $data = array( // using MDB2-managed sequences 'id' => $mdb2->nextId('events'), 'name' => "Breakfast a Tiffany's", 'datetime' => MDB2_Date::unix2Mdbstamp( strtotime('Jan 15, 2007') ) ); // The "datetime" value shows how you can use // any date format you wish as long as you're // able to get a unix timestamp out of it // In this case I'm using strtotime() // Then there is a call to MDB2's date helper // to get the MDB2 timestamp // auto insert // for the autoExecute() method we need to // load the Extended module $mdb2->loadModule('Extended'); $result = $mdb2->autoExecute( 'events', $data, MDB2_AUTOQUERY_INSERT ); // // Time to SELECT something // // Using date helpers again $start_date = MDB2_Date::date2Mdbstamp(0,0,0,12,31,1980); $end_date = MDB2_Date::date2Mdbstamp(0,0,0,12,31,2020); $sql = 'SELECT * FROM %s WHERE %s > %s AND %s < %s'; $sql = sprintf( $sql, $mdb2->quoteIdentifier('events'), // quote table name $mdb2->quoteIdentifier('datetime'), // quote field name $mdb2->quote($start_date, 'date'), // quote data as date $mdb2->quoteIdentifier('datetime'), // quote field name $mdb2->quote($end_date, 'date') // quote data as date ); $res = $mdb2->getAll($sql); // execute // // * Bad practice code follows * // Just some more inserts and selects, bad // practice because these queries are not // necessarily portable accross various RDBMS, // lacking proper quoting and preparation for ($i = 2; $i < 31; $i++) { $mdb2->query( 'INSERT INTO events VALUES(' . $i . ', "test event", "2005-05-05 00:00:00")' ); } $res = $mdb2->getCol( 'SELECT DISTINCT datetime FROM events' ); $res = $mdb2->getRow( 'SELECT * FROM events WHERE id IN (SELECT id FROM events WHERE id > 1)' ); |
Comments? Find me on BlueSky, Mastodon, LinkedIn, Threads, Twitter