我有一个用例,其中数据是多对多的,需要广泛的查询功能。
参与者和活动
用户/参与者可以注册多个事件。每个事件可以有很多参与者。这是一个多对多的关系。
考虑这样的数据集。
- 每个活动可以有 1000 万用户注册。
- 每个用户最多可以注册 1000 个事件
- 有 1000 个事件正在运行
需要以下查询:
- 查询 1. 获取所有注册活动的 Participants
- 查询 2. 获取参与者注册的所有事件
- 查询 3. 获取参与者即将发生的所有事件
用于处理查询 1和查询 2
EventParticipantTable:(eventId,participantId):1000 x 10M 记录
这需要搜索 1000 x 10M 记录?
数据集可以根据 eventId 拆分为块,以使其理想地仅扫描 10M 记录,但不确定如何在 PostgreSQL 中处理。
用于处理查询 3
事件表 + EventParticipantTable 加入
这需要两个表的连接,我首先获取即将发生的事件的事件表(基于开始和结束时间戳),并且对于每个匹配的 eventId,需要查找 EventParticipantTable 中是否存在查询的参与者 ID。
这需要搜索 1000 个事件 * (1000 * 10M) 事件参与者表条目?
在这种情况下,每个表 1000 x 10M 记录不是问题吗?
为了解决您的问题,我执行了以下操作(下面的所有代码都可以在此处的小提琴中找到):
这些测试已经在 db<>fiddle 服务器上运行——我们不完全了解机器的配置,也不知道在运行查询时还会发生什么。
我还在家用笔记本电脑上进行了测试:
PostgreSQL 12.7 实例是使用以下选项从源代码编译的:
系统设置是默认设置,除了pgtune的建议如下:
建议的默认更改:
由于我为具有大量写入以加快加载速度的系统阅读的内容而增加了
min_
和设置 - 不应该影响读取 - 丢失引用......max_wal
首先,我创建了一个函数来生成随机字符串(从这里):
然后,我创建了一个
event
表:也给它一个索引
event_name
- 我可以想象许多希望按名称搜索的场景。还有
event_date
:然后我创建了 100 个(笔记本电脑上为 1,000 个)事件,如下所示:
但!,你可能会尖叫......所有的活动日期都是过去的 - 是的,但如果你这样做,那么你将拥有过去的 50% 和未来的 50%:
检查
SELECT * FROM event;
- 结果:这样做(而不是使用文字日期)意味着小提琴将在几年后工作,因为
event_date
这只取决于小提琴何时运行而不是某个常数!表
participant
:participant_name
指数:然后创建了 10,000 个(笔记本电脑上为 10,000,000 - 10M)参与者:
现在,我们的连接表(或
Associative Entity
):现在,这就是事情变得有趣的地方。在笔记本电脑上运行查询 1(见下文)给出的响应时间约为 25 分钟 -不理想!
我尝试了各种“技巧”(
SET enable_seqscan = off
和SET enable_bitmapscan = off
- 见这里) - 基本上,我只是四处寻找我能在网上找到的任何东西......我终于硬着头皮进行了分区——那么,
ev_par
表的逻辑分区键是什么?好吧,这event_id
似乎是最好的候选者 - 其中有 1,000 个 - 整个表(仅数据)约为 350GB,因此 1,000 个约 350MB 的表 - 更易于管理!使用索引(PK + par_ev_ix - 见下文),该表约为 750GB!
因此,在最后一个括号 (
);
) 和分号之前,我们输入:我在这里、这里和这里找到了有用的信息(最有帮助的)。
基本上(简化),有 3 种类型的分区:
列表
CREATE TABLE customers (id INTEGER, status TEXT, arr NUMERIC) PARTITION BY LIST(status);
(父)和一个典型的分区将通过运行这样的东西来创建:
CREATE TABLE cust_active PARTITION OF customers FOR VALUES IN ('ACTIVE');
范围
CREATE TABLE customers (id INTEGER, status TEXT, arr NUMERIC) PARTITION BY RANGE(arr);
(父)和一个典型的分区将通过运行这样的东西来创建:
CREATE TABLE cust_arr_small PARTITION OF customers FOR VALUES FROM (MINVALUE) TO (25);
哈希
CREATE TABLE customers (id INTEGER, status TEXT, arr NUMERIC) PARTITION BY HASH(id);
(父)和一个典型的分区将通过运行这样的东西来创建:
CREATE TABLE cust_part1 PARTITION OF customers FOR VALUES WITH (modulus 3, remainder 0);
我们现在必须使用该
LIST
方法创建 1,000 个分区 - 那么,我们该怎么做,bash 脚本、PL/pgSQL... 其他?搜索时,我发现了这个(Hubert depesz Lubaczewski's**absolute gem**
] 11)的页面,其中包含以下片段:所以,我修改了这段代码如下:
这会生成我们想要的 1,000 个分区(显示了前两个 DDL 分区语句):
我用左填充了分区名称,
0
以便它们在使用时正确排序\d+ ev_par
。PRIMARY KEY
最后,我们在表的“逆”上放置一个索引ev_par
——即这样使用
par_id
first 的搜索也将被编入索引。在填充表之前,我通过运行以下命令(从此处)禁用了表上的约束:
然后我通过
CROSS JOIN
在两个表之间使用 a 来填充它。我将此过程拆分为 1,000 个单独的事务,以适应上面的分区代码,如下所示:所以,现在我们的 ev_par 表中有 1,000,000 条记录。在笔记本电脑上,这相当于 10,000,000,000 (100 亿) 条记录。请注意- 即使使用 SSD并且没有任何限制,这也需要大约 6 个小时!
然后,我们重新激活约束:
然后,我运行了这个查询(您的查询 1 - 获取所有注册活动的参与者):
结果:
但是,我们需要进行性能分析,所以我跑了
我们感兴趣的一行是这一行:
相当令人印象深刻!但是,在 100 亿记录表上运行时并没有那么令人印象深刻:
但是,在对数据进行分区后,查询 1 会返回:
那么,从 25 分钟到 5 秒 - 怎么会?
答案在于计划 - 未分区表(小提琴和笔记本电脑)的计划是相同的:
对于分区数据:
两条关键线是:
在第一种情况下,它扫描整个参与者表(100 亿条记录),在第二种情况下,它使用参与者
PRIMARY KEY
——这就是查询从 25 分钟缩短到 5 秒的方式!然后我运行了这个(查询 2 - 获取参与者注册的所有事件):
结果:
和:
该查询在未分区的 10Bn 表上运行也非常快——因为它是唯一
Seq Scan
在小event
表上的。两个大表在大约返回一个结果。0.5秒!计划:
dbfiddle 和笔记本电脑(未分区):
分区表:
因此,分区表
Index Only Scan
在 1,000 个分区上运行,而Seq Scan
在小event
表上运行 - 所以它也很快!最后,我运行了您的查询 3 - 参与者即将发生的所有事件。基本上,这仅涉及获取参与者的事件(查询 2)并向
WHERE
子句添加谓词 -event_date > NOW()
如下所示:结果:
50 个是 100 个事件的一半。执行时间为 0.4 毫秒(两个大表约为 0.5 秒),所以我们看起来不错!
As you can see, the queries with good indexes are pretty fast - obviously you'll have more records in your database, but since we're using BTREEs, the slowdown won't be O(n) - as long as they do use them - the partitioning scheme means that Query 1 does in the large table - not for the unpartitioned one though!
However, I think that the numbers shown give a good indication that PostgreSQL will have absolutely no problems running your queries. If you have a decent server with RAID and SSDs, you'll be humming!
You'll will require more partitions as you add events, but that shouldn't be too onerous - it'll take a couple of minutes at most to fill a single partition.
Obviously, you should benchmark on your own systems to obtain a realistic idea of real world performance for your own users.
So, to answer the question:
No, it is not an issue!
p.s. welcome to the forum! p.p.s. please always include your server version when asking questions!