Commit 8360c2aef8 for woocommerce

commit 8360c2aef8d7ed590e4f154bfb491df7860e924d
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date:   Wed Feb 18 08:24:29 2026 +0100

    [Performance] Orders: reduce the number of SQL queries required to persist a draft order during checkout (#63258)

    Optimize purging customers' usermeta in `wc_downloadable_product_permissions`, which is called multiple times during order save. The optimization leverages reads as guard conditions, as reads are performed from the in-memory cache until deletions are necessary. Previously unguarded deletions were causing cache invalidation and re-read from DB.

diff --git a/plugins/woocommerce/changelog/performance-63118-reduce-sqls-number-iteration-2 b/plugins/woocommerce/changelog/performance-63118-reduce-sqls-number-iteration-2
new file mode 100644
index 0000000000..63cbfbf953
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-63118-reduce-sqls-number-iteration-2
@@ -0,0 +1,4 @@
+Significance: minor
+Type: performance
+
+Performance: reduced the number of SQL queries required to persist a draft order during checkout.
diff --git a/plugins/woocommerce/includes/class-wc-order.php b/plugins/woocommerce/includes/class-wc-order.php
index b433eac37c..8ff2252897 100644
--- a/plugins/woocommerce/includes/class-wc-order.php
+++ b/plugins/woocommerce/includes/class-wc-order.php
@@ -1875,8 +1875,9 @@ class WC_Order extends WC_Abstract_Order {
 		if ( false === $needs_processing ) {
 			$needs_processing = 0;

-			if ( count( $this->get_items() ) > 0 ) {
-				foreach ( $this->get_items() as $item ) {
+			$line_items = $this->get_items();
+			if ( count( $line_items ) > 0 ) {
+				foreach ( $line_items as $item ) {
 					if ( $item->is_type( 'line_item' ) ) {
 						$product = $item->get_product();

diff --git a/plugins/woocommerce/includes/class-wc-shipping.php b/plugins/woocommerce/includes/class-wc-shipping.php
index 91138393e1..62b28419ff 100644
--- a/plugins/woocommerce/includes/class-wc-shipping.php
+++ b/plugins/woocommerce/includes/class-wc-shipping.php
@@ -139,6 +139,9 @@ class WC_Shipping {
 		// For backwards compatibility with 2.5.x we load any ENABLED legacy shipping methods here.
 		$maybe_load_legacy_methods = array( 'flat_rate', 'free_shipping', 'international_delivery', 'local_delivery', 'local_pickup' );

+		// Prime the settings options: reduces the number of executed SQLs.
+		wp_prime_option_caches( array_map( fn( string $method ) => sprintf( 'woocommerce_%s_settings', $method ), $maybe_load_legacy_methods ) );
+
 		foreach ( $maybe_load_legacy_methods as $method ) {
 			$options = get_option( 'woocommerce_' . $method . '_settings' );
 			if ( $options && isset( $options['enabled'] ) && 'yes' === $options['enabled'] ) {
diff --git a/plugins/woocommerce/includes/wc-order-functions.php b/plugins/woocommerce/includes/wc-order-functions.php
index e7049ff6f9..635805097a 100644
--- a/plugins/woocommerce/includes/wc-order-functions.php
+++ b/plugins/woocommerce/includes/wc-order-functions.php
@@ -99,7 +99,7 @@ function wc_get_order( $the_order = false ) {
  *
  * @since 2.2
  * @used-by WC_Order::set_status
- * @return array
+ * @return array<string,string>
  */
 function wc_get_order_statuses() {
 	$order_statuses = array(
@@ -473,8 +473,9 @@ function wc_downloadable_product_permissions( $order_id, $force = false ) {
 		return;
 	}

-	if ( count( $order->get_items() ) > 0 ) {
-		foreach ( $order->get_items() as $item ) {
+	$line_items = $order->get_items();
+	if ( count( $line_items ) > 0 ) {
+		foreach ( $line_items as $item ) {
 			$product = $item->get_product();

 			if ( $product && $product->exists() && $product->is_downloadable() ) {
@@ -499,18 +500,32 @@ add_action( 'woocommerce_order_status_processing', 'wc_downloadable_product_perm
  * @param int|WC_Order $order Order instance or ID.
  */
 function wc_delete_shop_order_transients( $order = 0 ) {
-	if ( is_numeric( $order ) ) {
+	if ( $order && is_numeric( $order ) ) {
 		$order = wc_get_order( $order );
 	}

 	// Clear customer's order related caches.
 	$order_id = 0;
-	if ( is_a( $order, 'WC_Order' ) ) {
-		$order_id    = $order->get_id();
+	if ( $order && is_a( $order, 'WC_Order' ) ) {
+		$order_id = $order->get_id();
+
 		$customer_id = $order->get_customer_id();
-		Users::delete_site_user_meta( $customer_id, 'wc_money_spent' );
-		Users::delete_site_user_meta( $customer_id, 'wc_order_count' );
-		Users::delete_site_user_meta( $customer_id, 'wc_last_order' );
+		if ( $customer_id ) {
+			// Optimization note: the function is fired multiple times during order persistence lifecycle, and by
+			// verifying that metas carry non-empty values we ensure no repetitive attempts dropping the metas.
+			$metas_to_purge = array_filter(
+				array(
+					is_numeric( Users::get_site_user_meta( $customer_id, 'wc_order_count' ) ) ? 'wc_order_count' : '',
+					is_numeric( Users::get_site_user_meta( $customer_id, 'wc_last_order' ) ) ? 'wc_last_order' : '',
+					is_numeric( Users::get_site_user_meta( $customer_id, 'wc_money_spent' ) ) ? 'wc_money_spent' : '',
+				)
+			);
+			if ( ! empty( $metas_to_purge ) ) {
+				foreach ( $metas_to_purge as $meta ) {
+					Users::delete_site_user_meta( $customer_id, $meta );
+				}
+			}
+		}
 	}

 	// Increments the transient version to invalidate cache.
@@ -668,9 +683,9 @@ function wc_create_refund( $args = array() ) {

 			// delete downloads that were refunded using order and product id, if present.
 			if ( ! empty( $refunded_order_and_products ) ) {
+				$download_data_store = WC_Data_Store::load( 'customer-download' );
 				foreach ( $refunded_order_and_products as $refunded_order_and_product ) {
-					$download_data_store = WC_Data_Store::load( 'customer-download' );
-					$downloads           = $download_data_store->get_downloads( $refunded_order_and_product );
+					$downloads = $download_data_store->get_downloads( $refunded_order_and_product );
 					if ( ! empty( $downloads ) ) {
 						foreach ( $downloads as $download ) {
 							$download_data_store->delete_by_id( $download->get_id() );
@@ -950,12 +965,12 @@ function wc_update_total_sales_counts( $order_id ) {

 	$operation = $recorded_sales && $reflected_order ? 'decrease' : 'increase';

-	if ( count( $order->get_items() ) > 0 ) {
-		foreach ( $order->get_items() as $item ) {
+	$line_items = $order->get_items();
+	if ( count( $line_items ) > 0 ) {
+		$data_store = WC_Data_Store::load( 'product' );
+		foreach ( $line_items as $item ) {
 			$product_id = $item->get_product_id();
-
 			if ( $product_id ) {
-				$data_store = WC_Data_Store::load( 'product' );
 				$data_store->update_product_sales( $product_id, absint( $item->get_quantity() ), $operation );
 			}
 		}
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 41798a70d2..29a389d044 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -35550,7 +35550,7 @@ parameters:
 		-
 			message: '#^Parameter \#1 \$object_or_string of function is_a expects object, WC_Order\|WC_Order_Refund\|false given\.$#'
 			identifier: argument.type
-			count: 2
+			count: 1
 			path: includes/wc-order-functions.php

 		-
diff --git a/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php
index 9b6f7b7113..657309492e 100644
--- a/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php
@@ -7,6 +7,7 @@

 use Automattic\WooCommerce\Enums\OrderInternalStatus;
 use Automattic\WooCommerce\Enums\OrderStatus;
+use Automattic\WooCommerce\Internal\Utilities\Users;

 /**
  * Class WC_Order_Functions_Test
@@ -541,4 +542,27 @@ class WC_Order_Functions_Test extends \WC_Unit_Test_Case {
 			$this->assertStringContainsString( '--', $result, "Edge case should preserve double hyphens: {$content}" );
 		}
 	}
+
+	/**
+	 * Test `wc_delete_shop_order_transients`: purging user metas depending on the order state.
+	 */
+	public function test_wc_delete_shop_order_transients_usermeta_purge(): void {
+		$customer    = WC_Helper_Customer::create_customer();
+		$customer_id = $customer->get_id();
+		$order       = WC_Helper_Order::create_order( $customer_id, null, array( 'status' => OrderStatus::COMPLETED ) );
+		$order_id    = $order->get_id();
+
+		// Verify the metas getting purged for order a state different from checkout draft.
+		Users::update_site_user_meta( $customer_id, 'wc_order_count', 123 );
+		Users::update_site_user_meta( $customer_id, 'wc_last_order', 456 );
+		Users::update_site_user_meta( $customer_id, 'wc_money_spent', 789 );
+		wc_delete_shop_order_transients( $order_id );
+		$this->assertSame( '', Users::get_site_user_meta( $customer_id, 'wc_order_count' ) );
+		$this->assertSame( '', Users::get_site_user_meta( $customer_id, 'wc_last_order' ) );
+		$this->assertSame( '', Users::get_site_user_meta( $customer_id, 'wc_money_spent' ) );
+
+		// Cleanup.
+		$order->delete();
+		$customer->delete();
+	}
 }