分类: linux

  • JVM性能调优-收集

    1、JVM源码分析之SystemGC完全解读
    http://mp.weixin.qq.com/s/V1Y6DIoscTuv7RVlIZgVpw

    2、JVM源码分析之堆外内存完全解读
    http://mp.weixin.qq.com/s/WgQkXxBJDF7QdTHFFGVypg

    3、JVM源码分析之Object.wait/notify(All)完全解读
    http://mp.weixin.qq.com/s/4oCEWVrs67aONxEgMaOVFg

    4、JDK的sql设计不合理导致的驱动类初始化死锁问题
    http://mp.weixin.qq.com/s/XVXEZK71ZKGbIgvCGl7tIg

    5、JVM源码分析之FinalReference完全解读
    http://mp.weixin.qq.com/s/-ER8S28tb17-f51S_NgADQ

    6、如何定位消耗CPU最多的线程
    http://mp.weixin.qq.com/s/c-KuGjI_VH1dTxIWtxZJEg

    7、不可逆的类初始化过程
    http://mp.weixin.qq.com/s/HK5JsmGjvOe_93TwlmZNdg

    8、JVM源码分析之javaagent原理完全解读
    http://mp.weixin.qq.com/s/OLeWL70E0qFACzw5Ri8GTw

    9、JDK8在泛型类型推导上的变化
    http://mp.weixin.qq.com/s/lz5RyWUmneCDpGSqyVLy9Q

    10、JVM源码分析之自定义类加载器如何拉长YGC
    http://mp.weixin.qq.com/s/fiuB2f3Gv5XDka0rrP2eAw

    11、进程物理内存远大于Xmx的问题分析
    http://mp.weixin.qq.com/s/XJ1xXz8dtMv1DVcZVf73GA

    12、JVM源码分析之Attach机制实现完全解读
    http://mp.weixin.qq.com/s/-ER8S28tb17-f51S_NgADQ

    13、JVM源码分析之栈溢出完全解读
    http://mp.weixin.qq.com/s/1d8W-eyzsnGDr9-uSfag6Q

    14、JVM源码分析之JDK8下的僵尸(无法回收)类加载器
    http://mp.weixin.qq.com/s/mbLuRjfw56wglBmOdtVskQ

    15、消失的死锁
    http://mp.weixin.qq.com/s/KOzt6kOXH3MapR3Ug43gvw

    16、YGC前后新生代变大
    http://mp.weixin.qq.com/s/YigddMVvRj7nO1xAZOXa1Q

    17、诡异GC问题收集
    http://mp.weixin.qq.com/s/rX6mDmZDQ9SWtze4F0hvvQ

    18、JVM源码分析之jstat工具原理完全解读
    http://mp.weixin.qq.com/s/gCE9eXbtMuze3jhuRm1YXA

    19、JVM源码分析之不可控的堆外内存
    http://mp.weixin.qq.com/s/MgMYy-K0G753-_qCsqFyVA

    20、JVM源码分析之临门一脚的OutOfMemoryError完全解读
    http://mp.weixin.qq.com/s/M8TFyVzS5MvwL2M639m3Bg

    21、JVM源码分析之Metaspace解密
    http://mp.weixin.qq.com/s/SsXbRvtvawKDHstFpU4uog

    22、JVM源码分析之不保证顺序的Class.getMethods
    http://mp.weixin.qq.com/s/XrAD1Q09mJ-95OXI2KaS9Q

    23、JVM源码分析之String.intern()导致的YGC不断变长
    http://mp.weixin.qq.com/s/RtIZd4zaa-UkxoxtUsXd8Q

    24、JVM源码分析之自定义类加载器如何拉长YGC
    http://mp.weixin.qq.com/s/rsYm1WTMKv5V2SRHmwSdlw

    25、Java的时间为何从1970年1月1日开始
    http://mp.weixin.qq.com/s/mUTOMe4rXhKWp5sOHZK9Xw

    26、JVM源码分析之System.currentTimeMillis及nanoTime原理详解
    http://mp.weixin.qq.com/s/w37FXrVjQL36i8m_WLpf_w

    27、JVM源码分析之一个Java进程究竟能创建多少线程
    http://mp.weixin.qq.com/s/K8Y1wOloEwj1yQGEf7TnZQ

    28、JVM源码分析之警惕存在内存泄漏风险的FinalReference(增强版)
    https://mp.weixin.qq.com/s/igboi4xvjT4hTEFqZjLxEA

    29、假笨说-从X86指令深扒JVM的位移操作
    http://mp.weixin.qq.com/s/Mbg4y8fZdE4Mi37uz8-czA

    30、假笨说-我是如何走上JVM这条贼船的
    http://mp.weixin.qq.com/s/u7AWMDORvYa1TV4La18ObQ

    31、假笨说-从一起GC血案谈到反射原理
    http://mp.weixin.qq.com/s/5H6UHcP6kvR2X5hTj_SBjA

    32、来云栖社区聊聊Java开发者规范吧
    https://mp.weixin.qq.com/s/9kFI8WDxreHszt0Xa7fl4g

    33、假笨说-谨防JDK8重复类定义造成的内存泄漏
    https://mp.weixin.qq.com/s/3sb_ovHhhTXTid3G5iZUew

    34、假笨说-类初始化死锁导致线程被打爆!打爆!爆!
    http://mp.weixin.qq.com/s/UwEO8hFq-EL3a_VjMRydkA

    35、假笨说-又抓了一个导致频繁GC的鬼–数组动态扩容
    http://mp.weixin.qq.com/s/HKdpmmvJKq45QZdV4Q2cYQ

    36、假笨说-关于数组动态扩容导致频繁GC的问题,我还有话说
    http://mp.weixin.qq.com/s/GuPpF5LWydwZVnEiz6KoHw

    37、假笨说-查JVM参数就找JVMPocket(JVM口袋)小程序吧
    http://mp.weixin.qq.com/s/XJrH8lN6N0i7juf2AKZmmA

    38、假笨说-给JVMPocket提建议,赠您JVM的好书,可好?
    http://mp.weixin.qq.com/s/sNB42zc60oRAZL9Y54-e6g

    39、假笨说-警惕大量类加载器的创建导致诡异的Full GC
    http://mp.weixin.qq.com/s/qgpMMR8-493-Y9uwWiMdRg

    40、揪出一个导致GC慢慢变长的JVM设计缺陷
    http://mp.weixin.qq.com/s/m0YpJuHB3pkrvUylYbe5eg

    41、假笨说-关于内存溢出,咱再聊点有意思的
    http://mp.weixin.qq.com/s/ET8C8VrbcCLFSipCLKgvYA

    42、假笨说-JVM参数,我准备做些分享,你想听吗
    http://mp.weixin.qq.com/s/OhalT8Y4MgOWCiNA3y9iQg

    43、假笨说参数-对象晋升相关的MaxTenuringThreshold
    http://mp.weixin.qq.com/s/9NULcNlV7G4Ssgn5FSFzbg

    44、假笨说参数-GC日志其实也支持滚动输出的
    http://mp.weixin.qq.com/s/aGT31AQyH7NRqnRGArE2eg

     

  • lsof查看端口被谁占用

    使用 lsof 查找打开的文件

    通过查看打开的文件,了解更多关于系统的信息。了解应用程序打开了哪些文件或者哪个应用程序打开了特定的文件,作为系统管理员,这将使得您能够作出更好的决策。例如,您不应该卸载具有打开文件的文件系统。使用 lsof,您可以检查打开的文件,并根据需要在卸载之前中止相应的进程。同样地,如果您发现了一个未知的文件,那么可以找出到底是哪个应用程序打开了这个文件。

    在 UNIX® 环境中,文件无处不在,这便产生了一句格言:“任何事物都是文件”。通过文件不仅仅可以访问常规数据,通常还可以访问网络连接和硬件。在有些情况下,当您使用 ls 请求目录清单时,将出现相应的条目。在其他情况下,如传输控制协议 (TCP) 和用户数据报协议 (UDP) 套接字,不存在相应的目录清单。但是在后台为该应用程序分配了一个文件描述符,无论这个文件的本质如何,该文件描述符为应用程序与基础操作系统之间的交互提供了通用接口。

    因为应用程序打开文件的描述符列表提供了大量关于这个应用程序本身的信息,所以能够查看这个列表将是很有帮助的。完成这项任务的实用程序称为 lsof,它对应于“list open files”(列出打开的文件)。几乎在每个 UNIX 版本中都有这个实用程序,但奇怪的是,大多数供应商并没有将其包含在操作系统的初始安装中。要获取更多关于 lsof 的信息,请参见参考资料部分。

    lsof 简介

    只需输入 lsof 就可以生成大量的信息,如清单 1 所示。因为 lsof 需要访问核心内存和各种文件,所以必须以 root 用户的身份运行它才能够充分地发挥其功能。

    清单 1. lsof 的示例输出
    bash-3.00# lsof 
    COMMAND    PID   USER   FD   TYPE        DEVICE SIZE/OFF      NODE NAME
    sched        0   root  cwd   VDIR         136,8     1024         2 /
    init         1   root  cwd   VDIR         136,8     1024         2 /
    init         1   root  txt   VREG         136,8    49016      1655 /sbin/init
    init         1   root  txt   VREG         136,8    51084      3185 /lib/libuutil.so.1
    vi        2013   root    3u  VREG         136,8        0      8501 /var/tmp/ExXDaO7d
    ...

    每行显示一个打开的文件,除非另外指定,否则将显示所有进程打开的所有文件。CommandPID 和 User 列分别表示进程的名称、进程标识符 (PID) 和所有者名称。DeviceSIZE/OFFNode 和 Name 列涉及到文件本身的信息,分别表示指定磁盘的名称、文件的大小、索引节点(文件在磁盘上的标识)和该文件的确切名称。根据 UNIX 版本的不同,可能将文件的大小报告为应用程序在文件中进行读取的当前位置(偏移量)。清单 1 来自一台可以报告该信息的 Sun Solaris 10 计算机,而 Linux® 没有这个功能。

    FD 和 Type 列的含义最为模糊,它们提供了关于文件如何使用的更多信息。FD 列表示文件描述符,应用程序通过文件描述符识别该文件。Type 列提供了关于文件格式的更多描述。我们来具体研究一下文件描述符列,清单 1 中出现了三种不同的值。cwd 值表示应用程序的当前工作目录,这是该应用程序启动的目录,除非它本身对这个目录进行更改。txt 类型的文件是程序代码,如应用程序二进制文件本身或共享库,再比如本示例的列表中显示的 init 程序。最后,数值表示应用程序的文件描述符,这是打开该文件时返回的一个整数。在清单 1 输出的最后一行中,您可以看到用户正在使用 vi 编辑 /var/tmp/ExXDaO7d,其文件描述符为 3。u 表示该文件被打开并处于读取/写入模式,而不是只读 (r) 或只写 (w) 模式。有一点不是很重要但却很有帮助,初始打开每个应用程序时,都具有三个文件描述符,从 0 到 2,分别表示标准输入、输出和错误流。正因为如此,大多数应用程序所打开的文件的 FD 都是从 3 开始。

    与 FD 列相比,Type 列则比较直观。根据具体操作系统的不同,您会发现将文件和目录称为 REG 和 DIR(在 Solaris 中,称为 VREG 和 VDIR)。其他可能的取值为 CHR 和 BLK,分别表示字符和块设备;或者 UNIXFIFO 和 IPv4,分别表示 UNIX 域套接字、先进先出 (FIFO) 队列和网际协议 (IP) 套接字。

    转到 /proc 目录

    尽管与使用 lsof 没有什么直接的关系,但对 /proc 目录进行简要的介绍是有必要的。/proc 是一个目录,其中包含了反映内核和进程树的各种文件。这些文件和目录并不存在于磁盘中,因此当您对这些文件进行读取和写入时,实际上是在从操作系统本身获取相关信息。大多数与 lsof相关的信息都存储于以进程的 PID 命名的目录中,所以 /proc/1234 中包含的是 PID 为 1234 的进程的信息。

    在 /proc 目录的每个进程目录中存在着各种文件,它们可以使得应用程序简单地了解进程的内存空间、文件描述符列表、指向磁盘上的文件的符号链接和其他系统信息。lsof 实用程序使用该信息和其他关于内核内部状态的信息来产生其输出。稍后我将把 lsof 的输出与 /proc 目录中的信息联系起来。

    常见用法

    前面,我向您介绍了如何简单地运行不带任何参数的 lsof,以便显示关于每个进程所打开的文件的信息。本文余下的部分将重点关注如何使用 lsof 来显示所需的信息以及如何正确地对其进行解释。

    查找应用程序打开的文件

    lsof 常见的用法是查找应用程序打开的文件的名称和数目。您可能想尝试找出某个特定应用程序将日志数据记录到何处,或者正在跟踪某个问题。例如,UNIX 限制了进程能够打开文件的数目。通常这个数值很大,所以不会产生问题,并且在需要时,应用程序可以请求更大的值(直到某个上限)。如果您怀疑应用程序耗尽了文件描述符,那么可以使用 lsof 统计打开的文件数目,以进行验证。

    要指定单个进程,可以使用 -p 参数,后面加上该进程的 PID。因为这样做不仅会返回该应用程序所打开的文件,还会返回共享库和代码,所以通常需要对输出进行筛选。要完成此任务,可以使用 -d 标志根据 FD 列进行筛选,使用 -a 标志表示两个参数都必须满足 (AND)。如果没有 -a标志,缺省的情况是显示匹配任何一个参数 (OR) 的文件。清单 2 显示了 sendmail 进程打开的文件,并使用 txt 对这些文件进行筛选。

    清单 2. 带有 PID 筛选器并进行 txt 文件描述符筛选的 lsof 输出
    sh-3.00# lsof -a -p 605 -d ^txt
    COMMAND  PID USER   FD   TYPE  DEVICE SIZE/OFF     NODE NAME
    sendmail 605 root  cwd   VDIR  136,8     1024    23554 /var/spool/mqueue
    sendmail 605 root    0r  VCHR  13,2            6815752 /devices/pseudo/mm@0:null
    sendmail 605 root    1w  VCHR  13,2            6815752 /devices/pseudo/mm@0:null
    sendmail 605 root    2w  VCHR  13,2            6815752 /devices/pseudo/mm@0:null
    sendmail 605 root    3r  DOOR             0t0       58
    		/var/run/name_service_door(door to nscd[81]) (FA:->0x30002b156c0)
    sendmail 605 root    4w  VCHR  21,0           11010052 
    						/devices/pseudo/log@0:conslog->LOG
    sendmail 605 root    5u  IPv4 0x300010ea640      0t0      TCP *:smtp (LISTEN)
    sendmail 605 root    6u  IPv6 0x3000431c180      0t0      TCP *:smtp (LISTEN)
    sendmail 605 root    7u  IPv4 0x300046d39c0      0t0      TCP *:submission (LISTEN)
    sendmail 605 root    8wW VREG         281,3       32  8778600 /var/run/sendmail.pid

    清单 2 为 lsof 指定了三个参数。第一个是 -a,它表示当所有的参数都为真时,才显示这个文件。第二个参数是 -p 605,它限制仅输出 PID 为 605 的进程,可以通过 ps 命令获取这个信息。最后一个参数 -d ^txt,它表示筛选出其中 txt 类型的记录(脱字符号 [^] 表示排除)。

    清单 2 的输出提供了关于进程行为的信息。如 cwd 行所示,该应用程序的工作目录为 /var/spool/mqueue。文件描述符 0、1 和 2 分配给了 /dev/null(Solaris 大量使用符号链接,所以这里显示了相应的伪设备)。FD 3 是一个 Solaris 门(高速远程过程调用 (RPC) 接口),以只读模式打开。FD 4 中的内容比较有趣,因为它是一个字符设备的只读句柄,实质上是 /dev/log。从这个文件中,您可以收集该应用程序向 UNIX syslog 守护进程进行的记录,所以 /etc/syslog.conf 规定了日志文件的位置。

    作为一个网络应用程序,sendmail 对网络端口进行监听。文件描述符 5、6 和 7 可以告诉您,该应用程序正以 IPv4 和 IPv6 模式监听简单邮件传输协议 (SMTP) 端口,并以 IPv4 模式监听提交端口。最后一个文件描述符是只写的,并且指向 /var/run/sendmail.pid。FD 列中的大写 W 表示该应用程序具有对整个文件的写锁。该文件用于确保每次只能打开一个应用程序实例。

    查找打开某个文件的应用程序

    在其他情况下,您有一个文件或目录,并且需要知道哪个应用程序控制了该文件(打开了该文件)。清单 2 显示了由 sendmail 进程打开了 /var/run/sendmail.pid。如果您不知道这个信息,那么在给定文件名的情况下,lsof 可以提供该信息。清单 3 显示了相应的输出。

    清单 3. 要求 lsof 显示关于某个文件的信息
    bash-3.00# lsof /var/run/sendmail.pid
    COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
    sendmail 605 root    8wW VREG  281,3       32 8778600 /var/run/sendmail.pid

    正如输出所示,进程 sendmail(PID 为 605)控制了文件 /var/run/sendmail.pid,并且通过排它锁打开该文件以便进行写入。如果出于某种原因,您需要删除这个文件,那么正确的做法是中止该进程,而不是直接删除这个文件。否则,这个守护进程下次可能无法正常启动,或者可能稍后会启动另一个实例,从而导致争用。

    有时您只知道在文件系统的某处打开了文件。在卸载文件系统时,如果该文件系统中有任何打开的文件,那么操作将会失败。通过指定装入点的名称,您可以使用 lsof 显示一个文件系统中所有打开的文件。清单 4 显示了如何尝试卸载 /export/home,然后使用 lsof 找出谁在使用该文件系统。

    清单 4. 使用 lsof 找出谁在使用文件系统
    bash-3.00# umount /export/home
    umount: /export/home busy
    bash-3.00# lsof /export/home
    COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
    bash    1943 root  cwd   VDIR  136,7     1024    4 /export/home/sean
    bash    2970 sean  cwd   VDIR  136,7     1024    4 /export/home/sean
    ct      3030 sean  cwd   VDIR  136,7     1024    4 /export/home/sean
    ct      3030 sean    1w  VREG  136,7        0   25 /export/home/sean/output

    在这个示例中,用户 sean 正在其 home 目录中进行一些操作。有两个 bash(一种 Shell)实例正在运行,并且当前目录设置为 sean 的 home 目录。还有一个名为 ct 的应用程序正运行于相同的目录,并且其标准输出(文件描述符 1)重定向到一个名为 output 的文件。要成功地卸载 /export/home,应该在通知用户以确保情况正常之后,中止这些进程。

    这个示例说明了应用程序的当前工作目录非常重要,因为它仍保持着文件资源,并且可以防止文件系统被卸载。这就是为什么大部分守护进程(后台进程)将它们的目录更改为根目录、或服务特定的目录(如 sendmail 示例中的 /var/spool/mqueue)的原因,以避免该守护进程阻止卸载不相关的文件系统。如果 sendmail 从 /export/home/sean 目录启动,并且没有将其目录更改为 /var/spool/mqueue,那么在卸载 /export/home 前必须中止它。

    如果您对非装入点目录中打开的文件感兴趣,那么必须通过 +d 或 +D 指定该目录的名称,具体使用其中的哪一个标志取决于您需要递归到子目录(+D)或者不需要递归到子目录(+d)。例如,要查看 /export/home/sean 中所有打开的文件,可以使用 lsof +D /export/home/sean。在前面的示例中,相关的目录是一个装入点,而这里与前面的示例存在细微的差别,并且限制了 lsof 和内核之间的交互。这还会引起潜在的问题,即 lsof /export/home 与 lsof /export/home/(请注意尾部的斜杠)有所区别。第一种方式可以正常工作,因为它指向了装入点。第二种方式不会生成任何输出,因为它指向了目录。如果您在 Shell 中使用 Tab 键自动完成命令,那么可能碰到这个问题,其中会帮助您添加结尾的斜杠。在这种情况下,您可以删除这个斜杠或者使用 +D 指定目录。前者是首选的方法,因为与指定任意的目录相比,其执行速度更快。

    不常见的用法

    在前面的部分中,我们研究了 lsof 的基本用法,即显示打开的文件和控制它们的进程之间的关系。当您想对系统进行一些烦琐的操作,而又不希望破坏别人重要的文档时,这种方法很有帮助。您还可以使用相同的方法执行一些高难度的 UNIX 操作。

    恢复删除的文件

    当 UNIX 计算机受到入侵时,常见的情况是日志文件被删除,以掩盖攻击者的踪迹。管理错误也可能导致意外删除重要的文件,比如在清理旧日志时,意外地删除了数据库的活动事务日志。有时可以恢复这些文件,并且 lsof 可以为您提供帮助。

    当进程打开了某个文件时,只要该进程保持打开该文件,即使将其删除,它依然存在于磁盘中。这意味着,进程并不知道文件已经被删除,它仍然可以向打开该文件时提供给它的文件描述符进行读取和写入。除了该进程之外,这个文件是不可见的,因为已经删除了其相应的目录条目。

    前面曾在转到 /proc 目录部分中说过,通过在适当的目录中进行查找,您可以访问进程的文件描述符。在随后的内容中,您看到了 lsof 可以显示进程的文件描述符和相关的文件名。您能明白我的意思吗?

    但愿它真的这么简单!当您向 lsof 传递文件名时,比如在 lsof /file/I/deleted 中,它首先使用 stat() 系统调用获得有关该文件的信息,不幸的是,这个文件已经被删除。在不同的操作系统中,lsof 可能可以从核心内存中捕获该文件的名称。清单 5 显示了一个 Linux 系统,其中意外地删除了 Apache 日志,我正使用 grep 工具查找是否有人打开了该文件。

    清单 5. 在 Linux 中使用 lsof 查找删除的文件
    # lsof | grep error_log
    httpd      2452     root    2w      REG       33,2      499    3090660
    					/var/log/httpd/error_log (deleted)
    httpd      2452     root    7w      REG       33,2      499    3090660
    					/var/log/httpd/error_log (deleted)
    ... more httpd processes ...

    在这个示例中,您可以看到 PID 2452 打开文件的文件描述符为 2(标准错误)和 7。因此,可以在 /proc/2452/fd/7 中查看相应的信息,如清单 6 所示。

    清单 6. 通过 /proc 查找删除的文件
    # cat /proc/2452/fd/7
    [Sun Apr 30 04:02:48 2006] [notice] Digest: generating secret for digest authentication
    [Sun Apr 30 04:02:48 2006] [notice] Digest: done
    [Sun Apr 30 04:02:48 2006] [notice] LDAP: Built with OpenLDAP LDAP SDK

    Linux 的优点在于,它保存了文件的名称,甚至可以告诉我们它已经被删除。在遭到破坏的系统中查找相关内容时,这是非常有用的内容,因为攻击者通常会删除日志以隐藏他们的踪迹。Solaris 并不提供这些信息。然而,我们知道 httpd 守护进程使用了 error_log 文件,所以可以使用 ps 命令找到这个 PID,然后可以查看这个守护进程打开的所有文件。

    清单 7. 在 Solaris 中查找删除的文件
    # lsof -a -p 8663 -d ^txt
    COMMAND  PID   USER   FD   TYPE        DEVICE SIZE/OFF    NODE NAME
    httpd   8663 nobody  cwd   VDIR         136,8     1024       2 /
    httpd   8663 nobody    0r  VCHR          13,2          6815752 /devices/pseudo/mm@0:null
    httpd   8663 nobody    1w  VCHR          13,2          6815752 /devices/pseudo/mm@0:null
    httpd   8663 nobody    2w  VREG         136,8      185  145465 / (/dev/dsk/c0t0d0s0)
    httpd   8663 nobody    4r  DOOR                    0t0      58 /var/run/name_service_door
    						(door to nscd[81]) (FA:->0x30002b156c0)
    httpd   8663 nobody   15w  VREG         136,8      185  145465 / (/dev/dsk/c0t0d0s0)
    httpd   8663 nobody   16u  IPv4 0x300046d27c0      0t0     TCP *:80 (LISTEN)
    httpd   8663 nobody   17w  VREG         136,8        0  145466 
                                                              /var/apache/logs/access_log
    httpd   8663 nobody   18w  VREG         281,3        0 9518013 /var/run (swap)

    我使用 -a 和 -d 参数对输出进行筛选,以排除代码程序段,因为我知道需要查找的是哪些文件。Name 列显示出,其中的两个文件(FD 2 和 15)使用磁盘名代替了文件名,并且它们的类型为 VREG(常规文件)。在 Solaris 中,删除的文件将显示文件所在的磁盘的名称。通过这个线索,就可以知道该 FD 指向一个删除的文件。实际上,查看 /proc/8663/fd/15 就可以得到所要查找的数据。

    如果可以通过文件描述符查看相应的数据,那么您就可以使用 I/O 重定向将其复制到文件中,如 cat /proc/8663/fd/15 > /tmp/error_log 。此时,您可以中止该守护进程(这将删除 FD,从而删除相应的文件),将这个临时文件复制到所需的位置,然后重新启动该守护进程。

    对于许多应用程序,尤其是日志文件和数据库,这种恢复删除文件的方法非常有用。正如您所看到的,有些操作系统(以及不同版本的 lsof)比其他的系统更容易查找相应的数据。

    查找网络连接

    网络连接也是文件,这意味着可以使用 lsof 获得关于它们的信息。您曾在清单 2 中看到过这样的示例。该示例假设您已经知道 PID,但是有时候并非如此。如果您只知道相应的端口,那么可以使用 -i 参数利用套接字信息进行搜索。清单 8 显示了对 TCP 端口 25 的搜索。

    清单 8. 查找监听端口 25 的进程
    # lsof -i :25
    COMMAND  PID USER   FD   TYPE        DEVICE SIZE/OFF NODE NAME
    sendmail 605 root    5u  IPv4 0x300010ea640      0t0  TCP *:smtp (LISTEN)
    sendmail 605 root    6u  IPv6 0x3000431c180      0t0  TCP *:smtp (LISTEN)

    需要以 protocol:@ip:port 的形式向 lsof 实用程序传递相关信息,其中的 protocol 为 TCP 或 UDP(可以使用 4 或 6 作为前缀,表示 IP 的版本),IP 为可解析的名称或 IP 地址,而 port 为数字或表示该服务的名称(来自 /etc/services)。需要一个或多个元素(端口、IP、协议)。在清单 8 中,:25 表示端口 25。输出显示,进程 605 正在使用 IPv6 和 IPv4 监听端口 25。如果您对 IPv4 不感兴趣,那么可以将筛选器改为 6:25,以表示监听端口 25 的 IPv6 套接字,或者直接使用 6 表示所有的 IPv6 连接。

    除了显示出这些守护进程正在监听的对象,lsof 还可以发现发生的连接,同样是使用 -i 参数。清单 9 显示了搜索与 192.168.1.10 之间的所有连接。

    清单 9. 搜索活动的连接
    # lsof -i @192.168.1.10
    

     

     

  • 为应用选择和创建最佳索引,加速数据读取

    由于SQL问题导致的数据库故障层出不穷,索引问题是SQL问题中出现频率最高的,常见的索引问题包括:无索引,隐式转换,索引创建不合理。

    当数据库中出现访问表的SQL没创建索引导致全表扫描,如果表的数据量很大扫描大量的数据,执行效率过慢,占用数据库连接,连接数堆积很快达到数据库的最大连接数设置,新的应用请求将会被拒绝导致故障发生。

    隐式转换是指SQL查询条件中的传入值与对应字段的数据定义不一致导致索引无法使用。常见隐式转换如字段的表结构定义为字符类型,但SQL传入值为数字;或者是字段定义collation为区分大小写,在多表关联的场景下,其表的关联字段大小写敏感定义各不相同。隐式转换会导致索引无法使用,进而出现上述慢SQL堆积数据库连接数跑满的情况。

    索引使用策略及优化

    创建索引

    • 在经常查询而不经常增删改操作的字段加索引。
    • order by与group by后应直接使用字段,而且字段应该是索引字段。
    • 一个表上的索引不应该超过6个。
    • 索引字段的长度固定,且长度较短。
    • 索引字段重复不能过多。
    • 在过滤性高的字段上加索引。

    使用索引注意事项

    • 使用like关键字时,前置%会导致索引失效。
    • 使用null值会被自动从索引中排除,索引一般不会建立在有空值的列上。
    • 使用or关键字时,or左右字段如果存在一个没有索引,有索引字段也会失效。
    • 使用!=操作符时,将放弃使用索引。因为范围不确定,使用索引效率不高,会被引擎自动改为全表扫描。
    • 不要在索引字段进行运算。
    • 在使用复合索引时,最左前缀原则,查询时必须使用索引的第一个字段,否则索引失效;并且应尽量让字段顺序与索引顺序一致。
    • 避免隐式转换,定义的数据类型与传入的数据类型保持一致。

    无索引案例

    无索引案例一

    1. 查看表结构。
      1. mysql> show create table customers;
      1. CREATE TABLE `customers` (
      2.  `cust_id` int(11) NOT NULL AUTO_INCREMENT,
      3.  `cust_name` char(50) NOT NULL,
      4.  `cust_address` char(50) DEFAULT NULL,
      5.  `cust_city` char(50) DEFAULT NULL,
      6.  `cust_state` char(5) DEFAULT NULL,
      7.  `cust_zip` char(10) DEFAULT NULL,
      8.  `cust_country` char(50) DEFAULT NULL,
      9.  `cust_contact` char(50) DEFAULT NULL,
      10.  `cust_email` char(255) DEFAULT NULL,
      11. PRIMARY KEY (`cust_id`),
      12.  ) ENGINE=InnoDB AUTO_INCREMENT=10006 DEFAULT CHARSET=utf8
    2. 执行语句。
      1. mysql> select * from customers where cust_zip = '44444' limit 0,1 \G;
    3. 执行计划。
      1. mysql> explain select * from customers where cust_zip = '44444' limit 0,1 \G;
      1. id: 1
      2. select_type: SIMPLE
      3. table: customers
      4. type: ALL
      5. possible_keys: NULL
      6. key: NULL
      7. key_len: NULL
      8. ref: NULL
      9. rows: 505560
      10.  Extra: Using where

      执行计划看到type为ALL,是全表扫描,每次执行需要扫描505560行数据,这是非常消耗性能的,那么下面将介绍优化方式。

    4. 添加索引。
      1. mysql> alter table customers add index idx_cus(cust_zip);
    5. 执行计划。
      1. mysql> explain select * from customers where cust_zip = '44444' limit 0,1 \G;
      1. id: 1
      2. select_type: SIMPLE
      3. table: customers
      4. type: ref
      5. possible_keys: idx_cus
      6. key: idx_cus
      7. key_len: 31
      8. ref: const
      9. rows: 4555
      10.  Extra: Using index condition

      执行计划看到type为ref,基于索引的等值查询,或者表间等值连接。

    无索引案例二

    1. 表结构同上案例相同,执行语句。
      1. mysql> select cust_id,cust_name,cust_zip from customers where cust_zip = '42222'order by cust_zip,cust_name;
    2. 执行计划。
      1. mysql> explain select cust_id,cust_name,cust_zip from customers where cust_zip = '42222'order by cust_zip,cust_name\G;
      1. id: 1
      2. select_type: SIMPLE
      3. table: customers
      4. type: ALL
      5. possible_keys: NULL
      6. key: NULL
      7. key_len: NULL
      8. ref: NULL
      9. rows: 505560
      10.  Extra: Using filesort
    3. 添加索引。
      1. mysql> alter table customers add index idx_cu_zip_name(cust_zip,cust_name);
    1. 执行计划。
      1. mysql> explain select cust_id,cust_name,cust_zip from customers where cust_zip = '42222'order by cust_zip,cust_name\G;
      1. id: 1
      2. select_type: SIMPLE
      3. table: customers
      4. type: ref
      5. possible_keys: idx_cu_zip_name
      6. key: idx_cu_zip_name
      7. key_len: 31
      8. ref: const
      9. rows: 4555
      10.  Extra: Using where; Using index

      order by使用字段,而且字段应该是索引字段。

    隐式转换案例

    隐式转换案例一

    1. mysql> explain select * from customers where cust_zip = 44444 limit 0,1 \G;
    1. id: 1
    2. select_type: SIMPLE
    3. table: customers
    4. type: ALL
    5. possible_keys: idx_cus
    6. key: NULL
    7. key_len: NULL
    8. ref: NULL
    9. rows: 505560
    10.  Extra: Using where
    1. mysql> show warnings;
    2. Warning Cannot use range access on index 'idx_cus' due to type or collation conversion on field 'cust_zip'

    上述案例中由于表结构定义cust_zip字段是字符串数据类型,而应用传入的是数字,导致了隐式转换,无法使用索引。

    解决方案:

    1. 将cust_zip字段修改为数字数据类型。
    2. 将应用中传入的字符类型改为数据类型。

    隐式转换案例二

    1. 查看表结构。
      1. mysql> show create table customers1;
      1. CREATE TABLE `customers1` (
      2.  `cust_id` varchar(10) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
      3.  `cust_name` char(50) NOT NULL,
      4. KEY `idx_cu_id` (`cust_id`)
      5.  ) ENGINE=InnoDB DEFAULT CHARSET=utf8
      6. mysql> show create table customers2;
      7. CREATE TABLE `customers2` (
      8.  `cust_id` varchar(10) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
      9.  `cust_name` char(50) NOT NULL,
      10. KEY `idx_cu_id` (`cust_id`)
      11.  ) ENGINE=InnoDB DEFAULT CHARSET=utf8
    2. 执行语句。
      1. mysql> select customers1.* from customers2 left join customers1 on customers1.cust_id=customers2.cust_id where customers2.cust_id='x';
    3. 执行计划。
      1. mysql> explain select customers1.* from customers2 left join customers1 on customers1.cust_id=customers2.cust_id where customers2.cust_id='x'\G;
      1.  *************************** 1. row ***************************
      2. id: 1
      3. select_type: SIMPLE
      4. table: customers2
      5. type: ref
      6. possible_keys: idx_cu_id
      7. key: idx_cu_id
      8. key_len: 33
      9. ref: const
      10. rows: 1
      11.  Extra: Using where; Using index
      1.  *************************** 2. row ***************************
      2. id: 1
      3. select_type: SIMPLE
      4. table: customers1
      5. type: ALL
      6. possible_keys: NULL
      7. key: NULL
      8. key_len: NULL
      9. ref: NULL
      10. rows: 1
      11.  Extra: Using where; Using join buffer (Block Nested Loop)
    4. 修改COLLATE。
      1. mysql> alter table customers1 modify column cust_id varchar(10) COLLATE utf8_bin ;
    5. 执行计划。
      1. mysql> explain select cust_id,cust_name,cust_zip from customers where cust_zip = '42222'order by cust_zip,cust_name\G;
      1. id: 1
      2. select_type: SIMPLE
      3. table: customers2
      4. type: ref
      5. possible_keys: idx_cu_id
      6. key: idx_cu_id
      7. key_len: 33
      8. ref: const
      9. rows: 1
      10.  Extra: Using where; Using index
      1. id: 1
      2. select_type: SIMPLE
      3. table: customers1
      4. type: ref
      5. possible_keys: idx_cu_id
      6. key: idx_cu_id
      7. key_len: 33
      8. ref: const
      9. rows: 1
      10.  Extra: Using where

      字段的COLLATE一致后执行计划使用到了索引,所以一定要注意表字段的collate属性的定义保持一致。

    总结

    在使用索引时,我们可以通过explain查看SQL的执行计划,判断是否使用了索引以及发生了隐式转换,创建合适的索引。索引太复杂,创建需谨慎。

  • 数据库变慢的分析

    问题描述:用户的数据库发现相同的一条sql 语句,数据量百万级左右,在原来SQL 中执行大概是0.015s,而在云数据库下直接运行是5分左右,执行非常的慢,已经严重的影响了用户使用云数据库使用的信心。

    可能原因:为什么在用户的数据库上执行只需要0.015s,而到云数据库后变为了5分?根据经验,很有可能是SQL 的执行计划改变了,而导致执行时间剧增。

    问题排查:通过explain 查看sql 的执行计划,一步一步进行优化。

    通过分析,我们可以从执行计划上分析b 表做了一个全表扫描(执行计划的最后一行),查看b 表中tid 并无索引,所以我们这里可以进行优化,来减少查询过程中关联的行数,来达到优化:

    ———————————————————————————————————————

     

    我们可以看到执行计划中的rows 已经从452变为了2(执行计划的最后一行),

    由于mysql 表关联只有nest loop join 这种算法,所以我们可以估算一下这里的优化:

    原始执行一:1055789*1*1*1*1*452 扫描的行数

    新执行计划二:1055789*1*1*1*1*2 扫描的行数

    执行时间:

     

    我们看到执行时间已经由原来的6分20秒下降到了10秒,我们继续优化;

    可以看到该sql 的结果集只有区区的8行,但是扫描的行数却是非常之大的(1055789*1*1*1*1*2),在优化sql 的非常关键的一点就是优化sql 的执行路程,t=s/v;如果我们能够优化S,那么速度肯定会一下子提上来; 那么我们在看看sql 中最后的一句:

    -> WHERE EXISTS

    -> (SELECT 1 FROM xxxx_test5 b WHERE a.tid = b.tid);

    sql 查询中是要查询出每笔订单的详细信息而不得不关联其他一些表,但是最后的一个exist 限定了我们最后结果的范围,在看看xxxx_test5 这张表有多大:

    mysql> SELECT COUNT(*) FROM xxxx_test5;

    +———-+

    | COUNT(*) |

    +———-+

    | 403 |

    +———-+

    1 ROW IN SET (0.00 sec)

    mysql> SELECT COUNT(*) FROM xxxx_test5 b ,xxxx_test a WHERE

    a.tid = b.tid ;

    +———-+

    | COUNT(*) |

    +———-+

    | 8 |

    +———-+

    1 ROW IN SET (0.42 sec)

    两张表关联后只有8行记录,如果我们将订单表xxxx_test 和限定表先做关联,在和其他的一些订单信息表做连接,将会极大减小关联的行数;在进一步改写sql:

     

    分析执行计划,我们发现限定表xxxx_test5做了驱动表,驱动表的变化才是导致问题的最根本原因,扫描的行数:452*1*1*1*1; 这个时候sql 的执行速度就飞一般感觉了:

    Mysql–>;

    SELECT a.oi………….

    ……..省去结果

    8 ROWS IN SET (0.13 sec)

    总结:由于环境迁移,导致sql 执行计划改变,这就是云数据库变慢的最终原因了。

     

    案例二:隐式转换导致全表扫描

    问题描述:用户网站打开缓慢,质疑云数据库性能不好。

    可能原因:用户的数据存放在数据库中,网站访问数据库的时间较长,大多web应用程序设计,SQL没有优化或索引建立的不好导致;

    问题排查:通过查看数据库的慢日志,发现大量的慢sql,执行时间超过了2S。

    UPDATE USER SET xx=xx+N.N WHERE

    account=130000870343 LIMIT 10

    SELECT * FROM USER WHERE

    account=13056870 LIMIT 10

    怀疑在user 表上是否建立索引:

    CREATE TABLE `user` (

    `id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,

    `account` char(11) NOT NULL COMMENT ‘???’,

    …………………….

    …………………….

    PRIMARY KEY (`id`),

    UNIQUE KEY `username` (`account`),

    …………………….

    ) ENGINE=InnoDB CHARSET=utf8 ;

    查看执行计划,居然查询使用了全表扫描: db@3027 16:55:06>explain

    select * from user where account=13056870343;

    +—-+————-+——–+——+—————+——+———+——+——+————-+

    | id | select_type | table | type | possible_keys | key | key_len | ref |

    rows | Extra |

    +—-+————-+——–+——+—————+——+———+——+——+————-+

    | 1 | SIMPLE | t_user | ALL | username | NULL | NULL | NULL | 799 |

    Using where |

    +—-+————-+——–+——+—————+——+———+——+——+————-+

    1 row in set (0.00 sec)

    为什么这里会是全表扫描?account 上不是已经建立索引来吗?仔细一看,

    account 定义为了字符串,而传入的条件为数字,我们知道数字的精度是比字符串高的,所以这里做了隐士转换:to_number(account)=13056870343

    (to_number 为将字符串转换为数字),这样即使account 上有索引,也没法使用了,因此我们将传入的数字改为字符串:

    db@3027 16:55:13>EXPLAIN SELECT * FROM USER WHERE

    account=’13056870343′;

    +—-+————-+——–+——-+—————+———-+——

    | id | select_type | TABLE | TYPE | possible_keys | KEY |

    key_len | REF | ROWS | Extra |

    +—-+————-+——–+——-+—————+———-+——

    | 1 | SIMPLE | t_user | const | username | username | 33

    | const | 1 | |

    +—-+————-+——–+——-+—————+———-+——

    1 ROW IN SET (0.00 sec)

    可以看到数据已经能够索引到索引username 了。

    总结:由于用户在设计表结构的时候字段定义使用了字符串,而传入的条件却传入了数字造成了隐士转换,这是数据库应用中经常出现的典型问题; 数据库足够稳定,但不论在怎么强的数据库,也经不起劣质SQL 的挑战,优化sql 是长期的一项优化措施。

    从上面的三个案例,我们可以总结一下,用户在使用数据库的时候,发现数据库执行sql 超时,性能较差,连接超时等等这些问题,大多数情况下,是由于应用程序的设计,sql 没有优化,或者索引建立的不好而导致;除非实例不可用(主机down 掉,实例服务停掉,实例由于空间太大而被锁定)而导致用户应用不可用(实例的故障RDS 会有监控报警)。

  • 慢SQL排查方法  

    慢 SQL 的一般排查步骤为:
    1.定位慢 SQL;
    2.定位性能损耗节点;
    3.定位性能损耗原因并处理。
    说明:排查过程中,建议通过 MySQL 命令行进行连接:mysql -hIP -PPORT -uUSER -pPASSWORD -c 。请务必加上 “-c”,防止 MySQL 客户端过滤掉注释(默认)从而影响 HINT 的执行。
    定位慢 SQL
    定位慢 SQL 一般有两种场景:历史信息可从慢 SQL 记录中查询;实时慢 SQL 执行信息可使用SHOW PROCESSLIST 指令展示。
    查看慢 SQL 记录
    执行以下指令查询慢 SQL Top 10: mysql> SHOW SLOW limit 10;
    查看当前实时 SQL 执行信息: mysql>SHOW PROCESSLIST WHERE COMMAND != ‘Sleep’;
    0004.png

    定位性能损耗节点

    从慢 SQL 记录或者实时 SQL 执行信息中定位到慢 SQL 后,可以执行 TRACE 指令跟踪该 SQL 的运行时间,以便定位瓶颈。TRACE 命令会实际执行 SQL,在执行过程中记录所有节点消耗的时间,并返回执行结果。
    针对定位的慢 SQL,可以执行以下指令: mysql> trace select detail_url, sum(distinct price) from t_item group by detail_url;
    TRACE 指令执行完毕后,可以执行 SHOW TRACE 命令查看结果,根据每个组件的时间消耗来判断慢 SQL 的瓶颈: mysql> SHOW TRACE;
    SHOW TRACE 返回的结果中,根据 TIME_COST (单位毫秒)列可以判断哪个节点上的执行时间消耗大。同时可以看到对应的 GROUP_NAME (即 DRDS/RDS 节点),以及 STATEMENT 列信息(即正在执行的 SQL)。
    将组装好的 HINT 及带 EXPLAIN 前缀的 STATEMENT 拼装成新的 SQL 并执行。EXPLAIN 指令不会真正执行,而只是显示该 SQL 的执行计划信息。出现了 Using temporary; Using filesort 现象,说明没有正确的使用索引从而导致执行缓慢。此时可以修正索引问题后重新执行。

  • 创建分表

    通过分区字段(shardkey)把一个大表水平拆分到多个数据库,下面给大家介绍下分表的方法:

    如何选择分区字段

    一旦定好分区字段,就不能轻易修改分区字段,因此开发人员需要提前评估。选择分区字段的时候主要考虑两个维度:

    • 通过该字段能否对数据进行均衡的存储和访问
    • 多个相关联的表能否使用同一个字段。(相同分区字段数据会存储在同一个物理分片中,大多数的业务逻辑需要进行join时,可无需走分布式事务逻辑而直接在单节点内执行,效率大大提示)

    举个例子,如果业务有两张表,一个用于记录用户的基本信息,一个用于用户的订单信息,此时如果选取用户ID作为shardkey,则理论数据分布和访问都会比较均衡,同时单个用户对应的基本信息和订单信息都会在一个后端数据库中,方便后续的join等操作。

    shardkey选择的限制

    普通的分表创建时在最后面指定shardkey的值,该值为表中的一个字段名字,会用于后续sql的路由选择:

    shardkey字段有限定要求如下:

    1.如存在主键或者唯一索引,则shardkey字段必须是主键以及所有唯一索引的一部分
    2.shardkey字段的类型必须是int,bigint,smallint/char/varchar
    3.shardkey字段的值尽量使用ascii码,网关不会转换字符集,所以不同字符集可能会路由到不同的分区(且尽量不要有中文)
    4.不要update shardkey字段的值,如必须则先delete,再insert5.`shardkey=` 放在create语句的最后面,如下示例
    6.访问数据尽量都能带上shardkey字段
    
    
    

    创建一张分表

        mysql> create table test.right ( a int not null,b int not null, c char(20) not null,primary key(a,b) ,unique key(a,c)) shardkey=a;
        Query OK, 0 rows affected (0.12 sec)
    

    shardkey'是系统标记分片字段的关键字,不可占用。
    shardkey=noshardkey_allset定义该表为广播表的关键字,广播表表示该表不分表,但会在每个物理分片中都存储一份。

    常见DML操作

    使用分表时,对DML有一定的要求,具体如下(下面的例子中a为shardkey):

    SELECT最好带上shardkey

    select最好带上shardkey字段,由于分布式路由默认采用hash方式。

    • 若是=或者in,路由将自动跳转到对应分片,此时效率最高。
    • 若无=或者in,分布式系统会自动全表扫描,然后在网关进行结果集聚合,此时效率较低:

    例如:下面两条sql根据shardkey的值直接可以发送到对应的数据库,通常5ms内可以处理完成。

        mysql> select a,b,c from test.test1 where a=2 order by b;
        mysql> select a,b,c from test.test1 where a in (2) order by b;
    

    例如:如下的sql都会发到所有的后端数据库,然后需要对数据进行额外的汇总排序,通常要5~20ms才能处理完成。

        mysql> select a,b,c from test.test1 where a>2 order by b;
        mysql> select a,b,c from test.test1 where c=2 order by b;
    

    insert/replace字段必须包含shardkey

    insert/replace字段必须包含shardkey,否则路由不知道应该将数据插入到哪个物理分片,会拒绝执行该sql;

        mysql> insert into test.test1 (b,c) values(4,"record3");
        ERROR 1105 (07000): Proxy Warning - sql have no shardkey
    
        mysql> insert into test.test1 (a,c) values(4,"record3");
        Query OK, 1 row affected (0.01 sec)
    

    使用广播表或单表时除外。

    delete/update字段必须包含shardkey

    delete/update时为了安全考虑,执行该类sql的时候必须带有where条件,系统拒绝执行该sql命令,where条件最好也和select一样带上shardkey:

        mysql> delete from test.test1;
        ERROR 1005 (07000): Proxy Warning - sql is not legal,tokenizer_gram went wrong
        mysql> delete from test.test1 where a=1;
        Query OK, 1 row affected (0.01 sec)
    

    使用广播表或单表时除外。

    修改shardkey字段值

    同时update不能修改shardkey的字段的值,需要的话先insert再delete

        mysql> update test.test1 set a=10 where d=1;
        ERROR 7013 (HY000): Proxy ERROR:combine_sql_key return null,something went wrong
        mysql> update test.test1 set d=1 where a=1;
        Query OK, 0 rows affected (0.00 sec)
    

    不能更换shardkey字段类型、修改字段名称、删除shardkey字段、或更换shardkey字段,除非您新建一个表。

    常见问题:

    表没有主键:

        mysql> create table test.e1 ( a int ,b int) shardkey=a;
        ERROR 1105 (HY000): This table type requires a primary key
    

    主键或者唯一键没有包含shardkey:

        mysql> create table test.e2 ( a int not null,b int not null, c char(20) not null,primary key(a,b) ) shardkey=c;
        ERROR 1105 (HY000): A PRIMARY KEY must include all columns in the table's partitioning function
    

    shardkey的拼写错误或者列名错误:

        mysql> create table test.e3 ( a int key,b int,c char(20)) shardkey1=d;
        ERROR 1911 (HY000): Unknown option 'shardkey1'
        mysql> create table test.e4 ( a int key,b int,c char(20)) shardkey=d;
        ERROR 7008 (HY000): Proxy ERROR:shardkey must be one of the column
  • SQL 优化方法(二)

    Mysql 数据库作为数据持久化的存储系统,在实际业务中应用广泛。在应用也经常会因为 SQL 遇到各种各样的瓶颈。最常用的 Mysql 引擎是 innodb,索引类型是 B-Tree 索引,增删改查等操作最经常遇到的问题是“查”,查询又以索引为重点。

    接下来的内容,安排如下:

    1. 介绍索引的工作原理
    2. 引用实例具体介绍索引
    3. 如何使用 explain 排查线上问题
    4. 实际碰到的问题汇总

    索引如何工作

    当查询时,Mysql 的查询优化器会使用统计数据预估使用各个索引的代价(COST),与不使用索引的代价(COST)比较。Mysql 会选择代价最低的方式执行查询。Mysql 如何使用索引,可以用下面的伪代码来说明:

    min_cost = INIT_VALUE
    
    min_cost_index = NONE
    
    for(index in all_indexs):
    
        if (index match WHERE_CLAUSE):
    
            cur_cost = COST(index)
    
            if(cur_cost < min_cost):
    
                min_cost = cur_cost
    
                min_cost_index = index
    

    INIT_VALUE:不使用索引时的代价

    all_indexs:查询表上所有的索引 COST:基本是由“估计需要扫描的行数”(rows)来确定

    WHERE_CLAUSE:查询 SQL 中的 WHERE 子句

    大致的意思:Mysql 会遍历该查询相关的表(table)的每一条索引,然后判断该索引能否被本次查询使用(possible_keys)。当索引可以使用时,Mysql 预估使用该索引进行查询的 cost ,然后选择预估代价最低的代价的方式(key)执行查询。

    索引匹配(match)

    怎样判断索引是否匹配(match)SQL查询?

    1、索引的左前缀规则;索引中的列由左向右逐一匹配,如果中间某一列不能使用索引则后序列不在查询中不再被使用。

    例如,如果有一个3列索引(str_col1,col2,col3),其中str_col1为字符串,则对(str_col1)(str_col1,col2)(str_col1,col2,col3)上的查询进行了索引。

    如果列不构成索引最左面的前缀,MySQL 不能使用索引。假定有下面显示的 SELECT 语句。

    SELECT * FROM tbl_name WHERE str_col1=val1;
    
    SELECT * FROM tbl_name WHERE str_col1=val1 AND col2=val2;
    
    SELECT * FROM tbl_name WHERE col2=val2;
    
    SELECT * FROM tbl_name WHERE col2=val2 AND col3=val3;
    

    如果(str_col1,col2,col3)有一个索引,只有前2个查询使用索引。第3个和第4个查询确实包括索引的列,但(col2)(col2,col3)不是(col1,col2,col3)的最左边的前缀。

    2、where 语句中列的表达式为 = 、 > 、 >= 、< 、<= 、BETWEEN 、ISNULL 或者 LIKE ’ pattern ’(其中’ pattern ’不以通配符开始)

    3、每个 AND 组作为表达式匹配索引。

    SELECT * FROM tbl_name WHERE (str_col1=val1 OR col4 =val4) AND col2=val2;

    因为str_col1=val1ORcol4 =val4作为一组,col4不匹配索引中的列,所以查询不匹配索引。

    4、如果表达式中存在类型转换或者列上有复杂函数则与该列不匹配索引中的列。

    SELECT * FROM tbl_name WHERE str_col1=1;
    
    SELECT * FROM tbl_name WHERE SUBSTRING(str_col1,1,8) = ‘title’;
    

    第1个查询,因为1是整数、str_col1是字符串,所以不匹配索引;第2个查询str_col1有复杂函数,同样不匹配索引。

    索引的COST

    Mysql 如何计算索引的 COST?

    索引的 cost 基本是由“估计需要扫描的行数”(rows)来确定。数据来源于information_schema,在 Mysql 启动的时候读入内存,运行时只使用内存值,存储引擎会动态更新这些值。

    我们可以通过 explain 看下“估计需要扫描的函数”,可以通过optimizer_trace查询适用每一条 SQL 的具体的 cost 值。explain 也是线上排查问题的利器,后面会重点介绍。

    索引实例分析

    索引的字段究竟是怎么从 where 语句中提取,并被 Mysql 使用呢,下面将以一个实例分析这个过程。内容全文为摘取何登成的文章《 SQL 中的 where 条件,在数据库中提取与应用浅析》,并做了部分删改。

    我们创建一张测试表,一个索引索引,然后插入几条记录。(注意:下面的实例,使用的表的结构不是 InnoDB 引擎所采用的聚簇索引表。图例仅为说明,原理适用 innodb )

    create table t1 (a int primary key, b int, c int, d int, e varchar(20));
    
    create index idx_t1_bcd on t1(b, c, d);
    
    insert into t1 values (4,3,1,1,’d’);
    insert into t1 values (1,1,1,1,’a’);
    insert into t1 values (8,8,8,8,’h’):
    insert into t1 values (2,2,2,2,’b’);
    insert into t1 values (5,2,3,5,’e’);
    insert into t1 values (3,3,2,2,’c’);
    insert into t1 values (7,4,5,5,’g’);
    insert into t1 values (6,6,4,4,’f’);
    

    t1表的存储结构如下图所示(只画出了idx_t1_bcd索引与 t1 表结构,没有包括 t1 表的主键索引):

    简单说明上图,idx_t1_bcd索引上有[b,c,d]三个字段,不包括[a,e]字段。idx_t1_bcd索引,首先按照b字段排序,b字段相同,则按照c字段排序,以此类推。

    考虑以下 SQL :

    select * from t1 where b >= 2 and b < 8 and c > 1 and d != 4 and e != ‘a’;

    可以发现where条件使用到了[b,c,d,e]四个字段,而 t1 表的idx_t1_bcd索引,恰好使用了[b,c,d]这三个字段,那么走idx_t1_bcd索引进行条件过滤,应该是一个不错的选择。

    所有SQL的where条件,均可归纳为3大类:Index Key (First Key & Last Key),Index Filter,Table Filter。

    接下来,让我们来详细分析者3大类分别是如何定义,以及如何提取的。

    1、Index Key

    用于确定 SQL 查询在索引中的连续范围(起始范围+结束范围)的查询条件,被称之为 Index Key。由于一个范围,至少包含一个起始与一个终止,Index Key 也被拆分为 Index First Key 和 Index Last Key ,分别用于定位索引查找的起始,以及索引查询的终止条件。

    • Index First Key

    提取规则:从索引的第一个键值开始,检查其在where条件中是否存在,若存在并且条件是= 、>= ,则将对应的条件加入Index First Key 之中,继续读取索引的下一个键值,使用同样的提取规则;若存在并且条件是>,则将对应的条件加入Index First Key 中,同时终止Index First Key的提取;若不存在,同样终止 Index First Key 的提取。

    针对上面的SQL,应用这个提取规则,提取出来的 Index First Key 为(b >= 2, c > 1)。由于 c 的条件为 >,提取结束,不包括d。

    • Index Last Key

    提取规则:从索引的第一个键值开始,检查其在 where 条件中是否存在,若存在并且条件是=、<=,则将对应条件加入到Index Last Key中,继续提取索引的下一个键值,使用同样的提取规则;若存在并且条件是 < ,则将条件加入到Index Last Key中,同时终止提取;若不存在,同样终止 Index Last Key 的提取。

    针对上面的SQL,应用这个提取规则,提取出来的 Index Last Key 为(b < 8),由于是 < 符号,因此提取b之后结束。

    2、Index Filter

    在完成 Index Key 的提取之后,我们根据 where 条件固定了索引的查询范围,但是此范围中的项,并不都是满足查询条件的项。在上面的 SQL 用例中,(3,1,1),(6,4,4)均属于范围中,但是又均不满足 SQL 的查询条件。

    Index Filter 的提取规则:同样从索引列的第一列开始,检查其在 where 条件中是否存在:若存在并且 where 条件仅为 =,则跳过第一列继续检查索引下一列,下一索引列采取与索引第一列同样的提取规则;若 where 条件为 >=、>、<、<= 其中的几种,则跳过索引第一列,将其余 where 条件中索引相关列全部加入到Index Filter之中;若索引第一列的where条件包含 =、>=、>、<、<= 之外的条件,则将此条件以及其余 where 条件中索引相关列全部加入到 Index Filter 之中;若第一列不包含查询条件,则将所有索引相关条件均加入到 Index Filter 之中。

    针对上面的用例 SQL,索引第一列只包含 >=、< 两个条件,因此第一列可跳过,将余下的c、d两列加入到 Index Filter 中。因此获得的 Index Filter 为 c > 1 and d != 4 。

    3、Table Filter

    Table Filter 是最简单,最易懂,也是提取最为方便的。提取规则:所有不属于索引列的查询条件,均归为 Table Filter 之中。

    同样,针对上面的用例 SQL,Table Filter 就为 e != ‘a’。

    根据以上实例其实可以总结出一些规律,WHERE 语句究竟怎样(是否)匹配索引,不用迷信出自他人之口的规则。只需要简单的按照索引自左向右的每一列,从 WHERE 语句提取条件,能否从索引树的根节点出发,到达索引树的叶节点,成功匹配出一个或几个范围区间,即能自己自行判断是否能使用索引。反过来,最左前缀匹配、Like 不能以通配符开始、AND 分组,也都是由 B-Tree 本身特性决定的。

    索引问题排查

    前面我们谈使用索引的 cost 的值提到过explain。下面介绍 explain 的值,并以一个实际遇到的问题说明如何排查问题。

    Explain详解

    使用一个示例 SQL 来解释 explain :

    select id from r_ibeacon_biz_device_d where ftime >= 20151126 and ftime <= 20151126 and biz_id = 11602 limit 50;

    IDX_BID_FTIME<biz_id, ftime>是表r_ibeacon_biz_device_d的其中一条索引。
    Biz_id,ftime均为bigint类型。

    我们着重关注几个重点字段的重点值:

    - type:索引的使用方式

      eq_ref      …  索引,关联匹配若干行
       ref          …  索引(前缀)匹配   
        range        …  索引范围扫(BETWEEN、IN、>=、LIKE)得到数据
       index        …  索引全扫描
        all           …  表全扫描
    

    示例中使用的索引是使用全索引范围扫描,所以type为range

    - possible_keys:适用查询的索引列表。示例中有三条索引适用本次查询。

    - key: 查询实际执行使用的索引。示例使用的为IDX_BID_FTIME

    - key_len:查询使用索引的长度。

     null    1字节
       tinyint  1字节
       int    4字节
       bigint  8字节
       double  8字节
       datetime 8字节
       timestamp 4字节
       varchr(10)变长字段且允许NULL: 10*(Character Set:utf8=3,gbk=2,latin1=1)+1(NULL)+2(变长字段)
       char(10)固定字段且允许NULL: 10*(Character Set:utf8=3,gbk=2,latin1=1)+1(NULL)
    

    以上是常用类型的长度,示例中key_len为18,即:8字节( biz_id bigint )+1字节( biz_id 允许为 null )+8字节( ftimebigint )+1字节( ftime 允许为 null )。所以本次查询是使用了索引的所有字段加速查询

    – rows:查询预估扫描的行数

    Explain跟进问题

    摇一摇周边后台的数据统计接口尔会有小尖峰,涉及了一条 SQL:

    一条SQL搞定卡方检验计算select d.id from r_ibeacon_biz_page_d d where d.ftime >= 20151126 and d.ftime <= 20151126 and d.biz_id = 11023 and d.page_id = 778495 limit 0,20;
    r_ibeacon_biz_page_d 的主要字段信息如下:

    ftime  bigint(20)
    biz_id  bigint(20) 
    page_id varchar(200)
    

    索引为:IDX_BID_PID_FTIME<biz_id,page_id,ftime>

    Explain结果如下

    观察以上explain结果可以看到一切正常,SQL“符合预期”的走了索引。但是rows稍微多了点,但是看起来也“好像”ok。但是问题就是出现尖峰。

    问题排查:

    首先,注意到的一点就是 explain 中的 type 异常,是 ref 。按照上面的解释,如果走了索引那应该是 range 类型才对啊。

    其次,观察key_len,9,发现确实有些不对,怎么会这么小。按照类型所占字节,9刚好为biz_id的长度,确定这条SQL 虽然走了索引,但是只使用了 biz_id 字段。原因呢?

    然后执行“desc r_ibeacon_biz_page_d”,查看表结构的索引字段,突然发现page_id的类型怎么是 varchar,再看SQL中page_id=11023。突然意识到了什么,此时刚好违反索引匹配的第四条规则。更改SQL“page_id=11023”为“page_id=‘11023’”验证,如下

    可以看到type=rangekey_len=621,符合预期。接下来要做的就是更改表中page_id的类型为 bigint。隔天再看接口的尖峰果然削平。

    Explain 是一个很好的工具,可以用来验证 SQL 是否使用了索引,更重要的是验证 SQL 是否如预期的使用索引上。排查线上问题还有 profile 和 optimizer_trace,由于实际没有太多用到暂且不表。

    常见问题汇总

    – Range怎么使用索引?

    详见上文

    - Order by使用索引吗?

    该问题可以由以下资料解释:

    SQL queries with an order by clause don’t need to sort the result explicitly if the relevant index already delivers the rows in the required order. That means the same index that is used for the where clause must also cover the order by clause.
    总之一句话:索引本身并不能避免排序,当根据索引取出的数据已经满足order by子句的要求就可以避免排序操作。

    - order by太慢?

    避免数据排序,采用索引排序(分页查询文艺写法)

    `- limit offset太慢?

    避免大offset,使用where语句过滤更多的行。更多参考的实践《 Efficient Pagination Using MySQL 》

    – 为什么不走索引(索引也走了,还是慢)?

    类型是否一致: int vs char(varchar)、varchar(32)vs varchar(64)
    字符集是否一致:涉及表关联时,两表字符集是否一致。

  • SQL 优化方法

    SQL 优化的基本原则

    在MySQL 执行的 SQL 计算称为可下推计算。可下推计算能够减少数据传输,减少网络层的开销,提升 SQL 语句的执行效率。

    因此,SQL 语句优化的基本原则为:尽量让更多的计算可下推到 MySQL 上执行。

    可下推计算主要包括:

    • JOIN 连接;
    • 过滤条件,如 WHERE 或 HAVING 中的条件;
    • 聚合计算,如 COUNT,GROUP BY 等;
    • 排序,如 ORDER BY;
    • 去重,如 DISTINCT;
    • 函数计算,如 NOW() 函数等;
    • 子查询。

    注意:上述列表只是列出可下推计算的各种可能形式,并不代表所有的子句/条件或者子句/条件的组合一定是可下推计算。

    不同类型和条件的 SQL 优化有不同的侧重点和方法,下面将针对以下几种情况介绍 SQL 优化的具体方法:

    • 单表 SQL 优化
      • 过滤条件优化
      • 查询返回行数优化
      • 分组及排序优化
    • JOIN 优化
      • 可下推的 JOIN 优化
      • 分布式 JOIN 优化
    • 子查询优化

    单表 SQL 优化

    单表 SQL 优化有以下几个原则:

    • SQL 语句尽可能带有拆分键;
    • 拆分键的条件尽可能是等值条件;
    • 如果拆分键的条件是 IN 条件,则 IN 后面的值的数目应尽可能少(需要远少于分片数,并且数目不会随业务的增长而增多);
    • 如果 SQL 语句不带有拆分键,那么 DISTINCT、GROUP BY 和 ORDER BY 在同一个 SQL 语句中尽量只出现一种。

    过滤条件优化

    DRDS 的数据是按拆分键水平切分的,过滤条件中应尽量包含带有拆分键的条件,可以让 DRDS 根据拆分键对应的值将查询直接下推到特定的分库,避免 DRDS 做全表扫描。

    例如,表 test 的拆分键是 c1,过滤条件中若不带有拆分键,会做全表扫描:

    1. mysql> SELECT * FROM test WHERE c2 = 2;
    2. +----+----+
    3. | c1 | c2 |
    4. +----+----+
    5. | 2 | 2 |
    6. +----+----+
    7. 1 row in set (0.05 sec)

    对应的执行计划为:

    1. mysql> EXPLAIN SELECT * FROM test WHERE c2 = 2;
    2. +------------------------------------------------+--------------------------------------------------------------------+--------+
    3. | GROUP_NAME | SQL | PARAMS |
    4. +------------------------------------------------+--------------------------------------------------------------------+--------+
    5. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0004_RDS | select `test`.`c1`,`test`.`c2` from `test` where (`test`.`c2` = 2) | {} |
    6. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0007_RDS | select `test`.`c1`,`test`.`c2` from `test` where (`test`.`c2` = 2) | {} |
    7. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0005_RDS | select `test`.`c1`,`test`.`c2` from `test` where (`test`.`c2` = 2) | {} |
    8. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0002_RDS | select `test`.`c1`,`test`.`c2` from `test` where (`test`.`c2` = 2) | {} |
    9. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0003_RDS | select `test`.`c1`,`test`.`c2` from `test` where (`test`.`c2` = 2) | {} |
    10. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0006_RDS | select `test`.`c1`,`test`.`c2` from `test` where (`test`.`c2` = 2) | {} |
    11. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0000_RDS | select `test`.`c1`,`test`.`c2` from `test` where (`test`.`c2` = 2) | {} |
    12. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0001_RDS | select `test`.`c1`,`test`.`c2` from `test` where (`test`.`c2` = 2) | {} |
    13. +------------------------------------------------+--------------------------------------------------------------------+--------+
    14. 8 rows in set (0.00 sec)

    含拆分键的过滤条件的取值范围越小,越有助于提高 DRDS 的查询速度。

    例如,对表 test 查询时包含带有拆分键 c1 的范围过滤条件:

    1. mysql> SELECT * FROM test WHERE c1 > 1 AND c1 < 4;
    2. +----+----+
    3. | c1 | c2 |
    4. +----+----+
    5. | 2 | 2 |
    6. | 3 | 3 |
    7. +----+----+
    8. 2 rows in set (0.04 sec)

    对应的执行计划为:

    1. mysql> EXPLAIN SELECT * FROM test WHERE c1 > 1 AND c1 < 4;
    2. +------------------------------------------------+--------------------------------------------------------------------------------------------+--------+
    3. | GROUP_NAME | SQL | PARAMS |
    4. +------------------------------------------------+--------------------------------------------------------------------------------------------+--------+
    5. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0002_RDS | select `test`.`c1`,`test`.`c2` from `test` where ((`test`.`c1` > 1) AND (`test`.`c1` < 4)) | {} |
    6. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0003_RDS | select `test`.`c1`,`test`.`c2` from `test` where ((`test`.`c1` > 1) AND (`test`.`c1` < 4)) | {} |
    7. +------------------------------------------------+--------------------------------------------------------------------------------------------+--------+
    8. 2 rows in set (0.00 sec)

    等值条件会比范围条件执行得更快。例如:

    1. mysql> SELECT * FROM test WHERE c1 = 2;
    2. +----+----+
    3. | c1 | c2 |
    4. +----+----+
    5. | 2 | 2 |
    6. +----+----+
    7. 1 row in set (0.03 sec)

    对应的执行计划为:

    1. mysql> EXPLAIN SELECT * FROM test WHERE c1 = 2;
    2. +------------------------------------------------+--------------------------------------------------------------------+--------+
    3. | GROUP_NAME | SQL | PARAMS |
    4. +------------------------------------------------+--------------------------------------------------------------------+--------+
    5. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0002_RDS | select `test`.`c1`,`test`.`c2` from `test` where (`test`.`c1` = 2) | {} |
    6. +------------------------------------------------+--------------------------------------------------------------------+--------+
    7. 1 row in set (0.00 sec)

    此外,在向拆分表中插入数据时,插入字段中必须带有拆分键。

    例如,向表 test 中插入数据时带有拆分键 c1:

    1. mysql> INSERT INTO test(c1,c2) VALUES(8,8);
    2. Query OK, 1 row affected (0.07 sec)

    查询返回行数优化

    DRDS 在执行带有 LIMIT [ offset, ] row_count 的查询时,实际上是依次将 offset 之前的记录读取出来并直接丢弃,这样当 offset 非常大的时候,即使 row_count 很小,也会导致查询非常缓慢。例如以下的 SQL:

    1. SELECT * 
    2. FROM sample_order
    3. ORDER BY sample_order.id
    4. LIMIT 10000, 2

    它虽然只返回第10000与10001两条记录,可它的执行时间为12秒左右,这是因为 DRDS 实际读取的记录数为10002条:

    1. mysql> SELECT * FROM sample_order ORDER BY sample_order.id LIMIT 10000,2;
    2. +--------------+------------+--------------+--------------+------------+
    3. | id | sellerId | trade_id | buyer_id | buyer_nick |
    4. +--------------+------------+--------------+--------------+------------+
    5. | 242012755468 | 1711939506 | 242012755467 | 244148116334 | zhangsan |
    6. | 242012759093 | 1711939506 | 242012759092 | 244148138304 | wangwu |
    7. +--------------+------------+--------------+--------------+------------+
    8. 2 rows in set (11.93 sec)

    对应的执行计划为:

    1. mysql> EXPLAIN SELECT * FROM sample_order ORDER BY sample_order.id LIMIT 10000,2;
    2. +------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------+
    3. | GROUP_NAME | SQL | PARAMS |
    4. +------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------+
    5. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0004_RDS | select `sample_order`.`id`,`sample_order`.`sellerId`,`sample_order`.`trade_id`,`sample_order`.`buyer_id`,`sample_order`.`buyer_nick` from `sample_order`order by `sample_order`.`id` asc limit 0,10002 | {} |
    6. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0007_RDS | select `sample_order`.`id`,`sample_order`.`sellerId`,`sample_order`.`trade_id`,`sample_order`.`buyer_id`,`sample_order`.`buyer_nick` from `sample_order`order by `sample_order`.`id` asc limit 0,10002 | {} |
    7. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0005_RDS | select `sample_order`.`id`,`sample_order`.`sellerId`,`sample_order`.`trade_id`,`sample_order`.`buyer_id`,`sample_order`.`buyer_nick` from `sample_order`order by `sample_order`.`id` asc limit 0,10002 | {} |
    8. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0002_RDS | select `sample_order`.`id`,`sample_order`.`sellerId`,`sample_order`.`trade_id`,`sample_order`.`buyer_id`,`sample_order`.`buyer_nick` from `sample_order`order by `sample_order`.`id` asc limit 0,10002 | {} |
    9. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0003_RDS | select `sample_order`.`id`,`sample_order`.`sellerId`,`sample_order`.`trade_id`,`sample_order`.`buyer_id`,`sample_order`.`buyer_nick` from `sample_order`order by `sample_order`.`id` asc limit 0,10002 | {} |
    10. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0006_RDS | select `sample_order`.`id`,`sample_order`.`sellerId`,`sample_order`.`trade_id`,`sample_order`.`buyer_id`,`sample_order`.`buyer_nick` from `sample_order`order by `sample_order`.`id` asc limit 0,10002 | {} |
    11. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0000_RDS | select `sample_order`.`id`,`sample_order`.`sellerId`,`sample_order`.`trade_id`,`sample_order`.`buyer_id`,`sample_order`.`buyer_nick` from `sample_order`order by `sample_order`.`id` asc limit 0,10002 | {} |
    12. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0001_RDS | select `sample_order`.`id`,`sample_order`.`sellerId`,`sample_order`.`trade_id`,`sample_order`.`buyer_id`,`sample_order`.`buyer_nick` from `sample_order`order by `sample_order`.`id` asc limit 0,10002 | {} |
    13. +------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------+
    14. 8 rows in set (0.01 sec)

    针对上述情况,SQL 优化方向是先查出 id 集合,再通过 IN 匹配真正的记录内容,改写后的 SQL 查询如下:

    1. SELECT * 
    2. FROM sample_order o
    3. WHERE o.id IN ( 
    4. SELECT id
    5. FROM sample_order
    6. ORDER BY id
    7. LIMIT 10000, 2 )

    这样改写的目的是先用内存缓存 id(前提是 id 数目不多),如果 sample_order 表的拆分键是 id,那么 DRDS 还可以将这样的 IN 查询通过规则计算下推到不同的分库来查询,避免全表扫描和不必要的网络 IO。观察改写后的 SQL 查询效果:

    1. mysql> SELECT *
    2.  -> FROM sample_order o
    3.  -> WHERE o.id IN ( SELECT id FROM sample_order ORDER BY id LIMIT 10000,2 );
    4. +--------------+------------+--------------+--------------+------------+
    5. | id | sellerId | trade_id | buyer_id | buyer_nick |
    6. +--------------+------------+--------------+--------------+------------+
    7. | 242012755468 | 1711939506 | 242012755467 | 244148116334 | zhangsan |
    8. | 242012759093 | 1711939506 | 242012759092 | 244148138304 | wangwu |
    9. +--------------+------------+--------------+--------------+------------+
    10. 2 rows in set (1.08 sec)

    执行时间由原来的12秒减少到1.08秒,缩减了一个数量级。

    对应的执行计划为:

    1. mysql> EXPLAIN SELECT *
    2.  -> FROM sample_order o
    3.  -> WHERE o.id IN ( SELECT id FROM sample_order ORDER BY id LIMIT 10000,2 );
    4. +------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------+--------+
    5. | GROUP_NAME | SQL | PARAMS |
    6. +------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------+--------+
    7. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0002_RDS | select `o`.`id`,`o`.`sellerId`,`o`.`trade_id`,`o`.`buyer_id`,`o`.`buyer_nick` from `sample_order` `o` where (`o`.`id` IN (10002)) | {} |
    8. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0001_RDS | select `o`.`id`,`o`.`sellerId`,`o`.`trade_id`,`o`.`buyer_id`,`o`.`buyer_nick` from `sample_order` `o` where (`o`.`id` IN (10001)) | {} |
    9. +------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------+--------+
    10. 2 rows in set (0.03 sec)

    分组及排序优化

    在 DRDS 中,如果在一条 SQL 查询中必须同时使用 DISTINCT、GROUP BY 与 ORDER BY,应尽可能保证 DISTINCT、GROUP BY 与 ORDER BY 语句后所带的字段相同,且尽量为拆分键,使最终的 SQL 查询只返回少量数据。这样能够让分布式查询中消耗的网络带宽最小,并且不需要取出大量数据在临时表内进行排序,系统的性能能够达到最优状态。

    JOIN 优化

    DRDS 的 JOIN 查询分为可下推的 JOIN 和不可下推的 JOIN(即分布式 JOIN)两类,其优化策略各不相同。

    可下推的 JOIN 优化

    可下推的 JOIN 主要分为以下几类:

    • 单表(即非拆分表)之间的 JOIN;
    • 参与 JOIN 的表在过滤条件中均带有拆分键作为条件,并且拆分算法相同(即通过拆分算法计算的数据分布在相同分片上);
    • 参与 JOIN 的表均按照拆分键作为 JOIN 条件,并且拆分算法相同;
    • 广播表(也称为小表广播)与拆分表之间的 JOIN。

    使用 DRDS 时,应尽可能将 JOIN 查询优化成能够在分库上执行的可下推的 JOIN 形式。

    以广播表与拆分表之间的 JOIN 为例,应将广播表作为 JOIN 驱动表(将 JOIN 中的左表称为驱动表)。DRDS 的广播表在各个分库都会存放一份同样的数据,当作为 JOIN 驱动表时,该表与分表的 JOIN 可以转化为单库的 JOIN 并进行合并计算,提高查询性能。

    例如,有以下的三个表做 JOIN 查询(其中表 sample_area 是广播表,sample_item 和 sample_buyer 是拆分表),查询执行时间约15秒:

    1. mysql> SELECT sample_area.name
    2.  -> FROM sample_item i JOIN sample_buyer b ON i.sellerId = b.sellerId JOIN sample_area a ON b.province = a.id
    3.  -> WHERE a.id < 110107 
    4.  -> LIMIT 0, 10;
    5. +------+
    6. | name |
    7. +------+
    8. | BJ |
    9. | BJ |
    10. | BJ |
    11. | BJ |
    12. | BJ |
    13. | BJ |
    14. | BJ |
    15. | BJ |
    16. | BJ |
    17. | BJ |
    18. +------+
    19. 10 rows in set (14.88 sec)

    如果调整一下 JOIN 的顺序,将广播表放在最左边作为 JOIN 驱动表,则整个 JOIN 查询在 DRDS 中会被下推为单库 JOIN 查询:

    1. mysql> SELECT sample_area.name
    2.  -> FROM sample_area a JOIN sample_buyer b ON b.province = a.id JOIN sample_item i ON i.sellerId = b.sellerId
    3.  -> WHERE a.id < 110107 
    4.  -> LIMIT 0, 10;
    5. +------+
    6. | name |
    7. +------+
    8. | BJ |
    9. | BJ |
    10. | BJ |
    11. | BJ |
    12. | BJ |
    13. | BJ |
    14. | BJ |
    15. | BJ |
    16. | BJ |
    17. | BJ |
    18. +------+ 
    19. 10 rows in set (0.04 sec)

    查询执行时间从15秒减少到0.04秒,性能提升非常明显。

    注意:广播表在分库上通过同步机制实现数据一致,有秒级延迟。

    分布式 JOIN 优化

    如果一个 JOIN 查询不可下推(即 JOIN 条件和过滤条件中均不带有拆分键),则需要由 DRDS 完成查询中的部分计算,即分布式 JOIN。

    通常将分布式 JOIN 中的表按照数据量大小分为两类:

    • 小表:经过条件过滤后,参与 JOIN 计算的中间结果的数据量比较少(一般少于 100 条,或者相较于其它表数据更少)的表;
    • 大表:经过条件过滤后,参与 JOIN 计算的中间结果的数据量比较大(一般多于 100 条,或者相较于其它表数据更多)的表。

    在 DRDS 层的 JOIN 计算中,大多数情况下采用的 JOIN 算法都是 Nested Loop 及其派生算法(若 JOIN 有排序要求,则使用 Sort Merge 算法)。采用 Nested Loop 算法时,如果 JOIN 中左表的数据量越少,那么 DRDS 对右表做查询的次数就越少,如果右表上建有索引或者表中的数据量也很少,则 JOIN 的速度会更快。因此,在 DRDS 中,分布式 JOIN 的左表被称为驱动表,对分布式 JOIN 的优化应将小表作为驱动表,且让驱动表带有尽可能多的过滤条件。

    以下面的分布式 JOIN 为例,查询约需要24秒:

    1. mysql> SELECT t.title, t.price
    2.  -> FROM sample_order o,
    3.  -> ( SELECT * FROM sample_item i WHERE i.id = 242002396687 ) t
    4.  -> WHERE t.source_id = o.source_item_id AND o.sellerId < 1733635660;
    5. +----------------------------------+--------+
    6. | title | price |
    7. +----------------------------------|--------+
    8. | Sample Item for Distributed JOIN | 239.00 |
    9. | Sample Item for Distributed JOIN | 239.00 |
    10. | Sample Item for Distributed JOIN | 239.00 |
    11. | Sample Item for Distributed JOIN | 239.00 |
    12. | Sample Item for Distributed JOIN | 239.00 |
    13. | Sample Item for Distributed JOIN | 239.00 |
    14. | Sample Item for Distributed JOIN | 239.00 |
    15. | Sample Item for Distributed JOIN | 239.00 |
    16. | Sample Item for Distributed JOIN | 239.00 |
    17. | Sample Item for Distributed JOIN | 239.00 |
    18. +----------------------------------+--------+
    19. 10 rows in set (23.79 sec)

    通过初步分析,上述 JOIN 查询是一个 INNER JOIN,并不知道参与 JOIN 计算的中间结果的实际数据量,可以对 o 表与 t 表分别做 COUNT() 查询得到实际数据。

    对于 o 表,观察到 WHERE 条件中的 o.sellerId < 1733635660 只与 o 表相关,可以将其提取出来,附加到 o 表的 COUNT() 查询条件中,得到如下的查询结果:

    1. mysql> SELECT COUNT(*) FROM sample_order o WHERE o.sellerId < 1733635660;
    2. +----------+
    3. | count(*) |
    4. +----------+
    5. | 504018 |
    6. +----------+
    7. 1 row in set (0.10 sec)

    o 表的中间结果约有50万条记录。类似地,t 表是一个子查询,直接将其抽取出来进行 COUNT() 查询:

    1. mysql> SELECT COUNT(*) FROM sample_item i WHERE i.id = 242002396687;
    2. +----------+
    3. | count(*) |
    4. +----------+
    5. | 1 |
    6. +----------+
    7. 1 row in set (0.01 sec)

    t 表的中间结果只有1条记录,所以可确定 o 表为大表,t 表为小表。根据尽量将小表作为分布式 JOIN 驱动表的原则,将 JOIN 查询调整后的查询结果为:

    1. mysql> SELECT t.title, t.price
    2.  -> FROM ( SELECT * FROM sample_item i WHERE i.id = 242002396687 ) t,
    3.  -> sample_order o
    4.  -> WHERE t.source_id = o.source_item_id AND o.sellerId < 1733635660;
    5. +----------------------------------+--------+
    6. | title | price |
    7. +----------------------------------|--------+
    8. | Sample Item for Distributed JOIN | 239.00 |
    9. | Sample Item for Distributed JOIN | 239.00 |
    10. | Sample Item for Distributed JOIN | 239.00 |
    11. | Sample Item for Distributed JOIN | 239.00 |
    12. | Sample Item for Distributed JOIN | 239.00 |
    13. | Sample Item for Distributed JOIN | 239.00 |
    14. | Sample Item for Distributed JOIN | 239.00 |
    15. | Sample Item for Distributed JOIN | 239.00 |
    16. | Sample Item for Distributed JOIN | 239.00 |
    17. | Sample Item for Distributed JOIN | 239.00 |
    18. +----------------------------------+--------+
    19. 10 rows in set (0.15 sec)

    查询时间从约24秒减少到0.15秒,性能提升非常明显。

    子查询优化

    在包含子查询的 SQL 优化中,应尽可能将查询下推到具体的分库上执行,并减少 DRDS 层的计算量。要达到这一目标,可以尝试两个方面的优化:

    • 将子查询的形式改写为多表 JOIN 形式,并参照 JOIN 优化方法进一步优化;
    • 尽量在 JOIN 条件或过滤条件中带上拆分键,有利于 DRDS 将查询下推到特定的分库,避免全表扫描。

    以下面的子查询为例:

    1. SELECT o.*
    2. FROM sample_order o
    3. WHERE NOT EXISTS
    4.  (SELECT sellerId FROM sample_seller s WHERE o.sellerId = s.id)

    可将其改写为 JOIN 的形式:

    1. SELECT o.*
    2. FROM sample_order o LEFT JOIN sample_seller s ON o.sellerId = s.id
    3. WHERE s.id IS NULL
  • MySQL 索引及查询优化总结

    一个简单的对比测试

    前面的案例中,c2c_zwdb.t_file_count表只有一个自增id,FFileName字段未加索引的sql执行情况如下:

    在上图中,type=all,key=null,rows=33777。该sql未使用索引,是一个效率非常低的全表扫描。如果加上联合查询和其他一些约束条件,数据库会疯狂的消耗内存,并且会影响前端程序的执行。

    这时给FFileName字段添加一个索引:

    alter table c2c_zwdb.t_file_count add index index_title(FFileName);

    再次执行上述查询语句,其对比很明显:

    在该图中,type=ref,key=索引名(index_title),rows=1。该sql使用了索引index_title,且是一个常数扫描,根据索引只扫描了一行。

    比起未加索引的情况,加了索引后,查询效率对比非常明显。

    MySQL索引

    通过上面的对比测试可以看出,索引是快速搜索的关键。MySQL索引的建立对于MySQL的高效运行是很重要的。对于少量的数据,没有合适的索引影响不是很大,但是,当随着数据量的增加,性能会急剧下降。如果对多列进行索引(组合索引),列的顺序非常重要,MySQL仅能对索引最左边的前缀进行有效的查找。

    下面介绍几种常见的MySQL索引类型。

    索引分单列索引和组合索引。单列索引,即一个索引只包含单个列,一个表可以有多个单列索引,但这不是组合索引。组合索引,即一个索引包含多个列。

    1、MySQL索引类型

    (1) 主键索引 PRIMARY KEY

    它是一种特殊的唯一索引,不允许有空值。一般是在建表的时候同时创建主键索引。

    当然也可以用 ALTER 命令。记住:一个表只能有一个主键。

    (2) 唯一索引 UNIQUE

    唯一索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。可以在创建表的时候指定,也可以修改表结构,如:

    ALTER TABLE table_name ADD UNIQUE (column)

    (3) 普通索引 INDEX

    这是最基本的索引,它没有任何限制。可以在创建表的时候指定,也可以修改表结构,如:

    ALTER TABLE table_name ADD INDEX index_name (column)

    (4) 组合索引 INDEX

    组合索引,即一个索引包含多个列。可以在创建表的时候指定,也可以修改表结构,如:

    ALTER TABLE table_name ADD INDEX index_name(column1column2column3)

    (5) 全文索引 FULLTEXT

    全文索引(也称全文检索)是目前搜索引擎使用的一种关键技术。它能够利用分词技术等多种算法智能分析出文本文字中关键字词的频率及重要性,然后按照一定的算法规则智能地筛选出我们想要的搜索结果。

    可以在创建表的时候指定,也可以修改表结构,如:

    ALTER TABLE table_name ADD FULLTEXT (column)

    2、索引结构及原理

    mysql中普遍使用B+Tree做索引,但在实现上又根据聚簇索引和非聚簇索引而不同,本文暂不讨论这点。

    b+树介绍

    下面这张b+树的图片在很多地方可以看到,之所以在这里也选取这张,是因为觉得这张图片可以很好的诠释索引的查找过程。

    如上图,是一颗b+树。浅蓝色的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据项(深蓝色所示)和指针(黄色所示),如磁盘块1包含数据项17和35,包含指针P1、P2、P3,P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。

    真实的数据存在于叶子节点,即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非叶子节点不存储真实的数据,只存储指引搜索方向的数据项,如17、35并不真实存在于数据表中。

    查找过程

    在上图中,如果要查找数据项29,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO,在内存中用二分查找确定29在17和35之间,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘的IO)可以忽略不计,通过磁盘块1的P2指针的磁盘地址把磁盘块3由磁盘加载到内存,发生第二次IO,29在26和30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存,发生第三次IO,同时内存中做二分查找找到29,结束查询,总计三次IO。真实的情况是,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本非常非常高。

    性质

    (1) 索引字段要尽量的小。

    通过上面b+树的查找过程,或者通过真实的数据存在于叶子节点这个事实可知,IO次数取决于b+数的高度h。

    假设当前数据表的数据量为N,每个磁盘块的数据项的数量是m,则树高h=㏒(m+1)N,当数据量N一定的情况下,m越大,h越小;

    而m = 磁盘块的大小/数据项的大小,磁盘块的大小也就是一个数据页的大小,是固定的;如果数据项占的空间越小,数据项的数量m越多,树的高度h越低。这就是为什么每个数据项,即索引字段要尽量的小,比如int占4字节,要比bigint8字节少一半。

    (2) 索引的最左匹配特性。

    当b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+数是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了, 这个是非常重要的性质,即索引的最左匹配特性。

    建索引的几大原则

    (1) 最左前缀匹配原则

    对于多列索引,总是从索引的最前面字段开始,接着往后,中间不能跳过。比如创建了多列索引(name,age,sex),会先匹配name字段,再匹配age字段,再匹配sex字段的,中间不能跳过。mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配。

    一般,在创建多列索引时,where子句中使用最频繁的一列放在最左边。

    看一个补符合最左前缀匹配原则和符合该原则的对比例子。

    实例:表c2c_db.t_credit_detail建有索引(Flistid,Fbank_listid)

    不符合最左前缀匹配原则的sql语句:

    select * from t_credit_detail where Fbank_listid=’201108010000199’\G

    该sql直接用了第二个索引字段Fbank_listid,跳过了第一个索引字段Flistid,不符合最左前缀匹配原则。用explain命令查看sql语句的执行计划,如下图:

    从上图可以看出,该sql未使用索引,是一个低效的全表扫描。

    符合最左前缀匹配原则的sql语句:

    select * from t_credit_detail where Flistid=’2000000608201108010831508721′ and Fbank_listid=’201108010000199’\G

    该sql先使用了索引的第一个字段Flistid,再使用索引的第二个字段Fbank_listid,中间没有跳过,符合最左前缀匹配原则。用explain命令查看sql语句的执行计划,如下图:

    从上图可以看出,该sql使用了索引,仅扫描了一行。

    对比可知,符合最左前缀匹配原则的sql语句比不符合该原则的sql语句效率有极大提高,从全表扫描上升到了常数扫描。

    (2) 尽量选择区分度高的列作为索引。

    比如,我们会选择学号做索引,而不会选择性别来做索引。

    (3) =和in可以乱序

    比如a = 1 and b = 2 and c = 3,建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式。

    (4) 索引列不能参与计算,保持列“干净”

    比如:Flistid+1>‘2000000608201108010831508721‘。原因很简单,假如索引列参与计算的话,那每次检索时,都会先将索引计算一次,再做比较,显然成本太大。

    (5) 尽量的扩展索引,不要新建索引。

    比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。

    索引的不足

    虽然索引可以提高查询效率,但索引也有自己的不足之处。

    索引的额外开销:

    (1) 空间:索引需要占用空间;

    (2) 时间:查询索引需要时间;

    (3) 维护:索引须要维护(数据变更时);

    不建议使用索引的情况:

    (1) 数据量很小的表

    (2) 空间紧张

    常用优化总结

    优化语句很多,需要注意的也很多,针对平时的情况总结一下几点:

    1、有索引但未被用到的情况(不建议)

    (1) Like的参数以通配符开头时

    尽量避免Like的参数以通配符开头,否则数据库引擎会放弃使用索引而进行全表扫描。

    以通配符开头的sql语句,例如:select * from t_credit_detail where Flistid like ‘%0’\G

    这是全表扫描,没有使用到索引,不建议使用。

    不以通配符开头的sql语句,例如:select * from t_credit_detail where Flistid like ‘2%’\G

    很明显,这使用到了索引,是有范围的查找了,比以通配符开头的sql语句效率提高不少。

    (2) where条件不符合最左前缀原则时

    例子已在最左前缀匹配原则的内容中有举例。

    (3) 使用!= 或 <> 操作符时

    尽量避免使用!= 或 <>操作符,否则数据库引擎会放弃使用索引而进行全表扫描。使用>或<会比较高效。

    select * from t_credit_detail where Flistid != ‘2000000608201108010831508721’\G

    (4) 索引列参与计算

    应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。

    select * from t_credit_detail where Flistid +1 > ‘2000000608201108010831508722’\G

    (5) 对字段进行null值判断

    应尽量避免在where子句中对字段进行null值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:
    低效:select * from t_credit_detail where Flistid is null ;

    可以在Flistid上设置默认值0,确保表中Flistid列没有null值,然后这样查询:
    高效:select * from t_credit_detail where Flistid =0;

    (6) 使用or来连接条件

    应尽量避免在where子句中使用or来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:
    低效:select * from t_credit_detail where Flistid = ‘2000000608201108010831508721’ or Flistid = ‘10000200001’;

    可以用下面这样的查询代替上面的 or 查询:
    高效:select from t_credit_detail where Flistid = ‘2000000608201108010831508721’ union all select from t_credit_detail where Flistid = ‘10000200001’;

    2、避免select *

    在解析的过程中,会将’*’ 依次转换成所有的列名,这个工作是通过查询数据字典完成的,这意味着将耗费更多的时间。

    所以,应该养成一个需要什么就取什么的好习惯。

    3、order by 语句优化

    任何在Order by语句的非索引项或者有计算表达式都将降低查询速度。

    方法:1.重写order by语句以使用索引;

      2.为所使用的列建立另外一个索引
    
      3.绝对避免在order by子句中使用表达式。
    

    4、GROUP BY语句优化

    提高GROUP BY 语句的效率, 可以通过将不需要的记录在GROUP BY 之前过滤掉

    低效:

    SELECT JOB , AVG(SAL)

    FROM EMP

    GROUP by JOB

    HAVING JOB = ‘PRESIDENT’

    OR JOB = ‘MANAGER’

    高效:

    SELECT JOB , AVG(SAL)

    FROM EMP

    WHERE JOB = ‘PRESIDENT’

    OR JOB = ‘MANAGER’

    GROUP by JOB

    5、用 exists 代替 in

    很多时候用 exists 代替 in 是一个好的选择:
    select num from a where num in(select num from b)
    用下面的语句替换:
    select num from a where exists(select 1 from b where num=a.num)

    6、使用 varchar/nvarchar 代替 char/nchar

    尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。

    7、能用DISTINCT的就不用GROUP BY

    SELECT OrderID FROM Details WHERE UnitPrice > 10 GROUP BY OrderID

    可改为:

    SELECT DISTINCT OrderID FROM Details WHERE UnitPrice > 10

    8、能用UNION ALL就不要用UNION

    UNION ALL不执行SELECT DISTINCT函数,这样就会减少很多不必要的资源。

    9、在Join表的时候使用相当类型的例,并将其索引

    如果应用程序有很多JOIN 查询,你应该确认两个表中Join的字段是被建过索引的。这样,MySQL内部会启动为你优化Join的SQL语句的机制。

    而且,这些被用来Join的字段,应该是相同的类型的。例如:如果你要把 DECIMAL 字段和一个 INT 字段Join在一起,MySQL就无法使用它们的索引。对于那些STRING类型,还需要有相同的字符集才行。(两个表的字符集有可能不一样)

  • MySQL 数据库设计总结

    规则1:一般情况可以选择MyISAM存储引擎,如果需要事务支持必须使用InnoDB存储引擎。

    注意:MyISAM存储引擎 B-tree索引有一个很大的限制:参与一个索引的所有字段的长度之和不能超过1000字节。另外MyISAM数据和索引是分开,而InnoDB的数据存储是按聚簇(cluster)索引有序排列的,主键是默认的聚簇(cluster)索引,因此MyISAM虽然在一般情况下,查询性能比InnoDB高,但InnoDB的以主键为条件的查询性能是非常高的。

    规则2:命名规则。

    1. 数据库和表名应尽可能和所服务的业务模块名一致
    2. 服务与同一个子模块的一类表应尽量以子模块名(或部分单词)为前缀或后缀
    3. 表名应尽量包含与所存放数据对应的单词
    4. 字段名称也应尽量保持和实际数据相对应
    5. 联合索引名称应尽量包含所有索引键字段名或缩写,且各字段名在索引名中的顺序应与索引键在索引中的索引顺序一致,并尽量包含一个类似idx的前缀或后缀,以表明期对象类型是索引。
    6. 约束等其他对象也应该尽可能包含所属表或其他对象的名称,以表明各自的关系

    规则3:数据库字段类型定义

    1. 经常需要计算和排序等消耗CPU的字段,应该尽量选择更为迅速的字段,如用TIMESTAMP(4个字节,最小值1970-01-01 00:00:00)代替Datetime(8个字节,最小值1001-01-01 00:00:00),通过整型替代浮点型和字符型
    2. 变长字段使用varchar,不要使用char
    3. 对于二进制多媒体数据,流水队列数据(如日志),超大文本数据不要放在数据库字段中

    规则4:业务逻辑执行过程必须读到的表中必须要有初始的值。避免业务读出为负或无穷大的值导致程序失败

    规则5:并不需要一定遵守范式理论,适度的冗余,让Query尽量减少Join

    规则6:访问频率较低的大字段拆分出数据表。有些大字段占用空间多,访问频率较其他字段明显要少很多,这种情况进行拆分,频繁的查询中就不需要读取大字段,造成IO资源的浪费。

    规则7:大表可以考虑水平拆分。大表影响查询效率,根据业务特性有很多拆分方式,像根据时间递增的数据,可以根据时间来分。以id划分的数据,可根据id%数据库个数的方式来拆分。

    一.数据库索引

    规则8:业务需要的相关索引是根据实际的设计所构造sql语句的where条件来确定的,业务不需要的不要建索引,不允许在联合索引(或主键)中存在多于的字段。特别是该字段根本不会在条件语句中出现。

    规则9:唯一确定一条记录的一个字段或多个字段要建立主键或者唯一索引,不能唯一确定一条记录,为了提高查询效率建普通索引

    规则10:业务使用的表,有些记录数很少,甚至只有一条记录,为了约束的需要,也要建立索引或者设置主键。

    规则11:对于取值不能重复,经常作为查询条件的字段,应该建唯一索引(主键默认唯一索引),并且将查询条件中该字段的条件置于第一个位置。没有必要再建立与该字段有关的联合索引。

    规则12:对于经常查询的字段,其值不唯一,也应该考虑建立普通索引,查询语句中该字段条件置于第一个位置,对联合索引处理的方法同样。

    规则13:业务通过不唯一索引访问数据时,需要考虑通过该索引值返回的记录稠密度,原则上可能的稠密度最大不能高于0.2,如果稠密度太大,则不合适建立索引了。

    当通过这个索引查找得到的数据量占到表内所有数据的20%以上时,则需要考虑建立该索引的代价,同时由于索引扫描产生的都是随机I/O,生其效率比全表顺序扫描的顺序I/O低很多。数据库系统优化query的时候有可能不会用到这个索引。

    规则14:需要联合索引(或联合主键)的数据库要注意索引的顺序。SQL语句中的匹配条件也要跟索引的顺序保持一致。

    注意:索引的顺势不正确也可能导致严重的后果。

    规则15:表中的多个字段查询作为查询条件,不含有其他索引,并且字段联合值不重复,可以在这多个字段上建唯一的联合索引,假设索引字段为 (a1,a2,…an),则查询条件(a1 op val1,a2 op val2,...am op valm)m<=n,可以用到索引,查询条件中字段的位置与索引中的字段位置是一致的。

    规则16:联合索引的建立原则(以下均假设在数据库表的字段a,b,c上建立联合索引(a,b,c))

    1. 联合索引中的字段应尽量满足过滤数据从多到少的顺序,也就是说差异最大的字段应该房子第一个字段
    2. 建立索引尽量与SQL语句的条件顺序一致,使SQL语句尽量以整个索引为条件,尽量避免以索引的一部分(特别是首个条件与索引的首个字段不一致时)作为查询的条件
    3. Where a=1,where a>=12 and a<15,where a=1 and b<5 ,where a=1 and b=7 and c>=40为条件可以用到此联合索引;而这些语句where b=10,where c=221,where b>=12 and c=2则无法用到这个联合索引。
    4. 当需要查询的数据库字段全部在索引中体现时,数据库可以直接查询索引得到查询信息无须对整个表进行扫描(这就是所谓的key-only),能大大的提高查询效率。
      当a,ab,abc与其他表字段关联查询时可以用到索引
    5. 当a,ab,abc顺序而不是b,c,bc,ac为顺序执行Order by或者group不要时可以用到索引
    6. 以下情况时,进行表扫描然后排序可能比使用联合索引更加有效
      a.表已经按照索引组织好了
      b.被查询的数据站所有数据的很多比例。

    规则17:重要业务访问数据表时。但不能通过索引访问数据时,应该确保顺序访问的记录数目是有限的,原则上不得多于10.

    二.Query语句与应用系统优化

    规则18:合理构造Query语句

    1. Insert语句中,根据测试,批量一次插入1000条时效率最高,多于1000条时,要拆分,多次进行同样的插入,应该合并批量进行。注意query语句的长度要小于mysqld的参数 max_allowed_packet
    2. 查询条件中各种逻辑操作符性能顺序是and,or,in,因此在查询条件中应该尽量避免使用在大集合中使用in
    3. 永远用小结果集驱动大记录集,因为在mysql中,只有Nested Join一种Join方式,就是说mysql的join是通过嵌套循环来实现的。通过小结果集驱动大记录集这个原则来减少嵌套循环的循环次数,以减少IO总量及CPU运算次数
    4. 尽量优化Nested Join内层循环。
    5. 只取需要的columns,尽量不要使用select *
    6. 仅仅使用最有效的过滤字段,where 字句中的过滤条件少为好
    7. 尽量避免复杂的Join和子查询

      Mysql在并发这块做得并不是太好,当并发量太高的时候,整体性能会急剧下降,这主要与Mysql内部资源的争用锁定控制有关,MyIsam用表锁,InnoDB好一些用行锁。

    规则19:应用系统的优化

    1. 合理使用cache,对于变化较少的部分活跃数据通过应用层的cache缓存到内存中,对性能的提升是成数量级的。
    2. 对重复执行相同的query进行合并,减少IO次数。
    3. 事务相关性最小原则
Copyright © 2014-2025 奋奋的愤愤 | 京ICP备14029030号-1