linux操作系统 数据库——索引、触发器、事物(存储引擎)

                 数据库——索引、触发器、事物

一、数据库存储采用的数据结构

    来源:http://kb.cnblogs.com/page/45712/

        一、引言

      对数据库索引的关注从未淡出我的们的讨论,那么数据库索引是什么样的?聚集索引与非聚集索引有什么不同?希望本文对各位同仁有一定的帮助。有不少存疑的地方,诚心希望各位不吝赐教指正,共同进步。[最近首页之争沸沸扬扬,也不知道这个放在这合适么,苦劳?功劳?……]

   二、B-Tree

    我们常见的数据库系统,其索引使用的数据结构多是B-Tree或者B+Tree。例如,MsSql使用的是B+TreeOracleSysbase使用的是B-Tree。所以在最开始,简单地介绍一下B-Tree 

   B-Tree不同于Binary Tree(二叉树,最多有两个子树),一棵M阶的B-Tree满足以下条件:
1)每个结点至多有M个孩子;
2)除根结点和叶结点外,其它每个结点至少有M/2个孩子;
3)根结点至少有两个孩子(除非该树仅包含一个结点);
4)所有叶结点在同一层,叶结点不包含任何关键字信息;
5)有K个关键字的非叶结点恰好包含K+1个孩子;

   另外,对于一个结点,其内部的关键字是从小到大排序的。以下是B-TreeM=4)的样例: 

  

   对于每个结点,主要包含一个关键字数组Key[],一个指针数组(指向儿子)Son[]。在B-Tree内,查找的流程是:使用顺序查找(数组长度较短时)或折半查找方法查找Key[]数组,若找到关键字K,则返回该结点的地址及KKey[]中的位置;否则,可确定K在某个Key[i]Key[i+1]之间,则从Son[i]所指的子结点继续查找,直到在某结点中查找成功;或直至找到叶结点且叶结点中的查找仍不成功时,查找过程失败。

   接着,我们使用以下图片演示如何生成B-TreeM=4,依次插入1~6):从图可见,当我们插入关键字4时,由于原结点已经满了,故进行分裂,基本按一半的原则进行分裂,然后取出中间的关键字2,升级(这里是成为根结点)。其它的依类推,就是这样一个大概的过程。 

  

二、数据库索引 

    1.什么是索引 

    在数据库中,索引的含义与日常意义上的“索引”一词并无多大区别(想想小时候查字典),它是用于提高数据库表数据访问速度的数据库对象。
    A)索引可以避免全表扫描。多数查询可以仅扫描少量索引页及数据页,而不是遍历所有数据页。
    B)对于非聚集索引,有些查询甚至可以不访问数据页。
    C)聚集索引可以避免数据插入操作集中于表的最后一个数据页。
    D)一些情况下,索引还可用于避免排序操作。  

当然,众所周知,虽然索引可以提高查询速度,但是它们也会导致数据库系统更新数据的性能下降,因为大部分数据更新需要同时更新索引。 

   2.索引的存储 

    一条索引记录中包含的基本信息包括:键值(即你定义索引时指定的所有字段的值)+逻辑指针(指向数据页或者另一索引页)。 

  

   当你为一张空表创建索引时,数据库系统将为你分配一个索引页,该索引页在你插入数据前一直是空的。此页此时既是根结点,也是叶结点。每当你往表中插入一行数据,数据库系统即向此根结点中插入一行索引记录。当根结点满时,数据库系统大抵按以下步骤进行分裂:
    A)创建两个儿子结点
    B)将原根结点中的数据近似地拆成两半,分别写入新的两个儿子结点
    C)根结点中加上指向两个儿子结点的指针 

   通常状况下,由于索引记录仅包含索引字段值(以及4-9字节的指针),索引实体比真实的数据行要小许多,索引页相较数据页来说要密集许多。一个索引页可以存储数量更多的索引记录,这意味着在索引中查找时在I/O上占很大的优势,理解这一点有助于从本质上了解使用索引的优势。 

   3.索引的类型 

    A)聚集索引,表数据按照索引的顺序来存储的。对于聚集索引,叶子结点即存储了真实的数据行,不再有另外单独的数据页。
    B)非聚集索引,表数据存储顺序与索引顺序无关。对于非聚集索引,叶结点包含索引字段值及指向数据页数据行的逻辑指针,该层紧邻数据页,其行数量与数据表行数据量一致。 

    在一张表上只能创建一个聚集索引,因为真实数据的物理顺序只可能是一种。如果一张表没有聚集索引,那么它被称为堆集Heap)。这样的表中的数据行没有特定的顺序,所有的新行将被添加的表的末尾位置。 

    4.聚集索引 

    在聚集索引中,叶结点也即数据结点,所有数据行的存储顺序与索引的存储顺序一致。 

  

    1)聚集索引与查询操作 

    如上图,我们在名字字段上建立聚集索引,当需要在根据此字段查找特定的记录时,数据库系统会根据特定的系统表查找的此索引的根,然后根据指针查找下一个,直到找到。例如我们要查询“Green”,由于它介于[Bennet,Karsen],据此我们找到了索引页1007,在该页中“Green”介于[Greane, Hunter]间,据此我们找到叶结点1133(也即数据结点),并最终在此页中找以了目标数据行。 

    此次查询的IO包括3个索引页的查询(其中最后一次实际上是在数据页中查询)。这里的查找可能是从磁盘读取(Physical Read)或是从缓存中读取(Logical Read),如果此表访问频率较高,那么索引树中较高层的索引很可能在缓存中被找到。所以真正的IO可能小于上面的情况。 

    2)聚集索引与插入操作

    最简单的情况下,插入操作根据索引找到对应的数据页,然后通过挪动已有的记录为新数据腾出空间,最后插入数据。

    如果数据页已满,则需要拆分数据页(页拆分是一种耗费资源的操作,一般数据库系统中会有相应的机制要尽量减少页拆分的次数,通常是通过为每页预留空间来实现):
    A)在该使用的数据段(extent)上分配新的数据页,如果数据段已满,则需要分配新段。
    B)调整索引指针,这需要将相应的索引页读入内存并加锁。
    C)大约有一半的数据行被归入新的数据页中。
    D)如果表还有非聚集索引,则需要更新这些索引指向新的数据页。

    特殊情况:

    A)如果新插入的一条记录包含很大的数据,可能会分配两个新数据页,其中之一用来存储新记录,另一存储从原页中拆分出来的数据。
    B)通常数据库系统中会将重复的数据记录存储于相同的页中。
    C)类似于自增列为聚集索引的,数据库系统可能并不拆分数据页,页只是简单的新添数据页。

    3)聚集索引与删除操作

    删除行将导致其下方的数据行向上移动以填充删除记录造成的空白。

    如果删除的行是该数据页中的最后一行,那么该数据页将被回收,相应的索引页中的记录将被删除。如果回收的数据页位于跟该表的其它数据页相同的段上,那么它可能在随后的时间内被利用。如果该数据页是该段的唯一一个数据页,则该段也被回收。

    对于数据的删除操作,可能导致索引页中仅有一条记录,这时,该记录可能会被移至邻近的索引页中,原索引页将被回收,即所谓的“索引合并”。

   5.非聚集索引 

    非聚集索引与聚集索引相比:
    A)叶子结点并非数据结点
    B)叶子结点为每一真正的数据行存储一个-指针
    C)叶子结点中还存储了一个指针偏移量,根据页指针及指针偏移量可以定位到具体的数据行。
    D)类似的,在除叶结点外的其它索引结点,存储的也是类似的内容,只不过它是指向下一级的索引页的。

    聚集索引是一种稀疏索引,数据页上一级的索引页存储的是页指针,而不是行指针。而对于非聚集索引,则是密集索引,在数据页的上一级索引页它为每一个数据行存储一条索引记录。

    对于根与中间级的索引记录,它的结构包括:
    A)索引字段值
    BRowId(即对应数据页的页指针+指针偏移量)。在高层的索引页中包含RowId是为了当索引允许重复值时,当更改数据时精确定位数据行。
    C)下一级索引页的指针

    对于叶子层的索引对象,它的结构包括:
    A
)索引字段值
    BRowId

  

    1)非聚集索引与查询操作

    针对上图,如果我们同样查找“Green”,那么一次查询操作将包含以下IO3个索引页的读取+1个数据页的读取。同样,由于缓存的关系,真实的IO实际可能要小于上面列出的。

    2)非聚集索引与插入操作

如果一张表包含一个非聚集索引但没有聚集索引,则新的数据将被插入到最末一个数据页中,然后非聚集索引将被更新。如果也包含聚集索引,该聚集索引将被用于查找新行将要处于什么位置,随后,聚集索引、以及非聚集索引将被更新。

    3)非聚集索引与删除操作

如果在删除命令的Where子句中包含的列上,建有非聚集索引,那么该非聚集索引将被用于查找数据行的位置,数据删除之后,位于索引叶子上的对应记录也将被删除。如果该表上有其它非聚集索引,则它们叶子结点上的相应数据也要删除。

如果删除的数据是该数所页中的唯一一条,则该页也被回收,同时需要更新各个索引树上的指针。

由于没有自动的合并功能,如果应用程序中有频繁的随机删除操作,最后可能导致表包含多个数据页,但每个页中只有少量数据。

   6.索引覆盖 

    索引覆盖是这样一种索引策略:当某一查询中包含的所需字段皆包含于一个索引中,此时索引将大大提高查询性能。

    包含多个字段的索引,称为复合索引。索引最多可以包含31个字段,索引记录最大长度为600B。如果你在若干个字段上创建了一个复合的非聚集索引,且你的查询中所需Select字段及Where,Order By,Group By,Having子句中所涉及的字段都包含在索引中,则只搜索索引页即可满足查询,而不需要访问数据页。由于非聚集索引的叶结点包含所有数据行中的索引列值,使用这些结点即可返回真正的数据,这种情况称之为索引覆盖

在索引覆盖的情况下,包含两种索引扫描:
    A)匹配索引扫描
    B)非匹配索引扫描

    1)匹配索引扫描

    此类索引扫描可以让我们省去访问数据页的步骤,当查询仅返回一行数据时,性能提高是有限的,但在范围查询的情况下,性能提高将随结果集数量的增长而增长。

    针对此类扫描,索引必须包含查询中涉及的的所有字段,另外,还需要满足:Where子句中包含索引中的引导列Leading Column),例如一个复合索引包含A,B,C,D四列,则A引导列。如果Where子句中所包含列是BCD或者BD等情况,则只能使用非匹配索引扫描。

    2)非配置索引扫描

正如上述,如果Where子句中不包含索引的导引列,那么将使用非配置索引扫描。这最终导致扫描索引树上的所有叶子结点,当然,它的性能通常仍强于扫描所有的数据页。

 

三、数据库触发器

    

    来源:http://www.cnblogs.com/Alpha-Fly/archive/2012/04/09/2438419.html

触发器

  其实是一种特殊的存储过程。一般的存储过程是通过存储过程名直接调用,而触发器主要是

  通过事件(增、删、改)进行触发而被执行的。其在表中数据发生变化时自动强制执行。

  常见的触发器有两种:after(for)、instead of,用于insert、update、delete事件。

  after(for)        表示执行代码后,执行触发器

  instead of        表示执行代码前,用已经写好的触发器代替你的操作

 

触发器语法:

  create trigger 触发器的名字   on 操作表

  for|after         instead of

  update|insert|delete

  as

  SQL语句

 

触发器实现原理图

 

触发器示例

Example1

 

--禁止用户插入数据(实际上是先插入,然后立刻将其删除!)

 

  create trigger tr_insert on bank

 

  for          --for表示执行之后的操作

 

  insert       --即先执行了插入操作,同时在临时表中保存了插入记录

 

  as

 

   --执行完插入之后,在新生成的表中将刚刚插入的那条记录删除,

 

   --而此时得到的刚刚插入的记录的id是通过临时表 inserted得到的

 

  delete * from bank where cid=(select cid from inserted)

 

 

 

  生成上面的触发器后,当用户再输入insert语句后就见不到效果了!

 

  如:insert into bank values('0004',10000),是插入不进数据库的。

 

Example2

--删除谁就让谁的账户加上10元

  create trigger tr_dalete on bank

  instead of

  delete

  as

  update bank balance=balance+10 where cid=(select cid from deleted)

  生成这个触发器之后,当用户输入delete语句后,对应的那个id不但没有被删除掉,而且他的账户增加了10元

 

  如:delete from bank where cid='0002',执行完这句话后,编号为0002的账户会增加10元

四、数据库事物

    来源:http://socket.blog.163.com/blog/static/2098730042012514102700/

    80年代中国人结婚四大件:手表、自行车、缝纫机、收音机(三转一响)。要把事务娶回家需要四大件,所以事务很刻薄(ACID),四大件清单:原子性(Atom)、一致性(Consistent)、隔离性(Isolate)、持久性(Durable)。ACID就是数据库事务正确执行的四个基本要素的缩写。

1.        原子性:要么不谈,要谈就要结婚!

对于其数据修改,要么全都执行,要么全都不执行。如果系统只执行这些操作的一个子集,则可能会破坏事务的总体目标。最典型的问题就是银行转帐问题。

2.        一致性:恋爱时,什么方式爱我;结婚后还得什么方式爱我;

数据库原来有什么样的约束,事务执行之后还需要存在这样的约束,所有规则都必须应用于事务的修改,以保持所有数据的完整性。事务结束时,所有的内部数据结构(如 B 树索引或双向链表)、完整性约束(索引、主键)都必须是一致的。

3.        隔离性:闹完洞房后,是俩人的私事。

事务查看数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据。当事务可序列化时将获得最高的隔离级别。隔离性是事务机制里相对来说,比较复杂的,下文另说。

4.        持久性:一旦领了结婚证,无法后悔。

修改即使出现致命的系统故障也将一直保持。不要告诉我系统说commit成功了,回头电话告诉我,服务器机房断电了,我的事务涉及到的数据修改可能没有进入数据库。

 

抛开刚才的四大件比方,再谈隔离性。隔离性是指DBMS可以在并发执行的事务间提供不同级别的数据读写安全隔离措施。隔离的级别和并发事务的吞吐量之间存在反比关系。较高级别的隔离可能会带来较高的冲突和较多的事务流产。流产的事务要消耗资源,这些资源必须要重新被访问。所以这需要trade-off,到底是访问速度优先,还是数据绝对正确优先。

两台机器分别向数据库插入数据,在提交事务前再查询一次数据库,此时本机器的读操作可以读到本机器尚未提交的事务的数据吗?这个问题本身就是一种意识错误,事务是一个原子单位,任何隔离级别都改变不了这个真理。

我们先了解几个数据库不得不知的秘密,事务一旦启动,写写肯定是冲突的。那么读写呢?读分成两种,被别人读和读别人的数据。被别人读会产生脏数据的问题。读别人的会产生不可重复读和幻读两种情况。

A.        脏读读到的数据不是此刻真实的数据。

脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。打个比方:

我的支付宝余额3000元,事务A改为2000,但事务A尚未提交。与此同时,事务B正在读取,并且“成功”读取到余额为2000。随后,事务A由于网络或者IO原因执行失败,回滚成3000元,事务B拿到的值继续运算,就是错误的,这算是一次dirty read ' T, B 

银行转帐,AB1000元,B+1000,这个时候,B就能够读取到。这个时候A还没有减掉1000元,后悔了,没有提交,这个时候B把钱提走了,这不扯吗? 银行在早年代出现类似情形的bug.

B.        不可重复读:读了两次,值不一样。

一个事务对同一行数据重复读取两次,但是却得到了不同的结果。例如,在两次读取的中途,有人对该行数据进行了update操作,并提交,结果就让当然这个事务郁闷了

还是余额3000元,事务A是一个比较长的事务,一开始读取到3000,结果恰好我的水电自动扣款100成功(事务B执行成功),事务A在最后读取到的余额成了2900元。这就是不可重复读现象。

C.        幻读:原来没有,现在有了

事务在操作过程中进行两次查询,第二次查询的结果包含了第一次查询中未出现的数据(这里并不要求两次查询的SQL语句相同)。这是因为在两次查询过程中有另外一个事务插入数据造成的。

事务A正在统计到目前为止的订单数量,一开始读到的是10笔。结果恰好这个时候,家人用此支付宝买了一个家电。等事务A打算提交的时候发现成了11笔。

 

刚才说到了这三种应用事务产生的读写问题,事务产生了对应的4种隔离级别,在Mysql中利用如下:

SELECT @@TX_ISOLATION; 即可看到:说明Mysql的默认是可重复读取。 select @@global.tx_isolation; 查看全局的事务隔离级别。现在我们按照java.sql.Connection定义的四个常量值说开来:

1.        读未提交:TRANSACTION_READ_UNCOMMITTED = 1;

允许脏读取,但不允许更新丢失。如果一个事务已经开始写数据,则另外一个数据则不允许同时进行写操作,但允许其他事务读此行修改后却没有提交的数据,就是担心人家的事务出问题回滚(ROLLBACK)了,而你还拿这个脏数据继续计算。该隔离级别可以通过“排他写锁”实现。绝大部分的数据库没有二到这个地步。

设置:SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

表现:可以读取任何数据,但是如果更新到同一数据上,需要等待另一个事务执行完,有超时异常:

注意,即使是GLOBAL设置,也需要打开一个新的会话连接,才能生效,包括当前设置连接。如果是SESSION,当前会话马上生效,但绝不会影响到其它会话的隔离级别,使用SELECT @@TX_ISOLATION; 检查一下当前的隔离级别,免得穿越到秦国。任何Mysql设置,首先要清楚它是全局的,还是会话级别的。

2.        读提交:TRANSACTION_READ_COMMITTED   = 2;

允许不可重复读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。

设置:SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

表现:对同一数据更新需要等待,一个事务如果没有COMMIT,任何其它事务无法读取它的中间值。因为只是加了行共享锁,所以此时,还是可以读到一个事务里正在被update的数据。这里的问题是一个事务你还可以读另外一个事务正在更新的数据。

3.        可重复读取:TRANSACTION_REPEATABLE_READ  = 4;

禁止不可重复读取和脏读取,但是有时可能出现幻影数据,但在innodb中此隔离级别不允许幻象读,应该说这已经是较高级别的安全保证了。这可以通过“共享读锁”和“排他写锁”实现。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。

设置:SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

表现:除了之上两个要求之后,如果一个事务对一行的读取,即使其它事务的的确确已经修改了此项数据,他也还是会将错就错到底,不会去读这条新值,保证一个事务开始后,读取的任何数据都一份

4.        序列化:TRANSACTION_SERIALIZABLE     = 8;

与“可重复读取”隔离最大的区别是读也会加锁,另外事务无法更新。它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。任何数据的插入与更新,都是慢慢来,象单跑道起飞的飞机一样。在序列化隔离中,innodb会对每一个select语句后自动加上lock in share mode.

设置:SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;

表现:它依然可以同时起多个事务,但是如果对同一数据进行的任何读写操作,都必须一个等待另一个执行完再说,原则是先到的有锁定权。如果你执行一个update,对方也来一个update,那么出现:

Java中,修改事务隔离级别,void setTransactionIsolation(int level) throws SQLException;

如果出现两边会话设置隔离级别不一致的情况,属性互相独立,以更高隔离级别为准。实际上隔离级别只针对于锁。设定对应的隔离级别,对应的操作都有对应的锁去执行现场清理工作。锁事实上只有两种:

 A. 共享锁(Shared Lock也叫读锁.
     
共享锁表示对数据进行读操作。因此多个事务可以同时为一个对象加共享锁。
  B.
写锁(Write Lock)也叫排它锁
     
写锁表示对数据进行写操作。如果一个事务对对象加了排他锁,其他事务就不能再给它加任何锁了。wKioL1diWkSQjDS7AAAzpyCN0nA763.png

隔离级别的任何实现都离不开锁,DBMS,有一个善良而忠实的看门老者,不停地在给你开门、锁门、防止冲突产生。要mysql锁表的操作是LOCK,而解锁的操作是UNLOCK.

LOCK TABLES 表名 READ LOCAL;  其它连接的insert/update都无法做。(innodb

  在innodb中两个事务之间对于不同行的操作是可以的,所以它们实现的是行锁。

开始事务时,必须关闭自动提交,SET AUTOCOMMIT=0; 那么在AUTOCOMMIT=0之前的语句会顺利执行并逐条提交。支持事务的表必须是engine = INNODB。当错误时,全盘检查所有ROLLBACKstart位置。ROLLBACK相当于一个标记点,凡是打此标记的地方,都会被自动回滚。如果没有ROLLBACK,那么即使是autocommit=0,即会被正常提交,无法实现数据的回滚。

COMMIT对於单个事务事实上可有可无。因为end$$会强制提交事务。但在多个事务处理时,必须在某个点提交,否则回滚时存在问题。

只是如果AUTOCOMMIT默认为1,即使有SQL语句包含在事务里边,也是每条自动提交。注意这个时间的提交只是当前连接的操作员自己在YY,因为没有COMMIT,无论AUTOCOMMIT的值等于多少,都会不提交到数据库,让别的人看到。但这个时候非常有意思,因为自动提交为1,如果此事务包括的存储过程被再次调用一次,由于又开始一个新事务,会自动提交上一次的那个没有COMMIT的事务。所以就危害性来说,SET AUTOCOMMIT=0危害性要大得多。

即使COMMIT封闭了START TRANSACTION也无法自动把 AUTOMMIT自动置为1,这就是解释陈良允同学为什么那次操作,我无法看到结果,因为它的连接已经执行,却没有自动提交。可以在他的连接上看到,因为对他来说只是一个临时操作。

事务的开始、回滚、提交与否,与AUTOCOMMIT=0的值的改变没有任何关系!所以高度注意:前提是置AUTOCOMMIT=0,这时如果没有封闭一个事务是极其危险的,因为它非常容易引起锁表,这样影响就是全局性的。注:虽然对于每个连接@@autocommit这个这个系统变量是独立的,但是锁表是全局性的。所以正规操作,在一次存储过程中调用,不管事务提交几次,只需在整个存储过程开端设置一次AUTOCOMMIT=0即可,在整个存储过程结束后单独设置一次AUTOCOMMIT=1即可。难怪我以前写的程序会出现:

Error Code : 1205

Lock wait timeout exceeded; try restarting transaction

连接A UPDATE e_course SET agent_nick = '222' WHERE course_id =2;

连接BUPDATE e_course SET agent_nick = '222' WHERE course_id =3;

即使不是更新的同一行,也会超时。是因为我的存储过程里边连接Acommit没有封闭那个事务,使得事务的set AUTOCOMMIT这个值被置为0,这样,对于这个表的后续操作,则被锁住了。如果此时直接关闭连接A,那么连接B就可以顺利提交。如果在存储过程里写SET AUTOCOMMIT=1; 那么即使没有commit也会解锁。如果保险起见,在存储过程结束应该写上SET AUTOCOMMIT=1; 这样可以避免此连接万一没有释放会造成表锁。注意,这个情况只对此表来说,换了其他的表,可以非常正常地更新。这种阻塞的情况,如果存储过程修改成COMMIT; 或者是 set AUTOCOMMIT=1; 那么被阻塞的语句为立刻执行。使用START TRANSACTIONautocommit仍然被禁用,直到您使用COMMITROLLBACK结束事务为止。然后autocommit模式恢复到原来的状态。

mysqlautocommit(自动提交)默认是开启,其对mysql的性能有一定影响,举个例子来说,如果你插入了1000数据,mysqlcommit1000次的,如果我们把autocommit关闭掉,通过程序来控制,只要一次commit就可以了。





    

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章