本篇记录的是 MySQL 数据库开发注意要点,是 [MySQL数据库开发的三十六条军规——石展@赶集]的学习笔记。
核心军规
-
尽量不在数据库做运算
- 复杂运算移到程序端CPU
- 尽可能简单应用MySQL
-
控制单表数据量
- 一年内的单表数据量预估
- 纯INT不超1000W
- 含CHAR不超500W
- 建议单库不超过300-400个表
- 一年内的单表数据量预估
-
保持表身段苗条
- 表字段数少而精
- IO高效
- 全表遍历
- 表修复快
- 提高并发
- alter table 快
- 单表1G体积 500W行评估
- 顺序读1G文件需N秒
- 单行不超过200Byte
- 单表不超50个纯INT字段
- 单表不超20个CHAR(10)字段
- 单表字段数上限控制在20~50个
- 表字段数少而精
-
平衡范式与冗余
- 严格遵循三大范式
- 效率优先、提升性能
- 没有绝对的对与错
- 适当时牺牲范式、加入冗余
- 但会增加代码复杂度
-
拒绝3B
- 大SQL (BIG SQL)
- 大事务 (BIG Transaction)
- 大批量 (BIG Batch)
-
核心军规小结
- 尽量不在数据库做运算
- 控制单表数据量
- 保持表身段苗条
- 平衡范式与冗余
- 拒绝3B
字段类军规
-
用好数值字段类型
- 三类数值类型:
-
TINYINT(1Byte)
-
SMALLINT(2B)
-
MEDIUMINT(3B)
-
INT(4B)、BIGINT(8B)
-
FLOAT(4B)、DOUBLE(8B)
-
DECIMAL(M,D)
-
- 三类数值类型:
-
将字符转化为数字
- 数字型VS字符串型索引
- 更高效
- 查询更快
- 占用空间更小
- 举例:用无符号INT存储IP,而非CHAR(15)
- INT UNSIGNED
- INET_ATON()
- INET_NTOA()
- 数字型VS字符串型索引
-
优先使用ENUM或SET
- 优先使用ENUM或SET
- 字符串
- 可能值已知且有限
- 存储
- ENUM占用1字节,转为数值运算
- SET视节点定,最多占用8字节
- 比较时需要加‘单引号(即使是数值)
- 举例
sex
enum(‘F’,‘M’) COMMENT ‘性别’c1
enum(‘0’,‘1’,‘2’,‘3’) COMMENT ‘职介审核’
- 优先使用ENUM或SET
-
避免使用NULL字段
- 避免使用NULL字段
- 很难进行查询优化
- NULL列加索引,需要额外空间
- 含NULL复合索引无效
- 举例
c
int(10) NOT NULL DEFAULT 0
- 避免使用NULL字段
-
少用并拆分TEXT/BLOB
- TEXT类型处理性能远低亍VARCHAR
- 强制生成硬盘临时表
- 浪费更多空间
- VARCHAR(65535)==>64K (注意UTF-8)
- 尽量不用TEXT/BLOB数据类型
- 若必须使用则拆分到单独的表
- 举例:
- TEXT类型处理性能远低亍VARCHAR
|
|
-
不在数据库里存图片
-
字段类军规小结
- 用好数值字段类型
- 将字符转化为数字
- 优先使用枚丼ENUM/SET
- 避免使用NULL字段
- 少用并拆分TEXT/BLOB
- 不在数据库里存图片
索引类军规
-
谨慎合理添加索引
- 谨慎合理添加索引
- 改善查询
- 减慢更新
- 索引不是越多越好
- 能不加的索引尽量不加
- 综合评估数据密度和数据分布
- 最好不超过字段数20%
- 谨慎合理添加索引
-
字符字段必须建前缀索引
- 区分度
- 单字母区分度:
26
- 4字母区分度:
26*26*26*26=456,976
- 5字母区分度:
26*26*26*26*26=11,881,376
- 6字母区分度:
26*26*26*26*26*26=308,915,776
- 单字母区分度:
- 字符字段必须建前缀索引
- 区分度
|
|
-
不在索引列做运算
- 不在索引列进行数学运算或凼数运算
- 无法使用索引
- 导致全表扫描
- 不在索引列进行数学运算或凼数运算
-
自增列或全局ID做INNODB主键
- 对主键建立聚簇索引
- 二级索引存储主键值
- 主键不应更新修改
- 按自增顺序插入值
- 忌用字符串做主键
- 聚簇索引分裂
- 推荐用独立于业务的AUTO_INCREMENT列或全局ID生成 器做代理主键
- 若不指定主键,InnoDB会用唯一且非空值索引代替
-
尽量不用外键
- 线上OLTP系统(线下系统另论)
- 外键可节省开发量
- 有额外开销
- 逐行操作
- 可‘到达’其它表,意味着锁
- 高并发时容易死锁
- 线上OLTP系统(线下系统另论)
-
索引类军规小结
- 谨慎合理添加索引
- 字符字段必须建前缀索引
- 不在索引列做运算
- 自增列或全局ID做INNODB主键
- 尽量不用外键
SQL 类军规
-
SQL语句尽可能简单
- 大SQL VS 多个简单SQL
- 传统设计思想
- BUT MySQL NOT
- 一条SQL叧能在一个CPU运算
- 5000+ QPS的高并发中,1秒大SQL意味着?
- 可能一条大SQL就把整个数据库堵死
- 拒绝大SQL,拆解成多条简单SQL
- 简单SQL缓存命中率更高
- 减少锁表时间,特别是MyISAM
- 用上多CPU
- 大SQL VS 多个简单SQL
-
保持事务(连接)短小
- 保持事务/DB连接短小精悍
- 事务/连接使用原则:即开即用,用完即关
- 不事务无关操作放到事务外面, 减少锁资源的占用
- 不破坏一致性前提下,使用多个短事务代替长事务
- 举例
- 发贴时的图片上传等待
- 大量的sleep连接
- 保持事务/DB连接短小精悍
-
尽可能避免使用SP/TRIG/FUNC
- 线上OLTP系统(线下库另论)
- 尽可能少用存储过程
- 尽可能少用触发器
- 减用使用MySQL凼数对结果进行处理
- 线上OLTP系统(线下库另论)
-
尽量不用 SELECT *
- 用SELECT * 时
- 更多消耗CPU、内存、IO、网络带宽
- 先向数据库请求所有列,然后丢掉丌需要列?
- 尽量不用SELECT * ,叧取需要数据列
- 更安全的设计:减少表变化带来的影响
- 为使用covering index提供可能性
- Select/JOIN减少硬盘临时表生成,特别是有TEXT/BLOB时
- 举例
- SELECT * FROM tag WHERE id = 999184
- ⬇️
- SELECT keyword FROM tag WHERE id = 999184
- 用SELECT * 时
-
改写OR为IN()
- 同一字段,将or改写为in()
- OR效率:O(n)
- IN 效率:O(Log n)
- 当n很大时,OR会慢很多
- 同一字段,将or改写为in()
-
注意控制IN的个数,建议n小于200
-
举例
|
|
- 改写OR为UNION
- 不同字段,将or改为union
- 减少对丌同字段进行 “or” 查询
- Merge index往往很弱智
- 如果有足够信心:
set global optimizer_switch='index_merge=off'
;
- 举例
- 不同字段,将or改为union
|
|
- 避免负向查询和% 前缀模糊查询
-
避免负向查询
- NOT、!=、<>、!<、!>、NOT EXISTS、NOT IN、 NOT LIKE等
-
避免 % 前缀模糊查询
- B+ Tree
- 使用不了索引
- 导致全表扫描
-
举例
-
|
|
-
减少COUNT(*)
- MyISAM VS INNODB
- 不带 WHERE COUNT()
- 带 WHERE COUNT()
- COUNT(*)的资源开销大,尽量不用或少用
- 计数统计
- 实时统计:用memcache,双向更新,凌晨跑基准
- 非实时统计:尽量用单独统计表,定期重算
- MyISAM VS INNODB
-
LIMIT高效分页
- 传统分页:
Select * from table limit 10000,10;
- LIMIT原理:
- Limit 10000,10
- 偏移量越大则越慢
- 推荐分页:
Select * from table WHERE id>=23423 limit 11;#10+1 (每页10条)
select * from table WHERE id>=23434 limit 11;
- 传统分页:
-
LIMIT的高效分页
- 分页方式二:
Select * from table WHERE id >= ( select id from table limit 10000,1 ) limit 10;
- 分页方式三:
SELECT * FROM table INNER JOIN (SELECT id FROM table LIMIT 10000,10) USING (id) ;
- 分页方式四:
- 程序取ID:
select id from table limit 10000,10
; Select * from table WHERE id in (123,456...)
;
- 程序取ID:
- 分页方式二:
示例:
|
|
- 用UNION ALL 而非 UNION
- 若无需对结果进行去重,则用UNION ALL
- UNION有去重开销
- 举例
- 若无需对结果进行去重,则用UNION ALL
|
|
- 分解联接保证高并发
- 高并发DB不建议进行两个表以上的JOIN
- 适当分解联接保证高并发
- 可缓存大量早期数据
- 使用了多个MyISAM表
- 对大表的小ID IN()
- 联接引用同一个表多次
- 举例
|
|
- GROUP BY 去除排序
- GROUP BY 实现
- 分组
- 自动排序
- 无需排序:Order by NULL
- 特定排序:Group by DESC/ASC
- 举例
- GROUP BY 实现
|
|
-
同数据类型的列值比较
- 原则:数字对数字,字符对字符
- 数值列不字符类型比较
- 同时转换为双精度
- 进行比对
- 字符列不数值类型比较
- 字符列整列转数值
- 不会使用索引查询
-
同数据类型的列值比较
- 举例:字符列不数值类型比较
|
|
-
Load data 导数据
- 批量数据快导入:
- 成批装载比单行装载更快,丌需要每次刷新缓存
- 无索引时装载比索引装载更快
- Insert values ,values,values 减少索引刷新
- Load data比insert快约20倍
- 尽量不用 INSERT … SELECT
- 延迟
- 同步出错
- 批量数据快导入:
-
打散大批量更新
- 大批量更新凌晨操作,避开高峰
- 凌晨不限制,白天上限默认为100条/秒(特殊再议)
- 举例:
|
|
- SQL类军规小结
- SQL语句尽可能简单
- 保持事务(连接)短小
- 尽可能避免使用SP/TRIG/FUNC
- 尽量不用 SELECT *
- 改写OR语句
- 避免负向查询和% 前缀模糊查询
- 减少COUNT(*)
- LIMIT的高效分页
- 用UNION ALL 而非 UNION
- 分解联接保证高并发
- GROUP BY 去除排序
- 同数据类型的列值比较
- Load data导数据
- 打散大批量更新
- Know Every SQL!
约定类军规
-
隔离线上线下
- 构建数据库的生态环境
- 开发无线上库操作权限
- 原则:线上连线上,线下连线下
- 实时数据用real库
- 模拟环境用sim库
- 测试用qa库
- 开发用dev库
- 构建数据库的生态环境
-
禁止未经DBA确认的子查询
- MySQL子查询
- 大部分情况优化较差
- 特别WHERE中使用IN id的子查询
- 一般可用JOIN改写
- MySQL子查询
-
永远不在程序端显式加锁
- 永远不在程序端对数据库显式加锁
- 外部锁对数据库不可控
- 高并发时是灾难
- 极难调试和排查
- 并发扣款等一致性问题
- 采用事务
- 相对值修改
- Commit前二次较验冲突
- 永远不在程序端对数据库显式加锁
-
统一字符集为UTF8
- 字符集:
- MySQL 4.1 以前叧有latin1
- 为多语言支持增加多字符集
- 也带来了N多问题
- 保持简单
-
统一命名规范
- 库表等名称统一用小写
- Linux VS Windows
- MySQL库表大小写敏感
- 字段名的大小写不敏感
- 索引命名默认为“idx_字段名”
- 库名用缩写,尽量在2~7个字母
- DataSharing ==> ds
- 注意避免用保留字命名
- 库表等名称统一用小写
-
约定类军规小结
- 隔离线上线下
- 禁止未经DBA确认的子查询上线
- 永远不在程序端显式加锁
- 统一字符集为UTF8
- 统一命名规范