构建nginx,php的docker镜像-手动配置

php-fpm

开启一个测试的容器

docker run -di -p 9005:9000 --name php-fpm-test php:5.6.13-fpm

对容器进行自定义

登录进容器

docker exec -it php-fpm-test /bin/bash

查看配置文件位置(用于获取到配置文件位置,以便修改相关配置)

查找ini配置

php-fpm -i | grep ini
...
Configuration File (php.ini) Path => /usr/local/etc/php
Scan this dir for additional .ini files => /usr/local/etc/php/conf.d

安装xdebug(php版本对应的xdebug版本可以在 https://xdebug.org/download/historical 中找)

pecl install xdebug-2.5.5

安装vim

apt-get update
apt-get install vim

在php.ini中引入xdebug(后面的路径不是固定的,可能会变,也可以使用 docker-php-ext-install,docker-php-ext-enable等命令引入 引入)

vim /usr/local/etc/php/conf.d/xdebug.ini
zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20131226/xdebug.so

安装其他扩展

# 已有的扩展可以直接启用
docker-php-ext-enable xdebug
# 没有的扩展安装
docker-php-ext-install mbstring
docker-php-ext-install pdo_mysql
# gd
apt-get update
apt-get install libpng-dev -y
apt-get install libjpeg62-turbo-dev -y
apt-get install libfreetype6-dev -y
# imagick
apt-get install -y imagemagick php5-imagick libpng-dev libmagickwand-dev libmagickcore-dev
echo extension=imagick.so > /usr/local/etc/php/conf.d/imagick.ini
# bcmath
docker-php-ext-install bcmath

退出容器

Ctrl + p + q

使用容器建立新的images

docker commit -m "php 5.6.13 dev" -a "jin" 8428368dabbc php-jin:php-fpm5.6.13

连接nginx和php-fpm

创建网络

docker network create --driver bridge web-network

连接(以下两种方式任选一种即可)

连接方式1(已启动的容器加入)

docker network connect web-network docker-nginx

连接方式2(未启动的容器指定--network)

docker run --name="docker-php-fpm5.6.13" -v "f:/git_project":/usr/share/nginx/html --network web-network -d php-jin:php-fpm5.6.13
docker run --name="docker-nginx" -p 80:80 -v "f:/git_project":/usr/share/nginx/html -v f:/docker/php-docker/nginx-conf/conf.d/:/etc/nginx/conf.d/ --network web-network -d nginx

设置nginx的php-fpm配置

server {
        listen        80;
        server_name  xxx.com;
        root   "/usr/share/nginx/html/xxx.com/frontend/web";
        location / {
        index index.php;
        if ( !-e $request_filename )
        {
            rewrite ^/(.*)$ /index.php?r=$1 last;
        }
        }
        location ~ \.php(.*)$ {
            fastcgi_pass   docker-php-fpm5.6.13:9000;
            fastcgi_index  index.php;
            fastcgi_split_path_info  ^((?U).+\.php)(/?.+)$;
            fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
            fastcgi_param  PATH_INFO  $fastcgi_path_info;
            fastcgi_param  PATH_TRANSLATED  $document_root$fastcgi_path_info;
            include        fastcgi_params;
        }
}

docker学习

概述

docker用于开发者打包代码及运行环境,避免环境不一致导致代码不能运行。也可用作虚拟化。

与传统虚拟化技术的区别

  • 传统虚拟化技术是在虚拟出一套硬件之后,在其上运行一个完整的操作系统,再在该系统上运行应用程序
  • 容器的应用程序直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器更为轻便
  • 每个容器之间互相隔离,每个容器有自己的文件系统,容器之间进程不会互相影响,能区分计算资源

组成

三大组成 : 容器(镜像跑起来之后就是实例(docker run xxx)),镜像,仓库(存放多个镜像)

基本使用

docker info : 查看docker相关信息

docker --help : 帮助

docker ps : 查看正在运行的容器

docker run [OPTIONS] IMAGE [COMMAND] [ARG...] : 启动

docker run -d --name="nginx" -p 80:80 nginx
OPTIONS说明
--name : 为容器执行一个名称,--name="centos"
-d : 后台运行容器,并返回容器ID,也即启动守护式容器
-i : 以交互模式运行容器,通常与“-t”一起使用
-t : 为容器重新分配一个伪输入终端,通常与“-i”一起使用
-P : 随机端口映射
-p : 指定端口映射,有以下四种合适

  • ip:hostPort:containerPort
  • ip::containerPort
  • hostPort:containerPort
  • containerPort
    例子 : docker run -it --name="mycentos" centos:centos6.9

Ctrl + P + Q 退出当前容器且不关闭容器

exit 退出且关闭容器

docker attach : 进入正在运行的容器

例子 : docker attach 73da45cc0ea5

docker commit : 提交当前容器并使之成为一个新的镜像

格式 : docker commit -m "描述" -a "作者" 容器ID 要创建的目标镜像名:标签名
例子 : docker commit -m "centos with vim" -a "jin" 6bf0ddc92102 jin/centos:1.0

容器数据卷

docker命令直接添加

docker -v 宿主目录:容器目录

例子 : docker run -it --name="mycentos" -v e:/testVolume:/testVolume centos:centos6.9

注意 : windows系统需要设置docker deskep(Settings -> File sharing)

DockerFile添加

VOLUME指令 : 给容器添加一个或多个数据卷

容器间传递共享(--volumes-from)

docker run --volumes-from xxx
例子 : docker run -it --name="mycentos2" --volumes-from mycentos centos:centos6.9

DockerFile

执行流程

  1. docker从基础镜像运行一个容器
  2. 执行一条指令并对容器做出修改
  3. 执行类似docker commit的操作提交一个新的镜像曾
  4. docker再基于刚提交的镜像运行一个新容器
  5. 执行dockerfile中的下一条指令直到所有的指令执行完成

Dokerfile体系结构

FROM : 基础镜像。当前镜像基于该镜像创建
MAINTAINER : 镜像维护者的姓名和邮箱
RUN : 容器构建时需要运行的命令
EXPOSE : 当前容器对外暴露出的端口
WORKDIR : 再创建容器后,终端的默认工作目录
ENV : 用来在构建镜像的过程中设置环境变量
ADD : 拷贝+解压缩
COPY : 拷贝
VOLUME : 数据卷
CMD : 指定容器启动时要执行的命令(有多个时,最后一个生效,该指令会被run之后的参数替换)
ENTRYPOINT : 指定容器启动时要执行的命令(有多个时,最后一个生效,run之后的参数会追加到该命令后(传参))
ONBUILD : 当构建一个被继承的Dockerfile时运行命令,父镜像在被子继承后父镜像的ONBUILD会被触发

例子1 : 制作一个镜像,获取当前外网IP

FROM centos:centos6.9
MAINTAINER jin<jinblog@qq.com>

ENV MYPATH /usr/local
WORKDIR $MYPATH

# 当命令是以 ["XXX"] 的形式定义时,命令将会以可执行二进制文件来执行,而不是shell
RUN yum install curl -y

CMD curl ip.sb
# docker build -f Dockerfile1.txt -t mycentos:1.0 .

例子2 : 制作一个镜像,获取当前外网IP,并能接受curl的参数

FROM centos:centos6.9
MAINTAINER jin<jinblog@qq.com>

ENV MYPATH /usr/local
WORKDIR $MYPATH

# 当命令是以 ["XXX"] 的形式定义时,命令将会以可执行二进制文件来执行,而不是shell
RUN yum install curl -y

ENTRYPOINT ["curl", "ip.sb"]

数据库索引设计与优化09-其他评估事项

评估CPU时间(CQUBE)

可以简单的借鉴QUBE的方法来评估SQL执行的CPU时间(CQUBE),用RS代表排序的记录数 :
CPU时间 = 100μs x TR + 5μs x TS + 100μs x F + 10μs x RS

单次顺序访问的CPU时间

按照QUBE方法中的定义,一次顺序访问是指在顺序扫描过程中读取一条记录,然后使用非匹配谓词进行过滤(匹配谓词已被用于定义锁扫描片段的厚度)。当然,如果是全表或者全所有扫描,那么所有的谓词都是非匹配谓词。

单次顺序访问的CPU时间主要跟单条记录的长度有关,因为CPU时间主要耗费在数据页的处理上。另外,非匹配谓词的个数和复杂度是另一个主要因素,还有锁机制和压缩等其他因素。

在具体的平台上,评估单次顺序访问的CPU时间相对简单,接下来我们就展示三个具体测量的例子。

在第一个例子中有两个WHERE条件恒为非真的单表SELECT语句。两个语句的WHERE条件中都只有一个谓词,且谓词为非索引列。这样,优化器不得不为这两个SELECT语句选择使用全表扫描的方式。其中的CUST表有111000个4KB大小的数据页,共1000000条长度为400字节的记录。另外的INVOICE表有77000个数据页,共4000000条长度为80字节的记录。

表没有进行压缩,所有列都是固定长度,两个WHERE条件也足够简单。锁的粒度是数据页级别的,隔离级别是CS。一些额外的谓词,特别是复杂的谓词会增加CPU时间,另外,数据记录长度的增加,记录变长,使用压缩页同样会增加CPU时间。

使用一台旧的繁忙的服务器(单处理器100minps)所测得的运行时间如下:
全部扫描的SQL的CPU时间
CUST(1000000行,111000个4KB大小的页)2.5s,2.5μs每行
INVOICE(4000000行,770000个4KB大小的页)5s,1.25μs每行

假设记录数和页数是唯一的两个重要因素,那么我们可以计算出下面两个变量的系数 :
1000000X + 111000Y = 2.5s
4000000X + 77000Y = 5s

X = 每行的CPU时间 = 1μs
Y = 每页的CPU时间 = 13μs

X和Y的值清楚的表明了列的长度和个数对CPU时间的影响。

在第二个例子中采用了一个大型的繁忙的服务器(单处理器400mips);扫描一张有222000个4KB大小的页,共13000000行短记录的表,结果集同样是0。该表经过了压缩,而且记录是变长的,但条件谓词依然是简单谓词。

最终测得的CPU时间是9s,平均每行0.7μs。

在第三个例子中使用了一个小型的服务器(单处理器750MHz),扫描的CUST表有1000000行400字节的长记录,结果集共提取1078行记录,并进行排序操作。在WHERE条件中,有两个简单的谓词。最终测得的CPU时间是10.4s。

TS的CPU时间一共是10.4s - X,X代表1078行记录的FETCH和排序时间,利用默认的CPU系数进行计算 :
X = 1078 x 110μs ≈ 0.1s

于是,长记录的单次顺序访问的CPU时间就变成了10μs。

看起来5μs可以作为一个合理的系数值来使用,但是最好能在具体的平台上,使用长记录和短记录实测一下CPU时间。

单次随机访问的CPU时间

有以下几个原因会导致随机访问的CPU时间高于顺序访问的CPU时间

  1. 几乎每次随机读取都会引起数据页请求
  2. 如果数据页没有在缓冲池中,I/O相关的CPU时间会比较多。而由于顺序读取每次会读取多个数据页,所以平均每个数据页的I/O代价就相对较低。另外,平均每页的CPU成本能够平摊至许多顺序访问上。
  3. 由于随机访问是不可预测的,所以CPU无法将数据页预读到告诉CPU缓存中,从而可能会导致明显的内存等待。
  4. 按照QUBE的定义,一次对索引的随机访问忽略了访问非叶子页的成本,因为QUBE假定他们是位于缓冲池中的。然而,从计算CPU时间的角度上看,随机访问一个三层索引上的记录将引起三次数据页请求。这也是为什么索引上的随机访问会比表上的随机访问耗时更多。

计算单次随机访问的CPU时间的一个简单方法是,对比使用半宽或宽索引(以避免不必要的随机读取)前后的CPU时间差异。

另外一个方法是,消除掉低成本的操作(比如顺序访问和排序)。我们通过一个实例来描述这种方法 : 一个嵌套循环连接的SELECT查询语句,有813次对三层索引的随机访问,在一台100mips的机器上运行,各项指标如下 :
TR = 814(中等长度的索引行)
磁盘读 = 845(磁盘随机读取)
TS = 8130(较短长度的索引行)
F = 814
RS = 0
CPU时间 = 401ms

单次随机访问的CPU时间可以通过实测的TS系数及F系数计算得出 :
401ms - [(8130 x 1μs) + (814 x 50μs)] / 814 ≈ 434μs
转化为250mips的机器434μs/2.5 ≈ 173μs

这个CPU时间是在一个缓存命中率为0的测试环境得出的(数据库缓冲池和磁盘缓冲区都没有命中,即845次随机读取都来自磁盘)。在第一次测量完成后不接再次执行同样的事务时,消耗的CPU时间是165ms。在这个例子中,假设其他部分的CPU时间开销是48ms,那么单次随机访问的CPU时间就是 :
165ms - 48ms / 814 ≈ 144μs

在第一个例子中,平均一次随机读取的磁盘物理读个数是845/814 ≈ 1.04,而在第二次例子中完全没有磁盘I/O。这一结果验证了一条重要的原则 : 减少磁盘读缓存或者物理磁盘驱动器上的随机读取的CPU时间的一个重要因素。

根据经验,在当前的硬件条件下,单次随机访问的CPU时间通常在以下之一范围区间里 :
表访问 : 10~100μs
索引访问 : 20~300μs

影响随机访问CPU时间的最大因素还是CPU的高速缓存,如果大量的随机访问内存中的少了数据页,那么单次随机访问的CPU时间可能会小于10μs,实际上可能接近于顺序访问的耗时。

结论

单次随机访问的CPU时间100μs仍然可以用来进行快速评估。但是,按照之前的讨论,CPU时间会受到许多因素的影响,所以,当做出重要决定时(例如基于CPU时间来评估表的反范式化,或添加一个新索引等方案时),需要使用一个合理的时间范围进行评估。比如,对于一个大索引,其随机访问的CPU时间可以假设在100~300μs之间。

然而,在评估CPU时间的潜在收益时,这可能会导致问题。比如,如果一个涉及10000次随机访问的SQL语句消耗了600ms的CPU时间,但是消除9000次随机访问并不会减少900ms的CPU时间。我们需要评估600ms中有多少消耗在TR上。

单次FETCH调用的CPU时间

单次FETCH调用的CPU时间消耗(除去访问的时间)依赖于平台以及程序与DBMS的连接方式。在一个具有多层结构和拥有事务管理器的应用设计下,发送SQL语句到数据库管理系统(以及将结果集传输回来)的CPU时间大概是50μs~100μs。在其他不同的环境下,尤其是当SQL语句嵌入在数据库服务器上的批处理程序中时,单次FETCH操作的CPU时间大概在10μs~20μs之间。

由批处理程序发起的FETCH访问的代价会相对较低。在一些场景下,100μs这一默认系数可能是悲观的。如下述场景所测得的那样。

例如,在之前的例子中,在一台大型的繁忙的机器上(单核440mips)借助全表扫描,结果集为空,得到的TS的CPU时间是0.7μs。然后又执行了一次该SELECT语句,只是这次所有记录都满足条件。该查询涉及222000个4KB大小的页,共13000000条记录,且SELECT列表只包含一个列。测得的总CPU时间是115s,单次FETCH操作平均耗时808μs(115s/13000000≈8.8μs)。因此,单次FETCH操作的CPU时间就是8μs(808μs - 0.7μs)。这是一个下限值,如果查询的列比较多,那么CPU时间会相对更长。对于中型的服务器(单核250mips),只查询一个列的SELECT语句的单次FETCH调用的CPU时间下限约为(440mips/250mips)x8μs≈14μs。

每排序一行的平均CPU时间

在内存充足且没有引起磁盘I/O的情况下,排序操作的CPU时间基本和记录数呈线性关系。平台相关的系数很容易测试 : 首先执行一条对至少10000行记录排序的SELECT语句,一段时间后,在执行同样一条没有ORDER BY的SQL语句。平均排序一行的CPU时间是10μs。

CPU评估举例

在许多决策支持过程中,具备CPU需求的评估能力是有用的。

宽索引还是理想索引

单纯从响应时间来看,理想的索引并非具有完全的优势,我们给出了如下结论 :

虽然三星索引有一定的优势,尤其是在结果集为空的情况下,但是这个优势并不也别明显,而且会带来与新增索引相关的额外开销。

宽索引和最佳索引的QUBE如下。
宽索引 : 结果集为空

索引 LNAME, FNAME, CITY, CNO      TR = 1      TS = 10000
提取 0 x 0.1ms
LRT                               TR = 1      TS = 10000
                                  1 x 10ms    10000 x 0.01ms
                                  10ms + 100ms + 0ms = 110ms

最佳索引 : 20行结果记录(1屏)

索引 LNAME, CITY, FNAME, CNO      TR = 1      TS = 20
提取 20 x 0.1ms
LRT                               TR = 1      TS = 20
                                  1 x 10ms    20 x 0.01ms
                                  10ms + 0.2ms + 2ms = 110ms

现在来评估一下宽索引和理想索引的CQUBE值 :
宽索引 100μs x 1 + 5μs x 10000 + 100μs x 0 + 10μs x 0 ≈ 50ms
最佳索引 100μs x 1 + 5μs x 20 + 100μs x 20 + 10μs x 0 ≈ 2ms

从CPU的角度我们可以清楚的看到,使用我们之前得出的CPU系数,理想索引比宽索引的收益要高25倍。下图展示了所有索引的LRT和CPU指标。

索引LRT(最差输入下)CPU(最差输入下)
LNAME, FNAME100s1s
LNAME, FNAME, CITY0.2s54ms
LNAME, FNAME, CITY, CNO0.1s50ms
LNAME, CITY, FNAME, CNO0.01s2ms

增加一个索引,对于CUST表的插入和删除只会增加几毫秒的CPU时间(一次随机访问以及写相关的CPU时间)

嵌套循环(及反范式化)还是MS/HJ

在前面的连接查询的例子中,最终有两种可选方案 :

  1. 使用嵌套循环式的BJQ连接(使用反范式化)。
  2. 使用理想索引进行合并扫描(MS)或哈希连接(无反范式化)。

第二种方案的性能表现很好 : 在最差输入条件情况下,即返回1000行结果集的情况下,耗时为1.8s。但这个方案的唯一问题就是CPU时间。毕竟,扫描两个索引片,需要120000次随机访问以及对20000 + 100000行记录的合并排序或哈希连接。所以,在确定选择第二个方案之前,还需要慎重评估一下CPU的时间。

与之形成对比的是,用第一种方案获取一屏结果集的CPU时间非常短,且无排序 :
TR = 1 + 20 = 21, TS = 20 + 0 = 20, F = 20

所以,CPU时间 = 21 x 100μs + 20 x 5μs + 20 x 100μs ≈ 4ms(每屏结果集)。

方案2(使用合并扫描)

第一步 : 访问索引(CCTRY, CNO, CNAME, CTYPE)

TR = 1 TR = 100000
CPU时间 = 100μs x 1 + 5μs x 100000 ≈ 500ms

第二步 : 访问索引(IEUR DESC, CNO, INO)

TR = 1 TR = 20000
CPU时间 = 100μs x 1 + 5μs x 20000 ≈ 100ms

第三步 : 合并排序并FETCH结果行

将CUST作为外层扫描使得结果行按照所需的顺序排列。因此,最终的CPU时间如下。
排序及合并的CPU时间为 : 2 x 20000 x 0.01ms = 0.4s
提取的CPU时间为 : 2000 x 0.1ms = 0.2s

CPU时间

在最差输入下,这一访问路径下的CPU时间是上述三部分的总和 :
0.5s + 0.1s + 0.6s = 1.2s

方案2(使用哈希连接)

在这种方式下,第一步和第二步与合并扫描的前两步一样---CPU时间 = 00.5s + 0.1s = 0.6s

第三步 : 使用哈希函数进行匹配并FETCH结果集

优化器优先选择为来自INVOICE表的20000行短记录建立哈希表,表大小可能是20000 x 30字节 = 0.6MB。在生成环境中,该哈希表可以一次性全部缓存在内存当中,但不太可能全部缓存在只有1MB大小的CPU缓存中。

建立好哈希表后,开始扫描CUST表的索引片段。扫描的匹配过程需要随机访问哈希表100000次,单次访问的CPU时间依赖于CPU缓存缓存的命中率。若给定的是一个1MB大小的CPU缓存,那么我们假设单次哈希表访问的CPU时间是1μsμs ~ 50μs,于是,100000次哈希表访问的CPU时间就是
100000 x (1...50μs) = 0.1s ... 5s

最后,提取结果集,提取操作的CPU时间为 :
2000 x 0.1ms = 0.2s

如果哈希连接成本比较高,由于合并排序连接的CPU时间估值为0.4s,那么优化器很可能会选择使用合并扫描。在必要情况下,可以使用相应的MERGE提示来指定该访问路径。

CPU时间

在最差输入条件下,使用这一访问路径的CPU时间是上述三部分的总和,至少为 :
0.5s + 0.1s + 0.3s = 0.9s

结论

第一种方案需要大约4ms的CPU时间来提取一屏结果集,第二种方案需要1s的CPU时间来提取整个结果集(1000条记录,50屏)。对于比较小的结果集,合并扫描的CPU时间是0.6s(扫描,排序,合并20000条INVOICE索引记录),而哈希连接的CPU时间则是0.2s(扫描20000条INVOICE索引记录)。

所以,如果SELECT语句执行的不是非常频繁,第二种方案很可能是可接受的。在极端情况下,可以创建两个游标,一个使用合并连接(针对大结果集),一个使用哈希连接(针对小结果集),从而用最小的代价避免表的反范式改造。

数据库索引设计与优化08-索引和索引重组

B树索引的物理结构

在前面,索引被描述为一个表,该表包含了其所属表的列的子集,连同指向表行的指针一起。我们假设DBMS可以直接定位带匹配列所定义的索引片的第一行,然后继续扫描,知道发现一行不满足匹配列条件的索引行时停止。之前都是假设一个索引行对于一个表行,即使是索引不唯一的时候也是如此。而且,索引行总是以索引列所定义的顺序来显示的。当通过计算访问次数来评估索引时,在脑海中有这么一个假设是非常重要的。不过B树索引的真实物理结果并不是如此。

索引行被存储在索引的叶子页上。典型的索引页的大小为4KB或8KB。索引行的长度一般为索引列的总长度外加约10个字节的控制信息。如果索引行的长度为200字节,并且索引为唯一索引,那么一个8KB的叶子页大约可以存放40个索引行。如果是非唯一索引,每个索引键值后可能会有多个指针,在许多DBMS产品中,这些指针会以其值的顺序来存储。这样即使同一个索引键值对应100万条记录,DBMS也能够快速的找到那个需要删除的指针。

DBMS如何查找索引行

SQL 11.1

SELECT COLX
FROM TABLE
WHERE COLI = 12

假设优化器决定使用索引COLI来执行SQL 11.1。

DBMS首先通过内部系统表找到指向根页的指针,在读取根页之后,DBMS需要读取第二个叶子页来查找与键值12相关联的指针。

一次随机读取会从磁盘上读取一个页。如果系统表中那些用于定位根页的表页已经在数据库缓冲池中,那么这个查询可能需要进行三次随机读 : 一次根页,一次叶子页及一次表页。然而,在当前的硬件条件下,非叶子页很可能已经被缓存在数据库缓冲池中,或者至少在磁盘的读缓存中,因为它们经常被频繁的访问。所以,很有了可能只会产生两次随机读,改查询的的同步I/O时间约为2 x 10ms = 20ms,在当前的处理器条件下,CPU时间相对小得多,所以该查询的还是很可能就在20ms左右。

当数据库需要读取一个索引片时,如上述他会先读取第一个索引行。第一个索引行有一个指针指向下一个索引行,DBMS会一致沿着这个指针链访问,直到遇到一个不满足匹配谓词的索引行为止。所有的索引行都通过索引值链在一起。

在SQL 11.2中,SELECT语句所要查找的是LNAME以字符串“JO”开始,且CITY以字符串“LO”开始的记录。在这一场景下,DBMS会首先在键值JO后追加字母“A”至最大长度,如JOAA...AAA,尝试用该值在索引(LNAME, CITY)上找到一行记录。
SQL 11.2

DECLARE CURSOR112 CURSOR FOR
SELECT CNO, FNAME
FROM CUST
WHERE LNAME BETWEEN :LNAME1 AND :LNAME2
      AND
      CITY BETWEEN :CITY1 AND :CITY2
ORDER BY FNAME

当找到该键值在索引上的位置后,DBMS随之通过链表读取后续的索引行,直至一个不以“JO”开始的索引行为止。

插入一行会发生什么

如果一张表有一个聚簇索引,那么DBMS会根据聚簇索引的键值尝试将插入的记录放在它所属的表页(主页)中。如果这行记录在主页里放不下,或者当前页被锁住,那么DBMS将会检查邻近的页。在最坏的情况下,新的行会被插入到标的最坏一页。依赖于DBMS和表的类型,已经插入的行通常都不会被移动,否则这将意味着更新表上已经建立的所有索引上的相关指针。当有许多表行未能存在在主页中时,如果表行的顺序很重要,则需要对这个表进行重组---对于那些涉及多张大表的大规模批处理任务而言,通常需要这么做。

当往表中插入一条记录时,DBMS会尝试将索引行添加至其索引建所属的叶子页上,但是该索引页可能没有足够的空闲空间来存放这个索引行,在这种情况下,DBMS将会分裂该叶子页。其中一半的行将被移动到一个新的叶子页上,并尽可能地靠近被分裂的页,但是在最坏的情况下,这个索引页可能会被放置在索引的末尾。除了在每个叶子页上预留部分比例的空闲空间外,也许可以在索引被创建或重组时,每n个页面预留一个空页---当索引分裂无法避免时,这会是一个不错的办法。

叶子页的分裂验证吗

分裂一个索引页只需一次额外的同步读,约10ms。除了两个叶子页以外,DBMS通常还必须更新一个非叶子页,而它很可能已经在内存或者读缓存中了。

在叶子页分裂后,查询任何一条索引行的速度很可能同之前一样快。在最坏情况下,一次分裂会创建一个新的索引层级,但是如果不是从磁盘读取非叶子页的话,这只会增加很少的CPU时间。

然而,叶子页的分裂会导致一个索引片变得更慢。目前为止,我们在所有的场景中都假设串联索引行的链指针总是指向同一页或者下一页,这些页可能已被DBMS预读取。在索引被创建或者重组后,这种假设是接近真实情况的,但是索引片上的每处叶子页分裂都可能会增加额外的两次随机访问---一次是为了查找索引行被移动至的索引页,一次是为了返回到扫描的原始位置。其中第一次随机访问很可能会导致一次磁盘的随机读取(10ms)。

假设我们需要扫描100000个索引行,根据QUBE,这将花费
1 x 10ms + 100000 x 0.01ms ≈ 1s

当每个叶子页包含20个索引行(不存在叶子页分裂)时,DBMS必须读取5000个叶子页。如果这些是相邻的,并且顺序读的速度为0.1ms每页(40MB/s,页大小为4KB),那么I/O时间为 :
10ms + 5000 x 0.1ms = 510ms

如果1%的叶子页发生过分裂,扫描100000个索引行可能需要进行50次随机I/O(5000的1%)。即便是这样很小比例的分裂,I/O时间也增加了一倍 :
51 x 10ms + 5000 x 0.1ms = 1010ms

在叶子页分裂后,计算索引片扫描的I/O时间的通用公式为
(1 + (LPSR x A/B)) x ORIG

LPSR : 叶子页的分裂比率(分裂的数量/叶子页的数量)
A : 单次随机读的I/O的时间
B : 单次顺序读的I/O的时间
ORIG : 无叶子页分裂时的I/O时间

这个公式基于两个假设 :

  1. 顺序I/O时间为NLP x B(访问第一个子叶的随机I/O忽略不计)
  2. 随机I/O时间为LPSR x NLP x A(每个叶子页分裂涉及一次随机I/O)

随着A/B的增加,叶子页分类比例的预警阈值在不断降低。当A/B = 100(假设)时,若LPSR达到1%,那么I/O时间就翻倍了。

什么时候应该对索引进行重组

插入模式

索引重组是为了恢复索引行正确的物理位置,他对于索引片扫描和全索引扫描的性能而言很重要。因为插入模式的不同,增加的索引行可能会以无序的方式来创建。我们需要记住,更新一个列意味着需要删除旧的索引行,并增加一个新的索引行,新索引行的位置由新的索引键值来确定。

下文对三种基本插入模式的讨论基于如下假设

  1. 索引是唯一索引。
  2. 被删除的索引行锁腾出的空间在重组之前可以被新的索引行重用。

新索引行被添加至索引的尾部(永远递增的键)

假设插入了一个索引行,其索引键值比任何已经存在的索引键值都要大,则DBMS就不会分类最后的叶子页,那么就不需要空闲的空间或者进行索引重组了。然而,如果在索引前面的索引行被定期地删除,那么为了回收空闲的空间,索引可能不得不进行重组(一个“爬行”的索引)。

有一个重要的例外场景 : 如果索引行是变长的,那么就需要有空闲的空间去适应任何索引行的增长。

随机插入模式

我们稍后将会看到,尽管考虑了空闲空间和重组,对于不同的索引行长度(短,中,长)的处理也是不同的。越长的索引行越难处理,越短的越好处理。

索引页大小索引类型索引大小
4KB< 80字节
8KB< 160字节
4KB中等80 ~ 200字节
8KB中等160 ~ 400字节
4KB> 200字节
8KB> 400字节

短索引行的选择捷径

当索引行比较短时,为一个随机插入情况的索引选择P值是相对简单的。
基本建议 : P = 10%,当索引已经增长超过5%时重组索引
略。。。

数据库索引设计与优化07-多索引访问

简介

许多数据库管理系统支持从一张表的多个索引处收集制作,或是从单个索引的几个索引片收集,然后比较这些指针集并访问满足WHERE语句中所有谓词条件的数据行。这一能力被称为多索引访问,或被称为索引与(索引交集)和索引或(索引并集)。

索引与

假设表TX上有索引A和索引B,如何使用如下的索引的来完成SQL 10.1的查询

SELECT B, C
FROM TX
WHERE A = :A
      AND
      B BETWEEN :B1 AND :B2
ORDER BY B

在高昂的磁盘空间成本导致宽索引不太普遍时,索引与是很重要的。对与必须在A和索引B之前作出选择,而后读取表行以校验其余谓词条件的场景,该特性提供了另一种替代方案。如下

  • 从索引片①和索引片②上手机所有满足响应谓词条件的索引行指针
  • 按页号的顺序对两个指针集合进行排序③
  • 合并这两个已排序的指针集合③
  • 只针对那些同时满足两个WHERE子句的结果行进行表访问④
    每个结果行进行一次表访问;这些表访问将会比传统随机读的方式访问得更快,因为指针已经按照页号进行了排序,所以对表页的扫描将通过跳跃式顺序读的方式完成。

那么这一访问路径与单个组合索引相比哪个更好?相比多索引访问我们更倾向于后者,因为使用后者对索引的访问次数更少。即便如此,优化器仍然可能会选择跳跃式顺序读的方式,如果这么做看起来性能不错的话。当然,对该查询来说,一个宽索引(A,B,C)是更高效的方案。

在现今的操作型应用中,索引并不是一种普遍的访问路径。如果通过EXPLAIN发现了索引与,则应当考虑使用一个宽索引将其替代,因为使用上述机制进行索引与操作有三个严重的缺陷 :

  1. 当一个简单谓词有一个较高的过滤因子时,顺序访问的量可能会过多
  2. 即便多个索引片都是从一个符合查询排序的索引上读取的,ORDER BY子句仍会引起一次排序,因为对指针集的排序操作破坏了从索引继承而来的原始顺序。
  3. 如果从索引片上仅仅手机指向表行的指针,那么一个只需要访问索引的访问路径是无法实现的。这一数据库管理系统的实现相关的问题将在后面讨论。

假设有一张表CUST,表上有三个单列索引(SEX, WEIGHT, HEIGHT),表上总共有100万行数据,SQL 10.2如下。如果优化器决定从这三个索引上收集匹配的指针,那么数据库管理系统就必须访问50万的SEX索引行。并且,针对每一位身材高大的男士都将进行一次表访问。若数据库管理系统依照上述的方式进行多索引访问,那么即使SEX索引变宽也无法避免表访问。
SQL 10.2

SELECT LNAME, FNAME, CNO
FROM CUST
WHERE SEX = 'M'
      AND
      (WEIGHT > 90 OR HEIGHT > 190)
ORDER BY LNAME, FNAME

与查询表一同使用索引与

如果应用系统会对一个表生成SELECT语句,且这些SELECT语句带有许多不同且不可预测的WHERE子句,那么此时索引与是一个很有用的功能。

查询表是对查询进行了优化了的事实数据表。他们在表的行数不是特别多的时候是可行的,比如几百万行数据。正是由于对查询进行了优化,所以不再需要进行表连接,所有查询语句所需的列都在一个表中了。

多索引访问和事实数据表

也许在一个大的事实数据表上创建多个窄索引,主键索引及外键索引的方案看起来不错。这样将会比宽索引耗费更少的磁盘空间。然而,如果该表有10亿行数据,那么基于B树索引的多索引扫描,两桶大量的指针扫描和排序一起,在当前的硬件条件下可能会运行的太慢。

用位图索引进行多索引访问

略...

索引或

索引或是比索引与更重要的一个特性。
在多索引访问功能被引入作为优化器的一部分之前,对于SQL 10.3中所示的SELECT语句,优化器将会选择哪种访问路径呢?如果该表有一个单列索引A和一个单列索引B,那么优化器只有一个合理的选择,即全部扫描,因为这两个谓词都是非布尔的,数据库管理系统并不能因为其中一个谓词被检查出不满足条件而排除该行。
SQL 10.3

SELECT B, C
FROM TX                          1000000 rows
WHERE A = :A                     FF = 0...0.01%
      OR
      B BETWEEN :B1 AND :B2      FF = 0...0.05%

即使使用一个宽索引(A, B, C)也不会有太大的帮助,因为数据库管理系统必须扫描整个索引,这涉及一百万次的顺序访问。

在使用索引或时,会进行如下操作

  • 采集两个指针集①和②---100次顺序访问A,500次顺序访问B,在最差输入条件下合计共600次访问。
  • 对指针进行排序③---非常快。
  • 去除重复的指针③---非常快。
  • 读取满足条件的表行④---在最差输入下且没有重复指针的情况下,共600次随机访问,100 x 10ms = 6s;相较之下,全索引扫描花费的时长为 1000000 x 0.01ms = 10s

在快速EXPLAIN复查的过程中,我们应当对SELECT语句进行多索引访问检查;不合适的索引或者有害的OR可能会被识别出来,这些识别出的问题必须用UNION来替代,或者对游标进行拆分。

索引连接

略。。。