笔记 - 高性能 MySQL (第四版)
posted on 21 Nov 2022 under category note
书评: https://unifreak.github.io/book_review/high_performance_mysql_4th_edition
商业选项: SolarWinds.
开源选项:
Threads_running 可作为关键指标, 它跟踪当前正运行的查询数量. 如果它快速增长, 说明查询不够快; 如果它超过了 cpu 核数, 可能表明服务器正处于不稳定状态. 它与 max_connections 差值, 可以说明 工作是否过载.
SELECT
t.TABLE_SCHEMA AS `schema`,
t.TABLE_NAME AS `table`,
t.AUTO_INCREMENT AS `auto_increment`,
c.DATA_TYPE AS `pk_type`,
(
t.AUTO_INCREMENT / (
CASE DATA_TYPE WHEN 'tinyint' THEN IF(
COLUMN_TYPE LIKE '%unsigned', 255,
127
) WHEN 'smallint' THEN IF(
COLUMN_TYPE LIKE '%unsigned', 65535,
32767
) WHEN 'mediumint' THEN IF(
COLUMN_TYPE LIKE '%unsigned', 16777215,
8388607
) WHEN 'int' THEN IF(
COLUMN_TYPE LIKE '%unsigned', 4294967295,
2147483647
) WHEN 'bigint' THEN IF(
COLUMN_TYPE LIKE '%unsigned', 18446744073709551615,
9223372036854775807
) END / 100
)
) AS `max_value`
FROM
information_schema.TABLES t
INNER JOIN information_schema.COLUMNS c ON t.TABLE_SCHEMA = c.TABLE_SCHEMA
AND t.TABLE_NAME = c.TABLE_NAME
WHERE
t.AUTO_INCREMENT IS NOT NULL
AND c.COLUMN_KEY = 'PRI'
AND c.DATA_TYPE LIKE '%int';
Performance Schema (以下简写为 PS).提供了服务器内部运行的操作上的底层指标.
“程序插桩 (instrument)” 指在 mysql 代码中插入探测代码. setup_instruments 表包含了支持的 插桩列表. 所有插桩的名称由 / 分隔的部件组成, 如 statement/sql/select
, 最左代表桩类型, 其 余则是从通用到特定的子系统.
“消费者表 (consumer)” 指是插桩发送信息的目的地, 测量结果存在 performance schema 库的多个表 中.
sys Schema 是为了方便使用 performance schema 的, 它全部基于 performance_schema 上的视 图和存储例程.
PS 将数据存在 PERFORMANCE_SCHEMA 引擎的表中, 这个引擎将数据存在内存中. 可通过启动变量来调 整使用的内存大小.
使用 PS 有如下局限性:
早期版本的 PS 实现不够理想, 资源消耗过高, 一般建议关闭; 对于新版建议启用并按需动态地启用插桩和 消费者表, 它可以解决查询性能, 锁定, I/O, 错误等问题.
将配置变量 performance_schema 设为 ON 即可启用 PS.
有三个方法启用插桩:
同理, 也有三个方启用消费者表:
对于特定 “对象” (包括: EVENT, FUNCTION, PROCEDURE, ENABLE, TRIGGER) 的监控配置可通过 setup_objects 表完成;
对于后台线程的监控配置可通过 setup_threads 表完成; 对于用户线程则可通过 setup_actors 表.
相关桩:
Instrument class Description
---------------------------------------------------------------------------
statement/sql SQL statements, such as SELECT or CREATE TABLE
statement/sp Stored procedures control
statement/scheduler Event scheduler
statement/com Commands, such as quit, KILL, DROP DATABASE, or Binlog Dump.
Some are not available for users and are called
by the mysqld process itself.
statement/abstract Class of four commands: clone, Query, new_packet, and relay_log
对于常规 SQL 语句, 要关注以下输出列:
Column Desc
------------------------------------------------------------------------------------
CREATED_TMP_DISK_TABLES The query created this number of disk-based temporary
tables. You have two options to resolve this issue:
optimize the query or increase maximum size for in-
memory temporary tables.
CREATED_TMP_TABLES The query created this number of memory-based
temporary tables. Use of in-memory temporary tables is
not bad per se. However, if the underlying table grows,
they may be converted into disk-based tables. It is good
to be prepared for such situations in advance.
SELECT_FULL_JOIN The JOIN performed a full table scan because there is
no good index to resolve the query otherwise. You need
to reconsider your indexes unless the table is very
small.
SELECT_FULL_RANGE_JOIN If the JOIN used a range search of the referenced
table.
SELECT_RANGE If the JOIN used a range search to resolve rows in the
first table. This is usually not a big issue.
SELECT_RANGE_CHECK If the JOIN is without indexes, which checks for keys
after each row. This is a very bad symptom, and you
need to reconsider your table indexes if this value is
greater than zero.
SELECT_SCAN If the JOIN did a full scan of the first table. This is an
issue if the table is large.
SORT_MERGE_PASSES The number of merge passes that the sort has to
perform. If the value is greater than zero and the query
performance is slow, you may need to increase
sort_buffer_size.
SORT_RANGE If the sort was done using ranges.
SORT_ROWS The number of sorted rows. Compare with the value of
the returned rows. If the number of sorted rows is
higher, you may need to optimize your query.
SORT_SCAN If the sort was done by scanning a table. This is a very
bad sign unless you purposely select all rows from the
table without using an index.
NO_INDEX_USED No index was used to resolve the query.
NO_GOOD_INDEX_USED Index used to resolve the query is not the best. You
need to reconsider your indexes if this value is greater
than zero.
可以使用上述列与 0 比较, 筛出想要优化的语句, 如
# 没有使用合适索引
WHERE NO_INDEX_USED > 0 OR NO_GOOD_INDEX_USED > 0;
# 创建了临时表
WHERE CREATED_TMP_TABLES > 0 OR CREATED_TMP_DISK_TABLES > 0;
# 返回了错误
WHERE ERRORS > 0;
# 时间超过 5s
WHERE TIMER_WAIT > 5000000000;
sys schema 相关视图:
statement_analysis
statement_with_(errors_or_warnings|full_table_scans)
statement_with_(runtimes_in_95th_percentile|sorting|temp_tables)
启用 ‘stage/%’ 模式的桩, 然后查看 events_stages_* 表, 可以用来找出 “查询执行的哪个阶段花费 了非常长时间”.
注意, 只有通信服务模块支持, 引擎不支持剖析.
读写比例:
SELECT EVENT_NAME, COUNT(EVENT_NAME)
FROM events_statements_history_long
GROUP BY EVENT_NAME;
语句延迟:
SELECT EVENT_NAME, COUNT(EVENT_NAME),
SUM(LOCK_TIME/1000000) AS latency_ms
FROM events_statements_history
GROUP BY EVENT_NAME ORDER BY latency_ms DESC;
启用 wait/lock/meta-data/sql/mdl 桩.
memory 类的桩.
三个相关表: global_variables, session_variables, variables_by_thread.
表: events_errors_summary_global_by_error.
最常见的瓶颈是 CPU 耗尽. I/O 饱和也会发生, 但比 CPU 耗尽少得多. 配置大内存的主要原因不是为了 在内存中保存数据, 而是为了避免磁盘 I/O.
RAID 代替不了备份.
应明智的使用 RAID 缓存: 用于读操作通常是浪费 (因为 linux 和数据库服务器都有更大的缓存), 通常 将其用于写操作. 但除非有备用电池单元 (BBU), 否则不应启用写缓存.
大多数时候, 默认值就行, 只在发生异常情况时才更改它们:
DNS 过程可能很慢, 建议启用 skip_name_resolve.
可能需要将本地端口范围以及请求队列调大:
$ echo 1024 65535 > /proc/sys/net/ipv4/ip_local_port_range
$ echo 4096 > /proc/sys/net/ipv4/tcp_max_sync_backlog
大多数 FS 表现相近, 单纯为性能而寻找 FS 实际是一种干扰.
最好使用日志型 FS, 如 ext4, XFS 或 ZFS, 否则系统崩溃后检查 FS 可能需要很长时间. ext4 在 特定内核版本中存在性能问题, 但它是一个可以接受的选项. 通常建议使用 XFS.
对于 ext4, 日志级别可设置为 3 (在 /etc/fstab 挂载选项中设置); 最好禁用记录访问时间:
/dev/sda2 /usr/lib/mysql ext3 noatime,nodiratime,data=writeback 0 1
对于磁盘队列调度器, noop 和 deadline 的差别很小, 最重要的是不要使用 CFQ, 它会导致非常糟 的响应时间.
确保更快内存访问的最佳方法之一, 仍然是用 tcmalloc/jemalloc 替换内置的 glibc.
建议完全不使用交换空间. 并将 swappiness 设置为 0:
$ echo 0 > /proc/sys/vm/swappiness
大多数时候都希望 vmstat 的 si,so 值为 0, 绝对不希望看到它超过 10.
强烈建议识别所有关键进程 (如 ssh, mysql), 主动调整 OOM Killer 分值, 防止它们被首先终止.
系统自带的:
第三方工具:
为 mysql 创建合适的配置文件是一不迂回的过程.
建议:
不建议:
找到配置文件:
$ which mysqld
/usr/sbin/mysqld
$ /usr/sbin/mysqld --verbose --help | grep -A 1 'Default options'
/eetc/mysql/my.cnf ~/.my.cnf /usr/etc/my.cnff
配置文件采用 INI 格式. 配置的作用域有以下三种, 每个具体配置项可能有多种作用域:
除了配置文件, 很多变量也可以用 SET 动态更改:
SET sort_buffer_size = <value>;
SET GLOBAL sort_buffer_size = <value>;
SET @@sort_buffer_size := <value>;
SET @@session.sort_buffer_size := <value>;
SET @@global.sort_buffer_size := <value>;
一般来说, 使用 SET 的更改重启后失效, 但 v8 引入了 “持久化系统变量” 功能, 来进行持久化:
SET PERSIST ...
在每次更改后, 应检查 SHOW GLOBAL VARIABLES 确保其生效了.
以下是基于 v8.0 构建的:
[mysqld]
# GENERAL
datadir = /var/lib/mysql
socket = /var/lib/mysql/mysql.sock
pid_file = /var/lib/mysql/mysql.pid
user = mysql
port = 3306
# INNODB
innodb_buffer_pool_size = <value>
innodb_log_file_size = <value>
innodb_file_per_table = 1
innodb_flush_method = O_DIRECT
# LOGGING
log_error = /var/lib/mysql/mysql-error.log
log_slow_queries = /var/lib/mysql/mysql-slow.log
# OTHER
tmp_table_size = 32M
max_heap_table_size = 32M
max_connections = <value>
thread_cache_size = <value>
table_open_cache = <value>
# 应尽可能大: 找开文件句柄的成本很低, 否则可能看到 "too many open files" 错误.
open_files_limit = 65535
[client]
socket = /var/lib/mysql/mysql.sock
port = 3306
v8.0 引入了 innodb_dedicated_server 这个选项, 用它来让 mysql 自配置上面四个 innodb_* 变量通常是最佳方式.
使用 innodb_dedicated_server 通常会占用 50%-75% 内存, 这样, 至少有 25% 内存可用于每个连 接的内存分配, OS 开销和其他内存设置.
了解峰值使用期间内存使用是很有用的, 但不需要假设最坏情况. 如假设配置为最多允许 100 个连接, 理 论上所有 100 个连接可能都执行大型查询, 但实际上不太可能会发生. 使用许多大型临时表或复杂存储过 程的查询最有可能占用大量内存.
判断 OS 是否有足够内存的方式就是看有没有发生交换.
这通常是最重要的变量. 可以使用 SHOW 或 innotop 监控 innodb 缓冲池的内存使用情况.
不要过度分配, 太大缓冲池会有以下挑战:
thread_cache_size (-1) 指定了可以保存在缓存中的线程数, 通常不需更改.
要检查它是否足够大, 需查看 Threads_created 状态, 应尽量保持每秒创建的新线程数少于 10 个. 另外为了处理波动, 需查看 Threads_connected: 如果它通常在 100 到 120 之间, 可以将缓存大小设 为 20 (差值).
把它设置过小也并不会节省内存. 每个处于缓存或休眠状态的线程通常只有 256K 大.
一些选项会影响将数据同步到磁盘和执行恢复的方式, 对性能影响很大, 它们也代表了性能和数据安全 之间的权衡.
“事务日志” 文件总大小由 innodb_log_file_size, innodb_log_files_in_group 控制, 这对写 入性能非常重要. 建议让 innodb_dedicated_server 自动管理.
“日志缓冲区” 不需要设置太大, 建议 innodb_log_buffer_size 的范围是 1-8MB, 除非写入很多大的 BLOB 记录. 使用 innodb_flush_log_at_trx_commit 控制它刷到磁盘上的日志文件的频率如下:
在崩溃和断电时, 设置 0 和 2 通常会导致最多 1 秒的数据丢失, 但某些情况下, 刷新暂停时, 可能 会丢失超过 1 秒的事务. 有时, 硬盘控制器或 OS 通过将数据放入另一个缓存 (如硬盘自身缓存) 来 “假装” 刷盘, 这样速度更快但非常危险, 如果驱动器断电, 数据不仅可能丢失, 还可能导致数据损坏.
高性能事务需求的最佳配置是将它设为 1 并将日志文件放在有备用电池的写缓存和 SSD 的 RAID 卷 上, 这既安全又非常快. 任何需要重要工作的生产数据库都需要这种硬件.
innodb_flush_method 可以控制 innodb 与 FS 的实际交互方式, 它同时影响日志文件和数据文件. 如果我们有刚刚所述的硬件, 建议设为 O_DIRECT, 否则还是让 innodb_dedicated_server 自行设置 吧.
innodb_data_home_dir = /var/lib/mysql/
innodb_data_file_path = ibdata1:1G;ibdata2:1G;ibdata3:1G;autoextend:max:2G
上面的配置创建了一个 3GB 的表空间, 跨越了 3 个文件, 并允许空间不足时自动扩展最后一个文件. 如 果允许自动扩展, 最好用 max: 设置一个上限, 以防止它变的过大, 因为一旦增长, 就无法再收缩 (除非 你将数据导出, 关闭 mysql 并删除所有文件, 再恢复数据).
将多个文件分散到不同驱动器上不会有太多性能提升, 因为 innodb 还是将它们串连起来使用: 填满 一个再填下一个. 更好的方式是使用 RAID 控制器分散负载.
使用 innodb_file_per_table 可以让 innodb 为每个表使用单独文件, 在以前版本中它会导致 DROP TABLE 变慢, 但 v8.0 中建议使用, 它提供了额外的可管理性和可视性.
如果事务长时间保持打开, innodb 将无法删除旧版本的行. 如果 undo 日志很大, 且表空间因此而增大, 可以设置 innodb_max-purge_lag 强制 mysql 放慢速度来让 innodb 的清理线程跟上. 否则 innodb 会不断写入数据直到磁盘或表空间耗尽. 这是别无选择的选择, 两害取其轻.
sync_binlog 控制二进制日志的刷盘行为, 强烈建议设置为 1.
对于 v5.7 如果遇到并发问题, 通常解决方案就是升级服务器.
对于新版, 大多数情况下无需限制并发性. 如果遇到并发瓶颈, 最好的选择是数据分片. 不可行的话才 考虑使用 innodb_thread_concurrency 限制并发性. 建义首先将其置为与可用 CPU 数相同的值, 然 后跟据需要调整. 另外两个相关配置是 innodb_thread_sleep_delay 和 innodb_concurrency_tickets.
应根据系统将要运行的特定查询设计 schema.
几个原则:
Skeema 是一个在跨多个环境的版本控制中管理 schema 更改的开源解决方案.
在生产环境运行 schema 更改, 可以考虑:
创建一个最优索引经常需要配合重写查询.
评价索引的 “三星系统”:
“选择性” 指不重复的索引值 (也叫基数) 与记录总数的比值.
高选择性的索引可在查找时过滤更多的行. 既要选择足够长的前缀以保证较高的选择性, 又不能太长以节约空 间. 即前缀的基数应接近于完整列的基数. 如可以使用以下 SQL 计算不同长度前缀时的选择性:
SELECT COUNT(DISTINCT LEFT(city, 3))/COUNT(*) AS sel3,
COUNT(DISTINCT LEFT(city, 4))/COUNT(*) AS sel4,
COUNT(DISTINCT LEFT(city, 5))/COUNT(*) AS sel5,
COUNT(DISTINCT LEFT(city, 6))/COUNT(*) AS sel6,
COUNT(DISTINCT LEFT(city, 7))/COUNT(*) AS sel7
FROM sakila.city_demo;
前缀索引能使索引更小更快, 但:
在多列上独立创建多个单列索引, 在大部分情况下不能提高查询性能. 虽
然 mysql 引入了 “索引合并” 一定程度上能用多个单列索引定位指定行. 索引合并可应用于以下情况:
但它更多是索引设计不佳的指示, 因为:
如在 EXPLAIN 中的 Extra 可以看到类似 “Using union(PRIMARY, idx_fk_film_id);”, 就说明 用了索引合并, 可以:
为多列索引选择正确的列顺序依赖于使用该索引的查询语句 (WHERE), 同时要考虑满足排序 (ORDER BY) 和分组 (GROUP BY) 的需要.
如无需考虑排序和分组, 通常将选择性高的放在前面. 同时也要注意列中的离群值.
innodb 用主键来聚集数据. 如果没有定义主键, 会选择一个唯一的非空索引代替; 如果没有这样的索引, 它会隐式定义一个主键来作为聚簇索引 – 所有需要使用这种隐藏主键的表都依赖于一个单点的 “自增值”, 这可能会导致非常高的锁竞争.
使用聚簇索引的优点是: 关联的数据存在一起, 索引和数据存在一起, 能减少 I/O; 使用覆盖索引扫描的查 询可以直接使用叶节点中的主键值.
它的缺点是:
避免随机的 (如 UUID) 聚簇索引. 如果没有什么数据需要聚集, 可以定义一个 AUTO_INCREMENT 的 “代理键” 作为主键.
但按主键插入也可能有以下问题:
使用了覆盖索引时, EXPLAIN 的 Extra 列会有 “Using index” 信息.
mysql 有两种生成有序结果的方式: 通过排序操作, 或者按索引顺序扫描. 如果 EXPLAIN 中的 type 为 “index”, 则说明使用了后者.
扫描索引本身很快, 但如果索引不能覆盖所需的全部列, 就不得不每扫描一条索引记录就回表一次, 这基本上都是随机 I/O, 因此按索引顺序读取数据的速度通常要比顺序地全表扫描慢. 设计索引应尽 可能同时满足这两个任务.
能使用索引做排序的条件是:
综上, 假设有如下表:
CREATE TABLE rental (
...PRIMARY KEY (rental_id),
UNIQUE KEY rental_date (
rental_date, inventory_id, customer_id
),
KEY idx_fk_inventory_id (inventory_id),
KEY idx_fk_customer_id (customer_id),
KEY idx_fk_staff_id (staff_id),
...
);
则以下能用:
... WHERE rental_date = '2005-05-25' ORDER BY inventory_id, customer_id;
... WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC;
... WHERE rental_date > '2005-05-25' ORDER BY rental_date, inventory_id;
以下不能用:
... WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC, customer_id ASC;
... WHERE rental_date = '2005-05-25' ORDER BY inventory_id, staff_id;
... WHERE rental_date = '2005-05-25' ORDER BY customer_id;
... WHERE rental_date > '2005-05-25' ORDER BY inventory_id, customer_id;
... WHERE rental_date = '2005-05-25' AND inventory_id IN(1,2) ORDER BY customer_id
使用索引做排序的另一个最重要的场景是, 查询语句中同时有 ORDER BY 和 LIMIT 子句的情况.
mysql 允许在相同列上创建多个相同索引, 但应该避免. 可以使用 Percona 的 pt-duplicate-key-checker 来识别这类索引.
另外, 使用下面语句可以找出哪些索引从来没被使用过:
SELECT * FROM sys.schema_unused_indexes;
可以用 CHECK TABLE 检查表是否损坏.
可以用 REPAIR TABLE 修复表, 或一个 noop 的 ALTER 来重建表.
如果 innodb 的表发生了损坏, 一定是发生了严重错误, 需立刻调查一下原因. 通可通过设置 innodb_ force_recovery 参数进入 innodb 的强制恢复模式来修复数据.
innodb 通过抽样方式来计算统计信息, 可用 innodb_stats_sample_pages 设置样本页数量. 它也会 在以下情况触发统计信息的更新:
如果表很多, 这可能导致变慢, 可以通过关闭 innodb_stats_on_metadata 参数来避免.
可以用 ANALYZE TABLE 来重新生成统计信息, 用 INFORMATIONI_SCHEMA.STATISTICS 查询这些信息.
有三种类型的 “数据碎片”:
可以用 OPTIMIZE TABLE 或导出再导入的方式重新整理数据, 或者一个 noop 的 ALTER 来重建表.
查询是由一系列子任务组成的, 优化查询实际上就是优化其子任务: 要么不做, 要么少做, 要么快做.
应用层: 是否请求了不必要的数据?
服务器层: 是否在扫描额外记录?
mysql 能使用以下三种方式应用 WHERE 条件, 从好到坏:
如果发现查询需扫描大量数据但只返回少数行, 通常要:
使用多个简单查询替代一个复杂查询.
mysql 从设计上让连接和断开连接都很轻量, 在返回一个小的查询结果方面很高效.
切分查询.
可以将一个大查询分而治之, 分成小查询, 每个查询只完成一小部分, 分多次完成. 如清除大量数据时, 一次删除一万行数据一般来说是比较高效且对服务器影响较小的做法. 也可在中间都暂停一会儿, 将原本 的一次性压力分散到一个长时间段中.
分解联接查询.
即将一个联接查询分成对每个表进行一次单表查询, 然后在应用中进行联接. 这样做在如下情况下有优势:
mysql 无法像很多其它数据库那样, 利用多核来并行执行查询 – 不要花时间去尝试.
mysql 不允许同时进行查询和更新. 下面是一个符合标准的的 SQL:
UPDATE tbl AS outer_tbl
SET c = (
SELECT count(*) FROM tbl AS inner_tbl
WHERE inner_tbl.type = outer_tbl.type
);
但在 mysql 中会报:
ERROR 1093 (HY000): You can't specify target table 'outer_tbl' for update in FROM clause
可以使用 “生成表” 的形式绕过这种限制:
UPDATE tbl
INNER JOIN(
SELECT type, count(*) AS c
FROM tbl
GROUP BY type
) AS der USING(type)
SET tbl.c = der.c;
以下这些技巧是版本相关的, 对于未来 mysql 版本未必适用.
COUNT() 有两种非常不同的作用:
COUNT(*)
是 COUNT() 的一种特殊用法, mysql 不会将 * 扩展为全部列名, 而是统计行数.若只关心行数, 应始终使用 COUNT(*)
, 这样能更清晰的表达意图, 也能避免糟糕的性能.
使用 COUNT() 在同一个查询中统计同一列的不同值的数量, 就是在统计值, 如下面的 SQL 统计 color 分别为 blue 和 red 的数量:
SELECT
COUNT(color = 'blue' OR NULL) AS blue,
COUNT(color = 'red' OR NULL) AS red
FROM items;
通常来说, COUNT() 查询需要扫描大量行才能获得精确结果, 因此是很难优化的, 思路是:
“快速, 精确, 简单” 三者只能选两个.
有时需要对返回的分组再做一次 “超级聚合”. 一般使用 WITH ROLLUP 来实现, 但可能优化不够好. 优 化思路如下:
在分页的偏移量非常大的时候, 如 LIMIT 1000,20 这样的查询, mysql 需要查询 10020 条记录然后只 返回最后 20 条, 代价非常高.
优化思路是:
限制可见的分页的数量.
“延迟连接”: 尽可能地使用索引覆盖扫描, 而非查询所有行. 然后根据需要做一次联接操作再返回所需的列.
如下面的分页查询:
SELECT film_id, description FROM sakila.film ORDER BY title LIMIT 50, 5;
如果表非常大, 可以改成这样:
SELECT film.film_id, film.description
FROM sakila.film
INNER JOIN (
SELECT film_id FROM sakila.film
ORDER BY title LIMIT 50, 5
) AS lim USING(film_id);
如此, 内层的 SELECT file_id 可以仅用索引找出所需的行, 然后与整个表联接, 从行中取出 description. 类似技术也适用于带有 LIMIT 子句的联接.
通过预先计算出边界值, 将 LIMIT 查询转为已知位置的查询, 让 mysql 通过范围扫描获得对应的结 果. 对数据进行排名的问题也类似, 但往往还会同时和 GROUP BY 混合使用. 在这种情况下通常需要预 先计算并存储排名信息.
SELECT film_id, description FROM sakila.film
WHERE position BETWEEN 50 AND 54 ORDER BY position;
使用书签记录上页的位置. 如假设上页中主键最大的记录是 16030:
SELECT * FROM sakila.rental
WHERE rental_id > 16030
ORDER BY rental_id DESC LIMIT 20;
分页时另一个常用技巧是在 LIMIT 语句中加上 SQL_CALC_FOUND_ROWS 提示, 这样就可以获得去掉 LIMIT 后满足条件的行数, 因此可以作为总数. 但实际上, 加上这个提示后, 会让 mysql 扫描所有满足 条件的行才能计算出, 该提示的代价可能非常高.
更好的设计是:
COUNT(*)
.mysql 无法将限制条件从 UNION 的外层下推到内层, 本能限制部分返回结果的条件无法应用到内层上. 如:
(SELECT ...)
UNION ALL
(SELECT ...)
LIMIT 20;
这个查询会把两张表的所有记录放在临时表中, 然后取出前 20 条. 因此很多优化策略在 UNION 查询中都 没法很好的使用, 经常需要手工地将 WHERE, LIMIT, ORDER BY 等子句 “下推” 到 UNION 的各个子句 中, 如为两个子查询分别加上 LIMIT:
(SELECT ... LIMIT 20)
UNION ALL
(SELECT ... LIMIT 20)
LIMIT 20;
除非确实需要服务器消除重复行, 否则一定要使用 UNION ALL 而非 UNION. 如果没有 ALL, mysql 会给临时表加上 DISTINCT, 导致对整个临时表做唯一性检查, 这样代价很高.
复制常见用途是:
建议:
建议的配置:
innodb_flush_log_at_trx_commit = 1;
# 每次事务执行时把二进制日志写入磁盘.
sync_binlog = 1;
# 以前, mysql 的复制依赖磁盘文件来踪踪复制位置, 这意味着复制完成事务操作后, 还需要完成同
# 步写入磁盘操作. 如果事务提交和同步之间发生崩溃, 文件将可能包含错误的位置信息.
# 使用下面配置, 可将位置信息存在 innodb 表中, 以原子操作完成.
replay_log_info_repository = TABLE;
# 让副本在检测到崩溃时丢弃所在本地中继日志, 从源获取丢失的数据. 这确保在崩溃中发生的磁盘上
# 的损坏或不完整的中继日志是可恢复的. 配置这个参数后, 就无需再配置 sync_relay_log 了.
replay_log_recovery = ON;
有时会故意让副本有一些复制延迟: 假设意外的删了一个表, 从备份中恢复可能需要很久, 如果使用了延迟 副本, 则可以找到 DROP TABLE 对应的 GTID, 使副本复制到表被删除的之前的时间点, 这会大大减少修 复时间.
mysql “多线程复制” 的能力可以在副本端运行多个SQL 线程, 加快本地中继日志的应用. 开启多线程 复制由于需要使用协调线程, 会有一定开销. 把 replica_parallel_workers 设为非 0 值即可开启, 一般是 3-4 个之间, 超出此范围的线程很少被用到.
它有两种模式 (由 replica_parallel_type 设置):
“组提交” 即在 fsync() 之前等待一段时间让更多事务成批. 相关配置有 binlog_group_commit_sync_delay, binlog_group_commit_sync_no_delay_count.
启用 “半同步复制” 后, 源在完成每个事务提交时, 都需要确保事务至少被 rpl_semi_sync_source_ wait_for_replica_count 个副本所接收并将其写入了中继日志 (不一定应用到本地数据中). 注意, 如果在一定时间内没有副本确认事务, mysql 会恢复到标准的 “异步复制” 模式, 事务并不会失败. 即 半同步复制不是一种防止数据丢失的方法, 只是让你拥有更具弹性的故障切换.
这会为事务增加额外延迟, 需根据情况考虑是否开启. 考虑到异步复制的回退, 一般不会启用它.
可以让副本仅复制一部分数据 – 这个功能并没有想象中那么实用, 更重要的是 *_do_db*
和 *_ignore _db
并不像预期那样工作: 你可能认为它会根据数据库名过滤, 但它们是根据当前的默认数据库进行过 滤的. 不建议使用此功能.
计划内切换的一般步骤为:
计划外切换的一般步骤为:
还需注意, 旧源重新上线时要启用 super_read_only, 防止意外写入.
在能满足需求前提下, 建议尽可能保持复制拓扑的简单.
这种拓扑不用担心因复制延迟导致的读延迟. 但如果达到读扩展上限, 要么需要分片, 要么需要演进到更复 杂的拓扑.
应用必须对延迟读有一定的容忍度.
双源主动-主动:
双源主动-被动:
有副本的双源:
环形复制:
多源复制:
复制延迟: 用 SHOW REPLICA STATUS 的 Seconds_behind_source 显示了副本的延迟, 但不总是准 确的, 最好忽略它并使用心跳来检测, 如 Percona 的 heartbeat 脚本.
常见问题及解决方案:
“还原” 指从备份中获取数据并加载到 mysql. “恢复” 指当异常发生后对系统的拯救, 包括还原.
有两种主要类型的备份:
建议:
在线备份和离线备份 逻辑备份和裸文件备份 备分什么? 增量备份和差异备份
工具:
读池和分片.
Vitness 和 ProxySQL.