数据库SQL调优的几种方式

Posted 带你去吃小豆花

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据库SQL调优的几种方式相关的知识,希望对你有一定的参考价值。

最近在复习SQL调优,总结了下主要有以下几种方式:

目录

char  vs varchar

开启慢查询日志来定位查询慢的语句

适当使用索引  

合理使用关键字

优化查询缓存

分割数据表

非规范化的方式


char  vs varchar

1、如果文本字段始终是固定长度的(例如,US 邮编,其始终具有“XXXXX-XXXX”形式的规范表示),那么推荐使用char。varchar 类型的长度是可变的,而 char 类型是一个定长的字段,以 char(10) 为例,不管真实的存储内容多大或者是占了多少空间,都会消耗掉 10 个字符的空间,通俗来讲,当定义为 char(10) 时,即使插入的内容是 'abc' 3 个字符,它依然会占用 10 个字节,其中包含了 7 个空字节。

2、char 在快速、随机访问时效率很高。使用 varchar,如果你想读取下一个字符串,不得不先读取到当前字符串的末尾,查找效率比较低。

3、char 长度最大为 255 个字符,varchar 长度最大为 65535 个字符

为什么varchar(255)而不是varchar(258),varchar(257)呢?

使用255是因为它是8位数字可以计算的最大字符数。它最大限度地利用了8位计数,而不需要另一个整字节来计算255以上的字符。

开启慢查询日志来定位查询慢的语句

启动慢查询日志,看哪些语句慢。默认是禁用慢查询日志的,--slow_query_log[=0|1] 指定为1,将启用日志。如果参数为0,此选项将禁用日志。--slow_query_log_file=file_name 来改变慢查询日志名称。

先找到慢查询的原因:

1、打开数据库慢查询日志,定义超时时间,比如超过2S就是慢查询

2、定位执行效率低的慢查询

              show processlist

3、也可以通过explain来分析执行计划,explain得到的信息要主要关注:type字段,

       possible_keys字段,key字段,key_len字段,rows,extra字段等

一般情况下慢查询的原因有以下这些:

       没用索引或者索引失效

       用了索引但是走了全表扫描

慢查询如何优化

索引+sql语句+架构优化+数据库结构优化

索引:避免索引失效

sql语句:优化insert语句:多条插入写一条,数据有序的插入;分页优化:通过子查询优化,比如查询2w起后面10条数据,先通过子查询拿到20000的id,然后通过主键索引,通过B+树定位到拿到的id对应的行数据,然后再向后取10条数据;

架构优化:数据库读写分离,主库写,从库读;

数据库结构优化:将字段比较多的表分解成多个表,将字段使用频率高的和字段使用频率低的分开,对于一些要进行联合查询的表,可以考虑建立中间表

合理使用关键字

比如mysql还有其他更危险的关键字,应该谨慎使用。其中包括INSERT DELAYED,它告诉MySQL立即插入数据并不重要(例如,在日志记录情况下)。问题是,在高负载情况下,插入可能会无限期延迟,导致插入队列停滞。

优化查询缓存

查询缓存相关的服务器变量:

query_cache_min_res_unit:查询缓存中内存块的最小分配单位,默认4k,较小值会减少浪费,但会导致更频繁的内存分配操作,较大值会带来浪费,会导致碎片过多,内存不足
query_cache_limit:单个查询结果能缓存的最大值,单位字节,默认为1M,对于查询结果过大而无法缓存的语句,建议使用SQL_NO_CACHE
query_cache_size:查询缓存总共可用的内存空间;单位字节,必须是1024的整数倍,最小值40KB,低于此值有警报
query_cache_wlock_invalidate:如果某表被其它的会话锁定,是否仍然可以从查询缓存中返回结果,默认值为OFF,表示可以在表被其它会话锁定的场景中继续从缓存返回数据;ON则表示不允许
query_cache_type:是否开启缓存功能,取值为ON, OFF, DEMAND

查询缓存相关的状态变量:
show gloable status like 'Qcache%' ;

innodb_buffer_pool_size:这是任何使用innodb的安装都要考虑的#1设置。缓冲池是缓存数据和索引的地方:使其尽可能大将确保大多数读取操作使用内存而不是磁盘。典型值为5-6GB(8GB RAM)、20-25GB(32GB RAM)、100-120GB(128GB RAM)。
innodb_log_file_size:这是重做日志的大小。重做日志用于确保写入速度和持久性,以及在崩溃恢复期间。在MySQL 5.1之前,很难进行调整,因为您既需要大的重做日志来获得良好的性能,也需要小的重做日志来实现快速的崩溃恢复。幸运的是,自MySQL 5.5以来,崩溃恢复性能有了很大提高,因此您现在可以拥有良好的写性能和快速的崩溃恢复。在MySQL 5.5之前,重做日志的总大小限制为4GB(默认为有2个日志文件)。这在MySQL 5.6中得到了提升。

max_connections:如果经常遇到“连接数过多”错误,则最大连接数太低。由于应用程序无法正确关闭与数据库的连接,因此经常需要比默认的151个连接多得多的连接。

查询缓存的优化路线

 在早期版本mysql均支持缓存,但是随着redis等内存型高性能的缓存技术兴起,mysql已经抛弃自己的缓存功能,mysql8.0以后不再支持缓存功能。

适当使用索引  

每个索引都需要与表中的行数成比例的空间,因此太多的索引最终会占用更多内存。由于每次写入都需要更新相应的索引,因此写入操作的性能也会受到影响。通过分析代码,可以发现一个平衡点。这因系统和实施而异。

  • 查询(SELECTGROUP BYORDER BYJOIN)的列如果用了索引会更快
  • 索引通常表示为自平衡的 B 树,可以保持数据有序,并允许在对数时间内进行搜索,顺序访问,插入,删除操作
  • 设置索引,会将数据存在内存中,占用了更多内存空间
  • 写入操作会变慢,因为索引需要被更新
  • 加载大量数据时,禁用索引再加载数据,然后重建索引,这样也许会更快

        索引何时会失效?

索引 (Index) 是帮助 MySQL 高效获取数据的数据结构。我们可以简单理解为:快速查找排好序的一种数据结构。

当索引不起作用时,会引起全表查询,索引就会失效,引起慢查询,有以下几种情况:

  1. 模糊查询,比如以%开头的like查询。
  2. 在索引列上操作or, not in , !=,<>,等操作
  3. 如果查询条件有or,并且or的前后条件中有一个列没有索引,则涉及的索引都不会用到

1、避免索引失效,减少%like这种索引失效语句

2、合理创建索引

3、分页查询优化,可以通过子查询,关联查询优化,比如要查询从10000行开始的10行数据,看似只返回了10条数据,但是数据库引擎需要查询10010数据,然后将前面的10000条数据丢弃,性能可想而知,针对这种情况,我们可以先定位到上次分页的id,然后对id做条件索引查询;或者将原有的sql拆成2步,首先查询出一页数据中的最小id,然后通过索引树,定位到最小id索引树节点位置,通过偏移量来读取后面的10条数据

4、避免使用select *

select *的查询过程:先在字段的二级索引B+树上,查出对应的主键id列表

然后进行回表操作,在主键索引中 查询id对应的行数据

5.通过explain来分析SQL执行计划,看是否用到了索引

explain得到的字段有:key(使用到的索引),rows(MYSQL估计为了查找目标行而需要读取的行数:),possible_keys(查询可能会使用的索引)等,主要关注type

Type:以怎样的方式查找表中的行的方式(下列几个类型性能逐渐变好)

All:   全表扫描

Index:  根据索引的次序进行全表扫描,若在extra出现using index表示使用覆盖索引,而非全表扫描

Range:  根据索引实现的范围扫描

Ref:  根据索引返回表中匹配某单个值的所有行

Eq_ref:  仅返回一个行,但需要和某个参考值作比较

Const,system:  根据具有唯一性的索引(比如主键)查找时,返回的是一行

NULL:类似于覆盖查询

Id:当前查询中,每个Select语句的编号

复杂类型的查询三种:

  1. 简单子查询:
  2. 用于from中的子查询
  3. 联合查询,union

注意:union查询的分析结果中会出现一个额外的匿名临时表

Select_type:

简单查询为simple

复杂查询:

Subquery:简单子查询

Derived:用于from中的子查询

Union:用于union第一个之后的select语句

possible_keys:  查询可能会使用的索引

Key:  使用到的索引

Key_len:  索引中使用的字节数,比如索引有70个字节数,只是用了20个

ref:  在利用key所表示的索引完成查询时,所用的列或某常量值

rows:  MYSQL估计为了查找目标行而需要读取的行数:

Extra:额外信息

Using index:会使用覆盖索引,以避免访问表

Using where:服务器将在存储引擎检索后,再进行一次过滤

Using temporary:对结果排序时会使用临时表

Using filesort:对结果使用一个外部索引排序

举例说明explain的用法:

MariaDB [testdb]> explain select sname,age from stu union select tname,age from teacher\\G;

*************************** 1. row ***************************

           id: 1

  select_type: PRIMARY

        table: stu

         type: ALL

possible_keys: NULL

          key: NULL

      key_len: NULL

          ref: NULL

         rows: 7

        Extra:

*************************** 2. row ***************************

           id: 2

  select_type: UNION

        table: teacher

         type: ALL

possible_keys: NULL

          key: NULL

      key_len: NULL

          ref: NULL

         rows: 3

        Extra:

*************************** 3. row ***************************

           id: NULL   联合前两个表   匿名临时表

  select_type: UNION RESULT

        table: <union1,2>

         type: ALL

possible_keys: NULL

          key: NULL

      key_len: NULL

          ref: NULL

         rows: NULL

        Extra:

3 rows in set (0.00 sec)

ERROR: No query specified

分割数据表

将热点数据拆分到单独的数据表中,可以有助于缓存

例如,在博客上,可能会在许多地方显示条目标题(例如,最近发布的文章列表). 经常访问的数据保存在一个表中,而不经常访问的数据保存在另一个表中。由于数据现在已分区,因此不经常访问的数据占用的内存更少。

非规范化的方式

非规范化: 非规范化是一种用于先前规范化数据库以提高性能的策略。在计算中,非规范化是指通过添加数据的冗余副本或对数据进行分组,以牺牲某些写入性能为代价,尝试提高数据库的读取性能的过程。

这通常是由需要执行大量读取操作的关系数据库软件的性能或可扩展性引起的。一般情况下,只需要在性能需要的地方进行非规范化。

参考:system-design-primer/README-zh-Hans.md at master · donnemartin/system-design-primer · GitHubMySQL查询缓存优化_Tomorrow Is Forever的技术博客_51CTO博客

JVM调优的几种场景(建议收藏)


最近很多小伙伴跟我说,自己学了不少JVM的调优知识,但是在实际工作中却不知道何时对JVM进行调优。今天,就为大家介绍几种JVM调优的场景。

点击上方,关注我,java干货及时送达

在阅读本文时,假定大家已经了解了运行时的数据区域和常用的垃圾回收算法,也了解了Hotspot支持的垃圾回收器。

cpu占用过高

cpu占用过高要分情况讨论,是不是业务上在搞活动,突然有大批的流量进来,而且活动结束后cpu占用率就下降了,如果是这种情况其实可以不用太关心,因为请求越多,需要处理的线程数越多,这是正常的现象。话说回来,如果你的服务器配置本身就差,cpu也只有一个核心,这种情况,稍微多一点流量就真的能够把你的cpu资源耗尽,这时应该考虑先把配置提升吧。

第二种情况,cpu占用率长期过高,这种情况下可能是你的程序有那种循环次数超级多的代码,甚至是出现死循环了。排查步骤如下:

(1)用top命令查看cpu占用情况

这样就可以定位出cpu过高的进程。在linux下,top命令获得的进程号和jps工具获得的vmid是相同的:

(2)用top -Hp命令查看线程的情况

可以看到是线程id为7287这个线程一直在占用cpu

(3)把线程号转换为16进制

[root@localhost ~]# printf "%x" 7287
1c77

记下这个16进制的数字,下面我们要用

(4)用jstack工具查看线程栈情况

[root@localhost ~]# jstack 7268 | grep 1c77 -A 10
"http-nio-8080-exec-2" #16 daemon prio=5 os_prio=0 tid=0x00007fb66ce81000 nid=0x1c77 runnable [0x00007fb639ab9000]
   java.lang.Thread.State: RUNNABLE
 at com.spareyaya.jvm.service.EndlessLoopService.service(EndlessLoopService.java:19)
 at com.spareyaya.jvm.controller.JVMController.endlessLoop(JVMController.java:30)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:498)
 at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
 at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
 at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)

通过jstack工具输出现在的线程栈,再通过grep命令结合上一步拿到的线程16进制的id定位到这个线程的运行情况,其中jstack后面的7268是第(1)步定位到的进程号,grep后面的是(2)、(3)步定位到的线程号。

从输出结果可以看到这个线程处于运行状态,在执行com.spareyaya.jvm.service.EndlessLoopService.service这个方法,代码行号是19行,这样就可以去到代码的19行,找到其所在的代码块,看看是不是处于循环中,这样就定位到了问题。

死锁

死锁并没有第一种场景那么明显,web应用肯定是多线程的程序,它服务于多个请求,程序发生死锁后,死锁的线程处于等待状态(WAITING或TIMED_WAITING),等待状态的线程不占用cpu,消耗的内存也很有限,而表现上可能是请求没法进行,最后超时了。在死锁情况不多的时候,这种情况不容易被发现。

可以使用jstack工具来查看

(1)jps查看java进程

[root@localhost ~]# jps -l
8737 sun.tools.jps.Jps
8682 jvm-0.0.1-SNAPSHOT.jar

(2)jstack查看死锁问题

由于web应用往往会有很多工作线程,特别是在高并发的情况下线程数更多,于是这个命令的输出内容会十分多。jstack最大的好处就是会把产生死锁的信息(包含是什么线程产生的)输出到最后,所以我们只需要看最后的内容就行了

Java stack information for the threads listed above:
===================================================
"Thread-4":
 at com.spareyaya.jvm.service.DeadLockService.service2(DeadLockService.java:35)
 - waiting to lock <0x00000000f5035ae0> (a java.lang.Object)
 - locked <0x00000000f5035af0> (a java.lang.Object)
 at com.spareyaya.jvm.controller.JVMController.lambda$deadLock$1(JVMController.java:41)
 at com.spareyaya.jvm.controller.JVMController$$Lambda$457/1776922136.run(Unknown Source)
 at java.lang.Thread.run(Thread.java:748)
"Thread-3":
 at com.spareyaya.jvm.service.DeadLockService.service1(DeadLockService.java:27)
 - waiting to lock <0x00000000f5035af0> (a java.lang.Object)
 - locked <0x00000000f5035ae0> (a java.lang.Object)
 at com.spareyaya.jvm.controller.JVMController.lambda$deadLock$0(JVMController.java:37)
 at com.spareyaya.jvm.controller.JVMController$$Lambda$456/474286897.run(Unknown Source)
 at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

发现了一个死锁,原因也一目了然。

内存泄漏

我们都知道,java和c++的最大区别是前者会自动收回不再使用的内存,后者需要程序员手动释放。在c++中,如果我们忘记释放内存就会发生内存泄漏。但是,不要以为jvm帮我们回收了内存就不会出现内存泄漏。

程序发生内存泄漏后,进程的可用内存会慢慢变少,最后的结果就是抛出OOM错误。发生OOM错误后可能会想到是内存不够大,于是把-Xmx参数调大,然后重启应用。这么做的结果就是,过了一段时间后,OOM依然会出现。最后无法再调大最大堆内存了,结果就是只能每隔一段时间重启一下应用。

内存泄漏的另一个可能的表现是请求的响应时间变长了。这是因为频繁发生的GC会暂停其它所有线程(Stop The World)造成的。

为了模拟这个场景,使用了以下的程序

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main 

    public static void main(String[] args) 
        Main main = new Main();
        while (true
            try 
                Thread.sleep(1);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            main.run();
        
    

    private void run() 
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) 
            executorService.execute(() -> 
                // do something...
            );
        
    

运行参数是-Xms20m -Xmx20m -XX:+PrintGC,把可用内存调小一点,并且在发生gc时输出信息,运行结果如下

...
[GC (Allocation Failure)  12776K->10840K(18432K), 0.0309510 secs]
[GC (Allocation Failure)  13400K->11520K(18432K), 0.0333385 secs]
[GC (Allocation Failure)  14080K->12168K(18432K), 0.0332409 secs]
[GC (Allocation Failure)  14728K->12832K(18432K), 0.0370435 secs]
[Full GC (Ergonomics)  12832K->12363K(18432K), 0.1942141 secs]
[Full GC (Ergonomics)  14923K->12951K(18432K), 0.1607221 secs]
[Full GC (Ergonomics)  15511K->13542K(18432K), 0.1956311 secs]
...
[Full GC (Ergonomics)  16382K->16381K(18432K), 0.1734902 secs]
[Full GC (Ergonomics)  16383K->16383K(18432K), 0.1922607 secs]
[Full GC (Ergonomics)  16383K->16383K(18432K), 0.1824278 secs]
[Full GC (Allocation Failure)  16383K->16383K(18432K), 0.1710382 secs]
[Full GC (Ergonomics)  16383K->16382K(18432K), 0.1829138 secs]
[Full GC (Ergonomics) Exception in thread "main"  16383K->16382K(18432K), 0.1406222 secs]
[Full GC (Allocation Failure)  16382K->16382K(18432K), 0.1392928 secs]
[Full GC (Ergonomics)  16383K->16382K(18432K), 0.1546243 secs]
[Full GC (Ergonomics)  16383K->16382K(18432K), 0.1755271 secs]
[Full GC (Ergonomics)  16383K->16382K(18432K), 0.1699080 secs]
[Full GC (Allocation Failure)  16382K->16382K(18432K), 0.1697982 secs]
[Full GC (Ergonomics)  16383K->16382K(18432K), 0.1851136 secs]
[Full GC (Allocation Failure)  16382K->16382K(18432K), 0.1655088 secs]
java.lang.OutOfMemoryError: Java heap space

可以看到虽然一直在gc,占用的内存却越来越多,说明程序有的对象无法被回收。但是上面的程序对象都是定义在方法内的,属于局部变量,局部变量在方法运行结果后,所引用的对象在gc时应该被回收啊,但是这里明显没有。

为了找出到底是哪些对象没能被回收,我们加上运行参数-XX:+HeapDumpOnOutOfMemoryError  -XX:HeapDumpPath=heap.bin,意思是发生OOM时把堆内存信息dump出来。运行程序直至异常,于是得到heap.dump文件,然后我们借助eclipse的MAT插件来分析,如果没有安装需要先安装。

然后File->Open Heap Dump... ,然后选择刚才dump出来的文件,选择Leak Suspects

MAT会列出所有可能发生内存泄漏的对象

可以看到居然有21260个Thread对象,3386个ThreadPoolExecutor对象,如果你去看一下java.util.concurrent.ThreadPoolExecutor的源码,可以发现线程池为了复用线程,会不断地等待新的任务,线程也不会回收,需要调用其shutdown方法才能让线程池执行完任务后停止。

其实线程池定义成局部变量,好的做法是设置成单例。

上面只是其中一种处理方法

在线上的应用,内存往往会设置得很大,这样发生OOM再把内存快照dump出来的文件就会很大,可能大到在本地的电脑中已经无法分析了(因为内存不足够打开这个dump文件)。这里介绍另一种处理办法:

(1)用jps定位到进程号

C:\\Users\\spareyaya\\IdeaProjects\\maven-project\\target\\classes\\org\\example\\net>jps -l
24836 org.example.net.Main
62520 org.jetbrains.jps.cmdline.Launcher
129980 sun.tools.jps.Jps
136028 org.jetbrains.jps.cmdline.Launcher

因为已经知道了是哪个应用发生了OOM,这样可以直接用jps找到进程号135988

(2)用jstat分析gc活动情况

jstat是一个统计java进程内存使用情况和gc活动的工具,参数可以有很多,可以通过jstat -help查看所有参数以及含义

C:\\Users\\spareyaya\\IdeaProjects\\maven-project\\target\\classes\\org\\example\\net>jstat -gcutil -t -h8 24836 1000
Timestamp         S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
           29.1  32.81   0.00  23.48  85.92  92.84  84.13     14    0.339     0    0.000    0.339
           30.1  32.81   0.00  78.12  85.92  92.84  84.13     14    0.339     0    0.000    0.339
           31.1   0.00   0.00  22.70  91.74  92.72  83.71     15    0.389     1    0.233    0.622

上面是命令意思是输出gc的情况,输出时间,每8行输出一个行头信息,统计的进程号是24836,每1000毫秒输出一次信息。

输出信息是Timestamp是距离jvm启动的时间,S0、S1、E是新生代的两个Survivor和Eden,O是老年代区,M是Metaspace,CCS使用压缩比例,YGC和YGCT分别是新生代gc的次数和时间,FGC和FGCT分别是老年代gc的次数和时间,GCT是gc的总时间。虽然发生了gc,但是老年代内存占用率根本没下降,说明有的对象没法被回收(当然也不排除这些对象真的是有用)。

(3)用jmap工具dump出内存快照

jmap可以把指定java进程的内存快照dump出来,效果和第一种处理办法一样,不同的是它不用等OOM就可以做到,而且dump出来的快照也会小很多。

jmap -dump:live,format=b,file=heap.bin 24836

这时会得到heap.bin的内存快照文件,然后就可以用eclipse来分析了。

总结

以上三种严格地说还算不上jvm的调优,只是用了jvm工具把代码中存在的问题找了出来。我们进行jvm的主要目的是尽量减少停顿时间,提高系统的吞吐量。但是如果我们没有对系统进行分析就盲目去设置其中的参数,可能会得到更坏的结果,jvm发展到今天,各种默认的参数可能是实验室的人经过多次的测试来做平衡的,适用大多数的应用场景。如果你认为你的jvm确实有调优的必要,也务必要取样分析,最后还得慢慢多次调节,才有可能得到更优的效果。

好了,今天就到这儿吧,我们下期见~~

如果觉得分享的不错,欢迎大家分享、转发、在看!

转自: cnblogs.com/spareyaya/p/13174003.html

其他学习资料
  1. 尚硅谷 Java 学科全套教程(总 207.77GB)
  2. 2021 最新版 Java 微服务学习线路图 + 视频
  3. 阿里技术大佬整理的《Spring 学习笔记.pdf》
  4. 2021 版 java 高并发常见面试题汇总.pdf
  5. Idea 快捷键大全.pdf

点击上方卡片关注我

以上是关于数据库SQL调优的几种方式的主要内容,如果未能解决你的问题,请参考以下文章

SQL 优化

JVM调优的几种场景(建议收藏)

JVM调优的几种场景(建议收藏)

查看Oracle执行计划的几种常用方法-系列1 .

如何优化SQL语句

oracle数据库