perf(selfop): skip unavailable dashboard aggregates

This commit is contained in:
2026-10-13 03:45:01 +08:00
parent 7ae91eb276
commit ca4e7086e9
3 changed files with 52 additions and 7 deletions
@@ -17,7 +17,9 @@ public final class AnalyticsInventorySql {
private static String defectiveQuantity(){return "CASE WHEN MIN(CASE WHEN product_quality NOT IN (0,1) THEN 0 WHEN product_quality=1 THEN CASE WHEN "+valid("physical_quantity")+" THEN 1 ELSE 0 END ELSE 1 END)=1 "
+"THEN SUM(CASE WHEN product_quality=1 THEN physical_quantity ELSE 0 END) END";}
static String cte(AnalyticsSqlRequest p){return cte(p,false);}
private static String cte(AnalyticsSqlRequest p,boolean stockFirst){
static String dashboardCte(AnalyticsSqlRequest p){return cte(p,false,true);}
private static String cte(AnalyticsSqlRequest p,boolean stockFirst){return cte(p,stockFirst,false);}
private static String cte(AnalyticsSqlRequest p,boolean stockFirst,boolean dashboard){
return "WITH candidates AS ("+keys(p)+"), profiles AS (SELECT c.customer_code,c.sku_code,COALESCE(i.product_name,s.product_name) product_name,"
+"CASE WHEN i.quantity_unit='PCS' THEN 1 ELSE 0 END stock_unit_valid,"
+"CASE WHEN s.sku_code IS NULL OR s.quantity_unit='PCS' THEN 1 ELSE 0 END sales_unit_valid,"
@@ -29,9 +31,10 @@ public final class AnalyticsInventorySql {
+" stock AS (SELECT customer_code,sku_code,"+quantity("physical_quantity")+" physical_quantity,"+quantity("frozen_quantity")+" frozen_quantity,"
+quantity("transit_quantity")+" transit_quantity,MAX(CASE WHEN product_quality=0 AND "+valid("available_quantity")+" THEN available_quantity END) available_quantity,"
+defectiveQuantity()+" defective_physical_quantity,MAX(observed_at_utc) observed_at_utc "
+"FROM ll_selfop_inventory_snapshot WHERE snapshot_run_id=#{p.inventoryRun} AND"+scope(p)+" GROUP BY customer_code,sku_code),"
+"FROM ll_selfop_inventory_snapshot WHERE "+(dashboard&&!p.stockFresh()?"1=0 /* dashboard unused stock */ AND ":"")
+"snapshot_run_id=#{p.inventoryRun} AND"+scope(p)+" GROUP BY customer_code,sku_code),"
+(stockFirst?stockPageKeys():"")
+salesCte(p,stockFirst)
+salesCte(p,stockFirst,dashboard&&!p.complete30())
+" values_by_sku AS (SELECT c.customer_code,c.sku_code,c.product_name,c.unit_valid,"
+"CASE WHEN c.sales_evidence IS NULL THEN c.stock_evidence WHEN c.stock_evidence IS NULL THEN c.sales_evidence ELSE LEAST(c.sales_evidence,c.stock_evidence) END evidence_at_utc,"
+stockValue("physical_quantity")+","+stockValue("available_quantity")+","+stockValue("frozen_quantity")+","+stockValue("transit_quantity")+","+stockValue("defective_physical_quantity")+",st.observed_at_utc,"
@@ -47,12 +50,12 @@ public final class AnalyticsInventorySql {
+"WHEN physical_quantity>0 AND available_quantity>0 AND outbound_quantity30d IS NOT NULL THEN CASE WHEN outbound_quantity30d>0 AND available_quantity*30>outbound_quantity30d*#{o.coverageDaysThreshold} THEN 1 ELSE 0 END END above_threshold "
+"FROM qualified q), result AS (SELECT c.*,CASE WHEN physical_quantity IS NULL OR available_quantity IS NULL OR outbound_quantity30d IS NULL THEN 1 ELSE 0 END unassessable FROM calculated c) ";
}
private static String salesCte(AnalyticsSqlRequest p,boolean stockFirst){String d=stockFirst?"d.":"";
private static String salesCte(AnalyticsSqlRequest p,boolean stockFirst,boolean unused){String d=stockFirst?"d.":"";
return " sales AS (SELECT "+(stockFirst?"/*+ JOIN_ORDER(k,d) INDEX(d idx_selfop_daily_sku_cover) */ ":"")
+d+"customer_code,"+d+"sku_code,SUM("+d+"outbound_quantity) observed_quantity,SUM("+d+"outbound_order_count) order_count,"
+"MIN(CASE WHEN "+d+"quantity_unit='PCS' AND "+d+"outbound_quantity>=0 THEN 1 ELSE 0 END) unit_valid FROM ll_selfop_sku_daily_sales "
+(stockFirst?"d JOIN stock_paged k ON k.customer_code=d.customer_code AND k.sku_code=d.sku_code ":"")
+"WHERE "+d+"sales_run_id=#{p.salesRun} AND"+scope(p,d)+" AND "+d+"business_date>=#{p.from} AND "+d+"business_date<#{p.until} GROUP BY "+d+"customer_code,"+d+"sku_code),";
+"WHERE "+(unused?"1=0 /* dashboard unused sales */ AND ":"")+d+"sales_run_id=#{p.salesRun} AND"+scope(p,d)+" AND "+d+"business_date>=#{p.from} AND "+d+"business_date<#{p.until} GROUP BY "+d+"customer_code,"+d+"sku_code),";
}
private static String stockAmount(String field){return "CASE WHEN c.stock_unit_valid=1 AND #{p.stockFresh}=1 THEN st."+field+" END";}
private static String stockValue(String field){return stockAmount(field)+" "+field;}
@@ -7,7 +7,12 @@ import java.util.Map;
public final class DashboardReadSql {
private DashboardReadSql(){}
public static String report(Map<String,Object> args){
var p=(AnalyticsSqlRequest)args.get("p");StringBuilder sql=new StringBuilder(AnalyticsInventorySql.cte(p));
var p=(AnalyticsSqlRequest)args.get("p");
// Only dashboard outputs are considered here. Without complete30 all sales-dependent
// rankings are empty; observed sales still come from the independent daily query.
// Stale stock quantities are already masked to NULL. Keep both profile versions so
// candidate identities, observation evidence and unknown reasons remain unchanged.
StringBuilder sql=new StringBuilder(AnalyticsInventorySql.dashboardCte(p));
// SKU identity is unique already; DISTINCT is a MySQL merge barrier so the four consumers
// share one calculation rather than expanding profile/stock/sales joins for each ranking.
sql.append(", dashboard_result AS (SELECT DISTINCT * FROM result)");
@@ -45,6 +45,32 @@ class DashboardSqlSemanticTest {
assertEquals(1,count(unknown.get(0),"candidate_skus"));assertEquals(1,count(unknown.get(0),"physical_quantity_unknown"));
assertNull(unknown.get(0).get("physical_quantity_known"));assertEquals(1,count(unknown.get(0),"bucket_unknown"));
}
@Test void unavailableAggregateBranchesPreserveEveryReportValueAndIndependentObservedDailySales()throws Exception{
fixture("SOLD","90","60","30");fixture("UNSOLD","10","10",null);
fixture("NOAVAILABLE","5","0",null);fixture("ZERO","0","0",null);fixture("UNKNOWN",null,null,null);
profile(11,"A","SAME","PCS");profile(11,"B","SAME","BOX");
daily("A","SAME",D.minusDays(1),"7","PCS");daily("B","SAME",D.minusDays(1),"3","BOX");
profile(22,"C","STOCKONLY","PCS");stock("STOCKONLY","4","4");
daily("C","SOLD",D.minusDays(2),"-1","PCS");
for(boolean complete:List.of(false,true))for(boolean fresh:List.of(false,true)){
var p=request(complete,fresh);var args=Map.<String,Object>of("p",p,"o",options());
String sql=DashboardReadSql.report(args);
assertEquals(!complete,sql.contains("1=0 /* dashboard unused sales */ AND "));
assertEquals(!fresh,sql.contains("1=0 /* dashboard unused stock */ AND "));
String ordinary=AnalyticsInventorySql.cte(p);
assertFalse(ordinary.contains("dashboard unused"));
assertEquals(ordinary,AnalyticsInventorySql.dashboardCte(p)
.replace("1=0 /* dashboard unused sales */ AND ","").replace("1=0 /* dashboard unused stock */ AND ",""));
assertTrue(sql.contains("profile_run_id=#{p.salesRun}"));assertTrue(sql.contains("profile_run_id=#{p.inventoryRun}"));
var actual=query(DashboardReadMapper.class,"report",args);
var original=query(DashboardReadMapper.class,"report",args,true);
assertEquals(new HashSet<>(original),new HashSet<>(actual),"complete="+complete+", fresh="+fresh);
assertEquals(original.size(),actual.size());
var days=query(DashboardReadMapper.class,"daily",Map.of("p",p,"chartFrom",D.minusDays(2),"chartUntil",D));
var yesterday=days.stream().map(DashboardReadMapper.Day.class::cast).filter(day->day.businessDate().equals(D.minusDays(1))).findFirst().orElseThrow();
assertEquals(0,new BigDecimal("37").compareTo(yesterday.observedQuantity()));assertEquals(1L,yesterday.invalidRowCount());
}
}
@Test void combinedReportReturnsAtMostThirtyRowsWithoutMultiplyingStatistics()throws Exception{
for(int i=0;i<15;i++){fixture("SOLD-"+i,"30.04","30.04","30");fixture("UNSOLD-"+i,"5","5",null);}
var rows=report();assertEquals(30,rows.size());
@@ -99,6 +125,7 @@ class DashboardSqlSemanticTest {
private void stock(String sku,String physical,String available)throws Exception{try(var p=connection.prepareStatement("INSERT INTO ll_selfop_inventory_snapshot VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)")){
p.setLong(1,22);p.setString(2,"demo");p.setString(3,"W");p.setString(4,"C");p.setString(5,sku);p.setInt(6,0);p.setBigDecimal(7,physical==null?null:new BigDecimal(physical));p.setBigDecimal(8,available==null?null:new BigDecimal(available));p.setBigDecimal(9,BigDecimal.ZERO);p.setBigDecimal(10,BigDecimal.ZERO);p.setObject(11,D.atStartOfDay());p.setString(12,physical==null?"UNKNOWN":"VALID");p.setString(13,physical==null?"FIELD_MISSING":null);p.executeUpdate();}}
private AnalyticsSqlRequest request(){return new AnalyticsSqlRequest("demo","W",null,null,11,22,D.minusDays(30),D,D,D.minusDays(30),D.minusDays(30),D,D.minusDays(30).atStartOfDay().minusHours(8),D.atStartOfDay(),true,true,true,true,true,0,20,"DASHBOARD");}
private AnalyticsSqlRequest request(boolean complete,boolean fresh){var p=request();return new AnalyticsSqlRequest(p.account(),p.warehouse(),null,null,p.salesRun(),p.inventoryRun(),p.from(),p.until(),p.anchor(),p.windowFrom(),p.completeFrom(),p.completeUntil(),p.coveredFrom(),p.coveredUntil(),p.salesKnown(),p.formalSales(),complete,complete,fresh,p.offset(),p.size(),p.kind());}
private static AnalyticsInventoryOptions options(){return new AnalyticsInventoryOptions(null,null,null,null,null,null,null,null);}
@SuppressWarnings("unchecked") private List<Map<String,Object>> report(){
return ((List<Map<String,Object>>)(List<?>)query(DashboardReadMapper.class,"report",Map.of("p",request(),"o",options()))).stream().map(row->{
@@ -109,12 +136,22 @@ class DashboardSqlSemanticTest {
private List<DashboardReadMapper.Ranked> ranks(){return report().stream().filter(row->row.get("rank_code")!=null).map(DashboardQueryService::ranked).toList();}
private static long count(Map<String,Object> stats,String key){return ((Number)stats.get(key)).longValue();}
private List<?> query(Class<?> mapper,String method,Map<String,Object> args){
return query(mapper,method,args,false);
}
private List<?> query(Class<?> mapper,String method,Map<String,Object> args,boolean originalAggregates){
var original=config.getMappedStatement(mapper.getName()+"."+method);var bound=original.getBoundSql(args);var meta=org.apache.ibatis.reflection.SystemMetaObject.forObject(args);
StringBuilder sql=new StringBuilder();int index=0;boolean quoted=false;
for(int i=0;i<bound.getSql().length();i++){char ch=bound.getSql().charAt(i);if(ch=='\'')quoted=!quoted;
if(ch!='?'||quoted){sql.append(ch);continue;}String key=bound.getParameterMappings().get(index++).getProperty();Object v=meta.getValue(key);
if(v==null)sql.append("NULL");else if(v instanceof Boolean b)sql.append(b?1:0);else if(v instanceof Number)sql.append(v);else sql.append("'").append(v.toString().replace("'","''")).append("'");
}
String id="dashboard.literal."+(++sequence);config.addMappedStatement(new MappedStatement.Builder(config,id,new org.apache.ibatis.builder.StaticSqlSource(config,sql.toString()),SqlCommandType.SELECT).resultMaps(original.getResultMaps()).build());return session.selectList(id);
String rendered=sql.toString();
// This fixture literalizes bound booleans. H2 then folds the false stock CASE to
// an untyped NULL before SUM resolves its type; MySQL retains the DECIMAL type.
// Preserve only that NULL's fixture-column type, equally for old and new SQL.
for(String field:List.of("physical_quantity","available_quantity","frozen_quantity","transit_quantity","defective_physical_quantity"))
rendered=rendered.replace("CASE WHEN c.stock_unit_valid=1 AND 0=1 THEN st."+field+" END","CAST(NULL AS DECIMAL(20,6))");
if(originalAggregates)rendered=rendered.replace("1=0 /* dashboard unused sales */ AND ","").replace("1=0 /* dashboard unused stock */ AND ","");
String id="dashboard.literal."+(++sequence);config.addMappedStatement(new MappedStatement.Builder(config,id,new org.apache.ibatis.builder.StaticSqlSource(config,rendered),SqlCommandType.SELECT).resultMaps(original.getResultMaps()).build());return session.selectList(id);
}
}