Quickstart and Examples The mysqlnd query cache plugin is easy to use. This quickstart will demo typical use-cases, and provide practical advice on getting started. It is strongly recommended to read the reference sections in addition to the quickstart. It is safe to begin with the quickstart. However, before using the plugin in mission critical environments we urge you to read additionally the background information from the reference sections. Most of the examples use the mysqli extension because it is the most feature complete PHP MySQL extension. However, the plugin can be used with any PHP MySQL extension that is using the mysqlnd library.
Architecture and Concepts The query cache plugin is implemented as a PHP extension. It is written in C and operates under the hood of PHP. During the startup of the PHP interpreter, it gets registered as a mysqlnd plugin to replace selected mysqlnd C methods. Hereby, it can change the behaviour of any PHP MySQL extension (mysqli, PDO_MYSQL, mysql) compiled to use the mysqlnd library without changing the extensions API. This makes the plugin compatible with each and every PHP MySQL application. Because existing APIs are not changed, it is almost transparent to use. Please, see the mysqlnd plugin API description for a discussion of the advantages of the plugin architecture and a comparison with proxy based solutions. Transparent to use At PHP run time PECL/mysqlnd_qc can proxy queries send from PHP (mysqlnd) to the MySQL server. It then inspects the statement string to find whether it shall cache its results. If so, result set is cached using a storage handler and further executions of the statement are served from the cache for a user-defined period. The Time to Live (TTL) of the cache entry can either be set globally or on a per statement basis. A statement is either cached if the plugin is instructed to cache all statements globally using a or, if the query string starts with the SQL hint (/*qc=on*/). The plugin is capable of caching any query issued by calling appropriate API calls of any of the existing PHP MySQL extensions. Flexible storage: various storage handler Various storage handler are supported to offer different scopes for cache entries. Different scopes allow for different degrees in sharing cache entries among clients. default (built-in): process memory, scope: process, one or more web requests depending on PHP deployment model used APC: shared memory, scope: single server, multiple web requests SQLite: memory or file, scope: single server, multiple web requests MEMCACHE: main memory, scope: single or multiple server, multiple web requests user (built-in): user-defined - any, scope: user-defined - any Support for the APC, SQLite and MEMCACHE storage handler has to be enabled at compile time. The default and user handler are built-in. It is possible to switch between compiled-in storage handlers on a per query basis at run time. However, it is recommended to pick one storage handler and use it for all cache entries. Built-in slam defense to avoid overloading To avoid overload situations the cache plugin has a built-in slam defense mechanism. If a popular cache entries expires many clients using the cache entries will try to refresh the cache entry. For the duration of the refresh many clients may access the database server concurrently. In the worst case, the database server becomes overloaded and it takes more and more time to refresh the cache entry, which in turn lets more and more clients try to refresh the cache entry. To prevent this from happening the plugin has a slam defense mechanism. If slam defense is enabled and the plugin detects an expired cache entry it extends the life time of the cache entry before it refreshes the cache entry. This way other concurrent accesses to the expired cache entry are still served from the cache for a certain time. The other concurrent accesses to not trigger a concurrent refresh. Ideally, the cache entry gets refreshed by the client which extended the cache entries lifespan before other clients try to refresh the cache and potentially cause an overload situation. Unique approach to caching PECL/mysqlnd_qc has a unique approach to caching result sets that is superior to application based cache solutions. Application based solutions first fetch a result set into PHP variables. Then, the PHP variables are serialized for storage in a persistent cache, and then unserialized when fetching. The mysqlnd query cache stores the raw wire protocol data sent from MySQL to PHP in its cache and replays it, if still valid, on a cache hit. This way, it saves an extra serialization step for a cache put that all application based solutions have to do. It can store the raw wire protocol data in the cache without having to serialize into a PHP variable first and deserializing the PHP variable for storing in the cache again.
Setup The plugin is implemented as a PHP extension. See also the installation instructions to install the PECL/mysqlnd_qc extension. Compile or configure the PHP MySQL extension (mysqli, PDO_MYSQL, mysql) that you plan to use with support for the mysqlnd library. PECL/mysqlnd_qc is a plugin for the mysqlnd library. To use the plugin with any of the existing PHP MySQL extensions (APIs), the extension has to use the mysqlnd library. Then, load the extension into PHP and activate the plugin in the PHP configuration file using the PHP configuration directive named mysqlnd_qc.enable_qc. Enabling the plugin (php.ini)
Caching queries There are four ways to trigger caching of a query. Use of SQL hints on a per query basis User supplied callbacks to decide on a per query basis, for example, using mysqlnd_qc_is_select mysqlnd_set_cache_condition for rule based automatic per query decisions mysqlnd_qc.cache_by_default = 1 to cache all queries blindly Use of SQL hints and mysqlnd_qc.cache_by_default = 1 are explained below. Please, refer to the function reference on mysqlnd_qc_is_select for a description of using a callback and, mysqlnd_qc_set_cache_condition on how to set rules for automatic caching. A SQL hint is a SQL standards compliant comment. As a SQL comment it is ignored by the database. A statement is considered eligible for caching if it either begins with the SQL hint enabling caching or it is a SELECT statement. An individual query which shall be cached must begin with the SQL hint /*qc=on*/. It is recommended to use the PHP constant MYSQLND_QC_ENABLE_SWITCH instead of using the string value. not eligible for caching and not cached: INSERT INTO test(id) VALUES (1) not eligible for caching and not cached: SHOW ENGINES eligible for caching but uncached: SELECT id FROM test eligible for caching and cached: /*qc=on*/SELECT id FROM test The examples SELECT statement string is prefixed with the MYSQLND_QC_ENABLE_SWITCH SQL hint to enable caching of the statement. The SQL hint must be given at the very beginning of the statement string to enable caching. Using the <literal>MYSQLND_QC_ENABLE_SWITCH</literal> SQL hint query("DROP TABLE IF EXISTS test"); $mysqli->query("CREATE TABLE test(id INT)"); $mysqli->query("INSERT INTO test(id) VALUES (1), (2)"); /* Will be cached because of the SQL hint */ $start = microtime(true); $res = $mysqli->query("/*" . MYSQLND_QC_ENABLE_SWITCH . "*/" . "SELECT id FROM test WHERE id = 1"); var_dump($res->fetch_assoc()); $res->free(); printf("Total time uncached query: %.6fs\n", microtime(true) - $start); /* Cache hit */ $start = microtime(true); $res = $mysqli->query("/*" . MYSQLND_QC_ENABLE_SWITCH . "*/" . "SELECT id FROM test WHERE id = 1"); var_dump($res->fetch_assoc()); $res->free(); printf("Total time cached query: %.6fs\n", microtime(true) - $start); ?> ]]> &examples.outputs.similar; string(1) "1" } Total time uncached query: 0.000740s array(1) { ["id"]=> string(1) "1" } Total time cached query: 0.000098s ]]> If nothing else is configured, as it is the case in the quickstart example, the plugin will use the built-in default storage handler. The default storage handler uses process memory to hold a cache entry. Depending on the PHP deployment model, a PHP process may serve one or more web requests. Please, consult the web server manual for details. Details make no difference for the examples given in the quickstart. The query cache plugin will cache all queries regardless if the query string begins with the SQL hint which enables caching or not, if the PHP configuration directive mysqlnd_qc.cache_by_default is set to 1. The setting mysqlnd_qc.cache_by_default is evaluated by the core of the query cache plugins. Neither the built-in nor user-defined storage handler can overrule the setting. The SQL hint /*qc=off*/ can be used to disable caching of individual queries if mysqlnd_qc.cache_by_default = 1 It is recommended to use the PHP constant MYSQLND_QC_DISABLE_SWITCH instead of using the string value. Using the <literal>MYSQLND_QC_DISABLE_SWITCH</literal> SQL hint query("DROP TABLE IF EXISTS test"); $mysqli->query("CREATE TABLE test(id INT)"); $mysqli->query("INSERT INTO test(id) VALUES (1), (2)"); /* Will be cached although no SQL hint is present because of mysqlnd_qc.cache_by_default = 1*/ $res = $mysqli->query("SELECT id FROM test WHERE id = 1"); var_dump($res->fetch_assoc()); $res->free(); $mysqli->query("DELETE FROM test WHERE id = 1"); /* Cache hit - no automatic invalidation and still valid! */ $res = $mysqli->query("SELECT id FROM test WHERE id = 1"); var_dump($res->fetch_assoc()); $res->free(); /* Cache miss - query must not be cached because of the SQL hint */ $res = $mysqli->query("/*" . MYSQLND_QC_DISABLE_SWITCH . "*/SELECT id FROM test WHERE id = 1"); var_dump($res->fetch_assoc()); $res->free(); ?> ]]> &examples.outputs; string(1) "1" } array(1) { ["id"]=> string(1) "1" } NULL ]]> PECL/mysqlnd_qc forbids caching of statements for which at least one column from the statements result set shows no table name in its meta data by default. This is usually the case for columns originating from SQL functions such as NOW() or LAST_INSERT_ID(). The policy aims to prevent pitfalls if caching by default is used. Example showing which type of statements are not cached query("DROP TABLE IF EXISTS test"); $mysqli->query("CREATE TABLE test(id INT)"); $mysqli->query("INSERT INTO test(id) VALUES (1)"); for ($i = 0; $i < 3; $i++) { $start = microtime(true); /* Note: statement will not be cached because of NOW() use */ $res = $mysqli->query("SELECT id, NOW() AS _time FROM test"); $row = $res->fetch_assoc(); /* dump results */ var_dump($row); printf("Total time: %.6fs\n", microtime(true) - $start); /* pause one second */ sleep(1); } ?> ]]> &examples.outputs.similar; string(1) "1" ["_time"]=> string(19) "2012-01-11 15:43:10" } Total time: 0.000540s array(2) { ["id"]=> string(1) "1" ["_time"]=> string(19) "2012-01-11 15:43:11" } Total time: 0.000555s array(2) { ["id"]=> string(1) "1" ["_time"]=> string(19) "2012-01-11 15:43:12" } Total time: 0.000549s ]]> It is possible to enable caching for all statements including those which has columns in their result set for which MySQL reports no table, such as the statement from the example. Set mysqlnd_qc.cache_no_table = 1 to enable caching of such statements. Please, note the difference in the measured times for the above and below examples. Enabling caching for all statements using the <literal>mysqlnd_qc.cache_no_table</literal> ini setting query("DROP TABLE IF EXISTS test"); $mysqli->query("CREATE TABLE test(id INT)"); $mysqli->query("INSERT INTO test(id) VALUES (1)"); for ($i = 0; $i < 3; $i++) { $start = microtime(true); /* Note: statement will not be cached because of NOW() use */ $res = $mysqli->query("SELECT id, NOW() AS _time FROM test"); $row = $res->fetch_assoc(); /* dump results */ var_dump($row); printf("Total time: %.6fs\n", microtime(true) - $start); /* pause one second */ sleep(1); } ?> ]]> &examples.outputs.similar; string(1) "1" ["_time"]=> string(19) "2012-01-11 15:47:45" } Total time: 0.000546s array(2) { ["id"]=> string(1) "1" ["_time"]=> string(19) "2012-01-11 15:47:45" } Total time: 0.000187s array(2) { ["id"]=> string(1) "1" ["_time"]=> string(19) "2012-01-11 15:47:45" } Total time: 0.000167s ]]> Although mysqlnd_qc.cache_no_table = 1 has been created for use with mysqlnd_qc.cache_by_default = 1 it is bound it. The plugin will evaluate the mysqlnd_qc.cache_no_table whenever a query is to be cached, no matter whether caching has been enabled using a SQL hint or any other measure.
Setting the TTL The default invalidation strategy of the query cache plugin is Time to Live (TTL). The built-in storage handlers will use the default TTL defined by the PHP configuration value mysqlnd_qc.ttl unless the query string contains a hint for setting a different TTL. The TTL is specified in seconds. By default cache entries expire after 30 seconds The example sets mysqlnd_qc.ttl=3 to cache statements for three seconds by default. Every second it updates a database table record to hold the current time and executes a SELECT statement to fetch the record from the database. The SELECT statement is cached for three seconds because it is prefixed with the SQL hint enabling caching. The output verifies that the query results are taken from the cache for the duration of three seconds before they are refreshed. Setting the TTL with the <literal>mysqlnd_qc.ttl</literal> ini setting query("DROP TABLE IF EXISTS test"); $mysqli->query("CREATE TABLE test(id VARCHAR(255))"); for ($i = 0; $i < 7; $i++) { /* update DB row */ if (!$mysqli->query("DELETE FROM test") || !$mysqli->query("INSERT INTO test(id) VALUES (NOW())")) /* Of course, a real-life script should do better error handling */ die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error)); /* select latest row but cache results */ $query = "/*" . MYSQLND_QC_ENABLE_SWITCH . "*/"; $query .= "SELECT id AS _time FROM test"; if (!($res = $mysqli->query($query)) || !($row = $res->fetch_assoc())) { printf("[%d] %s\n", $mysqli->errno, $mysqli->error); } $res->free(); printf("Wall time %s - DB row time %s\n", date("H:i:s"), $row['_time']); /* pause one second */ sleep(1); } ?> ]]> &examples.outputs.similar; As can be seen from the example, any TTL based cache can serve stale data. Cache entries are not automatically invalidated, if underlying data changes. Applications using the default TTL invalidation strategy must be able to work correctly with stale data. A user-defined cache storage handler can implement any invalidation strategy to work around this limitation. The default TTL can be overruled using the SQL hint /*qc_tt=seconds*/. The SQL hint must be appear immediately after the SQL hint which enables caching. It is recommended to use the PHP constant MYSQLND_QC_TTL_SWITCH instead of using the string value. Setting TTL with SQL hints query("DROP TABLE IF EXISTS test"); $mysqli->query("CREATE TABLE test(id INT)"); $mysqli->query("INSERT INTO test(id) VALUES (1), (2)"); printf("Default TTL\t: %d seconds\n", ini_get("mysqlnd_qc.ttl")); /* Will be cached for 2 seconds */ $sql = sprintf("/*%s*//*%s%d*/SELECT id FROM test WHERE id = 1", MYSQLND_QC_ENABLE_SWITCH, MYSQLND_QC_TTL_SWITCH, 2); $res = $mysqli->query($sql); var_dump($res->fetch_assoc()); $res->free(); $mysqli->query("DELETE FROM test WHERE id = 1"); sleep(1); /* Cache hit - no automatic invalidation and still valid! */ $res = $mysqli->query($sql); var_dump($res->fetch_assoc()); $res->free(); sleep(2); /* Cache miss - cache entry has expired */ $res = $mysqli->query($sql); var_dump($res->fetch_assoc()); $res->free(); printf("Script runtime\t: %d seconds\n", microtime(true) - $start); ?> ]]> &examples.outputs.similar; string(1) "1" } array(1) { ["id"]=> string(1) "1" } NULL Script runtime : 3 seconds ]]>
Pattern based caching An application has three options for telling PECL/mysqlnd_qc whether a particular statement shall be used. The most basic approach is to cache all statements by setting mysqlnd_qc.cache_by_default = 1. This approach is often of little practical value. But it enables users to make a quick estimation about the maximum performance gains from caching. An application designed to use a cache may be able to prefix selected statements with the appropriate SQL hints. However, altering an applications source code may not always be possible or desired, for example, to avoid problems with software updates. Therefore, PECL/mysqlnd_qc allows setting a callback which decides if a query is to be cached. The callback is installed with the mysqlnd_qc_set_is_select function. The callback is given the statement string of every statement inspected by the plugin. Then, the callback can decide whether to cache the function. The callback is supposed to return &false; if the statement shall not be cached. A return value of &true; makes the plugin try to add the statement into the cache. The cache entry will be given the default TTL ( mysqlnd_qc.ttl). If the callback returns a numerical value it is used as the TTL instead of the global default. Setting a callback with <function>mysqlnd_qc_set_is_select</function> true, /* 3 - use TTL = 3 seconds */ "@SELECT\s+.*\s+FROM\s+news@ismU" => 3 ); /* check if query does match pattern */ foreach ($patterns as $pattern => $ttl) { if (preg_match($pattern, $query)) { printf("is_select(%45s): cache\n", $query); return $ttl; } } printf("is_select(%45s): do not cache\n", $query); return false; } /* install callback */ mysqlnd_qc_set_is_select("is_select"); /* Connect, create and populate test table */ $mysqli = new mysqli("host", "user", "password", "schema", "port", "socket"); $mysqli->query("DROP TABLE IF EXISTS test"); $mysqli->query("CREATE TABLE test(id INT)"); $mysqli->query("INSERT INTO test(id) VALUES (1), (2), (3)"); /* cache put */ $mysqli->query("SELECT id FROM test WHERE id = 1"); /* cache hit */ $mysqli->query("SELECT id FROM test WHERE id = 1"); /* cache put */ $mysqli->query("SELECT * FROM test"); $stats = mysqlnd_qc_get_core_stats(); printf("Cache put: %d\n", $stats['cache_put']); printf("Cache hit: %d\n", $stats['cache_hit']); ?> ]]> &examples.outputs.similar; The examples callback tests if a statement string matches a pattern. If this is the case, it either returns &true; to cache the statement using the global default TTL or an alternative TTL. To minimize application changes the callback can put into and registered in an auto prepend file.
Slam defense A badly designed cache can do more harm than good. In the worst case a cache can increase database server load instead of minimizing it. An overload situation can occur if a highly shared cache entry expires (cache stampeding). Cache entries are shared and reused to a different degree depending on the storage used. The default storage handler stores cache entries in process memory. Thus, a cache entry can be reused for the life-span of a process. Other PHP processes cannot access it. If Memcache is used, a cache entry can be shared among multiple PHP processes and even among multiple machines, depending on the set up being used. If a highly shared cache entry stored, for example, in Memcache expires, many clients gets a cache miss. Many client requests can no longer be served from the cache but try to run the underlying query on the database server. Until the cache entry is refreshed, more and more clients contact the database server. In the worst case, a total lost of service is the result. The overload can be avoided using a storage handler which limits the reuse of cache entries to few clients. Then, at the average, its likely that only a limited number of clients will try to refresh a cache entry concurrently. Additionally, the built-in slam defense mechanism can and should be used. If slam defense is activated an expired cache entry is given an extended life time. The first client getting a cache miss for the expired cache entry tries to refresh the cache entry within the extended life time. All other clients requesting the cache entry are temporarily served from the cache although the original TTL of the cache entry has expired. The other clients will not experience a cache miss before the extended life time is over. Enabling the slam defense mechanism The slam defense mechanism is enabled with the PHP configuration directive mysqlnd_qc.slam_defense. The extended life time of a cache entry is set with mysqlnd_qc.slam_defense_ttl. The function mysqlnd_qc_get_core_stats returns an array of statistics. The statistics slam_stale_refresh and slam_stale_hit are incremented if slam defense takes place. It is not possible to give a one-fits-all recommendation on the slam defense configuration. Users are advised to monitor and test their setup and derive settings accordingly.
Finding cache candidates A statement should be considered for caching if it is executed often and has a long run time. Cache candidates are found by creating a list of statements sorted by the product of the number of executions multiplied by the statements run time. The function mysqlnd_qc_get_query_trace_log returns a query log which help with the task. Collecting a query trace is a slow operation. Thus, it is disabled by default. The PHP configuration directive mysqlnd_qc.collect_query_trace is used to enable it. The functions trace contains one entry for every query issued before the function is called. Collecting a query trace query("SELECT 1 AS _one FROM DUAL"); $res->free(); } /* dump trace */ var_dump(mysqlnd_qc_get_query_trace_log()); ?> ]]> &examples.outputs; array(8) { ["query"]=> string(26) "SELECT 1 AS _one FROM DUAL" ["origin"]=> string(102) "#0 qc.php(7): mysqli->query('SELECT 1 AS _on...') #1 {main}" ["run_time"]=> int(0) ["store_time"]=> int(25) ["eligible_for_caching"]=> bool(false) ["no_table"]=> bool(false) ["was_added"]=> bool(false) ["was_already_in_cache"]=> bool(false) } [1]=> array(8) { ["query"]=> string(26) "SELECT 1 AS _one FROM DUAL" ["origin"]=> string(102) "#0 qc.php(7): mysqli->query('SELECT 1 AS _on...') #1 {main}" ["run_time"]=> int(0) ["store_time"]=> int(8) ["eligible_for_caching"]=> bool(false) ["no_table"]=> bool(false) ["was_added"]=> bool(false) ["was_already_in_cache"]=> bool(false) } } ]]> Assorted information is given in the trace. Among them timings and the origin of the query call. The origin property holds a code backtrace to identify the source of the query. The depth of the backtrace can be limited with the PHP configuration directive mysqlnd_qc.query_trace_bt_depth. The default depth is 3. Setting the backtrace depth with the <literal>mysqlnd_qc.query_trace_bt_depth</literal> ini setting query("DROP TABLE IF EXISTS test"); $mysqli->query("CREATE TABLE test(id INT)"); $mysqli->query("INSERT INTO test(id) VALUES (1), (2), (3)"); /* dummy queries to fill the query trace */ for ($i = 0; $i < 3; $i++) { $res = $mysqli->query("SELECT id FROM test WHERE id = " . $mysqli->real_escape_string($i)); $res->free(); } $trace = mysqlnd_qc_get_query_trace_log(); $summary = array(); foreach ($trace as $entry) { if (!isset($summary[$entry['query']])) { $summary[$entry['query']] = array( "executions" => 1, "time" => $entry['run_time'] + $entry['store_time'], ); } else { $summary[$entry['query']]['executions']++; $summary[$entry['query']]['time'] += $entry['run_time'] + $entry['store_time']; } } foreach ($summary as $query => $details) { printf("%45s: %5dms (%dx)\n", $query, $details['time'], $details['executions']); } ?> ]]> &examples.outputs.similar;
Measuring cache efficiency PECL/mysqlnd_qc offers three ways to measure the cache efficiency. The function mysqlnd_qc_get_normalized_query_trace_log returns statistics aggregated by the normalized query string, mysqlnd_qc_get_cache_info gives storage handler specific information which includes a list of all cached items, depending on the storage handler. Additionally, the core of PECL/mysqlnd_qc collects high-level summary statistics aggregated per PHP process. The high-level statistics are returned by mysqlnd_qc_get_core_stats. The functions mysqlnd_qc_get_normalized_query_trace_log and mysqlnd_qc_get_core_stats will not collect data unless data collection has been enabled through their corresponding PHP configuration directives. Data collection is disabled by default for performance considerations. It is configurable with the mysqlnd_qc.time_statistics option, which determines if timing information should be collected. Collection of time statistics is enabled by default but only performed if data collection as such has been enabled. Recording time statistics causes extra system calls. In most cases, the benefit of the monitoring outweighs any potential performance penalty of the additional system calls. Collecting statistics data with the <literal>mysqlnd_qc.time_statistics</literal> ini setting query("DROP TABLE IF EXISTS test"); $mysqli->query("CREATE TABLE test(id INT)"); $mysqli->query("INSERT INTO test(id) VALUES (1), (2), (3)"); /* dummy queries */ for ($i = 1; $i <= 4; $i++) { $query = sprintf("/*%s*/SELECT id FROM test WHERE id = %d", MYSQLND_QC_ENABLE_SWITCH, $i % 2); $res = $mysqli->query($query); $res->free(); } var_dump(mysqlnd_qc_get_core_stats()); ?> ]]> &examples.outputs.similar; string(1) "2" ["cache_miss"]=> string(1) "2" ["cache_put"]=> string(1) "2" ["query_should_cache"]=> string(1) "4" ["query_should_not_cache"]=> string(1) "3" ["query_not_cached"]=> string(1) "3" ["query_could_cache"]=> string(1) "4" ["query_found_in_cache"]=> string(1) "2" ["query_uncached_other"]=> string(1) "0" ["query_uncached_no_table"]=> string(1) "0" ["query_uncached_no_result"]=> string(1) "0" ["query_uncached_use_result"]=> string(1) "0" ["query_aggr_run_time_cache_hit"]=> string(2) "28" ["query_aggr_run_time_cache_put"]=> string(3) "900" ["query_aggr_run_time_total"]=> string(3) "928" ["query_aggr_store_time_cache_hit"]=> string(2) "14" ["query_aggr_store_time_cache_put"]=> string(2) "40" ["query_aggr_store_time_total"]=> string(2) "54" ["receive_bytes_recorded"]=> string(3) "136" ["receive_bytes_replayed"]=> string(3) "136" ["send_bytes_recorded"]=> string(2) "84" ["send_bytes_replayed"]=> string(2) "84" ["slam_stale_refresh"]=> string(1) "0" ["slam_stale_hit"]=> string(1) "0" ["request_counter"]=> int(1) ["process_hash"]=> int(1929695233) } ]]> For a quick overview, call mysqlnd_qc_get_core_stats. It delivers cache usage, cache timing and traffic related statistics. Values are aggregated on a per process basis for all queries issued by any PHP MySQL API call. Some storage handler, such as the default handler, can report cache entries, statistics related to the entries and meta data for the underlying query through the mysqlnd_qc_get_cache_info function. Please note, that the information returned depends on the storage handler. Values are aggregated on a per process basis. Example <function>mysqlnd_qc_get_cache_info</function> usage query("DROP TABLE IF EXISTS test"); $mysqli->query("CREATE TABLE test(id INT)"); $mysqli->query("INSERT INTO test(id) VALUES (1), (2), (3)"); /* dummy queries to fill the query trace */ for ($i = 1; $i <= 4; $i++) { $query = sprintf("/*%s*/SELECT id FROM test WHERE id = %d", MYSQLND_QC_ENABLE_SWITCH, $i % 2); $res = $mysqli->query($query); $res->free(); } var_dump(mysqlnd_qc_get_cache_info()); ?> ]]> &examples.outputs.similar; int(2) ["handler"]=> string(7) "default" ["handler_version"]=> string(5) "1.0.0" ["data"]=> array(2) { ["Localhost via UNIX socket 3306 root test|/*qc=on*/SELECT id FROM test WHERE id = 1"]=> array(2) { ["statistics"]=> array(11) { ["rows"]=> int(1) ["stored_size"]=> int(71) ["cache_hits"]=> int(1) ["run_time"]=> int(391) ["store_time"]=> int(27) ["min_run_time"]=> int(16) ["max_run_time"]=> int(16) ["min_store_time"]=> int(8) ["max_store_time"]=> int(8) ["avg_run_time"]=> int(8) ["avg_store_time"]=> int(4) } ["metadata"]=> array(1) { [0]=> array(8) { ["name"]=> string(2) "id" ["orig_name"]=> string(2) "id" ["table"]=> string(4) "test" ["orig_table"]=> string(4) "test" ["db"]=> string(4) "test" ["max_length"]=> int(1) ["length"]=> int(11) ["type"]=> int(3) } } } ["Localhost via UNIX socket 3306 root test|/*qc=on*/SELECT id FROM test WHERE id = 0"]=> array(2) { ["statistics"]=> array(11) { ["rows"]=> int(0) ["stored_size"]=> int(65) ["cache_hits"]=> int(1) ["run_time"]=> int(299) ["store_time"]=> int(13) ["min_run_time"]=> int(11) ["max_run_time"]=> int(11) ["min_store_time"]=> int(6) ["max_store_time"]=> int(6) ["avg_run_time"]=> int(5) ["avg_store_time"]=> int(3) } ["metadata"]=> array(1) { [0]=> array(8) { ["name"]=> string(2) "id" ["orig_name"]=> string(2) "id" ["table"]=> string(4) "test" ["orig_table"]=> string(4) "test" ["db"]=> string(4) "test" ["max_length"]=> int(0) ["length"]=> int(11) ["type"]=> int(3) } } } } } ]]> It is possible to further break down the granularity of statistics to the level of the normalized statement string. The normalized statement string is the statements string with all parameters replaced with question marks. For example, the two statements SELECT id FROM test WHERE id = 0 and SELECT id FROM test WHERE id = 1 are normalized into SELECT id FROM test WHERE id = ?. Their both statistics are aggregated into one entry for SELECT id FROM test WHERE id = ?. Example <function>mysqlnd_qc_get_normalized_query_trace_log</function> usage query("DROP TABLE IF EXISTS test"); $mysqli->query("CREATE TABLE test(id INT)"); $mysqli->query("INSERT INTO test(id) VALUES (1), (2), (3)"); /* dummy queries to fill the query trace */ for ($i = 1; $i <= 4; $i++) { $query = sprintf("/*%s*/SELECT id FROM test WHERE id = %d", MYSQLND_QC_ENABLE_SWITCH, $i % 2); $res = $mysqli->query($query); $res->free(); } var_dump(mysqlnd_qc_get_normalized_query_trace_log()); ?> ]]> &examples.outputs.similar; array(9) { ["query"]=> string(25) "DROP TABLE IF EXISTS test" ["occurences"]=> int(0) ["eligible_for_caching"]=> bool(false) ["avg_run_time"]=> int(0) ["min_run_time"]=> int(0) ["max_run_time"]=> int(0) ["avg_store_time"]=> int(0) ["min_store_time"]=> int(0) ["max_store_time"]=> int(0) } [1]=> array(9) { ["query"]=> string(27) "CREATE TABLE test (id INT )" ["occurences"]=> int(0) ["eligible_for_caching"]=> bool(false) ["avg_run_time"]=> int(0) ["min_run_time"]=> int(0) ["max_run_time"]=> int(0) ["avg_store_time"]=> int(0) ["min_store_time"]=> int(0) ["max_store_time"]=> int(0) } [2]=> array(9) { ["query"]=> string(46) "INSERT INTO test (id ) VALUES (? ), (? ), (? )" ["occurences"]=> int(0) ["eligible_for_caching"]=> bool(false) ["avg_run_time"]=> int(0) ["min_run_time"]=> int(0) ["max_run_time"]=> int(0) ["avg_store_time"]=> int(0) ["min_store_time"]=> int(0) ["max_store_time"]=> int(0) } [3]=> array(9) { ["query"]=> string(31) "SELECT id FROM test WHERE id =?" ["occurences"]=> int(4) ["eligible_for_caching"]=> bool(true) ["avg_run_time"]=> int(179) ["min_run_time"]=> int(11) ["max_run_time"]=> int(393) ["avg_store_time"]=> int(12) ["min_store_time"]=> int(7) ["max_store_time"]=> int(25) } } ]]> The source distribution of PECL/mysqlnd_qc contains a directory web/ in which web based monitoring scripts can be found which give an example how to write a cache monitor. Please, follow the instructions given in the source. Since PECL/mysqlnd_qc 1.1.0 it is possible to write statistics into a log file. Please, see mysqlnd_qc.collect_statistics_log_file.
Beyond TTL: user-defined storage The query cache plugin supports the use of user-defined storage handler. User-defined storage handler can use arbitrarily complex invalidation algorithms and support arbitrary storage media. All user-defined storage handlers have to provide a certain interface. The functions of the user-defined storage handler will be called by the core of the cache plugin. The necessary interface consists of seven public functions. Both procedural and object oriented user-defined storage handler must implement the same set of functions. Using a user-defined storage handler $data, "row_count" => $row_count, "valid_until" => time() + $ttl, "hits" => 0, "run_time" => $run_time, "store_time" => $store_time, "cached_run_times" => array(), "cached_store_times" => array(), ); return TRUE; } function query_is_select($query) { printf("\t%s('%s'): ", __FUNCTION__, $query); $ret = FALSE; if (stristr($query, "SELECT") !== FALSE) { /* cache for 5 seconds */ $ret = 5; } printf("%s\n", (FALSE === $ret) ? "FALSE" : $ret); return $ret; } function update_query_run_time_stats($key, $run_time, $store_time) { global $__cache; printf("\t%s(%d)\n", __FUNCTION__, func_num_args()); if (isset($__cache[$key])) { $__cache[$key]['hits']++; $__cache[$key]["cached_run_times"][] = $run_time; $__cache[$key]["cached_store_times"][] = $store_time; } } function get_stats($key = NULL) { global $__cache; printf("\t%s(%d)\n", __FUNCTION__, func_num_args()); if ($key && isset($__cache[$key])) { $stats = $__cache[$key]; } else { $stats = array(); foreach ($__cache as $key => $details) { $stats[$key] = array( 'hits' => $details['hits'], 'bytes' => strlen($details['data']), 'uncached_run_time' => $details['run_time'], 'cached_run_time' => (count($details['cached_run_times'])) ? array_sum($details['cached_run_times']) / count($details['cached_run_times']) : 0, ); } } return $stats; } function clear_cache() { global $__cache; printf("\t%s(%d)\n", __FUNCTION__, func_num_args()); $__cache = array(); return TRUE; } /* Install procedural user-defined storage handler */ if (!mysqlnd_qc_set_user_handlers("get_hash", "find_query_in_cache", "return_to_cache", "add_query_to_cache_if_not_exists", "query_is_select", "update_query_run_time_stats", "get_stats", "clear_cache")) { printf("Failed to install user-defined storage handler\n"); } /* Connect, create and populate test table */ $mysqli = new mysqli("host", "user", "password", "schema", "port", "socket"); $mysqli->query("DROP TABLE IF EXISTS test"); $mysqli->query("CREATE TABLE test(id INT)"); $mysqli->query("INSERT INTO test(id) VALUES (1), (2)"); printf("\nCache put/cache miss\n"); $res = $mysqli->query("SELECT id FROM test WHERE id = 1"); var_dump($res->fetch_assoc()); $res->free(); /* Delete record to verify we get our data from the cache */ $mysqli->query("DELETE FROM test WHERE id = 1"); printf("\nCache hit\n"); $res = $mysqli->query("SELECT id FROM test WHERE id = 1"); var_dump($res->fetch_assoc()); $res->free(); printf("\nDisplay cache statistics\n"); var_dump(mysqlnd_qc_get_cache_info()); printf("\nFlushing cache, cache put/cache miss"); var_dump(mysqlnd_qc_clear_cache()); $res = $mysqli->query("SELECT id FROM test WHERE id = 1"); var_dump($res->fetch_assoc()); $res->free(); ?> ]]> &examples.outputs.similar; string(1) "1" } query_is_select('DELETE FROM test WHERE id = 1'): FALSE Cache hit query_is_select('SELECT id FROM test WHERE id = 1'): 5 get_hash(5) find_query_in_cache(1) return_to_cache(1) update_query_run_time_stats(3) array(1) { ["id"]=> string(1) "1" } Display cache statistics get_stats(0) array(4) { ["num_entries"]=> int(1) ["handler"]=> string(4) "user" ["handler_version"]=> string(5) "1.0.0" ["data"]=> array(1) { ["18683c177dc89bb352b29965d112fdaa"]=> array(4) { ["hits"]=> int(1) ["bytes"]=> int(71) ["uncached_run_time"]=> int(398) ["cached_run_time"]=> int(4) } } } Flushing cache, cache put/cache miss clear_cache(0) bool(true) query_is_select('SELECT id FROM test WHERE id = 1'): 5 get_hash(5) find_query_in_cache(1) add_query_to_cache_if_not_exists(6) NULL ]]>