原作者:Egor Rogov
翻译:魏波 编辑:孙祥斌
在之前的文章中,我们讨论了PostgreSQL 索引引擎,访问方法的接口以及以下方法:Hash索引,B树,GiST,SP-GiST,GIN和RUM。本文的主题是BRIN索引。
BRIN
一般概念
简单地说,对于值与其在表中的物理位置相关的列,BRIN工作得很好。换句话说,如果一个没有ORDER BY子句的查询,实际上以递增或递减的顺序返回列值(并且该列上没有索引)。
此访问方法是在Axle范围内创建的,Axle是一个用于大型分析数据库的欧洲项目,着眼于几TB或数十TB的表。BRIN的一项重要功能使我们能够在此类表上创建索引,它的体积小且维护开销最小。
其工作原理如下:表被分成是几页大的范围(或几个块大) 。因此BRIN又被称为:块范围索引。索引存储每个范围内数据的汇总信息,通常是最小值和最大值。假定执行的查询的条件包含列;如果所搜索的值未进入该间隔,则可以跳过整个范围;但如果确实定位到了它们,则必须仔细检查所有块中的所有行,以在其中选择匹配的行。
将BRIN视为索引,而不是顺序扫描的加速器。如果我们将每个范围视为“虚拟”分区,则可以将BRIN视为分区的替代方案。
现在让我们更详细地讨论索引的结构。
结构体
带有摘要信息的页面与元数据之间有一定的偏移量。这些页面上的每个索引行都包含一个范围的摘要信息。
在元页面和摘要数据之间,找到具有反向范围映射(简称为“ revmap”)的页面。实际上,这是指向相应索引行的指针(TID)的数组。

扫描引擎
如果索引不包含对表行的引用,该如何使用?此访问方法当然不能按TID返回TID行,但可以构建位图。位图页面可以有两种:精确到行,不精确到页。这是一个不准确的位图。
更新索引
当将新版本的行添加到表页面时,我们确定该行包含在哪个范围中,并使用范围图查找包含摘要信息的索引行。这些都是简单的算术运算。例如,假设范围的大小为4,在第13页上,出现行值为42的行版本。范围的数字(从零开始)是13/4 = 3,因此,在《revmap》中,我们采用偏移量为3的指针(其顺序号为4)。
此范围的最小值为31,最大值为40。由于新值42不在间隔内,因此我们更新最大值(请参见图)。但是,如果新值仍在存储的限制内,则无需更新索引。

示例
demo=#createtable flights_bi(airport_code char(3),airport_coord point, -- geo coordinates of airportairport_utc_offset interval,-- time zoneflight_no char(6), -- flight numberflight_type text. -- flight type: departure arrivalscheduled_time timestamptz, -- scheduled departure/arrival time of flightactual_time timestamptz, -- actual time of flightaircraft_code char(3),seat_no varchar(4), -- seat numberfare_conditions varchar(10),-- travel classpassenger_id varchar(20), passenger_name text);
DO $$<<local>>DECLAREcurdate date := (SELECT min(scheduled_departure) FROM flights);utc_offset interval;BEGINWHILE (curdate <= bookings.now()::date) LOOPutc_offset := interval '12 hours'; WHILE (utc_offset >= interval '2 hours') LOOPINSERT INTO flights_bi WITH flight (airport_code,airport_coord,flight_id,flight_no,scheduled_time,actual_time,aircraft_code,flight_type) AS ( -- прибытияSELECT a.airport_code,a.coordinates,f.flight_id,f.flight_no,f.scheduled_departure,f.actual_departure,f.aircraft_code, 'departure'FROM airports a,flights f,pg_timezone_names tznWHERE a.airport_code = f.departure_airportAND f.actual_departure IS NOT NULLAND tzn.name = a.timezoneAND tzn.utc_offset = local.utc_offsetAND timezone(a.timezone, f.actual_departure)::date = curdate UNION ALL-- вылетыSELECT a.airport_code,a.coordinates,f.flight_id,f.flight_no,f.scheduled_arrival,f.actual_arrival,f.aircraft_code, 'arrival'FROM airports a,flights f,pg_timezone_names tznWHERE a.airport_code = f.arrival_airportAND f.actual_arrival IS NOT NULLAND tzn.name = a.timezoneAND tzn.utc_offset = local.utc_offsetAND timezone(a.timezone, f.actual_arrival)::date = curdate) SELECT f.airport_code,f.airport_coord, local.utc_offset,f.flight_no,f.flight_type,f.scheduled_time,f.actual_time,f.aircraft_code,s.seat_no,s.fare_conditions,t.passenger_id,t.passenger_nameFROM flight fJOIN seats sON s.aircraft_code = f.aircraft_codeLEFT JOIN boarding_passes bpON bp.flight_id = f.flight_idAND bp.seat_no = s.seat_noLEFT JOIN ticket_flights tfON tf.ticket_no = bp.ticket_noAND tf.flight_id = bp.flight_idLEFT JOIN tickets tON t.ticket_no = tf.ticket_no;RAISE NOTICE '%, %', curdate, utc_offset;utc_offset := utc_offset - interval '1 hour'; END LOOP;curdate := curdate + 1; END LOOP;END;$$;
demo=#select count(*)from flights_bi;count----------30517076(1 row)demo=#select pg_size_pretty(pg_total_relation_size('flights_bi'));pg_size_pretty----------------4127 MB(1 row)
问
我们应该在哪些列上创建索引?
demo=#analyze flights_bi;demo=#select attname, correlation from pg_statswhere tablename='flights_bi' order by correlationdesc nulls last;attname | correlation--------------------+-------------scheduled_time | 0.999994actual_time | 0.999994fare_conditions | 0.796719flight_type | 0.495937airport_utc_offset | 0.438443aircraft_code | 0.172262airport_code | 0.0543143flight_no | 0.0121366seat_no | 0.00568042passenger_name | 0.0046387passenger_id | -0.00281272airport_coord |(12 rows)
该值不太接近零(理想情况下,接近正负1),这告诉我们,BRIN索引将是适用的。
出差航班类别«fare_condition»(该列包含三个唯一值)和航班类型«flight_type» (两个唯一 值)出乎意料地位于第二和第三位。这是一种错觉:形式上的相关性很高,而实际上在几个 连续的页面上肯定会遇到所有可能的值,这意味着BRIN不会发挥任何作用。 接下来是时区 «airport_utc_offset»:所考虑的示例中,在一天周期内,按时区
«by construction»对机场进行了排序。
我们将进一步试验这两个字段(时间和时区)。
可能削弱相关性
当数据更改时,“构造”位置的相关性很容易减弱。这里的问题不在于对某个特定值的更改,而
在于多版本并发控制的结构:在一个页面上删除过时的行版本,但是可以在任何可用的空闲
空间插入新版本。因此,更新时整行会混淆。
我们可以通过减少«fillfactor»存储参数的值来部分地控制这种效果,并通过这种方式为将来的 更新在页面上留下空闲空间。但是我们想要增加一个已经很大的表的大小吗?此外,这并不 能解决删除的问题:它们还通过释放现有页面内的空间为新行设置陷阱。因此,会将到达文 件末尾的行插入到任意位置。 顺便说一下,这是一个奇怪的事实。由于BRIN索引不包含对表行的引用,因此它的可用性不
应完全阻止HOT更新,但它确实可以。
因此,BRIN的设计主要用于根本没有更新或更新幅度很小的大型表。但是,它完美地应对了
新行的增加(到表的末尾)。这并不奇怪,因为创建此访问方法是为了查看数据仓库和分析
报告。
问
我们需要选择多大的范围?
如果我们处理的是TB级的表,那么在选择范围大小时,我们主要关心的可能是不要使BRIN索
引太大。然而,在我们的情况下,可以更准确地分析数据。
为此,我们可以选择列的唯一值,并查看它们出现在多少页上。值的本地化增加了成功应用B
RIN索引的机会。此外,找到的页数将提示范围的大小。但是,如果该值在所有页面上都“分
散”,则BRIN是无用的。
当然,我们应该使用这种技术来密切注意数据的内部结构。例如,将每个日期(更确切地说是
时间戳,还包括时间)视为唯一值是没有意义的,需要将其四舍五入为天。
从技术上讲,可以通过查看隐藏的“ ctid”列的值来完成此分析,该值提供了指向行版本(TID)
的指针:页面数和页面内行数。不幸的是,没有传统的技术可以将TID分解为两个部分,因此
,我们必须通过文本表示来转换类型:
demo=#select min(numblk), round(avg(numblk)) avg, max(numblk)from (select count(distinct (ctid::text::point)[0]) numblkfrom flights_bigroup by scheduled_time::date) t;min | avg | max------+------+------1192 | 1500 | 1796(1 row)demo=#select relpagesfrom pg_classwhere relname ='flights_bi';relpages----------528172(1 row)
我们可以看到,每一天在页面上分布得相当均匀,并且每天间略有混淆(150*365 = 547500,
只比表中的页面数528172大一点)。 此处的重要信息是特定数量的页面。使用128页的常规范围大小,每天将填充9-14个范围。这 似乎很正常:查询特定的一天,我们可以预期出现10%左右的错误。 咱们试试吧: demo=#createindex on flights_biusing brin(scheduled_time);
索引的大小小至184KB:
demo=#select pg_size_pretty(pg_total_relation_size('flights_bi_scheduled_time_idx'));pg_size_pretty----------------184 kB(1 row)在这种情况下,以损失精度为代价来增加范围的大小是没有意义的。但是,如果需要,我们 可以减少大小,而精度将相反地增加(随着索引的大小)。 现在让我们看看时区。在这里,我们也不能使用暴力手段。所有值均应除以天周期数,因为 分布会在每天重复。此外,由于只有几个时区,我们可以查看整个分布: demo=#select airport_utc_offset, count(distinct (ctid::text::point)[0])/365 numblkfrom flights_bi group byairport_utc_offsetorder by 2;airport_utc_offset | numblk--------------------+--------12:00:00 | 606:00:00 | 802:00:00 | 1011:00:00 | 1308:00:00 | 2809:00:00 | 2910:00:00 | 4004:00:00 | 4707:00:00 | 11005:00:00 | 23103:00:00 | 932(11 rows)
平均而言,每个时区的数据每天填充133页,但分布高度不均匀:
Petropavlovsk-Kamchatskiy和Anadyr的数据仅有六页,而莫斯科及其附近地区则需要数百页。
范围的默认大小在这里不合适。例如,我们将其设置为四个页面:
demo=#createindex on flights_biusing brin(airport_utc_offset) with(pages_per_range=4);demo=#select pg_size_pretty(pg_total_relation_size('flights_bi_airport_utc_offset_idx'));pg_size_pretty----------------6528 kB(1 row)执行计划
让我们看看索引是如何工作的。让我们选择某一天,例如一周前(在演示数据库中,“今天”由 “ booking.now”函数确定): demo=# \set d'bookings.now()::date - interval \'7 days\''demo=#explain (costs off,analyze)select *from flights_biwhere scheduled_time >= :d and scheduled_time < :d + interval '1 day';QUERY PLAN--------------------------------------------------------------------------------Bitmap Heap Scan on flights_bi (actual time=10.282..94.328 rows=83954 loops=1)Recheck Cond: ...Rows Removed by Index Recheck: 12045Heap Blocks: lossy=1664-> Bitmap Index Scan on flights_bi_scheduled_time_idx(actual time=3.013..3.013 rows=16640 loops=1)Index Cond: ...Planning time: 0.375 msExecution time: 97.805 ms
如我们所见,计划器使用了创建的索引。它有多精确?满足查询条件的行数(位图堆扫描节
点的“行”)与使用索引返回的总行数(相同的值加上通过索引重新检查删除的行)之比告诉我
们。在本例中,是83954 (83954 + 12045),大约为预期值的90%(这个值会随着时间的
变化而更改)。
位图索引扫描节点的“实际行”中的16640数字来自哪里?问题是,这个计划的该节点构建了不准确的(逐页)位图,并且完全不知道需要显示一些内容该位图将接触多少行。因此,绝望
地假设一页包含10行。位图总共包含1664页(该值在Heap Blocks: lossy=1664中显示);因此
,我们只得到16640。这是一个毫无意义的数字,不值得注意。
机场如何呢?例如,让我们以符拉迪沃斯托克(Vladivostok)的时区为例,该时区每天填充28页:
demo=#explain (costs off,analyze)select *from flights_biwhere airport_utc_offset = interval '8 hours';QUERY PLAN----------------------------------------------------------------------------------Bitmap Heap Scan on flights_bi (actual time=75.151..192.210 rows=587353 loops=1)Recheck Cond: (airport_utc_offset = '08:00:00'::interval)Rows Removed by Index Recheck: 191318Heap Blocks: lossy=13380-> Bitmap Index Scan on flights_bi_airport_utc_offset_idx(actual time=74.999..74.999 rows=133800 loops=1)Index Cond: (airport_utc_offset = '08:00:00'::interval)Planning time: 0.168 msExecution time: 212.278 ms计划器再次使用创建的BRIN索引。准确性较差(在这种情况下约为75%),但这是可以预期 的,因为相关性较低。
当然,可以在位图级别上连接几个BRIN索引(就像其他索引一样)。例如,以下是所选时区一个月的数据(注意“ BitmapAnd”节点): demo=# \set d'bookings.now()::date - interval \'60 days\''demo=#explain (costs off,analyze)select *from flights_biwhere scheduled_time >= :d and scheduled_time < :d + interval '30 days'and airport_utc_offset = interval '8 hours';QUERY PLAN---------------------------------------------------------------------------------Bitmap Heap Scan on flights_bi (actual time=62.046..113.849 rows=48154 loops=1)Recheck Cond: ...Rows Removed by Index Recheck: 18856Heap Blocks: lossy=1152-> BitmapAnd (actual time=61.777..61.777 rows=0 loops=1)-> Bitmap Index Scan on flights_bi_scheduled_time_idx(actual time=5.490..5.490 rows=435200 loops=1)Index Cond: ...-> Bitmap Index Scan on flights_bi_airport_utc_offset_idx(actual time=55.068..55.068 rows=133800 loops=1)Index Cond: ...Planning time: 0.408 msExecution time: 115.475 ms与B树比较
如果我们在与BRIN相同的字段上创建常规B树索引该怎么办?
demo=#createindex flights_bi_scheduled_time_btree on flights_bi(scheduled_time);demo=#select pg_size_pretty(pg_total_relation_size('flights_bi_scheduled_time_btree'));pg_size_pretty----------------654 MB(1 row)它看起来比我们的BRIN大数千倍!但是,查询的执行速度要快一些:计划器使用统计信息来 确定数据是物理排序的,不需要构建位图,并且主要是不需要重新检查索引条件: demo=#explain (costs off,analyze)select *from flights_biwhere scheduled_time >= :d and scheduled_time < :d + interval '1 day';QUERY PLAN----------------------------------------------------------------Index Scan using flights_bi_scheduled_time_btree on flights_bi(actual time=0.099..79.416 rows=83954 loops=1)Index Cond: ...Planning time: 0.500 msExecution time: 85.044 ms这就是BRIN的妙处:我们牺牲了效率,但获得了很大的空间。 操作符类别
minmax
对于其值可以相互比较的数据类型,摘要信息由最小值和最大值组成。相应的运算符类别的名 称包含«minmax»,例如«date_minmax_ops»。实际上,这些是我们目前考虑的数据类型,大 多数类型都是这种类型。 inclusive
并非为所有数据类型都定义比较运算符。例如,没有为代表机场地理坐标的点(«point»类 型)定义它们。顺便说一下,正是由于这个原因,统计信息并未显示此列的相关性。 demo=#select attname, correlationfrom pg_statswhere tablename='flights_bi' and attname ='airport_coord';attname | correlation---------------+-------------airport_coord |(1 row)但是,面对许多这种类型我们能够引入“边界区域”的概念,例如,几何形状的边界矩形。我们 详细讨论了GiST索引如何使用此功能。类似地,BRIN还支持在具有以下数据类型的列上收集 摘要信息:范围内所有值的边界区域就是摘要值。 与GiST不同,BRIN的汇总值必须与被索引的值属于同一类型。因此,我们不能为点建立索引 ,尽管很明显,坐标可以在BRIN中工作:经度与时区紧密相连。幸运的是,在将点转换为简化 的矩形后,没有任何东西会妨碍在表达式上创建索引。同时,我们将一个范围的大小设置为 一个页面,只是为了显示极限情况: demo=#createindex on flights_biusing brin (box(airport_coord)) withpages per range=1);
即使在这样极端情况下,索引的大小也只有30 MB:
demo=#select pg_size_pretty(pg_total_relation_size('flights_bi_box_idx'));pg_size_pretty----------------30 MB(1 row)现在我们可以通过坐标来创建限制机场的查询。例如: demo=#select airport_code, airport_namefrom airportswhere box(coordinates) <@ box '120,40,140,50';airport_code | airport_name--------------+-----------------KHV | Khabarovsk-NovyiVVO | Vladivostok(2 rows)
但是,计划器将拒绝使用我们的索引。 demo=#analyze flights_bi;demo=#explain select *from flights_biwhere box(airport_coord) <@ box'120,40,140,50';QUERY PLAN---------------------------------------------------------------------Seq Scan on flights_bi (cost=0.00..985928.14 rows=30517 width=111)Filter: (box(airport_coord) <@ '(140,50),(120,40)'::box)为什么?让我们禁用顺序扫描,看看会发生什么: demo=#set enable_seqscan =off;demo=#explain select *from flights_biwhere box(airport_coord) <@ box'120,40,140,50';QUERY PLAN--------------------------------------------------------------------------------Bitmap Heap Scan on flights_bi (cost=14079.67..1000007.81 rows=30517 width=111)Recheck Cond: (box(airport_coord) <@ '(140,50),(120,40)'::box)-> Bitmap Index Scan on flights_bi_box_idx(cost=0.00..14072.04 rows=30517076 width=0)Index Cond: (box(airport_coord) <@ '(140,50),(120,40)'::box)看来可以使用索引,但是计划器认为位图必须建立在整个表上(请看“Bitmap Index Scan”节
点的“行”),在这种情况下,选择顺序扫描也就不足为奇了。这里的问题是,对于几何类型,
PostgreSQL不收集任何统计信息,计划器必须盲目行动:
demo=#select *from pg_statswhere tablename ='flights_bi_box_idx' \gx-[ RECORD 1 ]----------+-------------------schemaname | bookingstablename | flights_bi_box_idxattname | boxinherited | fnull_frac | 0avg_width | 32n_distinct | 0most_common_vals |most_common_freqs |histogram_bounds |correlation |most_common_elems |most_common_elem_freqs |elem_count_histogram |
但是没有人对该索引有任何抱怨,它确实可以正常工作: demo=# explain (costs off,analyze)select * from flights_bi where box(airport_coord) <@ box '120,40,140,50';QUERY PLAN----------------------------------------------------------------------------------Bitmap Heap Scan on flights_bi (actual time=158.142..315.445 rows=781790 loops=1)Recheck Cond: (box(airport_coord) <@ '(140,50),(120,40)'::box)Rows Removed by Index Recheck: 70726Heap Blocks: lossy=14772-> Bitmap Index Scan on flights_bi_box_idx(actual time=158.083..158.083 rows=147720 loops=1)Index Cond: (box(airport_coord) <@ '(140,50),(120,40)'::box)Planning time: 0.137 msExecution time: 340.593 ms结论是这样的:如果几何图形有任何重要要求,则需要PostGIS。它仍然可以收集统计信息。 内部构造
传统的扩展名 «pageinspect» 使我们能够查看BRIN索引的内部。
首先,元信息将提示我们范围的大小以及为«revmap»分配了多少页:demo=# select *from brin_metapage_info(get_raw_page('flights_bi_scheduled_time_idx',0));magic | version | pagesperrange | lastrevmappage------------+---------+---------------+----------------0xA8109CFA | 1 | 128 | 3(1 row)这里的第1-3页分配给«revmap»,而其余部分包含摘要数据。我们可以从«revmap»中获得对
每个范围的汇总数据的引用。例如,第一个范围的信息,包含前128页,位于以下位置:
demo=# select *from brin_revmap_data(get_raw_page('flights_bi_scheduled_time_idx',1))limit 1;pages---------(6,197)(1 row)这是摘要数据本身:
demo=# select allnulls, hasnulls, valuefrom brin_page_items(get_raw_page('flights_bi_scheduled_time_idx',6),'flights_bi_scheduled_time_idx')where itemoffset = 197;allnulls | hasnulls | value----------+----------+----------------------------------------------------f | f | {2016-08-15 02:45:00+03 .. 2016-08-15 17:15:00+03}(1 row)
下一个范围:
demo=# select *from brin_revmap_data(get_raw_page('flights_bi_scheduled_time_idx',1))offset 1 limit 1;pages---------(6,198)(1 row)demo=# select allnulls, hasnulls, valuefrom brin_page_items(get_raw_page('flights_bi_scheduled_time_idx',6),'flights_bi_scheduled_time_idx')where itemoffset = 198;allnulls | hasnulls | value----------+----------+----------------------------------------------------f | f | {2016-08-15 06:00:00+03 .. 2016-08-15 18:55:00+03}(1 row)
对于«inclusion»类,«value»字段将显示类似的内容
{(94.4005966186523,69.3110961914062),(77.6600036621,51.6693992614746) .. f .. f}第一个值是嵌入矩形,最后的«f»字母表示缺少空元素(第一个)和缺少不可合并值(第二个)。实 际上,惟一不可合并的值是«IPv4»和«IPv6»地址(«inet»数据类型)。 属性
提醒您已经提供的查询 以下是访问方法的属性: amname | name | pg_indexam_has_property--------+---------------+-------------------------brin | can_order | fbrin | can_unique | fbrin | can_multi_col | tbrin | can_exclude | f可以在几个列上创建索引。在这种情况下,将为每列收集其自己的摘要统计信息,但对于每 个范围将它们一起存储。当然,如果一个相同大小的范围适用于所有列,则该索引才有意义。 以下索引层属性可用: name | pg_index_has_property---------------+-----------------------clusterable | findex_scan | fbitmap_scan | tbackward_scan | f显然,仅支持位图扫描。
但是,缺乏群集似乎令人困惑。看来,由于BRIN索引对行的物理顺序敏感,因此能够根据索引对数据进行聚类是合乎逻辑的。但是事实并非如此。我们只能创建一个“常规”索引(B树或 GiST,取决于数据类型)并根据它进行聚类。顺便说一句,您是否要考虑到排他锁,执行时 间以及重建过程中磁盘空间的消耗,来对一个庞大的表进行聚类? 以下是列层属性: name | pg_index_column_has_property--------------------+------------------------------asc | fdesc | fnulls_first | fnulls_last | forderable | fdistance_orderable | freturnable | fsearch_array | fsearch_nulls | t唯一可用的属性是操作NULL值的能力。 在下一篇中我们将继续一起探究Bloom索引。 I Love PG 关于我们
中国开源软件推进联盟PostgreSQL分会(简称:PG分会)于2017年成立,由国内多家PG生态企业所共同发起,业务上接受工信部产业发展研究院指导。PG分会致力于构建PG产业生态,推动PG产学研用发展,是国内一家PG行业协会组织。
技术文章精彩回顾 PostgreSQL学习的九层宝塔 PostgreSQL职业发展与学习攻略 搞懂PostgreSQL数据库透明数据加密之加密算法介绍 一文读懂PostgreSQL-12分区表 PostgreSQL源码学习之:RegularLock Postgresql源码学习之词法和语法分析 PostgreSQL buffer管理 最佳实践—PG数据库系统表空间重建 PostgreSQL V12中的流复制配置 2019,年度数据库舍 PostgreSQL 其谁? PostgreSQL使用分片(sharding)实现水平可扩展性 一文搞懂PostgreSQL物化视图 PostgreSQL原理解析之:PostgreSQL备机是否做checkpoint PostgreSQL复制技术概述 PG活动精彩回顾 见证精彩|PostgresConf.CN2019大会盛大开幕 PostgresConf.CN2019大会DAY2|三大分论坛,精彩不断 PostgresConf.CN2019培训日|爆满!Training Day现场速递! 「PCC-Training Day」培训日Day2圆满结束,PCC2019完美收官 创建PG全球生态!PostgresConf.CN2019大会盛大召开 首站起航!2019“让PG‘象’前行”上海站成功举行 走进蓉城丨2019“让PG‘象’前行”成都站成功举行 中国PG象牙塔计划发布,首批合作高校授牌仪式在天津举行 PostgreSQL实训基地落户沈阳航空航天大学和渤海大学,高校数据库课改正当时 群英论道聚北京,共话PostgreSQL 相聚巴厘岛| PG Conf.Asia 2019 DAY0、DAY1简报 相知巴厘岛| PG Conf.Asia 2019 DAY2简报 相惜巴厘岛| PG Conf.Asia 2019 DAY3简报 独家|硅谷Postgres大会简报 全球规模最大的PostgreSQL会议等你来! PG培训认证精彩回顾 关于中国PostgreSQL培训认证,你想知道的都在这里! 首批中国PGCA培训圆满结束,首批认证考试将于10月18日和20日举行! 中国首批PGCA认证考试圆满结束,203位考生成功获得认证! 中国第二批PGCA认证考试圆满结束,115位考生喜获认证! 请查收:中国首批PGCA证书! 重要通知:三方共建,中国PostgreSQL认证权威升级! 一场考试迎新年 | 12月28日,首次PGCE中级认证考试开考! 近500人参与!首次PGCE中级、第三批次PGCA初级认证考试落幕!







