perf(selfop): cover range reads and stream yearly SKU aggregation

This commit is contained in:
2026-10-13 01:05:52 +08:00
parent a41c271a67
commit 99d206eb00
5 changed files with 54 additions and 7 deletions
@@ -4189,7 +4189,8 @@ CREATE TABLE ll_selfop_sku_daily_sales (
quantity_unit VARCHAR(32) NOT NULL COMMENT '同一统计维度内一致的业务单位',
PRIMARY KEY (id),
UNIQUE KEY uk_selfop_daily_version (sales_run_id,source_account,customer_code,warehouse_code,sku_code,business_date),
KEY idx_selfop_daily_window (sales_run_id,business_date,customer_code,sku_code),
KEY idx_selfop_daily_window_cover (sales_run_id,source_account,warehouse_code,business_date,customer_code,sku_code,outbound_quantity,outbound_order_count,quantity_unit),
KEY idx_selfop_daily_sku_cover (sales_run_id,source_account,warehouse_code,customer_code,sku_code,business_date,outbound_quantity,outbound_order_count,quantity_unit),
CONSTRAINT chk_selfop_sku_daily_sales CHECK (outbound_quantity >= 0 AND outbound_order_count >= 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='完整销量版本的稀疏日事实';
@@ -28,7 +28,8 @@ public final class AnalyticsReadSql {
}
return sql;
}
private static String cte(String kind){
private static String cte(AnalyticsSqlRequest request){
String kind=request.kind();
if("HOT".equals(kind))return hotSalesCte();
if("SLOW".equals(kind))return rankingCte();
String localEvidence="CONVERT_TZ(evidence_at_utc,'+00:00','+08:00')";
@@ -91,7 +92,11 @@ public final class AnalyticsReadSql {
// Aggregate facts before joining SKU evidence. Repeating boundary expressions
// inside every fact's SUM dominated the long-window MySQL execution profile.
int rangeStart=sql.indexOf(" ranged AS (");int rangeEnd=sql.indexOf(" metric_sales AS (",rangeStart);
sql=sql.substring(0,rangeStart)+" ranged AS (SELECT d.customer_code,d.sku_code,SUM(d.outbound_quantity) observed_sum,"
// Only the measured maximum chart window uses SKU-ordered coverage; short windows retain date-range access.
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,"
+"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
@@ -168,7 +173,7 @@ public final class AnalyticsReadSql {
public static String page(java.util.Map<String,Object> parameters){
var p=(AnalyticsSqlRequest)parameters.get("p");
if("HOT".equals(p.kind()))return typed(hotPage(),p);
return typed(cte(p.kind())+"SELECT customer_code,sku_code,product_name,quantity_unit,evidence_at_utc,unit_valid,observed_quantity,sku_order_count,"
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);
@@ -236,7 +241,7 @@ public final class AnalyticsReadSql {
"CASE WHEN inventory_quality<>'VALID' THEN CONCAT('INVENTORY_',inventory_quality) WHEN available_quantity<=0 THEN 'NO_AVAILABLE_INVENTORY' "
+"WHEN window_quantity IS NULL THEN 'SALES_WINDOW_UNAVAILABLE' ELSE 'OBSERVATION_INSUFFICIENT' END";
String eligible=filter(p.kind()).replace(" WHERE ","").strip();
return typed(cte(p.kind())+", classified AS (SELECT CASE WHEN "+eligible+" THEN 1 ELSE 0 END eligible,"
return typed(cte(p)+", classified AS (SELECT CASE WHEN "+eligible+" THEN 1 ELSE 0 END eligible,"
+reason+" reason FROM result), counts AS (SELECT eligible,CASE WHEN eligible=1 THEN NULL ELSE reason END reason,COUNT(*) amount "
+"FROM classified GROUP BY eligible,CASE WHEN eligible=1 THEN NULL ELSE reason END) "
+"SELECT LEAST(SUM(CASE WHEN eligible=1 THEN amount ELSE 0 END) OVER (),100) total,reason,"
@@ -3902,7 +3902,8 @@ CREATE TABLE ll_selfop_sku_daily_sales (
quantity_unit VARCHAR(32) NOT NULL COMMENT '同一统计维度内一致的业务单位',
PRIMARY KEY (id),
UNIQUE KEY uk_selfop_daily_version (sales_run_id,source_account,customer_code,warehouse_code,sku_code,business_date),
KEY idx_selfop_daily_window (sales_run_id,business_date,customer_code,sku_code),
KEY idx_selfop_daily_window_cover (sales_run_id,source_account,warehouse_code,business_date,customer_code,sku_code,outbound_quantity,outbound_order_count,quantity_unit),
KEY idx_selfop_daily_sku_cover (sales_run_id,source_account,warehouse_code,customer_code,sku_code,business_date,outbound_quantity,outbound_order_count,quantity_unit),
CONSTRAINT chk_selfop_sku_daily_sales CHECK (outbound_quantity >= 0 AND outbound_order_count >= 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='完整销量版本的稀疏日事实';
@@ -80,7 +80,11 @@ class SelfoperatedAnalyticsSchemaContractTest {
}
assertTrue(run.contains("uk_selfop_inventory_daily (source_account,warehouse_code,inventory_daily_sample_date)"));
assertTrue(run.contains("CONSTRAINT chk_selfop_inventory_daily CHECK"));
assertTrue(ddl.get("ll_selfop_sku_daily_sales").contains("idx_selfop_daily_window (sales_run_id,business_date,customer_code,sku_code)"));
var daily = ddl.get("ll_selfop_sku_daily_sales");
assertTrue(daily.contains("KEY idx_selfop_daily_window_cover (sales_run_id,source_account,warehouse_code,business_date,customer_code,sku_code,outbound_quantity,outbound_order_count,quantity_unit)"));
assertTrue(daily.contains("KEY idx_selfop_daily_sku_cover (sales_run_id,source_account,warehouse_code,customer_code,sku_code,business_date,outbound_quantity,outbound_order_count,quantity_unit)"));
assertFalse(daily.contains("KEY idx_selfop_daily_window ("));
assertTrue(daily.contains("UNIQUE KEY uk_selfop_daily_version (sales_run_id,source_account,customer_code,warehouse_code,sku_code,business_date)"));
assertTrue(ddl.get("ll_selfop_outbound_order").contains("idx_selfop_order_daily (source_account,warehouse_code,sales_eligible,business_date,id)"));
assertTrue(ddl.get("ll_selfop_sync_stage").contains("stage_batch_id,object_type,object_key_hash"));
assertTrue(ddl.get("ll_selfop_sync_stage").contains("observation_sequence IS NOT NULL"));
@@ -42,6 +42,42 @@ public class AnalyticsSqlSemanticTest {
public static LocalDateTime convertZone(LocalDateTime value,String from,String to) {
return value==null?null:value.atOffset(ZoneOffset.of(from)).withOffsetSameInstant(ZoneOffset.of(to)).toLocalDateTime();
}
@Test void skuOrderedCoverHintIsLimitedToExactly366DaySummary() {
for(int days:List.of(1,7,30,365,366,367)) {
var p=request("SUMMARY",D.minusDays(days),D,null,null,utc(D,8),0,20);
String sql=AnalyticsReadSql.page(Map.of("p",p));
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"));
assertEquals(sql.indexOf(hint),sql.lastIndexOf(hint));
}
assertFalse(AnalyticsReadSql.count(Map.of("p",p)).contains("INDEX("));
}
for(String kind:List.of("HOT","SLOW","INVENTORY")) {
var p=request(kind,D.minusDays(366),D,null,null,utc(D,8),0,20);
assertFalse(AnalyticsReadSql.page(Map.of("p",p)).contains("INDEX("),kind);
}
}
@Test void hinted366DaySummaryPreservesSparseZeroUnknownAndDateBoundaries() throws Exception {
LocalDate from=D.minusDays(365),until=D.plusDays(1);
profile(11,"C","SOLD","VALID_OUTBOUND",utc(from,0),"PCS");
profile(22,"C","ZERO","INVENTORY_OBSERVATION",utc(from,0),"PCS");
profile(22,"C","UNKNOWN","NONE",null,"PCS");
daily(11,"W","C","SOLD",from,"2");daily(11,"W","C","SOLD",D,"3");
daily(11,"W","C","SOLD",from.minusDays(1),"999");daily(11,"W","C","SOLD",until,"999");
daily(33,"W","C","SOLD",from,"999");daily(11,"OTHER","C","SOLD",from,"999");
var p=new AnalyticsSqlRequest("demo","W",null,null,11,22,from,until,D,D.minusDays(30),from,D,
utc(from,0),utc(D,8),true,true,false,true,true,0,20,"SUMMARY");
assertEquals(3,mapper.count(p));var rows=page(p);assertEquals(3,rows.size());
var sold=rows.get(0);assertEquals("SOLD",sold.skuCode());assertDecimal("5",sold.observedQuantity());
assertEquals(2L,sold.skuOrderCount());assertEquals(365,sold.completeDayCount());
assertEquals(1,sold.partialDayCount());assertEquals(0,sold.unknownDayCount());
var zero=rows.get(1);assertEquals("ZERO",zero.skuCode());assertDecimal("0",zero.observedQuantity());
assertEquals(365,zero.completeDayCount());assertEquals(1,zero.partialDayCount());
var unknown=rows.get(2);assertEquals("UNKNOWN",unknown.skuCode());assertNull(unknown.observedQuantity());
assertEquals(0,unknown.completeDayCount());assertEquals(0,unknown.partialDayCount());assertEquals(366,unknown.unknownDayCount());
}
@Test void sqlReturnsTwoCompleteDaysAndTodayPartialObservedFive() throws Exception {
profile(11,"C","SKU","VALID_OUTBOUND",utc(D.minusDays(40),0),"PCS");
profile(22,"C","SKU","INVENTORY_OBSERVATION",utc(D.minusDays(35),0),"PCS");