数据库相关知识回顾与总结

数据库范式

https://www.zhihu.com/question/24696366

索引

  • 索引可以加快数据库的检索速度
  • 索引降低了插入、删除、修改等维护任务的速度
  • 唯一索引可以确保每一行的唯一性
  • 通过使用索引,可以在查询的过程中使用优化隐藏器,提高系统的性能
  • 索引需要占用物理和数据空间

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

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

B-Tree、B+-Tree、B*-Tree

主码索引、聚集索引(聚集主码)、非主码索引(辅助索引)、唯一索引、外键索引、复合索引、外键索引、单列索引
MySQL索引背后的数据结构及算法原理

数据库的并发控制

事务(Transaction)

事务是什么?

事务是并发控制的基本单位。所谓事务,它就是一组操作序列,这些操作要么执行要么都不执行,它是一组不可分割的工作单位。事务是数据库维护数据一致性的单位,在每个事务结束时,都能保持数据一致性。

从上面的描述可以看出事务的提出主要是为了解决并发情况下保持数据一致性的问题。

事务具有以下4个基本特征

  • 原子性(Atomic):事务中包含的每个操作都被看做一个逻辑单元,这个逻辑单元中的操作要么全部成功,要么全部失败。(记录之前的版本,允许回滚)

  • 一致性(Consistency):事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。换句话说,事务执行前和事务执行后数据内在的逻辑始终是成立的。比如转帐前后两人的存款总和始终不变。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统运行中发生故障,有些事务尚未完成就被迫中断,这时数据库就处于一种不正确的状态,或者说是不一致的状态。例如在一在进行转账的操作中,需要从账户A取出100元转入账户B。那么就可以定义一个事务,该事物包括两个操作:第一个操作是从账户A中减去100元,第二个操作是向账户B中转入100元。这两个操作要么全做,要么全不做。全做或者全不做,数据库就会处于一致性状态。如果只做一个操作,则逻辑上就会发生错误,减少或增加100元,数据库就 处于不一致的状态了。所以说一致性和原子性是密不可分的。

    但是现在问题来了——原子性就一定能够保证一致性吗?

    答案是否定的:原子性不能完全保证一致性。因为在多个事务并行进行的情况下,即使保证了每个事务的原子性,仍然可能导致数据不一致的结果。例如事务1需要将100元转入账户A:先读取A的账户余额的值,然后在这个值上加上100.但是,在这两个操作之间,事务2修改了账户A的值,为它增加了100元,那么最后结果应该是A增加了200元。但事实上,当事务1最终完成时,账户A只增加了100元,因为事务2的执行结果被事务1覆盖掉了。所以为了保证并发事务的一致性,就引入了事务的隔离性。(事务开始和结束之间的中间状态不会被其他事务看到)

  • 隔离性(Isolation):一个事务的执行不能被其他事务干扰。即一个事务的内部操作及使用的数据对其他并发事务是隔离的,并发执行的各个事务之间不能互相干扰。要达到这么一种效果:对于任意两个并发的事务T1和T2,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发的执行。关于事务的隔离性数据库提供了多种隔离级别,后面会提到。(适当地破坏一致性来提升性能与并行度 例如:最终一致 ~= 读未提交)
  • 持久性(Durability):持久性是指一个事务一旦提交,它对数据库中数据的改变就是永久性的。接下来的操作或故障不应该对其执行结果有影响。(每一次的事务提交之后就会保证不丢失)
保证事务ACID特性是事务管理的重要任务。事务ACID特性可能遭到破坏的因素有:
  1. 多个事务并行运行时,不同的事务操作交叉执行;
  2. 事务在运行过程中被强行停止。

在第一种情况下,数据库管理系统必须保证多个事务的交叉运行不影响这些事务的原子性;在第二种情况下,数据库管理系统必须保证被强行终止的事务对数据库和其他事务没有任何影响。

下面来说说事务的隔离性

当多个线程都开启事务操作数据库中的数据时,数据库系统要能进行隔离操作,以保证各个线程获取数据的准确性,在介绍隔离性之前先说说如果不设置事务隔离级别可能会发生什么问题:

  • 脏读(dirty read):脏读是指在一个事务处理过程里读取到了另一个未提交的事务中的数据。例如,事务T1修改某一数据并将其写回磁盘,事务T2读取同一数据后,T1由于某种原因被撤销,此时被T1修改过的数据恢复原值,T2读到的数据就与数据库中的数据不一致,则T2读到的数据就为“脏数据”,即不正确的数据。(修改时允许读取导致脏读)

  • 不可重复读(non-repeatable read):不可重复读是指在同一个事务内,针对同一数据进行多次相同的查询返回的结果不同。即当事务T1读取某一数据之后,事务T2对其进行了修改,当事务T1再次读该数据时,得到与前一次不同的值。(读取时允许修改导致不可重复读)

  • 丢失修改(lost update):

    1. 第一类丢失修改:事务T1撤销时,把已经提交的事务T2的更新数据覆盖了;
    2. 第二类丢失修改:两个事务T1和T2读入同一数据并修改,T2提交的结果破坏了T1提交的结果,导致T1的修改被丢失。(修改时允许修改导致丢失更新)
  • 幻读(phantom read):(读取时允许插入或删除导致幻读)

    1. 事务T1按一定条件从数据库中都去了某些数据记录之后,事务T2删除了其中部分记录2,当T1再次按相同条件读取数据时,发现某些记录神秘地消失了;
    2. 事务T1按一定条件从数据库中读取某些数据记录后,事务T2插入了一些记录,当T1再次按相同条件读取数据时,发现多了一些记录。

幻读和不可重复读有什么区别?

因为从上面的描述来看,幻读和不可重复读都是在同一次事务中按相同的条件查询发现结果不一致。即从总的结果来看,似乎两者都表现为两次读取的结果不一致。那幻读和不可重复读有什么区别呢?

  • 不可重复读的重点是修改:同样的条件,我们读取过的数据,再次读取出来发现值不一样了
  • 幻读的重点在于新增或者删除:同样的条件,第一次和第二次读取出来的记录数不一样

但是如果从控制的角度来看,两者的区别就比较大了:

  • 对于不可重复读,只需要锁住满足条件的记录
  • 对于幻读,它是由于并发事务增加或者删除记录导致的,不能像不可重复读通过记录加锁解决,因为对于新增的记录根本无法加锁。需要将事务串行化,才能避免幻读。
事务隔离级别(由低到高)
  • 读未提交(Read uncommited):如果一个事务已经开始写数据,则另外一个事务不允许同时进行写操作,但允许其他事务读取此行数据。该隔离级别可以通过“排它锁”实现。读未提交避免了丢失修改,但是却可能出现脏读——也就是事务T1都到了事务T2未提交的数据;

  • 读已提交(Read committed):读取数据的事务允许其他食物继续访问该行数据,但是未提交的写事务将会禁止其他事物访问该行。读已提交是在读未提交的基础上避免了脏读。即一个事务在写数据的同时,不允许其他事务读取未提交的数据,可以避免脏读现象的发生。可是问题又来了——某行记录有一列值为0,事务T1正在对其进行更新操作,把值update为1,同时事务T2正在对该行记录进行查询操作。由于T2不允许读取T1未提交的数据,则此时T2读取到的值为0。假设T1已经提交,但是T2并未结束,并且又对该记录进行了一次查询,发现此列数据的值变为了1。问题出现了:T2在对同一数据进行相同的两次查询操作时,得到的数据的值并不一致,此时发生了不可重复读的问题。

  • 可重复读(Repeated read):读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。

    可以通过“共享读锁”和“排他写锁”避免了不可重复读和脏读,但是有时可能出现幻读。

  • 序列化(Serializable):提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个执行,但是不能并发执行。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。序列化是最高的事务隔离级别,同时代价也花费最高,但是性能很低,一般很少用。在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻读。

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据系统的隔离级别设为Read Committed。它能够避免脏读,而且具有较好的并发性能。尽管它会导致不可重复读、幻读等并发问题。但是在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。

大多数数据库的默认级别就是Read committed,比如 Sql Server和Oracle

MySQL的默认隔离级别就是Repeatable read

事务相关的SQL语句

  • BEGIN /BEGIN WORK / START TRANSACTION:开启事务
  • COMMIT:提交事务
  • ROLLBACK:回滚事务

封锁

封锁是实现并发控制的一个非常重要的技术。所谓封锁就是事务T在对某个数据对象例如表、记录等操作之前,先向系统发出请求,对其加锁。加锁后事务T就对该数据对象有了一定的控制,在事务T释放它的锁之前,其他事务不能更新此数据对象。例如,事务T1要修改A,若在读出A之前先锁住A,其他事务就不能再读取和修改A了,直到T1修改并写回A后解除了对A的封锁为止。这样,就不会丢失T1的修改。

确切的控制由封锁的类型决定。基本的封锁类型有两种:排他锁(exclusive locks,简称X锁)和共享锁(share locks,简称S锁)

  • X锁(排他写锁):若事务T1对数据对象A加上X锁,则只允许T读取和修改A,其他任何事物都不能再对A加任何类型的锁,直到T释放A上的锁为止。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A;
  • S锁(共享读锁):若事务T对数据A加上S锁,则事务T可以读A但是不能修改A,其他事务只能对A加S锁而不能加X锁,直到T释放A上的S锁为止。这就保证了其他食物可以读A,但在T释放A上的S锁之前不能对A进行任何修改。

封锁协议

  • 一级封锁协议:事务T在对数据对象A进行修改之前,必须对其加X锁,直至事务结束才释放。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK);

    在一级加锁协议中,如果仅仅是对数据进行读操作而不进行修改,是不需要进行加锁的。所以只能避免修改丢失而不能避免不可重复读和脏读。

  • 二级封锁协议:在一级加锁协议的基础上增加事务T在读取数据R之前必须先对其加S锁,读完后即可释放S锁;

    二级加锁协议除防止了丢失修改,还可进一步防止读脏数据。例如:事务T1正在对数据对象R进行修改,此前已经对R加上了X锁,此时事务T2想读取R,就必须对R加上S锁,但是T2发现R已经被T1加上了X锁,于是T2只能等待T1释放了在R上加的锁之后才能对R加S锁并读取。这能防止T2读取到T1未提交的数据,从而避免了脏读。

    但是在二级封锁协议中,由于读完数据后即可释放S锁,所以它不能保证可重复读。

  • 三级封锁协议:三级封锁协议是指,在一级封锁协议的基础上增加事务T在读取数据R之前对其加S锁直至事务结束才释放。

    三级封锁协议除了防止丢失修改和读“脏”数据之外,还进一步防止了不可重复读。

上述三级协议的主要区别在于什么操作需要申请加锁,以及何时释放锁(即锁的持有时间)。不同的封锁协议使事务达到的一致性是不同的,封锁协议越高,一致性程度越强。

活锁和死锁

  • 活锁:如果事务T1封锁了数据R,事务T2又请求封锁R,于是T2等待。T3也请求封锁R,当T1释放了R上的锁之后系统首先批准了T3的请求,T2继续等待;然后T4又请求封锁R,T3在释放R上的锁之后系统又批准了T4的请求,T2有可能永远等待,这就是活锁的情形。

    避免活锁的简单方法就是采用先来先服务的策略。当多个事务请求封锁同一数据对象时,封锁子系统按请求锁的先后次序对事务进行排队,数据对象上的锁一旦释放就批准批准申请队列中第一个事务获得锁。

  • 死锁:事务T1封锁了数据R1,事务T2封锁了数据R2;同时,事务T1请求封锁R2,因为T2已经封锁了R2,所以T1只能等待。T2也请求封锁R1,由于R1被T1封锁了,R2也只能等待。由于它们互相等待,T1和T2两个事务永远也不能结束,于是就形成了死锁。

    目前数据库解决死锁的问题主要有两类方法:

    一. 死锁的预防

    ​ 在数据库中,产生死锁的原因是两个或多个事务都已经封锁了一些数据对象,然后又都请求对已被事务封锁的对象加锁,从而出现死锁。防止死锁的发生其实就是要破坏产生死锁的条件。预防死锁发生通常有以下两种方法。

    • 一次封锁法:一次封锁法要求每个事务必须一次将所有要使用的数据全部加锁,否则就不能继续执行下去。一次封锁法虽然可以有效防止死锁的发生,但是增加了锁的粒度,从而降低了系统的并发性。并且数据库是不断变化的,所以事先很难精确地确定每个事务所需进行加锁的对象,为此只能扩大封锁范围,将事务在执行过程中可能需要封锁的数据对象全部加锁,这就进一步降低了并发度;

    • 顺序封锁法:顺序封锁法是预先对数据对象规定一个封锁顺序,所有事务都按这个顺序实施封锁。例如在B树结构的索引中,可规定封锁的顺序必须是从根节点开始,然后是下一级的子节点,逐级封锁。顺序封锁法可以有效地避免死锁,但是要实现顺序封锁法十分的困难,因为很难事先确定每一个事务要封锁哪些对象,因此也就很难按规定的顺序去实施加锁。

      由此可见数据库中不适合预防死锁,只适合进行死锁的诊断与解除

    二、死锁的诊断与解除

    ​ 数据库系统中诊断死锁的方法与操作系统类似,一般使用超时法或事务等待图法

    • 超时法:如果一个事务的等待时间超过了规定的时限,那么就认为其发生了死锁。超时法实现简单,但其不足也十分明显,一是有可能误判了死锁,如事务因为其他原因而使等待时间超过时限,系统就会误认为发生了死锁;二是若时限设置得太长,则不能及时发现死锁。
    • 事务等待图法:事务等待图是一个有向图G=(T,U),T为结点的集合,每个结点表示正在运行的事务;U为边的集合,每条边表示事务等待的情况。若T1等待T2,则在T1,T2之间画一条有向边,从T1指向T2。事务等待图动态地反应了所有事务的等待情况。并发控制子系统周期性(比如每隔数秒)生成事务等待图,并进行检测。如果发现图中存在回路,则表示系统中出现了死锁。

    数据库管理系统的并发控制系统一旦检测到系统中存在死锁,就要设法解除。通常采用的方法是选择一个处理死锁代价最小的事务,将其撤销,释放此事务持有的所有的锁,使其他事务得以继续运行下去。当然,对撤销的事务所进行的数据修改必须加以恢复。

两段锁协议

  • 在对任何数据进行读、写操作之前,首先要申请并获得对该数据的封锁;
  • 在释放一个锁之后,事务不再申请和获得任何其他封锁。

所谓两段锁的含义是,事务分为两个阶段:第一个阶段是获得封锁,也称为拓展阶段,在这个阶段,事务可以申请获得任何数据项上的任何类型的锁,但是不能释放锁;第二个阶段是释放封锁,也称为收缩阶段,在这个阶段,事务可以释放任何数据项上的任何类型的锁,但是不能再申请任何锁。

乐观锁与悲观锁

  • 乐观锁(Optimistic Lock)相对于悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。

    ​ 乐观锁的实现方式

    1. 使用数据版本机制实现,这是乐观锁最常用的一种实现方式。数据版本即为数据增加一个版本表示,一般是通过为数据表增加一个数字类型的“version”字段来实现。当读取数据的时候将version字段的值一并读出,数据每更新一次对此version加一。当提交更新的时候,判断数据表对应记录的当前版本信息与之前取出来的进行对比。如果数据表当前版本号与之前取出来的version的值相等,则予以更新否则认为是过期数据;
    2. 乐观锁的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的数据表中增加一个时间戳(timestamp),和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和更新前取到的时间戳进行对比,如果一致则正常进行接下来的操作,否则就是版本冲突认为是过期数据。

其实乐观锁机制就是应用到了CAS(Compare And Set)思想,在某些情况下可以减少对数据库、关系或者是记录的加锁,提高并发性能。

  • 悲观锁(Pessimistic Lock)需要使用到数据库的锁机制。

在实际生产环境中,如果并发量不大并且不允许脏读,我们就可以使用悲观锁解决并发问题;但如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以我们就要选择乐观锁定的方法。

分库分表

在网站刚刚搭建的时候我们常常只使用一台数据库服务器。可是随着业务的发展,数据库中的数据不断增加,单台服务器逐渐不能满足我们日常的生产需求了,此时我们可以考虑对数据库进行拆分。分库分表的目的就是要把一个数据库切分成多个部分放到不同的数据库上,从而缓解单一数据库的性能问题。(这里暂时先不考虑读写分离,只讨论分库分表)。

数据库的切分主要分为两个方面:水平切分和垂直切分。不太严格地讲,如果数据库是因为表多而数据多,这时候适合使用垂直切分,即把关系紧密(比如同一模块)的表切分出来放在一个数据库上。如果表并不多,但是每张表的数据非常多,这时候适合水平切分,即把表的数据按某种规则(比如说ID散列)切分到多个数据库上。现实当中更多是这两种情况混杂在一起,这时候需要根据实际情况做出选择,也可能会综合使用垂直切分与水平切分,从而将原有数据库切分成类似矩阵一样可以无限扩充的数据库阵列。

  • 垂直切分

    垂直切分最大的特点就是规则简单,实施起来也比较方便,尤其适合各业务之间耦合度非常低、相互影响非常小并且业务逻辑非常清晰的系统。在这种系统中,可以很容易做到将不同业务模块所使用的表拆分到不同的数据库中。根据不同的表来进行拆分,对应用程序的影响也更小,拆分规则也会比较清晰。

  • 水平切分

    水平切分相对于垂直切分来说略微复杂,因为在水平切分中药将同一个表中的不同数据拆分到不同的数据库当中。对于应用程序来说水平切分相比垂直切分,规则更加复杂,并且后期的数据维护也会比较麻烦。

    所以对于垂直切分来说,只要把业务逻辑和数据表间的关联关系理清楚了,可以比较方便地进行数据库的分布式部署。然而我们该如何确定水平拆分的拆分规则呢?要想做到数据的水平切分,那么在每一个表中必须要用冗余字段作为切分依据和标记字段。幸运的话,我们可以选用特定表中的ID作为区分字段,基于此就有如下三种分库分表的方式(当然还可以有其它的方式):

  1. 号段分区

    ID为110000的放入DB1,ID为1000120000的放入DB2,以此类推。

    优点:可部分迁移

    缺点:数据分布不均

  2. hash取模区分区

    对ID进行hash,然后将得到的hash对一个特定的数字进行取模运算,比如说hashcode%4假如结果为0就放入DB1,结果为1就放入DB2,以此类推。

    优点:数据分布均匀

    缺点:数据迁移的时候比较麻烦,不能按照机器性能分摊数据

  3. 在认证库中保存数据库配置

    建立一个DB,这个DB单独保存特定ID到DB的映射关系,每次访问数据库的时候都要先查询一次这个数据库,以得到具体的DB信息,然后才能进行我们需要的操作。

    优点:灵活性强,一对一关系

    缺点:每次查询之前都要多一次查询,性能大打折扣

但是在进行垂直拆分和水平拆分之后,虽然有助于减轻对数据库的压力,但是一系列的问题也随之而来

  • 垂直拆分会带来如下影响:

    1. 单机的ACID被打破了。数据到了多机之后,原来在单机通过事务来进行的处理逻辑会受到很大的影响。我们面临的选择是:要么放弃原来的单机事务,修改实现,要么引入分布式事务;
    2. 一些Join操作会变得比较困难,因为数据可能已经在两个数据库中了,所以不能很方便地利用数据库自身的Join了,需要应用或者其他方式解决;
    3. 靠外键去进行约束的场景会受到影响。
  • 水平拆分会带来如下影响:

    1. 同样可能会有ACID被打破的情况;
    2. 同样有可能有Join操作被影响的情况;
    3. 靠外键去进行约束的场景会有影响;
    4. 依赖单库的自增序列生成唯一ID会受影响;
    5. 针对单个逻辑意义上的表的查询要跨库了。

    针对以上问题的解决方案:

    1. 分布式事务问题:使用两阶段提交协议。我们在单库上完成相关的数据操作之后,就会直接提交或者回滚,而在分布式系统中,在提交之前增加了准备的阶段,所以称为两阶段提交。事务在第一阶段对资源进行准备,如果在准备阶段有一个资源失败,那么在第二阶段的处理就是回滚所有资源,否则进行Commit操作。但是在实际应用当中,由于事务管理器自身的稳定性、可用性的影响,以及网络通信中可能产生的问题,出现的情况会复杂很多。此外,事务管理器在多个资源之间进行协调,它自身要进行很多日志记录的工作。网络上的交互次数的增多以及引入事务管理器的开销,是使用两阶段提交协议使得分布式事务的开销增大的两个方面。因此,在进行垂直拆分和水平拆分后,需要想清楚是否一定要引入两阶段的分布式事务,在必要的情况下才建议使用。

    2. 多机Sequence问题:当转变为水平分库时,原来单库的Sequence以及ID的做法需要改变。我们需要从连续性唯一性两方面来考虑问题。如果只从唯一性考虑,我们可以参考UUID的生成方式,或者根据自己的业务情况使用各个种子(不同维度的标识,例如IP、MAC、机器名、时间、本机计数器等因素)来生成唯一的ID。这样生成的ID虽然保证了唯一性,但在整个分布式系统中的连续性不好。接下来看看连续性。这里的连续性指的是在整个分布式环境中生成的ID的连续性。在单机环境中,其实就是一个单点来完成这个任务,在分布式系统中,我们可以用一个独立的系统来完成这个工作。

      这里提供一个解决方案:

      我们把所有ID集中放在一个地方进行管理,对每个ID序列独立管理,每台机器使用ID时都从这个ID生成器上进行获取。

      可能涉及的问题:

      ​ ① 性能问题。每次都从远程获取ID会有资源损耗。加入是一次取一段ID,然后缓存到本地,这样就不需要每次都去远程机器获取ID了,但是如果应用取了一段ID之后宕机了,那就浪费了一段可用ID;

      ② 生成器的稳定性问题。ID生成器作为一个无状态的集群存在,其可用性要靠整个集群来保证。

      ​ ③存储问题。底层存储的选择空间大,需要根据不同类型进行对应的容灾方案。有两种方式可以解决这个问题:第一,我们在底层使用一个独立的存储来记录每个ID序列当前的最大值,并控制并发更新,这样一来ID生成器的逻辑就很简单了。第二种是直接把ID生成器舍去,把相关的逻辑放到需要ID的应用本身。不过我们不希望生成器之间还有通信,因此数据的ID并不是严格按照进入数据库的顺序而增大的,在管理上也需要有额外的功能,这是需要进行权衡的地方。

      1. 跨库Join问题:**在分库之后,如果需要Join的数据还放在一个库里面,那就可以进行Join操作。例如,我们根据用户id进行用户相关信息的分库,那么如果查询某个用户在不同表中的一些关联信息,还是可以进行Join操作的。如果需要Join的数据已经分布在多个库当中,那就需要完成跨库Join的操作,这会比较麻烦,解决思路有以下几种:

        ①在应用层把原来数据库中的Join操作分成多次数据库操作。举个例子,我们有用户基本信息表,也有用户出售的商品的信息表,需求是查出来登记手机号为152XXXXXXX的用户在售的商品总数。这在单库时用一个SQL的Join就解决了,而如果商品信息与用户信息分离了,我们就需要现在应用层根据手机号找到用户id,然后再根据用户id找到相关的商品总数。

        ​ ②数据冗余。也就是对一些常用的信息进行冗余,这样就可以把原来需要Join的操作变为单表查询。这需要结合具体业务场景。

        ​ ③借助外部系统(例如搜索引擎)解决一些跨库问题。

      2. 外键约束问题:外键约束的问题比较难解决,不能完全依赖数据库本身来完成之前的功能。如果需要对分库后的单库做外键约束,就需要分库后每个单库的数据是内聚的,否则就只能靠应用层的判断了、容错方式了。

SQL优化

我们可以从四个方面来对数据库进行优化:SQL及索引、数据库表结构、系统配置、硬件。这四种优化成本依次升高,但是优化效果却依次降低。所以我们对数据库的优化应该更关注成本低效果好的SQL及索引优化和数据库表结构优化。

优化手段:

  • 开启慢查询日志对有效率问题的SQL进行监控;
  • 使用explain查看SQL的执行计划;
  • 建立合适的索引,尽量选择在where从句、group by从句、order by从句、no从句中出现的列,索引字段越小越好,如果定义联合索引要把离散度大的列放在前面;
  • 尽可能使用小的数据类型,尽量使用简单的数据类型(比如在MySQL中int要比varchar处理起来更简单),尽可能使用not null字段,少使用text等大的数据类型,一定要用的时候考虑分表;
  • 范式化
  • 表的垂直拆分,水平拆分。

参考资料:

《数据库系统概论》(第5版)

《大型网站系统与Java中间件实践》

http://www.cnblogs.com/zhongxinWang/p/4262650.html

http://blog.csdn.net/bluishglc/article/details/6161475

https://www.zhihu.com/question/30272728

http://www.cnblogs.com/fjdingsd/p/5273008.html

http://bbs.csdn.net/topics/120024254

推荐阅读更多精彩内容