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

MySQL · 引擎特性 · InnoDB 同步机制

$
0
0

前言

现代操作系统以及硬件基本都支持并发程序,而在并发程序设计中,各个进程或者线程需要对公共变量的访问加以制约,此外,不同的进程或者线程需要协同工作以完成特征的任务,这就需要一套完善的同步机制,在Linux内核中有相应的技术实现,包括原子操作,信号量,互斥锁,自旋锁,读写锁等。InnoDB考虑到效率和监控两方面的原因,实现了一套独有的同步机制,提供给其他模块调用。本文的分析默认基于MySQL 5.6,CentOS 6,gcc 4.8,其他版本的信息会另行指出。

基础知识

同步机制对于其他数据库模块来说相对独立,但是需要比较多的操作系统以及硬件知识,这里简单介绍一下几个有用的概念,便于读者理解后续概念。

内存模型:主要分为语言级别的内存模型和硬件级别的内存模型。语言级别的内存模型,C/C++属于weak memory model,简单的说就是编译器在进行编译优化的时候,可以对指令进行重排,只需要保证在单线程的环境下,优化前和优化后执行结果一致即可,执行中间过程不保证跟代码的语义顺序一致。所以在多线程的环境下,如果依赖代码中间过程的执行顺序,程序就会出现问题。硬件级别的内存模型,我们常用的cpu,也属于弱内存模型,即cpu在执行指令的时候,为了提升执行效率,也会对某些执行进行乱序执行(按照wiki提供的资料,在x86 64环境下,只会发生读写乱序,即读操作可能会被乱序到写操作之前),如果在编程的时候不做一些措施,同样容易造成错误。

内存屏障:为了解决弱内存模型造成的问题,需要一种能控制指令重排或者乱序执行程序的手段,这种技术就叫做内存屏障,程序员只需要在代码中插入特定的函数,就能控制弱内存模型带来的负面影响,当然,由于影响了乱序和重排这类的优化,对代码的执行效率有一定的影响。具体实现上,内存屏障技术分三种,一种是full memory barrier,即barrier之前的操作不能乱序或重排到barrier之后,同时barrier之后的操作不能乱序或重排到barrier之前,当然这种full barrier对性能影响最大,为了提高效率才有了另外两种:acquire barrier和release barrier,前者只保证barrier后面的操作不能移到之前,后者只保证barrier前面的操作不移到之后。

互斥锁:互斥锁有两层语义,除了大家都知道的排他性(即只允许一个线程同时访问)外,还有一层内存屏障(full memory barrier)的语义,即保证临界区的操作不会被乱序到临界区外。Pthread库里面常用的mutex,conditional variable等操作都自带内存屏障这层语义。此外,使用pthread库,每次调用都需要应用程序从用户态陷入到内核态中查看当前环境,在锁冲突不是很严重的情况下,效率相对比较低。

自旋锁:传统的互斥锁,只要一检测到锁被其他线程所占用了,就立刻放弃cpu时间片,把cpu留给其他线程,这就会产生一次上下文切换。当系统压力大的时候,频繁的上下文切换会导致sys值过高。自旋锁,在检测到锁不可用的时候,首先cpu忙等一小会儿,如果还是发现不可用,再放弃cpu,进行切换。互斥锁消耗cpu sys值,自旋锁消耗cpu usr值。

递归锁:如果在同一个线程中,对同一个互斥锁连续加锁两次,即第一次加锁后,没有释放,继续进行对这个锁进行加锁,那么如果这个互斥锁不是递归锁,将导致死锁。可以把递归锁理解为一种特殊的互斥锁。

死锁:构成死锁有四大条件,其中有一个就是加锁顺序不一致,如果能保证不同类型的锁按照某个特定的顺序加锁,就能大大降低死锁发生的概率,之所以不能完全消除,是因为同一种类型的锁依然可能发生死锁。另外,对同一个锁连续加锁两次,如果是非递归锁,也将导致死锁。

原子操作

现代的cpu提供了对单一变量简单操作的原子指令,即这个变量的这些简单操作只需要一条cpu指令即可完成,这样就不用对这个操作加互斥锁了,在锁冲突不激烈的情况下,减少了用户态和内核态的切换,化悲观锁为乐观锁,从而提高了效率。此外,现在外面很火的所谓无锁编程(类似CAS操作),底层就是用了这些原子操作。gcc为了方便程序员使用这些cpu原子操作,提供了一系列__sync开头的函数,这些函数如果包含内存屏障语义,则同时禁止编译器指令重排和cpu乱序执行。

InnoDB针对不同的操作系统以及编译器环境,自己封装了一套原子操作,在头文件os0sync.h中。下面的操作基于Linux x86 64位环境, gcc 4.1以上的版本进行分析。

os_compare_and_swap_xxx(ptr, old_val, new_val)类型的操作底层都使用了gcc包装的__sync_bool_compare_and_swap(ptr, old_val, new_val)函数,语义为,交换成功则返回true,ptr是交换后的值,old_val是之前的值,new_val是交换后的预期值。这个原子操作是个内存屏障(full memory barrier)。

os_atomic_increment_xxx类型的操作底层使用了函数__sync_add_and_fetchos_atomic_decrement_xxx类型的操作使用了函数__sync_sub_and_fetch,分别表示原子递增和原子递减。这个两个原子操作也都是内存屏障(full memory barrier)。

另外一个比较重要的原子操作是os_atomic_test_and_set_byte(ptr, new_val),这个操作使用了__sync_lock_test_and_set(ptr, new_val)这个函数,语义为,把ptr设置为new_val,同时返回旧的值。这个操作提供了原子改变某个变量值的操作,InnoDB锁实现的同步机制中,大量的用了这个操作,因此比较重要。需要注意的是,参看gcc文档,这个操作不是full memory barrier,只是一个acquire barrier,简单的说就是,代码中__sync_lock_test_and_set之后操作不能被乱序或者重排到__sync_lock_test_and_set之前,但是__sync_lock_test_and_set之前的操作可能被重排到其之后。

关于内存屏障的专门指令,MySQL 5.7提供的比较完善。os_rmb表示acquire barrier,os_wmb表示release barrier。如果在编程时,需要在某个位置准确的读取一个变量的值时,记得在读取之前加上os_rmb,同理,如果需要在某个位置保证一个变量已经被写了,记得在写之后调用os_wmb。

条件通知机制

条件通知机制在多线程协作中非常有用,一个线程往往需要等待其他线程完成指定工作后,再进行工作,这个时候就需要有线程等待和线程通知机制。Pthread_cond_XXX类似的变量和函数来完成等待和通知的工作。InnoDB中,对Pthread库进行了简单的封装,并在此基础上,进一步抽象,提供了一套方便易用的接口函数给调用者使用。

系统条件变量

在文件os0sync.cc中,os_cond_XXX类似的函数就是InnoDB对Pthread库的封装。常用的几个函数如:
os_cond_t是核心的操作对象,其实就是pthread_cond_t 的一层typedef 而已,os_cond_init初始化函数,os_cond_destroy销毁函数,os_cond_wait条件等待,不会超时,os_cond_wait_timed条件等待,如果超时则返回,os_cond_broadcast唤醒所有等待线程,os_cond_signal只唤醒其中一个等待线程,但是在阅读源码的时候发现,似乎没有什么地方调用了os_cond_signal……

此外,还有一个os_cond_module_init函数,用来window下的初始化操作。
在InnoDB 下,os_cond_XXX模块的函数主要是给InnoDB自己设计的条件变量使用。

InnoDB条件变量

如果在InnoDB层直接使用系统条件变量的话,主要有四个弊端,首先,弊端1,系统条件变量的使用需要与一个系统互斥锁(详见下一节)相配合使用,使用完还要记得及时释放,使用者会比较麻烦。接着,弊端2,在条件等待的时候,需要在一个循环中等待,使用者还是比较麻烦。最后,弊端3,也是比较重要的,不方便系统监控。

基于以上几点,InnoDB基于系统的条件变量和系统互斥锁自己实现了一套条件通知机制。主要在文件os0sync.cc中实现,相关数据结构以及接口进一层的包装在头文件os0sync.h中。使用方法如下:

InnoDB条件变量核心数据结构为os_event_t,类似pthread_cont_t。如果需要创建和销毁则分别使用os_event_createos_event_free函数。需要等待某个条件变量,先调用os_event_reset(原因见下一段),然后使用os_event_wait,如果需要超时等待,使用os_event_wait_time替换os_event_wait即可,os_event_wait_XXX这两个函数,解决了弊端1和弊端2,此外,建议把os_event_reset返回值传给他们,这样能防止多线程情况下的无限等待(详见下下段)。如果需要发出一个条件通知,使用os_event_set。这个几个函数,里面都插入了一些监控信息,方便InnoDB上层管理。怎么样,方便多了吧~

多线程环境下可能发生的问题

首先来说说两个线程下会发生的问题。创建后,正常的使用顺序是这样的,线程A首先os_event_reset(步骤1),然后os_event_wait(步骤2),接着线程B做完该做的事情后,执行os_event_set(步骤3)发送信号,通知线程A停止等待,但是在多线程的环境中,会出现以下两种步骤顺序错乱的情况:乱序A: 步骤1--步骤3--步骤2,乱序B: 步骤3--步骤1--步骤2。对于乱序B,属于条件通知在条件等待之前发生,目前InnoDB条件变量的机制下,会发生无限等待,所以上层调用的时候一定要注意,例如在InnoDB在实现互斥锁和读写锁的时候为了防止发生条件通知在条件等待之前发生,在等待之前对lock_word 再次进行了判断,详见InnoDB自旋互斥锁这一节。为了解决乱序A,InnoDB在核心数据结构os_event 中引入布尔型变量is_set,is_set 这个变量就表示是否已经发生过条件通知,在每次调用条件通知之前,会把这个变量设置为true(在os_event_reset时改为false,便于多次通知),在条件等待之前会检查一下这变量,如果这个变量为true,就不再等待了。所以,乱序A也能保证不会发生无限等待。

接着我们来说说大于两个线程下可能会发生的问题。线程A和C是等待线程,等待同一个条件变量,B是通知线程,通知A和C结束等待。考虑一个乱序C:线程A执行os_event_reset(步骤1),线程B马上就执行os_event_set(步骤2)了,接着线程C执行了os_event_reset(步骤3),最后线程A执行os_event_wait(步骤4),线程C执行os_event_wait(步骤5)。乍一眼看,好像看不出啥问题,但是实际上你会发现A和C线程在无限等待了。原因是,步骤2,把is_set这个变量设置为false,但是在步骤3,线程C通过reset又把它给重新设回false了…… 然后线程A和C在os_event_wait中误以为还没有发生过条件通知,就开始无限等待了。为了解决这个问题,InnoDB在核心数据结构os_event中引入64位整形变量signal_count,用来记录已经发出条件信号的次数。每次发出一个条件通知,这个变量就递增1。os_event_reset的返回值就把当前的signal_count 值取出来。os_event_wait如果发现有这个参数的传入,就会判断传入的参数与当前的signal_count 值是否相同,如果不相同,表示这个已经通知过了,就不会进入等待了。举个例子,假设乱序C,一开始的signal_count 为100,步骤1把这个参数传给了步骤4,在步骤4中,os_event_wait会发现传入值100与当前的值101(步骤2中递增了1)不同,所以线程A认为信号已经发生过了,就不会再等待了。然而,线程C呢?步骤3返回的值应该是101,传给步骤5后,发生于当前值一样,继续等待。仔细分析可以发现,线程C是属于条件变量通知发生在等待之前(步骤2,步骤3,步骤5),上一段已经说过了,针对这种通知提前发出的,目前InnoDB没有非常好的解法,只能调用者自己控制。

总结一下, InnoDB条件变量能方便InnoDB上层做监控,也简化了条件变量使用的方法,但是调用者上层逻辑必须保证条件通知不能过早的发出,否则就会有无限等待的可能。

互斥锁

互斥锁保证一段程序同时只能一个线程访问,保证临界区得到正确的序列化访问。同条件变量一样,InnoDB 对Pthread 的mutex 简单包装了一下,提供给其他模块用(主要是辅助其他自己实现的数据结构,不用InnoDB 自己的互斥锁是为了防止递归引用,详见辅助结构这一节)。但与条件变量不同的是,InnoDB 自己实现的一套互斥锁并没有依赖Pthread 库,而是依赖上述的原子操作(如果平台不支持原子操作则使用Pthread 库,但是这种情况不太会发生,因为gcc在4.1就支持原子操作了)和上述的InnoDB 条件变量。

系统互斥锁

相比与系统条件变量,系统互斥锁除了包装Pthread库外,还做了一层简单的监控统计,结构名为os_mutex_t。在文件os0sync.cc中,os_mutex_create创建mutex,并调用os_fast_mutex_init_func创建pthread的mutex,值得一提的是,创建pthread mutex的参数是my_fast_mutexattr的东西,其在MySQL server层函数my_thread_global_init初始化 ,只要pthread库支持,则默认成初始化为PTHREAD_MUTEX_ADAPTIVE_NP和PTHREAD_MUTEX_ERRORCHECK。前者表示,当锁释放,之前在等待的锁进行公平的竞争,而不是按照默认的优先级模式。后者表示,如果发生了递归的加锁,即同一个线程对同一个锁连续加锁两次,第二次加锁会报错。另外三个有用的函数为,销毁锁os_mutex_free,加锁os_mutex_enter,解锁os_mutex_exit

一般来说,InnoDB 上层模块不需要直接与系统互斥锁打交道,需要用锁的时候一般用InnoDB 自己实现的一套互斥锁。系统互斥锁主要是用来辅助实现一些数据结构,例如最后一节提到的一些辅助结构,由于这些辅助结构可能本身就要提供给InnoDB 自旋互斥锁用,为了防止递归引用,就暂时用系统互斥锁来代替。

InnoDB 自旋互斥锁

为什么InnoDB 需要实现自己的一套互斥锁,不直接用上述的系统互斥锁呢?这个主要有以下几个原因,首先,系统互斥锁是基于pthread mutex 的,Heikki Tuuri(同步模块的作者,也是Innobase的创始人)认为在当时的年代pthread mutex 上下文切换造成的cpu开销太大,使用spin lock的方式在多处理器的机器上更加有效,尤其是在锁竞争不是很严重的时候,Heikki Tuuri还总结出,在spin lock大概自旋20微秒的时候在多处理的机器下效率最高。其次,不使用pthread spin lock 的原因是,当时在1995年左右的时候,spin lock的类似实现,效率很低,而且当时的spin lock不支持自定义自旋时间,要知道自旋锁在单处理器的机器上没什么卵用。最后,也是为了更加完善的监控需求。总的来说,有历史原因,有监控需求也有自定义自旋时间的需求,然后就有了这一套InnoDB 自旋互斥锁。

InnoDB 自旋互斥锁的实现主要在文件sync0sync.cc 和sync0sync.ic 中,头文件sync0sync.h 定义了核心数据结构ib_mutex_t。使用方法很简单,mutex_create创建锁,mutex_free释放锁,mutex_enter尝试获得锁,如果已经被占用了,则等待。mutex_exit释放锁,同时唤醒所有等待的线程,拿到锁的线程开始执行,其余线程继续等待。mutex_enter_nowait这个函数类似pthread 的trylock,只要已检测到锁不用,就直接返回错误,不进行自旋等待。总体来说,InnoDB 自旋互斥锁的用法和语义跟系统互斥锁一模一样,但是底层实现却大相径庭。

在ib_mutex_t 这个核心数据结构中,最重要的是前面两个变量:event 和lock_word。lock_word 为0表示锁空闲,1表示锁被占用,InnoDB 自旋互斥锁使用__sync_lock_test_and_set这个函数对lock_word 进行原子操作,加锁的时候,尝试把其设置为1,函数返回值不指示是否成功,指示的是尝试设置之前的值,因此如果返回值是0,表示加锁成功,返回是1表示失败。如果加锁失败,则会自旋一段时间,然后等待在条件变量event(os_event_wait)上,当锁占用者释放锁的时候,会使用os_event_set来唤醒所有的等待者。简单的来说,byte 类型的lock_word 基于平台提供的原子操作来实现互斥访问,而event 是InnoDB 条件变量类型,用来实现锁释放后唤醒等待线程的操作。

接下来,详细介绍一下,mutex_entermutex_exit的逻辑,InnoDB自旋互斥锁的精华都在这两个函数中。
mutex_enter 的伪代码如下:

if (__sync_lock_test_and_set(mutex->lock_word, 1) == 0) {
    get mutex successfully;
    return;
}
loop1:
    i = 0;
loop2:
    /*指示点1*/
    while (mutex->lock_word ! = 0 && i < SPIN_ROUNDS) {
             random spin using ut_delay, spin max time depend on SPIN_WAIT_DELAY;
             i++;
}
if (i == SPIN_ROUNDS) {
    yield_cpu;
}
/*指示点2*/
if (__sync_lock_test_and_set(mutex->lock_word, 1) == 0) {
    get mutex successfully;
    return;
}
if (i < SPIN_ROUNDS) {
     goto loop2
}
/*指示点4*/
get cell from sync array and call os_event_reset(mutex->event);
mutex->waiter =1;
/*指示点3*/
for (i = 0; i < 4; i++) {
    if (__sync_lock_test_and_set(mutex->lock_word, 1) == 0) {
        get mutex successfully;
        free cell;
        return;
    }
}
sync array wait and os_event_wait(mutex->event);
goto loop1;

代码还是有点小复杂的。这里分析几点如下:

1. SPIN_ROUNDS控制了在放弃cpu时间片(yield_cpu)之前,一共进行多少次忙等,这个参数就是对外可配置的innodb_sync_spin_loops,而SPIN_WAIT_DELAY控制了每次忙等的时间,这个参数也就是对外可配置的innodb_spin_wait_delay。这两个参数一起决定了自旋的时间。Heikki Tuuri建议在单处理器的机器上调小spin的时间,在对称多处理器的机器上,可以适当调大。比较有意思的是innodb_spin_wait_delay 的单位,这个是100MHZ的奔腾处理器处理1毫秒的时间,默认innodb_spin_wait_delay 配置成6,表示最多在100MHZ的奔腾处理器上自旋6毫秒。由于现在cpu都是按照GHZ来计算的,所以按照默认配置自旋时间往往很短。此外,自旋不真是cpu傻傻的在那边100%的跑,在现代的cpu上,给自旋专门提供了一条指令,在笔者的测试环境下,这条指令是pause,查看Intel的文档,其对pause的解释是:不会发生用户态和内核态的切换,cpu在用户态自旋,因此不会发生上下文切换,同时这条指令不会消耗太多的能耗…… 所以那些说spin lock太浪费电的不攻自破了。另外,编译器也不会把ut_delay 给优化掉,因为其里面估计修改了一个全局变量。

2. yield_cpu 操作在笔者的环境中,就是调用了pthread_yield 函数,这个函数把放弃当前cpu 的时间片,然后把当前线程放到cpu可执行队列的末尾。

3. 在指示点1后面的循环,没有采用原子操作读取数据,是因为,Heikki Tuuri认为由于原子操作在内存和cpu cache 之间会产生过的数据交换,如果只是读本地的cache,可以减少总线的争用。即使本地读到脏的数据,也没关系,因为在跳出循环的指示点2,依然会再一次使用原子操作进行校验。

4. get cell 这个操作是从sync array 执行的,sync array 详见辅助数据结构这一节,简单的说就是提供给监控线程使用的。

5. 注意一下,os_event_resetos_event_wait这两个函数的调用位置,另外,有一点必须清楚,就是os_event_set(锁持有者释放所后会调用这个函数通知所有等待者)可能在这整段代码执行到任意位置出现,有可能出现在指示点4的位置,这样就构成了条件变量通知在条件变量等待之前,会造成无限等待。为了解决这个问题,才有了指示点3下面的代码,需要重新再次检测一下lock_word,另外,即使os_event_set发生在os_event_reset之后,有了这些代码,也能让当前线程提前拿到锁,不用执行后续os_event_wait的代码,一定程度上提高了效率。

mutex_exit 的伪代码就简单多了,如下:

__sync_lock_test_and_set(mutex->lock_word, 0);

/* A problem: we assume that mutex_reset_lock word
        is a memory barrier, that is when we read the waiters
        field next, the read must be serialized in memory
        after the reset. A speculative processor might
        perform the read first, which could leave a waiting
        thread hanging indefinitely.
Our current solution call every second
        sync_arr_wake_threads_if_sema_free()
        to wake up possible hanging threads if
        they are missed in mutex_signal_object. */

if (mutex->waiter != 0) {
     mutex->waiter = 0;
     os_event_set(mutex->event);
}

1. waiter 是ib_mutex_t 中的一个变量,用来表示当前是否有线程在等待这个锁。整个代码逻辑很简单,就是先把lock_word 设置为0,然后如果发现有等待者,就把所有等待者给唤醒。facebook的mark callaghan在2014年测试过,相比现在已经比较完善的pthread库,InnoDB自旋互斥锁只在并发量相对较低(小于256线程)和锁等待时间比较短的情况下有优势,在高并发且较长的锁等待时间情况下,退化比较严重,其中一个很重要的原因就是InnoDB自旋互斥锁在锁释放的时候需要唤醒所有等待者。由于os_event_ret底层通过pthread_cond_boardcast 来通知所有的等待者,一种改进是把pthread_cond_boardcast 改成pthread_cond_signal,即只唤醒一个线程,但Inaam Rana Mark测试后发现,如果只唤醒一个线程的话,在高并发的情况下,这个线程可能不会立刻被cpu调度到。由此看来,似乎唤醒一个特定数量的等待者是一个比较好的选择。

2. 伪代码中的这段注释笔者估计加上去的,大意是由于编译器或者cpu的指令重排乱序执行,mutex->waiter这个变量的读取可能在发生在原子操作之前,从而导致一些无限等待的问题。然后还专门开了一个叫做sync_arr_wake_threads_if_sema_free的函数来做清理。这个函数是在后台线程srv_error_monitor_thread中做的,每隔1秒钟执行一次。在现代的cpu和编译器上,完全可以用内存屏障的技术来防止指令重排和乱序执行,这个函数可以被去掉,官方的意见貌似是,不要这么激进,万一其他地方还需要这个函数呢。详见BUG #79477。

总体来说,InnoDB 自旋互斥锁的底层实现还是比较有意思的,非常适合学习研究。这套锁机制在现在完善的Pthread 库和高达4GMHZ的cpu下,已经有点力不从心了,mark callaghan研究发现,在高负载的压力下,使用这套锁机制的InnoDB,大部分cpu时间都给了sys 和usr,基本没有空闲,而pthread mutex 在相同情况下,却有平均80%的空闲。同时,由于ib_mutex_t 这个结构体体积比较庞大,当buffer pool 比较大的时候,会发现锁占用了很多的内存。最后,从代码风格上来说,有不少代码没有解耦,如果需要把锁模块单独打成一个函数库,比较困难。

基于上述几个缺陷,MySQL 5.7及后续的版本中,对互斥锁进行了大量的重新,包括以下几点(WL#6044):

  1. 使用了C++中的类继承关系,系统互斥锁和InnoDB 自己实现的自旋互斥锁都是一个父类的子类。
  2. 由于bool pool的锁对性能要求比较高,因此使用静态继承(也就是模板)的方式来减少继承中虚指针造成的开销。
  3. 保留旧的InnoDB 自旋互斥锁,并实现了一种基于futex的锁。简单的说,futex锁与上述的原子操作类似,能减少用户态和内核态切换的开销,但同时保留类似mutex的使用方法,大大降低了程序编写的难度。

InnoDB 读写锁

与条件变量、互斥锁不同,InnoDB 里面没有Pthread 库的读写锁的包装,其完全依赖依赖于原子操作和InnoDB 的条件变量,甚至都不需要依赖InnoDB 的自旋互斥锁。此外,读写锁还实现了写操作的递归锁,即同一个线程可以多次获得写锁,但是同一个线程依然不能同时获得读锁和写锁。InnoDB 读写锁的核心数据结构rw_lock_t中,并没有等待队列的信息,因此不能保证先到的请求一定会先进入临界区。这与系统互斥量用PTHREAD_MUTEX_ADAPTIVE_NP来初始化有异曲同工之妙。

InnoDB 读写锁的核心实现在源文件sync0rw.cc 和sync0rw.ic 中,核心数据结构rw_lock_t 定义在sync0rw.h 中。使用方法与InnoDB 自旋互斥锁很类似,只不过读请求和写请求要调用不同的函数。加读锁调用rw_lock_s_lock, 加写锁调用rw_lock_x_lock,释放读锁调用rw_lock_s_unlock, 释放写锁调用rw_lock_x_unlock,创建读写锁调用rw_lock_create,释放读写锁调用rw_lock_free。函数rw_lock_x_lock_nowaitrw_lock_s_lock_nowait表示,当加读写锁失败的时候,直接返回,而不是自旋等待。

核心机制

rw_lock_t 中,核心的成员有以下几个:lock_word, event, waiters, wait_ex_event,writer_thread, recursive。

与InnoDB 自旋互斥锁的lock_word 不同,rw_lock_t 中的lock_word 是int 型,注意不是unsigned 的,其取值范围是(-2*X_LOCK_DECR, X_LOCK_DECR],其中X_LOCK_DECR为0x00100000,差不多100多W的一个数。在InnoDB 自旋互斥锁互斥锁中,lock_word 的取值范围只有0,1,因为这两个状态就能把互斥锁的所有状态都表示出来了,也就是说,只需要查看一下这个lock_word 就能确定当前的线程是否能获得锁。rw_lock_t 中的lock_word 也扮演了相同的角色,只需要查看一下当前的lock_word 落在哪个取值范围中,就确定当前线程能否获得锁。至于rw_lock_t 中的lock_word 是如何做到这一点的,这其实是InnoDB 读写锁乃至InnoDB 同步机制中最神奇的地方,下文我们会详细分析。

event 是一个InnoDB 条件变量,当当前的锁已经被一个线程以写锁方式独占时,后续的读锁和写锁都等待在这个event 上,当这个线程释放写锁时,等待在这个event 上的所有读锁和写锁同时竞争。waiters 这变量,与event 一起用,当有等待者在等待时,这个变量被设置为1,否则为0,锁被释放的时候,需要通过这个变量来判断有没有等待者从而执行os_event_set

与InnoDB 自旋互斥锁不同,InnoDB 读写锁还有wait_ex_event 和recursive 两个变量。wait_ex_event 也是一个InnoDB 条件变量,但是它用来等待第一个写锁(因为写请求可能会被先前的读请求堵住),当先前到达的读请求都读完了,就会通过这个event 来唤醒这个写锁的请求。

由于InnoDB 读写锁实现了写锁的递归,因此需要保存当前写锁被哪个线程占用了,后续可以通过这个值来判断是否是这个线程的写锁请求,如果是则加锁成功,否则失败,需要等待。线程的id就保存在writer_thread 这个变量中。

recursive 是个bool 变量,用来表示当前的读写锁是否支持递归写模式,在某些情况下,例如需要另外一个线程来释放这个读写锁(insert buffer需要这个功能)的时候,就不要开启递归模式了。

接下来,我们来详细介绍一下lock_word 的变化规则:

  1. 当有一个读请求加锁成功时,lock_word 原子递减1。
  2. 当有一个写请求加锁成功时,lock_word 原子递减X_LOCK_DECR。
  3. 如果读写锁支持递归写,那么第一个递归写锁加锁成功时,lock_word 依然原子递减X_LOCK_DECR,而后续的递归写锁加锁成功是,lock_word 只是原子递减1。

在上述的变化规则约束下,lock_word 会形成以下几个区间:

lock_word == X_LOCK_DECR表示锁空闲,即当前没有线程获得了这个锁
0 < lock_word < X_LOCK_DECR表示当前有X_LOCK_DECR - lock_word个读锁
lock_word == 0表示当前有一个写锁
-X_LOCK_DECR < lock_word < 0表示当前有-lock_word个读锁,他们还没完成,同时后面还有一个写锁在等待
lock_word <= -X_LOCK_DECR表示当前处于递归锁模式,同一个线程加了2 - (lock_word + X_LOCK_DECR)次写锁

另外,还可以得出以下结论

  1. 由于lock_word 的范围被限制(rw_lock_validate)在(-2*X_LOCK_DECR, X_LOCK_DECR]中,结合上述规则,可以推断出,一个读写锁最多能加X_LOCK_DECR个读锁。在开启递归写锁的模式下,一个线程最多同时加X_LOCK_DECR+1个写锁。
  2. 在读锁释放之前,lock_word 一定处于(-X_LOCK_DECR, 0)U(0, X_LOCK_DECR)这个范围内。
  3. 在写锁释放之前,lock_word 一定处于(-2*X_LOCK_DECR, -X_LOCK_DECR]或者等于0这个范围内。
  4. 只有在lock_word 大于0的情况下才可以对它递减。有一个例外,就是同一个线程需要加递归写锁的时候,lock_word 可以在小于0的情况下递减。

接下来,举个读写锁加锁的例子,方便读者理解读写锁底层加锁的原理。
假设有读写加锁请求按照以下顺序依次到达:R1->R2->W1->R3->W2->W3->R4,其中W2和W3是属于同一个线程的写加锁请求,其他所有读写请求均来自不同线程。初始化后,lock_word 的值为X_LOCK_DECR(十进制值为1048576)。R1读加锁请求首先到,其发现lock_word 大于0,表示可以加读锁,同时lock_word 递减1,结果为1048575,R2读加锁请求接着来到,发现lock_word 依然大于0,继续加读锁并递减lock_word,最终结果为1048574。注意,如果R1和R2几乎是同时到达,即使时序上是R1先请求,但是并不保证R1首先递减,有可能是R2首先拿到原子操作的执行权限。如果在R1或者R2释放锁之前,写加锁请求W1到来,他发现lock_word 依旧大于0,于是递减X_LOCK_DECR,并把自己的线程id记录在writer_thread这个变量里,再检查lock_word 的值(此时为-2),由于结果小于0,表示前面有未完成的读加锁请求,于是其等待在wait_ex_event这个条件变量上。后续的R3, W2, W3, R4请求发现lock_word 小于0,则都等待在条件变量event上,并且设置waiter为1,表示有等待者。假设R1先释放读锁(lock_word 递增1),R2后释放(lock_word 再次递增1)。R2释放后,由于lock_word 变为0了,其会在wait_ex_event上调用os_event_set,这样W3就被唤醒了,他可以执行临界区内的代码了。W3执行完后,lock_word 被恢复为X_LOCK_DECR,然后其发现waiter为1,表示在其后面有新的读写加锁请求在等待,然后在event上调用os_event_set,这样R3, W2, W3, R4同时被唤醒,进行原子操作执行权限争抢(可以简单的理解为谁先得到cpu调度)。假设W2首先抢到了执行权限,其会把lock_word 再次递减为0并自己的线程id记录在writer_thread这个变量里,当检查lock_word 的时候,发现值为0,表示前面没有读请求了,于是其就进入临界区执行代码了。假设此时,W3得到了cpu的调度,由于lock_word 只有在大于0的情况下才能递减,所以其递减lock_word 失败,但是其通过对比writer_thread和自己的线程id,发现前面的写锁是自己加的,如果这个时候开启了递归写锁,即recursive值为true,他把lock_word 再次递减X_LOCK_DECR(现在lock_word 变为-X_LOCK_DECR了),然后进入临界区执行代码。这样就保证了同一个线程多次加写锁也不发生死锁,也就是递归锁的概念。后续的R3和R4发现lock_word 小于等于0,就直接等待在event条件变量上,并设置waiter为1。直到W2和W3都释放写锁,lock_word 又变为X_LOCK_DECR,最后一个释放的,检查waiter变量发现非0,就会唤醒event上的所有等待者,于是R3和R4就可以执行了。

读写锁的核心函数函数结构跟InnoDB自旋互斥锁的基本相同,主要的区别就是用rw_lock_x_lock_lowrw_lock_s_lock_low替换了__sync_lock_test_and_set原子操作。rw_lock_x_lock_lowrw_lock_s_lock_low就按照上述的lock_word 的变化规则来原子的改变(依然使用了__sync_lock_test_and_set)lock_word 这个变量。

在MySQL 5.7中,读写锁除了可以加读锁(Share lock)请求和加写锁(exclusive lock)请求外,还可以加share exclusive 锁请求,锁兼容性如下:

 LOCK COMPATIBILITY MATRIX
    S   SX  X
 S  +   +   -
 SX +   -   -
 X  -   -   -

按照WL#6363的说法,是为了修复index->lock 这个锁的冲突。

辅助结构

InnoDB 同步机制中,还有很多使用的辅助结构,他们的作用主要是为了监控方便和死锁的预防和检测。这里主要介绍sync array, sync thread level array 和srv_error_monitor_thread。

sync array 主要的数据结构是sync_array_t,可以把他理解为一个数据,数组中的元素为sync_cell_t。当一个锁(InnoDB 自旋互斥锁或者InnoDB 读写锁,下同)需要发生os_event_wait等待时,就需要在sync array 中申请一个sync_cell_t 来保存当前的信息,这些信息包括等待锁的指针(便于死锁检测),在哪一个文件以及哪一行发生了等待(也就是mutex_enter, rw_lock_s_lock 或者rw_lock_x_lock 被调用的地方,只在debug 模式下有效),发生等待的线程(便于死锁检测)以及等待开始的时间(便于统计等待的时间)。当锁释放的时候,就把相关联的sync_cell_t 重置为空,方便复用。sync_cell_t 在sync_array_t 中的个数,是在初始化同步模块时候就指定的,其个数一般为OS_THREAD_MAX_N,而OS_THREAD_MAX_N 是在InnoDB 初始化的时候被计算,其包括了系统后台开启的所有线程,以及max_connection 指定的个数,还预留了一些。由于一个线程在某一个时刻最多只能发生一个锁等待,所以不用担心sync_cell_t不够用。从上面也可以看出,在每个锁进行等待和释放的时候,都需要对sync array操作,因此在高并发的情况下,单一的sync array 可能成为瓶颈,在MySQL 5.6中,引入了多sync array, 个数可以通过innodb_sync_array_size 进行控制,这个值默认为1,在高并发的情况下,建议调高。

InnoDB 作为一个成熟的存储引擎,包含了完善的死锁预防机制和死锁检测机制。在每次需要锁等待时,即调用os_event_wait之前,需要启动死锁检测机制来保证不会出现死锁,从而造成无限等待。在每次加锁成功(lock_word 递减后,函数返回之前)时,都会启动死锁预防机制,降低死锁出现的概率。当然,由于死锁预防机制和死锁检测机制需要扫描比较多的数据,算法上也有递归操作,所以只在debug 模式下开启。

死锁检测机制主要依赖sync array 中保存的信息以及死锁检测算法来实现。死锁检测机制通过sync_cell_t 保存的等待锁指针和发生等待的线程以及教科书上的有向图环路检测算法来实现,具体实现在sync_array_deadlock_stepsync_array_detect_deadlock中实现,仔细研究后发现个小问题,由于sync_array_find_thread函数仅仅在当前的sync array 中遍历,当有多个sync array 时(innodb_sync_array_size > 1),如果死锁发生在不同的sync array 上,现有的死锁检测算法将无法发现这个死锁。

死锁预防机制是由sync thread level array 和全局锁优先级共同保证的。InnoDB 为了降低死锁发生的概率,上层的每种类型的锁都有一个优先级。例如回滚段锁的优先级就比文件系统page 页的优先级高,虽然两者底层都是InnoDB 互斥锁或者InnoDB 读写锁。有了这个优先级,InnoDB 规定,每个锁创建是必须制定一个优先级,同一个线程的加锁顺序必须从优先级高到低,即如果一个线程目前已经加了一个低优先级的锁A,在释放锁A 之前,不能再请求优先级比锁A 高(或者相同)的锁。形成死锁需要四个必要条件,其中一个就是不同的加锁顺序,InnoDB 通过锁优先级来降低死锁发生的概率,但是不能完全消除。原因是可以把锁设置为SYNC_NO_ORDER_CHECK 这个优先级,这是最高的优先级,表示不进行死锁预防检查,如果上层的程序员把自己创建的锁都设置为这个优先级,那么InnoDB 提供的这套机制将完全失效,所以要养成给锁设定优先级的好习惯。sync thread level array 是一个数组,每个线程单独一个,在同步模块初始化时分配了OS_THREAD_MAX_N 个,所以不用担心不够用。这个数组中记录了某个线程当前锁拥有的所有锁,当新加了一个锁B时,需要扫描一遍这个数组,从而确定目前线程所持有的锁的优先级都比锁B 高。

最后,我们来讲讲srv_error_monitor_thread 这个线程。这是一个后台线程,在InnoDB 启动的时候启动,每隔1秒钟执行一下指定的操作。跟同步模块相关的操作有两点,去除无限等待的锁和报告长时间等待的异常锁。

去除无限等待的锁,如上文所述,就是sync_arr_wake_threads_if_sema_free 这个函数。这个函数通过遍历sync array,如果发现锁已经可用(sync_arr_cell_can_wake_up),但是依然有等待者,则直接调用os_event_set把他们唤醒。这个函数是为了解决由于cpu乱序执行或者编译器指令重排导致锁无限等待的问题,但是可以通过内存屏障技术来避免,所以可以去掉。

报告长时间等待的异常锁,通过sync_cell_t 里面记录的锁开始等待时间,我们可以很方便的统计锁等待发生的时间。在目前的实现中,当锁等待超过240秒的时候,就会在错误日志中看到信息。如果同一个锁被检测到等到超过600秒且连续10次被检测到,则InnoDB 会通过assert 来自杀。相信当做运维DBA的同学一定看到过如下的报错:

InnoDB: Warning: a long semaphore wait:
--Thread 139774244570880 has waited at log0read.h line 765 for 241.00 seconds the semaphore:
Mutex at 0x30c75ca0 created file log0read.h line 522, lock var 1
Last time reserved in file /home/yuhui.wyh/mysql/storage/innobase/include/log0read.h line 765, waiters flag 1
InnoDB: ###### Starts InnoDB Monitor for 30 secs to print diagnostic info:
InnoDB: Pending preads 0, pwrites 0

一般出现这种错误都是pread 或者pwrite 长时间不返回,导致锁超时。至于pread 或者pwrite 长时间不返回的root cause 常常是有很多的读写请求在极短的时间内到达导致磁盘扛不住或者磁盘已经坏了。

总结

本文详细介绍了原子操作,条件变量,互斥锁以及读写锁在InnoDB 引擎中的实现。原子操作由于其能减少不必要的用户态和内核态的切换以及更精简的cpu 指令被广泛的应用到InnoDB 自旋互斥锁和InnoDB 读写锁中。InnoDB 条件变量使用更加方便,但是一定要注意条件通知必须在条件等待之后,否则会有无限等待发生。InnoDB 自旋互斥锁加锁和解锁过程虽然复杂但是都是必须的操作。InnoDB 读写锁神奇的lock_word 控制方法给我们留下了深刻影响。正因为InnoDB 底层同步机制的稳定、高效,MySQL 在我们的服务器上才能运行的如此稳定。


MySQL · myrocks · myrocks index condition pushdown

$
0
0

index condition pushdown

Index condition pushdown(ICP)是直到mysql5.6才引入的特性,主要是为了减少通过二级索引查找主键索引的次数。目前ICP相关的文章也比较多,本文主要从源码角度介绍ICP的实现。讨论之前,我们先再温习下。

以下图片来自mariadb

  • 引入ICP之前
    screenshot.png

  • 引入ICP之后
    screenshot.png

再来看个例子

CREATE TABLE `t1` (
  `a` int(11) DEFAULT NULL,
  `b` char(8) DEFAULT NULL,
  `c` int(11) DEFAULT '0',
  `pk` int(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`pk`),
  KEY `idx1` (`a`,`b`)
) ENGINE=ROCKSDB;
INSERT INTO t1 (a,b) VALUES (1,'a'),(2,'b'),(3,'c');
INSERT INTO t1 (a,b) VALUES (4,'a'),(4,'b'),(4,'c'),(4,'d'),(4,'e'),(4,'f');

set optimizer_switch='index_condition_pushdown=off';

## 关闭ICP(Using where)
explain select * from t1 where a=4 and b!='e';
+----+-------------+-------+-------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type  | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+-------+-------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | t1    | range | idx1          | idx1 | 14      | NULL |    2 | Using where |
+----+-------------+-------+-------+---------------+------+---------+------+------+-------------+

## 关闭ICP走cover index(Using where; Using index)
explain select a,b from t1 where a=4 and b!='e';
+----+-------------+-------+------+---------------+------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | t1    | ref  | idx1          | idx1 | 5       | const |    4 | Using where; Using index |
+----+-------------+-------+------+---------------+------+---------+-------+------+--------------------------+

set optimizer_switch='index_condition_pushdown=on';

## 开启ICP(Using index conditione)
explain select * from t1 where a=4 and b!='e';
+----+-------------+-------+-------+---------------+------+---------+------+------+-----------------------+
| id | select_type | table | type  | possible_keys | key  | key_len | ref  | rows | Extra                 |
+----+-------------+-------+-------+---------------+------+---------+------+------+-----------------------+
|  1 | SIMPLE      | t1    | range | idx1          | idx1 | 14      | NULL |    2 | Using index condition |
+----+-------------+-------+-------+---------------+------+---------+------+------+-----------------------+

## 开启ICP仍然是cover index(Using where; Using index)
explain select a,b from t1 where a=4 and b!='e';
+----+-------------+-------+------+---------------+------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | t1    | ref  | idx1          | idx1 | 5       | const |    4 | Using where; Using index |
+----+-------------+-------+------+---------------+------+---------+-------+------+--------------------------+

这里总结下ICP的条件

server层主要负责判断是否符合ICP的条件,符合ICP则把需要的condition push到engine层。
engine层通过二级索引查找数据时,用server层push的condition再做一次判断,如果符合条件才会去查找主索引。

目前mysql支持ICP的引擎有MyISAM和InnoDB,MyRocks引入rocksdb后,也支持了ICP。
server层实现是一样的,engine层我们主要介绍innodb和rocksdb的实现。

server层

关键代码片段如下

make_join_readinfo()
  
switch (tab->type) {
    case JT_EQ_REF:
    case JT_REF_OR_NULL:
    case JT_REF:
      if (tab->select)
        tab->select->set_quick(NULL);
      delete tab->quick;
      tab->quick=0;
      /* fall through */
    case JT_SYSTEM:
    case JT_CONST:
      /* Only happens with outer joins */
      if (setup_join_buffering(tab, join, options, no_jbuf_after,
                               &icp_other_tables_ok))
        DBUG_RETURN(true);
      if (tab->use_join_cache != JOIN_CACHE::ALG_NONE)
        tab[-1].next_select= sub_select_op;

      if (table->covering_keys.is_set(tab->ref.key) &&
          !table->no_keyread)
        table->set_keyread(TRUE);
      else
        push_index_cond(tab, tab->ref.key, icp_other_tables_ok,
                        &trace_refine_table);
      break;

从代码中看出只有符合的类型rangerefeq_ref, and ref_or_null 二级索引才可能会push_index_cond。

而这里通过covering_keys来判断并排除使用了cover index的情况。covering_keys是一个bitmap,保存了所有可能用到的覆盖索引。在解析查询列以及条件列时会设置covering_keys,详细可以参考setup_fields,setup_wild,setup_conds。

engine层

innodb

innodb在扫描二级索引时会根据是否有push condition来检查记录是否符合条件(row_search_idx_cond_check)
逻辑如下:

row_search_for_mysql()
......
  if (prebuilt->idx_cond)
  {
      row_search_idx_cond_check //检查condition
      row_sel_get_clust_rec_for_mysql //检查通过了才会去取主索引数据
  }
....

典型的堆栈如下

handler::compare_key_icp
innobase_index_cond
row_search_idx_cond_check
row_search_for_mysql
ha_innobase::index_read
ha_innobase::index_first
ha_innobase::rnd_next
handler::ha_rnd_next
rr_sequential
join_init_read_record
sub_select
do_select

rocksdb

rocksdb在扫描二级索引时也会根据是否有push condition来检查记录是否符合条件

逻辑如下

read_row_from_secondary_key()
{
   find_icp_matching_index_rec//push了condition才会检查condition
   get_row_by_rowid//检查通过了才会去取主索引数据
}

典型的堆栈如下

handler::compare_key_icp
myrocks::ha_rocksdb::check_index_cond
myrocks::ha_rocksdb::find_icp_matching_index_rec 
myrocks::ha_rocksdb::read_row_from_secondary_key 
myrocks::ha_rocksdb::index_read_map_impl 
myrocks::ha_rocksdb::read_range_first
handler::multi_range_read_next 

other

ICP对cover index作出了严格的限制,而实际上应该可以放开此限制,这样可以减少enging层传第给server层的数据量,至少可以减少server层的内存使用。欢迎指正!

PgSQL · 案例分享 · PostgreSQL+HybridDB解决企业TP+AP混合需求

$
0
0

背景

随着IT行业在更多的传统行业渗透,我们正逐步的在进入DT时代,让数据发挥价值是企业的真正需求,否则就是一堆废的并且还持续消耗企业人力,财力的数据。

传统企业可能并不像互联网企业一样,有大量的开发人员、有大量的技术储备,通常还是以购买IT软件,或者以外包的形式在存在。

数据的核心 - 数据库,很多传统的行业还在使用传统的数据库。

但是随着IT向更多行业的渗透,数据类型越来越丰富(诸如人像、X光片、声波、指纹、DNA、化学分子、图谱数据、GIS、三维、多维 等等…… ),数据越来越多,怎么处理好这些数据,怎么让数据发挥价值,已经变成了对IT行业,对数据库的挑战。

对于互联网行业来说,可能对传统行业的业务并不熟悉,或者说互联网那一套技术虽然在互联网中能很好的运转,但是到了传统行业可不一定,比如说用于科研、军工的GIS,和互联网常见的需求就完全不一样。

除了对数据库功能方面的挑战,还有一方面的挑战来自性能方面,随着数据的爆炸,分析型的需求越来越难以满足,主要体现在数据的处理速度方面,而常见的hadoop生态中的处理方式需要消耗大量的开发人员,同时并不能很好的支持品种繁多的数据类型,即使GIS可能也无法很好的支持,更别说诸如人像、X光片、声波、指纹、DNA、化学分子、图谱数据、GIS、三维、多维 等等。

那么我们有什么好的方法来应对这些用户的痛处呢?

且看ApsaraDB产品线的PostgreSQL与HybridDB如何来一招左右互搏,左手在线事务处理,右手数据分析挖掘,解决企业痛处。

传统的业务场景分析

以Oracle数据库为例,系统具备以下特点

1. 可靠性

通过REDO日志提供可靠保障。

支持同步和异步模式,同步模式可以做到已提交的事务不丢失。

异步模式不保证已提交的事务不丢失,不保证一致性。

20170101_02_pic_001.jpg

2. 高可用

通过主备模式以及集群套件提供高可用支持

通过共享存储,RAC集群套件提供高可用支持, 注意应用连接设计时,不同的INSTANCE连接的应用应该访问不同的数据块,否则可能会因为GC锁带来性能严重下降。

通过共享存储,主机集群套件提供高可用支持

20170101_02_pic_002.jpg

3. 容灾

通过存储层远程增量镜像提供异地容灾

通过主备模式以及增量复制提供异地容灾

20170101_02_pic_003.jpg

4. 备份恢复

通过归档和基础备份提供在线备份以及时间点恢复功能

20170101_02_pic_004.jpg

5. 性能诊断

AWR报告,通常包括

TOP SQL、wait event stats、io time、db time

6. 功能

PL/SQL编程,C嵌入式SQL,SQL:2013标准

数据库编程

支持PL/SQL开发语言

支持C嵌入式开发

SQL兼容性

SQL: 2013

GIS

多种索引支持

数据类型丰富

语法例子

with, connect by, with, grouping set, rollup, cube

many building functions, OP, aggs

SQL HINT、物化视图、RLS(行安全策略)

7. 扩展性

通过RAC和共享存储,扩展主机的方式扩展,支持CPU并行计算

注意应用连接设计时,不同的INSTANCE连接的应用应该访问不同的数据块,否则可能会因为GC锁带来性能严重下降。

20170101_02_pic_005.jpg

8. 多租户隔离

比如Oracle 12C提出的PDB

20170101_02_pic_006.jpg

9. 价格

通常按核收费,按特性收费,LICENSE 昂贵

用户痛点分析

随着用户数据量的增长,数据库的处理能力逐渐成为瓶颈。

1. 数据库的计算能力

以ORACLE为例,传统的非MPP架构数据库,在执行大数据量运算时,受制于硬件限制,对于OLAP场景显得很吃力。

2. 数据挖掘分析能力

以ORACLE为例,传统的数据库没有机器学习套件,即使有,也受制于它的架构,无法发挥应对数据挖掘分析需求。

3. 扩展能力

RAC的扩展能力受到共享存储的限制,存储容易成为瓶颈

RAC的模式下面,必须确保APP不会跨实例访问相同的数据块,否则GC锁冲突严重,性能比单节点下面明显。

4. 可编程能力

支持的服务端编程语言仅PL/SQL,C。

不支持高级的类型扩展,函数扩展,OP扩展,索引扩展。

不适合企业快速发展的IT需求。

价格

昂贵

DT时代企业对数据的处理需求

除了对数据库基本的增删改查需求,备份恢复容灾需求外。企业对数据处理的要求越来越高。

比如很多时候,用户可能要实时的对数据进行清洗、分析、或者根据数据触发事件。

随着更多的业务接入IT系统,用户需要存储越来越多的非结构化的数据、贴近实际需求的数据(比如人像、化学分子式、X光片、基因串、等等现实世界的数据属性),很多数据库在这种情况下显得力不从心,只能靠应用程序来处理,由于数据离计算单元越来越远,效率变得低下。

20170101_02_pic_007.jpg

阿里云ApsaraDB OLTP+OLAP需求 解决方案剖析

20170101_02_pic_008.jpg

1. 通过以下PostgreSQL特性,可以支持OLTP+ 本地的10TB量级OLAP需求。

1. LLVM、CPU并行计算

2. 聚合算子复用

3. BRIN索引接口

2. 通过插件支持更多的业务数据类型需求

比如JSONB、图片、人像、化学分子式、基因串、GIS、路由等。

3. 流式数据处理方法

使用pipelineDB可以与kafka, jstrom, PostgreSQL无缝结合,以及标准的SQL接口,兼容PostgreSQL.

4. PB级的分析、挖掘需求

HybridDB基于开源的MPP数据库GPDB打造,有许多特点

支持弹性的增加节点,扩容时按表分区粒度进行,因此不堵塞其他表分区的读写

支持SQL标准以及诸多OLAP特性,

支持行列混合存储、多级分区、块级压缩、多节点并行计算、多节点数据并行导入、

支持丰富的数据类型,包括JSON、GIS、全文检索、语感、以及常见的文本、数值类型。

支持MADLib机器学习库,有上百种常见的挖掘算法,通过SQL调用UDF训练数据集即可,结合MPP实现了多节点并行的挖掘需求

支持数据节点间透明的数据重分布,广播,在进行多表JOIN时,支持任意列的JOIN,

支持随机分布,或按列分布,支持多列哈希分布,

支持哈希分区表、范围分区表、多级分区

支持用户使用python \ java编写数据库端的UDF

支持使用r客户端通过pivotalR包连接数据库,并将R的分析请求自动转换为MADlib库或SQL请求,实现R的隐式并行分析,同时数据和计算在一起,大幅提升了性能

支持HLL等估算数据类型,

支持透明的访问阿里云高性能对象存储OSS,通过OSS EXT插件,可以透明的并行访问OSS的数据,

支持PostgreSQL生态,吸纳更多已有的PostgreSQL生态中的特性

5. 透明的冷热分离技术

20170101_02_pic_009.jpg

一份数据,共享分析需求

在企业中,通常会有专门的分析师岗位,分析师在做建模前,需要经历很多次的试错,才能找到比较好的,可以固定的分析模型。

试错期间,根据分析师的想法,结合业务表现,分析师可能需要对数据反复的训练,这就对数据库有两种要求

1. 实时响应

2. 不干扰正常业务

实时响应对于MPP来说并不能,通常一个QUERY下去,毫秒级就可以响应,不需要等待任务调度。

而不干扰正常业务,这一点,可能就需要好好商榷了,因为一个QUERY可能把资源用光,当然,我们可以通过HybridDB的资源组来进行控制,比如给分析师的QUERY资源优先级降到最低,尽量减少对业务的干扰。

另外我们还有一种更加彻底的方法,数据共享,你可以把需要试错的数据集放到OSS中,然后启用一个空的PostgreSQL实例或者HybridDB实例,这些实例与生产实例完全无关,但是它可以去访问OSS的数据,建立外部表即可,分析师可以使用这些实例,对数据集进行分析,不会影响生产。

6. 多个数据库的衔接

通过rds_dbsync, dts, 或者云上BI、ETL厂商提供的ETL接口,几乎可以将任意数据源的数据实时的同步到HybridDB进行分析。

通过 OSS 高速并行导入导出

高速 OSS 并行导入导出

dbsync 项目

HybridDB最佳实践——实现OLAP和OLTP一体化打造

7. PostgreSQL,HybridDB 数据库可靠性分析

原理与Oracle类似,同时支持用户自由选择同步或异步模式,异步模式牺牲了数据可靠性,提升性能,同时不影响一致性。

20170101_02_pic_010.jpg

8. 多副本

20170101_02_pic_011.jpg

用户可以根据事务对可靠性的要求,选择副本数。

比如涉及用户账户的事务,至少要求2个副本。

而对于与用户无关的日志事务,1个副本,甚至异步都可以。

给用户设计应用时,提供了最大的灵活度。

9. 高可用方案

PostgreSQL高可用

20170101_02_pic_012.jpg

PostgreSQL的高可用的方案与Oracle类似,支持共享存储的方案,同时还支持流式复制的多副本方案,可以做到数据的0丢失。

HybridDB高可用

20170101_02_pic_013.jpg

HybridDB的高可用方案,为mirror的方式,同步复制,数据0丢失。

master的fts负责数据节点的failover和failback。

master节点的ha则交由上层的集群应用来解决。

10. 容灾

20170101_02_pic_014.jpg

对于多机房容灾,PostgreSQL和HybridDB在数据库层面支持流式的复制解决方案。

同时还支持传统的存储或文件系统层面的镜像容灾。

11. PostgreSQL备份与恢复

对于存储在OSS对象存储中的数据,备份的只是DDL,即外部表的建表语句。

而对于存储在数据库中的数据,使用的备份方法与Oracle类似,支持REDO的增量备份,也支持数据块级别的增量备份(具体见我写过的块级增量备份文档)。

20170101_02_pic_015.jpg

12. HybridDB备份与恢复

每个节点并行的进行。

20170101_02_pic_016.jpg

13. 性能诊断和资源控制

与Oracle类似,支持常见的指标TOP SQL、wait event stats、io time、db time

同时支持对long query进行监控,包括long query的执行计划翻转,执行树中每个节点耗费的时间,对BUFFER产生的操作,物理读等

对于HybridDB,使用resource queue控制不同用户对资源的使用

14. 数据库功能(PostgreSQL)

数据库功能方面,PostgreSQL超越了传统数据库所能COVER的数据类型、检索、和数据的运算。

1. 数据库编程

服务端支持PLpgSQL、python、java、R、javascript、perl、tcl 等开发语言

支持C嵌入式开发

plpgsql与Oracle PL/SQL功能不相上下

2. SQL兼容性

SQL: 2013

3. 语法例子

with, connect by(用WITH支持), with, grouping set, rollup, cube

many building functions, OP, aggs

PostGIS、JSONB

SQL PLAN HINT、物化视图、RLS(行安全策略)

多种索引支持(btree, hash, brin, gin, gist, sp-gist, rum, bloom)

支持全文检索、模糊查询、正则匹配(走索引)

数据类型丰富(常用类型、数组、范围、估值类型、分词、几何、序列、地球、GIS、网络、大对象、比特串、字节流、UUID、XML、JSONB、复合、枚举…… )

4. 支持ORACLE兼容包插件

5. 支持插件、支持FDW(透明访问外部数据)、支持LANGUAGE扩展

6. 支持多个聚合函数共用SFUNC,提升性能

7. 扩展能力

支持用户自定义数据类型、操作符、索引、UDF、窗口、聚合

15. 数据库功能(HybridDB)

1. 数据库编程

服务端支持PLpgSQL、pljava等开发语言

plpgsql与Oracle PL/SQL功能不相上下

2. SQL兼容性

with, connect by(用WITH支持), with, grouping set, rollup, cube

内置丰富的函数、操作符、聚合、窗口查询

多种索引支持(btree),支持函数索引,支持partial index

支持全文检索、字符串模糊查询(fuzzystrmatch)

数据类型丰富(数字、字符串、比特串、货币、字节流、时间、布尔、几何、网络地址、数组、GIS、XML、JSON、复合、枚举、。。。。。。)

支持ORACLE兼容包插件orafunc

3. 支持列存、行存、混合存储

4. 支持隐式并行计算

5. 支持机器学习库

6. 支持支持OSS_EXT(透明访问OSS对象数据)

7. 支持HLL数据评估插件

8. 扩展能力

支持用户自定义数据类型、操作符、索引、UDF、窗口、聚合

16. 数据库扩展能力(PostgreSQL)

20170101_02_pic_017.jpg

17. 数据库扩展能力(HybridDB)

20170101_02_pic_018.jpg

18. 多租户功能

20170101_02_pic_019.jpg

如何解决传统用户对OLTP+OLAP需求的痛处

1. 计算能力

由于传统数据库,比如ORACLE并非MPP架构,在执行大数据量运算时,受制于硬件限制,对于10TB以上的OLAP场景很吃力。

1.1 解决办法

PostgreSQL 多CPU并行计算,解决TB级本地实时分析需求

PostgreSQL 数据通过REDO日志实时流式同步到HybridDB,解决PB级别OLAP场景需求。

2. 数据挖掘分析能力

由于传统数据库,比如ORACLE没有机器学习套件,即使有,也受制于它的架构,无法发挥应对数据挖掘分析需求。

2.1 解决办法

PostgreSQL和HybridDB都内置了MADLib机器学习库,支持几百种挖掘算法。

通过R,Python服务端编程,支持更多的挖掘需求。

3. 扩展能力

RAC的扩展能力受到共享存储的限制,存储容易成为瓶颈

RAC的模式下面,必须确保APP不会跨实例访问相同的数据块,否则GC锁冲突严重,性能比单节点下面明显。

3.1 解决办法

PostgreSQL fdw based sharding + multimaster,支持单元化和水平扩展需求

HybridDB MPP天然支持水平扩展

4. 可编程能力

支持的服务端编程语言仅PL/SQL,C。

不支持高级的类型扩展,函数扩展,OP扩展,索引扩展。

不适合企业快速发展的IT需求。

4.1 解决办法

PostgreSQL, HybridDB 支持plpgsql, C, python, java等多种语言的服务端编程。

支持数据类型、索引、函数、操作符、聚合、窗口函数等扩展。

一些不完全benchmark数据

20170101_02_pic_020.jpg
20170101_02_pic_021.jpg

一些不完全用户

20170101_02_pic_022.jpg

方案小结

在DT时代,让数据发挥价值是企业的真正需求,否则就是一堆废的并且还持续消耗企业人力,财力的数据。

使用本方案,可以让企业更加轻松的驾驭暴增的数据,不管是什么数据类型,什么数据来源,是流式的还是在线或离线的数据分析需求,统统都能找到合理的方法来处置。

1. 高度兼容传统数据库,如Oracle

包括数据类型,过程语言,语法,内置函数,自定义函数,自定义数据类型

2. 解决了传统数据库如Oracle方案的痛点

3. 计算能力

PostgreSQL 多CPU并行计算,解决TB级本地实时分析需求

PostgreSQL 数据通过REDO日志实时流式同步到HybridDB,解决PB级别OLAP场景需求。

4. 数据挖掘分析能力

PostgreSQL和HybridDB都内置了MADLib机器学习库,支持几百种挖掘算法。

通过R,Python服务端编程,支持更多的挖掘需求。

5. 扩展能力

PostgreSQL fdw based sharding + multimaster,支持单元化和水平扩展需求

HybridDB MPP 天然支持水平扩展

6. 可编程能力

PostgreSQL, HybridDB 支持plpgsql, C, python, java等多种语言的服务端编程。

支持数据类型、索引、函数、操作符、聚合、窗口函数等扩展。

7. 支持估值类型

快速的输出PV,UV,COUNT(DISTINCT)等估值。

8. 共享一份数据,构建多个分析实例

通常在企业中有分析师的角色,分析师要对数据频繁的根据不同的分析框架进行分析,如果都发往主库,可能导致主库的计算压力变大。

用户可以将历史数据,或者维度数据存放到共用的存储(如OSS),通过FDW共享访问,一份数据可以给多个实例加载分析。可以为分析师配备独立的计算实例,数据则使用FDW从共享存储(如OSS)加载,与主库分离。

9. HybridDB优势

支持AO列存,块级压缩,机器学习,混合存储,MPP水平扩展,隐式并行,R,JAVA服务端编程语言支持,PB级别数据挖掘需求。

MongoDB · 特性分析 · 网络性能优化

$
0
0

从 C10K 说起

对于高性能即时通讯技术(或者说互联网编程)比较关注的开发者,对C10K问题(即单机1万个并发连接问题)应该都有所了解。『C10K』概念最早由 Dan Kegel 发布于其个人站点,即出自其经典的《The C10K problem》一文[1]。

于是FreeBSD推出了kqueue,Linux推出了epoll,Windows推出了IOCP。这些操作系统提供的功能就是为了解决C10K问题。

常用网络模型

方案名称接受连接网络 IO计算任务
1thread-per-connection1个线程N个线程在网络线程执行
2单线程 Reactor1个线程在连接线程执行在连接线程执行
3Reactor + 线程池1个线程在连接线程执行C2线程
4one loop per thread1个线程C1线程在网络线程执行
5one loop per thread + 线程池1个线程C1线程C2线程

注:N 表示并发连接数,C1和 C2 与连接数无关,与 CPU 数组相关的常数。

当然,还有一些用户态的解决方案,例如Intel DPDK。来自KVM核心的研发团队推出了一款数据库叫做ScyllaDB[2],其使用了SeaStar网络框架[3],SeaStar是目前可用性最好的用户态网络编程框架,在基于DPDK实现了socket语义之后,进一步提供了线程乃至协程的语义封装,便于上层应用使用。

看看利用Seastar加速的http和memcached并发可以达到多少级别,并且随着cpu核数增加线性增长,就能感受用户态网络的巨大威力了。
httpd
memcached

各个方案中各有优缺点,要根据不同的业务场景因地制宜,包括但不限于:CPU 密集型,IO 密集型,长连接,短连接,同步,异步,硬件特性。

官方 IO 模型分析

MongoDB 现在使用的同步 IO 模型,如图:

m2

由主线程进行 accept 连接,然后针对每一个连接创建一个线程进行处理,「thread per connection」这种模型

  • 其一,不适合短连接服务,创建/删除线程的开销是巨大的,体现在创建线程时间和至少1MB 内存的使用。
  • 其二,伸缩性受到线程数的限制,200+线程数的调度对 OS 也是不小的负担。另外随着线程数的增加, 由于 mongo 本身业务的特性,对数据处理的并发度并不高,DB锁和全局的原子操作造成的 context-switch 也是急剧上升,性能反而下降,频繁的线程切换对于 cache 也不友好。

下面一张图可以看出各级缓存之间的响应时间差距,以及内存访问到底有多慢!
cache

改进方案

基于上述思考和 MongoDB 的业务特性:

  • 部分命令可能阻塞
  • 长连接
  • 高并发需求
  • 通用性

我们选型方案5,「one loop per thread」加上线程池,利用分而治之的思想。
有一个 main reactor 负责 accept 连接,然后把连接挂载某个sub reactor(采用round-robin的方式来选择sub reactor),这样该连接的所用操作都在那个sub reactor所处的线程池中完成。如图:

m1.png

同步模型到异步模型的转变,也引入了一些问题需要我们解决:

  • 非 pingpong 模型,乱序问题;
  • 请求优先级反转;
  • 线程池忙等;
  • cache miss 是否减少。

以上问题我们将在下文中一一展开。

网络框架对比

libevent: libevent 就如名字所言,是一个异步事件框架。从 OS 那里获得事件, 然后派发。派发机制就是“回调函数”。异步异步,归根结底就是处理从操作系统获得的事件。

libev: 就设计哲学来说,libev的诞生,是为了修复libevent设计上的一些错误决策。例如,全局变量的使用。

libuv: 开发node的过程中需要一个跨平台的事件库,他们首选了libev,但又要支持Windows,故重新封装了一套,*nix下用libev实现,Windows下用IOCP实现。

boost.asio: 跨平台的 Proactor 模型实现,目前已经进入 C++17 的标准提案

libco: libco是微信后台大规模使用的c/c++网络协程库。

调研与验证

asio在 MongoDB 上使用越来越多[4],而且满足我们跨平台的需求。如下在调研了性能后,我们还是选择了 asio

使用 boost.asio编写 echo客户端,服务端, 验证 asio网络框架的极限性能,指导我们对 MongoDB 的性能优化。

echo客户端的代码: https://gist.github.com/yjhjstz/ee1820efe0ff0c1ed83a6eb4649c7985

模型1- echo service 端:
https://gist.github.com/yjhjstz/4eceba80ecd328a87784a0fe0b602d6c

    // 省略 ....
    echo_server();
    boost::thread_group tg;
    for (int i = 0; i < thread_count; ++i)
        tg.create_thread([]{ ios.run(); });
    tg.join_all();
    return 0;

一个 IO 线程 + 线程池的模型,但锁的竞争被放大,QPS 在35W左右。

模型2 —- echo service 端 :
https://gist.github.com/yjhjstz/a9eb964fd20d6e5c186d7a2ba3921c8f#file-server-cpp
多个 IO 线程的模型,基本没有锁竞争,QPS 在90W+。

代码实现

修改原则:

  • 尽量增加接口,尽量不改动原有接口
  • 利用重载,实现自己的类,做到代码开关可控。

实现略,开源 patch 准备中,请期待。

回答四个问题

同步模型到异步模型的转变,一些问题需要我们解决:

问题1的解决得益于 asio 提供的 Preactor 编程模型,async_* 的驱动在每个 IO 线程(或者线程池),但单个命令不返回客户端,下个数据包就不会被触发响应。

请求优先级反转的问题具体场景可以是心跳包或者其他。我们隔离了单独的IO 线程去处理和管理此类连接。

针对问题3,线程池忙等,我们实现了动态可伸缩的线程池,通过配置线程池中线程的最小值和最大值实现动态申请和回收线程。

问题4,我们使用 perf stat 来验证:

// 未优化前
[jianghua.yjh@r101072xxx.sqa.zmf /home/jianghua.yjh]
$sudo perf stat -e cache-references,cache-misses -p 21782

 Performance counter stats for process id '21782':

    31,807,891,996 cache-references
     1,515,770,600 cache-misses              #    4.765 % of all cache refs

     126.836238857 seconds time elapsed

// 优化后
[jianghua.yjh@r101072xxx.sqa.zmf /home/jianghua.yjh]
$sudo perf stat -e cache-references,cache-misses -p 20047
 Performance counter stats for process id '20047':

    35,495,507,358 cache-references
     1,344,188,577 cache-misses              #    3.787 % of all cache refs

      99.501870882 seconds time elapsed

cache-miss在优化后降了1个百分点。进一步的,我们发现 update_curr相比占比上升,也就是花在线程调度上的工作导致了部分的 cache-miss, 锁的 lock, unlock 排在了前面(如图),
perf-cache.png

锁这块我们还在努力研究优化中~~

性能测试报告

测试环境 (standalone)

  1. Linux, Intel(R) Xeon(R) CPU E5-2630 0 @ 2.30GHz, SSD
  2. 136 部署修改过的 YCSB测试工具, 137 部署 mongod, 针对大量不同的连接数进行测试,场景 workloada。
  3. 随着连接数的增加,刚开始qps会不断成倍增长,当server资源已经达到上限时,继续增加连接数,qps会降低,同时平均的延时也在增加;
    因测试存在一定的偶然性,测试结果里个别数据项可能跟总的趋势不匹配,但经多次测试验证,总的趋势是类似。
  4. 请求延时分布统计了平均延时、95%的请求延时(95%的请求延时小于该值)、99%请求延时。

workloada QPS(50% read + 50% update )

workloada_ops_merge.png

Latency update (同步模型)

workloada_update_latency.png

Latency update (异步模型)

workloada_update_latency.png

总结

  • 在高并发场景下,排队控制导致同步模型平均update延时超过1S,降级到不可用的状态。
  • 异步模型性能保持稳定,优化后我们获得了60%+的 QPS 收益。
  • 调优过程中使用到了很多性能剖析利器 ,可以参考博文:Linux常用性能调优工具索引

参考

MySQL · 捉虫动态 · event_scheduler 慢日志记错

$
0
0

问题背景

最近遇到了 event_scheduler 在记录慢日志时的一个 bug,在这里分享给大家。

为了方便描述问题,先构造一个简单的 event,如下:

delimiter //
create event event1 on schedule every 5 second starts now() ends date_add(now(), interval 1 hour)
do begin
select sleep(1);
select * from t1;
select sleep(2);
end //
delimiter ;

其中的 t1 表中,有 2 条记录。

同时打开 event_scheduer 和 slow_log,并把慢日志的时间设置为 1s。

set global event_scheduler = on;
set global slow_query_log = on;
set global long_query_time = 1;
set global log_output = 'TABLE';

待 event 执行段时间后,查询 slow_log 会看到如下的结果:

+---------------------+------------+------------+-----------+-----------+---------------+------+----------------+-----------+-----------+------------------+-----------+
| start_time          | user_host  | query_time | lock_time | rows_sent | rows_examined | db   | last_insert_id | insert_id | server_id | sql_text         | thread_id |
+---------------------+------------+------------+-----------+-----------+---------------+------+----------------+-----------+-----------+------------------+-----------+
| 2017-01-14 16:15:33 | root[root] | 00:00:01   | 00:00:00  |         1 |             0 | test |              0 |         0 |         1 | select sleep(1)  |         4 |
| 2017-01-14 16:15:33 | root[root] | 00:00:01   | 00:00:01  |         3 |             2 | test |              0 |         0 |         1 | select * from t1 |         4 |
| 2017-01-14 16:15:35 | root[root] | 00:00:03   | 00:00:01  |         4 |             0 | test |              0 |         0 |         1 | select sleep(2)  |         4 |
| 2017-01-14 16:15:38 | root[root] | 00:00:01   | 00:00:00  |         1 |             0 | test |              0 |         0 |         1 | select sleep(1)  |         5 |
| 2017-01-14 16:15:38 | root[root] | 00:00:01   | 00:00:01  |         3 |             2 | test |              0 |         0 |         1 | select * from t1 |         5 |
| 2017-01-14 16:15:40 | root[root] | 00:00:03   | 00:00:01  |         4 |             0 | test |              0 |         0 |         1 | select sleep(2)  |         5 |
+---------------------+------------+------------+-----------+-----------+---------------+------+----------------+-----------+-----------+------------------+-----------+

可以看到,slow_log 中的 select * from t1select sleep(2)相关记录是有问题的:

  1. select * from t1不应该被记为慢日志,同时其中的 lock_time(应该为0) 和 rows_sent(应该为2) 都是错的;
  2. select sleep(2)的 query_time(应该为2),lock_time(应该为0),和 rows_sent(应该为1) 也都是错的。

rows_sent 记错比较好确认,query_time 和 lock_time 记错我们可以从 start_time 和 thread_id 对照确认,另外select sleep(2)是没有拿锁的,不应该有等锁的时间。

问题分析

为了搞清楚这个问题,我们需要了解 slow_log 是怎么记的。

slow_log 是在语句执行完后记录的,因为加锁时间和返回记录数这些信息,在执行之后才知道,general_log 记录是在语句解析完执行前。

slow_log 是否记录判读的逻辑在 log_slow_applicable()中:

a) 是否没有用到索引,并且 log_queries_not_using_indexes打开(这种情况下还可能触发 throttle 导致不记录)
b) 被标记为慢 SQL(thd->server_status & SERVER_QUERY_WAS_SLOW)

我们这里只关心 b) 这种 case。

在 SQL 执行结束时,会先调用 update_server_status()来判断是否是慢 SQL,逻辑如下:

void update_server_status()
{
  ulonglong end_utime_of_query= current_utime();
  if (end_utime_of_query > utime_after_lock + variables.long_query_time)
    server_status|= SERVER_QUERY_WAS_SLOW;
}

utime_after_lock 表示的是拿到锁的时间点,server 层通过 THD::set_time_after_lock()设置,引擎层(目前只有 InnoDB 支持)如果有锁请求等待时间的话,会累加到这个变量上,通过 thd_storage_lock_wait()函数。

因此一个 SQL 因为等锁而导致执行时间长的话,是不会记入慢 SQL 的。

select * from t1语句执行结束,调用 update_server_status()时,根据执行时间判断是不满足慢 SQL 的,但是因为 event 在执行前 server_status 没有重置,后面调用 log_slow_applicable()时,SERVER_QUERY_WAS_SLOW这个标志位还在,因此最终记到 slow_log 里了。

因此一个 event 在执行中,其中只要有一个语句是慢 SQL,那么后面所有的都会被记成慢SQL。

而其中时间记错的原因也是这样,每次执行语句前,start_utime 没有重置,而 utime_after_lock会在执行 select * from t1时拿锁被更新。

query_time 和 lock_time 的计算逻辑如下(LOGGER::slow_log_print()):

 if (thd->start_utime)
 {
   query_utime= (current_utime - thd->start_utime);
   lock_utime=  (thd->utime_after_lock - thd->start_utime);
 }

因此 event 中语句的 query_time 一直是增加的,lock_time 也不是 0。

需要注意的是,start_time 记录的并不是语句开始执行的时间,而是记入 slow_log 时的时间。

rows_sent 也是因为没有重置,一直都是累加的,而 rows_examined 会在 JOIN::exec()中被重置,因此记的是对的。

问题影响和解决

出现这种问题的前提是:

  1. 用了 MySQL 的 event_scheduler,并且 event 有多个 SQL 语句;
  2. 其中有一个 SQL 是慢SQL。

这个 bug 目前最新的 5.6.35/5.7.17 都受影响,官方已经确认,详见 bug#84450

知道原因后,fix 也就比较简单了,在 event 中每个 SQL 语句执行前,把 server_status, start_utime, m_sent_rows_count 重置掉就好了。

正常的用户 SQL 的执行逻辑就是这么干的,在 mysql_parse()里会调用 THD::reset_for_next_command(),但是 event 执行过程中并没有调用这个函数。

PgSQL · 引擎介绍 · 向量化执行引擎简介

$
0
0

摘要

本文为大家介绍一下向量化执行引擎的引入原因,前提条件,架构实现以及它能够带来哪些收益。 希望读者能够通过对这篇文章阅读能够对向量化执行引擎的应用特征与架构有一个概要的认识。

关键字

向量化执行引擎, MonetDB,Tuple, 顺序访问,随机访问, OLAP, MPP,火山模型,列存表,编译执行

背景介绍

过去的20-30年计算机硬件能力的持续发展,使得计算机的计算能力飞速提升。然后,我们很多的应用却没有做到足够的调整到与硬件能力配套的程度,因此也就不能够充分的将计算机强大的计算能力转换为软件的生产力。这样的问题在今天的通用数据库系统中也是一个比较突出的问题,因为这些通用数据库系统往往都已经有十数年或者几十年的历史了,它们也存在着不能够充分利用现在硬件能力的情况。

为什么会出现向量化执行引擎

制约数据库系统利用硬件能力的因素

  • 查询执行模型
    传统的数据库查询执行都是采用一次一tuple的pipleline执行模式。这样CPU的大部分处理时不是用来真正的处理数据,而是在遍历查询操作树,这样CPU的有效利用率不高。同时这也会导致低指令缓存性能和频繁跳转。更加糟糕的是,这种方式的执行,不能够利用到现在新硬件的新的能力来加速查询的执行。
    关于执行引擎这块,当前有一另外一个解决方案就是改变一次一tuple 为一次一列的模式。这也是我们向量化执行引擎的一个基础。在本文后面的部分会为大家详细介绍。

  • 存储
    从存储层面上来看,磁盘读写能力的提升并没有CPU硬件计算能力提升的那么迅速,加上通常数据库在对于随机访问的支持,对于数据的存放位置没有做特殊的要求。目前对于磁盘来说,顺序读取的效率是比随机读取的效率要高的。但是通常数据库很多数据存储都更倾向(或者说在运行了一段时间以后)数据是处于随机存放的状态。
    另一方面,目前磁盘读写能力已经远远的跟不上CPU数据执行的速度了。这种状况有一种解决方案就是使用列存储方案。因为列存储能够最大化的利用磁盘的读写能力,来提升IO带宽。

  • 列存简介
    这里我们也顺便给大家简单介绍一下列存储的一些特点以及优势,方便大家理解为什么向量化执行引擎必须要构架在列存储的表上才能够发挥出最大的优势。近年来,使用列存来实现存储的数据库这一技术成为,大型分析型数据越来越青睐的一种OLAP数据库的存储选型方案。 使用列存技术能够为查询的执行带来下列潜在的优势:

  1. 压缩能力的提升:
    因为列存储技术在数据表的存储上使用数据表的列(记录的一个属性)为单位存储数据,这样类型一致的数据被放在一起,这样类似的数据在进行压缩的时候,能够达到一个比较好的压缩比。
  2. 减少I/O的读入总量:
    因为列存按列为单位,这样,我们在读取数据的时候仅需要读入需要的列,相对于行存将所有数据读取上来再提取对应的属性,减少了I/O总量。
  3. 减少查询执行过程中的节点函数调用次数:
    以Greenplum 为例,如果当前列存的每次以一个数据块(segment,通常一个数据块包含某列1024行值)返回给上层节点的话,会极大的减少函数调用次数,达到提升查询执行性能的效果。
  4. 向量化执行:
    因为列存每列的各行数据存储在一起,可以认为这些数据是以数组的方式存储的,基于这样的特征,当该列数据需要进行某一同样操作,可以使用SIMD进一步提升计算效率,即便运算的机器上不支持SIMD, 也可以通过一个循环来高效完成对这个数据块各个值的计算。
  5. 延迟物化:
    延迟物化是在整个查询的计算过程中,尽量使用列存储,这样可以进一步减少在各个查询计划树节点之间传递数据的总量。

向量化执行引擎能够发挥效率的前提

那么向量化执行引擎是不是针对所有的数据库场景都能够达到很好的效果呢?答案是否定的。要获得到向量化执行引擎的收益,是需要有一定的条件支持的:首先向量化执行引擎效率的发挥需要数据库能够提供列存表的支持。 对于传统的行存表来说,谈向量化执行是不可能的。通常向量化执行引擎都是用在OLAP数仓类系统,因为通常分析型系统通常都是数据处理密集型负载,基本上都是采用顺序方式来访问表中大部分的数据,然后进行计算,最后将计算结果输出给终端用户。对于典型的OLTP点查询,这种类型的查询执行,使用行存表反而比列存表更好。那么读到这里是不是觉得向量化执行引擎似乎本身限制也挺多的!没有它我是不是一样可以满足数据库用户的需要呢? 如果跟按照现在OLAP数仓系统数据容量动辄T 级别甚至P 级别,一条复杂报表查询的执行可能会用几千秒,如果使用向量化执行引擎能可以比原来的行存提升3-5倍的执行效率的话,也许原来不能被接受的查询时长,现在可以被接受了。
其次,向量化引擎本身应该如何实现呢?
目前主要有两种关于向量化执行引擎的实现方法,

  1. 仍然使用火山模型,只不过一次返回一组列。这种模型的优势是仍然使用(火山模型),这个优化器于执行器模型已经很成熟,剩下需要的工作量就在于如何将一次一tuple的处理模式,修改为一次向上返回一组列存行值(例如:100-1000行)处理方式,难度相对较小;
  2. 将整个模型改造成为层次型的执行模式,这种模式需要将优化好的执行计划树,最终转换为编译执行,即,一次调用下来之后,每一层都完成后才向上返回数据,这样能够最大程度的减少各层次节点间的调用次数。提高CPU的有效计算效率。这里我们称这种模型为编译执行模型。
    后续会给大家对这两种模型进行更加详细的介绍

向量化执行引擎的架构

接下来我们将就之前讲的两种执行引擎来给大家在架构原理上来深入一些的了解向量化执行引擎。

2017-01-15-yamin-pic1.png

图1中描述的就是火山模型实现的行存执行引擎与列存执行引擎,其中左边代表的是当前比较流行的传统行存火山模型,右边代表的是列存实现的火山模型,从上图我们可以看到火山模式是从执行计划树的根节点开始向叶子节点递归调用,然后有叶子节点,通常是各种的扫描节点返回一条符合过滤条件的tuple 给上层节点处理,每一层节点在处理完该tuple之后继续网上层节点传递记录(Agg节点不是立刻往上层节点返回数据,它需要计算完所有的Tuple,才能继续往上层节点返回,所以这里AGG算子在处理好这个Tuple之后,又会往下调用扫描算子返回下一条符合过滤条件的记录)。这样处理完整个表的记录之后,AGG算子会把数据返回到上一层节点继续处理,在整个过程中需要AGG算子缓存中间结果。
右边列存执行引擎,执行逻辑基本上与左边行存执行引擎一致,但是每次扫描处理的是一组组以col组织的列数据集合,这样我们最为直观的观察就是从上层节点向下层节点的调用次数少了。相应的CPU的利用率得到了提高,另外数据被组织在一起。可以利用硬件发展带来的一些收益;如SIMD, 循环优化,将所有数据加载到CPU的缓存当中去,提高缓存命中率,提升效率。在列存储与向量化执行引擎的双重优化下,查询执行的速度会有一个非常巨大的飞跃大约3-5倍。后续我们会在技术实现的过程中给出更为详细的性能测试对比报告,敬请期待。

2017-01-15-yamin-pic2.png

图2 给我们现实的是火山模型的向量化执行引擎与编译执行的向量化执行引擎之间执行方式的对比。鉴于在图1部分已经介绍过了火山模型,在这里我们讲一下编译执行模型,这个模型也是从根节点开始往叶子节点调用,但是只调用一次,从叶子节点开始每一层都是执行完所有的操作之后,才向上返回结果。这样整颗执行计划树从跟节点到叶子节点只需要调用一次,彻底消除了因节点间函数调用而导致的CPU利用率不高的这个问题。 同样列存执行引擎所能够拿到的好处也是可以被编译执行模型使用。 但是这种模型有一个缺点就是每一个节点都需要将数据进行缓存,在数据量比较大的情况下,内存可能放不下这些数据,需要写盘,这样会造成额外的开销。

向量化执行引擎的优势与需要注意的方面

这部分我们主要给大家提一下向量化执行引擎的优势与需要注意的地方。

优势:

  • 向量化执行引擎可以减少节点间的调度,提高CPU的利用率。
  • 因为列存数据,同一列的数据放在一起,导致向量化执行引擎在执行的时候拥有了更多的机会能够利用的当前硬件与编译的新优化特征。
  • 因为列存数据存储将同类型的类似数据放在一起使得压缩比能够达到更高,这样可以拉近一些磁盘IO能力与计算能力的差距。

需要注意的问题:

  • 通信库效率问题
    当前比较主流的OLAP类型的数据库架构,通常首选是MPP架构的数据库,因为MPP架构上是share nothing架构的,所以它的集群各执行节点是有通信需要的,通信效率的高低也是决定了查询执行效率。另外就是大集群情况下,如果使用tcp方式连接,连接数会受限。
  • 数据读写争抢问题
    这个问题本身不是向量化执行引擎的,而是列存带来的,因为列存储表每一列单独存储为一个文件,这样在写盘的时候有优化与没有优化的差距还是非常明显的。
  • 列存数据过滤效率问题
    列存数据过滤率问题,这个问题是源于,列存数据中的一个处理单元是由连续的N个值放在一起组成的一个Col(数组),然后再由多个Col的数组组成了一个处理单元。在进行过率的时候如何能够更加紧凑的放置数据是需要我们考虑列存在过滤掉效率和存放之间如何优化的问题。
  • 表达式计算问题(LLVM)
    LLVM优化可以将表达式计算由遍历树多层调用模式变为,只调用一个函数的扁平式执行方式。这样可以极大的提高表达式的执行性能。值得一提的是LLVM技术的优势也可以应用在执行计划编译执行模型的构建上面。

总结

本文旨在给大家一个向量化执行引擎的概要介绍,给大家有一个初步印象,在我们阿里云自己的向量化执行引擎设计开发的过程中,会在很多方面遇到不少问题,
后续我们也尽量给大家分享这些细节的实现。

SQL Server · 特性分析 · 2012列存储索引技术

$
0
0

摘要

MS SQL Server 2012首次引入了列存储索引(Columnstore Index)来加速数据分析(OLAP)和数据仓库(Data Warehouse)场景的查询,它主要是通过将数据按列压缩存储的方式来减少查询对磁盘IOPS开销和CPU开销,最终达到提升查询效率,降低响应时间的目的。当然,列存储索引也不是一把万能的钥匙,在SQL Server 2012版本中它有诸多非常严苛限制条件。
这篇文章会从以下几个方面来介绍列存储索引:

  • 列存储索引所涉及到的基本概念

  • 列存储索引的结构

  • 列存储索引对查询性能的影响

  • MS SQL Server 2012上列存储索引的限制

  • 解决列存储索引表只读问题

概念

首先让我们来看看列存储索引涉及到的几个关键的概念。

列存储技术

列存储技术背后的核心思想并不是微软首创的,早在上20世纪70年代,基于列的存储系统就与传统的行存储数据库管理系统一同出现了。微软数据库产品首次尝试从传统的行存储结构转变为面向列的存储方案是在SQL Server2012这个产品,以期望这种方案能够以最低限度的额外工作量换取更高的性能。

列存储索引

MS SQL Server列存储索引是使用列式数据格式(称为列存储)压缩存储、检索和管理数据的技术。它主要目标是将尽量多的数据加载至内存中,在进行数据处理时,使用访问内存的方式来替换从磁盘中读取数据。这种处理方式有两大优点,一是速度更快,二是硬盘的IOPS(每秒读写次数)消耗更低。

列存储索引压缩

数据压缩对于MS SQL Server来说已经不是什么新鲜玩意儿了,SQL Server支持数据库备份压缩,数据行压缩和数据页压缩,当然列存储索引压缩默认是开启的并且不允许禁用。相对于传统按行存储的结构来说,列存储索引这种按列来存储的特殊结构来说,压缩更加有效。这是因为表中的相同列数据属性相同,存储的数据也非常相近,有可能还有非常多的重复值,因此数据压缩比例更高,压缩效率更快,更少的磁盘I/O开销,数据读取需要更少的内存,相同内存中可以存储更多的数据。

使用下面语句,我们可以发现本文的测试表dbo.SalesOrder的列存储索引NCSIX_ALL_Columns压缩算法是COLUMNSTORE。

USE ColumnStoreDB
GO
SELECT DISTINCT
	table_name = object_name(part.object_id)
	,ix.name
	,part.data_compression_desc 
FROM sys.partitions as part
	INNER JOIN sys.indexes as ix
	ON part.object_id = ix.object_id
	AND part.index_id = ix.index_id
WHERE part.object_id = object_id('dbo.SalesOrder','U')
	AND ix.name = 'NCSIX_ALL_Columns'

结果如下:

图片24.png

Column Segment and Row Group

在列存储索引中,SQL Server引入了两个全新的概念:Column Segment和Row Group。

Column Segment:是SQL Server列存储索引最基本的存储单元,列存储索引的每一列数据会被划分为一个或者多个Column Segment。它是一组经过压缩后物理存储在相同存储介质的列值。

Row Group:是一组同时被压缩成列存储格式的行,每一个Row Group中包含了每个列的一个Column Segment。Row Group定义了每一个Column Segment的列值。
以上的解释还是非常抽象和难于理解,做一个图,我们就很好理解什么Column Segment和Row GROUP了。

01.png

Batch Mode Processing

在SQL Server OLAP的场景中,做BI类分析型查询语句往往需要扫描非常大量的数据记录数。Batch Mode Processing 是SQL Server新的查询处理算法,专门设计来高效地处理大量数据的批处理算法,以加快统计分析类查询的效率。其实这个算法的原理实现非常简单,SQL Server有一个专门的系统视图sys.column_store_segments来存放列存储索引的每个列的每个Segments的最小值和最大值,当SQL Server执行Batch Mode Processing查询时,只需要和查询筛选条件对比,就可以知道对应的Segment是否包含满足条件的数据,从而可以实现快速的跳过哪些不满足条件的Segment。由于每个Segment中包含成千上万条记录,所以SQL Server筛选出满足条件的效率非常高,因此大大节约了磁盘I/O和因此带来的CPU开销。这种跳过不满足条件Segment的算法专业术语叫Segment Elimination。

USE ColumnStoreDB
GO
SELECT 
	table_name = object_name(part.object_id)
	,col.name
	,seg.segment_id
	,seg.min_data_id
	,seg.max_data_id
	,seg.row_count
FROM sys.partitions as part
	INNER JOIN sys.column_store_segments as seg
	ON part.partition_id = seg.partition_id
	INNER JOIN sys.columns as col
	ON part.object_id = col.object_id
	AND seg.column_id = col.column_id
WHERE part.object_id = object_id('dbo.SalesOrder','U')
AND seg.column_id = 1
ORDER BY seg.segment_id

结果如下:

图片23.png

行列存储结构对比

其实在列存储索引引入SQL Server之前,SQL Server的索引我们通常不叫行存储索引,而是叫B-Tree索引,因为SQL Server的行存储索引是按照B-Tree结构来组织的。这一节我们来对比基于行存储和基于列存储的结构差异。

行存储结构

在传统的基于行存储的结构中,表中每一行数据会存储在一起。如果用户需要其中的某个或者某几个字段的数据,SQL Server系统必须先将满足条件的记录所有字段的值载入内存中,然后再从中筛选出用户需要的列。换句话说,用户的查询语句横向筛选是通过索引(传统的B-Tree索引)来快速完成,而纵向列的筛选由于基于行存储的设计而浪费了许多的系统性能,这些性能浪费包括载入过多列数据导致的内存浪费,磁盘I/O资源的浪费开销,和因此而带来的CPU开销。那就让我们看看基于行存储的结构图:

图片3.png

列存储结构

为了解决行存储结构导致资源浪费和多余开销,从MS SQL Server 2012开始,微软引入了基于列存储的新结构,具体使用在列存储索引这个方面。列存储结构是将数据按列来存储,每一列的数据存放在一起。这样当用户在执行查询的时候,可以快速拿到这一列的所有数据,而不会浪费多余的IO资源和相应的CPU开销。除了存储结构的变化外,微软还对列存储索引默认启用了数据压缩功能,进一步减少了IO开销。列存储索引的结构如下:
图片4.png

行列存储结构对比

以上是比较理论的认知,稍显抽象,让我们来看一个典型的例子,从具体的例子详细分析基于行存储和基于列存储的数据获取方式上的差异。
首先,让我们来创建测试环境数据库ColumnStoreDB,所需要使用到的表dbo.AutoType和dbo.SalesOrder,以及相应的数据初始化。为了照顾到后面《解决列存储索引表只读问题》章节,我将dbo.SalesOrder创建为分区表。

-- Create testing database
IF DB_ID('ColumnStoreDB') IS NULL
	CREATE DATABASE ColumnStoreDB;
GO

USE ColumnStoreDB
GO
IF OBJECT_ID('dbo.AutoType', 'U') IS NOT NULL
BEGIN
	DROP TABLE dbo.AutoType
END
GO

-- create demo table autoType
CREATE TABLE dbo.AutoType
(
   AutoID INT IDENTITY(101,1) NOT NULL PRIMARY KEY, 
   Make VARCHAR(20) NOT NULL,
   Model VARCHAR(20) NOT NULL,
   Color VARCHAR(15) NOT NULL,
   ModelYear SMALLINT NOT NULL
);

-- data init
INSERT INTO dbo.AutoType
SELECT 'Ford', 'Explorer', 'white', 2003 UNION ALL
SELECT 'Satum', 'Lon', 'blue', 2003 UNION ALL
SELECT 'Lexus', 'GX460', 'gray', 2010 UNION ALL
SELECT 'Honda', 'CRV', 'blue', 2007 UNION ALL
SELECT 'Subaru', 'Legacy', 'green', 2008 UNION ALL
SELECT 'Honda', 'Civic', 'red', 1996 UNION ALL
SELECT 'Nissan', 'Sentra', 'silver', 2012 UNION ALL
SELECT 'Chevrolet', 'Tahoe', 'green', 1995 UNION ALL
SELECT 'Toyota', 'Celica', 'red', 1992 UNION ALL
SELECT 'BMW', 'X5', 'gray', 2002 UNION ALL
SELECT 'Subaru', 'Impreze', 'silver', 2011 UNION ALL
SELECT 'Volkswagen', 'Jetta', 'black', 1995 UNION ALL
SELECT 'Chevrolet', 'Impala', 'red', 2008 UNION ALL
SELECT 'Jeep', 'Liberty', 'gray', 2012 UNION ALL
SELECT 'Dodge', 'Dakota', 'blue', 2000 
;

-- Create PARTITION FUNCTION & SCHEMA
CREATE PARTITION FUNCTION pf_SalesYear (datetime) 
AS RANGE LEFT FOR VALUES 
('2013-01-01 00:00', '2014-01-01 00:00', '2015-01-01 00:00', '2016-01-01 00:00', '2017-01-01 00:00', '2018-01-01 00:00')
;
GO

CREATE PARTITION scheme ps_SalesYear
AS  PARTITION pf_SalesYear 
ALL TO ([PRIMARY])
;
GO

-- create demo table SalesOrder
IF OBJECT_ID('dbo.SalesOrder', 'U') IS NOT NULL
BEGIN
	DROP TABLE dbo.SalesOrder
END
GO
CREATE TABLE dbo.SalesOrder
(
	OrderID INT NOT NULL
	,AutoID INT NOT NULL
	,UserID INT NOT NULL
	,OrderQty INT NOT NULL
	,Price DECIMAL(8,2) NOT NULL
	,UnitPrice AS Price * OrderQty
	,OrderDate DATETIME NOT NULL
) ON ps_SalesYear(OrderDate);

-- data init for 5 M records.
;WITH a 
AS (
	SELECT * 
	FROM (VALUES(1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) AS a(a)
), RoundData
AS(
SELECT TOP(5000000)
	OrderID = ROW_NUMBER() OVER (ORDER BY a.a)
	,AutoIDRound = abs(checksum(newid()))
	,Price = a.a * b.a * 10000
	,OrderQty = a.a + b.a + c.a + d.a + e.a + f.a + g.a + h.a
FROM a, a AS b, a AS c, a AS d, a AS e, a AS f, a AS g, a AS h
)
INSERT INTO dbo.SalesOrder(OrderID, AutoID, UserID, OrderQty, Price, OrderDate)
SELECT 
	OrderID
	,AutoID = cast(ROUND((13 * (AutoIDRound*1./cast(replace(AutoIDRound, AutoIDRound, '1' + replicate('0', len(AutoIDRound))) as bigint)) + 101), 0) as int)
	,UserID = cast(ROUND((500 * (AutoIDRound*1./cast(replace(AutoIDRound, AutoIDRound, '1' + replicate('0', len(AutoIDRound))) as bigint)) + 10000), 0) as int)
	,OrderQty
	,Price = cast(Price AS DECIMAL(8,2))
	,OrderDate = dateadd(day, -cast(ROUND((1099 * (AutoIDRound*1./cast(replace(AutoIDRound, AutoIDRound, '1' + replicate('0', len(AutoIDRound))) as bigint)) + 1), 0) as int) ,'2017-01-10')
FROM RoundData;
GO

假如目前用户需要获取所有汽车的制造商和相应的制造年份,即查询语句如下:

SELECT Make, ModelYear FROM dbo.AutoType;

那么,对于传统的基于行存储的结构,SQL Server会首先将AutoType这个表所有的数据页(这里假设占用了3 页)载入SQL Server内存缓存中,然后筛选出两个必须的列返回给用户。换句话来说,在返回用户必须的两个字段之前,用户必须等待3个Pages的数据全部加载进入内存(这3个页中包含了无用的其他三个字段),如此势必导致磁盘I/O,内存使用量和CPU开销的浪费,最终必然导致用户的执行时间会被拉长。
相反的,对于列存储结构而言,列中数据是按列式存储的(一个Column Segment只包含某一个列的数据),当用户提交查询以后,系统可以很快的拿到这两个列的值,而无需去获取其他三个多余字段的值。
以上文字描述,可以形象为如下的结构图,图的左上角为行存储结构,图的右上角为列存储结构,图的左下角是行存储结构载入内存的过程,图的右下角是用户需要的最终结果。
图片5.png

基于以上的分析,我们清楚的知道了基于列存储的结构和数据压缩功能为SQL Server执行查询语句大大节约了I/O消耗和因此而产生的CPU开销。

查询性能影响

基于上一节对列存储索引的特殊存储结构的分析,我们很清楚的知道列存储索引在执行查询过程中节约IOPS的同时,对数据仓库类统计查询语句性能有了非常大的性能提升,当然这里面也避免不了有非常多的坑。这一节让我们来看看列存储索引相对于行存储索引性能到底有多大的提升,在提升性能的同时,我们又需要踩过哪些坑,迈过哪些坎。

创建列存储索引

为了便于接下来的性能对比,我们同时创建了传统的B-Tree索引和列存储索引,代码如下:

-- create regular B-Tree Clustered Index
CREATE UNIQUE CLUSTERED INDEX CIX_OrderID 
ON dbo.SalesOrder(OrderID, OrderDate) 
WITH (ONLINE = ON)
ON ps_SalesYear(OrderDate);

--create nonclustered columnstore index
CREATE NONCLUSTERED COLUMNSTORE INDEX NCSIX_ALL_Columns 
ON dbo.SalesOrder(OrderID, AutoID, UserID, OrderQty, Price, OrderDate)
GO

性能提升

首先让我们来分析下列存储索引为什么会提升查询性能:

  • 列存储索引可以提供非常高压缩比,官方数据是可以节约10X的存储开销和成本。因为用户表中每列存储的数据属性相同,数据内容相似,有的甚至有非常多的重复值。

  • 高数据压缩比为高性能查询提供了基础。因为在查询过程中压缩后的数据将使用更少磁盘IOPS来读取,相同内存大小可以存放更多的数据,高效的磁盘I/O带来CPU使用率的下降。

  • Batch Mode Processing提高查询性能,官方数据可以提高2-4X的性能。因为SQL Server可以一次性处理多行数据。

  • 用户查询经常使用某一列或者某几个列字段数据。列存储索引可以大大减少物理磁盘的I/O开销。

按照官方的数据统计列存储索引查询性能有10X到100X的性能提升,当然也取决于表结构的设计和具体的查询语句。接下来让我们用实际例子看看列存储索引相较于B-Tree索引对查询性能的提升。

/*--=====================================================
MS SQL Server 2012 columnstore index query performance compare
--=====================================================*/
USE ColumnStoreDB
GO

DBCC DROPCLEANBUFFERS
GO

SET STATISTICS TIME ON
SET STATISTICS IO ON

-- Columnstore index query
SELECT 
	AutoID
	,SalesQty = SUM(OrderQty)
	,SalesAmount = SUM(UnitPrice)
FROM dbo.SalesOrder WITH(INDEX=NCSIX_ALL_Columns)
GROUP BY AutoID

-- B-Tree Index query
SELECT 
	AutoID
	,SalesQty = SUM(OrderQty)
	,SalesAmount = SUM(UnitPrice)
FROM dbo.SalesOrder WITH (INDEX = CIX_OrderID)
GROUP BY AutoID

I/O、CPU和执行时间的对比:
图片6.png
列存储索引的I/O逻辑读消耗为336,B-Tree索引I/O逻辑读为21231,是前者的63倍;列存储索引CPU消耗为125;B-Tree索引CPU消耗为2526,是前者的20倍;列存储索引执行时间消耗为66,B-Tree索引执行时间消耗为873,是前者的13倍。

执行计划中的性能消耗对比:
图片7.png
列存储索引占5%,而B-Tree 索引占95%,也就是B-Tee储索引性能消耗是列存储索引的19倍。

执行计划对比:
图片8.png
列存储索引走的Batch Mode Processing,B-Tree索引走的是Row Mode Processing;列存储索引的CPU预估消耗为0.275008,B-Tree索引的CPU预估消耗为2.75008,是前者的10倍;列存储索引预估I/O消耗为0.0246065,B-Tee索引预估的I/O消耗为15.5661,是前者的632倍。

总结一下列存储索引在IO读、CPU消耗、时间消耗和执行计划展示中的性能有13到63倍的性能提升,平均有28.75倍提升,总结如下图所示:

图片9.png

踩坑场景

相较于B-Tree索引,列存储索引在IO,CPU,时间消耗方面,列存储索引的效率都有非常明显的提升。但是,请不要高兴得太早,如果不注意查询语句的写法,列存储索引可能不会走高效的Batch Mode Processing,而走低效的Row Mode Processing,进而达不到你想要的效果。以下几个小节是教大家如何升级打怪,踩坑迈坎。

INNER JOIN使用Batch Mode而OUTER JOIN使用Row Mode

在OLAP场景中,创建了列存储索引的表,使用Batch Mode处理查询效率要远远高于Row Mode处理。其中一种典型的情况是使用等值连接(INNER JOIN)SQL Server会采用Batch Mode来处理查询,而使用外链接(OUTER JOIN)的情况,SQL Server会采取Row Mode处理查询,效率天壤之别。让我们来看看如何改写OUTER JOIN查询,使查询计划能够走到Batch Mode,从而大大提高查询性能。

INNER JOIN使用Batch Mode:显示所有存在销售记录的汽车销售收入和销量

-- Batch mode processing will be used when INNER JOIN
SELECT 
	at.Model
	,TotalAmount = SUM(ord.UnitPrice)
	,TotalQty = SUM(ord.OrderQty)
FROM dbo.AutoType AS at
	INNER JOIN dbo.SalesOrder AS ord
	ON ord.AutoID = at.AutoID
GROUP BY at.Model

实际的执行计划Actual Execution Mode为Batch
图片10.png

OUTER JOIN使用Row Mode:显示所有汽车的销售收入和销量

-- BUT row mode processing will be used when OUTER JOIN
SELECT 
	at.Model
	,TotalAmount = ISNULL(SUM(ord.UnitPrice), 0)
	,TotalQty = ISNULL(SUM(ord.OrderQty), 0)
FROM dbo.AutoType AS at
	LEFT OUTER JOIN dbo.SalesOrder AS ord
	ON ord.AutoID = at.AutoID
GROUP BY at.Model
ORDER BY ISNULL(SUM(ord.UnitPrice), 0) DESC

实际的执行计划Actual Execution Mode为Row
图片11.png
如何踩过OUTER JOIN使用Row Mode的坑呢?思路其实很简单,因为从第一部分我们知道,使用INNER JOIN执行计划是可以走到Batch Mode的,那么我们可以先找出有销售记录的所有汽车的销售额和销量结果集(这个结果集应该已经非常小了),然后再使用AutoType表来OUTER JOIN第一步的结果集,就得到我们想要的结果。

-- OUTER JOIN workaround
;WITH intermediateData
AS
(
	SELECT 
		at.AutoID
		,TotalAmount = SUM(ord.UnitPrice)
		,TotalQty = SUM(ord.OrderQty)
	FROM dbo.AutoType AS at
		INNER JOIN dbo.SalesOrder AS ord
		ON ord.AutoID = at.AutoID
	GROUP BY at.AutoID
)
SELECT 
	at.Model
	,TotalAmount = ISNULL(itm.TotalAmount, 0)
	,TotalQty = ISNULL(itm.TotalQty, 0)
FROM dbo.AutoType AS at
	LEFT OUTER JOIN intermediateData AS itm
	ON itm.AutoID = at.AutoID
ORDER BY itm.TotalAmount DESC

在处理掉数据量最大的这一步,实际的执行计划Actual Execution Mode为Batch
图片12.png

OUTER JOIN踩坑前后写法执行的CPU和时间消耗对比:
图片13.png

IN & EXISTS使用Row Mode

IN和EXISTS写法在查询列存储表时,也是使用Row Mode,所以我们需要改写为使用Batch Mode执行方式。方法是使用INNER JOIN方式或者是在IN字句里面使用常量。

-- DEMO 2: IN & EXISTS both use row mode processing

IF OBJECT_ID('dbo.HondaAutoTypes', 'U') IS NOT NULL
BEGIN
	TRUNCATE TABLE dbo.HondaAutoTypes
	DROP TABLE dbo.HondaAutoTypes
END

SELECT *
	INTO dbo.HondaAutoTypes
FROM dbo.AutoType
WHERE make = 'Honda'

-- IN use row mode
SELECT 
		OrderDay = CONVERT(CHAR(10), ord.OrderDate, 120)
		,TotalAmount = SUM(ord.UnitPrice)
		,TotalQty = SUM(ord.OrderQty)
FROM dbo.SalesOrder AS ord
WHERE ord.AutoID IN(SELECT AutoID FROM dbo.HondaAutoTypes)
GROUP BY CONVERT(CHAR(10), ord.OrderDate, 120)
ORDER BY 1 DESC

-- EXISTS use row mode too.
SELECT 
		OrderDay = CONVERT(CHAR(10), ord.OrderDate, 120)
		,TotalAmount = SUM(ord.UnitPrice)
		,TotalQty = SUM(ord.OrderQty)
FROM dbo.SalesOrder AS ord
WHERE EXISTS(SELECT TOP 1 * FROM dbo.HondaAutoTypes WHERE AutoID = ord.AutoID)
GROUP BY CONVERT(CHAR(10), ord.OrderDate, 120)
ORDER BY 1 DESC


-- IN & EXISTS workaround using INNER JOIN
SELECT 
		OrderDay = CONVERT(CHAR(10), ord.OrderDate, 120)
		,TotalAmount = SUM(ord.UnitPrice)
		,TotalQty = SUM(ord.OrderQty)
FROM dbo.SalesOrder AS ord
	INNER JOIN dbo.HondaAutoTypes AS hat
	ON ord.AutoID = hat.AutoID
GROUP BY CONVERT(CHAR(10), ord.OrderDate, 120)
ORDER BY 1 DESC

-- or we also can use IN(<list of constants>) to make it use batch mode.
SELECT 
		OrderDay = CONVERT(CHAR(10), ord.OrderDate, 120)
		,TotalAmount = SUM(ord.UnitPrice)
		,TotalQty = SUM(ord.OrderQty)
FROM dbo.SalesOrder AS ord
WHERE ord.AutoID IN(104,106)
GROUP BY CONVERT(CHAR(10), ord.OrderDate, 120)
ORDER BY 1 DESC

执行计划对比:
图片14.png
执行CPU和时间消耗对比:
图片15.png

UNION ALL

在大多数情况下UNION ALL操作会导致列存储表走Row Mode执行方式,这种方式相对于Batch Mode执行性能低很多。改写UNION ALL方式非常有技巧,具体的思路是:

先将UNION ALL的各个查询分之汇总出来,然后将各分支的汇总数据再UNION ALL起来,最后再做一次统计汇总,就是我们想要的结果集了。

-- DEMO 3: UNION ALL usually use row mode

IF OBJECT_ID('dbo.partSalesOrder', 'U') IS NOT NULL
BEGIN
	TRUNCATE TABLE dbo.partSalesOrder
	DROP TABLE dbo.partSalesOrder
END

SELECT TOP 100 *
	INTO dbo.partSalesOrder
FROM dbo.SalesOrder
WHERE OrderID < 2500000;

-- UNION ALL mostly use row mode
;WITH unionSalesOrder
AS
(
	SELECT *
	FROM dbo.SalesOrder AS ord
	UNION ALL
	SELECT *
	FROM dbo.partSalesOrder AS pord

)

SELECT 
	OrderDay = CONVERT(CHAR(10), ord.OrderDate, 120)
	,TotalAmount = SUM(ord.UnitPrice)
	,TotalQty = SUM(ord.OrderQty)
FROM dbo.AutoType AS at
	INNER JOIN unionSalesOrder AS ord
	ON ord.AutoID = at.AutoID
GROUP BY CONVERT(CHAR(10), ord.OrderDate, 120)
ORDER BY 1 DESC

-- UNION ALL workaround
;WITH unionSaleOrders
AS(
	SELECT 
		OrderDay = CONVERT(CHAR(10), ord.OrderDate, 120)
		,TotalAmount = SUM(ord.UnitPrice)
		,TotalQty = SUM(ord.OrderQty)
	FROM dbo.AutoType AS at
		INNER JOIN SalesOrder AS ord
		ON ord.AutoID = at.AutoID
	GROUP BY CONVERT(CHAR(10), ord.OrderDate, 120)
), unionPartSaleOrders
AS
(
	SELECT 
		OrderDay = CONVERT(CHAR(10), ord.OrderDate, 120)
		,TotalAmount = SUM(ord.UnitPrice)
		,TotalQty = SUM(ord.OrderQty)
	FROM dbo.AutoType AS at
		INNER JOIN dbo.partSalesOrder AS ord
		ON ord.AutoID = at.AutoID
	GROUP BY CONVERT(CHAR(10), ord.OrderDate, 120)
), unionAllData
AS
(
	SELECT *
	FROM unionSaleOrders
	UNION ALL
	SELECT *
	FROM unionPartSaleOrders
)
SELECT 
	OrderDay
	,TotalAmount = SUM(TotalAmount)
	,TotalQty = SUM(TotalQty)
FROM unionAllData
GROUP BY OrderDay
ORDER BY OrderDay DESC

执行计划的对比
图片16.png
执行过程CPU和时间消耗对比:
图片17.png

Scalar Aggregates

有时候,我们一个简单的统计查询记录总数的操作在列存储表中也可以有优化的写法,思想是将一个一次性的统计汇总化简为繁,使用分级汇总的方式获取一个较小的结果集,最后再求总和。这种优化方法的思想是利用了第一次统计汇总可以使用Batch Mode操作方式,从而大大提高查询效率。

-- DEMO 4: Scalar Aggregates
SELECT COUNT(*)
FROM dbo.SalesOrder

-- workaround 
;WITH salesOrderByAutoId([AutoID], cnt)
AS(
	SELECT [AutoID], count(*)
	FROM dbo.SalesOrder
	GROUP BY [AutoID]
)
SELECT SUM(cnt)
FROM salesOrderByAutoId

-- END DEMO 4

执行计划对比:
图片18.png
执行CPU和时间消耗对比:
图片19.png

Multiple DISTINCT Aggregates

当SQL Server有两个或者两个以上的DISTINCT聚合操作的时候,会产生Table Spool操作,这个动作的产生当然会伴随着Table Spool的读写操作,更要命的是SQL Server对Table Spool读写操作是单线程的并且采用Row Mode方式执行,所以执行效率非常低下。改写的方式是采用将多个DISTINCT操作分开汇总,最后再INNER JOIN在一起,以此来避免Table Spool操作。

-- DEMO 5: Multiple DISTINCT Aggregates
SELECT 
		OrderDay = CONVERT(CHAR(10), ord.OrderDate, 120)
		,AutoIdCount = COUNT(DISTINCT ord.[AutoID])
		,UserIdCount = COUNT(DISTINCT ord.[UserID])
FROM dbo.AutoType AS at
	INNER JOIN dbo.SalesOrder AS ord
	ON ord.AutoID = at.AutoID
GROUP BY CONVERT(CHAR(10), ord.OrderDate, 120)

-- workaround
;WITH autoIdsCount(orderDay, AutoIdCount)
AS(
	SELECT 
			OrderDay = CONVERT(CHAR(10), ord.OrderDate, 120)
			,AutoIdCount = COUNT(DISTINCT ord.[AutoID])
	FROM dbo.AutoType AS at
		INNER JOIN dbo.SalesOrder AS ord
		ON ord.AutoID = at.AutoID
	GROUP BY CONVERT(CHAR(10), ord.OrderDate, 120)
), userIdsCount(orderDay, UserIdCount)
AS(
	SELECT 
			OrderDay = CONVERT(CHAR(10), ord.OrderDate, 120)
			,UserIdCount = COUNT(DISTINCT ord.[UserID])
	FROM dbo.AutoType AS at
		INNER JOIN dbo.SalesOrder AS ord
		ON ord.AutoID = at.AutoID
	GROUP BY CONVERT(CHAR(10), ord.OrderDate, 120)
)
SELECT
	auto.orderDay
	,auto.AutoIdCount
	,ur.UserIdCount
FROM autoIdsCount AS auto
	INNER JOIN userIdsCount AS ur
	ON auto.orderDay = ur.orderDay
-- END DEMO 5

执行计划对比:
图片20.png

执行CPU和时间消耗对比:
图片21.png

限制条件

我们看到列存储索引对统计查询(数据分析OLAP或者数据仓库Data Warehouse场景)性能有非常显著的提升,按照微软SQL Server的一向“恶习”,新特性的推出都附带有很多的限制条件,当然列存储索引也不例外。而这些限制条件在不同的SQL Server 版本中又各不相同,这一小节,让我们来看看列存储索引在SQL Server 2012版本中的诸多限制。

SQL Server 2012列存储索引表的限制

  • 每张表只允许创建一个列存储索引

  • 不支持创建Clustered列存储索引,仅支持Nonclustered格式(注:从SQL Server 2014及以后的产品已经支持创建Clustered列存储索引)

  • 不支持创建Unique和Filtered列存储索引

  • 不支持对列存储索引的排序ASC或者DESC功能

  • 计算列不允许包含在列存储索引中

  • 列存储索引不支持在线创建(ONLINE选项)

  • 列存储索引不支持Include字句

  • 不允许重组列存储索引

  • 不支持在视图上创建列存储索引

  • 创建了列存储索引的表会成为只读表,不允许UPDATE、DELETE和INSERT(注:从SQL Server 2014及以后开始已经提供可更新列存储索引)

  • 列存储索引包含的列不能超过1024

  • 列存储索引不能包含稀疏列

  • 列存储索引不能包含Filestream列

  • 列存储索引不关心行记录数和行数据库分布,所以不使用统计信息

  • 列存储索引不能与以下功能共同使用
    • 数据库复制技术(Replication)
    • 更改跟踪(Change Tracking)
    • 变更数据库捕获(Data Change Capture)
    • 文件流(Filestream)
    • 行、列压缩功能(Data Compression)
  • 列存储索引包含列字段数据类型不允许:
    • 二进制数据类型:Binary、varbinary
    • BLOB数据类型:ntext、text、image、varchar(max) 、nvarchar(max)、xml
    • Uniqueidentifier
    • Rowversion和timestamp
    • sql_variant
    • 精度大于18位的decimal和numeric
    • 标量大于2的datetimeoffset
    • CLR 类型

限制测试

以下是SQL Server 2012诸多限制的一个简单测试。

/*--=====================================================
MS SQL Server 2012 Columnstore index limitations
--=====================================================*/

-- Just accept only one columnstore index
-- create another one
CREATE NONCLUSTERED COLUMNSTORE INDEX NCSIX_SalesOrder 
ON dbo.SalesOrder(OrderID,AutoID,OrderQty,Price,OrderDate);
GO

-- Does not support CLUSTERED columnstore index
CREATE CLUSTERED COLUMNSTORE INDEX CCSIX_SalesOrder 
ON dbo.SalesOrder(OrderID,AutoID,OrderQty,Price,OrderDate);
GO

-- Does not support UNIQUE columnstore index
CREATE UNIQUE COLUMNSTORE INDEX UCSIX_SalesOrder 
ON dbo.SalesOrder(OrderID,AutoID,OrderQty,Price,OrderDate);
GO

-- Does not accept ASC/DESC
CREATE NONCLUSTERED COLUMNSTORE INDEX NCSIX_SalesOrder_OrderDate
ON dbo.SalesOrder(OrderDate ASC);
GO

-- Does not accept computed column
CREATE NONCLUSTERED COLUMNSTORE INDEX NCSIX_SalesOrder_OrderDate
ON dbo.SalesOrder(OrderDate);
GO

-- Does not support online build index
CREATE NONCLUSTERED COLUMNSTORE INDEX NCSIX_SalesOrder_OrderDate
ON dbo.SalesOrder(OrderDate)
WITH (ONLINE = ON);
GO

-- Does not support include action
CREATE NONCLUSTERED COLUMNSTORE INDEX NCSIX_SalesOrder_OrderDate_@OrderID
ON dbo.SalesOrder(OrderDate)
INCLUDE(OrderID);
GO

-- Does not accept data length more than 18 numeric
CREATE NONCLUSTERED COLUMNSTORE INDEX NCSIX_SalesOrder_UnitPrice
ON dbo.SalesOrder(UnitPrice);
GO

-- Doesn't allow ALTER INDEX REORGANIZE
ALTER INDEX NCSIX_ALL_Columns
ON dbo.SalesOrder REORGANIZE;

-- Doesn't support create base on view
IF OBJECT_ID('dbo.V_SalesOrder', 'V') IS NOT NULL
	DROP TABLE dbo.V_SalesOrder
GO
CREATE VIEW dbo.V_SalesOrder
AS
SELECT TOP 100 *
FROM dbo.SalesOrder
GO

CREATE NONCLUSTERED COLUMNSTORE INDEX NCSIX_V_SalesOrder_ALL_Columns 
ON dbo.V_SalesOrder(OrderID,AutoID,OrderQty,Price,OrderDate);
GO

-- columnstore index table will be read only
UPDATE TOP (1) A
SET OrderQty = 100
FROM dbo.SalesOrder AS A

DELETE TOP(1) A
FROM dbo.SalesOrder AS A

INSERT INTO dbo.SalesOrder(OrderID, AutoID, OrderQty, Price, OrderDate)
SELECT TOP 1 OrderID = OrderID + 1, AutoID, OrderQty, Price, OrderDate
FROM dbo.SalesOrder

GO
-- There is no statistics for columnstore indexes
DBCC SHOW_STATISTICS('dbo.SalesOrder','NCSIX_ALL_Columns')

错误信息如下:

Msg 35339, Level 16, State 1, Line 97
Multiple nonclustered columnstore indexes are not supported.
Msg 35338, Level 15, State 1, Line 103
Clustered columnstore index is not supported.
Msg 35301, Level 15, State 1, Line 107
CREATE INDEX statement failed because a columnstore index cannot be unique. Create the columnstore index without the UNIQUE keyword or create a unique index without the COLUMNSTORE keyword.
Msg 35302, Level 15, State 1, Line 112
CREATE INDEX statement failed because specifying sort order (ASC or DESC) is not allowed when creating a columnstore index. Create the columnstore index without specifying a sort order.
Msg 35339, Level 16, State 1, Line 117
Multiple nonclustered columnstore indexes are not supported.
Msg 35318, Level 15, State 1, Line 124
CREATE INDEX statement failed because the ONLINE option is not allowed when creating a columnstore index. Create the columnstore index without specifying the ONLINE option.
Msg 35311, Level 15, State 1, Line 128
CREATE INDEX statement failed because a columnstore index cannot have included columns.   Create the columnstore index on the desired columns without specifying any included columns.
Msg 35341, Level 16, State 1, Line 134
CREATE INDEX statement failed. A columnstore index cannot include a decimal or numeric data type with a precision greater than 18.  Reduce the precision of column 'UnitPrice' to 18 or omit column 'UnitPrice'.
Msg 35326, Level 16, State 1, Line 139
ALTER INDEX statement failed because a columnstore index cannot be reorganized. Reorganization of a columnstore index is not necessary.
Msg 2714, Level 16, State 3, Procedure V_SalesOrder, Line 1 [Batch Start Line 145]
There is already an object named 'V_SalesOrder' in the database.
Msg 35305, Level 16, State 1, Line 152
CREATE INDEX statement failed because a columnstore index cannot be created on a view. Consider creating a columnstore index on the base table or creating an index without the COLUMNSTORE keyword on the view.
Msg 35330, Level 15, State 1, Line 157
UPDATE statement failed because data cannot be updated in a table with a columnstore index. Consider disabling the columnstore index before issuing the UPDATE statement, then rebuilding the columnstore index after UPDATE is complete.

这里特别把列存储索引不存在统计信息截图如下:
图片22.png

如何解决列存储索引表只读的问题

从限制条件章节,我们知道SQL Server 2012的列存储索引表不允许进行DML操作,因为建立了列存储索引的表会自动变成只读表,那么我们如何解决这个问题呢?

升级到SQL Server 2014或更高版本

将SQL Server 2012数据库整个大版本升级到SQL Server 2014或者2016。因为从SQL Server 2014开始,列存储索引表已经支持更新操作。当然升级动作非常复杂,只是为了解决列存储表只读问题而升级版本,有点本末倒置,因此这个方法不是本文的讨论范畴。

禁用列存储索引

在SQL Server 2012中解决列存储索引表只读问题的方法是,在执行DML语句之前,先禁用列存储索引,完成DML以后,再重建列存储索引。这个过程相当于先删除列存储索引,DML操作后重新创建列存储索引。

-- DEMO: columnstore index table read_only fixing
USE ColumnStoreDB
GO

ALTER INDEX NCSIX_ALL_Columns
ON dbo.SalesOrder DISABLE;
GO

UPDATE TOP (1) A
SET OrderQty = OrderQty + 1
FROM dbo.SalesOrder AS A

GO
ALTER INDEX NCSIX_ALL_Columns
ON dbo.SalesOrder REBUILD;
GO

-- END DEMO

分区交换

分区交换方法的实现步骤如下:

  • 新建一个中间步骤表,表结构保持和列存储表结构一致,包括字段,数据类型,索引等

  • 初始化一部分数据,注意OrderID不能与列存储表OrderID有交集

  • 做中间步骤表的SWITCH动作到列存储表的一个空的分区上

-- DEMO: columnstore index table read_only fixing using partion switch
IF OBJECT_ID('dbo.SalesOrder_staging', 'U') IS NOT NULL
BEGIN
	DROP TABLE dbo.SalesOrder_staging
END
GO
CREATE TABLE dbo.SalesOrder_staging
(
	OrderID INT NOT NULL
	,AutoID INT NOT NULL
	,UserID INT NOT NULL
	,OrderQty INT NOT NULL
	,Price DECIMAL(8,2) NOT NULL
	,UnitPrice AS Price * OrderQty
	,OrderDate DATETIME NOT NULL,
    CONSTRAINT check_OrderDate CHECK ([OrderDate] > '2018-01-01 00:00' and [OrderDate]<='2019-01-01 00:00')
) ON [PRIMARY];

-- data init for 5 M records.
DECLARE
	@OrderID INT
;
SELECT @OrderID =  max(OrderID) 
FROM dbo.SalesOrder;

;WITH a 
AS (
	SELECT * 
	FROM (VALUES(1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) AS a(a)
), RoundData
AS(
SELECT TOP(500000)
	OrderID = @OrderID + ROW_NUMBER() OVER (ORDER BY a.a)
	,AutoIDRound = abs(checksum(newid()))
	,Price = a.a * b.a * 10000
	,OrderQty = a.a + b.a + c.a + d.a + e.a + f.a + g.a + h.a
FROM a, a AS b, a AS c, a AS d, a AS e, a AS f, a AS g, a AS h
)
INSERT INTO dbo.SalesOrder_staging(OrderID, AutoID, UserID, OrderQty, Price, OrderDate)
SELECT 
	OrderID
	,AutoID = cast(ROUND((13 * (AutoIDRound*1./cast(replace(AutoIDRound, AutoIDRound, '1' + replicate('0', len(AutoIDRound))) as bigint)) + 101), 0) as int)
	,UserID = cast(ROUND((500 * (AutoIDRound*1./cast(replace(AutoIDRound, AutoIDRound, '1' + replicate('0', len(AutoIDRound))) as bigint)) + 10000), 0) as int)
	,OrderQty
	,Price = cast(Price AS DECIMAL(8,2))
	,OrderDate = dateadd(day, cast(ROUND((330 * (AutoIDRound*1./cast(replace(AutoIDRound, AutoIDRound, '1' + replicate('0', len(AutoIDRound))) as bigint)) + 1), 0) as int) ,'2018-01-01')
FROM RoundData;
GO

-- create regular B-Tree Clustered Index
CREATE UNIQUE CLUSTERED INDEX CIX_OrderID
ON dbo.SalesOrder_staging(OrderID, OrderDate) 
WITH (ONLINE = ON)
ON [PRIMARY];
GO

--create nonclustered columnstore index
CREATE NONCLUSTERED COLUMNSTORE INDEX NCSIX_ALL_Columns 
ON dbo.SalesOrder_staging(OrderID, AutoID, UserID, OrderQty, Price, OrderDate)
GO


ALTER PARTITION scheme ps_SalesYear NEXT used [PRIMARY];

--alter partition function pf_SalesYear() split range ('2019-01-01 00:00');
--go

ALTER TABLE dbo.SalesOrder_staging switch TO dbo.SalesOrder 
PARTITION $PARTITION.pf_SalesYear('2018-01-01 01:00');
GO

注意:

  • SalesOrder_staging表的OrderID初始值比表SalesOrder表中的OrderID最大值要大

  • SalesOrder_staging表的Check约束必须要满足Partition函数的限制,比如LEFT操作是左开右闭

  • SalesOrder_staging表的索引结构必须和SalesOrder表索引结构保持一致,否则Switch会报错

  • 如果SalesOrder表初始分区数不够用,请使用ALTER PARTITION FUNCTION SPLIT方式分割出新的分区

  • 由于Partition函数是LEFT,左开右闭,所以最后的Switch传入日期不能是小于等于’2018-01-01 00:00’(我在这里踩了坑,花了很长时间才走出来),必须比这个时间点稍微大些,否则系统会认为数据属于前一个分区而导致Switch失败。

最后总结

这篇文章从列存储索引的几个基本概念引入,谈到了列存储索引的结构以及列存储索引对查询性能的提升,然后谈到了需要踩过的几个坑,再接着聊到了列存储索引在SQL Server 2012中的限制,最后讲我们如何破解列存储表只读的问题。由于篇幅原因,关于SQL Server 2014和2016中的列存储索引没有过多涉及。预知后事如何,且听下月分解。

引用文章

Columnstore Indexes in SQL Server 2012

How to update a table with a columnstore index

PgSQL · 乱入拜年 · 小鸡吉吉和小象Pi吉(PostgreSQL)的鸡年传奇

$
0
0

背景

我家有只小鸡鸡,它的名字叫吉吉。

20170120_01_pic_001.jpg

吉吉有一位铁杆鸡友大象Pi吉哥哥(PostgreSQL)。

20170120_01_pic_002.jpg

吉吉给大伙拜年啦,祝大家鸡年吉祥,新年新气息,与好鸡友大象哥哥愉快的玩耍,鸡情四射。

故事从吉吉和好鸡友大象哥哥偶遇的那天开始。

  • 有一天,大象哥哥摆下国际象棋擂台,邀请各路英豪前来,就这样和吉吉偶遇啦。

《想挑战AlphaGO吗?先和PostgreSQL玩一玩?? PostgreSQL与人工智能(AI)》

  • 我们家的吉吉很勤劳,每天早上都打鸣,勤劳的小伙伴闻鸡起舞(你收到我们吉吉发出的异步消息了吗)

《从微信小程序 到 数据库”小程序” , 鬼知道我经历了什么》

《从电波表到数据库小程序之 - 数据库异步广播(notify/listen)》

  • 吉吉虽然喜欢裸奔,但是安全第一它还是忍住了。

《PostgreSQL 数据库安全指南》

《DBA专供 冈本003系列 - 数据库安全第一,过个好年》

  • 吉吉对着大象藏了一个AR红包,你想不想来抢吉吉藏的红包呢?

《(AR虚拟现实)红包 技术思考 - GIS与图像识别的完美结合》

  • 吉吉最近有点体力消耗过度,有点健忘了,很多事情记不起来,自己的模糊的几个关键词,还好大象哥哥可以从模糊信息中帮吉吉回忆。

《PostgreSQL 全表 全字段 模糊查询的毫秒级高效实现 - 搜索引擎颤抖了》

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

《PostgreSQL 百亿数据 秒级响应 正则及模糊查询》

  • 吉吉的手机收藏了好多妹子的照片,还有好多小段子,小视频(嘿嘿),终于有一天手机空间不足了,想让好基友大象哥哥帮忙删掉一些重复或者相似的照片,还有相似的文档。大象哥哥高兴的答应了吉吉的请求。

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

《从相似度算法谈起 - Effective similarity search in PostgreSQL》

《PostgreSQL 在视频、图片去重,图像搜索业务中的应用》

《从真假美猴王谈起 - 让套牌车、克隆x 无处遁形的技术手段思考》

  • 天气不错,吉吉带着老婆、小孩、还有丈母娘老丈人一起去逛大象哥哥开的商场,老婆说,吉吉我们去看电影吧,小吉吉说要去玩木马王国,老丈人和丈母娘想去买衣服。还好是个大象哥哥开的是超级商场,这些当然都不在话下,吉吉一家人愉快的玩耍。

《元旦技术大礼包 - ApsaraDB的左右互搏术 - 解决企业痛处 TP+AP混合需求 - 无须再唱《爱你痛到不知痛》》

  • 吉吉的老婆开了个服装店,吉吉想帮她提高销售额,找大象哥哥帮忙,哇塞原来大象哥哥可以知道目标用户群体,还能实时推销呢。

《恭迎万亿级营销(圈人)潇洒的迈入毫秒时代 - 万亿user_tags级实时推荐系统数据库设计》

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

  • 吉吉是个环保主义者,它在很多地方部署了很多传感器,可以实时的获取雾霾指数、河流、湖泊的水质、天气的变化、土壤的指标。它把这些实时的数据交给了大象哥哥。吉吉想知道这些数据是不是正常的,不正常的时候吉吉要第一时间收到短信,如果雾霾严重吉吉就知道该带口罩出门了。

《流计算风云再起 - PostgreSQL携PipelineDB力挺IoT》

《PostgreSQL 如何潇洒的处理每天上百TB的数据增量》

《PostgreSQL 9.5 new feature - BRIN (block range index) index》

  • 吉吉部署的传感器太多了,一天要存下几百亿的数据,大象哥哥的硬盘空间告警,眼看就要存不下吉吉的数据了,还好大象哥哥有压缩技术,解除了爆仓危机。

《旋转门数据压缩算法在PostgreSQL中的实现 - 流式压缩在物联网、监控、传感器等场景的应用》

  • 吉吉存在大象哥哥那里的的数据量越来越大,大象哥哥找来了很多大象兄弟,它们一起帮吉吉存储。

《PostgreSQL 9.6 sharding based on FDW & pg_pathman》

  • 吉吉想对传感器的数据做一个抽样,大象哥哥很爽快的答应了吉吉的要求

《PostgreSQL 巧妙的数据采样方法》

  • 吉吉很讨厌人贩子,它和大象哥哥、警察叔叔一起想对策(大象哥哥提出了好多抓捕人贩子的技术,什么图像识别呀、关系演算呀、模糊查询呀、等等),抓捕人贩子。

《一场IT民工 与 人贩子 之间的战争 - 只要人人都献出一点爱》

《金融风控、公安刑侦、社会关系、人脉分析等需求分析与数据库实现 - PostgreSQL图数据库场景应用》

  • 吉吉是只很有社会责任感的鸡鸡,它听说过很多起危化品泄露、爆炸的事件,总想有什么对策可以帮助监管起来。大象哥哥愉快的接受了吉吉的建议。

《从天津滨海新区大爆炸、危化品监管聊聊 IT人背负的社会责任感》

  • 吉吉准备给新房子装修一番,它请了一名刷漆工,一名铺瓷砖,一名木匠、还有一名装水电。可惜他们每个人只会干一件事情,好慢哦。吉吉请大象哥哥帮忙,大象哥哥教会了工人所有的技能,这几个工人一起干活,很快就把房子装修好了。

《分析加速引擎黑科技 - LLVM、列存、多核并行、算子复用 大联姻 - 一起来开启PostgreSQL的百宝箱》

《PostgreSQL 并行计算tpc-h测试和优化分析》

《PostgreSQL 9.6 引领开源数据库攻克多核并行计算难题》

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

  • 吉吉的好基友大象哥哥生病了,吉吉找到了一本《黄帝内经》,望闻问切一番,药到病除。又可以和大象哥哥愉快的玩耍了。

《PostgreSQL 源码性能诊断(perf profiling)指南》

《PostgreSQL AWR报告》

《PostgreSQL 函数调试、诊断、优化 & auto_explain ?》

《PostgreSQL on Linux 最佳部署手册》

  • 这几天是双十一全球狂欢节,吉吉的老婆自然加入了狂欢的队伍,一整天都在搜索喜欢的商品,而且绝不错过每个小时的准点秒杀。吉吉看着满满的购物车,心那是哇凉哇凉的。

《聊一聊双十一背后的技术 - 不一样的秒杀技术, 裸秒》

《聊一聊双十一背后的技术 - 分词和搜索》

《聊一聊双十一背后的技术 - 毫秒分词算啥, 试试正则和相似度》

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

  • 吉吉很惊讶,老婆买的东西居然第二天就到货了。吉吉很奇怪,就找大象哥哥了解今年为什么双十一的物流这么快,大象哥哥把这个秘密告诉了吉吉。

《聊一聊双十一背后的技术 - 物流、动态路径规划》

  • 吉吉问大象哥哥,双十一这么多人来你这查询,你不怕崩溃吗,大象哥哥偷偷的告诉了吉吉这个秘密

《如何防止数据库雪崩》

  • 吉吉和大象哥哥打赌,龟兔赛跑,到底谁会赢呢?大象哥哥以它能够预知未来的小伎俩,赢得了胜利。

《官人要杯咖啡吗? - PostgreSQL实时监测PLAN tree的执行进度 - pg_query_state》

  • 吉吉大大小小有几千个老婆,它每天要安排老婆们上山锻炼,好生蛋嘛。但是总有一些小老婆会偷懒的,不知道躲哪里休闲去了。为了找出那些偷懒的小老婆,吉吉在山上某处安装了监控,只要进入那个范围就会上报信息到大象哥哥那里,凡是没有上报信息的老婆,就是偷懒的。因为上报的信息每天有几千万,就为了找到偷懒的老婆,吉吉每天要花好多时间来甄别。大象哥哥给吉吉找到了好方法,一眨眼的时间就能找出偷懒的老婆啦。老婆们再也不敢偷懒了。

《用PostgreSQL找回618秒逝去的青春 - 递归收敛优化》

  • 吉吉最近有点失落,找大象哥哥诉苦,没想到大象哥哥居然有算命的本事。

《用PostgreSQL描绘人生的高潮、尿点、低谷 - 窗口/帧 or 斜率/导数/曲率/微积分?》

  • 吉吉的老丈人喜欢玩点股票,但是股市有风险,入市须谨慎。吉吉的好朋友大象哥哥可以通过线性回归预测股价,但是他不敢告诉吉吉的老丈人,怕他沉迷股市。

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

《用PostgreSQL了解一些统计学术语以及计算方法和表示方法 - 1》

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

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

  • 吉吉家里买了个网络电视,它觉得搜索中文好麻烦,于是大象哥哥帮他出了一个点子,可以按照中文拼音首字母来搜索了

《在PostgreSQL中实现按拼音、汉字、拼音首字母搜索的例子》

  • 吉吉把越来越多的数据存在了大象哥哥那里保管,大象哥哥是最值得信赖的,因为它绝对的可靠,数据会存储多份,同时还可以帮吉吉做好加密的事情,比如吉吉的一些敏感数据,小视频呀,密码呀啥的。

《PostgreSQL 透明加密(TDE,FDE) - 块级加密》

《PostgreSQL 可靠性分析 - 关于redo block原子写》

《PostgreSQL 9.6 同步多副本 与 remote_apply事务同步级别》

  • 有一次吉吉不小心把存在大象哥哥那里的重要数据给删了,还好大象哥哥有备份,马上就找回来了。

《PostgreSQL 最佳实践 - 在线增量备份与任意时间点恢复》

《PostgreSQL 最佳实践 - 任意时间点恢复源码分析》

《PostgreSQL 最佳实践 - pg_rman 数据库恢复示例 与 软件限制解说》

  • 最近吉吉迷上了一款社交游戏,摇一摇可以找附近的人,找附近的加油站,饭馆等。大象哥哥告诉了吉吉这个查找附近的秘密。

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

  • 吉吉来采访一下车厢里的乘客,大家都买到春节回家的票了吗?乘客们都开心的回答,买到了。

《PostgreSQL 与 12306 抢火车票的思考》

  • 越来越多人开始和大象哥哥成为了好朋友,但是也有一些不懂大象哥哥的人,吉吉担心大象哥哥名誉受损,给大象哥哥来平反了。

《为PostgreSQL讨说法 - 浅析《UBER ENGINEERING SWITCHED FROM POSTGRES TO MYSQL》》

《PostgreSQL 数据库开发规范》

  • 吉吉把大象哥哥的前世今生写成了书,让大家更了解大象哥哥,不会踩坑,没有痛苦,没有烦恼。

《PostgreSQL 前世今生》

吉吉和大象(PostgreSQL)哥哥,祝大家吉祥如意,幸福安康,阖家欢乐!

记得要JIJI向上哦

20170120_01_pic_003.jpg

20170120_01_pic_004.jpg

Count


MySQL · 特性分析 · 5.7 error log 时区和系统时区不同

$
0
0

问题描述

现象

5.6 和 5.7 时区设置相同,select now()也显示当前时间

5.7 error log 中时间和当前时间差8小时

screenshot.png

问题分析

5.6 写 error log 函数如下

取时间的函数是localtime_r(&skr, &tm_tmp)

日志中时间和系统时区相同

    static void print_buffer_to_file(enum loglevel level, const char *buffer,                                                                                                                                       
                                     size_t length)
    {   
      time_t skr; 
      struct tm tm_tmp;
      struct tm *start;
      DBUG_ENTER("print_buffer_to_file");
      DBUG_PRINT("enter",("buffer: %s", buffer));
    
      mysql_mutex_lock(&LOCK_error_log);
    
      skr= my_time(0);
      localtime_r(&skr, &tm_tmp);
      start=&tm_tmp;
    
      fprintf(stderr, "%d-%02d-%02d %02d:%02d:%02d %lu [%s] %.*s\n",
              start->tm_year + 1900,
              start->tm_mon + 1,  
              start->tm_mday,
              start->tm_hour,
              start->tm_min,
              start->tm_sec,
              current_pid,
              (level == ERROR_LEVEL ? "ERROR" : level == WARNING_LEVEL ?
               "Warning" : "Note"),
              (int) length, buffer);
    
      fflush(stderr);
    
      mysql_mutex_unlock(&LOCK_error_log);
      DBUG_VOID_RETURN;
    }

5.7 写 error log 函数如下

取时间的函数是 make_iso8601_timestamp(my_timestamp)

    static void print_buffer_to_file(enum loglevel level, const char *buffer,
                                     size_t length)
    {
      DBUG_ENTER("print_buffer_to_file");
      DBUG_PRINT("enter",("buffer: %s", buffer));
 
      char my_timestamp[iso8601_size];
 
      my_thread_id thread_id= 0;
 
      /*
        If the thread system is up and running and we're in a connection,
        add the connection ID to the log-line, otherwise 0.
      */
      if (THR_THD_initialized && (current_thd != NULL))
        thread_id= current_thd->thread_id();
 
      make_iso8601_timestamp(my_timestamp);
 
      /*
        This must work even if the mutex has not been initialized yet.
        At that point we should still be single threaded so that it is
        safe to write without mutex.
      */
      if (error_log_initialized)
        mysql_mutex_lock(&LOCK_error_log);
 
      if (error_log_buffering)
      {
        // Logfile not open yet, buffer messages for now.
        if (buffered_messages == NULL)
          buffered_messages= new (std::nothrow) std::string();
        std::ostringstream s;
        s << my_timestamp << ""<< thread_id;
        if (level == ERROR_LEVEL)
          s << " [ERROR] ";
        else if (level == WARNING_LEVEL)
          s << " [Warning] ";
        else
          s << " [Note] ";
        s << buffer << std::endl;
        buffered_messages->append(s.str());
      }
      else
      {
        fprintf(stderr, "%s %u [%s] %.*s\n",
                my_timestamp,
                thread_id,
                (level == ERROR_LEVEL ? "ERROR" : level == WARNING_LEVEL ?
                 "Warning" : "Note"),
                (int) length, buffer);
 
        fflush(stderr);
      }
 
      if (error_log_initialized)
        mysql_mutex_unlock(&LOCK_error_log);
      DBUG_VOID_RETURN;
    }

make_iso8601_timestamp 中有代码段如下

参数 opt_log_timestamps 控制时间

 if (opt_log_timestamps == 0)
    gmtime_r(&seconds, &my_tm);
  else
  {   
    localtime_r(&seconds, &my_tm);

opt_log_timestamps 对应 sys_vars.cc 中的 log_timestamps

取值 const char *timestamp_type_names[]= {“UTC”, “SYSTEM”, NullS};

log_timestamps = 0 时,日志中是 UTC 时区

log_timestamps = 1 时,日志中是 SYSTEM 时区

5.7 默认 log_timestamps = 0

5.7 error log 使用系统时区

set global log_timestamps = 1;

TokuDB · 源码分析 · 一条query语句的执行过程

$
0
0

Mysql是基于代价cost来选择索引,如果一个表有好几个索引,optimizer会分别计算每个索引访问的代价,选择代价最小的索引进行访问,这个索引也被称为access path。

Pickup index

Mysql在执行query语句的时候会在server层计算每个可选索引的代价,并选择代价最小的索引作为访问路径(access path)去引擎读取数据。
server层的handler类为引擎层提供一个框架来计算索引的代价。

  1. scan_time:计算全表扫描需要执行时间
  2. records_in_range:计算索引在search condition范围内包含多少行数据
  3. read_time:计算索引range query执行时间

Tokudb的records_in_range:根据search condition区间的大小做不同的处理。

  • 如果search condition为NULL,并且start_key和end_key均为NULL,这个函数调用estimate_num_rows去读ft的内存统计信息in_memory_stats.numrows,得到索引有多少个<key,value>pair,也即unique key的个数。因为mysql的二级索引的key都会拼上pk,到了索引层所有的key都是unique的。
  • 把search condition的start_key和end_key封装成ft的key,调用ft的keys_range64函数计算落在<start_key,end_key>区间的key个数。less表示小于start_key的个数,equal1表示等于start_key的个数,middle表示大于等于start key且小于end_key的个数,equal2表示等于end_key的个数,greater表示大于等于end_key的个数。这个函数递归计算,代码比较多,但是不复杂。
  • 如果start_key和end_key落在同一个basement节点,就读取那个basement节点并把满足条件的记录条数返回给server层。
  • 如果search conditionn跨越多个basement节点,就需要把索引中存储的键值key个数(in_memory_stats.numrows)分摊到从root到leaf路径上的每一层节点上,这样得到每一层节点的权重。然后把start_key到end_key区间在每一层节点上的权重累加起来得到区间的记录条数。

当start_key和end_key不在同一个basement节点时,keys_range64是通过估算得到记录条数的。
估算的值受FT tree layout影响,FT tree可以是瘦高的,也可以是扁平的,不同的layout计算的结果可能会差别比较大。
而且,tokudb的键值key个数(in_memory_stats.numrows)也是个统计值,是每次在leaf节点做msn apply更新的,这个值也可能不准确。

ha_rows ha_tokudb::records_in_range(uint keynr, key_range* start_key, key_range* end_key) {
    DBT *pleft_key, *pright_key;
    DBT left_key, right_key;
    ha_rows ret_val = HA_TOKUDB_RANGE_COUNT;
    DB *kfile = share->key_file[keynr];
    uint64_t rows = 0;
    int error;

    // get start_rows and end_rows values so that we can estimate range
    // when calling key_range64, the only value we can trust is the value for less
    // The reason is that the key being passed in may be a prefix of keys in the DB
    // As a result, equal may be 0 and greater may actually be equal+greater
    // So, we call key_range64 on the key, and the key that is after it.
    if (!start_key && !end_key) {
        error = estimate_num_rows(kfile, &rows, transaction);
        if (error) {
            ret_val = HA_TOKUDB_RANGE_COUNT;
            goto cleanup;
        }
        ret_val = (rows <= 1) ? 1 : rows;
        goto cleanup;
    }
    if (start_key) {
        uchar inf_byte = (start_key->flag == HA_READ_KEY_EXACT) ? COL_NEG_INF : COL_POS_INF;
        pack_key(&left_key, keynr, key_buff, start_key->key, start_key->length, inf_byte);
        pleft_key = &left_key;
    } else {
        pleft_key = NULL;
    }
    if (end_key) {
        uchar inf_byte = (end_key->flag == HA_READ_BEFORE_KEY) ? COL_NEG_INF : COL_POS_INF;
        pack_key(&right_key, keynr, key_buff2, end_key->key, end_key->length, inf_byte);
        pright_key = &right_key;
    } else {
        pright_key = NULL;
    }
    // keys_range64 can not handle a degenerate range (left_key > right_key), so we filter here
    if (pleft_key && pright_key && tokudb_cmp_dbt_key(kfile, pleft_key, pright_key) > 0) {
        rows = 0;
    } else {
        uint64_t less, equal1, middle, equal2, greater;
        bool is_exact;
        error = kfile->keys_range64(kfile, transaction, pleft_key, pright_key,
                                    &less, &equal1, &middle, &equal2, &greater, &is_exact);
        if (error) {
            ret_val = HA_TOKUDB_RANGE_COUNT;
            goto cleanup;
        }
        rows = middle;
    }

    // MySQL thinks a return value of 0 means there are exactly 0 rows
    // Therefore, always return non-zero so this assumption is not made
    ret_val = (ha_rows) (rows <= 1 ? 1 : rows);

cleanup:
    if (tokudb_debug & TOKUDB_DEBUG_RETURN) {
        TOKUDB_HANDLER_TRACE("return %" PRIu64 " %" PRIu64, (uint64_t) ret_val, rows);
    }
    DBUG_RETURN(ret_val);
}

用户创建的表可能只有数据没有索引,也可能有好几个索引。optimizer选择索引的过程:用search condition找出可用索引的集合,然后尝试用每个可选索引计算代价,也就是计算read_time。这个值是根据records_in_range返回的记录条数计算出来的。
Optimizer会选择代价最小的索引(在server层被称为access path)去引擎里面取数据,访问access path的方式可能是index point query/index range query也可能是full index scan,也可能是full table scan。

Read data

选定了索引,server层会把索引信息(keynr)传给引擎,在引擎层创建索引的cursor去读取数据。一般来说,对于full table scan引擎层会隐式转成pk index scan。

Full table scan

我们先来看一下full table scan的函数:

  • rnd_init: 调用index_init隐式转为pk index scan,并锁表
  • rnd_next:调用get_next读取下一行数据,这个函数后面会详细讨论
  • rnd_end:结束scan
int ha_tokudb::rnd_init(bool scan) {
    int error = 0;
    range_lock_grabbed = false;
    error = index_init(MAX_KEY, 0);
    if (error) { goto cleanup;}

    if (scan) {
        error = prelock_range(NULL, NULL);
        if (error) { goto cleanup; }
        range_lock_grabbed = true;
    }

    error = 0;
cleanup:
    if (error) {
        index_end();
        last_cursor_error = error;
    }
    TOKUDB_HANDLER_DBUG_RETURN(error);
}

int ha_tokudb::rnd_next(uchar * buf) {
    ha_statistic_increment(&SSV::ha_read_rnd_next_count);
    int error = get_next(buf, 1, NULL, false);
    TOKUDB_HANDLER_DBUG_RETURN(error);
}

int ha_tokudb::rnd_end() {
    range_lock_grabbed = false;
    TOKUDB_HANDLER_DBUG_RETURN(index_end());
}

Index scan

ICP

当search condition非空时,server层可能会选择使用ICP (index condition pushdown),把search condition下推到引擎层来做过滤。

  • keyno_arg:server层选的索引index:keyno_arg
  • idx_cond_arg:server层的search condition,可能包含过滤条件
Item* ha_tokudb::idx_cond_push(uint keyno_arg, Item* idx_cond_arg) {
    toku_pushed_idx_cond_keyno = keyno_arg;
    toku_pushed_idx_cond = idx_cond_arg;
    return idx_cond_arg;
}

Index init

前面提到full table scan会隐式转为pk index scan,在rnd_init中调用index_init把tokudb_active_index设置为primary_key。
Handler类成员active_index表示当前索引index,这个值等于MAX_KEY(64)表示full table scan。
Tokudb类成员tokudb_active_index表示tokudb当前的索引index,一般来说这个值跟active_index是一样的。
Full table scan是个例外,active_index等于MAX_KEY,tokudb_active_index等于primary_key。
Index_init中最重要的工作就是创建cursor,并且重置bulk fetch信息。bulk fetch将在get_next函数中详细讨论。

int ha_tokudb::index_init(uint keynr, bool sorted) {
    int error;
    THD* thd = ha_thd();

    /*
       Under some very rare conditions (like full joins) we may already have
       an active cursor at this point
     */
    if (cursor) {
        int r = cursor->c_close(cursor);
        assert(r==0);
        remove_from_trx_handler_list();
    }
    active_index = keynr;

    if (active_index < MAX_KEY) {
        DBUG_ASSERT(keynr <= table->s->keys);
    } else {
        DBUG_ASSERT(active_index == MAX_KEY);
        keynr = primary_key;
    }
    tokudb_active_index = keynr;

#if TOKU_CLUSTERING_IS_COVERING
    if (keynr < table->s->keys && table->key_info[keynr].option_struct->clustering)
        key_read = false;
#endif

    last_cursor_error = 0;
    range_lock_grabbed = false;
    range_lock_grabbed_null = false;
    DBUG_ASSERT(share->key_file[keynr]);
    cursor_flags = get_cursor_isolation_flags(lock.type, thd);
    if (use_write_locks) {
        cursor_flags |= DB_RMW;
    }
    if (get_disable_prefetching(thd)) {
        cursor_flags |= DBC_DISABLE_PREFETCHING;
    }
    if ((error = share->key_file[keynr]->cursor(share->key_file[keynr], transaction, &cursor, cursor_flags))) {
        last_cursor_error = error;
        cursor = NULL;             // Safety
        goto exit;
    }
    cursor->c_set_check_interrupt_callback(cursor, tokudb_killed_thd_callback, thd);
    memset((void *) &last_key, 0, sizeof(last_key));

    add_to_trx_handler_list();

    if (thd_sql_command(thd) == SQLCOM_SELECT) {
        set_query_columns(keynr);
        unpack_entire_row = false;
    }
    else {
        unpack_entire_row = true;
    }
    invalidate_bulk_fetch();
    doing_bulk_fetch = false;
    maybe_index_scan = false;
    error = 0;
exit:
    TOKUDB_HANDLER_DBUG_RETURN(error);
}

Prepare index

初始化cursor之后,server层会调下面四个函数之一去拿<start_key,end_key>区间的range锁。

  • prepare_index_scan
  • prepare_index_key_scan
  • prepare_range_scan
  • read_range_first

这四个函数都是调用prelock_range去拿rangelock。

  • prepare_index_scan拿的是<负无穷,正无穷>区间的rangelock,其实就是锁表。
  • prepare_index_key_scan只拿对应key的rangelock,
  • prepare_range_scan和read_range_first都是拿<start_key,end_key>区间的rangelock。前者是处理reverse index range scan的,后者是处理index range scan的。

Start_key和end_key就是server层传下来的range区间的起点和终点,是server层的数据结构,prelock_range会生成相应的索引key并获取索引key的rangelock。

Full table scan的时候,rnd_init直接调用prelock_range拿<负无穷,正无穷>区间的rangelock,也就是锁表。
由于full table scan转pk index scan是在引擎内部做隐式转换,sever层并不知道,不走prepare_index_scan。

Rangelock的机制在之前的月报有提到。
如果既不是SERIALIZABLE隔离级别,也不是为写操作读取数据,调用prelock_range是不会真的去拿rangelock锁的。此种情况的rangelock锁是在query成功返回前拿的,防止并发事务更新相应的数据。

static int
c_set_bounds(DBC *dbc, const DBT *left_key, const DBT *right_key, bool pre_acquire, int out_of_range_error) {
    if (out_of_range_error != DB_NOTFOUND &&
        out_of_range_error != TOKUDB_OUT_OF_RANGE &&
        out_of_range_error != 0) {
        return toku_ydb_do_error(
            dbc->dbp->dbenv,
            EINVAL,
            "Invalid out_of_range_error [%d] for %s\n",
            out_of_range_error,
            __FUNCTION__
            );
    }
    if (left_key == toku_dbt_negative_infinity() && right_key == toku_dbt_positive_infinity()) {
        out_of_range_error = 0;
    }
    DB *db = dbc->dbp;
    DB_TXN *txn = dbc_struct_i(dbc)->txn;
    HANDLE_PANICKED_DB(db);
    toku_ft_cursor_set_range_lock(dbc_ftcursor(dbc), left_key, right_key,
                                   (left_key == toku_dbt_negative_infinity()),
                                   (right_key == toku_dbt_positive_infinity()),
                                   out_of_range_error);
    if (!db->i->lt || !txn || !pre_acquire)
        return 0;
    //READ_UNCOMMITTED and READ_COMMITTED transactions do not need read locks.
    if (!dbc_struct_i(dbc)->rmw && dbc_struct_i(dbc)->iso != TOKU_ISO_SERIALIZABLE)
        return 0;

    toku::lock_request::type lock_type = dbc_struct_i(dbc)->rmw ?
        toku::lock_request::type::WRITE : toku::lock_request::type::READ;
    int r = toku_db_get_range_lock(db, txn, left_key, right_key, lock_type);
    return r;
}

Read index

如果server层指定了range的start_key和end_key,handler的执行框架会根据execution plan指定的方式访问索引数据。

第一行数据的访问方式:

  • Index point query:server层直接调用index_read
  • Index range scan且start_key非空:server层通常是调用read_range_first函数读取第一行数据。read_range_first最终也是调用index_read
  • Index range scan且start_key为空:server层直接调用index_first
  • Index reverse range scan且end_key非空:server层通常是调用index_read读数据
  • Index reverse range scan且end_key为空:server层直接调用index_last
  • Full index scan:server层直接调用index_first
  • Reverse full index scan:server层直接调用index_last

Index_read函数比较长,举几个常见的场景来说明
1) index point query:

  • HA_READ_KEY_EXACT

2) index range query:

  • HA_READ_AFTER_KEY:处理大于start_key的情况
  • HA_READ_KEY_OR_NEXT:处理大于等于start_key的情况

3) reverse index range query:

  • HA_READ_BEFORE_KEY:处理小于end_key的情况
  • HA_READ_PREFIX_LAST_OR_PREV:处理小于等于end_key的情况
int ha_tokudb::index_read(
    uchar* buf,
    const uchar* key,
    uint key_len,
    enum ha_rkey_function find_flag) {

    invalidate_bulk_fetch();

    DBT row;
    DBT lookup_key;
    int error = 0;
    uint32_t flags = 0;
    THD* thd = ha_thd();
    tokudb_trx_data* trx = (tokudb_trx_data*)thd_get_ha_data(thd, tokudb_hton);
    struct smart_dbt_info info;
    struct index_read_info ir_info;

    HANDLE_INVALID_CURSOR();

    // if we locked a non-null key range and we now have a null key, then
    // remove the bounds from the cursor
    if (range_lock_grabbed &&
        !range_lock_grabbed_null &&
        index_key_is_null(table, tokudb_active_index, key, key_len)) {
        range_lock_grabbed = range_lock_grabbed_null = false;
        cursor->c_remove_restriction(cursor);
    }

    ha_statistic_increment(&SSV::ha_read_key_count);
    memset((void *) &row, 0, sizeof(row));

    info.ha = this;
    info.buf = buf;
    info.keynr = tokudb_active_index;

    ir_info.smart_dbt_info = info;
    ir_info.cmp = 0;

    flags = SET_PRELOCK_FLAG(0);
    switch (find_flag) {
    case HA_READ_KEY_EXACT: /* Find first record else error */ {
        pack_key(&lookup_key, tokudb_active_index, key_buff3, key, key_len, COL_NEG_INF);
        DBT lookup_bound;
        pack_key(&lookup_bound, tokudb_active_index, key_buff4, key, key_len, COL_POS_INF);
        ir_info.orig_key = &lookup_key;
        error = cursor->c_getf_set_range_with_bound(cursor, flags, &lookup_key, &lookup_bound, SMART_DBT_IR_CALLBACK(key_read), &ir_info);
        if (ir_info.cmp) {
            error = DB_NOTFOUND;
        }
        break;
    }
    case HA_READ_AFTER_KEY: /* Find next rec. after key-record */
        pack_key(&lookup_key, tokudb_active_index, key_buff3, key, key_len, COL_POS_INF);
        error = cursor->c_getf_set_range(cursor, flags, &lookup_key, SMART_DBT_CALLBACK(key_read), &info);
        break;
    case HA_READ_BEFORE_KEY: /* Find next rec. before key-record */
        pack_key(&lookup_key, tokudb_active_index, key_buff3, key, key_len, COL_NEG_INF);
        error = cursor->c_getf_set_range_reverse(cursor, flags, &lookup_key, SMART_DBT_CALLBACK(key_read), &info);
        break;
    case HA_READ_KEY_OR_NEXT: /* Record or next record */
        pack_key(&lookup_key, tokudb_active_index, key_buff3, key, key_len, COL_NEG_INF);
        error = cursor->c_getf_set_range(cursor, flags, &lookup_key, SMART_DBT_CALLBACK(key_read), &info);
        break;
    //
    // This case does not seem to ever be used, it is ok for it to be slow
    //
    case HA_READ_KEY_OR_PREV: /* Record or previous */
        pack_key(&lookup_key, tokudb_active_index, key_buff3, key, key_len, COL_NEG_INF);
        ir_info.orig_key = &lookup_key;
        error = cursor->c_getf_set_range(cursor, flags, &lookup_key, SMART_DBT_IR_CALLBACK(key_read), &ir_info);
        if (error == DB_NOTFOUND) {
            error = cursor->c_getf_last(cursor, flags, SMART_DBT_CALLBACK(key_read), &info);
        }
        else if (ir_info.cmp) {
            error = cursor->c_getf_prev(cursor, flags, SMART_DBT_CALLBACK(key_read), &info);
        }
        break;
    case HA_READ_PREFIX_LAST_OR_PREV: /* Last or prev key with the same prefix */
        pack_key(&lookup_key, tokudb_active_index, key_buff3, key, key_len, COL_POS_INF);
        error = cursor->c_getf_set_range_reverse(cursor, flags, &lookup_key, SMART_DBT_CALLBACK(key_read), &info);
        break;
    case HA_READ_PREFIX_LAST:
        pack_key(&lookup_key, tokudb_active_index, key_buff3, key, key_len, COL_POS_INF);
        ir_info.orig_key = &lookup_key;
        error = cursor->c_getf_set_range_reverse(cursor, flags, &lookup_key, SMART_DBT_IR_CALLBACK(key_read), &ir_info);
        if (ir_info.cmp) {
            error = DB_NOTFOUND;
        }
        break;
    default:
        TOKUDB_HANDLER_TRACE("unsupported:%d", find_flag);
        error = HA_ERR_UNSUPPORTED;
        break;
    }
    error = handle_cursor_error(error,HA_ERR_KEY_NOT_FOUND,tokudb_active_index);
    if (!error && !key_read && tokudb_active_index != primary_key && !key_is_clustering(&table->key_info[tokudb_active_index])) {
        error = read_full_row(buf);
    }
    trx->stmt_progress.queried++;
    track_progress(thd);

cleanup:
    TOKUDB_HANDLER_DBUG_RETURN(error);
}

Index_read在调用ydb_cursor.cc中的回调函数时,flags参数初始化为0。ydb_cursor.cc中注册的回调函数会检查tokudb_cursor->rmw标记,如果tokudb_cursor->rmw是0,并且不是SERIALIZABLE隔离级别,函数
query_context_with_input_init会设置context的do_locking字段,告诉toku_ft_cursor_set_range在成功返回前去拿rangelock。

static int
c_getf_set_range_with_bound(DBC *c, uint32_t flag, DBT *key, DBT *key_bound, YDB_CALLBACK_FUNCTION f, void *extra) {
    HANDLE_PANICKED_DB(c->dbp);
    HANDLE_CURSOR_ILLEGAL_WORKING_PARENT_TXN(c);

    int r = 0;
    QUERY_CONTEXT_WITH_INPUT_S context; //Describes the context of this query.
    query_context_with_input_init(&context, c, flag, key, NULL, f, extra);
    while (r == 0) {
        //toku_ft_cursor_set_range will call c_getf_set_range_callback(..., context) (if query is successful)
        r = toku_ft_cursor_set_range(dbc_ftcursor(c), key, key_bound, c_getf_set_range_callback, &context);
        if (r == DB_LOCK_NOTGRANTED) {
            r = toku_db_wait_range_lock(context.base.db, context.base.txn, &context.base.request);
        } else {
            break;
        }
    }
    query_context_base_destroy(&context.base);
    return r;
}

static int
c_getf_set_range_callback(uint32_t keylen, const void *key, uint32_t vallen, const void *val, void *extra, bool lock_only) {
    QUERY_CONTEXT_WITH_INPUT super_context = (QUERY_CONTEXT_WITH_INPUT) extra;
    QUERY_CONTEXT_BASE       context       = &super_context->base;

    int r;
    DBT found_key = { .data = (void *) key, .size = keylen };

    //Lock:
    //  left(key,val)  = (input_key, -infinity)
    //  right(key) = found ? found_key : infinity
    //  right(val) = found ? found_val : infinity
    if (context->do_locking) {
        const DBT *left_key = super_context->input_key;
        const DBT *right_key = key != NULL ? &found_key : toku_dbt_positive_infinity();
        r = toku_db_start_range_lock(context->db, context->txn, left_key, right_key, query_context_determine_lock_type(context), &context->request);
    } else {
        r = 0;
    }

    //Call application-layer callback if found and locks were successfully obtained.
    if (r==0 && key!=NULL && !lock_only) {
        DBT found_val = { .data = (void *) val, .size = vallen };
        context->r_user_callback = context->f(&found_key, &found_val, context->f_extra);
        r = context->r_user_callback;
    }

    //Give ft-layer an error (if any) to return from toku_ft_cursor_set_range
    return r;
}

Get next

取到第一行数据后,server层会根据execution plan来调用 index_next(index_next_same)或者index_prev来取后面的记录。

  • index_next_same:读取相同index的下一个记录。
  • index_next:读取range区间内的下一个记录,如果设置ICP,还会对找到的记录进行过滤条件匹配。
  • index_prev:读取range区间内的上一个记录,如果设置ICP,还会对找到的记录进行过滤条件匹配。

还有两个读取数据的方法:

  • index_first:读取index的第一条记录
  • index_last:读取index最后一条记录

这几个函数比较简单,这里只分析index_next函数,感兴趣的朋友可以自行分析其余的函数。
index_next直接调用get_next函数读取下一条记录。

int ha_tokudb::index_next(uchar * buf) {
    TOKUDB_HANDLER_DBUG_ENTER("");
    ha_statistic_increment(&SSV::ha_read_next_count);
    int error = get_next(buf, 1, NULL, key_read);
    TOKUDB_HANDLER_DBUG_RETURN(error);
}

Bulk fetch

Tokudb为range query做了一个优化,被称作bulk fetch。对当前basement节点上落在range区间的key进行批量读取,一次msg apply多次读key操作,同时也减轻leaf节点读写锁争抢,避免频繁拿锁放锁。
Tokudb为提供bulk fetch功能,增加了如下几个数据成员:

  • doing_bulk_fetch:标记是否正在进行bulk fetch
  • range_query_buff:缓存批量读取数据的buffer
  • size_range_query_buff:range_query_buff的malloc_size
  • bytes_used_in_range_query_buff:range_query_buff的实际size
  • curr_range_query_buff_offset:range_query_buff的当前位置
  • bulk_fetch_iteration和rows_fetched_using_bulk_fetch是统计数据,控制批量大小
class ha_tokudb : public handler {
private:
    ...
    uchar* range_query_buff; // range query buffer
    uint32_t size_range_query_buff; // size of the allocated range query buffer
    uint32_t bytes_used_in_range_query_buff; // number of bytes used in the range query buffer
    uint32_t curr_range_query_buff_offset; // current offset into the range query buffer for queries to read
    uint64_t bulk_fetch_iteration;
    uint64_t rows_fetched_using_bulk_fetch;
    bool doing_bulk_fetch;
    ...
};

Get_next函数首先判读是否可以从当前的bulk fetch bufffer中读取数据,判读的标准是bytes_used_in_range_query_buff - curr_range_query_buff_offset > 0,表示bulk fetch buffer有数据可以读取。

  1. 如果条件成立,调用read_data_from_range_query_buff直接从bulk fetch buffer中读数据。
  2. 如果bulk fetch buffer没有数据可读了,需要检查icp_went_out_of_range判断是否已超出range范围,那样的话表示没有更多数据,可以直接返回。
  3. 如果前面两个条件都不满足,需要调用cursor读取后面的数据。如果是bulk fetch的情况,需要调用invalidate_bulk_fetch重置bulf fetch的数据结构。

如果用户禁掉bulk fetch的功能,该如何处理呢?禁掉bulk fetch,第1和第2两个条件都不满足,直接执行invalidate_bulk_fetch,然后检查doing_bulk_fetch标记为false,调用cursor读取数据。这部分比较简单,请读者自行分析。

bulk fetch的处理跟非bulk fetch的处理类似,最大的区别在于cursor->c_getf_XXX方法的回调函数和回调函数参数。它们分别被设置为smart_dbt_bf_callback和struct smart_dbt_bf_info结构的指针。

smart_dbt_bf_info结构告诉回调函数如何缓存当前数据,并且如何读取下一行数据。

  • ha:tokudb handler指针
  • need_val:bulk fetch buffer是否要缓存value,对于pk和cluster index情况设置成true,其他情况为false
  • director:读取数据的方向。1表示next,-1表示prev
  • thd:server层的线程指针
  • buf:server层提供的buffer
  • key_to_compare:比较key,只有index_next_same需要设置这个参数
typedef struct smart_dbt_bf_info {
    ha_tokudb* ha;
    bool need_val;
    int direction;
    THD* thd;
    uchar* buf;
    DBT* key_to_compare;
} *SMART_DBT_BF_INFO

一个批量读取完成后,get_next会调用read_data_from_range_query_buff,从bulk fetch buffer中取数据。

Get_next成功读取一行数据后,需要判断是否是需要回表读取row。对于pk和cluster index的情况,index中存储了完整数据不需要回表。

int ha_tokudb::get_next(
    uchar* buf,
    int direction,
    DBT* key_to_compare,
    bool do_key_read) {

    int error = 0;
    HANDLE_INVALID_CURSOR();

    if (maybe_index_scan) {
        maybe_index_scan = false;
        if (!range_lock_grabbed) {
            error = prepare_index_scan();
        }
    }

    if (!error) {
        uint32_t flags = SET_PRELOCK_FLAG(0);

        // we need to read the val of what we retrieve if
        // we do NOT have a covering index AND we are using a clustering secondary
        // key
        bool need_val =
            (do_key_read == 0) &&
            (tokudb_active_index == primary_key ||
             key_is_clustering(&table->key_info[tokudb_active_index]));

        if ((bytes_used_in_range_query_buff -
             curr_range_query_buff_offset) > 0) {
            error = read_data_from_range_query_buff(buf, need_val, do_key_read);
        } else if (icp_went_out_of_range) {
            icp_went_out_of_range = false;
            error = HA_ERR_END_OF_FILE;
        } else {
            invalidate_bulk_fetch();
            if (doing_bulk_fetch) {
                struct smart_dbt_bf_info bf_info;
                bf_info.ha = this;
                // you need the val if you have a clustering index and key_read is not 0;
                bf_info.direction = direction;
                bf_info.thd = ha_thd();
                bf_info.need_val = need_val;
                bf_info.buf = buf;
                bf_info.key_to_compare = key_to_compare;
                //
                // call c_getf_next with purpose of filling in range_query_buff
                //
                rows_fetched_using_bulk_fetch = 0;
                // it is expected that we can do ICP in the smart_dbt_bf_callback
                // as a result, it's possible we don't return any data because
                // none of the rows matched the index condition. Therefore, we need
                // this while loop. icp_out_of_range will be set if we hit a row that
                // the index condition states is out of our range. When that hits,
                // we know all the data in the buffer is the last data we will retrieve
                while (bytes_used_in_range_query_buff == 0 &&
                       !icp_went_out_of_range && error == 0) {
                    if (direction > 0) {
                        error =
                            cursor->c_getf_next(
                                cursor,
                                flags,
                                smart_dbt_bf_callback,
                                &bf_info);
                    } else {
                        error =
                            cursor->c_getf_prev(
                                cursor,
                                flags,
                                smart_dbt_bf_callback,
                                &bf_info);
                    }
                }
                // if there is no data set and we went out of range,
                // then there is nothing to return
                if (bytes_used_in_range_query_buff == 0 &&
                    icp_went_out_of_range) {
                    icp_went_out_of_range = false;
                    error = HA_ERR_END_OF_FILE;
                }
                if (bulk_fetch_iteration < HA_TOKU_BULK_FETCH_ITERATION_MAX) {
                    bulk_fetch_iteration++;
                }

                error =
                    handle_cursor_error(
                        error,
                        HA_ERR_END_OF_FILE,
                        tokudb_active_index);
                if (error) {
                    goto cleanup;
                }

                //
                // now that range_query_buff is filled, read an element
                //
                error =
                    read_data_from_range_query_buff(buf, need_val, do_key_read);
            } else {
                struct smart_dbt_info info;
                info.ha = this;
                info.buf = buf;
                info.keynr = tokudb_active_index;

                if (direction > 0) {
                    error =
                        cursor->c_getf_next(
                            cursor,
                            flags,
                            SMART_DBT_CALLBACK(do_key_read),
                            &info);
                } else {
                    error =
                        cursor->c_getf_prev(
                            cursor,
                            flags,
                            SMART_DBT_CALLBACK(do_key_read),
                            &info);
                }
                error =
                    handle_cursor_error(
                        error,
                        HA_ERR_END_OF_FILE,
                        tokudb_active_index);
            }
        }
    }

    //
    // at this point, one of two things has happened
    // either we have unpacked the data into buf, and we
    // are done, or we have unpacked the primary key
    // into last_key, and we use the code below to
    // read the full row by doing a point query into the
    // main table.
    //
    if (!error &&
        !do_key_read &&
        (tokudb_active_index != primary_key) &&
        !key_is_clustering(&table->key_info[tokudb_active_index])) {
        error = read_full_row(buf);
    }

    if (!error) {
        THD *thd = ha_thd();
        tokudb_trx_data* trx =
            static_cast<tokudb_trx_data*>(thd_get_ha_data(thd, tokudb_hton));
        trx->stmt_progress.queried++;
        track_progress(thd);
        if (thd_killed(thd))
            error = ER_ABORTING_CONNECTION;
    }
cleanup:
    return error;
}

下面我们一起看看回调函数smart_dbt_bf_callback的处理。这个函数是fill_range_query_buf简单封装,当成功读取一行索引数据后,把结果缓存到bulk fetch buffer中,并继续读取下一行数据。

static int smart_dbt_bf_callback(
    DBT const* key,
    DBT const* row,
    void* context) {
    SMART_DBT_BF_INFO info = (SMART_DBT_BF_INFO)context;
    return
        info->ha->fill_range_query_buf(
            info->need_val,
            key,
            row,
            info->direction,
            info->thd,
            info->buf,
            info->key_to_compare);
}

接下来,让我们把目光聚焦在fill_range_query_buf函数。参数key和value是当前读取到索引key和value;其余的参数是从smart_dbt_bf_info中结构提取出来的server层调用时指定的信息。

如果指定了key_to_compare,需要判断当前读取的key是否等于key_to_compare,因为二级索引的key后面拼了pk,所以这里做的是前缀比较。如果前缀不匹配,表示已经读到一个新key,设置icp_went_out_of_range并退出。

如果server层设置了ICP信息,需要判断当前读取的索引key是否在range范围内。
一般来说,判断是否在range范围内的方法是跟prelocked_right_range(range scan)或者prelocked_left_range(reverse range scan)比较的。
而ICP的情况下,判断是否在range范围内是跟end_range做比较的。
对于索引key不在range范围内的情况,设置icp_went_out_of_range并返回。

如果当前读到的索引key是在range范围内,ICP的情况还要做过滤条件检查。如果满足过滤条件,就存储到bulk fetch buffer中;不满足过滤条件,就跳过这条记录取下一条。

把key存储到bulk fetch buffer中时,需要检查need_val。为true时,先存key后存value;否则,只存key。
Value要存的数据可能是整个row,可能是set_query_columns函数记录的那些字段的数据。如果是第二种情况,需要把相应字段的数据提取出来。
Bulk fetch buffer中的数据按照一定格式存储,先存4个字节的size,接着存data。
当前key的存储位置是在bytes_used_in_range_query_buff偏移位置。
把key/value数据缓存到bulk fetch buffer中以后,还需要更新bytes_used_in_range_query_buff指向下一次写入的位置。

对于非ICP的情况,在fill_range_query_buf函数的最后判断是否超出range范围。这里跟prelocked_right_range(range scan)或者prelocked_left_range(reverse range scan)做比较。这两个值是在prelock_range函数设置的,也是rangelock的范围。

如果当前读取的key属于range范围内,需要继续读取下一条数据到bulk fetch buffer中,fill_range_query_buf返回TOKUDB_CURSOR_CONTINUE告诉toku_ft_search继续读取当前basement节点的下一条数据。
bulk fetch不能跨越basement节点,因为无法保证其他basement节点上是否做过msg apply。

int ha_tokudb::fill_range_query_buf(
    bool need_val,
    DBT const* key,
    DBT const* row,
    int direction,
    THD* thd,
    uchar* buf,
    DBT* key_to_compare) {

    int error;
    //
    // first put the value into range_query_buf
    //
    uint32_t size_remaining =
        size_range_query_buff - bytes_used_in_range_query_buff;
    uint32_t size_needed;
    uint32_t user_defined_size = tokudb::sysvars::read_buf_size(thd);
    uchar* curr_pos = NULL;

    if (key_to_compare) {
        int cmp = tokudb_prefix_cmp_dbt_key(
            share->key_file[tokudb_active_index],
            key_to_compare,
            key);
        if (cmp) {
            icp_went_out_of_range = true;
            error = 0;
            goto cleanup;
        }
    }

    // if we have an index condition pushed down, we check it
    if (toku_pushed_idx_cond &&
        (tokudb_active_index == toku_pushed_idx_cond_keyno)) {
        unpack_key(buf, key, tokudb_active_index);
        enum icp_result result =
            toku_handler_index_cond_check(toku_pushed_idx_cond);

        // If we have reason to stop, we set icp_went_out_of_range and get out
        // otherwise, if we simply see that the current key is no match,
        // we tell the cursor to continue and don't store
        // the key locally
        if (result == ICP_OUT_OF_RANGE || thd_killed(thd)) {
            icp_went_out_of_range = true;
            error = 0;
            DEBUG_SYNC(ha_thd(), "tokudb_icp_asc_scan_out_of_range");
            goto cleanup;
        } else if (result == ICP_NO_MATCH) {
            // if we are performing a DESC ICP scan and have no end_range
            // to compare to stop using ICP filtering as there isn't much more
            // that we can do without going through contortions with remembering
            // and comparing key parts.
            if (!end_range &&
                direction < 0) {

                cancel_pushed_idx_cond();
                DEBUG_SYNC(ha_thd(), "tokudb_icp_desc_scan_invalidate");
            }

            error = TOKUDB_CURSOR_CONTINUE;
            goto cleanup;
        }
    }

    // at this point, if ICP is on, we have verified that the key is one
    // we are interested in, so we proceed with placing the data
    // into the range query buffer

    if (need_val) {
        if (unpack_entire_row) {
            size_needed = 2*sizeof(uint32_t) + key->size + row->size;
        } else {
            // this is an upper bound
            size_needed =
                // size of key length
                sizeof(uint32_t) +
                // key and row
                key->size + row->size +
                // lengths of varchars stored
                num_var_cols_for_query * (sizeof(uint32_t)) +
                // length of blobs
                sizeof(uint32_t);
        }
    } else {
        size_needed = sizeof(uint32_t) + key->size;
    }
    if (size_remaining < size_needed) {
        range_query_buff =
            static_cast<uchar*>(tokudb::memory::realloc(
                static_cast<void*>(range_query_buff),
                bytes_used_in_range_query_buff + size_needed,
                MYF(MY_WME)));
        if (range_query_buff == NULL) {
            error = ENOMEM;
            invalidate_bulk_fetch();
            goto cleanup;
        }
        size_range_query_buff = bytes_used_in_range_query_buff + size_needed;
    }
    //
    // now we know we have the size, let's fill the buffer, starting with the key
    //
    curr_pos = range_query_buff + bytes_used_in_range_query_buff;

    *reinterpret_cast<uint32_t*>(curr_pos) = key->size;
    curr_pos += sizeof(uint32_t);
    memcpy(curr_pos, key->data, key->size);
    curr_pos += key->size;
    if (need_val) {
        if (unpack_entire_row) {
            *reinterpret_cast<uint32_t*>(curr_pos) = row->size;
            curr_pos += sizeof(uint32_t);
            memcpy(curr_pos, row->data, row->size);
            curr_pos += row->size;
        } else {
            // need to unpack just the data we care about
            const uchar* fixed_field_ptr = static_cast<const uchar*>(row->data);
            fixed_field_ptr += table_share->null_bytes;

            const uchar* var_field_offset_ptr = NULL;
            const uchar* var_field_data_ptr = NULL;

            var_field_offset_ptr =
                fixed_field_ptr +
                share->kc_info.mcp_info[tokudb_active_index].fixed_field_size;
            var_field_data_ptr =
                var_field_offset_ptr +
                share->kc_info.mcp_info[tokudb_active_index].len_of_offsets;

            // first the null bytes
            memcpy(curr_pos, row->data, table_share->null_bytes);
            curr_pos += table_share->null_bytes;
            // now the fixed fields
            //
            // first the fixed fields
            //
            for (uint32_t i = 0; i < num_fixed_cols_for_query; i++) {
                uint field_index = fixed_cols_for_query[i];
                memcpy(
                    curr_pos,
                    fixed_field_ptr + share->kc_info.cp_info[tokudb_active_index][field_index].col_pack_val,
                    share->kc_info.field_lengths[field_index]);
                curr_pos += share->kc_info.field_lengths[field_index];
            }

            //
            // now the var fields
            //
            for (uint32_t i = 0; i < num_var_cols_for_query; i++) {
                uint field_index = var_cols_for_query[i];
                uint32_t var_field_index =
                    share->kc_info.cp_info[tokudb_active_index][field_index].col_pack_val;
                uint32_t data_start_offset;
                uint32_t field_len;

                get_var_field_info(
                    &field_len,
                    &data_start_offset,
                    var_field_index,
                    var_field_offset_ptr,
                    share->kc_info.num_offset_bytes);
                memcpy(curr_pos, &field_len, sizeof(field_len));
                curr_pos += sizeof(field_len);
                memcpy(
                    curr_pos,
                    var_field_data_ptr + data_start_offset,
                    field_len);
                curr_pos += field_len;
            }

            if (read_blobs) {
                uint32_t blob_offset = 0;
                uint32_t data_size = 0;
                //
                // now the blobs
                //
                get_blob_field_info(
                    &blob_offset,
                    share->kc_info.mcp_info[tokudb_active_index].len_of_offsets,
                    var_field_data_ptr,
                    share->kc_info.num_offset_bytes);
                data_size =
                    row->size -
                    blob_offset -
                    static_cast<uint32_t>((var_field_data_ptr -
                        static_cast<const uchar*>(row->data)));
                memcpy(curr_pos, &data_size, sizeof(data_size));
                curr_pos += sizeof(data_size);
                memcpy(curr_pos, var_field_data_ptr + blob_offset, data_size);
                curr_pos += data_size;
            }
        }
    }

    bytes_used_in_range_query_buff = curr_pos - range_query_buff;
    assert_always(bytes_used_in_range_query_buff <= size_range_query_buff);

    //
    // now determine if we should continue with the bulk fetch
    // we want to stop under these conditions:
    //  - we overran the prelocked range
    //  - we are close to the end of the buffer
    //  - we have fetched an exponential amount of rows with
    //  respect to the bulk fetch iteration, which is initialized
    //  to 0 in index_init() and prelock_range().

    rows_fetched_using_bulk_fetch++;
    // if the iteration is less than the number of possible shifts on
    // a 64 bit integer, check that we haven't exceeded this iterations
    // row fetch upper bound.
    if (bulk_fetch_iteration < HA_TOKU_BULK_FETCH_ITERATION_MAX) {
        uint64_t row_fetch_upper_bound = 1LLU << bulk_fetch_iteration;
        assert_always(row_fetch_upper_bound > 0);
        if (rows_fetched_using_bulk_fetch >= row_fetch_upper_bound) {
            error = 0;
            goto cleanup;
        }
    }

    if (bytes_used_in_range_query_buff +
        table_share->rec_buff_length >
        user_defined_size) {
        error = 0;
        goto cleanup;
    }
    if (direction > 0) {
        // compare what we got to the right endpoint of prelocked range
        // because we are searching keys in ascending order
        if (prelocked_right_range_size == 0) {
            error = TOKUDB_CURSOR_CONTINUE;
            goto cleanup;
        }
        DBT right_range;
        memset(&right_range, 0, sizeof(right_range));
        right_range.size = prelocked_right_range_size;
        right_range.data = prelocked_right_range;
        int cmp = tokudb_cmp_dbt_key(
            share->key_file[tokudb_active_index],
            key,
            &right_range);
        error = (cmp > 0) ? 0 : TOKUDB_CURSOR_CONTINUE;
    } else {
        // compare what we got to the left endpoint of prelocked range
        // because we are searching keys in descending order
        if (prelocked_left_range_size == 0) {
            error = TOKUDB_CURSOR_CONTINUE;
            goto cleanup;
        }
        DBT left_range;
        memset(&left_range, 0, sizeof(left_range));
        left_range.size = prelocked_left_range_size;
        left_range.data = prelocked_left_range;
        int cmp = tokudb_cmp_dbt_key(
            share->key_file[tokudb_active_index],
            key,
            &left_range);
        error = (cmp < 0) ? 0 : TOKUDB_CURSOR_CONTINUE;
    }
cleanup:
    return error;
}

Bulk fetch buffer数据准备好了,我们就可以从read_data_from_range_query_buff读取数据了。
Curr_range_query_buff_offset表示当前读取的位置。
首先读key信息。如果need_value为true,还要读取data信息。可能读整行数据,也可能只需要读取函数set_query_columns设置的那些字段。
读取完成之后,调整curr_range_query_buff_offset指向下一次读取的位置。

int ha_tokudb::read_data_from_range_query_buff(uchar* buf, bool need_val, bool do_key_read) {
    // buffer has the next row, get it from there
    int error;
    uchar* curr_pos = range_query_buff+curr_range_query_buff_offset;
    DBT curr_key;
    memset((void *) &curr_key, 0, sizeof(curr_key));

    // get key info
    uint32_t key_size = *(uint32_t *)curr_pos;
    curr_pos += sizeof(key_size);
    uchar* curr_key_buff = curr_pos;
    curr_pos += key_size;

    curr_key.data = curr_key_buff;
    curr_key.size = key_size;

    // if this is a covering index, this is all we need
    if (do_key_read) {
        assert_always(!need_val);
        extract_hidden_primary_key(tokudb_active_index, &curr_key);
        read_key_only(buf, tokudb_active_index, &curr_key);
        error = 0;
    }
    // we need to get more data
    else {
        DBT curr_val;
        memset((void *) &curr_val, 0, sizeof(curr_val));
        uchar* curr_val_buff = NULL;
        uint32_t val_size = 0;
        // in this case, we don't have a val, we are simply extracting the pk
        if (!need_val) {
            curr_val.data = curr_val_buff;
            curr_val.size = val_size;
            extract_hidden_primary_key(tokudb_active_index, &curr_key);
            error = read_primary_key( buf, tokudb_active_index, &curr_val, &curr_key);
        }
        else {
            extract_hidden_primary_key(tokudb_active_index, &curr_key);
            // need to extract a val and place it into buf
            if (unpack_entire_row) {
                // get val info
                val_size = *(uint32_t *)curr_pos;
                curr_pos += sizeof(val_size);
                curr_val_buff = curr_pos;
                curr_pos += val_size;
                curr_val.data = curr_val_buff;
                curr_val.size = val_size;
                error = unpack_row(buf,&curr_val, &curr_key, tokudb_active_index);
            }
            else {
                if (!(hidden_primary_key && tokudb_active_index == primary_key)) {
                    unpack_key(buf,&curr_key,tokudb_active_index);
                }
                // read rows we care about

                // first the null bytes;
                memcpy(buf, curr_pos, table_share->null_bytes);
                curr_pos += table_share->null_bytes;

                // now the fixed sized rows
                for (uint32_t i = 0; i < num_fixed_cols_for_query; i++) {
                    uint field_index = fixed_cols_for_query[i];
                    Field* field = table->field[field_index];
                    unpack_fixed_field(
                        buf + field_offset(field, table),
                        curr_pos,
                        share->kc_info.field_lengths[field_index]
                        );
                    curr_pos += share->kc_info.field_lengths[field_index];
                }
                // now the variable sized rows
                for (uint32_t i = 0; i < num_var_cols_for_query; i++) {
                    uint field_index = var_cols_for_query[i];
                    Field* field = table->field[field_index];
                    uint32_t field_len = *(uint32_t *)curr_pos;
                    curr_pos += sizeof(field_len);
                    unpack_var_field(
                        buf + field_offset(field, table),
                        curr_pos,
                        field_len,
                        share->kc_info.length_bytes[field_index]
                        );
                    curr_pos += field_len;
                }
                // now the blobs
                if (read_blobs) {
                    uint32_t blob_size = *(uint32_t *)curr_pos;
                    curr_pos += sizeof(blob_size);
                    error = unpack_blobs(
                        buf,
                        curr_pos,
                        blob_size,
                        true
                        );
                    curr_pos += blob_size;
                    if (error) {
                        invalidate_bulk_fetch();
                        goto exit;
                    }
                }
                error = 0;
            }
        }
    }

    curr_range_query_buff_offset = curr_pos - range_query_buff;
exit:
    return error;
}

Index_end

所有数据都读完之后,handler框架会调用index_end关闭cursor,并重置一些状态变量。

int ha_tokudb::index_end() {
    range_lock_grabbed = false;
    range_lock_grabbed_null = false;
    if (cursor) {
        int r = cursor->c_close(cursor);
        assert_always(r==0);
        cursor = NULL;
        remove_from_trx_handler_list();
        last_cursor_error = 0;
    }
    active_index = tokudb_active_index = MAX_KEY;

    //
    // reset query variables
    //
    unpack_entire_row = true;
    read_blobs = true;
    read_key = true;
    num_fixed_cols_for_query = 0;
    num_var_cols_for_query = 0;

    invalidate_bulk_fetch();
    invalidate_icp();
    doing_bulk_fetch = false;
    close_dsmrr();

    TOKUDB_HANDLER_DBUG_RETURN(0);
}

这就是一条query语句在tokudb引擎执行的大致过程。下个月见!

AliSQL · 开源 · Sequence Engine

$
0
0

Introduction

单调递增的唯一值,是在持久化数据库系统中常见的需求,无论是单节点中的业务主键,还是分布式系统中的全局唯一值,亦或是多系统中的幂等控制。不同的数据库系统有不同的实现方法,比如MySQL提供的AUTO_INCREMENT,Oracle,SQL Server提供SEQUENCE等。

在MySQL数据库中,如果业务系统希望封装唯一值,比如增加日期,用户等信息,AUTO_INCREMENT的方法会带来很大的不便,在实际的系统设计的时候, 也存在不同的折中方法,比如:

  • 序列值由Application或者Proxy来生成,不过弊端很明显,状态带到应用端,增加了扩容和缩容的复杂度。
  • 序列值由数据库通过模拟的表来生成,但需要中间件来封装和简化获取唯一值的逻辑。

AliSQL自主实现了SEQUENCE ENGINE,通过引擎的设计方法,尽可能的兼容其他数据库的使用方法,简化获取序列值复杂度。

Github开源地址:https://github.com/alibaba/AliSQL

Description

AliSQL开源的SEQUENCE,实现了MySQL存储引擎的设计接口,但底层的数据仍然使用现有的存储引擎,比如InnoDB或者MyISAM来保存持久化数据,以便尽可能的保证现有的外围工具比如XtraBackup等工具的兼容,所以SEQUENCE ENGINE仅仅是一个逻辑引擎。

对sequence对象的访问通过SEQUENCE handler接口,这一层逻辑引擎主要实现NEXTVAL的滚动,CACHE的管理等,最后透传给底层的基表数据引擎,实现最终的数据访问。

下面我们透过语法来看下AliSQL SEQUENCE的使用。

Syntax

1. CREATE SEQUENCE Syntax:

CREATE SEQUENCE [IF NOT EXISTS] schema.sequence_name
   [START WITH <constant>]
   [MINVALUE <constant>]
   [MAXVALUE <constant>]
   [INCREMENT BY <constant>]
   [CACHE <constant> | NOCACHE]
   [CYCLE | NOCYCLE]
  ;

SEQUENCE OPTIONS:

  • START
    Sequence的起始值

  • MINVALUE
    Sequence的最小值,如果这一轮结束并且是cycle的,那么下一轮将从MINVALUE开始

  • MAXVALUE
    Sequence的最大值,如果到最大值并且是nocycle的,那么将会得到以下报错:
    ERROR HY000: Sequence 'db.seq' has been run out.

  • INCREMENT BY
    Sequence的步长

  • CACHE/NOCACHE
    Cache的大小,为了性能考虑,可以设置cache的size比较大,但如果遇到实例重启,cache内的值会丢失

  • CYCLE/NOCYCLE
    表示sequence如果用完了后,是否允许从MINVALUE重新开始

例如:

  create sequence s
       start with 1
       minvalue 1
       maxvalue 9999999
       increment by 1
       cache 20
       cycle;

2. SHOW SEQUENCE Syntax

SHOW CREATE [TABLE|SEQUENCE] schema.sequence_name;

CREATE SEQUENCE schema.sequence_name (
  `currval` bigint(21) NOT NULL COMMENT 'current value',
  `nextval` bigint(21) NOT NULL COMMENT 'next value',
  `minvalue` bigint(21) NOT NULL COMMENT 'min value',
  `maxvalue` bigint(21) NOT NULL COMMENT 'max value',
  `start` bigint(21) NOT NULL COMMENT 'start value',
  `increment` bigint(21) NOT NULL COMMENT 'increment value',
  `cache` bigint(21) NOT NULL COMMENT 'cache size',
  `cycle` bigint(21) NOT NULL COMMENT 'cycle state',
  `round` bigint(21) NOT NULL COMMENT 'already how many round'
) ENGINE=InnoDB DEFAULT CHARSET=latin1

由于SEQUENCE是通过真正的引擎表来保存的,所以SHOW COMMAND看到仍然是engine table。

3. QUERY STATEMENT Syntax

SELECT [NEXTVAL | CURRVAL | *] FROM schema.sequence_name;
SELECT [NEXTVAL | CURRVAL | *] FOR schema.sequence_name;

这里支持两种访问方式,FROM和FOR:

  • FROM clause: 兼容正常的SELECT查询语句,返回的结果是基表的数据,不迭代NEXTVAL。
  • FOR clause:兼容SQL Server的方法,返回的结果是迭代后NEXTVAL的值。
mysql> select * from s;
+---------+---------+----------+---------------------+-------+-----------+-------+-------+-------+
| currval | nextval | minvalue | maxvalue            | start | increment | cache | cycle | round |
+---------+---------+----------+---------------------+-------+-----------+-------+-------+-------+
|       0 |   30004 |        1 | 9223372036854775807 |     1 |         1 | 10000 |     0 |     0 |
+---------+---------+----------+---------------------+-------+-----------+-------+-------+-------+
1 row in set (0.00 sec)

mysql> select * for s;
+---------+---------+----------+---------------------+-------+-----------+-------+-------+-------+
| currval | nextval | minvalue | maxvalue            | start | increment | cache | cycle | round |
+---------+---------+----------+---------------------+-------+-----------+-------+-------+-------+
|       0 |   20014 |        1 | 9223372036854775807 |     1 |         1 | 10000 |     0 |     0 |
+---------+---------+----------+---------------------+-------+-----------+-------+-------+-------+

4. 兼容性

因为要兼容MYSQLDUMP的备份方式,所以支持另外一种CREATE SEQUENCE方法,即:通过创建SEQUENCE表和INSERT一行初始记录的方式, 比如:

  CREATE SEQUENCE schema.sequence_name (
  `currval` bigint(21) NOT NULL COMMENT 'current value',
  `nextval` bigint(21) NOT NULL COMMENT 'next value',
  `minvalue` bigint(21) NOT NULL COMMENT 'min value',
  `maxvalue` bigint(21) NOT NULL COMMENT 'max value',
  `start` bigint(21) NOT NULL COMMENT 'start value',
  `increment` bigint(21) NOT NULL COMMENT 'increment value',
  `cache` bigint(21) NOT NULL COMMENT 'cache size',
  `cycle` bigint(21) NOT NULL COMMENT 'cycle state',
  `round` bigint(21) NOT NULL COMMENT 'already how many round'
) ENGINE=InnoDB DEFAULT CHARSET=latin1

INSERT INTO schema.sequence_name VALUES(0,0,1,9223372036854775807,1,1,10000,1,0);
COMMIT;

但强烈建议使用native的CREATE SEQUENCE方法。

5. 语法限制

  • Sequence不支持subquery和join
  • FOR clause只支持sequence表,普通引擎表不支持
  • 可以使用SHOW CREATE TABLE或者SHOW CREATE SEQUENCE来访问SEQUENCE结构,但不能使用SHOW CREATE SEQUENCE访问普通表
  • 不支持CREATE TABLE的时候指定SEQUENCE引擎,sequence表只能通过CREATE SEQUENCE的语法来创建

High level architecture

1. Sequence initialization

Sequence对象的创建,会转化成拥有固定[CURRVAL, NEXTVAL, MINVALUE, MAXVALUE, START, INCREMENT, CACHE, CYCLE, ROUND]这9个字段的引擎表,并根据CREATE SEQUENCE clause的定义,初始化了一条数据,所以sequence对象实质上是拥有一条记录的存储引擎表,SLAVE复制的BINLOG使用CREATE SEQUENCE ...语句生成的QUERY EVENT来完成。

2. Sequence interface

SEQUENCE handler实现了一部分的handler interface,并定义了两个重要的属性,SEQUENCE_SHARE和BASE_TABLE_FILE,SEQUENCE_SHARE保存着共享的sequence对象属性和CACHE的值,NEXTVAL的值首先从cache中获取,只有在cache使用完了,才会查询基表。
BASE_TABLE_FILE是基表的handler,对持久化的数据的访问和修改,都通过BASE_TABLE_FILE handler进行访问。

3. Sequence cache

Sequence对象的CACHE值保存在SEQUENCE_SHARE中,使用SEQUENCE_SHARE::MUTEX进行保护,所有对cache的访问是串行的。比如cache size是20,那么SEQUENCE_SHARE中只是保存一个cache_end值,当访问的NEXTVAL到了cache_end,就会从基表中获取下一个batch放到cache中。NEXTVAL根据INCREMENT BY设置的步长进行迭代。

4. Sequence update

当cache用完了之后,会从基表中获取下一个batch,这样会更新基表中的记录,查询会转化成更新语句,
其更新的主要步骤如下:

  1. 升级SEQUENCE的MDL_SHARE_READ METADATA LOCK 到 MDL_SHARE_WRITE级别
  2. 持有GLOBAL MDL_INTENSIVE_EXCLUSIVE METADATA LOCK
  3. 开启AUTONOMOUS TRANSACTION
  4. 更新记录并生成BINLOG EVENT
  5. 持有COMMIT METADATA LOCK
  6. XA提交AUTONOMOUS TRANSACTION 并释放MDL锁

5. Autonomous transaction

因为nextval不支持ROLLBACK重用,所以必须重启一个自治事务来脱离事务上下文,
其步骤如下:

  1. 备份当前基表引擎的事务上下文
  2. 备份当前BINLOG引擎的上下文
  3. SEQUENCE和BINLOG分别注册AUTONOMOUS TRANSACTION
  4. 等更新完成,XA提交AUTONOMOUS TRANSACTION
  5. 还原当前事务上下文

6. Sequence read only

因为SEQUENCE的SELECT语句会转换成UPDATE语句,所以SELECT NEXTVAL FOR s statement须持有 MDL_SHARE_WRITE 和 GLOBAL MDL_INTENSIVE_EXCLUSIVE METADATA LOCK 进行,以便在READ ONLY的时候,阻塞对sequence对象的访问。

7. Skip cache

这里指两种CACHE:

  • 一种是SEQUENCE的CACHE,可以使用SELECT NEXTVAL FORM Sequence_name来skip。
  • 另外一种是QUERY CACHE,所有的SEQUENCE都设置了不支持QUERY CACHE,这样避免由于QUERY CACHE导致NEXTVAL没有迭代。

8. Sequence backup

由于SEQUENCE是通过真正的引擎表来保存的,所以类似XtraBackup这样的物理备份可以直接使用,而类似于MYSQLDUMP这样的逻辑备份,SEQUENCE会备份成CREATE SEQUENCE语句和INSERT语句的组合来完成。

Next Release

本次开源了部分功能,下一次release将继续开源SEQUENCE的部分功能:

  • 支持CURRVAL的访问,CURRVAL表示当前session的上一次的NEXTVAL访问的值。
  • 兼容更多数据库的访问方法,比如:
Oracle Syntax:
  SELECT sequence_name.nextval FROM DUAL;  

PostgreSQL Syntax:
  nextval(regclass);
  currval(regclass);
  setval(regclass, bigint);

Usage Scenario

1. 更具有业务含义的主键设计 .

例如:[八位日期 + 四位USER ID + sequence_number]的流水业务单据号的设计格式,可以通过SELECT NEXTVAL FOR Sequence和应用封装的方式实现,相比较无意义的id数字,这种格式会带来几个优势:

  • 保持和时间同步的有序性,有利于数据的归档,比如可以直接使用这种ID来进行按日/月/年RANGE分区, 无缝使用MySQL的partition特性
  • 增加USER的id信息,可以作为天然的分库分表逻辑位, 提升数据节点可扩展性
  • 保持数字的有序性,保证InnoDB这种聚簇索引表的插入性能稳定

业界目前采用的设计方法:

  • Booking使用了AUTO_INCREMENT的方法, 先插入一个无业务含义的数字,然后使用last_insert_id()方法获取ID值,最后在业务逻辑中使用这个ID值。 其劣势就是必须先插入,并没有办法再修改这个无业务含义的id。
  • Twitter采用了另外一种格式,[41 bits timestamp + 10 bits configured machine ID + 12 bits sequence number], sequence number的生成机制没有透露,machine ID的的设计,使用Zookeeper来管理的machine ID或者机器的MAC address。
  • UUID的方法,这种方式生成了一个随机的唯一值,严重影响了插入的性能,并且增大了索引大小,降低了命中率,没有任何优势。

2. 分布式节点的唯一值设计

分布式SEQUENCE生成:

  • 可以为每一个节点设计sequence,比如为每个节点设计不同的INCREMENT BY步长来达到MySQL AUTO_INCREMENT中,设置auto_increment_increment和auto_increment_offset的效果,但相比较auto increment的全局配置,并且保存在my.cnf中的方法,SEQUENCE可以把这些配置当做sequence对象的属性持久化保存下来,优势明显。但不推荐使用这种方法来设计唯一值,会给运维留下不少坑。
  • 使用类似twitter的方法,每一个节点上创建sequence,然后增加节点信息到sequence number中,生成唯一值。

集中式SEQUENCE生成:

  • 对于分布式节点中的ID需求,使用独立的集中式的sequence服务来生成,但如果要保证持续可用,sequence服务仍然需要设计成多节点的,比如Flickr的Ticket Servers设计:

Sequence服务节点上创建Ticket表:

CREATE TABLE `Tickets64` (
  `id` bigint(20) unsigned NOT NULL auto_increment,
  `stub` char(1) NOT NULL default '',
  PRIMARY KEY  (`id`),
  UNIQUE KEY `stub` (`stub`)
) ENGINE=MyISAM

+-------------------+------+
| id                | stub |
+-------------------+------+
| 72157623227190423 |    a |
+-------------------+------+

使用以下语句,生成ID值:
SQL REPLACE INTO Tickets64 (stub) VALUES ('a'); SELECT LAST_INSERT_ID();
因为PHOTOS,COMMENTS,FAVORITES,TAGS都需要ID, 所以会建不同的ticket表来完成,为了保持持续可用,采用了:

TicketServer1:
auto-increment-increment = 2
auto-increment-offset = 1

TicketServer2:
auto-increment-increment = 2
auto-increment-offset = 2

来保证高可用。
如果使用sequence对象,可以大大简化ID的获取逻辑,并更加安全。

MySQL · myrocks · myrocks之备份恢复

$
0
0

myrocks支持逻辑备份和物理备份,逻辑备份仍然采用mysqldump,物理备份采用自己开发的myrocks_hotbackup工具,传统的物理备份工具Xtrabackup不支持rocksdb。由于rocksdb的存储特性,myrocks不管是逻辑备份还是物理备份,与innodb的备份恢复均有较大差别。

逻辑备份

myrocks的mysqldump工具支持rocksdb的逻辑备份,其使用方式与原生的mysqldump备份innodb没有区别,一般的使用方式如下

mysqldump -uroot -h 127.0.0.1 -P 3306 --default-character-set=binary --single-transaction --master-data=2 --all-databases

虽然使用方式相同,但内部实现会用一些差别
传统的mysqldump备份方式简化如下

  1. 加锁FTWL:FLUSH TABLE WITH READ LOCK
  2. 设置RR模式:SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
  3. 开启一致读:START TRANSACTION WITH CONSISTENT SNAPSHOT
  4. 获取位点:SHOW MASTER STATUS
  5. 解锁:UNLOCK TABLES
  6. 依次导出数据select * from table

myrocks的mysqldump备份方式简化如下

  1. 设置读取时不缓存到block cache:SET SESSION rocksdb_skip_fill_cache=1
  2. 设置RR模式:SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
  3. 开启一致读:START TRANSACTION WITH CONSISTENT ROCKSDB SNAPSHOT
  4. 依次导出数据select * from table

可以看出myrocoks mysqldump导数据时,设置了engine层的优化rocksdb_skip_fill_cache。同时少了传统的FTWL的加锁和解锁操作,换了新的快照获取方式START TRANSACTION WITH CONSISTENT ROCKSDB SNAPSHOT, 此方式会加一些内存锁(LOCK_log等)同时返回位点信息,此方式比FTWL更高效。

mysql> START TRANSACTION WITH CONSISTENT ROCKSDB SNAPSHOT;
+------------------+----------+------------------------------------------+
| File             | Position | Gtid_executed                            |
+------------------+----------+------------------------------------------+
| mysql-bin.000003 |     1010 | a3d923e4-f19b-11e6-ba57-2c44fd7a5210:1-4 |
+------------------+----------+------------------------------------------+
1 row in set (0.00 sec)

myrocoks mysqldump不能同时备份innodb和rocksdb, 备份innodb时也采用新的START TRANSACTION WITH CONSISTENT INNODB SNAPSHOT方式。如果需支持同时备份innodb和rocksdb,需修改mysqldump采用老的START TRANSACTION WITH CONSISTENT SNAPSHOT方式,同时开启innodb和rocksdb的快照。

物理备份

myrocks 有专门的物理备份工具myrocks_hotbackup,此工具是一个python脚本,源码在scripts/myrocks_hotbackup,总共才600多行,整个备份逻辑比较简单。

分析myrocks_hotbackup之前,先介绍下myrocks的checkpoint快照功能
执行以下语句会在目录/path/to/backup下创建一个一致性的数据快照
SET GLOBAL rocksdb_create_checkpoint = '/path/to/backup'
创建快照过程如下

  1. 禁止SST文件的删除操作
  2. 创建空目录/path/to/backup
  3. 在/path/to/backup下为每个SST文件的创建硬链接
  4. copy MANIFEST和OPTIONS文件到/path/to/backup下
  5. 备份WAL文件,最后一个WAL文件通过copy方式,其他WAL文件通过硬链接备份到/path/to/backup下
  6. 允许SST文件的删除操作

Note: SST内容是不会变化的,从而能够以硬链接的方式备份文件,充分利用硬链接的特性。同时快照过程中禁止删除SST文件,从而保证MANIFEST文件的一致性。

checkpoint快照中SST文件占主要部分,SST通过hardlink方式建立,使得快照操作比较快,同时也节省了空间。

再来看看myrocks_hotbackup的备份过程

  1. 通过SET GLOBAL rocksdb_create_checkpoint=‘path_n’建立快照
  2. 依次备份快照中的文件,先备份SST文件再备份, 再备份WAL,MANIFEST和OPTIONS文件。备份SST过程会比较长,如果超过checkpoint_interval(由参数–interval指定)SST文件还没有备份完,就会清理当前快照,返回步骤1重新开始。
  3. 步骤1,2完成后,rocksdb相关的文件备份完成,清理最后一次checkpoint快照文件。步骤1,2可能重复执行多次。
  4. 开始备份mysql其他文件。比如其他数据库文件,test,mysql数据等,另外还有datadir下其他文件,但过滤掉这些文件’master.info’, ‘relay-log.info’, ‘worker-relay-log.info’,’auto.cnf’, ‘gaplock.log’, ‘ibdata’, ‘ib_logfile’。
  • checkpoint renewing

上面步骤1,2重复建立快照的过程称为checkpoint renewing,图片来自Facebook
屏幕快照 2017-02-19 上午7.01.20.png

checkpoint renewing 过程中已经备份过的SST文件不会重复备份,只在最后一次checkpoint snapshot中备份WAL文件,MANIFEST和OPTIONS文件。checkpoint renewing 使得我们备份的数据比较新,从而通过此备份集建立的备库与主库同步的时间比较短。

  • 远程备份

myrocks_hotbackup只支持远程备份,暂时不支持本地备份。支持tar, xbstream, 另外还支持 facebook开源的网络传输工具WDT, 号称性能有10倍的提升。

一个用tar方式备份的例子

myrocks_hotbackup -u root -P 3306 --stream=tar --checkpoint_dir='xxx' | ssh  xx.xx.xx.xx  'tar -xi -C dest_path'

备份日志如下,从日志也可以看整个备份的过程

2017-02-14 15:26:37.076 INFO Starting backup.
2017-02-14 15:26:37.092 INFO Set datadir: /path/data/
2017-02-14 15:26:37.092 INFO Creating checkpoint at /xxx/1
2017-02-14 15:26:37.096 INFO Created checkpoint at /home/zhangyuan.zy/build/fbmyrocks/backup_tmp/1
2017-02-14 15:26:37.096 INFO Starting backup from snapshot: target files 2
2017-02-14 15:26:37.112 INFO Backup WAL..
2017-02-14 15:26:37.113 INFO Backup Manifest..
2017-02-14 15:26:37.131 INFO Cleaned up checkpoint from /xxx/1
2017-02-14 15:26:37.131 INFO Sent   0.00 GB of sst files, 2 files in total.
2017-02-14 15:26:37.131 INFO RocksDB Backup Done.
2017-02-14 15:26:37.132 INFO Taking MySQL misc backups..
2017-02-14 15:26:37.132 INFO Starting MySQL misc file traversal from database test..
2017-02-14 15:26:37.139 INFO Starting MySQL misc file traversal from database performance_schema..
2017-02-14 15:26:37.145 INFO Starting MySQL misc file traversal from database mysql..
2017-02-14 15:26:37.601 INFO Traversing misc files from data directory..
2017-02-14 15:26:37.601 INFO Skipping gaplock.log
2017-02-14 15:26:37.607 INFO Skipping auto.cnf
2017-02-14 15:26:37.613 INFO MySQL misc backups done.
2017-02-14 15:26:37.614 INFO All Backups Done.
  • 备份恢复

备份完成后,所有的数据都在同一个目录下,我们需要通过--move_back 将数据移动到我们需要的地方。

myrocks_hotbackup --move_back --datadir=/path/data --rocksdb_datadir=/path/data/.rocksdb --rocksdb_waldir=/path/data/.rocksdb --backup_dir=dest_path
  • 通过备份集搭建一个备库

传统的物理备份工具xtrabackup备份过程中会通过加锁获取一致的binlog位点信息,并保存到文件中,恢复后通过这些位点信息来重建复制关系。而myrocks_hotbackup备份过程中没有加锁,也没有保存位点信息。

在myrocks中enging层rocksdb会保持位点信息,每次事务提交时,都会将binlog位点以及gtid信息保存到数据字典BINLOG_INFO_INDEX_NUMBER中。

BINLOG_INFO_INDEX_NUMBER
key: Rdb_key_def::BINLOG_INFO_INDEX_NUMBER (0x4)
value: version, {binlog_name,binlog_pos,binlog_gtid}

备份集通过--move_back恢复后,直接启动mysqld,mysqld在recover 过程中会将数据字典BINLOG_INFO_INDEX_NUMBER的信息打印到错误日志中,例如

RocksDB: Last binlog file position 1010, file name mysql-bin.000003
RocksDB: Last MySQL Gtid a3d923e4-f19b-11e6-ba57-2c44fd7a5210:4

从错误日志中解析出位点,然后可以通过以下方式建立复制关系

show gtid_executed in '$binlog_file' from $binlog_pos;
set global gtid_purged='$gtid_executed';
change master to master_host='xx.xx.xx.xx', master_port=${MASTER_MYPORT}, master_user='root', master_auto_position=1, master_connect_retry=1;
set global gtid_purged='$gtid_executed';
start slave;
  • myrocks_hotbackup仅支持rocksdb备份,不支持innod备份

总结

myrocks支持物理备份和逻辑备份,但这两种方式都只支持rocksdb备份,如果需要同时指出innodb和rocksdb的备份,还需要对备份逻辑稍加改造才行。
myrocks的物理备份方式比较高效,一般建议采用myrocks_hotbackup物理备份方式。

MySQL · 挖坑 · LOCK_active_mi/LOCK_msp_map 优化思路

$
0
0

背景

在MySQL中Slave相关操作一直存在一把大锁——LOCK_active_mi (5.5及之前版本,以及MariaDB),或LOCK_msp_map(5.6及之后的版本)。
在Slave操作中大家可能经常会遇到如下懵逼的操作:

  1. 线程1:STOP SLAVE;有事务要回滚,一直不结束,然后LOCK_active_mi一直被这个线程持有
  2. 线程2:SHOW SLAVE STATUS;拿不到LOCK_active_mi,无法执行。

SHOW SLAVE STATUS 经常作为监控脚本的语句被自动执行,然后就不停地被卡住,线程堆积,直到 too many connections。

等到了5.6引入了多源复制之后,这个问题就更严重了,LOCK_msr_map需要在访问任何通道时都被持有,因此操作两个不同的通道也可能冲突。

Percona曾经推出了SHOW SLAVE STATUS NO_BLOCK这样的语法,不加锁查看复制状态,但是,毕竟这不是根治之法,一方面查看的数据并不一定对,还可能Crash(例如查看过程中通道被删除了),并且需要专门的语法。

特别是5.6还支持了多线程复制,IO THREAD可以多个(多通道),SQL THREAD可以并行(并行复制),这种情况下,LOCK_msr_map这么大一把锁就更加显得格格不入了。

解决思路

我们先来分析一下,对各个Slave通道的操作到底有哪些是真的互斥。

  1. 并发读写同一个通道的运行状态:
    例如 mi->running,mi->info_thd 等,已有mi->run_lock保护IO线程,mi->rli->run_lock保护SQL线程。

  2. 并发读写同一个通道的执行数据:
    例如 mi->master_log_pos,mi->rpl_filter 等,已有mi->data_lock保护IO线程,mi->rli->data_lock保护SQL线程。

  3. 并发读写同一个通道的错误码和错误消息:
    例如 mi->last_error 等,已有mi->err_lock保护IO线程,mi->rli->err_lock保护SQL线程。

  4. 对于多源复制,增减通道:
    msr_map结构的增删改查需要保护,否则可能在遍历所有通道时有通道增加或删除,那遍历结果就不对了。这里真的需要LOCK_msr_map保护。

可见,除了msr_map的操作真的需要全局互斥以外,其他的操作其实都有Master_info内的锁可以保护,在mi内部解决矛盾就可以,根本无需全局锁。

MySQL 5.7 给了一个改进方案,是将LOCK_msr_map从mysql_mutex_t(pthread_mutex_t)改成了Checkable_rwlock。这个方案可以解决部分只读操作时可以相互并发,但是并没有解决LOCK_msr_map保护范围太广的问题。上面我们给出的STOP SLAVE卡住(wr_lock)和SHOW SLAVE STATUS执行互斥的问题就没有解决。

为了彻底解决这个问题,我们可以参考InnoDB怎么保证Buffer Pool中Page的并发性的:

  1. 每当有线程正在访问Page时,将计数器(bpage->io_fix)加一,就把这个Page Pin在内存中了。
  2. LRU淘汰Page时,看到io_fix还不是0,就不能从内存中清理,因为还有人在访问,必须等到0才能清除。
  3. 对Page内容的操作,有Latch来保证,避免同时有人修改页面。

因此我们也可以在每个Master_info中加一个计数器(mi->users),有线程要使用mi,就将计数器加一,不用了就减一,以此来代替加锁放锁,再用一个专门的锁(sleep_lock)来保护计数器就可以了。

加锁操作成了:

void Master_info::use()
{
  mysql_mutex_lock(&sleep_lock);
  users++;
  mysql_mutex_unlock(&sleep_lock);
}

放锁操作成了:

void Master_info::release()
{
  mysql_mutex_lock(&sleep_lock);
  if (!--users && killed)
    mysql_cond_signal(&sleep_cond);
  mysql_mutex_unlock(&sleep_lock);
  DBUG_VOID_RETURN;

}

每次放锁时发一个信号量,让remove_mi操作能收到信号量后再执行删除Master_info的操作。

然后原本需要LOCK_msr_map保护的Master_info操作,可以缩小范围,只需要在取出mi时拿锁就可以了。

Master_info *get_master_info(const char *connection_name)
{
  Master_info *mi;
  DBUG_ENTER("get_master_info");
  /* Protect against inserts into msr_map */
  mysql_mutex_lock(&LOCK_msr_map);
  if ((mi= msr_map.get_mi(connection_name)))

    mi->use();
  mysql_mutex_unlock(&LOCK_msr_map);
  DBUG_RETURN(mi);
}

再把原来需要get_mi调用的地方,全部修改为get_master_info这个调用,就可以删掉其mysql_mutex_lock(&LOCK_msr_map)加锁保护了,放锁的mysql_mutex_unlock(&LOCK_msr_map)语句全部改成mi->release()即可。这样就不存在全局锁定了。

比如启动一个通道的复制:

if ((mi= get_master_info(lex->mi.channel)))
  {    
    res= start_slave(thd, mi, 1 /*net report */); 
    mi->release();
}

完全不需要 mysql_mutex_lock(&LOCK_msr_map)和mysql_mutex_unlock(&LOCK_msr_map)来包住start_slave了对不对!

但这种修改就带来了另一个问题,要删除一个Master_info的时候,可能还有线程在使用这个mi。
因此在析构函数中需要增加一个等待,让这个mi的所有调用都释放了再清理这个mi。
有了计数器这个也很容易做到,每当收到计数器减一的信号时,看一下是不是计数器到0了,到0了就说明所有使用者全部释放了,就可以正常删除了。

void Master_info::wait_until_free()
{
  mysql_mutex_lock(&sleep_lock);
  killed= 1;
  while (users)
    mysql_cond_wait(&sleep_cond, &sleep_lock);
  mysql_mutex_unlock(&sleep_lock);
}

效果

这样改进以后,我们再来看最开始这个典型的案例:

  1. STOP SLAVE执行卡住,那么会导致这个mi或者所有mi的计数器加一。
  2. SHOW SLAVE STATUS执行,在这个mi或者所有mi的计数器加一。
    并不涉及到相互锁定,只是此时无法删除通道而已,这也是合理的。两个线程都能愉快的执行自己的任务。

补丁我们会在之后的AliSQL开源版本中开源,敬请期待。

MySQL · 源码分析 · 词法分析及其性能优化

$
0
0

简介

MySQL 支持标准的 SQL 语言,具体实现的时候必然要涉及到词法分析和语法分析。早期的程序可能会优先考虑手工实现词法分析和语法分析,现在大多数场合下都会采用工具来简化实现。MySQL、PostgreSQL 等采用 C/C++ 实现的开源数据库采用的是现代的 yacc/lex 组合,也就是 GNU bison/flex。其他比较流行的工具还有 ANTLR、JavaCC 等等。这些工具大多采用扩展的 BNF 语法,并支持很多定制化选项,使得语法比较容易维护和实现。MySQL 语法分析器的入口函数是 MYSQLparse(),词法分析器的入口函数为 MYSQLlex()。不过, MySQL 的词法分析器是手工打造的,并且为了提高关键字的查找效率做了针对性的优化。这个博客上有点介绍,建议在阅读代码之前先了解一下。

背景知识

MySQL 的语法分析器采用的工具是 bison,对应的语法文件是 sql/sql_yacc.yy。bison 处理语法文件的输出是 sql/sql_yacc.cc 和 sql/sql_yacc.h。对应的 sql/CMakeLists.txt 中有相关的 make 规则:

INCLUDE(${CMAKE_SOURCE_DIR}/cmake/bison.cmake)
RUN_BISON(
  ${CMAKE_CURRENT_SOURCE_DIR}/sql_yacc.yy 
  ${CMAKE_CURRENT_BINARY_DIR}/sql_yacc.cc
  ${CMAKE_CURRENT_BINARY_DIR}/sql_yacc.h
)

实际在 make 的时候,这个过程比较复杂。也可以单独 make 词法语法分析的部分,例如:

$ make -C sql gen_lex_token

阅读代码的时候,可以查找 MYSQLparse,以找到语法分析的代码路径。下面是清除掉生成的 yacc 代码再查找的结果:

$ make -C sql clean
$ grep --color=auto -rwIn MYSQLparse sql/
sql/sql_parse.cc:6748:extern int MYSQLparse(class THD *thd); // from sql_yacc.cc
sql/sql_parse.cc:6752:  This is a wrapper of MYSQLparse(). All the code should call parse_sql()
sql/sql_parse.cc:6753:  instead of MYSQLparse().
sql/sql_parse.cc:6858:  bool mysql_parse_status= MYSQLparse(thd) != 0;
sql/sql_parse.cc:6917:    Check that if MYSQLparse() failed either thd->is_error() is set, or an
sql/sql_lex.cc:3442:  parser before returning an error from MYSQLparse. If your

MySQL 手工打造的词法分析器对应的源代码文件是 sql/sql_lex.h 和 sql/sql_lex.cc。词法分析的入口函数是 MYSQLlex()。解析出一个 token 的函数为 lex_one_token()。词法分析出来的每个 token 都会对应一个语法分析器中的终结符,它们的字符串表示在 sql/lex.h 中。这些符号被分为两组,SQL 关键字以及 SQL 函数,在代码中对应数组 symbols[] 和 sql_functions[]。通常而言,在语法/词法分析过程中为了判断某个 token 是否为 SQL 的关键字,可以直接二分查找字符串数组。考虑到关键字列表是固定的一个集合,MySQL 对此作了专门的优化,用 Trie 树来进一步提高效率。下一节介绍这部分代码的实现。

查找树的实现

查找树的产生用的是一个独立的小程序 gen_lex_hash[.cc]。CMake 产生的 Makefile 规则为在文件 sql/CMakeFiles/sql.dir/build.make 中:

sql/lex_hash.h: sql/gen_lex_hash
  $(CMAKE_COMMAND) -E cmake_progress_report /home/x/mysql/CMakeFiles $(CMAKE_PROGRESS_153)
  @$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --blue --bold "Generating lex_hash.h"
  cd /home/x/mysql/sql && ./gen_lex_hash > lex_hash.h

可以看到,它产生的代码在 sql/lex_hash.h 中。里头包含了两个大数组:sql_functions_map[] 和 symbols_map[],以及一个函数 get_hash_symbol()。

具体的实现自然分为两个部分,一个是产生树,另一个是查找产生的树。

树的查找

最主要的函数就是 get_hash_symbol(),它的调用和被调用关系为:

调用关系

注:上图是使用 Graphviz 产生的。

文件 gen_lex_hash.cc 的代码注释中有一个树的示例:

查找树的示
例

可以看出,根节点是按照字符串长度从小到大排序组织的。对于每种长度的字符串,要记录首字母和尾字母以及下一层节点的指针。中间节点除了是按照字符从小到大排序外,其它部分与根节点相同。叶子节点就是 symbols 数组的成员。树的查找就是一个自然的遍历过程。

树的产生

理解了上面的树的结构,就很好理解树的产生逻辑了。它的做法是读取关键字数组,产生一个原始的查找树(参看函数 generate_find_structs);然后,调整这个树,产生一个数组,也就是不用链表表示的树(参看函数 print_find_structs)。主要的函数和调用关系如下:

调用关系

其中:insert_symbols 处理的是 SQL 关键字,insert_sql_functions 处理的是函数名。get_hash_struct_by_len 处理的是树的根节点,insert_into_hash 处理的是树的内节点,递归执行。

为了更好的理解,可以在处理到输入数组不同位置时,查看当时对应的树。例如:

Table 1:查找树的产生

img

试试折半查找

如果要验证一下这个优化与普通的折半查找的性能差异,需要做一些适当的修改才行。测试中用 perf 之类的工具会发现比较函数会成为热点。在修改代码时需要注意:
1. symbols、sql_functions 这两个数组不一定是按照顺序排列的,需要认真确认。
2. 查找符号时,字符串并没有以 ‘\0’ 结尾,做比较要注意。
3. 要修改的文件 sql/lex_hash.h 是自动产生的,需要用自己的代码替换其中的 get_hash_symbol 函数。

总结

本文是基于 MySQL 5.6 做的分析。可以看到 MySQL 对词法分析中的关键字查找热点做了性能改进。也可以发现代码的结构不是特别清晰,存在一些代码冗余和明显的可改进之处。 WL#8016: Parser for optimizer hints 在重构的过程中顺便将其改掉了。

Footnotes:

1 MySQL: Query Parsing <https://blog.imaginea.com/mysql-query-parsing/>
2 MySQL Download <http://dev.mysql.com/downloads/mysql/#downloads>
3 Graphviz <http://www.graphviz.org/Gallery.php>
4 WL#8016: Parser for optimizer hints <https://dev.mysql.com/worklog/task/?id=8016>

SQL优化 · 经典案例 · 索引篇

$
0
0

Introduction

在这些年的工作之中,由于SQL问题导致的数据库故障层出不穷,下面将过去六年工作中遇到的SQL问题总结归类,还原问题原貌,给出分析问题思路和解决问题的方法,帮助用户在使用数据库的过程中能够少走一些弯路。总共包括四部分:索引篇,SQL改写篇,参数优化篇,优化器篇四部分,今天将介绍第一部分:索引篇。

索引问题是SQL问题中出现频率最高的,常见的索引问题包括:无索引,隐式转换。当数据库中出现访问表的SQL无索引导致全表扫描,如果表的数据量很大,扫描大量的数据,应用请求变慢占用数据库连接,连接堆积很快达到数据库的最大连接数设置,新的应用请求将会被拒绝导致故障发生。隐式转换是指SQL查询条件中的传入值与对应字段的数据定义不一致导致索引无法使用。常见隐士转换如字段的表结构定义为字符类型,但SQL传入值为数字;或者是字段定义collation为区分大小写,在多表关联的场景下,其表的关联字段大小写敏感定义各不相同。隐式转换会导致索引无法使用,进而出现上述慢SQL堆积数据库连接数跑满的情况。

无索引案例:

表结构

CREATE TABLE `user` (
……
mo bigint NOT NULL DEFAULT '' ,
KEY ind_mo (mo) 
……
) ENGINE=InnoDB;

SELECT uid FROM `user` WHERE mo=13772556391 LIMIT 0,1

执行计划

mysql> explain  SELECT uid FROM `user` WHERE mo=13772556391 LIMIT 0,1;
           id: 1
  select_type: SIMPLE
        table: user
         type: ALL
possible_keys: NULL
          key: NULL
         rows: 707250
         Extra: Using where

从上面的SQL看到执行计划中ALL,代表了这条SQL执行计划是全表扫描,每次执行需要扫描707250行数据,这是非常消耗性能的,该如何进行优化?添加索引。
验证mo字段的过滤性

mysql> select count(*) from user where mo=13772556391;
|   0    |

可以看到mo字段的过滤性是非常高的,进一步验证可以通过select count(*) as all_count,count(distinct mo) as distinct_cnt from user,通对比 all_count和distinct_cnt这两个值进行对比,如果all_cnt和distinct_cnt相差甚多,则在mo字段上添加索引是非常有效的。

添加索引

mysql> alter table user add index ind_mo(mo);
mysql>SELECT uid FROM `user` WHERE mo=13772556391 LIMIT 0,1;
Empty set (0.05 sec)

执行计划

mysql> explain  SELECT uid FROM `user` WHERE mo=13772556391 LIMIT 0,1\G;
*************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: user
             type: index
    possible_keys: ind_mo
              key: ind_mo
             rows: 1
            Extra: Using where; Using index

隐式转换案例一

表结构

  CREATE TABLE `user` (
  ……
  mo char(11) NOT NULL DEFAULT '' ,
  KEY ind_mo (mo)
  ……
  ) ENGINE=InnoDB;

执行计划

mysql> explain extended select uid from`user` where mo=13772556391 limit 0,1;
mysql> show warnings;
Warning1:Cannot use  index 'ind_mo' due to type or collation conversion on field 'mo'                                                                        
Note:select `user`.`uid` AS `uid` from `user` where (`user`.`mo` = 13772556391) limit 0,1

如何解决

mysql> explain   SELECT uid FROM `user` WHERE mo='13772556391' LIMIT 0,1\G;
*************************** 1. row ***************************
              id: 1
     select_type: SIMPLE
           table: user
            type: ref
   possible_keys: ind_mo
             key: ind_mo
            rows: 1
           Extra: Using where; Using index

上述案例中由于表结构定义mo字段后字符串数据类型,而应用传入的则是数字,进而导致了隐式转换,索引无法使用,所以有两种方案:
第一,将表结构mo修改为数字数据类型。
第二,修改应用将应用中传入的字符类型改为数据类型。

隐式转换案例二

表结构

CREATE TABLE `test_date` (
     `id` int(11) DEFAULT NULL,
     `gmt_create` varchar(100) DEFAULT NULL,
     KEY `ind_gmt_create` (`gmt_create`)
) ENGINE=InnoDB AUTO_INCREMENT=524272;

5.5版本执行计划

mysql> explain  select * from test_date where gmt_create BETWEEN DATE_ADD(NOW(), INTERVAL - 1 MINUTE) AND   DATE_ADD(NOW(), INTERVAL 15 MINUTE) ;
+----+-------------+-----------+-------+----------------+----------------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys  | key | key_len | ref  | rows | Extra       |
+----+-------------+-----------+-------+----------------+----------------+---------+------+------+-------------+
|1|SIMPLE| test_date |range| ind_gmt_create|ind_gmt_create|303| NULL | 1 | Using where |

5.6版本执行计划

mysql> explain select * from test_date where gmt_create BETWEEN DATE_ADD(NOW(), INTERVAL - 1 MINUTE) AND   DATE_ADD(NOW(), INTERVAL 15 MINUTE) ; 
+----+-------------+-----------+------+----------------+------+---------+------+---------+-------------+
| id | select_type | table | type | possible_keys  | key  | key_len | ref | rows | Extra|
+----+-------------+-----------+------+----------------+------+---------+------+---------+-------------+
| 1 | SIMPLE| test_date | ALL | ind_gmt_create | NULL | NULL | NULL | 2849555 | Using where |
+----+-------------+-----------+------+----------------+------+---------+------+---------+-------------+

|Warning|Cannot use range access on index 'ind_gmt_create' due to type on field 'gmt_create'

上述案例是用户在5.5版本升级到5.6版本后出现的隐式转换,导致数据库cpu压力100%,所以我们在定义时间字段的时候一定要采用时间类型的数据类型。

隐式转换案例三

表结构

  CREATE TABLE `t1` (
  `c1` varchar(100) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  `c2` varchar(100) DEFAULT NULL,
  KEY `ind_c1` (`c1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 

CREATE TABLE `t2` (
  `c1` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
  `c2` varchar(100) DEFAULT NULL,
  KEY `ind_c2` (`c2`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 

执行计划

mysql> explain     select t1.* from  t2 left  join  t1 on t1.c1=t2.c1 where t2.c2='b';
+----+-------------+-------+------+---------------+--------+---------+-------+--------+-------------+
| id | select_type | table | type | possible_keys |key| key_len | ref   | rows   | Extra    |
+----+-------------+-------+------+---------------+--------+---------+-------+--------+-------------+
| 1 | SIMPLE | t2 | ref  | ind_c2 | ind_c2 | 303     | const |    258 | Using where |
|1  |SIMPLE |t1  |ALL   | NULL   | NULL   | NULL    | NULL  | 402250 |    |

修改COLLATE

mysql> alter table t1 modify column c1 varchar(100) COLLATE utf8_bin ;                
Query OK, 401920 rows affected (2.79 sec)
Records: 401920  Duplicates: 0  Warnings: 0

执行计划

mysql> explain   select t1.* from  t2 left  join  t1 on t1.c1=t2.c1 where t2.c2='b';
+----+-------------+-------+------+---------------+--------+---------+------------+-------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref | rows  | Extra       |
+----+-------------+-------+------+---------------+--------+---------+------------+-------+-------------+
|  1 | SIMPLE| t2| ref | ind_c2| ind_c2 | 303     | const      |   258 | Using where |
|  1 |SIMPLE| t1|ref| ind_c1  | ind_c1 | 303     | test.t2.c1 | 33527 |             |
+----+-------------+-------+------+---------------+--------+---------+------------+-------+-------------+

可以看到修改了字段的COLLATE后执行计划使用到了索引,所以一定要注意表字段的collate属性的定义保持一致。

两个索引的常见误区

  • 误区一:对查询条件的每个字段建立单列索引,例如查询条件为:A=?and B=?and C=?。
    在表上创建了3个单列查询条件的索引ind_A(A),ind_B(B),ind_C(C),应该根据条件的过滤性,创建适当的单列索引或者组合索引。

  • 误区二:对查询的所有字段建立组合索引,例如查询条件为select A,B,C,D,E,F from T where G=?。
    在表上创建了ind_A_B_C_D_E_F_G(A,B,C,D,E,F,G)。

索引最佳实践

  • 在使用索引时,我们可以通过explain+extended查看SQL的执行计划,判断是否使用了索引以及发生了隐式转换。
  • 由于常见的隐式转换是由字段数据类型以及collation定义不当导致,因此我们在设计开发阶段,要避免数据库字段定义,避免出现隐式转换。
  • 由于MySQL不支持函数索引,在开发时要避免在查询条件加入函数,例如date(gmt_create)。
  • 所有上线的SQL都要经过严格的审核,创建合适的索引。

MySQL · 新特性分析 · CTE执行过程与实现原理

$
0
0

众所周知,Common table expression(CTE)是在大多数的关系型数据库里都存在的特性,包括ORACLE, SQLSERVER,POSTGRESQL等,唯独开源数据库老大MySQL缺失。CTE作为一个方便用户使用的功能,原本是可以利用普通的SQL语句替代的,但是对于复杂的CTE来说,要模拟出CTE的效果还是需要很大的功夫。如果考虑性能那就更是难上加难了。2013年Guilhem Bichot发表的一篇blog模拟了CTE的场景,
从该篇blog中可以看出,对于模拟复杂CTE的场景的难度就可见一斑。2016年9月份,Guilhem实现了MySQL自己的CTE特性,并在MySQL的lab release中进行了发布,邀请评测。本篇文章就是对这个lab release中的CTE实现过程进行一个剖析,让我们了解一下CTE在MySQL内部是如何实现的。

首先,我们看一下简单非递归的CTE的工作过程

CREATE TABLE t(a int);
INSERT INTO t VALUES(1),(2);

下面我们尝试执行一些语句:

mysql> WITH cte(x) as
    -> (SELECT * FROM t)
    -> SELECT * FROM cte;
+------+
| x    |
+------+
|    1 |
+------+
1 row in set (0.00 sec)

可以看到CTE可以工作了。

mysql> SET OPTIMIZER_SWITCH='derived_merge=off';
Query OK, 0 rows affected (0.00 sec)
为了清楚的看到OPTIMIZER的优化过程,我们先暂且关闭derived_merge特性。

mysql> EXPLAIN WITH cte(x) as
    -> (SELECT * FROM t)
    -> SELECT * FROM cte;
+----+-------------+------------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table      | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra |
+----+-------------+------------+------------+------+---------------+------+---------+------+------+----------+-------+
|  1 | PRIMARY     | <derived2> | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    2 |   100.00 | NULL  |
|  2 | DERIVED     | t          | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    1 |   100.00 | NULL  |
+----+-------------+------------+------------+------+---------------+------+---------+------+------+----------+-------+
2 rows in set, 1 warning (0.00 sec)

mysql> show warnings;                                                                                                                                                                                            
+-------+------+-------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                             |
+-------+------+-------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1003 | with `cte` (`x`) as (/* select#2 */ select `test`.`t`.`a` AS `a` from `test`.`t`) /* select#1 */ select `cte`.`x` AS `x` from `cte` |
+-------+------+-------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

从上面的EXPLAIN输出结果我们可以看到,CTE内部优化过程走的流程和subquery是一样的。下面我们打开derived_merge特性来继续看一下。

mysql> SET OPTIMIZER_SWITCH='derived_merge=on';
Query OK, 0 rows affected (0.00 sec)

mysql> EXPLAIN WITH cte(x) as
    -> (SELECT * FROM t)
    -> SELECT * FROM cte;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
|  1 | SIMPLE      | t     | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

mysql> show warnings;
+-------+------+-------------------------------------------------------------+
| Level | Code | Message                                                     |
+-------+------+-------------------------------------------------------------+
| Note  | 1003 | /* select#1 */ select `test`.`t`.`a` AS `x` from `test`.`t` |
+-------+------+-------------------------------------------------------------+
1 row in set (0.00 sec)

从执行计划上我们可以看出CTE已经被优化掉了,并且被merge到了subquery的上层查询。难道CTE仅仅只是subquery的一个替代?那么CTE除了递归特性(稍后介绍),与subquery的区别在哪里呢?下面我们继续看一个栗子:
为了清楚的看到区别,我们还是关闭derived_merge特性。

mysql> SET OPTIMIZER_SWITCH='derived_merge=off';
Query OK, 0 rows affected (0.00 sec)

mysql> EXPLAIN WITH cte(x) as
		(SELECT * FROM t)
		SELECT * FROM 
			(SELECT * FROM cte) AS t1,
			(SELECT * FROM cte) AS t2;
mysql> 执行计划截取片断如下
...
 {
        "table": {
          "table_name": "t2",
          "access_type": "ALL",
          "rows_examined_per_scan": 2,
          "rows_produced_per_join": 4,
          "filtered": "100.00",
          "using_join_buffer": "Block Nested Loop",
          "cost_info": {
            "read_cost": "10.10",
            "eval_cost": "0.80",
            "prefix_cost": "21.40",
            "data_read_per_join": "64"
          },
          "used_columns": [
            "x"
          ],
          "materialized_from_subquery": {
            "using_temporary_table": true,
            "dependent": false,
            "cacheable": true,
            "query_block": {
              "select_id": 4,
              "cost_info": {
                "query_cost": "10.50"
              },
              "table": {
                "table_name": "cte",
                "access_type": "ALL",
                "rows_examined_per_scan": 2,
                "rows_produced_per_join": 2,
                "filtered": "100.00",
                "cost_info": {
                  "read_cost": "10.10",
                  "eval_cost": "0.40",
                  "prefix_cost": "10.50",
                  "data_read_per_join": "32"
                },
                "used_columns": [
                  "x"
                ],
                "materialized_from_subquery": {
                  "sharing_temporary_table_with": { <<注意这里临时表是共享的
                    "select_id": 3
                  }
                }
              }
            }
          }
        }
      }

我们可以看到对于CTE来说,多次利用只会被执行一次。而对于subquery来说,对于每一条query都至少会执行一次。

那么CTE是如何实现多次利用的呢?让我们看看代码:
首先了解一下Common_table_expr这个类的定义:

class Common_table_expr
{
public:
  // 构造函数
  Common_table_expr(MEM_ROOT *mem_root) : references(mem_root),
    recursive(false), tmp_tables(mem_root)
    {}
  // 该函数负责按照CTE的定义(包括CTE的alias,已经自定义的列名)生成一个新的临时表信息,进而替代resolve derived table过程中生成的临时表信息。	
  TABLE *clone_tmp_table(THD *thd, const char *alias);
  // 克隆第一个临时表信息来替换对Query中所有(包含递归CTE定义)对CTE的引用
  bool substitute_recursive_reference(THD *thd, SELECT_LEX *sl);
  // Query中除了CTE自身定义外对该CTE的所有引用的一个数组。
  Mem_root_array<TABLE_LIST *> references;
  /// 是否是递归CTE
  bool recursive;
  /** 
    Array中所有的临时表都是与该CTE相关的,Query中每次用到CTE都会对应生成一个临时表信息。
    但是只有第一个临时表会被存储引擎创建,其他都是共享该临时表。
  */
  List of all TABLEs pointing to the tmp table created to materialize this
  Mem_root_array<TABLE *> tmp_tables;
};

接下来是代码中对于CTE多次引用共享一个临时表实例的代码片断。

bool TABLE_LIST::create_derived(THD *thd)
{
  DBUG_ENTER("TABLE_LIST::create_derived");

  SELECT_LEX_UNIT *const unit= derived_unit();

  // @todo: Be able to assert !table->is_created() as well
  DBUG_ASSERT(unit && uses_materialization() && table);

  if (!table->is_created()) // 当第2次为CTE创建临时表的时候,此时发现临时表还没有创建
  {
    Derived_refs_iterator it(table); 
    while (TABLE *t= it.get_next()) // 这里会遍历CTE表达式相关的所有已经创建的临时表
      if (t->is_created()) // 找到已经创建好的临时表
      {   
		// 直接再次打开临时表,不再重新生成一个临时表。从而达到CTE临时表被共享利用的过程。
        if (open_tmp_table(table)) 
		
          DBUG_RETURN(true);
        break;
      }   
  }

接下来,我们研究一下递归CTE

下面看一个栗子

CREATE TABLE t(a int);
INSERT INTO t VALUES(2),(5);
mysql> WITH RECURSIVE my_cte AS 
		(SELECT a from t UNION ALL SELECT 2+a FROM my_cte WHERE a<10 ) 
		SELECT * FROM my_cte;
+------+
| a    |
+------+
|    2 |
|    5 |
|    4 |
|    7 |
|    6 |
|    9 |
|    8 |
|   11 |
|   10 |
+------+
9 rows in set (15 min 54.43 sec)

对于递归的CTE,结构分为两个部分,一部分是SEED部分,就是不包含CTE自身的部分,作为接下来递归的初始值。另一个部分就是递归如何产生新的记录。对于上面的栗子而言:
SEED部分就是SELECT a from t;递归CTE的新纪录生成规则为SELECT 2+a FROM my_cte WHERE a<10。
对应到代码中是MySQL是如何执行的呢?首先看一个为CTE定义的执行器类结构的重要成员:

class Recursive_executor
{
private:
  // 对应到CTE的定义部分
  SELECT_LEX_UNIT *unit;

  // 对应CTE递归的次数
  uint iteration_counter;
  ...
public:
  // 负责初始化CTE执行器并打开临时表
  bool initialize();
  // 该函数负责定位SEED部分还是CTE递归规则部分,当iteration_counter=0时,返回SEED部分,否则返回CTE递归规则部分
  SELECT_LEX *first_select() const;
  // 该函数是用来辅助执行器定位SEED部分的结尾以及CTE递归规则的结尾
  SELECT_LEX *last_select() const
  // 该函数用来判断CTE是否依旧满足递归条件,如果满足执行器便会继续执行CTE的递归部分
  bool more_iterations();
}

下面代码片段描述了CTE的执行过程:

bool SELECT_LEX_UNIT::execute(THD *thd)
{
  ...

  do
  {
    for (auto sl= recursive_executor.first_select();
         sl != recursive_executor.last_select();
         sl= sl->next_select())
    {
	  // 设置当前执行SEED部分或者CTE递归部分
      thd->lex->set_current_select(sl);

      // 根据LIMIT语句定义LIMIT相关执行部分
      if (set_limit(thd, sl))
        DBUG_RETURN(true);

      // 执行当前查询。这里由于不再重新打开表,所以对于临时表每次都会扫描到每次递归新产生的数据,也就是每次递归所使用到的新的SEED结果。
      sl->join->exec();
      status= sl->join->error != 0;

	  // 如果包含UNION操作
      if (sl == union_distinct)
      {
        // This is UNION DISTINCT, so there should be a fake_select_lex
        DBUG_ASSERT(fake_select_lex != NULL);
        if (table->file->ha_disable_indexes(HA_KEY_SWITCH_ALL))
          DBUG_RETURN(true); /* purecov: inspected */
        table->no_keyread= 1;
      }
      if (status)
        DBUG_RETURN(true);

      if (union_result->flush())
        DBUG_RETURN(true); /* purecov: inspected */
    }
  } while (recursive_executor.more_iterations()); // 这里执行器判断是否需要继续递归

  ...
}

从上面的代码我们了解了CTE的具体工作过程,那么下面我们用具体的例子说明一下MySQL中CTE的执行过程。

CREATE TABLE category(
        category_id INT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(20) NOT NULL,
        parent INT DEFAULT NULL
);

INSERT INTO category VALUES(1,'ELECTRONICS',NULL),(2,'TELEVISIONS',1),(3,'TUBE',2),
        (4,'LCD',2),(5,'PLASMA',2),(6,'PORTABLE ELECTRONICS',1),(7,'MP3 PLAYERS',6),(8,'FLASH',7),
        (9,'CD PLAYERS',6),(10,'2 WAY RADIOS',6);

我们按电器种类广度遍历一下category表:

mysql> WITH RECURSIVE cte AS
    -> (
    ->   SELECT category_id, name, 0 AS depth FROM category WHERE parent IS NULL
    ->   UNION ALL
    ->   SELECT c.category_id, c.name, cte.depth+1 FROM category c JOIN cte ON
    ->     cte.category_id=c.parent
    -> )
    -> SELECT * FROM cte ORDER BY depth;
+-------------+----------------------+-------+
| category_id | name                 | depth |
+-------------+----------------------+-------+
|           1 | ELECTRONICS          |     0 |
|           2 | TELEVISIONS          |     1 |
|           6 | PORTABLE ELECTRONICS |     1 |
|           5 | PLASMA               |     2 |
|           7 | MP3 PLAYERS          |     2 |
|           9 | CD PLAYERS           |     2 |
|          10 | 2 WAY RADIOS         |     2 |
|           3 | TUBE                 |     2 |
|           4 | LCD                  |     2 |
|           8 | FLASH                |     3 |
+-------------+----------------------+-------+
10 rows in set (18.65 sec)

递归执行过程如下:

  1. 查找parent IS NULL的第一种类别,我们可以得到ELECTRONICS
  2. 接着查找parent == ELECTRONICS的第二类电器种类,可以看出我们可以得到TELEVISIONS和PORTABLE ELECTRONICS
  3. 接着查找parent == TELEVISIONS 和 parent == PORTABLE ELECTRONICS,我们可以得到第三类电器分别是PLASMA,MP3 PLAYERS,CD PLAYERS,2 WAY RADIOS,TUBE,LCD
  4. 接着继续查找属于第三类电器种类的产品,最后得到 FLASH。
  5. 执行完毕。

综上所述,本篇文章简要的分析了MySQL Lab release中发布的CTE特性的实现方式,并对新增重点代码片段进行了介绍。希望能够帮助大家能对CTE的工作原理以及实现过程有个详细的了解。

PgSQL · 源码分析 · PG优化器物理查询优化

$
0
0

在之前的一篇月报中,我们已经简单地分析过PG的优化器(PgSQL · 源码分析 · PG优化器浅析),着重分析了SQL逻辑优化,也就是尽量对SQL进行等价或者推倒变换,以达到更有效率的执行计划。本次月报将会深入分析PG优化器原理,着重物理查询优化,包括表的扫描方式选择、多表组合方式、多表组合顺序等。

表扫描方式

表扫描方式主要包含顺序扫描、索引扫描以及Tid扫描等方式,不同的扫描方式

  • Seq scan,顺序扫描物理数据页
postgres=> explain select * from t1 ;
                     QUERY PLAN
-----------------------------------------------------
 Seq Scan on t1  (cost=0.00..14.52 rows=952 width=8)
  • Index scan,先通过索引值获得物理数据的位置,再到物理页读取
postgres=> explain select * from t1 where a1 = 10;
                             QUERY PLAN
--------------------------------------------------------------------
 Index Scan using t1_a1_key on t1  (cost=0.28..8.29 rows=1 width=8)
   Index Cond: (a1 = 10)
  • Tid scan,通过page号和item号直接定位到物理数据
postgres=> explain select * from t1 where ctid='(1,10)';
                    QUERY PLAN
--------------------------------------------------
 Tid Scan on t1  (cost=0.00..4.01 rows=1 width=8)
   TID Cond: (ctid = '(1,10)'::tid)

选择度计算

  • 全表扫描选择度计算

全表扫描时每条记录都会返回,所以选择度为1,所以rows=10000

EXPLAIN SELECT * FROM tenk1;

                         QUERY PLAN
-------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..458.00 rows=10000 width=244)


 SELECT relpages, reltuples FROM pg_class WHERE relname = 'tenk1';

 relpages | reltuples
----------+-----------
      358 |     10000
  • 整型大于或者小于选择度计算
EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 1000;

                                   QUERY PLAN
--------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=24.06..394.64 rows=1007 width=244)
   Recheck Cond: (unique1 < 1000)
   ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..23.80 rows=1007 width=0)
         Index Cond: (unique1 < 1000)

SELECT histogram_bounds FROM pg_stats
WHERE tablename='tenk1' AND attname='unique1';

                   histogram_bounds
------------------------------------------------------
 {0,993,1997,3050,4040,5036,5957,7057,8029,9016,9995}
selectivity = (1 + (1000 - bucket[2].min)/(bucket[2].max - bucket[2].min))/num_buckets
            = (1 + (1000 - 993)/(1997 - 993))/10
            = 0.100697
rows = rel_cardinality * selectivity
     = 10000 * 0.100697
     = 1007  (rounding off)
  • 字符串等值选择度计算
EXPLAIN SELECT * FROM tenk1 WHERE stringu1 = 'CRAAAA';

                        QUERY PLAN
----------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..483.00 rows=30 width=244)
   Filter: (stringu1 = 'CRAAAA'::name)
SELECT null_frac, n_distinct, most_common_vals, most_common_freqs FROM pg_stats
WHERE tablename='tenk1' AND attname='stringu1';
null_frac         | 0
n_distinct        | 676
most_common_vals|{EJAAAA,BBAAAA,CRAAAA,FCAAAA,FEAAAA,GSAAAA,JOAAAA,MCAAAA,NAAAAA,WGAAAA}
most_common_freqs | {0.00333333,0.003,0.003,0.003,0.003,0.003,0.003,0.003,0.003,0.003}
selectivity = mcf[3]
            = 0.003
rows = 10000 * 0.003
     = 30

备注:如果值不在most_common_vals里面,计算公式为:

selectivity = (1 - sum(mvf))/(num_distinct - num_mcv)
  • cost计算

代价模型:总代价=CPU代价+IO代价+启动代价

postgres=> explain select * from t1 where a1 > 10;
                     QUERY PLAN
-----------------------------------------------------
 Seq Scan on t1  (cost=0.00..16.90 rows=942 width=8)
   Filter: (a1 > 10)
(2 rows)
其中:
postgres=> select relpages, reltuples from pg_class where relname = 't1';
 relpages | reltuples
----------+-----------
        5 |       952
(1 row)
cpu_operator_cost=0.0025
cpu_tuple_cost=0.01
seq_page_cost=1
random_page_cost=4

总cost = cpu_tuple_cost * 952 + seq_page_cost * 5 + cpu_operator_cost * 952
= 16.90
其他扫描方式cost计算可以参考如下函数:

postgres=> select amcostestimate,amname from pg_am ;
  amcostestimate  | amname
------------------+--------
 btcostestimate   | btree
 hashcostestimate | hash
 gistcostestimate | gist
 gincostestimate  | gin
 spgcostestimate  | spgist
(5 rows)

表组合方式

  • Nest Loop

screenshot.png

SELECT  * FROM     t1 L, t2 R WHERE  L.id=R.id

假设:

M = 20000 pages in L, pL = 40 rows per page,
N = 400 pages in R, pR = 20 rows per page.

select relpages, reltuples from pg_class where relname=‘t1’

L和R进行join

for l in L do
  for r in R do
    if rid == lid  then ret += (r, s)

对于外表L每一个元组扫描内表R所有的元组
总IO代价: M + (pL * M) * N = 20000 + (4020000)400
= 320020000

  • MergeJoin

screenshot.png

主要分为3步:

(1) Sort L on lid 代价MlogM

(2) Sort R on rid 代价NlogN

(3) Merge the sorted L and R on lid and rid 代价M+N

  • HashJoin

使用HashJoin的前提是其中假设一个表可以完全放在内存中,实际过程中可能统计信息有偏差,优化器认为一个表可以放到内存中,事实上数据在内存中放不下,需要使用临时文件,这样会降低性能。

screenshot.png

表的组合顺序

不同的组合顺序将会产生不同的代价,想要获得最佳的组合顺序,如果枚举所有组合顺序,那么将会有N!的排列组合,计算量对于优化器来说难以承受。PG优化器使用两种算法计算更优的组合顺序,动态规划和遗传算法。对于连接比较少的情况使用动态规划,否则使用遗传算法。

  • 动态规划求解过程

PG优化器主要考虑将执行计划树生成以下三种形式:

screenshot.png

动态规划的思想可以参考百度百科动态规划,主要将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。具体应用在表组合顺序上,则是先考虑单表最优访问访问,然后考虑两种组合,再考虑多表组合,最终得到更优的解。

screenshot.png

SQL Server · 特性介绍 · 聚集列存储索引

$
0
0

摘要

微软在SQL Server 2012引入了列存储技术,使得OLAP和Data warehouse场景性能提升10X,并且数据压缩能力超过传统表7X。这项技术包含三个方面的创新:列存储索引、Batch Mode Processing和基于Column Segment的压缩。但是,SQL Server 2012列存储索引的一个致命缺点是列存储索引表会进入只读状态,用户无法更新操作。SQL Server 2014引入了可更新聚集列存储索引技术来解决列存储索引表只读的问题,使得列存储索引表使用的范围和场景大大增加。

名词解释

SQL Server 2014使用聚集列存储索引来解决列存储索引表只读问题的同时,引入了几个全新的名称。

Clustered Column Store Index

聚集列存储索引是一个可更新的整张表数据物理存储结构,并且每张表只允许创建一个聚集列存储索引,不能与其他的索引并存。我们可以对聚集列存储索引进行Insert、Delete、Update、Merge和Bulk Load操作。这也是SQL Server 2014 Clustered Columnstore Index正真强大的地方。下面这张来自微软官方的图描述了聚集列存储索引的物理存储结构:

01.png

Delta Store

由于column store是基于column segment压缩而存放的结构,为了减少DML操作导致的列存储索引碎片和提高更新性能,系统在做数据更新操作(Insert和Update)的时候,不会直接去操作已经压缩存储的column store(这样系统开销成本太高),而是引入一个临时存储的中间结构Delta Store。Delta Store结构和传统的基于B-Tree结构的row store没有任何差异,它存在的意义在于提高系统的DML操作效率、提升Bulk Loading的性能和减少Clustered Column Store Index碎片。

Delete Bitmap

对于更新操作中的删除动作,会比较特殊,聚集列存储索引采用的是标记删除的方式,而没有立即物理删除column store中的数据。为了达到标记删除的目的,SQL Server 2014引入了另一个B-Tree结构,它叫着Delete Bitmap,Delete Bitmap中记录了被标记删除的Row Group对应的RowId,在后续查询过程中,系统会过滤掉已经被标记删除的Row Group。

Tuple Mover

当不断有数据插入Clustered Column Store Index表的时候,Delta Store中存储的数据会越来越多,当大量数据存储在Delta Store中以后,势必会导致用户的查询语句性能降低(因为Delta Store是B-Tree结构,相对列存储结构性能降低)。为了解决这个问题,SQL Server 2014引入了一个全新的后台进程,叫着Tuple Mover。Tuple Mover后台进程会周期性的检查Closed Delta Store并且将其压缩转化为相应的Column Store Segment。Tuple Mover进程每次读取一个Closed Delta Store,在此过程中,并不会阻塞其他进程对Delta Store的读取操作(但会阻塞并发删除和更新操作)。当Tuple Mover完成压缩处理和转化以后,会创建一个用户可见的新的Column Store Segment,并使Delta Store结构中相应的数据不可见(正真的删除操作会等待所有读取该Delta Store进程退出后),在这以后的用户读取行为会直接从压缩后的Column Store中读取。
来一张Tuple Mover的手绘图(画外音:手绘图是一种情怀,就像劳斯莱斯是纯手工打造一样):

02.png

数据操作

基于Delta Store和Delete Bitmap的特殊设计,SQL Server 2014聚集列存储索引看起来是做到了可更新操作,实际上聚集列存储索引本身是不可变的。它是在借助了两个可变结构以后,达到了可更新的目的。这三部分结构示意图如下所示:

03.png

接下来,我们看看SQL Server 2014聚集列存储索引表DML操作原理。其中DML操作包括:INSERT、DELETE、UPDATE、MERGE和BULK操作,其中以BULK批量数据操作最为复杂,也是这一节需要详细讲解的地方。

INSERT

当我们执行INSERT操作的时候,INSERT的这一条新的记录不是立即进入Column Store中,而是进入Delta Store B-Tree结构中,Delta Store结构中存储的数据会在重组(Reorganize)聚集列存储索引的时候进入Column Store。INSERT完成后的数据读取操作,SQL Server会从Column Store和Delta store两个结构中读取,然后返回给用户。

DELETE

当我们执行DELETE操作的时候,数据库系统并不会直接从Clustered Store中直接删除数据,而是往Delete Bitmap结构中插入一条带有rowid的记录,系统会在聚集列存储索引重建(Rebuild)的时候最终删除Column Store中的数据。DELETE操作完成后的数据读取操作,SQL Server从Column Store中读取数据,然后通过rowid过滤掉在Delete Bitmaps中已经标记删除的数据,最后返回给用户。

UPDATE

当我们执行UPDATE操作的时候,数据库系统将这个操作分解成DELETE和INSERT操作,即先标记删除老的这条记录然后插入新的记录。SQL Server系统会在Delete Bitmaps中先插入一条带有rowid的记录,然后在Delta Store中插入一条新的记录数据。

MERGE

当我们执行MERGE操作的时候,数据库系统会将这个操作分解为相应的INSERT、DELETE或者是UPDATE操作。

BULK LOADING

在Bulk LOADING大批次数据导入介绍之前,我们必须要重点介绍几个重要的数字:

102400:数据是否直接进入Column Store的Row Group 行数的最小临界值。

1048576:一个Row Group可以容纳的最大行数。

BatchSize:Bulk Insert操作的参数,表示每批次处理的记录数。

Rows:需要批量导入聚集列存储索引表的记录总数,Rows应该总是大于等于0的整数。

在聚集列存储索引表场景中,微软SQL Server 2014推荐使用Bulk Insert进行大批次数据的更新,效率更高,维护成本也更低。聚集列存储索引针对Bulk Insert处理的逻辑是:如果Bulk Insert操作的总记录条数(Rows)小于102400条,那么所有数据会被加载到Delta Store结构中;如果Rows大于等于102400,会参照Bulk Insert的BatchSize进一步细分:BatchSize小于102400时,所有数据全部进入Delta Store;BatchSize大于等于102400时,大部分数据进入Column Store,剩下小部分数据进入Delta Store。比如:rows总共有102400 * 2 + 3 = 204803条,BatchSize为102399时,所有数据会进入Delta Store;BatchSize为102400时,会有两个Row Group的数据共204800进入Column Store,剩下3条数据进入Delta Store。
参见微软官方的流程图:

04.png

这个流程图把大致的Bulk Insert数据流向说清楚了,但是它没有把几个具体的数字和相关的逻辑表达的很清楚。现在,我们需要把详细逻辑理解清楚,以此来指导我们进行Bulk Insert来提高大批次数据导入的效率。为了表达清楚Bulk Insert的详细逻辑,参见下面的伪代码,每一个BEGIN END之间是一个语句块:

IF Rows < 102400
BEGIN
		All Rows Insert into Delta Store
END
ELSE IF Rows >= 102400 & Rows < 1048576
BEGIN
	IF BatchSize < 102400
	BEGIN
		All Rows Insert into Delta Store Batchly
	END
	ELSE IF BatchSize >= 102400
	BEGIN
		SomeData Insert into Column Store Batchly
		SomeData Remaining Insert into Delete Store
	END
END
ELSE IF Rows >= 1048576
BEGIN
	IF BatchSize < 102400
	BEGIN
		All Rows Insert into Delta Store Batchly
	END
	ELSE IF BatchSize >= 102400 & BatchSize < 1048576
	BEGIN
		SomeData Insert into Column Store Batchly
		SomeData Remaining Insert into Delete Store
	END
	ELSE IF BatchSize >= 1048576
	BEGIN
		SomeData Insert into Column Store Batchly
		IF SomeData Remaining >= 102400
		BEGIN
			Some of the SomeData Remaining will be Inserted into Column Store Batchly (batchsize = 102400)
			The left of the SomeData Remaining will be Inserted into Delta Store
		END
		ELSE IF SomeData Remaining < 102400
		BEGIN
			Some of the SomeData Remaining will be Inserted into Delta Store
		END
	END
END

伪代码写出来的逻辑还是很丑陋,也比较复杂,难于理解。再次手绘工作流程图:

05.png

理论解释了,伪代码写好了,流程图也手绘了,接下来测试三种场景的Bulk LOADING。

创建测试环境

准备测试数据库、临时存储过程、需要用到的三个数据文件。

-- Create testing database
IF DB_ID('ColumnStoreDB') IS NULL
	CREATE DATABASE ColumnStoreDB;
GO

USE ColumnStoreDB
GO
-- create temp procedure
IF OBJECT_ID('tempdb..#UP_ReCreateTestTable', 'P') IS NOT NULL
BEGIN
	DROP PROC #UP_ReCreateTestTable
END
GO

CREATE PROC #UP_ReCreateTestTable
AS
BEGIN
	SET NOCOUNT ON
	-- create demo table SalesOrder
	IF OBJECT_ID('dbo.SalesOrder', 'U') IS NOT NULL
	BEGIN
		EXEC('DROP TABLE dbo.SalesOrder')
	END
	CREATE TABLE dbo.SalesOrder
	(
		OrderID INT NOT NULL
		,AutoID INT NOT NULL
		,UserID INT NOT NULL
		,OrderQty INT NOT NULL
		,Price DECIMAL(8,2) NOT NULL
		,UnitPrice DECIMAL(19,2) NOT NULL
		,OrderDate DATETIME NOT NULL
	);

	CREATE CLUSTERED COLUMNSTORE INDEX CCI_SalesOrder 
	ON dbo.SalesOrder;
END
GO

三个数据文件,分别存在102399、204803和2199555条记录,使用BCP从上一期测试环境SQL Server 2012数据库导出,BCP导出脚本如下:

BCP  "SELECT TOP 102399 * FROM ColumnStoreDB.dbo.SalesOrder" QueryOut "Scenario1.102399Rows" /c /T /S CHERISH-PC\SQL2012
BCP  "SELECT TOP 204803 * FROM ColumnStoreDB.dbo.SalesOrder" QueryOut "Scenario2.204803Rows" /c /T /S CHERISH-PC\SQL2012
BCP  "SELECT TOP 2199555 * FROM ColumnStoreDB.dbo.SalesOrder" QueryOut "Scenario3.2199555Rows" /c /T /S CHERISH-PC\SQL2012

测试需要的环境至此准备完毕。

Rows小于102400

这个场景需要总共需要导入的数据量为102399条,小于102400条记录数,所以数据无法直接进入Column Store结构Row Group中。

-- Scenario 1 : BULK LOADING ROWS:  102399 = 102400 - 1

EXEC #UP_ReCreateTestTable

BEGIN TRAN Scenario1

BULK INSERT dbo.SalesOrder
FROM 'C:\Temp\Scenario1.102399Rows'
WITH (
	BATCHSIZE = 102400
	,KEEPIDENTITY
);

SELECT * 
FROM sys.column_store_row_groups 
WHERE object_id = object_id('dbo.SalesOrder','U')
ORDER BY row_group_id DESC;

SELECT 
	database_name = DB_NAME(resource_database_id)
	,resource_type
	,resource_description
	,request_mode
	,request_type
	,request_status
	--,* 
FROM sys.dm_tran_locks
WHERE request_session_id = @@SPID

ROLLBACK TRAN Scenario1
-- END Scenario 1 

执行结果如下:

06.png

从展示的结果来看,Rows为102399条数据数小于Row Group进入Column Store的最小值102400。所以数据直接进入了Delta Store结构,并且数据在Bulk Insert的时候,会对表对应的Row Group加上X锁。

Rows大于等于102400小于1048576

这个场景需要总共导入204803条记录。

-- Scenario 2 : BULK LOADING ROWS: 204803 = 102400 * 2 + 3

EXEC #UP_ReCreateTestTable

BEGIN TRAN Scenario2

BULK INSERT dbo.SalesOrder
FROM 'C:\Temp\Scenario2.204803Rows'
WITH (
	BATCHSIZE = 102400	-- 102400 / 102401
	,KEEPIDENTITY
);

SELECT * 
FROM sys.column_store_row_groups 
WHERE object_id = object_id('dbo.SalesOrder','U')
ORDER BY row_group_id DESC;

SELECT 
	database_name = DB_NAME(resource_database_id)
	,resource_type
	,resource_description
	,request_mode
	,request_type
	,request_status
	--,* 
FROM sys.dm_tran_locks
WHERE request_session_id = @@SPID

ROLLBACK TRAN Scenario2
-- END Scenario 2

执行结果展示如下:

07.png

总的记录数Rows为204803 = 102400 * 2 + 3 超过102400并且Batch Size是大于等于102400的,所以,最后数据会插入到Column Store的2个Row Groups,剩下的3条数据进入Delta Store存储结构,在数据导入过程中,对三个Row Group加了X锁。

Rows大于等于1048576

这种情况相对来说是最为复杂的,我们以Bulk Insert 2199555 (等于1048576 * 2 + 102400 + 3)条记录,BatchSize分别102399和1048577为例。

-- Scenario 3 : BULK LOADING ROWS: 2199555 = 1048576 * 2 + 102400 + 3

EXEC #UP_ReCreateTestTable

BEGIN TRAN Scenario3

BULK INSERT dbo.SalesOrder
FROM 'C:\Temp\Scenario3.2199555Rows'
WITH (
	BATCHSIZE = 102399 -- 102399  / 1048577
	,KEEPIDENTITY
);

SELECT * 
FROM sys.column_store_row_groups 
WHERE object_id = object_id('dbo.SalesOrder','U')
ORDER BY row_group_id DESC;

SELECT 
	database_name = DB_NAME(resource_database_id)
	,resource_type
	,resource_description
	,request_mode
	,request_type
	,request_status
	--,* 
FROM sys.dm_tran_locks
WHERE request_session_id = @@SPID

ROLLBACK TRAN Scenario3
-- END Scenario 3

当BatchSize为102399时,执行结果展示如下:

08.png

从这个结果来看,当BatchSize小于102400时,所有的数据Bulk插入操作都是进入Delta Store结构(后台进程Tuple Mover会将Delta Store结构中数据迁移到Column Store结构)。由于数据不是直接进入Column Store结构,而是全部数据聚集在Delta Store结构中(Delta Store是传统的B-Tree)。根据之前的介绍,这个时候的用户查询操作,系统会取Column Store和Delta Store两者中的数据,势必会给Delta Store带来巨大读取压力。因为,这部分新插入的200多万条数据无法使用列存储的优势,还是必须走传统的B-Tree结构查询。
如果调大BatchSize的值为比Row Group中可以存放的最大记录数还大。BatchSize为1048577的执行结果展示如下:

09.png

从这个结果分析可以得出结论,当BatchSize修改为1048577后,Bulk Insert操作的数据会直接进入Column Store,形成三个Row Group,而不是暂存在Delta Store结构中,仅剩下2条数据存放在Delta Store中。让我们来看看这个结果到底是怎么形成的,首先分解下总的记录数2199555,拆开来可以表示为:1048576 * 2 + 102400 + 3,换句话说,当BatchSize设置为1048577时,我们每个Row Group中可以直接存放1048576条记录,剩下的102403条记录也满足Row Group存放的最小记录数,SQL Server取了102401条放入Column Store,最后余下2条不满足Row Group存放的最小记录数,所以存放到了Delta Store结构中。这样做可以最大限度的发挥Column Store的优点,而避免Delta Store B-Tree查询的缺点,从而最大限度的提升查询性能。

Bulk Insert总结

Bulk Insert操作总结,如果总的记录数Rows小于102400,所有的数据记录直接进入Delta Store;如果总的记录数大于等于102400,但小于1048576,并且Batch Size也大于等于102400,绝大多数数据会进入Column Store,少量会进入Delta Store;如果总的记录数大于等于1048576,并且Batch Size也大于等于102400,绝大多数数据会进入Column Store,少量会进入Delta Store,最优的Batch Size值是1048576,使得压缩的Column Store中每个Row Group可以存放最多的数据记录数。

限制条件

相对于SQL Server 2012列存储索引表的限制而言,详情参见SQL Server · 特性分析 · 2012列存储索引技术,SQL Server 2014聚集列存储索引的限制有了很大改进的同时,也加入了新的限制。

限制改善

SQL Server 2014列存储索引相对于SQL Server 2012,有了不少的改善,比如:

  • SQL Server 2014既支持Nonclustered Columnstore Index也指出Clustered Columnstore Index,并且Clustered Columnstore Index表支持DML可更新操作。使得列存储索引使用的范围和场景大大增加。

  • 开始支持二进制类型:binary(n)、varbinary(n),但不包varbinary(max)。

  • 支持精度大于18位的decimal和numeric数据类型。

  • 支持Uniqueidentifier数据类型

  • Change tracking:Clustered Columnstore Index表支持;而Nonclustered Columnstore Index表不支持,因为表之只读的。

  • Change data capture:Clustered Columnstore Index表支持;而Nonclustered Columnstore Index表不支持,因为表是只读的。

新增限制

SQL Server 2014同样也新增了不少的限制,比如:

  • 整个表仅允许建立一个聚集列存储索引,不允许再有其他的索引。

  • 聚集列存储索引表不支持Linked Server,链接服务器不可访问聚集列存储索引表;非聚集列存储索引表是支持链接服务器的。

  • 聚集列存储索引没有对数据做任何排序。按照常理,“聚集”类型意味着数据排序,但聚集列存储索引表是没有按照任何列物理排序的,这个需要特别注意。

  • 聚集列存储索引表不支持游标和触发器;非聚集列存储索引表是支持游标和触发器的。

Readable secondary:在SQL Server 2014 AlwaysOn 场景中,Clustered Columnstore Index不支持secondary只读角色;Nonclustered Columnstore Index支持secondary只读角色

引用文章

PgSQL · 应用案例 · 聚集存储 与 BRIN索引

$
0
0

背景

在现实生活中,人们的各种社会活动,会产生很多的行为数据,比如购物、刷卡、打电话、开房、吃饭、玩游戏、逛网站、聊天 等等。

如果可以把它当成一个虚拟现实(AR)的游戏,我们所有的行为都被记录下来了。

又比如,某些应用软件,在征得你的同意的情况下,可能会记录你的手机行为、你的运动轨迹等等,这些数据可能会不停的上报到业务数据库中,每条记录也许代表某个人的某一次行为。

全球人口非常多,每个人每时每刻都在产生行为数据的话,对于单个人的数据来说,他产生的第一条行为和他产生的第二条行为数据中间可能被其他用户的数据挤进来(如果是堆表存储的话,就意味着这两条数据不在一起,可能相隔好多条记录)。

行为、轨迹数据有啥用?

除了我们常说的群体分析(大数据分析)以外,还涉及到微观查询。

比如最近很火的《三生三世十里桃花》,天族也许会对翼族的首领(比如玄女)进行监控,微观查询他的所有轨迹。

pic

又或者神盾局,对某些人物行为轨迹的明细跟踪和查询

pic

微观查询(行为、轨迹明细)的痛点

为了提升数据的入库速度,通常我们会使用堆表存储,堆表存储的最大特点是写入极其之快,通常一台普通服务器能做到GB/s的写入速度,但是,如果你要频繁根据用户ID查询他产生的轨迹数据的话,会涉及大量的离散IO。查询性能也许就不如写入性能了。

有哪些技术能降低离散IO、提升大范围轨迹数据查询的吞吐?

1. 聚集存储

比如按照用户ID来聚集存储,把每个人的数据按照他个人产生数据的顺序进行聚集存储(指物理介质),那么在根据用户ID进行查询时(比如一次查询出某人在某个时间段的所有行为,假设有1万条记录,那么聚集前也许要扫描10000个数据块,而聚集后也许只需要扫描几十个数据块)。

2. 行列变换

将轨迹数据根据用户ID进行聚合,存入单行,比如某人每天产生1万条轨迹数据,每天的轨迹数据聚合为一条。

聚合为一条后,扫描的数据块可以明显减少,提升按聚集KEY查询的效率。

3. index only scan

将数据按KEY组织为B数,但是B树叶子节点的相邻节点并不一定是物理相邻的,它们实际上是通过链表连接的,所以即使是INDEX ONLY SCAN,也不能保证不产生离散IO,反而基本上都是离散IO。只是扫描的数据块总数变少了。

所以这个场景,index only scan并不是个好主意哦。

对于以上三种方法,任何一种都只能针对固定的KEY进行数据组织,所以,如果你的查询不仅仅局限于用户ID,比如还有店铺ID,商品ID等其他轨迹查询维度,那么一份数据不可避免的也会产生离散IO。

此时,你可以使用存储换时间,即每个查询维度,各冗余一份数据,每份数据选择对应的聚集列(比如三份冗余数据,分别对应聚集列:用户ID、商品ID、店铺ID)。

PostgreSQL 聚集存储

PostgreSQL 的表使用的是堆存储,插入时根据FSM和空间搜索算法寻找合适的数据块,记录插入到哪个数据块是不受控制的。

对于数据追加型的场景,表的数据文件会不断扩大,在文件末尾扩展数据块来扩展存储空间。

FSM算法参考

src/backend/storage/freespace/README

那么如何让PostgreSQL按照指定KEY聚集存储呢,PostgreSQL 提供了一个SQL语法cluster,可以让表按照指定索引的顺序存储。

PS,这种方法是一次性的,并不是实时的。

Command:     CLUSTER  
Description: cluster a table according to an index  
Syntax:  
CLUSTER [VERBOSE] table_name [ USING index_name ]  
CLUSTER [VERBOSE]  

这种方法很适用于行为、轨迹数据,为什么这么说呢?

首先这种数据有时间维度,另一方面这种数据通常有被跟踪对象的唯一标识,例如用户ID,这个标识即后期的查询KEY。

我们可以对这类数据按被跟踪对象的唯一标识HASH后分片,打散到多个数据库或分区表。

同时在每个分区表,再按时间维度进行二级分区,比如按小时分区。

每个小时对前一个小时的数据使用cluster,对堆表按被跟踪对象的唯一标识进行聚集处理。

查询时,按被跟踪对象的唯一标识+时间范围进行检索,扫描的数据块就非常少(除了当前没有聚集处理的数据)。

这种方法即能保证数据插入的高效,也能保证轨迹查询的高效。

PostgreSQL BRIN 聚集数据 块级索引

我们通常所认知的除了BTREE,HASH索引,还有一种块级索引BRIN,是针对聚集数据(流式数据、值与物理存储线性相关)的一种轻量级索引。

比如每连续的128个数据块,计算它们的统计信息(边界值、最大、最小值、COUNT、SUM、NULL值个数等)。

这种索引非常小,查询性能也非常高。

有几篇文档介绍BRIN

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

《PostgreSQL 9.5 new feature - lets BRIN be used with R-Tree-like indexing strategies For “inclusion” opclasses》

《PostgreSQL 9.5 new feature - BRIN (block range index) index》

PostgreSQL 行列变换

除了聚集存储,还有一种提升轨迹查询效率的方法。行列变换。

比如每个被跟踪对象,一天产生1万条记录,将这1万条数据聚合为一条。查询时效率也非常高。

但是问题来了,这种方法不适合除了时间条件以外,还有其他查询条件的场景。譬如某个用户某个时间段内,在某个场所(这个是新增条件)的消费记录。

这显然需要一个新的索引来降低数据扫描。

排除这个需求,如果你只有被跟踪ID+时间 两个维度的查询需求,那么使用行列变换不失为一种好方法。

如何实施行列变换

PostgreSQL支持多种数据类型,包括 表类型,复合类型,数组、hstore、JSON。

表类型 - 在创建表时,自动被创建,指与表结构一致的数据类型。

复合类型 - 用户可以根据需要自己定义,比如定义一个复数类型 create type cmp as (c1 float8, c2 float8);

数组 - 基于基本类型的一维或者多维数组,表类型也支持数组,可用于行列变换,将多条记录存储为一个数组。

hstore - key-value类型,可以有多个KV组。

json - 无需多言。

行列变换后,我们留几个字段:

被跟踪ID,时间段(时间范围类型tsrange),合并字段(表数组、HSTORE、JSON都可以)

聚集、行列变换 测试

同一份数据,测试离散、聚集、行列变换后的性能。

堆表 - 离散存储

1. 构造1万个ID,每个ID一万条记录,总共1亿记录,全离散存储。

create unlogged table test(id int, info text, crt_time timestamp);  
  
insert into test select generate_series(1,10000), md5(id::text), clock_timestamp() from generate_series(1,10000) t(id);  
  
postgres=# \dt+  
                           List of relations  
 Schema |        Name        | Type  |  Owner   |  Size   | Description   
--------+--------------------+-------+----------+---------+-------------  
 public | test               | table | postgres | 7303 MB |   

2. 创建btree索引

set maintenance_work_mem ='32GB';  
create index idx_test_id on test using btree (id);  
  
postgres=# \di+  
                                     List of relations  
 Schema |       Name       | Type  |  Owner   |       Table        |  Size   | Description   
--------+------------------+-------+----------+--------------------+---------+-------------  
 public | idx_test_id      | index | postgres | test               | 2142 MB |   

3. 通过查询物理行号、记录,确认离散度

select ctid,* from test where id=1;  
  
postgres=# select ctid,* from test where id=1;  
     ctid     | id |               info               |          crt_time            
--------------+----+----------------------------------+----------------------------  
 (0,1)        |  1 | c4ca4238a0b923820dcc509a6f75849b | 2017-02-19 21:26:49.270193  
 (93,50)      |  1 | c81e728d9d4c2f636f067f89cc14862c | 2017-02-19 21:26:49.301129  
 (186,99)     |  1 | eccbc87e4b5ce2fe28308fd9f2a7baf3 | 2017-02-19 21:26:49.330993  
 (280,41)     |  1 | a87ff679a2f3e71d9181a67b7542122c | 2017-02-19 21:26:49.360924  
 (373,90)     |  1 | e4da3b7fbbce2345d7772b0674a318d5 | 2017-02-19 21:26:49.390941  
 ... ...  
  
postgres=# select ctid,* from test where id=10000;  
     ctid     |  id   |               info               |          crt_time            
--------------+-------+----------------------------------+----------------------------  
 (93,49)      | 10000 | c4ca4238a0b923820dcc509a6f75849b | 2017-02-19 21:26:49.301121  
 (186,98)     | 10000 | c81e728d9d4c2f636f067f89cc14862c | 2017-02-19 21:26:49.330985  
 (280,40)     | 10000 | eccbc87e4b5ce2fe28308fd9f2a7baf3 | 2017-02-19 21:26:49.360917  
 (373,89)     | 10000 | a87ff679a2f3e71d9181a67b7542122c | 2017-02-19 21:26:49.390933  

4. 轨迹查询执行计划,使用最优查询计划

postgres=# explain (analyze,verbose,timing,costs,buffers) select * from test where id=1;  -- 优化器选择bitmapscan , 减少离散扫描。但是引入了ctid SORT。     
                                                         QUERY PLAN                                                            
-----------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on public.test  (cost=111.74..12629.49 rows=9816 width=45) (actual time=6.682..28.631 rows=10000 loops=1)  
   Output: id, info, crt_time  
   Recheck Cond: (test.id = 1)  
   Heap Blocks: exact=10000  
   Buffers: shared hit=10031  
   ->  Bitmap Index Scan on idx_test_id  (cost=0.00..109.29 rows=9816 width=0) (actual time=4.074..4.074 rows=10000 loops=1)  
         Index Cond: (test.id = 1)  
         Buffers: shared hit=31  
 Planning time: 0.119 ms  
 Execution time: 29.767 ms  
(10 rows)  
  
postgres=# set enable_bitmapscan =off;  
SET  
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from test where id=1;  -- 本例使用index scan更合适  
                                                              QUERY PLAN                                                                
--------------------------------------------------------------------------------------------------------------------------------------  
 Index Scan using idx_test_id on public.test  (cost=0.57..12901.82 rows=9816 width=45) (actual time=0.054..18.771 rows=10000 loops=1)  
   Output: id, info, crt_time  
   Index Cond: (test.id = 1)  
   Buffers: shared hit=10031  
 Planning time: 0.116 ms  
 Execution time: 19.674 ms  
(6 rows)  

5. 测试查询性能qps、吞吐

postgres=# alter role postgres set enable_bitmapscan = off;  
ALTER ROLE  
$ vi test.sql  
  
\set id random(1,10000)  
select * from test where id=:id;  
  
$ pgbench -M prepared -n -r -P 1 -f ./test.sql -c 64 -j 64 -T 100000  
  
... ...  
progress: 181.0 s, 1156.0 tps, lat 55.612 ms stddev 9.957  
progress: 182.0 s, 1157.9 tps, lat 55.365 ms stddev 9.855  
progress: 183.0 s, 1160.1 tps, lat 55.057 ms stddev 8.635  
progress: 184.0 s, 1147.0 tps, lat 55.596 ms stddev 9.151  
progress: 185.0 s, 1162.0 tps, lat 55.287 ms stddev 8.545  
progress: 186.0 s, 1156.0 tps, lat 55.463 ms stddev 9.733  
progress: 187.0 s, 1154.0 tps, lat 55.568 ms stddev 9.753  
progress: 188.0 s, 1161.0 tps, lat 55.240 ms stddev 9.108  
... ...  
  
1150其实已经很高,输出的吞吐达到了1150万行/s。    

6. TOP

top - 21:43:59 up 93 days,  8:01,  3 users,  load average: 64.94, 26.52, 11.48  
Tasks: 2367 total,  68 running, 2299 sleeping,   0 stopped,   0 zombie  
Cpu(s): 92.3%us,  6.7%sy,  0.0%ni,  0.1%id,  0.0%wa,  0.0%hi,  0.9%si,  0.0%st  
Mem:  529321828k total, 241868480k used, 287453348k free,  2745652k buffers  
Swap:        0k total,        0k used,        0k free, 212241588k cached   
  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                     
 9908 digoal  20   0 4677m  42m 1080 S 713.5  0.0  14:09.44 pgbench -M prepared -n -r -P 1 -f ./test.sql -c 64 -j 64 -T 100000                    
10005 digoal  20   0 41.9g 8.0g 8.0g R 88.0  1.6   1:40.42 postgres: postgres postgres 127.0.0.1(51375) SELECT                       
10006 digoal  20   0 41.9g 8.0g 8.0g R 88.0  1.6   1:40.94 postgres: postgres postgres 127.0.0.1(51376) SELECT  
... ...  

堆表 - 聚集存储

使用 cluster test using (idx_test_id); 即可将test表转换为以ID字段聚集存储。但是为了测试方便,我还是新建了2张聚集表。

聚集存储 BTREE 索引

1. 同一份数据,按照ID聚集存储,并创建btree索引。

create unlogged table cluster_test_btree (like test);  
  
insert into cluster_test_btree select * from test order by id;  
  
set maintenance_work_mem ='32GB';  
  
create index idx_cluster_test_btree_id on cluster_test_btree using btree (id);  

2. 索引大小、轨迹查询执行计划、查询效率

postgres=# \di+ idx_cluster_test_btree_id  
                                         List of relations  
 Schema |           Name            | Type  |  Owner   |       Table        |  Size   | Description   
--------+---------------------------+-------+----------+--------------------+---------+-------------  
 public | idx_cluster_test_btree_id | index | postgres | cluster_test_btree | 2142 MB |   
(1 row)  
  
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from cluster_test_btree where id=1;  
                                                                           QUERY PLAN                                                                             
----------------------------------------------------------------------------------------------------------------------------------------------------------------  
 Index Scan using idx_cluster_test_btree_id on public.cluster_test_btree  (cost=0.57..328.54 rows=10724 width=45) (actual time=0.054..4.259 rows=10000 loops=1)  
   Output: id, info, crt_time  
   Index Cond: (cluster_test_btree.id = 1)  
   Buffers: shared hit=125  
 Planning time: 0.118 ms  
 Execution time: 5.147 ms  
(6 rows)  

3. 通过查询物理行号、记录,确认已按ID聚集存储

postgres=# select ctid,* from cluster_test_btree where id=1 limit 10;  
  ctid  | id |               info               |          crt_time            
--------+----+----------------------------------+----------------------------  
 (0,1)  |  1 | 5f5c19fa671886b5f7f205d541157c1f | 2017-02-19 21:55:07.095403  
 (0,2)  |  1 | d69bc0b1aeafcc63c7d99509a65e0492 | 2017-02-19 21:55:07.157631  
 (0,3)  |  1 | 9f5506939986201d55a4353ff8b4028e | 2017-02-19 21:55:07.188382  
 (0,4)  |  1 | 81930c54e08b6d26d9638dd2e4656dc1 | 2017-02-19 21:55:07.126702  
 (0,5)  |  1 | d4fcc05bd8205c41fbe4f2645bf0c6b8 | 2017-02-19 21:55:07.219671  
 (0,6)  |  1 | 4fc8ed929e539525e3590f1607718f97 | 2017-02-19 21:55:07.281092  
 (0,7)  |  1 | 69b4fa3be19bdf400df34e41b93636a4 | 2017-02-19 21:55:07.250614  
 (0,8)  |  1 | 0602940f23884f782058efac46f64b0f | 2017-02-19 21:55:07.467121  
 (0,9)  |  1 | 812649f8ed0e2e1d911298ec67ed9e61 | 2017-02-19 21:55:07.498825  
 (0,10) |  1 | 966bc24f56ab8397ab2303e8e4cdb4c7 | 2017-02-19 21:55:07.436237  
(10 rows)  
  
postgres=# select ctid,* from cluster_test_btree where id=2 limit 10;  
  ctid   | id |               info               |          crt_time            
---------+----+----------------------------------+----------------------------  
 (93,50) |  2 | 05d8cccb5f47e5072f0a05b5f514941a | 2017-02-19 21:55:07.033735  
 (93,51) |  2 | a5329a91ef79db75900bd9cab3d96e43 | 2017-02-19 21:55:07.003142  
 (93,52) |  2 | 1299c1b7a9e0c2bf41af69c449464a49 | 2017-02-19 21:55:06.971847  
 (93,53) |  2 | 1b932eaf9f7c0cb84f471a560097ddb8 | 2017-02-19 21:55:07.064405  
 (93,54) |  2 | 9e740b84bb48a64dde25061566299467 | 2017-02-19 21:51:43.819157  
 (93,55) |  2 | 9e406957d45fcb6c6f38c2ada7bace91 | 2017-02-19 21:51:43.878693  
 (93,56) |  2 | 532b81fa223a1b1ec74139a5b8151d12 | 2017-02-19 21:51:43.848905  
 (93,57) |  2 | 45cef8e5b9570959bd9feaacae2bf38d | 2017-02-19 21:51:41.754017  
 (93,58) |  2 | e1021d43911ca2c1845910d84f40aeae | 2017-02-19 21:51:41.813908  
 (93,59) |  2 | 2da6cc4a5d3a7ee43c1b3af99267ed17 | 2017-02-19 21:51:41.843867  
(10 rows)  

4. 测试查询性能qps、吞吐

$ vi test.sql  
  
\set id random(1,10000)  
select * from cluster_test_btree where id=:id;  
  
$ pgbench -M prepared -n -r -P 1 -f ./test.sql -c 64 -j 64 -T 100000  
  
... ...  
progress: 127.0 s, 1838.1 tps, lat 34.972 ms stddev 7.326  
progress: 128.0 s, 1849.0 tps, lat 34.539 ms stddev 6.933  
progress: 129.0 s, 1854.9 tps, lat 34.441 ms stddev 6.694  
progress: 130.0 s, 1839.1 tps, lat 34.768 ms stddev 6.888  
progress: 131.0 s, 1838.0 tps, lat 34.773 ms stddev 6.710  
progress: 132.0 s, 1848.0 tps, lat 34.729 ms stddev 6.647  
progress: 133.0 s, 1866.0 tps, lat 34.404 ms stddev 5.923  
... ...  

5. TOP

top - 22:11:30 up 93 days,  8:29,  3 users,  load average: 69.59, 34.15, 18.39  
Tasks: 2366 total,  67 running, 2299 sleeping,   0 stopped,   0 zombie  
Cpu(s): 91.9%us,  7.8%sy,  0.0%ni,  0.2%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st  
Mem:  529321828k total, 233261056k used, 296060772k free,  2756952k buffers  
Swap:        0k total,        0k used,        0k free, 204198120k cached   
  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND    
27720 digoal  20   0 4677m  46m 1056 S 1082.7  0.0  18:23.07 pgbench -M prepared -n -r -P 1 -f ./test.sql -c 64 -j 64 -T 100000   
27735 digoal  20   0 41.9g 2.6g 2.6g R 82.5  0.5   1:16.90 postgres: postgres postgres [local] SELECT   
27795 digoal  20   0 41.9g 2.8g 2.8g R 82.2  0.6   1:21.54 postgres: postgres postgres [local] SELECT  

聚集存储 BRIN 索引

1. 同一份数据,按照ID聚集存储,并创建brin索引。

create unlogged table cluster_test_brin (like test);  
  
insert into cluster_test_brin select * from test order by id;  
  
set maintenance_work_mem ='32GB';  
  
create index idx_cluster_test_brin_id on cluster_test_brin using brin (id) with (pages_per_range=128);    -- 可以自行调整,本例1万条记录约占据83个数据块,128还是比较合适的值。  
  
alter role postgres reset enable_bitmapscan ;  

2. 索引大小、轨迹查询执行计划、查询效率

postgres=# \di+ idx_cluster_test_brin_id   
                                        List of relations  
 Schema |           Name           | Type  |  Owner   |       Table       |  Size  | Description   
--------+--------------------------+-------+----------+-------------------+--------+-------------  
 public | idx_cluster_test_brin_id | index | postgres | cluster_test_brin | 232 kB |   
(1 row)  
  
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from cluster_test_brin where id=1;  
                                                                QUERY PLAN                                                                  
------------------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on public.cluster_test_brin  (cost=115.61..13769.87 rows=10724 width=45) (actual time=7.467..11.458 rows=10000 loops=1)  
   Output: id, info, crt_time  
   Recheck Cond: (cluster_test_brin.id = 1)  
   Rows Removed by Index Recheck: 3696  
   Heap Blocks: lossy=128  
   Buffers: shared hit=159  
   ->  Bitmap Index Scan on idx_cluster_test_brin_id  (cost=0.00..112.93 rows=10724 width=0) (actual time=7.446..7.446 rows=1280 loops=1)  
         Index Cond: (cluster_test_brin.id = 1)  
         Buffers: shared hit=31  
 Planning time: 0.111 ms  
 Execution time: 12.361 ms  
(11 rows)  

bitmapscan 隐含了ctid sort,所以启动时间就耗费了7.4毫秒。

如果brin未来支持index scan,而非bitmapscan,可以压缩这部分时间,批量查询效率达到和精确索引btree不相上下。

扫描的数据块数量比非聚集存储少了很多。

3. 通过查询物理行号、记录,确认已按ID聚集存储

postgres=# select ctid,* from cluster_test_brin where id=1 limit 10;  
  ctid  | id |               info               |          crt_time            
--------+----+----------------------------------+----------------------------  
 (0,1)  |  1 | 5f5c19fa671886b5f7f205d541157c1f | 2017-02-19 21:55:07.095403  
 (0,2)  |  1 | d69bc0b1aeafcc63c7d99509a65e0492 | 2017-02-19 21:55:07.157631  
 (0,3)  |  1 | 9f5506939986201d55a4353ff8b4028e | 2017-02-19 21:55:07.188382  
 (0,4)  |  1 | 81930c54e08b6d26d9638dd2e4656dc1 | 2017-02-19 21:55:07.126702  
 (0,5)  |  1 | d4fcc05bd8205c41fbe4f2645bf0c6b8 | 2017-02-19 21:55:07.219671  
 (0,6)  |  1 | 4fc8ed929e539525e3590f1607718f97 | 2017-02-19 21:55:07.281092  
 (0,7)  |  1 | 69b4fa3be19bdf400df34e41b93636a4 | 2017-02-19 21:55:07.250614  
 (0,8)  |  1 | 0602940f23884f782058efac46f64b0f | 2017-02-19 21:55:07.467121  
 (0,9)  |  1 | 812649f8ed0e2e1d911298ec67ed9e61 | 2017-02-19 21:55:07.498825  
 (0,10) |  1 | 966bc24f56ab8397ab2303e8e4cdb4c7 | 2017-02-19 21:55:07.436237  
(10 rows)  
  
postgres=# select ctid,* from cluster_test_brin where id=2 limit 10;  
  ctid   | id |               info               |          crt_time            
---------+----+----------------------------------+----------------------------  
 (93,50) |  2 | 05d8cccb5f47e5072f0a05b5f514941a | 2017-02-19 21:55:07.033735  
 (93,51) |  2 | a5329a91ef79db75900bd9cab3d96e43 | 2017-02-19 21:55:07.003142  
 (93,52) |  2 | 1299c1b7a9e0c2bf41af69c449464a49 | 2017-02-19 21:55:06.971847  
 (93,53) |  2 | 1b932eaf9f7c0cb84f471a560097ddb8 | 2017-02-19 21:55:07.064405  
 (93,54) |  2 | 9e740b84bb48a64dde25061566299467 | 2017-02-19 21:51:43.819157  
 (93,55) |  2 | 9e406957d45fcb6c6f38c2ada7bace91 | 2017-02-19 21:51:43.878693  
 (93,56) |  2 | 532b81fa223a1b1ec74139a5b8151d12 | 2017-02-19 21:51:43.848905  
 (93,57) |  2 | 45cef8e5b9570959bd9feaacae2bf38d | 2017-02-19 21:51:41.754017  
 (93,58) |  2 | e1021d43911ca2c1845910d84f40aeae | 2017-02-19 21:51:41.813908  
 (93,59) |  2 | 2da6cc4a5d3a7ee43c1b3af99267ed17 | 2017-02-19 21:51:41.843867  
(10 rows)  

4. 测试查询性能qps、吞吐

$ vi test.sql  
  
\set id random(1,10000)  
select * from cluster_test_brin where id=:id;  
  
$ pgbench -M prepared -n -r -P 1 -f ./test.sql -c 64 -j 64 -T 100000  
  
... ...  
progress: 198.0 s, 1161.0 tps, lat 55.246 ms stddev 10.578  
progress: 199.0 s, 1158.0 tps, lat 55.201 ms stddev 10.542  
progress: 200.0 s, 1160.0 tps, lat 55.294 ms stddev 9.898  
progress: 201.0 s, 1133.0 tps, lat 56.063 ms stddev 9.988  
progress: 202.0 s, 1149.0 tps, lat 55.974 ms stddev 10.166  
progress: 203.0 s, 1156.0 tps, lat 55.076 ms stddev 9.668  
progress: 204.0 s, 1145.0 tps, lat 56.078 ms stddev 11.279  
... ...  

5. TOP

top - 22:22:28 up 93 days,  8:40,  2 users,  load average: 67.27, 34.03, 22.74  
Tasks: 2362 total,  69 running, 2293 sleeping,   0 stopped,   0 zombie  
Cpu(s): 94.3%us,  5.6%sy,  0.0%ni,  0.1%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st  
Mem:  529321828k total, 240541544k used, 288780284k free,  2759436k buffers  
Swap:        0k total,        0k used,        0k free, 211679508k cached   
  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND   
34823 digoal  20   0 4678m  28m 1060 S 672.2  0.0  14:29.74 pgbench -M prepared -n -r -P 1 -f ./test.sql -c 64 -j 64 -T 100000   
34861 digoal  20   0 41.9g 3.3g 3.3g R 89.5  0.6   1:53.75 postgres: postgres postgres [local] SELECT   
34866 digoal  20   0 41.9g 3.2g 3.2g R 89.5  0.6   1:50.73 postgres: postgres postgres [local] SELECT  

堆表 - 行列变换 (array, jsonb)

1. 同一份数据,按照ID聚合为单行数组的存储。

其他还可以选择jsonb , hstore。

create unlogged table array_row_test (id int, ar test[]);  
  
set work_mem ='32GB';  
set maintenance_work_mem ='32GB';  
  
insert into array_row_test select id,array_agg(test) from test group by id;  
  
create index idx_array_row_test_id on array_row_test using btree (id) ;  

2. 索引大小、轨迹查询执行计划、查询效率

postgres=# \dt+  
                           List of relations  
 Schema |        Name        | Type  |  Owner   |  Size   | Description   
--------+--------------------+-------+----------+---------+-------------  
 public | array_row_test     | table | postgres | 4543 MB |   
 public | cluster_test_brin  | table | postgres | 7303 MB |   
 public | cluster_test_btree | table | postgres | 7303 MB |   
 public | test               | table | postgres | 7303 MB |   
  
postgres=# \di+ idx_array_row_test_id   
                                     List of relations  
 Schema |         Name          | Type  |  Owner   |     Table      |  Size  | Description   
--------+-----------------------+-------+----------+----------------+--------+-------------  
 public | idx_array_row_test_id | index | postgres | array_row_test | 248 kB |   
(1 row)  
  
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from array_row_test where id=1;  
                                                                  QUERY PLAN                                                                    
----------------------------------------------------------------------------------------------------------------------------------------------  
 Index Scan using idx_array_row_test_id on public.array_row_test  (cost=0.29..2.90 rows=1 width=22) (actual time=0.030..0.031 rows=1 loops=1)  
   Output: id, ar  
   Index Cond: (array_row_test.id = 1)  
   Buffers: shared hit=1 read=2  
 Planning time: 0.205 ms  
 Execution time: 0.063 ms  
(6 rows)  

3. 行列变换后的数据举例

postgres=# select ctid,* from array_row_test where id=1;  
....  
 (40,66) |  1 | {"(1,c4ca4238a0b923820dcc509a6f75849b,\"2017-02-19 21:49:50.69805\")","(1,c81e728d9d4c2f636f067f89cc14862c,\"2017-02-19 21:49:50.728135\")","(1,eccbc87e4b5ce2fe28308fd9f2a7baf3,\"2017-02-19 21:49:50.7581\")","(1,a87ff679a  
2f3e71d9181a67b7542122c,\"2017-02-19 21:49:50.787969\")",.....  
  
postgres=# select id, (ar[1]).id, (ar[1]).info, (ar[1]).crt_time from array_row_test where id=1;  
 id | id |               info               |         crt_time            
----+----+----------------------------------+---------------------------  
  1 |  1 | c4ca4238a0b923820dcc509a6f75849b | 2017-02-19 21:49:50.69805  
(1 row)  

4. 测试查询性能qps、吞吐

$ vi test.sql  
  
\set id random(1,10000)  
select * from array_row_test where id=:id;  
  
$ pgbench -M prepared -n -r -P 1 -f ./test.sql -c 64 -j 64 -T 100000  
  
... ...  
progress: 133.0 s, 668.0 tps, lat 96.340 ms stddev 17.262  
progress: 134.0 s, 667.0 tps, lat 97.162 ms stddev 18.090  
progress: 135.0 s, 660.7 tps, lat 97.272 ms stddev 18.852  
progress: 136.0 s, 670.3 tps, lat 95.921 ms stddev 18.195  
progress: 137.0 s, 646.0 tps, lat 96.839 ms stddev 18.015  
progress: 138.0 s, 655.0 tps, lat 97.890 ms stddev 17.992  
progress: 139.0 s, 667.0 tps, lat 96.570 ms stddev 21.196  
... ...  

5. TOP

top - 23:05:05 up 93 days,  9:23,  3 users,  load average: 28.26, 10.03, 9.97  
Tasks: 2365 total,  69 running, 2296 sleeping,   0 stopped,   0 zombie  
Cpu(s): 58.7%us, 40.9%sy,  0.0%ni,  0.3%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st  
Mem:  529321828k total, 278064448k used, 251257380k free,  2774244k buffers  
Swap:        0k total,        0k used,        0k free, 249425004k cached   
  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND   
 1234 digoal  20   0 4742m  57m 1060 S 108.2  0.0   0:30.89 pgbench -M prepared -n -r -P 1 -f ./test.sql -c 64 -j 64 -T 100000   
 1361 digoal  20   0 41.9g 236m 230m R 100.0  0.0   0:28.01 postgres: postgres postgres [local] SELECT   
 1270 digoal  20   0 41.9g 233m 230m R 99.3  0.0   0:28.02 postgres: postgres postgres [local] SELECT  

TOP可以看出,test 表 array 存储的效率并不高,你也许可以尝试一下JSON或者hstore,可能更好。

perf top -ag  
  
  samples  pcnt function                        DSO  
  _______ _____ _______________________________ ___________________________________  
  
107022.00 21.0% _spin_lock                      [kernel.kallsyms]                    
 67193.00 13.2% array_out                       /home/digoal/pgsql10/bin/postgres  
 55955.00 11.0% record_out                      /home/digoal/pgsql10/bin/postgres  
 21401.00  4.2% pglz_decompress                 /home/digoal/pgsql10/bin/postgres  
 19150.00  3.8% clear_page_c_e                  [kernel.kallsyms]                    
 16093.00  3.2% AllocSetCheck                   /home/digoal/pgsql10/bin/postgres  
 15998.00  3.1% __memset_sse2                   /lib64/libc-2.12.so                  
 14778.00  2.9% array_isspace                   /home/digoal/pgsql10/bin/postgres  
 10105.00  2.0% AllocSetAlloc                   /home/digoal/pgsql10/bin/postgres  

试试jsonb

postgres=# create unlogged table jsonb_row_test (id int, jb jsonb);  
CREATE TABLE  
postgres=# set work_mem ='32GB';  
SET  
postgres=# set maintenance_work_mem ='32GB';  
SET  
postgres=# insert into jsonb_row_test select id,jsonb_agg(test) from test group by id;  
  
create index idx_jsonb_row_test_id on jsonb_row_test using btree (id) ;  
  
  
 public | array_row_test     | table | postgres | 4543 MB |   
 public | cluster_test_brin  | table | postgres | 7303 MB |   
 public | cluster_test_btree | table | postgres | 7303 MB |   
 public | jsonb_row_test     | table | postgres | 4582 MB |   
 public | idx_jsonb_row_test_id     | index | postgres | jsonb_row_test     | 248 kB  |   
  
  
$ vi test.sql  
  
\set id random(1,10000)  
select * from jsonb_row_test where id=:id;  
  
$ pgbench -M prepared -n -r -P 1 -f ./test.sql -c 64 -j 64 -T 100000  
  
... ...  
progress: 70.0 s, 1263.0 tps, lat 50.403 ms stddev 8.996  
progress: 71.0 s, 1243.9 tps, lat 51.317 ms stddev 8.989  
progress: 72.0 s, 1287.2 tps, lat 49.906 ms stddev 9.093  
progress: 73.0 s, 1267.0 tps, lat 50.506 ms stddev 9.212  
progress: 74.0 s, 1227.0 tps, lat 52.532 ms stddev 9.383  
progress: 75.0 s, 1248.0 tps, lat 50.941 ms stddev 9.405  
progress: 76.0 s, 1303.1 tps, lat 49.079 ms stddev 7.944  
progress: 77.0 s, 1265.9 tps, lat 50.837 ms stddev 9.926  
progress: 78.0 s, 1304.0 tps, lat 48.952 ms stddev 8.413  
progress: 79.0 s, 1317.1 tps, lat 48.582 ms stddev 7.886  
... ...  
  
TOP  
  
... ...  
top - 23:36:51 up 93 days,  9:54,  3 users,  load average: 24.53, 8.29, 7.87  
Tasks: 2367 total,  68 running, 2298 sleeping,   1 stopped,   0 zombie  
Cpu(s): 72.5%us, 27.3%sy,  0.0%ni,  0.1%id,  0.1%wa,  0.0%hi,  0.0%si,  0.0%st  
Mem:  529321828k total, 282957188k used, 246364640k free,  2783884k buffers  
Swap:        0k total,        0k used,        0k free, 254291420k cached   
  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND   
22333 digoal  20   0 4742m  74m 1060 S 288.2  0.0   1:15.78 pgbench -M prepared -n -r -P 1 -f ./test.sql -c 64 -j 64 -T 100000   
22461 digoal  20   0 41.9g 397m 393m R 96.1  0.1   0:25.33 postgres: postgres postgres [local] SELECT   
22413 digoal  20   0 41.9g 391m 388m R 95.4  0.1   0:25.32 postgres: postgres postgres [local] SELECT  
... ...  
  
perf  
  
  samples  pcnt function                        DSO  
  _______ _____ _______________________________ ___________________________________  
  
141697.00 20.9% escape_json                     /home/digoal/pgsql10/bin/postgres  
 81266.00 12.0% pglz_decompress                 /home/digoal/pgsql10/bin/postgres  
 33469.00  4.9% _spin_lock_irqsave              [kernel.kallsyms]                    
 31359.00  4.6% JsonbIteratorNext               /home/digoal/pgsql10/bin/postgres  
 26631.00  3.9% AllocSetAlloc                   /home/digoal/pgsql10/bin/postgres  
 25430.00  3.8% _spin_lock_irq                  [kernel.kallsyms]                    
 24923.00  3.7% memcpy                          /lib64/libc-2.12.so                  
 20437.00  3.0% clear_page_c_e                  [kernel.kallsyms]                    
 15921.00  2.3% appendBinaryStringInfo          /home/digoal/pgsql10/bin/postgres  

1亿数据, 性能比拼图

存储格式按KEY查询轨迹 TPS输出吞吐CPU利用率索引大小表大小
离散存储11551155 万行/s99.8%2.1 GB7.3 GB
聚集存储 BTREE索引18401840 万行/s99.8%2.1 GB7.3 GB
聚集存储 BRIN索引11551155 万行/s99.8%232 KB7.3 GB
行列变换 array660660 行/s99.8%248 KB4.5 GB
行列变换 jsonb12551255 行/s99.8%248 KB4.5 GB

聚集存储后的好处

聚集存储后,我们看到,按聚集列搜索数据时,需要扫描的数据块更少了,查询效率明显提升。

对于聚集列,不需要创建BTREE精确索引,使用BRIN索引就可以满足高性能的查询需求。节约了大量的空间,同时提升了数据的写入效率。

聚集存储还可以解决另一个问题,比如潜在的宽表需求(例如超过1万个列的宽表,通过多行来表示,甚至每行的数据结构都可以不一样,例如通过某个字段作为行头,来表示行的数据结构)。

PostgreSQL 内核级聚集存储

在内核层面实现聚集存储,而不是通过cluster来实现。

数据插入就不能随便找个有足够剩余空间的PAGE了,需要根据插入的聚集列的值,找到对应的PAGE进行插入。

所以它可能依赖一颗以被跟踪对象ID为KEY的B树,修改对应的fsm算法,在插入时,找到对应ID的PAGE。

不过随着数据的不断写入,很难保证单个ID的所有值都在连续的物理空间中。总会有碎片存在的。

还有一点,如果采样预分配的方式,一些不活跃的ID,可能会浪费一些最小单元的空间(比如最小单元是1PAGE)。

小结

按KEY聚集存储解决了按KEY查询大量数据的IO放大(由于离散存储)问题,例如轨迹查询,微观查询。

对于PostgreSQL用户来说,目前,你可以选择行列变换,或者异步聚集存储的方式来达到同样的目的。

行列变换,你可以使用表级数组,或者JSONB来存储聚集后的记录,从效率来看JSONB更高,而值得优化的有两处代码pglz_decompress, escape_json。

对于异步聚集,你可以选择聚集KEY,分区KEY(通常是时间)。异步的将上一个时间段的分区,按KEY进行聚合。

PostgreSQL 聚集表的聚集KEY,你可以选择BRIN索引,在几乎不失查询效率的同时,解决大量的存储空间。

不管使用哪种方式,一张表只能使用一种聚集KEY(s),如果有多个聚集维度的查询需求,为了达到最高的查询效率,你可存储多份冗余数据,每份冗余数据采用不同的聚集KEY。

将来,PostgreSQL可能会在内核层面直接实现聚集存储的选项。你也许只需要输入聚集KEY,最小存储粒度、等参数,就可以将表创建为聚集表。

将来,PostgreSQL brin索引可能会支持index scan,而不是目前仅有的bitmap scan。

参考

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

《PostgreSQL 9.5 new feature - lets BRIN be used with R-Tree-like indexing strategies For “inclusion” opclasses》

《PostgreSQL 9.5 new feature - BRIN (block range index) index》

《分析加速引擎黑科技 - LLVM、列存、多核并行、算子复用 大联姻 - 一起来开启PostgreSQL的百宝箱》

PgSQL · 应用案例 · GIN索引在任意组合查询中的应用

$
0
0

背景

很多人小时候都有一个武侠梦,独孤求败更是金庸武侠小说里的一位传奇人物。

纵横江湖三十馀载,杀尽仇寇奸人,败尽英雄豪杰,天下更无抗手,无可奈何,惟隐居深谷,以雕为友。 呜呼,生平求一敌手而不可得,诚寂寥难堪也。

独孤老前辈的佩剑描写非常有意思,从使用的佩剑,可以看出一个人的武功修为。

第一柄是一柄青光闪闪的无名利剑。「凌厉刚猛,无坚不摧,弱冠前以之与河朔群雄争锋。」

第二柄是紫薇软剑,「三十岁前所用,误伤义士不祥,乃弃之深谷。」

第三柄是玄铁重剑,「重剑无锋,大巧不工,四十岁之前恃之横行天下。」

第四柄是柄已腐朽的木剑,原因是独孤求败「四十岁后,不滞于物,草木竹石均可为剑。」

IT行业也很相似,开发语言有C, python, java, ….各式各样的可选,数据库有Oracle, PostgreSQL, MySQL等等,也是各式各样的产品可选。

宝剑赠英雄,美玉配佳人。

不管你是李逍遥、还是杨过、小龙女,希望搞IT的你也能找到合适你的武器。

pic

pic

进入正题。

多列的组合查询在实际的应用中较为常见,比如淘宝购物网站的商品搜索

pic

有发货地、分类、是否包邮、是否货到付款、是否天猫、二手、等等许多选项,这些选项在设计时可能使用多个字段来表示(当然,有些可能会使用BIT合并成单个字段来表述)。

举个非常简单的例子

CREATE TABLE test2 (      
  major int,      
  minor int,      
  name varchar      
);      

查询条件中,包含test2表的两个字段的检索

SELECT name FROM test2 WHERE major = constant AND minor = constant;      

这种情况下,我们可以使用两个索引,也可以使用一个复合索引(多列索引)。

CREATE INDEX test2_mm_idx ON test2 (major, minor);      

以上例子可以转化为对多个字段的任意组合查询需求(任意单一、任意两个、任意三个,任意若干个字段的查询需求)。

看过我写的文档的童鞋,可能会联想到我在之前写过一篇文档

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

不过本文并不是要讲bloom,本文要讲一讲另一种技术(gin索引的暗藏功能,多列展开式B树)。

在开始正文之前,大家有没有想过这些问题呢?

1. 哪些索引方法支持多列索引?

PostgreSQL目前支持btree, hash, gin, gist, sp-gist, brin, bloom, rum等众多索引访问方法,哪些访问方法支持多列索引呢?

2. 多列索引的内部存储结构如何?

比如b-tree单列索引很好理解,就是以被索引列值为KEY的类B-Tree(nbtree)结构。但是当使用多列索引时,内部是如何组织的呢?

不同的索引方法,内部组织有什么差异呢?

3. 多列索引支持哪些查询组合

比如index on (a,b,c)三列,那么哪些查询条件能用上多列索引呢?比如where a=? and b>?

不同的索引方法,适用的查询条件是不是都一样呢?

4. 不同的查询组合,使用多列索引的效率如何,效率是否一样(是否与索引访问方法有关?)

比如b-tree index on (a,b,c)三列,where a=? and b>? 以及 where b>? and c=? 效率一样吗?

5. 多列索引,每个列的顺序是否可以指定

比如,是不是所有的索引方法都可创建这样的索引 index on (a,b desc, c desc nulls first)

6. 同样的查询组合,使用什么索引方法更高效

比如 where a=? and b=? and c=? 这样的查询,适用gin好还是b-tree好呢?

7. 如何选择合适的索引方法,与查询条件,数据分布有关系吗?

要回答这些问题,需要对索引方法,内部存储结构有一定的了解。

本文将以gin和btree为例,讲解一下multi column index,它们的内部存储结构,适应的场景。

一 单列索引的内部结构

建议读者先了解一下单列索引的内部结构,本文就不展开了,可以参考我之前写的一些文章。

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

《B-Tree和B+Tree》

《PostgreSQL GIN索引实现原理》

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

二 PostgreSQL use bitmap combine single column indexs

在没有multi column index时,如果我们有多个列的查询条件,通常可以使用选择性好的列,或者多个列索引的组合。

PostgreSQL 使用多个列索引组合查询时,可以使用多列查询结果的ctid bitmap and or ,筛选出最终符合多列条件的ctid。

不仅适用于多列的查询条件,也适用于单列的多个查询条件。

例如

WHERE x = 42 OR x = 47 OR x = 53 OR x = 99      
      
WHERE x = 5 AND y = 6      

https://www.postgresql.org/docs/9.6/static/indexes-bitmap-scans.html

Combining Multiple Indexes

A single index scan can only use query clauses that use the index's columns with operators of its operator class and are joined with AND.       
For example, given an index on (a, b) a query condition like WHERE a = 5 AND b = 6 could use the index, but a query like WHERE a = 5 OR b = 6 could not directly use the index.      
      
Fortunately, PostgreSQL has the ability to combine multiple indexes (including multiple uses of the same index) to handle cases that cannot be implemented by single index scans.       
The system can form AND and OR conditions across several index scans.       
For example, a query like WHERE x = 42 OR x = 47 OR x = 53 OR x = 99 could be broken down into four separate scans of an index on x, each scan using one of the query clauses.       
The results of these scans are then ORed together to produce the result.       
Another example is that if we have separate indexes on x and y,       
one possible implementation of a query like WHERE x = 5 AND y = 6 is to use each index with the appropriate query clause and then AND together the index results to identify the result rows.      
      
支持单列组合条件,也支持多列组合条件。      
      
To combine multiple indexes, the system scans each needed index and prepares a bitmap in memory giving the locations of table rows that are reported as matching that index's conditions.       
The bitmaps are then ANDed and ORed together as needed by the query. Finally, the actual table rows are visited and returned.       
      
注意bitmap扫描是按CTID顺序输出,而非KEY的顺序输出,所以如果对列有ORDER BY的需求,那么会需要额外的sort,优化器会根据实际情况选择合适的执行计划(bitmap 或 单个索引filter但是不用sort)      
      
The table rows are visited in physical order, because that is how the bitmap is laid out;       
this means that any ordering of the original indexes is lost, and so a separate sort step will be needed if the query has an ORDER BY clause.       
For this reason, and because each additional index scan adds extra time, the planner will sometimes choose to use a simple index scan even though additional indexes are available that could have been used as well.      
      
In all but the simplest applications, there are various combinations of indexes that might be useful, and the database developer must make trade-offs to decide which indexes to provide.       
Sometimes multicolumn indexes are best, but sometimes it's better to create separate indexes and rely on the index-combination feature.       
For example, if your workload includes a mix of queries that sometimes involve only column x, sometimes only column y, and sometimes both columns,       
you might choose to create two separate indexes on x and y, relying on index combination to process the queries that use both columns.       
You could also create a multicolumn index on (x, y).       
This index would typically be more efficient than index combination for queries involving both columns,       
but as discussed in Section 11.3, it would be almost useless for queries involving only y, so it should not be the only index.       
A combination of the multicolumn index and a separate index on y would serve reasonably well. For queries involving only x, the multicolumn index could be used,       
though it would be larger and hence slower than an index on x alone. The last alternative is to create all three indexes,       
but this is probably only reasonable if the table is searched much more often than it is updated and all three types of query are common.       
If one of the types of query is much less common than the others, you'd probably settle for creating just the two indexes that best match the common types.      
      
在选择单列还是多列索引时,请根据查询需求选择。如果查询很多,更新插入删除很少,那么如果查询条件有a, b, a and b这类的,可以建立3个索引,分别是(a), (b), (a,b)。        

三 哪些索引方法支持multi column index

Currently, only the B-tree, GiST, GIN, and BRIN index types support multicolumn indexes.

Up to 32 columns can be specified. (This limit can be altered when building PostgreSQL; see the file pg_config_manual.h.)

目前PostgreSQL的B-tree, GiST, GIN, and BRIN索引方法,支持多列索引。

目前支持最多32个列的多列索引,实际上可以更大(通过调整pg_config_manual.h可以做到更大,但是还有另一个限制,indextuple不能超过约1/4的数据块大小,也就是说复合索引列很多的情况下,可能会触发这个限制)。

四 multi column index的查询组合与效率解读

Multicolumn Indexes

https://www.postgresql.org/docs/current/static/indexes-multicolumn.html

由于b-tree, gin , gist, brin都支持multi column索引,但是这几种索引的内部存储方式不一样,所以不同的组合查询的效率也不一样。

例如a,b,c三列的组合索引,select * from tbl where a=? and b>? 以及 where b=?,这两种查询组合,哪个效率高?和索引方法有大大的关系。

btree 查询组合与效率

b-tree多列索引支持任意列的组合查询

A multicolumn B-tree index can be used with query conditions that involve any subset of the index’s columns, but the index is most efficient when there are constraints on the leading (leftmost) columns.
虽然b-tree多列索引支持任意列的组合查询,但是最有效的查询还是包含驱动列条件的查询。

The exact rule is that equality constraints on leading columns, plus any inequality constraints on the first column that does not have an equality constraint, will be used to limit the portion of the index that is scanned.
对于b-tree的多列索引来说,一个查询要扫描索引的哪些部分呢?

从驱动列开始算,按索引列的顺序算到非驱动列的第一个不相等条件为止(没有任何条件也算)。

Constraints on columns to the right of these columns are checked in the index, so they save visits to the table proper, but they do not reduce the portion of the index that has to be scanned.

For example, given an index on (a, b, c) and a query condition WHERE a = 5 AND b >= 42 AND c < 77, the index would have to be scanned from the first entry with a = 5 and b = 42 up through the last entry with a = 5.

(WHERE a = 5 AND b >= 42 AND c < 77),从a=5, b=42开始的所有索引条目,都会被扫描。

Index entries with c >= 77 would be skipped, but they’d still have to be scanned through.

其他例子

(WHERE b >= 42 AND c < 77),所有索引条目,都会被扫描。只要不包含驱动列,则扫描所有索引条目。

(WHERE a = 5 AND c < 77),a=5的所有索引条目,都会被扫描。

(WHERE a >= 5 AND b=1 and c < 77),从a=5开始的所有索引条目,都会被扫描。

This index could in principle be used for queries that have constraints on b and/or c with no constraint on a — but the entire index would have to be scanned, so in most cases the planner would prefer a sequential table scan over using the index.

建议有频繁的复合查询,并且复合查询带有驱动列以及其他列的查询时,可以考虑使用多列索引。

gist 查询组合与效率

gist多列索引支持任意列的组合查询。

A multicolumn GiST index can be used with query conditions that involve any subset of the index’s columns.

Conditions on additional columns restrict the entries returned by the index, but the condition on the first column is the most important one for determining how much of the index needs to be scanned.

注意与b-tree不一样的地方,驱动列的选择性决定了需要扫描多少索引条目,扫描多少条目与非驱动列无关(而b-tree是与非驱动列也有关的)。

A GiST index will be relatively ineffective if its first column has only a few distinct values, even if there are many distinct values in additional columns.

如果驱动列的选择性不好、其他列的选择性很好,即使查询条件同时包含了 驱动列以及其他列 ,也需要扫描很多索引条目,因为扫描多少索引条目和其他列无关。

这么说,并不建议使用gist多列索引。

如果一定要使用GIST多列索引,请一定要把选择性好的列作为驱动列。

gin 查询组合与效率

gin多列索引支持任意列的组合查询。

并且任意查询条件的查询效率都是一样的。

A multicolumn GIN index can be used with query conditions that involve any subset of the index’s columns.

Unlike B-tree or GiST, index search effectiveness is the same regardless of which index column(s) the query conditions use.

brin 查询组合与效率

brin多列索引支持任意列的组合查询。

并且任意查询条件的查询效率都是一样的。

如果有brin组合查询的必要(比如多个与ctid线性相关的列的范围查询,无所谓线性的方向),任何时候都建议使用BRIN的multi column index,除非想针对不同的列使用不同的pages_per_range(比如有些列10个块的范围和另外一些列100个块的范围覆盖差不多,那么建议它们使用不同的pages_per_range)

A multicolumn BRIN index can be used with query conditions that involve any subset of the index’s columns.

Like GIN and unlike B-tree or GiST, index search effectiveness is the same regardless of which index column(s) the query conditions use.

The only reason to have multiple BRIN indexes instead of one multicolumn BRIN index on a single table is to have a different pages_per_range storage parameter.

五 multi column使用建议

多列索引每个列的operator class必须和实际查询匹配,在创建索引时可以指定。

Of course, each column must be used with operators appropriate to the index type; clauses that involve other operators will not be considered.

Multicolumn indexes should be used sparingly.

In most situations, an index on a single column is sufficient and saves space and time.

Indexes with more than three columns are unlikely to be helpful unless the usage of the table is extremely stylized.

六 多列索引,在不同组合下的查询效率差异,原理剖析 - 从索引内部结构说起

前面分析了b-tree, gin都支持任意组合查询。

但是b-tree推荐使用包含驱动列的查询条件,如果查询条件未包含驱动列,则需要扫描整个复合索引。

而gin则通吃,可以输入任意组合列作为查询条件,并且效率一致。

例如

index on (a,b,c)

b-tree 对于包含驱动列a查询条件的SQL,效率可能比较好,不包括a查询条件的SQL,即使走索引,也要扫描整个索引的所有条目。

而gin 则无论任何查询条件,效果都一样。

这是为什么呢?必须从索引的内部存储组织结构来分析。

b-tree multi column index 剖析

btree 对被索引列按创建索引时指定的顺序排序,然后建立B树。

如create index idx on tbl using btree (a,b desc nulls first,c desc, e);

所以B树中的KEY实际上就是被索引列的组合对象,这个结构决定了什么查询能用上这个复合索引。

(a,b,c), row?      
(a,b,c), row?      
(a,b,c), row?      
....      

要达到最高效的使用这种复合索引,必须带上驱动列的条件。

如果order by要用上索引,那么必须order by的写法要与创建索引时指定的顺序一致。

例如select * from tbl where a=? order by a,b desc nulls first;

gin multi column index 剖析

gin 的复合索引很有趣,它将所有列展开,然后将展开后的数据(列ID+值)排序并建立B树。

因此在gin的复合索引中,B树的KEY实际上是列ID+值。

(column_a, v1), row?      
(column_b, v1), row?      
(column_b, v2), row?      
(column_c, v2), row?      
....      

这样的树,以任意组合进行查询,效率都一样。

where a=? 与 where b=? 效率一样,而且和B-tree的单列索引的效率几乎一致(当索引层级一致时)。

where a=? and b=? 与 where b=? and c=? 效率一样(复合索引查两次,在内部使用bitmapAnd取得结果)。

仅仅当多列组合查询时,gin效率可能比不上b-tree的带驱动列的查询(因为b-tree索引不需要bitmapAnd,而gin需要内部bitmapAnd)。

七 例子

创建一个测试表,包含3个字段

postgres=# create table t3(c1 int, c2 text, c3 int);      
CREATE TABLE      

插入100万记录,其中c2,c3的值固定

postgres=# postgres=# insert into t3 select generate_series(1,1000000),'test',1;      
INSERT 0 1000000      

创建gin复合索引

postgres=# create index idx_t3_1 on t3 using gin(c1,c2,c3);      
CREATE INDEX      

查询c1=1,效率与单列索引一致

这个查询结果也可以说明另一个问题,不同列并不是单纯展开后直接构建B树,它依旧添加了列ID进来,所以即使c3=1有100万记录,并不影响c1=1的扫描PAGE数。

postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t3 where c1=1;      
                                                   QUERY PLAN                                                          
-----------------------------------------------------------------------------------------------------------------      
 Bitmap Heap Scan on public.t3  (cost=5.01..6.02 rows=1 width=13) (actual time=0.021..0.021 rows=1 loops=1)      
   Output: c1, c2, c3      
   Recheck Cond: (t3.c1 = 1)      
   Heap Blocks: exact=1      
   Buffers: shared hit=5      
   ->  Bitmap Index Scan on idx_t3_1  (cost=0.00..5.01 rows=1 width=0) (actual time=0.016..0.016 rows=1 loops=1)      
         Index Cond: (t3.c1 = 1)      
         Buffers: shared hit=4      
 Planning time: 0.076 ms      
 Execution time: 0.047 ms      
(10 rows)      

查询c2=?,效率与单列索引一致

postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t3 where c2='test';      
                                                            QUERY PLAN                                                                  
----------------------------------------------------------------------------------------------------------------------------------      
 Bitmap Heap Scan on public.t3  (cost=8121.00..26027.00 rows=1000000 width=13) (actual time=74.467..179.603 rows=1000000 loops=1)      
   Output: c1, c2, c3      
   Recheck Cond: (t3.c2 = 'test'::text)      
   Heap Blocks: exact=5406      
   Buffers: shared hit=5542      
   ->  Bitmap Index Scan on idx_t3_1  (cost=0.00..7871.00 rows=1000000 width=0) (actual time=73.640..73.640 rows=1000000 loops=1)      
         Index Cond: (t3.c2 = 'test'::text)      
         Buffers: shared hit=136      
 Planning time: 0.130 ms      
 Execution time: 230.770 ms      
(10 rows)      
      
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t3 where c2='t';      
                                                   QUERY PLAN                                                          
-----------------------------------------------------------------------------------------------------------------      
 Bitmap Heap Scan on public.t3  (cost=5.00..6.01 rows=1 width=13) (actual time=0.014..0.014 rows=0 loops=1)      
   Output: c1, c2, c3      
   Recheck Cond: (t3.c2 = 't'::text)      
   Buffers: shared hit=4      
   ->  Bitmap Index Scan on idx_t3_1  (cost=0.00..5.00 rows=1 width=0) (actual time=0.013..0.013 rows=0 loops=1)      
         Index Cond: (t3.c2 = 't'::text)      
         Buffers: shared hit=4      
 Planning time: 0.081 ms      
 Execution time: 0.039 ms      
(9 rows)      

查询c3=?,效率与单列索引一致

      
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t3 where c3=1;      
                                                            QUERY PLAN                                                                  
----------------------------------------------------------------------------------------------------------------------------------      
 Bitmap Heap Scan on public.t3  (cost=8121.00..26027.00 rows=1000000 width=13) (actual time=77.949..182.939 rows=1000000 loops=1)      
   Output: c1, c2, c3      
   Recheck Cond: (t3.c3 = 1)      
   Heap Blocks: exact=5406      
   Buffers: shared hit=5542      
   ->  Bitmap Index Scan on idx_t3_1  (cost=0.00..7871.00 rows=1000000 width=0) (actual time=77.116..77.116 rows=1000000 loops=1)      
         Index Cond: (t3.c3 = 1)      
         Buffers: shared hit=136      
 Planning time: 0.083 ms      
 Execution time: 234.558 ms      
(10 rows)      
      
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t3 where c3=2;      
                                                   QUERY PLAN                                                          
-----------------------------------------------------------------------------------------------------------------      
 Bitmap Heap Scan on public.t3  (cost=5.00..6.01 rows=1 width=13) (actual time=0.015..0.015 rows=0 loops=1)      
   Output: c1, c2, c3      
   Recheck Cond: (t3.c3 = 2)      
   Buffers: shared hit=4      
   ->  Bitmap Index Scan on idx_t3_1  (cost=0.00..5.00 rows=1 width=0) (actual time=0.014..0.014 rows=0 loops=1)      
         Index Cond: (t3.c3 = 2)      
         Buffers: shared hit=4      
 Planning time: 0.081 ms      
 Execution time: 0.040 ms      
(9 rows)      

gin任意组合(不需要限定驱动列)多列查询的隐含bitmapAnd, bitmapOr操作

postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t3 where c1=2 and c3=1;      
                                                   QUERY PLAN                                                          
-----------------------------------------------------------------------------------------------------------------      
 Bitmap Heap Scan on public.t3  (cost=9.01..10.03 rows=1 width=13) (actual time=0.044..0.044 rows=1 loops=1)      
   Output: c1, c2, c3      
   Recheck Cond: ((t3.c1 = 2) AND (t3.c3 = 1))      
   Heap Blocks: exact=1      
   Buffers: shared hit=10      
   ->  Bitmap Index Scan on idx_t3_1  (cost=0.00..9.01 rows=1 width=0) (actual time=0.040..0.040 rows=1 loops=1)      
         Index Cond: ((t3.c1 = 2) AND (t3.c3 = 1))      
         Buffers: shared hit=9      
 Planning time: 0.061 ms      
 Execution time: 0.063 ms      
(10 rows)      

没有驱动列,一样高效无比

postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t3 where c2='test' and c3=2;      
                                                   QUERY PLAN                                                          
-----------------------------------------------------------------------------------------------------------------      
 Bitmap Heap Scan on public.t3  (cost=9.00..10.02 rows=1 width=13) (actual time=0.052..0.052 rows=0 loops=1)      
   Output: c1, c2, c3      
   Recheck Cond: ((t3.c2 = 'test'::text) AND (t3.c3 = 2))      
   Buffers: shared hit=9      
   ->  Bitmap Index Scan on idx_t3_1  (cost=0.00..9.00 rows=1 width=0) (actual time=0.051..0.051 rows=0 loops=1)      
         Index Cond: ((t3.c2 = 'test'::text) AND (t3.c3 = 2))      
         Buffers: shared hit=9      
 Planning time: 0.086 ms      
 Execution time: 0.075 ms      
(9 rows)      

gin复合索引的展开式B树决定了不能按单列设置顺序

postgres=# create index idx_t1_0 on t1 using gin (c1, c2 desc);      
ERROR:  0A000: access method "gin" does not support ASC/DESC options      
LOCATION:  ComputeIndexAttrs, indexcmds.c:1248      

八 btree vs gin 多列索引

1. 由于btree index, 多列值根据创建索引的DDL指定顺序sort后,多列的值组合后作为一个KEY存储在B树中。

例如4条记录如下

1,1,2;       
1,100,2;       
2,1,10;       
1,1,3;       

btree 中的key排序后分布(有多少条记录,就有多少KEY)

1,1,2; 1,1,3; 1,100,2; 2,1,10;       

2. GIN MULTI COLUMN INDEX 构建了一个包含多种数据类型的B-TREE , 将多列的数据展开后,排序后分布 (key的数量为每列的count distinct总和)

column1,1; column2,1; column1,2; column3,2; column3,3; column3,10; column2,100;       

更形象的比喻

比如有三幅扑克牌(每幅54张牌),每一幅代表一列,如果要创建3列的复合索引,那么B-TREE会创建出54个条目的B树,而GIN会创建出包含162个条目的B树。

请看这个例子,可以说明这个情况

postgres=# create table t2(c1 int2, c2 int4, c3 int8, c4 numeric, c5 text, c6 timestamp);      
CREATE TABLE      
postgres=# insert into t2 select c1,c1,c1,c1,c5,c6 from (select trunc(random()*1000) c1, md5(random()::text) c5, now()+(random()*10000||' sec')::interval c6 from generate_series(1,100000)) t;      
INSERT 0 100000      
postgres=# create index idx_t2_1 on t2 using gin (c1,c2,c3,c4,c5,c6);      
CREATE INDEX      
postgres=# select count(distinct c4) from t2;      
 count       
-------      
  1000      
(1 row)      
      
postgres=# select count(distinct c1) from t2;      
 count       
-------      
  1000      
(1 row)      
      
postgres=# select count(distinct c2) from t2;      
 count       
-------      
  1000      
(1 row)      
      
postgres=# select count(distinct c3) from t2;      
 count       
-------      
  1000      
(1 row)      
      
postgres=# select count(distinct c5) from t2;      
 count       
-------      
 99996      
(1 row)      
      
postgres=# select count(distinct c6) from t2;      
 count        
--------      
 100000      
(1 row)      
      
postgres=# select 99996+100000+4000;      
 ?column?       
----------      
   203996      
(1 row)      
      
postgres=# select * from gin_metapage_info(get_raw_page('idx_t2_1',0));      
 pending_head | pending_tail | tail_free_size | n_pending_pages | n_pending_tuples | n_total_pages | n_entry_pages | n_data_pages | n_entries | version       
--------------+--------------+----------------+-----------------+------------------+---------------+---------------+--------------+-----------+---------      
   4294967295 |   4294967295 |              0 |               0 |                0 |          2611 |          2610 |            0 |    203996 |       2      
(1 row)      
      
n_entries = count(distinct indexed column1) + ....       

b-tree 要高效的使用复合索引,必须带驱动列的查询条件。

GIN 使用复合索引,可以任意组合查询条件,当有多列查询条件时,使用隐含的bitmapAnd or bitmapOr。

什么情况下,gin比btree慢?

通常,多列查询时,如果使用了驱动列,那么B-TREE索引会更快一些,因为它不需要使用bitmapAnd or bitmapOr,而GIN需要。

其他情况,gin都比btree的复合查询快。

九 gin 多列索引的独门秘籍

通过前面的分析,我们已经摸清楚了GIN的复合索引结构(展开式B树),并且也知道GIN是key+ctid list的类b+tree树组织形式,它可以有很高的压缩比,也可以高效的查询单KEY。

那么GIN具备这些特性后,有哪些独门秘籍呢?

1. 任意组合查询,都可以达到很高效,你不需要创建多个b-tree索引了,一个GIN搞定它(不必担心GIN的IO,有fastupdate技术支撑,并且读写(entry合并)不堵塞)。

比如淘宝的搜索页面,用户可能根据任意属性,勾选任意条件进行查询。

创建一张测试表, 6个字段

postgres=# create table taobao(c1 int, c2 int, c3 timestamp, c4 text, c5 numeric, c6 text);  
CREATE TABLE  

插入1000万随机记录

postgres=# insert into taobao select random()*2000, random()*3000, now()+((50000-100000*random())||' sec')::interval , md5(random()::text), round((random()*1000000)::numeric,2), md5(random()::text) from generate_series(1,10000000);  
INSERT 0 10000000  

创建GIN多列索引

postgres=# create index idx_taobao_gin on taobao using gin(c1,c2,c3,c4,c5,c6);  

数据样例

postgres=# select * from taobao limit 10;  
  c1  |  c2  |             c3             |                c4                |    c5     |                c6                  
------+------+----------------------------+----------------------------------+-----------+----------------------------------  
 1405 |  882 | 2017-02-06 09:41:24.985878 | 49982683517aab7d194f3affe74ba827 |  65157.79 | 256ad6d098a6536e3548b5af91a26557  
 1277 | 1269 | 2017-02-06 09:52:16.313212 | b40c1febdb7f62c916d3632a03092261 | 940751.10 | 9911b203e38b57c55a769312c2aaaeba  
  870 |  159 | 2017-02-06 05:59:46.853421 | 96a0f84d9f9381d77364d93ca2d7aa6f | 419618.52 | 1e716d90055d3b32027a5e80a19e2f4f  
 1990 | 1100 | 2017-02-07 00:35:26.849744 | 684b66b25eb57d97f604eb9d92dfc8b0 | 764940.62 | eea82a253995a70da23a9f9ba3015175  
  625 | 1076 | 2017-02-06 01:48:13.929789 | 7fe094f548cffa367ebeb28d4f188875 | 482201.22 | 81ba27a8123dbd3e3a741c5984368709  
  968 |  554 | 2017-02-06 06:05:29.971936 | 6c8b34e4eb7c5eca3a8ad6131c5aecd3 | 583617.05 | b5cfc01c845cc87a4eb8a425ac9b2e01  
 1795 |  667 | 2017-02-06 20:18:46.027376 | b4ef2282064ef3e7a4eb3c624ba42334 | 911819.49 | a794b4635bb314972d891c7218063642  
  222 | 1041 | 2017-02-06 20:29:15.686187 | f3a6bc2b683e272c293d09353fb2465b | 722814.10 | 22b3b06a69b134dee9e3236907637683  
 1596 | 2153 | 2017-02-05 22:38:54.795333 | 1455f7f7f198d09e380e680321a31968 | 672431.00 | efac7b0a2154e25321a522b98903df41  
  313 | 2955 | 2017-02-06 22:59:49.117719 | 7840bbd2be443904b094b9f8919730c0 | 525295.10 | b5feb900191fd404f3a3de407fa62c93  
(10 rows)  
  
 public | taobao | table | postgres | 1202 MB |   
 public | idx_taobao_gin | index | postgres | taobao | 3761 MB |   

查询测试

任意单一字段

postgres=# explain (analyze,verbose,timing,costs,buffers) select * from taobao where c1=1;  
                                                          QUERY PLAN                                                            
------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on public.taobao  (cost=46.51..4873.01 rows=4840 width=90) (actual time=1.602..10.038 rows=5043 loops=1)  
   Output: c1, c2, c3, c4, c5, c6  
   Recheck Cond: (taobao.c1 = 1)  
   Heap Blocks: exact=4958  
   Buffers: shared hit=4966  
   ->  Bitmap Index Scan on idx_taobao_gin  (cost=0.00..45.30 rows=4840 width=0) (actual time=0.849..0.849 rows=5043 loops=1)  
         Index Cond: (taobao.c1 = 1)  
         Buffers: shared hit=8  
 Planning time: 0.319 ms  
 Execution time: 10.346 ms  
(10 rows)  
  
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from taobao where c2=882;  
                                                          QUERY PLAN                                                            
------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on public.taobao  (cost=34.16..3287.73 rows=3246 width=90) (actual time=1.175..7.024 rows=3373 loops=1)  
   Output: c1, c2, c3, c4, c5, c6  
   Recheck Cond: (taobao.c2 = 882)  
   Heap Blocks: exact=3350  
   Buffers: shared hit=3358  
   ->  Bitmap Index Scan on idx_taobao_gin  (cost=0.00..33.35 rows=3246 width=0) (actual time=0.651..0.651 rows=3373 loops=1)  
         Index Cond: (taobao.c2 = 882)  
         Buffers: shared hit=8  
 Planning time: 0.094 ms  
 Execution time: 7.236 ms  
(10 rows)  
  
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from taobao where c3='2017-02-06 09:41:24.985878';  
                                                      QUERY PLAN                                                         
-----------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on public.taobao  (cost=8.01..9.02 rows=1 width=90) (actual time=0.025..0.025 rows=1 loops=1)  
   Output: c1, c2, c3, c4, c5, c6  
   Recheck Cond: (taobao.c3 = '2017-02-06 09:41:24.985878'::timestamp without time zone)  
   Heap Blocks: exact=1  
   Buffers: shared hit=6  
   ->  Bitmap Index Scan on idx_taobao_gin  (cost=0.00..8.01 rows=1 width=0) (actual time=0.019..0.019 rows=1 loops=1)  
         Index Cond: (taobao.c3 = '2017-02-06 09:41:24.985878'::timestamp without time zone)  
         Buffers: shared hit=5  
 Planning time: 0.125 ms  
 Execution time: 0.057 ms  
(10 rows)  
  
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from taobao where c4='b40c1febdb7f62c916d3632a03092261';  
                                                      QUERY PLAN                                                         
-----------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on public.taobao  (cost=8.01..9.02 rows=1 width=90) (actual time=0.028..0.028 rows=1 loops=1)  
   Output: c1, c2, c3, c4, c5, c6  
   Recheck Cond: (taobao.c4 = 'b40c1febdb7f62c916d3632a03092261'::text)  
   Heap Blocks: exact=1  
   Buffers: shared hit=6  
   ->  Bitmap Index Scan on idx_taobao_gin  (cost=0.00..8.01 rows=1 width=0) (actual time=0.023..0.023 rows=1 loops=1)  
         Index Cond: (taobao.c4 = 'b40c1febdb7f62c916d3632a03092261'::text)  
         Buffers: shared hit=5  
 Planning time: 0.101 ms  
 Execution time: 0.058 ms  
(10 rows)  
  
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from taobao where c5='764940.62';  
                                                      QUERY PLAN                                                         
-----------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on public.taobao  (cost=8.01..9.02 rows=1 width=90) (actual time=0.028..0.028 rows=1 loops=1)  
   Output: c1, c2, c3, c4, c5, c6  
   Recheck Cond: (taobao.c5 = 764940.62)  
   Heap Blocks: exact=1  
   Buffers: shared hit=6  
   ->  Bitmap Index Scan on idx_taobao_gin  (cost=0.00..8.01 rows=1 width=0) (actual time=0.023..0.023 rows=1 loops=1)  
         Index Cond: (taobao.c5 = 764940.62)  
         Buffers: shared hit=5  
 Planning time: 0.127 ms  
 Execution time: 0.069 ms  
(10 rows)  
  
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from taobao where c6='test';  
                                                      QUERY PLAN                                                         
-----------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on public.taobao  (cost=8.01..9.02 rows=1 width=90) (actual time=0.023..0.023 rows=0 loops=1)  
   Output: c1, c2, c3, c4, c5, c6  
   Recheck Cond: (taobao.c6 = 'test'::text)  
   Buffers: shared hit=5  
   ->  Bitmap Index Scan on idx_taobao_gin  (cost=0.00..8.01 rows=1 width=0) (actual time=0.020..0.020 rows=0 loops=1)  
         Index Cond: (taobao.c6 = 'test'::text)  
         Buffers: shared hit=5  
 Planning time: 0.088 ms  
 Execution time: 0.054 ms  
(9 rows)  

任意2字段

postgres=# explain (analyze,verbose,timing,costs,buffers) select * from taobao where c2=1 and c3='2017-02-06 09:41:24.985878';  
                                                       QUERY PLAN                                                         
------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on public.taobao  (cost=15.00..16.02 rows=1 width=90) (actual time=0.060..0.060 rows=0 loops=1)  
   Output: c1, c2, c3, c4, c5, c6  
   Recheck Cond: ((taobao.c2 = 1) AND (taobao.c3 = '2017-02-06 09:41:24.985878'::timestamp without time zone))  
   Buffers: shared hit=11  
   ->  Bitmap Index Scan on idx_taobao_gin  (cost=0.00..15.00 rows=1 width=0) (actual time=0.057..0.057 rows=0 loops=1)  
         Index Cond: ((taobao.c2 = 1) AND (taobao.c3 = '2017-02-06 09:41:24.985878'::timestamp without time zone))  
         Buffers: shared hit=11  
 Planning time: 0.100 ms  
 Execution time: 0.093 ms  
(9 rows)  
  
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from taobao where c2=1 or c3='2017-02-06 09:41:24.985878';  
                                                             QUERY PLAN                                                               
------------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on public.taobao  (cost=42.98..3305.68 rows=3247 width=90) (actual time=1.165..6.947 rows=3330 loops=1)  
   Output: c1, c2, c3, c4, c5, c6  
   Recheck Cond: ((taobao.c2 = 1) OR (taobao.c3 = '2017-02-06 09:41:24.985878'::timestamp without time zone))  
   Heap Blocks: exact=3295  
   Buffers: shared hit=3308  
   ->  BitmapOr  (cost=42.98..42.98 rows=3247 width=0) (actual time=0.650..0.650 rows=0 loops=1)  
         Buffers: shared hit=13  
         ->  Bitmap Index Scan on idx_taobao_gin  (cost=0.00..33.35 rows=3246 width=0) (actual time=0.638..0.638 rows=3329 loops=1)  
               Index Cond: (taobao.c2 = 1)  
               Buffers: shared hit=8  
         ->  Bitmap Index Scan on idx_taobao_gin  (cost=0.00..8.01 rows=1 width=0) (actual time=0.011..0.011 rows=1 loops=1)  
               Index Cond: (taobao.c3 = '2017-02-06 09:41:24.985878'::timestamp without time zone)  
               Buffers: shared hit=5  
 Planning time: 0.099 ms  
 Execution time: 7.161 ms  
(15 rows)  

任意3字段

postgres=# explain (analyze,verbose,timing,costs,buffers) select * from taobao where c4='b40c1febdb7f62c916d3632a03092261' and c5=1 and c6='test';  
                                                                 QUERY PLAN                                                                   
--------------------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on public.taobao  (cost=22.00..23.02 rows=1 width=90) (actual time=0.051..0.051 rows=0 loops=1)  
   Output: c1, c2, c3, c4, c5, c6  
   Recheck Cond: ((taobao.c4 = 'b40c1febdb7f62c916d3632a03092261'::text) AND (taobao.c5 = '1'::numeric) AND (taobao.c6 = 'test'::text))  
   Buffers: shared hit=13  
   ->  Bitmap Index Scan on idx_taobao_gin  (cost=0.00..22.00 rows=1 width=0) (actual time=0.048..0.048 rows=0 loops=1)  
         Index Cond: ((taobao.c4 = 'b40c1febdb7f62c916d3632a03092261'::text) AND (taobao.c5 = '1'::numeric) AND (taobao.c6 = 'test'::text))  
         Buffers: shared hit=13  
 Planning time: 0.115 ms  
 Execution time: 0.084 ms  
(9 rows)  
  
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from taobao where c4='b40c1febdb7f62c916d3632a03092261' or c5=1 or c6='test';  
                                                              QUERY PLAN                                                                
--------------------------------------------------------------------------------------------------------------------------------------  
 Bitmap Heap Scan on public.taobao  (cost=24.03..27.08 rows=3 width=90) (actual time=0.061..0.062 rows=1 loops=1)  
   Output: c1, c2, c3, c4, c5, c6  
   Recheck Cond: ((taobao.c4 = 'b40c1febdb7f62c916d3632a03092261'::text) OR (taobao.c5 = '1'::numeric) OR (taobao.c6 = 'test'::text))  
   Heap Blocks: exact=1  
   Buffers: shared hit=16  
   ->  BitmapOr  (cost=24.03..24.03 rows=3 width=0) (actual time=0.055..0.055 rows=0 loops=1)  
         Buffers: shared hit=15  
         ->  Bitmap Index Scan on idx_taobao_gin  (cost=0.00..8.01 rows=1 width=0) (actual time=0.025..0.025 rows=1 loops=1)  
               Index Cond: (taobao.c4 = 'b40c1febdb7f62c916d3632a03092261'::text)  
               Buffers: shared hit=5  
         ->  Bitmap Index Scan on idx_taobao_gin  (cost=0.00..8.01 rows=1 width=0) (actual time=0.016..0.016 rows=0 loops=1)  
               Index Cond: (taobao.c5 = '1'::numeric)  
               Buffers: shared hit=5  
         ->  Bitmap Index Scan on idx_taobao_gin  (cost=0.00..8.01 rows=1 width=0) (actual time=0.012..0.012 rows=0 loops=1)  
               Index Cond: (taobao.c6 = 'test'::text)  
               Buffers: shared hit=5  
 Planning time: 0.110 ms  
 Execution time: 0.122 ms  
(18 rows)  

任意4字段

不再举例,都能用上索引

2. 由于ctid list组织,所以可以做到很好的压缩,前面讲的posting list compress就是这个功效。所以它节约空间,提升效率。

postgres=# create table t4(c1 int,c2 int);  
CREATE TABLE  
postgres=# insert into t4 select generate_series(1,10000000),1;  
INSERT 0 10000000  
  
postgres=# create index btree_t4_c1 on t4 using btree(c1);  
CREATE INDEX  
postgres=# create index btree_t4_c2 on t4 using btree(c2);  
CREATE INDEX  
  
postgres=# create index btree_t4_c1 on t4 using btree(c1);  
CREATE INDEX  
postgres=# create index btree_t4_c2 on t4 using btree(c2);  
CREATE INDEX  
  
postgres=# create index gin_t4_c1 on t4 using gin(c1);  
CREATE INDEX  
postgres=# create index gin_t4_c2 on t4 using gin(c2);  
CREATE INDEX  
  
postgres=# \di+  
                            List of relations  
 Schema |    Name     | Type  |  Owner   | Table |  Size   | Description   
--------+-------------+-------+----------+-------+---------+-------------  
 public | btree_t4_c1 | index | postgres | t4    | 214 MB  | // btree为全entry索引,  
 public | btree_t4_c2 | index | postgres | t4    | 214 MB  | // 所以即使c2字段1000万全重复值,存储空间也一样  
 public | gin_t4_c1   | index | postgres | t4    | 534 MB  | // 原本存储heap ctid的,被拆分为pos 2 bytes、blkid 4 bytes来存储posting list的长度、posting tree root的blkid,所以比btree多了一点  
 public | gin_t4_c2   | index | postgres | t4    | 10 MB   | // ctid list(posting list)自动压缩,压缩比很高  

3. 使用btree_gin插件,可以对任意标量数据类型创建GIN索引,前面已有例子。

4. gin对多值类型(如数组、文本、全文检索)的支持就不多说了,那是GIN的发源地,支持非常棒。

注意,目前gin还不支持sort,所以如果你有大数据量的ORDER BY limit 小数据输出需求,建议还是使用b-tree。

postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t4 where c2=1 order by c2 limit 1;  
                                                                 QUERY PLAN                                                                    
---------------------------------------------------------------------------------------------------------------------------------------------  
 Limit  (cost=81163.88..81163.90 rows=1 width=8) (actual time=754.234..754.235 rows=1 loops=1)  
   Output: c1, c2  
   Buffers: shared hit=1307  
   ->  Bitmap Heap Scan on public.t4  (cost=81163.88..250411.70 rows=9999985 width=8) (actual time=754.234..754.234 rows=1 loops=1)  
         Output: c1, c2  
         Recheck Cond: (t4.c2 = 1)  
         Heap Blocks: exact=1  
         Buffers: shared hit=1307  
         ->  Bitmap Index Scan on gin_t4_c2  (cost=0.00..78663.89 rows=9999985 width=0) (actual time=745.651..745.651 rows=10000000 loops=1)   
             // gin还不适合limit输出,但是可以通过修改内核来改进  
               Index Cond: (t4.c2 = 1)  
               Buffers: shared hit=1306  
 Planning time: 0.091 ms  
 Execution time: 754.261 ms  
(13 rows)  
  
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t4 order by c2 limit 1;  
                                                               QUERY PLAN                                                                  
-----------------------------------------------------------------------------------------------------------------------------------------  
 Limit  (cost=0.43..0.46 rows=1 width=8) (actual time=0.031..0.031 rows=1 loops=1)  
   Output: c1, c2  
   Buffers: shared hit=1 read=3  
   ->  Index Scan using btree_t4_c2 on public.t4  (cost=0.43..221670.43 rows=10000000 width=8) (actual time=0.030..0.030 rows=1 loops=1)  
         Output: c1, c2  
         Buffers: shared hit=1 read=3  
 Planning time: 0.141 ms  
 Execution time: 0.048 ms  
(8 rows)  
  
postgres=# drop index btree_t4_c2;  
  
// 不限制c2任何条件的话,不能使用gin,order by也不能使用gin  
  
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t4 order by c2 limit 1;  
                                                                    QUERY PLAN                                                                      
--------------------------------------------------------------------------------------------------------------------------------------------------  
 Limit  (cost=10000194247.77..10000194247.78 rows=1 width=8) (actual time=1719.522..1719.523 rows=1 loops=1)  
   Output: c1, c2  
   Buffers: shared hit=44248  
   ->  Sort  (cost=10000194247.77..10000219247.74 rows=9999985 width=8) (actual time=1719.520..1719.520 rows=1 loops=1)  
         Output: c1, c2  
         Sort Key: t4.c2  
         Sort Method: top-N heapsort  Memory: 25kB  
         Buffers: shared hit=44248  
         ->  Seq Scan on public.t4  (cost=10000000000.00..10000144247.85 rows=9999985 width=8) (actual time=0.009..754.991 rows=10000000 loops=1)  
               Output: c1, c2  
               Buffers: shared hit=44248  
 Planning time: 0.084 ms  
 Execution time: 1719.543 ms  
(13 rows)  
  
// 限制c2任何条件的话,可以使用gin,但是order by依旧不能使用gin  
postgres=# set enable_sort=off;  
SET  
Time: 0.112 ms  
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from t4 where c2>0 order by c2 limit 1;  
                                                                     QUERY PLAN                                                                       
----------------------------------------------------------------------------------------------------------------------------------------------------  
 Limit  (cost=10000301719.00..10000301719.00 rows=1 width=8) (actual time=2801.874..2801.875 rows=1 loops=1)  
   Output: c1, c2  
   Buffers: shared hit=45554  
   ->  Sort  (cost=10000301719.00..10000326719.00 rows=10000000 width=8) (actual time=2801.873..2801.873 rows=1 loops=1)  
         Output: c1, c2  
         Sort Key: t4.c2  
         Sort Method: top-N heapsort  Memory: 25kB  
         Buffers: shared hit=45554  
         ->  Bitmap Heap Scan on public.t4  (cost=82471.00..251719.00 rows=10000000 width=8) (actual time=817.773..1905.348 rows=10000000 loops=1)  
               Output: c1, c2  
               Recheck Cond: (t4.c2 > 0)  
               Heap Blocks: exact=44248  
               Buffers: shared hit=45554  
               ->  Bitmap Index Scan on gin_t4_c2  (cost=0.00..79971.00 rows=10000000 width=0) (actual time=809.234..809.234 rows=10000000 loops=1)  
                     Index Cond: (t4.c2 > 0)  
                     Buffers: shared hit=1306  
 Planning time: 0.103 ms  
 Execution time: 2801.909 ms  
(18 rows)  

5. btree_gin目前的查询输入必须和类型完全匹配,例如int4与int8不能匹配,timestamp与date不能匹配。而btree暂时没有这个问题。

原因与btree_gin插件创建的operator class有关

Apparently you're using contrib/btree_gin, because in the core system
that would just fail.  btree_gin lacks any support for cross-type
operators, so it can't index "timestamp > date" comparisons.

contrib/btree_gin/btree_gin–1.0.sql

CREATE OPERATOR CLASS int4_ops
DEFAULT FOR TYPE int4 USING gin
AS
    OPERATOR        1       <,
    OPERATOR        2       <=,
    OPERATOR        3       =,
    OPERATOR        4       >=,
    OPERATOR        5       >,
    FUNCTION        1       btint4cmp(int4,int4),
    FUNCTION        2       gin_extract_value_int4(int4, internal),
    FUNCTION        3       gin_extract_query_int4(int4, internal, int2, internal, internal),
    FUNCTION        4       gin_btree_consistent(internal, int2, anyelement, int4, internal, internal),
    FUNCTION        5       gin_compare_prefix_int4(int4,int4,int2, internal),
STORAGE         int4;

   
postgres=# \df *.*cmp*
                                                       List of functions
   Schema   |           Name            | Result data type |                   Argument data types                    |  Type  
------------+---------------------------+------------------+----------------------------------------------------------+--------
...
 pg_catalog | btint24cmp                | integer          | smallint, integer                                        | normal
 pg_catalog | btint28cmp                | integer          | smallint, bigint                                         | normal
 pg_catalog | btint2cmp                 | integer          | smallint, smallint                                       | normal
 pg_catalog | btint42cmp                | integer          | integer, smallint                                        | normal
 pg_catalog | btint48cmp                | integer          | integer, bigint                                          | normal
 pg_catalog | btint4cmp                 | integer          | integer, integer                                         | normal
 pg_catalog | btint82cmp                | integer          | bigint, smallint                                         | normal
 pg_catalog | btint84cmp                | integer          | bigint, integer                                          | normal
 pg_catalog | btint8cmp                 | integer          | bigint, bigint                                           | normal
...

例子

postgres=# create table t1(id int, info text, crt_time timestamp);
CREATE TABLE
postgres=# create table t2(id int8, info text, crt_time timestamp);
CREATE TABLE
postgres=# create table t3(id int8, info text, crt_time timestamp);
CREATE TABLE
postgres=# create index idx_t1_id on t1(id);
CREATE INDEX
postgres=# create index idx_t1_crt_time on t1(crt_time);
CREATE INDEX
postgres=# create index idx_t2_id on t2(id);
CREATE INDEX
postgres=# create index idx_t3 on t3 using gin (id,info,crt_time);
CREATE INDEX
postgres=# set enable_seqscan=off;
SET

btree索引,可以使用隐式类型转换,不需要显示转换

int与int2,int8的隐式转换,  timestamp与date的隐式转换

postgres=# explain select * from t1 where id=1::int8;
                               QUERY PLAN                               
------------------------------------------------------------------------
 Bitmap Heap Scan on t1  (cost=1.50..7.01 rows=6 width=44)
   Recheck Cond: (id = '1'::bigint)
   ->  Bitmap Index Scan on idx_t1_id  (cost=0.00..1.50 rows=6 width=0)
         Index Cond: (id = '1'::bigint)
(4 rows)

postgres=# explain select * from t1 where id=1::int2;
                               QUERY PLAN                               
------------------------------------------------------------------------
 Bitmap Heap Scan on t1  (cost=1.50..7.01 rows=6 width=44)
   Recheck Cond: (id = '1'::smallint)
   ->  Bitmap Index Scan on idx_t1_id  (cost=0.00..1.50 rows=6 width=0)
         Index Cond: (id = '1'::smallint)
(4 rows)

postgres=# explain select * from t1 where crt_time=now();
                                  QUERY PLAN                                  
------------------------------------------------------------------------------
 Bitmap Heap Scan on t1  (cost=1.50..7.03 rows=6 width=44)
   Recheck Cond: (crt_time = now())
   ->  Bitmap Index Scan on idx_t1_crt_time  (cost=0.00..1.50 rows=6 width=0)
         Index Cond: (crt_time = now())
(4 rows)

postgres=# explain select * from t1 where crt_time=current_date;
                                  QUERY PLAN                                  
------------------------------------------------------------------------------
 Bitmap Heap Scan on t1  (cost=1.50..7.05 rows=6 width=44)
   Recheck Cond: (crt_time = ('now'::cstring)::date)
   ->  Bitmap Index Scan on idx_t1_crt_time  (cost=0.00..1.50 rows=6 width=0)
         Index Cond: (crt_time = ('now'::cstring)::date)
(4 rows)

插件 btree_gin 索引,暂时不支持跨类型的COMPARE,必须显示    

postgres=# explain select * from t3 where crt_time=current_date;
                              QUERY PLAN                               
-----------------------------------------------------------------------
 Seq Scan on t3  (cost=10000000000.00..10000000028.73 rows=5 width=48)
   Filter: (crt_time = ('now'::cstring)::date)
(2 rows)

postgres=# explain select * from t3 where crt_time=current_date::timestamp;
                                       QUERY PLAN                                       
----------------------------------------------------------------------------------------
 Bitmap Heap Scan on t3  (cost=2.65..7.19 rows=5 width=48)
   Recheck Cond: (crt_time = (('now'::cstring)::date)::timestamp without time zone)
   ->  Bitmap Index Scan on idx_t3  (cost=0.00..2.65 rows=5 width=0)
         Index Cond: (crt_time = (('now'::cstring)::date)::timestamp without time zone)
(4 rows)

postgres=# explain select * from t3 where crt_time=current_date::timestamptz;
                                QUERY PLAN                                 
---------------------------------------------------------------------------
 Seq Scan on t3  (cost=10000000000.00..10000000031.40 rows=5 width=48)
   Filter: (crt_time = (('now'::cstring)::date)::timestamp with time zone)
(2 rows)

postgres=# explain select * from t3 where id=1;
                              QUERY PLAN                               
-----------------------------------------------------------------------
 Seq Scan on t3  (cost=10000000000.00..10000000023.38 rows=5 width=48)
   Filter: (id = 1)
(2 rows)

postgres=# explain select * from t3 where id=1::int8;
                             QUERY PLAN                              
---------------------------------------------------------------------
 Bitmap Heap Scan on t3  (cost=2.64..7.14 rows=5 width=48)
   Recheck Cond: (id = '1'::bigint)
   ->  Bitmap Index Scan on idx_t3  (cost=0.00..2.64 rows=5 width=0)
         Index Cond: (id = '1'::bigint)
(4 rows)

JOIN也是如此

btree 支持自动隐式转换

postgres=# explain select * from t1,t2 where t1.id=t2.id ;
                                     QUERY PLAN                                      
-------------------------------------------------------------------------------------
 Merge Join  (cost=0.30..155.27 rows=6046 width=92)
   Merge Cond: (t2.id = t1.id)
   ->  Index Scan using idx_t2_id on t2  (cost=0.15..30.50 rows=1070 width=48)
   ->  Materialize  (cost=0.15..34.23 rows=1130 width=44)
         ->  Index Scan using idx_t1_id on t1  (cost=0.15..31.40 rows=1130 width=44)
(5 rows)

插件 btree_gin 暂时不支持跨类型join

postgres=# explain select * from t1,t3 where t1.id=t3.id;
                                      QUERY PLAN                                      
--------------------------------------------------------------------------------------
 Merge Join  (cost=10000000074.69..10000000199.46 rows=6046 width=92)
   Merge Cond: (t1.id = t3.id)
   ->  Index Scan using idx_t1_id on t1  (cost=0.15..31.40 rows=1130 width=44)
   ->  Sort  (cost=10000000074.54..10000000077.21 rows=1070 width=48)
         Sort Key: t3.id
         ->  Seq Scan on t3  (cost=10000000000.00..10000000020.70 rows=1070 width=48)
(6 rows)

postgres=# explain select * from t2,t3 where t2.id=t3.id;
                                  QUERY PLAN                                   
-------------------------------------------------------------------------------
 Nested Loop  (cost=0.21..1599.15 rows=5724 width=96)
   ->  Index Scan using idx_t2_id on t2  (cost=0.15..30.50 rows=1070 width=48)
   ->  Bitmap Heap Scan on t3  (cost=0.05..1.42 rows=5 width=48)
         Recheck Cond: (id = t2.id)
         ->  Bitmap Index Scan on idx_t3  (cost=0.00..0.05 rows=5 width=0)
               Index Cond: (id = t2.id)
(6 rows)

postgres=# explain select * from t1,t3 where (t1.id)::int8=t3.id;
                                   QUERY PLAN                                   
--------------------------------------------------------------------------------
 Nested Loop  (cost=10000000000.06..10000001694.13 rows=6046 width=92)
   ->  Seq Scan on t1  (cost=10000000000.00..10000000021.30 rows=1130 width=44)
   ->  Bitmap Heap Scan on t3  (cost=0.06..1.43 rows=5 width=48)
         Recheck Cond: (id = (t1.id)::bigint)
         ->  Bitmap Index Scan on idx_t3  (cost=0.00..0.05 rows=5 width=0)
               Index Cond: (id = (t1.id)::bigint)
(6 rows)

绑定变量不受影响,因为绑定变量的类型是指定的。

postgres=# prepare p (int8) as select * from t3 where id=$1;
PREPARE
postgres=# explain execute p(1::int4);
                             QUERY PLAN                              
---------------------------------------------------------------------
 Bitmap Heap Scan on t3  (cost=2.64..7.14 rows=5 width=48)
   Recheck Cond: (id = '1'::bigint)
   ->  Bitmap Index Scan on idx_t3  (cost=0.00..2.64 rows=5 width=0)
         Index Cond: (id = '1'::bigint)
(4 rows)

postgres=# explain execute p(1::int2);
                             QUERY PLAN                              
---------------------------------------------------------------------
 Bitmap Heap Scan on t3  (cost=2.64..7.14 rows=5 width=48)
   Recheck Cond: (id = '1'::bigint)
   ->  Bitmap Index Scan on idx_t3  (cost=0.00..2.64 rows=5 width=0)
         Index Cond: (id = '1'::bigint)
(4 rows)

postgres=# explain execute p1(current_date);
                                     QUERY PLAN                                      
-------------------------------------------------------------------------------------
 Bitmap Heap Scan on t3  (cost=2.64..7.14 rows=5 width=48)
   Recheck Cond: (crt_time = '2017-02-08 00:00:00'::timestamp without time zone)
   ->  Bitmap Index Scan on idx_t3  (cost=0.00..2.64 rows=5 width=0)
         Index Cond: (crt_time = '2017-02-08 00:00:00'::timestamp without time zone)
(4 rows)

6. btree_gin暂时还不可用于group by

postgres=# \d t3
                 Table "public.t3"
  Column  |            Type             | Modifiers 
----------+-----------------------------+-----------
 id       | bigint                      | 
 info     | text                        | 
 crt_time | timestamp without time zone | 
Indexes:
    "idx_t3" gin (id, info, crt_time)

postgres=# explain select id from t3 group by id,info,crt_time;
                                      QUERY PLAN                                      
--------------------------------------------------------------------------------------
 Group  (cost=10000000074.54..10000000085.24 rows=200 width=48)
   Group Key: id, info, crt_time
   ->  Sort  (cost=10000000074.54..10000000077.21 rows=1070 width=48)
         Sort Key: id, info, crt_time
         ->  Seq Scan on t3  (cost=10000000000.00..10000000020.70 rows=1070 width=48)
(5 rows)

postgres=# explain select id from t3 group by id;
                                     QUERY PLAN                                      
-------------------------------------------------------------------------------------
 Group  (cost=10000000074.54..10000000079.89 rows=200 width=8)
   Group Key: id
   ->  Sort  (cost=10000000074.54..10000000077.21 rows=1070 width=8)
         Sort Key: id
         ->  Seq Scan on t3  (cost=10000000000.00..10000000020.70 rows=1070 width=8)
(5 rows)
  
btree 可以

postgres=# \d t1
                 Table "public.t1"
  Column  |            Type             | Modifiers 
----------+-----------------------------+-----------
 id       | integer                     | 
 info     | text                        | 
 crt_time | timestamp without time zone | 
Indexes:
    "idx_t1_crt_time" btree (crt_time)
    "idx_t1_id" btree (id)

postgres=# explain select id from t1 group by id;
                                    QUERY PLAN                                     
-----------------------------------------------------------------------------------
 Group  (cost=0.15..34.23 rows=200 width=4)
   Group Key: id
   ->  Index Only Scan using idx_t1_id on t1  (cost=0.15..31.40 rows=1130 width=4)
(3 rows)

十 小结

宝剑赠英雄,美玉配佳人。

PostgreSQL 数据库,了解越多,才会更加随心所欲,驾驭自如。

参考

https://www.postgresql.org/docs/9.6/static/indexes-bitmap-scans.html

https://www.postgresql.org/docs/current/static/indexes-multicolumn.html

https://www.postgresql.org/docs/9.6/static/btree-gin.html

https://www.postgresql.org/docs/9.6/static/btree-gist.html

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

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

《B-Tree和B+Tree》

《PostgreSQL GIN索引实现原理》

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

Viewing all 691 articles
Browse latest View live