You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

大数据场景下关联JSONB列的Count查询优化求助

超大规模JSONB表关联去重计数的优化方案

问题场景

需基于JSONB列内嵌套对象过滤,同时关联另一张表进行去重计数。原查询语句如下:

SELECT COUNT(*) AS total
FROM (
    SELECT DISTINCT s.mid
    FROM "whatsapp-cloud".statuses s
    LEFT JOIN "whatsapp-cloud".outbounds o ON s.mid = o.mid 
    WHERE s."data"->'entry'->0->'changes'->0->'value'->'statuses'->0->'conversation'->'origin'->>'type' = 'utility'
        AND s."type" = 'delivered'
        AND s.created_at::date = '2023-02-28'::date
        and s.account_id  = {account_id}
        AND o."type" = 'template'
) AS counter;

涉及表信息

核心表whatsapp-cloud.statuses存储空间达77GB,约43492221行数据。两张表的DDL结构如下:

statuses表DDL

CREATE TABLE "whatsapp-cloud".statuses (
id varchar(50) NOT NULL DEFAULT uuid_generate_v1()::character varying(50),
client_id varchar(50) NOT NULL,
channel_id varchar(50) NOT NULL,
account_id varchar(50) NOT NULL,
application_id varchar(50) NOT NULL,
"oid" varchar(150) NULL,
xid varchar(100) NULL,
mid varchar(200) NOT NULL,
"type" varchar(50) NULL,
error varchar(50) NULL,
"data" jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT statuses_pkey PRIMARY KEY (id));
CREATE INDEX idx_statuses_account_id ON "whatsapp-cloud".statuses USING btree (account_id);
CREATE INDEX idx_statuses_channel_id ON "whatsapp-cloud".statuses USING btree (channel_id);
CREATE INDEX idx_statuses_cid ON "whatsapp-cloud".statuses USING btree (oid);
CREATE INDEX idx_statuses_client_id ON "whatsapp-cloud".statuses USING btree (client_id);
CREATE INDEX idx_statuses_mid ON "whatsapp-cloud".statuses USING btree (mid);
CREATE INDEX idx_statuses_xid ON "whatsapp-cloud".statuses USING btree (xid);
CREATE INDEX idx_whatsapp_cloud_type ON "whatsapp-cloud".statuses USING btree (type);
CREATE INDEX statuses_date_of_tz_idx ON "whatsapp-cloud".statuses USING btree (date_of_tz(created_at));

outbounds表DDL

CREATE TABLE "whatsapp-cloud".outbounds (
id varchar(50) NOT NULL DEFAULT uuid_generate_v1()::character varying(50),
xid varchar(100) NULL,
mid varchar(200) NULL,
client_id varchar(50) NOT NULL,
channel_id varchar(50) NOT NULL,
account_id varchar(50) NOT NULL,
application_id varchar(50) NULL,
"type" varchar(50) NULL,
recipient varchar(350) NOT NULL,
error varchar(50) NULL,
"data" jsonb NOT NULL,
"result" jsonb NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamp NULL,
CONSTRAINT outbounds_pkey PRIMARY KEY (id));
CREATE INDEX idx_outbound_created_at ON "whatsapp-cloud".outbounds USING btree (((timezone('UTC'::text, created_at))::date));
CREATE INDEX idx_outbounds_account_id ON "whatsapp-cloud".outbounds USING btree (account_id);
CREATE INDEX idx_outbounds_channel_id ON "whatsapp-cloud".outbounds USING btree (channel_id);
CREATE INDEX idx_outbounds_client_id ON "whatsapp-cloud".outbounds USING btree (client_id);
CREATE INDEX idx_outbounds_mid ON "whatsapp-cloud".outbounds USING btree (mid);
CREATE INDEX idx_outbounds_xid ON "whatsapp-cloud".outbounds USING btree (xid);
CREATE INDEX outbounds_date_of_tz_idx ON "whatsapp-cloud".outbounds USING btree (date_of_tz(created_at));
CREATE INDEX outbounds_month_of_tz_idx ON "whatsapp-cloud".outbounds USING btree (month_of_tz(created_at));
CREATE INDEX outbounds_type_idx ON "whatsapp-cloud".outbounds USING btree (type);
CREATE INDEX outbounds_year_of_tz_idx ON "whatsapp-cloud".outbounds USING btree (year_of_tz(created_at));
CREATE UNIQUE INDEX uidx_outbounds_xid ON "whatsapp-cloud".outbounds USING btree (xid, client_id);

性能问题

因JSONB嵌套字段过滤+表关联,查询执行耗时差异极大:结果为0时约1秒,结果为1万/10万条时耗时剧增。每日需针对数百个account_id执行该查询,CRON任务从00:01开始,通常耗时超13小时,最长近17小时。

Count=0时的EXPLAIN ANALYZE结果:

Aggregate  (cost=23344.51..23344.52 rows=1 width=8) (actual time=0.089..0.090 rows=1 loops=1)
  Output: count(*)
  ->  Unique  (cost=23344.49..23344.50 rows=1 width=62) (actual time=0.085..0.086 rows=0 loops=1)
        Output: s.mid
        ->  Sort  (cost=23344.49..23344.49 rows=1 width=62) (actual time=0.084..0.085 rows=0 loops=1)
              Output: s.mid
              Sort Key: s.mid
              Sort Method: quicksort  Memory: 25kB
              ->  Nested Loop  (cost=1.13..23344.48 rows=1 width=62) (actual time=0.048..0.049 rows=0 loops=1)
                    Output: s.mid
                    ->  Index Scan using idx_statuses_account_id on "whatsapp-cloud".statuses s  (cost=0.56..23335.89 rows=1 width=62) (actual time=0.047..0.048 rows=0 loops=1)
                          Output: s.id, s.client_id, s.channel_id, s.account_id, s.application_id, s.oid, s.xid, s.mid, s.type, s.error, s.data, s.created_at
                          Index Cond: ((s.account_id)::text = '{account_id}'::text)
                          Filter: (((s.type)::text = 'delivered'::text) AND ((s.created_at)::date = '2023-02-28'::date) AND (((((((((((s.data -> 'entry'::text) -> 0) -> 'changes'::text) -> 0) -> 'value'::text) -> 'statuses'::text) -> 0) -> 'conversation'::text) -> 'origin'::text) ->> 'type'::text) = 'utility'::text))
                    ->  Index Scan using idx_outbounds_mid on "whatsapp-cloud".outbounds o  (cost=0.56..8.58 rows=1 width=62) (never executed)
                          Output: o.id, o.xid, o.mid, o.client_id, o.channel_id, o.account_id, o.application_id, o.type, o.recipient, o.error, o.data, o.result, o.created_at, o.updated_at
                          Index Cond: ((o.mid)::text = (s.mid)::text)
                          Filter: ((o.type)::text = 'template'::text)
Planning Time: 316.699 ms
Execution Time: 0.258 ms

优化方案

1. 重构查询逻辑,替换LEFT JOIN为EXISTS子查询

原查询中LEFT JOIN后添加o.type = 'template'过滤,实际等价于INNER JOIN,使用EXISTS可以避免DISTINCT的排序开销(原计划中出现了Sort和Unique步骤),且EXISTS找到匹配项后立即停止扫描,性能更优。修改后的查询:

SELECT COUNT(DISTINCT s.mid) AS total
FROM "whatsapp-cloud".statuses s
WHERE s.account_id = {account_id}
  AND s.type = 'delivered'
  AND s.created_at::date = '2023-02-28'::date
  AND s.data->'entry'->0->'changes'->0->'value'->'statuses'->0->'conversation'->'origin'->>'type' = 'utility'
  AND EXISTS (
    SELECT 1
    FROM "whatsapp-cloud".outbounds o
    WHERE o.mid = s.mid
      AND o.type = 'template'
  );

2. 创建JSONB字段的复合表达式索引

原查询中JSONB的嵌套路径固定,创建包含所有过滤条件的复合表达式索引,避免每次查询解析JSONB并回表:

CREATE INDEX idx_statuses_filter_full 
ON "whatsapp-cloud".statuses 
USING btree (
  account_id,
  type,
  (created_at::date),
  ((data->'entry'->0->'changes'->0->'value'->'statuses'->0->'conversation'->'origin'->>'type'))
);

该索引覆盖了account_id、type、created_at日期、JSONB嵌套的type字段,查询可直接走索引扫描,无需访问表数据。

3. 利用已有的时区日期索引

原查询使用s.created_at::date,如果date_of_tz是业务中统一的时区处理函数,建议将过滤条件改为:

date_of_tz(s.created_at) = '2023-02-28'::date

这样可以直接利用已创建的statuses_date_of_tz_idx索引,避免类型转换的额外开销。

4. 优化outbounds表的查询索引

针对EXISTS子查询的条件mid = s.mid AND type = 'template',创建复合索引:

CREATE INDEX idx_outbounds_mid_type 
ON "whatsapp-cloud".outbounds 
USING btree (mid, type);

该索引可以直接定位到符合条件的记录,无需先扫描mid索引再过滤type。

5. 批量处理多个account_id

避免循环逐个查询数百个account_id,改为批量查询减少规划时间(原单查询规划时间达316ms):

SELECT s.account_id, COUNT(DISTINCT s.mid) AS total
FROM "whatsapp-cloud".statuses s
WHERE date_of_tz(s.created_at) = '2023-02-28'::date
  AND s.type = 'delivered'
  AND s.data->'entry'->0->'changes'->0->'value'->'statuses'->0->'conversation'->'origin'->>'type' = 'utility'
  AND EXISTS (
    SELECT 1
    FROM "whatsapp-cloud".outbounds o
    WHERE o.mid = s.mid
      AND o.type = 'template'
  )
  AND s.account_id IN ('acc_1', 'acc_2', ...) -- 批量传入需要统计的account_id列表
GROUP BY s.account_id;

6. 可选:去除不必要的DISTINCT

如果业务上statuses表中同一个mid不会重复满足所有过滤条件,可以去掉COUNT(DISTINCT),直接使用COUNT(*)进一步提升性能,但需先确认业务逻辑。


内容的提问来源于stack exchange,提问作者Ibnu Agiels Althur

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 20:22:30