perf(selfop): seek latest outbound date for inventory page

This commit is contained in:
2026-10-13 10:11:06 +08:00
parent 2d7b6e4381
commit d08ed2f2ca
3 changed files with 60 additions and 4 deletions
@@ -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<String,Object> 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));
@@ -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")<sql.indexOf("ORDER BY d.customer_code DESC"));
immutableOnly(sql);
}
@Test void inventoryHistoryOnlyEnrichesQualifiedPageAndNeverCountsOrOwnerTotals(){
var c=config();var p=request("INVENTORY");var o=new AnalyticsInventoryOptions(null,"COVERAGE_GT_THRESHOLD","PHYSICAL_DESC",90,null,null,null,null);
String page=bound(c,"inventoryPage",Map.of("p",p,"o",o)).getSql();
@@ -389,6 +389,45 @@ public class AnalyticsSqlSemanticTest {
for(String sort:List.of("PHYSICAL_DESC","COVERAGE_DAYS_DESC","OUTBOUND_30D_ASC"))
assertFalse(AnalyticsInventorySql.inventoryPage(Map.of("p",p,"o",new AnalyticsInventoryOptions(null,"ALL",sort,null,null,null,null,null))).contains("stock_paged"));
}
@Test void stockFirstLastOutboundSkipsInvalidLatestRowsAndKeepsAllScopeBoundaries() throws Exception {
for(String sku:List.of("HISTORY","NONE","ANCHOR")){
profile(22,"C",sku,"INVENTORY_OBSERVATION",utc(D.minusDays(90),0),"PCS");stock("C",sku,0,"10");
}
profile(22,"B","HISTORY","INVENTORY_OBSERVATION",utc(D.minusDays(90),0),"PCS");stock("B","HISTORY",0,"10");
historyRow(11,"demo","W","C","HISTORY",D.minusDays(40),"4","PCS");
historyRow(11,"demo","W","C","HISTORY",D.minusDays(2),"100","BOX");
historyRow(11,"demo","W","C","HISTORY",D.minusDays(1),"0","PCS");
historyRow(11,"demo","W","C","HISTORY",D.plusDays(1),"100","PCS");
historyRow(99,"demo","W","C","HISTORY",D,"100","PCS");
historyRow(11,"other-account","W","C","HISTORY",D,"100","PCS");
historyRow(11,"demo","OTHER","C","HISTORY",D,"100","PCS");
historyRow(11,"demo","W","B","HISTORY",D.minusDays(3),"9","PCS");
historyRow(11,"demo","W","C","OTHER-SKU",D,"100","PCS");
historyRow(11,"demo","W","C","NONE",D.minusDays(3),null,"PCS");
historyRow(11,"demo","W","C","NONE",D.minusDays(2),"8",null);
historyRow(11,"demo","W","C","NONE",D.minusDays(1),"-1","PCS");
historyRow(11,"demo","W","C","NONE",D,"0","PCS");
historyRow(11,"demo","W","C","ANCHOR",D,"1","PCS");
var p=request("INVENTORY",D.minusDays(30),D,null,null,utc(D,8),0,20);
var options=new AnalyticsInventoryOptions(null,"ALL","AVAILABLE_DESC",null,null,null,null,null);
var rows=inventoryRows(p,options);assertEquals(4,rows.size());
for(var row:rows){
LocalDate expected=row.skuCode().equals("ANCHOR")?D:row.skuCode().equals("NONE")?null:row.customerCode().equals("B")?D.minusDays(3):D.minusDays(40);
assertEquals(expected,row.lastOutbound(),row.customerCode()+"/"+row.skuCode());
}
// The pre-existing grouped path remains an independent semantic baseline.
var legacy=inventoryRows(p,new AnalyticsInventoryOptions(null,"ALL","PHYSICAL_DESC",null,null,null,null,null));
assertEquals(legacy,rows);
var selected=inventoryRows(request("INVENTORY",D.minusDays(30),D,"C","HISTORY",utc(D,8),0,20),options);
assertEquals(1,selected.size());assertEquals(D.minusDays(40),selected.get(0).lastOutbound());
}
private void historyRow(long version,String account,String warehouse,String owner,String sku,LocalDate day,String quantity,String unit)throws Exception {
try(var statement=schema.prepareStatement("INSERT INTO ll_selfop_sku_daily_sales VALUES(?,?,?,?,?,?,?,?,?,?)")){
statement.setLong(1,nextId++);statement.setLong(2,version);statement.setString(3,account);statement.setString(4,warehouse);
statement.setString(5,owner);statement.setString(6,sku);statement.setObject(7,day);
statement.setBigDecimal(8,quantity==null?null:new BigDecimal(quantity));statement.setLong(9,1L);statement.setString(10,unit);statement.executeUpdate();
}
}
@Test void stockFirstDoesNotTurnUnavailableSalesOrExpiredInventoryIntoKnownValues() throws Exception {
profile(22,"C","SKU","INVENTORY_OBSERVATION",utc(D.minusDays(60),0),"PCS");stock("C","SKU",0,"10");daily(11,"W","C","SKU",D.minusDays(1),"3");
var options=new AnalyticsInventoryOptions(null,null,null,90,null,null,null,null);