3-MySQL DBA笔记-开发基础

五迷三道 提交于 2019-12-13 18:13:04

第二部分 开发篇
本篇首先讲述数据库开发的一些基础知识,如关系数据模型、常用的SQL语法、范式、索引、事务等,
然后介绍编程开发将会涉及的数据库的一些技巧,最后结合生产实际,提供一份开发规范供大家参考。

第3章 开发基础
本章将为读者介绍MySQL数据库相关的开发基础,首先,介绍一些基础概念,然后讲解关系数据模型和SQL基础。
由于在互联网开发者中,PHP开发者占据了相当大的比重,因此这里也将简要介绍下PHP开发者应该掌握的一些基础知识和开发注意事项。
最后,要接触的是MySQL数据库更深层次的内容——索引、主键、字符集等。

3.1 相关基础概念
(1)框架
在软件开发过程中,研发人员经常借助框架(framework)来辅助自己进行软件开发。
成熟的框架可以帮助处理很多细节性的问题,并完成一些基础性的工作,如生成访问数据库的代码、简化网络编程,这样开发者就会有更多的时间和精力专注于业务逻辑的设计。
但目前仍存在的一个问题是,一些框架对于数据库的使用不符合我们的预期,或者说不友好,故而有必要先了解一下开发框架是如何存取数据的。
大家有兴趣的话,可深入学习和使用如下这些业内使用比较广泛的一些框架,如 Django(Python)、Ruby onRails(Ruby)、Zend Framework(PHP)、Spring(JAVA)等。
(2)数据模型
数据模型(data model)是数据的定义和格式,即数据是如何组织的。
关系数据模型是以二维表的结构来表示实体与实体之间的联系,每个二维表又可称为关系。关系可以看作是一系列记录的集合。
如,员工关系表(见表3-1)和项目关系表(见表3- 2)。
从以上两个关系表中可以看出,项目表和员工表是存在某种关系的。
众多的关系表,以及关系表之间的关系,构成了关系数据模型,而支持关系模型的数据库管理系统则称之为关系数据库管理系统。
其他的模型还有XML和图数据模型(graph data model)等。
XML是一种层次结构的数据结构,使用标签、标签值来标识信息,如下面的这个xml文件。
<note>
<to>George</to>
<from>John</from>
<heading>Reminder</heading>
<body>Don't forget the meeting!</body>
</note>
而图数据模型存储的数据则是以点、线的方式进行存储的。
(3)schema
schema可译作“模式”,不同的数据库管理系统,schema的意义会有些不同。
依据维基百科的定义:schema指的是用数据库管理系统支持的语言描述的数据结构,它定义了数据是如何组织构建的。
典型的关系数据模型,是以数据库表的形式来组织数据的,数据存储于一系列设计好的表中。也就是说,关系数据库的schema就是数据库中各种关系的结构化描述。
一般来说,数据建模就是设计数据表的过程,一般在项目初期就设计好表结构, 在开发过程中可能会不断地调整表结构,但一旦应用上线,表结构往往就不会频繁变更了。
若项目积累了大量数据,这时再修改表结构可能会很耗时,从而严重影响在线服务,所以前期进行一个优良的数据库表设计是很有必要的,这也考验着开发人员的数据建模能力。
数据库表的设计一般由经验丰富的开发人员来负责,如果DBA时间精力允许,也会参与到重要的项目数据库表设计中。
MySQL中的schema可以看作是数据库(database)的同义词。我们创建一个schema,其实就是创建一个数据库(create database)。而在其他数据库中,schema的概念则略有不同。
(4)结构化数据
结构化数据通常是指被记录信息的类型,格式等属性是固定的,一般可存储于关系数据库或电子表格中,可以用数据记录的形式进行表达和存储,
如产品及其零部件的名称、代号、设计日期、类型等信息。
结构化数据往往需要预先定义好业务数据类型的模型,确定这些数据类型是如何存储、处理和访问的。
例如确定业务数据的哪些字段信息需要存储,以及这些信息的数据类型(数字、货币、字符串、日期等)和数据输入的校验(如字符个数、日期范围等)。
很长时间以来,关系数据库或电子表格软件是处理结构化数据的最佳工具,所以业内也有人简单地把存储在关系数据库中能用二维表格表示的数据称为结构化数据,
如来自于企业内部已经被变换成固定规则、格式的数据,而把不方便用关系数据库存取的数据称为非结构化数据,如市场比较和分析报告、股票行情等就是以非结构化的、不可预测的格式呈现的数据。
(5)非结构化数据
有些信息无法用数字或统一的结构来表示,或者说没有一个预定义的数据模型,
如文本、照片和图形图像、声音、视频、 网页、PDF文件、PowerPoint演示文稿、电子邮件、博客、Wiki和文字处理文档等,我们将其称之为非结构化数据。
(6)半结构化数据
半结构化数据介于结构化数据和非结构化数据之间,它可看作是一种结构化数据,但是缺乏严格的数据模型,
半结构化数据可通过标签或其他类型的标记识别数据中的某些元素,但半结构化数据不具有刚性结构。
XML和其他标记语言经常被用来管理半结构化数据。
例如,文字处理软件现在可以定义元数据,用于显示作者的姓名和创建日期,但数据的主体——文本文件仍然是非结构化数据。
电子邮件有发件人、收件人、日期、时间和其他标识信息,但电子邮件消息的内容和附件仍然是非结构化数据。
照片或图形图像能使用一些关键字进行标识,如创作者、日期、地点和关键字,从而能够组织和定位照片和图形图像,但图像本身是非结构化数据。
相对于非结构化数据,结构化数据往往存储于关系数据库中,可以利用关系数据库进行高效地存储和检索,
但现实中的数据并不是总能被固定的结构来描述的,生活也并不总是合适整齐的小盒子。
非结构化数据和半结构化数据是现实世界的主要数据,而且正在以惊人的速度激增,它们的增长比结构化数据的增长更快,
在大数据时代,非结构化(半结构化)数据的提取、 存储和管理是一个难点,非结构化数据能否被有效地管理和应用,这对于企业未来的发展道路影响深远。
(7)DDL
数据定义语言(Data DefinitionLanguage,DDL)是负责数据结构定义与数据库对象定义的语言。
为了设计schema,如创建数据库,创建表,这时就需要用到数据定义语言。
我们常用的有CREATE、ALTER、DROP语句。例如,
创建数据库的语句如下:CREATE DATABASE databae_name;
创建表的语句如下:CREATE TABLE table_name (id INT, name VARCHAR(10));
添加字段的语句如下: ALTER TABLE table_name ADD COLUMN column_name INT ;
删除表的语句如下:DROP TABLE table_name;
(8)DML
数据操作语言(Data ManipulationLanguage,DML)是用来查询和修改数据的语句,
包括SELECT、INSERT、UPDATE、 DELETE 4种语句,分别代表查询、插入、更新与删除,
有很多开发人员将它们称之为“CRUD”(Create、Read、Update和 Delete),对应的操作见表3-3。

3.2 数据模型
3.2.1 关系数据模型介绍
目前数据库领域使用最广泛的就是关系数据模型,业内主流的数据库产品都是建立在关系数据模型之上的,如Oracle、MS SQLServer、MySQL、PostgreSQL、DB2。
关系型数据库系统的技术发展了几十年,已经相当成熟,在数据库中也得到了高效的实现。
关系型数据库管理系统的标准语言——结构化查询语言(SQL),是一种高级的非过程化编程语言,它已经成为事实上的工业标准而被广泛使用,而且也变成了一项必须被程序员掌握的标准技能。
下面仍然以3.1节的两个表为例(见表3-4和表3-5),说明一些概念。
表3-4 员工关系表(employee) 表3-5 项目关系表(project)
从表3-4和表3-5可以看出,关系数据模型是由一系列的“关系”组成的。“关系”也就是我们所说的表(table)。
每个表也存在一个或多个属性(字段),如“员工编号”、“姓名”、“性别”。
每个字段均有对应的数据类型(type),如“整型”、“字符型”、“枚 举类型”。
关系模型建立后,就可以在这些关系(表)中插入、修改、删除、查询数据了。
1.关于NULL
如果某个字段的值是未知的或未定义的,数据库会提供一个特殊的值NULL来表示。
NULL值很特殊(NULL不等于任何值,null is null),在关系数据库中应该小心处理。
例如对表employee,运行查询语句“select*fromemployee where 绩效得分<=85 or 绩效得分>85;”可能很多人认为这样能获取所有记录,
但实际上,由于王刚和张卫的绩效得分是未知的(NULL),因此他们不会被包含在查询结果中。
2.关于key和索引
key常指表中能唯一标识一笔记录的字段(属性)或多个字段的组合。
现实中,key和索引可以简单地看作同义词,key不一定唯一标识一笔记录,本书以后的论述中会使用“索引”、“主键索引”、“唯一索引”这些术语。
我们可以通过某个记录的索引/key去查找记录。
数据库管理系统为了高效地检索记录,往往会创建各种索引结构加速检索记录,或者按照索引/key的顺序存储记录,所以基于记录的索引/key会很容易查找到记录。
关系数据库中的表之间的关联往往也是通过索引来进行关联的,
比如上面的project表,项目组成员存储的是员工编号,可以通过员工编号和另外一张员工关系表——employee表(员工编号字段上有主 键索引)进行关联。
3.2.2 实体–关系建模
由于设计人员、研发人员和最终用户看待和使用数据的方式不同,
因此可能会导致数据库的设计不能反映真实的需求,以及后期出现的扩展性问题,
为了能够更准确理地解数据的本质,理解使用这些数据的方法,我们需要有一个通用的模型,这个模型和技术实现无关。
实体关系图(ER模型)就是这样一个通用模型的例子。以下介绍ER建模的一些关键概念。
(1)ER建模
1976年Peter Chen首次提出了EntityRelationship Modeling(实体关系建模)概念,并发明了陈氏表示法(Peter Chen’s notation)。
随着问题复杂度的增加,适应范围的增广,截至今天出现了许多ER模型的表示法,如Barker ERInformation Engineering(IE)和IDEF1X或Crow’s foot表示法。
各种表示法都有它们的优缺点和适用领域,但它们都基于同样的建模概念。
ER建模是一种自上而下的数据库设计方法。
我们通过标识模型中必须要表示的重要数据(称为实体)及数据之间的关系开始ER建模,然后增加细节信息,如实体和关系所要具有的信息(称为属性)。
该方法的输出是实体类型、关系类型和约束条件 的清单。
(2)UML
UML(Unified Modeling Language,统一建模语言)是一种分析人员和开发人员广泛使用的标准建模语言,它可以以图形化的方式表示实体、关系。
UML最初用于软件设计,目前已经扩展到业务和数据库设计。
UML包括分析、实施、部署过程中指定任何事项所必需的元素和图表。
通过使用几种图表和数十种元素,UML能表达不同程度的系统抽象。
对于ER建模,我们只需 要了解常用于ER建模的一些视图和表示即可。
(3)实体
实体代表现实世界的一组对象集合,可以粗略地认为它是名词,如学生、雇员、订单、演员、电影。实体一般用矩形来表示。
(4)关系
关系指特定实体之间的关系。可以粗略地认为是动词,如公司拥有员工、演员演电影。关系用线来表示。一般为二元关系。
关系的基数指参与关系的实体数目。二元关系的基数就是我们所说的一对一、一对多、多对多。
在数据库设计中,需要选 择合适的基数表示法,如IDEF1X表示法、关系表示法或Crow’s foot表示法。
本书中的例子一般使用Crow’s foot表示法,下面简 要介绍下Crow’s foot表示法。
对于Crow’s foot表示法,实体表示为矩形框,关系表示为矩形框之间的线,线两端的形状表示关系的基数。
空心圆表示零或多,单阴影线标记表示一或多,单阴影线标记和空心圆表示零或一,双阴影线标记表示恰好为一。
许多建模工具都可以使用Crows’foot表示法,如ARIS、SystemArchitect、Visio、PowerDesigner、MySQLWorkbench等。
属性指实体或关系的特征,如实体雇员的姓名、地址、生日、身份证ID等。
如果要一起显示实体和属性,那么就把代表实体的矩形分为两部分,上半部分显示实体名,下半部分列出属性名。
在图3-1中,Artist(艺术家)实体和Song(歌曲)实体的关系是艺术家演唱歌曲。
这两个实体使用的是Crow’s foot表示法,靠近Song实体一端的符号表示“0、1或更多”,靠近Artist一端的符号表示“1且只有1 个”,所以图3-1表示一个艺术家可以演唱0首、1首或者多首歌曲。
关于ER建模更详细的信息,请阅读其他相关书籍。
3.2.3 其他数据模型
1.XML数据模型
对于结构化数据,除了关系模型,还可以使用XML数据模型存取数据。
XML(eXtensible Markup Language)是可扩展标记语言,最开始设计XML的目的是为了在Internet上交换数据。
标记是指计算机所能理解的信息符号,通过此种标记,计算机之间可以处理包含各种信息的文章等。
如何定义这些标记?既可以选择国际通用的标记语言,比如HTML,也可以使用像XML这样由相关人士自由决定的标记语言,这就是语言的可扩展性。
XML被广泛用作跨平台之间数据交互的形式,主要针对数据的内容,通过不同的格式化描述手段(XSL、CSS等)来完成 最终的形式展现(生成对应的HTML、PDF或其他的文件格式)。
常用的查询语言是XPath,即XML路径语言(XML path language),它是一种用来确定XML文档中某部分的位置的语言。
XPath基于XML的树状结构,提供在数据结构树中找寻节点的能力。
在图3-2中,一个形式良好的XML文档或XML字符串,经过CSS或XSL解析器的解析,最终生成客户端可接受的展现形式。
以下是一个XML的例子。
<?xml version="1.0" ?>
<person sex="female">
<firstname>Anna</firstname>
<lastname>Smith</lastname>
</person>
可以看到XML的格式和HTML文件比较类似,但两者也有不同之处。
XML被设计为传输和存储数据,其标签描述的是数据的内容。
HTML被设计用来显示数据,其标签是用来格式化数据的。
由于XML文件的标签描述的是数据的内容,因此XML文件可以看作“自描述”的文件。
一个形式良好的XML主要包含如下三个基本部分。
元素,如上面的person。元素允许嵌套,如person包含子元素firstname、lastname。元素有开始标签和关闭标签,如上面的<person></person>。
属性,元素还可以拥有属性,如上面例子中的sex=“female”。
文本,如上面例子中的Anna、Smith。
XML作为一项数据交换的标准被广泛使用,因此某种意义上,XML也是关系数据模型的竞争者。
表3-6对关系数据模型和 XML数据模型做了简要对比。
在下面的XML文档中,第一本书没有输入price(价格)信息,而后面的两本书添加了price信息,这种数据结构的不一致性在XML中是允许存在的,
这就意味着,可以在以后给<book>元素添加或删除子元素,因此大大增加了灵活性。
<bookstore>
<book category="COOKING">
<title lang="en">Everyday Italian</title>
<author>Giada De Laurentiis</author>
<year>2005</year>
</book>
<book category="CHILDREN">
<title lang="en">Harry Potter</title>
<author>J K. Rowling</author>
<year>2005</year>
<price>29.99</price>
</book>
<book category="WEB">
<title lang="en">Learning XML</title>
<author>Erik T. Ray</author>
<year>2003</year>
<price>39.95</price>
</book>
</bookstore>
2.JSON数据模型
JSON(JavaScript Object Notation)与XML类似,也适用于存储半结构化数据。
JSON比XML出现得更晚,不像XML那样有比较完善的工具支持,但由于JSON更简洁,更符合程序语言的数据表达方式,
因此,在互联网开发中,一般选择JSON而不是XML,JavaScript的很多工具包如jQuery、ExtJS等都大量使用了JSON。
事实上,JSON已经成为了一种前端与服务器端的数据交换格式,前端程序通过Ajax发送JSON对象到后端,
服务器端脚本对JSON进行解析,将其还原成服务器端对象,然后进行一些处理,反馈给前端的仍然是JSON对象。
尽管JSON是JavaScript的一个子集,但JSON是独立于语言的文本格式,并且采用了类似于C语言家族的一些习惯。
许多程序语言都有解析器,可用来处理JSON数据。
JSON用于描述数据结构,有如下几种存在形式。
对象:是一个无序的“‘名称/值’对”集合。一个对象以“{”(左大括号)开始,“}”(右大括号)结束。每个“名称”后跟一 个“:”(冒号);“‘名称/值’对”之间使用“,”(逗号)分隔。
数组:是值(value)的有序集合。一个数组以“[”(左中括号)开始,“]”(右中括号)结束。值之间使用“,”(逗号)分 隔。
值:可以是双引号括起来的字符串(string)、数值(number)、true、false、NULL、对象(object)或数组(array)。这些结构可以嵌套。
下面来举一个例子。
"employees":[
{"firstName":"John", "lastName":"Doe"},
{"firstName":"Anna", "lastName":"Smith"},
{"firstName":"Peter","lastName":"Jones"} ]
在上面的例子中,对象employees是包含三个对象的数组。每个对象代表一条关于某人(有姓和名)的记录。
相对于传统的关系型数据库,一些基于文档存储的NoSQL非关系型数据库则选择JSON作为其数据存储格式,比较知名的产品有:MongoDB、CouchDB、RavenDB等。
下面两个表(表3-7和表3-8)列举了其与关系数据模型和XML数据模型的不同。
虽然XML会比JSON的存储占据更多字节,但是如果不是海量数据,一般是不会出现存储和性能上的问题的。
有人可能会认为XML要比JSON的数据结构复杂得多,但如果只是用到XML的一个子集,用基本的结构,同样也是很简洁的。
那为什么有人会觉得XML复杂呢?更多的原因是XML拥有大量的特性,设定很多,深入了解需要花费很多功夫,而JSON的简单模型,很快就可以掌握了。
JSON与XML最大的不同之处在于XML是一个完整的标记语言,而JSON不是,JSON仅仅是一种表达传输数据的方式,
正如名字所言,JavaScript对象表示法(JavaScript Object Notation,JSON)是通过字符来表示一个对象的。
XML的设计理念与JSON不同。
XML利用标记语言的特性提供了绝佳的扩展性能,如果数据模型复杂多变,想要单独定义自己传输数据的模型, 那么XML将是一个很好的选择,
但是我们所使用的数据结构往往不需要变动,这时JSON更简洁,它的数据结构很像程序语言定义的数据结构,
在你预先知道JSON结构的情况下,可以写出实用美观、可读性强的代码,如果要存储或传输的数据格式出现了变化,
此时就需要重新编码来解析存储,这方面的成本往往是可以接受的。

3.3 SQL基础
SQL是一种高级查询语言,它是声明式的,也就是说,只需要描述希望怎么获取数据,而不用考虑具体的算法实现。
3.3.1 变量
MySQL里的变量可分为用户变量和系统变量。
1.用户变量
用户变量与连接有关。也就是说,一个客户端定义的变量不能被其他客户端看到或使用。
当客户端退出时,该客户端连接的所有变量将自动释放。
这点不同于在函数或存储过程中通过DECLARE语句声明的局部变量,局部变量的生存周期在它被声 明的“BEGIN…END”块内。
对于用户变量的值,可以先保存在用户变量中,然后以后再引用它;这样就可以将值从一个语句传递到另外一个语句。
用户变量的形式为@var_name。
设置用户变量的一个途径是执行SET语句,语法如下:SET @var_name= expr[, @var_name= expr] ...
对于SET,可以使用“=”或“:=”作为分配符。分配给每个变量的expr可以为整数、实数、字符串或NULL值。
如: mysql> SET @t1=0, @t2=0, @t3=0; 或:SET @minMid=(select min(id) FROM table_name) ;
2.系统变量
MySQL服务器维护着两种系统变量:全局变量影响MySQL服务的整体运行方式;会话变量影响具体客户端连接的操作。
当服务器启动时,它将所有全局变量初始化为默认值。
这些默认值可以在选项文件中或在命令行中对指定的选项进行更改。
服务器启动后,通过连接服务器并执行SET GLOBAL var_name语句,可以动态更改这些全局变量。
要想更改全局变量,必须具有SUPER权限。
服务器还为每个连接的客户端维护一系列的会话变量。
在连接时使用相应全局变量的当前值对客户端的会话变量进行初始化。
对于动态会话变量,客户端可以通过SET SESSION var_name语句更改它们。
设置会话变量不需要特殊权限,但客户端只能更改自己的会话变量,而不能更改其他客户端的会话变量。
访问全局变量的任何客户端都可以看见对全局变量所做的更改。
然而,它只影响更改后连接的客户的相应会话变量,而不会影响目前已经连接的客户端的会话变量(即使客户端执行SET GLOBAL语句也不影响)。
也就是说,如果你的连接是短连接,那么修改全局变量后,客户端有重连的操作,就会立刻影响到客户端。
而对于长连接、连接池来说,连接可能一直在 MySQL里没有被销毁,也就不会有重连的操作,所以这种情况下对全局变量的修改一般不会影响到客户端。
可以使用如下几种语法形式来设置或检索全局变量或会话变量(下面的例子使用sort_buffer_size作为示例变量名)。
要想设置一个GLOBAL变量的值,可使用下面的语法。
mysql> SET GLOBAL sort_buffer_size=value;
mysql> SET @@global.sort_buffer_size=value;
要想设置一个SESSION变量的值,可使用下面的语法。
mysql> SET SESSION sort_buffer_size=value;
mysql> SET @@session.sort_buffer_size=value;
mysql> SET sort_buffer_size=value;
如果设置变量时不指定GLOBAL、SESSION或LOCAL,则默认使用SESSION。
要想检索一个GLOBAL变量的值,可使用下面的语法。
mysql> SELECT @@global.sort_buffer_size;
mysql> SHOW GLOBAL VARIABLES LIKE 'sort_buffer_size';
要想检索一个SESSION变量的值,可使用下面的语法。
mysql> SELECT @@sort_buffer_size;
mysql> SELECT @@session.sort_buffer_size;
mysql> SHOW VARIABLES LIKE 'sort_buffer_size';
当用SELECT@@var_name搜索一个变量时(也就是说,不指定GLOBAL、SESSION),MySQL会返回SESSION值(如果存在SESSION变量的话),否则返回GLOBAL值。
对于SHOW VARIABLES,如果不指定GLOBAL、SESSION的话,MySQL会返回SESSION值。
3.3.2 保留字
MySQL显式保留了表3-9(摘自官方文档)中的关键字。
其中大多数关键字被标准SQL用作列名和/或表名(例如 GROUP)。
少数被保留了,因为MySQL需要它们。
在生产环境下,常犯的一个错误是,使用了MySQL保留的关键字作表名、 列名,这会导致部署、升级失败或留下隐患。
3.3.3 MySQL注释
MySQL服务器支持如下3种注释风格。
从“#”字符至行尾。
从“--”序列到行尾。请注意,“--”(双破折号)注释风格要求第2个破折号的后面至少要跟一个空格符(例如空格、tab、换行符等)。
之所以要求使用空格,是为了防止出现非预期结果。
比如,对于语句“UPDATE account SET credit=credit--1”,则是表示credit的值减去-1,这样的语法是合格的,而不会误认为“--1”是注释。
说明:建议不要使用“--”这样的方式,生产环境可能由于忘记在“--”后面加空格从而导致误操作。
/*序列到后面的*/序列。结束序列不一定在同一行中,因此该语法允许注释跨越多行。
下面的例子显示了3种风格的注释。
mysql> SELECT 1+1; # This comment continues to the end of line
mysql> SELECT 1+1; -- This comment continues to the end of line
mysql> SELECT 1 /* this is an in-line comment */ + 1;
mysql> SELECT 1+
/*this is a
multiple-line comment
*/
1;
MySQL对标准SQL进行了扩展,如果使用了它们,将无法把代码移植到其他数据库的服务器上。可以用“/*…*/”注释掉这些扩展。
如下例子中,MySQL服务器能够解析并执行注释中的代码,就像对待其他SQL语句一样,但其他数据库服务器将忽略这些扩展。
SELECT /*! STRAIGHT_JOIN */ col_name FROM table1,table2 WHERE ...
如果在字符“!”后添加了版本号,那么仅当MySQL的版本等于或高于指定的版本号时才会执行注释中的语法,比如下面这条 语句。
CREATE /*!32302 TEMPORARY */ TABLE t (a INT);
这就意味着,如果你的版本号为3.23.02或更高,那么MySQL服务器将使用TEMPORARY关键字。

3.3.4 数据类型
MySQL支持常用的数据类型:数值类型、日期/时间类型和字符串(字符)类型。
1.数值类型
数值类型可分为两类:整型和实数。
对于实数,MySQL支持确切精度的值(定点数)和近似精度的值(浮点数)。
确切精度的数值类型有DECIMAL类型,近似精度的数值类型有单精度(FLOAT)或双精度(DOUBLE)两种类型。
(1)整型
整型包括TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT,表3-10展示了各种整型的空间占用及表示的数值范围。
在表3-10中,无符号(unsigned)属性可扩展一倍的最大值上限。
注意:MySQL整型可设置一个“width”属性,这点很容易让人混淆。
实际上,这不是一个精度,只是告诉客户端工具显示多少个字符而已,如INT(11):11表示的不是数值范围,只是显示宽度(告诉交互式工具显示宽度,如MySQL客户端)。
由于MySQL的内部类型只支持到秒级别的精度,因此可以用BIGINT来存储精度到毫秒的时间戳。
开发数据库应用的时候,需要注意的是,应保留足够的范围来满足未来的数据增长需要,对于超过数值范围的插入/修改数据,MySQL将报错失败,
例如:对于SMALLINT类型,值的范围为-32768~32767,那么在自增ID列(后文会详述)的值已经到了 32767后,还继续插入记录,
就会报错“Duplicateentry'32767'for key'PRIMARY”,而且更新的值超过最高阈值时也会报错,如“Out ofrange valueforcolumn'id'at row1”。
可以设置unsigned属性来扩展数据范围。
(2)DECIMAL和NUMERIC类型(定点数)
定点数也就是DECIMAL型,指的是数据的小数点的位置是固定不变的。也就是说,小数点后面的位数是固定的。
DECIMAL和NUMERIC在MySQL中被视为相同的类型。它们用于保存必须为确切精度的值,例如货币数据。
当声明该类型的列时,可以(并且通常要)指定精度和标度;
比如,在DECIMAL(M,D)中,M是精度,表示数据的总长度,也就是十进制数字的位数,不包括小数点;D是标度,表示小数点后面的数字位数。
在MySQL 5.1中,M的范围是1~65,D的范围是0~30且不能大于M。
例如下面这条语句,5是精度,2是标度。 salary DECIMAL(5,2)
对于数值123456789.12345,可以这样定义,M=14,D=5。
在MySQL 5.1中以二进制格式保存DECIMAL和NUMERIC的值。
如果值太大超出了BIGINT的范围,也可以用DECIMAL存储整型。
定点数表达法的缺点在于其形式过于僵硬,固定的小数点位置决定了固定位数的整数部分和小数部分,不利于同时表达特别大的数或特别小的数。
(3)FLOAT和DOUBLE类型(浮点数)
浮点数(floating-point number)是属于有理数中某个特定子集的数的表示法,在计算机中用于近似地表示任意某个实数。
具体来说,这个实数是由一个整数或定点数(即尾数)乘以某个基数(计算机中通常是2)的整数次幂(指数)得到的,这种表示方法类似于基数为10的科学记数法。
比如123.45可以用十进制科学计数法表达为“1.2345×102”,其中1.2345为尾数,10为基数,2为指数。
浮点数利用指数达到了浮动小数点的效果,从而可以灵活地表达更大范围的实数。
在MySQL中,对于浮点列类型,单精度值(FLOAT)使用4个字节,双精度值(DOUBLE)使用8个字节。
(系统存储空间下)浮点数可以比整型、定点数表示更大的数值范围。 10^65 VS 2^64 ???肯定定点数表示的数值范围更大
为了保证最大可能的可移植性,对于使用近似数值存储的代码,应使用FLOAT或DOUBLE来表示,不规定精度或位数。
由于浮点数存在误差问题,如果用到浮点数,要特别注意误差的问题,并尽量避免做浮点数比较。
MySQL允许使用非标准语法:FLOAT(M,D)或DOUBLE(M,D)。
这里,“(M,D)”表示该值一共显示了M位整数,其中D位整数位于小数点后面。
例如,定义为FLOAT(7,4)的一个列可以显示为-999.9999。
MySQL保存值时会进行四舍五入,因此如果在 FLOAT(7,4)列内插入999.00009,近似结果是999.0001。
浮点型(FLOAT/DOUBLE)对比定点类型(DECIMAL)使用的空间更少,所以为了减少存储空间,应尽量不要使用 DECIMAL,除非是在保存确切精度的值时,比如货币数据。
2.日期/时间类型
表示时间值的日期和时间类型有DATETIME、DATE、TIMESTAMP、TIME和YEAR等。
每个时间类型都有一个有效值范围,TIMESTAMP类型有其特有的自动更新特性。
如果试图插入一个不合法的日期,MySQL将给出警告或错误。
可以使用ALLOW_INVALID_DATES SQL模式让MySQL接受某些日期,例如'1999-11-31'。
在这种模式下,MySQL只验证月的范围是否为从0到12,日的范围是否为从0到31。
有时应用程序希望保存一个特定的不合法日期,以便将来进行处理,这时可以利用这个模式。
但更常见的处理方式是设置一个不可能的特定的合法日期值,如'9999-01-01 00:00:00'。
如果是没有使用NO_ZERO_DATE的SQL模式,默认情况下,MySQL只允许在DATE或DATETIME列保存月和日是零的日期。
这在应用程序中需要保存一个你不知道确切日期的生日时非常有用,在这种情况下,只需要将日期保存为'1999-00- 00'或'1999-01-00'即可。
如果不使用NO_ZERO_DATE SQL模式,MySQL还允许将'0000-00-00'保存为“伪日期”。
这在某些情况下比使用NULL值更方便,并且数据和索引占用的空间更小。
MySQL以标准输出格式检索给定日期或时间类型的值,但它会尽力解释你指定的各种输入值格式。
尽管MySQL在尝试解释几种格式的值时,日期总是以“年–月–日”的顺序(例如,'98-09-04')来处理的,而不是以“月–日–年”或“日–月–年”的顺序 (例如,'09-04-98'、'04-09-98')。
包含两位年值的日期会令人产生困惑,因为不知道世纪。
MySQL使用以下规则解释两位年值的日期。
70~99范围的年值均转换为1970~1999。
00~69范围的年值均转换为2000~2069。
(1)DATETIME、DATE和TIMESTAMP类型
当需要同时包含日期和时间信息的值时,建议使用DATETIME(日期时间组合)类型。
MySQL以'YYYY-MM-DD HH:MM:SS'的格式检索和显示DATETIME值,但允许使用字符串或数字为DATETIME列分配值。
支持的范围为'1000-01- 0100:00:00'到'9999-12-3123:59:59'。
DATETIME类型占8个字节。
当只需要日期值而不需要时间部分时,建议使用DATE(日期)类型。
MySQL用'YYYY-MM-DD'格式检索和显示DATE值, 但允许使用字符串或数字为DATE列分配值。
支持的范围是'1000-01-01'到'9999-12-31'。
DATE类型占3个字节。
TIMESTAMP(时间戳)列用于在进行INSERT或UPDATE操作时记录日期和时间。
TIMESTAMP列的显示格式与DATETIME 列相同。
换句话说,显示宽度固定在19个字符,并且格式为'YYYY-MM-DDHH:MM:SS'。
TIMESTAMP的范围是从'1970-01- 0100:00:01'UTC到'2038-01-09 03:14:07'UTC。
TIMESTAMP类型占4个字节。
TIMESTAMP的值以UTC格式进行保存,存储时会对当前的时区进行转换,检索时再转换回当前的时区。
当前时区对应的 是time_zone系统变量。
控制TIMESTAMP列的初始化和更新的规则如下:
若将TIMESTAMP类型字段定义为default current_timestamp,那么插入一条记录时,该TIMESTAMP字段自动被赋值为当前时间。
若将TIMESTAMP类型字段定义为on updatec urrent_timestamp,那么修改一条记录时,该TIMESTAMP字段自动被修改为当前时间。
可以将这些类型联合使用,如default current_timestamp on update current_timestamp。
可以给TIMESTAMP字段指定一个默认值,也可以在SQL语句中指定TIMESTAMP字段的值。
可以使用任何常见格式指定DATETIME、DATE和TIMESTAMP的值。
对于'YYYY-MM-DD HH:MM:SS'或'YY-MM-DD HH:MM:SS'格式的字符串,允许“不严格”语法:任何标点符号都可以用作日期部分或时间部分之间的间隔符。
例如,'98-12-3111:30:45'、'98.12.3111+30+45'、'98/12/3111*30*45'和'98@12@3111^30^45'是 等价的。
对于'YYYY-MM-DD'或'YY-MM-DD'格式的字符串,也允许使用“不严格的”语法。例如,'98-12-31'、'98.12.31'、 '98/12/31'和'98@12@31'是等价的。
(2)TIME(时间)类型
该时间类型的范围是'-838:59:59'到'838:59:59'。
MySQL以'HH:MM:SS'格式检索和显示TIME值(或者对于大的小时值采 用'HHH:MM:SS'格式),但允许使用字符串或数字为TIME列分配值。
TIME类型占3个字节。
(3)YEAR(两位或四位格式的年)类型
YEAR类型表示两位或四位格式的年。
MySQL以YYYY格式显示YEAR值,但允许使用字符串或数字为YEAR列分配值。
默认是四位格式,在四位格式中,允许的值是1901~2155和0000。
在两位格式中,如果是两位字符串,那么范围为'00'~'99'。
'00'~'69'和'70'~'99'范围的值被分别转换为2000~2069和1970~1999 范围的YEAR值。
如果是两位整数,范围为1~99。1~69和70~99范围的值被分别转换为2001~2069和1970~1999范围的YEAR值。
请注意,两位整数范围与两位字符串范围稍有不同,因为你不能直接将零指定为数字并将它解释为2000。
你必须将它指定为一个字符串'0'或'00',或者它被解释为0000。
YEAR类型占1个字节。
3.字符串类型
字符串类型指CHAR、VARCHAR、BINARY、VARBINARY、BLOB、TEXT、ENUM和SET。
(1)CHAR和VARCHAR类型
CHAR与VARCHAR类型类似,但它们保存和检索数据的方式不同。
CHAR和VARCHAR类型声明的长度表示你想要保存的最大字符数。例如,CHAR(30)可以占用30个字符。
注意,在 CHAR(M)、VARCHAR(M)声明里,M是字符个数而不是字节。
如果分配给CHAR或VARCHAR列的值超过了列的最大长度,则对值进行裁剪以使其长度适合。
如果被裁剪掉的字符不是空格,则会产生一条警告。
CHAR是固定长度的字符串,它的长度固定为创建表时声明的长度。长度范围为0到255个字符。
当保存CHAR值时,在它们的右边填充空格以达到指定的长度。
当检索到CHAR值时,尾部的空格会被删除掉,这是MySQL服务器级别控制的,和存储引擎无关。
CHAR类型适合存储大部分值的长度都差不多的数据,例如MD5值。
VARCHAR列中的值为可变长度的字符串。
长度可以指定为0到65535之间的值(VARCHAR的最大有效长度由最大记录长度和使用的字符集确定。整体最大长度是65532字节)。
相对于固定长度的字符串,它需要更少的存储空间。
在保存VARCHAR 的值时,只保存需要的字符数,然后用1~2个字节来存储值的长度,所以如果是很短的值(如仅一个字符),那么耗费的存储空间比CHAR还会多些,
所以,如果想存储很短的类型,使用CHAR会更合适。
VARCHAR可选的一种场景是最长记录的长度值比平均长度的值大得多。
保存VARCHAR的值时不会进行填充。
当值保存和检索时尾部的空格仍会保留,这一点符合标准SQL。
实际上,各存储引擎存取VARCHAR和CHAR的方法不尽相同。
比如,内存引擎使用固定长度的行,会在内存中分配最大可能空间给VARCHAR类型,所以CHAR(5)和VARCHAR(200)在存储“hello”字符串时占据的空间大小是一样的,但 VARCHAR(200)会耗费更大的内存空间。
(2)BINARY和VARBINARY类型
BINARY和VARBINARY类似于CHAR和VARCHAR,不同的是,它们包含的是二进制字符串而不是非二进制字符串。
也就是说,它们包含的是字节字符串而不是字符字符串,它们的长度是字节长度而不是字符长度。
这说明它们没有字符集,并且排序和比较也是基于字节的二进制值进行的。
相对来说,二进制字符串的比较比字符字符串的比较更为简单有效。
对于“随机”字符串,如MD5()、SHA1()或UUID()生成的值会导致数据非常分散,没有明显的热点数据,还可能导致数据库缓存不能很好的工作。
因此建议把MD5()、UUID()之类的值再散列下,生成整型值。
(3)BLOB和TEXT类型
BLOB是一个二进制大对象,可以容纳可变数量的数据。
BLOB类型共有4种:TINYBLOB、BLOB、MEDIUMBLOB和 LONGBLOB。
BLOB是SMALLBLOB的同义词。
TEXT类型也有4种:TINYTEXT、TEXT、MEDIUMTEXT和LONGTEXT。
它们分别对应上面的4种BLOB类型,有相同的最大长度和存储需求。
TEXT是SMALLTEXT的同义词。
BLOB用于存储二进制字符串(字节字符串),而TEXT列则被视为非二进制字符串(字符字符串)的存储方式,它是有字符集和排序规则的,
这两种类型都用于存储大量数据,具体的存储方式按存储引擎各有不同。
在大多数情况下,可以将BLOB列视为能够存储足够大数据的VARBINARY列。
同样,也可以将TEXT列视为VARCHAR 列。
但是,BLOB和TEXT在以下几个方面不同于VARBINARY和VARCHAR。
保存或检索BLOB和TEXT列的值时不用删除尾部的空格。
对于BLOB和TEXT列的索引,必须指定索引前缀的长度。
BLOB和TEXT列不能有默认值。
排序时只使用该列的前max_sort_length个字节。max_sort_length的默认值是1024。
BLOB或TEXT对象的最大长度由其类型来确定,但在客户端和服务器之间实际可以传递的最大数据量是由可用内存数量和通信缓存区的大小来确定的。
可以通过更改max_allowed_packet变量的值更改消息缓存区的大小,但必须同时修改服务器和客户端的程序。
使用BLOB、TEXT等大字段可能会导致严重的性能问题,比如导致产生磁盘临时表。
MySQL的临时表分为“内存临时表”和“磁盘临时表”,其中内存临时表使用MySQL的MEMORY存储引擎,磁盘临时表使用MySQL的MyISAM存储引擎。
由于MEMORY存储引擎不支持BLOB和TEXT类型,所以如果有查询使用了BLOB或TEXT列且需要隐式使用临时表(MEMORY存储引擎)来进行排序,
那么将不得不使用磁盘临时表,磁盘比内存慢得多,这会导致很严重的性能问题。
(4)ENUM类型
ENUM(枚举)类型是一个字符串对象,其值通常选自一个允许值列表,该列表是在创建表时被定义的。
对于ENUM类型,需要慎重使用,如果候选值的集合可能发生改变,那么使用它就不见得是一个好主意。
对于一些属性有固定数量的候选值的场景,可以使用其他更通用的方案,这样也能更方便地迁移到其他数据库,
如用TINIINT类型代替ENUM 类型,可以靠应用程序去维护字符串值和TINIINT的映射关系,或者增加一个表来存储映射关系,或者就直接存储更“自然”的字符串值。
在现实世界中,空间大小一般已经不再是一个问题,自然、直观往往是更值得考虑的因素。
这里省略了对SET类型的介绍,感兴趣的读者可自行查阅相关图书。
4.数据类型存储需求
常用数值类型的存储需求见表3-11。
日期和时间类型的存储需求见表3-12。
字符串类型的存储需求见表3-13,其中的“L”代表字符串的字节长度。
要想计算用于保存具体CHAR、VARCHAR或TEXT列值的字节数,需要考虑该列使用的字符集。
例如utf8字符集,存储汉字是3个字节,存储英文字符是1个字节。
以上VARCHAR、VARBINARY、BLOB和TEXT类型都是可变长度的类型,它们的存储需求取决于如下3个因素:
列值的实际长度,列的最大可能长度(如行长度有65536个字节的限制)字符集。
例如,一个VARCHAR(255)列可以容纳一个最大长度为255个字符的字符串。
如果该列使用latin1字符集(每个字符占一个字节),那么所需的实际存储为字符串字节的长度(L),再加上一个字节以记录字符串的长度。
对于字符串'ABCD',L等于 4,存储需求是5个字节。
如果该列使用UCS2双字节字符集,那么存储要求为10个字节:'ABCD'的长度为8个字节,再需要2个字节来存储长度,因为它的最大长度大于255个字节(此时VARCHAR(255)最多为510个字节)。
可以存储在VARCHAR或VARBINARY列的字节还受到最大行长(65535字节)的限制。
很显然,对于VARCHAR列,如果 存储多字节字符,实际能够存储的字符会更少。
例如,utf8字符集每个字符最多三个字节,所以使用utf8字符集的VARCHAR列可以被声明为最多21844个字符。
5.选择合适的数据类型
MySQL支持许多数据类型,选择合适的数据类型可以获得更好的性能,从而更节省空间。
以下是一些指导原则。
(1)各表使用一致的数据类型
字段在每个表中都应该使用一样的数据类型、长度,因为以后可能需要进行JOIN(连接)操作,这样做是为了避免无谓的转换或可能出现不期望的结果。
我们不仅要考虑数据类型是如何存储的,也要清楚数据类型是如何计算和比较的。
(2)小往往更好
选择更短的数据类型。更短的类型意味着更少的磁盘空间、更少的内存空间、更少的CPU处理时间。
例如,如果列值的范围为从1~99999,若使用整数,则MEDIUMINTUNSIGNED是比较好的数据类型。
在所有可以表示该列值的类型中,该类型使用的存储最少。
(3)简单类型更好
简单的数据类型能够进行更快的处理。例如,整型值比字符类型运算得更快,因为字符的字符集和排序规则使字符的比较运算变得更为复杂。
生产环境中经常会看到用字符或整型来存储时间,为了使数据更友好、自然,建议还是使用MySQL内建的类型来存储日志时间会更好。
使用无符号整型来存储IP地址(IP本质上是一个无符号的整型,点分的形式只是为了方便我们阅读)也是常用的好办法,可用INET_ATON()和INET_NTOA执行转换。
(4)尽可能避免NULL值
应尽量显式定义“not NULL”,如果查询涉及的是NULL值的字段,MySQL会很难去优化查询。
可使用0、空字符串或特殊的值来代替NULL存储。
当然,也不要去刻意追求“not NULL”,因为更改NULL字段为“not NULL”,对性能的提升可能没什么太大的作用,让设计更自然、更具可理解性应该更值得看重。
熟悉Oracle数据库的读者需要留意,MySQL会索引NULL值,而Oracle则不会。

3.3.5 函数
以下介绍常用的函数和操作符。
1.数值函数
(1)算数操作符
可使用常见的算数操作符。例如‘+’、‘-’、‘*’、‘/’、DIV(整除)。
(2)数学函数
ABS(X):X的绝对值。
CEIL(X):返回不小于X的最小整数值。
FLOOR(X):返回不大于X的最大整数值。
CRC32(X):计算循环冗余码校验值并返回一个32比特无符号值。
RAND()、RAND(N):返回一个随机浮点值v,范围在0到1之间(即其范围为0≤v≤1.0)。若已指定一个整数参数N,则它 被用作种子值,用来产生重复序列。
注意不要使用此函数做随机排序,如下的语句形式效率会很差,仅适合很小的表。 SELECT * FROM table_name ORDER BY RAND() LIMIT 1;
SIGN(X):返回X的符号。
TRUNCATE(X,D):返回被舍去至小数点后D位的数字X。若D的值为0,则结果不带有小数点或不带有小数部分。
ROUND(X)、ROUND(X,D):返回参数X,其值接近于最近似的整数。在有两个参数的情况下,返回X,其值保留到小数点后D位,而第D位的保留方式为四舍五入。
若要保留X值到小数点左边的D位,可将D设为负值,例如,ROUND(123.45,-1) 的结果是120,ROUND(167.8,-2)的结果是200。
2.字符类型处理函数
CHAR_LENGTH(str):返回值为字符串str的长度,长度的单位为字符。一个多字节字符算作一个单字符。对于一个包含5 个二字节的字符集,LENGTH()的返回值为10,而CHAR_LENGTH()的返回值为5。
LENGTH(str):返回值为字符串str的长度,单位为字节。
CONCAT(str1,str2,...):返回结果为连接参数产生的字符串。如下查询将拼接'My'、'S'、'QL'3个字符串。 mysql> SELECT CONCAT('My', 'S', 'QL'); -> 'MySQL'
LEFT(str,len):从字符串str开始,返回最左len个字符。
RIGHT(str,len):从字符串str开始,返回最右len个字符。
SUBSTRING(str,pos)、SUBSTRING(str,pos,len):不带有len参数的格式是从字符串str返回一个子字符串,起始于位置pos。
带有len参数的格式是从字符串str返回一个长度同len字符相同的子字符串,起始于位置pos。
如下查询将返回字符串Quadratically第5个字符之后的所有字符。 mysql> SELECT SUBSTRING('Quadratically',5); -> 'ratically'
如下查询将返回字符串Quadratically第5个字符之后的6个字符。 mysql> SELECT SUBSTRING('Quadratically',5,6); -> 'ratica'
LOWER(str):返回字符串str转化为小写字母的字符。
UPPER(str):返回字符串str转化为大写字母的字符。
3.日期和时间函数
NOW():返回当前日期和时间的值,其格式为'YYYY-MM-DDHH:MM:SS'或YYYYMMDDHHMMSS。
CURTIME():将当前时间以'HH:MM:SS'或HHMMSS的格式返回。
CURDATE():将当前日期按照'YYYY-MM-DD'或YYYYMMDD格式的值返回。
DATEDIFF(expr1,expr2):是返回开始日期expr1与结束日期expr2之间相差的天数,计算中只用到这些值的日期部分。返回值为正数或负数。
DATE_ADD(date,INTERVAL expr type)、DATE_SUB(date,INTERVAL expr type):这些函数执行日期运算。
date是一个 DATETIME或DATE值,用来指定起始时间。
expr是一个表达式,用来指定从起始日期添加或减去的时间间隔值。
type为关键词,它指示了表达式被解释的方式。type常用的值有SECOND、MINUTE、HOUR、DAY、WEEK、MONTH、YEAR。
示例代码 如下所示。
mysql> SELECT DATE_ADD('1997-12-31 23:59:59',INTERVAL 1 SECOND); -> '1998-01-01 00:00:00'
mysql> SELECT DATE_ADD('1997-12-31 23:59:59',INTERVAL 1 DAY); -> '1998-01-01 23:59:59'
DATE_FORMAT(date,format):下面的代码会根据format字符串安排date值的格式。
常用的日期格式'YYYY-MM-DD HH:MM:SS',对应的format为'%Y-%m-%d %H:%i:%S',示例代码如下所示。
mysql>SELECT DATE_FORMAT('1997-10-04 22:23:00', '%H:%i:%s'); ->'22:23:00'
STR_TO_DATE(str,format):是DATE_FORMAT()函数的倒转。它将获取一个字符串str和一个格式字符串format。
若格式字符串包含日期和时间部分,则STR_TO_DATE()返回一个DATETIME值,若该字符串只包含日期部分或只包含时间部分,则返回一个DATE或TIME值。示例代码如下所示。
mysql> SELECT STR_TO_DATE('04/31/2004', '%m/%d/%Y'); ->2004-04-31
3.3.6 操作符及优先级
运算符的优先级决定了不同的运算符在表达式中计算的先后顺序。
一般情况下,级别高的运算符先进行计算,如果级别相同,MySQL则会按照表达式的顺序从左到右依次计算。
以下是按照从低到高的优先级列出的各种运算操作符。
:=
||、OR、XOR
&&、AND
NOT
BETWEEN、CASE、WHEN、THEN、ELSE
=、<=>、>=、>、<=、<、<>、!=、IS、LIKE、REGEXP、IN
|
&
<<、>>
-、+
*、/(DIV)、%(MOD)
^(按位异或)
-(负号)、~(按位取反)
!
如果不能确定优先级,可以使用圆括号()来改变优先级,并且这样会使计算过程更加清晰。
比如,如下的查询,我们会先计算最里层括号里面的表达式(2+3),然后计算外层的表达式。这点类似于我们学过的算术运算。
select 1*(3-2)*(3+3+3*(2+3));
3.3.7 MySQL示例employees数据库
MySQL提供了一个练习用的示范数据库employees。
可以从Employees DB on Launchpad(https://launchpad.net/test-db/ 中下载),在页面右侧选择“Latest version is 1.0.6”,建议下载“employees_db-full-1.0.6”。
employees示例数据库一共有6张表,约400万条记录,包含160MB的数据。
首先是安装数据库,安装命令如下。
cd tmp
wget https://launchpad.net/test-db/employees-db-1/1.0.6/+download/employees_db-full-1.0.6.tar.bz2
tar jxf employees_db-full-1.0.6.tar.bz2
cd employees_db
默认导入数据是InnoDB引擎,如果需要指定其他引擎,可以修改employees.sql文件,取消注释相应的引擎,命令如下:
set storage_engine = InnoDB;
-- set storage_engine = MyISAM;
-- set storage_engine = Falcon;
-- set storage_engine = PBXT;
-- set storage_engine = Maria;
使用MySQL命令将数据导入到实例中。 mysql -t < employees.sql
通过以下命令验证范例数据导入是否正确。 time mysql -t < test_employees_sha.sql
实体关系图3-3描述了employees示例数据库各表的结构和它们之间的关系。
图3-3中的各表通过一些字段相互关联,
如dept_emp表中存储了部门职员的信息,通过dept_emp.emp_no可以到employees表中去查询职员的记录。
各表之间的关系是通过连线和特殊符号标明的,钥匙标记表示这是主键,关系中属于“多”的这一边用一 个类似鸟爪的图形来表示,
如dept_emp表,主键是联合主键(emp_no,dept_no),employees和dept_emp表就是一对多的关系,
由于职员可能在不同时期属于不同的部门,那么employees表中一条职员的记录可能在dept_emp表中存在多条对应记录。
读者可自行下载此数据库,验证各表的数据和彼此之间的联系。

3.3.8 SQL语法
结构化查询语言(Structured QueryLanguage,SQL)是一种高级编程语言,是数据库中的标准数据查询语言,这种语言是描述性的,很容易上手,
你不需要了解数据是如何存储的也能编写出语句查询和修改数据,这项技能并非IT人士的专有领域,其他非计算机行业的人,虽然不会编程,
但也可以根据自己的业务需求,用SQL在公司的数据平台上查询数据。
所以,我们设计的库表,如果有其他业务部门要使用,而且是通过SQL的方式进行查询,那么表名、列名就需要考虑下自然性和易用性。
美国国家标准学会(ANSI)对SQL进行规范后,将其作为关系式数据库管理系统的标准语言,而后在国际标准组织的支持下成为了国际标准。
不过各种通行的数据库系统在其实践过程中都对SQL规范作了某些改编和扩充。
所以,实际上不同数据库系统之间的SQL并不能完全相互通用。
一般情况下,扩展语法后功能虽有所增强,但可能会导致移植性变差,而且对于整个系 统的吞吐率、性能可能也不会有明显的改善。
因此本书不会对MySQL的扩展语法进行详细介绍。
下面使用自带的MySQL命令行工具来演示示例。
1.SQL常见操作
使用如下命令可查看MySQL支持的选项。 shell> mysql – help
使用如下命令可连接MySQL Server。 shell> mysql -h host -P port -u user – p****
连接登录成功后,可以按Ctrl+D退出,或者输入QUIT退出,命令如下。 mysql> QUIT
下面来运行一个简单的查询,通过以下语句可查询当前MySQL Server的版本和当前日期。 mysql> SELECT VERSION(), CURRENT_DATE;
使用如下命令可创建数据库employees。 mysql> CREATE DATABASE employees;
显示数据库,切换到test数据库时可使用如下命令。 mysql> SHOW DATABASES; mysql> USE test
使用如下命令可显示当前的数据库。 SELECT DATABASE();
使用如下命令可显示数据库下的表。 mysql> SHOW TABLES
2.数据定义语句(DDL)
以下介绍常用的DDL语句。
(1)创建和删除表
可使用CREATE TABLE语句创建表。
CREATE TABLE employees_2 (
emp_no int(11) NOT NULL,
birth_date date NOT NULL,
first_name varchar(14) NOT NULL,
last_name varchar(16) NOT NULL,
gender enum('M','F') NOT NULL,
hire_date date NOT NULL,
primary key (emp_no)
) engine=innodb default charset=latin1
可使用DESC语句验证创建的表结构。 mysql> DESC employees_2
使用DROP TABLE语句删除表。 DROP TABLE employees_2;
(2)使用ALTERTABLE语句修改表结构
首先创建表t1。 mysql> CREATE TABLE t1 (a INTEGER,b CHAR(10));
把表t1重新命名为t2。 mysql> ALTER TABLE t1 RENAME t2;
把列a从INTERGER类型更改为TINYINT NOT NULL(名称保持不变),并把列b从CHAR(10)更改为CHAR(20),同时把列b 重新命名为列c。
mysql> ALTER TABLE t2 MODIFY a TINYINT NOT NULL, CHANGE b c CHAR(20);
添加一个新的TIMESTAMP列,名称为d。 mysql> ALTER TABLE t2 ADD d TIMESTAMP;
在列d和列a中添加索引。 mysql> ALTER TABLE t2 ADD INDEX (d), ADD INDEX (a);
删除列c。 mysql> ALTER TABLE t2 DROP COLUMN c;
添加一个新的AUTO_INCREMENT整数列,名称为c。 mysql> ALTER TABLE t2 ADD c INT UNSIGNED NOT NULL AUTO_INCREMENT, ADD PRIMARY KEY (c);
(3)使用CREATE INDEX语句创建索引
在表lookup的列id上创建索引。 CREATE INDEX id_index ON lookup (id);
在customer表的name列上创建一个索引,索引使用name列的前10个字符。 CREATE INDEX part_of_name ON customer (name(10));
在tbl_name表的a、b、c列上创建一个复合索引。 CREATE INDEX idx_a_b _c ON tbl_name(a,b,c);
(4)使用DROP INDEX语句删除索引
删除表tbl_name上的index_name索引时使用如下命令。 DROP INDEX index_name ON tbl_name;
(5)修改字符集和排序规则
可使用如下命令更改排序字符集。 ALTER TABLE test.tt1 CHANGE v2 v2 VARCHAR(10) CHARACTER SET utf8 COLLATE utf8_general_ci;
可使用如下命令更改排序规则。 ALTER TABLE table_name CHANGE col_a col_a VARCHAR(50) CHARACTER SET latin1 COLLATE latin1_bin

3.数据操作语句(DML)
以下是一些查询语句常用的语法和示例。
需要留意的是,我们日常所说的查询语句,不仅包括SELECT查询语句,也包括 INSERT、UPDATE、DELETE等修改数据的语句。
在创建表之后,就可以导入数据了,导入数据的方式有二:或采用LOAD DATA语句,或采用INSERT语句。
以下主要介绍INSERT、SELECT、UPDATE、DELETE语句,LOADDATA语句在以后的章节中会有介绍。
(1)INSERT语句
语法如下所示。 INSERT INTO table_name (column1, column2....) VALUES (value1, value2...);
具体示例如下。 INSERT INTO employees.employees (emp_no, birth_date, first_name, last_name, gender, hire_date) VALUES ('111111', '1976-11- 11', 'Gary', 'Chen', 'M', '1998-08-20');
MySQL支持用一条INSERT语句插入多条记录,这样可以加快数据的导入,比如下面的示例。
INSERT INTO employees.employees (emp_no, birth_date, first_name, last_name, gender, hire_date) VALUES
('111112', '1977-11-11', 'hua', 'chen', 'M', '2000-02-02'),
('111113', '1988-12-02', 'feng', 'yu', 'M', '2013-12-20'),
('111114', '1993-02-01', 'yong', 'chen', 'M', '2010-10-01');
(2)修改数据(UPDATE)
语法如下所示。 UPDATE table_name SET column_name1 = value1,column_name2 = value2,column_name3 = value3 ... [WHERE conditions];
以下命令可将员工编号为10001的员工姓名修改为garywang。 UPDATE employees set first_name='gary',last_name='wang' where emp_no=10001;
(3)删除(DELETE)
语法如下所示。 DELETE FROM table_name [WHERE conditions];
以下命令可删除员工编号为1000000的员工记录。 DELETE from employees WHERE emp_no = 1000000;
(4)SELECT语句
语法如下所示。 SELECT column_names FROM table_name [WHERE ...conditions];
可使用如下命令查询表employees的所有数据。 SELECT * FROM employees;
可使用如下命令查询表employees的emp_no、birth_date、first_name、last_name这几个特定列的数据。 SELECT emp_no,birth_date,first_name,last_name FROM employees;
查询employees表中出生日期晚于'1960-01-01'的员工,使用WHERE子句,加上比较操作符“>”即可,命令如下。SELECT emp_no,birth_date,first_name,last_name FROM employees WHERE birth_date > '1960-01-01';
可使用如下命令查询employees表中出生日期早于'1960-01-01'的员工。 SELECT emp_no,birth_date,first_name,last_name FROM employees WHERE birth_date < '1960-01-01';
可使用如下命令查询employees表中first_name等于Divier的员工。 SELECT * FROM employees WHERE first_name='Divier';
SELECT查询是需要我们重点掌握的,下文将详细讨论SELECT查询。
(1)SQL模式匹配
SQL有两个通配符,“-”匹配任意单个字符,“%”匹配任意多个字符(包括0个字符)。
模式匹配默认是区分大小写的,它一般使用LIKE或NOT LIKE这些比较操作符,
比如,要查询employees表中first_name列以 字母D开始的员工记录,可使用如下命令。
SELECT * FROM employees WHERE first_name LIKE 'D%';
查询employees表中first_name列以Ang开头,一共5个字符,last_name以Con开头,一共5个字符的记录时可使用如下命令。
mysql> SELECT emp_no,first_name,last_name,birth_date FROM employees WHERE first_name LIKE 'Ang__' and last_name LIKE 'Con__';
(2)逻辑操作符与或非(AND、OR、NOT)
可以用逻辑操作符组合成多个筛选条件,示例如下。
选择employees表first_name列等于Parto,而且last_name列等于Alpay的记录。
SELECT emp_no,birth_date,first_name,last_name,gender,hire_date FROM employees WHERE first_name='Parto' AND last_name='Alpay';
选择employees表中'1995-01-31'或'1996-11-21'入职的员工。
SELECT emp_no,birth_date,first_name,last_name,gender,hire_date FROM employees WHERE hire_date='1995-01-31' OR hire_date='1996-11-21';
选择employees表中last_name列不是以字母A开头的所有记录。
SELECT * FROM employees WHERE last_name NOT LIKE 'A%';
(3)范围操作符IN和BETWEEN
选择employees表中分别在'1964-06-01'、'1964-06-02'和'1964-06-04'这3天出生的员工时可使用如下命令。
SELECT * FROM employees WHERE birth_date IN ('1964-06-01','1964-06-02','1964-06-04');
选择employees表中在'1964-06-01'至'1964-06-04'期间出生的员工时可使用如下命令。
SELECT * FROM employees WHERE birth_date BETWEEN '1964-06-01' AND '1964-06-04';
(4)限制获取记录数(使用LIMIT子句)
只获取employees表中的5条记录(没顺序)时可使用如下命令。
SELECT * FROM employees LIMIT 5;
(5)排序(ORDERBY)
查询按出生日期排序的最老的10名员工时可使用如下命令。
SELECT * FROM employees ORDER BY birth_date ASC LIMIT 10;
查询按出生日期排序第100至109名的员工时可使用如下命令。
SELECT * FROM employees ORDER BY birth_date ASC LIMIT 100,10;
(6)数据计算
MySQL提供了一些计算函数,下面给出了计算日期的示例,后续章节会专门叙述此类常用的函数。
以下命令将计算employees表中员工的年龄,并且按first_name、last_name排序,返回记录数限制在10条。
SELECTemp_no, first_name, last_name, birth_date, curdate(), timestampdiff(year, birth_date, curdate()) as age
FROM employees ORDER BY first_name , last_name LIMIT 10;
(7)使用DISTINCT获取不重复的唯一值
以下命令将查询employees雇员表里唯一的first_name值。
SELECT DISTINCT first_name FROM employees ;
如果以上语句没有关键字DISTINCT,那么返回的记录里first_name会有许多重复值。
(8)聚集函数COUNT、MIN、MAX、AVG、SUM
查询表employees的总记录数时可使用如下命令。 SELECT COUNT(*) FROM employees;
查询表employees的最小员工号时可使用如下命令。 SELECT MIN(emp_no) FROM employees;
查询表employees的最大员工号时可使用如下命令。 SELECT MAX(emp_no) FROM employees;
查询雇员的平均薪水时可使用如下命令。 SELECT AVG(salary) FROM salaries WHERE to_date='9999-01-01' ;
salaries表存储了所有员工在不同时期的薪水,whereto_date='9999-01-01'可用于筛选出员工目前的薪水。
查询雇员薪水总额时可使用如下命令。 SELECT SUM(salary) FROM salaries WHERE to_date='9999-01-01' ;
以下查询将统计'1986-06-26'和'1985-11-21'分别有多少人入职。
SELECT SUM(hire_date='1986-06-26') AS sum_1986_06_26,SUM(hire_date='1985-11-21') AS sum_1985_11_21 FROM employees;
查询语句里的hire_date='1986-06-26'是一个计算表达式,满足条件时为1,不满足条件时为0。sum对表达式返回的值进行求和,这样就实现了统计。
当然这种写法不常见,也不推荐使用。
(9)分组统计GROUP BY子句
一般将GROUP BY语句和聚集函数一起使用,从而实现分组统计。
查询employees表,按照first_name分组,并根据first_name出现的次数按降序排序。
SELECT first_name,COUNT(*) cnt FROM employees GROUP BY first_name ORDER BY cnt DESC;
也支持如下形式的对多个列同时进行聚集计算。
SELECT MAX(a),MAX(b),MAX(c) FROM table_name WHERE ... GROUP BY d ;
我们可以在GROUP BY语句后添加HAVING子句,并对聚集结果进行筛选。
以下命令将查询employees表,按照first_name分组,列出重复次数大于270的first_name,并按照first_name重复的次数按降序排序。
SELECT first_name,COUNT(*) cnt FROM employees GROUP BY first_name HAVING cnt > 270 ORDER BY cnt DESC ;
注意:
SELECT之后的选择列表的每个列都要来自于GROUP BY的列,有些数据库有严格的约定,必须满足此条件,
MySQL对此没有强制要求,但需要清楚一个事实,数据库并不能保证其他列共有一个值,其他列的值可能是不固定的,
目前其他列的返回结果是分组结果中的第一条记录,是按物理存储顺序进行排序的,但这个排序并不可靠,MySQL也没有确保后续版本会这么定义。
SQL_MODE添加ONLY_FULL_GROUP_BY可以避免这种错误。
(10)并集操作(UNION和UNION ALL)
有时我们需要对两个结果集进行合并操作。
UNION和UNION ALL都是将两个结果集合并为一个,但UNION ALL比UNION更快。
UNION实际上是UNION DISTINCT,在进行表连接后会筛选掉重复的记录,所以在表连接后会对所产生的结果集进行排序运算,删除重复的记录再返回结果。
而UNION ALL则是不管有没有重复记录,都直接返回合并后的记录。
实际应用中,两个需要合并的结果集一般不会产生重复记录,所以建议在能够使用UNION ALL的情况下尽量使用UNION ALL,否则对于很大的结果集,可能会导致查询耗时很长。
UNION ALL的使用示例如下。 SELECT * FROM a UNION ALL SELECT * FROM b;
UNION的使用示例如下。 SELECT * FROM a UNION SELECT * FROM b;
(11)NULL值
NULL值的判断一般使用IS NULL或IS NOT NULL,不能使用以上的比较操作符=、<、>,因为NULL是一个特殊的值,表示这个值是未知的或没有定义的。
以下命令将查询employees表中first_name列以字母D开头的员工且last_name值不是NULL的记录。
SELECT * FROM employees WHERE first_name LIKE 'D%' AND last_name IS NOT NULL;
上述命令添加的条件“AND last_nameIS NOT NULL”仅是为了演示,实际上,由于last_name列上有约束——必须是NOT NULL,所以“AND last_nameIS NOTNULL”这个条件总是满足的。
对于GROUP BY子句,两个NULL值可以认为是相等的。
对于“ORDERBY…ASC”,NULL显示在前。对于“ORDERBY…DESC”,NULL值显示在后。
0或空字符串实际上都是有值的,所以在一个NOT NULL的列上插入0或空字符串是允许的。

4.JOIN(连接)
MySQL使用JOIN来连接多个表查询数据,主要使用的JOIN算法只有一种,那就是nested-loop join。
nested-loop join算法实现的机制很简单,就是从驱动表中选取数据作为循环基础数据,然后以这些数据作为查询条件到下一个表中进行查询,如此往复。
这个实现机制类似于foreach函数的遍历。因此带来的问题就是连接的表越多,函数嵌套的层数就越多,算法复杂度呈指数级增长。
因此,设计查询要尽量减少连接的表的个数。
驱动表是指:在使用多表嵌套连接时,首先,全表扫描该驱动表,然后用驱动表返回的结果集逐行去匹配被驱动的表(可以利用索引),
数据库基于成本可能会选择小表作为驱动表,而被驱动表使用索引进行连接。
JOIN语句的含义是把两张(或者多张)表的属性通过它们的值组合在一起,一般会遇到如下3种连接。
等值连接([INNER]JOIN) 左外连接(LEFT JOIN) 右外连接(RIGHT JOIN)
(1)内连接
内连接(INNER JOIN)是应用程序中普遍应用的“连接”操作,它一般都是默认的连接类型。
内连接基于连接谓词将两张表 (如A和B)的列组合在一起,从而产生新的结果表。
内连接可以被进一步分为等值连接、自然连接和交叉连接。较常用的是等值连接。
以下是等值连接的示例。
查询目前在职的所有员工的姓名及其所在的部门时可使用如下语句:
SELECT de.emp_no, first_name, last_name, dept_name
FROM dept_emp de INNER JOIN employees e ON de.emp_no = e.emp_no INNER JOIN departments d ON d.dept_no = de.dept_no
WHERE to_date = '9999-01-01';
自然连接(natural join)是等值连接的进一步特例化。
两表做自然连接时,两表中名称相同的所有列都将被比较,这是隐式的。
自然连接得到的结果表中,两表中名称相同的列只出现一次。
一般应该避免使用自然连接,因为我们无法指定连接列,
且这种写法隐藏了我们的JOIN关系,如果以后数据模型发生了变化,可能会导致出现非预期的结果。
交叉连接(笛卡儿积)
把表视为行记录的集合,交叉连接即返回这两个集合的笛卡儿积。这其实等价于内连接的连接条 件为“永真”,或者连接条件不存在的情况。
示例如下:在职员工的姓名和部门的等值连接
SELECT * FROM a JOIN b;
SELECT * FROM a,b;
SQL定义了两种不同的语法方式来表示“连接”。
一种是“显式连接符号”,显式地使用关键字JOIN,另一种是“隐式连接符号”,它使用所谓的“隐式连接符号”。
隐式连接符号把需要连接的表放到SELECT语句的FROM部分,并用逗号隔开,这样就构成了一个“交叉连接”,WHERE语句可能会放置一些过滤谓词(过滤条件)。
那些过滤谓词在功能上等价于显式连接符号。
如上所述的例子中,查询目前在职的所有员工的姓名及所在部门时,可以写成如下的形式。
SELECT de.emp_no, first_name, last_name, dept_name FROM dept_emp de, employees e, departments d WHERE de.dept_no = d.dept_no and de.emp_no = e.emp_no and de.to_date = '9999-01-01';
它等价于:
SELECT de.emp_no, first_name, last_name, dept_name FROM dept_emp de INNER JOIN employees e ON de.emp_no = e.emp_no INNER JOIN departments d ON d.dept_no = de.dept_no WHERE to_date = '9999-01-01';
ON表达式是任何可以用于WHERE子句的条件表达式,一般来说,你应该只在ON表达式里指定如何JOIN表,而把筛选结 果集的条件放到WHERE子句中。
(2)外连接(OUT JOIN)
并未要求连接的两表的每一条记录在对方的表中都必须有一条匹配的记录。连接表保留所有的记录,甚至这条记录没有匹配的记录也要保留。
外连接可依据连接表保留左表、右表或全部表的行而进一步分为左外连接、右外连接和全外连接。
全外连接一般没有什么意义,MySQL并不直接支持全外连接,但可以通过左右外连接的并集(UNION)来模拟实现。
1)左外连接(LEFT JOIN、LEFT OUTER JOIN)
左外连接也简称为左连接(LEFT JOIN),若A和B两表进行左外连接,那么结果表中将包含“左表”(即表A)的所有记录,即使那些记录在“右表”B中没有符合连接条件的匹配。
这就意味着即使ON语句在表B中的匹配项是0条,连接操作也还是会返回一条记录,只不过这条记录中的来自于表B的每一列的值都为NULL。
之前的示例曾演示过插入一些记录(员工号111111、111112、111113、111114)到employees表中,但还没有指定新插入员工记录的部门。
现在用如下语句联合查询下雇员表和部门–雇员表。
SELECTde.dept_no, first_name, last_name FROM employees e LEFT JOIN dept_emp de ON e.emp_no = de.emp_no WHERE e.emp_no IN (10001,10002,10003,111111 , 111112, 111113, 111114);
2)右外连接(RIGHT JOIN、RIGHT OUT JOIN)
右外连接也简称右连接(RIGHT JOIN),它与左外连接完全类似,只不过是连接表的顺序相反而已。
如果A表右连接B表,那么“右表”B中的每一行在连接表中至少会出现一次。
如果B表的记录在“左表”A中未找到匹配行,则连接表中来源于A表中 的列的值将设为NULL,示例如下。
SELECT * FROM A RIGHT JOIN B ON A.id = B.id
实际上,显式的右外连接很少使用,因为它的可读性不佳,所以总是被改写成左连接。
如果JOIN的层次比较多,则需要留意一下可读性,如果不能确定优先级,那么建议使用括号来明确优先级,以避免犯错误。
例如,在以下的例子中,由于逗号的优先级比JOIN表达式低,因此可能会导致我们犯错误。
SELECT t1.id,t2.id,t3.id FROM t1,t2 LEFT JOIN t3 ON (t3.id=t1.id) WHERE t1.id=t2.id;
上述代码实际上是: SELECT t1.id,t2.id,t3.id FROM t1,( t2 LEFT JOIN t3 ON (t3.id=t1.id) ) WHERE t1.id=t2.id;
但这其实并不是我们的本意,应该写成如下的形式(用括号来提升优先级别)。
SELECT t1.id,t2.id,t3.id FROM (t1,t2) LEFT JOIN t3 ON (t3.id=t1.id) WHERE t1.id=t2.id;
使用JOIN命令操作表的时候,需要留意如果表按条件筛选的记录是不确定的,可能就会导致非预期的结果。
对于如下的查询:
SELECT t1.id as t1_id, t1.code as t1_code, t2.id as t2_id, t2.code as t2_code, t2.name FROM t1 JOIN t2 ON t1.code = t2.code AND t2.code = 'b';
由于code='b'在两个表中都可以唯一确定一条记录,因此查询会返回合理的结果。
而对于如下查询:
SELECT t1.id as t1_id, t1.code as t1_code, t2.id as t2_id, t2.code as t2_code, t2.name FROM t1 JOIN t2 ON t1.code = t2.code AND t2.code = 'a';
由于t2表code='a'会返回多个值。最终的查询结果返回了3条记录,因此可能不是我们所需要的结果。

5.子查询
子查询是指查询语句里的SELECT语句。比如下面这条语句。
SELECT * FROM t1 WHERE column1 = (SELECT column1 FROM t2);
在这个示例中,SELECT * FROM t1是外部查询(外部语句),SELECT column1 FROM t2是子查询。
我们可以说,这个子查询是嵌套在外部查询中的,子查询嵌套的层次不宜过多,否则性能可能会很差。
许多人认为子查询的可读性更好,子查询在现实中的应用也很广泛,
但MySQL对于子查询的优化不佳,由于子查询一般可以改写成JOIN语句,因此一般建议使用JOIN的方式查询数据。
下面来看看示例。 查询薪水大于150000的员工的姓名。
SELECT emp_no, first_name, last_name FROM employees WHERE emp_no IN (SELECT emp_no FROM salaries WHERE to_date = '9999-01-01' AND salary > 150000);
可将上述语句改写成JOIN的方式,查询语句如下。
SELECT employees.emp_no, first_name, last_name FROM employees JOIN salaries ON salaries.emp_no = employees.emp_no WHERE salaries.to_date = '9999-01-01' AND salaries.salary > 150000;
说明:以上的例子使用了通配符“*”,生产环境DBA一般会建议不要使用“select *”这样的方式查询数据,这里使用通配符主要是为了书写方便,避免分散注意力,从而关注更重要的部分。

3.4 PHP开发
3.4.1 概述
一般的流行语言,如PHP、C、Perl、Java都对MySQL提供了完善支持,这其中PHP是最常用的使用MySQL数据库的语言,
互联网普遍使用的是LAMP/LNMP架构,这里的P可以理解为就是PHP,可以说PHP的应用范围相当广泛,尤其是在Web程序的开发上,
比如,我们熟知的Facebook,就是PHP、MySQL的重度使用者。
作为互联网开发者,我们有必要熟悉MySQL在各种语言环境下的使用,尤其是PHP。
以下简要介绍PHP与MySQL开发,
PHP(全称为Hypertext Preprocessor,即超文本预处理器)是一种开源的通用计算机脚本语言,它是服务器端的解释语言,可以嵌入到HTML页面中,这些代码在每次页面访问时都将被执行。
PHP代码将在Web服务器中被解释并且生成HTML或访问者看到的其他输出界面。
如果想要学习PHP+MySQL开发,那么首先需要搭建一个适合练习的开发环境。
对于操作系统,建议使用Linux;Web服务器建议使用Apache或Nginx;数据库当然是MySQL了。
网上有很多搭建LAMP(Linux、Apache、MySQL、PHP)环境的文档,大家可以搜索查看。
也有一些集成的自动安装包,如XAMPP,可以一键安装帮你部署好所有环境,但还是建议手动部署下环境,从而对Web服务器的配置文件、PHP的配置文件有一定的了解。
3.4.2 客户端访问过程
下面简单介绍下传统网站的访问数据流。
在客户端(用户)与数据库服务器之间往往还会涉及Web服务器和负载均衡设备。
作为一个开发者,需要清楚数据是如何在客户端和服务器之间进行传递的。
下面简单说明下客户端访问数据库服务器中间经过的环节,这里以Windows PC浏览器为例进行说明,并解释一些软硬件基础概念。
1.访问DNS服务
用户在浏览器的地址栏输入网址域名,浏览器会查询这个域名与IP的对应关系是否已经存在于本机的host文件中,
如果没有,则会把请求发送给本机指定的域名系统(DomainName System,DNS)服务器。
什么是域名系统服务器呢?
计算机世界是以IP地址来定位服务器或PC的,DNS这项服务的目的就是将域名翻译成IP,使用户可以更方便地访问互联网。
DNS服务器有一定的层级,如果某个DNS服务器不知道如何翻译,就会问另外一个,再不知道,再问下一个,这样就会有一个递归的过程。
幸运的是,DNS可以缓存查询结果,这样我们就不需要经历重复冗长的过程去查找一个域名映射的IP地址。
DNS系统中,常见的资源记录类型有如下两种。
主机记录(A记录):用于名称解析的重要记录,它将特定的主机名映射到对应主机的IP地址上。
别名记录(CNAME记录):用于将某个别名指向到某个A记录上,这样的好处是修改IP的时候改A记录就可以了,对于有大量子域名的网站可以简化操作、统一维护域名指向。
DNS查询有两种方式:递归和迭代。
DNS客户端设置使用的DNS服务器一般都是递归服务器,它负责全权处理客户端的DNS查询请求,直到返回最终结果。
而DNS服务器之间一般采用迭代查询的方式。
下面以查询zh.wikipedia.org为例。
客户端发送查询报文“query zh.wikipedia.org”至DNS服务器,DNS服务器首先检查自身缓存,如果存在记录则直接返回结果。
如果记录老化或不存在,则DNS服务器向根域名服务器发送查询报文“query zh.wikipedia.org”,根域名服务器返回“.org”域的权威域名服务器地址。
DNS服务器向“.org”域的权威域名服务器发送查询报文“query zh.wikipedia.org”,得到“.wikipedia.org”域的权威域名服务器地址。
DNS服务器向“.wikipedia.org”域的权威域名服务器发送查询报文“query zh.wikipedia.org”,得到主机zh的A记录,存入自身缓存并返回给客户端。
DNS系统还有一个很重要的概念:TTL(Time To Live的缩写),简单地说,它表示的是一条域名解析记录在DNS服务器上的缓存时间。
当一个递归域名服务器查询权威域名服务器获取某个域名的映射时,它会将该记录缓存上一定的时间,这个时间就是TTL指定的时间(以秒为单位)。
如果在一台Linux机器上反复运行命令“dig www.mysql.com”,就会发现这个缓存时间在减少,为什么呢?
因为在你的DNS缓存中,这笔记录能够保存的时间开始倒计数,如果TTL没有归零,缓存服务器会简单地用已缓存的记录答复查询请求。
若这个数字归零后,下次再有人重新搜寻这笔记录时,你的DNS就需要从权威域名服务器重新获取记录。
也就是说,如果更改了域名的指向,那么最长需要TTL时间才会完全生效。
了解DNS系统不仅仅是运维团队的事情,研发人员也有必要清楚其大概的机制。
我们在部署程序或设计迁移方案的时候, 需要清楚是否要申请域名、创建新的CNAME记录,是否需要修改TTL生效时间,是否需要修改A记录。
另外需要注意的是,虽然有TTL的机制,但由于国内移动网络的特殊性,DNS的修改可能长期不能生效,由于其特殊的分布式数据库的设计,如果遭到域名污染,往往也会造成巨大的影响,这种问题很难解决。
2.经过负载均衡软硬件设备
经过负载均衡软硬件设备如F5、Haproxy、LVS后,再把请求转发给后端的网络服务。
负载均衡(Load Balance),即将负载(工作任务)进行平衡、分摊到多个操作单元上进行执行,例如Web服务器、FTP服务器、企业关键应用服务器和其他关键任务服务器等,从而共同完成工作任务。
当后端的一台服务器宕机或过载,负载均衡软硬件设备将不再转发流量到这台服务器,转而发送到备用的服务器上,从而实现自动故障冗余切换。
最早的负载均衡技术是通过DNS来实现的,在DNS中为多个地址配置同一个名字,因而查询这个名字的客户机将得到其中的一个地址,从而使得不同的客户访问不同的服务器,达到负载均衡的目的。
DNS负载均衡是一种简单而且有效的方法,但是它不能区分服务器的差异,也不能反映服务器的当前运行状态,对于高并发、大流量的请求,很容易导致负载并不均衡。
现实中,它可能作为更上层的负载均衡存在,完成粗粒度的流量调度任务,比如在机房之间使用DNS负载均衡,在机房内部使用其他负载均衡方式。
F5负载均衡器是应用交付网络的全球领导者F5 Networks公司提供的一个负载均衡器专用设备,一般需要配置双机故障冗余切换。
F5主要应用于传统行业内,如电信、移动、银行等,也有许多互联网公司使用F5设备,虽然F5设备比较昂贵,但在一定规模下,它可以降低企业的成本,代替系统管理员、工程师管理各种资源。
互联网公司用得比较多的是F5 BIG-IP LTM(本地流量管理器),由于国内网络的复杂性,也有使用BIG-IP广域网流量管理器(BIG-IP GTM)的。
F5设备除了负载均衡外,还有一些其他的功能,如利用压缩技术降低带宽支出、减少连接数等。
F5等硬件设备毕竟是商业化的产品,比较昂贵,在一定规模下使用可以获得比较好的投资回报率,但在公司初创时,或者公司已经流量很大的时候,F5设备的成本优势则并不明显,
目前很多公司的F5设备已经逐步被LVS等其他软件替代,所以,对于互联网公司,一般建议使用开源软件实现负载均衡,常用的有LVS、Haproxy等,
它们和其他技术配合使用可以实现很好的扩展性,无论是在流量很小还是流量很大的情况下,都能够满足需要。
网上有很多关于LVS、Haproxy的资料,这里不再赘述。
3.经过反向代理服务
反向代理是代理服务器的一种,比如Squid、Vanish等。
它根据客户端的请求,从后端的服务器上获取资源,然后再将这些资源返回给客户端。
常用的代理服务为Squid,它可以作为缓存服务器,可以过滤流量保证网络安全,也可以作为代理服务器链中的一环,向上级代理转发数据或直接连接互联网,一些网站往往在前端增加Squid反向代理加速响应、提高吞吐量。
Squid可以缓存内容,特别是一些静态的数据,比如图片和文件,如果反向代理靠近用户的网络,那么用户就会得到延时很低的高质量访问,这正是CDN技术的核心。
4.到达Web服务器
Web服务器包括Nginx、Apache、Lighttpd、Tomcat、Resin等。
Apache HTTP Server(简称Apache)是Apache软件基金会的一个开放源代码的网页服务器,是使用最广泛、最流行Web服务器端软件之一。
Apache功能最完备,但占用的资源比较多,支持的连接数也比Nginx少,所以目前在互联网界,已经被Nginx抢了风头。
Nginx(发音同engine x)是一款由俄罗斯程序员Igor Sysoev所开发的轻量级的网页服务器,它也可以用作反向代理、负载均 衡器,但更常见的功能是Web服务器。
它是一款面向性能设计的HTTP服务器,以事件驱动的方式编写,很注重效率,所以在许多评测中,相比于Apache都有更高的性能,能支撑更多的并发请求。
在常见的网络架构中,Nginx往往配合php-fpm使用,Nginx负责处理静态请求,把PHP等动态请求抛给后端的php-fpm处理。
或者Nginx处理前端的静态请求,把Apache放在后端处理一些动态请求。
所以各种Web服务器之间可能也有一定的层级关系或功能分工,这点需要了解清楚。
5.调用应用服务器
应用程序服务器是通过很多协议来为应用程序提供商业逻辑的服务器。
根据我们的定义,作为应用程序服务器,它将通过各种协议(包括HTTP),把商业逻辑暴露给客户端应用程序。
Web服务器主要是处理向浏览器发送HTML以供浏览,而应用程序服务器则提供访问商业逻辑的途径以供客户端应用程序使用。
这里所说的应用服务器更多地属于内部调用的范畴。
一般Web服务器可以高效地处理简单的响应请求,但如果有复杂的商业逻辑的话,把这些业务逻辑放到独立的应用服务器上,
然后通过调用的方式来获取信息会更安全、性能更高、开发也更方便。
6.访问数据库
客户端不直接和数据库打交道,如果处理逻辑需要访问数据,则由应用服务器或Web服务器访问数据库,获取数据。
以上架构是比较普通的三层/四层架构,架构中也可能有一个缓存(Cache)服务,以减轻数据库的压力。
Web服务器、应用服务器到数据库服务器中间可能存在数据中间件,但更常规的做法是通过在Web服务器、应用服务程序配置文件里指定IP或内网域名来配置数据库路由。
生产环境里,如果出现了性能问题,研发人员往往第一时间就会怀疑是数据库出现了性能问题,但事实往往并非如此,
从上面的叙述可知,用户的访问请求经过了许多环节,有DNS、负载均衡设备、Web服务器,而且长距离网络中的数据传输物理上还需要经过许多设备,如路由器、交换机等,
由于国内网络的特殊性和复杂性,有时会碰到网络丢包,丢包很可能导致性能问题。
所以,如果出现了性能问题或访问异常,研发、运维人员就需要仔细甄别到底是哪个环节出现了问题,配合各种监控和日志记录,是有可能快速定位到问题症结所在的。
注意:数据库一般位于内网,若没有外网IP,则不需要暴露给外网。

3.4.3 开发工具
这里主要介绍一款常用的基于Web的管理工具phpMyAdmin。
phpMyAdmin是一个以PHP为基础的MySQL的数据库管理工具。
让管理者可以通过Web接口操作数据库,也就是于远端管理 MySQL数据库。
研发人员并不像DBA那样经常通过命令行来操作数据库,所以往往会不熟悉命令行,借由这套图形工具,可以方便地建立、修改、删除数据库及表、浏览数据、插入数据、删除数据。
phpMyAdmin也是很好的学习工具,可以生成SQL语句,验证语法,也可以生成常用的PHP语法。
对于研发人员,若要求熟练使用命令行操作数据库,那么这个要求确实高了些,他们应该把更主要的精力放在程序代码的编写上,所以,建议熟悉此类图形工具。
需要注意的是,未经配置的phpMyAdmin不安全,容易受到攻击,建议只将其部署在开发和测试环境中。
在生产环境中,如果需要部署phpMyAdmin,那么一定要先经过安全评估,确认没有安全漏洞,而这往往是需要通过改造phpMyAdmin来实现的。
还有一些客户端工具,如Workbench、toad for MySQL、SQLyog、Navicat for MySQL。
这其中,SQLyog功能最强大,但是, 它是收费软件,Workbench出自官方,对比以前的版本功能已经有了长足的进步,而且也一直在努力改进中,建议大家优先使 用这个工具。

3.4.4 操作数据
如果想要做Web开发,那么读者需要熟悉HTML、XML、JavaScript、Ajax等知识。
关于这些知识和PHP的基本语法和特性这里不做介绍,本章主要讲述PHP相对于数据的一些常用操作。
如果需要详细了解PHP开发知识,可以参考官方文档或《PHP与 MySQLWeb开发》 一书。
PHP提供了3种API可用于访问MySQL,分别是MySQL扩展、MySQLi扩展、PDO扩展。
官方的建议是使用MySQLi或PDO, 因为MySQL扩展在未来可能被废弃掉。
MySQLi只支持MySQL数据库,如果有跨数据库平台的需要,那么就要使用PDO了。
由于目前MySQL扩展仍然有广泛的应用,以下示例仍以MySQL扩展为例,同时也将列出MySQLi的代码供读者参考。
1.PHP连接数据库
在访问并处理数据库中的数据之前,必须先创建到达数据库的连接。
PHP提供了两个连接MySQL的函数mysql_pconnect()和mysql_connect()。
mysql_connect()函数的语法如下:mysql_connect(servername,username,password);
脚本一结束,就会关闭连接。如需提前关闭连接,请使用mysql_close()函数。
2.选择数据库
在执行语句前,需要先选择数据库。通过mysql_select_db()函数可选取数据库,它的语法如下。 bool mysql_select_db(db_name);
示例test_conn_use_db.php的语句如下。
<html>
<head>
<title>Selecting MySQL Database</title>
</head>
<body>
<?php
$dbhost = '127.0.0.1:3306';
$dbuser = 'garychen';
$dbpass = 'garychen';
$conn = mysql_connect($dbhost, $dbuser, $dbpass);
if(! $conn ) { die('Could not connect: ' . mysql_error()); }
echo 'Connected successfully';
$db_selected = mysql_select_db('employees',$conn);
if (!$db_selected) { die ('Can\'t use employees : ' . mysql_error()); }
echo "<br />";
echo 'use employees successfully';
mysql_close($conn);
?>
</body>
</html>
使用MySQLi连接数据库,语句如下。
<html>
<head>
<title> connect database </title>
</head>
<body>
<?php
$host="127.0.0.1";
$port=3306;
$socket="";
$user="garychen";
$password="garychen";
$dbname="employees";
$con = new mysqli($host, $user, $password, $dbname, $port, $socket) or die ('Could not connect to the database server' . mysqli_connect_error());
echo 'connect to employees database successfully';
$con->close();
?>
</body>
</html>
注意:无论是指定“localhost”还是“localhost:port”作为servername,MySQL客户端库都将覆盖之并尝试连接到本地套接字。如果希望使用TCP/IP连接,则用“127.0.0.1”替代“localhost”。

3.查询数据
先使用mysql_query()函数向MySQL发送查询或命令。然后使用mysql_fetch_array函数返回数据。
示例test_select.php语句如下。
<html>
<head>
<title>Selecting MySQL Database</title>
</head>
<body>
<?php
$con = mysql_connect("127.0.0.1","garychen","garychen");
if (!$con) {die('Could not connect: ' . mysql_error()); }
mysql_select_db("employees", $con);
$result = mysql_query("select * from departments");
while($row = mysql_fetch_array($result))
{
echo $row['dept_no'] . " " . $row['dept_name'];
echo "<br />";
}
mysql_close($con);
?>
</body>
</html>
上面这个例子在$result变量中存放了由mysql_query()函数返回的数据。
接下来,使用mysql_fetch_array()函数以数组的形式从记录集返回第一行。
随后对于mysql_fetch_array()函数的每个调用都会返回记录集中的下一行。
while loop语句会循环记录集中的所有记录。
为了输出每行的值,这里使用了PHP的$row变量($row['dept_no']和$row['dept_name'])。

4.使用MySQLi查询数据
首先新建连接(newmysqli),然后预处理查询语句(使用prepare方法),执行这个语句(使用execute方法),
最后,把结果集的列绑定到两个变量(使用bind_result方法),并获取实际数据(使用fetch方法),打印输出。
示例test_select2.php语句如下。
<html>
<head>
<title>select table</title>
</head>
<body>
<?php
$host="127.0.0.1";
$port=3306;
$socket="";
$user="garychen";
$password="garychen";
$dbname="employees";
$con = new mysqli($host, $user, $password, $dbname, $port, $socket) or die ('Could not connect to the database server' . mysqli_connect_error());
echo 'connect to employees database successfully';
echo "<br />";
echo "select departments table";
echo "<br />";
$query = "select * from departments";
if ($stmt = $con->prepare($query))
{
$stmt->execute();
$stmt->bind_result($field1, $field2);
while ($stmt->fetch())
{
printf("%s, %s\n", $field1, $field2);
echo "<br />";
}
$stmt->close();
}
$con->close();
?>
</body>
</html>

5.插入记录
类似SELECT查询,先连接数据库(使用mysql_connect函数),然后选择数据库(使用mysql_select_db函数),最后发送 INSERT语句给MySQL(使用mysql_query函数)。
示例test_insert.php语句如下。
<html>
<head>
<title>Insert records</title>
</head>
<body>
<?php
$con = mysql_connect("127.0.0.1","garychen","garychen");
if (!$con) {die('Could not connect: ' . mysql_error()); }
mysql_select_db("employees", $con);
$sql="INSERT INTO employees (emp_no, birth_date, first_name,last_name,gender,hire_date) VALUES (500000,'1990-08-01','Peter', 'wang', 'M','2011-11-11')";
if (!mysql_query($sql,$con)) {die('Error: ' . mysql_error()); }
echo "1 record added";
mysql_close($con);
?>
</body>
</html>
6.使用MySQLi插入记录
新建连接(new mysqli),然后使用mysqli_query方法操作数据库,执行查询。
示例test_insert2.php语句如下。
<html>
<head>
<title>insert records</title>
</head>
<body>
<?php
$host="127.0.0.1";
$port=3306;
$socket="";
$user="garychen";
$password="garychen";
$dbname="employees";
$con = new mysqli($host, $user, $password, $dbname, $port, $socket) or die ('Could not connect to the database server' . mysqli_connect_error());
echo 'connect to employees database successfully';
echo "<br />";
$sql="INSERT INTO employees (emp_no, birth_date, first_name,last_name,gender,hire_date) VALUES (500003,'1990-09-01','Peter', 'zhang', 'M','2011-12-12')";
if (!mysqli_query($con,$sql)) {die('Error: ' . mysqli_error($con)); }
echo "1 record added";
$con->close();
?>
</body>
</html>
更新数据和删除语句的操作这里不再赘述,大家可参考如下链接。
关于MySQL:http://www.tutorialspoint.com/php/mysql_update_php.htm
关于MySQLi:http://www.w3schools.com/php/php_mysql_update.asp

3.4.5 PHP数据库开发建议
对于开发人员来说,了解数据流和生产环境的物理部署是很重要的,开发人员不应该局限于自己的领域。
能够熟练查阅MySQL官方文档,虽然研发人员不必熟悉MySQL的每一个细节,但要善于查找资料,找到答案,一般我们经历的问题都是可以从文档或网络中找到答案的。
注意安全问题,防止SQL注入、跨站脚本攻击等恶意攻击。详细内容可参考4.2节“权限机制和安全”。
阅读源码,了解它们是如何访问数据的。一些网站,如sourceforge提供了许多优秀的源码。
注重用户体验,数据库的访问优化,往往和用户体验相关。
如果要访问数据库或数据接口,优化的方式不外乎两个,或者减少对数据的访问,或者让查询执行得更快。
比如一些大页面只改动了几行字,那么查询时可能就只需要检索小部分数据,而不是重载整个页面查询很多数据。
比如地图,只需要加载部分块区域,而不是整个图重新绘制。
比如翻页,传统的一些算法效率往往很差,影响用户体验。
要有性能分析器,如果想要开发高质量的程序,那么需要对自己的程序进行分析,特别是对于数据库访问的分析,比如记录数据库性能访问日志。
一些工具也有助于分析网站的响应,如firebug。

3.5 索引
3.5.1 索引介绍
数据库索引,是数据库管理系统中一个排序的数据结构,用于协助快速查询、更新数据库表中的数据。
它类似于书本上的索引,通过索引可以更便捷地找到书里面的内容而不需要查阅整本书。
对于海量数据的检索,索引往往是最有效的。
目前MySQL主要支持的几种索引有:B树索引(B-tree)、散列索引(hash)、空间索引(R-tree)和全文索引(full-text)。
如果没有特别指明,本书指的就是B-Tree索引。
由于索引是在存储引擎层实现的,所以不同的存储引擎的索引实现会有一些差异。
以下所述的是一些较通用的索引知识。
逻辑上又可以分为:单列索引、复合索引(多列索引)、唯一(Unique)索引和非唯一(NonUnique)索引。
如果索引键值的逻辑顺序与索引所服务的表中相应行的物理顺序相同,那么该索引被称为簇索引(cluster index),也称为聚集索引、聚簇索引,
也就是说数据和索引(B+树)在一起,记录被真实地保存在索引的叶子中,簇索引也称为索引组织表, 反之为非聚集索引。
我们常用的InnoDB表其实使用的就是聚集索引。
簇索引是一个很重要的概念,InnoDB作为最常使用的引擎,只有在熟悉了它的数据存储方式之后,才可能有针对性地对它进行调优。
簇索引的一些优点如下:
将相关的的数据保持在一起,叶子节点内可保存相邻近的记录。
因为索引和数据存储在一起,所以查找数据通常比非簇索引更快。由于主键是有序的,很显然,对于InnoDB表,最高效的存取方式是按主键存取唯一记录或进行小范围的主键扫描。
如果充分利用簇索引,它可以极大地提升性能,但簇索引也有许多不足之处。
簇索引对I/O密集型的负荷性能提升最佳,但如果数据是在内存中(访问次序不怎么重要),那么簇索引并没有明显益处。
插入操作很依赖于插入的顺序,按primary key的顺序插入是最快的。
更新簇索引列的成本比较高,因为InnoDB不得不将更新的行移动到新的位置。
全表扫描的性能不佳,尤其是数据存储得不那么紧密时,或者因为页分裂(pagesplit)而导致物理存储不连续。
二级索引的叶节点中存储了主键索引的值,如果主键采用的是较长的字符,那么索引可能会很大,且通过二级索引查找数据也需要进行两次索引查找。
3.5.2 使用索引的场景及注意事项
1.何种查询可以应用索引
(1)MySQL目前仅支持前导列
筛选记录的条件应能组成复合索引最左边的部分,即按最左前缀的原则进行筛选。
随着日后技术的发展,MySQL或许能够更有效率地利用复合索引多字段中的非前导列信息。
下面来看个例子,对于如下的复合索引idx_a_b_c: CREATE INDEX idx_a_b_c ON tb1(a,b,c);
只有使用如下条件才可能应用到这个复合索引。 WHERE a=?; WHERE a=? AND b=?; WHERE a=? AND b=? AND c=?; WHERE a=? AND c=?;
#注意这个查询仅仅利用了 MySQL索引的 a列信息
(2)索引列上的范围查找
在对某个条件进行范围查找时,如果这个列上有索引,且使用的是WHERE…BETWEEN…AND…、>、<等范围操作符时,那么可能就会用到索引范围查找。
一般应该避免大范围的索引范围查找,如果索引范围查找的成本太高,那么数据库可能会选择全表扫描的方式。
注意:IN(...)并不属于范围查找的范畴。
(3)JOIN列
在联合查询两个表时,比如查询语句为“SELECT a.col1,b.col2 FROM a JOIN b ON a.id=b.id”,其中id为主键,
若a表是驱动表,那么数据库可能全表扫描a表,并用a表的每个id去探测b表的索引查找匹配的记录。
(4)WHERE子句
WHERE子句的条件列是复合索引前面的索引列再加上紧跟的另一个列的范围查找。
比如,对于如下的复合索引idx_a_b_c_d: CREATE INDEX idx_a_b_c_d ON tb1(a,b,c,d);
只有使用如下条件才可能应用到这个复合索引。 WHERE a=? AND b=? AND c > 10000; WHERE a=? AND b=? AND c=? AND d<10000;
注意:MySQL索引仅支持最近一个范围的查询。也就是说,MySQL使用最左边的前缀,一直到碰到第一个范围的查找条件为止。
对于复合索引idx_a_b_c_d,我们来看如下的两个查询。 WHERE a=? AND b=? AND c > 10000 AND d<100000;
上面的例子中,d<100000这个筛选操作并不会走索引。
WHERE a>? AND b=? AND c= 10000 AND d = 100; 上面的例子中,a列上有范围查找,那么b、c、d等列上的索引信息将都不能被利用。
即对于复合索引,如果某部分索引已经用到了范围查找,那么这个列之后的索引信息将不能被利用。
所以如果想要创建索引,应该考虑把复合索引的范围查找列放到最后。
(5)MySQL优化器
MySQL优化器会做一些特殊优化,比如对于索引查找MAX(索引列),那么可以进行直接定位,在EXPLAIN输出的Extra信息里可以看到语句“Select tables optimized away”,
意思是这个查询所包含的MIN、MAX操作可以直接利用索引信息来解决,而不需要去检索物理记录。优化器确定只需要返回一行结果即可。

2.注意事项和建议
1)WHERE条件中的索引列不能是表达式的一部分,MySQL也不支持函数索引。
2)InnoDB的非主键索引存储的不是实际记录的指针,而是主键的值,所以主键最好是整型值,如自增ID,基于主键存取数据是最高效的,使用二级索引存取数据则需要进行两次索引查找。
3)最好是按主键的顺序导入数据,如果导入大量随机id的数据,那么可能需要运行OPTIMIZE TABLE命令来优化表。
4)索引应尽量是高选择性的,而且需要留意“基数(cardinality)”值,基数指的是一个列中不同值的个数,
显然,最大基数意味着该列中的每个值都是唯一的,最小基数意味着该列中的所有值都是相同的。
索引列的基数相对于表的行数较高时(也就是说重复值更少),索引的工作效果更好。
一些基数很小的列,如性别可能就不适合建立索引。
也存在这样一种特殊的情况,有些列虽然基数很小,但由于数据分布很不均匀因此也会导致某些值的记录数很少,那么这种情况也适合创建索引加速查找这部分数据。
5)使用更短的索引。可以考虑前缀索引,前缀索引仅索引前面一部分字符(值),但应确保所选择的前缀的长度可以保证大部分值是唯一的。
示例如下: ALTER TABLE test.test1 ADD KEY (col(6))
如下的SQL衡量了不同前缀索引的唯一值比例。
SELECT
COUNT(DISTINCT LEFT(col_name, 3))/COUNT(*) AS sel3,
COUNT(DISTINCT LEFT(col_name, 4))/COUNT(*) AS sel4,
COUNT(DISTINCT LEFT(col_name, 5))/COUNT(*) AS sel5,
COUNT(DISTINCT LEFT(col_name, 6))/COUNT(*) AS sel6,
COUNT(DISTINCT LEFT(col_name, 7))/COUNT(*) AS sel7
FROM table_name;
6)索引太多时可能会浪费空间,且降低修改数据的速度。所以,不要创建过多的索引,也不要创建重复的索引。
MySQL允许在同样的列上创建多个索引而不会提示报错,一些其他分支的版本有统计信息可以甄别出没有被使用的索引。
而对于官方版本,你可能需要借助工具清理掉过多的重复索引。
7)如果是唯一值的列,创建唯一索引会更佳,也可以确保不会出现重复数据。
8)使用覆盖索引(covering index)也可以大大提高性能。
所谓“覆盖索引”是指所有数据都可以从索引中得到,而不需要去读取物理记录。
例如某个复合索引idx_a_b_c建立在表tb1的 a、b、c列上,那么对于如下的SQL语句 select a,b from tb1 where a=? and b=? and c=?;
MySQL可以直接从索引idx_a_b_c中获取所有数据。使用覆盖索引也可以避免第2点所说的二次索引查找。
在EXPLAIN命令输出的查询计划里,如果Extra列是“using index”,那就表示使用的是覆盖索引。EXPLAIN的使用在下节详述。
9)利用索引来排序。MySQL有两种方式可以产生有序的结果。一种是使用文件排序(filesort)来对记录集进行排序,另一 种是扫描有序的索引。我们应尽量利用索引来排序。
在文件排序方式中,由于没有可以利用的有序索引来取得有序的数据,因此MySQL只能将取得的数据在内存中进行排序, 然后再将数据返回给客户端。
使用文件排序的方式,对小结果集进行排序会很快,但是如果是对大量的数据排序,速度将会很慢。
此外,还有如下注意事项。
尽量保证索引列和ORDER BY的列相同,且各列均按相同的方向排序。
如果要连接多张表,那么ORDER BY引用的列需要在表连接的顺序的首张表内。
如果不满足以上条件,则不能利用索引进行排序,那么MySQL将使用文件排序,可以用EXPLAIN工具确认查询是否使用了文件排序,文件排序是一个成本比较高的操作,应尽量避免。
利用索引来排序同样要遵循最左前缀的规则,前导列(等于确定值)加上排序列(ORDERBY的列)可以组合成最左前缀的也行。
比如,对于创建在表table1上的复合索引idx_a_b_c(创建在 列a、b、c上):
SELECT * FROM table1 ORDER BY a,b,c;
SELECT * FROM table1 WHERE a=? AND b=? ORDER BY c; 以上查询都可以利用有序索引来加速检索数据。
10)添加冗余索引,需要权衡。
什么是冗余索引?如果已有一个索引(columnA),那么一个新的索引(columnA,columnB)就是冗余索引,因为后面的索引包含了前面索引的所有信息。
冗余索引一般发生在添加索引的时候,有些人可能会选择添加一个新索引(columnA,columnB),而不是更改原来的 (columnA)为(columnA,columnB)。
一般来说,应该扩展原来的索引,而不是添加新的索引。
但在某些情况下,因为扩展索引会导致索引变得非常大,比如原来的索引是创建在一个整型列上的,要是再添加一个很长的字符列,那么索引会变得很大, 从而影响性能。
这种情况下,可能不得不选择添加新的复合索引,保留原来的索引,这样做的不利之处是增加了索引维护的开销,而且一个新的索引也需要占据内存空间。

3.5.3 索引的错误用法
以下是生产环境中常犯的一些错误,而且由于表结构不易调整,因此往往会导致严重的性能影响。
1)创建了太多的索引或无效的索引。
比如在WHERE条件的每个列上都建立单独的索引,当单个索引效率不高的时候,MySQL往往就会选择全表扫描,太多的索引可能会导致索引所占用的磁盘空间比实际数据还大得多。
2)对于复合索引,如果不考虑ORDER BY、GROUP BY这样的一些操作,那么把最具选择性的列放在前面是合适的,复合索引主要用于优化WHERE查找。
但如果是排序之类的操作,把最具选择性的列放在前面则不一定最有效,因为避免随机I/O和排序可能才是我们更值得考虑的。
3)忽略了值的分布。某些值只有少量记录,查询对这些值的筛选执行就会很快,而某些值即使经过了索引筛选,满足条件的仍然还有大量的记录,这样索引效果就会很差。
一般来说,数据表内值的分布应该尽量均匀,由于MySQL的统计信息不完善,数据分布不均匀很可能会产生很差的执行计划,导致严重的性能问问题。
4)InnoDB主键过长,导致二级索引过大。主键的选择,一般建议是整型。
以上介绍了索引的使用规则和建议。接下来介绍EXPLAIN工具。
互联网应用的开发,索引的调整往往是调优的一个重点, 特别是对数据库技术不熟练的团队,在数据量增长后可能会碰到各种性能问题,这通常是因为索引不佳而引起的。
EXPLAIN工具的应用不局限于索引的检查,但由于它和索引关系密切,下面将详细介绍下此工具。

3.5.4 如何使用EXPLAIN工具
无论是做研发还是DBA,都有必要学会EXPLAIN工具的使用。使用EXPLAIN工具可以确认执行计划是否良好,查询是否走了合理的索引。
不同版本的MySQL优化器各有不同,一些优化规则随着版本的发展可能会有变化,查询的执行计划随着数据的变化也可能会有变化。
对于这类情况可以使用EXPLAIN来验证自己的判断。
以下是对EXPLAIN工具的使用说明。
我们首先来介绍一下MySQL执行计划的调用方式,然后对执行计划显示的内容进行解读,最后来说一说MySQL执行计划的局限。
1.MySQL执行计划调用方式
我们使用EXPLAIN命令查看执行计划,语法形式类似如下语句。 EXPLAIN SELECT……
EXPLAIN命令还有如下两种变体。
1)EXPLAIN EXTENDEDSELECT…… 以上命令将执行计划“反编译”成SELECT语句,运行SHOW WARNINGS可得到被MySQL优化器优化后的查询语句。
2)EXPLAIN PARTITIONS SELECT…… 以上命令用于分区表的EXPLAIN命令。
2.执行计划包含的信息及解读
如下是一个显示执行计划的例子。
该例中EXPLAIN命令的输出信息可以告诉我们MySQL访问了哪些表,以及它是如何访问数据的。
里面有很重要的索引使用信息,我们可以据此判断我们的索引是否需要优化。
下面将详细阐述EXPLAIN输出的各项内容。
(1)id
id包含一组数字,表示查询中执行SELECT子句或操作表的顺序。
如果id相同,则执行顺序由上至下。
如果是子查询,id的序号会递增,id值越大则优先级越高,越先被执行。
如果id相同,则可以认为它们是一组,从上往下顺序执行。在所有组中,id值越大,优先级就越高,越先执行。???
(2)select_type
select_type表示查询中每个SELECT子句的类型(是简单还是复杂)。
下面是对select_type的详细说明。
SIMPLE:查询中不包含子查询或UNION。
查询中若包含任何复杂的子部分,最外层查询则被标记为PRIMARY。
在SELECT或WHERE列表中若包含了子查询,则该子查询被标记为SUBQUERY。
在FROM列表中包含的子查询将被标记为DERIVED(衍生)。
若第二个SELECT出现在UNION之后,则被标记为UNION;若UNION包含在FROM子句的子查询中,则外层SELECT将被标记为DERIVED。
从UNION表中获取结果的SELECT将被标记为UNION RESULT。
下面我们通过一个例子来说明查询的类型和执行的顺序。
第一行:id列为1,表示第一个SELECT,select_type列的PRIMARY表示该查询为外层查询,table列被标记为<derived3>,表示查询结果来自于一个衍生表,其中3代表该查询衍生自第3个SELECT查询,即id为3的SELECT。
第二行:id为3,表示该查询的执行次序为2(4→3),是整个查询中第3个SELECT的一部分。因为查询语句包含在FROM子句中,所以为DERIVED。
第三行:SELECT列表中的子查询,select_type为SUBQUERY,为整个查询中的第2个SELECT。
第四行:select_type为UNION,说明第4个SELECT是UNION里的第2个SELECT,最先执行。
第五行:代表从UNION的临时表中读取行的阶段,table列的<union1,4>表示对第1个和第4个SELECT的结果进行UNION操作。
(3)type
type表示MySQL在表中找到所需行的方式,又称“访问类型”,常见的类型如下: ALL-->index-->range-->ref-->eq_ref-->const,system-->NULL
以上类型,由左至右,由最差到最好。下面我们来详述每种类型。
ALL:FullTable Scan,MySQL将遍历全表以找到匹配的行。
index:FullIndex Scan,index与ALL区别为index类型只遍历索引树。
range:索引范围扫描,对索引的扫描开始于某一点,返回匹配值域的行,常见于between、<、>等的查询。
一般来说,索引范围扫描要检索的记录更少,因而成本也更低。大量的索引扫描,可能还会导致性能问题。
ref:非唯一性索引扫描,将返回匹配某个单独值的所有行。常见于使用非唯一索引或唯一索引的非唯一前缀进行的查找。
eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键或唯一索引扫描。
const、system:当MySQL对查询的某部分进行优化,并转换为一个常量时,可使用这些类型进行访问。
如将主键置于WHERE列表中,MySQL就能将该查询转换为一个常量。
system是const类型的特例,当查询的表只有一行的情况下,即可使用 system。explain select * from (select * from t1 where id=1) d1;
NULL:MySQL在优化过程中分解语句,执行时甚至不用访问表或索引。explain select * from t1 where id=(select min(id) from t2);
(4)possible_keys
possible_keys将指出MySQL能使用哪个索引在表中找到行,查询涉及的字段上若存在索引,则该索引将被列出,但不一定会被查询使用。
(5)key
key将显示MySQL在查询中实际使用到的索引,若没有使用索引,则显示为NULL。查询中若使用了覆盖索引,则该索引仅出现在key列表中。
(6)key_len
key_len表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。
上面的t1表col1和col2字段使用的是utf8字符集,utf8字符集的最大字符长度为3个字节,也就是说,它们可能需要12个字节存储一个值。
复合索引idx_col1_col2是创建在col1、col2列上的索引。
第一个查询(select * from t1 where col1='x')的key_len为13, 它只用到了复合索引idx_col1_col2的前半部分信息。
第二个查询(select * from t1 where col1='x' and col2='x')的key_len为26,它是完整的索引长度,由此可知t1表的索引 idx_col1_col2已被充分使用。
注意,key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得的,而不是通过表内检索得出的。
(7)ref
ref表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值。
如下的例子中,col1匹配t2表的col1,col2匹配 了一个常量,即ac:
(8)rows
rows表示MySQL根据表统计信息及索引选用的情况,估算地找到所需的记录所需要读取的行数。
如下的例子中,我们可以看到,在创建索引后,执行计划发生改变,所需要读取的行数减少了。
(9)Extra
Extra包含不适合在其他列中显示但十分重要的额外信息。它可能包含如下4种信息。
1)Using index。该值表示相应的SELECT操作中使用了覆盖索引。包含满足查询需要的所有数据的索引称为覆盖索引。
2)Using where。该值表示MySQL服务器在存储引擎收到记录后进行“后过滤”(Post-filter)。
如果查询未能使用索引,则Using where的作用只是提醒我们MySQL将用where子句来过滤结果集。
3)Using temporary。该值表示MySQL需要使用临时表来存储结果集,常见于排序和分组查询。
如上查询的EXPLAIN输出中Extra列同时有Using temporary和Using filesort,因此性能可能不佳。
如果我们更改了GROUP BY子 句,利用索引进行排序,则可以看到EXPLAIN输出里没有了Using temporary和Using filesort。
4)Using filesort。Using filesort即文件排序。MySQL中将无法利用索引完成的排序操作称为“文件排序”,常见于 order by 非索引列。
如果我们更改查询,利用索引进行排序,则可以优化掉文件排序,例如如下的查询,已经没有了filesort(文件排序)。

3.MySQL执行计划的局限
EXPLAIN不会告诉你关于触发器、存储过程的信息或用户自定义函数对查询的影响情况。
EXPLAIN不考虑各种Cache。
EXPLAIN不能显示MySQL在执行查询时所做的优化工作。
部分统计信息是估算的,并非精确值。
MySQL 5.6之前EXPALIN只能解释SELECT操作,其他操作需要重写为SELECT后才能查看执行计划。
如果FROM子句里有子查询,那么MySQL可能会执行这个子查询,如果有昂贵的子查询或使用了临时表的视图,那么 EXPLAIN其实会有很大的开销。

3.5.5 优化索引的方法学
以上介绍了索引的结构和使用索引的一些规则,随着项目经验的增长,开发人员对于数据库都有一个从不熟悉到熟悉的过程,
但由于开发人员的专注领域并不是数据库设计开发,而且不同的数据库产品也有差异,因此导致了部分开发人员对索引产生了一些误解。
生产环境中数据库出现性能问题,有80%的原因是索引策略导致的,表结构不易变动,而调整索引或SQL往往可以很快就能解决问题,在开发或上线后,可遵循以下的方法和步骤进行优化。
(1)有性能测量
在应用程序中记录访问数据库的性能日志,这样就可以对整体的访问吞吐有一个很直观很全面的统计,我们应该优化对数据库操作最频繁、最耗资源的那些SQL,
但由于性能统计框架的缺位,大部分中小公司更多地依赖于数据库自身的慢查询日志来定位耗时较长的SQL,由慢查询日志入手也是一个很好的出发点,
但可能存在一些滞后,不能及时发现性能问题,MySQL的慢查询日志默认记录查询时间超过1s的查询,4.3节将详细介绍慢查询日志。
(2)查看执行计划
找到消耗资源最多的查询请求后,可以使用EXPLAIN工具查看其执行计划,检查是否走的是合适的索引。
(3)优化索引
我们应该熟悉数据量、数据类型等信息及表之间的关系,按照自己的索引经验,调整或增加索引。
(4)测试验证
如果是线上生产环境,那么请不要在线上环境进行测试验证,除非是非常紧急的情况。应该选择在开发环境中尽量使用和线上环境一样的数据规模,来进行验证测试。
(5)上线
当确认优化达到了预期的效果后,就可以安排上线了。
有一个错误的观念是定期重建索引,这种方式在早期的传统数据库中用得很多,基于的主要理由是经过长期的生产运行, 索引变得越来越不平衡,但是否需要定期重建索引是有争议的。
MySQL在互联网行业一般是OLTP应用,索引重建将导致服务变得不可用,更重要的是,在绝大部分情况下,重建了和没有重建索引,性能上并没有什么区别,
唯一可能的场景是在大量删除导入数据后,会导致数据表严重变形。
如果需要重建索引,首先要证明,重建索引真的能够大大改善性能,否则建议不要做这种费力又不讨好的事情,
数据库索引本来就应该是“不好不坏”的状态,不要期望它始终以一种理想的状态在运行。

3.6 ID主键
下面先说明选择主键的注意事项。
1)建议主键是整型。
2)如果表中包含一列能够确保唯一、非空(NOTNULL),以及能够用来定位一条记录的字段,就不要因为传统而觉得一定要加上一个自增ID做主键。
3)主键也遵从索引的一些约定,注意联合主键的字段顺序。
4)为主键选择更有意义的名称,如ID这个名称太过笼统,表达的信息可能不准确。
1.自增ID主键
自增列是MySQL里的一种特殊的整型,我们定义一个列的整型的同时,可以设置它是否为自增的,一个表只能有一个列是自增列,且自增列必然是主键列。
自增列的默认起始值是1,默认可以按步长为1进行递增,自增列的增长将受两个MySQL全局参数的影响。
auto_increment_offset:确定AUTO_INCREMENT列值的起点。
auto_increment_increment:控制列值增加的间隔,即步长。
也可以单独定义某个表的起始值,如: mysql> ALTER TABLE tbl AUTO_INCREMENT = 100;
在复制环境中,设置这两个值可以减少主键冲突,关于这一点以后会在复制章节(第12章)中详述。
使用自增列的原因是唯一标识数据表的某行记录。它们也被用来优化表之间的连接。
连接单个列比连接多个列更快,连接整数列比连接其他大多数数据类型也更快。
总之,有很多使用它的理由。但也没有必要滥用自增ID,给每个表都设置一个自增ID做主键,有时可能存在另一个从逻辑上来说更加自然的主键。
另外,因为InnoDB引擎的ID主键是聚集索引,簇索引的数据和主键索引放在一起且是按主键索引进行排序的,那么基于自增主键的单个值查找和小范围查找将是最高效的。
研发人员有时倾向于使用字符串做主键,或者使用多个列的联合主键,但需要清楚一个事实:InnoDB的其他索引实际上存储了主键的值,这样做可能会导致索引空间大大增加。
InnoDB选择主键创建簇索引。如果没有主键,就会选取一个唯一非空的索引来替代;如果仍然找不到合适的列,那么将创建一个隐含的主键来创建簇索引。
选取一个唯一非空的索引做主键可能不是我们所期待的,一般的解决办法是删除我们不期望的主键(唯一索引),创建一个非空的自增列,再增加这个唯一索引。
例如,由于未定义主键,InnoDB自动把唯一索引idx_a_b(a,b)定义为主键了。我们想增加一个自增ID主键,并设置唯一索引 idx_a_b。idx_a_b表示这个索引是建立在a列和b列的复合索引。
ALTER TABLE table_name ADD COLUMN 'id' bigint UNSIGNED NOT NULL AUTO_INCREMENT first, DROP PRIMARY KEY, ADD PRIMARY KEY('id') , ADD INDEX idx_a_b on table_name(a,b);
2.自增ID可以插入指定的值
自增ID还有一个特性,那就是如果插入0值或NULL值,InnoDB会认为没有设定值,然后帮你自增一个值。
所以可以利用这个特性生成全局唯一ID、序列。
如果数据分片到许多实例、机器上,那么就需要一个全局唯一ID来标识记录了。如下是官方文档推荐的一个创建唯一序列的方法。
创建一个表,用来控制顺序计数器并使其初始化。 mysql> CREATE TABLE sequence (id INT NOT NULL); mysql> INSERT INTO sequence VALUES (0);
使用该表产生如下的序列数。 mysql> UPDATE sequence SET id=LAST_INSERT_ID(id+1); mysql> SELECT LAST_INSERT_ID();
高并发下,LAST_INSERT_ID函数可能会有一定的性能问题,但这种方法很简单,一般情况下是可以满足需要的。

3.7 字符集和国际化支持
3.7.1 什么是字符集
字符集(character set)是一套符号和编码。校对规则(collation)是在字符集内用于比较字符的一套规则,即字符集的排序规则。
假设我们有一个字母表使用了4个字母:'A'、'B'、'a'、'b'。现在为每个字母赋予一个数值:'A'=0,'B'=1,'a'=2,'b'=3,字母'A'是一个符号,数字0是'A'的编码,
那么这4个字母和它们的编码组合在一起就是一个字符集。
我们可以认为字符集是字符的二进制的编码方式,即二进制编码到一套符号的映射。
对于字符集,MySQL能够做如下这些事情。
使用多种字符集来存储字符串。
使用多种校对规则来比较字符串。
在同一台服务器、同一个数据库甚至在同一个表中,使用不同的字符集或校对规则来混合字符串。
允许定义任何级别的字符集和校对规则。
可使用SHOW CHARACTER SET语句列出可用的字符集。 mysql>SHOW CHARACTER SET;
可使用SHOW COLLATION语句列出utf8字符集的校对规则。 mysql>SHOW COLLATION LIKE 'utf8%';

3.7.2 国际化支持
因为现存编码不能在多语言电脑环境中使用,而且字符数有局限,所以诞生了Unicode(统一码、万国码、国际码、单一 码)。
Unicode是计算机科学领域里的一项业界标准。它对世界上大部分的文字系统进行了整理、编码,使得电脑可以用更为简单的方式来呈现和处理文字。
一个字符的Unicode编码是确定的,但Unicode的实现方式不同于编码方式。
在实际传输过程中,由于不同系统平台的设计不一定都是一致的,且出于节省空间的目的,对Unicode编码的实现方式也有所不同。
Unicode的实现方式称为Unicode转换格式 (Unicode Transformation Format,UTF)。
这其中有一种UTF-8编码,它是一种变长编码,MySQL中经常使用的utf8字符集就是 UTF-8编码。
UTF-8编码的思想是不同的Unicode字符采用变长字节序列编码:
基本拉丁字母、数字和标点符号使用一个字节;
大多数的欧洲和中东手写字母适合两个字节序列;
韩语、中文和日本象形文字使用三个字节序列。
utf8是MySQL存储Unicode数据的一种可选方法,MySQL还有其他的存储Unicode数据的字符集,这里就不做额外介绍了。
注意:utf8字符集的最大长度是3个字节(中文3个字节,对于英文数字仍然使用一个字节),默认校对(排序)规则为utf8_general_ci(不区分大小写)。
如果是CHAR类型,那么可能会导致空间浪费,因为任意字符都需要3个字节来存储。
如果是VARCHAR类型,那么英文、数字、标点符号只需要1个字符来存储即可。
对于utf8,还需要了解这样两个概念:超集、子集。
有字符集A、B。如果B支持的所有字符A都支持,那么字符集A是字符集B的超集。
如果A是B的超集,那么字符集B是字符集A的子集。
比如,GBK字符集是GB2312字符集的超集,它们又都是ASCII字符集的超集。

3.7.3 字符集设置
字符集设置可以分为两类:一类是创建对象的默认值;另一类是控制server端和client端交互通信的配置。
1.创建对象的默认值
字符集和校对规则有4个级别的默认设置:服务器级、数据库级、表级和连接级。
使用如下语句列出可用的字符集。 mysql> SHOW CHARACTER SET;
列出可用的校对规则可使用如下语句。 mysql> SHOW COLLATION
更低级别的配置会继承更高级别的配置。例如,如果创建一个数据库,不指定字符集,那么它会继承服务器级的默认字符集。
对于生产环境的升级脚本,建议在表级别指定默认的字符集,以避免歧义或继承了错的数据库默认字符集。
2.控制server端和client端交互通信的配置
绝大部分MySQL客户端都不具备同时支持多种字符集的能力,每次都只能使用一种字符集。
客户和服务器之间的字符集转 换工作是由如下几个MySQL系统变量来控制的。
character_set_server:MySQL Server默认字符集。
character_set_database:数据库默认字符集。
character_set_client:MySQL Server假定客户端发送的查询使用的字符集。
character_set_connection:MySQL Server接收客户端发布的查询后,将其转换为character_set_connection变量指定的字符集。
character_set_result:MySQL Server把结果集和错误信息转换为character_set_result指定的字符集,并发送给客户端。
图3-13说明了字符集的转换过程。 图3-13来自《High Performance MySQL》一书。
由图3-13可以知道,
当一个客户端和数据库打交道时,客户端、连接、操作系统、数据库、输出结果都有自己的字符集设置,如果字符集不一致,那么就可能需要进行转换,
一般情况下,目标字符集应确保是源字符集的超集,以确保转换正常,如果目标字符集不能容纳源字符集的编码或设置错了字符集,那么转换会导致乱码。
以下列举了一些常用的设置字符集的操作。 SET NAMES x
通过MySQL客户端导入数据时,在使用“mysql>source/path/imp_data.sql”命令的过程中有时可能会出现乱码,这时可能需要先运行SET NAMES x语句设置字符集。
SET NAMES x语句与下面这3个语句是等价的。
mysql> SET character_set_client = x;
mysql> SET character_set_connection = x;
mysql> SET character_set_results = x;
再来看看SET CHARACTERSET x语句,它等同于下面这3条语句。
mysql> SET character_set_client = x;
mysql> SET character_set_results = x;
mysql> SET collation_connection = @@collation_database;
有些客户端命令支持“--default-character-set”选项,此选项允许用户连接时设置字符集。它等同于以下这3条语句。
mysql> SET character_set_client = x;
mysql> SET character_set_connection = x;
mysql> SET character_set_results = x;
如果数据库服务器中有很多数据库使用不同的字符集,且有各种不同语系的客户端,很复杂,那么使用init-connect=SET NAMES binary是一种可以考虑的方式。
这个指令的目的是让clinet与server交互的时候以as-is模式(是什么就是什么,不做任何转 换)来传送。
索引可用来排序,但如果指定了用其他的非默认排序规则,那么将不能利用索引进行排序,比如在下面的语句中:
EXPLAIN SELECT col_1,col_2 FROM table_name ORDER BY col_2 COLLATE utf8_bin
col_2字段使用的是utf8字符集,且其上有索引,默认的utf8字符集的排序规则是utf8_general_ci,而上面的案例指定的是用 utf8_bin进行排序,那么EXPLAIN输出可以看到有filesort(文件排序),即没有利用到索引进行排序。
同理,如果连接两张表使用的连接列不是一样的字符集,那么也不能利用索引,因为必须先执行转换工作,可用EXPLAIN EXTENDED先进行确认。
默认情况下,MySQL的字符集是latin1(ISO_8859_1)。l
atin1字符集是单字节编码,应用于英文系列,最多能表示的字符范围是0~255(编码范围是0x00~0xFF),其中0x00~0x7F之间和ASCII码完全一致,因此它是向下兼容ASCII的。
latin1字符有限,如用来存储中文、日文、韩文、希伯来文等语言时往往会导致乱码,为了避免乱码,支持国际化,个人建议是生产环境都统一使用utf8字符集,除非你有特殊理由。
以下是关于在生产环境中使用utf8字符集的一些说明和注意事项。
1)为什么生产环境中建议使用utf8字符集?
主要是为了维护和开发都方便。大家都统一使用utf8字符集,将一劳永逸地避免各种乱码问题。
一个数据库如果存在各种字符集,就会很容易出错,也会大大提高开发的难度。国际化支持也是使用utf8字符集的一个考虑。
当然,utf8字符集也有弊端,主要就是空间的消耗。比如,对于CHAR(10),将需要用到30个字节来存放。
对于 VARCHAR(10),则是按照字符串的长度来存储的,虽然不存在过多的磁盘空间消耗,但MySQL内部实现的一些数据结构,如临时表需要分配最大可能的长度,也可能导致内存大大增加。
更多的空间还意味着更差的I/O性能。
有时,我们可能为了节省空间(如果空间真的是一个需要考虑的因素)而选择其他字符集(如用GBK存储汉字),
对于大批量的机器,特定的服务选择特定的字符集,这种情况下所节省的空间也是很可观的,
但对于一般的中小型公司,建议统一使 用utf8,一劳永逸地解决乱码问题是更明智的选择。
2)如何判断多字节字符集的字符串长度?
LENGTH()返回值为字符串的长度,单位为字节。一个多字节字符算作多字节。
CHAR_LENGTH()返回值为字符串的长度,长度的单位为字符。一个多字节字符算作一个单字符。
例如:对于一个包含了5个二字节的字符集,LENGTH()返回值为10,而CHAR_LENGTH()的返回值为5。
3)有时创建索引的时候,可能会提示出错。
MySQL会假定每个字符有3个字节,由于索引长度有限制,那么创建索引的时候,可能会提示下面这样的错误。
ERROR 1071 (42000): Specified key was too long; max key length is 1000 bytes
这时需要明白,自己创建的是多字节字符集,字节数实际上超过1000了。
4)UTF-8是可变长度的编码,使用1到4个字节来存储。但MySQL 5.1及以前的版本,对UTF-8的支持并不彻底,它的utf8只是3字节字符集,有些文字符号是不能存储的,如emoji表情。
MySQL5.5增加了字符集utf8mb4(4-Byte UTF-8 Unicode Encoding),可以存储一些MySQL utf8不能存储的字符,需要留意的是,
设置了utf8mb4字符集后,需要重启MySQL Server才能生效。

小结:
本章介绍了一般开发过程中需要了解的数据库知识。
理解软件架构的一些概念和数据建模有助于更全面地构建自己的知识体系,从而为研发中大型项目打下基础。
SQL是绝大多数IT人员甚至也是部分非IT专业人员的必备技能,在项目实战的过程中,你的SQL技能会越来越熟练。
研发是一个很庞大的体系,我们选取PHP数据库开发讲述了一些数据库操作的知识,希望对其他开发领域也有借鉴作用。
本章还讲述了索引、主键、字符集等知识,这些内容是开发过程中最普遍使用的知识。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!