perf(selfop): paginate sales summary before day coverage enrichment
This commit is contained in:
+25
-3
@@ -95,11 +95,32 @@ public final class AnalyticsReadSql {
|
||||
String rangedHint=request.from()!=null&&request.until()!=null
|
||||
&&java.time.temporal.ChronoUnit.DAYS.between(request.from(),request.until())==366
|
||||
?"/*+ INDEX(d idx_selfop_daily_sku_cover) */ ":"";
|
||||
sql=sql.substring(0,rangeStart)+" ranged AS (SELECT "+rangedHint+"d.customer_code,d.sku_code,SUM(d.outbound_quantity) observed_sum,"
|
||||
// Sorting needs only the observed quantity and its validity. The daily unique
|
||||
// key makes observed_count the union cardinality of actual days and the
|
||||
// continuous observed date interval: it is positive iff either is nonempty.
|
||||
// Strict UTC intersection rejects empty/reversed intervals without computing
|
||||
// every candidate's full/partial day boundaries before the page is selected.
|
||||
String observedIntersection="e.evidence_at_utc IS NOT NULL AND #{p.coveredFrom} IS NOT NULL AND #{p.coveredUntil} IS NOT NULL "
|
||||
+"AND GREATEST(e.evidence_at_utc,#{p.coveredFrom},CONVERT_TZ(CAST(#{p.from} AS DATETIME),'+08:00','+00:00'))"
|
||||
+"<LEAST(#{p.coveredUntil},CONVERT_TZ(CAST(#{p.until} AS DATETIME),'+08:00','+00:00'))";
|
||||
String sortPage=" sort_sales AS (SELECT "+rangedHint+"d.customer_code,d.sku_code,SUM(d.outbound_quantity) observed_sum,"
|
||||
+"COUNT(*) actual_days,MIN(d.quantity_unit) min_unit,MAX(d.quantity_unit) max_unit "
|
||||
+"FROM ll_selfop_sku_daily_sales d WHERE d.sales_run_id=#{p.salesRun} AND"+KEY_FILTER
|
||||
+"AND d.business_date>=#{p.from} AND d.business_date<#{p.until} GROUP BY d.customer_code,d.sku_code),"
|
||||
+" rankable AS (SELECT e.*,CASE WHEN e.profile_unit_valid=1 "
|
||||
+"AND (s.min_unit IS NULL OR (s.min_unit=e.quantity_unit AND s.max_unit=e.quantity_unit)) "
|
||||
+"AND #{p.salesKnown}=1 AND (COALESCE(s.actual_days,0)>0 OR ("+observedIntersection+")) "
|
||||
+"THEN COALESCE(s.observed_sum,0) END sort_observed_quantity "
|
||||
+"FROM evidence e LEFT JOIN sort_sales s ON s.customer_code=e.customer_code AND s.sku_code=e.sku_code),"
|
||||
+" paged_keys AS (SELECT * FROM rankable ORDER BY sort_observed_quantity IS NULL,sort_observed_quantity DESC,"
|
||||
+"sku_code ASC,customer_code ASC LIMIT #{p.size} OFFSET #{p.offset}),";
|
||||
sql=sql.substring(0,rangeStart)+" ranged AS (SELECT d.customer_code,d.sku_code,SUM(d.outbound_quantity) observed_sum,"
|
||||
+"COUNT(*) actual_days,SUM(d.outbound_order_count) order_sum,MIN(d.quantity_unit) min_unit,MAX(d.quantity_unit) max_unit,"
|
||||
+"SUM(CASE WHEN d.business_date>=#{p.completeFrom} AND d.business_date<#{p.completeUntil} AND d.business_date<#{p.anchor} THEN 1 ELSE 0 END) actual_complete "
|
||||
+"FROM ll_selfop_sku_daily_sales d WHERE d.sales_run_id=#{p.salesRun} AND"+KEY_FILTER
|
||||
+"FROM paged_keys k JOIN ll_selfop_sku_daily_sales d ON d.customer_code=k.customer_code AND d.sku_code=k.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.from} AND d.business_date<#{p.until} GROUP BY d.customer_code,d.sku_code),"+sql.substring(rangeEnd);
|
||||
sql=sql.replace(" bounds AS (",sortPage+" bounds AS (").replace("FROM evidence),","FROM paged_keys evidence),");
|
||||
String scopedCount="SELECT COUNT(*) FROM ll_selfop_sku_daily_sales d2 WHERE d2.sales_run_id=#{p.salesRun} "
|
||||
+"AND d2.source_account=#{p.account} AND d2.warehouse_code=#{p.warehouse} "
|
||||
+"AND d2.customer_code=b.customer_code AND d2.sku_code=b.sku_code ";
|
||||
@@ -175,7 +196,8 @@ public final class AnalyticsReadSql {
|
||||
return typed(cte(p)+"SELECT customer_code,sku_code,product_name,quantity_unit,evidence_at_utc,unit_valid,observed_quantity,sku_order_count,"
|
||||
+"complete_day_count,partial_day_count,unknown_day_count,window_quantity,window_sku_order_count,sales30_quantity,today_quantity,"
|
||||
+"physical_quantity,available_quantity,frozen_quantity,transit_quantity,inventory_observed_at_utc,inventory_quality,inventory_reason "
|
||||
+"FROM result"+filter(p.kind()).stripTrailing()+" ORDER BY "+order(p.kind())+" LIMIT #{p.size} OFFSET #{p.offset}",p);
|
||||
+"FROM result"+filter(p.kind()).stripTrailing()+" ORDER BY "+order(p.kind())
|
||||
+("SUMMARY".equals(p.kind())?"":" LIMIT #{p.size} OFFSET #{p.offset}"),p);
|
||||
}
|
||||
/** Rank on sales alone. Inventory enrichment and its quality checks touch only the
|
||||
* selected page; inventory missing/invalid units cannot erase a valid sales rank. */
|
||||
|
||||
+10
-2
@@ -62,8 +62,16 @@ class AnalyticsReadMapperTest {
|
||||
assertFalse(page.contains("metric_sales"));assertFalse(page.contains("FROM bounds WHERE 1=0"));
|
||||
assertTrue(page.contains("bounds AS (SELECT evidence.*,"));
|
||||
assertTrue(page.contains("counted AS (SELECT /*+ NO_MERGE(b) */ b.*,"));
|
||||
assertTrue(page.endsWith("ORDER BY observed_quantity IS NULL,observed_quantity DESC,sku_code ASC,customer_code ASC LIMIT ? OFFSET ?"));
|
||||
assertTrue(page.indexOf("GROUP BY d.customer_code,d.sku_code")<page.lastIndexOf("LIMIT"));
|
||||
assertTrue(page.endsWith("ORDER BY observed_quantity IS NULL,observed_quantity DESC,sku_code ASC,customer_code ASC"));
|
||||
assertTrue(page.contains("paged_keys AS (SELECT * FROM rankable ORDER BY sort_observed_quantity IS NULL,sort_observed_quantity DESC,sku_code ASC,customer_code ASC LIMIT ? OFFSET ?)"));
|
||||
assertTrue(page.indexOf("GROUP BY d.customer_code,d.sku_code")<page.indexOf("LIMIT"));
|
||||
assertTrue(page.indexOf("LIMIT")<page.indexOf("bounds AS"));
|
||||
assertEquals(page.indexOf("LIMIT"),page.lastIndexOf("LIMIT"));
|
||||
assertTrue(page.contains("FROM paged_keys evidence)"));
|
||||
assertTrue(page.contains("FROM paged_keys k JOIN ll_selfop_sku_daily_sales d ON d.customer_code=k.customer_code AND d.sku_code=k.sku_code"));
|
||||
String sorting=page.substring(page.indexOf("sort_sales AS"),page.indexOf("bounds AS"));
|
||||
assertFalse(sorting.contains("outbound_order_count"));assertFalse(sorting.contains("actual_complete"));
|
||||
assertTrue(sorting.contains("AND ?=1 AND (COALESCE(s.actual_days,0)>0 OR"));
|
||||
assertFalse(page.contains("FROM bounds b JOIN ll_selfop_sku_daily_sales"));
|
||||
assertTrue(page.contains("d2.customer_code=b.customer_code AND d2.sku_code=b.sku_code"));
|
||||
assertTrue(page.contains("d2.sales_run_id=? AND d2.source_account=? AND d2.warehouse_code=?"));
|
||||
|
||||
+72
-1
@@ -49,7 +49,7 @@ public class AnalyticsSqlSemanticTest {
|
||||
String hint="/*+ INDEX(d idx_selfop_daily_sku_cover) */";
|
||||
assertEquals(days==366,sql.contains(hint),"window="+days);
|
||||
if(days==366) {
|
||||
assertTrue(sql.contains("ranged AS (SELECT "+hint+" d.customer_code"));
|
||||
assertTrue(sql.contains("sort_sales AS (SELECT "+hint+" d.customer_code"));
|
||||
assertEquals(sql.indexOf(hint),sql.lastIndexOf(hint));
|
||||
}
|
||||
assertFalse(AnalyticsReadSql.count(Map.of("p",p)).contains("INDEX("));
|
||||
@@ -87,6 +87,77 @@ public class AnalyticsSqlSemanticTest {
|
||||
assertEquals(0,new BigDecimal("5").compareTo(row.observedQuantity()));
|
||||
assertEquals(2,row.completeDayCount());assertEquals(1,row.partialDayCount());assertEquals(0,row.unknownDayCount());
|
||||
}
|
||||
@Test void summaryPageFirstPreservesTiesNullUnitsZeroAndExactScope() throws Exception {
|
||||
for(String owner:List.of("A","B")){
|
||||
profile(11,owner,"TIE","VALID_OUTBOUND",utc(D.minusDays(40),0),"PCS");
|
||||
daily(11,"W",owner,"TIE",D.minusDays(1),"5");
|
||||
}
|
||||
profile(22,"C","ZERO","INVENTORY_OBSERVATION",utc(D.minusDays(40),0),"PCS");
|
||||
profile(22,"C","UNKNOWN","NONE",null,"PCS");
|
||||
profile(11,"C","MIXED","VALID_OUTBOUND",utc(D.minusDays(40),0),"PCS");
|
||||
daily(11,"W","C","MIXED",D.minusDays(1),"900");daily(11,"W","C","MIXED",D,"900");
|
||||
profile(11,"C","NULL-UNIT","VALID_OUTBOUND",utc(D.minusDays(40),0),null);
|
||||
daily(11,"W","C","NULL-UNIT",D,"800");
|
||||
profile(22,"C","OUTSIDE","INVENTORY_OBSERVATION",utc(D.minusDays(40),0),"PCS");
|
||||
profile(99,"C","OTHER-VERSION","VALID_OUTBOUND",utc(D.minusDays(40),0),"PCS");
|
||||
daily(11,"W","C","NO-PROFILE",D,"9999");
|
||||
daily(99,"W","A","TIE",D,"9999");daily(11,"OTHER","A","TIE",D,"9999");
|
||||
daily(11,"W","A","TIE",D.plusDays(1),"9999");
|
||||
try(var s=schema.createStatement()){
|
||||
s.executeUpdate("UPDATE ll_selfop_sku_daily_sales SET quantity_unit='BOX' WHERE sku_code='MIXED' AND business_date=DATE '2026-10-12'");
|
||||
s.executeUpdate("UPDATE ll_selfop_sku_profile SET warehouse_code='OTHER' WHERE sku_code='OUTSIDE'");
|
||||
}
|
||||
var p=request("SUMMARY",D.minusDays(1),D.plusDays(1),null,null,utc(D,8),0,2);
|
||||
assertEquals(6,mapper.count(p));
|
||||
assertEquals(List.of("A/TIE","B/TIE"),page(p).stream().map(r->r.customerCode()+"/"+r.skuCode()).toList());
|
||||
var second=page(request("SUMMARY",D.minusDays(1),D.plusDays(1),null,null,utc(D,8),2,2));
|
||||
assertEquals(List.of("ZERO","MIXED"),second.stream().map(AnalyticsReadRows.Aggregate::skuCode).toList());
|
||||
assertDecimal("0",second.get(0).observedQuantity());assertNull(second.get(1).observedQuantity());
|
||||
assertFalse(second.get(1).unitValid());assertEquals(2,second.get(1).unknownDayCount());
|
||||
var last=page(request("SUMMARY",D.minusDays(1),D.plusDays(1),null,null,utc(D,8),4,2));
|
||||
assertEquals(List.of("NULL-UNIT","UNKNOWN"),last.stream().map(AnalyticsReadRows.Aggregate::skuCode).toList());
|
||||
assertTrue(last.stream().allMatch(r->r.observedQuantity()==null));
|
||||
assertTrue(page(request("SUMMARY",D.minusDays(1),D.plusDays(1),null,null,utc(D,8),6,2)).isEmpty());
|
||||
var scoped=page(request("SUMMARY",D.minusDays(1),D.plusDays(1),"B","TIE",utc(D,8),0,2));
|
||||
assertEquals(1,scoped.size());assertDecimal("5",scoped.get(0).observedQuantity());
|
||||
}
|
||||
@Test void summaryPageFirstCountsIntersectionAndActualDaysOutsideEvidenceOnlyOnSelectedPage() throws Exception {
|
||||
profile(11,"C","LATE","VALID_OUTBOUND",utc(D.minusDays(1),12),"PCS");
|
||||
daily(11,"W","C","LATE",D.minusDays(2),"2");daily(11,"W","C","LATE",D,"3");
|
||||
profile(22,"C","NO-PROOF","NONE",null,"PCS");daily(11,"W","C","NO-PROOF",D,"9");
|
||||
profile(22,"C","ZERO","INVENTORY_OBSERVATION",utc(D.minusDays(1),12),"PCS");
|
||||
var rows=page(request("SUMMARY",D.minusDays(2),D.plusDays(1),null,null,utc(D,8),1,1));
|
||||
assertEquals(1,rows.size());var late=rows.get(0);assertEquals("LATE",late.skuCode());
|
||||
assertDecimal("5",late.observedQuantity());assertEquals(2L,late.skuOrderCount());
|
||||
assertEquals(1,late.completeDayCount());assertEquals(2,late.partialDayCount());assertEquals(0,late.unknownDayCount());
|
||||
var noProof=page(request("SUMMARY",D.minusDays(2),D.plusDays(1),null,null,utc(D,8),0,1)).get(0);
|
||||
assertEquals("NO-PROOF",noProof.skuCode());assertDecimal("9",noProof.observedQuantity());
|
||||
assertEquals(0,noProof.completeDayCount());assertEquals(1,noProof.partialDayCount());assertEquals(2,noProof.unknownDayCount());
|
||||
var zero=page(request("SUMMARY",D.minusDays(2),D.plusDays(1),null,null,utc(D,8),2,1)).get(0);
|
||||
assertEquals("ZERO",zero.skuCode());assertDecimal("0",zero.observedQuantity());
|
||||
assertEquals(0,zero.completeDayCount());assertEquals(2,zero.partialDayCount());assertEquals(1,zero.unknownDayCount());
|
||||
}
|
||||
@Test void summaryPageFirstNeverSortsUnknownSalesOrDisjointCoverageAsKnownZero() throws Exception {
|
||||
profile(22,"C","A-UNKNOWN","NONE",null,"PCS");
|
||||
profile(22,"C","Z-EVIDENCE","INVENTORY_OBSERVATION",utc(D,0),"PCS");
|
||||
for(LocalDateTime[] span:List.of(
|
||||
new LocalDateTime[]{utc(D,0),utc(D,0)},
|
||||
new LocalDateTime[]{utc(D,8),utc(D,0)},
|
||||
new LocalDateTime[]{utc(D.minusDays(2),0),utc(D,0)},
|
||||
new LocalDateTime[]{utc(D.plusDays(1),0),utc(D.plusDays(2),0)},
|
||||
new LocalDateTime[]{null,utc(D,8)})){
|
||||
var p=new AnalyticsSqlRequest("demo","W",null,null,11,22,D,D.plusDays(1),D,D.minusDays(30),null,null,
|
||||
span[0],span[1],true,false,false,false,true,0,1,"SUMMARY");
|
||||
var first=page(p).get(0);assertEquals("A-UNKNOWN",first.skuCode());assertNull(first.observedQuantity());
|
||||
var last=new AnalyticsSqlRequest("demo","W",null,null,11,22,D,D.plusDays(1),D,D.minusDays(30),null,null,
|
||||
span[0],span[1],true,false,false,false,true,1,1,"SUMMARY");
|
||||
assertNull(page(last).get(0).observedQuantity());
|
||||
}
|
||||
daily(11,"W","C","Z-EVIDENCE",D,"99");
|
||||
var unavailable=new AnalyticsSqlRequest("demo","W",null,null,11,22,D,D.plusDays(1),D,D.minusDays(30),null,null,
|
||||
utc(D,0),utc(D,8),false,false,false,false,true,0,1,"SUMMARY");
|
||||
var first=page(unavailable).get(0);assertEquals("A-UNKNOWN",first.skuCode());assertNull(first.observedQuantity());
|
||||
}
|
||||
@Test void evidenceAfterCoveredTimeOnSameDayDoesNotBecomeObservedZero() throws Exception {
|
||||
profile(22,"C","LATE","INVENTORY_OBSERVATION",utc(D,16),"PCS");
|
||||
profile(22,"C","NO-EVIDENCE","NONE",null,"PCS");
|
||||
|
||||
Reference in New Issue
Block a user