From aaee077253d72a88443ac701a439a3546de4c834 Mon Sep 17 00:00:00 2001 From: Yvan <8574526@qq,com> Date: Thu, 5 Nov 2026 17:11:59 +0800 Subject: [PATCH] perf: avoid correlated channel scans in portrait queries --- internal/data/business/business_portrait.go | 12 +++++++----- internal/data/business/channel_metrics.go | 2 +- internal/data/business/channel_portrait.go | 6 +++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/internal/data/business/business_portrait.go b/internal/data/business/business_portrait.go index 7e9a3f7..82d9062 100644 --- a/internal/data/business/business_portrait.go +++ b/internal/data/business/business_portrait.go @@ -96,11 +96,13 @@ func (r *businessPortraitRepo) PlatformBusiness(ctx context.Context, key busines return &business.PortraitPlatformBusiness{ID: po.ID, Account: po.Account, RealName: po.RealName, Level: po.Level, Status: po.Status, Commerce1: po.Commerce1, Commerce2: po.Commerce2, CreatedUnix: po.CreatedUnix}, nil } -func portraitPromoteScope(db *gorm.DB, scope business.PortraitReportScope, sourceColumn, promoteColumn string) *gorm.DB { +// Build an uncorrelated channel set once, so MySQL can use a semijoin instead +// of reevaluating a descendant-tree predicate for each historical event. +func portraitPromoteScope(db *gorm.DB, scope business.PortraitReportScope) *gorm.DB { if scope.Root != nil { - return channelGameTree(db, business.ChannelGameScope{Root: scope.Root, IncludeSub: scope.IncludeSub}).Select("1").Where("p.source_db = " + sourceColumn + " AND p.id = " + promoteColumn) + return channelGameTree(db, business.ChannelGameScope{Root: scope.Root, IncludeSub: scope.IncludeSub}).Select("p.source_db,p.id") } - q := db.Table("tab_promote scope_promote").Select("1").Where("scope_promote.source_db = " + sourceColumn + " AND scope_promote.id = " + promoteColumn) + q := db.Table("tab_promote scope_promote").Select("scope_promote.source_db,scope_promote.id") if len(scope.Pairs) == 0 { return q.Where("1=0") } @@ -115,7 +117,7 @@ func portraitReportQuery(db *gorm.DB, scope business.PortraitReportScope, table, if len(scope.Sources) > 0 { q = q.Where("r.source_db IN ?", scope.Sources) } - return q.Where("EXISTS (?)", portraitPromoteScope(db, scope, "r.source_db", "r.promote_id")) + return q.Where("(r.source_db,r.promote_id) IN (?)", portraitPromoteScope(db, scope)) } func portraitIdentity(db *gorm.DB, dedup bool) string { if dedup { @@ -249,7 +251,7 @@ func portraitHourly(db *gorm.DB, scope business.PortraitReportScope, result *bus table, column, aggregate, condition string series map[string]string }{{"tab_user", "register_time", "COUNT(*)", "(r.puid = 0 OR r.puid IS NULL)", result.RegisterSeries}, {"tab_spend", "pay_time", "COALESCE(SUM(r.pay_amount),0)", "r.pay_status = 1", result.PaySeries}} { - q := db.Table(metric.table+" r").Where("r."+metric.column+" >= ? AND r."+metric.column+" < ?", start, end).Where(metric.condition).Where("EXISTS (?)", portraitPromoteScope(db, scope, "r.source_db", "r.promote_id")) + q := db.Table(metric.table+" r").Where("r."+metric.column+" >= ? AND r."+metric.column+" < ?", start, end).Where(metric.condition).Where("(r.source_db,r.promote_id) IN (?)", portraitPromoteScope(db, scope)) if len(scope.Sources) > 0 { q = q.Where("r.source_db IN ?", scope.Sources) } diff --git a/internal/data/business/channel_metrics.go b/internal/data/business/channel_metrics.go index 404aa1b..95863de 100644 --- a/internal/data/business/channel_metrics.go +++ b/internal/data/business/channel_metrics.go @@ -125,7 +125,7 @@ func (r *channelRepo) Games(ctx context.Context, scope business.ChannelGameScope mode = 1 } query := func(table string, dedup int) *gorm.DB { - return db.Table(table+" r").Where("r.source_db = ? AND r.game_id > 0 AND r.dedup_mode = ?", scope.Root.SourceDB, dedup).Where("EXISTS (?)", channelGameTree(db, scope).Select("1").Where("p.id=r.promote_id AND p.source_db=r.source_db")) + return db.Table(table+" r").Where("r.source_db = ? AND r.game_id > 0 AND r.dedup_mode = ?", scope.Root.SourceDB, dedup).Where("(r.source_db,r.promote_id) IN (?)", channelGameTree(db, scope).Select("p.source_db,p.id")) } // 累计起点按当前人数口径的日汇总最早日期决定,与原画像查询一致。 begin := scope.BeginDate diff --git a/internal/data/business/channel_portrait.go b/internal/data/business/channel_portrait.go index d193ca0..5d9f9dc 100644 --- a/internal/data/business/channel_portrait.go +++ b/internal/data/business/channel_portrait.go @@ -42,7 +42,7 @@ func (r *channelPortraitRepo) EarliestDate(ctx context.Context, scope business.C mode = 1 } var result struct{ FirstDate *string } - q := db.Table("tab_sw_report_day r").Where("r.source_db = ? AND r.dedup_mode = ? AND r.game_id > 0", scope.Root.SourceDB, mode).Where("EXISTS (?)", channelGameTree(db, scope).Select("1").Where("p.source_db=r.source_db AND p.id=r.promote_id")) + q := db.Table("tab_sw_report_day r").Where("r.source_db = ? AND r.dedup_mode = ? AND r.game_id > 0", scope.Root.SourceDB, mode).Where("(r.source_db,r.promote_id) IN (?)", channelGameTree(db, scope).Select("p.source_db,p.id")) if err := q.Select("CAST(MIN(r.stat_date) AS CHAR) AS first_date").Scan(&result).Error; err != nil { return "", false, business.ErrStorage } @@ -56,7 +56,7 @@ func channelTierKeys(db *gorm.DB, scope business.ChannelGameScope) *gorm.DB { if scope.IDCardDedup { mode = 1 } - return db.Table("tab_sw_report_day_key r").Where("r.source_db = ? AND r.dedup_mode = ? AND r.game_id > 0 AND r.metric = 1 AND r.stat_date >= ? AND r.stat_date <= ? AND r.identity_key IS NOT NULL", scope.Root.SourceDB, mode, scope.BeginDate, scope.EndDate).Where("EXISTS (?)", channelGameTree(db, scope).Select("1").Where("p.source_db=r.source_db AND p.id=r.promote_id")) + return db.Table("tab_sw_report_day_key r").Where("r.source_db = ? AND r.dedup_mode = ? AND r.game_id > 0 AND r.metric = 1 AND r.stat_date >= ? AND r.stat_date <= ? AND r.identity_key IS NOT NULL", scope.Root.SourceDB, mode, scope.BeginDate, scope.EndDate).Where("(r.source_db,r.promote_id) IN (?)", channelGameTree(db, scope).Select("p.source_db,p.id")) } func (r *channelPortraitRepo) TierData(ctx context.Context, scope business.ChannelGameScope) (*business.ChannelTierData, error) { result := &business.ChannelTierData{Available: true, RegisterKeys: []string{}, Payments: []*business.ChannelIdentityPay{}} @@ -94,7 +94,7 @@ func (r *channelPortraitRepo) TierData(ctx context.Context, scope business.Chann } } // 分层金额按历史成功支付累计,不受画像周期或注册日期范围限制。 - q := db.Table("tab_spend s").Where("s.source_db = ? AND s.pay_status = 1", scope.Root.SourceDB).Where("EXISTS (?)", channelGameTree(db, scope).Select("1").Where("p.source_db=s.source_db AND p.id=s.promote_id")) + q := db.Table("tab_spend s").Where("s.source_db = ? AND s.pay_status = 1", scope.Root.SourceDB).Where("(s.source_db,s.promote_id) IN (?)", channelGameTree(db, scope).Select("p.source_db,p.id")) if scope.IDCardDedup { var rows []struct { IDCard *string `gorm:"column:idcard"`