当前位置: 首页 > 后端技术 > Java

年轻人!如何写出好的SQL?

时间:2023-04-02 00:25:30 Java

博主(码农)负责的项目主要使用阿里云数据库MySQL。最近频繁出现慢SQL告警,最长执行时间高达5分钟。导出日志分析,主要是没有命中索引,没有分页处理。其实这是一个很低级的错误,不由得背脊一凉。团队成员的技术水平亟待提高。在改造这些SQL的过程中,总结了一些经验分享给大家。如有错误,请批评指正。MySQL性能的最大数据量不管数据量和并发数,谈性能都是耍流氓。MySQL不限制单表的最大记录数,这取决于操作系统的文件大小限制。文件系统单个文件大小限制FAT32最大4GNTFS最大64GBNTFS5.0最大2TBEXT2块大小为1024字节,最大文件容量为16GB;blocksize4096bytes,最大文件容量2TBEXT3blocksize4KB,最大文件容量4TBEXT4理论上可以如果大于16TB《阿里巴巴Java开发手册》,建议单表行数超过500万行数或单表容量超过2GB,建议分库分表。性能是由综合因素决定的。不管业务复杂度如何,影响程度是硬件配置、MySQL配置、数据表设计、索引优化。500万的价值仅供参考,并非铁律。博主曾经操作过一个单表,有4亿多行数据。分页查询最近20条记录耗时0.6秒。SQL语句大致是selectfield_1,field_2fromtablewhereid<#{prePageMinId}orderbyiddesclimit20,prePageMinId为上一页数据记录的最小ID。虽然当时查询速度还可以,但是随着数据的不断增长,总有一天肯定会不堪重负。分库分表是一项长期、高风险的工作。应该尽可能在当前架构上优化,比如升级硬件,迁移历史数据等,实在不行再做。对分库分表感兴趣的同学可以阅读分库分表的基本思想。最大并发数并发数是指数据库可以同时处理多少个请求,由max_connections和max_user_connections决定。**max_connections是指MySQL实例的最大连接数,上限为16384,max_user_connections是指每个数据库用户的最大连接数。MySQL会为每个连接提供一个缓冲区,这意味着更多的内存消耗。连接数设得太高,硬件负担不起,设得太低,又不能充分利用硬件。一般要求两者之比超过10%。计算方法如下:max_used_connections/max_connections*100%=3/100*100%≈3%查看最大连接数和最大响应数:showvariableslike'%max_connections%';显示像“%max_user_connections%”这样的变量;在配置文件my.cnf中修改最大连接数[mysqld]max_connections=100max_used_connections=20查询耗时0.5秒。建议将单次查询耗时控制在0.5秒以内。0.5秒是经验值。基于用户体验的3秒原则。如果用户的操作在3秒内没有响应,他们会感到厌烦甚至退出。响应时间=客户端UI渲染时间+网络请求时间+应用处理时间+查询数据库时间,0.5秒是留给数据库处理时间的1/6。实现原理与NoSQL数据库相比,MySQL是一个脆弱的家伙。就像体育课上的女学生。一言不合就和同学闹矛盾(扩容难),跑两步就气喘吁吁(容量小并发低),经常身体不适要请假(SQL约束太多)。现在大家都会做一些分布式,扩展应用程序比做数据库要容易的多,所以实现的原则就是少做数据库的工作,多做应用程序的工作。充分利用索引而不滥用它,要知道索引也很耗磁盘和CPU。不建议使用数据库函数格式化数据,交给应用程序处理。不建议使用外键约束,使用应用保证数据的准确性。在写多读少的场景不建议使用唯一索引,使用应用保证唯一性。适当冗余字段,尝试创建中间表,使用应用程序计算中间结果,用空间换取时间。不允许将非常耗时的事务拆分为应用程序的较小事务。预估重要数据表(如订单表)的负载和数据增长趋势,提前优化。数据表设计数据类型数据类型选择原则:更简单或占用空间更小。如果长度足够,整数类型尽量使用tinyint、smallint、medium\_int而不是int。如果确定了字符串长度,则使用char类型。如果varchar可以满足,就不要用text类型。如果精度高就用decimal类型,或者用BIGINT。例如,如果您有两位小数,请将其乘以100并保存。尝试使用时间戳而不是日期时间。type占字节descriptiondatetime8byte'1000-01-0100:00:00.000000'to'9999-12-3123:59:59.999999timestamp4byte'1970-01-0100:00:01.000000'to'2038-01-1903:14:07.999999'与datetime相比,timestamp占用空间小,以UTC格式存储,自动转换时区。AvoidingNullValuesMySQL在字段为NULL时仍然会占用空间,这会使索引和索引统计变得更加复杂。从NULL值更新到非NULL值不能就地完成,并且容易发生索引分裂并影响性能。尽可能将NULL值替换为有意义的值,避免在SQL语句中判断isnotnull。文本类型优化由于文本字段存储大量数据,表容量会很早就增加,影响其他字段的查询性能。建议抽取出来分表,与业务主键关联。索引优化索引分类普通索引:最基本的索引。复合索引:建立在多个字段上的索引可以加快复合查询条件的检索。唯一索引:与普通索引类似,但索引列的值必须是唯一的,允许空值。组合唯一索引:列值的组合必须是唯一的。主键索引:一种特殊的唯一索引,用于唯一标识数据表中的某条记录。不允许空值,一般受主键约束。全文索引:用于查询海量文本。MySQL5.6之后的InnoDB和MyISAM都支持全文索引。由于查询准确性和扩展性较差,更多的公司选择了Elasticsearch。索引优化分页查询很重要,如果查询数据量超过30%,MYSQL将不会使用索引。单表索引数不超过5个,单索引字段数不超过5个。字符串可以使用前缀索引,前缀长度限制在5-8个字符。字段唯一性太低,增加索引没有意义,比如:是否删除,性别。合理使用覆盖索引,如下图:selectlogin_name,nick_namefrommemberwherelogin\_name=?鱼塘挖了一个小口放水,水面上漂浮着各种漂浮物。浮萍和树叶总是从喷口中穿过,而树枝会挡住其他物体的通道,有时会卡住,需要手动移除。MySQL是一个鱼塘,最大并发数和网络带宽是出水口,用户SQL是一个漂浮的对象。没有分页参数的查询或影响大量数据的更新和删除操作都是分支。我们需要将它们分解并分批处理。例如:业务描述:将用户所有过期的优惠券更新为不可用状态。SQL语句:updatestatus=0FROMcouponWHEREexpire_date<=#{currentDate}andstatus=1;如果大量的优惠券需要更新到不可用状态,执行这条SQL可能会阻塞其他SQL。批处理伪代码如下:intpageNo=1;intPAGE_SIZE=100;while(true){ListbatchIdList=queryList('selectidFROM`coupon`WHEREexpire_date<=#{currentDate}andstatus=1limit#{(pageNo-1)*PAGE_SIZE},#{PAGE_SIZE}');如果(CollectionUtils.isEmpty(batchIdList)){返回;}update('updatestatus=0FROM`coupon`wherestatus=1andidin#{batchIdList}')pageNo++;}operator<>optimization通常<>operator不能使用索引。比如查询金额不是100元的订单:selectidfromorderswhereamount!=100;如果数量为100的订单非常少,这种情况下数据分布严重不均匀时,可以使用索引。鉴于这种不确定性,使用union聚合搜索结果,重写方法如下:(selectidfromorderswhereamount>100)unionall(selectidfromorderswhereamount<100andamount>0)ORinnodb下的优化引擎或无法使用组合索引,例如:selectid,product_namefromorderswheremobile_no='13421800407'oruser_id=100;或者不能命中mobile_no+user_id的组合索引,可以使用union,如下图:(selectid,product_namefromorderswheremobile_no='13421800407')union(selectid,product_namefromorderswhereuser_id=100);此时id和product\_name字段都有索引,查询效率最高。IN优化IN适用于大主表小子表,EXIST适用于小主表大子表。由于查询优化器的不断升级,两者在很多场景下的性能几乎是一样的。尝试更改为连接查询,例如:selectidfromorderswhereuser\_idin(selectidfromuserwherelevel='VIP');使用JOIN如下:selecto.idfromordersoleftjoinuseruono.user_id=u.idwhereu.level='VIP';如果不进行列操作,通常查询条件中的列操作会导致索引失败,如下图:querytheorderofthedayselectidfromorderwheredate_format(create_time,'%Y-%m-%d')='2019-07-01';date\_format函数将使该查询无法使用索引,重写后:selectidfromorderwherecreate_timebetween'2019-07-0100:00:00'and'2019-07-0123:59:59';避免Selectall如果不查询表中的所有列,避免使用SELECT*,它会进行全表扫描,不能有效使用索引。Like优化模糊查询,例如(字段已被索引):SELECTcolumnFROMtableWHEREfieldlike'%keyword%';此查询未命中索引,将其替换为以下内容:SELECTcolumnFROMtableWHEREfieldlike'keyword%';去掉前面的%查询会命中索引,但是产品经理一定要前后模糊匹配?您可以尝试全文索引全文,但Elasticsearch是终极武器。Join优化join的实现是使用NestedLoopJoin算法,即以驱动表的结果集作为基础数据,以驱动表的结果集作为过滤条件循环查询下表中的数据,然后合并结果。如果有多个join,则将之前的结果集作为循环数据,在后面的表中再次查询数据。驱动表和被驱动表尽量增加查询条件,满足ON条件少用Where,用小结果集驱动大结果集。为从动表的join字段添加索引。如果无法建立索引,请设置足够的JoinBufferSize。禁止join连接三个以上的表,尽量加入冗余字段。limitoptimizationlimit用于逐页查询,性能越往后越差。解决原理:缩小扫描范围,如下图:select*fromordersorderbyiddesclimit100000,10耗时0.4秒select*fromordersorderbyiddesclimit1000000,10耗时5.2秒,过滤出ID为缩小查询范围,写成:select*fromorderswhereid>(selectidfromordersorderbyiddesclimit1000000,1)orderbyiddesclimit0,10如果查询条件只有主键ID,写法如下:selectidfromorderswhereidbetween1000000and1000010orderbyiddesctakes0.3seconds如果以上方案还是很慢?不得不用游标,有兴趣的朋友阅读JDBC使用游标实现分页查询的方法其他数据库作为后端开发者,必须精通以MySQL或SQLServer为存储核心,同时也要积极关注到NoSQL数据库,它们已经足够成熟,被广泛采用,可以解决特定场景下的性能瓶颈。分类数据库特性Key-valueMemcache用于内容缓存,以及大量数据的高访问负载。Key-valueRedis用于内容缓存。支持比Memcache更多的数据类型,可以持久化数据列式存储。HBaseHadoop系统的核心数据库。海量结构化数据存储,大数据必备。Document-basedMongoDb,一个知名的基于文档的数据库,也可以用于缓存基于文档的CouchDBApache的开源项目,注重易用性,支持RESTAPI,基于文档的SequoiaDB,国内知名文档-基于数据库图的Neo4J,用于为社交网络、推荐系统等构建关系图。