Mysql 你不知道的 Limit
表
表结构
表名: restaurant
备注: 店铺信息表
字段名 | 字段类型 | 是否为null | 默认值 | 是否索引 | 索引名 | 备注 | |
---|---|---|---|---|---|---|---|
id | bigint(20) | no | 自增 | yes | id_UNIQUE | 自增主键 | |
saturn_city_id | int(11) | no | 0 | yes | ix_saturn_city_id | 城市id | |
is_valid | tinyint(4) | no | 0 | no | 是否有效 | ||
created_at | timestamp(3) | no | CURRENT_TIMESTAMP(3) | yes | ix_created_at | 创建时间 | |
updated_at | timestamp(3) | no | CURRENT_TIMESTAMP(3) | yes | ix_updated_at | 更新时间 | |
deleted_at | timestamp(3) | no | CURRENT_TIMESTAMP(3) | yes | ix_deleted_at | 删除时间 |
为什么 is_valid 没有索引
首先,不单单好似is_valid, 还有些类似 data_status 等表示枚举的字段,都没有必要增加索引, 因为这种字段的值往往屈指可数,加上索引之后一个只值仍然会对应很多条记录(这里所说的很多条一般都是10w条以上), 所以设置索引的效果并不明显。
还有 这种字段 特别是表示 记录/任务 状态的字段, 值最好不要以0,1这样数值,特别是0, 0 最好只代表 无效/初始 数据(因为0一般都是数据库的默认值),如果0这个值表示比较复杂的业务, 那么以后就会感觉很乱, 更要注意的是,不要用0表示 有效 营业 展示 这种带有true的意义。
物理删除 / 逻辑删除
db的表最好不设置物理删除,只做逻辑删除,这样数据就算业务上没有了价值,也可以分析出别的,具有商业价值的信息出来。 而且可以做业务回滚, 毕竟物理删除就真的是把数据从这个世界上删除了, 而数据对这个世界的重要性,已经不言而喻了。 如果数据过多, 可以选择归档(归档也只是把数据从线上表中转移到其他表中, 放置起来)。
操作记录
如果是业务表, 比如店铺基本数据, 商品基本数据等, 这种对操作及其敏感的数据,一定要做操作记录, 对 谁(who)什么时候(when) 在哪里(where) 做了什么(what) 都要记录下来,就算记录下来的数据爆炸,也要记录,可以把归档频率调高,比如一周一次归档,三天一次归档等。 这种数据是非常非常有价值的(如果有人恶意操作, 我们可以在业务上进行限制, 比如对商品的操作一天不能超过100次等)
业务
查询所有
1.如果想要查询某个城市下有效的店铺 ,可以使用 select * from restaurant where saturn_city_id = xxx and is_valid = 1
上面那个SQL由于Where条件里面只有 saturn_city_id
和 is_valid
两个字段, 且只有 saturn_city_id
有索引, 所以肯定走 satrun_city_id
这个索引
真的没有坑么
我们再看下上面的sql select * from restaurant where saturn_city_id = xxx and is_valid = 1
, 难道这样就足够了吗? 当然不是, 因为查询结构可能有上万甚至上十万条, 甚至可能造成OOM(如果数据量足够大的话)。 而且当 restaurant 表中一行纪录的数据足够多, 就会造成网络流量被打满, DB压力过大的情况, 所以 我们查询要改一改, 当然具体改成什么样子的,就要看真正的用途了, 如果只想要所有的店铺id, 可以使用 select id from restaurant where saturn_city_id = xxx and is_valid = 1
, 当然,如果数据量过大, 仍然会造成问题, 所以可能用如下的做法:
last_id = 0
limit = 500
while True:
shop_ids = select * from restaurant where saturn_city_id = xxx and id > last_id and is_valid = 1 and limit = 500
if not shop_ids:
break
for shop_id in shop_ids:
do_some_things(shop_id)
last_id = shop_ids[len(shop_ids) - 1]
为什么不用 limit offset
有过对大数据量查询经验的朋友肯定知道, sql执行时间 会随着 offset 的变大 直线增长(之前用offset遍历500w的数据, 当offset到100w的时候, sql响应时间就已经4s左右了, 可想而知当offset快到500w左右,会多么恐怖
使用 id > last_id 的响应时间是个常量, 且不会随着last_id的增长而增长
当Where中有两个索引字段
当你认为 上面那个sql已经天衣无缝的时候, 你就错了, mysql innodb引擎 只会选择一个索引使用, 上面那个sql的where条件中 有两个索引可以供 mysql 选择 ix_saturn_city_id
和 id_UNIQE
,当 mysql 选择 ix_saturn_city_id
索引走的时候万事大吉, 但是当选择 id_UNIQUE
的时候就是悲剧了, 因为 这时候是 全表扫描
Force Index
如果要强制让SQL走某个索引 可以使用 Force Index 关键字 ,如 :
select * from restaurant force index (ix_saturn_city_id) where saturn_city_id = xxx and id > xxx and is_valid = 1 limit 100
使用 sqlalchemy 的话要这样写:
shop_ids = shop_session.query(Restaurant.id).with_hint(Restaurant, 'FORCE INDEX(ix_saturn_city_id)').filter(Restaurant.saturn_city_id == xxx ).filter(Restaurant.is_valid == xxx).filter(Restaurant.id > last_id).limit(1000)
总结
当sql中查询的Where条件有多个, 且有多个索引可以供mysql选择, mysql就可能选错索引, 也由此如过想让mysql强制走某个索引, 可以使用 force index(xxxx)
关键字 。