diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsInventorySql.java b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsInventorySql.java index 9bb0def94..7fddf053a 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsInventorySql.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsInventorySql.java @@ -83,15 +83,22 @@ public final class AnalyticsInventorySql { boolean stockFirst=stockFirst(o); // ALL/available ordering depends only on stock; aggregate sales after selecting these keys. String paged=cte(p,stockFirst)+", paged AS (SELECT * FROM result" - +(stockFirst?"":filter(o)+" ORDER BY "+order(o)+" LIMIT #{p.size} OFFSET #{p.offset}")+")," - +" last_sales AS (SELECT "+(stockFirst?"/*+ INDEX(d idx_selfop_daily_sku_cover) */ ":"")+"d.customer_code,d.sku_code,MAX(d.business_date) last_outbound FROM ll_selfop_sku_daily_sales d " + +(stockFirst?"":filter(o)+" ORDER BY "+order(o)+" LIMIT #{p.size} OFFSET #{p.offset}")+") " + +(stockFirst?"":", last_sales AS (SELECT d.customer_code,d.sku_code,MAX(d.business_date) last_outbound FROM ll_selfop_sku_daily_sales d " +"JOIN paged k ON k.customer_code=d.customer_code AND k.sku_code=d.sku_code WHERE d.sales_run_id=#{p.salesRun} " +"AND d.source_account=#{p.account} AND d.warehouse_code=#{p.warehouse} AND d.business_date<=#{p.anchor} " - +"AND d.outbound_quantity>0 AND d.quantity_unit='PCS' GROUP BY d.customer_code,d.sku_code) "; + +"AND d.outbound_quantity>0 AND d.quantity_unit='PCS' GROUP BY d.customer_code,d.sku_code) "); String outerOrder=order(o).replace("physical_quantity","k.physical_quantity").replace("coverage_days","k.coverage_days") .replace("outbound_quantity30d","k.outbound_quantity30d").replace("available_quantity","k.available_quantity") .replace("sku_code","k.sku_code").replace("customer_code","k.customer_code"); - return typed(paged+"SELECT k.customer_code,k.sku_code,k.product_name,k.unit_valid,k.evidence_at_utc,k.physical_quantity,k.available_quantity,k.frozen_quantity,k.transit_quantity,k.defective_physical_quantity,k.observed_at_utc,k.owner_outbound,k.observed_outbound,k.order_count,l.last_outbound,k.observation_eligible,k.outbound_quantity30d,k.coverage_days,k.no_outbound,k.above_threshold,k.unassessable FROM paged k LEFT JOIN last_sales l ON l.customer_code=k.customer_code AND l.sku_code=k.sku_code ORDER BY "+outerOrder);} + return typed(paged+"SELECT k.customer_code,k.sku_code,k.product_name,k.unit_valid,k.evidence_at_utc,k.physical_quantity,k.available_quantity,k.frozen_quantity,k.transit_quantity,k.defective_physical_quantity,k.observed_at_utc,k.owner_outbound,k.observed_outbound,k.order_count,"+(stockFirst?lastOutboundLookup():"l.last_outbound")+" last_outbound,k.observation_eligible,k.outbound_quantity30d,k.coverage_days,k.no_outbound,k.above_threshold,k.unassessable FROM paged k "+(stockFirst?"":"LEFT JOIN last_sales l ON l.customer_code=k.customer_code AND l.sku_code=k.sku_code ")+"ORDER BY "+outerOrder);} + private static String lastOutboundLookup(){ + // Repeat the correlated equality keys in ORDER BY so MySQL can traverse the covering index in reverse. + return "(SELECT /*+ INDEX(d idx_selfop_daily_sku_cover) */ d.business_date FROM ll_selfop_sku_daily_sales d " + +"WHERE d.sales_run_id=#{p.salesRun} AND d.source_account=#{p.account} AND d.warehouse_code=#{p.warehouse} " + +"AND d.customer_code=k.customer_code AND d.sku_code=k.sku_code AND d.business_date<=#{p.anchor} " + +"AND d.outbound_quantity>0 AND d.quantity_unit='PCS' ORDER BY d.customer_code DESC,d.sku_code DESC,d.business_date DESC LIMIT 1)"; + } public static String inventoryCount(Map args){var p=(AnalyticsSqlRequest)args.get("p");var o=(AnalyticsInventoryOptions)args.get("o"); if(o.coverageBucket()==null&&o.coverageSelection()==null&&java.util.Set.of("FROZEN_STOCK","DEFECTIVE_STOCK").contains(o.attentionValue())) return typed(stockAttentionCount(p,o)); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsReadMapperTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsReadMapperTest.java index 5efca950e..51150a96d 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsReadMapperTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsReadMapperTest.java @@ -103,6 +103,16 @@ class AnalyticsReadMapperTest { if("HOT".equals(kind))assertFalse(stats.contains("ll_selfop_inventory_snapshot")); } } + @Test void stockFirstHistoryLookupRetainsCorrelatedScopeAndReverseIndexOrder(){ + var c=config();var p=request("INVENTORY");var o=new AnalyticsInventoryOptions(null,"ALL","AVAILABLE_DESC",null,null,null,null,null); + String sql=bound(c,"inventoryPage",Map.of("p",p,"o",o)).getSql(); + assertFalse(sql.contains("last_sales AS"));assertFalse(sql.contains("JOIN last_sales")); + assertTrue(sql.contains("d.customer_code=k.customer_code AND d.sku_code=k.sku_code")); + assertTrue(sql.contains("d.sales_run_id=? AND d.source_account=? AND d.warehouse_code=?")); + assertTrue(sql.contains("d.outbound_quantity>0 AND d.quantity_unit='PCS' ORDER BY d.customer_code DESC,d.sku_code DESC,d.business_date DESC LIMIT 1")); + assertTrue(sql.indexOf("stock_paged AS")