The Problem
A client flipped on High-Performance Order Storage (HPOS) on a WooCommerce 10.1 store over the weekend. By Monday morning the Analytics dashboard had gone completely flat. Net sales, gross sales, orders, refunds — all zero. The actual orders were fine in the order list. Stripe payouts were arriving normally. But every chart in WooCommerce → Analytics → Overview was a dead line at the bottom of the y-axis.
The Site Health screen and the WooCommerce status page both reported HPOS as enabled and "Compatibility Mode" turned off. The shop manager was halfway through writing me a frantic email when I logged in.
The data was not gone. The analytics tables that the Reports UI reads from had just stopped getting populated.
Why It Happens
WooCommerce Analytics does not query wp_wc_orders or wp_posts directly. It reads from a set of denormalised reporting tables: wp_wc_order_stats, wp_wc_order_product_lookup, wp_wc_order_tax_lookup, wp_wc_order_coupon_lookup, and a few more. Those tables are rebuilt incrementally by a background action every time an order's status changes.
The trigger that updates them lives in Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore and fires on woocommerce_after_order_object_save. When you flip from the legacy CPT storage to HPOS and disable compatibility mode in one move, two things happen at once. New orders save through the new data store, so the hook fires correctly for anything created after the switch. But the existing orders never re-trigger the lookup rebuild, and any order edited during the brief window where the storage backend was mid-toggle gets written to the new table without an accompanying lookup row.
The Analytics screen filters by date range. If your default view is "Month to date" and the switch happened on the 1st, every row in the visible range is missing from wp_wc_order_stats. The query returns zero. The dashboard shows zero. There is no error in the logs because, from the database's point of view, the query ran perfectly and found nothing.
The HPOS migration documentation mentions running the analytics rebuild after a migration, but it is buried under a heading most teams scroll past once they see "migration complete".
The Fix
The reporting tables are rebuildable from the source-of-truth order rows. There are three ways to trigger the rebuild — pick the one that matches the size of your store.
Option 1 — WP-CLI for any store over a few hundred orders. This is the only option I trust on production:
wp wc cot sync --batch-size=250
wp wc analytics update --batch-size=500
The first command makes sure the orders table itself is fully populated and any in-flight sync is finalised. The second walks every order in wp_wc_orders and rebuilds the corresponding rows in the analytics lookup tables. --batch-size controls how many orders are processed per database transaction. On a managed host with a tight max_execution_time, start at 250 and raise it if the command completes in under a minute.
Watch the output. Each batch logs the number of rows processed and a running total. On a store with 80,000 orders the full rebuild took 22 minutes on a 4 vCPU container. Most of that was the product lookup table, which is the wide one.
Option 2 — Admin UI for smaller stores. Go to WooCommerce → Status → Tools and run "Rebuild customers and order stats". This dispatches the same work through Action Scheduler. It is fine for a few thousand orders. Above that, the scheduler will time out a batch, retry, and you end up with partial rebuilds you have to chase.
Option 3 — Re-save through the data store, programmatically. Only useful if you want to selectively rebuild a date range, for example a refund batch that imported wrong:
$orders = wc_get_orders([
'date_created' => '2026-05-01...2026-05-23',
'limit' => -1,
'return' => 'ids',
]);
foreach ( $orders as $order_id ) {
$order = wc_get_order( $order_id );
if ( $order ) {
$order->save(); // Triggers woocommerce_after_order_object_save.
}
}
Run this from WP-CLI with wp eval-file rebuild.php, never from a browser request. The $order->save() call re-fires the analytics update hook for each order, which is exactly what you want.
Verify the rebuild worked. After the CLI command finishes, the row counts in the lookup tables should match the order count in wp_wc_orders:
SELECT COUNT(*) FROM wp_wc_orders WHERE status IN ('wc-completed', 'wc-processing', 'wc-on-hold');
SELECT COUNT(*) FROM wp_wc_order_stats;
The numbers should be within a handful of each other. Cancelled and failed orders are excluded from wp_wc_order_stats on purpose. If wp_wc_order_stats is still empty after the rebuild, the most common cause is a custom plugin that hooks woocommerce_analytics_update_order_stats and short-circuits the row insert. Disable third-party plugins one at a time and re-run the rebuild on a single order to isolate it.
Stop it happening on the next migration. The safe migration sequence is: enable HPOS with compatibility mode on, wait for the sync action to finish (check wp action-scheduler list --status=pending --group=wc_admin), confirm the lookup tables match, then disable compatibility mode. Doing all three steps in the same admin session is what produces the empty dashboard.
The Lesson
WooCommerce Analytics reads from denormalised lookup tables that are only populated on save. An HPOS toggle does not re-trigger that save for existing orders, so the reporting tables go stale immediately. wp wc analytics update rebuilds them from the orders table. Run it after every HPOS migration, and verify the row counts match before you tell the client the dashboard is fixed.
If your HPOS migration has left analytics broken or orders missing, that is the kind of cleanup I do every week. See my services. For a related HPOS gotcha I wrote up yesterday, read WooCommerce HPOS bulk status update fail.
Stuck on a WooCommerce upgrade or HPOS migration? Get it shipped.