MySQL/TDSQL 各个规格能支持的tps/qps
该结果是使用TPCC-C性能测试工具测出来的, 具体可能会和实际的有缩差别. 最好新业务上线进行压测.
MySQL的一个查询只能使用到1个cpu, 所以, 要想数据库性能达到比较优情况, 则cpu核数并发查询数有关(表现到数据库层面是活跃回话数 thread running和cpu的关系)
一般来说计算公式为: cpu核数 * 2 = 活跃会话数
一般情况下数据库的cpu保持在 15%~20%是相对安全的, 不能把数据库cpu打的太满, 如果打的太满的话, 在故障切换会有雪崩的情况。
当数据库挂掉, 切换, 这时候切换的目标实例会瞬时承担流量, 会有大量建立链接和数据库预热过程, 这时候数据导致数据库压力瞬间变大
也有可能目标库就承载了一些流量
表大小DDL时间
注意:
如果执行DDL的表的tps > 600的时候DDL会停止不动(这和DDL原理有关系)
DDL速度和每行数据大小有关, 如果每行的数据比较大, DDL数据也会有所下降
一个实例的DDL只能串行, 如: 一个实例中有10张表需要做DDL, 没张表数据量在1000w, 要完成10张表的变更需要10小时
分库分表
- 单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表
如果预计2年后的数据量根本达不到这个级别,请不要在创建表时就分库分表
- 不单单只分库, 还需要做到分表
主从复制是表级别的, 如果拆分到表, 能提高主从复制性能
如果只分库, 会导致数据库的数量增大, 从而导致不同对实例的连接数会成倍数增加, 最终导致链接数用满.
每行数据大小最好不要超过1k, 让一个数据页中存的行数越多越好
MySQL从磁盘获取数据是一页一页获取的, 如果一页中存的数据行数越多, 范围扫描的速度就越快
数据库发布
数据库发布 和 业务应用发布需要区分开. 数据库发布是有状态的, 不确定性高. 而且发布不是100%都能成功的, 特别对于大表, 和tps高的表, 数据库发布失败率很大, 而且会导致主从延时.
数据库发布至少提前一个发布周期进行, 如: 周四要发布应用上线, 在周二就需要将数据库发布执行了, 这样数据库发布如果有问题, 可以提前发现. 并且给应用发布提供足够的buffer
容量使用规范
仅供参考
项 | 健康上线值(不能操过这个值) |
---|---|
机器CPU | 20% |
机器写入iops | 1.5w, 600 |
机器io使用率 | 20% |
数据库活跃回话数 | 50 |
DDL规范
- 使用ALTER添加字段不能使用AFTER关键字, 默认字段都在表的最后, 并且对于同一个表的DDL操作放在一个语句中
- 反例
-- 使用了 after
ALTER TABLE yh_member
ADD name VARCHAR(20) NOT NULL DEFAULT '' COMMENT '姓名' AFTER id;
-- 同一个表DDL使用多条SQL
ALTER TABLE yh_user
ADD name VARCHAR(20) NOT NULL DEFAULT '' COMMENT '姓名';
ALTER TABLE yh_user
ADD is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0:否, 1:是';
ALTER TABLE yh_user
ADD INDEX idx_created_at(created_at);
- 正例
-- 不能使用AFTER
ALTER TABLE tbl_xxx
ADD name VARCHAR(20) NOT NULL DEFAULT '' COMMENT '姓名';
-- 对同一个表的DDL合并成一条SQL
ALTER TABLE yh_user
ADD name VARCHAR(20) NOT NULL DEFAULT '' COMMENT '姓名',
ADD is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除: 0:否, 1:是',
ADD INDEX idx_created_at(created_at);
- 库, 表字符集必须utf8mb4
在MySQL中只有utf8mb4才是真正的UTF-8, 只有utf8mb4才能支持类似emoji等4字节字符
- 字段允许适当冗余,以提高查询性能,但必须考虑数据一致。冗余字段应遵循:
不是频繁修改的字段
不是 varchar 超长字段,更不能是 text 字段
- 每个表必须要有id字段为主键, 在没有特殊要求的情况下id是bigint类型, 禁止使用int
- 方便对接外部系统,还有可能产生很多废数据
- 避免废弃数据对系统id的影响
1. 如果预期内表的数据量很大, 并且会有拆分的可能性id的值最好让程序生成. 不要使用MySQL中的自增
2.程序生成的id保证相对的自增, 这样能保证, 数据库的索引数据总在一部分是相对的热, 这块数据能驻留在内存, 查询就不需要再回磁盘
-
每个表必须要有is_deleted字段标记数据是否已经删除, 并且该字段需要有索引, 方便后期的一些维护性操作. is_deleted两个选项: 0:否, 1:是
-
每个表必须要有created_at和updated_at字段, 并且字段类型为DATETIME, 且值的生成应该让数据库自己生成, 业务程序不应该主动修改这两个字段的值. 如:
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
TIMESTAMP可以表达1970-2038年,而且TIMESTAMP需要4字节存储空间,而DATETIME需要5字节,存储1001-9999年 TIMESTAMP默认值, 会随着sql_mode参数不一样, 表现不同, 对程序的兼容性要求高.
- 时间字段格式统一存储成YYYY-MM-DD HH:MM:SS
- 表和字段必须要有清晰明确的注释, 为了后期更好的维护. 如:
CREATE TABLE user(
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增id',
name VARCHAR(20) NOT NULL DEFAULT '' COMMENT '用户名称',
.....
) COMMENT='用户表';
- 字段尽量设置为 NOT NULL, 为字段提供默认值. 如:
- 字符型的默认值为一个空字符值串
name VARCHAR(20) NOT NULL DEFAULT '' COMMENT '用户名称'
- 数值型默认值为数值 0
cnt DECIMAL(8, 2) NOT NULL DEFAULT 0 COMMENT '数量'
- 逻辑型的默认值为数值 0
is_deleted TINYINT NOT NULL DEFAULT '' COMMENT '是否删除: 0:否, 1:是'
NULL需要更多的存储空,无论是表还是索引中每行中的NULL的列都需要额外的空间来标识。NULL这种类型需要MySQL内部进行特殊处理,增加了数据库处理记录的复杂性,同等条件下,表中较多NULL字段会导致数据库处理性能下降.
- 小数类型为decimal,禁止使用 float 和 double
float 和 double 在存储的时候,存在精度损失的问题,很可能在值的比较时,得不到正确的结果。
- 用尽量少的存储空间来存数一个字段的数据, 不要使用char类型, 杜绝和核心出现大字段定义如: VARCHAR(2000), TEXT
- 能使用 tinyint/int/bigint的就不要使用 varchar
- 能使用 varchar(20) 的就不要 varchar(255)
- 能使用varchar(255)就不要使用varchar(256)
- 如果一个表有varchar(2000)需要把varchar(2000)变成text, 并且独立出另外一张表出来
- varchar(1)~varcahr(255)需而外1字节的存储位,
- varchar(256)~varchar(65535)需要额外2字节存储位
- 表的一行的数据定义大小不能超过16382个字符(varchar((65535-4)/4=16382)), text字段数据存储方式, 小部分存储在原表中, 大部分数据以指针的方式存储在另外空间.
- 合理将varchar或text作为json字符串使用, 并且json字符串是相对可解析的. 这样的字段不能出现在核心表中, 需要而外剥离开.
- 不要使用'null'字符串去表示空值
- 对于自动生成的schema不要太过信任,最好自己手动写
自动生成的schema, 会带很为而外的信息, 如: 字符集, after 等等
- 在设计阶段, 核心业务表所在的库, 需要和非核心表所在库需要区分开
- 需要做分库分表的, 在设计阶段就需要考虑到分布不均到问题, 尽量要让数据分布均匀.
命名规范
- 库, 表, 字段 命名需要 需见名知意, 并且需要有长度限制
- 库名: 不能超过15个字符
- 表名: 不能超过30个字符
- 字段:不能超过30个字符
- 表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只 出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。 因此,数据库名、表名、字段名,都不允许出现任何大写字母,避免节外生枝
- 正例:health_user, rdc_config, level3_name
- 反例:HealthUser, rdcConfig, level_3_name
- 表名不使用复数名词
- 正例: rdc_config, user_class, candy
- 反例: rdc_configs, user_classes, candies
说明:表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于DO类名也是单数形式,符合表达习惯。
- 库名与应用名称尽量一致。如: health
- 表的命名最好是加上业务名称_表的作用
正例: health_user, trade_config - 禁用保留字,如:desc、range、match、delayed 等,请参考 MySQL 官方保留字
MySQL关键字官网: https://dev.mysql.com/doc/refman/8.0/en/keywords.html
- 索引命名规则:
- 主键: 使用默认的名字PRIMARY KEY就行
- 唯一索引: uk_字段名
- 普通索引: idx_字段名
e.g:
CREATE TABLE `emp` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增id',
`emp_no` int NOT NULL COMMENT '员工编号',
`first_name` varchar(14) NOT NULL COMMENT '第一名',
...
PRIMARY KEY (`id`),
UNIQUE INDEX uk_emp_no(`emp_no`),
INDEX idx_first_name(`first_name`)
) COMMENT='员工表';
说明: uk_ 即 unique key; idx_ 即 index 的简称
- 所有命名必须使用全名,有默认约定的除外,如果超过 30 个字符,使用缩写,请尽量名字易懂简短,如:information -> info; address -> addr; connection -> conn 等
- 表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 tinyint ( 1表示是,0表示否)
- 正例: 表达逻辑删除的字段名 is_deleted, 1表示删除,0表示未删除
10 .如果修改字段含义或对字段表示的状态追加时,需要及时更新字段注释
索引规范
- 索引个数最好不要超过6个
- 组合索引字段最好不好超过3个, 读多的情况下如果3个字段索引能唯一定位一条数据, 就可以创建唯一索引
不要以为唯一索引影响了insert速度,这个速度损耗可以忽略,但提高查找速度是明显的; 另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必 然有脏数据产生
- 超过三个表禁止 join. 需要 join 的字段, 数据类型必须绝对一致. 多表关联查询时, 保证被关联的字段需要有索引. 如:
SELECT *
FROM tbl_a AS a
LEFT JOIN tbl_b AS b
ON a.member_id = b.member_id
WHERE a.name = 'xxx'
上面查询语句中关联条件ON a.member_id = b.member_id, 其中b.member_id需要有索引.
- 组合索引区分度高的放在最左边区分度最高的放左边,能够在一开始过滤掉很多无用数据,提高索引的效率。需要注意的是各个条件的顺序尽量和索引的顺序一致, 如下示例:
CREATE TABLE rdc_config (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增id',
rule_name VARCHAR(50) NOT NULL COMMENT '规则名称',
wid BIGINT NOT NULL COMMENT '仓库id',
goods_category_id BIGINT NOT NULL COMMENT '商品类别id'
...
PRIMARY KEY(id),
...
) COMMENT='物流中心配置表'
- 假设rdc_config表有1000w数据
- 字段rule_name的区分度是300w
- 字段wid的区分度为20w
- 字段goods_category_id 的区分度为50w
查看字段区分度方法: SELECT COUNT(DISTINCT rule_name)
错误索引
idx_rule_name_wid_gcid(rule_name, wid, goods_category_id)
idx_rule_wid_name_gcid(wid, rule_name, goods_category_id)
idx_rule_gcid_wid_name(goods_category_id, wid, rule_name)
正确索引
idx_rule_name_gcid_wid(rule_name, goods_category_id, wid)
-
不能使用外键约束
-
created_at,updated_at 字段必须有索引(为了避免索引归档还需要再次做DDL添加索引)
-
在varchar字段建索引时, 尽可能的指定长度, 没必要对全字段都建立索引, 根据实际的前缀的区分度长度建立索引即可
索引的长度与区分度是一对矛盾体, 一般对字符串类型数据, 长度为20的索引, 区分度会高达90%以上, 可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度来确定
例子:
-- 表结构
CREATE TABLE rdc_config (
...
rule_name VARCHAR(500) NOT NULL DEFAULT '' COMMENT '规则名称',
...
) COMMENT='物流中心配置表'
创建索引
ALTER TABLE rdc_config ADD INDEX idx_rule_name(rule_name(20))
查询区分度
SELECT COUNT(DISTINCT LEFT(rule_name, 20)) / COUNT(*) FROM rdc_config
- 如果有order by的场景, 请注意利用索引的有序性. order by 最后的字段是组合索引的一部分, 并且放在索引组合顺序的最后, 避免出现 file_sort 的情况, 影响查询性能.
错误示例:
索引中有范围查找, 则索引有序性无法利用
-- WHERE 条件
WHERE a>10 ORDER BY b
-- 创建索引(排序无法使用到索引)
INDEX idx_xxx(a, b)
正确示例:
-- WHERE条件
WHERE a=? AND b=? ORDER BY c
-- 需要创建的索引
INDEX idx_xxx(a, b, c)
- 利用覆盖索引来进行查询操作, 避免回表.
在只需要获取少数量字段的数据时, 可以考虑使用索引覆盖.
能够建立索引的种类: 主键索引, 唯一索引, 普通索引, 而覆盖索引是一种查询的效果,用explain的结果,extra列会出现:using index
- 利用延迟关联或者子查询优化超多分页场景
错误示例:
SELECT * FROM tbl LIMIT 0, 1000;
SELECT * FROM tbl LIMIT 1000, 1000;
...
SELECT * FROM tbl LIMIT 10000000, 1000;
正确示例:
-- 示例1 (建议)
SELECT * FROM tbl WHERE id > 0 LIMIT 1000;
SELECT * FROM tbl WHERE id > 1000 LIMIT 1000;
...
SELECT * FROM tbl WHERE id > 1000000 LIMIT 1000;
-- 示例2 (性能没1好)
SELECT a.*
FROM tbl AS a, (
SELECT id
FROM tbl
WHERE col_01 = 'xxx'
LIMIT 100000, 20
) AS b
WHERE a.id = b.id
MySQL并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写
- SQL性能优化的目标: 至少要达到range级别, 要求是ref级别, 如果可以是const最好.
EXPLAIN SELECT * FROM tbl WHERE c='xxx';
----+-------------+-------+-------+- ...
| id | select_type | table | type | ...
+----+-------------+-------+-------+ ...
| 1 | SIMPLE | S | range | ...
+----+-------------+-------+-------+ ...
- const: 单表中最多只有一个匹配行(主键或者唯一索引), 在优化阶段即可读取到数据
- ref: 指的是使用普通的索引(normal index)
- range: 对索引进行范围检索
- index: 全索引扫描
- all: 全表扫描
性能顺序(好->差): const -> ref -> range -> index -> all
- 防止因字段类型不同造成的隐式转换, 导致索引失效.
如: 字段code类型为:VARCHAR(20), 字段值为: '1239909238098'
错误示例: WHERE code = 1239909238098
正确示例: WHERE code = '1239909238098'
sql使用规范
- 不要使用 count(列名)或 count(常量)来替代 count(), count()是SQL92 定义的标准统计行数的语法, 跟数据库无关, 跟NULL和非NULL无关
错误示例:
SELECT COUNT(name) FROM tbl;
SELECT COUNT(1) FROM tbl;
正确示例:
SELECT COUNT(*) FROM tbl;
count(*)会统计包括值为NULL的行, 而count(name), count(1)不会统计该列为NULL值的行。 也可以变通的实现count效果,即查询数据到应用系统中,然后计算count.
- 在使用聚合函数的时候, 需要注意NULL值. 当字段值有NULL聚合函数的值也是NULL.
假设字段 money 字段中有NULL值
错误示例:
SELECT SUM(money), AVG(money) FROM tbl;
+------------+------------+
| SUM(money) | AVG(money) |
+------------+------------+
| NULL | NULL |
+------------+------------+
1 row in set (0.03 sec)
建议示例:
SELECT IF(ISNULL(SUM(money)), 0, SUM(money)),
IF(ISNULL(AVG(money)), 0, AVG(money))
FROM tbl;
+---------------------------------------+---------------------------------------+
| IF(ISNULL(SUM(money)), 0, SUM(money)) | IF(ISNULL(AVG(money)), 0, AVG(money)) |
+---------------------------------------+---------------------------------------+
| 1 | 1.0000 |
+---------------------------------------+---------------------------------------+
1 row in set (0.02 sec)
MySQL 8.0不存在这种情况
- 使用IS NULL来判断值为NULL, 使用IS NOT NULL来判断值不为NULL
错误示例:
SELECT * FROM tbl WHERE name = NULL;
SELECT * FROM tbl WHERE name <> NULL;
正确示例:
SELECT * FROM tbl WHERE name IS NULL;
SELECT * FROM tbl WHERE name IS NOT NULL;
- NULL <> NULL的返回结果是NULL,而不是false。
- NULL = NULL的返回结果是NULL,而不是true。
- NULL <> 1的返回结果是NULL,而不是true。
- 程序链接默认使用auto_commit=1, 并且单独select查询不需要开启事物
错误示例:
BEGIN;SELECT * FROM tbl;COMMIT;
正确示例:
链接打开 auto_commit=1;
SELECT * FROM tbl;
- 拒绝使用大事务, 大批量.
错误示例:
-- 大批量insert
INSERT INTO tbl VALUES(), (), (), ... ();
-- 没有索引全表update
UPDATE tbl set age = 10;
正确示例:
-- 批量拆分成单条
INSERT INTO tbl VALUES();
INSERT INTO tbl VALUES();
INSERT INTO tbl VALUES();
...
INSERT INTO tbl VALUES();
-- 通过区分度高的 索引/主键更新数据
UPDATE tbl set age = 10 WHERE id = 1;
1.大事务, 大批量, 会有长时间的锁
2.容易出现主从延时
3.sql尽可能简单(一条sql只能在一个cpu运算,大语句拆小语句,减少锁时间,一条大sql可以堵死整个库)
6.条件过滤字段不允许使用函数包裹
错误示例:
SELECT * FROM tbl WHERE SUBSTRING(created_at, 9) >= '2021-11-11';
正确示例:
SELECT * FROM tbl WHERE created_at >= '2021-11-11 00:00:00'
MySQL不支持函数索引, 使用函数, 索引就失效了
- 页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决
错误示例:
SELECT * FROM tbl WHERE name LIKE '%xxx%';
SELECT * FROM tbl WHERE name LIKE '%xxx';
SELECT * FROM tbl WHERE name REGEXP '^[aeiou]|ok$';
正确示例
在非要使用模糊匹配的情况下, 需要使用前缀模糊匹配
SELECT * FROM tbl WHERE name LIKE 'xxx%';
索引文件具有 B-Tree 的最左前缀匹配特性, 如果左边的值未确定, 那么无法使用此索引
- 不允许使用自定义方法和触发器
1.触发器和自定义方法, 不可控, 难以维护. 相关业务逻辑应该通过应用代码来解决. 不应该使用自定义方法和触发器来解决
2.自定义方法和触发器不主从复制. 当主从切换容易带来灾难性事故
- 不使用 select * (消耗cpu, io, 内存, 带宽, 这种程序不具有扩展性)
错误用法:
SELECT * FORM tbl WHERE id = 1;
正确用法:
SELECT id, name, age FROM tbl WHERE id = 1
- 条件不应该使用OR, OR使用不到索引
- OR 改写成IN. (OR的效率是n级别,IN的效率是log(n)级别) 如:
错误示例:
SELECT id FROM t WHERE id = 1 OR id = 2 OR id = 3
正确示例:
SELECT id FROM t WHERE id IN(1, 2, 3);
IN里面最好不要超过50个
- OR 改写成 UNION ALL(为了更好的使用索引), 如:
错误示例:
SELECT id FROM t WHERE phone = '159' OR name = 'john';
正确示例:
SELECT id FROM t WHERE phone = '159'
UNION ALL
SELECT id FROM t WHERE name = 'john'
1.UNION ALL最好不要超过5个
2.不要使用UNION, UNION ALL 不会去重复
- sql 索引扫描行数不能超过200(explain 扫描行数不能超过200)
范围扫描物理页数尽量不要超过10个页, MySQL1个页16kb,其中只有15k用于存储数据, 假设没行数据大小800B, 则10个页大概就是200行数据
-
一次性获取数据, 尽量不要超过20条记录(最好在数据库扫描一个page就能返回所有需要的数据)
-
UPDATE, REPLACE INTO, INSERT IGNORE INTO 不能同时并发操作同一行记录, 更新需要打散.
同时并发更新同一行数据会导致锁等待增加, 导致链接hang住, 从而导致应用链接用尽, 触发瞬间创建大量链接, 导致MySQL cpu瞬间增高, 导致MySQL活跃会话数增高, 导致MySQL处理能力下降.
- 不得使用外键与级联,一切外键概念必须在应用层解决
1.以学生和成绩的关系为例, 学生表中的 student_id 是主键, 那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id, 同时触发成绩表中的 student_id 更新, 即为级联更新
2.外键与级联更新适用于单机低并发, 不适合分布式, 高并发集群; 级联更新是强阻塞, 存在数据库更新风暴的风险; 外键影响数据库的插入速度
- 修正数据必须明确修正的数据并且需要明确的知道影响行数, 避免一次性修正多行
1.必须先将数据SELECT出来, 再通过主键/唯一键进行修正
2.修正的值必须是明确的, 不能在修正的字段上进行计算, 修正sql是幂等的
错误示例:
-- 一次性批量修正
UPDATE tbl SET money = 1 WHERE created_at >= '2022-01-01 00:00:00'
-- 修正数据不能幂等操作
UPDATE tbl SET money = money + 1 WHERE id = 1
正确示例:
-- 先select, 在update, 并且使用幂等操作
SELECT id FROM tbl WHERE created_at >= '2022-01-01 00:00:00';
UPDATE tbl SET money = 1 WHERE id = 1;
UPDATE tbl SET money = 1 WHERE id = 2;
...
UPDATE tbl SET money = 1 WHERE id = 100;
- 如果需要计算字符串的字符长度使用CHARACTER_LENGTH而不是LENGTH
SELECT LENGTH('中国');
+------------------+
| LENGTH('中国') |
+------------------+
| 6 |
+------------------+
1 row in set (0.01 sec)
SELECT CHARACTER_LENGTH('中国');
+----------------------------+
| CHARACTER_LENGTH('中国') |
+----------------------------+
| 2 |
+----------------------------+
1 row in set (0.00 sec)
17.大表中清空表不能使用DELETE语句, 应该联系DBA在非高峰期进行操作.
以上是公司中多个dba根据多年经验总结而成,本人将与公司有关的敏感信息去掉后,将公共的部分共享出来,希望大家的系统越来越稳定。
- sql语句中 order by id 会造成全表扫描,可以通过order by (id + 0) 让id列参与计算,失效索引,这样就会让优化器找到适合的索引.
番外:
表数据量太大造成的问题
1. ddl成本高,主从复制也很困难
2. 查询需要走索引
大表解决方案:
1. 归档
2. 尽量减少表的使用
网友评论