Towards Data Science just published a long walkthrough by Nikhil Dasari that takes linear regression from closed-form math all the way to stochastic gradient descent. The concrete claim in it: batch gradient descent has to read every row before it can make one parameter update, while stochastic gradient descent updates after a single row and still finds the minimum. My first reaction was that I make this exact trade on WordPress sites all the time, just without the calculus.
The math, compressed
Linear regression has an exact answer. The normal equation, β = (X^T X)^-1 X^T y, gives you every coefficient in one shot, and Dasari works it out on a ten-row salary dataset where it costs nothing. The problem is the inverse. It is the one expensive operation in the formula, and it gets worse fast as rows and features grow, which is why the article moves on to iterative methods at all.
Gradient descent keeps the derivative from that derivation and, instead of setting it to zero, nudges the parameters against the slope: β := β – α · ∇MSE. No inverse anywhere. α is the learning rate, and the article’s own numbers are a useful warning: on the raw salary data, where the target values sit roughly between 38,000 and 64,000, an α of 0.01 moves the intercept from 2 to about 989 in a single step. Small α crawls and needs thousands of iterations, large α overshoots and may never settle. The scikit-learn docs on SGD make the same point from the other direction: with the default learning rate schedule your data should have roughly zero mean and unit variance, and they recommend SGD-based regressors mainly for problems with more than about 10,000 samples.
Batch gradient descent computes that gradient over the whole dataset for each update. Stochastic gradient descent picks one random row, updates immediately, picks again. The path to the minimum gets noisy and zig-zag, but each step is nearly free, and with store-sized data that is what you want. Mini-batch, 32 or 64 rows per update in the article’s example, sits between the two, and it is what everyone ships.
Where stochastic gradient descent shows up in WordPress
Reading that, I did not think about neural networks. I thought about Action Scheduler, the job queue WooCommerce ships with. Its readme on GitHub reports live sites with queues above 50,000 pending actions, worked at a sustained rate over 10,000 per hour, and every one of those jobs is one item pulled, processed, marked done. Nobody would try to reprice or resync 50,000 orders inside a single PHP request, for the same reason nobody runs batch gradient descent over a million rows: the single full pass is the expensive part, and it has to fit inside your limits.
The vocabulary maps almost one to one, and if you have read my notes on splitting big WooCommerce background jobs you have seen the pattern already. A full-table loop inside one request is batch gradient descent. One scheduled action per row is pure stochastic: noisy, lots of overhead per unit of work, but it never holds a big lock or a big memory footprint. A batch of N rows per queue run is mini-batch, and that is the default instinct for store work, the same shape as the batch processing loop WordPress already ships.
The batch size plays the role of the learning rate. Too large and you hit PHP max_execution_time or a MySQL timeout partway through, which for a non-idempotent job means half-applied state. Too small and the queue drains slower than work arrives, so the backlog never goes away. WP-Cron adds its own stochasticity on top: the plugin handbook is blunt that WP-Cron runs on page load, that a task scheduled for 2 PM may run at 5 PM, and that the only guarantee is it will run eventually. Incremental progress with eventual execution fits that model well. Anything that needs exact timing or a final state verified in one pass fits it badly. The one place I would not stretch the analogy is cache warming, where you want the full key list up front.
How I’d run it on a store
The pattern I use is a self-rescheduling Action Scheduler action with its progress in an option. Each run takes a fixed number of rows, does the work, moves the offset, and schedules the next run only if there is more to do.
add_action( 'bbioon_rebuild_order_stats', 'bbioon_rebuild_order_stats_batch' );
function bbioon_rebuild_order_stats_batch() {
$batch = 50;
$offset = (int) get_option( 'bbioon_rebuild_offset', 0 );
$orders = wc_get_orders(
array(
'limit' => $batch,
'offset' => $offset,
'status' => array( 'wc-completed', 'wc-processing' ),
'orderby' => 'ID',
'order' => 'ASC',
)
);
foreach ( $orders as $order ) {
// One order at a time, no order depends on another.
}
if ( count( $orders ) === $batch ) {
update_option( 'bbioon_rebuild_offset', $offset + $batch, false );
as_schedule_single_action( time() + 60, 'bbioon_rebuild_order_stats' );
} else {
delete_option( 'bbioon_rebuild_offset' );
}
}
The stop condition is the batch coming back short, which is the honest stopping rule when you do not know the row count up front. State lives in one option and gets deleted at the end, so a rerun starts clean. Every row is processed independently, so a failed run repeats work but does not corrupt it. If your job must be atomic across rows, this is the wrong pattern; run it in a maintenance window or wrap it in a transaction somewhere that is not PHP-FPM.
Two checks before you trust it. In admin, Tools → Scheduled Actions should show the hook go pending, complete, pending again until it stops appearing. If WP-Cron is flaky on the host, drive the queue from WP-CLI with wp action-scheduler run, which is what WooCommerce’s own testing notes suggest, and point a real system cron at wp-cron.php on a fixed schedule instead of relying on traffic.
If you have a store where a job like this has outgrown a single request, moving heavy per-order loops into batches that fit the host’s limits is the kind of work I do. Happy to look at the specific job and pick the batch size with you.
On any client site this week, I’d grep for a foreach over wc_get_orders with no limit and treat each hit as a job waiting to become a queue. Then I’d pick a batch size that finishes comfortably under the host timeout, add the offset option, and let the queue drain.