四、数据库建模设计
文/Bruce.Liu1
1.建模简介
图片来自网络范式:英文名称是 Normal Form,它是英国人 E.F.Codd(埃德加·弗兰克·科德)在上个世纪70年代提出关系数据库模型后总结出来的,范式是关系数据库理论的基础,也是我们在设计数据库结构过程中所要遵循的规则和指导方法。目前有迹可寻的共有8种范式,依次是:1NF,2NF,3NF,BCNF,4NF,5NF,DKNF,6NF。通常所用到的只是前三个范式,即:第一范式(1NF),第二范式(2NF),第三范式(3NF)
埃德加·弗兰克·科德(Edgar Frank Codd,1923-2003)是密执安大学哲学博士,IBM公司研究员,被誉为“关系数据库之父”,并因为在数据库管理系统的理论和实践方面的杰出贡献于1981年获图灵奖。1970年,科德发表题为“大型共享数据库的关系模型”的论文,文中首次提出了数据库的关系模型。由于关系模型简单明了、具有坚实的数学理论基础,所以一经推出就受到了学术界和产业界的高度重视和广泛响应,并很快成为数据库市场的主流。20世纪80年代以来,计算机厂商推出的数据库管理系统几乎都支持关系模型,数据库领域当前的研究工作大都以关系模型为基础。
数据库建模:
在设计数据库时,对现实世界进行分析、抽象、并从中找出内在联系,进而确定数据库的结构,这一过程就称为数据库建模。它主要包括两部分内容:确定最基本的数据结构;对约束建模。
-
1.概念模型的表示方法
E-R图主要是由实体、属性和联系三个要素构成的。在E-R图中,使用了下面四种基本的图形符号。 -
2.确定系统实体、属性及联系
系统分析阶段建立数据字典和数据流程图->建立概念模型->逻辑模型->物理模型;利用系统分析阶段建立的数据字典,并对照数据流程图对系统中的各个数据项进行分类、组织,确定系统中的实体、实体的属性、标识实体的码以及实体之间联系的类型。
在数据字典中“数据项”是基本数据单位,一般可以作为实体的属性。“数据结构”、“数据存储”和“数据流”条目都可以作为实体,因为它们总是包含了若干的数据项。作为属性必须是不可再分的数据项,也就是说在属性中不能包含其他的属性。 -
3.确定局部(分)E-R图
根据上面的分析,可以画出部分实体-联系图。
在这些实体中有下画线的属性可以作为实体的码,这几个实体之间存在着1:1、l:n和m:n几种联系。 -
4.集成完整(总)E-R图
各个局部(分)E-R图画好以后,应当将它们合并起来集成为完整(总)E-R图。在集成时应当注意如下几点:- 消除不必要的冗余实体、属性和联系。
- 解决各分E-R图之间的冲突。
- 根据情况修改或重构E-R图。
2.三范式设计
什么是范式? 范式是“符合某一种级别的关系模式的集合,表示一个关系内部各属性之间的联系的合理化程度”。很晦涩吧?实际上你可以把它粗略地理解为一张数据表的表结构所符合的某种设计标准的级别。就像家里装修买建材,最环保的是E0级,其次是E1级,还有E2级等等。数据库范式也分为1NF,2NF,3NF,BCNF,4NF,5NF。一般在我们设计关系型数据库的时候,最多考虑到BCNF就够。符合高一级范式的设计,必定符合低一级范式,例如符合2NF的关系模式,必定符合1NF。
1.1.第一范式(1NF)
- 第一范式(1NF):属性原子性约束
属性是原子性的,即不可在拆分
考虑这样一张表:【联系人】(姓名,性别,电话) 。
如果在实际场景中,一个联系人有家庭电话和公司电话,那么这种表结构设计就没有达到 1NF。要符合 1NF 我们只需把列(电话)拆分,即:【联系人】(姓名,性别,家庭电话,公司电话)
1.2.第二范式(1NF)
- 第二范式(2NF):主键约束
一是表必须有一个主键;二是没有包含在主键中的列必须完全依赖于主键,而不能只依赖于主键的一部分。
考虑一个订单明细表:【OrderDetail】(OrderID,ProductID,UnitPrice,Discount,Quantity,ProductName)。
因为我们知道在一个订单中可以订购多种产品,所以单单一个 OrderID 是不足以成为主键的,主键应该是(OrderID,ProductID)。显而易见 Discount(折扣),Quantity(数量)完全依赖(取决)于主键(OderID,ProductID),而 UnitPrice(单价),ProductName 只依赖于 ProductID。所以 OrderDetail 表不符合 2NF。
可以把【OrderDetail】表拆分为【OrderDetail】(OrderID,ProductID,Discount,Quantity)和【Product】(ProductID,UnitPrice,ProductName)来消除原订单表中UnitPrice,ProductName多次重复的情况。
1.3.第三范式(1NF)
- 第三范式(3NF):冗余性约束
任何字段不能由其他字段派生出来。要求字段没有冗余。
考虑一个订单表【Order】(OrderID,OrderDate,CustomerID,CustomerName,CustomerAddr,CustomerCity)主键是(OrderID)。
其中 OrderDate,CustomerID,CustomerName,CustomerAddr,CustomerCity 等非主键列都完全依赖于主键(OrderID),所以符合 2NF。不过问题是 CustomerName,CustomerAddr,CustomerCity 直接依赖的是 CustomerID(非主键列),而不是直接依赖于主键,它是通过传递才依赖于主键,多个订单中有同一个用户下单,就会产生数据冗余; 3NF中说的传递依赖,就出现了,所以不符合 3NF。
通过拆分【Order】为【Order】(OrderID,OrderDate,CustomerID)和【Customer】(CustomerID,CustomerName,CustomerAddr,CustomerCity)从而达到 3NF。
总结:通常来说,在数据库建模设计中范式设计是指导思想,实际实践中往往还要结合业务场景适当的进行调整。
3.反范式设计
denormalization?字面上就是做范式的反义词,事实上也是。遵循范式总体上来说是为了保证数据的integrity和减少冗余,但是,从直觉上我们就可以知道,一个完全按照范式设计的冗余极低的数据库,很可能在性能上会输给冗余相对多一些的数据库(比如说3NF的数据库,表多,关系复杂,数据库的IO次数很多,性能会收到影响)。增加冗余而提高性能,这便是denormalization的意义。至于说很多可能的异常,规避或者减轻的的手段有很多,是否遵循范式并不是唯一因素,denormalization也并不会导致出现异常。
3.1.三范式优缺点
三范式的优点:
- 减少冗余
- 减少异常(delete,update,insert)
- 让数据组织的更加和谐(愚以为仅仅对于系统来说,对于人可完全不是这么回事)
三范式的缺点:
范式等级与复杂度是递进的;通过范式的不断升级,我们会发现应用的范式等级越高,则表越多。表多会带来很多问题:
- 查询时要连接多个表,增加了查询的复杂度
- 查询时需要连接多个表,降低了数据库查询性能
- 而现在的情况,磁盘空间成本基本可以忽略不计,所以数据冗余所造成的问题也并不是应用数据库范式的理由。
3.2.反范式场景
在单库中基本上都可以以三范式作为数据库关系设计的核心思想,but在数据大爆炸的互联网浪潮中,三范式就显得力不从心。
- 数据库水平拆分时,要通过数据冗余的方式减少join操作。
- 数据库垂直拆分时,同样三范式也满足不了需求。
- 应用程序解耦时,需要数据库层进行分离。
- 分布式数据库场景中,如何汇总数据。
4.开发规范
4.1.表设计
- 库名、表名、字段名使用小写字母,”_”分割,不超过 12 个字符,使用名词且见名知意
- 默认使用 innodb 存储引擎,使用其他引擎必须注明缘由【FAQ】
- 存储精确浮点数使用 DECIMAL,替代 FLOAT 和 DOUBLE
- 使用 int unsigned 存储 IP 地址【FAQ】
- 根据字段长度选择合适的字段类型,如数字类型有 tinyint,smallint,mediuint,int,bigint 五种类型,分别占用 1byte,2byte,3byte,4byte,8byte。需要特别注意,int(10)和 int(2)无区别,应该采用 tinyint(2)替代 int(2).
- 尽量使用 tinyint 代替 enum 和 set 类型,减少后台类型转换
- 尽量避免使用 text、blob 字段类型
- Varchar(N)中,N 表示的是字符数不是字节数,如 varchar(255),可以最大存储255 个汉字。N 值应尽可能小,单表 varchar 字段最大长度为 65536 个字节,在排序和创建临时表等内存操作时,会使用 N 值来申请内存,而非存储值的实际长度
- 表字符集统一使用 utf8
- 存储年使用 year 类型,存储日期使用 date 类型,存储时间(精确到秒)使用timestamp 类型,而非 datetime 类型。因为 timestamp 使用 4 个字节,而 datetime 使用 8 个字节【FAQ】.
- 字段全部定义为 NOT NULL【FAQ】
- 将过大字段拆分到其他表中,不在数据库中存储图片、文件等内容
- 固定长度的表会更快【FAQ】
- 尽量避免使用外键【FAQ】
- 字段尽量全部定义为 NOT NULL:
通常情况下最好指定列为not null ,除非真的需要存储NULL值。如果查询中包含可为NULL的列,对于MySQL来说更难优化,因为可为NULL的列使得索引、索引统计和值比较都更复杂。可为NULL的列会使用更多的存储空间,在MySQL里也需要特殊处理。当可为NULL的列被索引时,每个索引记录需要一个额外的字节字节,在MyISAM里还可能导致固定大小的索引(只有一个int列的索引)变成可变大小的索引。
通常把已经上线的null改为not null带来的性能提升比较小,所以调优时没有必要优先纠正这种设计。但是如果计划在列上建索引,就应该尽量把该列设计为not null
当然也有例外,指的一提的是,InnoDB使用单独的位(bit)来存储null值,所以对于稀疏数据(很多值为null,只有少数行为非null)的情况下有很好的空间效率。
int 类型可以定义为 not null default 0,
varchar 类型可以定义为 notnull default '' - 适当的拆分/冗余
A.当我们的表中存在类似于 TEXT 或者是很大的 VARCHAR类型的大字段的时候,如果我们大部分访问这张表的时候都不需要这个字段,我们就该义无反顾的将其拆分到另外的独立表中,以减少常用数据所占用的存储空间。这样做的一个明显好处就是每个数据块中可以存储的数据条数可以大大增加,既减少物理 IO 次数,也能大大提高内存中的缓存命中率。
B.被频繁引用且只能通过Join 2张(或者更多)大表的方式才能得到的独立小字段,这样的场景由于每次Join仅仅只是为了取得某个小字段的值,Join到的记录又大,会造成大量不必要的IO,完全可以通过空间换取时间的方式来优化。不过,冗余的同时需要确保数据的一致性不会遭到破坏,确保更新的同时冗余字段也被更新。 - 控制表的大小
mysql在处理大表(char的表>500W行,或int表>1000W)时,性能就开始明显降低,所以要采用不同的方式控制单表容量
- 根据数据冷热,对数据分级存储,历史归档
- 采用分库/分表/分区表,横向拆分控制单表容量
- 对于OLTP系统,控制单事务的资源消耗,遇到大事务可以拆解,采用化整为零模式,避免特例影响大众
- 单库不要超过500个表
- 单表字段数不要太多,最多不要大于50个
4.2.索引设计
- 索引命名规范:
- 索引名称全部使用小写;
- 非唯一索引按照“ix_字段名称字段名称[字段名称]”进行命名
- 唯一索引按照“uq_字段名称字段名称[字段名称]”进行命名
- 唯一索引由 3 个以下字段组成并且字段都是整形时,使用唯一索引作为主键。没有唯一索引
或唯一索引不符合上述条件时,使用自增 id 作为主键。注意唯一索引不和主键重复【FAQ】 - 单张表的索引数量控制在字段数的 20%以内,至多 5 个,索引数量过多会导致写入性能 的显著下降
- 合理创建复合索引。首先要避免冗余,ix_a_b_c 相当于同时创建了 ix-_a,ix_a_b,ix_a_b_c 三个索引;其次要避免索引过大,建议最大 4 列复合,列数过多很难提升索引的区分度,反而降低索引的性能
- 合理使用覆盖索引
- 对于长度大于 100 的 varchar 字段建立索引时,使用其他方法【FAQ】
- 使用 EXPLAIN 判断 SQL 语句是否合理使用索引,尽量避免 extra 列出现 FILE SORT,USINGTEMPORARY【FAQ】
- 索引不只用于 select 查询,update 和 delete 语句也需要根据 where 条件合理设计索引
- where 条件中的非等值条件(IN,BETWEEN,<,<=,>,>=)会导致后面的条件无法使用索引
4.3.SQL设计
- 使用 prepared statement,可以提升性能并且避免 SQL 注入【FAQ】
- 降低 SQL 的复杂度,把 MySQL 尽量当做存储使用:
- 避免在 SQL 语句中进行数学运算、函数计算、逻辑判断等操作
- 避免多表 join,尽量拆分成多条查询。如无法避免,在 join 表时应使用相同类型的列,并且在列上有索引【FAQ】
- 避免使用存储过程、触发器、函数等
- Insert 语句使用 batch 提交(insert into table values (),(),(),……),values 的个数不超过 500;sql 语句中 in 包含的值不超过 500
- Update,delete 语句避免使用 limit,如果确实需要分配处理大量数据,可以增加其他字段来限制每次处理的记录数,比如主键 id
- 避免使用 select *【FAQ】
- 避免使用 order by rand(),使用其他方式替换【FAQ】
- 使用合理的分页方式以提高分页的效率
- 统计表中记录数时使用 count(*),而不是 count(pk)或 count(1)
- 数据库默认开启查询缓存,合理利用查询缓存提升 sql 效率【FAQ】
- 当只需要 1 行数据时使用 limit 1【FAQ】
- 拆分大的 delete 和 insert【FAQ】
- Where 条件中使用合适的类型,数值不加引号,字符加引号,避免 MySQL 进行隐式类型转换,从而无法使用索引【FAQ】
- 避免使用 or,对同一个字段将 or 改为 in,对不同字段将 or 改为 union【FAQ】
- 尽量避免负向查询,如 NOT、!=、<>、!<、!>、NOT EXISTS、NOT IN、NOT LIKE 等
- 针对同一张表的 alter table 操作,应该用逗号分隔,一次完成
- 注意 MySQL 中 insert ignore into;insert on duplicate key update;replace into 的区别,在应用中合理使用【FAQ】
4.4.分表设计
-
单表数据量控制在 300w 以下,如果字段全部为 int 类型,控制在 500w 以下
-
避免使用 MySQL 自带的分区表功能,单表数据量时通过程序来分表,使用 hash 分表时,
表名后缀使用 16 进制表示,如 user_ff;使用日期分表时,表明后缀使用日期,如user_20130707 或 user_201307
4.5.其他
- 尽量减小事务:事务使用原则是即开即用,用完即关 ;事务无关操作放到事务外面, 减少锁资源的占用;在不破坏一致性前提下,使用多个短事务代替长事务
- 可重复读(repeatable read)是 MySQL 的默认事务隔离级别,解决了脏读(dirty read)和幻读(phantom read)的问题,原则上禁止修改事务的隔离级别。【FAQ】
- 尽量使用短连接,完成查询后要主动释放连接,避免 MySQL 中出现大量 sleep 线程
- 不管使用连接池还是直连 MySQL,执行查询前都需要考虑获取的 MySQL 连接可能已经断开,如断开可以重连。执行查询后都要检查查询是否成功,如不成功,考虑是否需要重
新执行。
4.6.注解
- FAQ1.2
从安全性和性能两个角度都建议使用 InnoDB 引擎。首先数据量较大的时候,系统崩溃后如何快速恢复是一个重要问题。相对而言,MyISAM 崩溃后发生损坏的概率比 InnoDB 高
得多,而且恢复速度慢,可能丢失数据。因此,即使不需要支持事务,也推荐使用 innoDB.另外为改善 InnoDB 的性能,Oracle 投入了大量资源,对 Innodb 内部做了大量优化,使得
其性能在绝大部分场景远高于 MyISAM。
- FAQ1.4
使用 INTUNSIGNED 而不是 char(15)来存储 ipv4 地址,通过 MySQL 函数 inet_ntoa 和
inet_aton 来进行转化。Ipv6 地址目前没有转化函数,需要使用 DECIMAL 或者两个 bigINT 来存储。
例如:
SELECT INET_ATON('209.207.224.40');
3520061480
SELECT INET_NTOA(3520061480);
209.207.224.40
FAQ1.10
INT[M],M值代表什么含义?
注意数值类型括号后面的数字只是表示宽度而跟存储范围没有关系,比如INT(3)默认显示3位,空格补齐,超出时正常显示,python、java客户端等不具备这个功能。
为什么建议使用TIMESTAMP来存储时间而不是DATETIME?
DATETIME和TIMESTAMP都是精确到秒,优先选择TIMESTAMP,因为TIMESTAMP只有4个字节,而DATETIME8个字节。同时TIMESTAMP具有自动赋值以及自动更新的特性。
如何使用TIMESTAMP的自动赋值属性?
a) 将当前时间作为ts的默认值:ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP。
b) 当行更新时,更新ts的值:ts TIMESTAMP DEFAULT 0 ON UPDATE CURRENT_TIMESTAMP。
c) 可以将1和2结合起来:ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
- FAQ1.11
除非有很特别的原因去使用 NULL 值,最好总是让字段保持 NOT NULL。
首先,确定“Empty”和“NULL”有多大的区别(如果是 INT,那就是 0 和 NULL)?如果觉得它们没有什么区别,那么就不要使用 NULL。(在 Oracle 里,NULL 和 Empty 的字符串
是一样的!)。NULL 的存储需要额外的空间,并且,在进行比较的时候,会增加程序的复杂性。 当然,这里并不是说就不能使用 NULL 了,现实情况是很复杂的,依然会有些情况
下,你需要使用 NULL 值。
下面摘自 MySQL 官方文档:
“NULL columns require additional space in the row to record whether their values are NULL. For MyISAM tables, each NULL column takes one bit extra, rounded up to the nearest byte.”
- FAQ1.13
如果表中的所有字段都是“固定长度”的,整个表会被认为是 “static” 或 “fixed-length”。 例如,表中没有如下类型的字段: VARCHAR,TEXT,BLOB。只要包括了其中一
个这些字段,那么这个表就不是“固定长度静态表”了,这样,MySQL 引擎会用另一种方法来处理。固定长度的表会提高性能,因为 MySQL 搜寻得会更快一些,因为这些固定的长度是很容易
计算下一个数据的偏移量的,所以读取的自然也会很快。而如果字段不是定长的,那么,每一次要找下一条的话,需要程序找到主键。并且,固定长度的表也更容易被缓存和重建。不过,唯一的副作用是,固定长度的字段会浪费一些空间,因为定长的字段无论你用不用,他都是要分配那么多的空间。
- FAQ1.14
外键能够简化开发,但是外键在高并发下带来严重的锁问题,对性能影响极大,可以考虑通过程序来保证约束条件。
- FAQ2.2
建议使用自增 id 作为主键,以提升写入性能。以写入 10 万行数据为例,对比如下:
主键字段
Int(10)auto_increment
Varchar(36):
快的方式。
写入时间
133 秒
147 秒
小结:InnoDB 是聚集索引,写入速度严重依赖于写入顺序,按照主键的顺序写入是最
- FAQ2.6
可以考虑使用前缀索引或模拟 hash 索引下面的表增加一列url_crc32,然后对url_crc32建立索引,减少索引字段的长度,提高效率。
CREATE TABLE url(
url VARCHAR(255) NOT NULL DEFAULT ‘’,
url_crc32 INT UNSIGNED NOT NULL DEFAULT 0,
……
index idx_url(url_crc32) )
- FAQ2.7
EXPLAIN 语句(在 MySQL 客户端中执行)可以获得 MySQL 如何执行 SELECT 语句的信息。通过对 SELECT 语句执行 EXPLAIN,可以知晓 MySQL 执行该 SELECT 语句时是否使用了索引、
全表扫描、临时表、排序等信息。尽量避免 MySQL 进行全表扫描、使用临时表、排序等。详见官方文档。
- FAQ3.1
Prepared Statements 很像存储过程,是一种运行在后台的 SQL 语句集合,我们可以从使用prepared statements 获得很多好处,无论是性能问题还是安全问题。比如可以检查一些你
绑定好的变量,这样可以保护你的程序不会受到“SQL 注入式”攻击。当然,你也可以手动地检查你的这些变量,然而,手动的检查容易出问题,而且很经常会被程序员忘了。当
我们使用一些 framework 或是 ORM 的时候,这样的问题会好一些。在性能方面,当一个相同的查询被使用多次的时候,这会为你带来可观的性能优势。你可
以给这些 Prepared Statements 定义一些参数,而 MySQL 只会解析一次。虽然最新版本的 MySQL 在传输 Prepared Statements 是使用二进制形势,所以这会使得网络传输非常有效率。
- FAQ3.2.2
如果应用程序有很多 JOIN 查询,应该确认两个表中 Join 的字段是被建过索引的。这样,MySQL 内部会启动为你优化 Join 的 SQL 语句的机制。而且,这些被用来 Join 的字段,应该
是相同的类型的。例如:如果要把 DECIMAL 字段和一个 INT 字段 Join 在一起,MySQL 就无法使用它们的索引。对于那些 STRING 类型,还需要有相同的字符集才行。(两个表的字符
集有可能不一样)
// 在 state 中查找 company
$r = mysql_query("SELECT company_name FROM users LEFT JOIN companies ON (users.state = companies.state) WHERE users.id = $user_id");
// 两个 state 字段应该是被建过索引的,而且应该是相当的类型,相同的字符集。
- FAQ3.5
从数据库里读出越多的数据,那么查询就会变得越慢。并且还会增加网络传输的负载。减少使用覆盖索引完成查询的可能性。所以,应该养成需要什么就取什么的好习惯。
// 不推荐
$r = mysql_query("SELECT * FROM user WHERE user_id = 1");
$d = mysql_fetch_assoc($r);
echo "Welcome {$d['username']}";
// 推荐
$r = mysql_query("SELECT username FROM user WHERE user_id = 1");
$d = mysql_fetch_assoc($r);
echo "Welcome {$d['username']}";
FAQ3.6
想打乱返回的数据行?随机挑一个数据?方便用法的后面有非常可怕的性能问题。 如果真的想把返回的数据行打乱,有 N 种方法可以达到这个目。这样使用会让你的数据库
性能呈指数级的下降,具体问题是:MySQL 会不得不去执行 RAND()函数(很耗 CPU 时间), 而且这是为了每一行记录去记行,然后再对其排序。就算是你用了 Limit 1 也无济于事(因
为要排序,很耗 IO)
下面的示例是随机挑一条记录
// 千万不要这样做:
$r = mysql_query("SELECT username FROM user ORDER BY RAND() LIMIT 1");
// 这要会更好:
$r = mysql_query("SELECT count(*) FROM user");
$d = mysql_fetch_row($r);
$rand = mt_rand(0,$d[0] - 1);
$r = mysql_query("SELECT username FROM user LIMIT $rand, 1");
- FAQ3.9
某些查询语句会让 MySQL 不使用缓存。请看下面的示例:
// 不能够使用缓存
$r = mysql_query("SELECT username FROM user WHERE signup_date >= CURDATE()");
// 能够使用查询缓存
$today = date("Y-m-d");
$r = mysql_query("SELECT username FROM user WHERE signup_date >= '$today'");
上面两条 SQL 语句的差别就是 CURDATE() ,MySQL 的查询缓存对这个函数不起作用。所以,像 NOW() 和 RAND() 或是其它的诸如此类的 SQL 函数都不会开启查询缓存,因为这些函数
的返回是会不定的易变的。所以,你所需要的就是用一个变量来代替 MySQL 的函数,从而使用缓存。
- FAQ3.10
当你查询表的有些时候,你已经知道结果只会有一条结果, 在这种情况下,加上 LIMIT 1 可以增加性能。此时,MySQL 数据库引擎会在找到一条数据后停止搜索,而不是继续往后查少下一条符合记录的数据。
下面的示例,只是为了找一下是否有“中国”的用户,很明显,后面的会比前面的更有效率。
// 没有效率的:
$r = mysql_query("SELECT * FROM user WHERE country = 'China'");
if (mysql_num_rows($r) > 0) {
// ...
}
// 有效率的:
$r = mysql_query("SELECT 1 FROM user WHERE country = 'China' LIMIT 1");
if (mysql_num_rows($r) > 0) {
// ...
}
- FAQ3.11
如果你需要在一个在线的网站上去执行一个大的 DELETE 或 INSERT 查询,你需要非常小心, 要避免你的操作让你的整个网站停止相应。因为这两个操作是会锁表的,表一锁住了,别的操作都进不来了。
Apache 会有很多的子进程或线程。所以,其工作起来相当有效率,而我们的服务器也不希望有太多的子进程,线程和数据库链接,这是极大的占服务器资源的事情,尤其是内存。
如果你把你的表锁上一段时间,比如 30 秒钟,那么对于一个有很高访问量的站点来说,这 30 秒所积累的访问进程/线程,数据库链接,打开的文件数,可能不仅仅会让你泊 WEB 服务 Crash,还可能会让你的整台服务器马上掛了。
所以,如果你有一个大的处理,你定你一定把其拆分,使用 LIMIT 条件是一个好的方法。
下面是一个示例:
while (1) {
//每次只做 1000 条
mysql_query("DELETE FROM logs WHERE log_date <= '2009-11-01' LIMIT 1000");
if (mysql_affected_rows() == 0) {
// 没得可删了,退出!
break;
}
// 每次都要休息一会儿
usleep(50000);
}
- FAQ3.12
因为 MySQL 进行隐式类型转化之后,可能会将索引字段类型转化成”=号”右边值的类型, 导致使用不到索引.
- FAQ3.13
OR 的时间复杂度为 0(n),in 的时间复杂度为 0(log n),也就是说用 in 效率更高.
例子:
使用 in 提升性能
Select * from opp WHERE phone=‘12347856' or phone=‘42242233';修改为
Select * from opp WHERE phone in ('12347856' , '42242233')
Merge index 效果不好,使用 union all 提升性能。
例子:
Select * from opp WHERE phone='010-88886666' or cellPhone='13800138000'; ? 修改为
Select * from opp WHERE phone='010-88886666' union all Select * from opp WHERE
cellPhone='13800138000';
- FAQ3.16
1.insert ignore into
当插入数据时,如出现错误时,如重复数据,将不返回错误,只以警告形式返回。所以使 用 ignore 请确保语句本身没有问题,否则也会被忽略掉。
2.on duplicate key update
当 primary key 或者 unique key 重复时,则执行 update 语句, 注意如果多个行匹配,只更新 1 行,所以应避免对带有多个 unique key 的表使用 ON DUPLICATE KEY 子句
3.replace into
如果存在 primary key or unique key 相同的记录,则先全部删除掉,再插入新记录。
- FAQ5.2
大多数数据库系统的默认隔离级别是提交读(READ COMMITTED),但 MySQL 是可重复读 (repeatable read)。提交读这个事务隔离级别也叫做不可重复读。如果业务场景确实因为
大量的并发插入导致锁问题严重,理论上可以通过降低隔离级别到提交读,减少锁争用, 但是降级一方面会产生幻读的问题,另一方面必须工作在 MySQL 的 binlog 格式是 row 的情
况下,否则会造成主从同步中断。所以,从数据安全性角度出发,原则上禁止修改事务的隔离级别,性能问题尽量通过优化业务逻辑来解决。
FAQ5.3 MySQL中innodb表需要指定primary key?
.基于聚集索引的增、删、改、查的效率相对是最高的;
.如果我们定义了主键(PRIMARY KEY),那么InnoDB会选择其作为聚集索引;
.如果没有显式定义主键,则InnoDB会选择第一个不包含有NULL值的唯一索引作为主键索引;
.如果也没有这样的唯一索引,则InnoDB会选择内置6字节长的ROWID作为隐含的聚集索引(ROWID随着行记录的写入而主键递增,这个ROWID不像ORACLE的ROWID那样可引用,是隐含的)。
综上总结,如果InnoDB表的数据写入顺序能和B+树索引的叶子节点顺序一致的话,这时候存取效率是最高的,也就是下面这几种情况的存取效率最高:
a.使用自增列(INT/BIGINT类型)做主键,这时候写入顺序是自增的,和B+数叶子节点分裂顺序一致;
b.该表不指定自增列做主键,同时也没有可以被选为主键的唯一索引(上面的条件),这时候InnoDB会选择内置的ROWID作为主键,写入顺序和ROWID增长顺序一致;
c.除此以外,如果一个InnoDB表又没有显示主键,又有可以被选择为主键的唯一索引,但该唯一索引可能不是递增关系时(例如字符串、UUID、多字段联合唯一索引的情况),该表的存取效率就会比较差。
- 附录
- 建模简介
- 三范式设计
- 反范式设计
- 开发规范