摘要本文通过一个线上告警案例剖析了电科金仓数据库(KES)中DISTINCT关键字的常见问题与优化策略。文章指出DISTINCT看似简单但暗藏陷阱在5000万行/月的大表查询中未走索引的全分区扫描导致CPU飙升至95%。作者详细解析了DISTINCT的执行原理HashAggregate和SortUnique两种方式和典型应用场景包括单列去重计数、分组内去重计数等并给出具体优化方案合理设置work_mem、建立覆盖索引、利用分区剪枝和并行查询等。文中特别强调要避免用DISTINCT掩盖JOIN问题注意NULL值处理并对比了DISTINCT与GROUP BY的性能差异。最后建议优化顺序应为先分析执行计划再调内存参数最后考虑SQL改写。电科金仓里 DISTINCT 那点事从一次线上告警说起上周三凌晨两点运维群里 我说某条统计 SQL 把 KES 一个节点的 CPU 干到 95%业务那边报表刷不出来问我能不能紧急看看。点开 Grafana慢查询榜首那条长这样SELECT DISTINCT user_id, shop_id FROM order_logs WHERE action_time 2024-01-01 AND action_time 2024-02-01;order_logs这张表我们这边的体量大概是 5000 万行 / 月一月的分区大概 1600 万。user_id shop_id去重没走索引走了全分区扫描 HashAggregatework_mem默认 4MB 根本扛不住落盘了所以慢。这事其实挺典型——DISTINCT看着简单坑都在细节里。借这个机会把电科金仓里DISTINCT这套东西拆一下顺手给后面踩坑的人省点时间。先把语义掰扯清楚很多人以为DISTINCT col1, col2是对 col1 去重、col2 随便留一个不对。是整行的唯一性。上面那条 SQL 的意思是(user_id, shop_id)这二元组去重不是单独给 user_id 去重。这点不搞清后面写 JOIN 的时候必翻车下面会说。KES 里DISTINCT落到执行器层面主要两条路HashAggregate扫数据往内存里塞哈希表key 是去重列的组合值撞了就丢。内存够的话这是最快的路子。Sort Unique哈希表塞不下或者优化器觉得排序更划算就先排再扫相邻行去重。I/O 代价高能避就避。走哪条看表大小、work_mem、统计信息explain 一下就知道了。我习惯先看EXPLAIN (ANALYZE, BUFFERS)比猜靠谱。几个我实际碰到的场景1单列去重计数SELECT COUNT(DISTINCT user_id) FROM order_logs;这条如果user_id上有索引KES 一般会选 Index Only Scan HashAggregate还行。但我们这边的坑是——order_logs的user_id原来没单独建索引是挂在复合索引第二个字段上的优化器没用上改成单列索引之后从 3.4s 降到 200ms。 顺手提一句KES 的索引选择逻辑对复合索引的前缀匹配敏感去重查询如果只用到复合索引的后缀字段经常走不上得单独补。2分组内去重计数SELECT shop_id, COUNT(DISTINCT user_id) FROM order_logs GROUP BY shop_id;这条是那晚告警的真身变种。问题不在 SQL 本身在work_mem4MB扛不住 1600 万行的哈希表落盘之后磁盘 I/O 直接炸。临时救活的办法有两个SET work_mem 512MB;会话级拉一下让哈希表留在内存里长期方案是这报表改物化视图凌晨刷一次白天查 MV没必要实时扫原表。我们后来选了 MV报表接口从 8s 降到 120ms运维群终于安静了。3数据清洗时的整行去重有个历史坑早期落库逻辑有 bug同一笔订单的order_id user_id shop_id action_time status会重复插差的是自增主键log_id。这时候你要这么写才对SELECT DISTINCT order_id, user_id, shop_id, action_time, status FROM order_logs;千万别SELECT DISTINCT *——log_id是主键每行都不一样DISTINCT 了个寂寞。这种问题新人容易栽我面人时候专门问过。几个容易踩的坑 用 DISTINCT 盖 JOIN 的臭虫见过太多这种代码SELECT DISTINCT u.name, o.order_id FROM users u LEFT JOIN orders o ON u.id o.user_id;一个用户 N 个订单JOIN 完必然 N 行开发嫌多就套个 DISTINCT 压回去。这是把药吃反了——正确路子是先对 orders 聚合 / 用 EXISTS让 JOIN 的基数本身就对再去谈去重。 NULL 的处理KES 里DISTINCT把所有 NULL 当相等的也就是说SELECT DISTINCT col里 NULL 只返一行。这点跟 Oracle 行为一致但从 MySQL 切过来的人有时候会懵以为 NULL 不算、会返多行。 DISTINCT vs GROUP BY网上老帖子里常说DISTINCT 比 GROUP BY 快放现在 KES 这代内核里基本不成立。单列的SELECT DISTINCT col优化器自己就重写成GROUP BY了执行计划一样挑哪个看你们团队 coding 风格就行。能让它跑快点的一些招覆盖索引如果去重的列就那么两三个复合索引直接盖住CREATE INDEX idx_shop_user ON order_logs(shop_id, user_id);SELECT DISTINCT shop_id, user_id就能纯走索引不回表I/O 省一大截。我们这边这索引建完那个 8s 的查询直接腰斩到 3s再加work_mem才到 120ms 那档。分区剪枝别漏条件order_logs我们是按月action_time分区的去重查询一定要把分区键带上传入不然优化器傻傻扫全表。上面那告警 SQL 其实条件是对的但有个同业务的变体没带action_time扫了 12 个分区那就是另一张工单了。并行KES 开得动并行的话大表 DISTINCT 收益明显。max_parallel_workers和parallel_setup_cost这两个参数得对着你们机器核数调不是越大越好——我们 64C 的机器上max_parallel_workers_per_gather设 4 的时候性价比最高再往上锁竞争反而拖。窗口函数有时候比 DISTINCT ON 稳每组取最新一条这种需求有人爱写DISTINCT ON (order_id) ... ORDER BY action_time DESC但我这边压测下来KES 对窗口函数 CTE 的优化反而更稳尤其数据倾斜的时候WITH t AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY order_id ORDER BY action_time DESC ) rn FROM order_logs ) SELECT * FROM t WHERE rn 1;order_id上有索引的话这套执行计划的稳定性比DISTINCT ON好倾斜严重的组不会抖。说回来DISTINCT这东西吧语法就那么几个字母但真要放到 KES 的生产环境里跑牵扯的是索引、内存、分区、并行一整套。我个人的习惯是看到慢的 DISTINCT 查询先explain analyze看走的是 Hash 还是 Sort再看work_mem够不够再看索引是不是真用上了最后才考虑改 SQL 写法。顺序别反反了就是瞎调。电科金仓这边的内核这几年迭代得挺快去重这块向量化、并行还在持续往里加后面应该会更省心。但眼下嘛——索引先建对参数先配好比啥都管用。文中案例来自我这边测试环境的慢查询治理表名 / 字段脱敏过数值是脱敏后的近似量级直接抄 SQL 前先在测试库 explain 一下别直接上生产。金仓社区 - 电科金仓官方技术社区