V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
drymonfidelia
V2EX  ›  .NET

ASP .NET Core + EF Core + MySQL 这个统计查询在 3 亿条记录的表下每次查询都需要 2~4 分钟,优化的办法只有用触发器或是后台每小时定期统计吗?

  •  1
     
  •   drymonfidelia · 4 天前 · 1309 次点击

    虽然后台慢不是不能用,但是销售一直在反映操作很慢让优化。销售后台时间选择器粒度只精确到天,不知道这个地方是不是可以做什么效果比较好的优化。

    正常情况下日订单在五百万左右。这个页面需要显示每个 SKU 在不同阶段(状态)的订单的数量。有考虑过用 Redis 但是我们的 Redis 是单机的只用来缓存,经常 flushall 。再单独加一台 Redis 觉得不划算

    return Ok(dbContext.Orders.Where(x => x.CreatedAt >= DateTimeOffset.FromUnixTimeMilliseconds(queryForm.StartDate) &&
                                                 x.CreatedAt <= DateTimeOffset.FromUnixTimeMilliseconds(queryForm.EndDate))
                .Include(x => x.Sku).Where(x => x.Sku != null)
                .GroupBy(o => o.SkuId)
                .Select(g => new
                {
                    SkuId = g.Key,
                    SkuName = g.Select(o => o.Sku.TitleEng).FirstOrDefault(),
                    Delivering = g.Count(o => o.Status == (int)OrderStatusEnum.Delivering),
                    Cancelled = g.Count(o => o.Status == (int)OrderStatusEnum.Cancelled),
                    InProcess = g.Count(o => o.Status == (int)OrderStatusEnum.InProcess),
                    InReview = g.Count(o => o.ReviewTasks.Any(t => t.Pending && t.Result == false)),
                    Total = g.Count()
                })
                .ToList());
    

    能想到的索引都已经加了

    [Index(nameof(Status))]
    [Index(nameof(Input))]
    [Index(nameof(SkuId))]
    [Index(nameof(UserId))]
    [Index(nameof(CreatedAt))]
    [Index(nameof(UpdatedAt))]
    [Index(nameof(OrderTag))]
    [Index(nameof(SendPending))]
    [Index(nameof(OrderSource))]
    [Index(nameof(UserId), nameof(SkuId), nameof(FromMobileApp))]
    [Index(nameof(UserId), nameof(Status))]
    [Index(nameof(SkuId), nameof(Status))]
    [Index(nameof(Status), nameof(RiskyScore))]
    [Index(nameof(UserId), nameof(Input))]
    [Index(nameof(UserId), nameof(InputTailing))]
    public class Order : BaseEntity
    { ... }
    
    19 条回复    2025-01-15 00:14:05 +08:00
    hez2010
        1
    hez2010  
       4 天前
    1. 你在代码里加的索引有通过 migration 应用到数据库吗?没同步到数据库表里面是没用的。
    2. 建议用异步方法 ToListAsync 。
    3. 你可以看看具体生成了什么 SQL ,你这个需求完全没有必要在 SQL 做 GroupBy 和 Select ,你可以先 Select 出来然后 ToList 再在应用端进行 GroupBy ,比如像下面这样,毕竟 MySQL 的数据库引擎的索引做的本身就完全是依托答辩,最好只把 MySQL 当作一个大号 KV 来用。
    ```cs
    return Ok((await dbContext.Orders.Where(x => x.CreatedAt >= DateTimeOffset.FromUnixTimeMilliseconds(queryForm.StartDate) &&
    x.CreatedAt <= DateTimeOffset.FromUnixTimeMilliseconds(queryForm.EndDate))
    .Include(x => x.Sku).Where(x => x.Sku != null)
    .Select(o => new
    {
    SkuId = o.SkuId,
    SkuName = o.Sku.TitleEng,
    Status = o.Status,
    ReviewTasks = o.ReviewTasks
    })
    .ToListAsync())
    .GroupBy(o => o.SkuId)
    .Select(g => new
    {
    SkuId = g.Key,
    SkuName = g.Select(o => o.SkuName).FirstOrDefault(),
    Delivering = g.Count(o => o.Status == (int)OrderStatusEnum.Delivering),
    Cancelled = g.Count(o => o.Status == (int)OrderStatusEnum.Cancelled),
    InProcess = g.Count(o => o.Status == (int)OrderStatusEnum.InProcess),
    InReview = g.Count(o => o.ReviewTasks.Any(t => t.Pending && t.Result == false)),
    Total = g.Count()
    })
    .ToList());
    ```
    drymonfidelia
        2
    drymonfidelia  
    OP
       4 天前
    @hez2010
    1. 有,这些索引是很早以前就加的,在 MySQL 里看了有应用
    3. 感谢,我测试下这种实现的效果
    sagaxu
        3
    sagaxu  
       4 天前
    日订单在五百万个还请不起 DBA 或者资深开发嘛,单表 15 个索引,且好几个重复无意义
    drymonfidelia
        4
    drymonfidelia  
    OP
       4 天前
    @sagaxu 以前有,离职了没招到新的。而且订单单价低,利润率不高
    drymonfidelia
        5
    drymonfidelia  
    OP
       4 天前
    @sagaxu 这些索引为什么是无意义的?都是根据代码里不同查询建的,MySQL 好像没有像 MongoDB 那样自动统计不同索引命中次数的功能,不懂怎么看哪些没用
    MoYi123
        6
    MoYi123  
       4 天前   ❤️ 1
    问数据库优化的问题不贴 explain 就算了, 现在连 sql 都没有了.
    yinmin
        7
    yinmin  
       4 天前 via iPhone
    直接在 mysql 里用 sql 的 select ,看看需要多久时间。另外,每天 500 万条记录,通常是需要每天凌晨做昨天数据预汇总(数据清洗),例如按小时先预汇总一次,然后从汇总表里取数据。
    yinmin
        8
    yinmin  
       4 天前 via iPhone
    清洗掉客户 id ,按小时(或者 15 分钟)汇总订单数据到“订单汇总表 1”,然后再按天汇总到“订单汇总表 2”。根据查询颗粒度,从“订单汇总表 1”或“订单汇总表 2”取数应该能优化到几秒的级别吧
    lbp0200
        9
    lbp0200  
       4 天前
    统计分析,不适合 MySQL ,建议用分析型数据库,比如 duckdb
    bsg1992
        10
    bsg1992  
       4 天前
    每天 500 万 一年就得 18 亿的数据 你确定 mysql 能扛得住?
    zhangeric
        11
    zhangeric  
       4 天前
    按时间分表呗,可以用 shardingcore 这个库
    zhuyw2006
        12
    zhuyw2006  
       4 天前
    简单的用 EFCORE ,复杂一点的用 Dapper+原生 SQL 比较方便优化
    netnr
        13
    netnr  
       4 天前
    @MoYi123 OP 给出的 LINQ 就是真实的业务场景,给 SQL 反而是包装问题再提问

    如 #9 所说,引入 DuckDB 直接统计应该能秒出,按时同步数据即可;

    另外,可以先尝试一下用 DuckDB 附加 MySQL 再直接执行 SQL ,可能有改进,
    我们有一个场景,在 MySQL 查询需要 16s ,通过 DuckDB 来查询降低到 4s
    encro
        14
    encro  
       4 天前
    你这个查询主要是 sku_id 和 CreatedAt ,需要索引是亿 sku_id 分区,以 createdAt 排序。

    另外,请开启慢查询日志。

    如果缺少 dba 我可以远程兼任下,一个月收费 2000 ,负责帮助发现问题以及给出解决方案。
    encro
        15
    encro  
       4 天前
    为什么 order 表会有 skuId ,一个 sku 一个订单?
    niubiman
        16
    niubiman  
       4 天前
    这个查询用 linq 手动 join 效果会更好一写
    drymonfidelia
        17
    drymonfidelia  
    OP
       3 天前
    @encro 是的,因为每个订单都要有独立的购买者信息,只能一个 sku 一个订单,导致订单量很大。
    @bsg1992 超过 3 个月的订单会归档。
    @lbp0200 DuckDB 有办法配合 MySQL 使用么?还是每条数据都需要写两次
    drymonfidelia
        18
    drymonfidelia  
    OP
       3 天前
    @zhangeric 有考虑过分表,但是如果按日分表,用户看历史订单需要同时查好几个表不知道效率会不会更低?按月分表的话感觉意义不是很大,因为这个场景是查指定日期间的统计数据,按理说不应该扫全表吧?
    @yinmin 预汇总有个问题就是订单的状态(处理中、运输中、已收货等)是会改变的,业务需要每个状态的订单数量。我觉得这样预汇总的数据就不太能用了,不知道有没有办法能解决
    MOONLIGHTT
        19
    MOONLIGHTT  
       3 天前
    看下 druid 吧,很成熟了已经。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2639 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 04:53 · PVG 12:53 · LAX 20:53 · JFK 23:53
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.