1 背景
现在pg15中有一个商超方面的业务环境:
表tab1 中存储了集团sku状态字段v_status 1为可售、0为不可售。约50万数据,每个sku都是唯一。其他字段是sku的一些附属信息,比如价格、规格、供应商等等;
表tab2 中存储了各个分店sku状态字段v_status 1为可售、0为不可售。约100家店,表总共5000万行数据,每个sku+店铺号是唯一。其他字段是分店对应的sku相关信息;
在分店确定sku是否可售时,需要根据:
1)分店sku 可售、集团sku 可售,则分店sku可售
2)分店sku 可售、集团sku 不可售,则分店sku不可售
3)分店sku 不可售、集团sku 可售,则分店sku不可售
4)分店sku 不可售、集团sku 不可售,则分店sku不可售
根据上面条件创建表tab1 、tab12 ;并使用pg_roaringbitmap、intarray、内置位数组 计算,某个分店不可售的sku有哪些?
2 表结构设计与业务逻辑理解
2.1 集团 SKU 状态表 (tab1
)
-- 集团 SKU 状态表
CREATE TABLE tab1 (
sku_id INT PRIMARY KEY, -- SKU 唯一标识
v_status SMALLINT NOT NULL, -- 状态 (1=可售, 0=不可售)
price NUMERIC(10,2), -- 价格
supplier TEXT -- 供应商(其他字段省略)
);
-- 插入示例数据(假设 50 万 SKU,其中 80% 可售)
INSERT INTO tab1 (sku_id, v_status,price ,supplier)
SELECT id, CASE WHEN random() < 0.8 THEN 1 ELSE 0 END,id,'jks ssca 大润发 金客隆 盒马 山姆'
FROM generate_series(1, 500000) AS id;
2.2 分店 SKU 状态表 (tab2
)
-- 分店 SKU 状态表
CREATE TABLE tab2 (
store_id INT NOT NULL, -- 分店 ID (共 100 家)
sku_id INT NOT NULL, -- SKU 唯一标识
v_status SMALLINT NOT NULL, -- 状态 (1=可售, 0=不可售)
stock INT, -- 库存(其他字段省略)
PRIMARY KEY (store_id, sku_id)
);
-- 插入示例数据(假设每个分店有 50 万 SKU,其中 70% 可售)
INSERT INTO tab2 (store_id, sku_id, v_status)
SELECT
store,
sku,
CASE WHEN random() < 0.7 THEN 1 ELSE 0 END
FROM generate_series(1, 100) AS store, -- 100 家分店
generate_series(1, 500000) AS sku; -- 每个分店 50 万 SKU
2.3 不可售 SKU 查询逻辑
根据规则,分店 SKU 不可售的条件为以下任意一种:
-
分店 SKU 可售,但集团 SKU 不可售。
-
分店 SKU 不可售,无论集团 SKU 状态如何。
即 不可售条件:(分店状态 = 1 AND 集团状态 = 0) OR (分店状态 = 0)
。
3 不同技术方案实现
3.1 使用 pg_roaringbitmap
SELECT extname, extversion FROM pg_extension WHERE extname = 'roaringbitmap';
CREATE EXTENSION roaringbitmap;
1. 创建位图表(存储可售 SKU)
-- 集团可售 SKU 位图
CREATE MATERIALIZED VIEW mv_group_roaring AS
SELECT rb_build_agg(sku_id) AS bitmap
FROM tab1
WHERE v_status = 1;
-- 分店可售 SKU 位图(以分店 1 为例)
CREATE MATERIALIZED VIEW mv_store1_roaring AS
SELECT store_id,rb_build_agg(sku_id) AS bitmap
FROM tab2
WHERE v_status = 1
group by store_id;
2. 查询分店 1 不可售的 SKU-- 计算不可售 SKU = (分店可售 SKU 位图 AND 集团不可售位图) OR (分店不可售 SKU)
----不可售的sku SELECT rb_iterate( rb_or( rb_and( (SELECT bitmap FROM mv_store1_roaring where store_id=1), rb_andnot(rb_build(ARRAY(SELECT sku_id FROM tab1)), (SELECT bitmap FROM mv_group_roaring)) ), rb_andnot(rb_build(ARRAY(SELECT sku_id FROM tab2 WHERE store_id = 1)), (SELECT bitmap FROM mv_store1_roaring where store_id=1)) ) ) AS sku_id; -----不可售的商品信息 with skuid as (SELECT rb_iterate( rb_or( rb_and( (SELECT bitmap FROM mv_store1_roaring where store_id=1), rb_andnot(rb_build(ARRAY(SELECT sku_id FROM tab1)), (SELECT bitmap FROM mv_group_roaring)) ), rb_andnot(rb_build(ARRAY(SELECT sku_id FROM tab2 WHERE store_id = 1)), (SELECT bitmap FROM mv_store1_roaring where store_id=1)) ) ) AS sku_id) select b.* from skuid a join tab2 b on a.sku_id = b.sku_id where b.store_id = 1;
3.分步拆解
步骤 1:计算条件 1(分店可售但集团不可售)
rb_and(
(SELECT bitmap FROM mv_store1_roaring), -- 分店可售的 SKU 位图
rb_andnot(
rb_build(ARRAY(SELECT sku_id FROM tab1)), -- 所有集团 SKU 的总位图(包括可售和不可售)
(SELECT bitmap FROM mv_group_roaring) -- 集团可售的 SKU 位图
)
)
-
逻辑:
-
rb_build(ARRAY(SELECT sku_id FROM tab1)
:构建集团所有 SKU 的完整位图(假设tab1
包含所有 SKU)。 -
rb_andnot(集团总位图, 集团可售位图)
:得到 集团不可售的 SKU 位图。 -
rb_and(分店可售位图, 集团不可售位图)
:筛选出分店可售但集团不可售的 SKU。
-
步骤 2:计算条件 2(分店不可售)
rb_andnot(
rb_build(ARRAY(SELECT sku_id FROM tab2 WHERE store_id = 1)), -- 分店 1 的所有 SKU 位图
(SELECT bitmap FROM mv_store1_roaring) -- 分店可售的 SKU 位图
)
-
逻辑:
-
rb_build(ARRAY(...))
:构建分店 1 的所有 SKU 位图(无论是否可售)。 -
rb_andnot(分店总位图, 分店可售位图)
:得到 分店不可售的 SKU 位图。
-
步骤 3:合并条件 1 和条件 2
rb_or(条件1位图, 条件2位图)
-
逻辑:将两个条件的结果合并,得到所有不可售的 SKU 位图。
步骤 4:转换为 SKU ID 列表
rb_iterate(...)
-
逻辑:将最终位图转换为可读的 SKU ID 列表。
4.关键函数说明
函数名 | 作用 | 示例 |
---|---|---|
rb_build(ARRAY) |
将数组转换为 Roaring Bitmap | rb_build(ARRAY[1,3,5]) → 位图包含1,3,5 |
rb_and(a, b) |
返回两个位图的交集(a AND b) | rb_and(位图A, 位图B) |
rb_andnot(a, b) |
返回 a 中存在但 b 中不存在的元素(a AND NOT b) | rb_andnot(位图A, 位图B) |
rb_or(a, b) |
返回两个位图的并集(a OR b) | rb_or(位图A, 位图B) |
rb_iterate(bitmap) |
将位图转换为可读的 SKU ID 列表 | rb_iterate(位图) → 1,3,5,7,... |
3.2 使用 intarray
1. 创建数组表
-- 集团可售 SKU 数组 CREATE MATERIALIZED VIEW mv_group_intarray AS SELECT array_agg(sku_id) AS skus FROM tab1 WHERE v_status = 1; -- 分店可售 SKU 数组(以分店 1 为例) CREATE MATERIALIZED VIEW mv_store1_intarray AS SELECT store_id ,array_agg(sku_id) AS skus
FROM tab2
WHERE v_status = 1
group by store_id ;
2. 查询分店 1 不可售的 SKU
-- 分店不可售 = (分店可售数组 ∩ 集团不可售数组) ∪ (分店不可售数组)
WITH
group_available AS (SELECT skus FROM mv_group_intarray),
store_available AS (SELECT skus FROM mv_store1_intarray where store_id=1),
group_unavailable AS (SELECT array_agg(sku_id) AS skus FROM tab1 WHERE v_status = 0),
store_unavailable AS (SELECT array_agg(sku_id) AS skus FROM tab2 WHERE store_id = 1 AND v_status = 0)
SELECT unnest(
store_available.skus & group_unavailable.skus | store_unavailable.skus
) AS sku_id
FROM store_available, group_unavailable, store_unavailable;
3.3 使用内置位数组
1. 创建位数组表(假设 SKU ID 连续)
-- 集团可售位图(假设最大 SKU ID 为 500000)
CREATE MATERIALIZED VIEW mv_group_bit AS
SELECT
bit_or(bitmask) AS bitmap
FROM (
SELECT set_bit('0'::bit(500000), sku_id - 1, 1) AS bitmask
FROM tab1
WHERE v_status = 1
) t;
-- 分店可售位图
CREATE MATERIALIZED VIEW mv_store1_bit AS
SELECT
store_id,
bit_or( -- 将每个 SKU 的位掩码合并为一个完整位图
set_bit('0'::bit(500000), sku_id - 1, 1)
) AS bitmap
FROM tab2
WHERE v_status = 1
GROUP BY store_id;
2. 查询分店 1 不可售的 SKU
-- 不可售条件:((分店可售 AND 集团不可售) OR 分店不可售)
SELECT
pos + 1 AS sku_id
FROM
generate_series(0, 499999) AS pos
WHERE
get_bit(
(
( (SELECT bitmap FROM mv_store1_bit where store_id=1) & ~(SELECT bitmap FROM mv_group_bit) )
| ~(SELECT bitmap FROM mv_store1_bit where store_id=1)
),
pos
) = 1;
4 对比分析
方案 | 存储空间 (分店 1) | 查询时间 (分店 1) | 优点 | 缺点 |
---|---|---|---|---|
pg_roaringbitmap | ~1 MB | ~600 ms | 压缩率高,动态扩展,支持海量数据 | 需安装插件 |
intarray | ~2 MB (50 万元素) | ~50 m | 无需插件,操作简单 | 性能低,内存占用高 |
内置位数组 | ~62.5 KB | ~2 min | 原生支持,存储紧凑 | SKU ID 必须连续且范围小 |
5 总结建议
-
推荐方案:使用 pg_roaringbitmap,适合大规模稀疏数据,查询性能最优。
-
替代方案:若 SKU ID 连续且数量较小(如 < 10 万),可使用 内置位数组。
-
避免场景:避免在超大规模数据中使用 intarray,可能引发内存问题。
通过合理选择技术方案,可高效解决商超场景下的 SKU 可售状态计算需求。