Quantcast
Channel: 数据库内核月报
Viewing all 689 articles
Browse latest View live

MySQL · 引擎特性 · 临时表那些事儿

$
0
0

前言

相比于普通的用户数据表,MySQL/InnoDB中的临时表,大家应该会陌生很多。再加上不同的临时表创建的时机和创建的位置都不固定,这也进一步加大神秘感。最让人捉摸不透的是,临时表很多时候会先创建文件,然后什么都不做,就把文件删除,留一个句柄读写,给人的感觉是神龙见首不见尾。本文分析了详细MySQL各个版本临时表的处理方式,希望对大家有所帮助。

综述

准确的说,我们常说的临时表分为两种,一种真的是表,用来存储用户发送的数,读写走的是表读写接口,读写的时候表一定在文件系统上存在,另外一种,应该是一种临时文件,用来存储SQL计算中间过程的数据,读写走的是文件读写接口,读写的时候文件可能已经被删除了,留一个文件句柄进行操作。

临时表

临时表可以分为磁盘临时表和内存临时表,而临时文件,只会存在于磁盘上,不会存在于内存中。具体来说,临时表的内存形态有Memory引擎和Temptable引擎,主要区别是对字符类型(varchar, blob,text类型)的存储方式,前者不管实际字符多少,都是用定长的空间存储,后者会用变长的空间存储,这样提高了内存中的存储效率,有更多的数据可以放在内存中处理而不是转换成磁盘临时表。Memory引擎从早期的5.6就可以使用,Temptable是8.0引入的新的引擎。另外一方面,磁盘临时表也有三种形态,一种是MyISAM表,一种是InnoDB临时表,另外一种是Temptable的文件map表。其中最后一种方式,是8.0提供的。

在5.6以及以前的版本,磁盘临时表都是放在数据库配置的临时目录,磁盘临时表的undolog都是与普通表的undo放在一起(注意由于磁盘临时表在数据库重启后就被删除了,不需要redolog通过奔溃恢复来保证事务的完整性,所以不需要写redolog,但是undolog还是需要的,因为需要支持回滚)。

在MySQL 5.7后,磁盘临时表的数据和undo都被独立出来,放在一个单独的表空间ibtmp1里面。之所以把临时表独立出来,主要是为了减少创建删除表时维护元数据的开销。

在MySQL 8.0后,磁盘临时表的数据单独放在Session临时表空间池(#innodb_temp目录下的ibt文件)里面,临时表的undo放在global的表空间ibtmp1里面。另外一个大的改进是,8.0的磁盘临时表数据占用的空间在连接断开后,就能释放给操作系统,而5.7的版本中需要重启才能释放。

目前有以下两种情况会用到临时表:

用户显式创建临时表

这种是用户通过显式的执行命令create temporary table创建的表,引擎的类型要么显式指定,要么使用默认配置的值(default_tmp_storage_engine)。内存使用就遵循指定引擎的内存管理方式,比如InnoDB的表会先缓存在Buffer Pool中,然后通过刷脏线程写回磁盘文件。

在5.6中,磁盘临时表位于tmpdir下,文件名类似#sql4d2b_8_0.ibd,其中#sql是固定的前缀,4d2b是进程号的十六进制表示,8是MySQL线程号的十六进制表示(show processlist中的id),0是每个连接从0开始的递增值,ibd是innodb的磁盘临时表(通过参数default_tmp_storage_engine控制)。在5.6中,磁盘临时表创建好后,对应的frm以及引擎文件就在tmpdir下创建完毕,可以通过文件系统ls命令查看到。在连接关闭后,相应文件自动删除。因此,我们如果在5.6的tmpdir里面看到很多类似格式文件名,可以通过文件名来判断是哪个进程,哪个连接使用的临时表,这个技巧在排查tmpdir目录占用过多空间的问题时,尤其适用。用户显式创建的这种临时表,在连接释放的时候,会自动释放并把空间释放回操作系统。临时表的undolog存在undo表空间中,与普通表的undo放在一起。有了undo回滚段,用户创建的这种临时表也能支持回滚了。

在5.7中,临时磁盘表位于ibtmp文件中,ibtmp文件位置及大小控制方式由参数innodb_temp_data_file_path控制。显式创建的表的数据和undo都在ibtmp里面。用户连接断开后,临时表会释放,但是仅仅是在ibtmp文件里面标记一下,空间是不会释放回操作系统的。如果要释放空间,需要重启数据库。另外,需要注意的一点是,5.6可以在tmpdir下直接看到创建的文件,但是5.7是创建在ibtmp这个表空间里面,因此是看不到具体的表文件的。如果需要查看,则需要查看INFORMATION_SCHEMA.INNODB_TEMP_TABLE_INFO这个表,里面有一列name,这里可以看到表名。命名规格与5.6的类似,因此也可以快速找到占用空间大的连接。

在8.0中,临时表的数据和undo被进一步分开,数据是存放在ibt文件中(由参数innodb_temp_tablespaces_dir控制),undo依然存放在ibtmp文件中(依然由参数innodb_temp_data_file_path控制)。存放ibt文件的叫做Session临时表空间,存放undo的ibtmp叫做Global临时表空间。这里介绍一下这个存放数据的Session临时表空间。Session临时表空间,在磁盘上的表现是一组以ibt文件组成的文件池。启动的时候,数据库会在配置的目录下重新创建,关闭数据库的时候删除。启动的时候,默认会创建10个ibt文件,每个连接最多使用两个,一个给用户创建的临时表用,另外一个给下文描述的优化器创建的隐式临时表使用。当然只有在需要临时表的时候,才会创建,如果不需要,则不会占用ibt文件。当10个ibt都被使用完后,数据库会继续创建,最多创建四十万个。当连接释放时候,会自动把这个连接使用的ibt文件给释放,同时回收空间。如果要回收Global临时表空间,依然需要重启。但是由于已经把存放数据的文件分离出来,且其支持动态回收(即连接断开即释放空间),所以5.7上困扰大家多时的空间占用问题,已经得到了很好的缓解。当然,还是有优化空间的,例如,空间需要在连接断开后,才能释放,而理论上,很多空间在某些SQL(如用户drop了某个显式创建的临时表)执行后,即可以释放。另外,如果需要查看表名,依然查看INFORMATION_SCHEMA.INNODB_TEMP_TABLE_INFO这个表。需要注意的是,8.0上,显式临时表不能是压缩表,而5.6和5.7可以。

优化器隐式创建临时表

这种临时表,是数据库为了辅助某些复杂SQL的执行而创建的辅助表,是否需要临时表,一般都是由优化器决定。与用户显式创建的临时表直接创建磁盘文件不同,如果需要优化器觉得SQL需要临时表辅助,会先使用内存临时表,如果超过配置的内存(min(tmp_table_size, max_heap_table_siz)),就会转化成磁盘临时表,这种磁盘临时表就类似用户显式创建的,引擎类型通过参数internal_tmp_disk_storage_engine控制。一般稍微复杂一点的查询,包括且不限于order by, group by, distinct等,都会用到这种隐式创建的临时表。用户可以通过explain命令,在Extra列中,看是否有Using temporary这样的字样,如果有,就肯定要用临时表。

在5.6中,隐式临时表依然在tmpdir下,在复杂SQL执行的过程中,就能看到这临时表,一旦执行结束,就被删除。值得注意的是,5.6中,这种隐式创建的临时表,只能用MyISAM引擎,即没有internal_tmp_disk_storage_engine这个参数可以控制。所以,当我们的系统中只有innodb表时,也会看到MyISAM的某些指标在变动,这种情况下,一般都是隐式临时表的原因。

在5.7中,隐式临时表是创建在ibtmp文件中的,SQL结束后,会标记删除,但是空间依然不会返还给操作系统,如果需要返还,则需要重启数据库。另外,5.7支持参数internal_tmp_disk_storage_engine,用户可以选择InnoDB或者MYISAM表作为磁盘临时表。

在8.0中,隐式临时表是创建在Session临时表空间中的,即与用户显式创建的临时表的数据放在一起。如果一个连接第一次需要隐式临时表,那么数据库会从ibt文件构成的池子中取出一个给这个连接使用,直到连接释放。上文中,我们也提到过,在8.0中,用户显式创建的临时表也会从池子中分配一个ibt来使用,每个连接最多使用两个ibt文件用来存储临时表。我们可以查询INFORMATION_SCHEMA.INNODB_SESSION_TEMP_TABLESPACES来确定ibt文件的去向。这个表中,每个ibt文件是一行,当前系统中有几个ibt文件就有几行。有一列叫做ID,如果此列为0,表示此ibt没有被使用,如果非0,表示被此ID的连接在用,比如ID为8,则表示process_id为8的连接在用这个ibt文件。另外,还有一列purpose,值为INTRINSIC表示是隐式临时表在用这个ibt,USER则表示是显示临时表在用。此外,还有一列size,表示当前的大小。用户可以查询这个表来确定整个数据库临时表的使用情况,十分方便。

在5.6和5.7中,内存临时表只能使用Memory引擎,到了8.0,多了一种Temptable引擎的选择。Temptable在存储格式有采用了变长存储,可以节省存储空间,进一步提高内存使用率,减少转换成磁盘临时表的次数。如果设置的磁盘临时表是InnoDB或者MYISAM,则需要一个转换拷贝的消耗。为了尽可能减少消耗,Temptable提出了一种overflow机制,即如果内存临时表超过配置大小,则使用磁盘空间map的方式,即打开一个文件,然后删除,留一个句柄进行读写操作。读写文件格式和内存中格式一样,这样就略过了转换这一步,进一步提高性能。注意,这个功能是在还没发布的8.0.16版本中才有的,因为还看不到代码,只能通过文档猜测其实现。在8.0.16中,参数internal_tmp_disk_storage_engine已经被去掉,磁盘临时表只能使用InnoDB形式或者TempTable的这种overflow形式。从文档中,我们似乎看出官方比较推荐使用TempTable这个新的引擎。具体性能提升情况,还需要等代码发布后,测试过才能得出结论。

临时文件

相比临时表,临时文件对大家可能更加陌生,临时文件更多的被使用在缓存数据,排序数据的场景中。一般情况下,被缓存或者排序的数据,首先放在内存中,如果内存放不下,才会使用磁盘临时文件的方式。临时文件的使用方式与一般的表也不太一样,一般的表创建完后,就开始读写数据,使用完后,才把文件删除,但是临时文件的使用方式不一样,在创建完后(使用mkstemp系统函数),马上调用unlink删除文件,但是不close文件,后续使用原来的句柄操作文件。这样的好处是,当进程异常crash,不会有临时文件因为没被删除而残留,但是坏处也是明显的,我们在文件系统上使用ls命令就看不到这个文件,需要使用lsof +L1来查看这种deleted属性的文件。

目前,我们主要在一下场景使用临时文件:

DDL中的临时文件

在做online DDL的过程中,很多操作需要对原表进行重建,对表重建前,需要对各种二级索引排序,而大量数据的排序,不太可能在内存中完成,需要依赖外部排序算法,MySQL使用了归并排序。这个过程中就需要创建临时文件。一般需要的空间大小与原表差不多。但是在使用完之后,会马上清理,所以在做DDL的时候,需要保留出足够的空间。用户可以通过指定innodb_tmpdir来指定这种排序文件的路径。这个参数可以动态修改,一般把他设置在有足够磁盘空间的路径上。临时文件的名字一般是类似ibXXXXXX,其中ib是固定前缀,XXXXXX是大小写字母以及数字的随机组合。

在做online DDL中,我们是允许用户对原表做DML操作的,即增删改查。我们不能直接插入原表中,因此需要一个地方记录对原表的修改操作,在DDL结束后,再应用在新表上。这个记录的地方就是online log,当然如果改动少的话,直接存在内存里(参数innodb_sort_buffer_size可控制,同时这个参数也控制online log每个读写块的大小)面即可。这个onlinelog也是用临时文件存,创建在innodb_tmpdir,最大大小为参数innodb_online_alter_log_max_size控制,如果超过这个大小了,DDL就会失败。临时文件的名字也类似上述的排序临时文件的名字。

在online DDL的最后阶段,需要把排序完的文件和中途产生的DML全都应用到一个中间文件上,中间文件文件名类似#sql-ib53-522550444.ibd,其中#sql-ib是固定的前缀,53是InnoDB层的table id,522550444是随机生成的数字。同时,在server层也会生成一个frm文件(8.0中没有),文件名类似#sql-4d2b_2a.frm,其中#sql是固定前缀,4d2b是进程号的十六进制表示,2a是线程号的十六进制表示(show processlist中的id)。因此我们也可以通过这个命名规则来找到哪个线程在做DDL。这里需要注意一点,这里说的中间文件,其实算是一个临时表,并不是上文说中临时文件,这些中间文件可以通过ls来查看。当在DDL中的最后一步,会把这两个临时文件命名回原来的表名。正因为这个特性,所以当数据库中途crash的时候,可能会在磁盘上留下残余无用的文件。遇到这种情况,可以先把frm文件重命名成与ibd文件一样的名字,然后使用DROP TABLE #mysql50##sql-ib53-522550444`来清理残余的文件。注意,如果不用drop命令,直接删除ibd文件,可能会导致数据字典里面依然有残余的信息,做法不太优雅。当然,在8.0中,由于使用了原子的数据字典,就不会出现这种残余文件了。

BinLog中的缓存操作

BinLog只有在事务提交的时候才会写入到文件中,在没提交前,会先放在内存中(由参数binlog_cache_size控制),如果内存放慢了,就会创建临时文件,使用方法也是先通过mkstemp创建,然后直接unlink,留一个句柄读写。临时文件名类似MLXXXXXX,其中ML是固定前缀,XXXXXX是大小写字母以及数字的随机组合。单个事务的BinLog太大,可能会导致整个BinLog的大小也过大,从而影响同步,因此我们需要尽可能控制事务大小。

优化创建的临时文件

有些操作,除了在引擎层需要依赖隐式临时表来辅助复杂SQL的计算,在Server层,也会创建临时文件来辅助,比如order by操作,会调用filesort函数。这个函数也会先使用内存(sort_buffer_size)排序,如果不够,就会创建一个临时文件,辅助排序。文件名类似MYXXXXXX,其中MY是固定前缀,XXXXXX是大小写字母以及数字的随机组合。

Load data中用的临时文件

在BinLog复制中,如果在主库上使用了Load Data命令,即从文件中导数据,数据库会把整个文件写入到RelayLog中,然后传到备库,备库解析RelayLog,从中抽取出对应的Load文件,然后在备库上应用。备库上这个文件存储的位置由参数slave_load_tmpdir控制。文档中建议这个目录不要配置在物理机的内存目录或者重启后会删除的目录。因为复制依赖这个文件,如果意外被删除,会导致复制中断。

其他

除了上文所述的几个地方外,还有其他几个地方也会用到临时文件:

  • 在InnoDB层,启动的时候会创建多个临时文件用来存储:最后一次外键或者唯一键错误; 最后一次死锁的信息; 最后的innodb状态信息。用临时文件而不用内存的原因猜测是,内存使用率不会因为写这些指标而波动。
  • 在Server层,分区表使用show create table时,会用到临时文件。另外在MYISAM表内部排序的时候也会用到临时文件。

相关参数

tmpdir:这个参数是临时目录的配置,在5.6以及之前的版本,临时表/文件默认都会放在这里。这个参数可以配置多个目录,这样就可以轮流在不同的目录上创建临时表/文件,如果不同的目录分别指向不同的磁盘,就可以达到分流的目的。

innodb_tmpdir:这个参数只要是被DDL中的排序临时文件使用的。其占用的空间会很大,建议单独配置。这个参数可以动态设置,也是一个Session变量。

slave_load_tmpdir:这个参数主要是给BinLog复制中Load Data时,配置备库存放临时文件位置时使用。因为数据库Crash后还需要依赖Load数据的文件,建议不要配置重启后会删除数据的目录。

internal_tmp_disk_storage_engine:当隐式临时表被转换成磁盘临时表时,使用哪种引擎,默认只有MyISAM和InnoDB。5.7及以后的版本才支持。8.0.16版本后取消的这个参数。

internal_tmp_mem_storage_engine:隐式临时表在内存时用的存储引擎,可以选择Memory或者Temptable引擎。建议选择新的Temptable引擎。

default_tmp_storage_engine:默认的显式临时表的引擎,即用户通过SQL语句创建的临时表的引擎。

tmp_table_size: min(tmp_table_size,max_heap_table_size)是隐式临时表的内存大小,超过这个值会转换成磁盘临时表。

max_heap_table_size:用户创建的Memory内存表的内存限制大小。

big_tables:内存临时表转换成磁盘临时表需要有个转化操作,需要在不同引擎格式中转换,这个是需要消耗的。如果我们能提前知道执行某个SQL需要用到磁盘临时表,即内存肯定不够用,可以设置这个参数,这样优化器就跳过使用内存临时表,直接使用磁盘临时表,减少开销。

temptable_max_ram:这个参数是8.0后才有的,主要是给Temptable引擎指定内存大小,超过这个后,要么就转换成磁盘临时表,要么就使用自带的overflow机制。

temptable_use_mmap:是否使用Temptable的overflow机制。

总结建议

MySQL的临时表以及临时文件其实是一个比较复杂的话题,涉及的模块比较多,出现的时机比较难把握,导致排查问题相比普通表也难不少。建议读者结合代码细细研究,这样才能定位在线上可能出现的棘手问题。


MSSQL · 最佳实践 · 使用SSL加密连接

$
0
0

摘要

在SQL Server安全系列专题月报分享中,往期我们已经陆续分享了:如何使用对称密钥实现SQL Server列加密技术使用非对称密钥实现SQL Server列加密使用混合密钥实现SQL Server列加密技术列加密技术带来的查询性能问题以及相应解决方案行级别安全解决方案SQL Server 2016 dynamic data masking实现隐私数据列打码技术使用证书做数据库备份加密SQL Server Always Encrypted这八篇文章,直接点击以上文章前往查看详情。本期月报我们分享SQL Server SSL证书连接加密技术,实现网络上传输层连接加密。

问题引入

在SQL Server关系型数据库中,我们可以使用透明数据加密(TDE)、行级别加密(Row-level Security)、数据打码(Dynamic Data Masking)和备份加密(Backup Encryption)等技术来实现数据库引擎层的安全。但是,在网络传输层,客户端和服务端之前默认没有数据加密传输保护。因此,为了提高链路安全性,我们可以启用SSL(Secure Sockets Layer)加密,SSL在传输层对网络连接进行加密,能提升数据通道的安全性,但同时会增加网络连接响应时间和CPU开销。

准备工作

为了方便观察,我们使用Microsoft Network Monitor 3.4(以下简称MNM)工具来观察网络传输层事件,如果您已经安装MNM,请跳过该准备工作部分。

首先,我们从微软官网下载MNM,根据需要下载对应的版本,我们这里下载64 bit版本,NM34_x64.exe。 接下来,安装MNM,直接执行NM34_x64.exe,然后按照向导完成安装。

最后,重启OS。

启用SSL证书之前

在启用SSL证书加密之前,客户端和SQL Server服务端的网络传输层默认没有加密保护的,我们可以通过如下步骤验证。

 创建测试表

 新建MNM抓取

 连接查询测试

 MNM中检查

 动态视图查看加密状态

创建测试表

为了测试方便,我们首先创建测试表CustomerInfo,存入三个客户敏感信息,包含客户名称和客户电话号码。

USE [TestDb]
GO
IF OBJECT_ID('dbo.CustomerInfo', 'U') IS NOT NULL
	DROP TABLE dbo.CustomerInfo
CREATE TABLE dbo.CustomerInfo
(
CustomerId		INT IDENTITY(10000,1)	NOT NULL PRIMARY KEY,
CustomerName	VARCHAR(100)			NOT NULL,
CustomerPhone	CHAR(11)				NOT NULL
);

-- Init Table
INSERT INTO dbo.CustomerInfo 
VALUES ('CustomerA','13402872514')
,('CustomerB','13880674722')
,('CustomerC','13487759293')
GO

新建MNM抓取

打开MNM,点击New Capture,然后Start,启动网络层时间抓取。

01.png

连接查询测试

从客户端,连接上对应的SQL Server,执行下面的查询语句,以便观察MNM抓取情况。

USE [TestDb]
GO
SELECT * FROM dbo.CustomerInfo WITH(NOLOCK)

执行结果如下:

02.png

MNM中检查

我们仔细观察MNM中的事件,发现在客户机和SQL Server服务端的网络传输层,使用的明文传输,如下截图:

03.png

从图中右下角红色方框中,我们可以清清楚楚的看到了这三个客户的姓名和对应的手机号码,我们使用MNM看到数据在网络传输层以明文传送,并未做任何加密,可能会存在数据被窃听的风险。

动态视图查看连接状态

当然,您也可以从SQL Server的连接动态视图看出,连接并未加密:

04.png

从MNM和SQL Server动态视图我们可以得出相同的结论是:客户端和SQL Server服务端数据在网络传输层默认以明文传送,并未加密传输,可能会存在数据被窃听的风险。那么,我们可以启动SSL证书来加密数据传输,以达到更为安全的目的。

启用SSL证书

启动SSL证书,分为以下几个部分:

 证书申请

 强制所有连接使用SSL

 加密特定客户端连接

证书申请

Start –> 输入:mmc.exe -> File -> Add/Remove Snap-ins -> Certificate -> add -> Computer account -> Next -> Local Computer -> Finish -> OK

05.png

展开Certificates -> 右键 Personal -> 选择 All Tasks -> 选择Request New Certificate -> 点击 Next -> 选中 Computer -> 点击Enroll -> 点击Finish。 右键点击对应证书 -> 选中All Tasks -> 选择Manage Private Keys… -> 授予 read 权限给本地账号NT Service\MSSQLSERVER。

强制所有连接使用SSL

强制所有连接加密

在SQL Server服务器上,Start -> Run -> sqlservermanager13.msc -> 右键点击Protocols for MSSQLSERVER -> Flags中将Force Encryption设置为Yes -> Certificate选项卡中选择证书 -> OK

06.png

重启SQL Service

强制所有连接设置完毕后,如果想要立即生效,请重启SQL Service。 注意: 这里需要特别注意,如果是目前线上正常运行的应用,请慎重测试后,打开强制所有连接使用SSL。

加密特定客户端连接

当然,您也可以不用打开强制所有的连接使用SSL,转而使用加密特定的客户端连接,这里以SSMS连接工具为例。

客户端导入证书

Start -> Run -> 输入:certmgr.msc -> 右键选择Trusted Root Certification Authorities -> All Tasks -> Import

07.png

选择SQL Server服务端生成的证书文件

08.png

Next -> Finish -> OK

SSMS启用加密连接

在SSMS连接服务端界面 -> 选择Options

09.png

然后选择Encrypt connection

10.png

然后,参照“连接查询测试”中方法进行连接测试。同样在连接管理视图中查看,我们可以看到连接已经加密: 11.png

至此,使用SSL证书加密加密客户端和SQL Server服务端连接的实验成功。

注意事项

由于使用了SSL证书来加密客户端和SQL Server服务端连接,在提升数据通信的安全性同时,加密解密操作也会导致网络连接响应时间增加和CPU使用率上升,对业务系统有一定的性能影响。因此,建议您仅在外网链路有加密需求的时候启用SSL加密,内网链路相对较安全,一般无需对链路加密。

最后总结

本期月报我们分享了如何启用SSL证书,来加密客户端和SQL Server服务端连接,提升网络传输层通信安全,使得数据在传输过程中被加密后,以密文传送,最大限度保证了链路安全。

Redis · 引擎特性 · radix tree 源码解析

$
0
0

Redis实现了不定长压缩前缀的radix tree,用在集群模式下存储slot对应的的所有key信息。本文将详述在Redis中如何实现radix tree。

核心数据结构

raxNode是radix tree的核心数据结构,其结构体如下代码所示:

typedef struct raxNode {
    uint32_t iskey:1;     
    uint32_t isnull:1;    
    uint32_t iscompr:1;   
    uint32_t size:29;     
    unsigned char data[];
} raxNode;
  • iskey:表示这个节点是否包含key
    • 0:没有key
    • 1:表示从头部到其父节点的路径完整的存储了key,查找的时候按子节点iskey=1来判断key是否存在
  • isnull:是否有存储value值,比如存储元数据就只有key,没有value值。value值也是存储在data中
  • iscompr:是否有前缀压缩,决定了data存储的数据结构
  • size:该节点存储的字符个数
  • data:存储子节点的信息
    • iscompr=0:非压缩模式下,数据格式是:[header strlen=0][abc][a-ptr][b-ptr][c-ptr](value-ptr?),有size个字符,紧跟着是size个指针,指向每个字符对应的下一个节点。size个字符之间互相没有路径联系。
    • iscompr=1:压缩模式下,数据格式是:[header strlen=3][xyz][z-ptr](value-ptr?),只有一个指针,指向下一个节点。size个字符是压缩字符片段

Rax Insert

以下用几个示例来详解rax tree插入的流程。假设j是遍历已有节点的游标,i是遍历新增节点的游标。

场景一:只插入abcd

z-ptr指向的叶子节点iskey=1,使用了压缩前缀。

场景二:在abcd之后插入abcdef

从abcd父节点的每个压缩前缀字符比较,遍历完所有abcd节点后指向了其空子节点,j = 0, i < len(abcded)。
查找到abcd的空子节点,直接将ef赋值到子节点上,成为abcd的子节点。ef节点被标记为iskey=1,用来标识abcd这个key。ef节点下再创建一个空子节点,iskey=1来表示abcdef这个key。

场景三:在abcd之后插入ab

ab在abcd能找到前两位的前缀,也就是i=len(ab),j < len(abcd)。
将abcd分割成ab和cd两个子节点,cd也是一个压缩前缀节点,cd同时被标记为iskey=1,来表示ab这个key。
cd下挂着一个空子节点,来标记abcd这个key。

场景四:在abcd之后插入abABC

abcABC在abcd中只找到了ab这个前缀,即i < len(abcABC),j < len(abcd)。这个步骤有点复杂,分解一下:

  • step 1:将abcd从ab之后拆分,拆分成ab、c、d 三个节点。
  • step 2:c节点是一个非压缩的节点,c挂在ab子节点上。
  • step 3:d节点只有一个字符,所以也是一个非压缩节点,挂在c子节点上。
  • step 4:将ABC 拆分成了A和BC, A挂在ab子节点上,和c节点属于同一个节点,这样A就和c同属于父节点ab。
  • step 5:将BC作为一个压缩前缀的节点,挂在A子节点下。
  • step 6:d节点和BC节点都挂一个空子节点分别标识abcd和abcABC这两个key。

场景五:在abcd之后插入Aabc

abcd和Aabc没有前缀匹配,i = 0,j = 0。
将abcd拆分成a、bcd两个节点,a节点是一个非压缩前缀节点。
将Aabc拆分成A、abc两个节点,A节点也是一个非压缩前缀节点。
将A节点挂在和a相同的父节点上。
同上,在bcd和abc这两个节点下挂空子节点来分别表示两个key。

Rax Remove

删除

删除一个key的流程比较简单,找到iskey的节点后,向上遍历父节点删除非iskey的节点。如果是非压缩的父节点并且size > 1,表示还有其他非相关的路径存在,则需要按删除子节点的模式去处理这个父节点,主要是做memove和realloc。

合并

删除一个key之后需要尝试做一些合并,以收敛树的高度。
合并的条件是:

  • iskey=1的节点不能合并
  • 子节点只有一个字符
  • 父节点只有一个子节点(如果父节点是压缩前缀的节点,那么只有一个子节点,满足条件。如果父节点是非压缩前缀的节点,那么只能有一个字符路径才能满足条件)

结束语

云数据库Redis版(ApsaraDB for Redis)是一种稳定可靠、性能卓越、可弹性伸缩的数据库服务。基于飞天分布式系统和全SSD盘高性能存储,支持主备版和集群版两套高可用架构。提供了全套的容灾切换、故障迁移、在线扩容、性能优化的数据库解决方案。欢迎各位购买使用:云数据库 Redis 版

MySQL · 引擎分析 · InnoDB history list 无法降到0的原因

$
0
0

熟悉InnoDB的朋友都知道,innodb的history list长度代表了有多少undo日志还没有被清理掉,可以通过show engine innodb status 命令来获得。如果发现history list的长度越大,要么就是实例的复杂非常高,要么就是可能有大查询,或者事务没提交,导致Undo log无法分析。

但如果仔细观察,大家是否发现,history list居然无法降到0,即使做一次slow shutdown也不行。因为理论上来说,如果undo日志都已经purge干净了,理论上应该能下降为0。

为了更好的理解,我们先普及几个概念。首先innodb支持多个rollback segment,每个segment包含约1024个slot。

当事务开启时,会给它指定使用哪个rollback segment,然后在真正执行操作时,分配具体的slot,通常会有两种slot:

  • update_undo: 只用于事务内的update语句
  • insert_undo:只用于事务内的insert语句

通常如果事务内只包含一种操作类型,则只使用一个slot。但也有例外,例如insert操作,如果insert的记录在page上已经存在了,但是是无效的,那么久可以直接通过更新这条无效记录的方式来实现插入,这时候使用的是update_undo.

为什么要分成两种undo slot,而不是只用一个slot处理所有呢?这是因为在提交阶段的undo处理不同:

对于Insert undo, 有两种处理方式

  • Free: 直接清理掉,因为我们知道新插入的记录产生的Undo不会被任何查询语句所引用,因此可以直接释放undo,这里的undo log不会累加到history list上
  • reuse: 当undo 只占用一个page,且page使用低于一定比例时(事实上,第二个条件对于insert undo可以移除掉),放到cachd list上,以备重用。 在重用时,会将该page reset掉

对于update_undo: 也有两种处理方式:

  • Purge: 这里会加入到其对应rollback segment的history list数据页列表上,history list长度加1
  • Reuse: 同样会将undo加到history list上,history list长度加1。by the way, update undo和insert的重用方式不同,它会在undo page上新建一个undo log header, 而不是重置page。这意味着一个undo页上可能有多个undo log分属不同的事务,但只有一个可能是活跃的。

那么回到最初的问题,既然undo log都加到history list了,为啥在undo purge完成后,未重置为0呢?

我们来看看如下函数

    trx_purge_truncate
          trx_purge_truncate_history
                          trx_purge_truncate_rseg_history

在函数trx_purge_truncate_rseg_history中,有如下代码段:

if ((mach_read_from_2(seg_hdr + TRX_UNDO_STATE) == TRX_UNDO_TO_PURGE)
    && (mach_read_from_2(log_hdr + TRX_UNDO_NEXT_LOG) == 0)) {

  /* We can free the whole log segment */

  mutex_exit(&(rseg->mutex));
  mtr_commit(&mtr);

  trx_purge_free_segment(rseg, hdr_addr, n_removed_logs);

  n_removed_logs = 0;
} else {
  mutex_exit(&(rseg->mutex));
  mtr_commit(&mtr);
}

这里做了特殊判断,只有状态为PURGE的undo log才做了free segment清理。对于cached状态的undo留在原地。个人猜测是因为这些undo log可以留作重用, 在重用之后,再做一次性清理。

为了验证猜测,修改函数trx_undo_set_state_at_finish,使undo log状态,要么为TRX_UNDO_TO_FREE, 要么为TRX_UNDO_TO_PURGE。

在给实例加了一定的负载,再做一次slow shutdown重启后,history list length的长度果然变成了0。验证了其无法重置为0是由于cached undo导致。

MySQL · 关于undo表空间的一些新变化

$
0
0

基于版本为 MySQL8.0.3

InnoDB的undo log是其实现多版本的关键组件,在物理上以数据页的形式进行组织。在早期版本中(<5.6),undo tablespace是在ibdata中,因此一个常见的问题是由于大事务不提交导致ibdata膨胀,这时候通常只有重建数据库一途来缩小空间。到了MySQL5.6版本, InnoDB开始支持独立的undo tablespace,也就是说,undo log可以存储于ibdata之外。但这个特性依然鸡肋:

  1. 首先你必须在install实例的时候就指定好独立Undo tablespace, 在install完成后不可更改。
  2. Undo tablepsace的space id必须从1开始,无法增加或者删除undo tablespace。

到2. 了MySQL5.7版本中,终于引入了一个期待已久的功能:即在线truncate undo tablespace。DBA终于摆脱了undo空间膨胀的苦恼。

在MySQL8.0,InnoDB再进一步,对undo log做了进一步的改进:

  1. 无需从space_id 1开始创建undo tablespace,这样解决了In-place upgrade或者物理恢复到一个打开了Undo tablespace的实例所产生的space id冲突。不过依然要求undo tablespace的space id是连续分配的(fsp_is_undo_tablespace), space id的范围为(0xFFFFFFF0UL - 128, 0xFFFFFFF0UL - 1) (Bug #23517560)
  2. 从8.0.3版本开始,默认undo tablespace的个数从0调整为2,也就是在8.0版本中,独立undo tablespace被默认打开。修改该参数为0会报warning并在未来不再支持(WL#10583)
  3. 允许动态的增加undo tablespace的个数,也就是说可以动态调整innodb_undo_tablespaces。当调大该参数时,会去创建新的undo tablespace。但如果设小该值,则仅仅是不实用多出来的Undo tablespace,目前不会去主动删除它们(innodb_undo_tablespaces_update), WL#9507
  4. Undo tablespace的命名从undoNNN修改为undo_NNN
  5. 和以前版本最大的不同之处就是,在8.0之前只能创建128个回滚段,而在8.0版本开始,每个Undo tablespace可以创建128个回滚段,也就是说,总共有innodb_rollback_segments * innodb_undo_tablespaces个回滚段。这个改变的好处是在高并发下可以显著的减少因为分配到同一个回滚段内的事务间产生的锁冲突
  6. Innodb_undo_truncate参数默认打开,这意味着默认情况下,undo tablespace超过1GB(参数innodb_max_undo_log_size来控制)时,就会触发online truncate
  7. 支持undo tablespace加密( 参考文档) PS: 本文是本人在看代码过程中的粗略记录,不保证全面性 8.在ibdata中保留的32个slot原本用于临时表空间的回滚段,但事实上他们并没有做任何的持久化,因此在8.0中直接在内存中为其创建单独的内存结构,这32个slot可以用于持久化的undo回滚段(Bug #24462978!

主要代码commit(按照时间顺序由新到旧):

Commit 1

WL#10583: Stop using rollback segments in the system tablespace

* Change the minimum value of innodb_undo_tablespaces to 2
* Fix code that allows and checks for innodb_undo_tablespaces=0
* Fix all testcases affected

Commit 2

WL#9507: Make innodb_undo_tablespaces variable dynamic
WL#10498: InnoDB: Change Default for innodb_undo_tablespaces from 0 to 2
WL#10499: InnoDB: Change Default for innodb_undo_log_truncate from OFF to ON

* Introduce innodb_undo_tablespace_update() and
srv_undo_tablespaces_update() for online updates.
* Introduce innodb_rollback_segments_update() for online updates.
* Introduce srv_undo_tablespaces_uprade() to convert 5.7 undo
tablespaces to 8.0.1.
* Introduce srv_undo_tablespaces_downgrade() in case the upgrade from
5.7 fails.
* Introduce trx_rseg_adjust_rollback_segments() and
trx_rseg_add_rollback_segments() to consolidate the creation and use of
rollback segments in any tablespace.
* Introduce a new file format for undo tablespaces including a reserved
range for undo space IDs and an RSEG_ARRAY page.
* Rename auto-generated undo tablespace names from undonnn to undo-nnn
* Expand the undo namespace to support new undo file format.
* Various changes to allow online creation of undo spaces and rollback
segments.
* Change round robin routine for supplying rsegs to transactions to
support rseg arrays in each tablespace.
* Handle conversions between undo space_id and undo space number.
* Introduce undo_settings.test
* Adjust and improve a lot of testcases.

Commit 3

WL#10322: Deprecate innodb_undo_logs in 5.7

* Add (deprecated) to innodb-undo-logs description and mention that it
is actually setting the number of rollback segments.
* Delete “(deprecated)” from innodb-rollback-segments message.
* Add a deprecation warning message when innodb_undo_logs is used
at runtime and also at startup in a config or the command line.
* Return a warning when innodb_undo_logs is used at runtime.
* Rename srv_undo_logs to srv_rollback_segments in code
* Rename innodb_undo_logs to innodb_rollback_segments in all collections
and testcases except sysvars.innodb_undo_logs_basic.
* Fix sysvars.innodb_undo_logs_basic to suppress the deprecation warning.
Add a restart to exercise the deprecation code for using it at startup.

Commit 4

Bug #25572279 WARNING ALLOCATED TABLESPACE ID N FOR INNODB_UNDO00N
OLD MAXIMUM WAS N

This extra bootstrap warning was introduced in rb#13164:
Bug#24462978: Free up 32 slots in TRX_SYS page for non-temp rsegs.

It occurs when the database is initialized with undo tablespaces > rollback
segments. In this case, only the first undo tablespaces associated with the
limited rollback segments will be used. The remaining undo tablespaces
cannot be used because the rollback segment slots are not available in the
TRX_SYS page. But when starting the server, we have to open all unused
undo tablespaces. Along with opening the unused undo tablespaces, the
max_assigned_id needs to be incremented to avoid the warning. The rb#13164
patch was not doing that.

The solution is simply to call to fil_set_max_space_id_if_bigger(space_id)
  in srv_undo_tablespaces_open() instead of the other three place it does now.
  This makes sure that the condition "id > fil_system->max_assigned_id" in
  fil_space_create() is false which will prevent the reported warning message.

Commit 5

Bug #25551311   BACKPORT BUG #23517560 REMOVE SPACE_ID
RESTRICTION FOR UNDO TABLESPACES

Description:
============
The restriction that required the first undo tablespace to use space_id 1
is removed. The first undo tablespace can now use a space_id other than 1.
space_id values for undo tablespaces are still assigned in a consecutive
sequence.

Commit 6

WL#9289 InnoDB: Support Transparent Data Encryption for Undo Tablespaces
WL#9290 InnoDB: Support Transparent Data Encryption for Redo Log

Based on wl#8548, we provide encryption support for redo log and undo tablespaces.

For encrypting redo/undo log, as same as we did in wl#8548, we will en/decrypt the
redo log blocks/undo log pages in the I/O layer.
Which means, the en/decryption only happens when the redo/undo log read or
write from/to disk.

For redo log, encryption metadata will be stored in the header of first log file.
Same as wl#8548, there're 2 key levels here, master key and tablespace key.
Master key is stored in keyring plugin, and it's used to en/decrypt tablespace
key and iv. Tablespace key is for en/decrypt redo log blocks, and it will be
stored into the 3rd block of first redo log file(ib_logfile0).

For undo log, Same as regular tablespace, the encryption metadata will be stored
in the first page of data file.

We also added 2 new global variables innodb_redo_log_encrypt=ON/OFF,
innodb_undo_log_encrypt=ON/OFF for en/disable redo/undo log encryption.

MySQL · 引擎特性 · 新的事务锁调度VATS简介

$
0
0

传统的事务锁赋予方式是采用FIFS先来先服务的方式,从MySQL8.0.3开始,引入了一种新的模式CATS调度方式,全称为Contention-Aware Transaction Scheduling (或者叫做VATS, V=Variance). 顾名思义就是能够感知到事务竞争关系来实现全局最小开销的锁调度方式。

举个简单的例子,trx1和trx2同时等待一条记录锁,按照传统的方式,谁先进入等待队列,谁将优先获得锁。但如果同时有2个事务等待trx1,10个事务等待trx2,那么从全局来看收益最大的显然是让trx2获取到行锁。

当被挂起等待的事务数超过32个时,会自动切换到新的调度方式

相关资料先列一下:

官方博客

论文一: A Top-Down Approach to Achieving Performance Predictability in Database Systems

论文二: Contention-Aware Lock Scheduling for Transactional Databases

论文三: Identifying the Major Sources of Variance in Transaction Latencies: Towards More Predictable Databases

Release Note:

InnoDB now uses Variance-Aware Transaction Scheduling (VATS) for scheduling the release of transaction locks when the system is highly loaded, which helps reduce lock sys wait mutex contention. Lock scheduling uses VATS when >= 32 threads are suspended in the lock wait queue. For more information about Variance-Aware Transaction Scheduling (VATS), see Identifying the Major Sources of Variance in Transaction Latencies: Towards More Predictable Databases.

WL#10793: InnoDB: Use CATS for scheduling lock release under high load

主要代码变更见这个commit: fb056f442a96114c74d291302e8c4406c8c8e1af, 或者commit log搜WL#10793关键字

这个功能的核心有两个,一个是如何去维护每个事务的权重,在代码里以trx_t::age表示,第二个是基于新的调度算法,如何去选择被调度的事务。

PS: 本文涉及函数基于MySQL8.0.3

何时使用VATS算法

是否使用新调度算法,需要满足如下条件(lock_use_fcfs()):

  • 当前线程不是复制线程
  • 并发等待线程数超过32(LOCK_VATS_THRESHOLD)

关于第二点,增加了lock_sys_t::n_waiting来追踪,在函数lock_wait_suspend_thread里递增,在lock_wait_table_release_slot里递减

事务权重维护

事务age的接口函数为lock_update_age 及lock_update_trx_age,在将新的事务所加入hash,或者完成一次grant操作后,都需要对事务age进行更新

先来看看函数lock_update_age是如何计算的:

  • 如果当前新建的锁对象不能立刻赋予,需要等待其他锁对象时,对于已经拿到这个记录上锁的事务进行age累加,值为当前锁对象事务的trx_t::age + 1,那些等待当前新锁的事务都需要去依次更新其age; 如果当前事务无需等待,则找到那些在等待该记录锁的事务,并累加这些事务的trx_t::age+1 到当前事务
  • 针对每个事务的age更新,是一个递归函数,函数接口为lock_update_trx_age
    • 将新的age值赋予给trx_t
    • 如果当前事务也处于等待状态的话,则找到其等待的锁被哪些事务持有,并将age值累加上去。
  • 通过如上的递归流程,确保了在等待向量图中每个事务的权重被正确的更新掉

Grant Lock

当释放掉一个锁时,需要检查是否有别的等待的锁可以获得锁,VATS调度的函数入口为lock_rec_dequeue_from_page –> lock_grant_vats

相比传统的grant方式,lock_grant_vats函数的逻辑要复杂许多:

  • 将当前锁对象移除后,剩下的在同一条记录上的锁被分为两个队列:
    • waiting队列中存储需要等待的锁对象
    • granted队列存储已经获的锁对象

waiting队列会做一个排序,排序规则从comment里拷贝的,如下:

1. If neither of them is a wait lock, the LHS one has higher priority.
2. If only one of them is a wait lock, it has lower priority.
3. If both are high priority transactions, the one with a lower seq
     number has higher priority.
4. High priority transaction has higher priority.
5. Otherwise, the one with an older transaction has higher priority.

简单来说,如果不考虑事务优先级,队列时按照trx_t::age进行排序。

  • 然后依次遍历waiting队列,如果无需等待(lock_rec_has_to_wait_vats), 则赋予记录锁,并将其移到哈希队列的头部
  • 无论是当前释放的锁移除出锁队列,还是任一等待的事务获得了锁,都需要去更新锁等待图相关联事务权重

MySQL · 引擎特性 · 增加系统文件追踪space ID和物理文件的映射

$
0
0

前面我们提到了MySQL5.7的几个崩溃恢复产生的性能退化 为了解决崩溃恢复的效率问题, MySQL8.0对crash recovery的逻辑进行了进一步的优化。 在之前的版本中,InnoDB通过向redo log中写入日志来追踪在一次checkpoint后修改过的表空间信息,这样就无需在crash recovery时打开所有的表空间,只需搜集哪些被影响到的表空间。而到了8.0新版本里,采用了一种全新的方式:单独创建了系统映射文件, 将space id及路径信息轮换着写到两个指定的系统文件tablespaces.open.1 and tablespaces.open.2中(ref Fil_Open::write)

实现的思路其实不复杂,就是将所有的表空间ID和对应的路径信息存储到系统文件中,在崩溃恢复时再按需打开。

系统文件更新

那么如何保证所有的表空间信息都一个不漏的存储到系统文件了呢 ? 实际上他跟踪了所有的表空间文件操作,并更新内存cache中(Fil_Open::m_spaces), 如下:

a. fil_node_open_file
    fil_system->m_open.enter();
    fil_system->m_open.log(node->space->id, node->name);
    fil_system->m_open.exit();

打开表空间文件后,写一条日志MLOG_FILE_OPEN, 并将表空间状态 Nodes::OPEN以及日志end lsn在内存中进行更新(Fil_Open::Nodes::load)

b. fil_node_close_file
    fil_system->m_open.enter();
    fil_system->m_open.close(node->space->id, node->name);
    fil_system->m_open.exit();

关闭表空间文件后, 将缓存的表空间信息LSN重置为0,并将状态设置为CLOSED (Fil_Open::Nodes::close)

c. fil_name_write_rename
    fil_system->m_open.enter();
    fil_system->m_open.log(space_id, new_name);
    fil_system->m_open.to_file();
    fil_system->m_open.exit();

在物理rename文件之前, 将新的表空间名通过MLOG_FILE_OPEN写到redo log中,记录新文件的状态到内存。

随后就将缓存的表空间信息写到系统映射文件中(Fil_Open::to_file)

d. fil_delete_tablespace
    fil_system->m_open.enter();
    fil_system->m_open.deleted(id);
    fil_system->m_open.exit();

在物理删除文件之后,将对应的表空间状态设置为DELETED (Fil_Open::deleted)

e. fil_ibd_create
    fil_system->m_open.enter();
              fil_system->m_open.open(space_id, file->name, log_get_lsn());
              fil_system->m_open.exit();

在物理创建表空间文件之后, 调用Fil_Open::open 将新文件的信息存储到内存中。同样的包含创建文件时的LSN

可见InnoDB在对文件进行打开,关闭,创建,删除,重命名这些操作时都进行了追踪,其中CREATE/DELETE/RENAME的cache更新均发生在记录对应的MLOG_FILE_*日志之前。

另外我们也可以看到,表空间信息不是直接写入的,而是经过zip压缩后再写的,以减少磁盘空间占用。

那么何时将缓存的信息刷到磁盘呢 ? 第一种情况是rename tablespace时,会做一次写文件 第二种情况是做checkpoint之前会去做一次flush(fil_tablespace_open_sync_to_disk), 相比第一种情况,这里先做一次清理(Fil_Open::purge -> Fil_Open::Nodes::purge),将状态为DELETED/MISSING的无效表空间记录删除掉,再刷到磁盘

当系统正常关闭时,InnoDB会去将系统文件中的信息全部清除掉(fil_tablespace_open_clear),因为崩溃恢复无需用到。

崩溃恢复

那么崩溃恢复时,如何使用该文件呢?

首先在启动时(srv_start), 当确定了需要崩溃恢复时(recv_recovery_from_checkpoint_start),就会去从系统映射文件中载入表空间信息到内存中(fil_tablespace_open_init_for_recovery –> Fil_Open::from_file)。

随后开始读redo log并解析, 如下堆栈:

recv_recovery_begin
    |--> recv_scan_log_recs
            |--> recv_parse_log_recs
                        |--> recv_single_rec
                                    |--> recv_multi_rec

在将redo log加入到hash table之前,会先进行判断,只有在文件中找到的表空间,才需要去apply日志。

if (space_id == TRX_SYS_SPACE
        || fil_tablespace_lookup_for_recovery(space_id)) {

      recv_add_to_hash_table(
                  type, space_id, page_no, body,
                          ptr + len, old_lsn, recv_sys->recovered_lsn);

} else {

      recv_sys->missing_ids.insert(space_id);
}

由于系统文件不是实时flush的,因此在解析到MLOG_FILE_*类型的redo时, 也要对缓存的表空间信息进行修正(fil_tablespace_name_recover –> fil_name_process_for_recovery) ,以确保所有需要apply redo的tablespace都load到内存中。

在执行崩溃恢复时,InnoDB会按需去打开表空间文件,然后再去apply日志。(recv_apply_hashed_log_recs –> fil_tablespace_open_for_recovery),只有那些需要做崩溃恢复的文件,才会被打开。

Note1: 本文所有代码相关的内容都是基于MySQL8.0.3,而目前版本还处于RC和快速开发的状态,不排除后面的版本逻辑,函数名等发生变化。
Note2: 主要代码在这个commit中,感兴趣的也可以自行阅读代码
Note3: 从8.0.11开始,又改成了打开全部ibd文件,但是改成了并行扫描

PgSQL · 应用案例 · PostgreSQL 9种索引的原理和应用场景

$
0
0

背景

PostgreSQL 拥有众多开放特性,例如

1、开放的数据类型接口,使得PG支持超级丰富的数据类型,除了传统数据库支持的类型,还支持GIS,JSON,RANGE,IP,ISBN,图像特征值,化学,DNA等等扩展的类型,用户还可以根据实际业务扩展更多的类型。

2、开放的操作符接口,使得PG不仅仅支持常见的类型操作符,还支持扩展的操作符,例如 距离符,逻辑并、交、差符号,图像相似符号,几何计算符号等等扩展的符号,用户还可以根据实际业务扩展更多的操作符。

3、开放的外部数据源接口,使得PG支持丰富的外部数据源,例如可以通过FDW读写MySQL, redis, mongo, oracle, sqlserver, hive, www, hbase, ldap, 等等只要你能想到的数据源都可以通过FDW接口读写。

4、开放的语言接口,使得PG支持几乎地球上所有的编程语言作为数据库的函数、存储过程语言,例如plpython , plperl , pljava , plR , plCUDA , plshell等等。用户可以通过language handler扩展PG的语言支持。

5、开放的索引接口,使得PG支持非常丰富的索引方法,例如btree , hash , gin , gist , sp-gist , brin , bloom , rum , zombodb , bitmap (greenplum extend),用户可以根据不同的数据类型,以及查询的场景,选择不同的索引。

6、PG内部还支持BitmapAnd, BitmapOr的优化方法,可以合并多个索引的扫描操作,从而提升多个索引数据访问的效率。

不同的索引接口针对的数据类型、业务场景是不一样的,接下来针对每一种索引,介绍一下它的原理和应用场景。

一、btree

原理

《深入浅出PostgreSQL B-Tree索引结构》

应用场景

b-tree适合所有的数据类型,支持排序,支持大于、小于、等于、大于或等于、小于或等于的搜索。

索引与递归查询结合,还能实现快速的稀疏检索。

《PostgrSQL 递归SQL的几个应用 - 极客与正常人的思维》

例子

postgres=# create table t_btree(id int, info text);    
CREATE TABLE    
postgres=# insert into t_btree select generate_series(1,10000), md5(random()::text) ;    
INSERT 0 10000    
postgres=# create index idx_t_btree_1 on t_btree using btree (id);    
CREATE INDEX    
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t_btree where id=1;    
                                                          QUERY PLAN                                                               
-------------------------------------------------------------------------------------------------------------------------------    
 Index Scan using idx_t_btree_1 on public.t_btree  (cost=0.29..3.30 rows=1 width=37) (actual time=0.027..0.027 rows=1 loops=1)    
   Output: id, info    
   Index Cond: (t_btree.id = 1)    
   Buffers: shared hit=1 read=2    
 Planning time: 0.292 ms    
 Execution time: 0.050 ms    
(6 rows)    

二、hash

原理

src/backend/access/hash/README

(hash index entries store only the hash code, not the actual data value, for each indexed item. )

应用场景

hash索引存储的是被索引字段VALUE的哈希值,只支持等值查询。

hash索引特别适用于字段VALUE非常长(不适合b-tree索引,因为b-tree一个PAGE至少要存储3个ENTRY,所以不支持特别长的VALUE)的场景,例如很长的字符串,并且用户只需要等值搜索,建议使用hash index。

例子

postgres=# create table t_hash (id int, info text);    
CREATE TABLE    
postgres=# insert into t_hash select generate_series(1,100), repeat(md5(random()::text),10000);    
INSERT 0 100    
    
-- 使用b-tree索引会报错,因为长度超过了1/3的索引页大小  
postgres=# create index idx_t_hash_1 on t_hash using btree (info);    
ERROR:  index row size 3720 exceeds maximum 2712 for index "idx_t_hash_1"    
HINT:  Values larger than 1/3 of a buffer page cannot be indexed.    
Consider a function index of an MD5 hash of the value, or use full text indexing.    
    
postgres=# create index idx_t_hash_1 on t_hash using hash (info);    
CREATE INDEX    
    
postgres=# set enable_hashjoin=off;    
SET    
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t_hash where info in (select info from t_hash limit 1);    
                                                             QUERY PLAN                                                                  
-------------------------------------------------------------------------------------------------------------------------------------    
 Nested Loop  (cost=0.03..3.07 rows=1 width=22) (actual time=0.859..0.861 rows=1 loops=1)    
   Output: t_hash.id, t_hash.info    
   Buffers: shared hit=11    
   ->  HashAggregate  (cost=0.03..0.04 rows=1 width=18) (actual time=0.281..0.281 rows=1 loops=1)    
         Output: t_hash_1.info    
         Group Key: t_hash_1.info    
         Buffers: shared hit=3    
         ->  Limit  (cost=0.00..0.02 rows=1 width=18) (actual time=0.012..0.012 rows=1 loops=1)    
               Output: t_hash_1.info    
               Buffers: shared hit=1    
               ->  Seq Scan on public.t_hash t_hash_1  (cost=0.00..2.00 rows=100 width=18) (actual time=0.011..0.011 rows=1 loops=1)    
                     Output: t_hash_1.info    
                     Buffers: shared hit=1    
   ->  Index Scan using idx_t_hash_1 on public.t_hash  (cost=0.00..3.02 rows=1 width=22) (actual time=0.526..0.527 rows=1 loops=1)    
         Output: t_hash.id, t_hash.info    
         Index Cond: (t_hash.info = t_hash_1.info)    
         Buffers: shared hit=6    
 Planning time: 0.159 ms    
 Execution time: 0.898 ms    
(19 rows)    

三、gin

原理

gin是倒排索引,存储被索引字段的VALUE或VALUE的元素,以及行号的list或tree。

( col_val:(tid_list or tid_tree) , col_val_elements:(tid_list or tid_tree) )

《PostgreSQL GIN索引实现原理》

《宝剑赠英雄 - 任意组合字段等效查询, 探探PostgreSQL多列展开式B树 (GIN)》

应用场景

1、当需要搜索多值类型内的VALUE时,适合多值类型,例如数组、全文检索、TOKEN。(根据不同的类型,支持相交、包含、大于、在左边、在右边等搜索)

2、当用户的数据比较稀疏时,如果要搜索某个VALUE的值,可以适应btree_gin支持普通btree支持的类型。(支持btree的操作符)

3、当用户需要按任意列进行搜索时,gin支持多列展开单独建立索引域,同时支持内部多域索引的bitmapAnd, bitmapOr合并,快速的返回按任意列搜索请求的数据。

例子

1、多值类型搜索

postgres=# create table t_gin1 (id int, arr int[]);    
CREATE TABLE    
    
postgres=# do language plpgsql $$    
postgres$# declare    
postgres$# begin    
postgres$#   for i in 1..10000 loop    
postgres$#     insert into t_gin1 select i, array(select random()*1000 from generate_series(1,10));    
postgres$#   end loop;    
postgres$# end;    
postgres$# $$;    
DO    
postgres=# select * from t_gin1 limit 3;    
 id |                    arr                        
----+-------------------------------------------    
  1 | {128,700,814,592,414,838,615,827,274,210}    
  2 | {284,452,824,556,132,121,21,705,537,865}    
  3 | {65,185,586,872,627,330,574,227,827,64}    
(3 rows)    
    
postgres=# create index idx_t_gin1_1 on t_gin1 using gin (arr);    
CREATE INDEX    
    
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t_gin1 where arr && array[1,2];    
                                                       QUERY PLAN                                                            
-------------------------------------------------------------------------------------------------------------------------    
 Bitmap Heap Scan on public.t_gin1  (cost=8.93..121.24 rows=185 width=65) (actual time=0.058..0.207 rows=186 loops=1)    
   Output: id, arr    
   Recheck Cond: (t_gin1.arr && '{1,2}'::integer[])    
   Heap Blocks: exact=98    
   Buffers: shared hit=103    
   ->  Bitmap Index Scan on idx_t_gin1_1  (cost=0.00..8.89 rows=185 width=0) (actual time=0.042..0.042 rows=186 loops=1)    
         Index Cond: (t_gin1.arr && '{1,2}'::integer[])    
         Buffers: shared hit=5    
 Planning time: 0.208 ms    
 Execution time: 0.245 ms    
(10 rows)    
    
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t_gin1 where arr @> array[1,2];    
                                                     QUERY PLAN                                                          
---------------------------------------------------------------------------------------------------------------------    
 Bitmap Heap Scan on public.t_gin1  (cost=7.51..9.02 rows=1 width=65) (actual time=0.022..0.022 rows=0 loops=1)    
   Output: id, arr    
   Recheck Cond: (t_gin1.arr @> '{1,2}'::integer[])    
   Buffers: shared hit=5    
   ->  Bitmap Index Scan on idx_t_gin1_1  (cost=0.00..7.51 rows=1 width=0) (actual time=0.020..0.020 rows=0 loops=1)    
         Index Cond: (t_gin1.arr @> '{1,2}'::integer[])    
         Buffers: shared hit=5    
 Planning time: 0.116 ms    
 Execution time: 0.044 ms    
(9 rows)    

2、单值稀疏数据搜索

postgres=# create extension btree_gin;    
CREATE EXTENSION    
postgres=# create table t_gin2 (id int, c1 int);    
CREATE TABLE    
postgres=# insert into t_gin2 select generate_series(1,100000), random()*10 ;    
INSERT 0 100000    
postgres=# create index idx_t_gin2_1 on t_gin2 using gin (c1);    
CREATE INDEX    
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t_gin2 where c1=1;    
                                                         QUERY PLAN                                                              
-----------------------------------------------------------------------------------------------------------------------------    
 Bitmap Heap Scan on public.t_gin2  (cost=84.10..650.63 rows=9883 width=8) (actual time=0.925..3.685 rows=10078 loops=1)    
   Output: id, c1    
   Recheck Cond: (t_gin2.c1 = 1)    
   Heap Blocks: exact=443    
   Buffers: shared hit=448    
   ->  Bitmap Index Scan on idx_t_gin2_1  (cost=0.00..81.62 rows=9883 width=0) (actual time=0.867..0.867 rows=10078 loops=1)    
         Index Cond: (t_gin2.c1 = 1)    
         Buffers: shared hit=5    
 Planning time: 0.252 ms    
 Execution time: 4.234 ms    
(10 rows)    

3、多列任意搜索

postgres=# create table t_gin3 (id int, c1 int, c2 int, c3 int, c4 int, c5 int, c6 int, c7 int, c8 int, c9 int);    
CREATE TABLE    
postgres=# insert into t_gin3 select generate_series(1,100000), random()*10, random()*20, random()*30, random()*40, random()*50, random()*60, random()*70, random()*80, random()*90;    
INSERT 0 100000    
postgres=# create index idx_t_gin3_1 on t_gin3 using gin (c1,c2,c3,c4,c5,c6,c7,c8,c9);    
CREATE INDEX    
    
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t_gin3 where c1=1 or c2=1 and c3=1 or c4=1 and (c6=1 or c7=2) or c8=9 or c9=10;    
                                                                                              QUERY PLAN                                                                                                   
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------    
 Bitmap Heap Scan on public.t_gin3  (cost=154.03..1364.89 rows=12286 width=40) (actual time=1.931..5.634 rows=12397 loops=1)    
   Output: id, c1, c2, c3, c4, c5, c6, c7, c8, c9    
   Recheck Cond: ((t_gin3.c1 = 1) OR ((t_gin3.c2 = 1) AND (t_gin3.c3 = 1)) OR (((t_gin3.c4 = 1) AND (t_gin3.c6 = 1)) OR ((t_gin3.c4 = 1) AND (t_gin3.c7 = 2))) OR (t_gin3.c8 = 9) OR (t_gin3.c9 = 10))    
   Heap Blocks: exact=834    
   Buffers: shared hit=867    
   ->  BitmapOr  (cost=154.03..154.03 rows=12562 width=0) (actual time=1.825..1.825 rows=0 loops=1)    
         Buffers: shared hit=33    
         ->  Bitmap Index Scan on idx_t_gin3_1  (cost=0.00..83.85 rows=9980 width=0) (actual time=0.904..0.904 rows=10082 loops=1)    
               Index Cond: (t_gin3.c1 = 1)    
               Buffers: shared hit=6    
         ->  Bitmap Index Scan on idx_t_gin3_1  (cost=0.00..9.22 rows=172 width=0) (actual time=0.355..0.355 rows=164 loops=1)    
               Index Cond: ((t_gin3.c2 = 1) AND (t_gin3.c3 = 1))    
               Buffers: shared hit=8    
         ->  BitmapOr  (cost=21.98..21.98 rows=83 width=0) (actual time=0.334..0.334 rows=0 loops=1)    
               Buffers: shared hit=13    
               ->  Bitmap Index Scan on idx_t_gin3_1  (cost=0.00..7.92 rows=42 width=0) (actual time=0.172..0.172 rows=36 loops=1)    
                     Index Cond: ((t_gin3.c4 = 1) AND (t_gin3.c6 = 1))    
                     Buffers: shared hit=6    
               ->  Bitmap Index Scan on idx_t_gin3_1  (cost=0.00..7.91 rows=41 width=0) (actual time=0.162..0.162 rows=27 loops=1)    
                     Index Cond: ((t_gin3.c4 = 1) AND (t_gin3.c7 = 2))    
                     Buffers: shared hit=7    
         ->  Bitmap Index Scan on idx_t_gin3_1  (cost=0.00..14.38 rows=1317 width=0) (actual time=0.124..0.124 rows=1296 loops=1)    
               Index Cond: (t_gin3.c8 = 9)    
               Buffers: shared hit=3    
         ->  Bitmap Index Scan on idx_t_gin3_1  (cost=0.00..12.07 rows=1010 width=0) (actual time=0.102..0.102 rows=1061 loops=1)    
               Index Cond: (t_gin3.c9 = 10)    
               Buffers: shared hit=3    
 Planning time: 0.272 ms    
 Execution time: 6.349 ms    
(29 rows)    

四、gist

原理

GiST stands for Generalized Search Tree.

It is a balanced, tree-structured access method, that acts as a base template in which to implement arbitrary indexing schemes.

B-trees, R-trees and many other indexing schemes can be implemented in GiST.

《从难缠的模糊查询聊开 - PostgreSQL独门绝招之一 GIN , GiST , SP-GiST , RUM 索引原理与技术背景》

应用场景

GiST是一个通用的索引接口,可以使用GiST实现b-tree, r-tree等索引结构。

不同的类型,支持的索引检索也各不一样。例如:

1、几何类型,支持位置搜索(包含、相交、在上下左右等),按距离排序。

2、范围类型,支持位置搜索(包含、相交、在左右等)。

3、IP类型,支持位置搜索(包含、相交、在左右等)。

4、空间类型(PostGIS),支持位置搜索(包含、相交、在上下左右等),按距离排序。

5、标量类型,支持按距离排序。

《PostgreSQL 百亿地理位置数据 近邻查询性能》

例子

1、几何类型检索

postgres=# create table t_gist (id int, pos point);    
CREATE TABLE    
postgres=# insert into t_gist select generate_series(1,100000), point(round((random()*1000)::numeric, 2), round((random()*1000)::numeric, 2));    
INSERT 0 100000    
postgres=# select * from t_gist  limit 3;    
 id |       pos           
----+-----------------    
  1 | (325.43,477.07)    
  2 | (257.65,710.94)    
  3 | (502.42,582.25)    
(3 rows)    
postgres=# create index idx_t_gist_1 on t_gist using gist (pos);    
CREATE INDEX    
    
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t_gist where circle '((100,100) 10)'  @> pos;    
                                                       QUERY PLAN                                                           
------------------------------------------------------------------------------------------------------------------------    
 Bitmap Heap Scan on public.t_gist  (cost=2.55..125.54 rows=100 width=20) (actual time=0.072..0.132 rows=46 loops=1)    
   Output: id, pos    
   Recheck Cond: ('<(100,100),10>'::circle @> t_gist.pos)    
   Heap Blocks: exact=41    
   Buffers: shared hit=47    
   ->  Bitmap Index Scan on idx_t_gist_1  (cost=0.00..2.53 rows=100 width=0) (actual time=0.061..0.061 rows=46 loops=1)    
         Index Cond: ('<(100,100),10>'::circle @> t_gist.pos)    
         Buffers: shared hit=6    
 Planning time: 0.147 ms    
 Execution time: 0.167 ms    
(10 rows)    
    
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t_gist where circle '((100,100) 1)' @> pos order by pos <-> '(100,100)' limit 10;    
                                                              QUERY PLAN                                                                   
---------------------------------------------------------------------------------------------------------------------------------------    
 Limit  (cost=0.28..14.60 rows=10 width=28) (actual time=0.045..0.048 rows=2 loops=1)    
   Output: id, pos, ((pos <-> '(100,100)'::point))    
   Buffers: shared hit=5    
   ->  Index Scan using idx_t_gist_1 on public.t_gist  (cost=0.28..143.53 rows=100 width=28) (actual time=0.044..0.046 rows=2 loops=1)    
         Output: id, pos, (pos <-> '(100,100)'::point)    
         Index Cond: ('<(100,100),1>'::circle @> t_gist.pos)    
         Order By: (t_gist.pos <-> '(100,100)'::point)    
         Buffers: shared hit=5    
 Planning time: 0.092 ms    
 Execution time: 0.076 ms    
(10 rows)    

2、标量类型排序

postgres=# create extension btree_gist;    
CREATE EXTENSION    
    
postgres=# create index idx_t_btree_2 on t_btree using gist(id);    
CREATE INDEX    
    
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t_btree order by id <-> 100 limit 1;    
                                                                QUERY PLAN                                                                     
-------------------------------------------------------------------------------------------------------------------------------------------    
 Limit  (cost=0.15..0.19 rows=1 width=41) (actual time=0.046..0.046 rows=1 loops=1)    
   Output: id, info, ((id <-> 100))    
   Buffers: shared hit=3    
   ->  Index Scan using idx_t_btree_2 on public.t_btree  (cost=0.15..408.65 rows=10000 width=41) (actual time=0.045..0.045 rows=1 loops=1)    
         Output: id, info, (id <-> 100)    
         Order By: (t_btree.id <-> 100)    
         Buffers: shared hit=3    
 Planning time: 0.085 ms    
 Execution time: 0.076 ms    
(9 rows)    

五、sp-gist

原理

SP-GiST is an abbreviation for space-partitioned GiST.

SP-GiST supports partitioned search trees, which facilitate development of a wide range of different non-balanced data structures, such as quad-trees, k-d trees, and radix trees (tries).

The common feature of these structures is that they repeatedly divide the search space into partitions that need not be of equal size.

Searches that are well matched to the partitioning rule can be very fast.

SP-GiST类似GiST,是一个通用的索引接口,但是SP-GIST使用了空间分区的方法,使得SP-GiST可以更好的支持非平衡数据结构,例如quad-trees, k-d tree, radis tree.

《Space-partitioning trees in PostgreSQL》

《SP-GiST for PostgreSQL User Manual》

应用场景

1、几何类型,支持位置搜索(包含、相交、在上下左右等),按距离排序。

2、范围类型,支持位置搜索(包含、相交、在左右等)。

3、IP类型,支持位置搜索(包含、相交、在左右等)。

例子

1、范围类型搜索

postgres=# create table t_spgist (id int, rg int4range);    
CREATE TABLE    
    
postgres=# insert into t_spgist select id, int4range(id, id+(random()*200)::int) from generate_series(1,100000) t(id);    
INSERT 0 100000    
postgres=# select * from t_spgist  limit 3;    
 id |   rg        
----+---------    
  1 | [1,138)    
  2 | [2,4)    
  3 | [3,111)    
(3 rows)    
    
postgres=# set maintenance_work_mem ='32GB';    
SET    
postgres=# create index idx_t_spgist_1 on t_spgist using spgist (rg);    
CREATE INDEX    
    
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t_spgist where rg && int4range(1,100);    
                                                       QUERY PLAN                                                            
-------------------------------------------------------------------------------------------------------------------------    
 Bitmap Heap Scan on public.t_spgist  (cost=2.55..124.30 rows=99 width=17) (actual time=0.059..0.071 rows=99 loops=1)    
   Output: id, rg    
   Recheck Cond: (t_spgist.rg && '[1,100)'::int4range)    
   Heap Blocks: exact=1    
   Buffers: shared hit=6    
   ->  Bitmap Index Scan on idx_t_spgist_1  (cost=0.00..2.52 rows=99 width=0) (actual time=0.043..0.043 rows=99 loops=1)    
         Index Cond: (t_spgist.rg && '[1,100)'::int4range)    
         Buffers: shared hit=5    
 Planning time: 0.133 ms    
 Execution time: 0.111 ms    
(10 rows)    
    
postgres=# set enable_bitmapscan=off;    
SET    
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t_spgist where rg && int4range(1,100);    
                                                             QUERY PLAN                                                                  
-------------------------------------------------------------------------------------------------------------------------------------    
 Index Scan using idx_t_spgist_1 on public.t_spgist  (cost=0.28..141.51 rows=99 width=17) (actual time=0.021..0.051 rows=99 loops=1)    
   Output: id, rg    
   Index Cond: (t_spgist.rg && '[1,100)'::int4range)    
   Buffers: shared hit=8    
 Planning time: 0.097 ms    
 Execution time: 0.074 ms    
(6 rows)    
    

六、brin

原理

BRIN 索引是块级索引,有别于B-TREE等索引,BRIN记录并不是以行号为单位记录索引明细,而是记录每个数据块或者每段连续的数据块的统计信息。因此BRIN索引空间占用特别的小,对数据写入、更新、删除的影响也很小。

BRIN属于LOSSLY索引,当被索引列的值与物理存储相关性很强时,BRIN索引的效果非常的好。

例如时序数据,在时间或序列字段创建BRIN索引,进行等值、范围查询时效果很棒。

应用场景

《BRIN (block range index) index》

《PostgreSQL 物联网黑科技 - 瘦身几百倍的索引(BRIN index)》

《PostgreSQL 聚集存储 与 BRIN索引 - 高并发行为、轨迹类大吞吐数据查询场景解说》

《PostgreSQL 并行写入堆表,如何保证时序线性存储 - BRIN索引优化》

例子

postgres=# create table t_brin (id int, info text, crt_time timestamp);    
CREATE TABLE    
postgres=# insert into t_brin select generate_series(1,1000000), md5(random()::text), clock_timestamp();    
INSERT 0 1000000    
    
postgres=# select ctid,* from t_brin limit 3;    
 ctid  | id |               info               |          crt_time              
-------+----+----------------------------------+----------------------------    
 (0,1) |  1 | e48a6cd688b6cc8e86ee858fa993b31b | 2017-06-27 22:50:19.172224    
 (0,2) |  2 | e79c335c679b0bf544e8ba5f01569df7 | 2017-06-27 22:50:19.172319    
 (0,3) |  3 | b75ec6db320891a620097164b751e682 | 2017-06-27 22:50:19.172323    
(3 rows)    
    
    
postgres=# select correlation from pg_stats where tablename='t_brin' and attname='id';    
 correlation     
-------------    
           1    
(1 row)    
    
postgres=# select correlation from pg_stats where tablename='t_brin' and attname='crt_time';    
 correlation     
-------------    
           1    
(1 row)    
    
postgres=# create index idx_t_brin_1 on t_brin using brin (id) with (pages_per_range=1);    
CREATE INDEX    
postgres=# create index idx_t_brin_2 on t_brin using brin (crt_time) with (pages_per_range=1);    
CREATE INDEX    
    
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t_brin where id between 100 and 200;    
                                                       QUERY PLAN                                                            
-------------------------------------------------------------------------------------------------------------------------    
 Bitmap Heap Scan on public.t_brin  (cost=43.52..199.90 rows=74 width=45) (actual time=1.858..1.876 rows=101 loops=1)    
   Output: id, info, crt_time    
   Recheck Cond: ((t_brin.id >= 100) AND (t_brin.id <= 200))    
   Rows Removed by Index Recheck: 113    
   Heap Blocks: lossy=2    
   Buffers: shared hit=39    
   ->  Bitmap Index Scan on idx_t_brin_1  (cost=0.00..43.50 rows=107 width=0) (actual time=1.840..1.840 rows=20 loops=1)    
         Index Cond: ((t_brin.id >= 100) AND (t_brin.id <= 200))    
         Buffers: shared hit=37    
 Planning time: 0.174 ms    
 Execution time: 1.908 ms    
(11 rows)    
    
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t_brin where crt_time between '2017-06-27 22:50:19.172224' and '2017-06-27 22:50:19.182224';    
                                                                                       QUERY PLAN                                                                                            
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------    
 Bitmap Heap Scan on public.t_brin  (cost=59.63..4433.67 rows=4474 width=45) (actual time=1.860..2.603 rows=4920 loops=1)    
   Output: id, info, crt_time    
   Recheck Cond: ((t_brin.crt_time >= '2017-06-27 22:50:19.172224'::timestamp without time zone) AND (t_brin.crt_time <= '2017-06-27 22:50:19.182224'::timestamp without time zone))    
   Rows Removed by Index Recheck: 2    
   Heap Blocks: lossy=46    
   Buffers: shared hit=98    
   ->  Bitmap Index Scan on idx_t_brin_2  (cost=0.00..58.51 rows=4494 width=0) (actual time=1.848..1.848 rows=460 loops=1)    
         Index Cond: ((t_brin.crt_time >= '2017-06-27 22:50:19.172224'::timestamp without time zone) AND (t_brin.crt_time <= '2017-06-27 22:50:19.182224'::timestamp without time zone))    
         Buffers: shared hit=52    
 Planning time: 0.091 ms    
 Execution time: 2.884 ms    
(11 rows)    

七、rum

原理

https://github.com/postgrespro/rum

rum 是一个索引插件,由Postgrespro开源,适合全文检索,属于GIN的增强版本。

增强包括:

1、在RUM索引中,存储了lexem的位置信息,所以在计算ranking时,不需要回表查询(而GIN需要回表查询)。

2、RUM支持phrase搜索,而GIN无法支持。

3、在一个RUM索引中,允许用户在posting tree中存储除ctid(行号)以外的字段VALUE,例如时间戳。

这使得RUM不仅支持GIN支持的全文检索,还支持计算文本的相似度值,按相似度排序等。同时支持位置匹配,例如(速度与激情,可以采用”速度” <2> “激情” 进行匹配,而GIN索引则无法做到)

位置信息如下

postgres=# select to_tsvector('english', 'hello digoal');  
     to_tsvector        
----------------------  
 'digoal':2 'hello':1  
(1 row)  
  
postgres=# select to_tsvector('english', 'hello i digoal');  
     to_tsvector        
----------------------  
 'digoal':3 'hello':1  
(1 row)  
  
postgres=# select to_tsvector('english', 'hello i am digoal');  
     to_tsvector        
----------------------  
 'digoal':4 'hello':1  
(1 row)  
  
postgres=# select to_tsquery('english', 'hello <1> digoal');  
      to_tsquery        
----------------------  
 'hello' <-> 'digoal'  
(1 row)  
  
postgres=# select to_tsquery('english', 'hello <2> digoal');  
      to_tsquery        
----------------------  
 'hello' <2> 'digoal'  
(1 row)  
  
postgres=# select to_tsquery('english', 'hello <3> digoal');  
      to_tsquery        
----------------------  
 'hello' <3> 'digoal'  
(1 row)  
  
postgres=# select to_tsvector('hello digoal') @@ to_tsquery('english', 'hello <1> digoal');  
 ?column?   
----------  
 t  
(1 row)  
  
postgres=# select to_tsvector('hello digoal') @@ to_tsquery('english', 'hello <2> digoal');  
 ?column?   
----------  
 f  
(1 row)  
  
postgres=# select to_tsvector('hello i digoal') @@ to_tsquery('english', 'hello <2> digoal');  
 ?column?   
----------  
 t  
(1 row)  

应用场景

《PostgreSQL 全文检索加速 快到没有朋友 - RUM索引接口(潘多拉魔盒)》

《从难缠的模糊查询聊开 - PostgreSQL独门绝招之一 GIN , GiST , SP-GiST , RUM 索引原理与技术背景》

《PostgreSQL结合余弦、线性相关算法 在文本、图片、数组相似 等领域的应用 - 3 rum, smlar应用场景分析》

例子

postgres=# create table rum_test(c1 tsvector);    
CREATE TABLE    
    
postgres=# CREATE INDEX rumidx ON rum_test USING rum (c1 rum_tsvector_ops);    
CREATE INDEX    
    
$ vi test.sql    
insert into rum_test select to_tsvector(string_agg(c1::text,',')) from  (select (100000*random())::int from generate_series(1,100)) t(c1);    
    
$ pgbench -M prepared -n -r -P 1 -f ./test.sql -c 50 -j 50 -t 200000    
    
postgres=# explain analyze select * from rum_test where c1 @@ to_tsquery('english','1 | 2') order by c1 <=> to_tsquery('english','1 | 2') offset 19000 limit 100;    
                                                               QUERY PLAN                                                                    
-----------------------------------------------------------------------------------------------------------------------------------------    
 Limit  (cost=18988.45..19088.30 rows=100 width=1391) (actual time=58.912..59.165 rows=100 loops=1)    
   ->  Index Scan using rumidx on rum_test  (cost=16.00..99620.35 rows=99749 width=1391) (actual time=16.426..57.892 rows=19100 loops=1)    
         Index Cond: (c1 @@ '''1'' | ''2'''::tsquery)    
         Order By: (c1 <=> '''1'' | ''2'''::tsquery)    
 Planning time: 0.133 ms    
 Execution time: 59.220 ms    
(6 rows)    
    
postgres=# create table test15(c1 tsvector);    
CREATE TABLE    
postgres=# insert into test15 values (to_tsvector('jiebacfg', 'hello china, i''m digoal')), (to_tsvector('jiebacfg', 'hello world, i''m postgresql')), (to_tsvector('jiebacfg', 'how are you, i''m digoal'));    
INSERT 0 3    
postgres=# select * from test15;    
                         c1                              
-----------------------------------------------------    
 ' ':2,5,9 'china':3 'digoal':10 'hello':1 'm':8    
 ' ':2,5,9 'hello':1 'm':8 'postgresql':10 'world':3    
 ' ':2,4,7,11 'digoal':12 'm':10    
(3 rows)    
postgres=# create index idx_test15 on test15 using rum(c1 rum_tsvector_ops);    
CREATE INDEX    
postgres=# select *,c1 <=> to_tsquery('hello') from test15;    
                         c1                          | ?column?     
-----------------------------------------------------+----------    
 ' ':2,5,9 'china':3 'digoal':10 'hello':1 'm':8     |  16.4493    
 ' ':2,5,9 'hello':1 'm':8 'postgresql':10 'world':3 |  16.4493    
 ' ':2,4,7,11 'digoal':12 'm':10                     | Infinity    
(3 rows)    
postgres=# explain select *,c1 <=> to_tsquery('postgresql') from test15 order by c1 <=> to_tsquery('postgresql');    
                                   QUERY PLAN                                       
--------------------------------------------------------------------------------    
 Index Scan using idx_test15 on test15  (cost=3600.25..3609.06 rows=3 width=36)    
   Order By: (c1 <=> to_tsquery('postgresql'::text))    
(2 rows)    

GIN VS RUM

GIN

postgres=# create table t_gin_1 (id int, ts tsvector);  
CREATE TABLE  
postgres=# insert into t_gin_1 values (1, to_tsvector('hello digoal')),(2, to_tsvector('hello i digoal')),(3, to_tsvector('hello i am digoal'));  
INSERT 0 3  
postgres=# create index idx_t_gin_1_1 on t_gin_1 using gin (ts);  
CREATE INDEX  
postgres=# explain select * from t_gin_1 where ts @@ to_tsquery('english', 'hello <1> digoal');  
                       QUERY PLAN                         
--------------------------------------------------------  
 Seq Scan on t_gin_1  (cost=0.00..1.04 rows=1 width=36)  
   Filter: (ts @@ '''hello'' <-> ''digoal'''::tsquery)  
(2 rows)  
  
postgres=# set enable_seqscan=off;  
SET  
postgres=# explain select * from t_gin_1 where ts @@ to_tsquery('english', 'hello <1> digoal');  
                                 QUERY PLAN                                   
----------------------------------------------------------------------------  
 Bitmap Heap Scan on t_gin_1  (cost=4.50..6.01 rows=1 width=36)  
   Recheck Cond: (ts @@ '''hello'' <-> ''digoal'''::tsquery)  
   ->  Bitmap Index Scan on idx_t_gin_1_1  (cost=0.00..4.50 rows=1 width=0)  
         Index Cond: (ts @@ '''hello'' <-> ''digoal'''::tsquery)  
(4 rows)  
  
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t_gin_1 where ts @@ to_tsquery('english', 'hello <1> digoal');  
                                                      QUERY PLAN                                                        
----------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on public.t_gin_1  (cost=4.50..6.01 rows=1 width=36) (actual time=0.029..0.030 rows=1 loops=1)  
   Output: id, ts  
   Recheck Cond: (t_gin_1.ts @@ '''hello'' <-> ''digoal'''::tsquery)  
   Rows Removed by Index Recheck: 2  
   Heap Blocks: exact=1  
   Buffers: shared hit=4  
   ->  Bitmap Index Scan on idx_t_gin_1_1  (cost=0.00..4.50 rows=1 width=0) (actual time=0.018..0.018 rows=3 loops=1)  
         Index Cond: (t_gin_1.ts @@ '''hello'' <-> ''digoal'''::tsquery)  
         Buffers: shared hit=3  
 Planning time: 0.106 ms  
 Execution time: 0.061 ms  
(11 rows)  

RUM

postgres=# create table t_gin_1 (id int, ts tsvector);  
CREATE TABLE  
postgres=# insert into t_gin_1 values (1, to_tsvector('hello digoal')),(2, to_tsvector('hello i digoal')),(3, to_tsvector('hello i am digoal'));  
INSERT 0 3  
postgres=#  create index idx_t_gin_1_1 on t_gin_1 using rum (ts rum_tsvector_ops);  
CREATE INDEX  
postgres=# explain select * from t_gin_1 where ts @@ to_tsquery('english', 'hello <1> digoal');  
                       QUERY PLAN                         
--------------------------------------------------------  
 Seq Scan on t_gin_1  (cost=0.00..1.04 rows=1 width=36)  
   Filter: (ts @@ '''hello'' <-> ''digoal'''::tsquery)  
(2 rows)  
  
postgres=# set enable_seqscan =off;  
SET  
postgres=# explain select * from t_gin_1 where ts @@ to_tsquery('english', 'hello <1> digoal');  
                                  QUERY PLAN                                    
------------------------------------------------------------------------------  
 Index Scan using idx_t_gin_1_1 on t_gin_1  (cost=2.00..4.01 rows=1 width=36)  
   Index Cond: (ts @@ '''hello'' <-> ''digoal'''::tsquery)  
(2 rows)  
  
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t_gin_1 where ts @@ to_tsquery('english', 'hello <1> digoal');  
                                                          QUERY PLAN                                                             
-------------------------------------------------------------------------------------------------------------------------------  
 Index Scan using idx_t_gin_1_1 on public.t_gin_1  (cost=2.00..4.01 rows=1 width=36) (actual time=0.049..0.049 rows=1 loops=1)  
   Output: id, ts  
   Index Cond: (t_gin_1.ts @@ '''hello'' <-> ''digoal'''::tsquery)  
   Buffers: shared hit=3  
 Planning time: 0.288 ms  
 Execution time: 0.102 ms  
(6 rows)  

八、bloom

原理

bloom索引接口是PostgreSQL基于bloom filter构造的一个索引接口,属于lossy索引,可以收敛结果集(排除绝对不满足条件的结果,剩余的结果里再挑选满足条件的结果),因此需要二次check,bloom支持任意列组合的等值查询。

bloom存储的是签名,签名越大,耗费的空间越多,但是排除更加精准。有利有弊。

CREATE INDEX bloomidx ON tbloom USING bloom (i1,i2,i3)  
       WITH (length=80, col1=2, col2=2, col3=4);  
  
签名长度 80 bit, 最大允许4096 bits  
col1 - col32,分别指定每列的bits,默认长度2,最大允许4095 bits.  

bloom provides an index access method based on Bloom filters.

A Bloom filter is a space-efficient data structure that is used to test whether an element is a member of a set. In the case of an index access method, it allows fast exclusion of non-matching tuples via signatures whose size is determined at index creation.

This type of index is most useful when a table has many attributes and queries test arbitrary combinations of them.

应用场景

bloom索引适合多列任意组合查询。

《PostgreSQL 9.6 黑科技 bloom 算法索引,一个索引支撑任意列组合查询》

例子

=# CREATE TABLE tbloom AS    
   SELECT    
     (random() * 1000000)::int as i1,    
     (random() * 1000000)::int as i2,    
     (random() * 1000000)::int as i3,    
     (random() * 1000000)::int as i4,    
     (random() * 1000000)::int as i5,    
     (random() * 1000000)::int as i6    
   FROM    
  generate_series(1,10000000);    
SELECT 10000000    
=# CREATE INDEX bloomidx ON tbloom USING bloom (i1, i2, i3, i4, i5, i6);    
CREATE INDEX    
=# SELECT pg_size_pretty(pg_relation_size('bloomidx'));    
 pg_size_pretty    
----------------    
 153 MB    
(1 row)    
=# CREATE index btreeidx ON tbloom (i1, i2, i3, i4, i5, i6);    
CREATE INDEX    
=# SELECT pg_size_pretty(pg_relation_size('btreeidx'));    
 pg_size_pretty    
----------------    
 387 MB    
(1 row)    
    
=# EXPLAIN ANALYZE SELECT * FROM tbloom WHERE i2 = 898732 AND i5 = 123451;    
                                                        QUERY PLAN    
---------------------------------------------------------------------------------------------------------------------------    
 Bitmap Heap Scan on tbloom  (cost=178435.39..178439.41 rows=1 width=24) (actual time=76.698..76.698 rows=0 loops=1)    
   Recheck Cond: ((i2 = 898732) AND (i5 = 123451))    
   Rows Removed by Index Recheck: 2439    
   Heap Blocks: exact=2408    
   ->  Bitmap Index Scan on bloomidx  (cost=0.00..178435.39 rows=1 width=0) (actual time=72.455..72.455 rows=2439 loops=1)    
         Index Cond: ((i2 = 898732) AND (i5 = 123451))    
 Planning time: 0.475 ms    
 Execution time: 76.778 ms    
(8 rows)    

九、zombodb

原理

zombodb是PostgreSQL与ElasticSearch结合的一个索引接口,可以直接读写ES。

https://github.com/zombodb/zombodb

应用场景

与ES结合,实现SQL接口的搜索引擎,实现数据的透明搜索。

例子

-- Install the extension:    
    
CREATE EXTENSION zombodb;    
    
-- Create a table:    
    
CREATE TABLE products (    
    id SERIAL8 NOT NULL PRIMARY KEY,    
    name text NOT NULL,    
    keywords varchar(64)[],    
    short_summary phrase,    
    long_description fulltext,     
    price bigint,    
    inventory_count integer,    
    discontinued boolean default false,    
    availability_date date    
);    
    
-- insert some data    
-- Index it:    
    
CREATE INDEX idx_zdb_products     
          ON products     
       USING zombodb(zdb('products', products.ctid), zdb(products))    
        WITH (url='http://localhost:9200/', shards=5, replicas=1);    
    
-- Query it:    
    
SELECT *     
  FROM products     
 WHERE zdb('products', ctid) ==> 'keywords:(sports,box) or long_description:(wooden w/5 away) and price < 100000';    

十、bitmap Index

原理

bitmap索引是Greenplum的索引接口,类似GIN倒排,只是bitmap的KEY是列的值,VALUE是BIT(每个BIT对应一行),而不是行号list或tree。

《Greenplum 最佳实践 - 什么时候选择bitmap索引》

应用场景

当某个字段的唯一值个数在100到10万之间(超出这个范围,不建议使用bitmap)时,如果表的记录数特别多,而且变更不频繁(或者是AO表),那么很适合BITMAP索引,bitmap索引可以实现快速的多个或单个VALUE的搜索。因为只需要对行号的BITMAP进行BIT与或运算,得到最终的BITMAP,从最终的BITMAP映射到行进行提取。

bitmap与btree一样,都支持 等于,大于,小于,大于等于,小于等于的查询。

例子

postgres=# create table t_bitmap(id int, info text, c1 int);  
NOTICE:  Table doesn't have 'DISTRIBUTED BY' clause -- Using column named 'id' as the Greenplum Database data distribution key for this table.  
HINT:  The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.  
CREATE TABLE  
postgres=# insert into t_bitmap select generate_series(1,1000000), 'test', random()*1000;  
INSERT 0 1000000  
postgres=# create index idx_t_bitmap_1 on t_bitmap using bitmap(c1);  
CREATE INDEX  
postgres=# explain analyze select * from t_bitmap where c1=1;  
                                       QUERY PLAN                                         
----------------------------------------------------------------------------------------  
 Gather Motion 3:1  (slice1; segments: 3)  (cost=0.00..200.27 rows=1 width=13)  
   Rows out:  0 rows at destination with 3.769 ms to end, start offset by 0.250 ms.  
   ->  Index Scan using idx_t_bitmap_1 on t_bitmap  (cost=0.00..200.27 rows=1 width=13)  
         Index Cond: c1 = 1  
         Rows out:  0 rows (seg0) with 0.091 ms to end, start offset by 3.004 ms.  
 Slice statistics:  
   (slice0)    Executor memory: 115K bytes.  
   (slice1)    Executor memory: 273K bytes avg x 3 workers, 273K bytes max (seg0).  
 Statement statistics:  
   Memory used: 128000K bytes  
 Total runtime: 4.110 ms  
(11 rows)  
  
postgres=# explain analyze select * from t_bitmap where c1<=10;  
                                       QUERY PLAN                                         
----------------------------------------------------------------------------------------  
 Gather Motion 3:1  (slice1; segments: 3)  (cost=0.00..200.27 rows=1 width=13)  
   Rows out:  0 rows at destination with 2.952 ms to end, start offset by 0.227 ms.  
   ->  Index Scan using idx_t_bitmap_1 on t_bitmap  (cost=0.00..200.27 rows=1 width=13)  
         Index Cond: c1 <= 10  
         Rows out:  0 rows (seg0) with 0.055 ms to end, start offset by 3.021 ms.  
 Slice statistics:  
   (slice0)    Executor memory: 115K bytes.  
   (slice1)    Executor memory: 273K bytes avg x 3 workers, 273K bytes max (seg0).  
 Statement statistics:  
   Memory used: 128000K bytes  
 Total runtime: 3.278 ms  
(11 rows)   

十一、varbitx

原理

varbitx是阿里云RDS的扩展包,丰富bit类型的函数接口,实际上并不是索引接口,但是在PostgreSQL中使用varbitx可以代替bitmap索引,达到同样的效果。

应用场景

《阿里云RDS for PostgreSQL varbitx插件与实时画像应用场景介绍》

《基于 阿里云 RDS PostgreSQL 打造实时用户画像推荐系统》

《PostgreSQL (varbit, roaring bitmap) VS pilosa(bitmap库)》

例子

略,请参考以上文章。

十二、部分索引

PostgreSQL允许用户创建部分索引,例如业务上只关心激活用户,所以可以只对激活用户创建索引。

例子

create table test(id int, info text, crt_time timestamp, active boolean);    
    
create index idx_test_id on test(id) where active;    
    
select * from test where active and id=?;  -- 可以使用部分索引    

十三、表达式索引

表达式索引也是PostgreSQL特有的特性,例如用户的数据需要转换后查询,例如某些设备上传的地理坐标的坐标系不符合国标,需要转换为国内的空间坐标来查询。

那么可以针对这类字段,创建表达式索引,将转换过程放到表达式中,查询时也使用表达式进行查询。

create table t_express (id int, pos geometry);    
    
create index idx_t_express_1 on t_express using gist ( ( ST_Transform(pos, 26986) ) );    
    
select * from t_express order by ST_Transform(pos, 26986) <-> ST_Transform(ST_GeomFromText('POINT(108.50000000001 22.8)', 4326), 26986) limit 10;    

十四、内部窥视索引存储

https://www.postgresql.org/docs/9.6/static/pageinspect.html

通过pageinspect插件,可以读取索引页的内容。

例子

test=# SELECT * FROM bt_page_stats('pg_cast_oid_index', 1);    
-[ RECORD 1 ]-+-----    
blkno         | 1    
type          | l    
live_items    | 256    
dead_items    | 0    
avg_item_size | 12    
page_size     | 8192    
free_size     | 4056    
btpo_prev     | 0    
btpo_next     | 0    
btpo          | 0    
btpo_flags    | 3    
    
test=# SELECT * FROM bt_page_items('pg_cast_oid_index', 1);    
 itemoffset |  ctid   | itemlen | nulls | vars |    data    
------------+---------+---------+-------+------+-------------    
          1 | (0,1)   |      12 | f     | f    | 23 27 00 00    
          2 | (0,2)   |      12 | f     | f    | 24 27 00 00    
          3 | (0,3)   |      12 | f     | f    | 25 27 00 00    
          4 | (0,4)   |      12 | f     | f    | 26 27 00 00    
          5 | (0,5)   |      12 | f     | f    | 27 27 00 00    
          6 | (0,6)   |      12 | f     | f    | 28 27 00 00    
          7 | (0,7)   |      12 | f     | f    | 29 27 00 00    
          8 | (0,8)   |      12 | f     | f    | 2a 27 00 00    
    
test=# SELECT * FROM brin_page_items(get_raw_page('brinidx', 5),    
                                     'brinidx')    
       ORDER BY blknum, attnum LIMIT 6;    
 itemoffset | blknum | attnum | allnulls | hasnulls | placeholder |    value         
------------+--------+--------+----------+----------+-------------+--------------    
        137 |      0 |      1 | t        | f        | f           |     
        137 |      0 |      2 | f        | f        | f           | {1 .. 88}    
        138 |      4 |      1 | t        | f        | f           |     
        138 |      4 |      2 | f        | f        | f           | {89 .. 176}    
        139 |      8 |      1 | t        | f        | f           |     
        139 |      8 |      2 | f        | f        | f           | {177 .. 264}    

小结

1、btree,适合任意单值类型,可用于=, >, <, >=, <=以及排序。

选择性越好(唯一值个数接近记录数)的列,越适合b-tree。

当被索引列存储相关性越接近1或-1时,数据存储越有序,范围查询扫描的HEAP PAGE越少。

2、hash,当字段超过单个索引页的1/4时,不适合b-tree索引。如果业务只有=的查询需求,使用hash index效率更高。

3、gin,倒排存储,(column value: row IDs tree|list)。适合多值列,也适合单值列。例如数组、全文检索、JSON、HSTORE等类型。

多值列搜索:包含、相交、不包含。

单值列搜索:等值。

适合多列组合索引(col1,col2,coln),适合任意列组合搜索。

目前gin索引仅支持bitmap scan(按heap page id顺序搜索)。

4、gist,适合数据有交错的场景,例如 全文检索、range类型、空间类型(点、线、面、多维对象… …)。

空间类型,支持几何搜索(包含、相交、上、下、左、右等)。支持KNN排序。

全文检索类型、范围类型,支持包含搜索。

《PostgreSQL 黑科技 - 空间聚集存储, 内窥GIN, GiST, SP-GiST索引》

https://www.citusdata.com/blog/2017/10/17/tour-of-postgres-index-types/

5、sp-gist,空间分区索引类型,适合不平衡数据集(例如xxxyyyzzz??????组成的VALUE,xxx, yyy, zzz,每个值包含一些数据集,每个数据集的数据量不平衡可能导致TREE不平衡)。

sp-gist索引结构,可以用于解决此类不平衡数据的倾斜问题。

适合空间数据类型。

SP-GiST: An Extensible Database Index for Supporting Space Partitioning Trees

pic

6、brin,块级索引,记录每个或每连续N个数据块的数据边界。

BRIN适合单值类型,当被索引列存储相关性越接近1或-1时,数据存储越有序,块的边界越明显,BRIN索引的效果就越好。

BRIN支持多列、单列。

BRIN适合搜索一个范围的数据。目前只支持BITMAP扫描方式(按heap page id顺序搜索)。

《万亿级电商广告 - brin黑科技带你(最低成本)玩转毫秒级圈人(视觉挖掘姊妹篇) - 阿里云RDS PostgreSQL, HybridDB for PostgreSQL最佳实践》

7、bloom,支持被索引字段的任意组合的等值搜索。

《PostgreSQL 9.6 黑科技 bloom 算法索引,一个索引支撑任意列组合查询》

8、rum,支持全文检索类型,支持单值列+全文检索列,支持近似文本搜索。

《从难缠的模糊查询聊开 - PostgreSQL独门绝招之一 GIN , GiST , SP-GiST , RUM 索引原理与技术背景》

9、zombodb,PG与ES搜索引擎结合的一种索引,在PG数据库中透明使用ES。

《[未完待续] PostgreSQL Elasticsearch 插件 - zomboDB》

10、bitmap,支持1000~10000个唯一值的列。适合多个值的 与或 条件搜索。

《Greenplum 最佳实践 - 什么时候选择bitmap索引》

11、varbitx,阿里云RDS PG提供的一种BIT类型管理插件,支持BIT的设置,搜索等操作。

《阿里云RDS for PostgreSQL varbitx插件与实时画像应用场景介绍》

12、部分索引,只检索部分列。在索引中过滤不需要被搜索,不适合建立索引的行。

13、表达式索引,对于不同的搜索条件,支持使用表达式索引提高查询速度。例如函数索引。

14、多索引bitmap合并扫描,多个索引可以使用BITMAP SCAN合并扫描。例如两个条件与搜索,使用BITMAP SCAN,可以跳过不需要扫描的数据块。

《PostgreSQL bitmapAnd, bitmapOr, bitmap index scan, bitmap heap scan》


PgSQL · 应用案例 · 任意字段组合查询

$
0
0

背景

《PostgreSQL 设计优化case - 大宽表任意字段组合查询索引如何选择(btree, gin, rum) - (含单个索引列数超过32列的方法)》

《PostgreSQL 任意字段数组合 AND\OR 条件,指定返回结果条数,构造测试数据算法举例》

《PostgreSQL ADHoc(任意字段组合)查询(rums索引加速) - 非字典化,普通、数组等组合字段生成新数组》

《PostgreSQL 实践 - 实时广告位推荐 2 (任意字段组合、任意维度组合搜索、输出TOP-K)》

《PostgreSQL 实践 - 实时广告位推荐 1 (任意字段组合、任意维度组合搜索、输出TOP-K)》

《PostgreSQL ADHoc(任意字段组合)查询 与 字典化 (rum索引加速) - 实践与方案1》

《PostgreSQL 如何高效解决 按任意字段分词检索的问题 - case 1》

《HTAP数据库 PostgreSQL 场景与性能测试之 20 - (OLAP) 用户画像圈人场景 - 多个字段任意组合条件筛选与透视》

《PostgreSQL 多字段任意组合搜索的性能》

1亿记录,128个字段,任意字段组合查询。性能如何?

PG凭什么可以搞定大数据量的任意字段组合实时搜索?

《PostgreSQL 并行计算解说 汇总》

《PostgreSQL 9种索引的原理和应用场景》

例子

1、测试表

do language plpgsql $$  
declare  
  sql text;  
begin  
  sql := 'create unlogged table test(id serial primary key,';  
  for i in 1..64 loop  
    sql := sql||' c'||i||' int default random()*100,';  
  end loop;  
  for i in 65..128 loop  
    sql := sql||' c'||i||' int default random()*1000000,';  
  end loop;  
  sql := rtrim(sql,',');  
  sql := sql||')';  
  execute sql;  
end;  
$$;  

2、写入1亿数据

vi test.sql  
insert into test (c1) select random()*100 from generate_series(1,100);  
  
  
nohup pgbench -M prepared -n -r -P 1 -f ./test.sql -c 50 -j 50 -t 20000 >/dev/null 2>&1 &  

3、写完后的大小

postgres=# \dt+ test  
                   List of relations  
 Schema | Name | Type  |  Owner   | Size  | Description   
--------+------+-------+----------+-------+-------------  
 public | test | table | postgres | 55 GB |   
(1 row)  
  
  
postgres=# select count(*) from test;  
   count     
-----------  
 100000000  
(1 row)  

4、高效率创建索引

vi idx.sql  
  
vacuum (analyze,verbose) test;  
set maintenance_work_mem='8GB';  
set max_parallel_workers=128;  
set max_parallel_workers_per_gather=32;  
set min_parallel_index_scan_size=0;  
set min_parallel_table_scan_size=0;  
set parallel_setup_cost=0;  
set parallel_tuple_cost=0;  
set max_parallel_maintenance_workers=16;  
alter table test set (parallel_workers=64);  
  
do language plpgsql $$  
declare  
  sql text;  
begin  
  for i in 1..128 loop  
    execute format('create index idx_test_%s on test (c%s) %s', i, i, 'tablespace tbs_8001');  
  end loop;  
end;  
$$;  
  
vacuum (analyze,verbose) test;  
  
  
  
  
nohup psql -f ./idx.sql >/dev/null 2>&1 &  

5、建完索引后

postgres=# \d+ test  
                                               Unlogged table "public.test"  
 Column |  Type   | Collation | Nullable |                 Default                  | Storage | Stats target | Description   
--------+---------+-----------+----------+------------------------------------------+---------+--------------+-------------  
 id     | integer |           | not null | nextval('test_id_seq'::regclass)         | plain   |              |   
 c1     | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c2     | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c3     | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c4     | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c5     | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c6     | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c7     | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c8     | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c9     | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c10    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c11    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c12    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c13    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c14    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c15    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c16    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c17    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c18    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c19    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c20    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c21    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c22    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c23    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c24    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c25    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c26    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c27    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c28    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c29    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c30    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c31    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c32    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c33    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c34    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c35    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c36    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c37    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c38    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c39    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c40    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c41    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c42    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c43    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c44    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c45    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c46    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c47    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c48    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c49    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c50    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c51    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c52    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c53    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c54    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c55    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c56    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c57    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c58    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c59    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c60    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c61    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c62    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c63    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c64    | integer |           |          | (random() * (100)::double precision)     | plain   |              |   
 c65    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c66    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c67    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c68    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c69    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c70    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c71    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c72    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c73    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c74    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c75    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c76    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c77    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c78    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c79    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c80    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c81    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c82    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c83    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c84    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c85    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c86    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c87    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c88    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c89    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c90    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c91    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c92    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c93    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c94    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c95    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c96    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c97    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c98    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c99    | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c100   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c101   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c102   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c103   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c104   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c105   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c106   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c107   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c108   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c109   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c110   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c111   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c112   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c113   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c114   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c115   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c116   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c117   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c118   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c119   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c120   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c121   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c122   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c123   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c124   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c125   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c126   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c127   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
 c128   | integer |           |          | (random() * (1000000)::double precision) | plain   |              |   
Indexes:  
    "test_pkey" PRIMARY KEY, btree (id)  
    "idx_test_1" btree (c1), tablespace "tbs_8001""idx_test_10" btree (c10), tablespace "tbs_8001""idx_test_100" btree (c100), tablespace "tbs_8001""idx_test_101" btree (c101), tablespace "tbs_8001""idx_test_102" btree (c102), tablespace "tbs_8001""idx_test_103" btree (c103), tablespace "tbs_8001""idx_test_104" btree (c104), tablespace "tbs_8001""idx_test_105" btree (c105), tablespace "tbs_8001""idx_test_106" btree (c106), tablespace "tbs_8001""idx_test_107" btree (c107), tablespace "tbs_8001""idx_test_108" btree (c108), tablespace "tbs_8001""idx_test_109" btree (c109), tablespace "tbs_8001""idx_test_11" btree (c11), tablespace "tbs_8001""idx_test_110" btree (c110), tablespace "tbs_8001""idx_test_111" btree (c111), tablespace "tbs_8001""idx_test_112" btree (c112), tablespace "tbs_8001""idx_test_113" btree (c113), tablespace "tbs_8001""idx_test_114" btree (c114), tablespace "tbs_8001""idx_test_115" btree (c115), tablespace "tbs_8001""idx_test_116" btree (c116), tablespace "tbs_8001""idx_test_117" btree (c117), tablespace "tbs_8001""idx_test_118" btree (c118), tablespace "tbs_8001""idx_test_119" btree (c119), tablespace "tbs_8001""idx_test_12" btree (c12), tablespace "tbs_8001""idx_test_120" btree (c120), tablespace "tbs_8001""idx_test_121" btree (c121), tablespace "tbs_8001""idx_test_122" btree (c122), tablespace "tbs_8001""idx_test_123" btree (c123), tablespace "tbs_8001""idx_test_124" btree (c124), tablespace "tbs_8001""idx_test_125" btree (c125), tablespace "tbs_8001""idx_test_126" btree (c126), tablespace "tbs_8001""idx_test_127" btree (c127), tablespace "tbs_8001""idx_test_128" btree (c128), tablespace "tbs_8001""idx_test_13" btree (c13), tablespace "tbs_8001""idx_test_14" btree (c14), tablespace "tbs_8001""idx_test_15" btree (c15), tablespace "tbs_8001""idx_test_16" btree (c16), tablespace "tbs_8001""idx_test_17" btree (c17), tablespace "tbs_8001""idx_test_18" btree (c18), tablespace "tbs_8001""idx_test_19" btree (c19), tablespace "tbs_8001""idx_test_2" btree (c2), tablespace "tbs_8001""idx_test_20" btree (c20), tablespace "tbs_8001""idx_test_21" btree (c21), tablespace "tbs_8001""idx_test_22" btree (c22), tablespace "tbs_8001""idx_test_23" btree (c23), tablespace "tbs_8001""idx_test_24" btree (c24), tablespace "tbs_8001""idx_test_25" btree (c25), tablespace "tbs_8001""idx_test_26" btree (c26), tablespace "tbs_8001""idx_test_27" btree (c27), tablespace "tbs_8001""idx_test_28" btree (c28), tablespace "tbs_8001""idx_test_29" btree (c29), tablespace "tbs_8001""idx_test_3" btree (c3), tablespace "tbs_8001""idx_test_30" btree (c30), tablespace "tbs_8001""idx_test_31" btree (c31), tablespace "tbs_8001""idx_test_32" btree (c32), tablespace "tbs_8001""idx_test_33" btree (c33), tablespace "tbs_8001""idx_test_34" btree (c34), tablespace "tbs_8001""idx_test_35" btree (c35), tablespace "tbs_8001""idx_test_36" btree (c36), tablespace "tbs_8001""idx_test_37" btree (c37), tablespace "tbs_8001""idx_test_38" btree (c38), tablespace "tbs_8001""idx_test_39" btree (c39), tablespace "tbs_8001""idx_test_4" btree (c4), tablespace "tbs_8001""idx_test_40" btree (c40), tablespace "tbs_8001""idx_test_41" btree (c41), tablespace "tbs_8001""idx_test_42" btree (c42), tablespace "tbs_8001""idx_test_43" btree (c43), tablespace "tbs_8001""idx_test_44" btree (c44), tablespace "tbs_8001""idx_test_45" btree (c45), tablespace "tbs_8001""idx_test_46" btree (c46), tablespace "tbs_8001""idx_test_47" btree (c47), tablespace "tbs_8001""idx_test_48" btree (c48), tablespace "tbs_8001""idx_test_49" btree (c49), tablespace "tbs_8001""idx_test_5" btree (c5), tablespace "tbs_8001""idx_test_50" btree (c50), tablespace "tbs_8001""idx_test_51" btree (c51), tablespace "tbs_8001""idx_test_52" btree (c52), tablespace "tbs_8001""idx_test_53" btree (c53), tablespace "tbs_8001""idx_test_54" btree (c54), tablespace "tbs_8001""idx_test_55" btree (c55), tablespace "tbs_8001""idx_test_56" btree (c56), tablespace "tbs_8001""idx_test_57" btree (c57), tablespace "tbs_8001""idx_test_58" btree (c58), tablespace "tbs_8001""idx_test_59" btree (c59), tablespace "tbs_8001""idx_test_6" btree (c6), tablespace "tbs_8001""idx_test_60" btree (c60), tablespace "tbs_8001""idx_test_61" btree (c61), tablespace "tbs_8001""idx_test_62" btree (c62), tablespace "tbs_8001""idx_test_63" btree (c63), tablespace "tbs_8001""idx_test_64" btree (c64), tablespace "tbs_8001""idx_test_65" btree (c65), tablespace "tbs_8001""idx_test_66" btree (c66), tablespace "tbs_8001""idx_test_67" btree (c67), tablespace "tbs_8001""idx_test_68" btree (c68), tablespace "tbs_8001""idx_test_69" btree (c69), tablespace "tbs_8001""idx_test_7" btree (c7), tablespace "tbs_8001""idx_test_70" btree (c70), tablespace "tbs_8001""idx_test_71" btree (c71), tablespace "tbs_8001""idx_test_72" btree (c72), tablespace "tbs_8001""idx_test_73" btree (c73), tablespace "tbs_8001""idx_test_74" btree (c74), tablespace "tbs_8001""idx_test_75" btree (c75), tablespace "tbs_8001""idx_test_76" btree (c76), tablespace "tbs_8001""idx_test_77" btree (c77), tablespace "tbs_8001""idx_test_78" btree (c78), tablespace "tbs_8001""idx_test_79" btree (c79), tablespace "tbs_8001""idx_test_8" btree (c8), tablespace "tbs_8001""idx_test_80" btree (c80), tablespace "tbs_8001""idx_test_81" btree (c81), tablespace "tbs_8001""idx_test_82" btree (c82), tablespace "tbs_8001""idx_test_83" btree (c83), tablespace "tbs_8001""idx_test_84" btree (c84), tablespace "tbs_8001""idx_test_85" btree (c85), tablespace "tbs_8001""idx_test_86" btree (c86), tablespace "tbs_8001""idx_test_87" btree (c87), tablespace "tbs_8001""idx_test_88" btree (c88), tablespace "tbs_8001""idx_test_89" btree (c89), tablespace "tbs_8001""idx_test_9" btree (c9), tablespace "tbs_8001""idx_test_90" btree (c90), tablespace "tbs_8001""idx_test_91" btree (c91), tablespace "tbs_8001""idx_test_92" btree (c92), tablespace "tbs_8001""idx_test_93" btree (c93), tablespace "tbs_8001""idx_test_94" btree (c94), tablespace "tbs_8001""idx_test_95" btree (c95), tablespace "tbs_8001""idx_test_96" btree (c96), tablespace "tbs_8001""idx_test_97" btree (c97), tablespace "tbs_8001""idx_test_98" btree (c98), tablespace "tbs_8001""idx_test_99" btree (c99), tablespace "tbs_8001"  
Options: parallel_workers=64  

写入性能如何

当前有129个索引,写入性能如何?

9505行/s。

transaction type: ./test.sql
scaling factor: 1
query mode: prepared
number of clients: 24
number of threads: 24
duration: 120 s
number of transactions actually processed: 11433
latency average = 252.195 ms
latency stddev = 70.089 ms
tps = 95.054689 (including connections establishing)
tps = 95.058210 (excluding connections establishing)
statement latencies in milliseconds:
       252.179  insert into test (c1) select random()*100 from generate_series(1,100);

瓶颈,磁盘读写5.5GB/s。

Total DISK READ :     207.91 K/s | Total DISK WRITE :       3.54 G/s  
Actual DISK READ:     207.91 K/s | Actual DISK WRITE:    2015.64 M/s  
  TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND   
55887 be/4 digoal     15.40 K/s  158.54 M/s  0.00 %  1.05 % postgres: postgres postgres [local] INSERT  
55872 be/4 digoal      7.70 K/s  157.62 M/s  0.00 %  0.84 % postgres: postgres postgres [local] INSERT  
55886 be/4 digoal     23.10 K/s  158.78 M/s  0.00 %  0.78 % postgres: postgres postgres [local] INSERT  
55897 be/4 digoal      7.70 K/s  158.79 M/s  0.00 %  0.75 % postgres: postgres postgres [local] INSERT  
55889 be/4 digoal      0.00 B/s  158.72 M/s  0.00 %  0.69 % postgres: postgres postgres [local] INSERT  
55894 be/4 digoal      0.00 B/s  157.25 M/s  0.00 %  0.69 % postgres: postgres postgres [local] INSERT  
55888 be/4 digoal      7.70 K/s  136.26 M/s  0.00 %  0.68 % postgres: postgres postgres [local] INSERT  
55885 be/4 digoal      7.70 K/s  143.24 M/s  0.00 %  0.67 % postgres: postgres postgres [local] INSERT  
55890 be/4 digoal      0.00 B/s  159.07 M/s  0.00 %  0.67 % postgres: postgres postgres [local] INSERT  
55865 be/4 digoal     15.40 K/s  158.27 M/s  0.00 %  0.65 % postgres: postgres postgres [local] INSERT  
55900 be/4 digoal      7.70 K/s  151.00 M/s  0.00 %  0.64 % postgres: postgres postgres [local] INSERT  
55891 be/4 digoal      0.00 B/s  160.40 M/s  0.00 %  0.63 % postgres: postgres postgres [local] INSERT  
55896 be/4 digoal      0.00 B/s  158.79 M/s  0.00 %  0.62 % postgres: postgres postgres [local] INSERT  
55902 be/4 digoal     15.40 K/s  157.65 M/s  0.00 %  0.62 % postgres: postgres postgres [local] INSERT  
55875 be/4 digoal      0.00 B/s  158.52 M/s  0.00 %  0.58 % postgres: postgres postgres [local] INSERT  
55892 be/4 digoal      7.70 K/s  136.20 M/s  0.00 %  0.58 % postgres: postgres postgres [local] INSERT  
55868 be/4 digoal      0.00 B/s  139.10 M/s  0.00 %  0.58 % postgres: postgres postgres [local] INSERT  
55895 be/4 digoal      0.00 B/s  159.75 M/s  0.00 %  0.57 % postgres: postgres postgres [local] INSERT  
55898 be/4 digoal      0.00 B/s  113.43 M/s  0.00 %  0.55 % postgres: postgres postgres [local] INSERT  
55880 be/4 digoal     46.20 K/s  121.68 M/s  0.00 %  0.50 % postgres: postgres postgres [local] INSERT  
55884 be/4 digoal     23.10 K/s  126.35 M/s  0.00 %  0.47 % postgres: postgres postgres [local] INSERT  
55901 be/4 digoal     15.40 K/s  117.46 M/s  0.00 %  0.46 % postgres: postgres postgres [local] INSERT  
55899 be/4 digoal      7.70 K/s  115.13 M/s  0.00 %  0.46 % postgres: postgres postgres [local] INSERT  

瓶颈在读写数据文件

postgres=# select wait_event_type,wait_event,count(*) from pg_stat_activity where wait_event is not null group by 1,2 order by 3 desc;  
 wait_event_type |     wait_event      | count   
-----------------+---------------------+-------  
 IO              | DataFileWrite       |    15  
 IO              | DataFileRead        |     5  
 Activity        | WalWriterMain       |     1  
 Activity        | LogicalLauncherMain |     1  
 Activity        | CheckpointerMain    |     1  
 Activity        | AutoVacuumMain      |     1  
(6 rows)  

任意字段组合查询性能如何

1、

postgres=# explain select count(*) from test where c1=2 and c99 between 100 and 1000 and c98 between 100 and 200 and c1=1;  
                                            QUERY PLAN                                               
---------------------------------------------------------------------------------------------------  
 Aggregate  (cost=1201.23..1201.24 rows=1 width=8)  
   ->  Result  (cost=1192.25..1201.22 rows=1 width=0)  
         One-Time Filter: false  
         ->  Bitmap Heap Scan on test  (cost=1192.25..1201.22 rows=1 width=0)  
               Recheck Cond: ((c98 >= 100) AND (c98 <= 200) AND (c99 >= 100) AND (c99 <= 1000))  
               Filter: (c1 = 2)  
               ->  BitmapAnd  (cost=1192.25..1192.25 rows=8 width=0)  
                     ->  Bitmap Index Scan on idx_test_98  (cost=0.00..125.98 rows=9571 width=0)  
                           Index Cond: ((c98 >= 100) AND (c98 <= 200))  
                     ->  Bitmap Index Scan on idx_test_99  (cost=0.00..1066.02 rows=81795 width=0)  
                           Index Cond: ((c99 >= 100) AND (c99 <= 1000))  
(11 rows)  
  
  
postgres=# select count(*) from test where c1=2 and c99 between 100 and 1000 and c98 between 100 and 200 and c2=1;  
 count   
-------  
     0  
(1 row)  
  
Time: 1.087 ms  

2、

set min_parallel_index_scan_size=0;  
set min_parallel_table_scan_size=0;  
set parallel_setup_cost=0;  
set parallel_tuple_cost=0;  
  
  
  
set work_mem='1GB';  
set max_parallel_workers=128;  
set max_parallel_workers_per_gather=24;  
set random_page_cost =1.1;  
set effective_cache_size ='400GB';  
alter table test set (parallel_workers=64);  
set enable_bitmapscan=off;  
postgres=# select count(*) from test where c1=2 and c99 between 100 and 10000;  
 count   
-------  
  9764  
(1 row)  
  
Time: 50.160 ms  
  
  
postgres=# select count(*) from test where c1=2 and c99 between 100 and 1000 and c98 between 100 and 200 and c2=1;  
 count   
-------  
     0  
(1 row)  
  
Time: 20.969 ms  
  
postgres=# select count(*) from test where c1=2 and c99 between 100 and 10000 and c108 between 100 and 10000;  
 count   
-------  
   102  
(1 row)  
  
Time: 72.359 ms  
  
postgres=# select count(*) from test where c1=2 and c99=1;  
 count   
-------  
     2  
(1 row)  
  
Time: 1.118 ms  

3、OR

set enable_bitmapscan=on;  
  
postgres=# explain select count(*) from test where c1=2 and c99=1 or c100 between 10 and 100;  
                                         QUERY PLAN                                           
--------------------------------------------------------------------------------------------  
 Aggregate  (cost=10000010781.91..10000010781.92 rows=1 width=8)  
   ->  Bitmap Heap Scan on test  (cost=10000000130.57..10000010758.33 rows=9430 width=0)  
         Recheck Cond: ((c99 = 1) OR ((c100 >= 10) AND (c100 <= 100)))  
         Filter: (((c1 = 2) AND (c99 = 1)) OR ((c100 >= 10) AND (c100 <= 100)))  
         ->  BitmapOr  (cost=130.57..130.57 rows=9526 width=0)  
               ->  Bitmap Index Scan on idx_test_99  (cost=0.00..2.39 rows=96 width=0)  
                     Index Cond: (c99 = 1)  
               ->  Bitmap Index Scan on idx_test_100  (cost=0.00..123.47 rows=9430 width=0)  
                     Index Cond: ((c100 >= 10) AND (c100 <= 100))  
(9 rows)  
  
Time: 1.281 ms  
postgres=#  select count(*) from test where c1=2 and c99=1 or c100 between 10 and 100;  
 count   
-------  
  9174  
(1 row)  
  
Time: 18.785 ms  

小结

任意维度查询case耗时
c1=2 and c99 between 100 and 10000;50 毫秒
c1=2 and c99 between 100 and 1000 and c98 between 100 and 200 and c2=1;21 毫秒
c1=2 and c99 between 100 and 10000 and c108 between 100 and 10000;72 毫秒
c1=2 and c99=1;1 毫秒
c1=2 and c99=1 or c100 between 10 and 100;19 毫秒

性能差异:

1、执行计划

2、扫描量

3、运算量(与结果集大小无直接关系,关键看扫描方法和中间计算量)。

写入能力:129个索引,写入9505行/s。瓶颈在IO侧,通过提升IO能力,加分区可以提高。

参考

《PostgreSQL 设计优化case - 大宽表任意字段组合查询索引如何选择(btree, gin, rum) - (含单个索引列数超过32列的方法)》

《PostgreSQL 任意字段数组合 AND\OR 条件,指定返回结果条数,构造测试数据算法举例》

《PostgreSQL ADHoc(任意字段组合)查询(rums索引加速) - 非字典化,普通、数组等组合字段生成新数组》

《PostgreSQL 实践 - 实时广告位推荐 2 (任意字段组合、任意维度组合搜索、输出TOP-K)》

《PostgreSQL 实践 - 实时广告位推荐 1 (任意字段组合、任意维度组合搜索、输出TOP-K)》

《PostgreSQL ADHoc(任意字段组合)查询 与 字典化 (rum索引加速) - 实践与方案1》

《PostgreSQL 如何高效解决 按任意字段分词检索的问题 - case 1》

《HTAP数据库 PostgreSQL 场景与性能测试之 20 - (OLAP) 用户画像圈人场景 - 多个字段任意组合条件筛选与透视》

《PostgreSQL 多字段任意组合搜索的性能》

《PostgreSQL 并行计算解说 汇总》

《PostgreSQL 9种索引的原理和应用场景》

PgSQL · 应用案例 · PostgreSQL 并行计算

$
0
0

背景

PostgreSQL 11 优化器已经支持了非常多场合的并行。简单估计,已支持几十种场景的并行计算。

并行计算到底带来了多大的性能提升?

是否满足实时分析的需求?

是否可以支持OLTP与OLAP混合业务使用?

《PostgreSQL 多模, 多应用场景实践》

PostgreSQL 11 并行计算使用场景、性能提升倍数

场景数据量关闭并行开启并行并行度开启并行性能提升倍数
全表扫描10 亿53.4 秒1.8 秒3229.7 倍
条件过滤10 亿53.4 秒1.87 秒3228.6 倍
哈希聚合10 亿142.3 秒4.8 秒3029.6 倍
分组聚合10 亿142.3 秒4.8 秒3029.6 倍
select into10 亿54.5 秒1.9 秒3228.7 倍
create table as10 亿54.7 秒2 秒3027.35 倍
CREATE MATERIALIZED VIEW10 亿54.7 秒2 秒3027.35 倍
create index10 亿964 秒252 秒323.83 倍
parallel CREATE INDEX CONCURRENTLY - 不堵塞读写10亿509.6 秒355 秒161.44 倍
排序10 亿76.9 秒2.75 秒3228 倍
自定义并行聚合1(求 distinct 数组 字段元素、以及count distinct)10 亿298.8 秒8.7 秒3634.3 倍
自定义并行聚合2(求 distinct 普通 字段元素、以及count distinct)10 亿96.5 秒3.43 秒3628 倍
自定义并行函数(UDF)10 亿456 秒16.5 秒3027.6 倍
普通并行(gather)10 亿70.2 秒2.5 秒3028.1 倍
归并并行(gather merge)10 亿78.2 秒2.76 秒3028.3 倍
rc (ud agg count distinct)10 亿107 秒3.65 秒3029.3 倍
rr (ud agg count distinct)10 亿107 秒3.65 秒3029.3 倍
parallel OLAP : 中间结果 parallel with unlogged table ; unlogged table并行求avg case10 亿73.6 秒2.5 秒3029.44 倍
parallel index scan10 亿19 秒1.58 秒2012 倍
parallel bitmap scan10 亿23.98 秒15.86 秒201.5 倍
parallel index only scan10 亿8 秒0.6 秒2013.33 倍
parallel nestloop join10亿 join 10亿 using (i) where t1.i<1000000014.4 秒4.6 秒83.13 倍
parallel merge join10亿 join 10亿 using (i) where t1.i<100000003.2 秒1 秒83.2 倍
parallel hash join10亿 join 10亿 using (i) where t1.i<10000000 and t2.i<100000008.1 秒1 秒208.1 倍
parallel hash join10亿 join 10亿 using (i)1071 秒92.3 秒2011.6 倍
parallel partition table wise join10亿 join 10亿 using (i)1006 秒76 秒2413.2 倍
parallel partition table wise agg10亿191 秒8 秒2423.9 倍
parallel append10亿70.5 秒3.16 秒2422.3 倍
parallel append merge10亿99.4 秒5.87 秒2416.93 倍
parallel union all10亿99 秒5.6 秒2417.68 倍
parallel CTE10亿65.65 秒3.33 秒2419.7 倍
parallel 递归查询, 树状查询, 异构查询, CTE, recursive CTE, connect by异构数据1亿,日志数据10亿5.14 秒0.29 秒2417.7 倍
parallel scan mult FDW tables (通过继承表方式)10亿180 秒7.8 秒2423.1 倍
parallel scan mult FDW tables (通过union all)10亿165.6 秒27.8 秒56 倍
parallel leader process10亿186 秒95 秒12 倍
parallel subquery20亿179.7 秒6.5 秒2827.6 倍

每项测试CASE请见参考部分。

参考

《PostgreSQL 并行计算解说 之29 - parallel 递归查询, 树状查询, 异构查询, CTE, recursive CTE, connect by》

《PostgreSQL 并行计算解说 之28 - parallel CREATE INDEX CONCURRENTLY - 不堵塞读写》

《PostgreSQL 并行计算解说 之27 - parallel subquery》

《PostgreSQL 并行计算解说 之26 - parallel gather | gathermerge - enable leader worker process》

《PostgreSQL 并行计算解说 之25 - parallel FDW scan (并行访问多个外部表) with parallel append (FDW must with IsForeignScanParallelSafe)》

《PostgreSQL 并行计算解说 之24 - parallel CTE (Common Table Express)》

《PostgreSQL 并行计算解说 之23 - parallel union all》

《PostgreSQL 并行计算解说 之23 - parallel append merge》

《PostgreSQL 并行计算解说 之22 - parallel append》

《PostgreSQL 并行计算解说 之21 - parallel partition table wise agg》

《PostgreSQL 并行计算解说 之20 - parallel partition table wise join》

《PostgreSQL 并行计算解说 之19 - parallel hash join》

《PostgreSQL 并行计算解说 之18 - parallel merge join》

《PostgreSQL 并行计算解说 之17 - parallel nestloop join》

《PostgreSQL 并行计算解说 之16 - parallel index only scan》

《PostgreSQL 并行计算解说 之15 - parallel bitmap scan》

《PostgreSQL 并行计算解说 之14 - parallel index scan》

《PostgreSQL 并行计算解说 之13 - parallel OLAP : 中间结果 parallel with unlogged table》

《PostgreSQL 并行计算解说 之12 - parallel in rc,rr 隔离级别》

《PostgreSQL 并行计算解说 之11 - parallel gather, gather merge》

《PostgreSQL 并行计算解说 之10 - parallel 自定义并行函数(UDF)》

《PostgreSQL 并行计算解说 之9 - parallel 自定义并行聚合》

《PostgreSQL 并行计算解说 之8 - parallel sort》

《PostgreSQL 并行计算解说 之7 - parallel create index》

《PostgreSQL 并行计算解说 之6 - parallel CREATE MATERIALIZED VIEW》

《PostgreSQL 并行计算解说 之5 - parallel create table as》

《PostgreSQL 并行计算解说 之4 - parallel select into》

《PostgreSQL 并行计算解说 之3 - parallel agg》

《PostgreSQL 并行计算解说 之2 - parallel filter》

《PostgreSQL 并行计算解说 之1 - parallel seq scan》

https://www.postgresql.org/docs/11/parallel-plans.html

《PostgreSQL 11 并行计算算法,参数,强制并行度设置》

《PostgreSQL 11 preview - 并行计算 增强 汇总》

《PostgreSQL 10 自定义并行计算聚合函数的原理与实践 - (含array_agg合并多个数组为单个一元数组的例子)》

《PostgreSQL 9.6 并行计算 优化器算法浅析》

MSSQL · 最佳实践 · 挑战云计算安全的存储过程

$
0
0

摘要

在SQL Server安全系列专题月报分享中,往期我们已经陆续分享了:如何使用对称密钥实现SQL Server列加密技术使用非对称密钥实现SQL Server列加密使用混合密钥实现SQL Server列加密技术列加密技术带来的查询性能问题以及相应解决方案行级别安全解决方案SQL Server 2016 dynamic data masking实现隐私数据列打码技术使用证书做数据库备份加密SQL Server Always Encrypted使用SSL加密连接这九篇文章,直接点击以上文章链接前往查看详情。本期月报我们分享SQL Server数据中,挑战云计算安全的几个存储过程。

问题引入

在SQL Server数据库中,存在三种类型的存储过程:系统存储过程,用户定义存储过程和扩展存储过程。系统存储过程是SQL Server服务安装时在系统中创建的,以“sp_”打头;用户自定义存储过程是指用户编写的存储过程;扩展存储过程是 SQL Server服务可以动态加载和运行的 DLL,可直接在SQL Server 实例的地址空间中运行。做为云计算厂商,SQL Server数据库中内置的这些非常好用的扩展存储过程,也给数据库PaaS平台安全提出了新的挑战。因此,我们有必要深入分析这些存在安全风险的扩展存储过程和对应方案,它们涉及的面非常广泛,包括:

 命令执行:在SQL Server中执行Windows命令

 注册表操作:在SQL Server中操作注册表

 OLE相关:在SQL Server中执行OLE相关动作

 文件相关:在SQL Server中操作文件相关

命令执行

执行命令的扩展存储过程,可以用来执行Windows操作系统的任何命令,只要在cmd里面能够做到的事情,都可以用它来执行。对,没错,它就是号称最最最为危险的存储过程xp_cmdshell,让我们来看看它的破坏力有多强。

开启xp_cmdshell

在使用xp_cmdshell执行命令之前,请使用sys.sp_configure系统存储过程来启用它,如下所示:

USE master  
GO  
EXEC sys.sp_configure 'show advanced options', 1  
RECONFIGURE WITH OVERRIDE
GO  

EXEC sys.sp_configure 'xp_cmdshell', 1  
RECONFIGURE WITH OVERRIDE  

GO  
EXEC sp_configure 'show advanced options', 0  
GO

查看主机IP

我们可以使用xp_cmdshell来查看SQL Server所在的主机IP,轻松拿到主机IP地址。

EXEC master.sys.xp_cmdshell 'ipconfig'

如下图所示:

01.png

创建文件夹

还可以创建文件,文件夹,查看文件夹中的内容,主机上文件暴露无遗。

EXEC master.sys.xp_cmdshell 'mkdir C:\tempfolder'
EXEC master.sys.xp_cmdshell 'dir C:\tempfolder'

如下所示:

02.png

创建用户

更要命的是,我们还可以创建用户,比如创建用户IAmTestUser,

-- create user IAmTestUser
EXEC master.sys.xp_cmdshell 'net user IAmTestUser IAmTestUser@1 /add', no_output
GO
-- query users
EXEC master.sys.xp_cmdshell 'net user'
GO

如下图所示:

03.png

添加到Administrator组

加到local Administrator组,然后想干什么就干什么了,如下:

-- add user IAmTestUser into local administrators group
EXEC master.sys.xp_cmdshell 'net localgroup administrators IAmTestUser /add', no_output
GO

删除用户

当然,还可以删除用户,影响正常用户活动。

-- remove the testing user
EXEC master.sys.xp_cmdshell 'net user IAmTestUser /delete', no_output
GO

禁用xp_cmdshell

以上仅仅是几个小演示,这个扩展存储过程非常危险,给数据库安全带来了极大挑战,所以,测试完毕后,必须关闭。

USE master  
GO  
EXEC sys.sp_configure 'show advanced options', 1  
RECONFIGURE WITH OVERRIDE
GO  
 
EXEC sys.sp_configure 'xp_cmdshell', 0  
RECONFIGURE WITH OVERRIDE  

GO  
EXEC sp_configure 'show advanced options', 0  
GO

从这几个简单的演示,我们可以体会到xp_cmdshell的破坏威力之大,因此,作为SQL Server云计算服务提供者,这个扩展存储过程是绝对不允许开放给用户使用的。

注册表操作

Windows注册表(Registry)是Microsoft Windows系统中的一个非常重要的数据库,它用于存储Windows系统本身和应用程序的设置信息。Windows注册表的本身的安全至关重要的,而SQL Server中一些内置的可操作注册表的扩展存储过程,给注册表的安全保护,提出了挑战,包括:

 xp_regwrite:写入注册表键值

 xp_regdeletevalue:删除注册表中的值

 xp_regdeletekey:删除注册表中的键

 xp_regread:读取注册表中的值

xp_regwrite

这个扩展存储过程可以写入注册表指定的键里指定的值。假如,我们需要在键HKEY_LOCAL_MACHINE\SOFTWARE\aaaKey\aaaValueName中写入值aaaValue,请使用如下方法:

EXEC master.sys.xp_regwrite
	@rootkey='HKEY_LOCAL_MACHINE',
	@key='SOFTWARE\aaaKey',
	@value_name='aaaValueName',
	@type='REG_SZ',
	@value='aaaValue'
;

结果展示如下:

04.png

xp_regdeletevalue

这个扩展存储过程可以用来删除注册表中指定的键中指定的值。比如把我们刚才创建的键值HKEY_LOCAL_MACHINE\SOFTWARE\aaaKey\aaaValueName中的Value删除掉。方法如下:

EXEC master.sys.xp_regdeletevalue
	@rootkey='HKEY_LOCAL_MACHINE',
	@key='SOFTWARE\aaaKey',
	@value_name='aaaValueName'
;

xp_regdeletekey

这个扩展存储过程可以删除注册表中指定的键,请小心谨慎使用。比如,删除我们刚才建立的测试注册表键HKEY_LOCAL_MACHINE\SOFTWARE\aaaKey。方法如下:

EXEC master.sys.xp_regdeletekey
	@rootkey='HKEY_LOCAL_MACHINE', 
	@key='SOFTWARE\aaaKey'
;

xp_regread

这个扩展存储过程可以读取注册表指定的键中指定的值,获取一些重要的敏感信息。比如,如下实例是获取SQL Server服务所在主机的主机名。

DECLARE 
	@hostName sysname
;
EXEC master.sys.xp_regread 
	@rootkey='HKEY_LOCAL_MACHINE', 
	@key='system\controlset001\control\computername\computername',
	@value_name='computername',
	@value=@hostName OUTPUT
;
SELECT 
	@hostName AS [host_name]
;

SQL Server中内置的关于注册表操作的扩展存储过程,也给我们的数据库系统安全,甚至是操作系统的安全带来了极大挑战。做为云厂商及云服务的提供者,必须严防死守,把好安全关。

OLE相关

与OLE相关的一系列存储过程:sp_OACreate,sp_OADestroy,sp_OAGetErrorInfo,sp_OAGetProperty,sp_OAMethod,sp_OASetProperty,sp_OAStop。这些存储过程和xp_cmdshell危险等级一样高。我们也必须严加防范。以下仅以sp_OACreate和sp_OAMethod配合使用为例说明。

启用OLE相关存储过程

要使用OLE相关存储过程,必须先启用对OLE存储过程的使用,方法如下:

USE master  
GO  
EXEC sys.sp_configure 'show advanced options', 1  
RECONFIGURE WITH OVERRIDE
GO  
 
EXEC sys.sp_configure 'Ole Automation Procedures', 1  
RECONFIGURE WITH OVERRIDE  

GO  
EXEC sp_configure 'show advanced options', 0  
GO

sp_OAmethod创建用户

使用OLE存储过程,创建Windows的用户。

DECLARE 
	@shell INT 
;
EXEC master.sys.sp_oacreate 
	'wscript.shell',
	@shell OUTPUT 
;

EXEC master.sys.sp_oamethod 
	@shell,
	'run',
	null,
	'C:\Windows\System32\cmd.exe /c net user IAmTestUser IAmTestUser@1 /add'
;

sp_OAmethod授权Admin

使用OLE存储过程,将刚才创建Windows的用户添加到local Administrator组中,便有了系统超级用户的所有权限。

DECLARE 
	@id INT 
;
EXEC master.sys.sp_oacreate 
	'wscript.shell',
	@id OUTPUT 
;

EXEC master.sys.sp_oamethod 
	@id,
	'run',
	null,
	'C:\Windows\System32\cmd.exe /c net localgroup administrators IAmTestUser /add'
;
GO

sp_OAmethod删除用户

当然,您可以删除用户。

DECLARE 
	@id INT 
;
EXEC master.sys.sp_oacreate 
	'wscript.shell',
	@id OUTPUT 
;

EXEC master.sys.sp_oamethod 
	@id,
	'run',
	null,
	'C:\Windows\System32\cmd.exe /c net user IAmTestUser /delete'
;
GO

禁用OLE相关存储过程

OLE存储过程测试完毕后,必须禁用掉,以免带来极大的安全风险。

USE master  
GO  
EXEC sys.sp_configure 'show advanced options', 1  
RECONFIGURE WITH OVERRIDE
GO  
 
EXEC sys.sp_configure 'Ole Automation Procedures', 1  
RECONFIGURE WITH OVERRIDE  

GO  
EXEC sp_configure 'show advanced options', 0  
GO

文件相关

SQL Server中除了可以执行windows command的扩展存储过程,操作注册表的扩展存储过程,以及OLE扩展存储过程,还有一类文件操作的存储过程,比如:

 xp_dirtree:查看文件目录结构

 xp_subdirs:查看子文件夹

 xp_fileexist:判断文件是否存储

 xp_delete_file:删除备份文件

xp_dirtree

这个扩展存储过程用来列出对应目录下所有文件和文件夹。

EXEC master.sys.xp_dirtree N'C:\tempfolder'

xp_subdirs

xp_subdirs用来显示给定的文件夹下的所有子文件夹。

EXEC master.sys.xp_subdirs N'C:\tempfolder'

xp_fileexist

确认某个文件是否存在。

EXEC master.sys.xp_fileexist 'C:\tempfolder\01\Deadlock_0_131700921198410000.xel'

xp_delete_file

这个扩展存储过程用于删除数据库备份文件,如下所示:

BACKUP DATABASE DDLCenter TO DISK = N'C:\tempfolder\01\DDLCenter.bak'
EXEC master.sys.xp_delete_file 0, 'C:\tempfolder\01\DDLCenter.bak',N'.bak'

应对方案

做为云计算厂商,为了保证SQL Server数据库平台的安全性,我们该如何应对这些高风险的扩展存储过程带给我们的安全挑战呢?我们的应对方案如下:

 禁用扩展存储过程

 删除扩展存储过程

 拒绝扩展存储过程执行权限

禁用扩展存储过程

有一些扩展存储过程属于是服务器外围配置选项,我们可以使用sys.sp_configure存储过程来开启和禁用它,这种方法在xp_cmdshell部分已经介绍过了,详情参见“开启xp_cmdshell”和“禁用xp_cmdshell”部分。

删除扩展存储过程

在SQL Server 2000及以前版本,我们还可以删除系统扩展存储过程,永绝后患。

EXEC master.sys.sp_dropextendedproc 'xp_cmdshell'

当然,删除后,你还是有后悔药可以再加回来

exec master.sys.sp_addextendedproc 'xp_cmdshell', 'C:\Program Files\Microsoft SQL Server\MSSQL\Binn\xplog70.dll'

而从SQL Server 2005开始,sys.sp_dropextendedproc不再支持对系统扩展存储过程的删除动作了,数据库管理员转而可以使用“拒绝扩展存储过程执行权限”的方式来达到相同的目的。

拒绝扩展存储过程执行权限

除了系统外围配置选项涉及的扩展存储过程可以通过sp_configure开启和禁用之外,剩下的系统扩展存储过程,我们只能通过拒绝PUBLIC角色可执行扩展存储过程权限的方式来禁用掉了,方法如下:

DENY EXECUTE ON sys.xp_regwrite TO PUBLIC;

此时,当用户再次执行写入注册表操作的时候,就会报告错误,比如:

EXEC master.sys.xp_regwrite
	@rootkey='HKEY_LOCAL_MACHINE',
	@key='SOFTWARE\aaaKey',
	@value_name='aaaValueName',
	@type='REG_SZ',
	@value='aaaValue'
;

错误信息如下所示:

05.png

这样从根本上达到了安全防范的目的,保护了云厂商SQL Server数据库PaaS平台的安全性。

检查扩展存储过程权限

最后,看看我们如何去查找某一个特定的系统扩展存储过程的权限呢?在此,以我们刚才拒绝执行权限的扩展存储过程xp_regwrite为例说明,如下:

SELECT 
	schema_name(obj.schema_id) AS [schema_name],
	obj.name AS [object_name],
	obj.type,
	b.permission_name,
	B.type,
	B.state_desc,
	C.name,
	c.type_desc    
FROM sys.all_objects AS obj WITH(NOLOCK)     
LEFT JOIN sys.database_permissions AS B  WITH(NOLOCK)
	ON B.major_id=obj.object_id AND B.minor_id=0 AND B.class=1      
LEFT JOIN sys.database_principals AS C  WITH(NOLOCK)
	ON C.principal_id = B.grantee_principal_id      
WHERE obj.name IN(SELECT object_name 
					FROM sys.system_components_surface_area_configuration AS x
					WHERE x.type = 'X' AND x.object_name = 'xp_regwrite')  
ORDER BY obj.name 

结果展示如下:

06.png

从检查的结果来看,PUBLIC角色已经被拒绝EXECUTE权限,达到了目的。

最后总结

本期月报我们分享了SQL Server数据库中几个有高安全风险的系统扩展存储过程,并没有完全罗列有安全风险的所有存储过程。保障云厂商SQL Server数据库PaaS平台的安全性,应对措施必不可少。

MySQL · 源码分析 · 聚合函数(Aggregate Function)的实现过程

$
0
0

总览

聚合函数(Aggregate Function)顾名思义,就是将一组数据进行统一计算,常常用于分析型数据库中,当然在应用中是非常重要不可或缺的函数计算方式。比如我们常见的COUNT/AVG/SUM/MIN/MAX等等。本文主要分析下该类函数实现的一些框架,不涉及到每个函数的详尽分析。聚合函数(Aggregate Function)实现的大部分代码在item_sum.h和item_sum.cc。

下面我们看一下代码,聚合函数(Aggregate Function)有哪些类型:

  enum Sumfunctype {
    COUNT_FUNC,           // COUNT
    COUNT_DISTINCT_FUNC,  // COUNT (DISTINCT)
    SUM_FUNC,             // SUM
    SUM_DISTINCT_FUNC,    // SUM (DISTINCT)
    AVG_FUNC,             // AVG
    AVG_DISTINCT_FUNC,    // AVG (DISTINCT)
    MIN_FUNC,             // MIN
    MAX_FUNC,             // MAX
    STD_FUNC,             // STD/STDDEV/STDDEV_POP or DISTINCT
    VARIANCE_FUNC,        // VARIANCE/VAR_POP and VAR_SAMP or DISTINCT
    SUM_BIT_FUNC,         // BIT_AND, BIT_OR and BIT_XOR
    UDF_SUM_FUNC,         // user defined functions
    GROUP_CONCAT_FUNC,    // GROUP_CONCAT or GROUP_CONCAT DISTINCT
    JSON_AGG_FUNC,        // JSON_ARRAYAGG and JSON_OBJECTAGG
    ROW_NUMBER_FUNC,      // Window functions
    RANK_FUNC,
    DENSE_RANK_FUNC,
    CUME_DIST_FUNC,
    PERCENT_RANK_FUNC,
    NTILE_FUNC,
    LEAD_LAG_FUNC,
    FIRST_LAST_VALUE_FUNC,
    NTH_VALUE_FUNC
  };

类Item_sum是聚合函数的基类。接下来我们继续看一下总体和主要的聚合函数具体在代码中的类结构和继承关系, overall.jpg

COUNT/SUM/AVG/STD/VAR_POP函数

MIN/MAX函数

BIT_OR/BIT_AND/BIT_XOR函数

不带GROUP BY聚合

下面我们来介绍下如何工作的,先来看看不带GROUP BY的聚合过程。该过程借助了一个辅助类Aggregator,而GROUP BY并不使用该辅助类。

在优化阶段,需要进行setup,比如初始化distinct或者sorting需要Temp table或者Temp tree结构,方便下阶段的聚合函数。具体根据不同函数有不同的实现。

JOIN::optimize--> 
JOIN::make_tmp_tables_info--> 
setup_sum_funcs--> 
Item_sum::aggregator_setup-->  
Aggregator_simple::setup-->
Item_sum::setup-->

在执行阶段,结果输出函数end_send_group调用init_sum_functions来对该SQL查询的所有SUM函数进行聚合计算。

JOIN::exec()--> 
do_select()--> 
sub_select()--> 
evaluate_join_record()--> 
end_send_group()--> 
init_sum_functions--> for all sum functions
reset_and_add()--> 
aggregator_clear()/aggregator_add()--> 
Item_sum_xxx::clear()/Item_sum_xxx::add()

在计算DISTINCT聚合时候,还需要必须实现aggregator::endup(),因为Distinct_aggregator::add() 只是通过某种方式采集了unique的行,但是并未保存,需要在这个阶段进行保存。这个过程也可以理解,因为在DISTINCT聚合过程中(add),在过程中无法判断是否为唯一。当然,这个并不适用于GROUP BY场景,因为GROUP BY场景本身就是通过临时表解决了唯一的问题。

带GROUP BY聚合

MySQL对于带GROUP BY的聚合,通常采用了Temp table的方式保存了(GROUP BY KEY, AGGR VALUE)。

JOIN::exec()--> 
do_select()--> 
sub_select()--> 
evaluate_join_record()--> 
sub_select_op()--> 
QEP_tmp_table::put_record-->
end_update-->
init_tmptable_sum_functions/update_tmptable_sum_func--> // 每个group by的key都会调用至少一次
reset_sum_func-->Item_sum_xxx::reset_field()/Item_sum_xxx::update_field()

Item_sum继承Item_result_field,意味着该类作为计算函数的同时,也保存输出的结果。具体可以看对应Item_sum_xxx::val_xxx的实现,该函数负责对上层结果或者客户端结果进行输出。

但是,对于特殊聚合函数如AVG/STD/VAR_POP等函数,在累加过程中,临时保存的变量值有多个,实际的输出结果必须通过加工处理,尤其是在GROUP BY的场景下,多个临时变量需要保存到Temp table中,下次累加的时候取出来,直到最终结果输出。因此,需要额外的辅助Item_result_field类,帮助该聚合函数进行最终结果输出。下图为各个辅助Item_result_field的继承关系。

举例来说,对于Item_avg_field类的最终结果(SELECT AVG(c1) FROM t1 GROUP BY c2)则需要通过Item_avg_field::val_xxx计算后进行输出,如:

double Item_avg_field::val_real() {
  // fix_fields() never calls for this Item
  double nr;
  longlong count;
  uchar *res;

  if (hybrid_type == DECIMAL_RESULT) return val_real_from_decimal();

  float8get(&nr, field->ptr);
  res = (field->ptr + sizeof(double));
  count = sint8korr(res);

  if ((null_value = !count)) return 0.0;
  return nr / (double)count;
}

调用的堆栈如下:

Item_avg_field::val_real
Item::send
THD::send_result_set_row
Query_result_send::send_data
end_send
evaluate_join_record
QEP_tmp_table::end_send
sub_select_op
sub_select
do_select
JOIN::exec

当然,这有个小Tips就是,如果内核需要实现多线程并行计算聚合函数的时候,我们就可以通过改造 对中间结果输出save_in_field_inner函数,让每个中间结果如2个value或者以上会按照自己的设计保存到相应的field->ptr中,保留到临时表中,堆栈如下:

// 这个函数是fake函数,主要其实就是调用默认的Item::save_in_field_inner基类函数。
type_conversion_status Item_avg_field::save_in_field_inner(Field *to,
                                                           bool no_conversions) {
  if (需要保留中间结果)
     to->store((char *)field->ptr, field->field_length, cs);
  else
     return Item::save_in_field_inner(to, no_conversions);
}

调用的堆栈如下:

Item_avg_field::save_in_field_inner
Item::save_in_field
fill_record
fill_record_n_invoke_before_triggers
Query_result_insert::store_values
Query_result_insert::send_data
end_send
evaluate_join_record
QEP_tmp_table::end_send
sub_select_op
sub_select
do_select
JOIN::exec

聚合函数的优化

不带where子句的简单COUNT

在简单求计数统计时候(SELECT COUNT(*) FROM t1),Server层和Innodb层实现了handler::ha_records用于直接返回准确的计数。由于加了WHERE子句会调用evaluate_join_record评估是否该返回行否和统计条件。详细调用堆栈如下:

ha_innobase::records
handler::ha_records
get_exact_record_count
end_send_count
do_select
JOIN::exec

无GROUP BY的MIN/MAX单行优化

如果恰好对index所在的列求MIN/MAX,而且只返回一行没有GROUP BY的情况下,那么这个是可以进行优化的,可以看执行计划的Extra信息变成Select tables optimized away而非使用Using temporary。

mysql> explain select min(c1) from ttt;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                        |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------------------+
|  1 | SIMPLE      | NULL  | NULL       | NULL | NULL          | NULL | NULL    | NULL | NULL |     NULL | Select tables optimized away |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------------------+
1 row in set, 1 warning (0.00 sec)

因此结果会在优化阶段就已经计算完毕返回到上层,堆栈如下:

ha_innobase::index_first
handler::ha_index_first 
get_index_min_value
opt_sum_query
JOIN::optimize

当然还有类似MIN(1)/MAX(1)的常量处理也类似,连innodb层都不会涉及到,这里就不再赘述了。

使用松散索引扫描Using index for group-by方式的聚合

这种是适用于特殊场景:MIN/MAX,因为不需要去扫描所有行去找到最大最小值。扫描的方式可以通过index直接跳到最大和最小的聚合值的位置。比如下面的例子,需要找到每个唯一c1的最最小值,恰好c1,c2是一个index上的属性列,那么可以通过定位c1,直接在索引上寻找(c1, min(c2)),无需扫描所有行。

create table t1 (c1 int not null, c2 char(6) not null, c3 int not  null, key(c1, c2, c3));
insert into t1 values (1, 'Const1', 2);
insert into t1 values (2, 'Const2', 4);
insert into t1 values (3, 'Const3', 4);
insert into t1 values (4, 'Const4', 9);
insert into t1 values (5, 'Const5', 9);
insert into t1 select * from t1;
insert into t1 select * from t1;
insert into t1 select * from t1;
insert into t1 select * from t1;
insert into t1 select * from t1;
insert into t1 select * from t1;
insert into t1 select * from t1;
# using IndexRangeScanIterator + QUICK_GROUP_MIN_MAX_SELECT Using index for group-by
explain select min(c2)  from ttt2 group by c1;
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+--------------------------+
| id | select_type | table | partitions | type  | possible_keys | key  | key_len | ref  | rows | filtered | Extra                    |
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+--------------------------+
|  1 | SIMPLE      | t1  | NULL       | range | c1            | c1   | 4       | NULL |    2 |   100.00 | Using index for group-by |
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+--------------------------+

详细堆栈如下:

handler::ha_index_last
QUICK_GROUP_MIN_MAX_SELECT::reset
IndexRangeScanIterator::Init
sub_select
JOIN::exec

index_first/index_next_different
IndexRangeScanIterator::Read
IndexRangeScanIterator::Init
sub_select
JOIN::exec

综述

综上所述,本篇文章主要从源码层面对MySQL 8.0 实现的聚合函数(Aggregate Function)进行了一下简要的分析。聚合函数(Aggregate Function)在无GROUP BY的情况下,利用定义成员变量保存对应计算结果的中间值,在有GROUP BY的情况下利用了Temp Table来保存对应的GROUP BY的键和聚合值,另外还介绍了一些聚合函数(Aggregate Function)的优化方式。当然这里面还有两类重要的聚合就是ROLL UP和WINDOWS函数,由于篇幅限制,未来篇章会单独介绍。希望该篇文章能够帮助广大读者了解MySQL聚合函数(Aggregate Function)的实现原理。

PgSQL · 最佳实践 · RDS for PostgreSQL 的逻辑订阅

$
0
0

背景

在RDS for PostgreSQL 10 中,不再需要使用额外的插件,直接在内核里面实现了表级别的逻辑订阅功能。如果用户具有以下的几个场景,可以尝试使用该逻辑订阅的功能:

  • 某些表复制到不同实例的需求。如果某个表需要在全国各地都有接入点的查询,可以使用不同的实例,其中一个为中心实例,其他查询实例订阅该表。
  • 将多个数据库实例的数据,同步到一个实例目标数据库进行数据分析。
  • 在不同的数据库版本之间,复制数据。
  • 将同一个数据库实例的表,复制到同一实例不同的目标库。

注:因为网络连通原因,目前只支持RDS for PostgreSQL 10 基础版开放。下面简单介绍下其具体的使用方法。

RDS for PostgreSQL 10 实践

确认发布端和订阅端网络

只有发布端和订阅端网络可连才能创建逻辑订阅。注意发布端的白名单需要增加订阅端IP:

  • 相同实例不同数据库无需增加
  • 不同实例如果在相同VPC 内部,最好是在白名单中增加VPC 的网段

发布端修改wal_level

要使用逻辑订阅,必须要设置发布端实例的wal_level 参数 >=logical。RDS for PostgreSQL 10 控制台支持该参数的修改,如下图:

image.png

注:该参数会重启实例,请选择合理的修改时间。

发布端创建 PUBLICATION

在发布端对特殊的表(ALL 代表全部的表)创建PUBLICATION 如下:

 CREATE PUBLICATION mypub FOR TABLE test ;

其中:

  • 逻辑订阅目前支持 insert, update, delete, truncate 中一种或者多种的订阅。
  • 支持update 和 delete 订阅的表需要设置 REPLICA IDENTITY唯一标示一行。
  • 同一个表可以发布多次。
  • 一个PUBLICATION 可以允许多个SUBSCRIPTION。
  • 保证事务级别的逻辑订阅,不会出现某个事务复制一半的情况。

其他 CREATE PUBLICATION 的语法详见链接

发布端带有replication 权限用户

复制源端必须提供带有replication 权限的用户,在RDS for PostgreSQL 10 中,初始账号具有replication 权限,由初始账号执行create role xxx with superuser 的用户也具有replication 权限。

发布端其他参数

  • max_replication_slots 默认16个,目前不支持修改
  • max_wal_senders 默认16个,目前不支持修改

订阅端

  1. max_replication_slots,大于等于该实例总共需要创建的订阅数

  2. max_logical_replication_workers,大于等于该实例总共需要创建的订阅数

  3. max_worker_processes, 大于等于max_logical_replication_workers + 1 + CPU并行计算 + 其他插件需要fork的进程数.

订阅端创建 SUBSCRIPTION

在订阅端创建于发布端相同的表结构:

create table test(id int);

在订阅端创建SUBSCRIPTION:

CREATE SUBSCRIPTION mysub CONNECTION 'dbname=demodb host=xxx.aliyun-inc.com port=3432 user=acc password=xxx' PUBLICATION mypub with (copy_data=true);

其中:

  • 必须使用订阅端实例的初始账号(或者初始账号创建的其他的rds_superuser 账号)来创建订阅。
  • 如果相同的实例不同数据库之间的订阅,host=localhost 而port 参数需要执行show port; 来获取。而且订阅端需要先手动创建replication_slot 如下,另外创建订阅时参数create_slot=false,否则创建订阅的语句会一直卡在那里。
select * from pg_create_logical_replication_slot(‘订阅名称>’,’ pgoutput’);
  • copy_data 表示如果复制源端已经有数据,则会把数据先全部复制过来。
  • 订阅端需要通过流复制协议连接到发布端,同时需要在发布端创建replication slot。
  • 要完全删除订阅,使用drop subscription,删除订阅后,本地的表不会被删除,数据也不会清除,仅仅是不在接收发布端的数据,对应发布端的replication slot 也会被删除。
  • 如果删除订阅时,发布端不能连接,则删除失败,需要先暂停该订阅(alter subscription),但是发布端的replication slot 没有被删除,需要手工维护。

冲突处理

逻辑订阅可以简单理解为将xlog 解析,然后在订阅端执行对应的SQL。当订阅端执行SQL 失败,则订阅就会暂停。冲突修复一般有如下方法:

  • 修改订阅端的数据解决冲突。例如insert违反了唯一约束时,可以DELETE订阅端造成唯一约束冲突的记录,然后启动逻辑订阅。
  • 在订阅端调用pg_replication_origin_advance(node_name text, pos pg_lsn)函数,node_name就是subscription name,pos指重新开始的LSN,从而跳过有冲突的事务,详见链接

逻辑订阅的监控

发布端

逻辑订阅属于一种逻辑复制,可以在发布端查看pg_stat_replication 来查看复制的各个lsn 位点如下:

demodb=> select * from pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 67297
usesysid         | 16384
usename          | acc
application_name | mysub
client_addr      | xxx
client_hostname  |
client_port      | 58841
backend_start    | 2019-04-18 18:27:29.031333+08
backend_xmin     |
state            | streaming
sent_lsn         | 0/11DB8728
write_lsn        | 0/11DB8728
flush_lsn        | 0/11DB8728
replay_lsn       | 0/11DB8728
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async

订阅端

订阅端提供了pg_stat_subscription 的视图来查看当前接受的lsn 位点,如下:

demodb=> select * from pg_stat_subscription;
-[ RECORD 1 ]---------+------------------------------
subid                 | 56043
subname               | mysub
pid                   | 59777
relid                 |
received_lsn          | 0/11DB8728
last_msg_send_time    | 2019-04-18 23:16:10.879015+08
last_msg_receipt_time | 2019-04-18 23:16:10.88364+08
latest_end_lsn        | 0/11DB8728
latest_end_time       | 2019-04-18 23:16:10.879015+08

其他

  • 不要用循环复制
  • 要维护好replication slot,如果管理不当,会造成WAL 日志不能及时清理,从而造成磁盘满锁定
  • 订阅端可以同时修改订阅的表
  • 订阅端的表可以配合触发器完成更丰富的功能
  • 如果被订阅的数据有主外键约束,请将其作为一个订阅。否则可能会有约束的问题

MySQL · 引擎特性 · 通过 SQL 管理 UNDO TABLESPACE

$
0
0

前言

InnoDB的undo log从5.6版本开始可以存储到单独的tablespace文件中,在5.7版本支持了在线undo文件truncate,解决了长期以来的undo膨胀问题。而到了8.0版本,对Undo tablespace做了进一步的优化:在新版本中,我们可以拥有更多的回滚段(每个Undo tablespace可以有128个回滚段,而在之前的版本中所有tablespace的回滚段不允许超过128个),减少了由于事务公用回滚段产生的锁冲突;可以在线动态的增删undo tablespace,使得undo的管理更加灵活。

在最近release的8.0.14版本中,开始支持SQL接口来创建,修改和删除 (undo space的管理不记录binlog)。可以预见未来将逐步废弃根据配置innodb_undo_tablespaces来创建undo tablespace, 通过SQL接口来创建undo tablespace将是唯一的接口。实际上在最新版本中已经将参数innodb_undo_tablespaces标记为deprecated状态,用户应尽量避免依赖该参数。

SQL语句

implict undo space

在安装实例时,会默认创建两个undo tablespace:

 mysql> SELECT * FROM  INFORMATION_SCHEMA.INNODB_TABLESPACES WHERE ROW_FORMAT = 'Undo'\G
*************************** 1. row ***************************
SPACE: 4294967279
NAME: innodb_undo_001
FLAG: 0
ROW_FORMAT: Undo
PAGE_SIZE: 16384
ZIP_PAGE_SIZE: 0
SPACE_TYPE: Undo
FS_BLOCK_SIZE: 0
FILE_SIZE: 0
ALLOCATED_SIZE: 0
SERVER_VERSION: 8.0.15
SPACE_VERSION: 1
ENCRYPTION: N
STATE: active
*************************** 2. row ***************************
SPACE: 4294967278
NAME: innodb_undo_002
FLAG: 0
ROW_FORMAT: Undo
PAGE_SIZE: 16384
ZIP_PAGE_SIZE: 0
SPACE_TYPE: Undo
FS_BLOCK_SIZE: 0
FILE_SIZE: 0
ALLOCATED_SIZE: 0
SERVER_VERSION: 8.0.15
SPACE_VERSION: 1
ENCRYPTION: N
STATE: active
2 rows in set (0.00 sec)

mysql> SHOW GLOBAL STATUS LIKE '%UNDO_TABLESPACE%';
+----------------------------------+-------+
| Variable_name                    | Value |
+----------------------------------+-------+
| Innodb_undo_tablespaces_total    | 2     |
| Innodb_undo_tablespaces_implicit | 2     |
| Innodb_undo_tablespaces_explicit | 0     |
| Innodb_undo_tablespaces_active   | 2     |
+----------------------------------+-------+
4 rows in set (0.00 sec)

创建新的undo space

你可以通过如下语句来创建独立的undo tablespace, 文件后缀必须以ibu结尾。新创建的tablespace为active状态

  mysql> CREATE UNDO TABLESPACE myundo ADD DATAFILE 'myundo.ibd';
  ERROR 3121 (HY000): The ADD DATAFILE filepath must end with '.ibu'.

  mysql> CREATE UNDO TABLESPACE myundo ADD DATAFILE 'myundo.ibu';
  Query OK, 0 rows affected (0.26 sec)

  mysql> SELECT * FROM  INFORMATION_SCHEMA.INNODB_TABLESPACES WHERE ROW_FORMAT = 'Undo' and NAME = 'myundo'\G
  *************************** 1. row ***************************
  SPACE: 4294967277
  NAME: myundo
  FLAG: 0
  ROW_FORMAT: Undo
  PAGE_SIZE: 16384
  ZIP_PAGE_SIZE: 0
  SPACE_TYPE: Undo
  FS_BLOCK_SIZE: 0
  FILE_SIZE: 0
  ALLOCATED_SIZE: 0
  SERVER_VERSION: 8.0.15
  SPACE_VERSION: 1
  ENCRYPTION: N
  STATE: active                       --> 此时状态为active
1 row in set (0.01 sec)

在创建undo space时,你可以使用绝对路径,也可以放在实例配置的undo目录下,但要注意一点:在崩溃恢复前undo space必须要能够被发现并打开,但这时候Innodb data dictionary还是处于不可用的状态,我们无法从其中获取准确的文件位置,只有–datadir, –innodb-home-directory, –innodb-undo-directory 和 –innodb-directories会被扫描掉,如果你放在其他地方,就可能造成找不到该tablespace, 导致实例数据不一致。

相关代码:

  • Server层接口类:Sql_cmd_create_undo_tablespace
  • 为undo tablespace预留的space id (但最多依然是127个undo tablespace, 每个space number会给一个范围内的space id, 默认512个id):
    • s_min_undo_space_id = 0xFFFFFFF0UL - 127 * 512
    • s_max_undo_space_id = 0xFFFFFFF0UL - 1
  • InnoDB入口函数: innodb_create_undo_tablespace
    • 获取下一个可用的space id: undo::get_next_available_space_num(), 先拿到空闲的space number,再分配一个可用的space id
    • srv_undo_tablespace_create: 创建undo space, 初始化回滚段并加入到全局事务系统中
    • 提交变更,持久化tablespace信息后,将其设置为active状态,此后事务可以从其中分配到回滚段

设置inactive

如果你不想使用某个Undo tablespace,可以将其设置为inactive状态, 但需要保证至少有连个active的undo tablespace, 这个限制的原因是:当一个undo tablespace正在被truncate时,至少有一个是可用的。

当被设置为Inactive状态之后,事务就不会从其中分配回滚段。

mysql> ALTER UNDO TABLESPACE myundo SET INACTIVE;
Query OK, 0 rows affected (0.01 sec)

相关代码:

  • server层接口类:Sql_cmd_alter_undo_tablespace
  • 在崩溃恢复data dicitonary提供服务后,需要将undo space状态更新到内存(apply_dd_undo_state())
  • innodb_alter_undo_tablespace–> innodb_alter_undo_tablespace_active
    • 设置Undo space 为active状态,并修改dd元数据
  • innodb_alter_undo_tablespace –> innodb_alter_undo_tablespace_inactive
    • 当undo space状态为empty时,直接返回
    • 当undo space状态为active时,需要确保至少两个active的undo space才允许操作,否则返回错误
    • 设置dd state为inactive,并修改回滚段状态
    • 设置truncate frequency为1并唤醒purge线程, 这样purge线程会更频繁的去做purge操作,加快undo space的回收

删除undo space

在删除一个undo tablespace之前,首先要把undo tablespace设置为inactive状态

  mysql> DROP UNDO TABLESPACE myundo;
  ERROR 1529 (HY000): Failed to drop UNDO TABLESPACE myundo

  mysql> ALTER UNDO TABLESPACE myundo SET INACTIVE;
  Query OK, 0 rows affected (0.01 sec)

  mysql> SELECT * FROM  INFORMATION_SCHEMA.INNODB_TABLESPACES WHERE ROW_FORMAT = 'Undo' and Name = 'myundo'\G
  *************************** 1. row ***************************
  SPACE: 4294967150
  NAME: myundo
  FLAG: 0
  ROW_FORMAT: Undo
  PAGE_SIZE: 16384
  ZIP_PAGE_SIZE: 0
  SPACE_TYPE: Undo
  FS_BLOCK_SIZE: 0
  FILE_SIZE: 0
  ALLOCATED_SIZE: 0
  SERVER_VERSION: 8.0.15
  SPACE_VERSION: 1
  ENCRYPTION: N
  STATE: empty     --> 此时undo space内没有任何Undo log, 已经是empty可删除状态
  1 row in set (0.00 sec)

  mysql> DROP UNDO TABLESPACE myundo;
  Query OK, 0 rows affected (0.02 sec)

即使状态为inactive的,但要保证如下几点才能被删除:

  • 没有任何事务需要看到其中的老版本数据,也就是说所有在该事务之前开启的read view必须全部关闭
  • 所有使用该undo tablespace的事务必须全部提交或回滚掉
  • purge线程需要将其中的Undo log全部清理掉

如果undo tablespace非空,在drop时,会返回错误码HA_ERR_TABLESPACE_IS_NOT_EMPTY. 所以在设置为inactive到真正可以删除可能存在时间差,我们可以通过监控INFORMATION_SCHEMA.INNODB_TABLESPACES中的undo space状态是否为empty来判定是否可以删除。 Note:系统创建的Undo space不允许被删除

相关代码:

  • Server层接口类:Sql_cmd_drop_undo_tablespace
  • InnoDB 入口函数: innodb_drop_undo_tablespace
    • 当undo space状态不为emtpy时或者是系统创建的Undo space时,不允许删除
    • invalidate buffer pool中该space的page
    • 从内存中删除,记录ddl log
    • 事务提交后,执行post ddl (Log_DDL::replay_delete_space_log)
      • 真正物理删除文件
      • 标记对应的space num为未使用状态

undo truncation

当参数innodb_undo_log_truncate打开时,所有隐式和显式创建的Undo tablespace都会在满足一定条件时被purge线程truncate掉. 当参数关闭时,则只有将Undo tablespace设置为Inactive状态时才会去truncate tablespace。 因此如果你想自己控制undo truncation, 可以关闭参数,在监控undo tablespace的大小,通过SET INACTIVE触发truncation, 再通过SET ACTIVE激活undo space。

相关代码:

  • 由purge线程发起,入口函数:trx_purge_truncate_marked_undo()
  • 需要获取MDL锁,来保护space不被alter/drop
  • 通过flush_observer flush当前space的page
  • trx_purge_truncate_marked_undo_low
    • trx_undo_truncate_tablespace:
      • 为当前space分配一个新的space id: undo::use_next_space_id(space_num)
      • fil_replace_tablespace: 删除当前undo space,重建文件并设置为新的space id
      • 重新初始化回滚段和内存信息
      • 根据新的space id,将所有变更刷到磁盘
      • 如果是用户创建的undo space,将状态设置为empty,否则设置为active状态
      • 更新DD

为何需要新的space id ? 这是因为在删除重建文件的过程中我们没有做checkpoint,这时候如果crash掉,有些redo log可能需要修改一些已经不存在的page,导致崩溃恢复时候(ref: bug93170)

Reference

1. WL#9508: InnoDB: Support CREATE/ALTER/DROP UNDO TABLESPACE2. WL#9507: InnoDB: Make the number of undo tablespaces and rollback segments dynamic3. 主要代码4. 官方文档5. MySQL8.0 · 引擎特性 · 关于undo表空间的一些新变化

MySQL · 最佳实践 · 通过Resource Group来控制线程计算资源

$
0
0

MySQL8.0增加了一个新功能resource group, 可以对不同的用户进行资源控制,例如对用户线程和后台系统线程给予不同的CPU优先级。

用户可以通过SQL接口创建不同的分组,这些分组可以作为sql的hit,也可以动态的绑定过去。本文主要简单介绍下用法,至于底层如何实现的,其实比较简单:创建的分组被存储到系统表中;在linux系统底层通过CPU_SET来绑定CPU,通过setpriority来设置线程的nice值

相关worklog: WL#9467: Resource Groups

创建resource group

首先系统自带两个resource group并且不可被修改

root@(none) 05:54:22>SELECT * FROM INFORMATION_SCHEMA.RESOURCE_GROUPS\G
*************************** 1. row ***************************
RESOURCE_GROUP_NAME: USR_default
RESOURCE_GROUP_TYPE: USER
RESOURCE_GROUP_ENABLED: 1
VCPU_IDS: 0-63
THREAD_PRIORITY: 0
*************************** 2. row ***************************
RESOURCE_GROUP_NAME: SYS_default
RESOURCE_GROUP_TYPE: SYSTEM
RESOURCE_GROUP_ENABLED: 1
VCPU_IDS: 0-63
THREAD_PRIORITY: 0
2 rows in set (0.00 sec)

如果你想设置thread priority,可能需要使用超级账户来启动Mysqld,这是系统限制,如果以非super账户启动,只能降低而不能提升优先级。在非super启动时,thread_priority会被忽略掉并报一个warning出来。

对于类型为system的系统后台线程,cpu priority只能从-20 ~0,而普通user线程,则在0~19之间,这样就保证了系统线程的优先级肯定比用户线程高。

如果设置不在范围内,就会报错

  root@(none) 10:27:09>CREATE RESOURCE GROUP test_user_rg   TYPE = USER   VCPU = 0-32,48-63   THREAD_PRIORITY = -10;
  ERROR 3654 (HY000): Invalid thread priority value -10 for User resource group test_user_rg. Allowed range is [0, 19].
                      我们尝试为user类线程创建一个resource group,使用0-32, 48-63号cpu, 线程优先级为10

                      root@(none) 10:27:14>CREATE RESOURCE GROUP test_user_rg   TYPE = USER   VCPU = 0-32,48-63   THREAD_PRIORITY = 10;
                      Query OK, 0 rows affected (0.01 sec)

  root@(none) 10:55:19>SELECT * FROM INFORMATION_SCHEMA.RESOURCE_GROUPS WHERE RESOURCE_GROUP_NAME = 'test_user_rg'\G
  *************************** 1. row ***************************
     RESOURCE_GROUP_NAME: test_user_rg
        RESOURCE_GROUP_TYPE: USER
        RESOURCE_GROUP_ENABLED: 1
                      VCPU_IDS: 0-32,48-63
                             THREAD_PRIORITY: 10
                             1 row in set (0.00 sec)
  CREATE/DELETE/ALTER RESOURCE GROUP都需要RESOURCE_GROUP_ADMIN权限,具体的语法见官方文档

使用resource group

创建好后,我们该如何使用resource group呢,主要有两种方式,一种是SET RESOURCE GROUP, 一种是通过SQL HINT的方式,以下是简单的测试:

设置当前session:

  root@(none) 11:01:08>SET RESOURCE GROUP test_user_rg;
  Query OK, 0 rows affected (0.00 sec)
  也可以指定hint的方式来设置:

  root@sb1 11:07:53>select /* + RESOURCE_GROUP(test_user_rg) */ * from sbtest1 where id <10;
  还可以通过thread id来设置其他运行中的session,注意这里的thread id不是show processlist看到的id,而是通过performance_schema.threads表看到的id

  xx@performance_schema 11:30:21>SELECT THREAD_ID, TYPE FROM performance_schema.threads WHERE PROCESSLIST_ID = 26\G
  *************************** 1. row ***************************
  THREAD_ID: 71
       TYPE: FOREGROUND
       1 row in set (0.00 sec)
  xx@performance_schema 11:30:43>SET RESOURCE GROUP test_user_rg for 71;
  Query OK, 0 rows affected (0.00 sec)

如果你想对InnoDB的后台线程来进行设置呢 ? 可以去查看performance_schema.threads表,例如我们对page cleaner进行优先级设置:

  xx@performance_schema 11:19:43>CREATE RESOURCE GROUP test_system_rg   TYPE = SYSTEM   VCPU = 49   THREAD_PRIORITY = -10;
  Query OK, 0 rows affected (0.00 sec)

  xx@performance_schema 11:24:11>SELECT THREAD_ID, TYPE FROM performance_schema.threads WHERE NAME LIKE '%page_flush_coor%'\G
  *************************** 1. row ***************************
  THREAD_ID: 13
       TYPE: BACKGROUND
       1 row in set (0.00 sec)
  xx@performance_schema 11:24:07>SET RESOURCE GROUP test_system_rg for 13;
  Query OK, 0 rows affected (0.00 sec)

可以看到,通过resource group,我们可以为任意的线程指定不同的计算资源。在未来我们甚至可以对这一功能进行扩展,例如某个线程的最大iops,读入数据占用buffer pool的百分比,或者对运维程序指定独立的cpu,避免干扰到正常的业务负载等等,还是有不少的想象空间的。


MySQL · 引擎特性 · Skip Scan Range

$
0
0

MySQL从8.0.13版本开始支持一种新的range scan方式,称为Loose Skip Scan。该特性由Facebook贡献。我们知道在之前的版本中,如果要使用到索引进行扫描,条件必须满足索引前缀列,比如索引idx(col1,col2), 如果where条件只包含col2的话,是无法有效的使用idx的, 它需要扫描索引上所有的行,然后再根据col2上的条件过滤。

新的优化可以避免全量索引扫描,而是根据每个col1上的值+col2上的条件,启动多次range scan。每次range scan根据构建的key值直接在索引上定位,直接忽略了那些不满足条件的记录。

示例 下例是从官方文档上摘取的例子:

  root@test 11:03:28>CREATE TABLE t1 (f1 INT NOT NULL, f2 INT NOT NULL, PRIMARY KEY(f1, f2));
  Query OK, 0 rows affected (0.00 sec)

  root@test 11:03:29>INSERT INTO t1 VALUES
  ->   (1,1), (1,2), (1,3), (1,4), (1,5),
  ->   (2,1), (2,2), (2,3), (2,4), (2,5);
  Query OK, 10 rows affected (0.00 sec)
  Records: 10  Duplicates: 0  Warnings: 0

  root@test 11:03:29>INSERT INTO t1 SELECT f1, f2 + 5 FROM t1;
  Query OK, 10 rows affected (0.00 sec)
  Records: 10  Duplicates: 0  Warnings: 0

  root@test 11:03:29>INSERT INTO t1 SELECT f1, f2 + 10 FROM t1;
  Query OK, 20 rows affected (0.00 sec)
  Records: 20  Duplicates: 0  Warnings: 0

  root@test 11:03:29>INSERT INTO t1 SELECT f1, f2 + 20 FROM t1;
  Query OK, 40 rows affected (0.00 sec)
  Records: 40  Duplicates: 0  Warnings: 0

  root@test 11:03:29>INSERT INTO t1 SELECT f1, f2 + 40 FROM t1;
  Query OK, 80 rows affected (0.00 sec)
  Records: 80  Duplicates: 0  Warnings: 0

  root@test 11:03:29>ANALYZE TABLE t1;
  +---------+---------+----------+----------+
  | Table   | Op      | Msg_type | Msg_text |
  +---------+---------+----------+----------+
  | test.t1 | analyze | status   | OK       |
  +---------+---------+----------+----------+
  1 row in set (0.00 sec)

  root@test 11:03:29>EXPLAIN SELECT f1, f2 FROM t1 WHERE f2 > 40;
  +----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+----------------------------------------+
  | id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra                                  |
  +----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+----------------------------------------+
  |  1 | SIMPLE      | t1    | NULL       | range | PRIMARY       | PRIMARY | 8       | NULL |   53 |   100.00 | Using where; Using index for skip scan |
  +----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+----------------------------------------+
  1 row in set, 1 warning (0.00 sec)

也可以从optimizer trace里看到如何选择的skip scan:

"skip_scan_range": {
  "potential_skip_scan_indexes": [
  {
    "index": "PRIMARY",
      "tree_travel_cost": 0.4,
      "num_groups": 3,
      "rows": 53,
      "cost": 10.625
  }
  ]
},
  "best_skip_scan_summary": {
    "type": "skip_scan",
    "index": "PRIMARY",
    "key_parts_used_for_access": [
      "f1",
    "f2"
      ],
    "range": [
      "40 < f2"
      ],
    "chosen": true
  },

我们从innodb的角度来看看这个SQL是如何执行的,我们知道每个index scan都会走到ha_innobase::index_read来构建search tuple,上述查询的执行步骤:

  • 第一次从Index left side开始scan
  • 第二次使用key(1,40) 扫描index,直到第一个range结束
  • 使用key(1), find_flag =HA_READ_AFTER_KEY, 找到下一个Key值2
  • 使用key(2,40),扫描Index, 直到range结束
  • 使用Key(2),去找大于2的key值,上例中没有,因此结束扫描

笔者在代码注入了日志,打印search_tuple(dtuple_print())

STEP 1: no search_tuple

STEP 2:
DATA TUPLE: 2 fields;
0: len 4; hex 80000001; asc     ;;
1: len 4; hex 80000028; asc    (;;

STEP 3:
DATA TUPLE: 1 fields;
0: len 4; hex 80000001; asc     ;;

STEP 4:
DATA TUPLE: 2 fields;
0: len 4; hex 80000002; asc     ;;
1: len 4; hex 80000028; asc    (;;

STEP 5:
DATA TUPLE: 1 fields;
0: len 4; hex 80000002; asc     ;;

从上述描述可以看到使用skip-scan的方式避免了全索引扫描,从而提升了性能,尤其是在索引前缀列区分度比较低的时候

条件 skip scan可以通过Hint或者optimizer_switch来控制(skip_scan),默认是打开的。根据worklog的描述,对于如下query:

SELECT A_1,...,A_k, B_1,...,B_m, C
FROM T
WHERE
EQ(A_1,...,A_k)
AND RNG(C);

需要满足如下条件才能使用 skip scan:

A) Table T has at least one compound index I of the form:
I = <A_1,...,A_k, B_1,..., B_m, C ,[D_1,...,D_n]>
Key parts A and D may be empty, but B and C must be non-empty.
B) Only one table referenced.
C) Cannot have group by/select distinct
D) Query must reference fields in the index only.
E) The predicates on A_1...A_k must be equality predicates and they need
to be constants. This includes the 'IN' operator.
F) The query must be a conjunctive query.
In other words, it is a AND of ORs:
(COND1(kp1) OR COND2(kp1)) AND (COND1(kp2) OR ...) AND ...
G) There must be a range condition on C.
H) Conditions on D columns are allowed. Conditions on D must be in
conjunction with range condition on C.

ref: get_best_skip_scan()

当skip scan拥有更低的cost时,会被选择,计算cost的函数是cost_skip_scan(),由于索引统计信息中已经基于不同的前缀列值估算了distinct value的个数(rec_per_key), 可以基于此去预估可能需要读的行数。 更具体的可以参考wl#11322中的描述,笔者对此不甚了解,故不做笔墨 ref: cost_skip_scan()

参考

官方文档:Skip Scan Range Access MethodWL#11322: SUPPORT LOOSE INDEX RANGE SCANS FOR LOW CARDINALITYBug#88103相关代码

MongoDB · 应用案例 · killOp 案例详解

$
0
0

MongoDB 提供 currentOp命令,列出当前正在执行的查询操作,并提供 killOp命令,用于中止一些耗时比较长,影响线上业务的操作,作为一种应急手段。

下图是一个 currentOp 命令的输出项之一,用户在获取到 opid 后,调用 killOp() 并没有把这个请求干掉。

_2019_05_23_12_25_34

为什么 opid 是负数?

opid 在 mongod 里是一个 uint32类型的整数,当你从 mongo shell 里看到 opid 为负数时,说明你的 mongod 已经成功执行超过21(INT32_MAX)次请求了,相当牛逼。

MongoDB 客户端与server是通过 BSON 来交换数据的,而在 bson 标准里,是没有 uint32类型的,所以 opid 最终是以 int32传递给客户端的,shell 拿到这个opid,当这个值超过 INT32_MAX 时,打印出来就是负数了。

负数的 opid 会 kill 会不掉么?

MongoDB 3.2.5 之前的确是有这个 bug,没有考虑到负数的情况,在 SERVER-23066里已经修复了,阿里云上3.2、3.4、4.0 的最新版本均已修复这个问题。

修复的代码也很简单,就是接收到负数opid时,将其转换为 uint32类型,详见 SERVER-23066 Make killOp accept negative opid

mongod 既然已经修复了,负数 opid 还是 kill 不掉?

此时 killOp 不成功,已经跟 opid 是否是负数没有关系了,本来在 MongoDB 的设计里,也不是所有操作都能被 kill 的。

killOp 的原理,为什么 killOp 能干掉请求?

MongoDB 一个用户连接,后端对应一个线程,本身一个请求开始后,会有一个线程一直执行,直到技术。能被 killOp 杀掉的请求,是因为请求在执行过程中会检测,是否收到了 kill 信号,如果收到了,就走结束请求的逻辑。所以 killOp 的作用也只是给对应的操作一个 kill 信号标志而已。

SomeCommand::Run() 
{
   for (someCondition) {
       doSomeThing();
       if (killOpReceived) { // SomeCommand 主动检测了 killOp 的信号,才能被 kill 掉
         break;
       }
   }
}

什么样的操作需要被 kill 掉?

运行逻辑很简单、开销很低的命令无需捕获 killOp 信号,这种操作 kill 掉也没什么意义,解决不了根本问题。而复杂命令,比如 find、update、createIndex、aggregation 等操作,可能持续遍历很多条记录,才一定需要具备被 kill 的能力。MongoDB 会在执行这些命令的执行逻辑里加入检查是否收到 kill 命令的逻辑。

加了 killOp 检测逻辑的命令,就一定能立马被 kill?

不一定,一个操作比如 createIndex,会分为很多步骤,命令解析、加锁、执行具体命令逻辑、释放锁、回包等,只有命令执行到具体执行逻辑里时,killOp 才会生效,如果一个操作还没有成功加上锁,本身每占用什么资源,而且对应的现成也没有执行,killOp 是不会生效的。

query 操作为什么会加写锁?

正常只读的请求、如 find、listIndexes 都是不会加写锁,但当 MongoDB 开启 profiling 的时候,请求执行超过一定阈值(默认100ms)的请求,会记录到 db.system.profile capped colleciton 里,写这个集合就需要加意向写锁(w),同时对于 capped collection 的写入,会有一个特殊的 METADATA 互斥写锁(W),有兴趣的研究代码,关键字列在下面.

const ResourceId resourceCappedInFlightForOtherDb =
    ResourceId(RESOURCE_METADATA, ResourceId::SINGLETON_CAPPED_IN_FLIGHT_OTHER_DB);
    
Lock::ResourceLock cappedInsertLockForLocalDb(
    txn->lockState(), resourceCappedInFlightForLocalDb, MODE_X);

MySQL · 源码分析 · LinkBuf设计与实现

$
0
0

简介

在MySQL8.0中增加了一个新的数据结构叫做Link_buf,它是一个无锁的数据结构,这个数据结构主要用于redolog以及buffer pool的flush list.

这个数据结构简单来看就是一个拥有固定大小的数组,而对于InnoDB使用来说里面保存的就是写入log buffer或者加入到flush list的数据的大小.数组的每个元素可以被原子的更新.

由于在8.0种写入log buffer会有空洞的产生,因此这个数据结构就用来track当前log buffer的写入情况,也就是说每次写入的数据大小都会保存在linkbuffer中,而每次写入的位置通过start lsn来得到(hash), 假设有空洞(某些lsn还没有写入),那么它对应在linkbuffer中的值就是0,这样就能很简单的track空洞.

最后要注意的是这个数据结构的前提就是LSN是一直增长且不会重复的.因此在InnoDB中只在redolog中使用.

之后在分析redolog的时候,我们可以详细的看到这个数据结构的使用.

源码分析

核心字段

我们先来看这个数据结构的核心字段.

  1. Distance 这个累心表示了我们的Link_buf所包含的内容的类型(一般是lsn_t).
  2. m_capacity 表示Link_buf的大小.
  3. m_links所有的内容都是保存在这里(也就是一个动态数组).
  4. m_tail表示当前buffer的结尾(这里的结尾的意思是第一个空洞的位置,也就是可以保证m_tail之前都是连续的).
template <typename Position = uint64_t>
class Link_buf {
 public:
  typedef Position Distance;
.....................................
  */** Capacity of the buffer. */*
  size_t m_capacity;

  */** Pointer to the ring buffer (unaligned). */*
  std::atomic<Distance> *m_links;

  */** Tail pointer in the buffer (expressed in original unit). */*
  alignas(INNOBASE_CACHE_LINE_SIZE) std::atomic<Position> m_tail;
};

 

构造函数

这里构造函数就是根据传递进来的capacity,创建对应大小的数组(m_links),然后初始化数组的内容.

template <typename Position>
Link_buf<Position>::Link_buf(size_t capacity)
    : m_capacity(capacity), m_tail(0) {
  if (capacity == 0) {
    m_links = nullptr;
    return;
  }

  ut_a((capacity & (capacity - 1)) == 0);

  m_links = UT_NEW_ARRAY_NOKEY(std::atomic<Distance>, capacity);

  for (size_t i = 0; i < capacity; ++i) {
    m_links[i].store(0);
  }
}

  

添加内容

add_link函数主要是用来将将要写入的数据的在lsn中的起始以及结束位置进行保存.流程如下。

  1. 首先根据from计算当前的写入lsn应该在数组的那个位置.
  2. 然后保存写入的大小到当前的slot.
template <typename Position>
inline void Link_buf<Position>::add_link(Position from, Position to) {
  ut_ad(to > from);
  ut_ad(to - from <= std::numeric_limits<Distance>::max());

  const auto index = slot_index(from);

  auto &slot = m_links[index];

  ut_ad(slot.load() == 0);

  slot.store(to - from);
}

slot_index函数就是用来计算slot,计算方式很简单,和数组的大小取模,这里或许有疑问了,如果当前的slot已经被其他的lsn占据了应该怎么办?这里的解决方式就是通过has_space进行判断.

template <typename Position>
inline size_t Link_buf<Position>::slot_index(Position position) const {
  return position & (m_capacity - 1);
}

判断空间

has_space函数就是用来判断对应的position是否已经被占据.

template <typename Position>
inline bool Link_buf<Position>::has_space(Position position) const {
  return tail() + m_capacity > position;
}

advance_tail_until

这个函数用来更新m_tail字段,m_tail字段之前解释过,主要是为了保证它之前的slot都是连续的.

template <typename Position>
template <typename Stop_condition>
bool Link_buf<Position>::advance_tail_until(Stop_condition stop_condition) {
  auto position = m_tail.load();

  while (true) {
    Position next;

    bool stop = next_position(position, next);

    if (stop || stop_condition(position, next)) {
      break;
    }

    */* Reclaim the slot. */*
    claim_position(position);

    position = next;
  }

  if (position > m_tail.load()) {
    m_tail.store(position);

    return true;

  } else {
    return false;
  }
}

而上面的代码可以看到每次都会读取next_position,这个函数用来返回下一个slot是否为0,如果是0则返回true,也就是说已经到达空洞.

template <typename Position>
bool Link_buf<Position>::next_position(Position *position*, Position &*next*) {
  const auto index = slot_index(position);

  auto &slot = m_links[index];

  const auto distance = slot.load();

  ut_ad(position < std::numeric_limits<Position>::max() - distance);

  next = position + distance;

  return distance == 0;
}

PgSQL · 应用案例 · PostgreSQL KPI分解,目标设定之 - 等比数列

$
0
0

背景

https://baike.baidu.com/item/%E7%AD%89%E6%AF%94%E6%95%B0%E5%88%97

什么是等比数列?

根据历史传说记载,国际象棋起源于古印度,至今见诸于文献最早的记录是在萨珊王朝时期用波斯文写的.据说,有位印度教宰相见国王自负虚浮,决定给他一个教训.他向国王推荐了一种在当时尚无人知晓的游戏.国王当时整天被一群溜须拍马的大臣们包围,百无聊赖,很需要通过游戏方式来排遣郁闷的心情.
国王对这种新奇的游戏很快就产生了浓厚的兴趣,高兴之余,他便问那位宰相,作为对他忠心的奖赏,他需要得到什么赏赐.宰相开口说道:请您在棋盘上的第一个格子上放1粒麦子,第二个格子上放2粒,第三个格子上放4粒,第四个格子上放8粒……即每一个次序在后的格子中放的麦粒都必须是前一个格子麦粒数目的两倍,直到最后一个格子第64格放满为止,这样我就十分满足了。“好吧!”国王哈哈大笑,慷慨地答应了宰相的这个谦卑的请求。
这位聪明的宰相到底要求的是多少麦粒呢?稍微算一下就可以得出:1+2+2^2+2^3+2^4+……+2^63=2^64-1,直接写出数字来就是18,446,744,073,709,551,615粒,这位宰相所要求的,竟是全世界在两千年内所产的小麦的总和!

等比数列的应用

等比数列在生活中也是常常运用的。如:银行有一种支付利息的方式——复利。即把前一期的利息和本金加在一起算作本金,在计算下一期的利息,也就是人们通常说的“利滚利”。按照复利计算本利和的公式:本利和=本金*(1+利率)^存期。
随着房价越来越高,很多人没办法像这样一次性将房款付清,总是要向银行借钱,既可以申请公积金也可以申请银行贷款,但是如果还款到一定时间后想了解自己还得还多少本金时,也可以利用数列来自己计算。众所周知,按揭贷款(公积金贷款)中一般实行按月等额还本付息。下面就来寻求这一问题的解决办法。若贷款数额 a0 元,贷款月利率为 p,还款方式每月等额还本付息 a 元,设第 n 月还款后的本金为 an,那么有:a1=a0(1+p)-a;a2=a1(1+p)-a;a3=a2(1+p)-a;……an+1=an(1+p)-a,…. 将其变形,得(an+1-a/p)/(an-a/p)=1+p。由此可见,{an-a/p} 是一个以 a1-a/p 为首项,1+p 为公比的等比数列。
其实类似的还有零存整取、整存整取等银行储蓄借贷,甚至还可以延伸到生物界的细胞分裂。

等比数列的图例:

例如增长比例为1.03,初始值为1,生成100个等比值,并绘图。

postgres=# select id,1*1.03^id from generate_series(1,100) id;  
 id  |      ?column?         
-----+---------------------  
   1 |  1.0300000000000000  
   2 |  1.0609000000000000  
   3 |  1.0927270000000000  
   4 |  1.1255088100000000  
   5 |  1.1592740743000000  
   6 |  1.1940522965290000  
   7 |  1.2298738654248700  
   8 |  1.2667700813876161  
   9 |  1.3047731838292446  
  10 |  1.3439163793441219  
  11 |  1.3842338707244456  
..............  
  79 | 10.3309617052720353  
  80 | 10.6408905564301964  
  81 | 10.9601172731231023  
  82 | 11.2889207913167953  
  83 | 11.6275884150562992  
  84 | 11.9764160675079882  
  85 | 12.3357085495332278  
  86 | 12.7057798060192246  
  87 | 13.0869532001998014  
  88 | 13.4795617962057954  
  89 | 13.8839486500919693  
  90 | 14.3004671095947284  
  91 | 14.7294811228825702  
  92 | 15.1713655565690473  
  93 | 15.6265065232661187  
  94 | 16.0953017189641023  
  95 | 16.5781607705330254  
  96 | 17.0755055936490161  
  97 | 17.5877707614584866  
  98 | 18.1154038843022412  
  99 | 18.6588660008313085  
 100 | 19.2186319808562477  
(100 rows)  

pic

图式为非线性曲线。

在对KPI进行分解时,也可以使用等比数列的方法。

例如当前的用户数有1万个,年底要做到2万个,阶段性的目标如何设定是比较合理的?

又比如一个新产品,从无到有,一年后要做到1000万,每个月应该需要设定多少的目标?

KPI 分解设定举例

1、某产品2019-01-01的用户数有1万个,2019-12-31要做到2万个,每个月目标如何设定是比较合理的?

第一天:10000  
第二天:10000*x  
10000*x*x  
10000*x*x*x  
...  
第一个月的目标: 10000*(x^30)  
第二个月的目标: 10000*(x^60)  
...  
第12个月的目标: 10000*(x^360)  

求X:

10000*(x^360) = 20000  
  
使用科学计算器即可:  
  
x^360 = 2  
  
x = 2 开360方根 = 1.0019272636246980060446500191489  

求每个月的目标:

postgres=# select i, 10000*(1.0019272636246980060446500191489^(30*i)) from generate_series(1,12) i;  
 i  |               ?column?                  
----+---------------------------------------  
  1 | 10594.6309435929526456182529494640000  
  2 | 11224.6204830937298143353304967930000  
  3 | 11892.0711500272106671749997056060000  
  4 | 12599.2104989487316476721060727850000  
  5 | 13348.3985417003436483083188118480000  
  6 | 14142.1356237309504880168872421010000  
  7 | 14983.0707687668149879928073203030000  
  8 | 15874.0105196819947475170563927290000  
  9 | 16817.9283050742908606225095246710000  
 10 | 17817.9743628067860948045241118180000  
 11 | 18877.4862536338699328382631333600000  
 12 | 20000.0000000000000000000000000110000  
(12 rows)  

2、一个新产品,从无到有(从1个细胞开始),一年后要做到1000万,每个月应该需要设定多少的目标?

第一天:1  
第二天:1*x  
1*x*x  
1*x*x*x  
...  
第一个月的目标: 1*(x^30)  
第二个月的目标: 1*(x^60)  
...  
第12个月的目标: 1*(x^360)  

求X:

1*(x^360) = 10000000  
  
使用科学计算器即可:  
  
x^360 = 10000000  
  
x = 10000000 开360方根 = 1.045789903003930609038766911147  

求每个月的目标:

select i, 1*(1.045789903003930609038766911147^(30*i)) from generate_series(1,12) i;  
  
postgres=# select i, 1*(1.045789903003930609038766911147^(30*i)) from generate_series(1,12) i;  
 i  |                ?column?                   
----+-----------------------------------------  
  1 |        3.831186849557287699111983662036  
  2 |       14.677992676220695409205171148171  
  3 |       56.234132519034908039495103977664  
  4 |      215.443469003188372175929356652017  
  5 |      825.404185268018425679628885772418  
  6 |     3162.277660168379331998893544434512  
  7 |    12115.276586285884463586029333237398  
  8 |    46415.888336127788924100763509229562  
  9 |   177827.941003892280122542119519419752  
 10 |   681292.069057961285497988179630667890  
 11 |  2610157.215682536785339580652993869030  
 12 | 10000000.000000000000000000000011341833  
(12 rows)  

显然这个数字是非常不科学的,不可能前半年每个月都低于1万,而从下半年开始就一下子增长那么多,所以问题出在哪里呢?

增长是与大环境有关的,x = 1.045789903003930609038766911147 这个值太大,年初到年底,增长倍数需要是一个合理的区间,例如年初到年底增长10倍,那么第一个月的值就应该是100万。重新计算X:

1000000*(x^11) = 10000000  
  
使用科学计算器即可:  
  
x^11 = 10  
  
x = 10 开11方根 = 1.2328467394420661390534007897309  

求每个月的目标:

第一个月为100万。

postgres=# select i+1, 100000*(1.5199110829529337171040338922572^(i)) from generate_series(1,11) i;  
 ?column? |                ?column?                   
----------+-----------------------------------------  
        2 | 1232846.7394420661390534007897309000000  
        3 | 1519911.0829529337171040338922571000000  
        4 | 1873817.4228603840477603332870082000000  
        5 | 2310129.7000831597589838307886841000000  
        6 | 2848035.8684358016549075949054963000000  
        7 | 3511191.7342151313213486458970854000000  
        8 | 4328761.2810830583474021368863977000000  
        9 | 5336699.2312063096581536941949412000000  
       10 | 6579332.2465756799227076122255576000000  
       11 | 8111308.3078968709132111001153555000000  
       12 | 9999999.9999999999999999999999957000000  
(11 rows)  

严重漏洞:以上解决的是每个周期的增长问题,并非总的计数。

以上算法,最后一个月是1000万,总数实际上是所有月份的累加,已经远超1000万。

而实际上的KPI测算是说一年总共要完成多少,而不是一年后的最后一个月要达到多少。

所以算法要调整,如下函数即可用于测算。

postgres=# create or replace function comp_x (  
  v_start float8,    -- 起始月、或起始测算周期的目标营收  
  v_x float8,     -- 每个月、或每个测算周期相比前一个周期的增长率  
  v_terms int    -- 总共多少个周期,如果是一年,每个周期是一个月,则取值12  
) returns float8 as $$  
declare  
  res float8 := 0;  
begin  
    -- 每个月的结果累加  
    -- 例如,要完成全年1000万目标,第一个月目标为a,每个月的增长率为x,公式如下  
    -- a + ax + ax^2 + ax^3 + ... + ax^11 = 1000万  
  for i in 1..v_terms loop  
    res := res + v_start*(v_x^(i-1));  
    raise notice '%: %', i, v_start*(v_x^(i-1));   
  end loop;  
  return res;  
end;  
$$ language plpgsql strict;  
CREATE FUNCTION  

年底要完成1000万目标,每个月应该完成多少?

1、首先要计算首月(第一个测算周期)的目标

如果保持每个月增长同等比例,那么肯定是越到后面的月份,每个月的营收会越大。因此第一个月一定要低于平均值,所以应该低于83万。

postgres=# select 1000/12;  
 ?column?   
----------  
       83  
(1 row)  

2、假设我们第一个月完成70万,等比数列的X差不多等于1.032,最后总的完成率就是1000万左右

pic

postgres=# select * from comp_x(700000, 1.032, 12);  
NOTICE:  1: 700000  
NOTICE:  2: 722400  
NOTICE:  3: 745516.8  
NOTICE:  4: 769373.3376  
NOTICE:  5: 793993.2844032  
NOTICE:  6: 819401.069504103  
NOTICE:  7: 845621.903728234  
NOTICE:  8: 872681.804647537  
NOTICE:  9: 900607.622396259  
NOTICE:  10: 929427.066312939  
NOTICE:  11: 959168.732434953  
NOTICE:  12: 989862.131872871  
      comp_x        
------------------  
 10048053.7529001  
(1 row)  

微调第二个参数,结果向1000万靠拢即可。

年底要完成1000万目标,每天应该完成多少?

1、首先要计算第一天(第一个测算周期)的目标

postgres=# select 10000000/365;  
 ?column?   
----------  
    27397  
(1 row)  

2、假设我们第一天完成1万,等比数列的X差不多等于1.00165,最后总的完成率就是1000万左右

pic

postgres=# select * from comp_x(10000, 1.0048525, 365);  
NOTICE:  1:     10000  
NOTICE:  2:     10048.525  
NOTICE:  3:     10097.2854675625  
NOTICE:  4:     10146.2825452938  
NOTICE:  5:     10195.5173813449  
NOTICE:  6:     10244.9911294379  
NOTICE:  7:     10294.7049488935  
NOTICE:  8:     10344.660004658  
NOTICE:  9:     10394.8574673306  
NOTICE:  10:     10445.2985131908  
NOTICE:  11:     10495.984324226  
NOTICE:  12:     10546.9160881593  
NOTICE:  13:     10598.0949984771  
NOTICE:  14:     10649.5222544572  
NOTICE:  15:     10701.199061197  
NOTICE:  16:     10753.1266296415  
NOTICE:  17:     10805.3061766118  
NOTICE:  18:     10857.7389248338  
NOTICE:  19:     10910.4261029666  
NOTICE:  20:     10963.3689456312  
NOTICE:  21:     11016.5686934399  
NOTICE:  22:     11070.0265930248  
NOTICE:  23:     11123.7438970674  
NOTICE:  24:     11177.721864328  
NOTICE:  25:     11231.9617596746  
NOTICE:  26:     11286.4648541134  
NOTICE:  27:     11341.232424818  
NOTICE:  28:     11396.2657551594  
NOTICE:  29:     11451.5661347364  
NOTICE:  30:     11507.1348594052  
NOTICE:  31:     11562.9732313104  
NOTICE:  32:     11619.0825589154  
NOTICE:  33:     11675.4641570325  
NOTICE:  34:     11732.1193468545  
NOTICE:  35:     11789.0494559851  
NOTICE:  36:     11846.2558184703  
NOTICE:  37:     11903.7397748294  
NOTICE:  38:     11961.5026720868  
NOTICE:  39:     12019.5458638031  
NOTICE:  40:     12077.8707101072  
NOTICE:  41:     12136.478577728  
NOTICE:  42:     12195.3708400264  
NOTICE:  43:     12254.5488770276  
NOTICE:  44:     12314.0140754534  
NOTICE:  45:     12373.7678287545  
NOTICE:  46:     12433.8115371435  
NOTICE:  47:     12494.1466076275  
NOTICE:  48:     12554.774454041  
NOTICE:  49:     12615.6964970793  
NOTICE:  50:     12676.9141643314  
NOTICE:  51:     12738.4288903138  
NOTICE:  52:     12800.242116504  
NOTICE:  53:     12862.3552913744  
NOTICE:  54:     12924.7698704257  
NOTICE:  55:     12987.487316222  
NOTICE:  56:     13050.509098424  
NOTICE:  57:     13113.8366938241  
..........................................
NOTICE:  352:     54690.6880014031  
NOTICE:  353:     54956.0745649299  
NOTICE:  354:     55222.7489167562  
NOTICE:  355:     55490.7173058748  
NOTICE:  356:     55759.9860116015  
NOTICE:  357:     56030.5613437228  
NOTICE:  358:     56302.4496426432  
NOTICE:  359:     56575.6572795341  
NOTICE:  360:     56850.1906564831  
NOTICE:  361:     57126.0562066436  
NOTICE:  362:     57403.2603943864  
NOTICE:  363:     57681.8097154501  
NOTICE:  364:     57961.7106970944  
NOTICE:  365:     58242.969898252  
     comp_x        
-----------------  
 10000122.392516  
(1 row)  

微调第二个参数,结果向1000万靠拢即可。

小结

1、如果设定的目标为最后一个测算周期,当期的目标,使用等比数列,计算方法如下:

求X:

初始周期值*(x^总期数) = 最后一个周期的当期目标  
  
使用科学计算器即可得到X:  
  
x^总期数 = 最后一个周期的当期目标/初始周期值  

求每个周期当期的目标(例如12个月):

postgres=# select i, 初始周期值*(x^(i)) from generate_series(1,12) i;  

2、如果设定的目标为整个测算周期的总和,那么使用函数,设定初始周期值,输入计算增长比例,得到每个周期的目标值

函数如下

postgres=# create or replace function comp_x (  
  v_start float8,    -- 起始月、或起始测算周期的目标营收  
  v_x float8,     -- 每个月、或每个测算周期相比前一个周期的增长率  
  v_terms int    -- 总共多少个周期,如果是一年,每个周期是一个月,则取值12  
) returns float8 as $$  
declare  
  res float8 := 0;  
begin  
    -- 每个月的结果累加  
    -- 例如,要完成全年1000万目标,第一个月目标为a,每个月的增长率为x,公式如下  
    -- a + ax + ax^2 + ax^3 + ... + ax^11 = 1000万  
  for i in 1..v_terms loop  
    res := res + v_start*(v_x^(i-1));  
    raise notice '%: %', i, v_start*(v_x^(i-1));   
  end loop;  
  return res;  
end;  
$$ language plpgsql strict;  
CREATE FUNCTION  

参考

https://www.postgresql.org/docs/11/functions-math.html

PgSQL · 应用案例 · PostgreSQL KPI 预测例子

$
0
0

背景

预测营收目标,预测KPI风险。使用PostgreSQL二维线性回归,更高级一点可以使用MADLIB多维线性回归。

预测方法:

例如有最近连续4周的营收数据,预测第一季度,第二季度,第三季度,第四季度的营收。

例如设定了财年的营收目标,有最近连续四周的营收完成比率,预测第一季度,第二季度,第三季度,第四季度的营收完成率。

公式:

自变量,第N周的数据  
  
因变量,第N+1周的数据  
  
src = array[第N周数据,第N+1周数据,...,第N+x周数据];  
  
  select   
  regr_slope(y,x),   -- 斜率  
  regr_intercept(y,x)   -- 截距  
  from (  
    select src[xx] x, src[xx+1] y from generate_series(1, array_length(src, 1)-1) xx  
  ) as t;  
  
预测第N+x+1周数据 = 第N+x周数据 * 斜率 + 截距  

例子

假设设定今年要完成100亿的营收目标,有如下连续4周的营收完成情况

时间 | 财年目标 | 完成营收 | 完成比例  
---|---|---|---  
20190422 | 100亿 | xxx | 3.9%   
20190415 | 100亿 | xxx | 2.7%   
20190408 | 100亿 | xxx | 1.6%   
20190401 | 100亿 | xxx | 0.49%   

财年的Q1,Q2,Q3,Q4时间点如下

  d1 date := '20190630';   
  d2 date := '20190930';   
  d3 date := '20191231';   
  d4 date := '20200331';   

预测财年每个Q的完成比例。

do language plpgsql $$  
declare  
  -- 录入连续4周的完成比率,一定要按顺序  
  src float8[] := array[0.49, 1.6, 2.7, 3.9];   
    
  -- 连续四周的最后一周的时间点  
  d0 date := '20190422';   
    
  -- 四个Q的时间节点  
  d1 date := '20190630';   
  d2 date := '20190930';   
  d3 date := '20191231';   
  d4 date := '20200331';   
    
  -- 四个Q离连续四周的最后一周的时间点的间隔周数  
  q1 int := round((d1-d0)/7, 0);   
  q2 int := round((d2-d0)/7, 0);   
  q3 int := round((d3-d0)/7, 0);   
  q4 int := round((d4-d0)/7, 0);   
    
  -- 斜率  
  slope float8;   
  -- 截距  
  intercept float8;   
    
  -- 每一次预测的下一个预测数,因变量数组  
  prev float8[];   
    
  -- 因变量数组的下标,从2开始动态计算  
  i int := 2;   
    
  -- 包含源数据、所有预测数据的大数组,作为每一次预测的源  
  tmp float8[];   
  
begin  
  -- 第一次预测,计算斜率、截距  
  select regr_slope(y,x), regr_intercept(y,x) into slope,intercept from (  
    select src[xx] x, src[xx+1] y from generate_series(1, array_length(src, 1)-1) xx  
  ) as t;  
  
  -- raise notice '%,%', slope, intercept;  
    
  -- 第一个预测到的因变量  
  prev[1] := round((src[array_length(src,1)]*slope + intercept)::numeric, 2);  
  -- raise notice '%,%', prev, src;  
  
loop  
  -- 将预测到的因变量数组追加到原始数组,生成tmp  
  tmp := array_cat(src, prev);  
  -- raise notice '%', tmp;  
  -- 使用tmp,计算截距、斜率  
  select regr_slope(y,x),regr_intercept(y,x) into slope,intercept from (  
    select tmp[xx] x, tmp[xx+1] y from generate_series(1, array_length(tmp, 1)-1) xx  
  ) as t;  
  
  -- 那截距、斜率计算因变量  
  prev[i] := round(((prev[i-1])*slope + intercept)::numeric, 2);  
  -- raise notice '%,%', prev, src;  
    
  -- 遇到关键节点,抛出对应预测数据  
  case i   
    when q1 then raise notice 'q1: %', prev[i];  
    when q2 then raise notice 'q2: %', prev[i];  
    when q3 then raise notice 'q3: %', prev[i];  
    when q4 then raise notice 'q4: %', prev[i];  
  else  
    null;  
  end case;  
    
  -- 到达Q4最后一天的周数,退出循环  
  exit when i=q4;  
    
  -- 周数累加  
  i := i+1;  
end loop;  
    
end;  
$$;  

结果

NOTICE:  q1: 16.93  
NOTICE:  q2: 49.17  
NOTICE:  q3: 100.18  
NOTICE:  q4: 185.56  
DO  

预测数据还不错。

预测数据说明

此预测方法为线性预测,在加速上升期的产品,实际曲线是斜率越来越大的,所以预测期越远的预测数值可能会越低于实际数值。

处于放缓上升速度的上升期的产品,实际曲线的斜率是越来越小的,所以预测期越远的预测数值可能会远大于实际数值。

参考

《PostgreSQL 多元线性回归 - 2 股票预测》

《在PostgreSQL中用线性回归分析(linear regression) - 实现数据预测》

《PostgreSQL 线性回归 - 股价预测 1》

《在PostgreSQL中用线性回归分析linear regression做预测 - 例子2, 预测未来数日某股收盘价》

Viewing all 689 articles
Browse latest View live