Ayy*_*ppa 6 postgresql scalability
我有一个用例,其中数据是多对多的,并且需要广泛的查询功能。
参与者和事件
一个用户/参与者可以注册多个事件。每个事件可以有很多参与者。这是一个多对多的关系。
考虑这样的数据集。
需要以下查询:
用于处理查询 1和查询 2
EventParticipantTable :(eventId,participantId):1000 x 10M 记录
这需要搜索 1000 x 10M 的记录吗?
数据集可以按 eventId 拆分为块,以使其理想地仅扫描 10M 记录,但不确定如何在 PostgreSQL 中处理。
用于处理查询 3
事件表 + EventParticipantTable 加入
这需要连接两个表,其中我首先获取即将发生的事件的 Events 表(基于开始和结束时间戳),并且对于每个匹配的 eventId 需要查找查询的参与者 ID 是否存在于 EventParticipantTable 中。
这需要搜索 1000 个事件 * (1000 * 10M) 个事件参与者表条目?
在这种情况下,每表 1000 x 10M 记录不是问题吗?
为了解决您的问题,我执行了以下操作(以下所有代码都可以在此处的小提琴上找到):
这些测试是在 db<>fiddle 服务器上运行的——我们并不完全知道机器的配置,也不知道在我们运行查询时发生了什么。
我还在家用笔记本电脑上进行了测试:
PostgreSQL 12.7 实例是使用以下选项从源代码编译的:
./configure --prefix=/home/pol/Downloads/db/dba_test/12.7/inst --enable-nls --with-python --with-icu --with-openssl --with-uuid=e2fs
Run Code Online (Sandbox Code Playgroud)
除pgtune的建议如下外,系统设置均为默认值:
DB Version: 12
OS Type: linux
DB Type: dw
Total Memory (RAM): 32 GB
CPUs num: 4
Data Storage: ssd
Run Code Online (Sandbox Code Playgroud)
推荐的默认更改:
max_connections = 40
shared_buffers = 8GB
effective_cache_size = 24GB
maintenance_work_mem = 2GB
checkpoint_completion_target = 0.9
wal_buffers = 16MB
default_statistics_target = 500
random_page_cost = 1.1
effective_io_concurrency = 200
work_mem = 52428kB
min_wal_size = 4GB -- used 16GB for this setting
max_wal_size = 16GB -- used 64GB for this setting
max_worker_processes = 4
max_parallel_workers_per_gather = 2
max_parallel_workers = 4
max_parallel_maintenance_workers = 2
Run Code Online (Sandbox Code Playgroud)
在min_和max_wal设置被撞到了,因为东西我读与写重加快装载系统-应该不会影响读取-失去引用(S)...
首先,我创建了一个函数来生成随机字符串(来自此处):
CREATE FUNCTION random_text(INTEGER)
RETURNS TEXT
LANGUAGE SQL
AS $$
select upper(
substring(
(SELECT string_agg(md5(random()::TEXT), '')
FROM generate_series(
1,
CEIL($1 / 32.)::integer)
), 1, $1) );
$$;
Run Code Online (Sandbox Code Playgroud)
然后,我创建了一个event表:
CREATE TABLE event
(
event_id SMALLINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
event_name TEXT NOT NULL UNIQUE,
event_date DATE NOT NULL
);
Run Code Online (Sandbox Code Playgroud)
还给了它一个索引event_name- 我可以想象出许多希望按名称搜索的场景。
CREATE INDEX ev_name_ix ON event USING BTREE
(event_name ASC);
Run Code Online (Sandbox Code Playgroud)
还有event_date:
CREATE INDEX ev_date_ix ON event USING BTREE
(event_date ASC);
Run Code Online (Sandbox Code Playgroud)
然后我创建了 100 个(在笔记本电脑上为 1,000 个)事件,如下所示:
INSERT INTO event (event_name, event_date)
SELECT random_text(10), CURRENT_DATE - INTERVAL '7 DAY'
FROM GENERATE_SERIES(1, 100);
Run Code Online (Sandbox Code Playgroud)
但!,你可能会尖叫......所有事件日期都在过去 - 是的,但如果你这样做,那么你将拥有过去的 50% 和未来的 50%:
UPDATE event
SET event_date =
(
CASE
WHEN MOD(event_id, 2) = 1 THEN event_date -- i.e. no change!
ELSE CURRENT_DATE + INTERVAL '7 DAY'
END
);
Run Code Online (Sandbox Code Playgroud)
检查SELECT * FROM event;- 结果:
event_id event_name event_date
1 A653585119 2021-07-30
2 01563801BB 2021-08-13
3 4ED87ABDEC 2021-07-30
4 EF0394645B 2021-08-13
...
... snipped for brevity
...
Run Code Online (Sandbox Code Playgroud)
这样做(而不是文字日期)意味着小提琴将在几年后工作,因为这event_date仅取决于小提琴的运行时间而不是某个常数!
该participant表:
CREATE TABLE participant
(
participant_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
participant_name TEXT NOT NULL -- might not be UNIQUE
);
Run Code Online (Sandbox Code Playgroud)
participant_name 指数:
CREATE INDEX par_name_ix ON participant USING BTREE
(participant_name ASC);
Run Code Online (Sandbox Code Playgroud)
然后创建了 10,000 个(笔记本电脑上为 10,000,000 - 10M)参与者:
INSERT INTO participant (participant_name)
SELECT random_text(10)
FROM GENERATE_SERIES(1, 10000);
Run Code Online (Sandbox Code Playgroud)
现在,我们的连接表(或Associative Entity):
CREATE TABLE ev_par
(
ev_id SMALLINT NOT NULL,
par_id INTEGER NOT NULL,
CONSTRAINT ev_par_pk PRIMARY KEY (ev_id, par_id),
CONSTRAINT ev_id_fk FOREIGN KEY (ev_id) REFERENCES event (event_id),
CONSTRAINT par_id_fk FOREIGN KEY (par_id) REFERENCES participant (participant_id)
);
Run Code Online (Sandbox Code Playgroud)
现在,这就是事情变得有趣的地方。在笔记本电脑上运行查询 1(见下文)给出了大约 25 分钟的响应时间 -不理想!
我尝试了各种“技巧”(SET enable_seqscan = off并且SET enable_bitmapscan = off- 见这里) - 基本上,我只是在尝试我可以在网上找到的任何东西......
我终于硬着头皮去做了分区——那么,ev_par表的逻辑分区键是什么?好吧,这event_id似乎是最好的候选者 - 其中有 1,000 个 - 整个表(仅数据)约为 350GB,因此可以提供约 350MB 的 1,000 个表 - 更易于管理!
使用索引(PK + par_ev_ix - 见下文),该表约为 750GB!
因此,在最后一个括号 ( );) 之后和分号之前,我们输入:
) PARTITION BY LIST (ev_id);
Run Code Online (Sandbox Code Playgroud)
基本上(简化),有 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...其他?搜索时,我发现**absolute gem**了一个包含以下片段的页面:
$ CREATE TABLE test_ranged (
id serial PRIMARY KEY,
payload TEXT
) partition BY range (id);
$ select format('CREATE TABLE %I partition OF test_ranged FOR VALUES FROM (%s) to (%s);', 'test_ranged_' || i, i, i+1)
FROM generate_series(1,10000) i \gexec
Run Code Online (Sandbox Code Playgroud)
所以,我修改了这段代码如下:
SELECT FORMAT('CREATE TABLE %I PARTITION OF ev_par FOR VALUES IN (%s);', 'ev_par_' || i, x)
FROM
(
SELECT LPAD (x, 4, '0') AS i, x
FROM
(
SELECT x::TEXT FROM GENERATE_SERIES (1, 1000) AS x
) AS tab1
) AS tab2 \gexec
Run Code Online (Sandbox Code Playgroud)
这会生成我们想要的 1,000 个分区(显示的前两个 DDL 分区语句):
format
-----------------------------------------------------------------
CREATE TABLE ev_par_0001 PARTITION OF ev_par FOR VALUES IN (1);
CREATE TABLE ev_par_0002 PARTITION OF ev_par FOR VALUES IN (2);
Run Code Online (Sandbox Code Playgroud)
我用左填充了分区名称,0以便在使用\d+ ev_par.
最后,我们PRIMARY KEY在ev_par表的“逆”上放置一个索引- 即
CREATE INDEX par_ev_ix ON ev_par USING BTREE
(par_id, ev_id);
Run Code Online (Sandbox Code Playgroud)
因此,使用par_idfirst 的搜索也将被索引。
在填充表之前,我通过运行以下命令(来自此处)禁用了表上的约束:
ALTER TABLE reference DISABLE TRIGGER ALL;
Run Code Online (Sandbox Code Playgroud)
然后我通过CROSS JOIN在两个表之间使用 a来填充它。我将这个过程拆分为 1,000 个单独的事务,以适应上面的分区代码,如下所示:
SELECT FORMAT(
'
BEGIN TRANSACTION;
INSERT INTO ev_par
SELECT e.event_id, p.participant_id
FROM event e, participant p
WHERE e.event_id = %s;
COMMIT;', i)
FROM
(
SELECT i::TEXT FROM GENERATE_SERIES (1, 1000) AS i
) AS tab1 \gexec
Run Code Online (Sandbox Code Playgroud)
所以,现在我们的 ev_par 表中有 1,000,000 条记录。在笔记本电脑上,这相当于 10,000,000,000(100 亿)条记录。请注意- 即使使用 SSD并且没有任何限制,这也需要大约 6 小时!
然后,我们重新激活约束:
ALTER TABLE reference ENABLE TRIGGER ALL;
Run Code Online (Sandbox Code Playgroud)
然后,我运行了这个查询(您的查询 1 - 获取所有注册活动的参与者):
SELECT ep.par_id, p.participant_name
FROM participant p
JOIN ev_par ep ON p.participant_id = ep.par_id
WHERE ev_id = 9;
Run Code Online (Sandbox Code Playgroud)
结果:
par_id participant_name
1 E036FD8DA0
2 7CC689B41F
3 E7F1508EE7
4 3CEF3FC3BD
5 9BF603F525
...
... snipped for brevity
...
Run Code Online (Sandbox Code Playgroud)
但是,我们需要性能分析,所以我跑了
EXPLAIN (ANALYZE, BUFFERS, TIMING, VERBOSE, COSTS)
<above query>
Run Code Online (Sandbox Code Playgroud)
我们感兴趣的一行是这样的:
Execution Time: 70.484 ms
Run Code Online (Sandbox Code Playgroud)
相当令人印象深刻!然而,当运行 100 亿记录表时,它并没有那么令人印象深刻:
Execution Time: ~ 25 mins
Run Code Online (Sandbox Code Playgroud)
但是,在对数据进行分区后,查询 1 返回:
Execution Time: 5795.941 ms
Run Code Online (Sandbox Code Playgroud)
那么,从 25 分钟到 5 秒 - 怎么会?
答案在于计划 - 未分区表(小提琴和笔记本电脑)的计划是相同的:
QUERY PLAN
Nested Loop (cost=0.43..4266.03 rows=5310 width=36) (actual time=0.127..69.629 rows=10000 loops=1)
Output: ep.par_id, p.participant_name
Inner Unique: true
Buffers: shared hit=35545 read=4510 written=1002
-> Seq Scan on public.participant p (cost=0.00..124.85 rows=6985 width=36) (actual time=0.058..2.070 rows=10000 loops=1)
Output: p.participant_id, p.participant_name
Buffers: shared read=55 written=13
-> Index Only Scan using ev_par_pk on public.ev_par ep (cost=0.43..4.89 rows=27 width=4) (actual time=0.006..0.006 rows=1 loops=10000)
Output: ep.ev_id, ep.par_id
Index Cond: ((ep.ev_id = 9) AND (ep.par_id = p.participant_id))
Heap Fetches: 10000
Buffers: shared hit=35545 read=4455 written=989
Planning Time: 0.167 ms
Execution Time: 70.494 ms
14 rows
Run Code Online (Sandbox Code Playgroud)
对于分区数据:
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Merge Join (cost=0.87..635564.73 rows=10000000 width=15) (actual time=0.096..5465.321 rows=10000000 loops=1)
Output: ep.par_id, p.participant_name
Merge Cond: (p.participant_id = ep.par_id)
Buffers: shared hit=152953
-> Index Scan using participant_pkey on public.participant p (cost=0.43..234216.54 rows=9999860 width=15) (actual time=0.032..1234.914 rows=10000000 loops=1)
Output: p.participant_id, p.participant_name
Buffers: shared hit=81380
-> Index Only Scan using ev_par_0009_par_id_ev_id_idx on public.ev_par_0009 ep (cost=0.43..251348.54 rows=10000000 width=4) (actual time=0.055..2005.403 rows=10000000 loops=1)
Output: ep.par_id
Index Cond: (ep.ev_id = 9)
Heap Fetches: 10000000
Buffers: shared hit=71573
Planning Time: 0.559 ms
Execution Time: 5795.941 ms
(14 rows)
Run Code Online (Sandbox Code Playgroud)
2条关键线是:
Non partitioned: -> Seq Scan on public.participant p
Parititioned: -> Index Scan using participant_pkey
Run Code Online (Sandbox Code Playgroud)
在第一种情况下,它扫描整个参与者表(100 亿条记录),在第二种情况下,它使用参与者PRIMARY KEY——这就是查询从 25 分钟到 5 秒的方式!
然后我运行了这个(查询 2 - 获取参与者注册的所有事件):
SELECT ep.ev_id, e.event_name
FROM event e
JOIN ev_par ep ON e.event_id = ep.ev_id
WHERE ep.par_id = 5432;
Run Code Online (Sandbox Code Playgroud)
结果:
ev_id event_name
1 CC69EBE53E
2 FD8BD9E311
3 FC94119C5A
4 511EA750E1
5 9956514FAA
...
... snipped for brevity
...
Run Code Online (Sandbox Code Playgroud)
和:
EXPLAIN (ANALYZE &c... Execution Time: 0.279 ms
Run Code Online (Sandbox Code Playgroud)
这个查询在未分区的 10Bn 表上运行也非常快 - 因为它是唯一Seq Scan在小event表上的。两个大表都返回了大约结果。0.5秒!
计划:
dbfiddle 和笔记本电脑(未分区):
QUERY PLAN
Nested Loop (cost=0.43..1366.00 rows=5310 width=34) (actual time=0.017..0.270 rows=100 loops=1)
Output: ep.ev_id, e.event_name
Inner Unique: true
Buffers: shared hit=402
-> Seq Scan on public.event e (cost=0.00..22.30 rows=1230 width=34) (actual time=0.008..0.017 rows=100 loops=1)
Output: e.event_id, e.event_name, e.event_date
Buffers: shared hit=2
-> Index Only Scan using ev_par_pk on public.ev_par ep (cost=0.43..18.40 rows=27 width=2) (actual time=0.002..0.002 rows=1 loops=100)
Output: ep.ev_id, ep.par_id
Index Cond: ((ep.ev_id = e.event_id) AND (ep.par_id = 5432))
Heap Fetches: 100
Buffers: shared hit=400
Planning Time: 0.109 ms
Execution Time: 0.290 ms
Run Code Online (Sandbox Code Playgroud)
分区表:
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Hash Join (cost=35.94..2695.64 rows=1000 width=13) (actual time=0.259..5.333 rows=1000 loops=1)
Output: ep.ev_id, e.event_name
Inner Unique: true
Hash Cond: (ep.ev_id = e.event_id)
Buffers: shared hit=4013
-> Append (cost=0.43..2657.50 rows=1000 width=2) (actual time=0.016..4.866 rows=1000 loops=1)
Buffers: shared hit=4000
-> Index Only Scan using ev_par_0001_par_id_ev_id_idx on public.ev_par_0001 ep (cost=0.43..2.65 rows=1 width=2) (actual time=0.015..0.016 rows=1 loops=1)
Output: ep.ev_id
Index Cond: (ep.par_id = 5432)
Heap Fetches: 1
Buffers: shared hit=4
-> Index Only Scan using ev_par_0002_par_id_ev_id_idx on public.ev_par_0002 ep_1 (cost=0.43..2.65 rows=1 width=2) (actual time=0.007..0.007 rows=1 loops=1)
Output: ep_1.ev_id
Index Cond: (ep_1.par_id = 5432)
Heap Fetches: 1
Buffers: shared hit=4
...
... 998 more Index Only Scans - snipped for brevity
...
-> Hash (cost=23.00..23.00 rows=1000 width=13) (actual time=0.248..0.248 rows=1000 loops=1)
Output: e.event_name, e.event_id
Buckets: 1024 Batches: 1 Memory Usage: 53kB
Buffers: shared hit=13
-> Seq Scan on public.event e (cost=0.00..23.00 rows=1000 width=13) (actual time=0.029..0.113 rows=1000 loops=1)
Output: e.event_name, e.event_id
Buffers: shared hit=13
Planning Time: 497.960 ms
Execution Time: 8.995 ms
(5016 rows)
Time: 538.058 ms
Run Code Online (Sandbox Code Playgroud)
因此,分区表Index Only Scan在 1,000 个分区上运行Seq Scan,在小event表上运行 - 所以它也很快!
最后,我运行了您的查询 3 - 参与者即将举行的所有活动。基本上,这仅涉及获取参与者的事件(查询 2)并向WHERE子句添加谓词-event_date > NOW()如下:
SELECT ep.ev_id, e.event_name, e.event_date
FROM event e
JOIN ev_par ep ON e.event_id = ep.ev_id
WHERE ep.par_id = 5432 AND e.event_date > NOW();
Run Code Online (Sandbox Code Playgroud)
结果:
ev_id event_name event_date
2 D980DE4C4E 2021-08-13
4 83DC72EF65 2021-08-13
6 CFFF3F2BAC 2021-08-13
8 0B07F148E8 2021-08-13
...
... snipped for brevity
...
10 rows of 50
Run Code Online (Sandbox Code Playgroud)
50 是 100 个事件的一半。执行时间为 0.4 毫秒(两个大表都为 0.5 秒),所以我们看起来不错!
如您所见,具有良好索引的查询非常快 - 显然您的数据库中会有更多记录,但是由于我们使用的是 BTREE,所以减速不会是 O(n) - 只要它们确实使用它们 - 分区方案意味着查询 1 在大表中执行 - 但不适用于未分区的表!
但是,我认为显示的数字很好地表明 PostgreSQL 在运行您的查询时绝对没有问题。如果您有一台配备 RAID 和 SSD 的不错的服务器,您一定会赞叹不已!
添加事件时,您将需要更多分区,但这不应该太繁重 - 填充单个分区最多需要几分钟。
显然,您应该在自己的系统上进行基准测试,以便为自己的用户获得真实世界性能的真实想法。
所以,回答这个问题:
在这种情况下,每表 1000 x 10M 记录不是问题吗?
不,这不是问题!
ps 欢迎来到论坛!pps 请在提问时始终包括您的服务器版本!