Before
Everybody says the shop is slow; nobody knows which query, or which file.
After
One recording session, then one sentence: this statement runs this many times per load, and here is what asked for it.
"The shop is slow" is not a bug report. You start a session, load the pages that feel slow, and the plugin tells you which statement ran how many times and which file asked for it. Outside a debugging session it registers no hooks on the request path and adds no queries at all, and the session expires on its own. The pro tier adds seven ready-made indexes — each with its reason and its cost — an order count cache, and slow query alerts.
A store that has been trading for a few years gets slow, and the reason is almost always the database: postmeta has grown, options is full of autoloaded rows that are read on every single request, and somewhere a loop is running one query per product. Until somebody looks, none of that has a name or an address.
Tools that answer this exist, and they share one flaw: they have to be running to see anything, and running them costs memory on every query. So they carry a warning about production, get switched on in the middle of an incident, and get left on afterwards.
This plugin is built the other way round. WordPress only keeps queries when the SAVEQUERIES constant is defined, and wpdb reads that constant on every query — which means it has to be defined before WordPress runs its first one, long before anyone can ask who is logged in. The answer here is a two-stage gate. At plugin load: if the session cookie carries a valid token, SAVEQUERIES is defined and queries collect in memory, nothing more. At shutdown, when WordPress finally knows who the user is: the manage_options capability, the session owner and the token are all re-checked, and if any of them fails the buffer is discarded and not a single row is written. Outside a session the plugin registers no hooks on the request path and adds no queries at all.
The output is an interpretation, not a list. Four hundred rows of SQL is data. A finding is this: "this statement runs 312 times per page load, from class-wc-product.php", together with concrete WooCommerce advice — prime the cache once with update_meta_cache() instead of reading each product's meta separately. Every finding carries a severity, so you know which one to take first and which one can wait.
Because this plugin works on a store's own database, privacy is part of the design rather than a closing note: statements touching users, usermeta, options and the WooCommerce order, session, API-key and payment-token tables are never stored verbatim. The normalised shape — which contains no real values at all — is kept as usual, the "store a sample statement" option is off by default, and the sample never appears in a REST payload because no screen displays it.
Before
Everybody says the shop is slow; nobody knows which query, or which file.
After
One recording session, then one sentence: this statement runs this many times per load, and here is what asked for it.
Before
A profiler has to stay on to see anything, and staying on costs memory on every query.
After
Outside a session no hooks are registered on the request path and no queries are added. The session expires by itself.
Before
The output is four hundred rows of SQL and the interpretation is your problem.
After
Queries of the same shape collapse into one row with a count, and the Findings tab says which row to fix first.
On the Record tab, pick a duration — thirty minutes by default, two hundred and forty at most — and write a label, such as "slow product page".
In another tab, open the pages that actually feel slow. While a session is open, an indicator sits in the admin bar so nobody can leave it running unnoticed.
Come back to this tab. Every finding has a severity, a sentence in plain language and a WooCommerce-specific piece of advice. The session ends on its own if you forget.
WooCommerce stores with a few years of data · stores with thousands of products or orders · stores whose admin has become slow · teams that lost something in the migration to HPOS · developers who have to name the plugin responsible · agencies maintaining a client's store · hosts who need to show the problem is not the server · anyone who has to turn "the shop is slow" into a bug report.
Everything sits inside the plugin itself. No cloud service, no Composer, and not one extra query while recording is off.
Nothing is recorded until you start a session. Outside one, the plugin registers no hooks on the request path and adds no queries. The session token is an HttpOnly cookie with an absolute expiry, and it never appears in a REST payload.
Instead of four hundred rows, a sentence: "this statement runs 312 times per page load, from class-wc-product.php" — with a severity and WooCommerce-specific advice for meta, order and term patterns.
When one shape runs many times in a single page load, that is the loop fetching one row at a time. The plugin pulls the responsible file out of the call stack and skips WordPress's own frames. The repeat threshold defaults to five and is configurable.
Literals are replaced with ? so that queries differing only by an id become one row with a count. Quoted strings are replaced before bare numbers, and numbers inside identifiers are left alone, so wp_woocommerce_order_items survives intact and an IN list of any length collapses to one shape.
Table sizes from information_schema — no COUNT(*), which is itself slow on a large table — the weight of autoloaded options with a warning threshold, and counts of orphaned rows: post meta, term meta, term relationships, order items, order item meta and expired sessions. It counts; it never deletes your store's data.
The inspector and the indexer both check whether the store keeps orders in wc_orders or still in posts, and look at the right thing accordingly.
Seven ready-made indexes for the WordPress and WooCommerce tables — including the meta_key/meta_value pair on postmeta and order item meta, and the status-and-date pair on the HPOS orders table. Each one explains why it exists and roughly what it costs before you add it, and warns when the table is large.
Every index created is recorded, and the remover refuses to touch an index it did not create. The ownership claim is written before ALTER TABLE runs, so if PHP times out halfway through a large table the index is not left unclaimed.
WooCommerce recalculates order counts by status on nearly every admin page load. This module caches them and invalidates with a generation counter rather than by deleting keys — and refreshes the moment an order changes.
An hourly check that emails you, or posts to a webhook, when slow queries start climbing on a store nobody is currently watching — with a cooldown, so one slow week does not turn into a hundred identical messages.
Statements touching users, usermeta, options and the WooCommerce order, session, API-key and payment-token tables are only ever kept in their normalised form, whatever the settings say. "Store a sample statement" is off by default, and the hd_wcdbp_never_sample_tables filter lets you add a sensitive table of your own.
Webhook bodies are signed with HMAC-SHA256 when a secret is set. Loopback, link-local, private-range and cloud metadata addresses are refused: host names are resolved before the decision, and the connection is pinned to the address that was checked. The secret is stripped from every REST payload and rendered as a write-only field.
All of them in docs/hooks.md, each with an example. Among them hd_wcdbp_snapshot to discard a recording, hd_wcdbp_never_sample_tables to add your own sensitive table, hd_wcdbp_index_recipes to define an index of your own, and hd_wcdbp_is_licensed for a staging clone that must not phone home.
A complete fa_IR translation ships with the plugin, and the admin loads admin-rtl.css when is_rtl(). SQL and file paths are marked dir="ltr" so they stay readable in a right-to-left admin. No jQuery — vanilla ES6, loaded on this plugin's own screen only.
| Capability | Detail | Included |
|---|---|---|
| Session-scoped recording | A 64-character token in an HttpOnly cookie with an absolute expiry | |
| Two-stage gate around SAVEQUERIES | The cookie only permits collection in memory; the capability is checked at shutdown | |
| No hooks on the request path | Outside a session nothing is registered and no query is added | |
| Query shape fingerprinting | Literals replaced with ?; table and column names left intact | |
| IN lists collapse | An IN list of any length becomes a single shape | |
| N+1 detection | With the responsible file, from the call stack, framework frames skipped | |
| Configurable slow threshold | 0.05 seconds by default, via hd_wcdbp_slow_threshold | |
| Configurable repeat threshold | Five repeats in a single page load by default | |
| Findings with a severity | High, medium or low, each with a plain-language sentence | |
| WooCommerce-specific advice | For instance update_meta_cache() for the postmeta pattern | |
| The recordings list | Newest first, slowest first, or everything from one context | |
| Request context recorded | frontend, admin, ajax, rest, cron or cli | |
| Retention window and a hard ceiling | A session on a busy page fills up quickly | |
| Cap on distinct shapes per request | A page with more than two hundred has a bigger problem | |
| Table sizes from information_schema | No COUNT(*) on a large table | |
| Autoloaded option weight | With a warning threshold and the twenty largest listed | |
| Orphaned row counts | Post meta, term meta, term relationships, order items, order item meta and expired sessions | |
| HPOS awareness | In the inspector and in the indexer | |
| The inspector scan is cached behind a lock | Two admins at once, one scan | |
| Seven ready-made indexes | postmeta, autoload, order item meta, item type, HPOS status and date, HPOS meta, session expiry | |
| Reason and cost before creation | Plus a warning when the table is large | |
| Ownership claimed before ALTER TABLE | A PHP timeout cannot leave an unclaimed index | |
| Rollback limited to the plugin's own indexes | An index you or another plugin added is never touched | |
| Recipe fields re-validated | A strict pattern before any ALTER TABLE | |
| Order status count cache | The counts WooCommerce recomputes on nearly every admin page | |
| Invalidation by generation counter | Rather than by deleting cache keys | |
| Slow query alerts | E-mail or webhook, on an hourly check | |
| HMAC-SHA256 webhook signature | When a secret is configured | |
| Alert cooldown | One slow week does not become a hundred identical messages | |
| Webhook address guard | Loopback, private range and cloud metadata refused, pinned to the address that was checked | |
| Sensitive tables never sampled | users, usermeta, options and the WooCommerce order, session, API-key and payment-token tables | |
| Recorded URLs cleaned | password, pwd, key, token and _wpnonce stripped from the query string | |
| Uninstall drops the indexes | uninstall.php always removes the indexes this plugin created |
| Item | Minimum |
|---|---|
| WordPress | 6.2 or newer, tested to 6.8 |
| PHP | 8.0 to 8.3 |
| WooCommerce | Required — the findings and the index recipes are written for WooCommerce's tables |
| Database | MySQL or MariaDB; the inspector reads table sizes from information_schema |
| Capability | manage_options to start a session and read the results, changeable through hd_wcdbp_manage_capability |
A store that has been trading for a few years, has become slow, and nobody knows exactly why. If you have thousands of products and orders and the admin or the product page has slowed down, this plugin gives the problem a name and an address. For a brand-new store with a hundred products it has little to say — the problem there is usually not the database. Its second audience is the developer or agency that has to show a client which plugin or which theme is responsible, with a file name rather than a guess.
That is exactly what it was built for. Outside a debugging session the plugin registers no hooks on the request path and adds no queries. During a session the overhead is the same as WordPress's own SAVEQUERIES, and it stops when the clock runs out. Holding the session cookie does not let anyone read the results either; that still requires manage_options. The one thing that genuinely changes your database is the pro indexer, which never runs on its own: you press a button, after a confirmation, and you can roll it back.
With recording off, the plugin registers no hooks on ordinary requests; the cookie check returns before any query and never touches the database. Admin CSS and JS load on this plugin's own screen only. During a session, query recording costs memory on every query — the same thing SAVEQUERIES does — and snapshot analysis happens at shutdown, never while the page is being built. The exact figure depends on your store, and we are not going to invent a number we have not measured on your site.
Both halves, both active. Diagnosis: session-scoped recording, grouping by query shape, N+1 detection with the file named, the Findings view and the database inspector. Fixing: the smart indexer with seven ready-made indexes and safe rollback, the order count cache, and slow query alerts by e-mail or signed webhook. So you learn where the problem is and you have the tools to fix it.
It can, and we are not hiding that. Depending on your MySQL or MariaDB version and the size of the table, an ALTER TABLE can take a while, and writes to that table can be slowed or blocked while it runs. That is why the plugin shows the table size and an estimated cost for each index before you create it, warns when the table is large, and never starts on its own. The advice is simple: a quiet hour, with a fresh backup. If PHP times out halfway through, the index is not left unclaimed, because the ownership claim is written before the statement runs.
Both. The plugin checks whether the store keeps orders in wc_orders or still in posts, and counts and recommends accordingly. The recipes cover both cases: the meta_key/meta_value pair on postmeta and order item meta for the legacy layout, and the status-and-date pair plus HPOS meta for the new one.
They are dropped. uninstall.php always removes the indexes this plugin created, whether or not the "delete data" option is on. Leaving schema changes behind after the plugin that made them is gone would be indefensible — later, nobody would know what that index was or whether it was safe to remove. An index you added yourself, or that another plugin added, is never touched.
The normalised query shape contains no real values, so it is always safe. What can contain real values is the sample — one example of the statement exactly as it ran. Statements touching users, usermeta, options and the WooCommerce order, session, API-key and payment-token tables are never stored with a sample, whatever the settings say. The sample option itself is off by default, the sample never appears in a REST payload because no screen displays it, and every recorded URL has password, pwd, key, token and _wpnonce stripped before it is stored.
Because no plugin can. WordPress runs queries during bootstrap, before any plugin file is included, and SAVEQUERIES has to be defined before those run to capture them. The plugin says so on screen rather than presenting a partial list as a complete one. If you need those too, you have to define SAVEQUERIES in wp-config.php — which is not the right thing to do on a live store.
The plugin leaves your decision alone and simply uses it. In exchange it tells you plainly, on screen, that in this mode every request on the site is being recorded and not just your session — which is usually not what a live store wants.
No. The plugin ships its own small autoloader and has no runtime dependencies, and no data is sent to any cloud service. The only outbound call is the pro tier's licence check and update against hatamidev.com; the updater only accepts a package URL on hatamidev.com over https, unless you add a host yourself through hd_wcdbp_update_hosts. For a staging clone that must not phone home, there is the hd_wcdbp_is_licensed filter.
You decide. There is a delete-data option in the settings and it is off by default; switch it on and deleting the plugin removes both recording tables and every setting. The indexes are the exception and are always dropped. Deactivating clears scheduled events and ends any running session, but never touches stored data.
Plugin bug fixes, installation and setup guidance, and help reading the findings and deciding about indexes. Optimising the store itself, rewriting the code of another plugin that turns out to be responsible, and problems caused by other themes or plugins fall outside support and are agreed separately.