MySQL8-秘籍-全-
MySQL8 秘籍(全)
原文:
zh.annas-archive.org/md5/F4A043A5A2DBFB9A7ADE5DAA21AA8E7F译者:飞龙
前言
MySQL 是当今世界上最受欢迎和广泛使用的关系型数据库之一。最近发布的 MySQL 8 承诺比以往更好、更高效,为您提供高性能的查询结果和作为管理员的易配置性。
这本书适合谁
这本书适合广泛的读者。曾在早期版本的 MySQL 上工作过的 MySQL 数据库管理员和开发人员将了解 MySQL 8 的特性以及如何利用它们。对于曾在其他关系型数据库管理系统(如 Oracle、MSSQL、PostgreSQL 和 Db2)上工作过的读者,这本书将是 MySQL 8 的快速入门指南。对于初学者,这本书是一本手册;他们可以参考这些技巧,找到问题的快速解决方案。
最重要的是,这本书让你“做好生产准备”。阅读完这本书后,您将有信心处理具有大型数据集的繁忙数据库服务器。
在我 10 年的 MySQL 经验中,我见证了小错误导致重大故障。在这本书中,涵盖了许多可能出错的场景,并标有警告标签。
这些主题以一种不需要初学者来回查找理解概念的方式引入。每个主题都提供了到 MySQL 文档或其他来源的参考链接,读者可以参考链接了解更多细节。
由于这本书是为初学者而写的,可能会有一些你已经了解的主题;可以随意跳过它们。
这本书涵盖了什么
熟能生巧。但是要进行练习,你需要一些知识和训练。这本书可以帮助你。这本书涵盖了大多数日常和实际场景。
第一章,“MySQL 8-安装和升级”,描述了如何在不同版本的 Linux 上安装 MySQL 8,从以前的稳定版本升级到 MySQL 8,以及从 MySQL 8 降级。
第二章,“使用 MySQL”,带你了解 MySQL 的基本用法,如创建数据库和表;以各种方式插入、更新、删除和选择数据;保存到不同的目的地;对结果进行排序和分组;连接表;管理用户;其他数据库元素,如触发器、存储过程、函数和事件;以及获取元数据信息。
第三章,“使用 MySQL(高级)”,涵盖了 MySQL 8 的最新添加,如 JSON 数据类型、通用表达式和窗口函数。
第四章,“配置 MySQL”,向您展示如何配置 MySQL 和基本配置参数。
第五章,“事务”,解释了关系型数据库管理系统的四个隔离级别以及如何使用 MySQL 进行事务处理。
第六章,“二进制日志”,演示了如何启用二进制日志记录,二进制日志的各种格式,以及如何从二进制日志中检索数据。
第七章,“备份”,涵盖了各种类型的备份,每种方法的利弊以及根据您的需求选择哪种备份方法。
第八章,“恢复数据”,涵盖了如何从不同的备份中恢复数据。
第九章,“复制”,解释了如何设置各种复制拓扑。关于将从主从复制切换到链式复制的从服务器和将从链式复制切换到主从复制的从服务器的技巧将会引起读者的兴趣。
第十章,“表维护”,涵盖了克隆表。管理大表是这一章将使您成为大师的内容。本章还涵盖了第三方工具的安装和使用。
第十一章,“管理表空间”,涉及教读者如何调整、创建、复制和管理表空间的配方。
第十二章,“管理日志”,带领读者了解错误、一般查询、慢查询和二进制日志。
第十三章,“性能调优”,详细解释了查询和模式调优。本章中有大量的配方涵盖了这一内容。
第十四章,“安全”,侧重于安全方面。涵盖了安全安装、限制网络和用户、设置和重置密码等内容的配方。
充分利用本书
对任何 Linux 系统的基本知识使您更容易理解本书。
使用的约定
本书中使用了许多文本约定。
CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。例如:“MySQL 对 libaio 库有依赖性。”
当我们希望引起您对命令行语句的特定部分的注意时,相关行或项目将以粗体显示:
shell> sudo yum repolist all | grep mysql8
mysql80-community/x86_64 MySQL 8.0 Community Server enabled: 16
mysql80-community-source MySQL 8.0 Community Server disabled
任何命令行输入或输出都以以下方式编写:
mysql> ALTER TABLE table_name REMOVE PARTITIONING;
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以这种形式出现在文本中。例如:“选择 Development Releases 标签以获取 MySQL 8.0,然后选择操作系统和版本。”
警告或重要说明会以这种形式出现。
提示和技巧会出现在这样的形式中。
章节
在本书中,您将经常看到几个标题(准备就绪、如何做…、工作原理…、还有更多…和另请参阅)。
为了清晰地说明如何完成配方,使用以下各节:
准备就绪
本节告诉您配方中可以期待什么,并描述了为配方设置任何软件或所需的任何初步设置的方法。
如何做…
本节包含了遵循配方所需的步骤。
工作原理…
本节通常包括对上一节发生的事情的详细解释。
还有更多…
本节包括有关配方的其他信息,以使您对配方更加了解。
另请参阅
本节提供了有用的链接,以获取其他有用的配方信息。
联系我们
我们始终欢迎读者的反馈。
一般反馈:发送电子邮件至feedback@packtpub.com,并在消息主题中提及书名。如果您对本书的任何方面有疑问,请给我们发送电子邮件至questions@packtpub.com。
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误确实会发生。如果您在本书中发现错误,我们将不胜感激。请访问www.packtpub.com/submit-errata,选择您的书,点击勘误提交表单链接,并输入详细信息。
盗版:如果您在互联网上发现我们作品的任何非法副本,我们将不胜感激,如果您能向我们提供位置地址或网站名称。请通过copyright@packtpub.com与我们联系,并提供材料链接。
如果您有兴趣成为作者:如果您在某个专题上有专业知识,并且有兴趣撰写或为一本书做出贡献,请访问 authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用了这本书,为什么不在购买它的网站上留下评论呢?潜在的读者可以看到并使用您的公正意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。谢谢!
有关 Packt 的更多信息,请访问 packtpub.com。
第一章:MySQL 8-安装和升级
在本章中,我们将介绍以下配方:
-
使用 YUM/APT 安装 MySQL
-
使用 RPM 或 DEB 文件安装 MySQL 8.0
-
使用通用二进制文件在 Linux 上安装 MySQL
-
启动或停止 MySQL 8
-
卸载 MySQL 8
-
使用 systemd 管理 MySQL 服务器
-
从 MySQL 8.0 降级
-
升级到 MySQL 8.0
-
安装 MySQL 实用程序
介绍
在本章中,您将了解 MySQL 8 安装、升级和降级步骤。有五种不同的安装或升级方式;本章涵盖了三种最常用的安装方法:
-
软件存储库(YUM 或 APT)
-
RPM 或 DEB 文件
-
通用二进制文件
-
Docker(未涵盖)
-
源代码编译(未涵盖)
如果您已经安装了 MySQL 并希望升级,请查看“升级到 MySQL 8”部分中的升级步骤。如果您的安装损坏,请查看“升级到 MySQL 8”部分中的卸载步骤。
安装之前,请记下操作系统和 CPU 架构。遵循的约定如下:
MySQL Linux RPM 包分发标识符
| 分发值 | 预期的用途 |
|---|---|
| el6, el7 | Red Hat 企业 Linux,Oracle Linux,CentOS 6 或 7 |
| fc23, fc24, fc25 | Fedora 23, 24 或 25 |
| sles12 | SUSE Linux 企业服务器 12 |
MySQL Linux RPM 包 CPU 标识符
| CPU 值 | 预期的处理器类型或系列 |
|---|---|
| i386, i586, i686 | 奔腾处理器或更高,32 位 |
| x86_64 | 64 位 x86 处理器 |
| ia64 | Itanium(IA-64)处理器 |
MySQL Debian 和 Ubuntu 7 和 8 安装包 CPU 标识符
| CPU 值 | 预期的处理器类型或系列 |
|---|---|
| i386 | 奔腾处理器或更高,32 位 |
| amd64 | 64 位 x86 处理器 |
MySQL Debian 6 安装包 CPU 标识符
| CPU 值 | 预期的处理器类型或系列 |
|---|---|
| i686 | 奔腾处理器或更高,32 位 |
| x86_64 | 64 位 x86 处理器 |
使用 YUM/APT 安装 MySQL
最常见和最简单的安装方式是通过软件存储库,您可以将官方的 Oracle MySQL 存储库添加到列表中,并通过软件包管理软件安装 MySQL。
主要有两种类型的存储库软件:
-
YUM(Centos,Red Hat,Fedora 和 Oracle Linux)
-
APT(Debian,Ubuntu)
如何做...
让我们看看以下安装 MySQL 8 的步骤:
使用 YUM 存储库
- 查找 Red Hat 或 CentOS 版本:
shell> cat /etc/redhat-release
CentOS Linux release 7.3.1611 (Core)
- 将 MySQL Yum 存储库添加到系统的存储库列表中。这是一个一次性操作,可以通过安装 MySQL 提供的 RPM 来执行。
您可以从dev.mysql.com/downloads/repo/yum/下载 MySQL YUM 存储库,并根据您的操作系统选择文件。
使用以下命令安装下载的发布包,将名称替换为下载的 RPM 包的特定于平台和版本的包名称:
shell> sudo yum localinstall -y mysql57-community-release-el7-11.noarch.rpm
Loaded plugins: fastestmirror
Examining mysql57-community-release-el7-11.noarch.rpm: mysql57-community-release-el7-11.noarch
Marking mysql57-community-release-el7-11.noarch.rpm to be installed
Resolving Dependencies
--> Running transaction check
---> Package mysql57-community-release.noarch 0:el7-11 will be installed
--> Finished Dependency Resolution
~
Verifying : mysql57-community-release-el7-11.noarch 1/1
Installed:
mysql57-community-release.noarch 0:el7-11
Complete!
- 或者您可以复制链接位置并直接使用 RPM 进行安装(安装后可以跳过下一步):
shell> sudo rpm -Uvh "https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm"
Retrieving https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm
Preparing... ################################# [100%]
Updating / installing...
1:mysql57-community-release-el7-11 ################################# [100%]
- 验证安装:
shell> yum repolist enabled | grep 'mysql.*-community.*'
mysql-connectors-community/x86_64 MySQL Connectors Community 42
mysql-tools-community/x86_64 MySQL Tools Community 53
mysql57-community/x86_64 MySQL 5.7 Community Server 227
- 设置发布系列。在撰写本书时,MySQL 8 不是一般可用性(GA)版本。因此,MySQL 5.7 将被选为默认发布系列。要安装 MySQL 8,您必须将发布系列设置为 8:
shell> sudo yum repolist all | grep mysql
mysql-cluster-7.5-community/x86_64 MySQL Cluster 7.5 Community disabled
mysql-cluster-7.5-community-source MySQL Cluster 7.5 Community disabled
mysql-cluster-7.6-community/x86_64 MySQL Cluster 7.6 Community disabled
mysql-cluster-7.6-community-source MySQL Cluster 7.6 Community disabled
mysql-connectors-community/x86_64 MySQL Connectors Community enabled: 42
mysql-connectors-community-source MySQL Connectors Community disabled
mysql-tools-community/x86_64 MySQL Tools Community enabled: 53
mysql-tools-community-source MySQL Tools Community - Sou disabled
mysql-tools-preview/x86_64 MySQL Tools Preview disabled
mysql-tools-preview-source MySQL Tools Preview - Sourc disabled
mysql55-community/x86_64 MySQL 5.5 Community Server disabled
mysql55-community-source MySQL 5.5 Community Server disabled
mysql56-community/x86_64 MySQL 5.6 Community Server disabled
mysql56-community-source MySQL 5.6 Community Server disabled
mysql57-community/x86_64 MySQL 5.7 Community Server enabled: 227
mysql57-community-source MySQL 5.7 Community Server disabled
mysql80-community/x86_64 MySQL 8.0 Community Server disabled
mysql80-community-source MySQL 8.0 Community Server disabled
- 禁用
mysql57-community并启用mysql80-community:
shell> sudo yum install yum-utils.noarch -y
shell> sudo yum-config-manager --disable mysql57-community
shell> sudo yum-config-manager --enable mysql80-community
- 验证
mysql80-community是否已启用:
shell> sudo yum repolist all | grep mysql8
mysql80-community/x86_64 MySQL 8.0 Community Server enabled: 16
mysql80-community-source MySQL 8.0 Community Server disabled
- 安装 MySQL 8:
shell> sudo yum install -y mysql-community-server
Loaded plugins: fastestmirror
mysql-connectors-community | 2.5 kB 00:00:00
mysql-tools-community | 2.5 kB 00:00:00
mysql80-community | 2.5 kB 00:00:00
Loading mirror speeds from cached hostfile
* base: mirror.web-ster.com
* epel: mirrors.cat.pdx.edu
* extras: mirrors.oit.uci.edu
* updates: repos.lax.quadranet.com
Resolving Dependencies
~
Transaction test succeeded
Running transaction
Installing : mysql-community-common-8.0.3-0.1.rc.el7.x86_64 1/4
Installing : mysql-community-libs-8.0.3-0.1.rc.el7.x86_64 2/4
Installing : mysql-community-client-8.0.3-0.1.rc.el7.x86_64 3/4
Installing : mysql-community-server-8.0.3-0.1.rc.el7.x86_64 4/4
Verifying : mysql-community-libs-8.0.3-0.1.rc.el7.x86_64 1/4
Verifying : mysql-community-common-8.0.3-0.1.rc.el7.x86_64 2/4
Verifying : mysql-community-client-8.0.3-0.1.rc.el7.x86_64 3/4
Verifying : mysql-community-server-8.0.3-0.1.rc.el7.x86_64 4/4
Installed:
mysql-community-server.x86_64 0:8.0.3-0.1.rc.el7
Dependency Installed:
mysql-community-client.x86_64 0:8.0.3-0.1.rc.el7
mysql-community-common.x86_64 0:8.0.3-0.1.rc.el7
mysql-community-libs.x86_64 0:8.0.3-0.1.rc.el7
Complete!
- 您可以使用以下命令检查已安装的软件包:
shell> rpm -qa | grep -i 'mysql.*8.*'
perl-DBD-MySQL-4.023-5.el7.x86_64
mysql-community-libs-8.0.3-0.1.rc.el7.x86_64
mysql-community-common-8.0.3-0.1.rc.el7.x86_64
mysql-community-client-8.0.3-0.1.rc.el7.x86_64
mysql-community-server-8.0.3-0.1.rc.el7.x86_64
使用 APT 存储库
- 将 MySQL APT 存储库添加到系统的存储库列表中。这是一个一次性操作,可以通过安装 MySQL 提供的
.deb文件来执行
您可以从dev.mysql.com/downloads/repo/apt/下载 MySQL APT 存储库。
或者您可以复制链接位置并使用wget直接在服务器上下载。您可能需要安装wget(sudo apt-get install wget):
shell> wget "https://repo.mysql.com//mysql-apt-config_0.8.9-1_all.deb"
- 使用以下命令安装下载的发布软件包,替换为下载的 APT 软件包的特定平台和版本的软件包名称:
shell> sudo dpkg -i mysql-apt-config_0.8.9-1_all.deb
(Reading database ... 131133 files and directories currently installed.)
Preparing to unpack mysql-apt-config_0.8.9-1_all.deb ...
Unpacking mysql-apt-config (0.8.9-1) over (0.8.9-1) ...
Setting up mysql-apt-config (0.8.9-1) ...
Warning: apt-key should not be used in scripts (called from postinst maintainerscript of the package mysql-apt-config)
OK
- 在安装软件包时,将要求您选择 MySQL 服务器和其他组件的版本。按Enter进行选择,使用上下键进行导航。
选择 MySQL 服务器和集群(当前选择:mysql-5.7)。
选择 mysql-8.0 预览(在撰写本文时,MySQL 8.0 尚未 GA)。您可能会收到警告,例如 MySQL 8.0-RC 请注意,MySQL 8.0 目前是一个 RC。它应该只安装以预览 MySQL 即将推出的功能,并不建议在生产环境中使用。 (RC是发布候选的缩写)。
如果要更改发布版本,请执行以下操作:
shell> sudo dpkg-reconfigure mysql-apt-config
- 使用以下命令从 MySQL APT 存储库更新软件包信息(此步骤是强制性的):
shell> sudo apt-get update
- 安装 MySQL。在安装过程中,您需要为 MySQL 安装的 root 用户提供密码。记住密码;如果忘记了,您将不得不重置 root 密码(参考重置 root 密码部分)。这将安装 MySQL 服务器的软件包,以及客户端和数据库公共文件的软件包:
shell> sudo apt-get install -y mysql-community-server
~
Processing triggers for ureadahead (0.100.0-19) ...
Setting up mysql-common (8.0.3-rc-1ubuntu14.04) ...
update-alternatives: using /etc/mysql/my.cnf.fallback to provide /etc/mysql/my.cnf (my.cnf) in auto mode
Setting up mysql-community-client-core (8.0.3-rc-1ubuntu14.04) ...
Setting up mysql-community-server-core (8.0.3-rc-1ubuntu14.04) ...
~
- 验证软件包。
ii表示软件包已安装:
shell> dpkg -l | grep -i mysql
ii mysql-apt-config 0.8.9-1 all Auto configuration for MySQL APT Repo.
ii mysql-client 8.0.3-rc-1ubuntu14.04 amd64 MySQL Client meta package depending on latest version
ii mysql-common 8.0.3-rc-1ubuntu14.04 amd64 MySQL Common
ii mysql-community-client 8.0.3-rc-1ubuntu14.04 amd64 MySQL Client
ii mysql-community-client-core 8.0.3-rc-1ubuntu14.04 amd64 MySQL Client Core Binaries
ii mysql-community-server 8.0.3-rc-1ubuntu14.04 amd64 MySQL Server
ii mysql-community-server-core 8.0.3-rc-1ubuntu14.04 amd64 MySQL Server Core Binaires
使用 RPM 包安装 MySQL 8.0
使用存储库安装 MySQL 需要访问公共互联网。出于安全考虑,大多数生产机器不连接到互联网。在这种情况下,您可以在系统管理上下载 RPM 或 DEB 文件,并将其复制到生产机器。
主要有两种类型的安装文件:
-
RPM(CentOS,Red Hat,Fedora 和 Oracle Linux)
-
DEB(Debian,Ubuntu)
有多个软件包需要安装。以下是每个软件包的列表和简要描述:
-
mysql-community-server:数据库服务器和相关工具。 -
mysql-community-client:MySQL 客户端应用程序和工具。 -
mysql-community-common:服务器和客户端库的公共文件。 -
mysql-community-devel:MySQL 数据库客户端应用程序的开发头文件和库,例如 Perl MySQL 模块。 -
mysql-community-libs:某些语言和应用程序需要动态加载和使用 MySQL 的共享库(libmysqlclient.so*)。 -
mysql-community-libs-compat:旧版本的共享库。如果您安装了针对旧版本 MySQL 动态链接的应用程序,但希望升级到当前版本而不破坏库依赖关系,请安装此软件包。
如何操作...
让我们看看如何使用以下类型的包:
使用 RPM 包
- 从 MySQL 下载页面
dev.mysql.com/downloads/mysql/下载 MySQL RPM tar 包,选择您的操作系统和 CPU 架构。在撰写本文时,MySQL 8.0 尚未 GA。如果它仍处于开发系列中,请选择 Development Releases 选项卡以获取 MySQL 8.0,然后选择操作系统和版本:
shell> wget 'https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.3-0.1.rc.el7.x86_64.rpm-bundle.tar'
~
Saving to: ‘mysql-8.0.3-0.1.rc.el7.x86_64.rpm-bundle.tar’
~
- 解压软件包:
shell> tar xfv mysql-8.0.3-0.1.rc.el7.x86_64.rpm-bundle.tar
- 安装 MySQL:
shell> sudo rpm -i mysql-community-{server-8,client,common,libs}*
- RPM 无法解决依赖关系问题,安装过程可能会出现问题。如果遇到此类问题,请使用此处列出的
yum命令(您应该可以访问依赖软件包):
shell> sudo yum install mysql-community-{server-8,client,common,libs}* -y
- 验证安装:
shell> rpm -qa | grep -i mysql-community
mysql-community-common-8.0.3-0.1.rc.el7.x86_64
mysql-community-libs-compat-8.0.3-0.1.rc.el7.x86_64
mysql-community-libs-8.0.3-0.1.rc.el7.x86_64
mysql-community-server-8.0.3-0.1.rc.el7.x86_64
mysql-community-client-8.0.3-0.1.rc.el7.x86_64
使用 APT 包
- 从 MySQL 下载页面
dev.mysql.com/downloads/mysql/下载 MySQL APT TAR:
shell> wget "https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-server_8.0.3-rc-1ubuntu16.04_amd64.deb-bundle.tar"
~
Saving to: ‘mysql-server_8.0.3-rc-1ubuntu16.04_amd64.deb-bundle.tar’
~
- 解压软件包:
shell> tar -xvf mysql-server_8.0.3-rc-1ubuntu16.04_amd64.deb-bundle.tar
- 安装依赖项。如果尚未安装,您可能需要安装
libaio1软件包:
shell> sudo apt-get install -y libaio1
- 升级
libstdc++6到最新版本:
shell> sudo add-apt-repository ppa:ubuntu-toolchain-r/test
shell> sudo apt-get update
shell> sudo apt-get upgrade -y libstdc++6
- 将
libmecab2升级到最新版本。如果未包括universe,则在文件末尾添加以下行(例如,zesty):
shell> sudo vi /etc/apt/sources.list
deb http://us.archive.ubuntu.com/ubuntu zesty main universe
shell> sudo apt-get update
shell> sudo apt-get install libmecab2
- 使用以下命令预配置 MySQL 服务器包。它会要求您设置 root 密码:
shell> sudo dpkg-preconfigure mysql-community-server_*.deb
- 安装数据库公共文件包、客户端包、客户端元包、服务器包和服务器元包(按顺序);您可以使用单个命令完成:
shell> sudo dpkg -i mysql-{common,community-client-core,community-client,client,community-server-core,community-server,server}_*.deb
- 安装共享库:
shell> sudo dpkg -i libmysqlclient21_8.0.1-dmr-1ubuntu16.10_amd64.deb
- 验证安装:
shell> dpkg -l | grep -i mysql
ii mysql-client 8.0.3-rc-1ubuntu14.04 amd64 MySQL Client meta package depending on latest version
ii mysql-common 8.0.3-rc-1ubuntu14.04 amd64 MySQL Common
ii mysql-community-client 8.0.3-rc-1ubuntu14.04 amd64 MySQL Client
ii mysql-community-client-core 8.0.3-rc-1ubuntu14.04 amd64 MySQL Client Core Binaries
ii mysql-community-server 8.0.3-rc-1ubuntu14.04 amd64 MySQL Server
ii mysql-community-server-core 8.0.3-rc-1ubuntu14.04 amd64 MySQL Server Core Binaires
ii mysql-server 8.0.3-rc-1ubuntu16.04 amd64 MySQL Server meta package depending on latest version
使用通用二进制文件在 Linux 上安装 MySQL
使用软件包安装需要先安装一些依赖项,并可能与其他软件包冲突。在这种情况下,您可以使用下载页面上提供的通用二进制文件安装 MySQL。二进制文件是使用先进的编译器预编译的,并使用最佳选项构建以获得最佳性能。
如何做到...
MySQL 依赖于libaio库。如果未在本地安装此库,数据目录初始化和随后的服务器启动步骤将失败。
在基于 YUM 的系统上:
shell> sudo yum install -y libaio
在基于 APT 的系统上:
shell> sudo apt-get install -y libaio1
从 MySQL 下载页面下载 TAR 二进制文件,网址为dev.mysql.com/downloads/mysql/,然后选择 Linux - 通用作为操作系统并选择版本。您可以直接使用wget命令直接在服务器上下载:
shell> cd /opt
shell> wget "https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.3-rc-linux-glibc2.12-x86_64.tar.gz"
使用以下步骤安装 MySQL:
- 添加
mysql组和mysql用户。所有文件和目录都应该在mysql用户下:
shell> sudo groupadd mysql
shell> sudo useradd -r -g mysql -s /bin/false mysql
- 这是安装位置(您可以将其更改为另一个位置):
shell> cd /usr/local
- 解压二进制文件。将解压后的二进制文件保留在相同位置,并将其符号链接到安装位置。通过这种方式,您可以保留多个版本,并且非常容易升级。例如,您可以下载另一个版本并将其解压到不同的位置;在升级时,您只需要更改符号链接:
shell> sudo tar zxvf /opt/mysql-8.0.3-rc-linux-glibc2.12-x86_64.tar.gz
mysql-8.0.3-rc-linux-glibc2.12-x86_64/bin/myisam_ftdump
mysql-8.0.3-rc-linux-glibc2.12-x86_64/bin/myisamchk
mysql-8.0.3-rc-linux-glibc2.12-x86_64/bin/myisamlog
mysql-8.0.3-rc-linux-glibc2.12-x86_64/bin/myisampack
mysql-8.0.3-rc-linux-glibc2.12-x86_64/bin/mysql
~
- 创建符号链接:
shell> sudo ln -s mysql-8.0.3-rc-linux-glibc2.12-x86_64 mysql
- 创建必要的目录并将所有权更改为
mysql:
shell> cd mysql
shell> sudo mkdir mysql-files
shell> sudo chmod 750 mysql-files
shell> sudo chown -R mysql .
shell> sudo chgrp -R mysql .
- 初始化
mysql,生成临时密码:
shell> sudo bin/mysqld --initialize --user=mysql
~
2017-12-02T05:55:10.822139Z 5 [Note] A temporary password is generated for root@localhost: Aw=ee.rf(6Ua
~
- 为 SSL 设置 RSA。有关 SSL 的更多详细信息,请参阅第十四章“使用 X509 部分设置加密连接”。请注意,为
root@localhost生成了一个临时密码:eJQdj8C*qVMq
shell> sudo bin/mysql_ssl_rsa_setup
Generating a 2048 bit RSA private key
...........+++
....................................+++
writing new private key to 'ca-key.pem'
-----
Generating a 2048 bit RSA private key
...........................................................+++
...........................................+++
writing new private key to 'server-key.pem'
-----
Generating a 2048 bit RSA private key
.....+++
..........................+++
writing new private key to 'client-key.pem'
-----
- 更改二进制文件的所有权为
root,将数据文件的所有权更改为mysql:
shell> sudo chown -R root .
shell> sudo chown -R mysql data mysql-files
- 将启动脚本复制到
init.d:
shell> sudo cp support-files/mysql.server /etc/init.d/mysql
- 将
mysql的二进制文件导出到PATH环境变量:
shell> export PATH=$PATH:/usr/local/mysql/bin
- 参考启动或停止 MySQL 8部分来启动 MySQL。
安装后,您将在/usr/local/mysql内获得以下目录:
| 目录 | 目录内容 |
|---|---|
bin |
mysqld服务器、客户端和实用程序 |
data |
日志文件、数据库 |
docs |
以 info 格式的 MySQL 手册 |
man |
Unix 手册页 |
include |
包括(头)文件 |
lib |
库 |
share |
其他支持文件,包括错误消息、示例配置文件、用于数据库安装的 SQL |
还有更多...
还有其他安装方法,例如:
- 从源代码编译。您可以从 Oracle 提供的源代码中编译和构建 MySQL,从而可以灵活定制构建参数、编译器优化和安装位置。强烈建议使用 Oracle 提供的预编译二进制文件,除非您需要特定的编译器选项或者您正在调试 MySQL。
这种方法很少使用,需要几个开发工具,超出了本书的范围。有关通过源代码安装的安装方法,您可以参考参考手册,网址为dev.mysql.com/doc/refman/8.0/en/source-installation.html。
- 使用 Docker。MySQL 服务器也可以使用 Docker 镜像安装和管理。有关安装、配置以及如何在 Docker 下使用 MySQL,请参阅
hub.docker.com/r/mysql/mysql-server/。
启动或停止 MySQL 8
安装完成后,您可以使用以下命令启动/停止 MySQL,这些命令因不同平台和安装方法而异。mysqld是mysql服务器进程。所有启动方法都调用mysqld脚本。
如何做...
让我们详细看看。除了启动和停止之外,我们还将了解有关检查服务器状态的一些内容。让我们看看如何。
启动 MySQL 8.0 服务器
您可以使用以下命令启动服务器:
- 使用
service:
shell> sudo service mysql start
- 使用
init.d:
shell> sudo /etc/init.d/mysql start
- 如果找不到启动脚本(在进行二进制安装时),可以从解压位置复制。
shell> sudo cp /usr/local/mysql/support-files/mysql.server /etc/init.d/mysql
- 如果您的安装包括
systemd支持:
shell> sudo systemctl start mysqld
- 如果没有
systemd支持,可以使用mysqld_safe启动 MySQL。mysqld_safe是mysqld的启动脚本,用于保护mysqld进程。如果mysqld被杀死,mysqld_safe会尝试重新启动进程:
shell> sudo mysqld_safe --user=mysql &
启动后,
-
服务器已初始化。
-
SSL 证书和密钥文件在
数据目录中生成。 -
安装并启用了
validate_password插件。 -
创建了一个超级用户帐户,
root'@'localhost。为超级用户设置了密码,并将其存储在错误日志文件中(不适用于二进制安装)。要显示它,请使用以下命令:
shell> sudo grep "temporary password" /var/log/mysqld.log
2017-12-02T07:23:20.915827Z 5 [Note] A temporary password is generated for root@localhost: bkvotsG:h6jD
您可以使用临时密码连接到 MySQL。
shell> mysql -u root -pbkvotsG:h6jD
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 7
Server version: 8.0.3-rc-log
Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
- 尽快使用生成的临时密码登录并为超级用户帐户设置自定义密码更改根密码:
# You will be prompted for a password, enter the one you got from the previous step
mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'NewPass4!';
Query OK, 0 rows affected (0.01 sec)
# password should contain at least one Upper case letter, one lowercase letter, one digit, and one special character, and that the total password length is at least 8 characters
停止 MySQL 8.0 服务器
停止 MySQL 并检查状态与启动它类似,只是一个单词的变化:
- 使用
service:
shell> sudo service mysqld stop
Redirecting to /bin/systemctl stop mysqld.service
- 使用
init.d:
shell> sudo /etc/init.d/mysql stop
[ ok ] Stopping mysql (via systemctl): mysql.service.
- 如果您的安装包括
systemd支持(参见使用 systemd 管理 MySQL 服务器部分):
shell> sudo systemctl stop mysqld
- 使用
mysqladmin:
shell> mysqladmin -u root -p shutdown
检查 MySQL 8.0 服务器的状态
- 使用
service:
shell> sudo systemctl status mysqld
● mysqld.service - MySQL Server
Loaded: loaded (/usr/lib/systemd/system/mysqld.service; enabled; vendor preset: disabled)
Drop-In: /etc/systemd/system/mysqld.service.d
└─override.conf
Active: active (running) since Sat 2017-12-02 07:33:53 UTC; 14s ago
Docs: man:mysqld(8)
http://dev.mysql.com/doc/refman/en/using-systemd.html
Process: 10472 ExecStart=/usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid $MYSQLD_OPTS (code=exited, status=0/SUCCESS)
Process: 10451 ExecStartPre=/usr/bin/mysqld_pre_systemd (code=exited, status=0/SUCCESS)
Main PID: 10477 (mysqld)
CGroup: /system.slice/mysqld.service
└─10477 /usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid --general_log=1
Dec 02 07:33:51 centos7 systemd[1]: Starting MySQL Server...
Dec 02 07:33:53 centos7 systemd[1]: Started MySQL Server.
- 使用
init.d:
shell> sudo /etc/init.d/mysql status
● mysql.service - LSB: start and stop MySQL
Loaded: loaded (/etc/init.d/mysql; bad; vendor preset: enabled)
Active: inactive (dead)
Docs: man:systemd-sysv-generator(8)
Dec 02 06:01:00 ubuntu systemd[1]: Starting LSB: start and stop MySQL...
Dec 02 06:01:00 ubuntu mysql[20334]: Starting MySQL
Dec 02 06:01:00 ubuntu mysql[20334]: *
Dec 02 06:01:00 ubuntu systemd[1]: Started LSB: start and stop MySQL.
Dec 02 06:01:00 ubuntu mysql[20334]: 2017-12-02T06:01:00.969284Z mysqld_safe A mysqld process already exists
Dec 02 06:01:55 ubuntu systemd[1]: Stopping LSB: start and stop MySQL...
Dec 02 06:01:55 ubuntu mysql[20445]: Shutting down MySQL
Dec 02 06:01:57 ubuntu mysql[20445]: .. *
Dec 02 06:01:57 ubuntu systemd[1]: Stopped LSB: start and stop MySQL.
Dec 02 07:26:33 ubuntu systemd[1]: Stopped LSB: start and stop MySQL.
- 如果您的安装包括
systemd支持(参见使用 systemd 管理 MySQL 服务器部分):
shell> sudo systemctl status mysqld
卸载 MySQL 8
如果您在安装过程中出现问题或不想要 MySQL 8 版本,则可以使用以下步骤卸载。在卸载之前,请确保备份文件(参见第七章备份),如果需要,请停止 MySQL。
如何做...
在不同系统上,卸载将以不同的方式处理。让我们看看如何。
在基于 YUM 的系统上
- 检查是否存在任何现有软件包:
shell> rpm -qa | grep -i mysql-community
mysql-community-libs-8.0.3-0.1.rc.el7.x86_64
mysql-community-common-8.0.3-0.1.rc.el7.x86_64
mysql-community-client-8.0.3-0.1.rc.el7.x86_64
mysql-community-libs-compat-8.0.3-0.1.rc.el7.x86_64
mysql-community-server-8.0.3-0.1.rc.el7.x86_64
- 删除软件包。您可能会收到通知,有其他软件包依赖于 MySQL。如果您打算再次安装 MySQL,可以通过传递
--nodeps选项忽略警告:
shell> rpm -e <package-name>
例如:
shell> sudo rpm -e mysql-community-server
- 要删除所有软件包:
shell> sudo rpm -qa | grep -i mysql-community | xargs sudo rpm -e --nodeps
warning: /etc/my.cnf saved as /etc/my.cnf.rpmsave
在基于 APT 的系统上
- 检查是否存在任何现有软件包:
shell> dpkg -l | grep -i mysql
- 使用以下命令删除软件包:
shell> sudo apt-get remove mysql-community-server mysql-client mysql-common mysql-community-client mysql-community-client-core mysql-community-server mysql-community-server-core -y
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following packages will be REMOVED:
mysql-client mysql-common mysql-community-client mysql-community-client-core mysql-community-server mysql-community-server-core mysql-server
0 upgraded, 0 newly installed, 7 to remove and 341 not upgraded.
After this operation, 357 MB disk space will be freed.
(Reading database ... 134358 files and directories currently installed.)
Removing mysql-server (8.0.3-rc-1ubuntu16.04) ...
Removing mysql-community-server (8.0.3-rc-1ubuntu16.04) ...
update-alternatives: using /etc/mysql/my.cnf.fallback to provide /etc/mysql/my.cnf (my.cnf) in auto mode
Removing mysql-client (8.0.3-rc-1ubuntu16.04) ...
Removing mysql-community-client (8.0.3-rc-1ubuntu16.04) ...
Removing mysql-common (8.0.3-rc-1ubuntu16.04) ...
Removing mysql-community-client-core (8.0.3-rc-1ubuntu16.04) ...
Removing mysql-community-server-core (8.0.3-rc-1ubuntu16.04) ...
Processing triggers for man-db (2.7.5-1) ...
或使用以下命令删除它们:
shell> sudo apt-get remove --purge mysql-\* -y
shell> sudo apt-get autoremove -y
- 验证软件包是否已卸载:
shell> dpkg -l | grep -i mysql
ii mysql-apt-config 0.8.9-1 all Auto configuration for MySQL APT Repo.
rc mysql-common 8.0.3-rc-1ubuntu16.04 amd64 MySQL Common
rc mysql-community-client 8.0.3-rc-1ubuntu16.04 amd64 MySQL Client
rc mysql-community-server 8.0.3-rc-1ubuntu16.04 amd64 MySQL Server
rc表示软件包已被删除(r),只保留了配置文件(c)。
卸载二进制文件
卸载二进制安装非常简单。您只需要删除符号链接:
- 更改目录到安装路径:
shell> cd /usr/local
- 检查
mysql指向的位置,这将显示它引用的路径:
shell> sudo ls -lh mysql
- 删除
mysql:
shell> sudo rm mysql
- 删除二进制文件(可选):
shell> sudo rm -f /opt/mysql-8.0.3-rc-linux-glibc2.12-x86_64.tar.gz
使用systemd管理 MySQL 服务器
如果您使用 RPM 或 Debian 软件包服务器安装 MySQL,则启动和关闭由systemd管理。对于安装了 MySQL 的平台,不会安装mysqld_safe,mysqld_multi和mysqld_multi.server。MySQL 服务器的启动和关闭由systemd使用systemctl命令进行管理。您需要配置systemd如下。
基于 RPM 的系统使用mysqld.service文件,基于 APT 的系统使用mysql.server文件。
如何做...
- 创建本地化的
systemd配置文件:
shell> sudo mkdir -pv /etc/systemd/system/mysqld.service.d
- 创建/打开
conf文件:
shell> sudo vi /etc/systemd/system/mysqld.service.d/override.conf
- 输入以下内容:
[Service]
LimitNOFILE=max_open_files (ex: 102400)
PIDFile=/path/to/pid/file (ex: /var/lib/mysql/mysql.pid)
Nice=nice_level (ex: -10)
Environment="LD_PRELOAD=/path/to/malloc/library" Environment="TZ=time_zone_setting"
- 重新加载
systemd:
shell> sudo systemctl daemon-reload
- 对于临时更改,您可以在不编辑
conf文件的情况下重新加载:
shell> sudo systemctl set-environment MYSQLD_OPTS="--general_log=1"
or unset using
shell> sudo systemctl unset-environment MYSQLD_OPTS
- 修改
systemd环境后,重新启动服务器以使更改生效。
启用mysql.serviceshell> sudo systemctl,并启用mysql.service:
shell> sudo systemctl unmask mysql.service
- 重新启动
mysql:
在 RPM 平台上:
shell> sudo systemctl restart mysqld
在 Debian 平台上:
shell> sudo systemctl restart mysql
从 MySQL 8.0 降级
如果您的应用程序表现不如预期,您可以随时降级到以前的 GA 版本(MySQL 5.7)。在降级之前,建议进行逻辑备份(参考第七章备份)。请注意,您只能降级一个先前的版本。假设您想要从 MySQL 8.0 降级到 MySQL 5.6,您必须先降级到 MySQL 5.7,然后从 MySQL 5.7 降级到 MySQL 5.6。
您可以通过两种方式完成:
-
原地降级(在 MySQL 8 内部降级)
-
逻辑降级
如何做...
在以下小节中,您将学习如何使用各种存储库、捆绑包等处理安装/卸载/升级/降级。
原地降级
在 MySQL 8.0 中的 GA 状态发布之间进行降级(请注意,您不能使用此方法降级到 MySQL 5.7):
-
关闭旧的 MySQL 版本
-
替换 MySQL 8.0 二进制文件或旧的二进制文件
-
在现有的
数据目录上重新启动 MySQL -
运行
mysql_upgrade实用程序
使用 YUM 存储库
- 准备 MySQL 进行缓慢关闭,以确保撤消日志为空,并且数据文件在不同版本之间的文件格式差异的情况下已完全准备好:
mysql> SET GLOBAL innodb_fast_shutdown = 0;
- 按照停止 MySQL 8.0 服务器部分中的说明关闭
mysql服务器:
shell> sudo systemctl stop mysqld
- 从
数据目录中删除InnoDB重做日志文件(ib_logfile*文件),以避免降级问题与重做日志文件格式更改之间的关联,这些更改可能发生在版本之间:
shell> sudo rm -rf /var/lib/mysql/ib_logfile*
- 降级 MySQL。要降级服务器,您需要卸载 MySQL 8.0,如卸载 MySQL 8部分中所述。配置文件将自动存储为备份。
列出可用版本:
shell> sudo yum list mysql-community-server
降级很棘手;最好在降级之前删除现有的软件包:
shell> sudo rpm -qa | grep -i mysql-community | xargs sudo rpm -e --nodeps
warning: /etc/my.cnf saved as /etc/my.cnf.rpmsave
安装旧版本:
shell> sudo yum install -y mysql-community-server-<version>
使用 APT 存储库
- 重新配置 MySQL 并选择旧版本:
shell> sudo dpkg-reconfigure mysql-apt-config
- 运行
apt-get update:
shell> sudo apt-get update
- 删除当前版本:
shell> sudo apt-get remove mysql-community-server mysql-client mysql-common mysql-community-client mysql-community-client-core mysql-community-server mysql-community-server-core -y
shell> sudo apt-get autoremove
- 安装旧版本(自动选择,因为您已经重新配置):
shell> sudo apt-get install -y mysql-server
使用 RPM 或 APT 捆绑包
卸载现有的软件包(参考卸载 MySQL 8部分)并安装新的软件包,可以从 MySQL 下载(参考使用 RPM 或 DEB 文件安装 MySQL 8.0部分)。
使用通用二进制文件
如果您通过二进制文件安装了 MySQL,您必须删除到旧版本的符号链接(参考卸载 MySQL 8部分),然后进行新安装(参考使用通用二进制文件在 Linux 上安装 MySQL部分):
-
按照启动或停止 MySQL 8部分中的说明启动服务器。请注意,所有版本的启动过程相同。
-
运行
mysql_upgrade实用程序:
shell> sudo mysql_upgrade -u root -p
- 重新启动 MySQL 服务器,以确保对系统表所做的任何更改生效:
shell> sudo systemctl restart mysqld
逻辑降级
以下是步骤概述:
-
使用逻辑备份从 MySQL 8.0 版本中导出现有数据(参考第七章备份中的逻辑备份方法)
-
安装 MySQL 5.7
-
将转储文件加载到 MySQL 5.7 版本中(参考恢复数据章节中的恢复方法)
-
运行
mysql_upgrade实用程序
以下是详细步骤:
- 您需要对数据库进行逻辑备份。(参考第七章,备份中的
mydumper进行更快的备份):
shell> mysqldump -u root -p --add-drop-table --routines --events --all-databases --force > mysql80.sql
-
按照启动或停止 MySQL 8部分中的说明关闭 MySQL 服务器。
-
移动
数据目录。如果要保留 MySQL 8,可以将数据目录移回(在步骤 1 中不需要恢复 SQL 备份):
shell> sudo mv /var/lib/mysql /var/lib/mysql80
- 降级 MySQL。要降级服务器,我们需要卸载 MySQL 8。配置文件会自动备份。
使用 YUM 存储库
卸载后,安装旧版本:
- 切换存储库:
shell> sudo yum-config-manager --disable mysql80-community
shell> sudo yum-config-manager --enable mysql57-community
- 验证
mysql57-community已启用:
shell> yum repolist enabled | grep "mysql.*-community.*"
!mysql-connectors-community/x86_64 MySQL Connectors Community 42
!mysql-tools-community/x86_64 MySQL Tools Community 53
!mysql57-community/x86_64 MySQL 5.7 Community Server 227
- 降级很棘手;最好在降级之前删除现有的软件包:
shell> sudo rpm -qa | grep -i mysql-community | xargs sudo rpm -e --nodeps
warning: /etc/my.cnf saved as /etc/my.cnf.rpmsave
- 列出可用版本:
shell> sudo yum list mysql-community-server
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile
* base: mirror.rackspace.com
* epel: mirrors.develooper.com
* extras: centos.s.uw.edu
* updates: mirrors.syringanetworks.net
Available Packages
mysql-community-server.x86_64 5.7.20-1.el7 mysql57-community
- 安装 MySQL 5.7:
shell> sudo yum install -y mysql-community-server
使用 APT 存储库
- 重新配置
apt以切换到 MySQL 5.7:
shell> sudo dpkg-reconfigure mysql-apt-config
- 运行
apt-get update:
shell> sudo apt-get update
- 删除当前版本:
shell> sudo apt-get remove mysql-community-server mysql-client mysql-common mysql-community-client mysql-community-client-core mysql-community-server mysql-community-server-core -y
shell> sudo apt-get autoremove
- 安装 MySQL 5.7:
shell> sudo apt-get install -y mysql-server
使用 RPM 或 APT 捆绑包
卸载现有的软件包(参考卸载 MySQL 8部分)并安装新的软件包,可以从 MySQL 下载(参考使用 RPM 或 DEB 文件安装 MySQL 8部分)下载。
使用通用二进制文件
如果通过二进制文件安装了 MySQL,必须删除到旧版本的符号链接(参考卸载 MySQL 8部分),然后进行新安装(参考在 Linux 上使用通用二进制文件安装 MySQL部分)。
降级 MySQL 后,必须恢复备份并运行mysql_upgrade实用程序:
-
启动 MySQL(参考启动或停止 MySQL 8部分)。您需要再次重置密码。
-
恢复备份(这可能需要很长时间,具体取决于备份的大小)。参考第八章,恢复数据,了解名为
myloader的快速恢复方法:
shell> mysql -u root -p < mysql80.sql
- 运行
mysql_upgrade:
shell> mysql_upgrade -u root -p
- 重新启动 MySQL 服务器,以确保对系统表所做的任何更改生效。参考启动或停止 MySQL 8部分:
shell> sudo /etc/init.d/mysql restart
升级到 MySQL 8.0
MySQL 8 使用包含事务表中数据库对象信息的全局数据字典。在以前的版本中,字典数据存储在元数据文件和非事务系统表中。您需要将数据目录从基于文件的结构升级到数据字典结构。
与降级一样,可以使用两种方法进行升级:
-
原地升级
-
逻辑升级
在升级之前,您还应该检查一些先决条件。
准备工作
- 检查过时的数据类型或触发器,其缺少或空的定义者或无效的创建上下文:
shell> sudo mysqlcheck -u root -p --all-databases --check-upgrade
- 不能有使用不支持本机分区的存储引擎的分区表。要识别这些表,执行此查询:
shell> SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE ENGINE NOT IN ('innodb', 'ndbcluster') AND CREATE_OPTIONS LIKE '%partitioned%';
如果有这些表,请将它们更改为InnoDB:
mysql> ALTER TABLE table_name ENGINE = INNODB;
或删除分区:
mysql> ALTER TABLE table_name REMOVE PARTITIONING;
- MySQL 5.7
mysql系统数据库中不能有与 MySQL 8.0数据字典使用的表同名的表。要识别具有这些名称的表,执行此查询:
mysql> SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE LOWER(TABLE_SCHEMA) = 'mysql' and LOWER(TABLE_NAME) IN ('catalogs', 'character_sets', 'collations', 'column_type_elements', 'columns', 'events', 'foreign_key_column_usage', 'foreign_keys', 'index_column_usage', 'index_partitions', 'index_stats', 'indexes', 'parameter_type_elements', 'parameters', 'routines', 'schemata', 'st_spatial_reference_systems', 'table_partition_values', 'table_partitions', 'table_stats', 'tables', 'tablespace_files', 'tablespaces', 'triggers', 'version', 'view_routine_usage', 'view_table_usage');
- 表中不能有外键约束名称超过 64 个字符。要识别约束名称过长的表,执行此查询:
mysql> SELECT CONSTRAINT_SCHEMA, TABLE_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS WHERE LENGTH(CONSTRAINT_NAME) > 64;
- 不受 MySQL 8.0 支持的表,如
ndb,应移至InnoDB:
mysql> ALTER TABLE tablename ENGINE=InnoDB;
如何做...
与以前的方法一样,以下各小节将带您了解各种系统、捆绑包等的详细信息。
原地升级
以下是步骤概述:
-
关闭旧的 MySQL 版本。
-
用新的 MySQL 二进制文件或软件包替换旧的(详细步骤涵盖了不同类型安装方法)。
-
在现有的
数据目录上重新启动 MySQL。 -
运行
mysql_upgrade实用程序。 -
在 MySQL 5.7 服务器上,如果有加密的
InnoDB表空间,通过执行此语句来旋转keyring主密钥:
mysql> ALTER INSTANCE ROTATE INNODB MASTER KEY;
以下是详细步骤:
- 配置您的 MySQL 5.7 服务器以执行慢关闭。通过慢关闭,
InnoDB在关闭之前执行完整的清除和更改缓冲区合并,以确保撤消日志为空,并且数据文件在发布之间的文件格式差异的情况下已经准备就绪。
这一步是最重要的,因为如果没有进行这一步,您将遇到以下错误:
[ERROR] InnoDB: Upgrade after a crash is not supported.
此重做日志是使用 MySQL 5.7.18 创建的。请按照dev.mysql.com/doc/refman/8.0/en/upgrading.html上的说明进行操作:
mysql> SET GLOBAL innodb_fast_shutdown = 0;
- 按照启动或停止 MySQL 8部分中的描述关闭 MySQL 服务器。
升级 MySQL 二进制文件或软件包。
基于 YUM 的系统
- 切换存储库:
shell> sudo yum-config-manager --disable mysql57-community
shell> sudo yum-config-manager --enable mysql80-community
- 验证
mysql80-community是否已启用:
shell> sudo yum repolist all | grep mysql8
mysql80-community/x86_64 MySQL 8.0 Community Server enabled: 16
mysql80-community-source MySQL 8.0 Community Server disabled
- 运行 yum update:
shell> sudo yum update mysql-server
基于 APT 的系统
- 重新配置
apt以切换到 MySQL 8.0:
shell> sudo dpkg-reconfigure mysql-apt-config
- 运行
apt-get update:
shell> sudo apt-get update
- 删除当前版本:
shell> sudo apt-get remove mysql-community-server mysql-client mysql-common mysql-community-client mysql-community-client-core mysql-community-server mysql-community-server-core -y
shell> sudo apt-get autoremove
- 安装 MySQL 8:
shell> sudo apt-get update
shell> sudo apt-get install mysql-server
shell> sudo apt-get install libmysqlclient21
使用 RPM 或 APT 捆绑包
卸载现有的软件包(参考卸载 MySQL 8部分),并安装新的软件包,可以从 MySQL 下载(参考使用 RPM 或 DEB 文件安装 MySQL 8.0部分)。
使用通用二进制文件
如果您通过二进制文件安装了 MySQL,则必须删除到旧版本的符号链接(参考卸载 MySQL 8部分),并进行新安装(参考使用通用二进制文件在 Linux 上安装 MySQL部分)。
启动 MySQL 8.0 服务器(参考启动或停止 MySQL 8 以启动 MySQL部分)。如果有加密的InnoDB表空间,请使用--early-plugin-load选项加载keyring插件。
服务器会自动检测数据字典表是否存在。如果没有,服务器将在数据目录中创建它们,填充它们的元数据,然后继续其正常的启动顺序。在此过程中,服务器将升级所有数据库对象的元数据,包括数据库、表空间、系统和用户表、视图和存储程序(存储过程和函数、触发器、事件调度器事件)。服务器还会删除以前用于存储元数据的文件。例如,升级后,您会注意到您的表不再有.frm文件。
服务器创建一个名为backup_metadata_57的目录,并将 MySQL 5.7 使用的文件移入其中。服务器将event和proc表重命名为event_backup_57和proc_backup_57。如果升级失败,服务器将所有更改恢复到数据目录。在这种情况下,您应该删除所有重做日志文件,在相同的数据目录上启动您的 MySQL 5.7 服务器,并修复任何错误的原因。然后,执行另一个 MySQL 5.7 服务器的慢关闭,并启动 MySQL 8.0 服务器再次尝试。
运行mysql_upgrade实用程序:
shell> sudo mysql_upgrade -u root -p
mysql_upgrade检查所有数据库中的所有表与当前版本的 MySQL 的不兼容性。它在 MySQL 5.7 和 MySQL 8.0 之间的mysql系统数据库中进行任何剩余的更改,以便您可以利用新的权限或功能。mysql_upgrade还将性能模式、INFORMATION_SCHEMA和sys schema对象更新到 MySQL 8.0 的最新状态。
重新启动 MySQL 服务器(参考启动或停止 MySQL 8 以启动 MySQL部分)。
逻辑升级
以下是步骤概述:
-
使用
mysqldump从旧的 MySQL 版本中导出现有数据 -
安装新的 MySQL 版本
-
将转储文件加载到新的 MySQL 版本中
-
运行
mysql_upgrade实用程序
以下是详细步骤:
- 您需要对数据库进行逻辑备份(参考第七章,备份中的
mydumper进行更快的备份):
shell> mysqldump -u root -p --add-drop-table --routines --events --all-databases --ignore-table=mysql.innodb_table_stats --ignore-table=mysql.innodb_index_stats --force > data-for-upgrade.sql
-
关闭 MySQL 服务器(参考启动或停止 MySQL 8部分)。
-
安装新的 MySQL 版本(参考就地升级部分)。
-
启动 MySQL 服务器(参考“启动或停止 MySQL 8”部分)。
-
重置临时
root密码:
shell> mysql -u root -p
Enter password: **** (enter temporary root password from error log)
mysql> ALTER USER USER() IDENTIFIED BY 'your new password';
- 恢复备份(这可能需要很长时间,具体取决于备份的大小)。参考第八章“恢复数据”中的
myloader快速恢复方法:
shell> mysql -u root -p --force < data-for-upgrade.sql
- 运行
mysql_upgrade实用程序:
shell> sudo mysql_upgrade -u root -p
- 重新启动 MySQL 服务器(参考“启动或停止 MySQL 8”部分)。
安装 MySQL 实用工具
MySQL 实用工具为您提供非常方便的工具,可以在没有太多手动操作的情况下顺利进行日常操作。
如何做...
可以通过以下方式在基于 YUM 和 APT 的系统上安装。让我们来看看。
在基于 YUM 的系统上
从 MySQL 下载页面[https://dev.mysql.com/downloads/utilities/]下载文件,选择 Red Hat Enterprise Linux/Oracle Linux,或直接使用wget从此链接下载:
shell> wget https://cdn.mysql.com//Downloads/MySQLGUITools/mysql-utilities-1.6.5-1.el7.noarch.rpm
shell> sudo yum localinstall -y mysql-utilities-1.6.5-1.el7.noarch.rpm
在基于 APT 的系统上
从 MySQL 下载页面[https://dev.mysql.com/downloads/utilities/]下载文件,选择 Ubuntu Linux,或直接使用wget从此链接下载:
shell> wget "https://cdn.mysql.com//Downloads/MySQLGUITools/mysql-utilities_1.6.5-1ubuntu16.10_all.deb"
shell> sudo dpkg -i mysql-utilities_1.6.5-1ubuntu16.10_all.deb
shell> sudo apt-get install -f
第二章:使用 MySQL
在本章中,我们将介绍以下内容:
-
使用命令行客户端连接到 MySQL
-
创建数据库
-
创建表
-
插入、更新和删除行
-
加载示例数据
-
选择数据
-
排序结果
-
分组结果(聚合函数)
-
创建用户
-
授予和撤销用户的访问权限
-
将数据选择到文件和表中
-
将数据加载到表中
-
连接表
-
存储过程
-
函数
-
触发器
-
视图
-
事件
-
获取有关数据库和表的信息
介绍
在接下来的教程中,我们将学到很多东西。让我们详细看看每一个。
使用命令行客户端连接到 MySQL
到目前为止,您已经学会了如何在各种平台上安装 MySQL 8.0。除了安装之外,您还将获得名为mysql的命令行客户端实用程序,我们将用它来连接到任何 MySQL 服务器。
准备工作
首先,您需要知道要连接到哪个服务器。如果您在一个主机上安装了 MySQL 服务器,并且正在尝试从不同的主机(通常称为客户端)连接到服务器,则应指定服务器的主机名或 IP 地址,并且客户端上应安装mysql-client软件包。在上一章中,您安装了 MySQL 服务器和客户端软件包。如果您已经在服务器上(通过 SSH),可以指定localhost、127.0.0.1或::1。
其次,由于您已连接到服务器,下一步需要指定要连接到服务器的端口。默认情况下,MySQL 在端口3306上运行。因此,您应该指定3306。
现在您知道要连接到哪里了。下一个明显的事情是用户名和密码以登录服务器。您还没有创建任何用户,因此使用 root 用户进行连接。在安装时,您可能已经提供了密码,请使用该密码进行连接。如果更改了密码,请使用新密码。
如何做...
可以使用以下任何命令连接到 MySQL 客户端:
shell> mysql -h localhost -P 3306 -u <username> -p<password>
shell> mysql --host=localhost --port=3306 --user=root --password=<password>
shell> mysql --host localhost --port 3306 --user root --password=<password>
shell> mysql --host=localhost --port=3306 --user=root --password
Enter Password:
shell> whoami
强烈建议不要在命令行中提供密码,而是可以将字段留空;系统会提示您输入密码:
mysql> ^DBye
shell>
-
传递
-P参数(大写)以指定端口。 -
传递
-p参数(小写)以指定密码。 -
在
-p参数后没有空格。 -
对于密码,在
=后没有空格。
默认情况下,主机被视为localhost,端口被视为3306,用户被视为当前 shell 用户。
- 要知道当前用户:
mysql> exit;
Bye
shell>
- 要断开连接,请按*Ctrl *+ D或输入
exit:
mysql> SELECT 1;
+---+
| 1 |
+---+
| 1 |
+---+
1 row in set (0.00 sec)
或使用:
mysql> SELECT ^C
mysql> SELECT \c
- 连接到
mysql提示后,您可以执行后跟分隔符的语句。默认分隔符是分号(;):
Warning: Using a password on the command line interface can be insecure.
- 要取消命令,请按*Ctrl *+ C或输入
\c:
customer id=1, first_name=Mike, last_name=Christensen country=USA
customer id=2, first_name=Andy, last_name=Hollands, country=Australia
customer id=3, first_name=Ravi, last_name=Vedantam, country=India
customer id=4, first_name= Rajiv, last_name=Perera, country=Sri Lanka
不建议使用 root 用户连接到 MySQL。您可以创建用户并通过授予适当的权限来限制用户,这将在创建用户和授予和撤销用户的访问权限部分中讨论。在那之前,您可以使用 root 用户连接到 MySQL。
另请参阅
连接后,您可能会注意到一个警告:
shell> mysql -u root -p
Enter Password:
mysql> CREATE DATABASE company;
mysql> CREATE DATABASE `my.contacts`;
要了解安全连接的安全方式,请参阅第十四章,安全。
一旦连接到命令行提示符,您可以执行 SQL 语句,这些语句可以由;、\g或\G终止。
;或\g—输出水平显示,\G—输出垂直显示。
创建数据库
好了,您已经安装了 MySQL 8.0 并连接到了它。现在是时候在其中存储一些数据了,毕竟这就是数据库的用途。在任何关系数据库管理系统(RDBMS)中,数据存储在行中,这是数据库的基本构建块。行包含列,我们可以在其中存储多组值。
例如,如果您想在数据库中存储有关客户的信息。
这是数据集:
mysql> USE company
mysql> USE `my.contacts`
你应该将它们保存为行:(1, 'Mike', 'Christensen', 'USA'), (2, 'Andy', 'Hollands', 'Australia'), (3, 'Ravi', 'Vedantam', 'India'), (4, 'Rajiv', 'Perera', 'Sri Lanka')。对于这个数据集,有三列描述的四行(id,first_name,last_name 和 country),它们存储在一个表中。表可以容纳的列数应该在创建表的时候定义,这是关系数据库管理系统的主要限制。但是,我们可以随时更改表的定义,但在这样做时应该重建整个表。在某些情况下,更改表时表将不可用。表的更改将在第九章中详细讨论,表维护。
数据库是许多表的集合,数据库服务器可以容纳许多这些数据库。流程如下:
数据库服务器—>数据库—>表(由列定义)—>行
数据库和表被称为数据库对象。任何操作,如创建、修改或删除数据库对象,都称为数据定义语言(DDL)。
数据的组织作为数据库构建的蓝图(分为数据库和表)称为模式。
如何做...
连接到 MySQL 服务器:
shell> mysql -u root -p company
反引号字符(`)用于引用标识符,如数据库和表名。当数据库名称包含特殊字符,如点(.)时,您需要使用它。
您可以在数据库之间切换:
mysql> SELECT DATABASE();
+------------+
| DATABASE() |
+------------+
| company |
+------------+
1 row in set (0.00 sec)
无需切换,您可以直接通过命令行指定所需数据库进行连接:
mysql> SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| company |
| my.contacts |
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
6 rows in set (0.00 sec)
要查找您当前连接的数据库,请使用以下命令:
mysql> SHOW VARIABLES LIKE 'datadir';
+---------------+------------------------+
| Variable_name | Value |
+---------------+------------------------+
| datadir | /usr/local/mysql/data/ |
+---------------+------------------------+
1 row in set (0.00 sec)
要查找您有权访问的所有数据库,请使用:
shell> sudo ls -lhtr /usr/local/mysql/data/
total 185M
-rw-r----- 1 mysql mysql 56 Jun 2 16:57 auto.cnf
-rw-r----- 1 mysql mysql 257 Jun 2 16:57 performance_sche_3.SDI
drwxr-x--- 2 mysql mysql 4.0K Jun 2 16:57 performance_schema
drwxr-x--- 2 mysql mysql 4.0K Jun 2 16:57 mysql
-rw-r----- 1 mysql mysql 242 Jun 2 16:57 sys_4.SDI
drwxr-x--- 2 mysql mysql 4.0K Jun 2 16:57 sys
-rw------- 1 mysql root 1.7K Jun 2 16:58 ca-key.pem
-rw-r--r-- 1 mysql root 1.1K Jun 2 16:58 ca.pem
-rw------- 1 mysql root 1.7K Jun 2 16:58 server-key.pem
-rw-r--r-- 1 mysql root 1.1K Jun 2 16:58 server-cert.pem
-rw------- 1 mysql root 1.7K Jun 2 16:58 client-key.pem
-rw-r--r-- 1 mysql root 1.1K Jun 2 16:58 client-cert.pem
-rw------- 1 mysql root 1.7K Jun 2 16:58 private_key.pem
-rw-r--r-- 1 mysql root 451 Jun 2 16:58 public_key.pem
-rw-r----- 1 mysql mysql 1.4K Jun 2 17:46 ib_buffer_pool
-rw-r----- 1 mysql mysql 5 Jun 2 17:46 server1.pid
-rw-r----- 1 mysql mysql 247 Jun 3 13:55 company_5.SDI
drwxr-x--- 2 mysql mysql 4.0K Jun 4 08:13 company
-rw-r----- 1 mysql mysql 12K Jun 4 18:58 server1.err
-rw-r----- 1 mysql mysql 249 Jun 5 16:17 employees_8.SDI
drwxr-x--- 2 mysql mysql 4.0K Jun 5 16:17 employees
-rw-r----- 1 mysql mysql 76M Jun 5 16:18 ibdata1
-rw-r----- 1 mysql mysql 48M Jun 5 16:18 ib_logfile1
-rw-r----- 1 mysql mysql 48M Jun 5 16:18 ib_logfile0
-rw-r----- 1 mysql mysql 12M Jun 10 10:29 ibtmp1
数据库作为data 目录内的一个目录创建。基于仓库的安装默认data 目录为/var/lib/mysql,而通过二进制安装则为/usr/local/mysql/data/。要了解您的当前data 目录,您可以执行:
mysql> CREATE TABLE IF NOT EXISTS `company`.`customers` (
`id` int unsigned AUTO_INCREMENT PRIMARY KEY,
`first_name` varchar(20),
`last_name` varchar(20),
`country` varchar(20)
) ENGINE=InnoDB;
检查data 目录内的文件:
mysql> SHOW ENGINES\G
*************************** 1\. row ***************************
Engine: MRG_MYISAM
Support: YES
Comment: Collection of identical MyISAM tables
Transactions: NO
XA: NO
Savepoints: NO
*************************** 2\. row ***************************
Engine: FEDERATED
Support: NO
Comment: Federated MySQL storage engine
Transactions: NULL
XA: NULL
Savepoints: NULL
*************************** 3\. row ***************************
Engine: InnoDB
Support: DEFAULT
Comment: Supports transactions, row-level locking, and foreign keys
Transactions: YES
XA: YES
Savepoints: YES
*************************** 4\. row ***************************
Engine: BLACKHOLE
Support: YES
Comment: /dev/null storage engine (anything you write to it disappears)
Transactions: NO
XA: NO
Savepoints: NO
*************************** 5\. row ***************************
Engine: CSV
Support: YES
Comment: CSV storage engine
Transactions: NO
XA: NO
Savepoints: NO
*************************** 6\. row ***************************
Engine: MEMORY
Support: YES
Comment: Hash based, stored in memory, useful for temporary tables
Transactions: NO
XA: NO
Savepoints: NO
*************************** 7\. row ***************************
Engine: PERFORMANCE_SCHEMA
Support: YES
Comment: Performance Schema
Transactions: NO
XA: NO
Savepoints: NO
*************************** 8\. row ***************************
Engine: ARCHIVE
Support: YES
Comment: Archive storage engine
Transactions: NO
XA: NO
Savepoints: NO
*************************** 9\. row ***************************
Engine: MyISAM
Support: YES
Comment: MyISAM storage engine
Transactions: NO
XA: NO
Savepoints: NO
9 rows in set (0.00 sec)
参见
您可能会对其他文件和目录感到好奇,例如information_schema和performance_schema,这些并非您所创建。information_schema将在获取数据库和表信息部分讨论,而performance_schema将在第十三章性能调优中的使用 performance_schema部分讨论。
创建表
在定义表中的列时,应提及列名、数据类型(整数、浮点数、字符串等)及默认值(如有)。MySQL 支持多种数据类型。更多详情请参阅 MySQL 文档(dev.mysql.com/doc/refman/8.0/en/data-types.html)。以下是所有数据类型的概览。JSON数据类型是一个新的扩展,将在第三章使用 MySQL(高级)中讨论:
-
数值类型:
TINYINT,SMALLINT,MEDIUMINT,INT,BIGINT, 和BIT。 -
浮点数类型:
DECIMAL,FLOAT, 和DOUBLE。 -
字符串类型:
CHAR,VARCHAR,BINARY,VARBINARY,BLOB,TEXT,ENUM, 和SET。 -
还支持空间数据类型。更多详情请参阅
dev.mysql.com/doc/refman/8.0/en/spatial-extensions.html。 -
JSON数据类型——将在下一章详细讨论。
您可以在一个数据库中创建多个表。
操作方法...
表包含列定义:
mysql> CREATE TABLE `company`.`payments`(
`customer_name` varchar(20) PRIMARY KEY,
`payment` float
);
选项解释如下:
-
点表示法:可以使用数据库名点表名(
database.table)引用表。如果已连接到数据库,则可以直接使用customers代替company.customers。 -
IF NOT EXISTS:如果存在同名表且您指定此子句,MySQL 仅会发出警告表明该表已存在。否则,MySQL 将抛出错误。 -
id:由于仅包含整数,故声明为整型。此外,还有两个关键字:AUTO_INCREMENT和PRIMARY KEY。 -
AUTO_INCREMENT:自动生成线性递增序列,因此您无需担心为每行分配id。 -
PRIMARY KEY:每行通过一个UNIQUE且NOT NULL的列来标识。表中只应定义一个这样的列。如果表包含AUTO_INCREMENT列,则将其视为PRIMARY KEY。 -
first_name、last_name和country:它们包含字符串,因此被定义为varchar。 -
引擎:除了列定义外,你还应该提及存储引擎。一些存储引擎类型包括
InnoDB、MyISAM、FEDERATED、BLACKHOLE、CSV和MEMORY。在所有引擎中,InnoDB是唯一的事务引擎,也是默认引擎。要了解更多关于事务的信息,请参阅第五章,事务。
要列出所有存储引擎,请执行以下操作:
mysql> SHOW TABLES;
+-------------------+
| Tables_in_company |
+-------------------+
| customers |
| payments |
+-------------------+
2 rows in set (0.00 sec)
你可以在数据库中创建多个表。
再创建一个表来跟踪付款:
mysql> SHOW CREATE TABLE customers\G
*************************** 1\. row ***************************
Table: customers
Create Table: CREATE TABLE `customers` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`first_name` varchar(20) DEFAULT NULL,
`last_name` varchar(20) DEFAULT NULL,
`country` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)
要列出所有表,请使用:
mysql> DESC customers;
+------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| first_name | varchar(20) | YES | | NULL | |
| last_name | varchar(20) | YES | | NULL | |
| country | varchar(20) | YES | | NULL | |
+------------+------------------+------+-----+---------+----------------+
4 rows in set (0.01 sec)
要查看表的结构,请执行以下操作:
shell> sudo ls -lhtr /usr/local/mysql/data/company
total 256K
-rw-r----- 1 mysql mysql 128K Jun 4 07:36 customers.ibd
-rw-r----- 1 mysql mysql 128K Jun 4 08:24 payments.ibd
或使用此方法:
mysql> CREATE TABLE new_customers LIKE customers;
Query OK, 0 rows affected (0.05 sec)
MySQL 在data 目录内创建.ibd文件:
mysql> SHOW CREATE TABLE new_customers\G
*************************** 1\. row ***************************
Table: new_customers
Create Table: CREATE TABLE `new_customers` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`first_name` varchar(20) DEFAULT NULL,
`last_name` varchar(20) DEFAULT NULL,
`country` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)
克隆表结构
你可以将一个表的结构克隆到新表中:
mysql> INSERT IGNORE INTO `company`.`customers`(first_name, last_name,country)
VALUES
('Mike', 'Christensen', 'USA'),
('Andy', 'Hollands', 'Australia'),
('Ravi', 'Vedantam', 'India'),
('Rajiv', 'Perera', 'Sri Lanka');
你可以验证新表的结构:
mysql> INSERT IGNORE INTO `company`.`customers`(id, first_name, last_name,country)
VALUES
(1, 'Mike', 'Christensen', 'USA'),
(2, 'Andy', 'Hollands', 'Australia'),
(3, 'Ravi', 'Vedantam', 'India'),
(4, 'Rajiv', 'Perera', 'Sri Lanka');
Query OK, 0 rows affected, 4 warnings (0.00 sec)
Records: 4 Duplicates: 4 Warnings: 4
另请参阅
有关Create Table中的许多其他选项,请参考dev.mysql.com/doc/refman/8.0/en/create-table.html。第十章,表维护将讨论表分区,第十一章,管理表空间将讨论表压缩。
插入、更新和删除行
INSERT、UPDATE、DELETE和SELECT操作被称为数据操纵语言(DML)语句。INSERT、UPDATE和DELETE也称为写操作,或简称为写。SELECT是一种读操作,简称为读。
如何操作...
让我们详细了解每一个操作。我相信你会喜欢学习这些内容。我建议你之后也尝试自己操作一些实例。在本节结束时,我们还将掌握截断表的方法。
插入
INSERT语句用于在表中创建新记录:
mysql> SHOW WARNINGS;
+---------+------+---------------------------------------+
| Level | Code | Message |
+---------+------+---------------------------------------+
| Warning | 1062 | Duplicate entry '1' for key 'PRIMARY' |
| Warning | 1062 | Duplicate entry '2' for key 'PRIMARY' |
| Warning | 1062 | Duplicate entry '3' for key 'PRIMARY' |
| Warning | 1062 | Duplicate entry '4' for key 'PRIMARY' |
+---------+------+---------------------------------------+
4 rows in set (0.00 sec)
或者,如果你想插入特定的id,可以明确提及id列:
mysql> UPDATE customers SET first_name='Rajiv', country='UK' WHERE id=4;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
IGNORE:如果行已存在且给出了IGNORE子句,则新数据被忽略,INSERT语句仍然成功,产生警告和重复项的数量。否则,如果没有给出IGNORE子句,INSERT语句会产生错误。行的唯一性由主键确定:
mysql> DELETE FROM customers WHERE id=4 AND first_name='Rajiv';
Query OK, 1 row affected (0.03 sec)
更新
UPDATE语句用于修改表中的现有记录:
mysql> REPLACE INTO customers VALUES (1,'Mike','Christensen','America');
Query OK, 2 rows affected (0.03 sec)
WHERE:这是用于过滤的子句。在WHERE子句后发出的任何条件都会被评估,过滤后的行将被更新。
WHERE子句是必需的。如果没有给出,它将UPDATE整个表。
建议在事务中进行数据修改,这样一旦发现问题,你可以轻松回滚更改。关于事务的更多信息,请参阅第五章,事务。
删除
删除记录可以按以下方式进行:
mysql> INSERT INTO payments VALUES('Mike Christensen', 200) ON DUPLICATE KEY UPDATE payment=payment+VALUES(payment);
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO payments VALUES('Ravi Vedantam',500) ON DUPLICATE KEY UPDATE payment=payment+VALUES(payment);
Query OK, 1 row affected (0.01 sec)
WHERE子句是必需的。如果没有给出,它将DELETE表中的所有行。
建议在事务中进行数据修改,这样如果发现任何错误,你可以轻松回滚更改。
REPLACE、INSERT、ON DUPLICATE KEY UPDATE
处理重复项的情况很多。行的唯一性由主键标识。如果行已存在,REPLACE 会简单地删除该行并插入新行。如果行不存在,REPLACE 的行为类似于 INSERT。
ON DUPLICATE KEY UPDATE 用于当你希望行已存在时采取行动。如果你指定 ON DUPLICATE KEY UPDATE 选项,并且 INSERT 语句导致 PRIMARY KEY 中出现重复值,MySQL 会根据新值更新旧行。
假设你希望每当从同一客户收到付款时更新之前的金额,并且如果客户首次付款,则同时插入一条新记录。为此,你需要定义一个金额列,并在每次收到新付款时更新它:
mysql> INSERT INTO payments VALUES('Mike Christensen', 300) ON DUPLICATE KEY UPDATE payment=payment+VALUES(payment);
Query OK, 2 rows affected (0.00 sec)
你可以看到有两行受影响,一行重复的被删除,一行新的被插入:
mysql> TRUNCATE TABLE customers;
Query OK, 0 rows affected (0.03 sec)
shell> wget 'https://codeload.github.com/datacharmer/test_db/zip/master' -O master.zip
当 Mike Christensen 下次支付 $300 时,这将更新该行并将此付款添加到之前的付款中:
shell> unzip master.zip
VALUES (payment):指的是 INSERT 语句中给出的值。Payment 指的是表中的列。
截断表
删除整个表需要很长时间,因为 MySQL 逐行执行操作。删除表中所有行(保留表结构)的最快方法是使用 TRUNCATE TABLE 语句。
截断是 MySQL 中的 DDL 操作,意味着一旦数据被截断,就无法回滚:
shell> cd test_db-master
shell> mysql -u root -p < employees.sql
mysql: [Warning] Using a password on the command line interface can be insecure.
INFO
CREATING DATABASE STRUCTURE
INFO
storage engine: InnoDB
INFO
LOADING departments
INFO
LOADING employees
INFO
LOADING dept_emp
INFO
LOADING dept_manager
INFO
LOADING titles
INFO
LOADING salaries
data_load_time_diff
NULL
加载示例数据
你已经创建了架构(数据库和表)并通过 INSERT、UPDATE 和 DELETE 添加了一些数据。为了解释后续章节,需要更多数据。MySQL 提供了一个示例 employee 数据库和大量数据供你使用。在本章中,我们将讨论如何获取这些数据并将其存储在我们的数据库中。
如何操作...
- 下载压缩文件:
shell> mysql -u root -p employees -A
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 35
Server version: 8.0.3-rc-log MySQL Community Server (GPL)
Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
- 解压缩文件:
mysql> SHOW TABLES;
+-------------------------+
| Tables_in_employees |
+-------------------------+
| current_dept_emp |
| departments |
| dept_emp |
| dept_emp_latest_date |
| dept_manager |
| employees |
| salaries |
| titles |
+-------------------------+
8 rows in set (0.00 sec)
- 加载数据:
mysql> DESC employees\G
*************************** 1\. row ***************************
Field: emp_no
Type: int(11)
Null: NO
Key: PRI
Default: NULL
Extra:
*************************** 2\. row ***************************
Field: birth_date
Type: date
Null: NO
Key:
Default: NULL
Extra:
*************************** 3\. row ***************************
Field: first_name
Type: varchar(14)
Null: NO
Key:
Default: NULL
Extra:
*************************** 4\. row ***************************
Field: last_name
Type: varchar(16)
Null: NO
Key:
Default: NULL
Extra:
*************************** 5\. row ***************************
Field: gender
Type: enum('M','F')
Null: NO
Key:
Default: NULL
Extra:
*************************** 6\. row ***************************
Field: hire_date
Type: date
Null: NO
Key:
Default: NULL
Extra:
6 rows in set (0.00 sec)
- 验证数据:
mysql> SELECT * FROM departments;
+---------+--------------------+
| dept_no | dept_name |
+---------+--------------------+
| d009 | Customer Service |
| d005 | Development |
| d002 | Finance |
| d003 | Human Resources |
| d001 | Marketing |
| d004 | Production |
| d006 | Quality Management |
| d008 | Research |
| d007 | Sales |
+---------+--------------------+
9 rows in set (0.00 sec)
mysql> SELECT emp_no, dept_no FROM dept_manager;
+--------+---------+
| emp_no | dept_no |
+--------+---------+
| 110022 | d001 |
| 110039 | d001 |
| 110085 | d002 |
| 110114 | d002 |
| 110183 | d003 |
| 110228 | d003 |
| 110303 | d004 |
| 110344 | d004 |
| 110386 | d004 |
| 110420 | d004 |
| 110511 | d005 |
| 110567 | d005 |
| 110725 | d006 |
| 110765 | d006 |
| 110800 | d006 |
| 110854 | d006 |
| 111035 | d007 |
| 111133 | d007 |
| 111400 | d008 |
| 111534 | d008 |
| 111692 | d009 |
| 111784 | d009 |
| 111877 | d009 |
| 111939 | d009 |
+--------+---------+
24 rows in set (0.00 sec)
mysql> SELECT COUNT(*) FROM employees;
+----------+
| COUNT(*) |
+----------+
| 300024 |
+----------+
1 row in set (0.03 sec)
选择数据
你已经在表中插入了和更新了数据。现在是时候学习如何从数据库检索信息了。在本节中,我们将讨论如何从我们创建的示例 employee 数据库中检索数据。
使用 SELECT 可以做很多事情。本节将讨论最常见的用例。有关语法和其他用例的更多详细信息,请参阅 dev.mysql.com/doc/refman/8.0/en/select.html。
如何操作...
从 employee 数据库的 departments 表中选择所有数据。你可以使用星号 (*) 来选择表中的所有列。不建议这样做,你应该始终只选择所需的数据:
mysql> SELECT emp_no FROM employees WHERE first_name='Georgi' AND last_name='Facello';
+--------+
| emp_no |
+--------+
| 10001 |
| 55649 |
+--------+
2 rows in set (0.08 sec)
选择列
假设你需要从 dept_manager 表中获取 emp_no 和 dept_no:
mysql> SELECT COUNT(*) FROM employees WHERE last_name IN ('Christ', 'Lamba', 'Baba');
+----------+
| COUNT(*) |
+----------+
| 626 |
+----------+
1 row in set (0.08 sec)
计数
从 employees 表中查找员工数量:
mysql> SELECT COUNT(*) FROM employees WHERE hire_date BETWEEN '1986-12-01' AND '1986-12-31';
+----------+
| COUNT(*) |
+----------+
| 3081 |
+----------+
1 row in set (0.06 sec)
基于条件过滤
查找名为Georgi Facello的员工的emp_no:
mysql> SELECT COUNT(*) FROM employees WHERE hire_date NOT BETWEEN '1986-12-01' AND '1986-12-31';
+----------+
| COUNT(*) |
+----------+
| 296943 |
+----------+
1 row in set (0.08 sec)
所有过滤条件均通过WHERE子句给出。除整数和浮点数外,其他所有内容都应放在引号内。
操作符
MySQL 支持多种过滤结果的操作符。详细列表请参考dev.mysql.com/doc/refman/8.0/en/comparison-operators.html。这里我们将讨论几个操作符。LIKE和RLIKE将在后续示例中详细解释:
-
等式: 参考前述示例,其中使用了
=进行筛选。 -
IN: 检查一个值是否在一组值中。例如,查找姓氏为
Christ、Lamba或Baba的员工数量:
mysql> SELECT COUNT(*) FROM employees WHERE first_name LIKE 'christ%';
+----------+
| COUNT(*) |
+----------+
| 1157 |
+----------+
1 row in set (0.06 sec)
-
BETWEEN...AND: 检查一个值是否在一个值范围内。例如,查找 1986 年 12 月雇佣的员工人数:
mysql> SELECT COUNT(*) FROM employees WHERE first_name LIKE 'christ%ed';
+----------+
| COUNT(*) |
+----------+
| 228 |
+----------+
1 row in set (0.06 sec)
-
NOT: 只需在前面加上NOT操作符即可简单否定结果。例如,查找非 1986 年 12 月雇佣的员工人数:
mysql> SELECT COUNT(*) FROM employees WHERE first_name LIKE '%sri%';
+----------+
| COUNT(*) |
+----------+
| 253 |
+----------+
1 row in set (0.08 sec)
简单模式匹配
你可以使用LIKE操作符。使用下划线(_)匹配恰好一个字符,使用%匹配任意数量的字符。
- 查找名字以
Christ开头的员工数量:
mysql> SELECT COUNT(*) FROM employees WHERE first_name LIKE '%er';
+----------+
| COUNT(*) |
+----------+
| 5388 |
+----------+
1 row in set (0.08 sec)
- 查找名字以
Christ开头且以ed结尾的员工数量:
mysql> SELECT COUNT(*) FROM employees WHERE first_name LIKE '__ka%';
+----------+
| COUNT(*) |
+----------+
| 1918 |
+----------+
1 row in set (0.06 sec)
- 查找名字中包含
sri的员工数量:
mysql> SELECT COUNT(*) FROM employees WHERE first_name RLIKE '^christ';
+----------+
| COUNT(*) |
+----------+
| 1157 |
+----------+
1 row in set (0.18 sec)
- 查找名字以
er结尾的员工数量:
mysql> SELECT COUNT(*) FROM employees WHERE last_name REGEXP 'ba$';
+----------+
| COUNT(*) |
+----------+
| 1008 |
+----------+
1 row in set (0.15 sec)
- 查找名字以任意两个字符开头,后接
ka,再接任意数量字符的员工数量:
mysql> SELECT COUNT(*) FROM employees WHERE last_name NOT REGEXP '[aeiou]';
+----------+
| COUNT(*) |
+----------+
| 148 |
+----------+
1 row in set (0.11 sec)
正则表达式
你可以在WHERE子句中使用RLIKE或REGEXP操作符使用正则表达式。REGEXP有多种用法,更多示例请参考dev.mysql.com/doc/refman/8.0/en/regexp.html:
| 表达式 | 描述 |
|---|---|
* |
零或多个重复 |
+ |
一个或多个重复 |
? |
可选字符 |
. |
任意字符 |
\. |
句点 |
^ |
以...开始 |
| ` | 表达式 |
| --- | --- |
* |
零或多个重复 |
+ |
一个或多个重复 |
? |
可选字符 |
. |
任意字符 |
\. |
句点 |
^ |
以...开始 |
| 以...结束 | |
[abc] |
仅a、b或c |
[^abc] |
非a、b、c |
[a-z] |
字符 a 至 z |
[0-9] |
数字 0 至 9 |
^...$ |
起始与结束 |
\d |
任意数字 |
\D |
任意非数字字符 |
\s |
任意空白字符 |
\S |
任意非空白字符 |
\w |
任意字母数字字符 |
\W |
任意非字母数字字符 |
{m} |
m次重复 |
{m,n} |
m至n次重复 |
- 查找名字以
Christ开头的员工数量:
mysql> SELECT first_name, last_name FROM employees WHERE hire_date < '1986-01-01' LIMIT 10;
+------------+------------+
| first_name | last_name |
+------------+------------+
| Bezalel | Simmel |
| Sumant | Peac |
| Eberhardt | Terkki |
| Otmar | Herbst |
| Florian | Syrotiuk |
| Tse | Herber |
| Udi | Jansch |
| Reuven | Garigliano |
| Erez | Ritzmann |
| Premal | Baek |
+------------+------------+
10 rows in set (0.00 sec)
- 查找所有姓氏以
ba结尾的员工数量:
mysql> SELECT COUNT(*) AS count FROM employees WHERE hire_date BETWEEN '1986-12-01' AND '1986-12-31';
+-------+
| count |
+-------+
| 3081 |
+-------+
1 row in set (0.06 sec)
- 查找姓氏不包含元音(a, e, i, o, u)的员工数量:
mysql> SELECT emp_no,salary FROM salaries ORDER BY salary DESC LIMIT 5;
+--------+--------+
| emp_no | salary |
+--------+--------+
| 43624 | 158220 |
| 43624 | 157821 |
| 254466 | 156286 |
| 47978 | 155709 |
| 253939 | 155513 |
+--------+--------+
5 rows in set (0.74 sec)
限制结果
选择 1986 年之前入职的任何 10 名员工的姓名。您可以通过在语句末尾使用LIMIT子句来实现这一点:
mysql> SELECT emp_no,salary FROM salaries ORDER BY 2 DESC LIMIT 5;
+--------+--------+
| emp_no | salary |
+--------+--------+
| 43624 | 158220 |
| 43624 | 157821 |
| 254466 | 156286 |
| 47978 | 155709 |
| 253939 | 155513 |
+--------+--------+
5 rows in set (0.78 sec)
使用表别名
默认情况下,您在SELECT子句中给出的任何列都将出现在结果中。在前面的示例中,您找到了计数,但它显示为COUNT(*)。您可以通过使用AS别名来更改它:
mysql> SELECT gender, COUNT(*) AS count FROM employees GROUP BY gender;
+--------+--------+
| gender | count |
+--------+--------+
| M | 179973 |
| F | 120051 |
+--------+--------+
2 rows in set (0.14 sec)
排序结果
您可以根据列或别名列对结果进行排序。您可以指定DESC表示降序,或ASC表示升序。默认情况下,排序将是升序。您可以将LIMIT子句与ORDER BY结合使用来限制结果。
如何操作...
找出收入最高的前五名员工的员工 ID。
mysql> SELECT first_name, COUNT(first_name) AS count FROM employees GROUP BY first_name ORDER BY count DESC LIMIT 10;
+-------------+-------+
| first_name | count |
+-------------+-------+
| Shahab | 295 |
| Tetsushi | 291 |
| Elgin | 279 |
| Anyuan | 278 |
| Huican | 276 |
| Make | 275 |
| Panayotis | 272 |
| Sreekrishna | 272 |
| Hatem | 271 |
| Giri | 270 |
+-------------+-------+
10 rows in set (0.21 sec)
您不必指定列名,也可以在SELECT语句中提及列的位置。例如,您正在选择SELECT语句中第二位置的薪资。因此,您可以指定ORDER BY 2:
mysql> SELECT '2017-06-12', YEAR('2017-06-12');
+------------+--------------------+
| 2017-06-12 | YEAR('2017-06-12') |
+------------+--------------------+
| 2017-06-12 | 2017 |
+------------+--------------------+
1 row in set (0.00 sec)
mysql> SELECT YEAR(from_date), SUM(salary) AS sum FROM salaries GROUP BY YEAR(from_date) ORDER BY sum DESC;
+-----------------+-------------+
| YEAR(from_date) | sum |
+-----------------+-------------+
| 2000 | 17535667603 |
| 2001 | 17507737308 |
| 1999 | 17360258862 |
| 1998 | 16220495471 |
| 1997 | 15056011781 |
| 1996 | 13888587737 |
| 1995 | 12638817464 |
| 1994 | 11429450113 |
| 2002 | 10243347616 |
| 1993 | 10215059054 |
| 1992 | 9027872610 |
| 1991 | 7798804412 |
| 1990 | 6626146391 |
| 1989 | 5454260439 |
| 1988 | 4295598688 |
| 1987 | 3156881054 |
| 1986 | 2052895941 |
| 1985 | 972864875 |
+-----------------+-------------+
18 rows in set (1.47 sec)
分组结果(聚合函数)
您可以使用GROUP BY子句对列进行分组,然后使用聚合函数,如COUNT、MAX、MIN和AVERAGE。您也可以在分组子句中对列使用函数。请参阅SUM示例,其中您将使用YEAR()函数。
如何操作...
前面提到的每个聚合函数都将在这里详细介绍给您。
COUNT
- 找出男性和女性员工的数量:
mysql> SELECT emp_no, AVG(salary) AS avg FROM salaries GROUP BY emp_no ORDER BY avg DESC LIMIT 10;
+--------+-------------+
| emp_no | avg |
+--------+-------------+
| 109334 | 141835.3333 |
| 205000 | 141064.6364 |
| 43624 | 138492.9444 |
| 493158 | 138312.8750 |
| 37558 | 138215.8571 |
| 276633 | 136711.7333 |
| 238117 | 136026.2000 |
| 46439 | 135747.7333 |
| 254466 | 135541.0625 |
| 253939 | 135042.2500 |
+--------+-------------+
10 rows in set (0.91 sec
- 您想要找出员工中最常见的 10 个名字。您可以使用
GROUP BY first_name来分组所有名字,然后使用COUNT(first_name)来计算组内的数量,最后使用ORDER BY计数来排序结果。将这些结果限制为前 10 名:
mysql> SELECT DISTINCT title FROM titles;
+--------------------+
| title |
+--------------------+
| Senior Engineer |
| Staff |
| Engineer |
| Senior Staff |
| Assistant Engineer |
| Technique Leader |
| Manager |
+--------------------+
7 rows in set (0.30 sec)
SUM
找出每年向员工支付的工资总额,并按工资排序结果。YEAR()函数返回给定日期的YEAR:
mysql> SELECT emp_no, AVG(salary) AS avg FROM salaries GROUP BY emp_no HAVING avg > 140000 ORDER BY avg DESC;
+--------+-------------+
| emp_no | avg |
+--------+-------------+
| 109334 | 141835.3333 |
| 205000 | 141064.6364 |
+--------+-------------+
2 rows in set (0.80 sec)
mysql> CREATE USER IF NOT EXISTS 'company_read_only'@'localhost'
IDENTIFIED WITH mysql_native_password
BY 'company_pass'
WITH MAX_QUERIES_PER_HOUR 500
MAX_UPDATES_PER_HOUR 100;
AVERAGE
找出平均薪资最高的 10 名员工:
ERROR 1819 (HY000): Your password does not satisfy the current policy requirements
DISTINCT
您可以使用DISTINCT子句来过滤表中的不同条目:
mysql> SELECT PASSWORD('company_pass');
+-------------------------------------------+
|PASSWORD('company_pass') |
+-------------------------------------------+
| *EBD9E3BFD1489CA1EB0D2B4F29F6665F321E8C18 |
+-------------------------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> CREATE USER IF NOT EXISTS 'company_read_only'@'localhost'
IDENTIFIED WITH mysql_native_password
AS '*EBD9E3BFD1489CA1EB0D2B4F29F6665F321E8C18'
WITH MAX_QUERIES_PER_HOUR 500
MAX_UPDATES_PER_HOUR 100;
使用 HAVING 进行筛选
您可以通过添加HAVING子句来筛选GROUP BY子句的结果。
例如,找出平均薪资超过 140,000 的员工:
mysql> GRANT SELECT ON company.* TO 'company_read_only'@'localhost';
Query OK, 0 rows affected (0.06 sec)
另请参阅
还有许多其他聚合函数,请参阅dev.mysql.com/doc/refman/8.0/en/group-by-functions.html了解更多信息。
创建用户
到目前为止,您仅使用 root 用户连接到 MySQL 并执行语句。root 用户在访问 MySQL 时绝不应使用,除非从localhost执行管理任务。您应该创建用户,限制访问,限制资源使用等。要创建新用户,您应该具有将在下一节中讨论的CREATE USER权限。在初始设置期间,您可以使用 root 用户创建其他用户。
如何操作...
使用 root 用户连接到 mysql 并执行CREATE USER命令来创建新用户。
mysql> GRANT INSERT ON company.* TO 'company_insert_only'@'localhost' IDENTIFIED BY 'xxxx';
Query OK, 0 rows affected, 1 warning (0.05 sec)
如果密码不够强,您可能会遇到以下错误。
mysql> SHOW WARNINGS\G
*************************** 1\. row ***************************
Level: Warning
Code: 1287
Message: Using GRANT for creating new user is deprecated and will be removed in future release. Create new user with CREATE USER statement.
1 row in set (0.00 sec)
上述语句将创建具有以下权限的用户:
-
* 用户名:company_read_only。 -
* 仅限从:localhost访问。 -
您可以限制访问 IP 范围。例如:
10.148.%.%。通过给出%,用户可以从任何主机访问。 -
* 密码:company_pass。 -
* 使用 mysql_native_password(默认)认证。 -
您也可以指定任何可插拔的认证,如
sha256_password、LDAP或 Kerberos。 -
用户每小时可执行的
* 最大查询次数为 500 次。 -
用户每小时可执行的
* 最大更新次数为 100 次。
当客户端连接到 MySQL 服务器时,它经历两个阶段:
-
访问控制—连接验证
-
访问控制—请求验证
在连接验证期间,服务器通过用户名和连接来源的主机名识别连接。服务器调用用户的认证插件并验证密码。同时检查用户是否被锁定。
在请求验证阶段,服务器检查用户是否对每个操作拥有足够的权限。
在上述语句中,您必须以明文形式给出密码,这可能会记录在命令历史文件$HOME/.mysql_history中。为了避免这种情况,您可以在本地服务器上计算哈希值,并直接指定哈希字符串。其语法相同,只是mysql_native_password BY 'company_pass'变为mysql_native_password AS 'hashed_string'。
mysql> GRANT INSERT, DELETE, UPDATE ON company.* TO 'company_write'@'%' IDENTIFIED WITH mysql_native_password AS '*EBD9E3BFD1489CA1EB0D2B4F29F6665F321E8C18';
Query OK, 0 rows affected, 1 warning (0.04 sec)
mysql> GRANT SELECT ON employees.employees TO 'employees_read_only'@'%' IDENTIFIED WITH mysql_native_password AS '*EBD9E3BFD1489CA1EB0D2B4F29F6665F321E8C18';
Query OK, 0 rows affected, 1 warning (0.03 sec)
mysql> GRANT SELECT(first_name,last_name) ON employees.employees TO 'employees_ro'@'%' IDENTIFIED WITH mysql_native_password AS '*EBD9E3BFD1489CA1EB0D2B4F29F6665F321E8C18';
Query OK, 0 rows affected, 1 warning (0.06 sec)
您可以直接通过授予权限来创建用户。请参考下一节了解如何授予权限。不过,MySQL 将在下一版本中弃用此功能。
另请参阅
更多关于创建用户选项,请参考dev.mysql.com/doc/refman/8.0/en/create-user.html。更安全的选项,如 SSL,使用其他认证方法将在第十四章安全中讨论。
授予和撤销用户访问权限
您可以限制用户访问特定的数据库或表,并且仅限于特定的操作,如SELECT、INSERT和UPDATE。要授予其他用户权限,您需要拥有GRANT权限。
如何操作...
在初始设置期间,您可以使用 root 用户授予权限。您也可以创建一个管理账户来管理用户。
授予权限
- 授予
company_read_only用户READ ONLY(SELECT)权限:
mysql> GRANT SELECT(salary) ON employees.salaries TO 'employees_ro'@'%';
Query OK, 0 rows affected (0.00 sec)
星号(*)代表数据库内的所有表。
- 授予新用户
company_insert_onlyINSERT权限:
mysql> CREATE USER 'dbadmin'@'%' IDENTIFIED WITH mysql_native_password BY 'DB@dm1n';
Query OK, 0 rows affected (0.01 sec)
mysql> GRANT ALL ON *.* TO 'dbadmin'@'%';
Query OK, 0 rows affected (0.01 sec)
- 授予新用户
company_writeWRITE权限:
mysql> GRANT GRANT OPTION ON *.* TO 'dbadmin'@'%';
Query OK, 0 rows affected (0.03 sec)
- 限制于特定表。将
employees_read_only用户限制为仅能从employees表执行SELECT操作:
mysql> SHOW GRANTS FOR 'employees_ro'@'%'\G
*************************** 1\. row ***************************
Grants for employees_ro@%: GRANT USAGE ON *.* TO `employees_ro`@`%`
*************************** 2\. row ***************************
Grants for employees_ro@%: GRANT SELECT (`first_name`, `last_name`) ON `employees`.`employees` TO `employees_ro`@`%`
*************************** 3\. row ***************************
Grants for employees_ro@%: GRANT SELECT (`salary`) ON `employees`.`salaries` TO `employees_ro`@`%`
- 您可以进一步限制到特定列。将
employees_ro用户限制为employees表的first_name和last_name列:
mysql> SHOW GRANTS FOR 'dbadmin'@'%'\G
*************************** 1\. row ***************************
Grants for dbadmin@%: GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, SHUTDOWN, PROCESS, FILE, REFERENCES, INDEX, ALTER, SHOW DATABASES, SUPER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER, CREATE TABLESPACE, CREATE ROLE, DROP ROLE ON *.* TO `dbadmin`@`%` WITH GRANT OPTION
*************************** 2\. row ***************************
Grants for dbadmin@%: GRANT BINLOG_ADMIN,CONNECTION_ADMIN,ENCRYPTION_KEY_ADMIN,GROUP_REPLICATION_ADMIN,REPLICATION_SLAVE_ADMIN,ROLE_ADMIN,SET_USER_ID,SYSTEM_VARIABLES_ADMIN ON *.* TO `dbadmin`@`%`
2 rows in set (0.00 sec)
- 扩展授权。您可以通过执行新的授权来扩展授权。将权限扩展到
employees_col_ro用户,使其能够访问salaries表的薪资信息:
mysql> REVOKE DELETE ON company.* FROM 'company_write'@'%';
Query OK, 0 rows affected (0.04 sec)
- 创建
SUPER用户。您需要一个管理账户来管理服务器。ALL表示所有权限,但不包括GRANT权限:
mysql> REVOKE SELECT(salary) ON employees.salaries FROM 'employees_ro'@'%';
Query OK, 0 rows affected (0.03 sec)
mysql> SELECT * FROM mysql.user WHERE user='dbadmin'\G
*************************** 1\. row ***************************
Host: %
User: dbadmin
Select_priv: Y
Insert_priv: Y
Update_priv: Y
Delete_priv: Y
Create_priv: Y
Drop_priv: Y
Reload_priv: Y
Shutdown_priv: Y
Process_priv: Y
File_priv: Y
Grant_priv: Y
References_priv: Y
Index_priv: Y
Alter_priv: Y
Show_db_priv: Y
Super_priv: Y
Create_tmp_table_priv: Y
Lock_tables_priv: Y
Execute_priv: Y
Repl_slave_priv: Y
Repl_client_priv: Y
Create_view_priv: Y
Show_view_priv: Y
Create_routine_priv: Y
Alter_routine_priv: Y
Create_user_priv: Y
Event_priv: Y
Trigger_priv: Y
Create_tablespace_priv: Y
ssl_type:
ssl_cipher:
x509_issuer:
x509_subject:
max_questions: 0
max_updates: 0
max_connections: 0
max_user_connections: 0
plugin: mysql_native_password
authentication_string: *AB7018ADD9CB4EDBEB680BB3F820479E4CE815D2
password_expired: N
password_last_changed: 2017-06-10 16:24:03
password_lifetime: NULL
account_locked: N
Create_role_priv: Y
Drop_role_priv: Y
1 row in set (0.00 sec)
- 授予
GRANT权限。用户应具有GRANT OPTION权限才能将权限授予其他用户。您可以将GRANT权限扩展到dbadmin超级用户:
mysql> UPDATE mysql.user SET host='localhost' WHERE user='dbadmin';
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.00 sec)
更多权限类型请参考dev.mysql.com/doc/refman/8.0/en/grant.html。
检查授权
您可以查看所有用户的授权。检查employee_col_ro用户的授权:
mysql> CREATE USER 'developer'@'%' IDENTIFIED WITH mysql_native_password AS '*EBD9E3BFD1489CA1EB0D2B4F29F6665F321E8C18' PASSWORD EXPIRE;
Query OK, 0 rows affected (0.04 sec
检查dbadmin用户的授权。您可以看到dbadmin用户可用的所有授权:
shell> mysql -u developer -pcompany_pass
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 31
Server version: 8.0.3-rc-log
Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> SHOW DATABASES;
ERROR 1820 (HY000): You must reset your password using ALTER USER statement before executing this statement.
撤销授权
撤销授权的语法与创建授权相同。您将权限授予用户,并从用户那里撤销权限。
- 撤销
'company_write'@'%'用户的DELETE权限:
mysql> ALTER USER 'developer'@'%' IDENTIFIED WITH mysql_native_password BY 'new_company_pass';
Query OK, 0 rows affected (0.03 sec)
- 撤销
employee_ro用户对薪资列的访问权限:
mysql> ALTER USER 'developer'@'%' PASSWORD EXPIRE;
Query OK, 0 rows affected (0.06 sec)
修改 mysql.user 表
所有用户信息及其权限都存储在mysql.user表中。如果您有权访问mysql.user表,您可以直接修改mysql.user表来创建用户和授予权限。
如果您间接修改授权表,使用如GRANT、REVOKE、SET PASSWORD或RENAME USER等账户管理语句,服务器会注意到这些更改并立即将授权表加载到内存中。
如果您直接使用如INSERT、UPDATE或DELETE等语句修改授权表,您的更改在重启服务器或告知服务器重新加载表之前不会影响权限检查。如果您直接更改授权表但忘记重新加载它们,您的更改在重启服务器之前不会生效。
通过发出FLUSH PRIVILEGES语句可以重新加载GRANT表。
查询mysql.user表以查找dbadmin用户的所有条目:
mysql> ALTER USER 'developer'@'%' PASSWORD EXPIRE INTERVAL 90 DAY;
Query OK, 0 rows affected (0.04 sec)
您可以看到dbadmin用户可以从任何主机(%)访问数据库。您只需更新mysql.user表并重新加载授权表,即可将其限制为localhost:
mysql> ALTER USER 'developer'@'%' ACCOUNT LOCK;
Query OK, 0 rows affected (0.05 sec)
shell> mysql -u developer -pnew_company_pass
mysql: [Warning] Using a password on the command line interface can be insecure.
ERROR 3118 (HY000): Access denied for user 'developer'@'localhost'. Account is locked.
设置用户密码过期
您可以设置用户密码在特定时间间隔后过期;之后,他们需要更改密码。
当应用程序开发者请求数据库访问时,您可以创建一个带有默认密码的账户,然后设置其过期。您可以将密码分享给开发者,之后他们必须更改密码才能继续使用 MySQL。
所有账户创建时密码过期时间等于default_password_lifetime变量,该变量默认禁用:
- 创建一个密码已过期的用户。当开发者首次登录并尝试执行任何语句时,会抛出
ERROR 1820 (HY000):。在执行此语句前,必须使用ALTER USER语句重置密码:
mysql> ALTER USER 'developer'@'%' ACCOUNT UNLOCK;
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE ROLE 'app_read_only', 'app_writes', 'app_developer';
Query OK, 0 rows affected (0.01 sec)
开发者需使用以下命令更改其密码:
mysql> GRANT SELECT ON employees.* TO 'app_read_only';
Query OK, 0 rows affected (0.00 sec)
- 手动使现有用户过期:
mysql> GRANT INSERT, UPDATE, DELETE ON employees.* TO 'app_writes';
Query OK, 0 rows affected (0.00 sec)
- 要求每 180 天更改一次密码:
mysql> GRANT ALL ON employees.* TO 'app_developer';
Query OK, 0 rows affected (0.04 sec)
锁定用户
若发现账户有任何问题,可以锁定它。MySQL 支持在使用CREATE USER或ALTER USER时进行锁定。
通过在ALTER USER语句中添加ACCOUNT LOCK子句来锁定账户:
mysql> CREATE user emp_read_only IDENTIFIED BY 'emp_pass';
Query OK, 0 rows affected (0.06 sec)
开发者将收到账户已锁定的错误信息:
mysql> CREATE user emp_writes IDENTIFIED BY 'emp_pass';
Query OK, 0 rows affected (0.04 sec)
确认后,你可以解锁账户:
mysql> CREATE user emp_developer IDENTIFIED BY 'emp_pass';
Query OK, 0 rows affected (0.01 sec)
为用户创建角色
MySQL 角色是权限的命名集合。与用户账户一样,角色可以被授予和撤销权限。用户账户可以被授予角色,从而将角色权限授予该账户。之前,你为读取、写入和管理创建了单独的用户。对于写权限,你已向用户授予INSERT、DELETE和UPDATE。相反,你可以将这些权限授予一个角色,然后将用户分配给该角色。通过这种方式,你可以避免向可能的许多用户账户单独授予权限。
- 创建角色:
mysql> CREATE user emp_read_write IDENTIFIED BY 'emp_pass';
Query OK, 0 rows affected (0.00 sec)
- 使用
GRANT语句为角色分配权限:
mysql> GRANT 'app_read_only' TO 'emp_read_only'@'%';
Query OK, 0 rows affected (0.04 sec)
mysql> GRANT 'app_writes' TO 'emp_writes'@'%';
Query OK, 0 rows affected (0.00 sec)
mysql> GRANT 'app_developer' TO 'emp_developer'@'%';
Query OK, 0 rows affected (0.00 sec)
- 创建用户。如果不指定任何主机,将采用
%:
mysql> GRANT 'app_read_only', 'app_writes' TO 'emp_read_write'@'%';
Query OK, 0 rows affected (0.05 sec)
mysql> GRANT SELECT ON employees.* TO 'user_ro_file'@'%' IDENTIFIED WITH mysql_native_password AS '*EBD9E3BFD1489CA1EB0D2B4F29F6665F321E8C18';
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql> GRANT FILE ON *.* TO 'user_ro_file'@'%' IDENTIFIED WITH mysql_native_password AS '*EBD9E3BFD1489CA1EB0D2B4F29F6665F321E8C18';
Query OK, 0 rows affected, 1 warning (0.00 sec)
shell> sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf
shell> sudo systemctl restart mysql
-
使用
GRANT语句为用户分配角色。你可以为一个用户分配多个角色。例如,你可以为
emp_read_write用户分配读写权限:
mysql> SELECT first_name, last_name INTO OUTFILE 'result.csv'
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n'
FROM employees WHERE hire_date<'1986-01-01' LIMIT 10;
Query OK, 10 rows affected (0.00 sec)
shell> sudo cat /var/lib/mysql/employees/result.csv
"Bezalel","Simmel"
"Sumant","Peac"
"Eberhardt","Terkki"
"Otmar","Herbst"
"Florian","Syrotiuk"
"Tse","Herber"
"Udi","Jansch"
"Reuven","Garigliano"
"Erez","Ritzmann"
"Premal","Baek"
mysql> CREATE TABLE titles_only AS SELECT DISTINCT title FROM titles;
Query OK, 7 rows affected (0.50 sec)
Records: 7 Duplicates: 0 Warnings: 0
mysql> INSERT INTO titles_only SELECT DISTINCT title FROM titles;
Query OK, 7 rows affected (0.46 sec)
Records: 7 Duplicates: 0 Warnings: 0
作为一项安全措施,避免使用%并限制应用程序部署所在 IP 的访问。
将数据选择到文件和表中
你可以使用SELECT INTO OUTFILE语句将输出保存到文件中。
你可以指定列和行分隔符,之后可以将数据导入其他数据平台。
操作方法...
你可以将输出目的地保存为文件或表。
保存为文件
- 要将输出保存到文件,你需要
FILE权限。FILE是一个全局权限,这意味着你不能将其限制在特定数据库上。但是,你可以限制用户的选择:
mysql> CREATE TABLE employee_names (
`first_name` varchar(14) NOT NULL,
`last_name` varchar(16) NOT NULL
) ENGINE=InnoDB;
Query OK, 0 rows affected (0.07 sec)
-
在 Ubuntu 上,默认情况下,MySQL 不允许你写入文件。你应该在配置文件中设置
secure_file_priv并重启 MySQL。你将在第四章《配置 MySQL》中了解更多关于配置的信息。在 CentOS、Red Hat 上,secure_file_priv被设置为/var/lib/mysql-files,这意味着所有文件都将保存在该目录中。 -
目前,请按如下方式启用。打开配置文件并添加
secure_file_priv = /var/lib/mysql:
shell> sudo ls -lhtr /var/lib/mysql/employees/result.csv
-rw-rw-rw- 1 mysql mysql 180 Jun 10 14:53 /var/lib/mysql/employees/result.csv
- 重启 MySQL 服务器:
mysql> LOAD DATA INFILE 'result.csv' INTO TABLE employee_names
FIELDS TERMINATED BY ','
OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n';
Query OK, 10 rows affected (0.01 sec)
Records: 10 Deleted: 0 Skipped: 0 Warnings: 0
以下语句将把输出保存为 CSV 格式:
mysql> LOAD DATA INFILE 'result.csv' INTO TABLE employee_names
FIELDS TERMINATED BY ','
OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 LINES;
你可以检查文件的输出,该文件将在{secure_file_priv}/{database_name}指定的路径中创建,在本例中是/var/lib/mysql/employees/。如果文件已存在,语句将失败,因此每次执行时你需要给出一个唯一名称或移动文件到不同位置:
mysql> LOAD DATA INFILE 'result.csv' REPLACE INTO TABLE employee_names FIELDS TERMINATED BY ','OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n';
Query OK, 10 rows affected (0.01 sec)
Records: 10 Deleted: 0 Skipped: 0 Warnings: 0
mysql> LOAD DATA INFILE 'result.csv' IGNORE INTO TABLE employee_names FIELDS TERMINATED BY ','OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n';
Query OK, 10 rows affected (0.06 sec)
Records: 10 Deleted: 0 Skipped: 0 Warnings: 0
保存为表
你可以将SELECT语句的结果保存到表中。即使表不存在,你也可以使用CREATE和SELECT来创建表并加载数据。如果表已存在,你可以使用INSERT和SELECT来加载数据。
你可以将标题保存到一个新的titles_only表中:
mysql> LOAD DATA LOCAL INFILE 'result.csv' IGNORE INTO TABLE employee_names FIELDS TERMINATED BY ','OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n';
如果表已存在,你可以使用INSERT INTO SELECT语句:
mysql> SELECT emp.emp_no, emp.first_name, emp.last_name
FROM employees AS emp
WHERE emp.emp_no=110022;
+--------+------------+------------+
| emp_no | first_name | last_name |
+--------+------------+------------+
| 110022 | Margareta | Markovitch |
+--------+------------+------------+
1 row in set (0.00 sec)
为了避免重复,你可以使用INSERT IGNORE。然而,在这种情况下,titles_only表没有PRIMARY KEY。因此,IGNORE子句没有任何影响。
向表中加载数据
你可以将表数据转储到文件中,反之亦然,即从文件加载数据到表中。这在加载大量数据时广泛使用,是向表中快速加载数据的超级方法。你可以指定列分隔符以将数据加载到相应的列中。你应该拥有对表的FILE权限和INSERT权限。
如何操作...
之前,你已将first_name和last_name保存到文件中。你可以使用同一文件将数据加载到另一个表中。在加载之前,你应该创建该表。如果表已存在,你可以直接加载。表的列应与文件的字段匹配。
创建一个表来存储数据:
mysql> SELECT dept_no FROM dept_manager AS dept_mgr WHERE dept_mgr.emp_no=110022;
+---------+
| dept_no |
+---------+
| d001 |
+---------+
1 row in set (0.00 sec)
确保文件存在:
mysql> SELECT dept_name FROM departments dept WHERE dept.dept_no='d001';
+-----------+
| dept_name |
+-----------+
| Marketing |
+-----------+
1 row in set (0.00 sec)
使用LOAD DATA INFILE语句加载数据:
mysql> SELECT
emp.emp_no,
emp.first_name,
emp.last_name,
dept.dept_name
FROM
employees AS emp
JOIN dept_manager AS dept_mgr
ON emp.emp_no=dept_mgr.emp_no AND emp.emp_no=110022
JOIN departments AS dept
ON dept_mgr.dept_no=dept.dept_no;
+--------+------------+------------+-----------+
| emp_no | first_name | last_name | dept_name |
+--------+------------+------------+-----------+
| 110022 | Margareta | Markovitch | Marketing |
+--------+------------+------------+-----------+
1 row in set (0.00 sec)
文件可以通过完整路径名给出以指定其确切位置。如果以相对路径名给出,则该名称相对于客户端程序启动的目录进行解释。
- 如果文件包含你想忽略的任何标题,指定
IGNORE n LINES:
mysql> SELECT
dept_name,
AVG(salary) AS avg_salary
FROM
salaries
JOIN dept_emp
ON salaries.emp_no=dept_emp.emp_no
JOIN departments
ON dept_emp.dept_no=departments.dept_no
GROUP BY
dept_emp.dept_no
ORDER BY
avg_salary
DESC;
+--------------------+------------+
| dept_name | avg_salary |
+--------------------+------------+
| Sales | 80667.6058 |
| Marketing | 71913.2000 |
| Finance | 70489.3649 |
| Research | 59665.1817 |
| Production | 59605.4825 |
| Development | 59478.9012 |
| Customer Service | 58770.3665 |
| Quality Management | 57251.2719 |
| Human Resources | 55574.8794 |
+--------------------+------------+
9 rows in set (8.29 sec)
- 你可以指定
REPLACE或IGNORE来处理重复项:
mysql> ALTER TABLE employees ADD INDEX name(first_name, last_name);
Query OK, 0 rows affected (1.95 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> SELECT
emp1.*
FROM
employees emp1
JOIN employees emp2
ON emp1.first_name=emp2.first_name
AND emp1.last_name=emp2.last_name
AND emp1.gender=emp2.gender
AND emp1.hire_date=emp2.hire_date
AND emp1.emp_no!=emp2.emp_no
ORDER BY
first_name, last_name;
+--------+------------+------------+-----------+--------+------------+
| emp_no | birth_date | first_name | last_name | gender | hire_date |
+--------+------------+------------+-----------+--------+------------+
| 232772 | 1962-05-14 | Keung | Heusch | M | 1986-06-01 |
| 493600 | 1964-01-26 | Keung | Heusch | M | 1986-06-01 |
| 64089 | 1958-01-19 | Marit | Kolvik | F | 1993-12-08 |
| 424486 | 1952-07-06 | Marit | Kolvik | F | 1993-12-08 |
| 40965 | 1952-05-11 | Marsha | Farrow | M | 1989-02-18 |
| 14641 | 1953-05-08 | Marsha | Farrow | M | 1989-02-18 |
| 422332 | 1954-08-17 | Naftali | Mawatari | M | 1985-09-14 |
| 427429 | 1962-11-06 | Naftali | Mawatari | M | 1985-09-14 |
| 19454 | 1955-05-14 | Taisook | Hutter | F | 1985-02-26 |
| 243627 | 1957-02-14 | Taisook | Hutter | F | 1985-02-26 |
+--------+------------+------------+-----------+--------+------------+
10 rows in set (34.01 sec)
- MySQL 假设你想要加载的文件在服务器上可用。如果你从远程客户端机器连接到服务器,你可以指定
LOCAL来加载位于客户端的文件。本地文件将从客户端复制到服务器。文件保存在服务器的标准临时位置。在 Linux 机器上,它是/tmp:
mysql> SELECT emp_no FROM titles WHERE title="Senior Engineer" AND from_date="1986-06-26";
+--------+
| emp_no |
+--------+
| 10001 |
| 84305 |
| 228917 |
| 426700 |
| 458304 |
+--------+
5 rows in set (0.14 sec)
连接表
到目前为止,你已经查看了从单个表插入和检索数据。在本节中,我们将讨论如何结合两个或更多表来检索结果。
一个完美的例子是,你想查找emp_no: 110022的员工姓名和部门编号:
-
部门编号和名称存储在
departments表中 -
员工编号及其他详细信息,如
first_name和last_name,存储在employees表中 -
员工与部门的映射存储在
dept_manager表中
如果你不想使用JOIN,你可以这样做:
- 从
employee表中查找emp_no为110022的员工姓名:
mysql> SELECT first_name, last_name FROM employees WHERE emp_no IN (< output from preceding query>)
mysql> SELECT first_name, last_name FROM employees WHERE emp_no IN (10001,84305,228917,426700,458304);
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| Georgi | Facello |
| Minghong | Kalloufi |
| Nechama | Bennet |
| Nagui | Restivo |
| Shuzo | Kirkerud |
+------------+-----------+
5 rows in set (0.00 sec
- 从
departments表中查找部门编号:
mysql> SELECT
first_name,
last_name
FROM
employees
WHERE
emp_no
IN (SELECT emp_no FROM titles WHERE title="Senior Engineer" AND from_date="1986-06-26");
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| Georgi | Facello |
| Minghong | Kalloufi |
| Nagui | Restivo |
| Nechama | Bennet |
| Shuzo | Kirkerud |
+------------+-----------+
5 rows in set (0.91 sec)
- 从
departments表中查找部门名称:
mysql> SELECT emp_no FROM salaries WHERE salary=(SELECT MAX(salary) FROM salaries);
+--------+
| emp_no |
+--------+
| 43624 |
+--------+
1 row in set (1.54 sec)
如何操作...
为了避免使用三个不同的语句在三个不同的表中查找,你可以使用JOIN将它们组合起来。需要注意的是,要连接两个表,你应该有一个或多个共同的列来连接。你可以基于emp_no将员工表和dept_manager表连接起来,它们都有emp_no列。虽然列名不需要匹配,但你应该弄清楚你可以基于哪个列进行连接。同样,dept_mgr和departments有一个共同的列dept_no。
就像给列起别名一样,你可以给表起一个别名,并使用别名引用该表的列。例如,你可以通过FROM employees AS emp给员工表起一个别名,并使用点表示法,如emp.emp_no,引用employees表的列:
mysql> CREATE TABLE employees_list1 AS SELECT * FROM employees WHERE first_name LIKE 'aa%';
Query OK, 444 rows affected (0.22 sec)
Records: 444 Duplicates: 0 Warnings: 0
mysql> CREATE TABLE employees_list2 AS SELECT * FROM employees WHERE emp_no BETWEEN 400000 AND 500000 AND gender='F';
Query OK, 39892 rows affected (0.59 sec)
Records: 39892 Duplicates: 0 Warnings: 0
我们来看另一个例子——你想找出每个部门的平均薪资。为此,你可以使用AVG函数并按dept_no分组。要获取部门名称,你可以将结果与departments表基于dept_no进行连接:
mysql> SELECT * FROM employees_list1 WHERE emp_no IN (SELECT emp_no FROM employees_list2);
使用自连接识别重复项
你想找出表中特定列的重复行。例如,你想找出哪些员工具有相同的first_name、相同的last_name、相同的gender和相同的hire_date。在这种情况下,你可以在JOIN子句中指定要查找重复项的列,将employees表与自身连接。你需要为每个表使用不同的别名。
你需要在你想要连接的列上添加索引。索引将在第十三章性能调优中讨论。现在,你可以执行此命令来添加索引:
mysql> SELECT l1.* FROM employees_list1 l1 JOIN employees_list2 l2 ON l1.emp_no=l2.emp_no;
mysql> SELECT * FROM employees_list1 WHERE emp_no NOT IN (SELECT emp_no FROM employees_list2);
你必须提及emp1.emp_no != emp2.emp_no,因为员工会有不同的emp_no。否则,同一员工会重复出现。
使用子查询
子查询是一个嵌套在另一个语句中的SELECT语句。假设你想找出在1986-06-26作为Senior Engineer开始工作的员工姓名。
你可以从titles表中获取emp_no,从employees表中获取姓名。你也可以使用JOIN来找出结果。
从 titles 表中获取emp_no:
mysql> SELECT l1.* FROM employees_list1 l1 LEFT OUTER JOIN employees_list2 l2 ON l1.emp_no=l2.emp_no WHERE l2.emp_no IS NULL;
查找名称的方法:
mysql> SELECT l1.* FROM employees_list1 l1 LEFT OUTER JOIN employees_list2 l2 ON l1.emp_no=l2.emp_no WHERE l2.emp_no IS NOT NULL;
其他子句,如EXISTS和EQUAL,在 MySQL 中也得到支持。更多详情,请参考参考手册,dev.mysql.com/doc/refman/8.0/en/subqueries.html:
/* DROP the existing procedure if any with the same name before creating */
DROP PROCEDURE IF EXISTS create_employee;
/* Change the delimiter to $$ */
DELIMITER $$
/* IN specifies the variables taken as arguments, INOUT specifies the output variable*/
CREATE PROCEDURE create_employee (OUT new_emp_no INT, IN first_name varchar(20), IN last_name varchar(20), IN gender enum('M','F'), IN birth_date date, IN emp_dept_name varchar(40), IN title varchar(50))
BEGIN
/* Declare variables for emp_dept_no and salary */
DECLARE emp_dept_no char(4);
DECLARE salary int DEFAULT 60000;
/* Select the maximum employee number into the variable new_emp_no */
SELECT max(emp_no) INTO new_emp_no FROM employees;
/* Increment the new_emp_no */
SET new_emp_no = new_emp_no + 1;
/* INSERT the data into employees table */
/* The function CURDATE() gives the current date) */
INSERT INTO employees VALUES(new_emp_no, birth_date, first_name, last_name, gender, CURDATE());
/* Find out the dept_no for dept_name */
SELECT emp_dept_name;
SELECT dept_no INTO emp_dept_no FROM departments WHERE dept_name=emp_dept_name;
SELECT emp_dept_no;
/* Insert into dept_emp */
INSERT INTO dept_emp VALUES(new_emp_no, emp_dept_no, CURDATE(), '9999-01-01');
/* Insert into titles */
INSERT INTO titles VALUES(new_emp_no, title, CURDATE(), '9999-01-01');
/* Find salary based on title */
IF title = 'Staff'
THEN SET salary = 100000;
ELSEIF title = 'Senior Staff'
THEN SET salary = 120000;
END IF;
/* Insert into salaries */
INSERT INTO salaries VALUES(new_emp_no, salary, CURDATE(), '9999-01-01');
END
$$
/* Change the delimiter back to ; */
DELIMITER ;
找出薪资最高的员工:
mysql> GRANT EXECUTE ON employees.* TO 'emp_read_only'@'%';
Query OK, 0 rows affected (0.05 sec)
SELECT MAX(salary) FROM salaries是给出最高薪资的子查询,要找出对应于该薪资的员工编号,你可以在WHERE子句中使用该子查询。
查找表间不匹配的行
假设您想查找一个表中不在其他表中的行。您可以通过两种方式实现这一点。使用NOT IN子句或使用OUTER JOIN。
要找到匹配的行,您可以使用普通JOIN,如果您想找到不匹配的行,您可以使用OUTER JOIN。普通JOIN意味着A 交集 B。OUTER JOIN给出A和B的匹配记录,以及A的NULL不匹配记录。如果您想要A-B的输出,您可以使用WHERE <JOIN COLUMN IN B> IS NULL子句。
要理解OUTER JOIN的用法,请创建两个employee表并插入一些值:
shell> mysql -u emp_read_only -pemp_pass employees -A
您已经知道如何找到同时存在于两个列表中的员工:
mysql> CALL create_employee(@new_emp_no, 'John', 'Smith', 'M', '1984-06-19', 'Research', 'Staff');
Query OK, 1 row affected (0.01 sec)
或者您可以使用JOIN:
mysql> SELECT @new_emp_no;
+-------------+
| @new_emp_no |
+-------------+
| 500000 |
+-------------+
1 row in set (0.00 sec)
要找出存在于employees_list1但不在employees_list2中的员工:
mysql> SELECT * FROM employees WHERE emp_no=500000;
+--------+------------+------------+-----------+--------+------------+
| emp_no | birth_date | first_name | last_name | gender | hire_date |
+--------+------------+------------+-----------+--------+------------+
| 500000 | 1984-06-19 | John | Smith | M | 2017-06-17 |
+--------+------------+------------+-----------+--------+------------+
1 row in set (0.00 sec)
mysql> SELECT * FROM salaries WHERE emp_no=500000;
+--------+--------+------------+------------+
| emp_no | salary | from_date | to_date |
+--------+--------+------------+------------+
| 500000 | 100000 | 2017-06-17 | 9999-01-01 |
+--------+--------+------------+------------+
1 row in set (0.00 sec)
mysql> SELECT * FROM titles WHERE emp_no=500000;
+--------+-------+------------+------------+
| emp_no | title | from_date | to_date |
+--------+-------+------------+------------+
| 500000 | Staff | 2017-06-17 | 9999-01-01 |
+--------+-------+------------+------------+
1 row in set (0.00 sec)
或者您可以使用OUTER JOIN:
shell> vi function.sql;
DROP FUNCTION IF EXISTS get_sal_level;
DELIMITER $$
CREATE FUNCTION get_sal_level(emp int) RETURNS VARCHAR(10)
DETERMINISTIC
BEGIN
DECLARE sal_level varchar(10);
DECLARE avg_sal FLOAT;
SELECT AVG(salary) INTO avg_sal FROM salaries WHERE emp_no=emp;
IF avg_sal < 50000 THEN
SET sal_level = 'BRONZE';
ELSEIF (avg_sal >= 50000 AND avg_sal < 70000) THEN
SET sal_level = 'SILVER';
ELSEIF (avg_sal >= 70000 AND avg_sal < 90000) THEN
SET sal_level = 'GOLD';
ELSEIF (avg_sal >= 90000) THEN
SET sal_level = 'PLATINUM';
ELSE
SET sal_level = 'NOT FOUND';
END IF;
RETURN (sal_level);
END
$$
DELIMITER ;
外连接为连接列表中第二个表的每个不匹配行创建NULL列。如果您使用RIGHT JOIN,则第一个表将为不匹配的行获取NULL值。
您也可以使用OUTER JOIN来查找匹配的行。而不是WHERE l2.emp_no IS NULL,给出WHERE emp_no IS NOT NULL:
mysql> SOURCE function.sql;
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.01 sec)
You have to pass the employee number and the function returns the income level.
mysql> SELECT get_sal_level(10002);
+----------------------+
| get_sal_level(10002) |
+----------------------+
| SILVER |
+----------------------+
1 row in set (0.00 sec)
mysql> SELECT get_sal_level(10001);
+----------------------+
| get_sal_level(10001) |
+----------------------+
| GOLD |
+----------------------+
1 row in set (0.00 sec)
mysql> SELECT get_sal_level(1);
+------------------+
| get_sal_level(1) |
+------------------+
| NOT FOUND |
+------------------+
1 row in set (0.00 sec)
存储过程
假设您需要在 MySQL 中执行一系列语句,而不是每次发送所有 SQL 语句,您可以将所有语句封装在一个程序中,并在需要时调用它。存储过程是一组不需要返回值的 SQL 语句。
除了 SQL 语句,您还可以利用变量来存储结果并在存储过程中进行编程操作。例如,您可以编写IF、CASE子句、逻辑运算和WHILE循环。
-
存储函数和过程也称为存储例程。
-
要创建存储过程,您应该拥有
CREATE ROUTINE权限。 -
存储函数将有一个返回值。
-
存储过程没有返回值。
-
所有代码都写在
BEGIN 和 END块内。 -
存储函数可以直接在
SELECT语句中调用。 -
存储过程可以使用
CALL语句调用。 -
由于存储过程中的语句应以分隔符(
;)结尾,因此您需要更改 MySQL 的分隔符,以便 MySQL 不会将存储例程内的 SQL 语句解释为普通语句。创建过程后,您可以将分隔符改回默认值。
如何操作...
例如,您想要添加一名新员工。您应该更新三个表,即employees、salaries和titles。而不是执行三个语句,您可以开发一个存储过程并调用它来创建新的employee。
您必须传递员工的first_name、last_name、gender和birth_date,以及员工加入的部门。您可以通过输入变量传递这些信息,并且您应该获得员工编号作为输出。存储过程不返回值,但它可以更新变量,您可以使用它。
以下是一个简单的存储过程示例,用于创建新员工并更新salary和department表:
mysql> SELECT * FROM employees WHERE hire_date = CURDATE();
创建存储过程,您可以:
-
将其粘贴到命令行客户端中
-
将其保存到文件中,并使用
mysql -u <user> -p employees < stored_procedure.sql将其导入 MySQL。 -
源
mysql> SOURCE stored_procedure.sql文件
要使用存储过程,需向emp_read_only用户授予执行权限:
mysql> SELECT DATE_ADD(CURDATE(), INTERVAL -7 DAY) AS '7 Days Ago';
使用CALL stored_procedure(OUT variable, IN values)语句和例程名称调用存储过程。
使用emp_read_only账户连接到 MySQL:
mysql> SELECT CONCAT(first_name, ' ', last_name) FROM employees LIMIT 1;
+------------------------------------+
| CONCAT(first_name, ' ', last_name) |
+------------------------------------+
| Aamer Anger |
+------------------------------------+
1 row in set (0.00 sec)
传递您希望存储@new_emp_no输出的变量,以及所需的输入值:
shell> vi before_insert_trigger.sql
DROP TRIGGER IF EXISTS salary_round;
DELIMITER $$
CREATE TRIGGER salary_round BEFORE INSERT ON salaries
FOR EACH ROW
BEGIN
SET NEW.salary=ROUND(NEW.salary);
END
$$
DELIMITER ;
选择存储在变量@new_emp_no中的emp_no值:
mysql> SOURCE before_insert_trigger.sql;
Query OK, 0 rows affected (0.06 sec)
Query OK, 0 rows affected (0.00 sec)
检查employees、salaries和titles表中是否创建了行:
mysql> INSERT INTO salaries VALUES(10002, 100000.79, CURDATE(), '9999-01-01');
Query OK, 1 row affected (0.04 sec)
您可以看到,尽管emp_read_only对表没有写入权限,但它仍能通过调用存储过程进行写入。如果存储过程的SQL SECURITY设置为INVOKER,则emp_read_only无法修改数据。请注意,如果您是通过localhost连接,需为localhost用户创建权限。
要列出数据库中的所有存储过程,执行SHOW PROCEDURE STATUS\G。要检查现有存储例程的定义,可执行SHOW CREATE PROCEDURE <procedure_name>\G。
还有更多...
存储过程也用于增强安全性。用户需对存储过程拥有EXECUTE权限才能执行。
根据存储例程的定义:
-
DEFINER子句指定存储例程的创建者。如未指定,则采用当前用户。 -
SQL SECURITY子句指定存储例程的执行上下文,可以是DEFINER或INVOKER。
DEFINER:即使只有例程的EXECUTE权限的用户也能调用并获取存储例程的输出,无论该用户对底层表是否有权限。只要DEFINER拥有权限就足够了。
INVOKER:安全上下文切换到调用存储例程的用户。在这种情况下,调用者应对底层表有访问权限。
参见
请参阅文档以获取更多示例和语法,位于dev.mysql.com/doc/refman/8.0/en/create-procedure.html。
函数
与存储过程类似,您也可以创建存储函数。主要区别在于函数应有返回值,并可在SELECT中调用。通常,存储函数用于简化复杂计算。
如何操作...
以下是一个如何编写函数及如何调用的示例。假设银行家想根据收入水平发放信用卡,而不暴露实际工资,您可以公开此函数以查找收入水平:
mysql> SELECT * FROM salaries WHERE emp_no=10002 AND from_date=CURDATE();
+--------+--------+------------+------------+
| emp_no | salary | from_date | to_date |
+--------+--------+------------+------------+
| 10002 | 100001 | 2017-06-18 | 9999-01-01 |
+--------+--------+------------+------------+
1 row in set (0.00 sec)
创建函数:
mysql> CREATE TABLE salary_audit (emp_no int, user varchar(50), date_modified date);
shell> vi before_insert_trigger.sql
DELIMITER $$
CREATE TRIGGER salary_audit
BEFORE INSERT
ON salaries FOR EACH ROW PRECEDES salary_round
BEGIN
INSERT INTO salary_audit VALUES(NEW.emp_no, USER(), CURDATE());
END; $$
DELIMITER ;
要列出数据库中的所有存储函数,执行SHOW FUNCTION STATUS\G。要检查现有存储函数的定义,你可以执行SHOW CREATE FUNCTION <function_name>\G。
在函数创建中给出DETERMINISTIC关键字非常重要。如果一个例程对于相同的输入参数总是产生相同的结果,则被认为是DETERMINISTIC,否则为NOT DETERMINISTIC。如果在例程定义中既没有给出DETERMINISTIC也没有给出NOT DETERMINISTIC,默认值为NOT DETERMINISTIC。要声明一个函数是确定性的,你必须明确指定DETERMINISTIC。
将一个NON DETERMINISTIC例程声明为DETERMINISTIC可能导致意外结果,因为这会使优化器做出错误的执行计划选择。将DETERMINISTIC例程声明为NON DETERMINISTIC可能会降低性能,因为可用的优化不会被使用。
内置函数
MySQL 提供了众多内置函数。你已经使用过CURDATE()函数来获取当前日期。
你可以在WHERE子句中使用函数:
mysql> INSERT INTO salaries VALUES(10003, 100000.79, CURDATE(), '9999-01-01');
Query OK, 1 row affected (0.06 sec)
- 例如,以下函数给出了一周前的确切日期:
mysql> SELECT * FROM salary_audit WHERE emp_no=10003;
+--------+----------------+---------------+
| emp_no | user | date_modified |
+--------+----------------+---------------+
| 10003 | root@localhost | 2017-06-18 |
+--------+----------------+---------------+
1 row in set (0.00 sec)
- 添加两个字符串:
mysql> CREATE ALGORITHM=UNDEFINED
DEFINER=`root`@`localhost`
SQL SECURITY DEFINER VIEW salary_view
AS
SELECT emp_no, salary FROM salaries WHERE from_date > '2002-01-01';
另请参阅
请参考 MySQL 参考手册,获取完整的函数列表,网址为dev.mysql.com/doc/refman/8.0/en/func-op-summary-ref.html。
触发器
触发器用于在触发事件之前或之后激活某些操作。例如,你可以有一个触发器在插入表中的每行之前激活,或者在更新每行之后激活。
在无需停机的情况下修改表时(参见第十章《表维护》中的《使用在线模式变更工具修改表》部分),以及出于审计目的,触发器非常有用。假设你想找出某行更新前的值,你可以编写一个触发器,在更新前将这些行保存到另一个表中。另一个表作为审计表,保存了之前的记录。
触发器的动作时间可以是BEFORE或AFTER,这表示触发器是在每行被修改之前还是之后激活。
触发事件可以是INSERT、DELETE或UPDATE:
-
INSERT:每当通过INSERT、REPLACE或LOAD DATA插入新行时,触发器就会激活 -
UPDATE:通过UPDATE语句 -
DELETE:通过DELETE或REPLACE语句
从 MySQL 5.7 开始,一个表可以同时拥有多个触发器。例如,一个表可以有两个BEFORE INSERT触发器。你需要使用FOLLOWS或PRECEDES指定哪个触发器应该先执行。
如何操作...
例如,你想在插入salaries表之前对薪资进行四舍五入。NEW指的是正在插入的新值:
mysql> SELECT emp_no, AVG(salary) as avg FROM salary_view GROUP BY emp_no ORDER BY avg DESC LIMIT 5;
通过源文件创建触发器:
mysql> SHOW FULL TABLES WHERE TABLE_TYPE LIKE 'VIEW';
通过插入一个浮点数来测试触发器:
mysql> SHOW CREATE VIEW salary_view\G
你可以看到薪资已被四舍五入:
mysql> UPDATE salary_view SET salary=100000 WHERE emp_no=10001;
Query OK, 1 row affected (0.01 sec)
Rows matched: 2 Changed: 1 Warnings: 0
mysql> INSERT INTO salary_view VALUES(10001,100001);
ERROR 1423 (HY000): Field of view 'employees.salary_view' underlying table doesn't have a default value
同样,您可以创建一个 BEFORE UPDATE 触发器来四舍五入工资。另一个例子:您想要记录哪个用户插入了 salaries 表。创建一个 audit 表:
mysql> SET GLOBAL event_scheduler = ON;
请注意,以下触发器先于 salary_round 触发器,由 PRECEDES salary_round 指定:
mysql> DROP EVENT IF EXISTS purge_salary_audit;
DELIMITER $$
CREATE EVENT IF NOT EXISTS purge_salary_audit
ON SCHEDULE
EVERY 1 WEEK
STARTS CURRENT_DATE
DO BEGIN
DELETE FROM salary_audit WHERE date_modified < DATE_ADD(CURDATE(), INTERVAL -7 day);
END $$
DELIMITER ;
插入到 salaries 中:
mysql> SHOW EVENTS\G
*************************** 1\. row ***************************
Db: employees
Name: purge_salary_audit
Definer: root@localhost
Time zone: SYSTEM
Type: RECURRING
Execute at: NULL
Interval value: 1
Interval field: MINUTE
Starts: 2017-06-18 00:00:00
Ends: NULL
Status: ENABLED
Originator: 0
character_set_client: utf8
collation_connection: utf8_general_ci
Database Collation: utf8mb4_0900_ai_ci
1 row in set (0.00 sec)
通过查询 salary_audit 表找出谁插入了工资:
mysql> SHOW CREATE EVENT purge_salary_audit\G
如果 salary_audit 表被删除或不可用,则 salaries 表上的所有插入都将被阻止。如果您不想进行审计,应先删除触发器,然后再删除表。
触发器会根据其复杂性对写入速度产生开销。
要检查所有触发器,执行 SHOW TRIGGERS\G。
要检查现有触发器的定义,执行 SHOW CREATE TRIGGER <trigger_name>。
另请参阅
有关更多详细信息,请参阅 MySQL 参考手册,网址为 dev.mysql.com/doc/refman/8.0/en/trigger-syntax.html。
视图
视图是基于 SQL 语句结果集的虚拟表。它也将具有行和列,就像真实表一样,但有一些限制,这些将在后面讨论。视图隐藏了 SQL 的复杂性,更重要的是,提供了额外的安全性。
如何操作...
假设您只想授予对 salaries 表的 emp_no 和 salary 列的访问权限,并且 from_date 在 2002-01-01 之后。为此,您可以创建一个提供所需结果的视图的 SQL。
mysql> ALTER EVENT purge_salary_audit DISABLE;
mysql> ALTER EVENT purge_salary_audit ENABLE;
现在 salary_view 视图已创建,您可以像查询任何其他表一样查询它:
mysql> USE INFORMATION_SCHEMA;
mysql> SHOW TABLES;
您可以看到视图只能访问特定行(即 from_date > '2002-01-01'),而不是所有行。您可以使用视图来限制用户对特定行的访问。
要列出所有视图,执行:
mysql> DESC INFORMATION_SCHEMA.TABLES;
+-----------------+--------------------------------------------------------------------+------+-----+-------------------+-----------------------------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+--------------------------------------------------------------------+------+-----+-------------------+-----------------------------+
| TABLE_CATALOG | varchar(64) | NO | | NULL | |
| TABLE_SCHEMA | varchar(64) | NO | | NULL | |
| TABLE_NAME | varchar(64) | NO | | NULL | |
| TABLE_TYPE | enum('BASE TABLE','VIEW','SYSTEM VIEW') | NO | | NULL | |
| ENGINE | varchar(64) | YES | | NULL | |
| VERSION | int(2) | YES | | NULL | |
| ROW_FORMAT | enum('Fixed','Dynamic','Compressed','Redundant','Compact','Paged') | YES | | NULL | |
| TABLE_ROWS | bigint(20) unsigned | YES | | NULL | |
| AVG_ROW_LENGTH | bigint(20) unsigned | YES | | NULL | |
| DATA_LENGTH | bigint(20) unsigned | YES | | NULL | |
| MAX_DATA_LENGTH | bigint(20) unsigned | YES | | NULL | |
| INDEX_LENGTH | bigint(20) unsigned | YES | | NULL | |
| DATA_FREE | bigint(20) unsigned | YES | | NULL | |
| AUTO_INCREMENT | bigint(20) unsigned | YES | | NULL | |
| CREATE_TIME | timestamp | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
| UPDATE_TIME | timestamp | YES | | NULL | |
| CHECK_TIME | timestamp | YES | | NULL | |
| TABLE_COLLATION | varchar(64) | YES | | NULL | |
| CHECKSUM | bigint(20) unsigned | YES | | NULL | |
| CREATE_OPTIONS | varchar(256) | YES | | NULL | |
| TABLE_COMMENT | varchar(256) | YES | | NULL | |
+-----------------+--------------------------------------------------------------------+------+-----+-------------------+-----------------------------+
21 rows in set (0.00 sec)
要检查视图的定义,执行:
mysql> SELECT SUM(DATA_LENGTH)/1024/1024 AS DATA_SIZE_MB, SUM(INDEX_LENGTH)/1024/1024 AS INDEX_SIZE_MB, SUM(DATA_FREE)/1024/1024 AS DATA_FREE_MB FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='employees';
+--------------+---------------+--------------+
| DATA_SIZE_MB | INDEX_SIZE_MB | DATA_FREE_MB |
+--------------+---------------+--------------+
| 17.39062500 | 14.62500000 | 11.00000000 |
+--------------+---------------+--------------+
1 row in set (0.01 sec)
您可能已经注意到 current_dept_emp 和 dept_emp_latest_date 视图,它们是 employee 数据库的一部分。您可以探索其定义并找出其用途。
不包含子查询、JOIN、GROUP BY 子句、联合等的简单视图可以更新。salary_view 是一个简单视图,如果底层表有默认值,则可以更新或插入。
mysql> SELECT * FROM COLUMNS WHERE TABLE_NAME='employees'\G
mysql> SELECT * FROM FILES WHERE FILE_NAME LIKE './employees/employees.ibd'\G
~~~
EXTENT_SIZE: 1048576
AUTOEXTEND_SIZE: 4194304
DATA_FREE: 13631488
~~~
如果表有默认值,即使它不匹配视图中的筛选条件,也可以插入一行。为了避免这种情况,并插入满足视图条件的行,必须在定义中提供 WITH CHECK OPTION。
VIEW 算法:
-
MERGE:MySQL 将输入查询与视图定义合并为一个查询,然后执行合并后的查询。MERGE算法仅适用于简单视图。 -
TEMPTABLE:MySQL 将结果存储在临时表中,然后针对该临时表执行输入查询。 -
UNDEFINED(默认):MySQL 自动选择MERGE或TEMPTABLE算法。MySQL 更喜欢MERGE算法而不是TEMPTABLE算法,因为MERGE算法效率更高。
事件
正如 Linux 服务器上的 cron 一样,MySQL 使用EVENTS来处理计划任务。MySQL 使用一个名为事件调度线程的特殊线程来执行所有计划事件。默认情况下,事件调度线程未启用(版本< 8.0.3),要启用它,请执行以下操作:
mysql> SELECT * FROM INNODB_TABLESPACES WHERE NAME='employees/employees'\G
*************************** 1\. row ***************************
SPACE: 118
NAME: employees/employees
FLAG: 16417
ROW_FORMAT: Dynamic
PAGE_SIZE: 16384
ZIP_PAGE_SIZE: 0
SPACE_TYPE: Single
FS_BLOCK_SIZE: 4096
FILE_SIZE: 32505856
ALLOCATED_SIZE: 32509952
1 row in set (0.00 sec)
如何操作...
假设您不再需要保留超过一个月的工资审计记录,您可以安排一个每天运行的事件,并从salary_audit表中删除超过一个月的记录。
shell> sudo ls -ltr /var/lib/mysql/employees/employees.ibd
-rw-r----- 1 mysql mysql 32505856 Jun 20 16:50 /var/lib/mysql/employees/employees.ibd
一旦创建了事件,它将自动执行清除工资审计记录的工作。
- 要检查事件,请执行以下操作:
mysql> SELECT * FROM INNODB_TABLESTATS WHERE NAME='employees/employees'\G
*************************** 1\. row ***************************
TABLE_ID: 128
NAME: employees/employees
STATS_INITIALIZED: Initialized
NUM_ROWS: 299468
CLUST_INDEX_SIZE: 1057
OTHER_INDEX_SIZE: 545
MODIFIED_COUNTER: 0
AUTOINC: 0
REF_COUNT: 1
1 row in set (0.00 sec)
- 要检查事件的定义,请执行以下操作:
mysql> SELECT * FROM PROCESSLIST\G
*************************** 1\. row ***************************
ID: 85
USER: event_scheduler
HOST: localhost
DB: NULL
COMMAND: Daemon
TIME: 44
STATE: Waiting for next activation
INFO: NULL
*************************** 2\. row ***************************
ID: 26231
USER: root
HOST: localhost
DB: information_schema
COMMAND: Query
TIME: 0
STATE: executing
INFO: SELECT * FROM PROCESSLIST
2 rows in set (0.00 sec
- 要禁用/启用事件,请执行以下操作:
[PRE171]
访问控制
所有存储程序(过程、函数、触发器和事件)和视图都有一个DEFINER。如果未指定DEFINER,则创建对象的用户将被选为DEFINER。
存储例程(过程和函数)和视图具有SQL SECURITY特性,其值为DEFINER或INVOKER,以指定对象是在定义者还是调用者上下文中执行。触发器和事件没有SQL SECURITY特性,并且始终在定义者上下文中执行。服务器根据需要自动调用这些对象,因此没有调用用户。
另请参阅
安排事件的方式有很多,详情请参阅dev.mysql.com/doc/refman/8.0/en/event-scheduler.html。
获取数据库和表的信息
您可能已经注意到数据库列表中的information_schema数据库。information_schema是一个包含有关所有数据库对象的元数据的视图集合。您可以连接到information_schema并探索所有表。本章解释了最广泛使用的表。您可以查询information_schema表或使用SHOW命令,这本质上做的是相同的事情。
INFORMATION_SCHEMA查询作为视图实现于数据字典表之上。INFORMATION_SCHEMA表中有两种类型的元数据:
-
静态表元数据:
TABLE_SCHEMA、TABLE_NAME、TABLE_TYPE和ENGINE。这些统计数据将直接从数据字典读取。 -
动态表元数据:
AUTO_INCREMENT、AVG_ROW_LENGTH及DATA_FREE。动态元数据经常变动(例如,每次INSERT后AUTO_INCREMENT值会递增)。在许多情况下,按需准确计算动态元数据会产生一定成本,且对于典型查询而言,精确性未必有益。以DATA_FREE统计为例,它显示表中空闲字节数——通常缓存值已足够。
MySQL 8.0 中,动态表元数据默认会被缓存。这可通过information_schema_stats设置(默认缓存)进行配置,并可改为SET @@GLOBAL.information_schema_stats='LATEST',以便始终直接从存储引擎获取动态信息(代价是查询执行速度略高)。
作为替代方案,用户也可对表执行ANALYZE TABLE,以更新缓存的动态统计信息。
大多数表都有TABLE_SCHEMA列,指代数据库名称,以及TABLE_NAME列,指代表名。
更多详情,请参考mysqlserverteam.com/mysql-8-0-improvements-to-information_schema/。
操作方法...
查看所有表的列表:
[PRE172]
TABLES
TABLES表包含了关于表的所有信息,如所属数据库TABLE_SCHEMA、行数(TABLE_ROWS)、ENGINE、DATA_LENGTH、INDEX_LENGTH及DATA_FREE:
[PRE173]
例如,你想了解employees数据库中的DATA_LENGTH、INDEX_LENGTH及DATE_FREE:
[PRE174]
COLUMNS
此表列出了每张表的所有列及其定义:
[PRE175]
FILES
你已知晓 MySQL 将InnoDB数据存储在data目录中(与数据库同名)的.ibd文件内。如需获取文件的更多信息,可查询FILES表:
[PRE176]
你应该密切关注DATA_FREE,它代表未分配的段加上由于碎片化而在段内空闲的数据。当你重建表时,可以释放DATA_FREE中显示的字节。
INNODB_SYS_TABLESPACES
文件大小也可在INNODB_TABLESPACES表中查询:
[PRE177]
你可以在文件系统中验证相同信息:
[PRE178]
INNODB_TABLESTATS
索引大小及大致行数可在INNODB_TABLESTATS表中查询:
[PRE179]
PROCESSLIST
最常用的视图之一是进程列表,它列出了服务器上运行的所有查询:
[PRE180]
或者您可以执行SHOW PROCESSLIST;以获得相同的输出。
其他表:ROUTINES包含函数和存储过程的定义。TRIGGERS包含触发器的定义。VIEWS包含视图的定义。
另请参阅
要了解INFORMATION_SCHEMA的改进,请参阅mysqlserverteam.com/mysql-8-0-improvements-to-information_schema/。
第三章:使用 MySQL(高级)
在本章中,我们将涵盖以下配方:
-
使用 JSON
-
公共表达式(CTE)
-
生成的列
-
窗口函数
介绍
在本章中,您将了解 MySQL 的新引入功能。
使用 JSON
正如您在上一章中所看到的,要在 MySQL 中存储数据,您必须定义数据库和表结构(模式),这是一个重大的限制。为了应对这一点,从 MySQL 5.7 开始,MySQL 支持JavaScript 对象表示(JSON)数据类型。以前没有单独的数据类型,它被存储为字符串。新的 JSON 数据类型提供了 JSON 文档的自动验证和优化的存储格式。
JSON 文档以二进制格式存储,这使得以下操作成为可能:
-
快速读取文档元素
-
当服务器再次读取 JSON 时,不需要从文本表示中解析值
-
直接通过键或数组索引查找子对象或嵌套值,而无需读取文档中它们之前或之后的所有值
如何做...
假设您想要存储有关员工的更多详细信息;您可以使用 JSON 保存它们:
CREATE TABLE emp_details(
emp_no int primary key,
details json
);
插入 JSON
INSERT INTO emp_details(emp_no, details)
VALUES ('1',
'{ "location": "IN", "phone": "+11800000000", "email": "abc@example.com", "address": { "line1": "abc", "line2": "xyz street", "city": "Bangalore", "pin": "560103"} }'
);
检索 JSON
您可以使用->和->>运算符检索 JSON 列的字段:
mysql> SELECT emp_no, details->'$.address.pin' pin FROM emp_details;
+--------+----------+
| emp_no | pin |
+--------+----------+
| 1 | "560103" |
+--------+----------+
1 row in set (0.00 sec)
要检索没有引号的数据,请使用->>运算符:
mysql> SELECT emp_no, details->>'$.address.pin' pin FROM emp_details;
+--------+--------+
| emp_no | pin |
+--------+--------+
| 1 | 560103 |
+--------+--------+
1 row in set (0.00 sec)
JSON 函数
MySQL 提供了许多处理 JSON 数据的函数。让我们看看最常用的函数。
漂亮的视图
要以漂亮的格式显示 JSON 值,请使用JSON_PRETTY()函数:
mysql> SELECT emp_no, JSON_PRETTY(details) FROM emp_details \G
*************************** 1\. row ***************************
emp_no: 1
JSON_PRETTY(details): {
"email": "abc@example.com",
"phone": "+11800000000",
"address": {
"pin": "560103",
"city": "Bangalore",
"line1": "abc",
"line2": "xyz street"
},
"location": "IN"
}
1 row in set (0.00 sec)
不漂亮的:
mysql> SELECT emp_no, details FROM emp_details \G
*************************** 1\. row ***************************
emp_no: 1
details: {"email": "abc@example.com", "phone": "+11800000000", "address": {"pin": "560100", "city": "Bangalore", "line1": "abc", "line2": "xyz street"}, "location": "IN"}
1 row in set (0.00 sec)
搜索
您可以在WHERE子句中使用col->>path运算符引用 JSON 列:
mysql> SELECT emp_no FROM emp_details WHERE details->>'$.address.pin'="560103";
+--------+
| emp_no |
+--------+
| 1 |
+--------+
1 row in set (0.00 sec)
您还可以使用JSON_CONTAINS函数搜索数据。如果找到数据,则返回1,否则返回0:
mysql> SELECT JSON_CONTAINS(details->>'$.address.pin', "560103") FROM emp_details;
+----------------------------------------------------+
| JSON_CONTAINS(details->>'$.address.pin', "560103") |
+----------------------------------------------------+
| 1 |
+----------------------------------------------------+
1 row in set (0.00 sec)
如何搜索键?假设您想要检查address.line1是否存在:
mysql> SELECT JSON_CONTAINS_PATH(details, 'one', "$.address.line1") FROM emp_details;
+--------------------------------------------------------------------------+
| JSON_CONTAINS_PATH(details, 'one', "$.address.line1") |
+--------------------------------------------------------------------------+
| 1 |
+--------------------------------------------------------------------------+
1 row in set (0.01 sec)
在这里,one表示至少应该存在一个键。假设您想要检查address.line1或address.line2是否存在:
mysql> SELECT JSON_CONTAINS_PATH(details, 'one', "$.address.line1", "$.address.line5") FROM emp_details;
+--------------------------------------------------------------------------+
| JSON_CONTAINS_PATH(details, 'one', "$.address.line1", "$.address.line2") |
+--------------------------------------------------------------------------+
| 1 |
+--------------------------------------------------------------------------+
如果要检查address.line1和address.line5是否都存在,可以使用and而不是one:
mysql> SELECT JSON_CONTAINS_PATH(details, 'all', "$.address.line1", "$.address.line5") FROM emp_details;
+--------------------------------------------------------------------------+
| JSON_CONTAINS_PATH(details, 'all', "$.address.line1", "$.address.line5") |
+--------------------------------------------------------------------------+
| 0 |
+--------------------------------------------------------------------------+
1 row in set (0.00 sec)
修改
您可以使用三种不同的函数修改数据:JSON_SET(),JSON_INSERT(),JSON_REPLACE()。在 MySQL 8 之前,我们需要对整个列进行完全更新,这不是最佳方式:
JSON_SET: 替换现有值并添加不存在的值。
假设您想要替换员工的邮政编码并添加昵称的详细信息:
mysql> UPDATE
emp_details
SET
details = JSON_SET(details, "$.address.pin", "560100", "$.nickname", "kai")
WHERE
emp_no = 1;
Query OK, 1 row affected (0.03 sec)
Rows matched: 1 Changed: 1 Warnings: 0
JSON_INSERT(): 插入值而不替换现有值
假设您想要添加一个新列而不更新现有值;您可以使用JSON_INSERT():
mysql> UPDATE emp_details SET details=JSON_INSERT(details, "$.address.pin", "560132", "$.address.line4", "A Wing") WHERE emp_no = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
在这种情况下,pin不会被更新;只会添加一个新的address.line4字段。
JSON_REPLACE(): 仅替换现有值
假设您只想替换字段而不添加新字段:
mysql> UPDATE emp_details SET details=JSON_REPLACE(details, "$.address.pin", "560132", "$.address.line5", "Landmark") WHERE
emp_no = 1;
Query OK, 1 row affected (0.04 sec)
Rows matched: 1 Changed: 1 Warnings: 0
在这种情况下,line5不会被添加。只有pin会被更新。
删除
JSON_REMOVE从 JSON 文档中删除数据。
假设您不再需要地址中的line5:
mysql> UPDATE emp_details SET details=JSON_REMOVE(details, "$.address.line5") WHERE emp_no = 1;
Query OK, 1 row affected (0.04 sec)
Rows matched: 1 Changed: 1 Warnings: 0
其他函数
其他一些函数如下:
JSON_KEYS(): 获取 JSON 文档中的所有键:
mysql> SELECT JSON_KEYS(details) FROM emp_details WHERE emp_no = 1;
*************************** 1\. row ***************************
JSON_KEYS(details): ["email", "phone", "address", "nickname", "locatation"]
JSON_LENGTH(): 给出 JSON 文档中元素的数量:
mysql> SELECT JSON_LENGTH(details) FROM emp_details WHERE emp_no = 1;
*************************** 1\. row ***************************
JSON_LENGTH(details): 5
参见
您可以在dev.mysql.com/doc/refman/8.0/en/json-function-reference.html查看完整的函数列表。
公共表达式(CTE)
MySQL 8 支持公共表达式,包括非递归和递归。
公共表达式使得可以使用命名的临时结果集,通过允许在SELECT语句和某些其他语句之前使用WITH子句。
为什么需要 CTE?在同一查询中不可能引用派生表两次。因此,派生表会被评估两次或多次,这表明存在严重的性能问题。使用 CTE,子查询只评估一次。
如何做...
递归和非递归 CTE 将在以下部分中进行讨论。
非递归 CTE
公共表达式(CTE)就像派生表一样,但其声明放在查询块之前,而不是在FROM子句中。
派生表
SELECT... FROM (subquery) AS derived, t1 ...
CTE
SELECT... WITH derived AS (subquery) SELECT ... FROM derived, t1 ...
CTE 可以在SELECT/UPDATE/DELETE之前,包括子查询WITH派生AS(子查询),例如:
DELETE FROM t1 WHERE t1.a IN (SELECT b FROM derived);
假设您想要找出每年薪水相对于上一年的百分比增长。没有 CTE,您需要编写两个子查询,它们本质上是相同的。MySQL 不足够聪明,无法检测到这一点,并且子查询会执行两次:
mysql> SELECT
q1.year,
q2.year AS next_year,
q1.sum,
q2.sum AS next_sum,
100*(q2.sum-q1.sum)/q1.sum AS pct
FROM
(SELECT year(from_date) as year, sum(salary) as sum FROM salaries GROUP BY year) AS q1, (SELECT year(from_date) as year, sum(salary) as sum FROM salaries GROUP BY year) AS q2
WHERE q1.year = q2.year-1;
+------+-----------+-------------+-------------+----------+
| year | next_year | sum | next_sum | pct |
+------+-----------+-------------+-------------+----------+
| 1985 | 1986 | 972864875 | 2052895941 | 111.0155 |
| 1986 | 1987 | 2052895941 | 3156881054 | 53.7770 |
| 1987 | 1988 | 3156881054 | 4295598688 | 36.0710 |
| 1988 | 1989 | 4295598688 | 5454260439 | 26.9732 |
| 1989 | 1990 | 5454260439 | 6626146391 | 21.4857 |
| 1990 | 1991 | 6626146391 | 7798804412 | 17.6974 |
| 1991 | 1992 | 7798804412 | 9027872610 | 15.7597 |
| 1992 | 1993 | 9027872610 | 10215059054 | 13.1502 |
| 1993 | 1994 | 10215059054 | 11429450113 | 11.8882 |
| 1994 | 1995 | 11429450113 | 12638817464 | 10.5812 |
| 1995 | 1996 | 12638817464 | 13888587737 | 9.8883 |
| 1996 | 1997 | 13888587737 | 15056011781 | 8.4056 |
| 1997 | 1998 | 15056011781 | 16220495471 | 7.7343 |
| 1998 | 1999 | 16220495471 | 17360258862 | 7.0267 |
| 1999 | 2000 | 17360258862 | 17535667603 | 1.0104 |
| 2000 | 2001 | 17535667603 | 17507737308 | -0.1593 |
| 2001 | 2002 | 17507737308 | 10243358658 | -41.4924 |
+------+-----------+-------------+-------------+----------+
17 rows in set (3.22 sec)
使用非递归 CTE,派生查询仅执行一次并被重用:
mysql>
WITH CTE AS
(SELECT year(from_date) AS year, SUM(salary) AS sum FROM salaries GROUP BY year)
SELECT
q1.year, q2.year as next_year, q1.sum, q2.sum as next_sum, 100*(q2.sum-q1.sum)/q1.sum as pct FROM
CTE AS q1,
CTE AS q2
WHERE
q1.year = q2.year-1;
+------+-----------+-------------+-------------+----------+
| year | next_year | sum | next_sum | pct |
+------+-----------+-------------+-------------+----------+
| 1985 | 1986 | 972864875 | 2052895941 | 111.0155 |
| 1986 | 1987 | 2052895941 | 3156881054 | 53.7770 |
| 1987 | 1988 | 3156881054 | 4295598688 | 36.0710 |
| 1988 | 1989 | 4295598688 | 5454260439 | 26.9732 |
| 1989 | 1990 | 5454260439 | 6626146391 | 21.4857 |
| 1990 | 1991 | 6626146391 | 7798804412 | 17.6974 |
| 1991 | 1992 | 7798804412 | 9027872610 | 15.7597 |
| 1992 | 1993 | 9027872610 | 10215059054 | 13.1502 |
| 1993 | 1994 | 10215059054 | 11429450113 | 11.8882 |
| 1994 | 1995 | 11429450113 | 12638817464 | 10.5812 |
| 1995 | 1996 | 12638817464 | 13888587737 | 9.8883 |
| 1996 | 1997 | 13888587737 | 15056011781 | 8.4056 |
| 1997 | 1998 | 15056011781 | 16220495471 | 7.7343 |
| 1998 | 1999 | 16220495471 | 17360258862 | 7.0267 |
| 1999 | 2000 | 17360258862 | 17535667603 | 1.0104 |
| 2000 | 2001 | 17535667603 | 17507737308 | -0.1593 |
| 2001 | 2002 | 17507737308 | 10243358658 | -41.4924 |
+------+-----------+-------------+-------------+----------+
17 rows in set (1.63 sec)
您可能会注意到,使用 CTE,结果相同,查询时间提高了 50%;可读性很好,并且可以多次引用。
派生查询不能引用其他派生查询:
SELECT ...
FROM (SELECT ... FROM ...) AS d1, (SELECT ... FROM d1 ...) AS d2 ...
ERROR: 1146 (42S02): Table ‘db.d1’ doesn’t exist
CTEs 可以引用其他 CTEs:
WITH d1 AS (SELECT ... FROM ...), d2 AS (SELECT ... FROM d1 ...)
SELECT
FROM d1, d2 ...
递归 CTE
递归 CTE 是一个带有引用自身名称的子查询的 CTE。WITH子句必须以WITH RECURSIVE开头。递归 CTE 子查询有两部分,种子查询和递归查询,由UNION [ALL]或UNION DISTINCT分隔。
种子SELECT执行一次以创建初始数据子集;递归SELECT被重复执行以返回数据子集,直到获得完整的结果集。当迭代不再生成任何新行时,递归停止。这对于深入研究层次结构(父/子或部分/子部分)非常有用:
WITH RECURSIVE cte AS
(SELECT ... FROM table_name /* seed SELECT */
UNION ALL
SELECT ... FROM cte, table_name) /* "recursive" SELECT */
SELECT ... FROM cte;
假设您想打印从1到5的所有数字:
mysql> WITH RECURSIVE cte (n) AS
( SELECT 1 /* seed query */
UNION ALL
SELECT n + 1 FROM cte WHERE n < 5 /* recursive query */
)
SELECT * FROM cte;
+---+
| n |
+---+
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
+---+
5 rows in set (0.00 sec)
在每次迭代中,SELECT生成一个新值的行,该值比上一行集合中的n值多 1。第一次迭代在初始行集(1)上操作,并产生1+1=2;第二次迭代在第一次迭代的行集(2)上操作,并产生2+1=3;依此类推。这将持续到递归结束,当n不再小于5时。
假设您想要对分层数据进行遍历,以生成每个员工的管理链的组织结构图(即从 CEO 到员工的路径)。使用递归 CTE!
创建一个带有manager_id的测试表:
mysql> CREATE TABLE employees_mgr (
id INT PRIMARY KEY NOT NULL,
name VARCHAR(100) NOT NULL,
manager_id INT NULL,
INDEX (manager_id),
FOREIGN KEY (manager_id) REFERENCES employees_mgr (id)
);
插入示例数据:
mysql> INSERT INTO employees_mgr VALUES
(333, "Yasmina", NULL), # Yasmina is the CEO (manager_id is NULL)
(198, "John", 333), # John has ID 198 and reports to 333 (Yasmina)
(692, "Tarek", 333),
(29, "Pedro", 198),
(4610, "Sarah", 29),
(72, "Pierre", 29),
(123, "Adil", 692);
执行递归 CTE:
mysql> WITH RECURSIVE employee_paths (id, name, path) AS
(
SELECT id, name, CAST(id AS CHAR(200))
FROM employees_mgr
WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, CONCAT(ep.path, ',', e.id)
FROM employee_paths AS ep JOIN employees_mgr AS e
ON ep.id = e.manager_id
)
SELECT * FROM employee_paths ORDER BY path;
它产生以下结果:
+------+---------+-----------------+
| id | name | path |
+------+---------+-----------------+
| 333 | Yasmina | 333 |
| 198 | John | 333,198 |
| 29 | Pedro | 333,198,29 |
| 4610 | Sarah | 333,198,29,4610 |
| 72 | Pierre | 333,198,29,72 |
| 692 | Tarek | 333,692 |
| 123 | Adil | 333,692,123 |
+------+---------+-----------------+
7 rows in set (0.00 sec)
WITH RECURSIVE employee_paths(id,name,path)AS是 CTE 的名称,列为(id,name,path)。
SELECT id,name,CAST(id AS CHAR(200))FROM employees_mgr WHERE manager_id IS NULL是选择 CEO 的种子查询(CEO 没有经理)。
SELECT e.id,e.name,CONCAT(ep.path,',',e.id) FROM employee_paths AS ep JOIN employees_mgr AS e ON ep.id = e.manager_id是递归查询。
递归查询生成的每一行都会找到直接向以前行生成的员工汇报的所有员工。对于这样的员工,该行包括员工 ID,名称和员工管理链。该链是经理的链,员工 ID 添加到末尾。
生成列
生成列也称为虚拟列或计算列。生成列的值是从列定义中包含的表达式计算出来的。有两种类型的生成列:
-
虚拟:当从表中读取记录时,该列将在读取时动态计算
-
存储:当在表中写入新记录时,将计算该列并将其存储在表中作为常规列
虚拟生成列比存储生成列更有用,因为虚拟列不占用任何存储空间。您可以使用触发器模拟存储生成列的行为。
如何做...
假设您的应用程序在从employees表中检索数据时使用full_name作为concat('first_name',' ','last_name'),而不是使用表达式,您可以使用虚拟列,该列在读取时计算full_name。您可以在表达式后面添加另一列:
mysql> CREATE TABLE `employees` (
`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,
`full_name` VARCHAR(30) AS (CONCAT(first_name,' ',last_name)),
PRIMARY KEY (`emp_no`),
KEY `name` (`first_name`,`last_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
请注意,您应该根据虚拟列修改插入语句。您可以选择使用完整的插入,如下所示:
mysql> INSERT INTO employees (emp_no, birth_date, first_name, last_name, gender, hire_date) VALUES (123456, '1987-10-02', 'ABC' , 'XYZ', 'F', '2008-07-28');
如果您想在INSERT语句中包含full_name,可以将其指定为DEFAULT。所有其他值都会抛出ERROR 3105 (HY000):错误。在employees表中指定生成列full_name的值是不允许的:
mysql> INSERT INTO employees (emp_no, birth_date, first_name, last_name, gender, hire_date, full_name) VALUES (123456, '1987-10-02', 'ABC' , 'XYZ', 'F', '2008-07-28', DEFAULT);
您可以直接从employees表中选择full_name:
mysql> SELECT * FROM employees WHERE emp_no=123456;
+--------+------------+------------+-----------+--------+------------+-----------+
| emp_no | birth_date | first_name | last_name | gender | hire_date | full_name |
+--------+------------+------------+-----------+--------+------------+-----------+
| 123456 | 1987-10-02 | ABC | XYZ | F | 2017-11-23 | ABC XYZ |
+--------+------------+------------+-----------+--------+------------+-----------+
1 row in set (0.00 sec)
如果您已经创建了表并希望添加新的生成列,请执行 ALTER TABLE 语句,这将在第十章 表维护中详细介绍
例子:
mysql> ALTER TABLE employees ADD hire_date_year YEAR AS (YEAR(hire_date)) VIRTUAL;
请参考dev.mysql.com/doc/refman/8.0/en/create-table-generated-columns.html了解更多关于生成列的信息。您将在第十三章 性能调优中了解虚拟列的其他用途,在添加索引和使用生成列为 JSON 添加索引部分。
窗口函数
通过使用窗口函数,您可以对与该行相关的行执行计算。这是通过使用OVER和WINDOW子句来实现的。
以下是您可以进行的计算列表:
-
CUME_DIST(): 累积分布值 -
DENSE_RANK(): 在其分区内当前行的排名,没有间隙 -
FIRST_VALUE(): 窗口帧的第一行的参数值 -
LAG(): 分区内当前行的前一行的参数值 -
LAST_VALUE(): 窗口帧的第一行的参数值 -
LEAD(): 分区内当前行的后一行的参数值 -
NTH_VALUE(): 窗口帧的第n行的参数值 -
NTILE(): 在其分区内当前行的桶编号 -
PERCENT_RANK(): 百分比排名值 -
RANK(): 在其分区内当前行的排名,有间隙 -
ROW_NUMBER(): 在其分区内当前行的编号
如何做...
窗口函数可以以各种方式使用。让我们在以下部分了解每一个。为了使这些示例起作用,您需要添加 hire_date_year 虚拟列
mysql> ALTER TABLE employees ADD hire_date_year YEAR AS (YEAR(hire_date)) VIRTUAL;
行号
您可以为每一行获取行号以对结果进行排名:
mysql> SELECT CONCAT(first_name, " ", last_name) AS full_name, salary, ROW_NUMBER() OVER(ORDER BY salary DESC) AS 'Rank' FROM employees JOIN salaries ON salaries.emp_no=employees.emp_no LIMIT 10;
+-------------------+--------+------+
| full_name | salary | Rank |
+-------------------+--------+------+
| Tokuyasu Pesch | 158220 | 1 |
| Tokuyasu Pesch | 157821 | 2 |
| Honesty Mukaidono | 156286 | 3 |
| Xiahua Whitcomb | 155709 | 4 |
| Sanjai Luders | 155513 | 5 |
| Tsutomu Alameldin | 155377 | 6 |
| Tsutomu Alameldin | 155190 | 7 |
| Tsutomu Alameldin | 154888 | 8 |
| Tsutomu Alameldin | 154885 | 9 |
| Willard Baca | 154459 | 10 |
+-------------------+--------+------+
10 rows in set (6.24 sec)
分区结果
您可以在OVER子句中对结果进行分区。假设您想要找出每年的薪水排名;可以按如下方式完成:
mysql> SELECT hire_date_year, salary, ROW_NUMBER() OVER(PARTITION BY hire_date_year ORDER BY salary DESC) AS 'Rank' FROM employees JOIN salaries ON salaries.emp_no=employees.emp_no ORDER BY salary DESC LIMIT 10;
+----------------+--------+------+
| hire_date_year | salary | Rank |
+----------------+--------+------+
| 1985 | 158220 | 1 |
| 1985 | 157821 | 2 |
| 1986 | 156286 | 1 |
| 1985 | 155709 | 3 |
| 1987 | 155513 | 1 |
| 1985 | 155377 | 4 |
| 1985 | 155190 | 5 |
| 1985 | 154888 | 6 |
| 1985 | 154885 | 7 |
| 1985 | 154459 | 8 |
+----------------+--------+------+
10 rows in set (8.04 sec)
您可以注意到,1986年和1987年的排名发生了变化,但1985年的排名保持不变。
命名窗口
您可以命名一个窗口,并且可以根据需要多次使用它,而不是每次重新定义它:
mysql> SELECT hire_date_year, salary, RANK() OVER w AS 'Rank' FROM employees join salaries ON salaries.emp_no=employees.emp_no WINDOW w AS (PARTITION BY hire_date_year ORDER BY salary DESC) ORDER BY salary DESC LIMIT 10;
+----------------+--------+------+
| hire_date_year | salary | Rank |
+----------------+--------+------+
| 1985 | 158220 | 1 |
| 1985 | 157821 | 2 |
| 1986 | 156286 | 1 |
| 1985 | 155709 | 3 |
| 1987 | 155513 | 1 |
| 1985 | 155377 | 4 |
| 1985 | 155190 | 5 |
| 1985 | 154888 | 6 |
| 1985 | 154885 | 7 |
| 1985 | 154459 | 8 |
+----------------+--------+------+
10 rows in set (8.52 sec)
第一个、最后一个和第 n 个值
您可以在窗口结果中选择第一个、最后一个和第 n 个值。如果行不存在,则返回NULL值。
假设您想要从窗口中找到第一个、最后一个和第三个值:
mysql> SELECT hire_date_year, salary, RANK() OVER w AS 'Rank',
FIRST_VALUE(salary) OVER w AS 'first',
NTH_VALUE(salary, 3) OVER w AS 'third',
LAST_VALUE(salary) OVER w AS 'last'
FROM employees join salaries ON salaries.emp_no=employees.emp_no
WINDOW w AS (PARTITION BY hire_date_year ORDER BY salary DESC)
ORDER BY salary DESC LIMIT 10;
+----------------+--------+------+--------+--------+--------+
| hire_date_year | salary | Rank | first | third | last |
+----------------+--------+------+--------+--------+--------+
| 1985 | 158220 | 1 | 158220 | NULL | 158220 |
| 1985 | 157821 | 2 | 158220 | NULL | 157821 |
| 1986 | 156286 | 1 | 156286 | NULL | 156286 |
| 1985 | 155709 | 3 | 158220 | 155709 | 155709 |
| 1987 | 155513 | 1 | 155513 | NULL | 155513 |
| 1985 | 155377 | 4 | 158220 | 155709 | 155377 |
| 1985 | 155190 | 5 | 158220 | 155709 | 155190 |
| 1985 | 154888 | 6 | 158220 | 155709 | 154888 |
| 1985 | 154885 | 7 | 158220 | 155709 | 154885 |
| 1985 | 154459 | 8 | 158220 | 155709 | 154459 |
+----------------+--------+------+--------+--------+--------+
10 rows in set (12.88 sec)
要了解窗口函数的其他用例,请参考mysqlserverteam.com/mysql-8-0-2-introducing-window-functions和dev.mysql.com/doc/refman/8.0/en/window-function-descriptions.html#function_row-number。
第四章:配置 MySQL
在本章中,我们将涵盖以下配方:
-
使用配置文件
-
使用全局和会话变量
-
使用启动脚本的参数
-
配置参数
-
更改数据目录
介绍
MySQL 有两种类型的参数:
-
静态,在重新启动 MySQL 服务器后生效
-
动态,可以在不重新启动 MySQL 服务器的情况下进行更改
变量可以通过以下方式设置:
-
配置文件:MySQL 有一个配置文件,我们可以在其中指定数据的位置,MySQL 可以使用的内存,以及各种其他参数。
-
启动脚本:您可以直接将参数传递给
mysqld进程。它仅在服务器的该调用中生效。 -
使用 SET 命令(仅动态变量):这将持续到服务器重新启动。您还需要在配置文件中设置变量,以使更改在重新启动时持久。使更改持久的另一种方法是在变量名称之前加上
PERSIST关键字或@@persist。
使用配置文件
默认配置文件为/etc/my.cnf(在 Red Hat 和 CentOS 系统上)和/etc/mysql/my.cnf(Debian 系统)。在您喜欢的编辑器中打开文件并根据需要修改参数。本章讨论了主要参数。
如何做...
配置文件由section_name指定的部分。所有与部分相关的参数都可以放在它们下面,例如:
[mysqld] <---section name <parameter_name> = <value> <---parameter values
[client] <parameter_name> = <value>
[mysqldump] <parameter_name> = <value>
[mysqld_safe] <parameter_name> = <value>
[server]
<parameter_name> = <value>
-
[mysql]:该部分由mysql命令行客户端读取 -
[client]:该部分由所有连接的客户端(包括mysql cli)读取 -
[mysqld]:该部分由mysql服务器读取 -
[mysqldump]:该部分由名为mysqldump的备份实用程序读取 -
[mysqld_safe]:由mysqld_safe进程(MySQL 服务器启动脚本)读取
除此之外,mysqld_safe进程从选项文件中的[mysqld]和[server]部分读取所有选项。
例如,mysqld_safe进程从mysqld部分读取pid-file选项。
shell> sudo vi /etc/my.cnf
[mysqld]
pid-file = /var/lib/mysql/mysqld.pid
在使用systemd的系统中,mysqld_safe将不会安装。要配置启动脚本,您需要在/etc/systemd/system/mysqld.service.d/override.conf中设置值。
例如:
[Service]
LimitNOFILE=max_open_files
PIDFile=/path/to/pid/file
LimitCore=core_file_limit
Environment="LD_PRELOAD=/path/to/malloc/library"
Environment="TZ=time_zone_setting"
使用全局和会话变量
正如您在前几章中所看到的,您可以通过连接到 MySQL 并执行SET命令来设置参数。
根据变量的范围,有两种类型的变量:
-
全局:适用于所有新连接
-
会话:仅适用于当前连接(会话)
如何做...
例如,如果您想记录所有慢于一秒的查询,可以执行:
mysql> SET GLOBAL long_query_time = 1;
要使更改在重新启动时持久,请使用:
mysql> SET PERSIST long_query_time = 1;
Query OK, 0 rows affected (0.01 sec)
或:
mysql> SET @@persist.long_query_time = 1;
Query OK, 0 rows affected (0.00 sec)
持久的全局系统变量设置存储在位于数据目录中的 mysqld-auto.cnf 中。
假设您只想记录此会话的查询,而不是所有连接的查询。您可以使用以下命令:
mysql> SET SESSION long_query_time = 1;
使用启动脚本的参数
假设您希望使用启动脚本启动 MySQL,而不是通过systemd,特别是用于测试或进行一些临时更改。您可以将变量传递给脚本,而不是在配置文件中更改它。
如何做...
shell> /usr/local/mysql/bin/mysqld --basedir=/usr/local/mysql --datadir=/usr/local/mysql/data --plugin-dir=/usr/local/mysql/lib/plugin --user=mysql --log-error=/usr/local/mysql/data/centos7.err --pid-file=/usr/local/mysql/data/centos7.pid --init-file=/tmp/mysql-init &
您可以看到--init-file参数被传递给服务器。服务器在启动之前执行该文件中的 SQL 语句。
配置参数
安装后,您需要配置的基本事项在本节中都有所涵盖。其余的都可以保持默认或根据负载稍后进行调整。
如何做...
让我们深入了解。
数据目录
由 MySQL 服务器管理的数据存储在一个名为数据目录的目录下。数据目录的每个子目录都是一个数据库目录,对应于服务器管理的数据库。默认情况下,
数据目录有三个子目录:
-
mysql:MySQL 系统数据库 -
performance_schema:提供了用于在运行时检查服务器内部执行的信息 -
sys:提供了一组对象,以帮助更轻松地解释性能模式信息
除此之外,data directory还包含日志文件、InnoDB表空间和InnoDB日志文件、SSL 和 RSA 密钥文件、mysqld的pid以及mysqld-auto.cnf,其中存储了持久化的全局系统变量设置。
要设置data directory的更改/添加datadir的值到配置文件。默认值为/var/lib/mysql:
shell> sudo vi /etc/my.cnf
[mysqld]
datadir = /data/mysql
你可以将其设置为任何你想要存储数据的地方,但你应该将data directory的所有权更改为mysql。
确保承载data directory的磁盘卷有足够的空间来容纳所有的数据。
innodb_buffer_pool_size
这是决定InnoDB存储引擎可以使用多少内存来缓存数据和索引的最重要的调整参数。设置得太低会降低 MySQL 服务器的性能,设置得太高会增加 MySQL 进程的内存消耗。MySQL 8 最好的地方在于innodb_buffer_pool_size是动态的,这意味着你可以在不重新启动服务器的情况下改变innodb_buffer_pool_size。
以下是如何调整它的简单指南:
-
查找数据集的大小。不要将
innodb_buffer_pool_size的值设置得高于数据集的大小。假设你有 12GB 的 RAM 机器,你的数据集大小为 3GB;那么你可以将innodb_buffer_pool_size设置为 3GB。如果你预计数据会增长,你可以根据需要随时增加它,而无需重新启动 MySQL。 -
通常,数据集的大小要比可用的 RAM 大得多。在总 RAM 中,你可以为操作系统、其他进程、MySQL 内部的每个线程缓冲区和
InnoDB之外的 MySQL 服务器分配一些内存。剩下的可以分配给InnoDB缓冲池大小。
这是一个非常通用的表,为你提供了一个很好的起点,假设它是一个专用的 MySQL 服务器,所有的表都是InnoDB,每个线程的缓冲区都保持默认值。如果系统内存不足,你可以动态减少缓冲池。
| RAM | 缓冲池大小(范围) |
|---|---|
| 4 GB | 1 GB-2 GB |
| 8 GB | 4 GB-6 GB |
| 12 GB | 6 GB-10 GB |
| 16 GB | 10 GB-12 GB |
| 32 GB | 24 GB-28 GB |
| 64 GB | 45 GB-56 GB |
| 128 GB | 108 GB-116 GB |
| 256 GB | 220 GB-245 GB |
innodb_buffer_pool_instances
你可以将InnoDB缓冲池划分为单独的区域,以提高并发性,通过减少不同线程对缓存页面的读写而产生的争用。例如,如果缓冲池大小为 64GB,innodb_buffer_pool_instances为 32,那么缓冲池将被分割成 32 个每个 2GB 的区域。
如果缓冲池大小超过 16GB,你可以设置实例,以便每个区域至少获得 1GB 的空间。
innodb_log_file_size
这是用于在数据库崩溃时重放已提交事务的重做日志空间的大小。默认值为 48MB,这对于生产工作负载可能不够。你可以先设置为 1GB 或 2GB。这个更改需要重新启动。停止 MySQL 服务器,并确保它在没有错误的情况下关闭。在my.cnf中进行更改并启动服务器。在早期版本中,你需要停止服务器,删除日志文件,然后启动服务器。在 MySQL 8 中,这是自动的。修改重做日志文件在第十一章中有解释,管理表空间,在更改 InnoDB 重做日志文件的数量或大小部分。
更改数据目录
你的数据可能会随着时间的推移而增长,当它超出文件系统时,你需要添加一个磁盘或将data directory移动到一个更大的卷中。
如何做...
- 检查当前的
data directory。默认情况下,data directory是/var/lib/mysql:
mysql> show variables like '%datadir%';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| datadir | /var/lib/mysql/ |
+---------------+-----------------+
1 row in set (0.04 sec)
- 停止
mysql并确保它已成功停止:
shell> sudo systemctl stop mysql
- 检查状态:
shell> sudo systemctl status mysql
应该显示已停止 MySQL Community Server。
- 在新位置创建目录并将所有权更改为
mysql:
shell> sudo mkdir -pv /data
shell> sudo chown -R mysql:mysql /data/
- 将文件移动到新的
data 目录:
shell> sudo rsync -av /var/lib/mysql /data
- 在 Ubuntu 中,如果已启用 AppArmor,您需要配置访问控制:
shell> vi /etc/apparmor.d/tunables/alias
alias /var/lib/mysql/ -> /data/mysql/,
shell> sudo systemctl restart apparmor
- 启动 MySQL 服务器并验证
data目录已更改:
shell> sudo systemctl start mysql
mysql> show variables like '%datadir%';
+---------------+--------------+
| Variable_name | Value |
+---------------+--------------+
| datadir | /data/mysql/ |
+---------------+--------------+
1 row in set (0.00 sec)
- 验证数据是否完好并删除旧的
data 目录:
shell> sudo rm -rf /var/lib/mysql
如果 MySQL 启动失败并显示错误—MySQL 数据目录在/var/lib/mysql 未找到,请创建一个:
执行sudo mkdir /var/lib/mysql/mysql -p
如果显示MySQL 系统数据库未找到,运行mysql_install_db工具,该工具将创建所需的目录。
第五章:事务
在本章中,我们将涵盖以下示例:
-
执行事务
-
使用保存点
-
隔离级别
-
锁定
介绍
在接下来的示例中,我们将讨论 MySQL 中的事务和各种隔离级别。事务意味着一组应该一起成功或失败的 SQL 语句。事务还应该满足原子性、一致性、隔离性和 持久性(ACID)属性。以一个非常基本的例子,从账户A转账到账户B。假设A有 600 美元,B有 400 美元,B希望从A转账 100 美元给自己。
银行将从A扣除 100 美元,并使用以下 SQL 代码将其添加到B(仅供说明):
mysql> SELECT balance INTO @a.bal FROM account WHERE account_number='A';
以编程方式检查@a.bal是否大于或等于 100:
mysql> UPDATE account SET balance=@a.bal-100 WHERE account_number='A';
mysql> SELECT balance INTO @b.bal FROM account WHERE account_number='B';
以编程方式检查@b.bal是否NOT NULL:
mysql> UPDATE account SET balance=@b.bal+100 WHERE account_number='B';
这四行 SQL 应该是一个单独的事务,并满足以下 ACID 属性:
-
原子性:要么所有的 SQL 都应该成功,要么都应该失败。不应该有任何部分更新。如果不遵守这个属性,如果数据库在运行两个 SQL 后崩溃,那么
A将会损失 100 美元。 -
一致性:事务必须以允许的方式仅改变受影响的数据。在这个例子中,如果带有
B的account_number不存在,整个事务应该被回滚。 -
隔离性:同时发生的事务(并发事务)不应该导致数据库处于不一致的状态。每个事务应该被执行,就好像它是系统中唯一的事务一样。没有任何事务应该影响任何其他事务的存在。假设
A在转账给B的同时完全转移了这 600 美元;两个事务应该独立运行,确保在转移金额之前的余额。 -
持久性:数据应该持久存在于磁盘上,不应该在任何数据库或系统故障时丢失。
InnoDB是 MySQL 中的默认存储引擎,支持事务,而 MyISAM 不支持事务。
执行事务
创建虚拟表和示例数据以理解这个示例:
mysql> CREATE DATABASE bank;
mysql> USE bank;
mysql> CREATE TABLE account(account_number varchar(10) PRIMARY KEY, balance int);
mysql> INSERT INTO account VALUES('A',600),('B',400);
如何做...
要开始一个事务(一组 SQL),执行START TRANSACTION或BEGIN语句:
mysql> START TRANSACTION;
or
mysql> BEGIN;
然后执行所有希望在事务内部的语句,比如从A转账 100 到B:
mysql> SELECT balance INTO @a.bal FROM account WHERE account_number='A';
Programmatically check if @a.bal is greater than or equal to 100
mysql> UPDATE account SET balance=@a.bal-100 WHERE account_number='A';
mysql> SELECT balance INTO @b.bal FROM account WHERE account_number='B';
Programmatically check if @b.bal IS NOT NULL
mysql> UPDATE account SET balance=@b.bal+100 WHERE account_number='B';
确保所有 SQL 都成功执行后,执行COMMIT语句,完成事务并提交数据:
mysql> COMMIT;
如果在中间遇到任何错误并希望中止事务,可以发出ROLLBACK语句而不是COMMIT。
例如,如果A想要转账到一个不存在的账户而不是发送给B,你应该中止事务并将金额退还给A:
mysql> BEGIN;
mysql> SELECT balance INTO @a.bal FROM account WHERE account_number='A';
mysql> UPDATE account SET balance=@a.bal-100 WHERE account_number='A';
mysql> SELECT balance INTO @b.bal FROM account WHERE account_number='C';
Query OK, 0 rows affected, 1 warning (0.07 sec)
mysql> SHOW WARNINGS;
+---------+------+-----------------------------------------------------+
| Level | Code | Message |
+---------+------+-----------------------------------------------------+
| Warning | 1329 | No data - zero rows fetched, selected, or processed |
+---------+------+-----------------------------------------------------+
1 row in set (0.02 sec)
mysql> SELECT @b.bal;
+--------+
| @b.bal |
+--------+
| NULL |
+--------+
1 row in set (0.00 sec)
mysql> ROLLBACK;
Query OK, 0 rows affected (0.01 sec)
自动提交
默认情况下,自动提交是ON,这意味着所有单独的语句在执行时都会被提交,除非它们在BEGIN...COMMIT块中。如果自动提交是OFF,你需要显式地发出COMMIT语句来提交一个事务。要禁用它,执行:
mysql> SET autocommit=0;
DDL 语句,比如数据库的CREATE或DROP,以及表或存储过程的CREATE、DROP或ALTER,不能被回滚。
有一些语句,比如 DDLs、LOAD DATA INFILE、ANALYZE TABLE、与复制相关的语句等会导致隐式的COMMIT。有关这些语句的更多细节,请参阅dev.mysql.com/doc/refman/8.0/en/implicit-commit.html。
使用保存点
使用保存点,你可以在事务中回滚到某些点,而不终止事务。你可以使用SAVEPOINT identifier来为事务设置一个名称,并使用ROLLBACK TO identifier语句来将事务回滚到指定的保存点,而不终止事务。
如何做...
假设A想要转账给多个账户;即使向一个账户的转账失败,其他账户也不应该被回滚:
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT balance INTO @a.bal FROM account WHERE account_number='A';
Query OK, 1 row affected (0.01 sec)
mysql> UPDATE account SET balance=@a.bal-100 WHERE account_number='A';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> UPDATE account SET balance=balance+100 WHERE account_number='B';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> SAVEPOINT transfer_to_b;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT balance INTO @a.bal FROM account WHERE account_number='A';
Query OK, 1 row affected (0.00 sec)
mysql> UPDATE account SET balance=balance+100 WHERE account_number='C';
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0
### Since there are no rows updated, meaning there is no account with 'C', you can rollback the transaction to SAVEPOINT where transfer to B is successful. Then 'A' will get back 100 which was deducted to transfer to C. If you wish not to use the save point, you should do these in two transactions.
mysql> ROLLBACK TO transfer_to_b;
Query OK, 0 rows affected (0.00 sec)
mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT balance FROM account WHERE account_number='A';
+---------+
| balance |
+---------+
| 400 |
+---------+
1 row in set (0.00 sec)
mysql> SELECT balance FROM account WHERE account_number='B';
+---------+
| balance |
+---------+
| 600 |
+---------+
1 row in set (0.00 sec)
隔离级别
当两个或更多事务同时发生时,隔离级别定义了事务与其他事务所做的资源或数据修改隔离的程度。有四种隔离级别;要更改隔离级别,您需要设置动态的具有会话级范围的tx_isolation变量。
如何做...
要更改此级别,请执行SET @@transaction_isolation = 'READ-COMMITTED';。
读取未提交
当前事务可以读取另一个未提交事务写入的数据,这也称为脏读。
例如,A想要向他的账户添加一些金额并转账给B。假设两个交易同时发生;流程将如下。
A最初有 400 美元,想要在向B转账 500 美元后向他的账户添加 500 美元。
| # 事务 1(添加金额) | # 事务 2(转账金额) |
|---|
|
BEGIN;
|
BEGIN;
|
|
UPDATE account
SET balance=balance+500
WHERE account_number='A';
| -- |
|---|
| -- |
SELECT balance INTO @a.bal
FROM account
WHERE account_number='A';
# A sees 900 here
|
|
ROLLBACK;
# Assume due to some reason the
transaction got rolled back
| -- |
|---|
| -- |
# A transfers 900 to B since
A has 900 in previous SELECT
UPDATE account
SET balance=balance-900
WHERE account_number='A';
|
| -- |
|---|
# B receives the amount UPDATE account
SET balance=balance+900
WHERE account_number='B';
|
| -- |
|---|
# Transaction 2 completes successfully
COMMIT;
|
您可以注意到事务 2已经读取了未提交或回滚的事务 1的数据,导致此事务后账户A的余额变为负数,这显然是不希望的。
读取提交
当前事务只能读取另一个事务提交的数据,这也称为不可重复读。
再举一个例子,A有 400 美元,B有 600 美元。
| # 事务 1(添加金额) | # 事务 2(转账金额) |
|---|
|
BEGIN;
|
BEGIN;
|
|
UPDATE account SET balance=balance+500
WHERE account_number='A';
| -- |
|---|
| -- |
SELECT balance INTO @a.bal
FROM account
WHERE account_number='A';
# A sees 400 here because transaction 1 has not committed the data yet
|
|
COMMIT;
| -- |
|---|
| -- |
SELECT balance INTO @a.bal
FROM account
WHERE account_number='A';
# A sees 900 here because transaction 1 has committed the data.
|
您可以注意到,在同一事务中,相同的SELECT语句获取了不同的结果。
可重复读
即使另一个事务已经提交了数据,事务仍将看到由第一个语句读取的相同数据。同一事务中的所有一致读取都读取第一次读取建立的快照。例外是可以读取同一事务中更改的数据的事务。
当事务开始并执行其第一次读取时,将创建一个读取视图,并保持打开直到事务结束。为了在事务结束之前提供相同的结果集,InnoDB使用行版本和UNDO信息。假设事务 1选择了一些行,另一个事务删除了这些行并提交了数据。如果事务 1仍然打开,它应该能够看到它一开始选择的行。已删除的行被保留在UNDO日志空间中以满足事务 1。一旦事务 1完成,这些行将被标记为从UNDO日志中删除。这称为多版本并发控制(MVCC)。
再举一个例子,A有 400 美元,B有 600 美元。
| # 事务 1(添加金额) | # 事务 2(转账金额) |
|---|
|
BEGIN;
|
BEGIN;
|
| -- |
|---|
SELECT balance INTO @a.bal
FROM account
WHERE account_number='A';
# A sees 400 here
|
|
UPDATE account
SET balance=balance+500
WHERE account_number='A';
| -- |
|---|
| -- |
SELECT balance INTO @a.bal
FROM account
WHERE account_number='A';
# A sees still 400 even though transaction 1 is committed
|
|
COMMIT;
| -- |
|---|
| -- |
COMMIT;
|
| -- |
|---|
SELECT balance INTO @a.bal
FROM account
WHERE account_number='A';
# A sees 900 here because this is a fresh transaction
|
这仅适用于SELECT语句,不一定适用于 DML 语句。如果您插入或修改了一些行,然后提交该事务,来自另一个并发的REPEATABLE READ事务的DELETE或UPDATE语句可能会影响那些刚刚提交的行,即使会话无法查询它们。如果一个事务更新或删除了另一个事务提交的行,这些更改将对当前事务可见。
例如:
| # 事务 1 | # 事务 2 |
|---|
|
BEGIN;
|
BEGIN;
|
|
SELECT * FROM account;
# 2 rows are returned
| -- |
|---|
| -- |
INSERT INTO account VALUES('C',1000);
# New account is created
|
| -- |
|---|
COMMIT;
|
|
SELECT * FROM account WHERE account_number='C';
# no rows are returned because of MVCC
| -- |
|---|
|
DELETE FROM account WHERE account_number='C';
# Surprisingly account C gets deleted
| -- |
|---|
| -- |
SELECT * FROM account;
# 3 rows are returned because transaction 1 is not yet committed
|
|
COMMIT;
| -- |
|---|
| -- |
SELECT * FROM account;
# 2 rows are returned because transaction 1 is committed
|
这是另一个例子:
| # 事务 1 | # 事务 2 |
|---|
|
BEGIN;
|
BEGIN;
|
|
SELECT * FROM account;
# 2 rows are returned
| -- |
|---|
| -- |
INSERT INTO account VALUES('D',1000);
|
| -- |
|---|
COMMIT;
|
|
SELECT * FROM account;
# 3 rows are returned because of MVCC
| -- |
|---|
|
UPDATE account SET balance=1000 WHERE account_number='D';
# Surprisingly account D gets updated
| -- |
|---|
|
SELECT * FROM account;
# Surprisingly 4 rows are returned
| -- |
|---|
可串行化
这提供了通过锁定所有被选中的行来提供最高级别的隔离。这个级别类似于REPEATABLE READ,但是如果禁用了自动提交,InnoDB会隐式地将所有普通的SELECT语句转换为SELECT...LOCK IN SHARE MODE。如果启用了自动提交,SELECT就是它自己的事务。
例如:
| # 事务 1 | # 事务 2 |
|---|
|
BEGIN;
|
BEGIN;
|
|
SELECT * FROM account WHERE account_number='A';
| -- |
|---|
| -- |
UPDATE account SET balance=1000 WHERE account_number='A';
# This will wait until the lock held by transaction 1
on row A is released
|
|
COMMIT;
| -- |
|---|
| -- |
# UPDATE will be successful now
|
另一个例子:
| # 事务 1 | # 事务 2 |
|---|
|
BEGIN;
|
BEGIN;
|
|
SELECT * FROM account WHERE account_number='A';
# Selects values of A
| -- |
|---|
| -- |
INSERT INTO account VALUES('D',2000);
# Inserts D
|
|
SELECT * FROM account WHERE account_number='D';
# This will wait until the transaction 2 completes
| -- |
|---|
| -- |
COMMIT;
|
|
# Now the preceding select statement returns values of D
| -- |
|---|
因此,可串行化等待锁,并始终读取最新提交的数据。
锁定
有两种类型的锁定:
-
内部锁定:MySQL 在服务器内部执行内部锁定,以管理多个会话对表内容的争用
-
外部锁定:MySQL 为客户会话提供了显式获取表锁的选项,以防止其他会话访问表。
内部锁定:主要有两种类型的锁:
-
行级锁定:锁定粒度到行级。只有访问的行被锁定。这允许多个会话同时写入访问,使它们适用于多用户、高并发和 OLTP 应用程序。只有
InnoDB支持行级锁定。 -
表级锁定:MySQL 对
MyISAM、MEMORY和MERGE表使用表级锁定,每次只允许一个会话更新这些表。这种锁定级别使这些存储引擎更适合只读、读多或单用户应用程序。
参考dev.mysql.com/doc/refman/8.0/en/internal-locking.html和dev.mysql.com/doc/refman/8.0/en/innodb-locking.html以了解更多关于InnoDB锁的信息。
外部锁定:您可以使用LOCK TABLE和UNLOCK TABLES语句来控制锁。
对READ和WRITE的表锁定如下所述:
-
READ:当表被锁定为READ时,多个会话可以从表中读取数据而不需要获取锁。此外,多个会话可以在同一张表上获取锁,这就是为什么READ锁也被称为共享锁。当持有READ锁时,没有会话可以向表中写入数据(包括持有锁的会话)。如果有任何写入尝试,它将处于等待状态,直到READ锁被释放。 -
WRITE:当表被锁定为WRITE时,除了持有锁的会话外,没有其他会话可以从表中读取和写入数据。直到现有锁被释放,其他会话甚至无法获取任何锁。这就是为什么这被称为独占锁。如果有任何读/写尝试,它将处于等待状态,直到WRITE锁被释放。
当执行UNLOCK TABLES语句或会话终止时,所有锁都会被释放。
如何做...
语法如下:
mysql> LOCK TABLES table_name [READ | WRITE]
要解锁表,请使用:
mysql> UNLOCK TABLES;
要锁定所有数据库中的所有表,请执行以下语句。在对数据库进行一致快照时使用。它会冻结对数据库的所有写入:
mysql> FLUSH TABLES WITH READ LOCK;
锁定队列
除了共享锁(一张表可以有多个共享锁)外,一张表上不能同时持有两个锁。如果一张表已经有一个共享锁,而独占锁来了,它将被保留在队列中,直到共享锁被释放。当独占锁在队列中时,所有后续的共享锁也会被阻塞并保留在队列中。
InnoDB在从表中读取/写入时会获取元数据锁。如果第二个事务请求WRITE LOCK,它将被保留在队列中,直到第一个事务完成。如果第三个事务想要读取数据,它必须等到第二个事务完成。
事务 1:
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM employees LIMIT 10;
+--------+------------+------------+-----------+--------+------------+
| emp_no | birth_date | first_name | last_name | gender | hire_date |
+--------+------------+------------+-----------+--------+------------+
| 10001 | 1953-09-02 | Georgi | Facello | M | 1986-06-26 |
| 10002 | 1964-06-02 | Bezalel | Simmel | F | 1985-11-21 |
| 10003 | 1959-12-03 | Parto | Bamford | M | 1986-08-28 |
| 10004 | 1954-05-01 | Chirstian | Koblick | M | 1986-12-01 |
| 10005 | 1955-01-21 | Kyoichi | Maliniak | M | 1989-09-12 |
| 10006 | 1953-04-20 | Anneke | Preusig | F | 1989-06-02 |
| 10007 | 1957-05-23 | Tzvetan | Zielinski | F | 1989-02-10 |
| 10008 | 1958-02-19 | Saniya | Kalloufi | M | 1994-09-15 |
| 10009 | 1952-04-19 | Sumant | Peac | F | 1985-02-18 |
| 10010 | 1963-06-01 | Duangkaew | Piveteau | F | 1989-08-24 |
+--------+------------+------------+-----------+--------+------------+
10 rows in set (0.00 sec)
注意COMMIT没有被执行。事务保持打开状态。
事务 2:
mysql> LOCK TABLE employees WRITE;
此语句必须等到事务 1 完成。
事务 3:
mysql> SELECT * FROM employees LIMIT 10;
即使事务 3 也不会产生任何结果,因为一个排他锁在队列中(它在等待事务 2 完成)。此外,它会阻塞表上的所有操作。
您可以通过从另一个会话中检查SHOW PROCESSLIST来检查这一点:
mysql> SHOW PROCESSLIST;
+----+------+-----------+-----------+---------+------+---------------------------------+----------------------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+------+-----------+-----------+---------+------+---------------------------------+----------------------------------+
| 20 | root | localhost | employees | Sleep | 48 | | NULL |
| 21 | root | localhost | employees | Query | 34 | Waiting for table metadata lock | LOCK TABLE employees WRITE |
| 22 | root | localhost | employees | Query | 14 | Waiting for table metadata lock | SELECT * FROM employees LIMIT 10 |
| 23 | root | localhost | employees | Query | 0 | starting | SHOW PROCESSLIST |
+----+------+-----------+-----------+---------+------+---------------------------------+----------------------------------+
4 rows in set (0.00 sec)
您可以注意到事务 2 和事务 3 都在等待事务 1。
要了解有关元数据锁的更多信息,请参考dev.mysql.com/doc/refman/8.0/en/metadata-locking.html。在使用FLUSH TABLES WITH READ LOCK时也可以观察到相同的行为。
事务 1:
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM employees LIMIT 10;
+--------+------------+------------+-----------+--------+------------+
| emp_no | birth_date | first_name | last_name | gender | hire_date |
+--------+------------+------------+-----------+--------+------------+
| 10001 | 1953-09-02 | Georgi | Facello | M | 1986-06-26 |
| 10002 | 1964-06-02 | Bezalel | Simmel | F | 1985-11-21 |
| 10003 | 1959-12-03 | Parto | Bamford | M | 1986-08-28 |
| 10004 | 1954-05-01 | Chirstian | Koblick | M | 1986-12-01 |
| 10005 | 1955-01-21 | Kyoichi | Maliniak | M | 1989-09-12 |
| 10006 | 1953-04-20 | Anneke | Preusig | F | 1989-06-02 |
| 10007 | 1957-05-23 | Tzvetan | Zielinski | F | 1989-02-10 |
| 10008 | 1958-02-19 | Saniya | Kalloufi | M | 1994-09-15 |
| 10009 | 1952-04-19 | Sumant | Peac | F | 1985-02-18 |
| 10010 | 1963-06-01 | Duangkaew | Piveteau | F | 1989-08-24 |
+--------+------------+------------+-----------+--------+------------+
10 rows in set (0.00 sec)
请注意,COMMIT没有被执行。事务保持打开状态。
事务 2:
mysql> FLUSH TABLES WITH READ LOCK;
事务 3:
mysql> SELECT * FROM employees LIMIT 10;
即使事务 3 也不会产生任何结果,因为FLUSH TABLES在获取锁之前需要等待表上的所有操作完成。此外,它会阻塞表上的所有操作。
您可以通过从另一个会话中检查SHOW PROCESSLIST来检查这一点。
mysql> SHOW PROCESSLIST;
+----+------+-----------+-----------+---------+------+-------------------------+--------------------------------------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+------+-----------+-----------+---------+------+-------------------------+--------------------------------------------------+
| 20 | root | localhost | employees | Query | 7 | Creating sort index | SELECT * FROM employees ORDER BY first_name DESC |
| 21 | root | localhost | employees | Query | 5 | Waiting for table flush | FLUSH TABLES WITH READ LOCK |
| 22 | root | localhost | employees | Query | 3 | Waiting for table flush | SELECT * FROM employees LIMIT 10 |
| 23 | root | localhost | employees | Query | 0 | starting | SHOW PROCESSLIST |
+----+------+-----------+-----------+---------+------+-------------------------+--------------------------------------------------+
4 rows in set (0.00 sec)
为了进行一致的备份,所有备份方法都使用FLUSH TABLES WITH READ LOCK,如果表上有长时间运行的事务,这可能非常危险。
第六章:二进制日志记录
在本章中,我们将介绍以下配方:
-
使用二进制日志记录
-
二进制日志格式
-
从二进制日志中提取语句
-
忽略数据库以写入二进制日志
-
重新定位二进制日志
介绍
二进制日志包含对数据库的所有更改的记录,包括数据和结构。二进制日志不用于不修改数据的语句,如SELECT或SHOW。运行启用二进制日志的服务器会略微影响性能。二进制日志是崩溃安全的。只有完整的事件或事务才会被记录或读取。
为什么要使用二进制日志?
-
复制:您可以使用二进制日志将对服务器所做的更改流式传输到另一个服务器。从服务器充当镜像副本,并可用于分发负载。接受写入的服务器称为主服务器,镜像副本服务器称为从服务器。
-
时间点恢复:假设您在星期日的 00:00 进行备份,而您的数据库在星期日的 8:00 崩溃。使用备份,您可以恢复到星期日的 00:00。使用二进制日志,您可以回放它们,以恢复到 08:00。
使用二进制日志记录
要启用binlog,必须设置log_bin和server_id并重新启动服务器。您可以在log_bin中提及路径和基本名称。例如,log_bin设置为/data/mysql/binlogs/server1,则二进制日志存储在/data/mysql/binlogs文件夹中,名称为server1.000001,server1.000002等。服务器每次启动或刷新日志或当前日志大小达到max_binlog_size时,都会创建一个新文件。它维护server1.index文件,其中包含每个二进制日志的位置。
如何做...
让我们看看如何处理日志。我相信您会喜欢学习它们。
启用二进制日志记录
- 启用二进制日志记录并设置
server_id。在您喜欢的编辑器中打开 MySQLconfig文件并追加以下行。选择server_id,使其对您基础架构中的每个 MySQL 服务器都是唯一的。
您还可以简单地将log_bin变量放在my.cnf中,而不设置任何值。在这种情况下,二进制日志将在data directory目录中创建,并使用hostname作为其名称。
shell> sudo vi /etc/my.cnf
[mysqld]
log_bin = /data/mysql/binlogs/server1
server_id = 100
- 重新启动 MySQL 服务器:
shell> sudo systemctl restart mysql
- 验证是否创建了二进制日志:
mysql> SHOW VARIABLES LIKE 'log_bin%';
+---------------------------------+-----------------------------------+
| Variable_name | Value |
+---------------------------------+-----------------------------------+
| log_bin | ON |
| log_bin_basename | /data/mysql/binlogs/server1 |
| log_bin_index | /data/mysql/binlogs/server1.index |
| log_bin_trust_function_creators | OFF |
| log_bin_use_v1_row_events | OFF |
+---------------------------------+-----------------------------------+
5 rows in set (0.00 sec)
mysql> SHOW MASTER LOGS;
+----------------+-----------+
| Log_name | File_size |
+----------------+-----------+
| server1.000001 | 154 |
+----------------+-----------+
1 row in set (0.00 sec)
shell> sudo ls -lhtr /data/mysql/binlogs
total 8.0K
-rw-r----- 1 mysql mysql 34 Aug 15 05:01 server1.index
-rw-r----- 1 mysql mysql 154 Aug 15 05:01 server1.000001
-
执行
SHOW BINARY LOGS;或SHOW MASTER LOGS;以显示服务器的所有二进制日志。 -
执行
SHOW MASTER STATUS;命令以获取当前二进制日志位置:
mysql> SHOW MASTER STATUS;
+----------------+----------+--------------+------------------+------------------+++++++++++++++++++++++++++++++++++++-+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+----------------+----------+--------------+------------------+-------------------+
| server1.000002 | 3273 | | | |
+----------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)
一旦server1.000001达到max_binlog_size(默认为 1GB),将创建一个新文件server1.000002并将其添加到server1.index中。您可以配置使用SET @@global.max_binlog_size=536870912动态设置max_binlog_size。
禁用会话的二进制日志记录
可能存在不希望将语句复制到其他服务器的情况。为此,可以使用以下命令禁用该会话的二进制日志记录:
mysql> SET SQL_LOG_BIN = 0;
在执行前一个语句后记录的所有 SQL 语句不会记录到二进制日志中。这仅适用于该会话。
要启用,可以执行以下操作:
mysql> SET SQL_LOG_BIN = 1;
移至下一个日志
您可以使用FLUSH LOGS命令关闭当前的二进制日志并打开一个新的:
mysql> SHOW BINARY LOGS;
+----------------+-----------+
| Log_name | File_size |
+----------------+-----------+
| server1.000001 | 154 |
+----------------+-----------+
1 row in set (0.00 sec)
mysql> FLUSH LOGS;
Query OK, 0 rows affected (0.02 sec)
mysql> SHOW BINARY LOGS;
+----------------+-----------+
| Log_name | File_size |
+----------------+-----------+
| server1.000001 | 198 |
| server1.000002 | 154 |
+----------------+-----------+
2 rows in set (0.00 sec)
过期二进制日志
基于写入次数,二进制日志会占用大量空间。将它们保持不变可能会在短时间内填满磁盘。清理它们是至关重要的:
- 使用
binlog_expire_logs_seconds和expire_logs_days设置日志的到期时间。
如果要设置以天为单位的到期时间,请设置expire_logs_days。例如,如果要删除两天前的所有二进制日志,SET @@global.expire_logs_days=2。将值设置为0会禁用自动到期。
如果您想要更细粒度,可以使用binlog_expire_logs_seconds变量,该变量设置二进制日志的过期时间(以秒为单位)。
此变量和expire_logs_days的效果是累积的。例如,如果expire_logs_days是1,binlog_expire_logs_seconds是43200,那么二进制日志将每 1.5 天清除一次。这与将binlog_expire_logs_seconds设置为129600并将expire_logs_days设置为 0 的结果相同。在 MySQL 8.0 中,binlog_expire_logs_seconds和expire_logs_days必须都设置为 0 才能禁用二进制日志的自动清除。
- 要手动清除日志,请执行
PURGE BINARY LOGS TO '<file_name>'。例如,如果有文件如server1.000001,server1.000002,server1.000003和server1.000004,如果您执行PURGE BINARY LOGS TO 'server1.000004',则所有文件直到server1.000003将被删除,server1.000004不会被触及:
mysql> SHOW BINARY LOGS;
+----------------+-----------+
| Log_name | File_size |
+----------------+-----------+
| server1.000001 | 198 |
| server1.000002 | 198 |
| server1.000003 | 198 |
| server1.000004 | 154 |
+----------------+-----------+
4 rows in set (0.00 sec)
mysql> PURGE BINARY LOGS TO 'server1.000004';
Query OK, 0 rows affected (0.00 sec)
mysql> SHOW BINARY LOGS;
+----------------+-----------+
| Log_name | File_size |
+----------------+-----------+
| server1.000004 | 154 |
+----------------+-----------+
1 row in set (0.00 sec)
您还可以执行命令PURGE BINARY LOGS BEFORE '2017-08-03 15:45:00',而不是指定日志文件。您还可以使用单词MASTER而不是BINARY。
mysql> PURGE MASTER LOGS TO 'server1.000004'也可以。
- 要删除所有二进制日志并重新开始,请执行
RESET MASTER:
mysql> SHOW BINARY LOGS;
+----------------+-----------+
| Log_name | File_size |
+----------------+-----------|
| server1.000004 | 154 |
+----------------+-----------+
1 row in set (0.00 sec)
mysql> RESET MASTER;
Query OK, 0 rows affected (0.01 sec)
mysql> SHOW BINARY LOGS;
+----------------+-----------+
| Log_name | File_size |
+----------------+-----------+
| server1.000001 | 154 |
+----------------+-----------+
1 row in set (0.00 sec)
如果您正在使用复制,清除或过期日志是一种非常不安全的方法。清除二进制日志的安全方法是使用mysqlbinlogpurge脚本,这将在第十二章 管理日志中介绍。
二进制日志格式
二进制日志可以以三种格式写入:
-
STATEMENT:实际的 SQL 语句被记录。 -
ROW:对每一行所做的更改都会被记录。
例如,更新语句更新了 10 行,所有 10 行的更新信息都被写入日志。而在基于语句的复制中,只有更新语句被写入。默认格式是ROW。
MIXED:MySQL 根据需要从STATEMENT切换到ROW。
有些语句在不同服务器上执行时可能会导致不同的结果。例如,UUID()函数的输出因服务器而异。这些语句称为非确定性语句,对于基于语句的复制来说是不安全的。在这种情况下,当您设置MIXED格式时,MySQL 服务器会切换到基于行的格式。
请参阅dev.mysql.com/doc/refman/8.0/en/binary-log-mixed.html了解有关不安全语句和切换发生的更多信息。
如何做...
您可以使用动态变量binlog_format来设置格式,该变量具有全局和会话范围。在全局级别设置它会使所有客户端使用指定的格式:
mysql> SET GLOBAL binlog_format = 'STATEMENT';
或者:
mysql> SET GLOBAL binlog_format = 'ROW';
请参阅dev.mysql.com/doc/refman/8.0/en/replication-sbr-rbr.html了解各种格式的优缺点。
- MySQL 8.0 使用版本 2 的二进制日志行事件,这些事件不能被 MySQL 5.6.6 之前的版本读取。将
log-bin-use-v1-row-events设置为1以使用版本 1,以便可以被 MySQL 5.6.6 之前的版本读取。默认值为0。
mysql> SET @@GLOBAL.log_bin_use_v1_row_events=0;
- 当您创建存储函数时,必须声明它是确定性的或者不修改数据。否则,它可能对二进制日志记录不安全。默认情况下,要接受
CREATE FUNCTION语句,必须显式指定DETERMINISTIC,NO SQL或READS SQL DATA中的至少一个。否则会发生错误:
ERROR 1418 (HY000): This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled (you *might* want to use the less safe log_bin_trust_function_creators variable)
您可以在例程中写入非确定性语句,并且仍然声明为DETERMINISTIC(这不是一个好的做法),如果您想要复制未声明为DETERMINISTIC的例程,可以设置log_bin_trust_function_creators变量:
mysql> SET GLOBAL log_bin_trust_function_creators = 1;
另请参阅
请参阅dev.mysql.com/doc/refman/8.0/en/stored-programs-logging.html了解有关存储程序如何复制的更多信息。
从二进制日志中提取语句
您可以使用(随 MySQL 一起提供的)mysqlbinlog实用程序从二进制日志中提取内容并将其应用到其他服务器上。
准备工作
使用各种二进制格式执行几个语句。当您将binlog_format设置为GLOBAL级别时,您必须断开连接并重新连接以获取更改。如果您想保持连接,请设置为SESSION级别。
切换到基于语句的复制(SBR):
mysql> SET @@GLOBAL.BINLOG_FORMAT='STATEMENT';
Query OK, 0 rows affected (0.00 sec)
更新几行:
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE salaries SET salary=salary*2 WHERE emp_no<10002;
Query OK, 18 rows affected (0.00 sec)
Rows matched: 18 Changed: 18 Warnings: 0
mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)
切换到基于行的复制(RBR):
mysql> SET @@GLOBAL.BINLOG_FORMAT='ROW';
Query OK, 0 rows affected (0.00 sec)
更新几行:
mysql> BEGIN;Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE salaries SET salary=salary/2 WHERE emp_no<10002;Query OK, 18 rows affected (0.00 sec)
Rows matched: 18 Changed: 18 Warnings: 0
mysql> COMMIT;Query OK, 0 rows affected (0.00 sec)
切换到MIXED格式:
mysql> SET @@GLOBAL.BINLOG_FORMAT='MIXED';
Query OK, 0 rows affected (0.00 sec)
更新几行:
mysql> BEGIN;Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE salaries SET salary=salary*2 WHERE emp_no<10002;Query OK, 18 rows affected (0.00 sec)
Rows matched: 18 Changed: 18 Warnings: 0
mysql> INSERT INTO departments VALUES('d010',UUID());Query OK, 1 row affected (0.00 sec)
mysql> COMMIT;Query OK, 0 rows affected (0.00 sec)
如何操作...
要显示server1.000001的内容,请执行以下操作:
shell> sudo mysqlbinlog /data/mysql/binlogs/server1.000001
您将获得类似以下内容的输出:
# at 226
#170815 12:49:24 server id 200 end_log_pos 312 CRC32 0x9197bf88 Query thread_id=5 exec_time=0 error_code=0
BINLOG '
~
~
在第一行中,# at后面的数字表示二进制日志文件中事件的起始位置(文件偏移量)。第二行包含语句在服务器上开始的时间戳。时间戳后面是server id、end_log_pos、thread_id、exec_time和error_code。
-
server id:是事件发生的服务器的server_id值(在本例中为200)。 -
end_log_pos:是下一个事件的起始位置。 -
thread_id:指示执行事件的线程。 -
exec_time:是在主服务器上执行事件所花费的时间。在从服务器上,它是从从服务器上的结束执行时间减去主服务器上的开始执行时间的差异。这个差异作为指示从服务器落后主服务器的程度的指标。 -
error_code:指示执行事件的结果。零表示没有发生错误。
观察
- 您在基于语句的复制中执行了
UPDATE语句,并且相同的语句记录在二进制日志中。除了服务器外,会话变量也保存在二进制日志中,以在从服务器上复制相同的行为:
# at 226
#170815 13:28:38 server id 200 end_log_pos 324 CRC32 0x9d27fc78 Query thread_id=8 exec_time=0 error_code=0
SET TIMESTAMP=1502803718/*!*/;
SET @@session.pseudo_thread_id=8/*!*/;
SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0,
@@session.unique_checks=1, @@session.autocommit=1/*!*/;
SET @@session.sql_mode=1436549152/*!*/;
SET @@session.auto_increment_increment=1, @@session.auto_increment_offset=1/*!*/;
/*!\C utf8 *//*!*/;
SET @@session.character_set_client=33,@@session.collation_connection=33,@@session.collation_server=255/*!*/;
SET @@session.lc_time_names=0/*!*/;
SET @@session.collation_database=DEFAULT/*!*/;
BEGIN
/*!*/;
# at 324
#170815 13:28:38 server id 200 end_log_pos 471 CRC32 0x35c2ba45 Query thread_id=8 exec_time=0 error_code=0
use `employees`/*!*/;
SET TIMESTAMP=1502803718/*!*/;
UPDATE salaries SET salary=salary*2 WHERE emp_no<10002 /*!*/;
# at 471
#170815 13:28:40 server id 200 end_log_pos 502 CRC32 0xb84cfeda Xid = 53
COMMIT/*!*/;
- 当使用基于行的复制时,保存的不是语句,而是以二进制格式保存的
ROW,您无法阅读。此外,您可以观察到,单个更新语句生成了如此多的数据。查看提取行事件显示部分,该部分解释了如何查看二进制格式。
BEGIN
/*!*/;
# at 660
#170815 13:29:02 server id 200 end_log_pos 722 CRC32 0xe0a2ec74 Table_map:`employees`.`salaries` mapped to number 165
# at 722
#170815 13:29:02 server id 200 end_log_pos 1298 CRC32 0xf0ef8b05 Update_rows: table id 165 flags: STMT_END_F
BINLOG '
HveSWRPIAAAAPgAAANICAAAAAKUAAAAAAAEACWVtcGxveWVlcwAIc2FsYXJpZXMABAMDCgoAAAEBAHTsouA=HveSWR/IAAAAQAIAABIFAAAAAKUAAAAAAAEAAgAE///wEScAAFSrAwDahA/ahg/wEScAAKrVAQDahA/ahg/wEScAAFjKAwDahg/ZiA/wEScAACzlAQDahg/ZiA/wEScAAGgIBADZiA/Zig/wEScAADQEAgDZiA/Zig/wEScAAJAQBADZig/ZjA/wEScAAEgIAgDZig/ZjA/wEScAAEQWBADZjA/Zjg/wEScAACILAgDZjA/Zjg/wEScAABhWBADZjg/YkA/wEScAAAwrAgDZjg/YkA/wEScAAHSJBADYkA/Ykg/wEScAALpEAgDYkA/Ykg/wEScAAFiYBADYkg/YlA/wEScAACxMAgDYkg/YlA/wEScAAGijBADYlA/Ylg/wEScAALRRAgDYlA/Ylg/wEScAAFCxBADYlg/XmA/wEScAAKhYAgDYlg/XmA/wEScAADTiBADXmA/Xmg/wEScAABpxAgDXmA/Xmg/wEScAAATyBADXmg/XnA/wEScAAAJ5AgDXmg/XnA/wEScAACTzBADXnA/Xng/wEScAAJJ5AgDXnA/Xng/wEScAANQuBQDXng/WoA/wEScAAGqXAgDXng/WoA/wEScAAOAxBQDWoA/Wog/wEScAAPCYAgDWoA/Wog/wEScAAKQxBQDWog/WpA/wEScAANKYAgDWog/WpA/wEScAAIAaBgDWpA8hHk7wEScAAEANAwDWpA8hHk7wEScAAIAaBgDSwg8hHk7wEScAAEANAwDSwg8hHk4Fi+/w '/*!*/;
# at 1298
#170815 13:29:02 server id 200 end_log_pos 1329 CRC32 0xa6dac5dc Xid = 56
COMMIT/*!*/;
- 当使用
MIXED格式时,UPDATE语句被记录为 SQL,而INSERT语句以基于行的格式记录,因为INSERT具有不确定性的UUID()函数:
BEGIN
/*!*/;
# at 1499
#170815 13:29:27 server id 200 end_log_pos 1646 CRC32 0xc73d68fb Query thread_id=8 exec_time=0 error_code=0
SET TIMESTAMP=1502803767/*!*/;
UPDATE salaries SET salary=salary*2 WHERE emp_no<10002 /*!*/;
# at 1646
#170815 13:29:50 server id 200 end_log_pos 1715 CRC32 0x03ae0f7e Table_map: `employees`.`departments` mapped to number 166
# at 1715
#170815 13:29:50 server id 200 end_log_pos 1793 CRC32 0xa43c5dac Write_rows: table id 166 flags: STMT_END_F
BINLOG 'TveSWRPIAAAARQAAALMGAAAAAKYAAAAAAAMACWVtcGxveWVlcwALZGVwYXJ0bWVudHMAAv4PBP4QoAAAAgP8/wB+D64DTveSWR7IAAAATgAAAAEHAAAAAKYAAAAAAAEAAgAC//wEZDAxMSRkMDNhMjQwZS04MWJkLTExZTctODQxMC00MjAxMGE5NDAwMDKsXTyk '/*!*/;
# at 1793
#170815 13:29:50 server id 200 end_log_pos 1824 CRC32 0x4f63aa2e Xid = 59
COMMIT/*!*/;
提取的日志可以通过管道传输到 MySQL 以重放事件。在重放二进制日志时最好使用 force 选项,因为如果它卡在某一点,它不会停止执行。稍后,您可以找出错误并手动修复数据。
shell> sudo mysqlbinlog /data/mysql/binlogs/server1.000001 | mysql -f -h <remote_host> -u <username> -p
或者您可以保存到文件中,以后执行:
shell> sudo mysqlbinlog /data/mysql/binlogs/server1.000001 > server1.binlog_extract
shell> cat server1.binlog_extract | mysql -h <remote_host> -u <username> -p
基于时间和位置提取
您可以通过指定位置从二进制日志中提取部分数据。假设您想进行时间点恢复。假设在2017-08-19 12:18:00执行了DROP DATABASE命令,并且最新可用的备份是2017-08-19 12:00:00,您已经恢复了。现在,您需要从12:00:01到2017-08-19 12:17:00恢复数据。请记住,如果您提取完整的日志,它也将包含DROP DATABASE命令,并且会再次擦除您的数据。
您可以通过--start-datetime和--stop-datatime选项指定时间窗口来提取数据。
shell> sudo mysqlbinlog /data/mysql/binlogs/server1.000001 --start-datetime="2017-08-19 00:00:01" --stop-datetime="2017-08-19 12:17:00" > binlog_extract
使用时间窗口的缺点是您将错过灾难发生时发生的事务。为了避免这种情况,您必须使用二进制日志文件中事件的文件偏移量。
一致的备份保存了已备份的二进制日志文件偏移量。一旦备份恢复,您必须从备份提供的偏移量提取二进制日志。您将在下一章中了解更多关于备份的内容。
假设备份给出了偏移量471,并且DROP DATABASE命令在偏移量1793处执行。您可以使用--start-position和--stop-position选项在偏移量之间提取日志:
shell> sudo mysqlbinlog /data/mysql/binlogs/server1.000001 --start-position=471 --stop-position=1793 > binlog_extract
确保提取的 binlog 中不再出现DROP DATABASE命令。
基于数据库提取
使用--database选项,您可以过滤特定数据库的事件。如果您多次使用此选项,则只会考虑最后一个选项。这对于基于行的复制非常有效。但对于基于语句的复制和MIXED,只有在选择默认数据库时才会输出。
以下命令从 employees 数据库中提取事件:
shell> sudo mysqlbinlog /data/mysql/binlogs/server1.000001 --database=employees > binlog_extract
如 MySQL 8 参考手册中所述,假设二进制日志是通过使用基于语句的日志记录执行这些语句创建的:
mysql>
INSERT INTO test.t1 (i) VALUES(100);
INSERT INTO db2.t2 (j) VALUES(200);
USE test;
INSERT INTO test.t1 (i) VALUES(101);
INSERT INTO t1 (i) VALUES(102);
INSERT INTO db2.t2 (j) VALUES(201);
USE db2;
INSERT INTO test.t1 (i) VALUES(103);
INSERT INTO db2.t2 (j) VALUES(202);
INSERT INTO t2 (j) VALUES(203);
mysqlbinlog --database=test不会输出前两个INSERT语句,因为没有默认数据库。
它会输出USE test后面的三个INSERT语句,但不会输出USE db2后面的三个INSERT语句。
mysqlbinlog --database=db2不会输出前两个INSERT语句,因为没有默认数据库。
它不会输出USE test 后面的三个INSERT语句,但会输出USE db2后面的三个INSERT语句。
提取行事件显示
在基于行的复制中,默认情况下显示二进制格式。要查看ROW信息,您必须将--verbose或-v选项传递给mysqlbinlog。行事件的二进制格式显示为以###开头的行形式的伪 SQL 语句的注释。您可以看到单个UPDATE语句被重写为每行的UPDATE语句:
shell> mysqlbinlog /data/mysql/binlogs/server1.000001 --start-position=660 --stop-position=1298 --verbose
~
~
# at 660
#170815 13:29:02 server id 200 end_log_pos 722 CRC32 0xe0a2ec74 Table_map: `employees`.`salaries` mapped to number 165
# at 722
#170815 13:29:02 server id 200 end_log_pos 1298 CRC32 0xf0ef8b05 Update_rows: table id 165 flags: STMT_END_F
BINLOG '
HveSWRPIAAAAPgAAANICAAAAAKUAAAAAAAEACWVtcGxveWVlcwAIc2FsYXJpZXMABAMDCgoAAAEB
AHTsouA=
~
~
'/*!*/;
### UPDATE `employees`.`salaries`
### WHERE
### @1=10001
### @2=240468
### @3='1986:06:26'
### @4='1987:06:26'
### SET
### @1=10001
### @2=120234
### @3='1986:06:26'
### @4='1987:06:26'
~
~
### UPDATE `employees`.`salaries`
### WHERE
### @1=10001
### @2=400000
### @3='2017:06:18'
### @4='9999:01:01'
### SET
### @1=10001
### @2=200000
### @3='2017:06:18'
### @4='9999:01:01'
SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /* added by mysqlbinlog */ /*!*/;
DELIMITER ;
# End of log file
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;
如果您只想看到伪 SQL 而不包含二进制行信息,请指定--base64-output="decode-rows"以及--verbose:
shell> sudo mysqlbinlog /data/mysql/binlogs/server1.000001 --start-position=660 --stop-position=1298 --verbose --base64-output="decode-rows"
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=1*/;
/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/;
DELIMITER /*!*/;
# at 660
#170815 13:29:02 server id 200 end_log_pos 722 CRC32 0xe0a2ec74 Table_map: `employees`.`salaries` mapped to number 165
# at 722
#170815 13:29:02 server id 200 end_log_pos 1298 CRC32 0xf0ef8b05 Update_rows: table id 165 flags: STMT_END_F
### UPDATE `employees`.`salaries`
### WHERE
### @1=10001
### @2=240468
### @3='1986:06:26'
### @4='1987:06:26'
### SET
### @1=10001
### @2=120234
### @3='1986:06:26'
### @4='1987:06:26'
~
重写数据库名称
假设您希望将生产服务器上的employees数据库的二进制日志还原为开发服务器上的employees_dev。您可以使用--rewrite-db='from_name->to_name'选项。这将重写所有from_name到to_name的出现。
要转换多个数据库,请多次指定该选项:
shell> sudo mysqlbinlog /data/mysql/binlogs/server1.000001 --start-position=1499 --stop-position=1646 --rewrite-db='employees->employees_dev'
~
# at 1499
#170815 13:29:27 server id 200 end_log_pos 1646 CRC32 0xc73d68fb Query thread_id=8 exec_time=0 error_code=0
use `employees_dev`/*!*/;
~
~
UPDATE salaries SET salary=salary*2 WHERE emp_no<10002
/*!*/;
SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /* added by mysqlbinlog */ /*!*/;
DELIMITER ;
# End of log file
~
您可以看到语句use employees_dev/*!*/;被使用。因此,在还原时,所有更改将应用于employees_dev数据库。
如 MySQL 参考手册中所述:
当与--database选项一起使用时,首先应用--rewrite-db选项,然后使用重写后的数据库名称应用--database选项。在这方面,提供选项的顺序没有任何区别。这意味着,例如,如果使用--rewrite-db='mydb->yourdb' --database=yourdb启动mysqlbinlog,则mydb和yourdb数据库中任何表的所有更新都包含在输出中。
另一方面,如果使用--rewrite-db='mydb->yourdb' --database=mydb启动,则mysqlbinlog根本不输出语句:因为在应用--database选项之前,对mydb的所有更新都首先被重写为对yourdb的更新,因此没有更新与--database=mydb匹配。
禁用二进制日志以进行恢复
在还原二进制日志时,如果您不希望mysqlbinlog进程创建二进制日志,您可以使用--disable-log-bin选项,以便不写入二进制日志:
shell> sudo mysqlbinlog /data/mysql/binlogs/server1.000001 --start-position=660 --stop-position=1298 --disable-log-bin > binlog_restore
您可以看到SQL_LOG_BIN=0被写入binlog还原文件,这将阻止创建 binlogs。
/*!32316 SET @OLD_SQL_LOG_BIN=@@SQL_LOG_BIN, SQL_LOG_BIN=0*/;
显示二进制日志文件中的事件
除了使用mysqlbinlog,您还可以使用SHOW BINLOG EVENTS命令来显示事件。
以下命令将显示server1.000008二进制日志中的事件。如果未指定LIMIT,则显示所有事件:
mysql> SHOW BINLOG EVENTS IN 'server1.000008' LIMIT 10; +----------------+-----+----------------+-----------+-------------+------------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+----------------+-----+----------------+-----------+-------------+------------------------------------------+
| server1.000008 | 4 | Format_desc | 200 | 123 | Server ver: 8.0.3-rc-log, Binlog ver: 4 |
| server1.000008 | 123 | Previous_gtids | 200 | 154 | |
| server1.000008 | 154 | Anonymous_Gtid | 200 | 226 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
| server1.000008 | 226 | Query | 200 | 336 | drop database company /* xid=4134 */ |
| server1.000008 | 336 | Anonymous_Gtid | 200 | 408 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
| server1.000008 | 408 | Query | 200 | 485 | BEGIN |
| server1.000008 | 485 | Table_map | 200 | 549 | table_id: 975 (employees.emp_details) |
| server1.000008 | 549 | Write_rows | 200 | 804 | table_id: 975 flags: STMT_END_F |
| server1.000008 | 804 | Xid | 200 | 835 | COMMIT /* xid=9751 */ |
| server1.000008 | 835 | Anonymous_Gtid | 200 | 907 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
+----------------+-----+----------------+-----------+-------------+------------------------------------------+
10 rows in set (0.00 sec)
您还可以指定位置和偏移量:
mysql> SHOW BINLOG EVENTS IN 'server1.000008' FROM 123 LIMIT 2,1; +----------------+-----+------------+-----------+-------------+--------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+----------------+-----+------------+-----------+-------------+--------------------------------------+
| server1.000008 | 226 | Query | 200 | 336 | drop database company /* xid=4134 */ |
+----------------+-----+------------+-----------+-------------+--------------------------------------+
1 row in set (0.00 sec)
忽略写入二进制日志的数据库
您可以通过在my.cnf中指定--binlog-do-db=db_name选项来选择应写入二进制日志的数据库。要指定多个数据库,必须使用此选项的多个实例。因为数据库名称可以包含逗号,如果提供逗号分隔的列表,该列表将被视为单个数据库的名称。您需要重新启动 MySQL 服务器才能生效。
如何操作...
打开my.cnf并添加以下行:
shell> sudo vi /etc/my.cnf
[mysqld]
binlog_do_db=db1
binlog_do_db=db2
binlog-do-db的行为从基于语句的日志记录更改为基于行的日志记录,就像mysqlbinlog实用程序中的--database选项一样。
在基于语句的日志记录中,只有那些默认数据库(即由USE选择的数据库)写入二进制日志的语句才会被写入。在使用基于语句的日志记录时,使用binlog-do-db选项时要非常小心,因为它的工作方式与您在使用基于语句的日志记录时所期望的方式不同。请参阅参考手册中提到的以下示例。
示例 1
如果服务器是使用--binlog-do-db=sales启动的,并且您发出以下语句,则UPDATE语句不会被记录:
mysql> USE prices;
mysql> UPDATE sales.january SET amount=amount+1000;
这种只检查默认数据库行为的主要原因是,仅从语句本身很难知道是否应该复制它。如果没有必要,仅检查默认数据库而不是所有数据库也更快。
示例 2
如果服务器是使用--binlog-do-db=sales启动的,则即使在设置--binlog-do-db时未包括价格,以下UPDATE语句也会被记录:
mysql> USE sales;
mysql> UPDATE prices.discounts SET percentage = percentage + 10;
当发出UPDATE语句时,因为销售是默认数据库,所以UPDATE被记录在日志中。
在基于行的日志记录中,它受到数据库db_name的限制。只有属于db_name的表的更改才会被记录;默认数据库对此没有影响。
--binlog-do-db在基于语句的日志记录中处理的另一个重要区别与基于行的日志记录有关,这涉及到引用多个数据库的语句。假设服务器是使用--binlog-do-db=db1启动的,并且执行了以下语句:
mysql> USE db1;
mysql> UPDATE db1.table1 SET col1 = 10, db2.table2 SET col2 = 20;
如果您使用基于语句的日志记录,则两个表的更新都将写入二进制日志。但是,使用基于行的格式时,只有table1的更改被记录;table2位于不同的数据库中,因此不会受到UPDATE的影响。
同样,您可以使用--binlog-ignore-db=db_name选项来忽略写入二进制日志的数据库。
有关更多信息,请参阅手册:dev.mysql.com/doc/refman/8.0/en/replication-rules.html。
重新定位二进制日志
由于二进制日志占用更多空间,有时您可能希望更改二进制日志的位置,以下过程有所帮助。仅更改log_bin是不够的,您必须移动所有二进制日志并使用新位置更新索引文件。mysqlbinlogmove实用程序通过自动化这些任务来简化您的工作。
如何操作...
安装 MySQL Utilities 以使用mysqlbinlogmove脚本。有关安装步骤,请参阅第一章,MySQL 8.0 – Installing and Upgrading。
- 停止 MySQL 服务器:
shell> sudo systemctl stop mysql
- 启动
mysqlbinlogmove实用程序。如果要将二进制日志从/data/mysql/binlogs更改为/binlogs,则应使用以下命令。如果您的基本名称不是默认值,则必须通过--bin-log-base name选项提及您的基本名称:
shell> sudo mysqlbinlogmove --bin-log-base name=server1 --binlog-dir=/data/mysql/binlogs /binlogs
#
# Moving bin-log files...
# - server1.000001
# - server1.000002
# - server1.000003
# - server1.000004
# - server1.000005
#
#...done.
#
- 编辑
my.cnf文件并更新log_bin的新位置:
shell> sudo vi /etc/my.cnf
[mysqld]
log_bin=/binlogs
- 启动 MySQL 服务器:
shell> sudo systemctl start mysql
新位置在 AppArmor 或 SELinux 中得到更新。
如果有很多二进制日志,服务器的停机时间会很长。为了避免这种情况,您可以使用--server选项来重新定位除当前正在使用的日志之外的所有二进制日志(具有更高的序列号)。然后停止服务器,使用前面的方法,重新定位最后一个二进制日志,这将会快得多,因为只有一个文件在那里。然后您可以更改my.cnf并启动服务器。
例如:
shell> sudo mysqlbinlogmove --server=root:pass@host1:3306 /new/location
第七章:备份
在本章中,我们将介绍以下内容:
-
使用 mysqldump 进行备份
-
使用 mysqlpump 进行备份
-
使用 mydumper 进行备份
-
使用平面文件进行备份
-
使用 XtraBackup 进行备份
-
备份实例
-
二进制日志备份
介绍
设置数据库后,下一个重要的事情是设置备份。在本章中,您将学习如何设置各种类型的备份。执行备份有两种主要方式。一种是逻辑备份,它将所有数据库、表结构、数据和存储例程导出为一组 SQL 语句,可以再次执行以重新创建数据库的状态。另一种类型是物理备份,它包含系统上数据库用于存储所有数据库实体的所有文件:
-
逻辑备份工具:
mysqldump、mysqlpump和mydumper(未随 MySQL 一起提供) -
物理备份工具:XtraBackup(未随 MySQL 一起提供)和平面文件备份
对于时间点恢复,备份应能够提供备份所涉及的二进制日志位置。这称为一致备份。
强烈建议从从属机器上的 filer 进行备份。
使用 mysqldump 进行备份
mysqldump是一个广泛使用的逻辑备份工具。它提供了各种选项,可以包括或排除数据库,选择要备份的特定数据,仅备份架构而不包括数据,或者仅备份存储例程而不包括其他内容等。
如何做...
mysqldump实用程序随mysql二进制文件一起提供,因此您无需单独安装它。本节涵盖了大多数生产场景。
语法如下:
shell> mysqldump [options]
在选项中,您可以指定用户名、密码和主机名以连接到数据库,如下所示:
--user <user_name> --password <password>
or
-u <user_name> -p<password>
在本章中,每个示例中都没有提到--user和--password,以便读者专注于其他重要选项。
所有数据库的完整备份
可以通过以下方式完成:
shell> mysqldump --all-databases > dump.sql
--all-databases选项备份所有数据库和所有表。>运算符将输出重定向到dump.sql文件。在 MySQL 8 之前,存储过程和事件存储在mysql.proc和mysql.event表中。从 MySQL 8 开始,相应对象的定义存储在数据字典表中,但这些表不会被转储。要在使用--all-databases进行转储时包括存储例程和事件,请使用--routines和--events选项。
包括例程和事件:
shell> mysqldump --all-databases --routines --events > dump.sql
您可以打开dump.sql文件查看其结构。前几行是转储时的会话变量。接下来是CREATE DATABASE语句,然后是USE DATABASE命令。接下来是DROP TABLE IF EXISTS语句,然后是CREATE TABLE;然后我们有实际的INSERT语句插入数据。由于数据存储为 SQL 语句,因此称为逻辑备份。
您会注意到,当您恢复转储时,DROP TABLE语句将在创建表之前清除所有表。
时间点恢复
为了获得时间点恢复,您应该指定--single-transaction和--master-data。
--single-transaction选项通过将事务隔离模式更改为REPEATABLE READ并在进行备份之前执行START TRANSACTION来提供一致的备份。仅在使用事务表(如InnoDB)时才有用,因为它会在不阻止任何应用程序的情况下转储发出START TRANSACTION时数据库的一致状态。
--master-data选项将服务器的二进制日志坐标打印到dump文件中。如果--master-data=2,它将作为注释打印。这还使用FLUSH TABLES WITH READ LOCK语句来获取二进制日志的快照。正如在第五章“事务”中所解释的那样,在存在任何长时间运行的事务时,这可能非常危险:
shell> mysqldump --all-databases --routines --events --single-transaction --master-data > dump.sql
转储主二进制坐标
备份始终在从服务器上进行。要获取备份时主服务器的二进制日志坐标,可以使用--dump-slave选项。如果要从主服务器获取二进制日志备份,请使用此选项。否则,请使用--master-data选项:
shell> mysqldump --all-databases --routines --events --single-transaction --dump-slave > dump.sql
输出将如下所示:
--
-- Position to start replication or point-in-time recovery from (the master of this slave)
--
CHANGE MASTER TO MASTER_LOG_FILE='centos7-bin.000001', MASTER_LOG_POS=463;
特定数据库和表
要仅备份特定数据库,请执行以下操作:
shell> mysqldump --databases employees > employees_backup.sql
要仅备份特定表,请执行以下操作:
shell> mysqldump --databases employees --tables employees > employees_backup.sql
忽略表
要忽略某些表,可以使用--ignore-table=database.table选项。要指定要忽略的多个表,请多次使用该指令:
shell> mysqldump --databases employees --ignore-table=employees.salary > employees_backup.sql
特定行
mysqldump可帮助您过滤备份的数据。假设您要备份 2000 年后加入的员工的备份:
shell> mysqldump --databases employees --tables employees --databases employees --tables employees --where="hire_date>'2000-01-01'" > employees_after_2000.sql
您可以使用LIMIT子句来限制结果:
shell> mysqldump --databases employees --tables employees --databases employees --tables employees --where="hire_date >= '2000-01-01' LIMIT 10" > employees_after_2000_limit_10.sql
从远程服务器备份
有时,您可能无法访问数据库服务器的 SSH(例如云实例,如 Amazon RDS)。在这种情况下,您可以使用mysqldump从远程服务器备份到本地服务器。为此,您需要使用--hostname选项提到hostname。确保用户具有适当的权限以连接和执行备份:
shell> mysqldump --all-databases --routines --events --triggers --hostname <remote_hostname> > dump.sql
备份以重建具有不同模式的另一个服务器
可能会出现这样的情况,您希望在另一台服务器上具有不同的模式。在这种情况下,您必须转储和还原模式,根据需要更改模式,然后转储和还原数据。根据您拥有的数据量,更改带有数据的模式可能需要很长时间。请注意,此方法仅在修改后的模式与插入兼容时才有效。修改后的表可以有额外的列,但应该具有原始表中的所有列。
仅模式,无数据
您可以使用--no-data仅转储模式:
shell> mysqldump --all-databases --routines --events --triggers --no-data > schema.sql
仅数据,无模式
您可以使用以下选项仅获取数据转储,而不包括模式。
--complete-insert将在INSERT语句中打印列名,这将在修改后的表中有额外列时有所帮助:
shell> mysqldump --all-databases --no-create-db --no-create-info --complete-insert > data.sql
备份以与其他服务器合并数据
您可以以任何一种方式备份以替换旧数据或在冲突发生时保留旧数据。
使用新数据替换
假设您希望将生产数据库中的数据还原到已经存在一些数据的开发机器。如果要将生产中的数据与开发中的数据合并,可以使用--replace选项,该选项将使用REPLACE INTO语句而不是INSERT语句。您还应该包括--skip-add-drop-table选项,该选项不会将DROP TABLE语句写入dump文件。如果表的数量和结构相同,还可以包括--no-create-info选项,该选项将跳过dump文件中的CREATE TABLE语句:
shell> mysqldump --databases employees --skip-add-drop-table --no-create-info --replace > to_development.sql
如果生产环境中有一些额外的表,那么在还原时上述转储将失败,因为开发服务器上不存在该表。在这种情况下,您不应添加--no-create-info选项,并在还原时使用force选项。否则,还原将在CREATE TABLE时失败,说表已经存在。不幸的是,mysqldump没有提供CREATE TABLE IF NOT EXISTS选项。
忽略数据
您可以在写入dump文件时使用INSERT IGNORE语句代替REPLACE。这将保留服务器上的现有数据并插入新数据。
使用 mysqlpump 进行备份
mysqlpump是一个与mysqldump非常相似的程序,具有一些额外的功能。
如何做...
有很多方法可以做到这一点。让我们详细看看每种方法。
并行处理
通过指定线程数(基于 CPU 数量)可以加快转储过程。例如,使用八个线程进行完整备份:
shell> mysqlpump --default-parallelism=8 > full_backup.sql
您甚至可以为每个数据库指定线程数。在我们的情况下,employees数据库与company数据库相比非常大。因此,您可以为employees生成四个线程,并为company数据库生成两个线程:
shell> mysqlpump -u root --password --parallel-schemas=4:employees --default-parallelism=2 > full_backup.sql
Dump progress: 0/6 tables, 250/331145 rows
Dump progress: 0/34 tables, 494484/3954504 rows
Dump progress: 0/42 tables, 1035414/3954504 rows
Dump progress: 0/45 tables, 1586055/3958016 rows
Dump progress: 0/45 tables, 2208364/3958016 rows
Dump progress: 0/45 tables, 2846864/3958016 rows
Dump progress: 0/45 tables, 3594614/3958016 rows
Dump completed in 6957
另一个例子是将线程分配给db1和db2的三个线程,db3和db4的两个线程,以及其余数据库的四个线程:
shell> mysqlpump --parallel-schemas=3:db1,db2 --parallel-schemas=2:db3,db4 --default-parallelism=4 > full_backup.sql
您会注意到有一个进度条,可以帮助您估计时间。
使用正则表达式排除/包含数据库对象
备份以prod结尾的所有数据库:
shell> mysqlpump --include-databases=%prod --result-file=db_prod.sql
假设某些数据库中有一些测试表,您希望将它们从备份中排除;您可以使用--exclude-tables选项指定,该选项将在所有数据库中排除名称为test的表:
shell> mysqlpump --exclude-tables=test --result-file=backup_excluding_test.sql
每个包含和排除选项的值都是适当对象类型的名称的逗号分隔列表。通配符字符允许在对象名称中使用:
-
%匹配零个或多个字符的任何序列 -
_匹配任何单个字符
除了数据库和表之外,您还可以包括或排除触发器、例程、事件和用户,例如,--include-routines、--include-events和--exclude-triggers。
要了解更多关于包含和排除选项的信息,请参阅dev.mysql.com/doc/refman/8.0/en/mysqlpump.html#mysqlpump-filtering。
备份用户
在mysqldump中,您将不会在CREATE USER或GRANT语句中获得用户的备份;相反,您必须备份mysql.user表。使用mysqlpump,您可以将用户帐户作为帐户管理语句(CREATE USER和GRANT)而不是插入到mysql系统数据库中:
shell> mysqlpump --exclude-databases=% --users > users_backup.sql
您还可以通过指定--exclude-users选项来排除一些用户:
shell> mysqlpump --exclude-databases=% --exclude-users=root --users > users_backup.sql
压缩备份
您可以压缩备份以减少磁盘空间和网络带宽。您可以使用--compress-output=lz4或--compress-output=zlib。
请注意,您应该具有适当的解压缩实用程序:
shell> mysqlpump -u root -pxxxx --compress-output=lz4 > dump.lz4
要解压缩,请执行此操作:
shell> lz4_decompress dump.lz4 dump.sql
使用zlib执行此操作:
shell> mysqlpump -u root -pxxxx --compress-output=zlib > dump.zlib
要解压缩,请执行此操作:
shell> zlib_decompress dump.zlib dump.sql
更快的重新加载
您会注意到在输出中,从CREATE TABLE语句中省略了次要索引。这将加快恢复过程。索引将使用ALTER TABLE语句在INSERT的末尾添加。
索引将在第十三章,性能调整中进行讨论。
以前,可以在mysql系统数据库中转储所有表。从 MySQL 8 开始,mysqldump和mysqlpump仅转储该数据库中的非“数据字典”表。
使用 mydumper 进行备份
mydumper是一个类似于mysqlpump的逻辑备份工具。
mydumper相对于mysqldump具有以下优点:
-
并行性(因此,速度)和性能(避免昂贵的字符集转换例程,并且整体代码效率高)。
-
一致性。它在所有线程中保持快照,提供准确的主从日志位置等。
mysqlpump不能保证一致性。 -
更容易管理输出(为表和转储的元数据分别创建文件,并且很容易查看/解析数据)。
mysqlpump将所有内容写入一个文件,这限制了加载选择性数据库对象的选项。 -
使用正则表达式包含和排除数据库对象。
-
终止长时间运行的事务以阻止备份和所有后续查询的选项。
mydumper是一个开源备份工具,您需要单独安装。本节将介绍 Debian 和 Red Hat 系统上的安装步骤以及mydumper的使用。
如何做...
让我们从安装开始,然后我们将学习与备份相关的许多事项,这些事项在本食谱中列出的每个子部分中都有。
安装
安装先决条件:
在 Ubuntu/Debain 上:
shell> sudo apt-get install libglib2.0-dev libmysqlclient-dev zlib1g-dev libpcre3-dev cmake git
在 Red Hat/CentOS/Fedora 上:
shell> yum install glib2-devel mysql-devel zlib-devel pcre-devel cmake gcc-c++ git
shell> cd /opt
shell> git clone https://github.com/maxbube/mydumper.git
shell> cd mydumper
shell> cmake .
shell> make
Scanning dependencies of target mydumper
[ 25%] Building C object CMakeFiles/mydumper.dir/mydumper.c.o
[ 50%] Building C object CMakeFiles/mydumper.dir/server_detect.c.o
[ 75%] Building C object CMakeFiles/mydumper.dir/g_unix_signal.c.o
shell> make install
[ 75%] Built target mydumper
[100%] Built target myloader
Linking C executable CMakeFiles/CMakeRelink.dir/mydumper
Linking C executable CMakeFiles/CMakeRelink.dir/myloader
Install the project...
-- Install configuration: ""
-- Installing: /usr/local/bin/mydumper
-- Installing: /usr/local/bin/myloader
或者,您可以使用 YUM 或 APT,在此处找到发布版本:github.com/maxbube/mydumper/releases
#YUM
shell> sudo yum install -y "https://github.com/maxbube/mydumper/releases/download/v0.9.3/mydumper-0.9.3-41.el7.x86_64.rpm"
#APT
shell> wget "https://github.com/maxbube/mydumper/releases/download/v0.9.3/mydumper_0.9.3-41.jessie_amd64.deb"
shell> sudo dpkg -i mydumper_0.9.3-41.jessie_amd64.deb
shell> sudo apt-get install -f
完整备份
以下命令将所有数据库备份到/backups文件夹中:
shell> mydumper -u root --password=<password> --outputdir /backups
在/backups文件夹中创建了多个文件。每个数据库都有其CREATE DATABASE语句,格式为<database_name>-schema-create.sql,每个表都有自己的模式和数据文件。模式文件存储为<database_name>.<table>-schema.sql,数据文件存储为<database_name>.<table>.sql。
视图存储为<database_name>.<table>-schema-view.sql。存储的例程,触发器和事件存储为<database_name>-schema-post.sql(如果目录未创建,请使用sudo mkdir –pv /backups):
shell> ls -lhtr /backups/company*
-rw-r--r-- 1 root root 69 Aug 13 10:11 /backups/company-schema-create.sql
-rw-r--r-- 1 root root 180 Aug 13 10:11 /backups/company.payments.sql
-rw-r--r-- 1 root root 239 Aug 13 10:11 /backups/company.new_customers.sql
-rw-r--r-- 1 root root 238 Aug 13 10:11 /backups/company.payments-schema.sql
-rw-r--r-- 1 root root 303 Aug 13 10:11 /backups/company.new_customers-schema.sql
-rw-r--r-- 1 root root 324 Aug 13 10:11 /backups/company.customers-schema.sql
如果有任何超过 60 秒的查询,mydumper将以以下错误失败:
** (mydumper:18754): CRITICAL **: There are queries in PROCESSLIST running longer than 60s, aborting dump,
use --long-query-guard to change the guard value, kill queries (--kill-long-queries) or use different server for dump
为了避免这种情况,您可以传递--kill-long-queries选项或将--long-query-guard设置为更高的值。
--kill-long-queries选项会杀死所有大于 60 秒或由--long-query-guard设置的值的查询。请注意,由于错误(bugs.launchpad.net/mydumper/+bug/1713201),--kill-long-queries也会杀死复制线程:
shell> sudo mydumper --kill-long-queries --outputdir /backups** (mydumper:18915): WARNING **: Using trx_consistency_only, binlog coordinates will not be accurate if you are writing to non transactional tables.
** (mydumper:18915): WARNING **: Killed a query that was running for 368s
一致备份
备份目录中的元数据文件包含一致备份的二进制日志坐标。
在主服务器上,它捕获二进制日志位置:
shell> sudo cat /backups/metadata
Started dump at: 2017-08-20 12:44:09
SHOW MASTER STATUS:
Log: server1.000008
Pos: 154
GTID:
在从服务器上,它捕获主服务器和从服务器的二进制日志位置:
shell> cat /backups/metadataStarted dump at: 2017-08-26 06:26:19
SHOW MASTER STATUS:
Log: server1.000012
Pos: 154
GTID:
SHOW SLAVE STATUS:
Host: 35.186.158.188
Log: master-bin.000013
Pos: 4633
GTID:
Finished dump at: 2017-08-26 06:26:24
单表备份
以下命令将employees数据库的employees表备份到/backups目录中:
shell> mydumper -u root --password=<password> -B employees -T employees --triggers --events --routines --outputdir /backups/employee_table
shell> ls -lhtr /backups/employee_table/
total 17M
-rw-r--r-- 1 root root 71 Aug 13 10:35 employees-schema-create.sql
-rw-r--r-- 1 root root 397 Aug 13 10:35 employees.employees-schema.sql
-rw-r--r-- 1 root root 3.4K Aug 13 10:35 employees-schema-post.sql
-rw-r--r-- 1 root root 75 Aug 13 10:35 metadata
-rw-r--r-- 1 root root 17M Aug 13 10:35 employees.employees.sql
文件的约定如下:
-
employees-schema-create.sql包含CREATE DATABASE语句 -
employees.employees-schema.sql包含CREATE TABLE语句 -
employees-schema-post.sql包含ROUTINES,TRIGGERS和EVENTS -
employees.employees.sql包含INSERT语句形式的实际数据
使用正则表达式备份特定数据库
您可以使用regex选项包括/排除特定数据库。以下命令将从备份中排除mysql和test数据库:
shell> mydumper -u root --password=<password> --regex '^(?!(mysql|test))' --outputdir /backups/specific_dbs
使用 mydumper 备份大表
为了加快大表的转储和恢复速度,您可以将其分成小块。块大小可以通过它包含的行数来指定,每个块将被写入单独的文件中:
shell> mydumper -u root --password=<password> -B employees -T employees --triggers --events --routines --rows=10000 -t 8 --trx-consistency-only --outputdir /backups/employee_table_chunks
-
-t:指定线程数 -
--trx-consistency-only:如果只使用事务表,例如InnoDB,使用此选项将最小化锁定 -
--rows:将表拆分为此行数的块
对于每个块,将创建一个文件,格式为<database_name>.<table_name>.<number>.sql;数字用五个零填充:
shell> ls -lhr /backups/employee_table_chunks
total 17M
-rw-r--r-- 1 root root 71 Aug 13 10:45 employees-schema-create.sql
-rw-r--r-- 1 root root 75 Aug 13 10:45 metadata
-rw-r--r-- 1 root root 397 Aug 13 10:45 employees.employees-schema.sql
-rw-r--r-- 1 root root 3.4K Aug 13 10:45 employees-schema-post.sql
-rw-r--r-- 1 root root 633K Aug 13 10:45 employees.employees.00008.sql
-rw-r--r-- 1 root root 634K Aug 13 10:45 employees.employees.00002.sql
-rw-r--r-- 1 root root 1.3M Aug 13 10:45 employees.employees.00006.sql
-rw-r--r-- 1 root root 1.9M Aug 13 10:45 employees.employees.00004.sql
-rw-r--r-- 1 root root 2.5M Aug 13 10:45 employees.employees.00000.sql
-rw-r--r-- 1 root root 2.5M Aug 13 10:45 employees.employees.00001.sql
-rw-r--r-- 1 root root 2.6M Aug 13 10:45 employees.employees.00005.sql
-rw-r--r-- 1 root root 2.6M Aug 13 10:45 employees.employees.00009.sql
-rw-r--r-- 1 root root 2.6M Aug 13 10:45 employees.employees.00010.sql
非阻塞备份
为了提供一致的备份,mydumper通过执行FLUSH TABLES WITH READ LOCK获取GLOBAL LOCK。
如果有任何长时间运行的事务(在第五章“事务”中解释)使用FLUSH TABLES WITH READ LOCK是多么危险。为了避免这种情况,您可以传递--kill-long-queries选项来杀死阻塞查询,而不是中止mydumper。
-
--trx-consistency-only:这相当于mysqldump的--single-transaction,但带有binlog位置。显然,此位置仅适用于事务表。使用此选项的优点之一是全局读锁仅用于线程协调,因此一旦事务开始,它就会被释放。 -
--use-savepoints 减少元数据锁定问题(需要
SUPER权限)。
压缩备份
您可以指定--compress选项来压缩备份:
shell> mydumper -u root --password=<password> -B employees -T employees -t 8 --trx-consistency-only --compress --outputdir /backups/employees_compress
shell> ls -lhtr /backups/employees_compress
total 5.3M
-rw-r--r-- 1 root root 91 Aug 13 11:01 employees-schema-create.sql.gz
-rw-r--r-- 1 root root 263 Aug 13 11:01 employees.employees-schema.sql.gz
-rw-r--r-- 1 root root 75 Aug 13 11:01 metadata
-rw-r--r-- 1 root root 5.3M Aug 13 11:01 employees.employees.sql.gz
仅备份数据
您可以使用--no-schemas选项跳过模式并进行仅数据备份:
shell> mydumper -u root --password=<password> -B employees -T employees -t 8 --no-schemas --compress --trx-consistency-only --outputdir /backups/employees_data
使用平面文件进行备份
这是一种物理备份方法,通过直接复制data directory中的文件来进行备份。由于在复制文件时会写入新数据,因此备份将是不一致的,无法使用。为了避免这种情况,您必须关闭 MySQL,复制文件,然后启动 MySQL。这种方法不适用于日常备份,但在维护窗口期间进行升级、降级或进行主机交换时非常合适。
如何做...
- 关闭 MySQL 服务器:
shell> sudo service mysqld stop
- 将文件复制到
data directory(您的目录可能不同):
shell> sudo rsync -av /data/mysql /backups
or do rsync over ssh to remote server
shell> rsync -e ssh -az /data/mysql/ backup_user@remote_server:/backups
- 启动 MySQL 服务器:
shell> sudo service mysqld start
使用 XtraBackup 进行备份
XtraBackup 是由 Percona 提供的开源备份软件。它在不关闭服务器的情况下复制平面文件,但为了避免不一致性,它使用重做日志文件。许多公司将其作为标准备份工具广泛使用。其优点是与逻辑备份工具相比非常快,恢复速度也非常快。
这是 Percona XtraBackup 的工作原理(摘自 Percona XtraBackup 文档):
-
它会复制您的
InnoDB数据文件,这将导致数据在内部不一致;然后它会对文件执行崩溃恢复,使它们成为一致的可用数据库。 -
这是因为
InnoDB维护着一个重做日志,也称为事务日志。这包含了对InnoDB数据的每一次更改的记录。当InnoDB启动时,它会检查数据文件和事务日志,并执行两个步骤。它将已提交的事务日志条目应用于数据文件,并对修改数据但未提交的任何事务执行撤消操作。 -
Percona XtraBackup 在启动时通过记住日志序列号(LSN)来工作,然后复制数据文件。这需要一些时间,因此如果文件在更改,则它们反映了数据库在不同时间点的状态。同时,Percona XtraBackup 运行一个后台进程,监视事务日志文件,并从中复制更改。Percona XtraBackup 需要不断执行此操作,因为事务日志是以循环方式写入的,并且一段时间后可以被重用。自执行以来,Percona XtraBackup 需要事务日志记录,以获取自开始执行以来对数据文件的每次更改。
如何做...
在撰写本文时,Percona XtraBackup 不支持 MySQL 8。最终,Percona 将发布支持 MySQL 8 的新版本 XtraBackup;因此只涵盖了安装部分。
安装
安装步骤在以下部分中。
在 CentOS/Red Hat/Fedora 上
- 安装
mysql-community-libs-compat:
shell> sudo yum install -y mysql-community-libs-compat
- 安装 Percona 存储库:
shell> sudo yum install http://www.percona.com/downloads/percona-release/redhat/0.1-4/percona-release-0.1-4.noarch.rpm
您应该看到以下输出:
Retrieving http://www.percona.com/downloads/percona-release/redhat/0.1-4/percona-release-0.1-4.noarch.rpm
Preparing... ########################################### [100%]
1:percona-release ########################################### [100%]
- 测试存储库:
shell> yum list | grep xtrabackup
holland-xtrabackup.noarch 1.0.14-3.el7 epel
percona-xtrabackup.x86_64 2.3.9-1.el7 percona-release-x86_64
percona-xtrabackup-22.x86_64 2.2.13-1.el7 percona-release-x86_64
percona-xtrabackup-22-debuginfo.x86_64 2.2.13-1.el7 percona-release-x86_64
percona-xtrabackup-24.x86_64 2.4.8-1.el7 percona-release-x86_64
percona-xtrabackup-24-debuginfo.x86_64 2.4.8-1.el7 percona-release-x86_64
percona-xtrabackup-debuginfo.x86_64 2.3.9-1.el7 percona-release-x86_64
percona-xtrabackup-test.x86_64 2.3.9-1.el7 percona-release-x86_64
percona-xtrabackup-test-22.x86_64 2.2.13-1.el7 percona-release-x86_64
percona-xtrabackup-test-24.x86_64 2.4.8-1.el7 percona-release-x86_64
- 安装 XtraBackup:
shell> sudo yum install percona-xtrabackup-24
在 Debian/Ubuntu 上
- 从 Percona 获取存储库软件包:
shell> wget https://repo.percona.com/apt/percona-release_0.1-4.$(lsb_release -sc)_all.deb
- 使用
dpkg安装下载的软件包。为此,请以root或sudo身份运行以下命令:
shell> sudo dpkg -i percona-release_0.1-4.$(lsb_release -sc)_all.deb
安装此软件包后,应该添加 Percona 存储库。您可以在/etc/apt/sources.list.d/percona-release.list文件中检查存储库设置。
- 记得更新本地缓存:
shell> sudo apt-get update
- 之后,您可以安装软件包:
shell> sudo apt-get install percona-xtrabackup-24
锁定备份实例
从 MySQL 8 开始,您可以锁定实例以进行备份,这将允许在线备份期间进行 DML,并阻止可能导致不一致快照的所有操作。
如何做...
在开始备份之前,请锁定实例以进行备份:
mysql> LOCK INSTANCE FOR BACKUP;
执行备份,完成后解锁实例:
mysql> UNLOCK INSTANCE;
二进制日志备份
您知道二进制日志对于时点恢复是必需的。在本节中,您将了解如何备份二进制日志。该过程将二进制日志从数据库服务器流式传输到远程备份服务器。您可以从从服务器或主服务器中获取二进制日志备份。如果您从主服务器获取二进制日志备份,并且从从服务器获取实际备份,您应该使用--dump-slave来获取相应的主日志位置。如果您使用mydumper或 XtraBackup,它会提供主服务器和从服务器的二进制日志位置。
如何做到这一点...
- 在服务器上创建一个复制用户。创建一个强密码:
mysql> GRANT REPLICATION SLAVE ON *.* TO 'binlog_user'@'%' IDENTIFIED BY 'binlog_pass';Query OK, 0 rows affected, 1 warning (0.03 sec)
- 检查服务器上的二进制日志:
mysql> SHOW BINARY LOGS;+----------------+-----------+
| Log_name | File_size |
+----------------+-----------+
| server1.000008 | 2451 |
| server1.000009 | 199 |
| server1.000010 | 1120 |
| server1.000011 | 471 |
| server1.000012 | 154 |
+----------------+-----------+
5 rows in set (0.00 sec)
您可以在服务器上找到第一个可用的二进制日志;从这里,您可以开始备份。在这种情况下,它是server1.000008。
- 登录到备份服务器并执行以下命令。这将从 MySQL 服务器复制二进制日志到备份服务器。您可以开始使用
nohup或disown:
shell> mysqlbinlog -u <user> -p<pass> -h <server> --read-from-remote-server --stop-never
--to-last-log --raw server1.000008 &
shell> disown -a
- 验证二进制日志是否已备份:
shell> ls -lhtr server1.0000*-rw-r-----. 1 mysql mysql 2.4K Aug 25 12:22 server1.000008
-rw-r-----. 1 mysql mysql 199 Aug 25 12:22 server1.000009
-rw-r-----. 1 mysql mysql 1.1K Aug 25 12:22 server1.000010
-rw-r-----. 1 mysql mysql 471 Aug 25 12:22 server1.000011
-rw-r-----. 1 mysql mysql 154 Aug 25 12:22 server1.000012
第八章:恢复数据
在本章中,我们将介绍以下配方:
-
从 mysqldump 和 mysqlpump 中恢复
-
使用 myloader 从 mydumper 恢复
-
从平面文件备份中恢复
-
执行时间点恢复
介绍
在本章中,您将了解各种备份恢复方法。假设备份和二进制日志在服务器上可用。
从 mysqldump 和 mysqlpump 中恢复
逻辑备份工具mysqldump和mysqlpump将数据写入单个文件。
如何做...
登录到备份可用的服务器:
shell> cat /backups/full_backup.sql | mysql -u <user> -p
or
shell> mysql -u <user> -p < /backups/full_backup.sql
要在远程服务器上恢复,可以提到-h <hostname>选项:
shell> cat /backups/full_backup.sql | mysql -u <user> -p -h <remote_hostname>
在恢复备份时,备份语句将记录到二进制日志中,这可能会减慢恢复过程。如果不希望恢复过程写入二进制日志,可以在会话级别使用SET SQL_LOG_BIN=0;选项禁用它:
shell> (echo "SET SQL_LOG_BIN=0;";cat /backups/full_backup.sql) | mysql -u <user> -p -h <remote_hostname>
或使用:
mysql> SET SQL_LOG_BIN=0; SOURCE full_backup.sql
还有更多...
-
由于备份恢复需要很长时间,建议在屏幕会话内启动恢复过程,以便即使失去与服务器的连接,恢复也将继续。
-
有时,在恢复过程中可能会出现故障。如果将
--force选项传递给 MySQL,恢复将继续:
shell> (echo "SET SQL_LOG_BIN=0;";cat /backups/full_backup.sql) | mysql -u <user> -p -h <remote_hostname> -f
使用 myloader 从 mydumper 恢复
myloader是用于使用mydumper获取的备份的多线程恢复的工具。 myloader与mydumper一起提供,您无需单独安装它。在本节中,您将学习恢复备份的各种方法。
如何做...
myloader的常见选项是要连接的 MySQL 服务器的主机名(默认值为localhost),用户名,密码和端口。
恢复完整数据库
shell> myloader --directory=/backups --user=<user> --password=<password> --queries-per-transaction=5000 --threads=8 --compress-protocol --overwrite-tables
选项解释如下:
-
--overwrite-tables:此选项如果表已经存在,则删除表 -
--compress-protocol:此选项在 MySQL 连接上使用压缩 -
--threads:此选项指定要使用的线程数;默认值为4 -
--queries-per-transaction:此选项指定每个事务的查询数;默认值为1000 -
--directory:指定要导入的转储目录
恢复单个数据库
您可以指定--source-db <db_name>仅恢复单个数据库。
假设您要恢复company数据库:
shell> myloader --directory=/backups --queries-per-transaction=5000 --threads=6 --compress-protocol --user=<user> --password=<password> --source-db company --overwrite-tables
恢复单个表
mydumper将每个表的备份写入单独的.sql文件。您可以拾取.sql文件并恢复:
shell> mysql -u <user> -p<password> -h <hostname> company -A -f < company.payments.sql
如果表被分成多个块,可以将所有块和与表相关的信息复制到一个目录中并指定位置。
复制所需的文件:
shell> sudo cp /backups/employee_table_chunks/employees.employees.* \
/backups/employee_table_chunks/employees.employees-schema.sql \
/backups/employee_table_chunks/employees-schema-create.sql \
/backups/employee_table_chunks/metadata \
/backups/single_table/
使用myloader进行加载;它将自动检测块并加载它们:
shell> myloader --directory=/backups/single_table/ --queries-per-transaction=50000 --threads=6 --compress-protocol --overwrite-tables
从平面文件备份中恢复
从平面文件恢复需要停止 MySQL 服务器,替换所有文件,更改权限,然后启动 MySQL。
如何做...
- 停止 MySQL 服务器:
shell> sudo systemctl stop mysql
- 将文件移动到
数据目录:
shell> sudo mv /backup/mysql /var/lib
- 更改所有权为
mysql:
shell> sudo chown -R mysql:mysql /var/lib/mysql
- 启动 MySQL:
shell> sudo systemctl start mysql
为了最小化停机时间,如果磁盘上有足够的空间,可以将备份复制到/var/lib/mysql2。然后停止 MySQL,重命名目录,然后启动服务器:
shell> sudo mv /backup/mysql /var/lib/mysql2
shell> sudo systemctl stop mysql
shell> sudo mv /var/lib/mysql2 /var/lib/mysql
shell> sudo chown -R mysql:mysql /var/lib/mysql
shell> sudo systemctl start mysql
执行时间点恢复
一旦完整备份恢复完成,您需要恢复二进制日志以进行时间点恢复。备份提供了直到备份可用的二进制日志坐标。
如第七章中所解释的,备份,在锁定备份实例部分,您应该根据--dump-slave或--master-data选项从正确的服务器选择二进制日志备份mysqldump。
如何做...
让我们深入了解如何做。这里有很多东西要学习。
mysqldump 或 mysqlpump
二进制日志信息存储在 SQL 文件中,作为基于您传递给mysqldump/mysqlpump的选项的CHANGE MASTER TO命令。
- 如果您使用了
--master-data,您应该使用从服务器的二进制日志:
shell> head -30 /backups/dump.sql
-- MySQL dump 10.13 Distrib 8.0.3-rc, for Linux (x86_64)
--
-- Host: localhost Database:
-- ------------------------------------------------------
-- Server version 8.0.3-rc-log
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!50606 SET @OLD_INNODB_STATS_AUTO_RECALC=@@INNODB_STATS_AUTO_RECALC */;
/*!50606 SET GLOBAL INNODB_STATS_AUTO_RECALC=OFF */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Position to start replication or point-in-time recovery from --
CHANGE MASTER TO MASTER_LOG_FILE='server1.000008', MASTER_LOG_POS=154;
在这种情况下,您应该从从服务器上位置154的server1.000008文件开始恢复。
shell> mysqlbinlog --start-position=154 --disable-log-bin /backups/binlogs/server1.000008 | mysql -u<user> -p -h <host> -f
- 如果您使用了
--dump-slave,您应该使用主服务器的二进制日志:
--
-- Position to start replication or point-in-time recovery from (the master of this slave)
--
CHANGE MASTER TO MASTER_LOG_FILE='centos7-bin.000001', MASTER_LOG_POS=463;
在这种情况下,您应该从主服务器上位置463的centos7-bin.000001文件开始恢复。
shell> mysqlbinlog --start-position=463 --disable-log-bin /backups/binlogs/centos7-bin.000001 | mysql -u<user> -p -h <host> -f
mydumper
二进制日志信息可在元数据中找到。
shell> sudo cat /backups/metadata Started dump at: 2017-08-26 06:26:19
SHOW MASTER STATUS:
Log: server1.000012
Pos: 154
</span> GTID:
SHOW SLAVE STATUS:
Host: 35.186.158.188
Log: centos7-bin.000001
Pos: 463
GTID:
Finished dump at: 2017-08-26 06:26:24
如果您已经从从服务器上获取了二进制日志备份,您应该从位置154的server1.000012文件开始恢复(SHOW MASTER STATUS):
shell> mysqlbinlog --start-position=154 --disable-log-bin /backups/binlogs/server1.000012 | mysql -u<user> -p -h <host> -f
如果您从主服务器上有二进制日志备份,您应该从位置463的centos7-bin.000001文件开始恢复(SHOW SLAVE STATUS):
shell> mysqlbinlog --start-position=463 --disable-log-bin /backups/binlogs/centos7-bin.000001 | mysql -u<user> -p -h <host> -f
第九章:复制
在这一章中,我们将涵盖以下内容:
-
设置复制
-
设置主-主复制
-
设置多源复制
-
设置复制过滤器
-
将从主-从复制切换到链式复制
-
将从链式复制切换到主-从复制
-
设置延迟复制
-
设置 GTID 复制
-
设置半同步复制
介绍
如第六章中所解释的,二进制日志,复制使得来自一个 MySQL 数据库服务器(主服务器)的数据被复制到一个或多个 MySQL 数据库服务器(从服务器)。复制默认是异步的;从服务器不需要永久连接以接收来自主服务器的更新。您可以配置复制所有数据库、选定的数据库,甚至是数据库中的选定表。
在这一章中,您将学习如何设置传统复制;复制选定的数据库和表;以及设置多源复制、链式复制、延迟复制和半同步复制。
在高层次上,复制的工作原理是这样的:在一个服务器上执行的所有 DDL 和 DML 语句(主服务器)都被记录到二进制日志中,这些日志被连接到它的服务器(称为从服务器)拉取。二进制日志简单地被复制到从服务器并保存为中继日志。这个过程由一个叫做IO 线程的线程来处理。还有一个叫做SQL 线程的线程,按顺序执行中继日志中的语句。
复制的工作原理在这篇博客中得到了很清楚的解释:
www.percona.com/blog/2013/01/09/how-does-mysql-replication-really-work/
复制的优点(摘自手册,网址为dev.mysql.com/doc/refman/8.0/en/replication.html):
-
扩展解决方案:将负载分散在多个从服务器上以提高性能。在这种环境中,所有的写入和更新必须在主服务器上进行。然而,读取可以在一个或多个从服务器上进行。这种模式可以提高写入的性能(因为主服务器专门用于更新),同时大大提高了在越来越多的从服务器上的读取速度。
-
数据安全:因为数据被复制到从服务器并且从服务器可以暂停复制过程,所以可以在从服务器上运行备份服务而不会破坏相应的主服务器数据。
-
分析:可以在主服务器上创建实时数据,而信息的分析可以在从服务器上进行,而不会影响主服务器的性能。
-
远程数据分发:您可以使用复制在远程站点创建数据的本地副本,而无需永久访问主服务器。
设置复制
有许多复制拓扑结构。其中一些是传统的主-从复制、链式复制、主-主复制、多源复制等。
传统复制 包括一个主服务器和多个从服务器。

链式复制 意味着一个服务器从另一个服务器复制,而另一个服务器又从另一个服务器复制。中间服务器被称为中继主服务器(主服务器 ---> 中继主服务器 ---> 从服务器)。

这主要用于当您想在两个数据中心之间设置复制时。主服务器和其从服务器将位于一个数据中心。次要主服务器(中继)从另一个数据中心的主服务器进行复制。另一个数据中心的所有从服务器都从次要主服务器进行复制。
主-主复制:在这种拓扑结构中,两个主服务器都接受写入并在彼此之间进行复制。

多源复制:在这种拓扑结构中,一个从服务器将从多个主服务器而不是一个主服务器进行复制。

如果要设置链式复制,可以按照此处提到的相同步骤进行,将主服务器替换为中继主服务器。
如何做...
在本节中,解释了单个从服务器的设置。相同的原则可以应用于设置链式复制。通常在设置另一个从服务器时,备份是从从服务器中获取的。
大纲:
-
在主服务器上启用二进制日志记录
-
在主服务器上创建一个复制用户
-
在从服务器上设置唯一的
server_id -
从主服务器备份
-
在从服务器上恢复备份
-
执行
CHANGE MASTER TO命令 -
开始复制
步骤:
-
在主服务器上:在主服务器上启用二进制日志记录并设置
SERVER_ID。参考第六章,二进制日志记录,了解如何启用二进制日志记录。 -
在主服务器上:创建一个复制用户。从服务器使用这个帐户连接到主服务器:
mysql> GRANT REPLICATION SLAVE ON *.* TO 'binlog_user'@'%' IDENTIFIED BY 'binlog_P@ss12';Query OK, 0 rows affected, 1 warning (0.00 sec)
- 在从服务器上:设置唯一的
SERVER_ID选项(它应该与主服务器上设置的不同):
mysql> SET @@GLOBAL.SERVER_ID = 32;
- 在从服务器上:通过远程连接从主服务器备份。您可以使用
mysqldump或mydumper。不能使用mysqlpump,因为二进制日志位置不一致。
mysqldump:
shell> mysqldump -h <master_host> -u backup_user --password=<pass> --all-databases --routines --events --single-transaction --master-data > dump.sql
当从另一个从服务器备份时,您必须传递--slave-dump选项。mydumper:
shell> mydumper -h <master_host> -u backup_user --password=<pass> --use-savepoints --trx-consistency-only --kill-long-queries --outputdir /backups
- 在从服务器上:备份完成后,恢复备份。参考第八章,恢复数据,了解恢复方法。
mysqldump:
shell> mysql -u <user> -p -f < dump.sql
mydumper:
shell> myloader --directory=/backups --user=<user> --password=<password> --queries-per-transaction=5000 --threads=8 --overwrite-tables
- 在从服务器上:在恢复备份后,您必须执行以下命令:
mysql> CHANGE MASTER TO MASTER_HOST='<master_host>', MASTER_USER='binlog_user', MASTER_PASSWORD='binlog_P@ss12', MASTER_LOG_FILE='<log_file_name>', MASTER_LOG_POS=<position>
mysqldump:备份转储文件中包含<log_file_name>和<position>。例如:
shell> less dump.sql
--
-- Position to start replication or point-in-time recovery from (the master of this slave)
--
CHANGE MASTER TO MASTER_LOG_FILE='centos7-bin.000001', MASTER_LOG_POS=463;
mydumper:<log_file_name>和<position>存储在元数据文件中:
shell> cat metadata
Started dump at: 2017-08-26 06:26:19
SHOW MASTER STATUS:
Log: server1.000012
Pos: 154122
GTID:
SHOW SLAVE STATUS:
Host: xx.xxx.xxx.xxx
Log: centos7-bin.000001
Pos: 463223
GTID:
Finished dump at: 2017-08-26 06:26:24
如果您从一个从服务器或主服务器备份以设置另一个从服务器,您必须使用SHOW SLAVE STATUS中的位置。如果要设置链式复制,可以使用SHOW MASTER STATUS中的位置。
- 在从服务器上,执行
START SLAVE命令:
mysql> START SLAVE;
- 您可以通过执行以下命令来检查复制的状态:
mysql> SHOW SLAVE STATUS\G
*************************** 1\. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: xx.xxx.xxx.xxx
Master_User: binlog_user
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: server1-bin.000001
Read_Master_Log_Pos: 463
Relay_Log_File: server2-relay-bin.000004
Relay_Log_Pos: 322
Relay_Master_Log_File: server1-bin.000001
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 463
Relay_Log_Space: 1957
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
Replicate_Ignore_Server_Ids:
Master_Server_Id: 32
Master_UUID: b52ef45a-7ff4-11e7-9091-42010a940003
Master_Info_File: /var/lib/mysql/master.info
SQL_Delay: 0
SQL_Remaining_Delay: NULL
Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
Master_Retry_Count: 86400
Master_Bind:
Last_IO_Error_Timestamp:
Last_SQL_Error_Timestamp:
Master_SSL_Crl:
Master_SSL_Crlpath:
Retrieved_Gtid_Set:
Executed_Gtid_Set:
Auto_Position: 0
Replicate_Rewrite_DB:
Channel_Name:
Master_TLS_Version:
1 row in set (0.00 sec)
您应该查找Seconds_Behind_Master,它显示了复制的延迟。如果是0,表示从服务器与主服务器同步;任何非零值表示延迟的秒数,如果是NULL,表示复制没有发生。
设置主-主复制
这个教程会吸引很多人,因为我们中的许多人都尝试过这样做。让我们深入了解一下。
如何做...
假设主服务器是master1和master2。
步骤:
-
按照第九章复制中描述的方法在
master1和master2之间设置复制。 -
使
master2成为只读:
mysql> SET @@GLOBAL.READ_ONLY=ON;
- 在
master2上,检查当前的二进制日志坐标。
mysql> SHOW MASTER STATUS;
+----------------+----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+----------------+----------+--------------+------------------+-------------------+
| server1.000017 | 473 | | | |
+----------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)
从前面的输出中,您可以从server1.000017和位置473开始在master1上启动复制。
- 根据前面步骤中的位置,在
master1上执行CHANGE MASTER TO命令:
mysql> CHANGE MASTER TO MASTER_HOST='<master2_host>', MASTER_USER='binlog_user', MASTER_PASSWORD='binlog_P@ss12', MASTER_LOG_FILE='<log_file_name>', MASTER_LOG_POS=<position>
- 在
master1上启动从服务器:
mysql> START SLAVE;
- 最后,您可以使
master2成为读写,应用程序可以开始向其写入。
mysql> SET @@GLOBAL.READ_ONLY=OFF;
设置多源复制
MySQL 多源复制使得复制从服务器能够同时接收来自多个源的事务。多源复制可用于将多个服务器备份到单个服务器,合并表分片,并将来自多个服务器的数据合并到单个服务器。多源复制在应用事务时不实现任何冲突检测或解决,如果需要,这些任务将留给应用程序。在多源复制拓扑中,从服务器为应该接收事务的每个主服务器创建一个复制通道。
在本节中,您将学习如何设置具有多个主服务器的从服务器。这种方法与在通道上设置传统复制相同。
如何做...
假设您要将server3设置为server1和server2的从服务器。您需要从server1到server3创建传统复制通道,并从server2到server3创建另一个通道。为了确保从服务器上的数据一致,请确保复制不同的数据库集或应用程序处理冲突。
开始之前,请从 server1 备份并在server3上恢复;类似地,从server2备份并在server3上恢复,如第九章“复制”中所述。
- 在
server3上,将复制存储库从FILE修改为TABLE。您可以通过运行以下命令动态更改它:
mysql> STOP SLAVE; //If slave is already running
mysql> SET GLOBAL master_info_repository = 'TABLE';
mysql> SET GLOBAL relay_log_info_repository = 'TABLE';
还要更改配置文件:
shell> sudo vi /etc/my.cnf
[mysqld]
master-info-repository=TABLE
relay-log-info-repository=TABLE
- 在
server3上,执行CHANGE MASTER TO命令,使其成为server1的从服务器,通道名为master-1。您可以随意命名:
mysql> CHANGE MASTER TO MASTER_HOST='server1', MASTER_USER='binlog_user', MASTER_PORT=3306, MASTER_PASSWORD='binlog_P@ss12', MASTER_LOG_FILE='server1.000017', MASTER_LOG_POS=788 FOR CHANNEL 'master-1';
- 在
server3上,执行CHANGE MASTER TO命令,使其成为server2的从服务器,通道为master-2:
mysql> CHANGE MASTER TO MASTER_HOST='server2', MASTER_USER='binlog_user', MASTER_PORT=3306, MASTER_PASSWORD='binlog_P@ss12', MASTER_LOG_FILE='server2.000014', MASTER_LOG_POS=75438 FOR CHANNEL 'master-2';
- 对于每个通道,执行
START SLAVE FOR CHANNEL语句如下:
mysql> START SLAVE FOR CHANNEL 'master-1';
Query OK, 0 rows affected (0.01 sec)
mysql> START SLAVE FOR CHANNEL 'master-2';
Query OK, 0 rows affected (0.00 sec)
- 通过执行
SHOW SLAVE STATUS语句验证从服务器的状态:
mysql> SHOW SLAVE STATUS\G
*************************** 1\. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: server1
Master_User: binlog_user
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: server1.000017
Read_Master_Log_Pos: 788
Relay_Log_File: server3-relay-bin-master@002d1.000002
Relay_Log_Pos: 318
Relay_Master_Log_File: server1.000017
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 788
Relay_Log_Space: 540
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
Replicate_Ignore_Server_Ids:
Master_Server_Id: 32
Master_UUID: 7cc7fca7-4deb-11e7-a53e-42010a940002
Master_Info_File: mysql.slave_master_info
SQL_Delay: 0
SQL_Remaining_Delay: NULL
Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
Master_Retry_Count: 86400
Master_Bind:
Last_IO_Error_Timestamp:
Last_SQL_Error_Timestamp:
Master_SSL_Crl:
Master_SSL_Crlpath:
Retrieved_Gtid_Set:
Executed_Gtid_Set:
Auto_Position: 0
Replicate_Rewrite_DB:
Channel_Name: master-1
Master_TLS_Version:
*************************** 2\. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: server2
Master_User: binlog_user
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: server2.000014
Read_Master_Log_Pos: 75438
Relay_Log_File: server3-relay-bin-master@002d2.000002
Relay_Log_Pos: 322
Relay_Master_Log_File: server2.000014
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 75438
Relay_Log_Space: 544
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
Replicate_Ignore_Server_Ids:
Master_Server_Id: 32
Master_UUID: b52ef45a-7ff4-11e7-9091-42010a940003
Master_Info_File: mysql.slave_master_info
SQL_Delay: 0
SQL_Remaining_Delay: NULL
Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
Master_Retry_Count: 86400
Master_Bind:
Last_IO_Error_Timestamp:
Last_SQL_Error_Timestamp:
Master_SSL_Crl:
Master_SSL_Crlpath:
Retrieved_Gtid_Set:
Executed_Gtid_Set:
Auto_Position: 0
Replicate_Rewrite_DB:
Channel_Name: master-2
Master_TLS_Version:
2 rows in set (0.00 sec)
- 要获取特定通道的从服务器状态,请执行:
mysql> SHOW SLAVE STATUS FOR CHANNEL 'master-1' \G
- 这是您可以使用性能模式监视指标的另一种方法:
mysql> SELECT * FROM performance_schema.replication_connection_status\G
*************************** 1\. row ***************************
CHANNEL_NAME: master-1
GROUP_NAME:
SOURCE_UUID: 7cc7fca7-4deb-11e7-a53e-42010a940002
THREAD_ID: 36
SERVICE_STATE: ON
COUNT_RECEIVED_HEARTBEATS: 73
LAST_HEARTBEAT_TIMESTAMP: 2017-09-15 12:42:10.910051
RECEIVED_TRANSACTION_SET:
LAST_ERROR_NUMBER: 0
LAST_ERROR_MESSAGE:
LAST_ERROR_TIMESTAMP: 0000-00-00 00:00:00.000000
LAST_QUEUED_TRANSACTION:
LAST_QUEUED_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP: 0000-00-00 00:00:00.000000
LAST_QUEUED_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP: 0000-00-00 00:00:00.000000
LAST_QUEUED_TRANSACTION_START_QUEUE_TIMESTAMP: 0000-00-00 00:00:00.000000
LAST_QUEUED_TRANSACTION_END_QUEUE_TIMESTAMP: 0000-00-00 00:00:00.000000
QUEUEING_TRANSACTION:
QUEUEING_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP: 0000-00-00 00:00:00.000000
QUEUEING_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP: 0000-00-00 00:00:00.000000
QUEUEING_TRANSACTION_START_QUEUE_TIMESTAMP: 0000-00-00 00:00:00.000000
*************************** 2\. row ***************************
CHANNEL_NAME: master-2
GROUP_NAME:
SOURCE_UUID: b52ef45a-7ff4-11e7-9091-42010a940003
THREAD_ID: 38
SERVICE_STATE: ON
COUNT_RECEIVED_HEARTBEATS: 73
LAST_HEARTBEAT_TIMESTAMP: 2017-09-15 12:42:13.986271
RECEIVED_TRANSACTION_SET:
LAST_ERROR_NUMBER: 0
LAST_ERROR_MESSAGE:
LAST_ERROR_TIMESTAMP: 0000-00-00 00:00:00.000000
LAST_QUEUED_TRANSACTION:
LAST_QUEUED_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP: 0000-00-00 00:00:00.000000
LAST_QUEUED_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP: 0000-00-00 00:00:00.000000
LAST_QUEUED_TRANSACTION_START_QUEUE_TIMESTAMP: 0000-00-00 00:00:00.000000
LAST_QUEUED_TRANSACTION_END_QUEUE_TIMESTAMP: 0000-00-00 00:00:00.000000
QUEUEING_TRANSACTION:
QUEUEING_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP: 0000-00-00 00:00:00.000000
QUEUEING_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP: 0000-00-00 00:00:00.000000
QUEUEING_TRANSACTION_START_QUEUE_TIMESTAMP: 0000-00-00 00:00:00.000000
2 rows in set (0.00 sec)
您可以通过附加FOR CHANNEL 'channel_name'指定通道的所有与从服务器相关的命令:
mysql> STOP SLAVE FOR CHANNEL 'master-1';
mysql> RESET SLAVE FOR CHANNEL 'master-2';
设置复制过滤器
您可以控制要复制的表或数据库。在主服务器上,您可以使用--binlog-do-db和--binlog-ignore-db选项控制要为其记录更改的数据库,如第六章“二进制日志”中所述。更好的方法是在从服务器端进行控制。您可以使用--replicate-*选项或通过创建复制过滤器动态地执行或忽略从主服务器接收的语句。
如何做...
要创建过滤器,您需要执行CHANGE REPLICATION FILTER语句。
仅复制数据库
假设您只想复制db1和db2。使用以下语句创建复制过滤器。
mysql> CHANGE REPLICATION FILTER REPLICATE_DO_DB = (db1, db2);
请注意,您应该在括号内指定所有数据库。
复制特定表
您可以使用REPLICATE_DO_TABLE指定要复制的表:
mysql> CHANGE REPLICATION FILTER REPLICATE_DO_TABLE = ('db1.table1');
假设您想要对表使用正则表达式;您可以使用REPLICATE_WILD_DO_TABLE选项:
mysql> CHANGE REPLICATION FILTER REPLICATE_WILD_DO_TABLE = ('db1.imp%');
您可以使用各种IGNORE选项使用正则表达式提及一些数据库或表。
忽略数据库
就像您可以选择复制数据库一样,您可以使用REPLICATE_IGNORE_DB忽略复制中的数据库:
mysql> CHANGE REPLICATION FILTER REPLICATE_IGNORE_DB = (db1, db2);
忽略特定表
您可以使用REPLICATE_IGNORE_TABLE和REPLICATE_WILD_IGNORE_TABLE选项忽略某些表。REPLICATE_WILD_IGNORE_TABLE选项允许使用通配符字符,而REPLICATE_IGNORE_TABLE仅接受完整的表名:
mysql> CHANGE REPLICATION FILTER REPLICATE_IGNORE_TABLE = ('db1.table1');
mysql> CHANGE REPLICATION FILTER REPLICATE_WILD_IGNORE_TABLE = ('db1.new%', 'db2.new%');
您还可以通过指定通道名称为通道设置过滤器:
mysql> CHANGE REPLICATION FILTER REPLICATE_DO_DB = (d1) FOR CHANNEL 'master-1';
另请参阅
有关复制过滤器的更多详细信息,请参阅dev.mysql.com/doc/refman/8.0/en/change-replication-filter.html。如果您使用多个过滤器,请参阅dev.mysql.com/doc/refman/8.0/en/replication-rules.html以了解有关 MySQL 如何评估过滤器的更多信息。
将从主从复制切换到链式复制
如果您设置了主从复制,服务器 B 和 C 从服务器 A 复制:服务器 A -->(服务器 B,服务器 C),并且您希望将服务器 C 设置为服务器 B 的从服务器,则必须在服务器 B 和服务器 C 上停止复制。然后使用START SLAVE UNTIL命令将它们带到相同的主日志位置。之后,您可以从服务器 B 获取主日志坐标,并在服务器 C 上执行CHANGE MASTER TO命令。
如何做...
- 在服务器 C 上:停止从服务器并注意
SHOW SLAVE STATUS\G命令中的Relay_Master_Log_File和Exec_Master_Log_Pos位置:
mysql> STOP SLAVE;
Query OK, 0 rows affected (0.01 sec)
mysql> SHOW SLAVE STATUS\G
*************************** 1\. row ***************************
Slave_IO_State:
Master_Host: xx.xxx.xxx.xxx
Master_User: binlog_user
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: server_A-bin.000023
Read_Master_Log_Pos: 2604
Relay_Log_File: server_C-relay-bin.000002
Relay_Log_Pos: 1228
Relay_Master_Log_File: server_A-bin.000023
~
Exec_Master_Log_Pos: 2604
Relay_Log_Space: 1437
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
~
1 row in set (0.00 sec)
- 在服务器 B 上:停止从服务器并注意
SHOW SLAVE STATUS\G命令中的Relay_Master_Log_File和Exec_Master_Log_Pos位置:
mysql> STOP SLAVE;
Query OK, 0 rows affected (0.01 sec)
mysql> SHOW SLAVE STATUS\G
*************************** 1\. row ***************************
Slave_IO_State:
Master_Host: xx.xxx.xxx.xxx
Master_User: binlog_user
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: server_A-bin.000023
Read_Master_Log_Pos: 8250241
Relay_Log_File: server_B-relay-bin.000002
Relay_Log_Pos: 1228
Relay_Master_Log_File: server_A-bin.000023
~
Exec_Master_Log_Pos: 8250241
Relay_Log_Space: 8248167
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
~
1 row in set (0.00 sec)
- 比较服务器 B 的日志位置和服务器 C,找出与服务器 A 最新的同步。通常,由于您首先在服务器 C 上停止了从服务器,服务器 B 将领先。在我们的情况下,日志位置是:
服务器 C:(server_A-bin.000023,2604)
服务器 B:(server_A-bin.000023,8250241)
服务器 B 领先,所以我们必须将服务器 C 带到服务器 B 的位置。
- 在服务器 C 上:使用
START SLAVE UNTIL语句同步到服务器 B 的位置:
mysql> START SLAVE UNTIL MASTER_LOG_FILE='centos7-bin.000023', MASTER_LOG_POS=8250241;
Query OK, 0 rows affected, 1 warning (0.03 sec)
mysql> SHOW WARNINGS\G
*************************** 1\. row ***************************
Level: Note
Code: 1278
Message: It is recommended to use --skip-slave-start when doing step-by-step replication with START SLAVE UNTIL; otherwise, you will get problems if you get an unexpected slave's mysqld restart
1 row in set (0.00 sec)
- 在服务器 C 上:等待服务器 C 追上,通过检查
SHOW SLAVE STATUS输出中的Exec_Master_Log_Pos和Until_Log_Pos(两者应该相同):
mysql> SHOW SLAVE STATUS\G
*************************** 1\. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: xx.xxx.xxx.xxx
Master_User: binlog_user
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: server_A-bin.000023
Read_Master_Log_Pos: 8250241
Relay_Log_File: server_C-relay-bin.000003
Relay_Log_Pos: 8247959
Relay_Master_Log_File: server_A-bin.000023
Slave_IO_Running: Yes
Slave_SQL_Running: No
~
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 8250241
Relay_Log_Space: 8249242
Until_Condition: Master
Until_Log_File: server_A-bin.000023
Until_Log_Pos: 8250241
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: NULL
~
1 row in set (0.00 sec)
- 在服务器 B 上:查找主状态,启动从服务器,并确保它正在复制:
mysql> SHOW MASTER STATUS;
+---------------------+----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+---------------------+----------+--------------+------------------+-------------------+
| server_B-bin.000003 | 36379324 | | | |
+---------------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)
mysql> START SLAVE;
Query OK, 0 rows affected (0.02 sec)
mysql> SHOW SLAVE STATUS\G
*************************** 1\. row ***************************
Slave_IO_State:
Master_Host: xx.xxx.xxx.xxx
Master_User: binlog_user
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: server_A-bin.000023
Read_Master_Log_Pos: 8250241
Relay_Log_File: server_B-relay-bin.000002
Relay_Log_Pos: 1228
Relay_Master_Log_File: server_A-bin.000023
~
Exec_Master_Log_Pos: 8250241
Relay_Log_Space: 8248167
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
~
1 row in set (0.00 sec)
- 在服务器 C 上:停止从服务器,执行
CHANGE MASTER TO命令,并指向服务器 B。您必须使用从上一步中获得的位置:
mysql> STOP SLAVE;
Query OK, 0 rows affected (0.04 sec)
mysql> CHANGE MASTER TO MASTER_HOST = 'Server B', MASTER_USER = 'binlog_user', MASTER_PASSWORD = 'binlog_P@ss12', MASTER_LOG_FILE='server_B-bin.000003', MASTER_LOG_POS=36379324;
Query OK, 0 rows affected, 1 warning (0.04 sec)
- 在服务器 C 上:启动复制并验证从服务器状态:
mysql> START SLAVE;
Query OK, 0 rows affected (0.00 sec)
mysql> SHOW SLAVE STATUS\G
Query OK, 0 rows affected, 1 warning (0.00 sec)
*************************** 1\. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: xx.xxx.xxx.xx
Master_User: binlog_user
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: server_B-bin.000003
Read_Master_Log_Pos: 36380416
Relay_Log_File: server_C-relay-bin.000002
Relay_Log_Pos: 1413
Relay_Master_Log_File: server_B-bin.000003
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
~
Exec_Master_Log_Pos: 36380416
Relay_Log_Space: 1622
~
Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
Replicate_Ignore_Server_Ids:
~
1 row in set (0.00 sec)
将从链式复制切换为主从复制
如果您设置了链式复制(例如服务器 A --> 服务器 B --> 服务器 C)并且希望使服务器 C 成为服务器 A 的直接从服务器,则必须在服务器 B 上停止复制,让服务器 C 追上服务器 B,然后找到服务器 A 对应于服务器 B 停止位置的坐标。使用这些坐标,您可以在服务器 C 上执行CHANGE MASTER TO命令,并使其成为服务器 A 的从服务器。
如何做...
- 在服务器 B 上:停止从服务器并记录主状态:
mysql> STOP SLAVE;
Query OK, 0 rows affected (0.04 sec)
mysql> SHOW MASTER STATUS;
+---------------------+----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+---------------------+----------+--------------+------------------+-------------------+
| server_B-bin.000003 | 44627878 | | | |
+---------------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)
- 在服务器 C 上:确保从服务器延迟已经追上。
Relay_Master_Log_File和Exec_Master_Log_Pos应该等于服务器 B 上主状态的输出。一旦延迟追上,停止从服务器:
mysql> SHOW SLAVE STATUS\G
*************************** 1\. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 35.186.157.16
Master_User: repl
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: server_B-bin.000003
Read_Master_Log_Pos: 44627878
Relay_Log_File: ubuntu2-relay-bin.000002
Relay_Log_Pos: 8248875
Relay_Master_Log_File: server_B-bin.000003
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 44627878
Relay_Log_Space: 8249084
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: 0
~
1 row in set (0.00 sec)
mysql> STOP SLAVE;
Query OK, 0 rows affected (0.01 sec)
- 在服务器 B 上:从
SHOW SLAVE STATUS输出中获取服务器 A 的坐标(注意Relay_Master_Log_File和Exec_Master_Log_Pos)并启动从服务器:
mysql> SHOW SLAVE STATUS\G
*************************** 1\. row ***************************
Slave_IO_State:
Master_Host: xx.xxx.xxx.xxx
Master_User: repl
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: server_A-bin.000023
Read_Master_Log_Pos: 16497695
Relay_Log_File: server_B-relay-bin.000004
Relay_Log_Pos: 8247776
Relay_Master_Log_File: server_A-bin.000023
Slave_IO_Running: No
Slave_SQL_Running: No
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 16497695
Relay_Log_Space: 8248152
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: NULL
mysql> START SLAVE;
Query OK, 0 rows affected (0.01 sec)
- 在服务器 C 上:停止从服务器并执行
CHANGE MASTER TO COMMAND指向服务器 A。使用从上一步中记录的位置(server_A-bin.000023和16497695)。最后启动从服务器并验证从服务器状态:
mysql> STOP SLAVE;
Query OK, 0 rows affected (0.07 sec)
mysql> CHANGE MASTER TO MASTER_HOST = 'Server A', MASTER_USER = 'binlog_user', MASTER_PASSWORD = 'binlog_P@ss12', MASTER_LOG_FILE='server_A-bin.000023', MASTER_LOG_POS=16497695;
Query OK, 0 rows affected, 1 warning (0.02 sec)
mysql> START SLAVE;
Query OK, 0 rows affected (0.07 sec)
mysql> SHOW SLAVE STATUS\G
*************************** 1\. row ***************************
Slave_IO_State:
Master_Host: xx.xxx.xxx.xxx
Master_User: binlog_user
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: server_A-bin.000023
Read_Master_Log_Pos: 16497695
Relay_Log_File: server_C-relay-bin.000001
Relay_Log_Pos: 4
Relay_Master_Log_File: server_A-bin.000023
Slave_IO_Running: No
Slave_SQL_Running: No
~
Skip_Counter: 0
Exec_Master_Log_Pos: 16497695
Relay_Log_Space: 154
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: 0
~
1 row in set (0.00 sec)
设置延迟复制
有时,您需要一个延迟的从服务器用于灾难恢复目的。假设主服务器上执行了灾难性语句(如DROP DATABASE命令)。您必须使用备份的时间点恢复来恢复数据库。这将导致巨大的停机时间,具体取决于数据库的大小。为了避免这种情况,您可以使用延迟的从服务器,它将始终比主服务器延迟一定的时间。如果发生灾难并且该语句未被延迟的从服务器应用,您可以停止从服务器并启动直到灾难性语句,以便灾难性语句不会被执行。然后将其提升为主服务器。
该过程与设置正常复制完全相同,只是在CHANGE MASTER TO命令中指定MASTER_DELAY。
延迟是如何衡量的?
在 MySQL 8.0 之前的版本中,延迟是基于Seconds_Behind_Master值来衡量的。在 MySQL 8.0 中,它是基于original_commit_timestamp和immediate_commit_timestamp来衡量的,这些值写入了二进制日志。
original_commit_timestamp是事务写入(提交)到原始主服务器的二进制日志时距离时代开始的微秒数。
immediate_commit_timestamp是事务写入(提交)到直接主服务器的二进制日志时距离时代开始的微秒数。
如何做...
- 停止从服务器:
mysql> STOP SLAVE;
Query OK, 0 rows affected (0.06 sec)
- 执行
CHANGE MASTER TO MASTER_DELAY =并启动从服务器。假设您想要 1 小时的延迟,您可以将MASTER_DELAY设置为3600秒:
mysql> CHANGE MASTER TO MASTER_DELAY = 3600;
Query OK, 0 rows affected (0.04 sec)
mysql> START SLAVE;
Query OK, 0 rows affected (0.00 sec)
- 在从服务器状态中检查以下内容:
SQL_Delay: 从服务器必须滞后主服务器的秒数。
SQL_Remaining_Delay: 延迟剩余的秒数。当存在延迟时,此值为 NULL。
Slave_SQL_Running_State: SQL 线程的状态。
mysql> SHOW SLAVE STATUS\G
*************************** 1\. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 35.186.158.188
Master_User: repl
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: server_A-bin.000023
Read_Master_Log_Pos: 24745149
Relay_Log_File: server_B-relay-bin.000002
Relay_Log_Pos: 322
Relay_Master_Log_File: server_A-bin.000023
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
~
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 16497695
Relay_Log_Space: 8247985
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
~
Seconds_Behind_Master: 52
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
~
SQL_Delay: 3600
SQL_Remaining_Delay: 3549
Slave_SQL_Running_State: Waiting until MASTER_DELAY seconds after master executed event
Master_Retry_Count: 86400
Master_Bind:
Last_IO_Error_Timestamp:
Last_SQL_Error_Timestamp:
~
1 row in set (0.00 sec)
请注意,一旦延迟被维持,Seconds_Behind_Master将显示为0。
设置 GTID 复制
全局事务标识符(GTID)是在原始服务器(主服务器)上提交的每个事务创建并关联的唯一标识符。此标识符不仅对于其起源服务器是唯一的,而且对于给定复制设置中的所有服务器也是唯一的。所有事务和所有 GTID 之间存在一对一的映射关系。
GTID 表示为一对坐标,用冒号(:)分隔。
GTID = source_id:transaction_id
source_id选项标识了原始服务器。通常,服务器的server_uuid选项用于此目的。transaction_id选项是由事务在此服务器上提交的顺序确定的序列号。例如,第一个提交的事务其transaction_id为1,在同一原始服务器上提交的第十个事务被分配了transaction_id为10。
正如您在之前的方法中所看到的,您必须在复制的起点上提到二进制日志文件和位置。如果您要将一个从服务器从一个主服务器切换到另一个主服务器,特别是在故障转移期间,您必须从新主服务器获取位置以同步从服务器,这可能很痛苦。为了避免这些问题,您可以使用基于 GTID 的复制,MySQL 会自动使用 GTID 检测二进制日志位置。
如何操作...
如果服务器之间已经设置了复制,请按照以下步骤操作:
- 在
my.cnf中启用 GTID:
shell> sudo vi /etc/my.cnf [mysqld]gtid_mode=ON
enforce-gtid-consistency=true
skip_slave_start
- 将主服务器设置为只读,并确保所有从服务器与主服务器保持同步。这非常重要,因为主服务器和从服务器之间不应该存在任何数据不一致。
On master mysql> SET @@global.read_only = ON; On Slaves (if replication is already setup) mysql> SHOW SLAVE STATUS\G
- 重新启动所有从服务器以使 GTID 生效。由于在配置文件中给出了
skip_slave_start,从服务器在指定START SLAVE命令之前不会启动。如果启动从服务器,它将失败,并显示此错误——The replication receiver thread cannot start because the master has GTID_MODE = OFF and this server has GTID_MODE = ON。
shell> sudo systemctl restart mysql
- 重新启动主服务器。重新启动主服务器后,它将以读写模式开始,并在 GTID 模式下开始接受写操作:
shell> sudo systemctl restart mysql
- 执行
CHANGE MASTER TO命令以设置 GTID 复制:
mysql> CHANGE MASTER TO MASTER_HOST = <master_host>, MASTER_PORT = <port>, MASTER_USER = 'binlog_user', MASTER_PASSWORD = 'binlog_P@ss12', MASTER_AUTO_POSITION = 1;
您可以观察到二进制日志文件和位置未给出;相反,给出了MASTER_AUTO_POSITION,它会自动找到已执行的 GTID。
- 在所有从服务器上执行
START SLAVE:
mysql> START SLAVE;
- 验证从服务器是否正在复制:
mysql> SHOW SLAVE STATUS\G
*************************** 1\. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: xx.xxx.xxx.xxx
Master_User: binlog_user
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: server1-bin.000002
Read_Master_Log_Pos: 345
Relay_Log_File: server2-relay-bin.000002
Relay_Log_Pos: 562
Relay_Master_Log_File: server1-bin.000002
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 345
Relay_Log_Space: 770
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
Replicate_Ignore_Server_Ids:
Master_Server_Id: 32
Master_UUID: b52ef45a-7ff4-11e7-9091-42010a940003
Master_Info_File: /var/lib/mysql/master.info
SQL_Delay: 0
SQL_Remaining_Delay: NULL
Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
Master_Retry_Count: 86400
Master_Bind:
Last_IO_Error_Timestamp:
Last_SQL_Error_Timestamp:
Master_SSL_Crl:
Master_SSL_Crlpath:
Retrieved_Gtid_Set: b52ef45a-7ff4-11e7-9091-42010a940003:1
Executed_Gtid_Set: b52ef45a-7ff4-11e7-9091-42010a940003:1
Auto_Position: 1
Replicate_Rewrite_DB:
Channel_Name:
Master_TLS_Version:
1 row in set (0.00 sec)
要了解有关 GTID 的更多信息,请参阅dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html。
设置半同步复制
默认情况下,复制是异步的。主服务器不知道写操作是否已到达从服务器。如果主服务器和从服务器之间存在延迟,并且主服务器崩溃,那么尚未到达从服务器的数据将会丢失。为了克服这种情况,您可以使用半同步复制。
在半同步复制中,主服务器会等待至少一个从服务器接收写操作。默认情况下,rpl_semi_sync_master_wait_point的值为AFTER_SYNC;这意味着主服务器将事务同步到从服务器消耗的二进制日志中。
之后,从服务器向主服务器发送确认,然后主服务器提交事务并将结果返回给客户端。因此,如果写入已到达中继日志,则从服务器无需提交事务。您可以通过将变量rpl_semi_sync_master_wait_point更改为AFTER_COMMIT来更改此行为。在这种情况下,主服务器将事务提交给存储引擎,但不将结果返回给客户端。一旦从服务器上提交了事务,主服务器将收到事务的确认,然后将结果返回给客户端。
如果要在更多从服务器上确认事务,可以增加动态变量rpl_semi_sync_master_wait_for_slave_count的值。您还可以通过动态变量rpl_semi_sync_master_timeout设置主服务器必须等待从服务器确认的毫秒数;默认值为10秒。
在完全同步复制中,主服务器会等待直到所有从服务器都提交了事务。要实现这一点,您必须使用 Galera Cluster。
如何做…
在高层次上,您需要在主服务器和所有希望进行半同步复制的从服务器上安装和启用半同步插件。您必须重新启动从服务器 IO 线程以使更改生效。您可以根据您的网络和应用程序调整rpl_semi_sync_master_timeout的值。1秒的值是一个很好的起点:
- 在主服务器上,安装
rpl_semi_sync_master插件:
mysql> INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
Query OK, 0 rows affected (0.86 sec)
验证插件是否已激活:
mysql> SELECT PLUGIN_NAME, PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME LIKE '%semi%';
+----------------------+---------------+
| PLUGIN_NAME | PLUGIN_STATUS |
+----------------------+---------------+
| rpl_semi_sync_master | ACTIVE |
+----------------------+---------------+
1 row in set (0.01 sec)
- 在主服务器上,启用半同步复制并调整超时时间(比如 1 秒):
mysql> SET @@GLOBAL.rpl_semi_sync_master_enabled=1;
Query OK, 0 rows affected (0.00 sec)
mysql> SHOW VARIABLES LIKE 'rpl_semi_sync_master_enabled';
+------------------------------+-------+
| Variable_name | Value |
+------------------------------+-------+
| rpl_semi_sync_master_enabled | ON |
+------------------------------+-------+
1 row in set (0.00 sec)
mysql> SET @@GLOBAL.rpl_semi_sync_master_timeout=1000;
Query OK, 0 rows affected (0.00 sec)
mysql> SHOW VARIABLES LIKE 'rpl_semi_sync_master_timeout';
+------------------------------+-------+
| Variable_name | Value |
+------------------------------+-------+
| rpl_semi_sync_master_timeout | 1000 |
+------------------------------+-------+
1 row in set (0.00 sec)
- 在从服务器上,安装
rpl_semi_sync_slave插件:
mysql> INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
Query OK, 0 rows affected (0.22 sec)
mysql> SELECT PLUGIN_NAME, PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME LIKE '%semi%';
+---------------------+---------------+
| PLUGIN_NAME | PLUGIN_STATUS |
+---------------------+---------------+
| rpl_semi_sync_slave | ACTIVE |
+---------------------+---------------+
1 row in set (0.08 sec)
- 在从服务器上,启用半同步复制并重新启动从服务器 IO 线程:
mysql> SET GLOBAL rpl_semi_sync_slave_enabled = 1;
Query OK, 0 rows affected (0.00 sec)
mysql> STOP SLAVE IO_THREAD;
Query OK, 0 rows affected (0.02 sec)
mysql> START SLAVE IO_THREAD;
Query OK, 0 rows affected (0.00 sec)
- 您可以通过以下方式监视半同步复制的状态:
要查找连接为半同步的客户端数量,请在主服务器上执行:
mysql> SHOW STATUS LIKE 'Rpl_semi_sync_master_clients';
+------------------------------+-------+
| Variable_name | Value |
+------------------------------+-------+
| Rpl_semi_sync_master_clients | 1 |
+------------------------------+-------+
1 row in set (0.01 sec)
当超时发生并且从服务器赶上时,主服务器在异步和半同步复制之间切换。要检查主服务器正在使用的复制类型,请检查Rpl_semi_sync_master_status的状态(打开表示半同步,关闭表示异步):
mysql> SHOW STATUS LIKE 'Rpl_semi_sync_master_status';
+-----------------------------+-------+
| Variable_name | Value |
+-----------------------------+-------+
| Rpl_semi_sync_master_status | ON |
+-----------------------------+-------+
1 row in set (0.00 sec)
您可以使用此方法验证半同步复制:
- 停止从服务器:
mysql> STOP SLAVE;
Query OK, 0 rows affected (0.01 sec)
- 在主服务器上,执行任何语句:
mysql> USE employees;
Database changed
mysql> DROP TABLE IF EXISTS employees_test;
Query OK, 0 rows affected, 1 warning (0.00 sec)
您会注意到主服务器已经切换到异步复制,因为即使在 1 秒后(rpl_semi_sync_master_timeout的值),它仍未收到从从服务器的任何确认:
mysql> SHOW STATUS LIKE 'Rpl_semi_sync_master_status';
+-----------------------------+-------+
| Variable_name | Value |
+-----------------------------+-------+
| Rpl_semi_sync_master_status | ON |
+-----------------------------+-------+
1 row in set (0.00 sec)
mysql> DROP TABLE IF EXISTS employees_test;
Query OK, 0 rows affected (1.02 sec)
mysql> SHOW STATUS LIKE 'Rpl_semi_sync_master_status';
+-----------------------------+-------+
| Variable_name | Value |
+-----------------------------+-------+
| Rpl_semi_sync_master_status | OFF |
+-----------------------------+-------+
1 row in set (0.01 sec
- 启动从服务器:
mysql> START SLAVE;
Query OK, 0 rows affected (0.02 sec)
- 在主服务器上,您会注意到主服务器已经切换回半同步复制。
mysql> SHOW STATUS LIKE 'Rpl_semi_sync_master_status';
+-----------------------------+-------+
| Variable_name | Value |
+-----------------------------+-------+
| Rpl_semi_sync_master_status | ON |
+-----------------------------+-------+
1 row in set (0.00 sec)
第十章:表维护
在本章中,我们将涵盖以下配方:
-
安装 Percona Toolkit
-
修改表
-
在数据库之间移动表
-
使用在线模式更改工具修改表
-
归档表
-
克隆表
-
分区表
-
分区修剪和选择
-
分区管理
-
分区信息
-
高效管理生存时间和软删除行
介绍
在维护数据库中,一个关键方面是管理表。通常,您需要更改一个大表或克隆一个表。在本章中,您将学习如何管理大表。由于 MySQL 不支持某些操作,因此使用了一些开源第三方工具。本章还涵盖了第三方工具的安装和使用。
安装 Percona Toolkit
Percona Toolkit 是一套高级开源命令行工具,由 Percona 开发和使用,用于执行各种手动执行的任务。安装在本节中介绍。在后面的部分,您将学习如何使用它。
如何做...
让我们看看如何在各种操作系统上安装 Percona Toolkit。
在 Debian/Ubuntu 上
- 下载存储库软件包:
shell> wget https://repo.percona.com/apt/percona-release_0.1-4.$(lsb_release -sc)_all.deb
- 安装存储库软件包:
shell> sudo dpkg -i percona-release_0.1-4.$(lsb_release -sc)_all.deb
- 更新本地软件包列表:
shell> sudo apt-get update
- 确保 Percona 软件包可用:
shell> apt-cache search percona
您应该看到类似以下的输出:
percona-xtrabackup-dbg - Debug symbols for Percona XtraBackup
percona-xtrabackup-test - Test suite for Percona XtraBackup
percona-xtradb-cluster-client - Percona XtraDB Cluster database client
percona-xtradb-cluster-server - Percona XtraDB Cluster database server
percona-xtradb-cluster-testsuite - Percona XtraDB Cluster database regression test suite
percona-xtradb-cluster-testsuite-5.5 - Percona Server database test suite
...
- 安装
percona-toolkit软件包:
shell> sudo apt-get install percona-toolkit
如果您不想安装存储库,也可以直接安装:
shell> wget https://www.percona.com/downloads/percona-toolkit/3.0.4/binary/debian/xenial/x86_64/percona-toolkit_3.0.4-1.xenial_amd64.deb
shell> sudo dpkg -i percona-toolkit_3.0.4-1.yakkety_amd64.deb;
shell> sudo apt-get install -f
在 CentOS/Red Hat/Fedora 上
- 安装存储库软件包:
shell> sudo yum install http://www.percona.com/downloads/percona-release/redhat/0.1-4/percona-release-0.1-4.noarch.rpm
如果成功,您应该看到以下内容:
Installed:
percona-release.noarch 0:0.1-4
Complete!
- 确保 Percona 软件包可用:
shell> sudo yum list | grep percona
您应该看到类似以下的输出:
percona-release.noarch 0.1-4 @/percona-release-0.1-4.noarch
Percona-Server-55-debuginfo.x86_64 5.5.54-rel38.7.el7 percona-release-x86_64
Percona-Server-56-debuginfo.x86_64 5.6.35-rel81.0.el7 percona-release-x86_64
Percona-Server-57-debuginfo.x86_64 5.7.17-13.1.el7 percona-release-x86_64
...
- 安装 Percona Toolkit:
shell> sudo yum install percona-toolkit
如果您不想安装存储库,可以直接使用 YUM 安装:
shell> sudo yum install https://www.percona.com/downloads/percona-toolkit/3.0.4/binary/redhat/7/x86_64/percona-toolkit-3.0.4-1.el7.x86_64.rpm
修改表
ALTER TABLE更改表的结构。例如,您可以添加或删除列,创建或销毁索引,更改现有列的类型,或重命名列或表本身。
在执行某些 alter 操作(例如更改列数据类型,添加SPATIAL INDEX,删除主键,转换字符集,添加/删除加密等)时,表上的 DML 操作将被阻止。如果表很大,则更改需要更长的时间,并且应用程序在此期间无法访问表,这是不希望发生的。在这种情况下,pt-online-schema更改是有帮助的,其中允许 DML 语句。
有两种 alter 操作算法:
-
原地(默认):不需要复制整个表数据
-
复制:将数据复制到临时磁盘文件并重命名
只有某些 alter 操作可以就地完成。在线 DDL 操作的性能在很大程度上取决于操作是在原地执行还是需要复制和重建整个表。请参阅dev.mysql.com/doc/refman/8.0/en/innodb-create-index-overview.html#innodb-online-ddl-summary-grid查看可以就地执行的操作类型,以及避免表复制操作的任何要求。
复制算法的工作原理(摘自参考手册-dev.mysql.com/doc/refman/8.0/en/alter-table.html)
不是就地执行的ALTER TABLE操作会创建原始表的临时副本。MySQL 等待正在修改表的其他操作,然后继续。它将更改合并到副本中,删除原始表,并重命名新表。在执行ALTER TABLE时,原始表可被其他会话读取。在ALTER TABLE操作开始后开始的对表的更新和写入将被暂停,直到新表准备就绪,然后会自动重定向到新表,而不会有任何更新失败。原始表的临时副本创建在新表的数据库目录中。这可能与重命名表到不同数据库的ALTER TABLE操作的原始表的数据库目录不同。
要了解 DDL 操作是就地执行还是表复制,请查看命令完成后显示的受影响行数值:
- 更改列的默认值(超快,根本不影响表数据),输出将类似于这样:
Query OK, 0 rows affected (0.07 sec)
- 添加索引(需要时间,但
0 行受影响表明表没有被复制),输出将类似于这样:
查询 OK,0 行受影响(21.42 秒)
- 更改列的数据类型(需要大量时间,并且确实需要重建表的所有行),输出将类似于这样:
Query OK, 1671168 rows affected (1 min 35.54 sec)
更改列的数据类型需要重建表的所有行,除了更改VARCHAR大小之外,可以使用在线ALTER TABLE来执行。请参阅使用在线模式更改工具修改表部分中提到的示例,该示例显示了如何使用pt-online-schema修改列属性。
如何做...
如果要向employees表添加新列,可以执行ADD COLUMN语句:
mysql> ALTER TABLE employees ADD COLUMN address varchar(100);
Query OK, 0 rows affected (5.10 sec)
Records: 0 Duplicates: 0 Warnings: 0
您会看到受影响的行数为0,这意味着表没有被复制,操作是就地完成的。
如果您想增加varchar列的长度,可以执行MODIFY COLUMN语句:
mysql> ALTER TABLE employees MODIFY COLUMN address VARCHAR(255);
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
如果您认为varchar(255)不足以存储地址,并且想要将其更改为tinytext,您可以使用MODIFY COLUMN语句。但是,在这种情况下,由于您正在修改列的数据类型,所有现有表的行都应该被修改,这需要表复制,并且会阻塞 DMLs:
mysql> ALTER TABLE employees MODIFY COLUMN address tinytext;
Query OK, 300025 rows affected (4.36 sec)
Records: 300025 Duplicates: 0 Warnings: 0
您会注意到受影响的行数为300025,这是表的大小。
还有其他各种操作,例如重命名列,更改默认值,重新排序列位置等;有关更多详细信息,请参阅dev.mysql.com/doc/refman/8.0/en/innodb-create-index-overview.html的手册。
添加一个虚拟生成的列只是一个元数据更改,几乎是瞬时的:
mysql> ALTER TABLE employees ADD COLUMN full_name VARCHAR(40) AS (CONCAT('first_name', ' ', 'last_name'));
Query OK, 0 rows affected (0.09 sec)
Records: 0 Duplicates: 0 Warnings: 0
但是,添加STORED GENERATED列和修改VIRTUAL GENERATED列不是在线的:
mysql> ALTER TABLE employees MODIFY COLUMN full_name VARCHAR(40) AS (CONCAT(first_name, '-', last_name)) VIRTUAL;
Query OK, 300026 rows affected (4.37 sec)
Records: 300026 Duplicates: 0 Warnings: 0
移动表格跨数据库
您可以通过执行RENAME TABLE语句来重命名表。
为了使以下示例起作用,请创建示例表和数据库
mysql> CREATE DATABASE prod;
mysql> CREATE TABLE prod.audit_log (id int NOT NULL, msg varchar(64));
mysql> CREATE DATABASE archive;
如何做...
例如,如果要将audit_log表重命名为audit_log_archive_2018,可以执行以下操作:
mysql> USE prod;
Database changed
mysql> RENAME TABLE audit_log TO audit_log_archive_2018;
Query OK, 0 rows affected (0.07 sec)
如果要将表从一个数据库移动到另一个数据库,可以使用点表示法指定数据库名称。例如,如果要将名为audit_log的表从名为prod的数据库移动到名为archive的数据库,执行以下操作:
mysql> USE prod
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
mysql> SHOW TABLES;
+------------------------+
| Tables_in_prod |
+------------------------+
| audit_log_archive_2018 |
+------------------------+
1 row in set (0.00 sec)
mysql> RENAME TABLE audit_log_archive_2018 TO archive.audit_log;
Query OK, 0 rows affected (0.03 sec)
mysql> SHOW TABLES;
Empty set (0.00 sec)
mysql> USE archive
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> SHOW TABLES;
+-------------------+
| Tables_in_archive |
+-------------------+
| audit_log |
+-------------------+
1 row in set (0.00 sec)
使用在线模式更改工具修改表
在本节中,您将了解 Percona 的pt-online-schema-change(pt-osc)工具,该工具用于执行ALTER TABLE操作而不会阻塞 DMLs。
pt-osc与 Percona Toolkit 一起提供。Percona Toolkit 的安装已在本章前面进行了描述。
它是如何工作的...
(摘自www.percona.com/doc/percona-toolkit/LATEST/pt-online-schema-change.html。)
pt-online-schema-change的工作原理是创建要更改的表的空副本,根据需要对其进行修改,然后将原始表中的行复制到新表中。复制完成后,它会将原始表移开,并用新表替换。默认情况下,它还会删除原始表。
数据复制过程是以小数据块的形式执行的,这些数据块的大小是不同的,以尝试使它们在特定的时间内执行。在复制过程中对原始表中的数据进行的任何修改都将反映在新表中,因为该工具会在原始表上创建触发器,以更新新表中的相应行。使用触发器意味着如果表上已经定义了任何触发器,该工具将无法工作。
当工具完成将数据复制到新表中后,它会使用原子RENAME TABLE操作同时重命名原始表和新表。完成此操作后,工具会删除原始表。
外键会使工具的操作复杂化并引入额外的风险。在外键引用表时,原子重命名原始表和新表的技术无法正常工作。工具必须在模式更改完成后更新外键以引用新表。该工具支持两种方法来实现这一点。您可以在--alter-foreign-keys-method的文档中了解更多信息。
如何做...
修改列数据类型的方法如下:
shell> pt-online-schema-change D=employees,t=employees,h=localhost -u root --ask-pass --alter="MODIFY COLUMN address VARCHAR(100)" --alter-foreign-keys-method=auto --execute
Enter MySQL password:
No slaves found. See --recursion-method if host server1 has slaves.
Not checking slave lag because no slaves were found and --check-slave-lag was not specified.
Operation, tries, wait:
analyze_table, 10, 1
copy_rows, 10, 0.25
create_triggers, 10, 1
drop_triggers, 10, 1
swap_tables, 10, 1
update_foreign_keys, 10, 1
Child tables:
`employees`.`dept_emp` (approx. 331143 rows)
`employees`.`titles` (approx. 442605 rows)
`employees`.`salaries` (approx. 2838426 rows)
`employees`.`dept_manager` (approx. 24 rows)
Will automatically choose the method to update foreign keys.
Altering `employees`.`employees`...
Creating new table...
Created new table employees._employees_new OK.
Altering new table...
Altered `employees`.`_employees_new` OK.
2017-09-24T09:56:49 Creating triggers...
2017-09-24T09:56:49 Created triggers OK.
2017-09-24T09:56:49 Copying approximately 299478 rows...
2017-09-24T09:56:56 Copied rows OK.
2017-09-24T09:56:56 Max rows for the rebuild_constraints method: 88074
Determining the method to update foreign keys...
2017-09-24T09:56:56 `employees`.`dept_emp`: too many rows: 331143; must use drop_swap
2017-09-24T09:56:56 Drop-swapping tables...
2017-09-24T09:56:56 Analyzing new table...
2017-09-24T09:56:56 Dropped and swapped tables OK.
Not dropping old table because --no-drop-old-table was specified.
2017-09-24T09:56:56 Dropping triggers...
2017-09-24T09:56:56 Dropped triggers OK.
Successfully altered `employees`.`employees`.
您会注意到该工具已经创建了一个具有修改结构的新表,为该表创建了触发器,将行复制到新表中,最后重命名了新表。
如果要更改已经具有触发器的salaries表,您需要指定--preserver-triggers选项,否则将出现错误:The table employees.salaries has triggers but --preserve-triggers was not specified.:
shell> pt-online-schema-change D=employees,t=salaries,h=localhost -u user --ask-pass --alter="MODIFY COLUMN salary int" --alter-foreign-keys-method=auto --execute --no-drop-old-table --preserve-triggers
No slaves found. See --recursion-method if host server1 has slaves.
Not checking slave lag because no slaves were found and --check-slave-lag was not specified.
Operation, tries, wait:
analyze_table, 10, 1
copy_rows, 10, 0.25
create_triggers, 10, 1
drop_triggers, 10, 1
swap_tables, 10, 1
update_foreign_keys, 10, 1
No foreign keys reference `employees`.`salaries`; ignoring --alter-foreign-keys-method.
Altering `employees`.`salaries`...
Creating new table...
Created new table employees._salaries_new OK.
Altering new table...
Altered `employees`.`_salaries_new` OK.
2017-09-24T11:11:58 Creating triggers...
2017-09-24T11:11:58 Created triggers OK.
2017-09-24T11:11:58 Copying approximately 2838045 rows...
2017-09-24T11:12:20 Copied rows OK.
2017-09-24T11:12:20 Adding original triggers to new table.
2017-09-24T11:12:21 Analyzing new table...
2017-09-24T11:12:21 Swapping tables...
2017-09-24T11:12:21 Swapped original and new tables OK.
Not dropping old table because --no-drop-old-table was specified.
2017-09-24T11:12:21 Dropping triggers...
2017-09-24T11:12:21 Dropped triggers OK.
Successfully altered `employees`.`salaries`
如果服务器有从属服务器,该工具在从现有表复制到新表时可能会创建从属延迟。为了避免这种情况,您可以指定--check-slave-lag(默认启用);它会暂停数据复制,直到此副本的延迟小于--max-lag,默认为 1 秒。您可以通过传递--max-lag选项来指定--max-lag。
如果要确保从属服务器的延迟不会超过 10 秒,请传递--max-lag=10:
shell> pt-online-schema-change D=employees,t=employees,h=localhost -u user --ask-pass --alter="MODIFY COLUMN address VARCHAR(100)" --alter-foreign-keys-method=auto --execute --preserve-triggers --max-lag=10
Enter MySQL password:
Found 1 slaves:
server2 -> xx.xxx.xxx.xx:socket
Will check slave lag on:
server2 -> xx.xxx.xxx.xx:socket
Operation, tries, wait:
analyze_table, 10, 1
copy_rows, 10, 0.25
create_triggers, 10, 1
drop_triggers, 10, 1
swap_tables, 10, 1
update_foreign_keys, 10, 1
Child tables:
`employees`.`dept_emp` (approx. 331143 rows)
`employees`.`titles` (approx. 442605 rows)
`employees`.`salaries` (approx. 2838426 rows)
`employees`.`dept_manager` (approx. 24 rows)
Will automatically choose the method to update foreign keys.
Altering `employees`.`employees`...
Creating new table...
Created new table employees._employees_new OK.
Waiting forever for new table `employees`.`_employees_new` to replicate to ubuntu...
Altering new table...
Altered `employees`.`_employees_new` OK.
2017-09-24T12:00:58 Creating triggers...
2017-09-24T12:00:58 Created triggers OK.
2017-09-24T12:00:58 Copying approximately 299342 rows...
2017-09-24T12:01:05 Copied rows OK.
2017-09-24T12:01:05 Max rows for the rebuild_constraints method: 86446
Determining the method to update foreign keys...
2017-09-24T12:01:05 `employees`.`dept_emp`: too many rows: 331143; must use drop_swap
2017-09-24T12:01:05 Skipping triggers creation since --no-swap-tables was specified along with --drop-new-table
2017-09-24T12:01:05 Drop-swapping tables...
2017-09-24T12:01:05 Analyzing new table...
2017-09-24T12:01:05 Dropped and swapped tables OK.
Not dropping old table because --no-drop-old-table was specified.
2017-09-24T12:01:05 Dropping triggers...
2017-09-24T12:01:05 Dropped triggers OK.
Successfully altered `employees`.`employees`.
有关更多详细信息和选项,请参阅 Percona 文档,网址为www.percona.com/doc/percona-toolkit/LATEST/pt-online-schema-change.html。
pt-online-schema-change仅在有主键或唯一键时才有效,否则将出现以下错误:
The new table `employees`.`_employees_new` does not have a PRIMARY KEY or a unique index which is required for the DELETE trigger.
因此,如果表没有任何唯一键,您无法使用pt-online-schema-change。
归档表
有时,您不希望保留旧数据并希望删除它。如果要删除所有上个月访问的行,如果表很小(<10k 行),您可以直接使用以下命令:
DELETE FROM <TABLE> WHERE last_accessed<DATE_ADD(NOW(), INTERVAL -1 MONTH)
如果表很大会发生什么?您知道InnoDB会创建一个UNDO日志来恢复失败的事务。因此,所有删除的行都保存在UNDO日志空间中,以便在DELETE语句中止时用于恢复。不幸的是,如果DELETE语句在中途中止,InnoDB会从UNDO日志空间复制行到表中,这可能会导致表无法访问。
为了克服这种行为,您可以LIMIT删除的行数并COMMIT事务,重复运行相同的操作,直到删除所有不需要的行。
这是一个伪代码示例:
WHILE count<=0:
DELETE FROM <TABLE> WHERE last_accessed<DATE_ADD(NOW(), INTERVAL -1 MONTH) LIMIT 10000;
count=SELECT COUNT(*) FROM <TABLE> WHERE last_accessed<DATE_ADD(NOW(), INTERVAL -1 MONTH);
如果last_accessed上没有INDEX,它可以锁定表。在这种情况下,您需要找出已删除行的主键,并基于PRIMARY KEY进行删除。
这是伪代码,假设id是PRIMARY KEY:
WHILE count<=0:
SELECT id FROM <TABLE> WHERE last_accessed < DATE_ADD(NOW(), INTERVAL -1 MONTH) LIMIT 10000;
DELETE FROM <TABLE> WHERE id IN ('ids from above statement');
count=SELECT COUNT(*) FROM <TABLE> WHERE last_accessed<DATE_ADD(NOW(), INTERVAL -1 MONTH);
您可以使用 Percona 的pt-archiver工具而不是编写删除行的代码,它本质上做的是相同的,并提供许多其他选项,例如将行保存到另一个表或文件中、对负载和复制延迟进行精细控制等。
如何做...
pt-archiver中有许多选项,我们将从简单的清除开始。
清除数据
如果您想要删除employees表中hire_date早于 30 年的所有行,您可以执行以下操作:
shell> pt-archiver --source h=localhost,D=employees,t=employees -u <user> -p<pass> --where="hire_date<DATE_ADD(NOW(), INTERVAL -30 YEAR)" --no-check-charset --limit 10000 --commit-each
您可以通过--source选项传递主机名、数据库名称和表名。您可以使用--limit选项批量限制要删除的行数。
如果您指定--progress,输出是一个标题行,加上间隔的状态输出。状态输出中的每一行都列出了当前日期和时间、pt-archiver运行了多少秒以及它归档了多少行。
如果您指定--statistics,pt-archiver将输出时间和其他信息,以帮助您确定归档过程中哪一部分花费了最多时间。
如果您指定--check-slave-lag,工具将暂停归档,直到从属滞后小于--max-lag。
归档数据
如果您想要在删除后将行保存到单独的表或文件中,可以指定--dest选项。
假设您想要将employees数据库的employees表的所有行移动到employees_archive表中,您可以执行以下操作:
shell> pt-archiver --source h=localhost,D=employees,t=employees --dest h=localhost,D=employees_archive -u <user> -p<pass> --where="1=1" --no-check-charset --limit 10000 --commit-each
如果您指定--where="1=1",它将复制所有行。
复制数据
如果您想要从一张表复制数据到另一张表,您可以使用mysqldump或mysqlpump备份特定行,然后将它们加载到目标表中。作为替代,您也可以使用pt-archive。如果您指定--no-delete选项,pt-archiver将不会从源中删除行。
shell> pt-archiver --source h=localhost,D=employees,t=employees --dest h=localhost,D=employees_archive -u <user> -p<pass> --where="1=1" --no-check-charset --limit 10000 --commit-each --no-delete
另请参阅
有关pt-archiver的更多详细信息和选项,请参阅www.percona.com/doc/percona-toolkit/LATEST/pt-archiver.html。
克隆表
如果您想要克隆一个表,有很多选项。
如何做...
- 使用
INSERT INTO SELECT语句:
mysql> CREATE TABLE employees_clone LIKE employees;
mysql> INSERT INTO employees_clone SELECT * FROM employees;
请注意,如果有任何生成的列,上述语句将不起作用。在这种情况下,您应该提供完整的插入语句,不包括生成的列。
mysql> INSERT INTO employees_clone SELECT * FROM employees;
ERROR 3105 (HY000): The value specified for generated column 'hire_date_year' in table 'employees_clone' is not allowed.
mysql> INSERT INTO employees_clone(emp_no, birth_date, first_name, last_name, gender, hire_date) SELECT emp_no, birth_date, first_name, last_name, gender, hire_date FROM employees;
Query OK, 300024 rows affected (3.21 sec)
Records: 300024 Duplicates: 0 Warnings: 0
但是在大表上,上述语句非常慢且危险。请记住,如果语句失败,为了恢复表状态,InnoDB会将所有行保存在UNDO日志中。
-
使用
mysqldump或mysqlpump备份单个表并在目标上恢复。如果表很大,这可能需要很长时间。 -
使用
Innobackupex备份特定表并将数据文件恢复到目标上。 -
使用
pt-archiver和--no-delete选项,它将把所需的行或所有行复制到目标表中。
您还可以使用可传输表空间来克隆表,这在《第十一章》的管理表空间部分的将文件表空间复制到另一个实例部分中有解释。
分区表
您可以使用分区将单个表的部分分布到文件系统中。用户选择的数据划分规则称为分区函数,可以是模数、简单匹配一组范围或值列表、内部哈希函数或线性哈希函数。
表的不同行可以分配到不同的物理分区,这称为水平分区。MySQL 不支持垂直分区,即将表的不同列分配到不同的物理分区。
有许多分区表的方法:
-
RANGE:这种类型的分区根据列值是否在给定范围内将行分配到分区中。 -
LIST:类似于基于RANGE的分区,只是根据与一组离散值匹配的列选择分区。 -
HASH:使用这种类型的分区,根据用户定义的表达式返回的值选择分区,该表达式在要插入表中的行的列值上操作。该函数可以由 MySQL 中的任何有效表达式组成,产生非负整数值。 -
KEY:这种类型的分区与HASH分区类似,只是提供了要评估的一个或多个列,并且 MySQL 服务器提供了自己的哈希函数。这些列可以包含除整数值以外的其他值,因为 MySQL 提供的哈希函数保证了整数结果,无论列数据类型如何。
前面的每种分区类型都有一个扩展。RANGE有RANGE COLUMNS,LIST有LIST COLUMNS,HASH有LINEAR HASH,KEY有LINEAR KEY。
对于[LINEAR] KEY,RANGE COLUMNS和LIST COLUMNS分区,分区表达式由一个或多个列的列表组成。
在RANGE,LIST和[LINEAR] HASH分区的情况下,分区列的值将传递给分区函数,该函数返回一个整数值,表示应将该特定记录存储在其中的分区的编号。此函数必须是非常数和非随机的。
数据库分区的一个非常常见的用途是按日期对数据进行分隔。
有关分区的优势和其他详细信息,请参阅dev.mysql.com/doc/refman/8.0/en/partitioning-overview.html。
请注意,分区仅适用于InnoDB表,并且外键尚不支持与分区一起使用。
如何做到...
您可以在创建表时指定分区,也可以通过执行ALTER TABLE命令来指定。分区列应该是表中所有唯一键的一部分。
如果您根据created_at列定义了分区,并且id是主键,那么您应该将create_at列包括在PRIMARY KEY中,即(id,created_at)。
以下示例假设没有外键引用到表。
如果您希望在 MySQL 8.0 中基于时间范围或间隔实现分区方案,有两种选择:
-
通过
RANGE对表进行分区,并且对分区表达式使用在DATE,TIME或DATETIME列上操作并返回整数值的函数。 -
通过
RANGE COLUMNS对表进行分区,使用DATE或DATETIME列作为分区列
范围分区
如果您想根据emp_no对employees表进行分区,并且想要在一个分区中保留 100,000 名员工,可以这样创建:
mysql> CREATE TABLE `employees` (
`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,
`address` varchar(100) DEFAULT NULL,
PRIMARY KEY (`emp_no`),
KEY `name` (`first_name`,`last_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE (emp_no)
(PARTITION p0 VALUES LESS THAN (100000) ENGINE = InnoDB,
PARTITION p1 VALUES LESS THAN (200000) ENGINE = InnoDB,
PARTITION p2 VALUES LESS THAN (300000) ENGINE = InnoDB,
PARTITION p3 VALUES LESS THAN (400000) ENGINE = InnoDB,
PARTITION p4 VALUES LESS THAN (500000) ENGINE = InnoDB);
因此,所有emp_no小于 100,000 的员工将进入分区p0,所有emp_no小于200000且大于100000的员工将进入分区p1,依此类推。
如果员工号大于500000,由于没有为它们定义分区,插入将失败并显示错误。为了避免这种情况,您必须定期检查并添加分区,或者创建一个MAXVALUE分区来捕获所有这些异常:
mysql> CREATE TABLE `employees` (
`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,
`address` varchar(100) DEFAULT NULL,
PRIMARY KEY (`emp_no`),
KEY `name` (`first_name`,`last_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE (emp_no)
(PARTITION p0 VALUES LESS THAN (100000) ENGINE = InnoDB,
PARTITION p1 VALUES LESS THAN (200000) ENGINE = InnoDB,
PARTITION p2 VALUES LESS THAN (300000) ENGINE = InnoDB,
PARTITION p3 VALUES LESS THAN (400000) ENGINE = InnoDB,
PARTITION p4 VALUES LESS THAN (500000) ENGINE = InnoDB,
PARTITION pmax VALUES LESS THAN MAXVALUE ENGINE = InnoDB
);
如果您想基于hire_date进行分区,可以使用YEAR(hire_date)函数作为分区表达式:
mysql> CREATE TABLE `employees` (
`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,
`address` varchar(100) DEFAULT NULL,
PRIMARY KEY (`emp_no`,`hire_date`),
KEY `name` (`first_name`,`last_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE (YEAR(hire_date))
(PARTITION p1980 VALUES LESS THAN (1980) ENGINE = InnoDB,
PARTITION p1990 VALUES LESS THAN (1990) ENGINE = InnoDB,
PARTITION p2000 VALUES LESS THAN (2000) ENGINE = InnoDB,
PARTITION p2010 VALUES LESS THAN (2010) ENGINE = InnoDB,
PARTITION p2020 VALUES LESS THAN (2020) ENGINE = InnoDB,
PARTITION pmax VALUES LESS THAN MAXVALUE ENGINE = InnoDB
);
MySQL 中的分区广泛用于date、datetime或timestamp列。如果您想要在数据库中存储一些事件,并且所有查询都基于时间范围,您可以使用这样的分区。
分区函数to_days()返回自0000-01-01以来的天数,这是一个整数:
mysql> CREATE TABLE `event_history` (
`event_id` int(11) NOT NULL,
`event_name` varchar(10) NOT NULL,
`created_at` datetime NOT NULL,
`last_updated` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`event_type` varchar(10) NOT NULL,
`msg` tinytext NOT NULL,
PRIMARY KEY (`event_id`,`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE (to_days(created_at))
(PARTITION p20170930 VALUES LESS THAN (736967) ENGINE = InnoDB,
PARTITION p20171001 VALUES LESS THAN (736968) ENGINE = InnoDB,
PARTITION p20171002 VALUES LESS THAN (736969) ENGINE = InnoDB,
PARTITION p20171003 VALUES LESS THAN (736970) ENGINE = InnoDB,
PARTITION p20171004 VALUES LESS THAN (736971) ENGINE = InnoDB,
PARTITION p20171005 VALUES LESS THAN (736972) ENGINE = InnoDB,
PARTITION p20171006 VALUES LESS THAN (736973) ENGINE = InnoDB,
PARTITION p20171007 VALUES LESS THAN (736974) ENGINE = InnoDB,
PARTITION p20171008 VALUES LESS THAN (736975) ENGINE = InnoDB,
PARTITION p20171009 VALUES LESS THAN (736976) ENGINE = InnoDB,
PARTITION p20171010 VALUES LESS THAN (736977) ENGINE = InnoDB,
PARTITION p20171011 VALUES LESS THAN (736978) ENGINE = InnoDB,
PARTITION p20171012 VALUES LESS THAN (736979) ENGINE = InnoDB,
PARTITION p20171013 VALUES LESS THAN (736980) ENGINE = InnoDB,
PARTITION p20171014 VALUES LESS THAN (736981) ENGINE = InnoDB,
PARTITION p20171015 VALUES LESS THAN (736982) ENGINE = InnoDB,
PARTITION pmax VALUES LESS THAN MAXVALUE ENGINE = InnoDB
);
如果要将现有表转换为分区表,并且分区键不是PRIMARY KEY的一部分,则需要删除PRIMARY KEY并将分区键作为PRIMARY KEY和所有唯一键的一部分添加。否则,您将收到错误ERROR 1503 (HY000): A PRIMARY KEY must include all columns in the table's partitioning function.。您可以按以下方式执行:
mysql> ALTER TABLE employees DROP PRIMARY KEY, ADD PRIMARY KEY(emp_no,hire_date);
Query OK, 0 rows affected (0.11 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> ALTER TABLE employees PARTITION BY RANGE (YEAR(hire_date))
(PARTITION p1980 VALUES LESS THAN (1980) ENGINE = InnoDB,
PARTITION p1990 VALUES LESS THAN (1990) ENGINE = InnoDB,
PARTITION p2000 VALUES LESS THAN (2000) ENGINE = InnoDB,
PARTITION p2010 VALUES LESS THAN (2010) ENGINE = InnoDB,
PARTITION p2020 VALUES LESS THAN (2020) ENGINE = InnoDB,
PARTITION pmax VALUES LESS THAN MAXVALUE ENGINE = InnoDB
);
Query OK, 300025 rows affected (4.71 sec)
Records: 300025 Duplicates: 0 Warnings: 0
有关RANGE分区的更多详细信息,请参阅dev.mysql.com/doc/refman/8.0/en/partitioning-range.html。
移除分区
如果您希望移除分区,可以执行REMOVE PARTITIONING语句:
mysql> ALTER TABLE employees REMOVE PARTITIONING;
Query OK, 0 rows affected (0.09 sec)
Records: 0 Duplicates: 0 Warnings: 0
RANGE COLUMNS分区
RANGE COLUMNS分区类似于RANGE分区,但允许您基于多个列值的范围定义分区。此外,您可以使用除整数类型以外的列来定义范围。RANGE COLUMNS分区与RANGE分区有以下显著不同:
-
RANGE COLUMNS不接受表达式,只接受列名 -
RANGE COLUMNS接受一个或多个列的列表 -
RANGE COLUMNS分区列不限于整数列;字符串、DATE和DATETIME列也可以用作分区列
您可以直接在RANGE COLUMNS中使用hire_date列,而不是使用to_days()或year()函数:
mysql> ALTER TABLE employees
PARTITION BY RANGE COLUMNS (hire_date)
(PARTITION p0 VALUES LESS THAN ('1970-01-01'),
PARTITION p1 VALUES LESS THAN ('1980-01-01'),
PARTITION p2 VALUES LESS THAN ('1990-01-01'),
PARTITION p3 VALUES LESS THAN ('2000-01-01'),
PARTITION p4 VALUES LESS THAN ('2010-01-01'),
PARTITION p5 VALUES LESS THAN (MAXVALUE)
);
Query OK, 300025 rows affected (4.71 sec)
Records: 300025 Duplicates: 0 Warnings: 0
或者您可以根据他们的last_name来划分员工。这将不能保证在分区之间的均匀分布:
mysql> ALTER TABLE employees
PARTITION BY RANGE COLUMNS (last_name)
(PARTITION p0 VALUES LESS THAN ('b'),
PARTITION p1 VALUES LESS THAN ('f'),
PARTITION p2 VALUES LESS THAN ('l'),
PARTITION p3 VALUES LESS THAN ('q'),
PARTITION p4 VALUES LESS THAN ('u'),
PARTITION p5 VALUES LESS THAN ('z')
);
Query OK, 300025 rows affected (4.71 sec)
Records: 300025 Duplicates: 0 Warnings: 0
使用RANGE COLUMNS,您可以在分区函数中放置多个列:
mysql> CREATE TABLE range_columns_example (
a INT,
b INT,
c INT,
d INT,
e INT,
PRIMARY KEY(a, b, c)
)
PARTITION BY RANGE COLUMNS(a,b,c) (
PARTITION p0 VALUES LESS THAN (0,25,50),
PARTITION p1 VALUES LESS THAN (10,50,100),
PARTITION p2 VALUES LESS THAN (10,100,200),
PARTITION p3 VALUES LESS THAN (MAXVALUE,MAXVALUE,MAXVALUE)
);
如果插入值a=10、b=20、c=100、d=100、e=100,它将进入p1。在设计按RANGE COLUMNS分区的表时,您可以通过使用mysql客户端来测试连续的分区定义,如下所示:
mysql> SELECT (10,20,100) < (0,25,50) p0, (10,20,100) < (10,50,100) p1, (10,20,100) < (10,100,200) p2;
+----+----+----+
| p0 | p1 | p2 |
+----+----+----+
| 0 | 1 | 1 |
+----+----+----+
1 row in set (0.00 sec)
在这种情况下,插入将进入p1。
LIST 和 LIST COLUMNS 分区
LIST分区类似于RANGE分区,每个分区根据列值在一组值列表中的成员资格而不是一组连续值范围中的成员资格来定义和选择。
您需要通过PARTITION BY LIST(<expr>)来定义,其中expr是一个列值或基于列值并返回整数值的表达式。
分区定义包含VALUES IN (<value_list>),其中value_list是一个逗号分隔的整数列表,而不是VALUES LESS THAN (<value>)。
如果您希望使用除整数以外的数据类型,可以使用LIST COLUMNS。
与RANGE分区不同,没有MAXVALUE这样的catch-all;分区表达式中应包含分区表达式的所有预期值。
假设有一个带有邮政编码和城市的客户表。例如,如果您想要将具有特定邮政编码的客户划分到一个分区中,您可以使用LIST分区:
mysql> CREATE TABLE customer (
customer_id INT,
zipcode INT,
city varchar(100),
PRIMARY KEY (customer_id, zipcode)
)
PARTITION BY LIST(zipcode) (
PARTITION pnorth VALUES IN (560030, 560007, 560051, 560084),
PARTITION peast VALUES IN (560040, 560008, 560061, 560085),
PARTITION pwest VALUES IN (560050, 560009, 560062, 560086),
PARTITION pcentral VALUES IN (560060, 560010, 560063, 560087)
);
如果您希望直接使用列而不是整数,可以使用LIST COLUMNS:
mysql> CREATE TABLE customer (
customer_id INT,
zipcode INT,
city varchar(100),
PRIMARY KEY (customer_id, city)
)
PARTITION BY LIST COLUMNS(city) (
PARTITION pnorth VALUES IN ('city1','city2','city3'),
PARTITION peast VALUES IN ('city4','city5','city6'),
PARTITION pwest VALUES IN ('city7','city8','city9'),
PARTITION pcentral VALUES IN ('city10','city11','city12')
);
HASH 和 LINEAR HASH 分区
使用HASH进行分区主要是为了确保数据在预定数量的分区中均匀分布。对于范围或列表分区,您必须明确指定给定列值或列值集应存储在哪个分区;而对于哈希分区,这个决定已经为您处理,您只需要指定一个要进行哈希处理的列值或基于列值的表达式,以及要将分区表分成的分区数。
如果要均匀分配员工,可以使用YEAR(hire_date)的HASH并指定分区的数量,而不是对YEAR(hire_date)进行RANGE分区。当使用PARTITION BY HASH时,存储引擎根据表达式的结果的模来确定要使用的分区。
例如,如果hire_date是1987-11-28,YEAR(hire_date)将是1987,MOD(1987,8)是3。因此,行进入第三个分区:
mysql> CREATE TABLE `employees` (
`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,
`address` varchar(100) DEFAULT NULL,
PRIMARY KEY (`emp_no`,`hire_date`),
KEY `name` (`first_name`,`last_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY HASH(YEAR(hire_date))
PARTITIONS 8;
最有效的哈希函数是对单个表列进行操作,并且其值随着列值的增加或减少而一致变化。
在LINEAR HASH分区中,您可以使用相同的语法,只是添加一个LINEAR关键字。MySQL 使用二的幂算法来确定分区,而不是使用MODULUS操作。有关更多详细信息,请参阅dev.mysql.com/doc/refman/8.0/en/partitioning-linear-hash.html:
mysql> CREATE TABLE `employees` (
`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,
`address` varchar(100) DEFAULT NULL,
PRIMARY KEY (`emp_no`,`hire_date`),
KEY `name` (`first_name`,`last_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY LINEAR HASH(YEAR(hire_date))
PARTITIONS 8;
KEY 和 LINEAR KEY 分区
按键分区类似于按哈希分区,只是哈希分区使用用户定义的表达式,而键分区的哈希函数由 MySQL 服务器提供。这个内部哈希函数基于与PASSWORD()函数相同的算法。
KEY只接受零个或多个列名的列表。如果将用作分区键的列,必须包括表的主键的一部分或全部,如果表有主键。如果未指定列名作为分区键,则使用表的主键,如果有的话:
mysql> CREATE TABLE `employees` (
`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,
`address` varchar(100) DEFAULT NULL,
PRIMARY KEY (`emp_no`,`hire_date`),
KEY `name` (`first_name`,`last_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY KEY()
PARTITIONS 8;
子分区
您可以进一步将每个分区划分为分区表。这称为子分区或复合分区:
mysql> CREATE TABLE `employees` (
`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,
`address` varchar(100) DEFAULT NULL,
PRIMARY KEY (`emp_no`,`hire_date`),
KEY `name` (`first_name`,`last_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE( YEAR(hire_date) )
SUBPARTITION BY HASH(emp_no)
SUBPARTITIONS 4 (
PARTITION p0 VALUES LESS THAN (1990),
PARTITION p1 VALUES LESS THAN (2000),
PARTITION p2 VALUES LESS THAN (2010),
PARTITION p3 VALUES LESS THAN (2020),
PARTITION p4 VALUES LESS THAN MAXVALUE
);
分区修剪和选择
MySQL 不会扫描没有匹配值的分区;这是自动的,称为分区修剪。MySQL 优化器评估给定值的分区表达式,确定包含该值的分区,并仅扫描该分区。
SELECT、DELETE和UPDATE语句支持分区修剪。INSERT语句目前无法修剪。
您还可以明确指定匹配给定WHERE条件的分区和子分区。
如何做...
分区修剪仅适用于查询,但支持对查询和多个 DML 语句进行分区的显式分区选择。
分区修剪
以基于emp_no进行分区的employees表为例:
mysql> CREATE TABLE `employees` (
`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,
`address` varchar(100) DEFAULT NULL,
PRIMARY KEY (`emp_no`,`hire_date`),
KEY `name` (`first_name`,`last_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE (YEAR(hire_date))
(PARTITION p1980 VALUES LESS THAN (1980) ENGINE = InnoDB,
PARTITION p1990 VALUES LESS THAN (1990) ENGINE = InnoDB,
PARTITION p2000 VALUES LESS THAN (2000) ENGINE = InnoDB,
PARTITION p2010 VALUES LESS THAN (2010) ENGINE = InnoDB,
PARTITION p2020 VALUES LESS THAN (2020) ENGINE = InnoDB,
PARTITION pmax VALUES LESS THAN MAXVALUE ENGINE = InnoDB
);
假设执行以下SELECT查询:
mysql> SELECT last_name,birth_date FROM employees WHERE hire_date='1999-02-01' AND first_name='Mariangiola';
MySQL 优化器检测到查询中使用了分区列,并自动确定要扫描的分区。
在此查询中,首先计算YEAR('1999-02-01'),即1999,然后扫描p2000分区而不是整个表。这大大减少了查询时间。
如果给出的不是hire_date='1999-02-01',而是一个范围,比如hire_date>='1999-02-01',那么将扫描p2000、p2010、p2020和pmax分区。
如果在WHERE子句中没有给出hire_date='1999-02-01'表达式,MySQL 必须扫描整个表。
要了解优化器扫描的分区,可以执行查询的EXPLAIN计划,该计划在第十三章的Explain plan部分中有解释,性能调整:
mysql> EXPLAIN SELECT last_name,birth_date FROM employees WHERE hire_date='1999-02-01' AND first_name='Mariangiola'\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: p2000
type: ref
possible_keys: name
key: name
key_len: 58
ref: const
rows: 120
filtered: 10.00
Extra: Using index condition
mysql> EXPLAIN SELECT last_name,birth_date FROM employees WHERE hire_date>='1999-02-01' AND first_name='Mariangiola'\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: p2000,p2010,p2020,pmax
type: ref
possible_keys: name
key: name
key_len: 58
ref: const
rows: 121
filtered: 33.33
Extra: Using index condition
1 row in set, 1 warning (0.00 sec)
分区选择
分区修剪是基于WHERE子句的自动选择。您可以在查询中明确指定要扫描的分区。查询可以是SELECT、DELETE、INSERT、REPLACE、UPDATE、LOAD DATA和LOAD XML。PARTITION选项用于从给定表中选择分区,您应该在所有其他选项之前,包括任何表别名,指定关键字PARTITION
mysql> SELECT emp_no,hire_date FROM employees PARTITION (p1990) LIMIT 10;
+--------+------------+
| emp_no | hire_date |
+--------+------------+
| 413688 | 1989-12-10 |
| 242368 | 1989-08-06 |
| 283280 | 1985-11-22 |
| 405098 | 1985-11-16 |
| 30404 | 1985-07-17 |
| 419259 | 1988-03-21 |
| 466254 | 1986-11-28 |
| 428971 | 1986-12-13 |
| 94467 | 1987-01-28 |
| 259555 | 1987-07-30 |
+--------+------------+
10 rows in set (0.00 sec)
同样,我们可以删除:
mysql> DELETE FROM employees PARTITION (p1980, p1990) WHERE first_name LIKE 'j%';
Query OK, 7001 rows affected (0.12 sec)
分区管理
在管理分区时最重要的是提前添加足够的分区以进行基于时间的RANGE分区。如果未能这样做,将在插入时出现错误,或者如果定义了MAXVALUE分区,则所有插入都将进入MAXVALUE分区。例如,考虑没有pmax分区的event_history表:
mysql> CREATE TABLE `event_history` (
`event_id` int(11) NOT NULL,
`event_name` date NOT NULL,
`created_at` datetime NOT NULL,
`last_updated` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`event_type` varchar(10) NOT NULL,
`msg` tinytext NOT NULL,
PRIMARY KEY (`event_id`,`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE (to_days(created_at))
(PARTITION p20170930 VALUES LESS THAN (736967) ENGINE = InnoDB,
PARTITION p20171001 VALUES LESS THAN (736968) ENGINE = InnoDB,
PARTITION p20171002 VALUES LESS THAN (736969) ENGINE = InnoDB,
PARTITION p20171003 VALUES LESS THAN (736970) ENGINE = InnoDB,
PARTITION p20171004 VALUES LESS THAN (736971) ENGINE = InnoDB,
PARTITION p20171005 VALUES LESS THAN (736972) ENGINE = InnoDB,
PARTITION p20171006 VALUES LESS THAN (736973) ENGINE = InnoDB,
PARTITION p20171007 VALUES LESS THAN (736974) ENGINE = InnoDB,
PARTITION p20171008 VALUES LESS THAN (736975) ENGINE = InnoDB,
PARTITION p20171009 VALUES LESS THAN (736976) ENGINE = InnoDB,
PARTITION p20171010 VALUES LESS THAN (736977) ENGINE = InnoDB,
PARTITION p20171011 VALUES LESS THAN (736978) ENGINE = InnoDB,
PARTITION p20171012 VALUES LESS THAN (736979) ENGINE = InnoDB,
PARTITION p20171013 VALUES LESS THAN (736980) ENGINE = InnoDB,
PARTITION p20171014 VALUES LESS THAN (736981) ENGINE = InnoDB,
PARTITION p20171015 VALUES LESS THAN (736982) ENGINE = InnoDB
);
该表接受INSERTS直到 2017 年 10 月 15 日;之后,INSERTS将失败。
另一个重要的事情是在数据超过保留期限后进行DELETE。
如何做...
要执行这些操作,您需要执行ALTER命令。
添加分区
要添加新分区,请执行ADD PARTITION (<PARTITION DEFINITION>)语句:
mysql> ALTER TABLE event_history ADD PARTITION (
PARTITION p20171016 VALUES LESS THAN (736983) ENGINE = InnoDB,
PARTITION p20171017 VALUES LESS THAN (736984) ENGINE = InnoDB
);
此语句会锁定整个表的时间非常短。
重新组织分区
如果存在MAXVALUE分区,则无法在MAXVALUE之后添加分区;在这种情况下,您需要将REORGANIZE MAXVALUE分区分成两个分区:
mysql> ALTER TABLE event_history REORGANIZE PARTITION pmax INTO (PARTITION p20171016 VALUES LESS THAN (736983) ENGINE = InnoDB,
PARTITION pmax VALUES LESS THAN MAXVALUE ENGINE = InnoDB);
请记住,当重新组织分区时,MySQL 必须大幅移动数据,表在此期间将被锁定。
您还可以将多个分区重新组织为单个分区:
mysql> ALTER TABLE event_history REORGANIZE PARTITION p20171001,p20171002,p20171003,p20171004,p20171005,p20171006,p20171007
INTO (PARTITION p2017_oct_week1 VALUES LESS THAN (736974));
删除分区
如果数据已经超过保留期限,您可以使用DROP整个分区,与传统的DELETE FROM TABLE语句相比,这是非常快速的。这对于高效地存档数据非常有帮助。
如果p20170930已经超过了保留期限,您可以使用ALTER TABLE ... DROP PARTITION语句删除该分区:
mysql> ALTER TABLE event_history DROP PARTITION p20170930;
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0
删除分区会从表中删除PARTITION DEFINITION。
截断分区
如果您希望在表中保留PARTITION DEFINITION并仅删除数据,可以执行TRUNCATE PARTITION命令:
mysql> ALTER TABLE event_history TRUNCATE PARTITION p20171001;
Query OK, 0 rows affected (0.08 sec)
管理 HASH 和 KEY 分区
对HASH和KEY分区执行的操作是完全不同的。您只能减少或增加分区的数量。
假设employees表是基于HASH进行分区的:
mysql> CREATE TABLE `employees` (
`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,
`address` varchar(100) DEFAULT NULL,
PRIMARY KEY (`emp_no`,`hire_date`),
KEY `name` (`first_name`,`last_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY HASH(YEAR(hire_date))
PARTITIONS 8;
要将分区从8减少到6,您可以执行COALESCE PARTITION语句,并指定要减少的分区数,即8-6=2:
mysql> ALTER TABLE employees COALESCE PARTITION 2;
Query OK, 0 rows affected (0.31 sec)
Records: 0 Duplicates: 0 Warnings: 0
要将分区从6增加到16,您可以执行ADD PARTITION语句,并指定要增加的分区数,即16-6=10:
mysql> ALTER TABLE employees ADD PARTITION PARTITIONS 10;
Query OK, 0 rows affected (5.11 sec)
Records: 0 Duplicates: 0 Warnings: 0
其他操作
您还可以执行其他操作,例如REBUILD,OPTIMIZE,ANALYZE和REPAIR语句,例如:
mysql> ALTER TABLE event_history REPAIR PARTITION p20171009, p20171010;
分区信息
本节讨论了获取有关现有分区的信息,可以通过多种方式完成。
如何做...
让我们深入了解一下。
使用 SHOW CREATE TABLE
要知道表是否已分区,可以执行SHOW CREATE TABLE\G语句,该语句显示了表定义以及分区,例如:
mysql> SHOW CREATE TABLE employees \G
*************************** 1\. row ***************************
Table: employees
Create Table: CREATE TABLE `employees` (
`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,
`address` varchar(100) DEFAULT NULL,
PRIMARY KEY (`emp_no`,`hire_date`),
KEY `name` (`first_name`,`last_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
/*!50100 PARTITION BY RANGE (YEAR(hire_date))
(PARTITION p1980 VALUES LESS THAN (1980) ENGINE = InnoDB,
PARTITION p1990 VALUES LESS THAN (1990) ENGINE = InnoDB,
PARTITION p2000 VALUES LESS THAN (2000) ENGINE = InnoDB,
PARTITION p2010 VALUES LESS THAN (2010) ENGINE = InnoDB,
PARTITION p2020 VALUES LESS THAN (2020) ENGINE = InnoDB,
PARTITION pmax VALUES LESS THAN MAXVALUE ENGINE = InnoDB) */
使用 SHOW TABLE STATUS
您可以执行SHOW TABLE STATUS命令,并在输出中检查Create_options:
mysql> SHOW TABLE STATUS LIKE 'employees'\G
*************************** 1\. row ***************************
Name: employees
Engine: InnoDB
Version: 10
Row_format: Dynamic
Rows: NULL
Avg_row_length: NULL
Data_length: NULL
Max_data_length: NULL
Index_length: NULL
Data_free: NULL
Auto_increment: NULL
Create_time: 2017-10-01 05:01:53
Update_time: NULL
Check_time: NULL
Collation: utf8mb4_0900_ai_ci
Checksum: NULL
Create_options: partitioned
Comment:
1 row in set (0.00 sec)
使用 EXPLAIN
EXPLAIN计划显示了查询所扫描的所有分区。如果您对SELECT * FROM <table>运行EXPLAIN计划,它将列出所有分区,例如:
mysql> EXPLAIN SELECT * FROM employees\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: p1980,p1990,p2000,p2010,p2020,pmax
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 292695
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
查询 INFORMATION_SCHEMA.PARTITIONS 表
与所有前面的方法相比,INFORMATION_SCHEMA.PARTITIONS提供了有关分区的更多信息:
mysql> SHOW CREATE TABLE INFORMATION_SCHEMA.PARTITIONS\G
*************************** 1\. row ***************************
Table: PARTITIONS
Create Table: CREATE TEMPORARY TABLE `PARTITIONS` (
`TABLE_CATALOG` varchar(512) NOT NULL DEFAULT '',
`TABLE_SCHEMA` varchar(64) NOT NULL DEFAULT '',
`TABLE_NAME` varchar(64) NOT NULL DEFAULT '',
`PARTITION_NAME` varchar(64) DEFAULT NULL,
`SUBPARTITION_NAME` varchar(64) DEFAULT NULL,
`PARTITION_ORDINAL_POSITION` bigint(21) unsigned DEFAULT NULL,
`SUBPARTITION_ORDINAL_POSITION` bigint(21) unsigned DEFAULT NULL,
`PARTITION_METHOD` varchar(18) DEFAULT NULL,
`SUBPARTITION_METHOD` varchar(12) DEFAULT NULL,
`PARTITION_EXPRESSION` longtext,
`SUBPARTITION_EXPRESSION` longtext,
`PARTITION_DESCRIPTION` longtext,
`TABLE_ROWS` bigint(21) unsigned NOT NULL DEFAULT '0',
`AVG_ROW_LENGTH` bigint(21) unsigned NOT NULL DEFAULT '0',
`DATA_LENGTH` bigint(21) unsigned NOT NULL DEFAULT '0',
`MAX_DATA_LENGTH` bigint(21) unsigned DEFAULT NULL,
`INDEX_LENGTH` bigint(21) unsigned NOT NULL DEFAULT '0',
`DATA_FREE` bigint(21) unsigned NOT NULL DEFAULT '0',
`CREATE_TIME` datetime DEFAULT NULL,
`UPDATE_TIME` datetime DEFAULT NULL,
`CHECK_TIME` datetime DEFAULT NULL,
`CHECKSUM` bigint(21) unsigned DEFAULT NULL,
`PARTITION_COMMENT` varchar(80) NOT NULL DEFAULT '',
`NODEGROUP` varchar(12) NOT NULL DEFAULT '',
`TABLESPACE_NAME` varchar(64) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
要了解有关表分区的更多详细信息,您可以通过指定数据库名称和表名称来查询INFORMATION_SCHEMA.PARTITIONS表,例如:
mysql> SELECT PARTITION_NAME FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_SCHEMA='employees' AND TABLE_NAME='employees';
+----------------+
| PARTITION_NAME |
+----------------+
| p1980 |
| p1990 |
| p2000 |
| p2010 |
| p2020 |
| pmax |
+----------------+
6 rows in set (0.00 sec)
您可以在该分区中获取诸如PARTITION_METHOD,PARTITION_EXPRESSION,PARTITION_DESCRIPTION和TABLE_ROWS等详细信息:
mysql> SELECT * FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_SCHEMA='employees' AND TABLE_NAME='employees' AND PARTITION_NAME='p1990'\G
*************************** 1\. row ***************************
TABLE_CATALOG: def
TABLE_SCHEMA: employees
TABLE_NAME: employees
PARTITION_NAME: p1990
SUBPARTITION_NAME: NULL
PARTITION_ORDINAL_POSITION: 2
SUBPARTITION_ORDINAL_POSITION: NULL
PARTITION_METHOD: RANGE
SUBPARTITION_METHOD: NULL
PARTITION_EXPRESSION: YEAR(hire_date)
SUBPARTITION_EXPRESSION: NULL
PARTITION_DESCRIPTION: 1990
TABLE_ROWS: 157588
AVG_ROW_LENGTH: 56
DATA_LENGTH: 8929280
MAX_DATA_LENGTH: NULL
INDEX_LENGTH: 8929280
DATA_FREE: 0
CREATE_TIME: NULL
UPDATE_TIME: NULL
CHECK_TIME: NULL
CHECKSUM: NULL
PARTITION_COMMENT:
NODEGROUP: default
TABLESPACE_NAME: NULL
1 row in set (0.00 sec)
有关更多详细信息,请参阅dev.mysql.com/doc/refman/8.0/en/partitions-table.html。
高效管理生存时间和软删除行
RANGE COLUMNS在管理生存期和软删除行方面非常有用。假设您有一个应用程序,它指定了行的到期时间(在超过到期时间后将被删除的行),并且到期时间是变化的。
假设应用程序可以执行以下类型的插入:
-
插入持久数据
-
带有到期日的插入
如果到期时间是恒定的,即所有插入的行都将在一定时间后被删除,我们可以使用范围分区。但是,如果到期时间是变化的,即一些行将在一周内被删除,一些将在一个月内被删除,一些将在一年内被删除,一些没有到期时间,那么就不可能创建分区。在这种情况下,您可以使用下面解释的RANGE COLUMNS分区。
它是如何工作的...
我们引入一个名为soft_delete的列,将由触发器设置。soft_delete列将成为范围列分区的一部分。
分区将是(soft_delete,expires)。soft_delete和 expires 共同控制行应该进入哪个分区。soft_delete 列决定了行的保留。如果 expires 为 0,则触发器将soft_delete值设置为 0,将行放入no_retention分区,如果 expires 的值超出分区范围,触发器将soft_delete值设置为 1,并将行放入long_retention分区。如果 expires 的值在分区范围内,触发器将soft_delete值设置为2。根据 expires 的值,行将被放入相应的分区。
总之,soft_delete将是:
-
0:如果过期值为 0 -
1:如果过期时间距离时间戳超过 30 天 -
2:如果过期时间距离时间戳不到 30 天
我们创建
-
1 个
no_retention分区(soft_delete = 0) -
1 个
long_retention分区(soft_delete = 1) -
8 个每日分区(
soft_delete = 2)
如何做...
您可以创建一个如下的表:
mysql> CREATE TABLE `customer_data` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`msg` text,
`timestamp` bigint(20) NOT NULL DEFAULT '0',
`expires` bigint(20) NOT NULL DEFAULT '0',
`soft_delete` tinyint(3) unsigned NOT NULL DEFAULT '1',
PRIMARY KEY (`id`,`expires`,`soft_delete`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
/*!50500 PARTITION BY RANGE COLUMNS(soft_delete,expires)
(PARTITION no_retention VALUES LESS THAN (0,MAXVALUE) ENGINE = InnoDB,
PARTITION long_retention VALUES LESS THAN (1,MAXVALUE) ENGINE = InnoDB,
PARTITION pd20171017 VALUES LESS THAN (2,1508198400000) ENGINE = InnoDB,
PARTITION pd20171018 VALUES LESS THAN (2,1508284800000) ENGINE = InnoDB,
PARTITION pd20171019 VALUES LESS THAN (2,1508371200000) ENGINE = InnoDB,
PARTITION pd20171020 VALUES LESS THAN (2,1508457600000) ENGINE = InnoDB,
PARTITION pd20171021 VALUES LESS THAN (2,1508544000000) ENGINE = InnoDB,
PARTITION pd20171022 VALUES LESS THAN (2,1508630400000) ENGINE = InnoDB,
PARTITION pd20171023 VALUES LESS THAN (2,1508716800000) ENGINE = InnoDB,
PARTITION pd20171024 VALUES LESS THAN (3,1508803200000) ENGINE = InnoDB,
PARTITION pd20171025 VALUES LESS THAN (3,1508869800000) ENGINE = InnoDB,
PARTITION pd20171026 VALUES LESS THAN (3,1508956200000) ENGINE = InnoDB) */;
将有一个缓冲周分区,将会在 42 天后,并且始终为空,以便我们可以分割和 7+2 个每日分区,带有 2 个缓冲。
mysql> DROP TRIGGER IF EXISTS customer_data_insert;
DELIMITER $$
CREATE TRIGGER customer_data_insert
BEFORE INSERT
ON customer_data FOR EACH ROW
BEGIN
SET NEW.soft_delete = (IF((NEW.expires = 0),0,IF((ROUND((((((NEW.expires - NEW.timestamp) / 1000) / 60) / 60) / 24),0) <= 7),2,1)));
END;
$$
DELIMITER ;
mysql> DROP TRIGGER IF EXISTS customer_data_update;
DELIMITER $$
CREATE TRIGGER customer_data_update
BEFORE UPDATE
ON customer_data FOR EACH ROW
BEGIN
SET NEW.soft_delete = (IF((NEW.expires = 0),0,IF((ROUND((((((NEW.expires - NEW.timestamp) / 1000) / 60) / 60) / 24),0) <= 7),2,1)));
END;
$$
DELIMITER ;
- 假设客户端插入了一个时间戳为 1508265000(2017-10-17 18:30:00)并且到期值为 1508351400(2017-10-18 18:30:00)的行,soft_delete 将为 2,这将使其进入分区 pd20171019
mysql> INSERT INTO customer_data(id, msg, timestamp, expires) VALUES(1,'test',1508265000000,1508351400000);
Query OK, 1 row affected (0.05 sec)
mysql> SELECT * FROM customer_data PARTITION (pd20171019);
+----+------+---------------+---------------+-------------+
| id | msg | timestamp | expires | soft_delete |
+----+------+---------------+---------------+-------------+
| 1 | test | 1508265000000 | 1508351400000 | 2 |
+----+------+---------------+---------------+-------------+
1 row in set (0.00 sec)
- 假设客户端没有设置到期时间,expires 列将为 0,这将使
soft_delete为0,并且将进入no_retention分区。
mysql> INSERT INTO customer_data(id, msg, timestamp, expires) VALUES(2,'non_expiry_row',1508265000000,0);
Query OK, 1 row affected (0.07 sec)
mysql> SELECT * FROM customer_data PARTITION (no_retention);
+----+----------------+---------------+---------+-------------+
| id | msg | timestamp | expires | soft_delete |
+----+----------------+---------------+---------+-------------+
| 2 | non_expiry_row | 1508265000000 | 0 | 0 |
+----+----------------+---------------+---------+-------------+
1 row in set (0.00 sec)
- 假设客户端希望设置到期时间(假设为 2017-10-19 06:30:00),到期列可以更新,这将把行从
no_retention分区移动到相应的分区(这会有一些性能影响,因为行必须在分区之间移动)
mysql> UPDATE customer_data SET expires=1508394600000 WHERE id=2;
Query OK, 1 row affected (0.06 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> SELECT * FROM customer_data PARTITION (no_retention);
Empty set (0.00 sec)
mysql> SELECT * FROM customer_data PARTITION (pd20171020);
+----+----------------+---------------+---------------+-------------+
| id | msg | timestamp | expires | soft_delete |
+----+----------------+---------------+---------------+-------------+
| 2 | non_expiry_row | 1508265000000 | 1508394600000 | 2 |
+----+----------------+---------------+---------------+-------------+
1 row in set (0.00 sec)
- 假设客户端设置了一个超出我们分区范围的到期时间,它将自动进入
long_retention分区。
mysql> INSERT INTO customer_data(id, msg, timestamp, expires) VALUES(3,'long_expiry',1507852800000,1608025600000);
mysql> SELECT * FROM customer_data PARTITION (long_retention);
+----+-------------+---------------+---------------+-------------+
| id | msg | timestamp | expires | soft_delete |
+----+-------------+---------------+---------------+-------------+
| 3 | long_expiry | 1507852800000 | 1608025600000 | 1 |
+----+-------------+---------------+---------------+-------------+
1 row in set (0.00 sec)
如果更新soft_delete,则跨分区移动行的速度很慢,行将从默认分区移动到其他分区。
扩展逻辑
我们可以扩展逻辑并增加soft_delete的值,以适应更多类型的分区。
-
0:如果过期值为 0 -
3:如果过期时间距离时间戳不到 7 天 -
2:如果过期时间距离时间戳不到 60 天 -
1:如果过期时间距离时间戳超过 60 天
soft_delete列将成为分区的一部分。我们创建
-
单个
no_retention分区,如果soft_delete的值为0 -
单个
long_retention分区,如果soft_delete 1的值 -
每周分区,如果
soft_delete 2的值 -
每日分区,如果
soft_delete 3的值
示例分区表结构
将有一个缓冲周分区,将会在 42 天后,并且始终为空,以便我们可以分割和 7+2 个每日分区,带有 2 个缓冲。
mysql> DROP TRIGGER IF EXISTS customer_data_insert;
DELIMITER $$
CREATE TRIGGER customer_data_insert
BEFORE INSERT
ON customer_data FOR EACH ROW
BEGIN
SET NEW.soft_delete = (IF((NEW.expires = 0),0,IF((ROUND((((((NEW.expires - NEW.timestamp) / 1000) / 60) / 60) / 24),0) <= 7),3,IF((ROUND((((((NEW.expires - NEW.timestamp) / 1000) / 60) / 60) / 24),0) <= 42),2,1))));
END;
$$
DELIMITER ;
mysql> DROP TRIGGER IF EXISTS customer_data_update;
DELIMITER $$
CREATE TRIGGER customer_data_update
BEFORE INSERT
ON customer_data FOR EACH ROW
BEGIN
SET NEW.soft_delete = (IF((NEW.expires = 0),0,IF((ROUND((((((NEW.expires - NEW.timestamp) / 1000) / 60) / 60) / 24),0) <= 7),3,IF((ROUND((((((NEW.expires - NEW.timestamp) / 1000) / 60) / 60) / 24),0) <= 42),2,1))));
END;
$$
DELIMITER ;
mysql> CREATE TABLE `customer_data` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`msg` text,
`timestamp` bigint(20) NOT NULL DEFAULT '0',
`expires` bigint(20) NOT NULL DEFAULT '0',
`soft_delete` tinyint(3) unsigned NOT NULL DEFAULT '1',
PRIMARY KEY (`id`,`expires`,`soft_delete`)
) ENGINE=InnoDB AUTO_INCREMENT=609585360 DEFAULT CHARSET=utf8
/*!50500 PARTITION BY RANGE COLUMNS(`soft_delete`,`expires`)
(
PARTITION no_retention VALUES LESS THAN (0,MAXVALUE) ENGINE = InnoDB,
PARTITION long_retention VALUES LESS THAN (1,MAXVALUE) ENGINE = InnoDB,
PARTITION pw20171022 VALUES LESS THAN (2,1508630400000) ENGINE = InnoDB,
PARTITION pw20171029 VALUES LESS THAN (2,1509235200000) ENGINE = InnoDB,
PARTITION pw20171105 VALUES LESS THAN (2,1509840000000) ENGINE = InnoDB,
PARTITION pw20171112 VALUES LESS THAN (2,1510444800000) ENGINE = InnoDB,
PARTITION pw20171119 VALUES LESS THAN (2,1511049600000) ENGINE = InnoDB,
PARTITION pw20171126 VALUES LESS THAN (2,1511654400000) ENGINE = InnoDB,
PARTITION pw20171203 VALUES LESS THAN (2,1512259200000) ENGINE = InnoDB,
-- buffer partition which will be 67 days away and will be always empty so that we can split
PARTITION pw20171210 VALUES LESS THAN (2,1512864000000) ENGINE = InnoDB,
PARTITION pd20171016 VALUES LESS THAN (3,1508112000000) ENGINE = InnoDB,
PARTITION pd20171017 VALUES LESS THAN (3,1508198400000) ENGINE = InnoDB,
PARTITION pd20171018 VALUES LESS THAN (3,1508284800000) ENGINE = InnoDB,
PARTITION pd20171019 VALUES LESS THAN (3,1508371200000) ENGINE = InnoDB,
PARTITION pd20171020 VALUES LESS THAN (3,1508457600000) ENGINE = InnoDB,
PARTITION pd20171021 VALUES LESS THAN (3,1508544000000) ENGINE = InnoDB,
PARTITION pd20171022 VALUES LESS THAN (3,1508630400000) ENGINE = InnoDB,
PARTITION pd20171023 VALUES LESS THAN (3,1508716800000) ENGINE = InnoDB,
PARTITION pd20171024 VALUES LESS THAN (3,1508803200000) ENGINE = InnoDB
) */;
管理分区
您可以在 Linux 中创建一个CRON或在 mysql 中创建一个EVENT来管理分区。随着保留期的临近,分区管理工具应该将缓冲分区重新组织为一个可用分区和一个缓冲分区,并且删除已经超过保留期的分区。
例如,以前提到的customer_data表为例。
在 20171203,您需要将分区 pw20171210 拆分为 pw20171210 和 pw20171217。
在 20171017,您需要将分区 pd20171024 拆分为 pd20171024 和 pd20171025。
如果没有查询锁定表,分割(重新组织)分区将非常快(~毫秒级),只要没有(或者非常少量的)数据。因此,在数据进入分区之前,我们应该通过重新组织来保持分区为空。
第十一章:管理表空间
在本章中,我们将涵盖以下内容:
-
更改 InnoDB REDO 日志文件的数量或大小
-
调整 InnoDB 系统表空间的大小
-
在数据目录之外创建文件表空间
-
将文件表空间复制到另一个实例"
-
管理 UNDO 表空间
-
管理通用表空间
-
压缩 InnoDB 表
介绍
在开始本章之前,您应该了解 InnoDB 的基础知识。
根据 MySQL 文档,
系统表空间(共享表空间) "InnoDB 系统表空间包含 InnoDB 数据字典(与 InnoDB 相关对象的元数据)并且是双写缓冲区、更改缓冲区和撤消日志的存储区域。系统表空间还包含在系统表空间中创建的任何用户创建的表的表和索引数据。系统表空间被认为是共享表空间,因为它被多个表共享。
系统表空间由一个或多个数据文件表示。默认情况下,在 MySQL 数据目录中创建一个名为 ibdata1 的系统数据文件。系统数据文件的大小和数量由 innodb_data_file_path 启动选项控制。
文件表空间
文件表空间是在其自己的数据文件中创建的单表表空间,而不是在系统表空间中创建。当启用 innodb_file_per_table 选项时,表将在文件表空间中创建。否则,InnoDB 表将在系统表空间中创建。每个文件表空间由一个.ibd 数据文件表示,默认情况下在数据库目录中创建。
文件表空间支持 DYNAMIC 和 COMPRESSED 行格式,支持变长数据的离页存储和表压缩等功能。
要了解文件表空间的优缺点,请参考dev.mysql.com/doc/refman/8.0/en/innodb-multiple-tablespaces.html和dev.mysql.com/doc/refman/8.0/en/innodb-parameters.html#sysvar_innodb_file_per_table。
通用表空间
通用表空间是使用 CREATE TABLESPACE 语法创建的共享 InnoDB 表空间。通用表空间可以在 MySQL 数据目录之外创建,能够容纳多个表,并支持所有行格式的表。
UNDO 表空间
撤消日志是与单个事务相关联的撤消日志记录的集合。撤消日志记录包含有关如何撤消事务对聚集索引记录的最新更改的信息。如果另一个事务需要查看原始数据(作为一致性读取操作的一部分),则从撤消日志记录中检索未修改的数据。撤消日志存在于撤消日志段中,这些段包含在回滚段中。回滚段驻留在系统表空间、临时表空间和 UNDO 表空间中。
UNDO 表空间包括一个或多个包含撤消日志的文件。InnoDB 使用的 UNDO 表空间数量由 innodb_undo_tablespaces 配置选项定义。
这些日志用于回滚事务,也用于多版本并发控制。
数据字典
数据字典是元数据,用于跟踪数据库对象,如表、索引和表列。对于 MySQL 8.0 中引入的 MySQL 数据字典,元数据实际上位于 MySQL 数据库目录中的 InnoDB 文件表空间文件中。对于 InnoDB 数据字典,元数据实际上位于 InnoDB 系统表空间中。
MySQL 数据字典
MySQL 服务器包含一个事务性的数据字典,用于存储有关数据库对象的信息。在以前的 MySQL 版本中,字典数据存储在元数据文件、非事务表和特定于存储引擎的数据字典中。
在以前的 MySQL 版本中,字典数据部分存储在元数据文件中。基于文件的元数据存储的问题包括昂贵的文件扫描、易受文件系统相关错误的影响、用于处理复制和崩溃恢复失败状态的复杂代码,以及缺乏可扩展性,使得难以为新功能和关系对象添加元数据。
MySQL 数据字典的好处包括:
-
统一存储字典数据的集中
数据字典模式的简单性 -
删除基于文件的元数据存储
-
字典数据的事务性、崩溃安全存储
-
字典对象的统一和集中缓存
-
一些
INFORMATION_SCHEMA表的更简单和改进的实现 -
原子 DDL
以下列出的元数据文件已从 MySQL 中删除。除非另有说明,以前存储在元数据文件中的数据现在存储在数据字典表中:
-
.frm文件:表定义的表元数据文件。 -
.par文件:分区定义文件。InnoDB在 MySQL 5.7 中停止使用.definition分区文件,引入了InnoDB表的本机分区支持。 -
.trn文件:触发器命名空间文件。 -
.trg文件:触发器参数文件。 -
.isl文件:包含在 MySQLdata directory之外创建的基于文件的表空间文件的InnoDB符号链接文件。 -
db.opt文件:数据库配置文件。这些文件,每个数据库目录一个,包含数据库默认字符集属性。
MySQL 数据字典的限制如下:
-
在
data directory下手动创建数据库目录(例如,使用mkdir)是不受支持的。手动创建的数据库目录不被 MySQL 服务器识别。 -
通过复制和移动 MyISAM 数据文件来移动存储在 MyISAM 表中的数据是不受支持的。使用此方法移动的表不会被服务器发现。
-
不支持使用复制的数据文件对个别 MyISAM 表进行简单备份和还原。
-
由于写入存储、撤销日志和重做日志,DDL 操作需要更长的时间,而不是
.frm文件。
字典数据的事务性存储
数据字典模式将字典数据存储在事务性(InnoDB)表中。数据字典表位于mysql数据库中,与非数据字典系统表一起。
数据字典表在名为mysql.ibd的单个InnoDB表空间中创建在 MySQL data directory中。mysql.ibd表空间文件必须驻留在 MySQL data directory中,其名称不能被修改或被其他表空间使用。以前,这些表是在 MySQL 数据库目录中的单独表空间文件中创建的。
更改 InnoDB 重做日志文件的数量或大小
ib_logfile0文件和ib_logfile1是默认的InnoDB重做日志文件,每个文件大小为 48 MB,创建在data directory内。如果您希望更改重做日志文件的大小,只需在配置文件中更改并重新启动 MySQL。在以前的版本中,您必须对 MySQL 服务器进行缓慢的关闭,删除重做日志文件,更改配置文件,然后启动 MySQL 服务器。
从 MySQL 8 开始,InnoDB检测到innodb_log_file_size与重做日志文件大小不同。它写入一个日志检查点,关闭并删除旧的日志文件,以请求的大小创建新的日志文件,并打开新的日志文件。
如何做...
- 检查当前文件的大小:
shell> sudo ls -lhtr /var/lib/mysql/ib_logfile*
-rw-r-----. 1 mysql mysql 48M Oct 7 10:16 /var/lib/mysql/ib_logfile1
-rw-r-----. 1 mysql mysql 48M Oct 7 10:18 /var/lib/mysql/ib_logfile0
- 停止 MySQL 服务器,并确保它在没有错误的情况下关闭:
shell> sudo systemctl stop mysqld
- 编辑配置文件:
shell> sudo vi /etc/my.cnf
[mysqld]
innodb_log_file_size=128M
innodb_log_files_in_group=4
- 启动 MySQL 服务器:
shell> sudo systemctl start mysqld
- 您可以验证 MySQL 在日志文件中的操作:
shell> sudo less /var/log/mysqld.log
2017-10-07T11:09:35.111926Z 1 [Warning] InnoDB: Resizing redo log from 2*3072 to 4*8192 pages, LSN=249633608
2017-10-07T11:09:35.213717Z 1 [Warning] InnoDB: Starting to delete and rewrite log files.
2017-10-07T11:09:35.224724Z 1 [Note] InnoDB: Setting log file ./ib_logfile101 size to 128 MB
2017-10-07T11:09:35.225531Z 1 [Note] InnoDB: Progress in MB:
100
2017-10-07T11:09:38.924955Z 1 [Note] InnoDB: Setting log file ./ib_logfile1 size to 128 MB
2017-10-07T11:09:38.925173Z 1 [Note] InnoDB: Progress in MB:
100
2017-10-07T11:09:42.516065Z 1 [Note] InnoDB: Setting log file ./ib_logfile2 size to 128 MB
2017-10-07T11:09:42.516309Z 1 [Note] InnoDB: Progress in MB:
100
2017-10-07T11:09:46.098023Z 1 [Note] InnoDB: Setting log file ./ib_logfile3 size to 128 MB
2017-10-07T11:09:46.098246Z 1 [Note] InnoDB: Progress in MB:
100
2017-10-07T11:09:49.715400Z 1 [Note] InnoDB: Renaming log file ./ib_logfile101 to ./ib_logfile0
2017-10-07T11:09:49.715497Z 1 [Warning] InnoDB: New log files created, LSN=249633608
- 您还可以查看新创建的日志文件:
shell> sudo ls -lhtr /var/lib/mysql/ib_logfile*
-rw-r-----. 1 mysql mysql 128M Oct 7 11:09 /var/lib/mysql/ib_logfile1
-rw-r-----. 1 mysql mysql 128M Oct 7 11:09 /var/lib/mysql/ib_logfile2
-rw-r-----. 1 mysql mysql 128M Oct 7 11:09 /var/lib/mysql/ib_logfile3
-rw-r-----. 1 mysql mysql 128M Oct 7 11:09 /var/lib/mysql/ib_logfile0
调整 InnoDB 系统表空间的大小
数据目录中的ibdata1文件是默认的系统表空间。您可以使用innodb_data_file_path和innodb_data_home_dir配置选项来配置ibdata1。innodb_data_file_path配置选项用于配置InnoDB系统表空间数据文件。innodb_data_file_path的值应该是一个或多个数据文件规范的列表。如果命名了两个或更多数据文件,请使用分号(;)字符将它们分开。
如果要在数据目录中包含一个固定大小的 50MB 数据文件名为ibdata1和一个 50MB 自动扩展文件名为ibdata2的表空间,可以进行如下配置:
shell> sudo vi /etc/my.cnf
[mysqld]
innodb_data_file_path=ibdata1:50M;ibdata2:50M:autoextend
如果ibdata文件变得如此庞大,特别是当未启用innodb_file_per_table且磁盘变满时,您可能希望在另一个磁盘上添加另一个数据文件。
如何做...
调整InnoDB系统表空间的大小是一个您会越来越想了解更多的主题。让我们深入了解其细节。
增加 InnoDB 系统表空间
假设innodb_data_file_path是ibdata1:50M:autoextend,大小已达到 76MB,您的磁盘只有 100MB,您可以添加另一个磁盘并配置在新磁盘上添加另一个表空间:
- 停止 MySQL 服务器:
shell> sudo systemctl stop mysql
- 检查现有
ibdata1文件的大小:
shell> sudo ls -lhtr /var/lib/mysql/ibdata1
-rw-r----- 1 mysql mysql 76M Oct 6 13:33 /var/lib/mysql/ibdata1
- 挂载新磁盘。假设它挂载在
/var/lib/mysql_extend上,更改所有权为mysql;确保文件尚未创建。如果您使用 AppArmour 或 SELinux,请确保正确设置别名或上下文:
shell> sudo chown mysql:mysql /var/lib/mysql_extend
shell> sudo chmod 750 /var/lib/mysql_extend
shell> sudo ls -lhtr /var/lib/mysql_extend
- 打开
my.cnf并添加以下内容:
shell> sudo vi /etc/my.cnf [mysqld]
innodb_data_home_dir=
innodb_data_file_path = ibdata1:76M;/var/lib/mysql_extend/ibdata2:50M:autoextend
由于ibdata1的现有大小为 76MB,您必须选择至少 76MB 的 maxvalue。下一个ibdata文件将在挂载在/var/lib/mysql_extend/上的新磁盘上创建。应该指定innodb_data_home_dir选项;否则,mysqld会查看不同的路径并因错误而失败:
2017-10-07T06:30:00.658039Z 1 [ERROR] InnoDB: Operating system error number 2 in a file operation.
2017-10-07T06:30:00.658084Z 1 [ERROR] InnoDB: The error means the system cannot find the path specified.
2017-10-07T06:30:00.658088Z 1 [ERROR] InnoDB: If you are installing InnoDB, remember that you must create directories yourself, InnoDB does not create them.
2017-10-07T06:30:00.658092Z 1 [ERROR] InnoDB: File .//var/lib/mysql_extend/ibdata2: 'create' returned OS error 71\. Cannot continue operation
- 启动 MySQL 服务器:
shell> sudo systemctl start mysql
- 验证新文件。由于您已将其指定为 50MB,因此文件的初始大小将为 50MB:
shell> sudo ls -lhtr /var/lib/mysql_extend/
total 50M
-rw-r-----. 1 mysql mysql 50M Oct 7 07:38 ibdata2
mysql> SHOW VARIABLES LIKE 'innodb_data_file_path';
+-----------------------+----------------------------------------------------------+
| Variable_name | Value |
+-----------------------+----------------------------------------------------------+
| innodb_data_file_path | ibdata1:12M;/var/lib/mysql_extend/ibdata2:50M:autoextend |
+-----------------------+----------------------------------------------------------+
1 row in set (0.00 sec)
缩小 InnoDB 系统表空间
如果不使用innodb_file_per_table,则所有表数据都存储在系统表空间中。如果删除表,则不会回收空间。您可以缩小系统表空间并回收磁盘空间。这需要较长的停机时间,因此建议在从服务器上执行该任务,并将其从轮换中取出,然后将其提升为主服务器。
您可以通过查询INFORMATION_SCHEMA表来检查可用空间:
mysql> SELECT SUM(data_free)/1024/1024 FROM INFORMATION_SCHEMA.TABLES;
+--------------------------+
| sum(data_free)/1024/1024 |
+--------------------------+
| 6.00000000 |
+--------------------------+
1 row in set (0.00 sec)
- 停止对数据库的写入。如果是主服务器,则
mysql> SET @@GLOBAL.READ_ONLY=1;;如果是从服务器,请停止复制并保存二进制日志坐标:
mysql> STOP SLAVE;
mysql> SHOW SLAVE STATUS\G
- 使用
mysqldump或mydumper进行完整备份,不包括sys数据库:
shell> mydumper -u root --password=<password> --trx-consistency-only --kill-long-queries --long-query-guard 500 --regex '^(?!sys)' --outputdir /backups
- 停止 MySQL 服务器:
shell> sudo systemctl stop mysql
- 删除所有
*.ibd、*.ib_log和ibdata文件。如果只使用InnoDB表,可以清除数据目录和存储系统表空间的所有位置(innodb_data_file_path):
shell> sudo rm -rf /var/lib/mysql/ib* /var/lib/mysql/<database directories>
shell> sudo rm -rf /var/lib/mysql_extend/*
- 初始化
数据目录:
shell> sudo mysqld --initialize --datadir=/var/lib/mysql
shell> chown -R mysql:mysql /var/lib/mysql/
shell> chown -R mysql:mysql /var/lib/mysql_extend/
- 获取临时密码:
shell> sudo grep "temporary password is generated" /var/log/mysql/error.log | tail -1
2017-10-07T09:33:31.966223Z 4 [Note] A temporary password is generated for root@localhost: lI-qerr5agpa
- 启动 MySQL 并更改密码:
shell> sudo systemctl start mysqld
shell> mysql -u root -plI-qerr5agpa
mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'xxxx';
Query OK, 0 rows affected (0.01 sec)
- 恢复备份。使用临时密码连接到 MySQL:
shell> /opt/mydumper/myloader --directory=/backups/ --queries-per-transaction=50000 --threads=6 --user=root --password=xxxx --overwrite-tables
- 如果是主服务器,请启用写入
mysql> SET @@GLOBAL.READ_ONLY=0;。如果是从服务器,请通过执行CHANGE MASTER TO COMMAND和START SLAVE;来恢复复制。
在数据目录之外创建基于文件的表空间
在上一节中,您了解了如何在另一个磁盘上创建系统表空间。在本节中,您将学习如何在另一个磁盘上创建单独的表空间。
如何做...
您可以挂载具有特定性能或容量特性的新磁盘,例如快速 SSD 或高容量 HDD,到目录并配置InnoDB以使用该磁盘。在目标目录中,MySQL 创建一个与数据库名称对应的子目录,并在其中为新表创建一个.ibd文件。请记住,您不能在ALTER TABLE语句中使用DATA DIRECTORY子句:
- 挂载新磁盘并更改权限。如果您使用 AppArmour 或 SELinux,请确保正确设置别名或上下文:
shell> sudo chown -R mysql:mysql /var/lib/mysql_fast_storage
shell> sudo chmod 750 /var/lib/mysql_fast_storage
- 创建表:
mysql> CREATE TABLE event_tracker (
event_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
event_name varchar(10),
ts timestamp NOT NULL,
event_type varchar(10)
)
TABLESPACE = innodb_file_per_table
DATA DIRECTORY = '/var/lib/mysql_fast_storage';
- 检查在新设备上创建的
.ibd文件:
shell> sudo ls -lhtr /var/lib/mysql_fast_storage/employees/
total 128K
-rw-r-----. 1 mysql mysql 128K Oct 7 13:48 event_tracker.ibd
将文件表空间复制到另一个实例
复制表空间文件(.ibd文件)是移动数据的最快方式,而不是通过mysqldump或mydumper导出和导入。数据立即可用,而不必重新插入和重建索引。有许多原因可能会复制InnoDB文件表空间到不同的实例:
-
在生产服务器上运行报告而不会给服务器增加额外负载
-
为新的从服务器设置相同的表数据
-
在出现问题或错误后恢复表或分区的备份版本
-
在 SSD 设备上有繁忙的表,或在高容量 HDD 设备上有大表
如何做...
概述是:在目的地创建与源上相同表定义的表,并在目的地上执行DISCARD TABLESPACE命令。在源上执行FLUSH TABLES FOR EXPORT,这确保了对命名表的更改已刷新到磁盘,因此可以在实例运行时进行二进制表复制。在该语句之后,表被锁定,不接受任何写入;但是,可以进行读取。您可以将该表的.ibd文件复制到目的地,在源上执行UNLOCK表,最后执行IMPORT TABLESPACE命令,该命令接受复制的.ibd文件。
例如,您希望将测试数据库中的events_history表从一个服务器(源)复制到另一个服务器(目的地)。
如果尚未创建,请创建event_history并插入一些行以进行演示:
mysql> USE test;
mysql> CREATE TABLE IF NOT EXISTS `event_history`(
`event_id` int(11) NOT NULL,
`event_name` varchar(10) DEFAULT NULL,
`created_at` datetime NOT NULL,
`last_updated` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`event_type` varchar(10) NOT NULL,
`msg` tinytext NOT NULL,
PRIMARY KEY (`event_id`,`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE (to_days(`created_at`))
(PARTITION 2017_oct_week1 VALUES LESS THAN (736974) ENGINE = InnoDB,
PARTITION p20171008 VALUES LESS THAN (736975) ENGINE = InnoDB,
PARTITION p20171009 VALUES LESS THAN (736976) ENGINE = InnoDB,
PARTITION p20171010 VALUES LESS THAN (736977) ENGINE = InnoDB,
PARTITION p20171011 VALUES LESS THAN (736978) ENGINE = InnoDB,
PARTITION p20171012 VALUES LESS THAN (736979) ENGINE = InnoDB,
PARTITION p20171013 VALUES LESS THAN (736980) ENGINE = InnoDB,
PARTITION p20171014 VALUES LESS THAN (736981) ENGINE = InnoDB,
PARTITION p20171015 VALUES LESS THAN (736982) ENGINE = InnoDB,
PARTITION p20171016 VALUES LESS THAN (736983) ENGINE = InnoDB,
PARTITION p20171017 VALUES LESS THAN (736984) ENGINE = InnoDB);
mysql> INSERT INTO event_history VALUES
(1,'test','2017-10-07','2017-10-08','click','test_message'),
(2,'test','2017-10-08','2017-10-08','click','test_message'),
(3,'test','2017-10-09','2017-10-09','click','test_message'),
(4,'test','2017-10-10','2017-10-10','click','test_message'),
(5,'test','2017-10-11','2017-10-11','click','test_message'),
(6,'test','2017-10-12','2017-10-12','click','test_message'),
(7,'test','2017-10-13','2017-10-13','click','test_message'),
(8,'test','2017-10-14','2017-10-14','click','test_message');
Query OK, 8 rows affected (0.01 sec)
Records: 8 Duplicates: 0 Warnings: 0
复制完整表
- 在目的地:创建与源上相同定义的表:
mysql> USE test;
mysql> CREATE TABLE IF NOT EXISTS `event_history`(
`event_id` int(11) NOT NULL,
`event_name` varchar(10) DEFAULT NULL,
`created_at` datetime NOT NULL,
`last_updated` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`event_type` varchar(10) NOT NULL,
`msg` tinytext NOT NULL,
PRIMARY KEY (`event_id`,`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE (to_days(`created_at`))
(PARTITION 2017_oct_week1 VALUES LESS THAN (736974) ENGINE = InnoDB,
PARTITION p20171008 VALUES LESS THAN (736975) ENGINE = InnoDB,
PARTITION p20171009 VALUES LESS THAN (736976) ENGINE = InnoDB,
PARTITION p20171010 VALUES LESS THAN (736977) ENGINE = InnoDB,
PARTITION p20171011 VALUES LESS THAN (736978) ENGINE = InnoDB,
PARTITION p20171012 VALUES LESS THAN (736979) ENGINE = InnoDB,
PARTITION p20171013 VALUES LESS THAN (736980) ENGINE = InnoDB,
PARTITION p20171014 VALUES LESS THAN (736981) ENGINE = InnoDB,
PARTITION p20171015 VALUES LESS THAN (736982) ENGINE = InnoDB,
PARTITION p20171016 VALUES LESS THAN (736983) ENGINE = InnoDB,
PARTITION p20171017 VALUES LESS THAN (736984) ENGINE = InnoDB);
- 在目的地:丢弃表空间:
mysql> ALTER TABLE event_history DISCARD TABLESPACE;
Query OK, 0 rows affected (0.05 sec)
- 在源上:执行
FLUSH TABLES FOR EXPORT:
mysql> FLUSH TABLES event_history FOR EXPORT;
Query OK, 0 rows affected (0.00 sec)
- 在源上:从源的
数据目录目录中复制所有与表相关的文件(.ibd,.cfg)到目的地的数据目录:
shell> sudo scp -i /home/mysql/.ssh/id_rsa /var/lib/mysql/test/event_history#P#* mysql@xx.xxx.xxx.xxx:/var/lib/mysql/test/
- 在源上:解锁表以进行写入:
mysql> UNLOCK TABLES;
Query OK, 0 rows affected (0.00 sec)
- 在目的地:确保文件的所有权设置为
mysql:
shell> sudo ls -lhtr /var/lib/mysql/test
total 1.4M
-rw-r----- 1 mysql mysql 128K Oct 7 17:17 event_history#P#p20171017.ibd
-rw-r----- 1 mysql mysql 128K Oct 7 17:17 event_history#P#p20171016.ibd
-rw-r----- 1 mysql mysql 128K Oct 7 17:17 event_history#P#p20171015.ibd
-rw-r----- 1 mysql mysql 128K Oct 7 17:17 event_history#P#p20171014.ibd
-rw-r----- 1 mysql mysql 128K Oct 7 17:17 event_history#P#p20171013.ibd
-rw-r----- 1 mysql mysql 128K Oct 7 17:17 event_history#P#p20171012.ibd
-rw-r----- 1 mysql mysql 128K Oct 7 17:17 event_history#P#p20171011.ibd
-rw-r----- 1 mysql mysql 128K Oct 7 17:17 event_history#P#p20171010.ibd
-rw-r----- 1 mysql mysql 128K Oct 7 17:17 event_history#P#p20171009.ibd
-rw-r----- 1 mysql mysql 128K Oct 7 17:17 event_history#P#p20171008.ibd
-rw-r----- 1 mysql mysql 128K Oct 7 17:17 event_history#P#2017_oct_week1.ibd
- 在目的地:导入表空间。只要表定义相同,就可以忽略警告。如果您也复制了
.cfg文件,则不会出现警告:
mysql> ALTER TABLE event_history IMPORT TABLESPACE;
Query OK, 0 rows affected, 12 warnings (0.31 sec)
- 在目的地:验证数据:
mysql> SELECT * FROM event_history;
+----------+------------+---------------------+---------------------+------------+--------------+
| event_id | event_name | created_at | last_updated | event_type | msg |
+----------+------------+---------------------+---------------------+------------+--------------+
| 1 | test | 2017-10-07 00:00:00 | 2017-10-08 00:00:00 | click | test_message |
| 2 | test | 2017-10-08 00:00:00 | 2017-10-08 00:00:00 | click | test_message |
| 3 | test | 2017-10-09 00:00:00 | 2017-10-09 00:00:00 | click | test_message |
| 4 | test | 2017-10-10 00:00:00 | 2017-10-10 00:00:00 | click | test_message |
| 5 | test | 2017-10-11 00:00:00 | 2017-10-11 00:00:00 | click | test_message |
| 6 | test | 2017-10-12 00:00:00 | 2017-10-12 00:00:00 | click | test_message |
| 7 | test | 2017-10-13 00:00:00 | 2017-10-13 00:00:00 | click | test_message |
| 8 | test | 2017-10-14 00:00:00 | 2017-10-14 00:00:00 | click | test_message |
+----------+------------+---------------------+---------------------+------------+--------------+
8 rows in set (0.00 sec)
如果您在生产系统上进行操作,为了最小化停机时间,您可以将文件复制到本地,这非常快。立即执行UNLOCK TABLES,然后将文件复制到目的地。如果您无法承受停机时间,可以使用 Percona XtraBackup,备份单个表,并应用重做日志,生成.ibd文件。您可以将它们复制到目的地并导入。
复制表的单个分区
您在源上添加了events_history表的新分区,并且希望仅将新分区复制到目的地。为了您的理解,请在events_history表上创建新分区并插入一些行:
mysql> ALTER TABLE event_history ADD PARTITION
(PARTITION p20171018 VALUES LESS THAN (736985) ENGINE = InnoDB,
PARTITION p20171019 VALUES LESS THAN (736986) ENGINE = InnoDB);
Query OK, 0 rows affected (0.06 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> INSERT INTO event_history VALUES
(9,'test','2017-10-17','2017-10-17','click','test_message'),(10,'test','2017-10-18','2017-10-18','click','test_message');
Query OK, 1 row affected (0.01 sec)
mysql> SELECT * FROM event_history PARTITION (p20171018,p20171019);
+----------+------------+---------------------+---------------------+------------+--------------+
| event_id | event_name | created_at | last_updated | event_type | msg |
+----------+------------+---------------------+---------------------+------------+--------------+
| 9 | test | 2017-10-17 00:00:00 | 2017-10-17 00:00:00 | click | test_message |
| 10 | test | 2017-10-18 00:00:00 | 2017-10-18 00:00:00 | click | test_message |
+----------+------------+---------------------+---------------------+------------+--------------+
2 rows in set (0.00 sec)
假设您希望将新创建的分区复制到目的地。
- 在目的地:创建分区:
mysql> ALTER TABLE event_history ADD PARTITION
(PARTITION p20171018 VALUES LESS THAN (736985) ENGINE = InnoDB,
PARTITION p20171019 VALUES LESS THAN (736986) ENGINE = InnoDB);
Query OK, 0 rows affected (0.05 sec)
Records: 0 Duplicates: 0 Warnings: 0
- 在目的地:仅丢弃要导入的分区:
mysql> ALTER TABLE event_history DISCARD PARTITION p20171018, p20171019 TABLESPACE;
Query OK, 0 rows affected (0.06 sec)
- 在源上:执行
FLUSH TABLE FOR EXPORT:
mysql> FLUSH TABLES event_history FOR EXPORT;
Query OK, 0 rows affected (0.01 sec)
- 在源上:将分区的
.ibd文件复制到目的地:
shell> sudo scp -i /home/mysql/.ssh/id_rsa \
/var/lib/mysql/test/event_history#P#p20171018.ibd \
/var/lib/mysql/test/event_history#P#p20171019.ibd \
mysql@35.198.210.229:/var/lib/mysql/test/
event_history#P#p20171018.ibd 100% 128KB 128.0KB/s 00:00 event_history#P#p20171019.ibd 100% 128KB 128.0KB/s 00:00
- 在目的地:确保所需分区的
.ibd文件已复制并且所有者为mysql:
shell> sudo ls -lhtr /var/lib/mysql/test/event_history#P#p20171018.ibd
-rw-r----- 1 mysql mysql 128K Oct 7 17:54 /var/lib/mysql/test/event_history#P#p20171018.ibd
shell> sudo ls -lhtr /var/lib/mysql/test/event_history#P#p20171019.ibd
-rw-r----- 1 mysql mysql 128K Oct 7 17:54 /var/lib/mysql/test/event_history#P#p20171019.ibd
- 在目的地上:执行
IMPORT PARTITION TABLESPACE:
mysql> ALTER TABLE event_history IMPORT PARTITION p20171018, p20171019 TABLESPACE;
Query OK, 0 rows affected, 2 warnings (0.10 sec)
只要表定义相同,您可以忽略警告。如果您也复制了.cfg文件,则不会出现警告:
mysql> SHOW WARNINGS;
+---------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message |
+---------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Warning | 1810 | InnoDB: IO Read error: (2, No such file or directory) Error opening './test/event_history#P#p20171018.cfg', will attempt to import without schema verification |
| Warning | 1810 | InnoDB: IO Read error: (2, No such file or directory) Error opening './test/event_history#P#p20171019.cfg', will attempt to import without schema verification |
+---------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+
2 rows in set (0.00 sec)
- 在目的地上:验证数据:
mysql> SELECT * FROM event_history PARTITION (p20171018,p20171019);
+----------+------------+---------------------+---------------------+------------+--------------+
| event_id | event_name | created_at | last_updated | event_type | msg |
+----------+------------+---------------------+---------------------+------------+--------------+
| 9 | test | 2017-10-17 00:00:00 | 2017-10-17 00:00:00 | click | test_message |
| 10 | test | 2017-10-18 00:00:00 | 2017-10-18 00:00:00 | click | test_message |
+----------+------------+---------------------+---------------------+------------+--------------+
2 rows in set (0.00 sec)
另请参阅
请参阅dev.mysql.com/doc/refman/8.0/en/tablespace-copying.html以了解有关此过程的限制的更多信息。
管理 UNDO 表空间
您可以通过动态变量innodb_max_undo_log_size(默认为 1GB)和innodb_undo_tablespaces(默认为 2GB,从 MySQL 8.0.2 开始为动态)来管理UNDO表空间的大小。
默认情况下,innodb_undo_log_truncate已启用。超过innodb_max_undo_log_size定义的阈值的表空间将被标记为截断。只有撤消表空间可以被截断。不支持截断驻留在系统表空间中的撤消日志。要进行截断,必须至少有两个撤消表空间。
如何做...
验证UNDO日志的大小:
shell> sudo ls -lhtr /var/lib/mysql/undo_00*
-rw-r-----. 1 mysql mysql 19M Oct 7 17:43 /var/lib/mysql/undo_002
-rw-r-----. 1 mysql mysql 16M Oct 7 17:43 /var/lib/mysql/undo_001
假设您想要减少大于 15MB 的文件。请记住,只能截断一个撤消表空间。选择要截断的撤消表空间是循环进行的,以避免每次都截断相同的撤消表空间。在撤消表空间中的所有回滚段被释放后,截断操作将运行,并且撤消表空间将被截断为其初始大小。撤消表空间文件的初始大小为 10MB:
- 确保
innodb_undo_log_truncate已启用:
mysql> SELECT @@GLOBAL.innodb_undo_log_truncate;
+-----------------------------------+
| @@GLOBAL.innodb_undo_log_truncate |
+-----------------------------------+
| 1 |
+-----------------------------------+
1 row in set (0.00 sec)
- 将
innodb_max_undo_log_size设置为 15MB:
mysql> SELECT @@GLOBAL.innodb_max_undo_log_size;
+-----------------------------------+
| @@GLOBAL.innodb_max_undo_log_size |
+-----------------------------------+
| 1073741824 |
+-----------------------------------+
1 row in set (0.00 sec)
mysql> SET @@GLOBAL.innodb_max_undo_log_size=15*1024*1024;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @@GLOBAL.innodb_max_undo_log_size;
+-----------------------------------+
| @@GLOBAL.innodb_max_undo_log_size |
+-----------------------------------+
| 15728640 |
+-----------------------------------+
1 row in set (0.00 sec)
- 直到其回滚段被释放,撤消表空间才能被截断。通常,清除系统每 128 次调用一次。为了加快撤消表空间的截断,使用
innodb_purge_rseg_truncate_frequency选项临时增加清除系统释放回滚段的频率:
mysql> SELECT @@GLOBAL.innodb_purge_rseg_truncate_frequency;
+-----------------------------------------------+
| @@GLOBAL.innodb_purge_rseg_truncate_frequency |
+-----------------------------------------------+
| 128 |
+-----------------------------------------------+
1 row in set (0.00 sec)
mysql> SET @@GLOBAL.innodb_purge_rseg_truncate_frequency=1;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @@GLOBAL.innodb_purge_rseg_truncate_frequency;
+-----------------------------------------------+
| @@GLOBAL.innodb_purge_rseg_truncate_frequency |
+-----------------------------------------------+
| 1 |
+-----------------------------------------------+
1 row in set (0.00 sec)
- 通常在繁忙的系统上,至少会启动一个清除操作,并且截断将已经开始。如果您在自己的机器上练习,可以通过创建一个大事务来启动清除:
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> DELETE FROM employees;
Query OK, 300025 rows affected (16.23 sec)
mysql> ROLLBACK;
Query OK, 0 rows affected (2.38 sec)
- 在删除正在进行时,您可以观察
UNDO日志文件的增长:
shell> sudo ls -lhtr /var/lib/mysql/undo_00*
-rw-r-----. 1 mysql mysql 19M Oct 7 17:43 /var/lib/mysql/undo_002
-rw-r-----. 1 mysql mysql 16M Oct 7 17:43 /var/lib/mysql/undo_001
shell> sudo ls -lhtr /var/lib/mysql/undo_00*
-rw-r-----. 1 mysql mysql 10M Oct 8 04:52 /var/lib/mysql/undo_001
-rw-r-----. 1 mysql mysql 27M Oct 8 04:52 /var/lib/mysql/undo_002
shell> sudo ls -lhtr /var/lib/mysql/undo_00*
-rw-r-----. 1 mysql mysql 10M Oct 8 04:52 /var/lib/mysql/undo_001
-rw-r-----. 1 mysql mysql 28M Oct 8 04:52 /var/lib/mysql/undo_002
shell> sudo ls -lhtr /var/lib/mysql/undo_00*
-rw-r-----. 1 mysql mysql 10M Oct 8 04:52 /var/lib/mysql/undo_001
-rw-r-----. 1 mysql mysql 29M Oct 8 04:52 /var/lib/mysql/undo_002
shell> sudo ls -lhtr /var/lib/mysql/undo_00*
-rw-r-----. 1 mysql mysql 10M Oct 8 04:52 /var/lib/mysql/undo_001
-rw-r-----. 1 mysql mysql 29M Oct 8 04:52 /var/lib/mysql/undo_002
您可能会注意到undo_001被截断为 10MB,而undo_002正在增长,以容纳DELETE语句的已删除行。
- 一段时间后,您可能会注意到
unod_002也被截断为 10MB:
shell> sudo ls -lhtr /var/lib/mysql/undo_00*
-rw-r-----. 1 mysql mysql 10M Oct 8 04:52 /var/lib/mysql/undo_001
-rw-r-----. 1 mysql mysql 10M Oct 8 04:54 /var/lib/mysql/undo_002
- 一旦您已经减少了
UNDO表空间,将innodb_purge_rseg_truncate_frequency设置为默认值128:
mysql> SELECT @@GLOBAL.innodb_purge_rseg_truncate_frequency;
+-----------------------------------------------+
| @@GLOBAL.innodb_purge_rseg_truncate_frequency |
+-----------------------------------------------+
| 1 |
+-----------------------------------------------+
1 row in set (0.00 sec)
mysql> SET @@GLOBAL.innodb_purge_rseg_truncate_frequency=128;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @@GLOBAL.innodb_purge_rseg_truncate_frequency;
+-----------------------------------------------+
| @@GLOBAL.innodb_purge_rseg_truncate_frequency |
+-----------------------------------------------+
| 128 |
+-----------------------------------------------+
1 row in set (0.01 sec)
管理通用表空间
直到 MySQL 8 之前,有两种类型的表空间:系统表空间和单独的表空间。这两种类型都有优点和缺点。为了克服缺点,MySQL 8 引入了通用表空间。与系统表空间类似,通用表空间是可以存储多个表数据的共享表空间。但是,您可以对通用表空间进行精细控制。较少的通用表空间中的多个表消耗的表空间元数据比在单独的文件表表空间中的相同数量的表少。
限制如下:
-
与系统表空间类似,截断或删除存储在通用表空间中的表会在通用表空间的
.ibd数据文件中创建内部的可用空间,该空间只能用于新的InnoDB数据。与文件表表空间一样,空间不会释放回操作系统。 -
通用表空间不支持属于通用表空间的表的可传输表空间。
在本节中,您将学习如何创建通用表空间以及向其中添加和删除表。
实际用法:
最初,InnoDB维护一个包含表结构的.frm文件。MySQL 需要打开和关闭.frm文件,这会降低性能。使用 MySQL 8,.frm文件被删除,所有的元数据都使用事务性数据字典处理。这使得可以使用通用表空间。
假设您正在为每个客户单独创建模式,并且每个客户都有数百个表的 SaaS 或多租户中使用 MySQL 5.7 或更早版本。如果您的客户增长,您将注意到性能问题。但是,随着 MySQL 8 中.frm文件的删除,性能得到了极大改善。此外,您可以为每个模式(客户)创建单独的表空间。
如何做...
让我们开始创建它。
创建通用表空间
您可以在 MySQL 的数据目录内或外创建通用表空间。
要在 MySQL 的数据目录中创建一个:
mysql> CREATE TABLESPACE `ts1` ADD DATAFILE 'ts1.ibd' Engine=InnoDB;
Query OK, 0 rows affected (0.02 sec)
要在外部创建表空间,请将新磁盘挂载到/var/lib/mysql_general_ts并将所有权更改为mysql:
shell> sudo chown mysql:mysql /var/lib/mysql_general_ts
mysql> CREATE TABLESPACE `ts2` ADD DATAFILE '/var/lib/mysql_general_ts/ts2.ibd' Engine=InnoDB;Query OK, 0 rows affected (0.02 sec)
向通用表空间添加表
在创建表时,您可以将表添加到表空间中,或者可以运行ALTER命令将表从一个表空间移动到另一个表空间:
mysql> CREATE TABLE employees.table_gen_ts1 (id INT PRIMARY KEY) TABLESPACE ts1;
Query OK, 0 rows affected (0.01 sec)
假设您想将employees表移动到TABLESPACE ts2:
mysql> USE employees;
Database changed
mysql> ALTER TABLE employees TABLESPACE ts2;
Query OK, 0 rows affected (3.93 sec)
Records: 0 Duplicates: 0 Warnings: 0
您可以注意到ts2.ibd文件的增加:
shell> sudo ls -lhtr /var/lib/mysql_general_ts/ts2.ibd
-rw-r-----. 1 mysql mysql 32M Oct 8 17:07 /var/lib/mysql_general_ts/ts2.ibd
在表空间之间移动非分区表
您可以按以下方式移动表:
- 这是如何将表从一个通用表空间移动到另一个通用表空间。
假设您想将employees表从ts2移动到ts1:
mysql> ALTER TABLE employees TABLESPACE ts1;
Query OK, 0 rows affected (3.83 sec)
Records: 0 Duplicates: 0 Warnings: 0
shell> sudo ls -lhtr /var/lib/mysql/ts1.ibd
-rw-r-----. 1 mysql mysql 32M Oct 8 17:16 /var/lib/mysql/ts1.ibd
- 这是如何将表移动到每个文件一个表。
假设您想将employees表从ts1移动到每个文件一个表:
mysql> ALTER TABLE employees TABLESPACE innodb_file_per_table;
Query OK, 0 rows affected (4.05 sec)
Records: 0 Duplicates: 0 Warnings: 0
shell> sudo ls -lhtr /var/lib/mysql/employees/employees.ibd
-rw-r-----. 1 mysql mysql 32M Oct 8 17:18 /var/lib/mysql/employees/employees.ibd
- 这是如何将表移动到系统表空间。
假设您想将employees表从每个文件一个表移动到系统表空间:
mysql> ALTER TABLE employees TABLESPACE innodb_system;
Query OK, 0 rows affected (5.28 sec)
Records: 0 Duplicates: 0 Warnings: 0
在通用表空间中管理分区表
您可以在多个表空间中创建具有分区的表:
mysql> CREATE TABLE table_gen_part_ts1 (id INT, value varchar(100)) ENGINE = InnoDB
PARTITION BY RANGE(id) (
PARTITION p1 VALUES LESS THAN (1000000) TABLESPACE ts1,
PARTITION p2 VALUES LESS THAN (2000000) TABLESPACE ts2,
PARTITION p3 VALUES LESS THAN (3000000) TABLESPACE innodb_file_per_table,
PARTITION pmax VALUES LESS THAN (MAXVALUE) TABLESPACE innodb_system);
Query OK, 0 rows affected (0.19 sec)
您可以在另一个表空间中添加新的分区,或者如果您没有提及任何内容,它将在表的默认表空间中创建。对分区表执行的ALTER TABLE tbl_name TABLESPACE tablespace_name操作只会修改表的默认表空间。它不会移动表分区。但是,在更改默认表空间之后,重建表的操作(例如使用ALGORITHM=COPY的ALTER TABLE操作)将分区移动到默认表空间,如果没有使用TABLESPACE子句显式定义另一个表空间。
如果您希望在表空间之间移动分区,则需要对分区进行REORGANIZE。例如,您想将分区p3移动到ts2:
mysql> ALTER TABLE table_gen_part_ts1 REORGANIZE PARTITION p3 INTO (PARTITION p3 VALUES LESS THAN (3000000) TABLESPACE ts2);
删除通用表空间
您可以使用DROP TABLESPACE命令删除表空间。但是,该表空间内的所有表应该被删除或移动:
mysql> DROP TABLESPACE ts2;
ERROR 3120 (HY000): Tablespace `ts2` is not empty.
在删除之前,您必须将table_gen_part_ts1表的ts2表空间中的分区p2和p3移动到其他表空间:
mysql> ALTER TABLE table_gen_part_ts1 REORGANIZE PARTITION p2 INTO (PARTITION p2 VALUES LESS THAN (3000000) TABLESPACE ts1);
mysql> ALTER TABLE table_gen_part_ts1 REORGANIZE PARTITION p3 INTO (PARTITION p3 VALUES LESS THAN (3000000) TABLESPACE ts1);
现在您可以删除表空间:
mysql> DROP TABLESPACE ts2;
Query OK, 0 rows affected (0.01 sec)
InnoDB 表的压缩
您可以创建数据以压缩形式存储的表。压缩可以帮助提高原始性能和可伸缩性。压缩意味着在磁盘和内存之间传输的数据更少,并且在磁盘和内存中占用的空间更少。
根据 MySQL 文档:
“因为处理器和缓存内存的速度增加比磁盘存储设备更快,许多工作负载受限于磁盘。数据压缩使数据库大小更小,减少 I/O,提高吞吐量,代价是增加 CPU 利用率。压缩对于读密集型应用特别有价值,在具有足够 RAM 以将经常使用的数据保留在内存中的系统上。对于具有辅助索引的表,好处尤为明显,因为索引数据也被压缩。”
要启用压缩,需要使用ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE选项创建或更改表。您可以变化KEY_BLOCK_SIZE参数,该参数在磁盘上使用比配置的innodb_page_size值更小的页面大小。如果表在系统表空间中,则压缩将无法工作。
要在通用表空间中创建压缩表,必须为创建表空间时指定的通用表空间定义FILE_BLOCK_SIZE。FILE_BLOCK_SIZE值必须是与innodb_page_size值相关的有效压缩页面大小,并且由CREATE TABLE或ALTER TABLE KEY_BLOCK_SIZE子句定义的压缩表的页面大小必须等于FILE_BLOCK_SIZE/1024。
在缓冲池中,压缩数据以小页面的形式保存,页面大小基于KEY_BLOCK_SIZE值。对于提取或更新列值,MySQL 还在缓冲池中创建一个包含未压缩数据的未压缩页面。在缓冲池中,对未压缩页面的任何更新也会被重写回等效的压缩页面。您可能需要调整缓冲池的大小,以容纳压缩和未压缩页面的额外数据,尽管在需要空间时未压缩页面会从缓冲池中驱逐,然后在下一次访问时再次解压缩。
何时使用压缩?
一般来说,压缩最适用于包含合理数量的字符串列的表,以及数据被读取的频率远远高于写入的情况。因为没有保证的方法来预测压缩是否对特定情况有益,所以始终要使用特定的工作负载和数据集在代表性配置上进行测试。
如何做...
您需要选择参数KEY_BLOCK_SIZE。innodb_page_size为 16,000;理想情况下,一半为 8,000,这是一个很好的起点。要调整压缩,请参阅dev.mysql.com/doc/refman/8.0/en/innodb-compression-tuning.html。
为file_per_table表启用压缩
- 确保启用了
file_per_table:
mysql> SET GLOBAL innodb_file_per_table=1;
- 在创建语句中指定
ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8:
mysql> CREATE TABLE compressed_table (id INT PRIMARY KEY) ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8;
Query OK, 0 rows affected (0.07 sec)
如果表已经存在,可以执行ALTER:
mysql> ALTER TABLE event_history ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8;
Query OK, 0 rows affected (0.67 sec)
Records: 0 Duplicates: 0 Warnings: 0
如果尝试压缩位于系统表空间中的表,将会出现错误:
mysql> ALTER TABLE employees ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8;
ERROR 1478 (HY000): InnoDB: Tablespace `innodb_system` cannot contain a COMPRESSED table
为file_per_table表禁用压缩
要禁用压缩,执行ALTER表并指定ROW_FORMAT=DYNAMIC或ROW_FORMAT=COMPACT,然后是KEY_BLOCK_SIZE=0。
例如,如果不希望在event_history表上使用压缩:
mysql> ALTER TABLE event_history ROW_FORMAT=DYNAMIC KEY_BLOCK_SIZE=0;
Query OK, 0 rows affected (0.53 sec)
Records: 0 Duplicates: 0 Warnings: 0
为通用表空间启用压缩
首先,您需要通过提及FILE_BLOCK_SIZE来创建一个压缩表空间;您不能更改表空间的FILE_BLOCK_SIZE。
如果要创建压缩表,需要在启用压缩的通用表空间中创建表;此外,KEY_BLOCK_SIZE必须等于FILE_BLOCK_SIZE/1024。如果不提及KEY_BLOCK_SIZE,则该值将自动从FILE_BLOCK_SIZE中获取。
您可以创建多个具有不同FILE_BLOCK_SIZE值的压缩通用表空间,并将表添加到所需的表空间中:
- 创建一个通用的压缩表空间。您可以创建一个
FILE_BLOCK_SIZE为 8k 的表空间,另一个为 4k 的表空间,并将所有KEY_BLOCK_SIZE为 8 的表移动到 8k,将 4 移动到 4k:
mysql> CREATE TABLESPACE `ts_8k` ADD DATAFILE 'ts_8k.ibd' FILE_BLOCK_SIZE = 8192 Engine=InnoDB;
Query OK, 0 rows affected (0.01 sec)
mysql> CREATE TABLESPACE `ts_4k` ADD DATAFILE 'ts_4k.ibd' FILE_BLOCK_SIZE = 4096 Engine=InnoDB;
Query OK, 0 rows affected (0.04 sec)
- 通过提及
ROW_FORMAT=COMPRESSED在这些表空间中创建压缩表:
mysql> CREATE TABLE compress_table_1_8k (id INT PRIMARY KEY) TABLESPACE ts_8k ROW_FORMAT=COMPRESSED;
Query OK, 0 rows affected (0.01 sec)
如果不提及ROW_FORMAT=COMPRESSED,将会出现错误:
mysql> CREATE TABLE compress_table_2_8k (id INT PRIMARY KEY) TABLESPACE ts_8k;
ERROR 1478 (HY000): InnoDB: Tablespace `ts_8k` uses block size 8192 and cannot contain a table with physical page size 16384
可选地,您可以提及KEY_BLOCK_SIZE=FILE_BLOCK_SIZE/1024:
mysql> CREATE TABLE compress_table_8k (id INT PRIMARY KEY) TABLESPACE ts_8k ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8;
Query OK, 0 rows affected (0.01 sec)
如果提及的内容不是FILE_BLOCK_SIZE/1024,将会出现错误:
mysql> CREATE TABLE compress_table_2_8k (id INT PRIMARY KEY) TABLESPACE ts_8k ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4;
ERROR 1478 (HY000): InnoDB: Tablespace `ts_8k` uses block size 8192 and cannot contain a table with physical page size 4096
- 只有
KEY_BLOCK_SIZE匹配时,才能将表从file_per_table表空间移动到压缩通用表空间。否则,将会出现错误:
mysql> CREATE TABLE compress_tables_4k (id INT PRIMARY KEY) TABLESPACE innodb_file_per_table ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4;
Query OK, 0 rows affected (0.02 sec)
mysql> ALTER TABLE compress_tables_4k TABLESPACE ts_4k;
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> ALTER TABLE compress_tables_4k TABLESPACE ts_8k;
ERROR 1478 (HY000): InnoDB: Tablespace `ts_8k` uses block size 8192 and cannot contain a table with physical page size 4096
第十二章:管理日志
在本章中,我们将涵盖以下内容:
-
管理错误日志
-
管理常规查询日志和慢查询日志
-
管理二进制日志
介绍
现在您将了解如何管理不同类型的日志:错误日志、常规查询日志、慢查询日志、二进制日志、中继日志和 DDL 日志。
管理错误日志
根据 MySQL 文档:
错误日志包含了mysqld启动和关闭时间的记录。它还包含了在服务器启动和关闭期间以及服务器运行时发生的错误、警告和注释等诊断消息。
错误日志子系统由执行日志事件过滤和写入的组件以及一个名为log_error_services的系统变量组成,该变量配置了要启用哪些组件以实现所需的日志记录结果。global.log_error_services的默认值是log_filter_internal; log_sink_internal:
mysql> SELECT @@global.log_error_services;
+----------------------------------------+
| @@global.log_error_services |
+----------------------------------------+
| log_filter_internal; log_sink_internal |
+----------------------------------------+
该值表示日志事件首先通过内置过滤组件log_filter_internal,然后通过内置日志写入组件log_sink_internal。组件顺序很重要,因为服务器按照列出的顺序执行组件。log_error_services值中命名的任何可加载(非内置)组件必须首先使用INSTALL COMPONENT进行安装,这将在本节中描述。
要了解所有类型的错误日志记录,请参阅dev.mysql.com/doc/refman/8.0/en/error-log.html
如何做...
错误日志在某种程度上很容易。让我们先看看如何配置错误日志。
配置错误日志
错误日志由log_error变量(启动脚本的--log-error)控制。
如果未给出--log-error,默认目的地是控制台。
如果给出--log-error而没有命名文件,则默认目的地是data directory中名为host_name.err的文件。
如果给出--log-error以命名文件,则默认目的地是该文件(如果名称没有后缀,则添加.err后缀),位于data directory下,除非给出绝对路径名以指定不同的位置。
log_error_verbosity系统变量控制服务器写入错误、警告和注释消息到错误日志的详细程度。允许的log_error_verbosity值为1(仅错误)、2(错误和警告)和3(错误、警告和注释),默认值为3。
要更改错误日志位置,请编辑配置文件并重新启动 MySQL:
shell> sudo mkdir /var/log/mysql
shell> sudo chown -R mysql:mysql /var/log/mysql
shell> sudo vi /etc/my.cnf
[mysqld]
log-error=/var/log/mysql/mysqld.log
shell> sudo systemctl restart mysql
验证错误日志:
mysql> SHOW VARIABLES LIKE 'log_error';
+---------------+---------------------------+
| Variable_name | Value |
+---------------+---------------------------+
| log_error | /var/log/mysql/mysqld.log |
+---------------+---------------------------+
1 row in set (0.00 sec)
要调整详细程度,您可以动态更改log_error_verbosity变量。但是,建议保持默认值3,以便记录错误、警告和注释消息:
mysql> SET @@GLOBAL.log_error_verbosity=2;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @@GLOBAL.log_error_verbosity;
+------------------------------+
| @@GLOBAL.log_error_verbosity |
+------------------------------+
| 2 |
+------------------------------+
1 row in set (0.00 sec)
旋转错误日志
假设错误日志文件变得更大,您想要对其进行旋转;您可以简单地移动文件并执行FLUSH LOGS命令:
shell> sudo mv /var/log/mysql/mysqld.log /var/log/mysql/mysqld.log.0;
shell> mysqladmin -u root -p<password> flush-logs
mysqladmin: [Warning] Using a password on the command line interface can be insecure.
shell> ls -lhtr /var/log/mysql/mysqld.log
-rw-r-----. 1 mysql mysql 0 Oct 10 14:03 /var/log/mysql/mysqld.log
shell> ls -lhtr /var/log/mysql/mysqld.log.0
-rw-r-----. 1 mysql mysql 3.4K Oct 10 14:03 /var/log/mysql/mysqld.log.0
您可以使用一些脚本自动化上述步骤,并将它们放入cron中。
如果错误日志文件的位置对服务器不可写,日志刷新操作将无法创建新的日志文件:
shell> sudo mv /var/log/mysqld.log /var/log/mysqld.log.0 && mysqladmin flush-logs -u root -p<password>
mysqladmin: [Warning] Using a password on the command line interface can be insecure.
mysqladmin: refresh failed; error: 'Unknown error'
使用系统日志进行日志记录
要使用系统日志进行日志记录,您需要加载名为log_sink_syseventlog的系统日志写入器。您可以使用内置过滤器log_filter_internal进行过滤:
- 加载系统日志写入器:
mysql> INSTALL COMPONENT 'file://component_log_sink_syseventlog';
Query OK, 0 rows affected (0.43 sec)
- 使其在重新启动时持久化:
mysql> SET PERSIST log_error_services = 'log_filter_internal; log_sink_syseventlog';
Query OK, 0 rows affected (0.00 sec)
mysql> SHOW VARIABLES LIKE 'log_error_services';
+--------------------+-------------------------------------------+
| Variable_name | Value |
+--------------------+-------------------------------------------+
| log_error_services | log_filter_internal; log_sink_syseventlog |
+--------------------+-------------------------------------------+
1 row in set (0.00 sec)
- 您可以验证日志将被定向到系统日志。在 CentOS 和 Red Hat 上,您可以在
/var/log/messages中检查;在 Ubuntu 上,您可以在/var/log/syslog中检查。
为了演示,服务器已重新启动。您可以在系统日志中看到这些日志:
shell> sudo grep mysqld /var/log/messages | tail
Oct 10 14:50:31 centos7 mysqld[20953]: InnoDB: Buffer pool(s) dump completed at 171010 14:50:31
Oct 10 14:50:32 centos7 mysqld[20953]: InnoDB: Shutdown completed; log sequence number 350327631
Oct 10 14:50:32 centos7 mysqld[20953]: InnoDB: Removed temporary tablespace data file: "ibtmp1"
Oct 10 14:50:32 centos7 mysqld[20953]: Shutting down plugin 'MEMORY'
Oct 10 14:50:32 centos7 mysqld[20953]: Shutting down plugin 'CSV'
Oct 10 14:50:32 centos7 mysqld[20953]: Shutting down plugin 'sha256_password'
Oct 10 14:50:32 centos7 mysqld[20953]: Shutting down plugin 'mysql_native_password'
Oct 10 14:50:32 centos7 mysqld[20953]: Shutting down plugin 'binlog'
Oct 10 14:50:32 centos7 mysqld[20953]: /usr/sbin/mysqld: Shutdown complete
Oct 10 14:50:33 centos7 mysqld[21220]: /usr/sbin/mysqld: ready for connections. Version: '8.0.3-rc-log' socket: '/var/lib/mysql/mysql.sock' port: 3306 MySQL Community Server (GPL)
如果有多个运行的mysqld进程,您可以使用[]中指定的 PID 进行区分。否则,您可以设置log_syslog_tag变量,该变量会在服务器标识符前附加一个连字符,从而得到一个mysqld-tag_val的标识符。
例如,您可以为实例添加标签,如instance1:
mysql> SELECT @@GLOBAL.log_syslog_tag;
+-------------------------+
| @@GLOBAL.log_syslog_tag |
+-------------------------+
| |
+-------------------------+
1 row in set (0.00 sec)
mysql> SET @@GLOBAL.log_syslog_tag='instance1';
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @@GLOBAL.log_syslog_tag;
+-------------------------+
| @@GLOBAL.log_syslog_tag |
+-------------------------+
| instance1 |
+-------------------------+
1 row in set (0.01 sec)
shell> sudo systemctl restart mysqld
shell> sudo grep mysqld /var/log/messages | tail
Oct 10 14:59:20 centos7 mysqld-instance1[21220]: InnoDB: Buffer pool(s) dump completed at 171010 14:59:20
Oct 10 14:59:21 centos7 mysqld-instance1[21220]: InnoDB: Shutdown completed; log sequence number 350355306
Oct 10 14:59:21 centos7 mysqld-instance1[21220]: InnoDB: Removed temporary tablespace data file: "ibtmp1"
Oct 10 14:59:21 centos7 mysqld-instance1[21220]: Shutting down plugin 'MEMORY'
Oct 10 14:59:21 centos7 mysqld-instance1[21220]: Shutting down plugin 'CSV'
Oct 10 14:59:21 centos7 mysqld-instance1[21220]: Shutting down plugin 'sha256_password'
Oct 10 14:59:21 centos7 mysqld-instance1[21220]: Shutting down plugin 'mysql_native_password'
Oct 10 14:59:21 centos7 mysqld-instance1[21220]: Shutting down plugin 'binlog'
Oct 10 14:59:21 centos7 mysqld-instance1[21220]: /usr/sbin/mysqld: Shutdown complete
Oct 10 14:59:22 centos7 mysqld[21309]: /usr/sbin/mysqld: ready for connections. Version: '8.0.3-rc-log' socket: '/var/lib/mysql/mysql.sock' port: 3306 MySQL Community Server (GPL)
您会注意到instance1标签附加到日志中,以便您可以轻松识别多个实例之间的区别。
如果要切换回原始日志记录,可以将log_error_services设置为'log_filter_internal; log_sink_internal':
mysql> SET @@global.log_error_services='log_filter_internal; log_sink_internal';
Query OK, 0 rows affected (0.00 sec)
以 JSON 格式记录错误
要使用 JSON 格式进行记录,您需要加载名为log_sink_json的 JSON 日志写入器。您可以使用内置过滤器log_filter_internal进行过滤:
- 安装 JSON 日志写入器:
mysql> INSTALL COMPONENT 'file://component_log_sink_json';
Query OK, 0 rows affected (0.05 sec)
- 使其在重新启动后保持不变:
mysql> SET PERSIST log_error_services = 'log_filter_internal; log_sink_json';
Query OK, 0 rows affected (0.00 sec)
- JSON 日志写入器根据
log_error系统变量给出的默认错误日志目的地确定其输出目的地:
mysql> SHOW VARIABLES LIKE 'log_error';
+---------------+---------------------------+
| Variable_name | Value |
+---------------+---------------------------+
| log_error | /var/log/mysql/mysqld.log |
+---------------+---------------------------+
1 row in set (0.00 sec)
- 日志将类似于
mysqld.log.00.json。
重新启动后,JSON 日志文件如下所示:
shell> sudo less /var/log/mysql/mysqld.log.00.json
{ "prio" : 2, "err_code" : 4356, "subsystem" : "", "SQL_state" : "HY000", "source_file" : "sql_plugin.cc", "function" : "reap_plugins", "msg" : "Shutting down plugin 'sha256_password'", "time" : "2017-10-15T12:29:08.862969Z", "err_symbol" : "ER_PLUGIN_SHUTTING_DOWN_PLUGIN", "label" : "Note" }
{ "prio" : 2, "err_code" : 4356, "subsystem" : "", "SQL_state" : "HY000", "source_file" : "sql_plugin.cc", "function" : "reap_plugins", "msg" : "Shutting down plugin 'mysql_native_password'", "time" : "2017-10-15T12:29:08.862975Z", "err_symbol" : "ER_PLUGIN_SHUTTING_DOWN_PLUGIN", "label" : "Note" }
{ "prio" : 2, "err_code" : 4356, "subsystem" : "", "SQL_state" : "HY000", "source_file" : "sql_plugin.cc", "function" : "reap_plugins", "msg" : "Shutting down plugin 'binlog'", "time" : "2017-10-15T12:29:08.863758Z", "err_symbol" : "ER_PLUGIN_SHUTTING_DOWN_PLUGIN", "label" : "Note" }
{ "prio" : 2, "err_code" : 1079, "subsystem" : "", "SQL_state" : "HY000", "source_file" : "mysqld.cc", "function" : "clean_up", "msg" : "/usr/sbin/mysqld: Shutdown complete\u000a", "time" : "2017-10-15T12:29:08.867077Z", "err_symbol" : "ER_SHUTDOWN_COMPLETE", "label" : "Note" }
{ "log_type" : 1, "prio" : 0, "err_code" : 1408, "msg" : "/usr/sbin/mysqld: ready for connections. Version: '8.0.3-rc-log' socket: '/var/lib/mysql/mysql.sock' port: 3306 MySQL Community Server (GPL)", "time" : "2017-10-15T12:29:10.952502Z", "err_symbol" : "ER_STARTUP", "SQL_state" : "HY000", "label" : "Note" }
如果要切换回原始日志记录,可以将log_error_services设置为'log_filter_internal; log_sink_internal':
mysql> SET @@global.log_error_services='log_filter_internal; log_sink_internal';
Query OK, 0 rows affected (0.00 sec)
要了解有关错误记录配置的更多信息,请参阅dev.mysql.com/doc/refman/8.0/en/error-log-component-configuration.html。
管理一般查询日志和慢查询日志
有两种方式可以记录查询。一种是通过一般查询日志,另一种是通过慢查询日志。在本节中,您将学习如何配置它们。
如何做...
我们将在以下子节中详细介绍。
一般查询日志
根据 MySQL 文档:
一般查询日志是mysqld正在执行的一般记录。当客户端连接或断开连接时,服务器会将信息写入此日志,并记录从客户端接收到的每个 SQL 语句。当您怀疑客户端中存在错误并想要确切知道客户端发送给mysqld的内容时,一般查询日志可能非常有用:
- 指定日志文件。如果不指定,它将在
data directory中以hostname.log的名称创建。
服务器会在data directory中创建文件,除非给出绝对路径名以指定不同的目录:
mysql> SET @@GLOBAL.general_log_file='/var/log/mysql/general_query_log';
Query OK, 0 rows affected (0.04 sec)
- 启用一般查询日志:
mysql> SET GLOBAL general_log = 'ON';
Query OK, 0 rows affected (0.00 sec)
- 您可以看到查询已记录:
shell> sudo cat /var/log/mysql/general_query_log
/usr/sbin/mysqld, Version: 8.0.3-rc-log (MySQL Community Server (GPL)). started with:
Tcp port: 3306 Unix socket: /var/lib/mysql/mysql.sock
Time Id Command Argument
2017-10-11T04:21:00.118944Z 220 Connect root@localhost on using Socket
2017-10-11T04:21:00.119212Z 220 Query select @@version_comment limit 1
2017-10-11T04:21:03.217603Z 220 Query SELECT DATABASE()
2017-10-11T04:21:03.218275Z 220 Init DB employees
2017-10-11T04:21:03.219339Z 220 Query show databases
2017-10-11T04:21:03.220189Z 220 Query show tables
2017-10-11T04:21:03.227635Z 220 Field List current_dept_emp
2017-10-11T04:21:03.233820Z 220 Field List departments
2017-10-11T04:21:03.235937Z 220 Field List dept_emp
2017-10-11T04:21:03.236089Z 220 Field List dept_emp_latest_date
2017-10-11T04:21:03.236337Z 220 Field List dept_manager
2017-10-11T04:21:03.237291Z 220 Field List employee_names
2017-10-11T04:21:03.237999Z 220 Field List employees
2017-10-11T04:21:03.247921Z 220 Field List titles
2017-10-11T04:21:03.248217Z 220 Field List titles_only
~
~
2017-10-11T04:21:09.483117Z 220 Query select count(*) from employees
2017-10-11T04:21:10.523421Z 220 Quit
一般查询日志会生成一个非常大的日志文件。在生产服务器上启用时要非常小心。它会严重影响服务器的性能。
慢查询日志
根据 MySQL 文档:
“慢查询日志由执行时间超过 long_query_time 秒并且需要至少 min_examined_row_limit 行才能检查的 SQL 语句组成。”
要记录所有查询,可以将long_query_time的值设置为0。long_query_time的默认值为10秒,min_examined_row_limit为0。
默认情况下,不会记录不使用索引进行查找的查询和管理语句(例如ALTER TABLE,ANALYZE TABLE,CHECK TABLE,CREATE INDEX,DROP INDEX,OPTIMIZE TABLE和REPAIR TABLE)。可以使用log_slow_admin_statements和log_queries_not_using_indexes更改此行为。
要启用慢查询日志,可以动态设置slow_query_log=1,并可以使用slow_query_log_file设置文件名。要指定日志目的地,请使用--log-output:
- 验证
long_query_time并根据您的要求进行调整:
mysql> SELECT @@GLOBAL.LONG_QUERY_TIME;
+--------------------------+
| @@GLOBAL.LONG_QUERY_TIME |
+--------------------------+
| 10.000000 |
+--------------------------+
1 row in set (0.00 sec)
mysql> SET @@GLOBAL.LONG_QUERY_TIME=1;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @@GLOBAL.LONG_QUERY_TIME;
+--------------------------+
| @@GLOBAL.LONG_QUERY_TIME |
+--------------------------+
| 1.000000 |
+--------------------------+
1 row in set (0.00 sec)
- 验证慢查询文件。默认情况下,它将在
data directory中以hostname-slow日志的形式存在:
mysql> SELECT @@GLOBAL.slow_query_log_file;
+---------------------------------+
| @@GLOBAL.slow_query_log_file |
+---------------------------------+
| /var/lib/mysql/server1-slow.log |
+---------------------------------+
1 row in set (0.00 sec)
mysql> SET @@GLOBAL.slow_query_log_file='/var/log/mysql/mysql_slow.log'; Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @@GLOBAL.slow_query_log_file;
+-------------------------------+
| @@GLOBAL.slow_query_log_file |
+-------------------------------+
| /var/log/mysql/mysql_slow.log |
+-------------------------------+
1 row in set (0.00 sec)
mysql> FLUSH LOGS;
Query OK, 0 rows affected (0.03 sec)
- 启用慢查询日志:
mysql> SELECT @@GLOBAL.slow_query_log;
+-------------------------+
| @@GLOBAL.slow_query_log |
+-------------------------+
| 0 |
+-------------------------+
1 row in set (0.00 sec)
mysql> SET @@GLOBAL.slow_query_log=1;
Query OK, 0 rows affected (0.01 sec)
mysql> SELECT @@GLOBAL.slow_query_log;
+-------------------------+
| @@GLOBAL.slow_query_log |
+-------------------------+
| 1 |
+-------------------------+
1 row in set (0.00 sec)
- 验证查询是否已记录(您必须执行一些长时间运行的查询才能在慢查询日志中看到它们):
mysql> SELECT SLEEP(2);
+----------+
| SLEEP(2) |
+----------+
| 0 |
+----------+
1 row in set (2.00 sec)
shell> sudo less /var/log/mysql/mysql_slow.log
/usr/sbin/mysqld, Version: 8.0.3-rc-log (MySQL Community Server (GPL)). started with:
Tcp port: 3306 Unix socket: /var/lib/mysql/mysql.sock
Time Id Command Argument
# Time: 2017-10-15T12:43:55.038601Z
# User@Host: root[root] @ localhost [] Id: 7
# Query_time: 2.000845 Lock_time: 0.000000 Rows_sent: 1 Rows_examined: 0
SET timestamp=1508071435;
SELECT SLEEP(2);
选择查询日志输出目的地
您可以通过指定log_output变量在 MySQL 本身中将查询记录到FILE或TABLE中,该变量可以是FILE或TABLE,也可以是FILE和TABLE。
如果将log_output指定为FILE,则一般查询日志和慢查询日志将分别写入general_log_file和slow_query_log_file指定的文件中。
如果您将log_output指定为TABLE,则一般查询日志和慢查询日志将分别写入mysql.general_log和mysql.slow_log表中。日志内容可以通过 SQL 语句访问。
例如:
mysql> SET @@GLOBAL.log_output='TABLE';
Query OK, 0 rows affected (0.00 sec)
mysql> SET @@GLOBAL.general_log='ON';
Query OK, 0 rows affected (0.02 sec)
执行一些查询,然后查询mysql.general_log表:
mysql> SELECT * FROM mysql.general_log WHERE command_type='Query' \G
~
~
*************************** 3\. row ***************************
event_time: 2017-10-25 10:56:56.416746
user_host: root[root] @ localhost []
thread_id: 2421
server_id: 32
command_type: Query
argument: show databases
*************************** 4\. row ***************************
event_time: 2017-10-25 10:56:56.418896
user_host: root[root] @ localhost []
thread_id: 2421
server_id: 32
command_type: Query
argument: show tables
*************************** 5\. row ***************************
event_time: 2017-10-25 10:57:08.207964
user_host: root[root] @ localhost []
thread_id: 2421
server_id: 32
command_type: Query
argument: select * from salaries limit 1
*************************** 6\. row ***************************
event_time: 2017-10-25 10:57:47.041475
user_host: root[root] @ localhost []
thread_id: 2421
server_id: 32
command_type: Query
argument: SELECT * FROM mysql.general_log WHERE command_type='Query'
您可以以类似的方式使用slow_log表:
mysql> SET @@GLOBAL.slow_query_log=1;
Query OK, 0 rows affected (0.00 sec)
mysql> SET @@GLOBAL.long_query_time=1;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT SLEEP(2);
+----------+
| SLEEP(2) |
+----------+
| 0 |
+----------+
1 row in set (2.00 sec)
mysql> SELECT * FROM mysql.slow_log \G
*************************** 1\. row ***************************
start_time: 2017-10-25 11:01:44.817421
user_host: root[root] @ localhost []
query_time: 00:00:02.000530
lock_time: 00:00:00.000000
rows_sent: 1
rows_examined: 0
db: employees
last_insert_id: 0
insert_id: 0
server_id: 32
sql_text: SELECT SLEEP(2)
thread_id: 2421
1 row in set (0.00 sec)
如果慢查询日志表变得非常庞大,您可以通过创建一个新表并交换它来进行轮换:
- 创建一个新表
mysql.general_log_new:
mysql> DROP TABLE IF EXISTS mysql.general_log_new;
Query OK, 0 rows affected, 1 warning (0.19 sec)
mysql> CREATE TABLE mysql.general_log_new LIKE mysql.general_log;
Query OK, 0 rows affected (0.10 sec)
- 使用
RENAME TABLE命令交换表:
mysql> RENAME TABLE mysql.general_log TO mysql.general_log_1, mysql.general_log_new TO mysql.general_log;
Query OK, 0 rows affected (0.00 sec)
管理二进制日志
在这一部分,将介绍在复制环境中管理二进制日志。基本的二进制日志处理已经在第六章“二进制日志记录”中介绍过,使用PURGE BINARY LOGS命令和expire_logs_days变量。
在复制环境中使用这些方法是不安全的,因为如果任何一个从服务器没有消耗二进制日志并且您已经删除了它们,从服务器将会失去同步,您将需要重建它。
安全地删除二进制日志的方法是检查每个从服务器上已读取了哪些二进制日志,并删除它们。您可以使用mysqlbinlogpurge实用程序来实现这一点。
如何做...
在任何一个服务器上执行mysqlbinlogpurge脚本,并指定主服务器和从服务器主机。该脚本连接到所有的从服务器,并找出最新应用的二进制日志。然后清除主二进制日志直到那一点。您需要一个超级用户来连接到所有的从服务器:
- 连接到任何一个服务器并执行
mysqlbinlogpurge脚本:
shell> mysqlbinlogpurge --master=dbadmin:<pass>@master:3306 --slaves=dbadmin:<pass>@slave1:3306,dbadmin:<pass>@slave2:3306
mysql> SHOW BINARY LOGS;
+--------------------+-----------+
| Log_name | File_size |
+--------------------+-----------+
| master-bin.000001 | 177 |
~
| master-bin.000018 | 47785 |
| master-bin.000019 | 203 |
| master-bin.000020 | 203 |
| master-bin.000021 | 177 |
| master-bin.000022 | 203 |
| master-bin.000023 | 57739432 |
+--------------------+-----------+
23 rows in set (0.00 sec)
shell> mysqlbinlogpurge --master=dbadmin:<pass>@master:3306 --slaves=dbadmin:<pass>@slave1:3306,dbadmin:<pass>@slave2:3306
# Latest binlog file replicated by all slaves: master-bin.000022
# Purging binary logs prior to 'master-bin.000023'
- 如果您希望在命令中不指定的情况下发现所有的从服务器,您应该在所有的从服务器上设置
report_host和report_port,并重新启动 MySQL 服务器。在每个从服务器上:
shell> sudo vi /etc/my.cnf
[mysqld]
report-host = slave1
report-port = 3306
shell> sudo systemctl restart mysql
mysql> SHOW VARIABLES LIKE 'report%';
+-----------------+---------------+
| Variable_name | Value |
+-----------------+---------------+
| report_host | slave1 |
| report_password | |
| report_port | 3306 |
| report_user | |
+-----------------+---------------+
4 rows in set (0.00 sec)
- 使用
discover-slaves-login选项执行mysqlbinlogpurge:
mysql> SHOW BINARY LOGS;
+--------------------+-----------+
| Log_name | File_size |
+--------------------+-----------+
| centos7-bin.000025 | 203 |
| centos7-bin.000026 | 203 |
| centos7-bin.000027 | 203 |
| centos7-bin.000028 | 154 |
+--------------------+-----------+
4 rows in set (0.00 sec)
shell> mysqlbinlogpurge --master=dbadmin:<pass>@master --discover-slaves-login=dbadmin:<pass>
# Discovering slaves for master at master:3306
# Discovering slave at slave1:3306
# Found slave: slave1:3306
# Discovering slave at slave2:3306
# Found slave: slave2:3306
# Latest binlog file replicated by all slaves: master-bin.000027
# Purging binary logs prior to 'master-bin.000028'
第十三章:性能调优
在本章中,我们将介绍以下内容:
-
解释计划
-
基准测试查询和服务器
-
添加索引
-
不可见索引
-
降序索引
-
使用 pt-query-digest 分析慢查询
-
优化数据类型
-
删除重复和冗余索引
-
检查索引使用
-
控制查询优化器
-
使用索引提示
-
使用生成列为 JSON 建立索引
-
使用资源组
-
使用 performance_schema
-
使用 sys 模式
介绍
本章将带您了解查询和模式调优。数据库是用于执行查询的,使其运行更快是调优的最终目标。数据库的性能取决于许多因素,主要是查询、模式、配置设置和硬件。
在本章中,我们将使用 employees 数据库来解释所有示例。您可能已经在前面的章节中以多种方式转换了 employees 数据库。建议在尝试本章中提到的示例之前,再次加载示例 employees 数据。您可以参考第二章加载示例数据了解如何加载示例数据。
解释计划
MySQL 执行查询的方式是数据库性能的主要因素之一。您可以使用EXPLAIN命令验证 MySQL 执行计划。从 MySQL 5.7.2 开始,您可以使用EXPLAIN来检查其他会话中当前执行的查询。EXPLAIN FORMAT=JSON提供了详细信息。
如何做...
让我们深入了解细节。
使用 EXPLAIN
解释计划提供了优化器执行查询的信息。您只需要在查询前加上EXPLAIN关键字:
mysql> EXPLAIN SELECT dept_name FROM dept_emp JOIN employees ON dept_emp.emp_no=employees.emp_no JOIN departments ON departments.dept_no=dept_emp.dept_no WHERE employees.first_name='Aamer'\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: NULL
type: ref
possible_keys: PRIMARY,name
key: name
key_len: 58
ref: const
rows: 228
filtered: 100.00
Extra: Using index
*************************** 2\. row ***************************
id: 1
select_type: SIMPLE
table: dept_emp
partitions: NULL
type: ref
possible_keys: PRIMARY,dept_no
key: PRIMARY
key_len: 4
ref: employees.employees.emp_no
rows: 1
filtered: 100.00
Extra: Using index
*************************** 3\. row ***************************
id: 1
select_type: SIMPLE
table: departments
partitions: NULL
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 16
ref: employees.dept_emp.dept_no
rows: 1
filtered: 100.00
Extra: NULL
3 rows in set, 1 warning (0.00 sec)
使用 EXPLAIN JSON
以 JSON 格式使用解释计划提供了关于查询执行的完整信息:
mysql> EXPLAIN FORMAT=JSON SELECT dept_name FROM dept_emp JOIN employees ON dept_emp.emp_no=employees.emp_no JOIN departments ON departments.dept_no=dept_emp.dept_no WHERE employees.first_name='Aamer'\G
*************************** 1\. row ***************************
EXPLAIN: {
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "286.13"
},
"nested_loop": [
{
"table": {
"table_name": "employees",
"access_type": "ref",
"possible_keys": [
"PRIMARY",
"name"
],
"key": "name",
"used_key_parts": [
"first_name"
],
"key_length": "58",
"ref": [
"const"
],
"rows_examined_per_scan": 228,
"rows_produced_per_join": 228,
"filtered": "100.00",
"using_index": true,
"cost_info": {
"read_cost": "1.12",
"eval_cost": "22.80",
"prefix_cost": "23.92",
"data_read_per_join": "30K"
},
"used_columns": [
"emp_no",
"first_name"
]
}
},
{
"table": {
"table_name": "dept_emp",
"access_type": "ref",
"possible_keys": [
"PRIMARY",
"dept_no"
],
"key": "PRIMARY",
"used_key_parts": [
"emp_no"
],
"key_length": "4",
"ref": [
"employees.employees.emp_no"
],
"rows_examined_per_scan": 1,
"rows_produced_per_join": 252,
"filtered": "100.00",
"using_index": true,
"cost_info": {
"read_cost": "148.78",
"eval_cost": "25.21",
"prefix_cost": "197.91",
"data_read_per_join": "7K"
},
"used_columns": [
"emp_no",
"dept_no"
]
}
},
{
"table": {
"table_name": "departments",
"access_type": "eq_ref",
"possible_keys": [
"PRIMARY"
],
"key": "PRIMARY",
"used_key_parts": [
"dept_no"
],
"key_length": "16",
"ref": [
"employees.dept_emp.dept_no"
],
"rows_examined_per_scan": 1,
"rows_produced_per_join": 252,
"filtered": "100.00",
"cost_info": {
"read_cost": "63.02",
"eval_cost": "25.21",
"prefix_cost": "286.13",
"data_read_per_join": "45K"
},
"used_columns": [
"dept_no",
"dept_name"
]
}
}
]
}
}
使用连接的 EXPLAIN
您可以为已运行的会话运行解释计划。您需要指定连接 ID:
要获取连接 ID,请执行:
mysql> SELECT CONNECTION_ID();
+-----------------+
| CONNECTION_ID() |
+-----------------+
| 778 |
+-----------------+
1 row in set (0.00 sec)
mysql> EXPLAIN FORMAT=JSON FOR CONNECTION 778\G
*************************** 1\. row ***************************
EXPLAIN: {
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "881.04"
},
"nested_loop": [
{
"table": {
"table_name": "employees",
"access_type": "index",
"possible_keys": [
"PRIMARY"
],
"key": "name",
"used_key_parts": [
"first_name",
"last_name"
],
"key_length": "124",
"rows_examined_per_scan": 1,
"rows_produced_per_join": 1,
"filtered": "100.00",
"using_index": true,
"cost_info": {
"read_cost": "880.24",
"eval_cost": "0.10",
"prefix_cost": "880.34",
"data_read_per_join": "136"
},
~
~
1 row in set (0.00 sec)
如果连接没有运行任何SELECT/UPDATE/INSERT/DELETE/REPLACE查询,它将抛出一个错误:
mysql> EXPLAIN FOR CONNECTION 779;
ERROR 3012 (HY000): EXPLAIN FOR CONNECTION command is supported only for SELECT/UPDATE/INSERT/DELETE/REPLACE
请参阅dev.mysql.com/doc/refman/8.0/en/explain-output.html了解更多关于解释计划格式的信息。JSON 格式在www.percona.com/blog/category/explain-2/explain-formatjson-is-cool/中有非常清晰的解释。
基准测试查询和服务器
假设您想要找出哪个查询更快。解释计划给了您一个想法,但有时您不能根据它来决定。如果查询时间在几十秒的数量级上,您可以在服务器上执行它们,并找出哪个更快。但是,如果查询时间在几毫秒的数量级上,您不能仅根据单次执行来决定。
您可以使用mysqlslap实用程序(它随 MySQL 客户端安装一起提供),它模拟了 MySQL 服务器的客户端负载,并报告每个阶段的时间。它的工作方式就好像多个客户端正在访问服务器。在本节中,您将了解mysqlslap的用法;在后续章节中,您将了解mysqlslap的强大之处。
如何做...
假设您想要测量查询的时间;如果您在 MySQL 客户端中执行它,您可以知道大约的执行时间,精度为 100 毫秒:
mysql> pager grep rows
PAGER set to 'grep rows'
mysql> SELECT e.emp_no, salary FROM salaries s JOIN employees e ON s.emp_no=e.emp_no WHERE (first_name='Adam');
2384 rows in set (0.00 sec)
您可以使用mysqlslap模拟客户端负载,并在多次迭代中同时运行前述 SQL:
shell> mysqlslap -u <user> -p<pass> --create-schema=employees --query="SELECT e.emp_no, salary FROM salaries s JOIN employees e ON s.emp_no=e.emp_no WHERE (first_name='Adam');" -c 1000 i 100
mysqlslap: [Warning] Using a password on the command line interface can be insecure.
Benchmark
Average number of seconds to run all queries: 3.216 seconds
Minimum number of seconds to run all queries: 3.216 seconds
Maximum number of seconds to run all queries: 3.216 seconds
Number of clients running queries: 1000
Average number of queries per client: 1
前述查询以 1,000 并发和 100 次迭代执行,平均耗时 3.216 秒。
您可以在文件中指定多个 SQL 并指定分隔符。mysqlslap运行文件中的所有查询:
shell> cat queries.sql
SELECT e.emp_no, salary FROM salaries s JOIN employees e ON s.emp_no=e.emp_no WHERE (first_name='Adam');
SELECT * FROM employees WHERE first_name='Adam' OR last_name='Adam';
SELECT * FROM employees WHERE first_name='Adam';shell> mysqlslap -u <user> -p<pass> --create-schema=employees --concurrency=10 --iterations=10 --query=query.sql --query=queries.sql --delimiter=";"
mysqlslap: [Warning] Using a password on the command line interface can be insecure.
Benchmark
Average number of seconds to run all queries: 5.173 seconds
Minimum number of seconds to run all queries: 5.010 seconds
Maximum number of seconds to run all queries: 5.257 seconds
Number of clients running queries: 10
Average number of queries per client: 3
您甚至可以自动生成表和 SQL 语句。这样,您可以将结果与先前的服务器设置进行比较:
shell> mysqlslap -u <user> -p<pass> --concurrency=100 --iterations=10 --number-int-cols=4 --number-char-cols=10 --auto-generate-sql
mysqlslap: [Warning] Using a password on the command line interface can be insecure.
Benchmark
Average number of seconds to run all queries: 1.640 seconds
Minimum number of seconds to run all queries: 1.511 seconds
Maximum number of seconds to run all queries: 1.791 seconds
Number of clients running queries: 100
Average number of queries per client: 0
您还可以使用performance_schema来获取所有与查询相关的指标,这在使用 performance_schema部分有解释。
添加索引
没有索引,MySQL 必须逐行扫描整个表来找到相关的行。如果表上有你正在过滤的列的索引,MySQL 可以快速在大数据文件中找到行,而不必扫描整个文件。
MySQL 可以在WHERE、ORDER BY和GROUP BY子句中使用索引来过滤行,也可以用于连接表。如果一个列上有多个索引,MySQL 会选择能够最大程度过滤行的索引。
你可以执行ALTER TABLE命令来添加或删除索引。索引的添加和删除都是在线操作,不会影响表上的 DML 操作,但在较大的表上需要很长时间。
主键(聚集索引)和次要索引
在继续之前,重要的是要理解主键(或聚集索引)是什么,以及次要索引是什么。
InnoDB按照主键的顺序存储行,以加快涉及主键列的查询和排序。这也被称为索引组织表,在 Oracle 术语中。所有其他索引都被称为次要键,它们存储主键的值(它们不直接引用行)。
假设表是:
mysql> CREATE TABLE index_example (
col1 int PRIMARY KEY,
col2 char(10),
KEY `col2`(`col2`)
);
表行根据col1的值进行排序和存储。如果你搜索col1的任何值,它可以直接指向物理行;这就是为什么聚集索引非常快。col2上的索引也包含col1的值,如果你搜索col2,col1的值会返回,然后在聚集索引中搜索实际行。
选择主键的提示:
-
它应该是
UNIQUE和NOT NULL。 -
选择尽可能小的键,因为所有的次要索引都存储主键。所以如果它很大,整体索引大小会占用更多的空间。
-
选择一个单调递增的值。物理行是根据主键排序的。所以如果你选择一个随机键,需要更多的行重新排列,这会导致性能下降。
AUTO_INCREMENT是主键的完美选择。 -
始终选择一个主键;如果找不到任何主键,添加一个
AUTO_INCREMENT列。如果你不选择任何主键,InnoDB内部会生成一个隐藏的聚集索引,带有 6 字节的行 ID。
如何做…
你可以通过查看表的定义来查看表的索引。你会注意到first_name和last_name上有一个索引。如果你通过指定first_name或者两者(first_name和last_name)来过滤行,MySQL 可以使用索引来加快查询。然而,如果你只指定last_name,索引就不能被使用;这是因为优化器只能使用索引的最左边的前缀。更详细的例子请参考dev.mysql.com/doc/refman/8.0/en/multiple-column-indexes.html:
mysql> ALTER TABLE employees ADD INDEX name(first_name, last_name);
Query OK, 0 rows affected (2.23 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> SHOW CREATE TABLE employees\G
*************************** 1\. row ***************************
Table: employees
Create Table: CREATE TABLE `employees` (
`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`),
KEY `name` (`first_name`,`last_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)
添加索引
你可以通过执行ALTER TABLE ADD INDEX命令添加索引。例如,如果你想在last_name上添加一个索引,请参考以下代码:
mysql> ALTER TABLE employees ADD INDEX (last_name);
Query OK, 0 rows affected (1.28 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> SHOW CREATE TABLE employees\G
*************************** 1\. row ***************************
Table: employees
Create Table: CREATE TABLE `employees` (
`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`),
KEY `name` (`first_name`,`last_name`),
KEY `last_name` (`last_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.01 sec)
你可以指定索引的名称;如果不指定,最左边的前缀将被用作名称。如果有任何重复,名称将被附加_2、_3等。
例如:
mysql> ALTER TABLE employees ADD INDEX index_last_name (last_name);
唯一索引
如果你希望索引是唯一的,可以指定关键字UNIQUE。例如:
mysql> ALTER TABLE employees ADD UNIQUE INDEX unique_name (last_name, first_name);
# There are few duplicate entries in employees database, the above statement is shown for illustration purpose only.
前缀索引
对于字符串列,可以创建只使用列值前导部分而不是整个列的索引。你需要指定前导部分的长度:
## `last_name` varchar(16) NOT NULL
mysql> ALTER TABLE employees ADD INDEX (last_name(10));
Query OK, 0 rows affected (1.78 sec)
Records: 0 Duplicates: 0 Warnings: 0
last_name的最大长度是16个字符,但索引只在前 10 个字符上创建。
删除索引
你可以使用ALTER TABLE命令删除索引:
mysql> ALTER TABLE employees DROP INDEX last_name;
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0
在生成的列上创建索引
不能在函数中使用列上的索引。假设你在hire_date上添加了一个索引:
mysql> ALTER TABLE employees ADD INDEX(hire_date);
Query OK, 0 rows affected (0.93 sec)
Records: 0 Duplicates: 0 Warnings: 0
在WHERE子句中有hire_date的查询可以使用hire_date上的索引:
mysql> EXPLAIN SELECT COUNT(*) FROM employees WHERE hire_date>'2000-01-01'\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: NULL
type: range
possible_keys: hire_date
key: hire_date
key_len: 3
ref: NULL
rows: 14
filtered: 100.00
Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)
相反,如果将hire_date放在函数中,MySQL 必须扫描整个表:
mysql> EXPLAIN SELECT COUNT(*) FROM employees WHERE YEAR(hire_date)>=2000\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: NULL
type: index
possible_keys: NULL
key: hire_date
key_len: 3
ref: NULL
rows: 291892
filtered: 100.00
Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)
因此,尽量避免在函数内部放置一个带索引的列。如果无法避免使用函数,则创建一个虚拟列并在虚拟列上添加索引:
mysql> ALTER TABLE employees ADD hire_date_year YEAR AS (YEAR(hire_date)) VIRTUAL, ADD INDEX (hire_date_year);
Query OK, 0 rows affected (1.16 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> SHOW CREATE TABLE employees\G
*************************** 1\. row ***************************
Table: employees
Create Table: CREATE TABLE `employees` (
`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,
`hire_date_year` year(4) GENERATED ALWAYS AS (year(`hire_date`)) VIRTUAL,
PRIMARY KEY (`emp_no`),
KEY `name` (`first_name`,`last_name`),
KEY `hire_date` (`hire_date`),
KEY `hire_date_year` (`hire_date_year`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)
现在,不再在查询中使用YEAR()函数,而是可以直接在WHERE子句中使用hire_date_year:
mysql> EXPLAIN SELECT COUNT(*) FROM employees WHERE hire_date_year>=2000\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: NULL
type: range
possible_keys: hire_date_year
key: hire_date_year
key_len: 2
ref: NULL
rows: 15
filtered: 100.00
Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)
请注意,即使使用YEAR(hire_date),优化器也会认识到YEAR()表达式与hire_date_year的定义相匹配,并且hire_date_year被索引;因此在执行计划构建过程中会考虑该索引:
mysql> EXPLAIN SELECT COUNT(*) FROM employees WHERE YEAR(hire_date)>=2000\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: NULL
type: range
possible_keys: hire_date_year
key: hire_date_year
key_len: 2
ref: NULL
rows: 15
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
不可见索引
如果要删除未使用的索引,而不是立即删除,可以将其标记为不可见,监视应用程序行为,然后再删除。以后,如果需要该索引,可以将其标记为可见,这与删除和重新添加索引相比非常快。
解释不可见索引,如果尚未添加普通索引,则需要添加普通索引。例如:
mysql> ALTER TABLE employees ADD INDEX (last_name);
Query OK, 0 rows affected (1.81 sec)
Records: 0 Duplicates: 0 Warnings: 0
如何做...
如果您希望删除last_name上的索引,而不是直接删除,可以使用ALTER TABLE命令将其标记为不可见:
mysql> EXPLAIN SELECT * FROM employees WHERE last_name='Aamodt'\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: NULL
type: ref
possible_keys: last_name
key: last_name
key_len: 66
ref: const
rows: 205
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
mysql> ALTER TABLE employees ALTER INDEX last_name INVISIBLE;
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> EXPLAIN SELECT * FROM employees WHERE last_name='Aamodt'\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 299733
filtered: 10.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
mysql> SHOW CREATE TABLE employees\G
*************************** 1\. row ***************************
Table: employees
Create Table: CREATE TABLE `employees` (
`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`),
KEY `name` (`first_name`,`last_name`),
KEY `last_name` (`last_name`) /*!80000 INVISIBLE */
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)
您会注意到通过last_name进行查询过滤时使用了last_name索引;标记为不可见后,它无法使用。您可以再次将其标记为可见:
mysql> ALTER TABLE employees ALTER INDEX last_name VISIBLE;
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
降序索引
在 MySQL 8 之前,索引定义可以包含顺序(升序或降序),但它只是被解析而不是被实现。索引值总是以升序存储的。MySQL 8.0 引入了对降序索引的支持。因此,索引定义中指定的顺序不会被忽略。降序索引实际上以降序存储键值。请记住,对降序查询来说,以相反的方式扫描升序索引是低效的。
考虑这样一种情况,在多列索引中,可以指定某些列为降序。这对于查询中有升序和降序ORDER BY子句的查询很有帮助。
假设您想对employees表进行排序,按first_name升序和last_name降序;MySQL 无法使用first_name和last_name上的索引。没有降序索引:
mysql> SHOW CREATE TABLE employees\G
*************************** 1\. row ***************************
Table: employees
Create Table: CREATE TABLE `employees` (
`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`),
KEY `name` (`first_name`,`last_name`),
KEY `last_name` (`last_name`) /*!80000 INVISIBLE */
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
在解释计划中,您会注意到索引名称(first_name和last_name)未被使用:
mysql> EXPLAIN SELECT * FROM employees ORDER BY first_name ASC, last_name DESC LIMIT 10\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 299733
filtered: 100.00
Extra: Using filesort
如何做...
- 添加一个降序索引:
mysql> ALTER TABLE employees ADD INDEX name_desc(first_name ASC, last_name DESC);
Query OK, 0 rows affected (1.61 sec)
Records: 0 Duplicates: 0 Warnings: 0
- 添加降序索引后,查询可以使用索引:
mysql> EXPLAIN SELECT * FROM employees ORDER BY first_name ASC, last_name DESC LIMIT 10\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: NULL
type: index
possible_keys: NULL
key: name_desc
key_len: 124
ref: NULL
rows: 10
filtered: 100.00
Extra: NULL
- 相同的索引可以用于另一种排序方式,即通过反向索引扫描按
first_name降序和last_name升序排序:
mysql> EXPLAIN SELECT * FROM employees ORDER BY first_name DESC, last_name ASC LIMIT 10\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: NULL
type: index
possible_keys: NULL
key: name_desc
key_len: 124
ref: NULL
rows: 10
filtered: 100.00
Extra: Backward index scan
使用pt-query-digest分析慢查询
pt-query-digest是 Percona Toolkit 的一部分,用于分析查询。可以通过以下任何一种方式收集查询:
-
慢查询日志
-
一般查询日志
-
进程列表
-
二进制日志
-
TCP 转储
Percona Toolkit 的安装在第十章中有介绍,表维护,安装 Percona Toolkit部分。在本节中,您将学习如何使用pt-query-digest。每种方法都有缺点。慢查询日志不包括所有查询,除非指定long_query_time为0,这会严重减慢系统。一般查询日志不包括查询时间。您无法从进程列表中获取完整的查询。只能使用二进制日志分析写入,并且使用 TCP 转储会导致服务器降级。通常,此工具用于具有 1 秒或更高long_query_time的慢查询日志。
如何做...
让我们详细分析使用pt-query-digest分析慢查询的细节。
慢查询日志
启用和配置慢查询日志在第十二章中有解释,管理日志,管理一般查询日志和慢查询日志。一旦启用慢查询日志并收集查询,就可以通过传递慢查询日志来运行pt-query-digest。
假设慢查询文件位于/var/lib/mysql/mysql-slow.log:
shell> sudo pt-query-digest /var/lib/mysql/ubuntu-slow.log > query_digest
摘要报告包含按查询执行次数乘以查询时间排名的查询。摘要中显示了查询的详细信息,如查询校验和(每种查询的唯一值)、平均时间、百分比时间和执行次数。您可以通过搜索查询校验和来深入了解特定查询。
摘要报告如下所示:
# 286.8s user time, 850ms system time, 232.75M rss, 315.73M vsz
# Current date: Sat Nov 18 05:16:55 2017
# Hostname: db1
# Files: /var/lib/mysql/db1-slow.log
# Rate limits apply
# Overall: 638.54k total, 2.06k unique, 0.49 QPS, 0.14x concurrency ______
# Time range: 2017-11-03 01:02:40 to 2017-11-18 05:16:47
# Attribute total min max avg 95% stddev median
# ============ ======= ======= ======= ======= ======= ======= =======
# Exec time 179486s 3us 2713s 281ms 21ms 15s 176us
# Lock time 1157s 0 36s 2ms 194us 124ms 49us
# Rows sent 18.25M 0 753.66k 29.96 212.52 1.63k 0.99
# Rows examine 157.39G 0 3.30G 258.45k 3.35k 24.78M 0.99
# Rows affecte 3.66M 0 294.77k 6.01 0.99 1.16k 0
# Bytes sent 3.08G 0 95.15M 5.05k 13.78k 206.42k 174.84
# Merge passes 2.84k 0 97 0.00 0 0.16 0
# Tmp tables 129.02k 0 1009 0.21 0.99 1.43 0
# Tmp disk tbl 25.20k 0 850 0.04 0 1.09 0
# Tmp tbl size 26.21G 0 218.27M 43.04k 0 2.06M 0
# Query size 178.92M 6 452.25k 293.81 592.07 5.26k 72.65
# InnoDB:
# IO r bytes 79.06G 0 2.09G 200.37k 0 12.94M 0
# IO r ops 7.26M 0 233.16k 18.39 0 1.36k 0
# IO r wait 96525s 0 3452s 233ms 0 18s 0
# pages distin 526.99M 0 608.33k 1.30k 964.41 9.15k 1.96
# queue wait 0 0 0 0 0 0 0
# rec lock wai 46s 0 9s 111us 0 28ms 0
# Boolean:
# Filesort 5% yes, 94% no
# Filesort on 0% yes, 99% no
# Full join 3% yes, 96% no
# Full scan 40% yes, 59% no
# Tmp table 13% yes, 86% no
# Tmp table on 2% yes, 97% no
查询概要将如下所示:
# Rank Query ID Response time Calls R/Call V/M Item
# ==== ================== ================ ====== ========= ===== ========
# 1 0x55F499860A034BCB 76560.4220 42.7% 47 1628.9451 18.06 SELECT orders
# 2 0x3A2F0B98DA39BCB9 10490.4155 5.8% 2680 3.9143 33... SELECT orders order_status
# 3 0x25119C7C31A24011 7378.8763 4.1% 1534 4.8102 30.11 SELECT orders users
# 4 0x41106CE92AD9DFED 5412.7326 3.0% 15589 0.3472 2.98 SELECT sessions
# 5 0x860DCDE7AE0AD554 5187.5257 2.9% 500 10.3751 54.99 SELECT orders sessions
# 6 0x5DF64920B008AD63 4517.5041 2.5% 58 77.8880 22.23 UPDATE SELECT
# 7 0xC9F9A31DE77B93A1 4473.0208 2.5% 58 77.1210 96... INSERT SELECT tmpMove
# 8 0x8BF88451DA989BFF 4036.4413 2.2% 13 310.4955 16... UPDATE SELECT orders tmpDel
从前面的输出中,您可以推断出对于查询#1(0x55F499860A034BCB),所有执行的累积响应时间为76560秒。这占所有查询的累积响应时间的 42.7%。执行次数为 47,平均查询时间为1628秒。
您可以通过搜索校验和来查找任何查询。完整的查询、解释计划的命令和表状态都会显示出来。例如:
# Query 1: 0.00 QPS, 0.06x concurrency, ID 0x55F499860A034BCB at byte 249542900
# This item is included in the report because it matches --limit.
# Scores: V/M = 18.06
# Time range: 2017-11-03 01:39:19 to 2017-11-18 01:46:50
# Attribute pct total min max avg 95% stddev median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count 0 47
# Exec time 42 76560s 1182s 1854s 1629s 1819s 172s 1649s
# Lock time 0 3s 102us 994ms 70ms 293ms 174ms 467us
# Rows sent 0 78.78k 212 5.66k 1.68k 4.95k 1.71k 652.75
# Rows examine 85 135.34G 2.11G 3.30G 2.88G 3.17G 303.82M 2.87G
# Rows affecte 0 0 0 0 0 0 0 0
# Bytes sent 0 3.22M 10.20k 226.13k 70.14k 201.74k 66.71k 31.59k
# Merge passes 0 0 0 0 0 0 0 0
# Tmp tables 0 0 0 0 0 0 0 0
# Tmp disk tbl 0 0 0 0 0 0 0 0
# Tmp tbl size 0 0 0 0 0 0 0 0
# Query size 0 11.66k 254 254 254 254 0 254
# InnoDB:
# IO r bytes 1 1.11G 0 53.79M 24.20M 51.29M 21.04M 20.30M
# IO r ops 1 142.14k 0 6.72k 3.02k 6.63k 2.67k 2.50k
# IO r wait 0 92s 0 14s 2s 5s 3s 1s
# pages distin 0 325.46k 6.10k 7.30k 6.92k 6.96k 350.84 6.96k
# queue wait 0 0 0 0 0 0 0 0
# rec lock wai 0 0 0 0 0 0 0 0
# Boolean:
# Full scan 100% yes, 0% no
# String:
# Databases lashrenew_... (32/68%), betsy_db (15/31%)
# Hosts 10.37.69.197
# InnoDB trxID CF22C985 (1/2%), CF23455A (1/2%)... 45 more
# Last errno 0
# rate limit query:100
# Users db1_... (32/68%), dba (15/31%)
# Query_time distribution
# 1us
# 10us
# 100us
# 1ms
# 10ms
# 100ms
# 1s
# 10s+ ################################################################
# Tables
# SHOW TABLE STATUS FROM `db1` LIKE 'orders'\G
# SHOW CREATE TABLE `db1`.`orders`\G
# SHOW TABLE STATUS FROM `db1` LIKE 'shipping_tracking_history'\G
# SHOW CREATE TABLE `db1`.`shipping_tracking_history`\G
# EXPLAIN /*!50100 PARTITIONS*/
SELECT tracking_num, carrier, order_id, userID FROM orders o WHERE tracking_num!=""
and NOT EXISTS (SELECT 1 FROM shipping_tracking_history sth WHERE sth.order_id=o.order_id AND sth.is_final=1)
AND o.date_finalized>date_add(curdate(),interval -1 month)\G
常规查询日志
您可以通过传递参数--type genlog来使用pt-query-digest分析常规查询日志。由于常规日志不报告查询时间,因此只显示计数聚合:
shell> sudo pt-query-digest --type genlog /var/lib/mysql/db1.log > general_query_digest
输出将类似于这样:
# 400ms user time, 0 system time, 28.84M rss, 99.35M vsz
# Current date: Sat Nov 18 09:02:08 2017
# Hostname: db1
# Files: /var/lib/mysql/db1.log
# Overall: 511 total, 39 unique, 30.06 QPS, 0x concurrency _______________
# Time range: 2017-11-18 09:01:09 to 09:01:26
# Attribute total min max avg 95% stddev median
# ============ ======= ======= ======= ======= ======= ======= =======
# Exec time 0 0 0 0 0 0 0
# Query size 92.18k 10 3.22k 184.71 363.48 348.86 102.22
查询概要将类似于这样:
# Profile
# Rank Query ID Response time Calls R/Call V/M Item
# ==== ================== ============= ===== ====== ===== ===============
# 1 0x625BF8F82D174492 0.0000 0.0% 130 0.0000 0.00 SELECT facebook_like_details
# 2 0xAA353644DE4C4CB4 0.0000 0.0% 44 0.0000 0.00 ADMIN QUIT
# 3 0x5D51E5F01B88B79E 0.0000 0.0% 44 0.0000 0.00 ADMIN CONNECT
进程列表
您可以使用pt-query-digest从进程列表中读取查询,而不是使用日志文件:
shell> pt-query-digest --processlist h=localhost --iterations 10 --run-time 1m -u <user> -p<pass>
run-time指定每次迭代应运行多长时间。在前面的示例中,该工具将在 10 分钟内每分钟生成一次报告。
二进制日志
要使用pt-query-digest分析二进制日志,您应该使用mysqlbinlog实用程序将其转换为文本格式:
shell> sudo mysqlbinlog /var/lib/mysql/binlog.000639 > binlog.00063
shell> pt-query-digest --type binlog binlog.000639 > binlog_digest
TCP 转储
您可以使用tcpdump命令捕获 TCP 流量,并将其发送到pt-query-digest进行分析:
shell> sudo tcpdump -s 65535 -x -nn -q -tttt -i any -c 1000 port 3306 > mysql.tcp.txt
shell> pt-query-digest --type tcpdump mysql.tcp.txt > tcpdump_digest
pt-query-digest中有很多选项可用,例如过滤特定时间窗口的查询、过滤特定查询和生成报告。有关更多详细信息,请参阅 Percona 文档www.percona.com/doc/percona-toolkit/LATEST/pt-query-digest.html。
另请参阅
请参阅engineering.linkedin.com/blog/2017/09/query-analyzer--a-tool-for-analyzing-mysql-queries-without-overh以了解有关分析所有查询的新方法而无需任何开销的更多信息。
优化数据类型
您应该定义表,使其在磁盘上占用最少的空间,同时容纳所有可能的值。
如果大小较小:
-
写入或从磁盘读取的数据较少,这使得查询更快。
-
在处理查询时,磁盘上的内容会加载到主内存中。因此,较小的表在主内存中占用的空间较小。
-
索引占用的空间较少。
如何做…
-
如果您想存储最大可能值为 500,000 的员工编号,则最佳数据类型是
MEDIUMINT UNSIGNED(占用 3 个字节)。如果您将其存储为占用 4 个字节的INT,则每行都会浪费一个字节。 -
如果您想存储长度可变且最大可能值为 20 的名字,最好将其声明为
varchar(20)。如果您将其存储为char(20),并且只有少数名字长度为 20 个字符,而其余名字长度都不到 10 个字符,那么您就浪费了 10 个字符的空间。 -
在声明
varchar列时,您应该考虑长度。虽然varchar在磁盘上进行了优化,但在加载到内存时,它会占用全部长度。例如,如果您将first_name存储在varchar(255)中,实际长度为 10,在磁盘上占用 10+1(用于存储长度的额外字节);但在内存中,它将占用 255 个字节的全部长度。 -
如果
varchar列的长度超过 255 个字符,则需要 2 个字节来存储长度。 -
如果不存储空值,请将列声明为
NOT NULL。这样可以避免测试每个值是否为空的开销,还可以节省一些存储空间:每列 1 位。 -
如果长度是固定的,请使用
char而不是varchar,因为varchar需要一个或两个字节来存储字符串的长度。 -
如果值是固定的,请使用
ENUM而不是varchar。例如,如果要存储可以是 pending、approved、rejected、deployed、undeployed、failed 或 deleted 的值,可以使用ENUM。它只占用 1 或 2 个字节,而char(10)占用 10 个字节。 -
优先使用整数而不是字符串。
-
尝试利用前缀索引。
-
尝试利用
InnoDB压缩。
参考dev.mysql.com/doc/refman/8.0/en/storage-requirements.html了解每种数据类型的存储要求,以及dev.mysql.com/doc/refman/8.0/en/integer-types.html了解每种整数类型的范围。
如果您想知道优化的数据类型,可以使用PROCEDURE ANALYZE函数。虽然不太准确,但可以对字段有一个大致的了解。不幸的是,它在 MySQL 8 中已被弃用:
mysql> SELECT user_id, first_name FROM user PROCEDURE ANALYSE(1,100)\G
*************************** 1\. row ***************************
Field_name: db1.user.user_id
Min_value: 100000@nat.test123.net
Max_value: test1234@nat.test123.net
Min_length: 22
Max_length: 33
Empties_or_zeros: 0
Nulls: 0
Avg_value_or_avg_length: 25.8003
Std: NULL
Optimal_fieldtype: VARCHAR(33) NOT NULL
*************************** 2\. row ***************************
Field_name: db1.user.first_name
Min_value: *Alan
Max_value: Zuniga 102031
Min_length: 3
Max_length: 33
Empties_or_zeros: 0
Nulls: 0
Avg_value_or_avg_length: 10.1588
Std: NULL
Optimal_fieldtype: VARCHAR(33) NOT NULL
2 rows in set (0.02 sec)
删除重复和冗余索引
您可以在一列上定义多个索引。可能会错误地再次定义相同的索引(相同的列,相同的列顺序或相同的键顺序),这被称为重复索引。如果只有部分索引(最左边的列)是重复的,则称为冗余索引。重复索引没有优势。冗余索引在某些情况下可能有用(在本节末尾的注释中提到了一个用例),但两者都会减慢插入速度。因此,识别并删除它们非常重要。
有三个工具可以帮助找出重复的索引:
-
pt-duplicate-key-checker是 Percona Toolkit 的一部分。安装 Percona Toolkit 在第十章,表维护,安装 Percona Toolkit部分有介绍。 -
mysqlindexcheck是 MySQL 实用程序的一部分。安装 MySQL 实用程序在第一章中有介绍,MySQL 8.0 – 安装和升级。 -
使用
sys模式,这将在下一节中介绍。
考虑以下employees表:
mysql> SHOW CREATE TABLE employees\G
*************************** 1\. row ***************************
Table: employees
Create Table: CREATE TABLE `employees` (
`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`),
KEY `last_name` (`last_name`) /*!80000 INVISIBLE */,
KEY `full_name` (`first_name`,`last_name`),
KEY `full_name_desc` (`first_name` DESC,`last_name`),
KEY `first_name` (`first_name`),
KEY `full_name_1` (`first_name`,`last_name`),
KEY `first_name_emp_no` (`first_name`,`emp_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
索引full_name_1是full_name的重复,因为两个索引都在相同的列上,列的顺序相同,键的顺序也相同(升序或降序)。
索引first_name是冗余索引,因为列first_name已经包含在first_name索引的最左边的后缀中。
索引first_name_emp_no是冗余索引,因为它包含了最右边的后缀中的主键。InnoDB的次要索引已经包含了主键,因此在次要索引中声明主键是多余的。但是,在过滤first_name并按emp_no排序的查询中可能会有用:
SELECT * FROM employees WHERE first_name='Adam' ORDER BY emp_no;
full_name_desc选项不是full_name的重复,因为键的排序不同。
如何做...
让我们详细了解一下删除重复和冗余索引。
pt-duplicate-key-checker
pt-duplicate-key-checker给出了删除重复键的确切ALTER语句:
shell> pt-duplicate-key-checker -u <user> -p<pass>
# A software update is available:
# ########################################################################
# employees.employees
# ########################################################################
# full_name_1 is a duplicate of full_name
# Key definitions:
# KEY `full_name_1` (`first_name`,`last_name`),
# KEY `full_name` (`first_name`,`last_name`),
# Column types:
# `first_name` varchar(14) not null
# `last_name` varchar(16) not null
# To remove this duplicate index, execute:
ALTER TABLE `employees`.`employees` DROP INDEX `full_name_1`;
# first_name is a left-prefix of full_name
# Key definitions:
# KEY `first_name` (`first_name`),
# KEY `full_name` (`first_name`,`last_name`),
# Column types:
# `first_name` varchar(14) not null
# `last_name` varchar(16) not null
# To remove this duplicate index, execute:
ALTER TABLE `employees`.`employees` DROP INDEX `first_name`;
# Key first_name_emp_no ends with a prefix of the clustered index
# Key definitions:
# KEY `first_name_emp_no` (`first_name`,`emp_no`)
# PRIMARY KEY (`emp_no`),
# Column types:
# `first_name` varchar(14) not null
# `emp_no` int(11) not null
# To shorten this duplicate clustered index, execute:
ALTER TABLE `employees`.`employees` DROP INDEX `first_name_emp_no`, ADD INDEX `first_name_emp_no` (`first_name`);
该工具建议您通过从最右边的后缀中删除PRIMARY KEY来缩短重复的聚集索引。请注意,这可能会导致另一个重复的索引。如果您希望忽略重复的聚集索引,可以传递--noclustered选项。
要检查特定数据库的重复索引,可以传递--databases <database name>选项:
shell> pt-duplicate-key-checker -u <user> -p<pass> --database employees
要删除键,甚至可以将pt-duplicate-key-checker的输出导入到mysql中:
shell> pt-duplicate-key-checker -u <user> -p<pass> | mysql -u <user> -p<pass>
mysqlindexcheck
请注意,mysqlindexcheck忽略降序索引。例如,full_name_desc(first_name降序和last_name)被视为full_name(first_name和last_name)的重复索引:
shell> mysqlindexcheck --server=<user>:<pass>@localhost:3306 employees --show-drops
WARNING: Using a password on the command line interface can be insecure.
# Source on localhost: ... connected.
# The following indexes are duplicates or redundant for table employees.employees:
#
CREATE INDEX `full_name_desc` ON `employees`.`employees` (`first_name`, `last_name`) USING BTREE
# may be redundant or duplicate of:
CREATE INDEX `full_name` ON `employees`.`employees` (`first_name`, `last_name`) USING BTREE
#
CREATE INDEX `first_name` ON `employees`.`employees` (`first_name`) USING BTREE
# may be redundant or duplicate of:
CREATE INDEX `full_name` ON `employees`.`employees` (`first_name`, `last_name`) USING BTREE
#
CREATE INDEX `full_name_1` ON `employees`.`employees` (`first_name`, `last_name`) USING BTREE
# may be redundant or duplicate of:
CREATE INDEX `full_name` ON `employees`.`employees` (`first_name`, `last_name`) USING BTREE
#
# DROP statements:
#
ALTER TABLE `employees`.`employees` DROP INDEX `full_name_desc`;
ALTER TABLE `employees`.`employees` DROP INDEX `first_name`;
ALTER TABLE `employees`.`employees` DROP INDEX `full_name_1`;
#
# The following index for table employees.employees contains the clustered index and might be redundant:
#
CREATE INDEX `first_name_emp_no` ON `employees`.`employees` (`first_name`, `emp_no`) USING BTREE
#
# DROP/ADD statement:
#
ALTER TABLE `employees`.`employees` DROP INDEX `first_name_emp_no`, ADD INDEX `first_name_emp_no` (first_name);
#
如前所述,多余的索引在某些情况下可能有用。您必须考虑您的应用程序是否需要这些情况。
创建索引以了解以下示例
mysql> ALTER TABLE employees DROP PRIMARY KEY, ADD PRIMARY KEY(emp_no, hire_date), ADD INDEX `name` (`first_name`,`last_name`);
mysql> ALTER TABLE salaries ADD INDEX from_date(from_date), ADD INDEX from_date_2(from_date,emp_no);
考虑以下employees和salaries表:
mysql> SHOW CREATE TABLE employees\G
*************************** 1\. row ***************************
Table: employees
Create Table: CREATE TABLE `employees` (
`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`,`hire_date`),
KEY `name` (`first_name`,`last_name`)
) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
mysql> SHOW CREATE TABLE salaries\G
*************************** 1\. row ***************************
Create Table: CREATE TABLE `salaries` (
`emp_no` int(11) NOT NULL,
`salary` int(11) NOT NULL,
`from_date` date NOT NULL,
`to_date` date NOT NULL,
PRIMARY KEY (`emp_no`,`from_date`),
KEY `from_date` (`from_date`),
KEY `from_date_2` (`from_date`,`emp_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
似乎from_date是from_date_2的多余索引,但是检查以下查询的解释计划!它使用了两个索引的交集。from_date索引用于过滤,from_date_2用于与employees表进行连接。优化器在每个表中只扫描一行:
mysql> EXPLAIN SELECT e.emp_no, salary FROM salaries s JOIN employees e ON s.emp_no=e.emp_no WHERE from_date='2001-05-23'\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: s
partitions: NULL
type: index_merge
possible_keys: PRIMARY,from_date_2,from_date
key: from_date_2,from_date
key_len: 3,3
ref: NULL
rows: 1
filtered: 100.00
Extra: Using intersect(from_date_2,from_date); Using where
*************************** 2\. row ***************************
id: 1
select_type: SIMPLE
table: e
partitions: NULL
type: ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: employees.s.emp_no
rows: 1
filtered: 100.00
Extra: Using index
2 rows in set, 1 warning (0.00 sec)
现在删除多余的from_date索引并检查解释计划。您可以看到优化器在salaries表中扫描 90 行和employees表中的一行。但是看一下ref列;它显示常量与key列中命名的索引(from_date_2)进行比较,以从表中选择行。您可以通过传递优化器提示或索引提示来测试这种行为,这将在下一节中介绍:
mysql> EXPLAIN SELECT e.emp_no, salary FROM salaries s JOIN employees e ON s.emp_no=e.emp_no WHERE from_date='2001-05-23'\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: s
partitions: NULL
type: ref
possible_keys: PRIMARY,from_date_2
key: from_date_2
key_len: 3
ref: const
rows: 90
filtered: 100.00
Extra: NULL
*************************** 2\. row ***************************
id: 1
select_type: SIMPLE
table: e
partitions: NULL
type: ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: employees.s.emp_no
rows: 1
filtered: 100.00
Extra: Using index
2 rows in set, 1 warning (0.00 sec)
现在您需要确定哪个查询更快:
-
计划 1:使用
intersect(from_date, from_date_2);扫描一行,ref 为空 -
计划 2:使用
from_date_2;扫描 90 行,ref 为常量
您可以使用mysqlslap实用程序来查找(不要直接在生产主机上运行),并确保并发小于max_connections。
计划 1 的基准测试如下:
shell> mysqlslap -u <user> -p<pass> --create-schema='employees' -c 500 -i 100 --query="SELECT e.emp_no, salary FROM salaries s JOIN employees e ON s.emp_no=e.emp_no WHERE from_date='2001-05-23'"
mysqlslap: [Warning] Using a password on the command line interface can be insecure.
Benchmark
Average number of seconds to run all queries: 0.466 seconds
Minimum number of seconds to run all queries: 0.424 seconds
Maximum number of seconds to run all queries: 0.568 seconds
Number of clients running queries: 500
Average number of queries per client: 1
计划 2 的基准测试如下:
shell> mysqlslap -u <user> -p<pass> --create-schema='employees' -c 500 -i 100 --query="SELECT e.emp_no, salary FROM salaries s JOIN employees e ON s.emp_no=e.emp_no WHERE from_date='2001-05-23'"
mysqlslap: [Warning] Using a password on the command line interface can be insecure.
Benchmark
Average number of seconds to run all queries: 0.435 seconds
Minimum number of seconds to run all queries: 0.376 seconds
Maximum number of seconds to run all queries: 0.504 seconds
Number of clients running queries: 500
Average number of queries per client: 1
结果表明,计划 1 和计划 2 的平均查询时间分别为 0.466 秒和 0.435 秒。由于结果非常接近,您可以选择删除多余的索引。使用计划 2。
这只是一个示例,可以让您学习并应用在您的应用程序场景中。
检查索引使用情况
在前面的部分中,您了解了如何删除多余和重复的索引。在设计应用程序时,您可能已经考虑过根据列过滤查询并添加索引。但是随着应用程序的变化,您可能不再需要该索引。在本节中,您将了解如何识别这些未使用的索引。
您可以通过两种方式找到未使用的索引:
-
使用
pt-index-usage(在本节中介绍) -
使用
sys模式(在下一节中介绍)
如何做到...
我们可以使用 Percona Toolkit 中的pt-index-usage工具进行索引分析。它从慢查询日志中获取查询,为每个查询运行解释计划,并识别未使用的索引。如果您有一系列查询,可以将它们保存为慢查询格式并将其传递给该工具。请注意,这只是一个近似值,因为慢查询日志不包括所有查询:
shell> sudo pt-index-usage slow -u <user> -p<password> /var/lib/mysql/db1-slow.log > unused_indexes
控制查询优化器
查询优化器的任务是为执行 SQL 查询找到最佳计划。在连接表时,可能有多个执行查询的计划,特别是要检查的计划数量呈指数增长。在本节中,您将了解如何调整优化器以满足您的需求。
以employees表为例,添加必要的索引;
mysql> CREATE TABLE `employees_index_example` (
`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`),
KEY `last_name` (`last_name`) /*!80000 INVISIBLE */,
KEY `full_name` (`first_name`,`last_name`),
KEY `full_name_desc` (`first_name` DESC,`last_name`),
KEY `first_name` (`first_name`),
KEY `full_name_1` (`first_name`,`last_name`),
KEY `first_name_emp_no` (`first_name`,`emp_no`),
KEY `last_name_2` (`last_name`(10))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Query OK, 0 rows affected, 1 warning (0.08 sec)
mysql> SHOW WARNINGS;
+---------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message |
+---------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
| Warning | 1831 | Duplicate index 'full_name_1' defined on the table 'employees.employees_index_example'. This is deprecated and will be disallowed in a future release. |
+---------+------+--------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
mysql> INSERT INTO employees_index_example SELECT emp_no,birth_date,first_name,last_name,gender,hire_date FROM employees;
mysql> RENAME TABLE employees TO employees_old;
mysql> RENAME TABLE employees_index_example TO employees;
假设您想检查first_name或last_name是否为Adam:
解释计划如下:
mysql> EXPLAIN SELECT emp_no FROM employees WHERE first_name='Adam' OR last_name='Adam'\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: NULL
type: index_merge
possible_keys: full_name,full_name_desc,first_name,full_name_1,first_name_emp_no,last_name_2
key: first_name,last_name_2
key_len: 58,42
ref: NULL
rows: 252
filtered: 100.00
Extra: Using sort_union(first_name,last_name_2); Using where
1 row in set, 1 warning (0.00 sec)
您会注意到优化器有许多选项可用于满足查询。它可以使用possible_keys中列出的任何索引:(full_name,full_name_desc,first_name,full_name_1,first_name_emp_no,last_name_2)。优化器验证所有计划,并确定哪个计划涉及的成本最低。
查询中涉及的一些成本示例包括从磁盘访问数据,从内存访问数据,创建临时表,在内存中对结果进行排序等。MySQL 为每个操作分配一个相对值,并对每个计划的总成本进行求和。它执行涉及最小成本的计划。
如何做...
您可以通过向查询传递提示或调整全局或会话级别的变量来控制优化器。甚至可以调整操作的成本。建议将这些值保留为默认值,除非您知道自己在做什么。
optimizer_search_depth
来自jorgenloland.blogspot.in/2012/04/improvements-for-many-table-joins-in.html的 Jørgen 观点如下:
"MySQL 使用贪婪搜索算法来找到最佳的表连接顺序。当你只连接几个表时,计算所有连接顺序组合的成本然后选择最佳计划是没有问题的。然而,由于可能的组合数为(#tables)!,计算它们的成本很快变得太高:例如,对于五个表,有 120 种组合是可以计算的。对于 10 个表,有 360 万种组合,对于 15 个表,有 1307 亿种组合。因此,MySQL 做出了一个权衡:使用启发式方法只探索有前途的计划。这应该显著减少 MySQL 需要计算的计划数量,但同时也存在风险找不到最佳计划。"
MySQL 文档中说:
"optimizer_search_depth 变量告诉优化器应该查看每个不完整计划的“未来”多远,以评估是否应进一步扩展。较小的 optimizer_search_depth 值可能导致查询编译时间减少数个数量级。例如,如果使用 optimizer_search_depth 接近查询中的表数,查询可能需要几个小时甚至几天才能编译。同时,如果使用 optimizer_search_depth 等于 3 或 4 进行编译,对于相同的查询,优化器可能在不到一分钟内编译。如果您不确定 optimizer_search_depth 的合理值是多少,可以将该变量设置为 0,告诉优化器自动确定值。"
optimizer_search_depth的默认值为62,非常贪婪,但由于启发式方法,MySQL 很快选择了计划。从文档中不清楚为什么默认值设置为62而不是0。
如果您要连接超过七个表,可以将optimizer_search_depth设置为0或传递优化器提示(您将在下一节中了解到)。自动选择最小值(表的数量,七),将搜索深度限制为合理值:
mysql> SHOW VARIABLES LIKE 'optimizer_search_depth';
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| optimizer_search_depth | 62 |
+------------------------+-------+
1 row in set (0.00 sec)
mysql> SET @@SESSION.optimizer_search_depth=0;
Query OK, 0 rows affected (0.00 sec)
如何知道查询在评估计划时花费了多少时间?
如果您要连接 10 个表(通常是 ORM 自动生成的),运行一个解释计划。如果花费更多时间,这意味着查询在评估计划时花费了太多时间。调整optimizer_search_depth的值(可能设置为0),并检查解释计划花费了多少时间。还要注意调整optimizer_search_depth的值时计划的变化。
优化器开关
optimizer_switch系统变量是一组标志。您可以将这些标志中的每一个设置为ON或OFF以启用或禁用相应的优化器行为。您可以在会话级别或全局级别动态设置它。如果您在会话级别调整了优化器开关,则该会话中的所有查询都会受到影响,如果在全局级别,则所有查询都会受到影响。
例如,您已经注意到前面的查询,SELECT emp_no FROM employees WHERE first_name='Adam' OR last_name='Adam',正在使用sort_union(first_name,last_name_2)。如果您认为该优化对该查询不正确,您可以调整optimizer_switch以切换到另一种优化:
mysql> SHOW VARIABLES LIKE 'optimizer_switch'\G
*************************** 1\. row ***************************
Variable_name: optimizer_switch
Value: index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_intersection=on,engine_condition_pushdown=on,index_condition_pushdown=on,mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on,use_index_extensions=on,condition_fanout_filter=on,derived_merge=on
1 row in set (0.00 sec)
最初,index_merge_union是打开的:
mysql> EXPLAIN SELECT emp_no FROM employees WHERE first_name='Adam' OR last_name='Adam'\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: NULL
type: index_merge
possible_keys: full_name,full_name_desc,first_name,full_name_1,first_name_emp_no,last_name_2
key: first_name,last_name_2
key_len: 58,42
ref: NULL
rows: 252
filtered: 100.00
Extra: Using sort_union(first_name,last_name_2); Using where
1 row in set, 1 warning (0.00 sec)
优化器能够使用sort_union:
mysql> SET @@SESSION.optimizer_switch="index_merge_sort_union=off";
Query OK, 0 rows affected (0.00 sec)
您可以在会话级别关闭index_merge_sort_union优化,以便只有该会话中的查询受到影响:
mysql> SHOW VARIABLES LIKE 'optimizer_switch'\G
*************************** 1\. row ***************************
Variable_name: optimizer_switch
Value: index_merge=on,index_merge_union=on,index_merge_sort_union=off,index_merge_intersection=on,engine_condition_pushdown=on,index_condition_pushdown=on,mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on,use_index_extensions=on,condition_fanout_filter=on,derived_merge=on
1 row in set (0.00 sec)
您会注意到在关闭index_merge_sort_union后计划发生变化;它不再使用sort_union优化:
mysql> EXPLAIN SELECT emp_no FROM employees WHERE first_name='Adam' OR last_name='Adam'\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: NULL
type: index
possible_keys: full_name,full_name_desc,first_name,full_name_1,first_name_emp_no,last_name_2
key: full_name
key_len: 124
ref: NULL
rows: 299379
filtered: 19.00
Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)
您还可以进一步发现,在这种情况下,使用sort_union是最佳选择。请参阅dev.mysql.com/doc/refman/8.0/en/switchable-optimizations.html获取有关所有类型优化器开关的更多详细信息。
优化器提示
不要在会话级别调整优化器开关或optimizer_search_depth变量,您可以提示优化器使用或不使用某些优化。优化器提示的范围仅限于给您更好地控制查询的语句,而优化器开关可以在会话或全局级别设置。
再次以前面的查询为例;如果您觉得使用sort_union不是最佳选择,您可以通过在查询本身中传递提示来关闭它:
mysql> EXPLAIN SELECT /*+ NO_INDEX_MERGE(employees first_name,last_name_2) */ * FROM employees WHERE first_name='Adam' OR last_name='Adam'\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: NULL
type: ALL
possible_keys: full_name,full_name_desc,first_name,full_name_1,first_name_emp_no,last_name_2
key: NULL
key_len: NULL
ref: NULL
rows: 299379
filtered: 19.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
请记住,在冗余索引部分,我们删除了冗余索引以找出哪个计划更好。而不是这样做,您可以使用优化器提示来忽略from_date和from_date_2的交集:
mysql> EXPLAIN SELECT /*+ NO_INDEX_MERGE(s from_date,from_date_2) */ e.emp_no, salary FROM salaries s JOIN employees e ON s.emp_no=e.emp_no WHERE from_date='2001-05-23'\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: s
partitions: NULL
type: ref
possible_keys: PRIMARY,from_date,from_date_2
key: from_date
key_len: 3
ref: const
rows: 90
filtered: 100.00
Extra: NULL
*************************** 2\. row ***************************
id: 1
select_type: SIMPLE
table: e
partitions: NULL
type: ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: employees.s.emp_no
rows: 1
filtered: 100.00
Extra: Using index
2 rows in set, 1 warning (0.00 sec)
另一个使用优化器提示的很好的例子是设置JOIN顺序:
mysql> EXPLAIN SELECT e.emp_no, salary FROM salaries s JOIN employees e ON s.emp_no=e.emp_no WHERE (first_name='Adam' OR last_name='Adam') ORDER BY from_date DESC\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: e
partitions: NULL
type: index_merge
possible_keys: PRIMARY,full_name,full_name_desc,first_name,full_name_1,first_name_emp_no,last_name_2
key: first_name,last_name_2
key_len: 58,42
ref: NULL
rows: 252
filtered: 100.00
Extra: Using sort_union(first_name,last_name_2); Using where; Using temporary; Using filesort
*************************** 2\. row ***************************
id: 1
select_type: SIMPLE
table: s
partitions: NULL
type: ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: employees.e.emp_no
rows: 9
filtered: 100.00
Extra: NULL
2 rows in set, 1 warning (0.00 sec)
在前面的查询中,优化器首先考虑employees表,并与salaries表进行连接。您可以通过传递提示/*+ JOIN_ORDER(s,e ) */来更改这一点:
mysql> EXPLAIN SELECT /*+ JOIN_ORDER(s, e) */ e.emp_no, salary FROM salaries s JOIN employees e ON s.emp_no=e.emp_no WHERE (first_name='Adam' OR last_name='Adam') ORDER BY from_date DESC\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: s
partitions: NULL
type: ALL
possible_keys: PRIMARY
key: NULL
key_len: NULL
ref: NULL
rows: 2838426
filtered: 100.00
Extra: Using filesort
*************************** 2\. row ***************************
id: 1
select_type: SIMPLE
table: e
partitions: NULL
type: eq_ref
possible_keys: PRIMARY,full_name,full_name_desc,first_name,full_name_1,first_name_emp_no,last_name_2
key: PRIMARY
key_len: 4
ref: employees.s.emp_no
rows: 1
filtered: 19.00
Extra: Using where
2 rows in set, 1 warning (0.00 sec)
您现在会注意到首先考虑salaries表,这样可以避免创建临时表,但它会对salaries表进行全表扫描。
优化器提示的另一个用例如下:而不是为每个语句或会话设置会话变量,您可以仅为该语句设置它们。假设您使用了一个对查询结果进行排序的ORDER BY子句,但在ORDER BY子句上没有索引。优化器使用sort_buffer_size来加速排序。默认情况下,sort_buffer_size的值为256K。如果sort_buffer_size不足,排序算法必须执行的合并次数会增加。您可以通过会话变量sort_merge_passes来衡量这一点:
mysql> SHOW SESSION status LIKE 'sort_merge_passes';
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Sort_merge_passes | 0 |
+-------------------+-------+
1 row in set (0.00 sec)
mysql> pager grep "rows in set"; SELECT * FROM employees ORDER BY hire_date DESC;nopager;
PAGER set to 'grep "rows in set"'
300025 rows in set (0.45 sec)
PAGER set to stdout
mysql> SHOW SESSION status LIKE 'sort_merge_passes';
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Sort_merge_passes | 8 |
+-------------------+-------+
1 row in set (0.00 sec)
您会注意到 MySQL 没有足够的sort_buffer_size,必须执行八次sort_merge_passes。您可以通过优化器提示将sort_buffer_size设置为诸如16M之类的大值,并检查sort_merge_passes:
mysql> SHOW SESSION status LIKE 'sort_merge_passes';
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Sort_merge_passes | 0 |
+-------------------+-------+
1 row in set (0.00 sec)
mysql> pager grep "rows in set"; SELECT /*+ SET_VAR(sort_buffer_size = 16M) */ * FROM employees ORDER BY hire_date DESC;nopager;
PAGER set to 'grep "rows in set"'
300025 rows in set (0.45 sec)
PAGER set to stdout
mysql> SHOW SESSION status LIKE 'sort_merge_passes';
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Sort_merge_passes | 0 |
+-------------------+-------+
1 row in set (0.00 sec)
当sort_buffer_size设置为16M时,您会注意到sort_merge_passes为0。
强烈建议通过使用索引来优化您的查询,而不是依赖于sort_buffer_size。您可以考虑增加sort_buffer_size的值,以加快无法通过查询优化或改进索引的ORDER BY或GROUP BY操作的速度。
使用SET_VAR,您可以在语句级别设置optimizer_switch:
mysql> EXPLAIN SELECT /*+ SET_VAR(optimizer_switch = 'index_merge_sort_union=off') */ e.emp_no, salary FROM salaries s JOIN employees e ON s.emp_no=e.emp_no WHERE from_date='2001-05-23'\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: e
partitions: NULL
type: index
possible_keys: PRIMARY
key: name
key_len: 124
ref: NULL
rows: 299379
filtered: 100.00
Extra: Using index
*************************** 2\. row ***************************
id: 1
select_type: SIMPLE
table: s
partitions: NULL
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 7
ref: employees.e.emp_no,const
rows: 1
filtered: 100.00
Extra: NULL
2 rows in set, 1 warning (0.00 sec)
您还可以为查询设置最大执行时间,这意味着查询在指定时间后会自动终止使用/*+ MAX_EXECUTION_TIME(毫秒) */:
mysql> SELECT /*+ MAX_EXECUTION_TIME(100) */ * FROM employees ORDER BY hire_date DESC;
ERROR 1028 (HY000): Sort aborted: Query execution was interrupted, maximum statement execution time exceeded
您可以提示优化器做很多其他事情,请参阅dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html获取完整列表和更多示例。
调整优化器成本模型
为了生成执行计划,优化器使用基于查询执行过程中各种操作成本的估算的成本模型。优化器具有一组编译的默认成本常量可供其使用,以便决定执行计划。您可以通过更新或插入mysql.engine_cost表并执行FLUSH OPTIMIZER_COSTS命令来调整它们:
mysql> SELECT * FROM mysql.engine_cost\G
*************************** 1\. row ***************************
engine_name: InnoDB
device_type: 0
cost_name: io_block_read_cost
cost_value: 1
last_update: 2017-11-20 16:24:56
comment: NULL
default_value: 1
*************************** 2\. row ***************************
engine_name: InnoDB
device_type: 0
cost_name: memory_block_read_cost
cost_value: 0.25
last_update: 2017-11-19 13:58:32
comment: NULL
default_value: 0.25
2 rows in set (0.00 sec)
假设您有一个超快的磁盘;您可以减少io_block_read_cost的cost_value:
mysql> UPDATE mysql.engine_cost SET cost_value=0.5 WHERE cost_name='io_block_read_cost';
Query OK, 1 row affected (0.08 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> FLUSH OPTIMIZER_COSTS;
Query OK, 0 rows affected (0.01 sec)
mysql> SELECT * FROM mysql.engine_cost\G
*************************** 1\. row ***************************
engine_name: InnoDB
device_type: 0
cost_name: io_block_read_cost
cost_value: 0.5
last_update: 2017-11-20 17:02:43
comment: NULL
default_value: 1
*************************** 2\. row ***************************
engine_name: InnoDB
device_type: 0
cost_name: memory_block_read_cost
cost_value: 0.25
last_update: 2017-11-19 13:58:32
comment: NULL
default_value: 0.25
2 rows in set (0.00 sec)
要了解更多有关优化器成本模型的信息,请参阅dev.mysql.com/doc/refman/8.0/en/cost-model.html。
使用索引提示
使用索引提示,您可以提示优化器使用或忽略索引。这与优化器提示不同。在优化器提示中,您提示优化器使用或忽略某些优化方法。索引和优化器提示可以分别或一起使用以实现所需的计划。索引提示是在表名后指定的。
当您执行涉及多个表连接的复杂查询时,如果优化器在评估计划时花费太多时间,您可以确定最佳计划并给出查询的提示。但请确保您建议的计划是最佳的,并且在所有情况下都应该有效。
如何做...
以评估冗余索引的使用为例,使用相同的查询;它使用intersect(from_date,from_date_2)。通过传递优化器提示(/*+ NO_INDEX_MERGE(s from_date,from_date_2) */),您避免了使用 intersect。您可以通过提示优化器忽略from_date_2索引来实现相同的行为:
mysql> EXPLAIN SELECT e.emp_no, salary FROM salaries s IGNORE INDEX(from_date_2) JOIN employees e ON s.emp_no=e.emp_no WHERE from_date='2001-05-23'\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: s
partitions: NULL
type: ref
possible_keys: PRIMARY,from_date
key: from_date
key_len: 3
ref: const
rows: 90
filtered: 100.00
Extra: NULL
*************************** 2\. row ***************************
id: 1
select_type: SIMPLE
table: e
partitions: NULL
type: ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: employees.s.emp_no
rows: 1
filtered: 100.00
Extra: Using index
2 rows in set, 1 warning (0.00 sec)
另一个用例是提示优化器并节省评估多个计划的成本。考虑以下employees表和查询(与控制查询优化器部分开头讨论的相同):
mysql> SHOW CREATE TABLE employees\G
*************************** 1\. row ***************************
Table: employees
Create Table: CREATE TABLE `employees` (
`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`),
KEY `last_name` (`last_name`) /*!80000 INVISIBLE */,
KEY `full_name` (`first_name`,`last_name`),
KEY `full_name_desc` (`first_name` DESC,`last_name`),
KEY `first_name` (`first_name`),
KEY `full_name_1` (`first_name`,`last_name`),
KEY `first_name_emp_no` (`first_name`,`emp_no`),
KEY `last_name_2` (`last_name`(10))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT emp_no FROM employees WHERE first_name='Adam' OR last_name='Adam'\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: NULL
type: index_merge
possible_keys: full_name,full_name_desc,first_name,full_name_1,first_name_emp_no,last_name_2
key: first_name,last_name_2
key_len: 58,42
ref: NULL
rows: 252
filtered: 100.00
Extra: Using sort_union(first_name,last_name_2); Using where
1 row in set, 1 warning (0.00 sec)
您可以看到优化器必须评估full_name、full_name_desc、first_name、full_name_1、first_name_emp_no、last_name_2索引以得出最佳计划。您可以通过传递USE INDEX(first_name,last_name_2)来提示优化器,这将消除对其他索引的扫描:
mysql> EXPLAIN SELECT emp_no FROM employees USE INDEX(first_name,last_name_2) WHERE first_name='Adam' OR last_name='Adam'\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: NULL
type: index_merge
possible_keys: first_name,last_name_2
key: first_name,last_name_2
key_len: 58,42
ref: NULL
rows: 252
filtered: 100.00
Extra: Using sort_union(first_name,last_name_2); Using where
1 row in set, 1 warning (0.00 sec)
由于这是一个简单的查询,表非常小,性能提升可以忽略不计。当查询复杂且每小时执行数百万次时,性能提升可能会显著。
使用生成的列进行 JSON 索引
JSON 列不能直接建立索引。因此,如果您想在 JSON 列上使用索引,可以使用虚拟列和虚拟列上的创建索引来提取信息。
如何做...
- 考虑您在第三章中创建的
emp_details表,使用 MySQL(高级),使用 JSON部分:
mysql> SHOW CREATE TABLE emp_details\G
*************************** 1\. row ***************************
Table: emp_details
Create Table: CREATE TABLE `emp_details` (
`emp_no` int(11) NOT NULL,
`details` json DEFAULT NULL,
PRIMARY KEY (`emp_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)
- 插入一些虚拟记录:
mysql> INSERT IGNORE INTO emp_details(emp_no, details) VALUES
('1', '{ "location": "IN", "phone": "+11800000000", "email": "abc@example.com", "address": { "line1": "abc", "line2": "xyz street", "city": "Bangalore", "pin": "560103"}}'),
('2', '{ "location": "IN", "phone": "+11800000000", "email": "def@example.com", "address": { "line1": "abc", "line2": "xyz street", "city": "Delhi", "pin": "560103"}}'),
('3', '{ "location": "IN", "phone": "+11800000000", "email": "ghi@example.com", "address": { "line1": "abc", "line2": "xyz street", "city": "Mumbai", "pin": "560103"}}'),
('4', '{ "location": "IN", "phone": "+11800000000", "email": "jkl@example.com", "address": { "line1": "abc", "line2": "xyz street", "city": "Delhi", "pin": "560103"}}'),
('5', '{ "location": "US", "phone": "+11800000000", "email": "mno@example.com", "address": { "line1": "abc", "line2": "xyz street", "city": "Sunnyvale", "pin": "560103"}}');
Query OK, 5 rows affected (0.00 sec)
Records: 5 Duplicates: 0 Warnings: 0
- 假设您想检索城市为
Bangalore的emp_no:
mysql> EXPLAIN SELECT emp_no FROM emp_details WHERE details->>'$.address.city'="Bangalore"\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: emp_details
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 5
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
您会注意到查询无法使用索引并扫描所有行。
- 您可以将城市作为虚拟列检索并在其上添加索引:
mysql> ALTER TABLE emp_details ADD COLUMN city varchar(20) AS (details->>'$.address.city'), ADD INDEX (city);
Query OK, 0 rows affected (0.22 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> SHOW CREATE TABLE emp_details\G
*************************** 1\. row ***************************
Table: emp_details
Create Table: CREATE TABLE `emp_details` (
`emp_no` int(11) NOT NULL,
`details` json DEFAULT NULL,
`city` varchar(20) GENERATED ALWAYS AS (json_unquote(json_extract(`details`,_utf8'$.address.city'))) VIRTUAL,
PRIMARY KEY (`emp_no`),
KEY `city` (`city`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.01 sec)
- 如果您现在检查解释计划,您会注意到查询能够使用
city上的索引并且只扫描一行:
mysql> EXPLAIN SELECT emp_no FROM emp_details WHERE details->>'$.address.city'="Bangalore"\G
*************************** 1\. row ***************************
id: 1
select_type: SIMPLE
table: emp_details
partitions: NULL
type: ref
possible_keys: city
key: city
key_len: 83
ref: const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
要了解有关生成列上的辅助索引的更多信息,请参阅dev.mysql.com/doc/refman/8.0/en/create-table-secondary-indexes.html。
使用资源组
您可以限制查询仅使用一定数量的系统资源,使用资源组。目前,只有 CPU 时间是可管理的资源,由虚拟 CPU(VCPU)表示,其中包括 CPU 核心、超线程、硬件线程等。您可以创建一个资源组并将 VCPUs 分配给它。除了 CPU 外,资源组的属性是线程优先级。
您可以为线程分配资源组,在会话级别设置默认资源组,或将资源组作为优化器提示传递。例如,您想要以最低优先级运行一些查询(比如报告查询);您可以将它们分配给具有最少资源的资源组。
如何做...
- 将
CAP_SYS_NICE功能设置为mysqld:
shell> ps aux | grep mysqld | grep -v grep
mysql 5238 0.0 28.1 1253368 488472 ? Sl Nov19 4:04 /usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid
shell> sudo setcap cap_sys_nice+ep /usr/sbin/mysqld
shell> getcap /usr/sbin/mysqld
/usr/sbin/mysqld = cap_sys_nice+ep
- 使用
CREATE RESOURCE GROUP语句创建资源组。您必须提到资源组名称、VCPUS 数量、线程优先级和类型,可以是USER或SYSTEM。如果不指定 VCPUs,将使用所有 CPU:
mysql> CREATE RESOURCE GROUP report_group
TYPE = USER
VCPU = 2-3
THREAD_PRIORITY = 15
ENABLE;
# You should have at least 4 CPUs for the above resource group to create. If you have less CPUs, you can use VCPU = 0-1 for testing the example.
VCPU 表示 CPU 编号为 0-5,包括 CPU 0、1、2、3、4 和 5;0-3、8-9 和 11 包括 CPU 0、1、2、3、8、9 和 11。
THREAD_PRIORITY类似于 CPU 的优先级;系统资源组的范围为-20 到 0,用户组的范围为 0 到 19。-20 是最高优先级,19 是最低优先级。
您还可以启用或禁用资源组。默认情况下,在创建时启用资源组。禁用的组不能分配线程。
- 创建后,您可以验证已创建的资源组:
mysql> SELECT * FROM INFORMATION_SCHEMA.RESOURCE_GROUPS\G
*************************** 1\. row ***************************
RESOURCE_GROUP_NAME: USR_default
RESOURCE_GROUP_TYPE: USER
RESOURCE_GROUP_ENABLED: 1
VCPU_IDS: 0-0
THREAD_PRIORITY: 0
*************************** 2\. row ***************************
RESOURCE_GROUP_NAME: SYS_default
RESOURCE_GROUP_TYPE: SYSTEM
RESOURCE_GROUP_ENABLED: 1
VCPU_IDS: 0-0
THREAD_PRIORITY: 0
*************************** 3\. row ***************************
RESOURCE_GROUP_NAME: report_group
RESOURCE_GROUP_TYPE: USER
RESOURCE_GROUP_ENABLED: 1
VCPU_IDS: 2-3
THREAD_PRIORITY: 15
USR_default和SYS_default是默认资源组,不能被删除或修改。
- 为线程分配一个组:
mysql> SET RESOURCE GROUP report_group FOR <thread_id>;
- 设置会话资源组;该会话中的所有查询将在
report_group下执行:
mysql> SET RESOURCE GROUP report_group;
- 使用
RESOURCE_GROUP优化器提示使用report_group执行单个语句:
mysql> SELECT /*+ RESOURCE_GROUP(report_group) */ * FROM employees;
更改和删除资源组
您可以动态调整资源组的 CPU 数量或thread_priority。如果系统负载过重,可以降低线程优先级:
mysql> ALTER RESOURCE GROUP report_group VCPU = 3 THREAD_PRIORITY = 19;
Query OK, 0 rows affected (0.12 sec)
类似地,当系统负载较轻时,您可以增加优先级:
mysql> ALTER RESOURCE GROUP report_group VCPU = 0-12 THREAD_PRIORITY = 0;
Query OK, 0 rows affected (0.12 sec)
您可以禁用资源组:
mysql> ALTER RESOURCE GROUP report_group DISABLE FORCE;
Query OK, 0 rows affected (0.00 sec)
您还可以使用DROP RESOURCE GROUP语句删除资源组:
mysql> DROP RESOURCE GROUP report_group FORCE;
如果给定FORCE,则正在运行的线程将移动到默认资源组(系统线程移动到SYS_default,用户线程移动到USR_default)。
如果未给出FORCE,则组中的现有线程将继续运行直到终止,但新线程不能分配给该组。
资源组仅限于本地服务器,并且与资源组相关的语句都不会被复制。要了解有关资源组的更多信息,请参阅dev.mysql.com/doc/refman/8.0/en/resource-groups.html。
使用 performance_schema
您可以使用performance_schema在运行时检查服务器的内部执行。这不应与信息模式混淆,信息模式用于检查元数据。
performance_schema中有许多事件消费者,会影响服务器的时间,例如函数调用、等待操作系统、SQL 语句执行阶段(如解析或排序)、单个语句或一组语句。所有收集的信息都存储在performance_schema中,不会被复制。
performance_schema默认启用;如果要禁用它,可以在my.cnf文件中设置performance_schema=OFF。默认情况下,并非所有消费者和仪器都启用;您可以通过更新performance_schema.setup_instruments和performance_schema.setup_consumers表来关闭/打开它们。
如何做...
我们将看看如何使用performance_schema。
启用/禁用 performance_schema
要禁用它,请将performance_schema设置为0:
shell> sudo vi /etc/my.cnf
[mysqld]
performance_schema = 0
启用/禁用消费者和仪器
您可以在setup_consumers表中看到可用的消费者列表,如下所示:
mysql> SELECT * FROM performance_schema.setup_consumers;
+----------------------------------+---------+
| NAME | ENABLED |
+----------------------------------+---------+
| events_stages_current | NO |
| events_stages_history | NO |
| events_stages_history_long | NO |
| events_statements_current | YES |
| events_statements_history | YES |
| events_statements_history_long | NO |
| events_transactions_current | YES |
| events_transactions_history | YES |
| events_transactions_history_long | NO |
| events_waits_current | NO |
| events_waits_history | NO |
| events_waits_history_long | NO |
| global_instrumentation | YES |
| thread_instrumentation | YES |
| statements_digest | YES |
+----------------------------------+---------+
15 rows in set (0.00 sec)
假设您想要启用events_waits_current:
mysql> UPDATE performance_schema.setup_consumers SET ENABLED='YES' WHERE NAME='events_waits_current';
类似地,您可以从setup_instruments表中禁用或启用仪器。有大约 1182 个仪器(取决于版本):
mysql> SELECT NAME, ENABLED, TIMED FROM setup_instruments LIMIT 10;
+---------------------------------------------------------+---------+-------+
| NAME | ENABLED | TIMED |
+---------------------------------------------------------+---------+-------+
| wait/synch/mutex/pfs/LOCK_pfs_share_list | NO | NO |
| wait/synch/mutex/sql/TC_LOG_MMAP::LOCK_tc | NO | NO |
| wait/synch/mutex/sql/MYSQL_BIN_LOG::LOCK_commit | NO | NO |
| wait/synch/mutex/sql/MYSQL_BIN_LOG::LOCK_commit_queue | NO | NO |
| wait/synch/mutex/sql/MYSQL_BIN_LOG::LOCK_done | NO | NO |
| wait/synch/mutex/sql/MYSQL_BIN_LOG::LOCK_flush_queue | NO | NO |
| wait/synch/mutex/sql/MYSQL_BIN_LOG::LOCK_index | NO | NO |
| wait/synch/mutex/sql/MYSQL_BIN_LOG::LOCK_log | NO | NO |
| wait/synch/mutex/sql/MYSQL_BIN_LOG::LOCK_binlog_end_pos | NO | NO |
| wait/synch/mutex/sql/MYSQL_BIN_LOG::LOCK_sync | NO | NO |
+---------------------------------------------------------+---------+-------+
10 rows in set (0.00 sec)
performance_schema 表
performance_schema中有五种主要类型的表。它们是当前事件表、事件历史表、事件摘要表、对象实例表和设置(配置)表:
mysql> SHOW TABLES LIKE '%current%';
+------------------------------------------+
| Tables_in_performance_schema (%current%) |
+------------------------------------------+
| events_stages_current |
| events_statements_current |
| events_transactions_current |
| events_waits_current |
+------------------------------------------+
4 rows in set (0.00 sec)
mysql> SHOW TABLES LIKE '%history%';
+------------------------------------------+
| Tables_in_performance_schema (%history%) |
+------------------------------------------+
| events_stages_history |
| events_stages_history_long |
| events_statements_history |
| events_statements_history_long |
| events_transactions_history |
| events_transactions_history_long |
| events_waits_history |
| events_waits_history_long |
+------------------------------------------+
8 rows in set (0.00 sec)
mysql> SHOW TABLES LIKE '%summary%';
+------------------------------------------------------+
| Tables_in_performance_schema (%summary%) |
+------------------------------------------------------+
| events_errors_summary_by_account_by_error |
| events_errors_summary_by_host_by_error |
~
~
| table_io_waits_summary_by_table |
| table_lock_waits_summary_by_table |
+------------------------------------------------------+
41 rows in set (0.00 sec)
mysql> SHOW TABLES LIKE '%setup%';
+----------------------------------------+
| Tables_in_performance_schema (%setup%) |
+----------------------------------------+
| setup_actors |
| setup_consumers |
| setup_instruments |
| setup_objects |
| setup_threads |
| setup_timers |
+----------------------------------------+
6 rows in set (0.00 sec)
假设您想要找出哪个文件被访问最多:
mysql> SELECT EVENT_NAME, COUNT_STAR from file_summary_by_event_name ORDER BY count_star DESC LIMIT 10;
+-------------------------------------------------+------------+
| EVENT_NAME | COUNT_STAR |
+-------------------------------------------------+------------+
| wait/io/file/innodb/innodb_data_file | 35014 |
| wait/io/file/sql/io_cache | 13454 |
| wait/io/file/sql/binlog | 8785 |
| wait/io/file/innodb/innodb_log_file | 2070 |
| wait/io/file/sql/query_log | 1257 |
| wait/io/file/innodb/innodb_temp_file | 96 |
| wait/io/file/innodb/innodb_tablespace_open_file | 88 |
| wait/io/file/sql/casetest | 15 |
| wait/io/file/sql/binlog_index | 14 |
| wait/io/file/mysys/cnf | 5 |
+-------------------------------------------------+------------+
10 rows in set (0.00 sec)
或者您想要找出哪个文件在写入时花费了最多的时间:
mysql> SELECT EVENT_NAME, SUM_TIMER_WRITE FROM file_summary_by_event_name ORDER BY SUM_TIMER_WRITE DESC LIMIT 10;
+-------------------------------------------------+-----------------+
| EVENT_NAME | SUM_TIMER_WRITE |
+-------------------------------------------------+-----------------+
| wait/io/file/innodb/innodb_data_file | 410909759715 |
| wait/io/file/innodb/innodb_log_file | 366157166830 |
| wait/io/file/sql/io_cache | 341899621700 |
| wait/io/file/sql/query_log | 203975010330 |
| wait/io/file/sql/binlog | 85261691515 |
| wait/io/file/innodb/innodb_temp_file | 25291378385 |
| wait/io/file/innodb/innodb_tablespace_open_file | 674778195 |
| wait/io/file/sql/SDI | 18981690 |
| wait/io/file/sql/pid | 10233405 |
| wait/io/file/archive/FRM | 0 |
+-------------------------------------------------+-----------------+
您可以使用events_statements_summary_by_digest表来获取查询报告,就像您为pt-query-digest所做的那样。花费时间最多的顶级查询:
mysql> SELECT SCHEMA_NAME, digest, digest_text, round(sum_timer_wait/ 1000000000000, 6) as avg_time, count_star FROM performance_schema.events_statements_summary_by_digest ORDER BY sum_timer_wait DESC LIMIT 1\G
*************************** 1\. row ***************************
SCHEMA_NAME: NULL
digest: 719f469393f90c27d84681a1d0ab3c19
digest_text: SELECT `sleep` (?)
avg_time: 60.000442
count_star: 1
1 row in set (0.00 sec)
按执行次数最多的顶级查询:
mysql> SELECT SCHEMA_NAME, digest, digest_text, round(sum_timer_wait/ 1000000000000, 6) as avg_time, count_star FROM performance_schema.events_statements_summary_by_digest ORDER BY count_star DESC LIMIT 1\G
*************************** 1\. row ***************************
SCHEMA_NAME: employees
digest: f5296ec6642c0fb977b448b350a2ba9b
digest_text: INSERT INTO `salaries` VALUES (...) /* , ... */
avg_time: 32.736742
count_star: 114
1 row in set (0.01 sec)
假设您想要查找特定查询的统计信息;而不是依赖于mysqlslap基准测试,您可以使用performance_schema来检查所有统计信息:
mysql> SELECT * FROM events_statements_summary_by_digest WHERE DIGEST_TEXT LIKE '%SELECT%employee%ORDER%' LIMIT 1\G
*************************** 1\. row ***************************
SCHEMA_NAME: employees
DIGEST: d3b56f71f362f1bf6b067bfa358c04ab
DIGEST_TEXT: EXPLAIN SELECT /*+ SET_VAR ( `sort_buffer_size` = ? ) */ `e` . `emp_no` , `salary` FROM `salaries` `s` JOIN `employees` `e` ON `s` . `emp_no` = `e` . `emp_no` WHERE ( `first_name` = ? OR `last_name` = ? ) ORDER BY `from_date` DESC
COUNT_STAR: 1
SUM_TIMER_WAIT: 643710000
MIN_TIMER_WAIT: 643710000
AVG_TIMER_WAIT: 643710000
MAX_TIMER_WAIT: 643710000
SUM_LOCK_TIME: 288000000
SUM_ERRORS: 0
SUM_WARNINGS: 1
SUM_ROWS_AFFECTED: 0
SUM_ROWS_SENT: 2
SUM_ROWS_EXAMINED: 0
SUM_CREATED_TMP_DISK_TABLES: 0
SUM_CREATED_TMP_TABLES: 0
SUM_SELECT_FULL_JOIN: 0
~
FIRST_SEEN: 2017-11-23 08:40:28.565406
LAST_SEEN: 2017-11-23 08:40:28.565406
QUANTILE_95: 301995172
QUANTILE_99: 301995172
QUANTILE_999: 301995172
QUERY_SAMPLE_TEXT: EXPLAIN SELECT /*+ SET_VAR(sort_buffer_size = 16M) */ e.emp_no, salary FROM salaries s JOIN employees e ON s.emp_no=e.emp_no WHERE (first_name='Adam' OR last_name='Adam') ORDER BY from_date DESC
QUERY_SAMPLE_SEEN: 2017-11-23 08:40:28.565406
QUERY_SAMPLE_TIMER_WAIT: 643710000
使用 sys 模式
sys模式帮助您以更简单和更易理解的形式解释从performance_schema收集的数据。performance_schema应该启用sys模式才能工作。要充分利用sys模式,您需要在performance_schema上启用所有的消费者和计时器,但这会影响服务器的性能。因此,只为您寻找的那些启用消费者。
带有x$前缀的视图以皮秒显示数据,其他表是人类可读的,这些数据被其他工具用于进一步处理。
如何做...
从sys模式启用一个工具:
mysql> CALL sys.ps_setup_enable_instrument('statement');
+------------------------+
| summary |
+------------------------+
| Enabled 22 instruments |
+------------------------+
1 row in set (0.08 sec)
Query OK, 0 rows affected (0.08 sec)
如果要重置为默认值,请执行以下操作:
mysql> CALL sys.ps_setup_reset_to_default(TRUE)\G
*************************** 1\. row ***************************
status: Resetting: setup_actors
DELETE FROM performance_schema.setup_actors WHERE NOT (HOST = '%' AND USER = '%' AND `ROLE` = '%')
1 row in set (0.01 sec)
~
*************************** 1\. row ***************************
status: Resetting: threads
UPDATE performance_schema.threads SET INSTRUMENTED = 'YES'
1 row in set (0.03 sec)
Query OK, 0 rows affected (0.03 sec)
sys模式中有许多表;本节显示了一些最常用的表。
每个主机的类型声明(INSERT 和 SELECT)
mysql> SELECT statement, total, total_latency, rows_sent, rows_examined, rows_affected, full_scans FROM sys.host_summary_by_statement_type WHERE host='localhost' ORDER BY total DESC LIMIT 5;
+------------+--------+---------------+-----------+---------------+---------------+------------+
| statement | total | total_latency | rows_sent | rows_examined | rows_affected | full_scans |
+------------+--------+---------------+-----------+---------------+---------------+------------+
| select | 208526 | 1.14 d | 27484761 | 799220003 | 0 | 9265 |
| Quit | 199551 | 4.76 s | 0 | 0 | 0 | 0 |
| insert | 9848 | 12.75 m | 0 | 0 | 5075058 | 0 |
| Ping | 4674 | 278.76 ms | 0 | 0 | 0 | 0 |
| set_option | 2552 | 634.76 ms | 0 | 0 | 0 | 0 |
+------------+--------+---------------+-----------+---------------+---------------+------------+
6 rows in set (0.00 sec)
每个用户的类型声明
mysql> SELECT statement, total, total_latency, rows_sent, rows_examined, rows_affected, full_scans FROM sys.user_summary_by_statement_type ORDER BY total DESC LIMIT 5;
+------------+--------+---------------+-----------+---------------+---------------+------------+
| statement | total | total_latency | rows_sent | rows_examined | rows_affected | full_scans |
+------------+--------+---------------+-----------+---------------+---------------+------------+
| select | 208535 | 1.14 d | 27485256 | 799246972 | 0 | 9273 |
| Quit | 199551 | 4.76 s | 0 | 0 | 0 | 0 |
| insert | 9848 | 12.75 m | 0 | 0 | 5075058 | 0 |
| Ping | 4674 | 278.76 ms | 0 | 0 | 0 | 0 |
| set_option | 2552 | 634.76 ms | 0 | 0 | 0 | 0 |
+------------+--------+---------------+-----------+---------------+---------------+------------+
5 rows in set (0.01 sec)
冗余索引
mysql> SELECT * FROM sys.schema_redundant_indexes WHERE table_name='employees'\G
*************************** 1\. row ***************************
table_schema: employees
table_name: employees
redundant_index_name: first_name
redundant_index_columns: first_name
redundant_index_non_unique: 1
dominant_index_name: first_name_emp_no
dominant_index_columns: first_name,emp_no
dominant_index_non_unique: 1
subpart_exists: 0
sql_drop_index: ALTER TABLE `employees`.`employees` DROP INDEX `first_name`
~
*************************** 8\. row ***************************
table_schema: employees
table_name: employees
redundant_index_name: last_name_2
redundant_index_columns: last_name
redundant_index_non_unique: 1
dominant_index_name: last_name
dominant_index_columns: last_name
dominant_index_non_unique: 1
subpart_exists: 1
sql_drop_index: ALTER TABLE `employees`.`employees` DROP INDEX `last_name_2`
8 rows in set (0.00 sec)
未使用的索引
mysql> SELECT * FROM sys.schema_unused_indexes WHERE object_schema='employees';
+---------------+----------------+-------------------+
| object_schema | object_name | index_name |
+---------------+----------------+-------------------+
| employees | departments | dept_name |
| employees | dept_emp | dept_no |
| employees | dept_manager | dept_no |
| employees | employees | name |
| employees | employees1 | last_name |
| employees | employees1 | full_name |
| employees | employees1 | full_name_desc |
| employees | employees1 | first_name |
| employees | employees1 | full_name_1 |
| employees | employees1 | first_name_emp_no |
| employees | employees1 | last_name_2 |
| employees | employees_mgr | manager_id |
| employees | employees_test | name |
| employees | emp_details | city |
+---------------+----------------+-------------------+
14 rows in set (0.00 sec)
每个主机执行的语句
mysql> SELECT * FROM sys.host_summary ORDER BY statements DESC LIMIT 1\G
*************************** 1\. row ***************************
host: localhost
statements: 431214
statement_latency: 1.15 d
statement_avg_latency: 231.14 ms
table_scans: 9424
file_ios: 671972
file_io_latency: 4.13 m
current_connections: 3
total_connections: 200193
unique_users: 1
current_memory: 0 bytes
total_memory_allocated: 0 bytes
1 row in set (0.02 sec)
表统计
mysql> SELECT * FROM sys.schema_table_statistics LIMIT 1\G
*************************** 1\. row ***************************
table_schema: employees
table_name: employees
total_latency: 14.03 h
rows_fetched: 731760045
fetch_latency: 14.03 h
rows_inserted: 300025
insert_latency: 2.81 s
rows_updated: 0
update_latency: 0 ps
rows_deleted: 0
delete_latency: 0 ps
io_read_requests: NULL
io_read: NULL
io_read_latency: NULL
io_write_requests: NULL
io_write: NULL
io_write_latency: NULL
io_misc_requests: NULL
io_misc_latency: NULL
1 row in set (0.01 sec)
带有缓冲区的表统计
mysql> SELECT * FROM sys.schema_table_statistics_with_buffer LIMIT 1\G
*************************** 1\. row ***************************
table_schema: employees
table_name: employees
rows_fetched: 731760045
fetch_latency: 14.03 h
rows_inserted: 300025
insert_latency: 2.81 s
rows_updated: 0
update_latency: 0 ps
rows_deleted: 0
delete_latency: 0 ps
~
innodb_buffer_allocated: 6.80 MiB
innodb_buffer_data: 6.23 MiB
innodb_buffer_free: 582.77 KiB
innodb_buffer_pages: 435
innodb_buffer_pages_hashed: 0
innodb_buffer_pages_old: 435
innodb_buffer_rows_cached: 147734
1 row in set (0.13 sec)
语句分析
此输出类似于performance_schema.events_statements_summary_by_digest和pt-query-digest的输出。
按执行次数最多的查询如下:
mysql> SELECT * FROM sys.statement_analysis ORDER BY exec_count DESC LIMIT 1\G
*************************** 1\. row ***************************
query: SELECT `e` . `emp_no` , `salar ... emp_no` WHERE `from_date` = ?
db: employees
full_scan:
exec_count: 159997
err_count: 0
warn_count: 0
total_latency: 1.98 h
max_latency: 661.58 ms
avg_latency: 44.54 ms
lock_latency: 1.28 m
rows_sent: 14400270
rows_sent_avg: 90
rows_examined: 28800540
rows_examined_avg: 180
rows_affected: 0
rows_affected_avg: 0
tmp_tables: 0
tmp_disk_tables: 0
rows_sorted: 0
sort_merge_passes: 0
digest: 94c925c0f00e06566d0447822066b1fe
first_seen: 2017-11-23 05:39:09
last_seen: 2017-11-23 05:45:45
1 row in set (0.01 sec)
消耗最大tmp_disk_tables的语句:
mysql> SELECT * FROM sys.statement_analysis ORDER BY tmp_disk_tables DESC LIMIT 1\G
*************************** 1\. row ***************************
query: SELECT `cat` . `name` AS `TABL ... SE `col` . `type` WHEN ? THEN
db: employees
full_scan:
exec_count: 195
err_count: 0
warn_count: 0
total_latency: 249.55 ms
max_latency: 2.84 ms
avg_latency: 1.28 ms
lock_latency: 97.95 ms
rows_sent: 732
rows_sent_avg: 4
rows_examined: 4245
rows_examined_avg: 22
rows_affected: 0
rows_affected_avg: 0
tmp_tables: 195
tmp_disk_tables: 195
rows_sorted: 732
sort_merge_passes: 0
digest: 8e8c46a210908a2efc2f1e96dd998130
first_seen: 2017-11-19 05:27:24
last_seen: 2017-11-20 17:24:34
1 row in set (0.01 sec)
要了解更多关于sys模式对象的信息,请参阅dev.mysql.com/doc/refman/8.0/en/sys-schema-object-index.html。
第十四章:安全
在本章中,我们将介绍以下配方:
-
安装安全
-
限制网络和用户
-
使用 mysql_config_editor 进行无密码身份验证
-
重置 root 密码
-
使用 X509 建立加密连接
-
设置 SSL 复制
介绍
本章涵盖了 MySQL 的安全方面,包括限制网络、强密码、使用 SSL、数据库内的访问控制、安装安全和安全插件。
安装安全
安装完成后,建议您使用mysql_secure_installation实用程序保护您的安装。
如何做...
shell> mysql_secure_installation
Securing the MySQL server deployment.
Enter password for user root:
The 'validate_password' plugin is installed on the server.
The subsequent steps will run with the existing configuration
of the plugin.
Using existing password for root.
Estimated strength of the password: 100
Change the password for root ? ((Press y|Y for Yes, any other key for No) :
... skipping.
By default, a MySQL installation has an anonymous user,
allowing anyone to log into MySQL without having to have
a user account created for them. This is intended only for
testing, and to make the installation go a bit smoother.
You should remove them before moving into a production
environment.
Remove anonymous users? (Press y|Y for Yes, any other key for No) : y
Success.
Normally, root should only be allowed to connect from
'localhost'. This ensures that someone cannot guess at
the root password from the network.
Disallow root login remotely? (Press y|Y for Yes, any other key for No) : y
Success.
By default, MySQL comes with a database named 'test' that
anyone can access. This is also intended only for testing,
and should be removed before moving into a production
environment.
Remove test database and access to it? (Press y|Y for Yes, any other key for No) : y
- Dropping test database...
Success.
- Removing privileges on test database...
Success.
Reloading the privilege tables will ensure that all changes
made so far will take effect immediately.
Reload privilege tables now? (Press y|Y for Yes, any other key for No) : y
Success.
All done!
默认情况下,mysqld进程在mysql用户下运行。您还可以通过更改mysqld使用的所有目录的所有权(例如datadir,如果有的话,binlog目录,其他磁盘上的表空间等)并在my.cnf中添加user=<user>来以另一个用户身份运行mysqld。有关更改 MySQL 用户的更多信息,请参阅dev.mysql.com/doc/refman/8.0/en/changing-mysql-user.html。
强烈建议不要将mysqld作为 Unix 根用户运行。一个原因是任何拥有FILE权限的用户都可以使服务器以 root 身份创建文件。
FILE 权限
在授予任何用户FILE权限时要小心,因为用户可以使用mysqld守护程序的权限在文件系统的任何位置写入文件,其中包括服务器的数据目录。但是,他们不能覆盖现有文件。此外,用户可以将 MySQL(或运行mysqld的用户)可以访问的任何文件读取到数据库表中。FILE是全局权限,这意味着您无法将其限制为特定数据库:
mysql> SHOW GRANTS;
+--------------------------------------------------------------------+
| Grants for company_admin@% |
+--------------------------------------------------------------------+
| GRANT FILE ON *.* TO `company_admin`@`%` |
| GRANT SELECT, INSERT, CREATE ON `company`.* TO `company_admin`@`%` |
+--------------------------------------------------------------------+
2 rows in set (0.00 sec)
mysql> USE company;
Database changed
mysql> CREATE TABLE hack (ibdfile longblob);
Query OK, 0 rows affected (0.05 sec)
mysql> LOAD DATA INFILE '/var/lib/mysql/employees/salaries.ibd' INTO TABLE hack CHARACTER SET latin1 FIELDS TERMINATED BY '@@@@@';
Query OK, 366830 rows affected (18.98 sec)
Records: 366830 Deleted: 0 Skipped: 0 Warnings: 0
mysql> SELECT * FROM hack;
注意,拥有FILE权限的公司用户可以从employees表中读取数据。
您不需要担心前面的黑客攻击,因为默认情况下可以读取和写入文件的位置仅限于/var/lib/mysql-files,使用secure_file_priv变量。问题出在当您将secure_file_priv变量设置为NULL,空字符串,MySQL数据目录或 MySQL 可以访问的任何敏感目录(例如 MySQL数据目录之外的表空间)时。如果将secure_file_priv设置为不存在的目录,将导致错误。
建议将secure_file_priv保留为默认值:
mysql> SHOW VARIABLES LIKE 'secure_file_priv';
+------------------+-----------------------+
| Variable_name | Value |
+------------------+-----------------------+
| secure_file_priv | /var/lib/mysql-files/ |
+------------------+-----------------------+
1 row in set (0.00 sec)
永远不要让任何人访问mysql.user表。有关安全准则的更多信息,请参阅dev.mysql.com/doc/refman/8.0/en/security-guidelines.html和dev.mysql.com/doc/refman/8.0/en/security-against-attack.html。
限制网络和用户
不要将数据库开放给整个网络,这意味着 MySQL 运行的端口(3306)不应该从其他网络访问。它应该只对应用服务器开放。您可以使用 iptables 或host.access文件设置防火墙以限制对端口3306的访问。如果您在云上使用 MySQL,提供商还将提供防火墙。
如何做...
要测试这一点,您可以使用telnet:
shell> telnet <mysql ip> 3306
# if telnet is not installed you can install it or use nc (netcat)
如果 telnet 挂起或连接被拒绝,这意味着端口已关闭。请注意,如果看到这样的输出,这意味着端口没有被阻止:
shell> telnet 35.186.158.188 3306
Trying 35.186.158.188...
Connected to 188.158.186.35.bc.googleusercontent.com.
Escape character is '^]'.
FHost '183.82.17.137' is not allowed to connect to this MySQL serverConnection closed by foreign host.
这意味着端口是打开的,但 MySQL 正在限制访问。
在创建用户时,避免从任何地方(%选项)授予访问权限。限制访问 IP 范围或子域。还限制用户只能访问所需的数据库。例如,employees数据库的read_only用户不应能够访问其他数据库:
mysql> CREATE user 'employee_read_only'@'10.10.%.%' IDENTIFIED BY '<Str0ng_P@$$word>';
Query OK, 0 rows affected (0.00 sec)
mysql> GRANT SELECT ON employee.* TO 'employee_read_only'@'10.10.%.%';
Query OK, 0 rows affected (0.01 sec)
employee_read_only用户将只能从10.10.%.%子网访问,并且只能访问employee数据库。
使用 mysql_config_editor 进行无密码身份验证
每当您使用命令行客户端输入密码时,您可能会注意到以下警告:
shell> mysql -u dbadmin -p'$troNgP@$$w0rd'
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 1345
Server version: 8.0.3-rc-log MySQL Community Server (GPL)
~
mysql>
如果不在命令行中传递密码并在提示时输入,您将不会收到警告:
shell> mysql -u dbadmin -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 1334
Server version: 8.0.3-rc-log MySQL Community Server (GPL)
~
mysql>
但是,当您在客户端实用程序上开发一些脚本时,使用密码提示很困难。避免这种情况的一种方法是将密码存储在home目录中的.my.cnf文件中。默认情况下,mysql命令行实用程序会读取.my.cnf文件,而不会要求输入密码:
shell> cat $HOME/.my.cnf
[client]
user=dbadmin
password=$troNgP@$$w0rd
shell> mysql
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 1396
Server version: 8.0.3-rc-log MySQL Community Server (GPL)
~
mysql>
请注意,您可以在不提供任何密码的情况下连接,但这会引起安全问题;密码是明文的。为了克服这一点,MySQL 引入了mysql_config_editor,它以加密格式存储密码。客户端程序可以解密文件(仅在内存中使用)以连接到服务器。
如何做...
使用mysql_config_editor创建.mylogin.cnf文件:
shell> mysql_config_editor set --login-path=dbadmin_local --host=localhost --user=dbadmin --password
Enter password:
通过更改登录路径,可以添加多个主机名和密码。如果更改了密码,可以再次运行此实用程序,以更新文件中的密码:
shell> mysql_config_editor set --login-path=dbadmin_remote --host=35.186.157.16 --user=dbadmin --password
Enter password:
如果要使用dbadmin用户登录35.186.157.16,只需执行mysql --login-path=dbadmin_remote:
shell> mysql --login-path=dbadmin_remote
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 215074
~
mysql> SELECT @@server_id;
+-------------+
| @@server_id |
+-------------+
| 200 |
+-------------+
1 row in set (0.00 sec)
要连接到localhost,只需执行mysql或mysql --login-path=dbadmin_local:
shell> mysql
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 1523
~
mysql> SELECT @@server_id;
+-------------+
| @@server_id |
+-------------+
| 1 |
+-------------+
1 row in set (0.00 sec)
shell> mysql --login-path=dbadmin_local
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 1524
~
mysql> SELECT @@server_id;
+-------------+
| @@server_id |
+-------------+
| 1 |
+-------------+
1 row in set (0.00 sec)
如果dbadmin的密码在所有服务器上都相同,可以通过指定主机名连接到任何服务器。您无需指定密码:
shell> mysql -h 35.198.210.229
Welcome to the MySQL monitor. Commands end with ; or \g.
~
mysql> SELECT @@server_id;
+-------------+
| @@server_id |
+-------------+
| 364 |
+-------------+
1 row in set (0.00 sec)
如果要打印所有登录路径,请执行以下操作:
shell> mysql_config_editor print --all
[dbadmin_local]
user = dbadmin
password = *****
host = localhost
[dbadmin_remote]
user = dbadmin
password = *****
host = 35.186.157.16
您可以注意到该实用程序掩盖了密码。如果尝试读取文件,您只会看到无意义的字符:
shell> cat .mylogin.cnf
?-z???|???-B????dU?bz4-?W???g?q?BmV?????K?I?? h%?+b???_??@V???vli?J???X`?qP
此实用程序仅帮助您避免存储明文密码并简化连接到 MySQL 的过程。有许多方法可以解密存储在.mylogin.cnf文件中的密码。因此,如果使用mysql_config_editor,请不要认为密码是安全的。您可以将此文件复制到其他服务器(仅当用户名和密码相同时才有效),而不是每次创建.mylogin.cnf文件。
重置 root 密码
如果忘记了 root 密码,可以通过以下两种方法重置密码。
如何做...
让我们深入了解细节。
使用 init-file
在类 Unix 系统上,您可以通过指定 init-file 停止服务器并启动服务器。您可以将ALTER USER 'root'@'localhost' IDENTIFIED BY 'New$trongPass1' SQL 代码保存在该文件中。MySQL 在启动时执行文件的内容,从而更改 root 用户的密码:
- 停止服务器:
shell> sudo systemctl stop mysqld
shell> pgrep mysqld
- 将 SQL 代码保存在
/var/lib/mysql/mysql-init-password中;使其仅对 MySQL 可读:
shell> vi /var/lib/mysql/mysql-init-password
ALTER USER 'root'@'localhost' IDENTIFIED BY 'New$trongPass1';
shell> sudo chmod 400 /var/lib/mysql/mysql-init-password
shell> sudo chown mysql:mysql /var/lib/mysql/mysql-init-password
- 使用
--init-file选项和其他所需选项启动 MySQL 服务器:
shell> sudo -u mysql /usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid --user=mysql --init-file=/var/lib/mysql/mysql-init-password
mysqld will log errors to /var/log/mysqld.log
mysqld is running as pid 28244
- 验证错误日志文件:
shell> sudo tail /var/log/mysqld.log
~
2017-11-27T07:32:25.219483Z 0 [Note] Execution of init_file '/var/lib/mysql/mysql-init-password' started.
2017-11-27T07:32:25.219639Z 4 [Note] Event Scheduler: scheduler thread started with id 4
2017-11-27T07:32:25.223528Z 0 [Note] Execution of init_file '/var/lib/mysql/mysql-init-password' ended.
2017-11-27T07:32:25.223610Z 0 [Note] /usr/sbin/mysqld: ready for connections. Version: '8.0.3-rc-log' socket: '/var/lib/mysql/mysql.sock' port: 3306 MySQL Community Server (GPL)
- 验证是否能够使用新密码登录:
shell> mysql -u root -p'New$trongPass1'
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 15
Server version: 8.0.3-rc-log MySQL Community Server (GPL)
~
mysql>
- 现在,最重要的事情!删除
/var/lib/mysql/mysql-init-password文件:
shell> sudo rm -rf /var/lib/mysql/mysql-init-password
- 可选地,您可以停止服务器,然后正常启动,而无需
--init-file选项。
使用--skip-grant-tables
在此方法中,您可以通过指定--skip-grant-tables停止服务器,然后启动服务器,这将不会加载授予表。您可以作为 root 连接到服务器而无需密码,并设置密码。由于服务器在没有授予的情况下运行,因此可能会有其他网络的用户连接到服务器。因此,从 MySQL 8.0.3 开始,--skip-grant-tables会自动启用--skip-networking,这将不允许远程连接:
- 停止服务器:
shell> sudo systemctl stop mysqld
shell> ps aux | grep mysqld | grep -v grep
- 使用
--skip-grant-tables选项启动服务器:
shell> sudo -u mysql /usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid --user=mysql --skip-grant-tables
mysqld will log errors to /var/log/mysqld.log
mysqld is running as pid 28757
- 连接到 MySQL 而不需要密码,执行
FLUSH PRIVILEGES重新加载授权,并更改用户以更改密码:
shell> mysql -u root
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 6
Server version: 8.0.3-rc-log MySQL Community Server (GPL)
~
mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.04 sec)
mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'New$trongPass1';
Query OK, 0 rows affected (0.01 sec)
- 使用新密码测试与 MySQL 的连接:
shell> mysql -u root -p'New$trongPass1'
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 7
~
mysql>
- 重新启动 MySQL 服务器:
shell> ps aux | grep mysqld | grep -v grep
mysql 28757 0.0 13.3 1151796 231724 ? Sl 08:16 0:00 /usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid --user=mysql --skip-grant-tables
shell> sudo kill -9 28757
shell> ps aux | grep mysqld | grep -v grep
shell> sudo systemctl start mysqld
shell> ps aux | grep mysqld | grep -v grep
mysql 29033 5.3 16.8 1240224 292744 ? Sl 08:27 0:00 /usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid
使用 X509 设置加密连接
如果客户端和 MySQL 服务器之间的连接未加密,任何可以访问网络的人都可以检查数据。如果客户端和服务器位于不同的数据中心,建议使用加密连接。默认情况下,MySQL 8 使用加密连接,但如果加密连接失败,它将退回到未加密连接。您可以通过检查Ssl_cipher变量的状态来测试。如果连接是由localhost建立的,则不会使用密码:
mysql> SHOW STATUS LIKE 'Ssl_cipher';
+---------------+--------------------+
| Variable_name | Value |
+---------------+--------------------+
| Ssl_cipher | DHE-RSA-AES256-SHA |
+---------------+--------------------+
1 row in set (0.00 sec)
如果不使用 SSL,Ssl_cipher将为空白。
您可以要求某些用户只能通过加密连接连接(通过指定REQUIRE SSL子句),并对其他用户将其作为可选项。
根据 MySQL 文档:
MySQL 支持使用 TLS(传输层安全)协议在客户端和服务器之间建立加密连接。TLS 有时被称为 SSL(安全套接字层),但 MySQL 实际上并不使用 SSL 协议进行加密连接,因为其加密较弱。TLS 使用加密算法来确保可以信任通过公共网络接收的数据。它具有检测数据更改、丢失或重放的机制。TLS 还包含使用 X509 标准进行身份验证的算法。
在本节中,您将学习如何使用 X509 建立 SSL 连接。
所有与 SSL(X509)相关的文件(ca.pem、server-cert.pem、server-key.pem、client-cert.pem和client-key.pem)都是 MySQL 在安装过程中创建并保存在数据目录下的。服务器需要ca.pem、server-cert.pem和server-key.pem文件,客户端使用client-cert.pem和client-key.pem文件连接到服务器。
如何操作...
- 验证
数据目录中的文件,更新my.cnf,重新启动服务器,并检查与 SSL 相关的变量。在 MySQL 8 中,默认情况下设置了以下值:
shell> sudo ls -lhtr /var/lib/mysql | grep pem
-rw-------. 1 mysql mysql 1.7K Nov 19 13:53 ca-key.pem
-rw-r--r--. 1 mysql mysql 1.1K Nov 19 13:53 ca.pem
-rw-------. 1 mysql mysql 1.7K Nov 19 13:53 server-key.pem
-rw-r--r--. 1 mysql mysql 1.1K Nov 19 13:53 server-cert.pem
-rw-------. 1 mysql mysql 1.7K Nov 19 13:53 client-key.pem
-rw-r--r--. 1 mysql mysql 1.1K Nov 19 13:53 client-cert.pem
-rw-------. 1 mysql mysql 1.7K Nov 19 13:53 private_key.pem
-rw-r--r--. 1 mysql mysql 451 Nov 19 13:53 public_key.pem
shell> sudo vi /etc/my.cnf
[mysqld]
ssl-ca=/var/lib/mysql/ca.pem
ssl-cert=/var/lib/mysql/server-cert.pem
ssl-key=/var/lib/mysql/server-key.pem
shell> sudo systemctl restart mysqld
mysql> SHOW VARIABLES LIKE '%ssl%';
+---------------+--------------------------------+
| Variable_name | Value |
+---------------+--------------------------------+
| have_openssl | YES |
| have_ssl | YES |
| ssl_ca | /var/lib/mysql/ca.pem |
| ssl_capath | |
| ssl_cert | /var/lib/mysql/server-cert.pem |
| ssl_cipher | |
| ssl_crl | |
| ssl_crlpath | |
| ssl_key | /var/lib/mysql/server-key.pem |
+---------------+--------------------------------+
9 rows in set (0.01 sec)
- 将
client-cert.pem和client-key.pem文件从服务器的数据目录复制到客户端位置:
shell> sudo scp -i $HOME/.ssh/id_rsa /var/lib/mysql/client-key.pem /var/lib/mysql/client-cert.pem <user>@<client_ip>:
# change the ssh private key path as needed.
- 通过传递
--ssl-cert和--ssl-key选项连接到服务器:
shell> mysql --ssl-cert=client-cert.pem --ssl-key=client-key.pem -h 35.186.158.188
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 666
Server version: 8.0.3-rc-log MySQL Community Server (GPL)
~
mysql>
- 强制用户只能通过 X509 连接:
mysql> ALTER USER `dbadmin`@`%` REQUIRE X509;
Query OK, 0 rows affected (0.08 sec)
- 测试连接:
shell> mysql --login-path=dbadmin_remote -h 35.186.158.188 --ssl-cert=client-cert.pem --ssl-key=client-key.pem
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 795
Server version: 8.0.3-rc-log MySQL Community Server (GPL)
~
mysql> ^DBye
- 如果不指定
--ssl-cert或--ssl-key,则将无法登录:
shell> mysql --login-path=dbadmin_remote -h 35.186.158.188
ERROR 1045 (28000): Access denied for user 'dbadmin'@'35.186.157.16' (using password: YES)
shell> mysql --login-path=dbadmin_remote -h 35.186.158.188 --ssl-cert=client-cert.pem
mysql: [ERROR] SSL error: Unable to get private key from 'client-cert.pem'
ERROR 2026 (HY000): SSL connection error: Unable to get private key
shell> mysql --login-path=dbadmin_remote -h 35.186.158.188 --ssl-key=client-key.pem
mysql: [ERROR] SSL error: Unable to get certificate from 'client-key.pem'
ERROR 2026 (HY000): SSL connection error: Unable to get certificate
默认情况下,所有与 SSL 相关的文件都保存在数据目录中。如果要将它们保存在其他位置,可以在my.cnf文件中设置ssl_ca、ssl_cert和ssl_key,然后重新启动服务器。您可以通过 MySQL 或 OpenSSL 生成一组新的 SSL 文件。要了解更详细的步骤,请参考dev.mysql.com/doc/refman/8.0/en/creating-ssl-rsa-files-using-mysql.html。还有许多其他身份验证插件可用。您可以参考dev.mysql.com/doc/refman/8.0/en/authentication-plugins.html了解更多详细信息。
设置 SSL 复制
如果启用 SSL 复制,主从之间的二进制日志传输将通过加密连接发送。这类似于前一节中解释的服务器/客户端连接。
如何操作...
-
在主库上,如前一节所述,您需要启用 SSL。
-
在主库上,将
client*证书复制到从库上:
mysql> sudo scp -i $HOME/.ssh/id_rsa /var/lib/mysql/client-key.pem /var/lib/mysql/client-cert.pem <user>@<client_ip>:
- 在从库上,创建
mysql-ssl目录以保存与 SSL 相关的文件,并正确设置权限:
shell> sudo mkdir /etc/mysql-ssl
shell> sudo cp client-key.pem client-cert.pem /etc/mysql-ssl/
shell> sudo chown -R mysql:mysql /etc/mysql-ssl
shell> sudo chmod 600 /etc/mysql-ssl/client-key.pem
shell> sudo chmod 644 /etc/mysql-ssl/client-cert.pem
- 在从库上,使用与从库相关的 SSL 更改执行
CHANGE_MASTER命令:
mysql> STOP SLAVE;
mysql> CHANGE MASTER TO MASTER_SSL=1, MASTER_SSL_CERT='/etc/mysql-ssl/client-cert.pem', MASTER_SSL_KEY='/etc/mysql-ssl/client-key.pem';
mysql> START SLAVE;
- 验证从库的状态:
mysql> SHOW SLAVE STATUS\G
*************************** 1\. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 35.186.158.188
~
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
~
Skip_Counter: 0
Exec_Master_Log_Pos: 354
Relay_Log_Space: 949
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: Yes
Master_SSL_CA_File: /etc/mysql-ssl/ca.pem
Master_SSL_CA_Path:
Master_SSL_Cert: /etc/mysql-ssl/client-cert.pem
Master_SSL_Cipher:
Master_SSL_Key: /etc/mysql-ssl/client-key.pem
Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
~
Master_UUID: fe17bb86-cd30-11e7-bc3b-42010a940003
Master_Info_File: mysql.slave_master_info
SQL_Delay: 0
SQL_Remaining_Delay: NULL
Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
~
1 row in set (0.00 sec)
- 一旦在所有从库上进行了与 SSL 相关的更改,在主库上,强制复制用户使用 X509:
mysql> ALTER USER `repl`@`%` REQUIRE X509;
Query OK, 0 rows affected (0.00 sec)
请注意,这可能会影响其他复制用户。作为替代方案,您可以创建一个带 SSL 的复制用户和一个普通的复制用户。
- 验证所有从库上的从库状态。


浙公网安备 33010602011771号