数据库三大范式是否遵守

  1. 所有列应该不可再分
  2. 必须存在业务主键,且非主键字段应该依赖于全部业务主键
  3. 非主键列不能依赖除主键列以外的其他列

    总结

  • 每一列字段应该不可再分,职责单一
  • 要有主键,最好是与业务无关的自增id
  • 不要有冗余字段,为了避免数据更新不一致,应该拆成两张表,用(逻辑)外键关联
  • 但不论范式化设计还是反范式化设计,都不能过度:
  • 完全遵守第三范式,有时会让查询变得非常麻烦,要么JOIN,要么内存中排序,有时甚至单表根本无法完成
  • 打破第三范式后,需要主动维护冗余数据,一不小心可能导致数据不一致

    数据类型,字段类型怎么选

    整数类型

    1. 如果业务允许,尽量设置unsigned,去除符号,范围翻倍
    2. int(11)里的11和占用字节大小无关
    3. 注意各个类型的选取标准
    4. 定长,没有国家/地区之分,没有字符集的差异 | 数据类型 | 占据空间 | 范围(有符号) | 范围(无符号) | 描述 | | —- | —- | —- | —- | —- | | tinyint | 1 个字节 | -2^7 ~ 2^7-1 | 0 - 255 | 小整数值 | | smallint | 2 个字节 | -2^15 ~ 2^15-1 | 0 - 65535 | 大整数值 | | mediumint | 3 个字节 | -2^23 ~ 2^23-1 | 0 - 16777215 | 大整数值 | | int | 4 个字节 | -2^31 ~ 2^31-1 | 0 - 4294967295 | 大整数值 | | bigint | 8 个字节 | -2^63 ~ 2^63-1 | 0 - 18446744073709551615 | 极大整数值 |

字符类型

括号里面的数字是多少,就代表是几个字符

mysql5.0以后,代表几个字符
而字符和字节的换算,则与编码方式有关,不同的字符所占的字节是不同的。:

  • ASCII码:
    一个英文字母(不分大小写)占一个字节的空间,一个中文汉字占两个字节的空间。一个二进制数字序列,在计算机中作为一个数字单元,一般为8位二进制数,换算为十进制。最小值0,最大值255。如一个ASCII码就是一个字节。
  • UTF-8编码:
    一个英文字符等于一个字节,一个中文(含繁体)等于三个字节。
  • Unicode编码:
    一个英文等于两个字节,一个中文(含繁体)等于两个字节。

  1. 字符类型的数字和实际大小有关,准确地说这里的数值和实际存储大小的上限有关。比如char(3),表示会固定占用3个字符空间,即使存储的值不够3个字符,照样会占着那块空间,但不能超过3个字符:
  2. char->定长
    1. 读写效率高于varchar,适合存储长度固定、频繁读写的数据
    2. 定长, 考虑字符集和(排序)校对集
  3. vachar->变长
    1. 通过varchar(10)的方式指定上限,适合存储长度波动、更新不频繁的数据
    2. varchar则需要浪费1~2个字节来存储当前值的实际长度,且更新会导致重新计算
    3. 不定长 要考虑字符集的转换与排序时的校对集,速度慢
  4. text/blob
    1. 无法使用内存临时表

      小数类型

      | 数据类型 | 占据空间 | 是够精确 | | —- | —- | —- | | float | 4个字节 | 非精确 | | double | 8 个字节 | 非精确 | | decimal | 每4个字节存9个数字,小数点占一个字节 | 精确 |

时间类型

数据类型 占据空间 取值范围
date 3个字节 1000-01-01 ~ 9999-12-31
time 3~6个字节 -838:59:59 ~ 838:59:59
datetime 5~8个字节 1000-01-01 00:00:00 ~ 9999-12-31 23:59:59
timestamp 4~7个字节 1970-01-01 00:00:01 ~ 2038-01-19 03:14:07

关于mysql存储时间的大佬建议
All date and time columns shall be INT UNSIGNED NOT NULL, and shall store a Unix timestamp in UTC.

类型总结

优先级

整型 > date,time > enum,char>varchar > blob
用的少的:
time定长,运算快,节省空间. 考虑时区,写sql时不方便 where > ‘2005-10-12’;
enum: 能起来约束值的目的, 内部用整型来存储,但与char联查时,内部要经历串与值的转化

尽量避免用NULL

原因: NULL不利于索引,要用特殊的字节来标注.
在磁盘上占据的空间其实更大

够用就行,不要慷慨

(如smallint,varchar(N)),大的字段浪费内存,影响速度

表设计

定长与变长分离

  1. 核心表存放int char这种定长的
  2. varchar, text,blob,这种变长字段,适合单放一张表, 用主键与核心表关联起来

    常用字段和不常用字段要分离

    分析字段的查询场景,查询频度低的字段,单拆出来

    合理添加冗余字段

    主要用的索引

    索引类型

    normal:普通索引
    unique:唯一索引
    FullText:全文检索,一般不用

    索引实现方式

    99%用btree

    特别注意,这里虽然写的是”BTREE”,但MySQL确实使用的是B+Tree

    几种错误用法

  3. 在where条件常用的列上都加上索引(正确做法,联合索引)

  4. 在多列上建立索引后,查询哪个列,索引都将发挥作用(正确做法:左前缀原则)

    索引题目

    假设某个表有一个联合索引(c1,c2,c3,c4)一下——只能使用该联合索引的c1,c2,c3部分
    A where c1=x and c2=x and c4>x and c3=x
    B where c1=x and c2=x and c4=x order by c3
    C where c1=x and c4= x group by c3,c2
    D where c1=x and c5=x order by c2,c3
    E where c1=x and c2=x and c5=? order by c2,c3
    分析
    image.png

    1%用Hash索引

  5. 理论查询时间复杂度为O(1)

  6. hash函数计算后的结果,是随机的,如果是在磁盘上放置数据
  7. 比主键为id为例, 那么随着id的增长, id对应的行,在磁盘上随机放置
  8. 不法对范围查询进行优化
  9. 无法利用前缀索引
    1. 比如在btree中, field列的值“helloworld”,并加索引查询xx=helloword,自然可以利用索引, xx=hello,也可以利用索引(左前缀索引)
    2. hash(‘helloword’),和hash(‘hello’),两者的关系仍为随机
  10. 排序也无法优化
  11. 必须回行.就是说 通过索引拿到数据位置,必须回到表中取数据

hash索引的优劣势

  • 优势:速度非常快,只需一次计算即可得到地址,时间复杂度O(1),而B+树是O(logn)
  • 劣势:不支持模糊查询、范围查询、排序(理由相同)

    聚簇索引与非聚簇索引

    b+tree的实现细节不同!

    聚簇索引

    innoDB采用聚簇索引
    主键索引 既存储索引值,又在叶子中存储行的数据
    如果没有主键, 则会Unique key做主键
    如果没有unique,则系统生成一个内部的rowid做主键.
    像innodb中,主键的索引结构中,既存储了主键值,又存储了行数据,这种结构称为聚簇索引
    辅助索引只存储索引列+主键,必要时进行“回表”操作
    优势: 根据主键查询条目比较少时,不用回行(数据就在主键节点下)
    劣势: 如果碰到不规则数据插入时,造成频繁的页分裂
    如果插入的时候是乱序的,查询出来是有序的!为什么呢 主索引是有序的,索引上面就带着信息
    可以理解成插入难,查询简单 sql优化笔记 - 图2

    非聚簇索引

    myisam采用非聚簇索引
    索引树互相不关联
    索引指向行在磁盘的位置
    如果是乱序插入的,查询出来还是乱序的
    image.png
    如果把引擎改成InnoDB,自动就变成有序的了
    image.png
    可以理解成插入简单,查询难

    总结

  • MyISAM:非聚簇索引,需要回表

  • InnoDB:
    • 聚簇索引:主键索引,叶子节点是表数据,不需要回表
    • 非聚簇索引:辅助索引(唯一索引、普通索引),叶子节点是主键,必要时需要根据主键回表查询

image.png
image.png
在固定硬盘的情况下,页分裂不明显

索引覆盖

索引覆盖是指如果查询的列恰好是索引的一部分,那么查询只需要在索引文件上进行,不需要回行到磁盘再找数据.这种查询速度非常快

面试题为什么有索引了还是慢

题目:一个表的id和ver加了索引,还有其他几个很大的字段
执行查询 select id from t8 order by id;很慢
执行查询 select id from t8 order by id,ver;很快
代码和sql模拟:
image.png
面试结论:
innoDB如果一行有很大的字段,哪怕加了索引查也很慢,这时候用联合索引和索引覆盖可以避开
当然最好还是不要把大的字段放在一个表里面
myisam如果有主键索引,根据主键排序和查询的话 可以无视其他很大的字段,因为索引和数据分开的(非聚簇)

理想的索引

  1. 查询频繁
  2. 区分度高
    1. 针对列中的值,从左往右截取部分,来建索引

截的越短, 重复度越高,区分度越小, 索引效果越不好
截的越长, 重复度越低,区分度越高, 索引效果越好,但带来的影响也越大—增删改变慢,并间影响查询速度.

  1. 长度小
    1. 索引长度直接影响索引文件的大小,影响增删改的速度,并间接影响查询速度(占用内存多).
  2. 尽量覆盖常用字段

    奇怪的索引

    crc32 伪哈希索引

    倒序索引(网站url)

    多列索引原则

    要根据业务原则和日志积累,
    可以有适当的冗余索引,a&b b&a 但是不要有重复索引 a&b&c a&b

    修复表

    相当于磁盘碎片整理
    比如: 表的引擎为innodb , 可以 alter table xxx engine innodb
    optimize table 表名 ,也可以修复.

    注意: 修复表的数据及索引碎片,就会把所有的数据文件重新整理一遍,使之对齐.
    这个过程,如果表的行数比较大,也是非常耗费资源的操作.
    所以,不能频繁的修复.

    如果表的Update操作很频率,可以按周/月,来修复.
    如果不频繁,可以更长的周期来做修复.

    sql优化

    耗时部分:
    查 ——> 沿着索引查,甚至全表扫描
    取 ——> 查到行后,把数据取出来(sending data)
    具体例子
    categoryId和其下的商品
    如果用left join 就会出现多次catId
    可以分成2次查询,有的规定就是禁止多次join
    sql语句的优化思路?
    答: 不查, 通过业务逻辑来计算,
    比如论坛的注册会员数,我们可以根据前3个月统计的每天注册数, 用程序来估算.

    少查, 尽量精准数据,少取行. 我们观察新闻网站,评论内容等,一般一次性取列表 10-30条左右.

    必须要查,尽量走在索引上查询行.

    取时, 取尽量少的列.

    explain详解

    大致有几个属性比较重要
    image.png

    id

    代表select 语句的编号, 如果是连接查询,表之间是平等关系, select 编号都是1,从1开始. 如果某select中有子查询,则编号递增
    image.png

    key

    最终用的索引.

    key_len

    使用的索引的最大长度

    type

    是指查询的方式, 非常重要,是分析”查数据过程”的重要依据
    可能的值

  3. all: 意味着从表的第1行,往后,逐行做全表扫描.,运气不好扫描到最后一行

    1. 示例 无索引 where name=’张三’
  4. index: 比all性能稍好一点,
    1. 通俗的说: all 扫描所有的数据行,相当于data_all index 扫描所有的索引节点,相当于index_all
    2. 示例1:查询的列 索引覆盖了,但是where条件有函数计算
    3. 示例2:查询的列 索引覆盖了,用索引排序了,但是取出全部
  5. range: 意思是查询时,能根据索引做范围的扫描
    1. where id>10086
  6. ref通过索引列,可以直接引用到某些数据行
    1. where cid=10
  7. const, system, null这3个分别指查询优化到常量级别,甚至不需要查找时间.
    1. 一般按照主键来查询时,易出现const,system
    2. 或者直接查询某个表达式,不经过表时, 出现NULL

      ref

      指连接查询时, 表之间的字段引用关系

      rows

      是指估计要扫描多少行

      extra

      index: 是指用到了索引覆盖,效率非常高
      using where 是指光靠索引定位不了,还得where判断一下
      using temporary 是指用上了临时表, group by 与order by 不同列时,或group by ,order by 别的表的列.
      using filesort : 文件排序(文件可能在磁盘,也可能在内存), 有个命令可以看在磁盘用了文件排序(炸了)text等大字段

      in 型子查询引出的陷阱

      在ecshop商城表中,查询6号栏目的商品, (注,6号是一个大栏目)
      最直观的: mysql> select goods_id,cat_id,goods_name from goods where cat_id in (select
      cat_id from ecs_category where parent_id=6);
      误区: 给我们的感觉是, 先查到内层的6号栏目的子栏目,如7,8,9,11
      然后外层, cat_id in (7,8,9,11)

      事实: 如下图, goods表全扫描, 并逐行与category表对照,看parent_id=6是否成立
      image.png

      原因: mysql的查询优化器,针对In型做优化,被改成了exists的执行效果.
      当goods表越大时, 查询速度越慢.

      改进: 用连接查询来代替子查询
      explain select goods_id,g.cat_id,g.goods_name from goods as g
      inner join (select cat_id from ecs_category where parent_id=6) as t
      using(cat_id) \G

      非常用的技巧

      exist子查询

      跟in的类似理解

      from子查询

      注意::内层from语句查到的临时表, 是没有索引的.
      所以: from的返回内容要尽量少.

      强制使用索引

      image.png

      count() 优化

      误区:
      1:myisam的count()非常快
      答: 是比较快,.但仅限于查询表的”所有行”比较快, 因为Myisam对行数进行了存储.
      一旦有条件的查询, 速度就不再快了.尤其是where条件的列上没有索引.

      2: 假如,id<100的商家都是我们内部测试的,我们想查查真实的商家有多少?
      select count() from lx_com where id>=100; (1000多万行用了6.X秒)
      小技巧:
      select count(
      ) from lx_com; 快
      select count() from lx_com where id<100; 快
      select count(
      ) frol lx_com -select count() from lx_com where id<100; 快
      select (select count(
      ) from lx_com) - (select count(*) from lx_com where id<100)

group by

注意:
1:分组用于统计,而不用于筛选数据.
比如: 统计平均分,最高分,适合, 但用于筛选重复数据,则不适合.
以及用索引来避免临时表和文件排序

2: 以A,B表连接为例 ,主要查询A表的列,
那么 group by ,order by 的列尽量相同,而且列应该显示声明为A的列

union优化

总是产生临时表
注意: union all 不过滤 效率提高,如非必须,请用union all
因为 union去重的代价非常高, 放在程序里去重.

limit优化

limit offset,N, 当offset非常大时, 效率极低,
原因是mysql并不是跳过offset行,然后单取N行,
而是取offset+N行,返回放弃前offset行,返回N行.
效率较低,当offset越大时,效率越低

优化办法:
1: 从业务上去解决
办法: 不允许翻过100页
以百度为例,一般翻页到70页左右.

1:不用offset,用条件查询.
例:
mysql> select id,name from lx_com limit 5000000,10;
+————-+——————————————————————+
| id | name |
+————-+——————————————————————+
| 5554609 | 温泉县人民政府供暖中心 |
………………
| 5554618 | 温泉县邮政鸿盛公司 |
+————-+——————————————————————+
10 rows in set (5.33 sec)

mysql> select id,name from lx_com where id>5000000 limit 10;
+————-+————————————————————————————+
| id | name |
+————-+————————————————————————————+
| 5000001 | 南宁市嘉氏百货有限责任公司 |
……………..
| 5000002 | 南宁市友达电线电缆有限公司 |
+————-+————————————————————————————+
10 rows in set (0.00 sec)

问题: 2次的结果不一致
原因: 数据被物理删除过,有空洞.
解决: 数据不进行物理删除(可以逻辑删除).

最终在页面上显示数据时,逻辑删除的条目不显示即可.
(一般来说,大网站的数据都是不物理删除的,只做逻辑删除 ,比如 is_delete=1)

3: 非要物理删除,还要用offset精确查询,还不限制用户分页,怎么办?
分析: 优化思路是 不查,少查,查索引,少取.
我们现在必须要查,则只查索引,不查数据,得到id.
再用id去查具体条目. 这种技巧就是延迟索引.
mysql> select id,name from lx_com inner join (select id from lx_com limit 5000000,10) as tmp using(id);