DBA的七个级别

用脚的DBA:飞机高铁,奔走四海,拿人钱财,替人消灾;

用腿的DBA:地铁先下,饭局离席,电脑膝上,随时处理;

用手的DBA:摸石过河,巡山探路,手痒手贱,搞点事故;

用嘴的DBA:下定计划,上学蓝图,会吹经验,能啃硬骨;

用脑的DBA:思量架构,沟通互联,居安思危,防患未燃;

用眼的DBA:高瞻远瞩,识人任将,判析行情,引领风向;

用胸的DBA:虚怀若谷,无为而治,大道至简,不知有之;

DBA许愿大法

昨晚双子座有流星雨,很多同学等流星划过的一瞬间要许愿,但因愿望太多太长,往往没有说完流星就已经消失了,而这种情况下,许的愿望就不灵了。

就此问题,根据我多年从业经验,给大家讲一个小技巧:你别把愿望说出来,先在心底默默地念一遍,等流星出现时,大喊一声:commit !

纠结的DBA

服务启动不了紧张,停不了更紧张。

delete执行快紧张,执行慢更紧张。

错误日志多紧张,没日志更紧张。

报警短信多紧张,没报警更紧张。

服务频繁重启紧张,好久不重启更紧张。

割接不顺利紧张,顺利更紧张。

成语新解DBA版

人艰不拆:人家写JOIN已经很艰辛了,就不要再要求拆库拆表了。

喜大普奔:喜欢写大SQL大事务大批量的人,最后普遍会泪奔。

十动然拒:数据库设置十个活动连接,然后别的请求都拒绝。

不明觉厉:不明白数据库原理还能睡的着觉的人都很厉害。

语死早:只会写SQL语言,会饿死得很早。

冷无缺:冷库,无索引,缺少备份。

累觉不爱:DBA累了就睡觉,不会有时间做爱。

男默女泪:DBA提起男朋友会沉默,提起女朋友会流泪。

细思恐极:仔细想来,服务器的性能到了极限了。

数据时代的新宠儿

互联网上曾有一张很流行的图片叫《互联网上的一天》,其中第一幅图的内容是“在一天内互联网上的流量信息可以刻满1亿6800万张DVD光盘”,这从一个侧面反映出了我们正处在一个数据信息高速膨胀的时代,相应地,也处在一个数据促进技术改革的时代。面对指数级别增长的数据和业务对数据库技术越来越苛刻的要求,Michael Stonebraker博士于2011年发表了一篇名为《New SQL: An Alternative to NoSQL and Old SQL for New OLTP Apps》的博客1,大笔一挥,将数据库的阵营三分天下:传统数据库OldSQL、非关系型数据库NoSQL和新型数据库NewSQL。我们就从科普的角度来介绍一下近来出现的这几个当下炙手可热的明星概念和它们的代言产品。

NoSQL

NoSQL并不是一个新鲜的词,这个词在1998年的时候就已经出现了,是Carlo Strozzi当时开发的一个轻量、开源且不提供SQL功能的数据库,有趣的是,他这个数据还是关系型数据库。2009年,Johan Oskarsson组织召开了一个讨论关于分布式数据库的会议,会上Eric Evans重新提到了这个术语2。这次会议中NoSQL还没有成为一种具体系统的代名词,它主要用来指非关系型、分布式且不提供ACID的数据库设计模式,从这一点来看,它确实已经与“关系”没有太大的关系了。

2009年在亚特兰大举行的“no:sql(east)”讨论会是一个里程碑,其口号是”select fun, profit from real-world where relational=false;”。因此,对NoSQL最普遍的解释是“非关系型”,强调Key-Value存储和文档数据库的优点,而不是单纯的反对RDBMS,即No Only SQL,也就是我们现在普遍认可的NoSQL。

提到Key-Value,就不得不提一个大名鼎鼎的数据库系统——Berkeley DB,它的历史跟Linux系统一样久远,它的名气跟它的名字一样威武。Berkeley DB最初开发的目的是以新的hash访问算法来代替旧的hsearch函数和大量的dbm实现。在1992年,BSD UNIX第4.4发行版中包含了Berkeley DB的第一个正式版。在1996年中期,Sleepycat软件公司成立,开始提供对Berkeley DB的商业支持3。在这以后,Berkeley DB得到了广泛的应用 。Berkeley DB的设计思想简单精巧,数据库里包含若干记录,每一个记录由Key-Value组成,Value可以是简单的数据类型,也可以是复杂的数据类型,Berkeley DB对Value也不做任何解释,完全由客户端自行处理,所以Berkeley DB数据库非常精小,不超过500K,却能管理大至256T的数据,彰显了Key-Value“四两拨千斤”的至胜大招。

2000年左右,MySQL AB公司成立并与Sleepycat公司建立了合作关系,MySQL引入了Berkeley DB(简称BDB)后,发布了3.23版本4,有不少MySQL DBA也是从这儿开始认识Berkeley DB的5。在MySQL 5.5.12版本中,MySQL不再支持BDB,传言与2006年Oracle收购Sleepycat有关,但是我们也不必为此操心,因为MySQL 5.6已经开始支持Memcached了6

2003年5月22日,大家所熟知的Memcached发布了,最初这是Danga Interactive为网站LiveJournal而开发的,后来被各大网站用于加速缓存。虽然Memcached在存储方式上使用了Key-Value模式,但因Memcached的存储全部在内存中,所以它不并是真正意义上的数据库系统。当然,数据落地从来就不是什么问题,2008年,MemcacheDB问世,你猜得没错,因为协议相同,所以你完全可以像使用Memcached一样使用它的API,最大的区别在于,MemcacheDB在底层使用了Berkeley DB了作为自己的Key-Value存储系统7

2003到2006年之间,Google公司连续发表了三篇关于分布式的文章,即关于江湖上津津乐道的“Google技术有三宝”:GFS8、MapReduce9和大表(BigTable)10。之后各种优秀的分布式系统纷纷隆重出场,如MongoDB、DynamoDB、CouchDB、Cassandra等11,真有点“乱花渐欲迷人眼”了。国内也出现了很多令人称赞的系统:如百度的网页库Bailing,KV存储系统Mola、腾讯的KV存储系统TDB以及淘宝的Tair系统12。以上我所提到的系统,都在为各自公司重要核心的业务支持着T级甚至是P级的数据服务。

随着存储硬件的价格降低,存储大数据已经不再需要付出是昂贵的代价了,GFS的设计理念就是利用廉价的硬件成本和容错功能提供总体性能较高的服务。大数据的“平民化”时代已经到来,各种中小型公司利用开源的分布式系统和Hadoop来部署自己的数据平台已经并不是难事,在开源社区的推动下,从事运维大数据集群和开发Map/Reduce程序的工程师也越来越多,大漠淘金的时代已经离我们越来越近了。

NewSQL

跟NoSQL比起来,NewSQL就显得年轻多了。2011年,The 451 Group公司的分析师Matthew Aslett发表了一篇分析报告,专门用来讨论与日俱增的新的数据系统对现在数据库商家的挑战,这篇报告的标题为《How will the database incumbents respond to NoSQL and NewSQL?》13,14,15,这是NewSQL一词第一次出现在人们的视野之中。

所有事物的优点,也就是它的缺点。与传统数据库OldSQL相比较,NoSQL的确有不可替代的优势,如NoSQL灵活的数据模型使得让DBA们谈虎色变的数据结构变更管理变得易如反掌,相形之下OldSQL的更显得 “死气沉沉”,但NoSQL不支持SQL查询和ACID也让自身一直处于风口浪尖上。这个时候,大家发现NoSQL并不是解救数据库的唯一出路,而且有时觉得NoSQL在某种程度是错误的(比如不支持SQL查询),所以迅速寻找并开发折衷的产品与服务,此情此景下,NewSQL便应运而生了。所以NewSQL可以看成是传统数据OldSQL和NoSQL的一个折衷或优化——既保留关系模型优势(SQL查询与ACID),又有NoSQL的扩展性和灵活性,或将关系性数据库的性提高到一个根本不用考虑再做扩展的水平。

要整合OldSQL和NoSQL的优势,NewSQL首先要傍一个信得过的OldSQL才能有可观的投资回报率,作为开源关系型数据库的老大,MySQL毫无疑问地成为了NewSQL的首选。当然,MySQL本身就有NewSQL的杀手锏——MySQL Cluster NDB16,目前版本是MySQL Cluster NDB 7.3。HandlerSocket17是较早的NewSQL试水者,它是以Daemon Plugin18的方式为MySQL扩展了NoSQL功能。Tokutek TokuDB19则是Tokutek公司为MySQL开发的一款存储引擎,今年刚刚在Github上开源。MemSQL20则是一款实现了与MySQL一样的API的内存数据库,在它的主页上写着“世界上最快的内存数据库”,是不是世界第一,我们等实践来检验吧。当然,还有很多优秀的厂商都在开发高效的NewSQL产品,如Clustrix21和VoltDB22等,文章最前面提到的Michael Stonebraker博士便是VoltDB的创始人。

我们不能武断地说某一种数据库过时了,不同的数据库模型都有它的历史意义,在这个我们都高呼大数据的时代,每一种SQL都有它无可比拟的优势,也会存在让它施展浑身解数的用武之地,我们希望都更多易用好用的新产品出现。如果说NoSQL是一场针对OldSQL数据库技术的革命,那NewSQL何尝不是另一场针对NoSQL的技术革命呢。所有的技术革命,都是在否定中成长,NewSQL的星星之火刚刚点燃了几堆火,但我们可以想象到,针对NewSQL的革命,也只是在不远的将来。

参考资料


  1. http://cacm.acm.org/blogs/blog-cacm/109710-new-sql-an-alternative-to-nosql-and-old-sql-for-new-oltp-apps/fulltext 

  2. http://blog.sym-link.com/2009/05/12/nosql_2009.html 

  3. https://www.ibm.com/developerworks/cn/linux/l-embdb/ 

  4. ‘Understanding MySQL Internals’ 

  5. http://dev.mysql.com/doc/refman/4.1/en/bdb-storage-engine.html 

  6. http://dev.mysql.com/doc/refman/5.6/en/innodb-memcached.html 

  7. http://memcachedb.org 

  8. http://labs.google.com/papers/gfs-sosp2003.pdf 

  9. http://labs.google.com/papers/mapreduce-osdi04.pdf 

  10. http://labs.google.com/papers/bigtable-osdi06.pdf 

  11. http://nosql-database.org 

  12. http://code.taobao.org/p/tair/src/ 

  13. http://www.the451group.com 

  14. http://en.wikipedia.org/wiki/Newsql 

  15. http://wenku.it168.com/d_000053815.shtml 

  16. http://dev.mysql.com/doc/refman/5.6/en/mysql-cluster.html 

  17. https://github.com/ahiguti/HandlerSocket-Plugin-for-MySQL 

  18. http://dev.mysql.com/doc/refman/5.6/en/daemon-plugins.html 

  19. http://www.tokutek.com/products/tokudb-for-mysql/ 

  20. http://www.memsql.com/ 

  21. http://www.clustrix.com/ 

  22. http://www.voltdb.com/ 

MySQL架构

对于这个庞然大物,MySQL很难形成一套正式的定义和规范。大部分代码都是早期写的,当初并没有按照一个未来大系统的代码来写,而倾向于为了解决某写具体问题而编写。尽管如此,它写得足够优美足够长远,达到拥有足够优质的代码段来组装成一个数据库服务软件的程度。

内核模块 (Core Modules)

在本部分我努力尝试了确定系统的核心模块,不过,我申明一下,我只是尝试让现在存在的东西变得正式一些。 MySQL的开发者极少考虑这些术语,更确切地说,他们倾向思考文件、目录、类、结构体和函数等。“这出现在mi_open()”要比“这发生在MyISAM存在引擎层上”更常被听到。因为MySQL开发者对代码太了如指掌了,所以他们能够从函数、结构体及类这个层面考虑问题。他们会发现这部分中的抽象概念毫无用处。但这对于那些常常考虑模块和管理方面术语的人来说还帮助的。

就MySQL而言,我使用“模块”这个术语是相当的不严谨。不像大家常说的那种模块,在很多情况下,你并不能轻松地拔出并用另一种实现来替换。模块的代码散布在好几个文件之中,而且你会从同一个文件中找到不同模块的代码。老代码中尤为如此。新的代码与模块代码模式融合得更好一些。所以,在我们的定义中,模块就是一段在某些方面理论上属于一起,并在服务中提供某些关键功能表的代码。

我们可以在服务中确认以下的模块:

  • 服务初始化模块 (Server Initialization Module)
  • 连接管理器 (Connection Manager)
  • 线程管理器 (Thread Manager)
  • 连接线程 (Connection Thread)
  • 用户权限认证模块 (User Authentication Module)
  • 访问控制模块 (Access Control Module)
  • 语法解析器 (Parser)
  • 命令调度器 (Command Dispatcher)
  • 查询缓存模块 (Query Cache Module)
  • 优化器 (Optimizer)
  • 表单管理器 (Table Manager)
  • 表单更新模块 (Table Modification Module)
  • 表单维护模块 (Table Maintenance Module)
  • 状态报告模块 (Statud Reporting Module)
  • 抽象存储引擎接口/表处理器 (Abstracted Storage Engine Interface/Table Handler)
  • 存储引擎实现(Storage Engine Implementation: MyISAM, InnoDB, MEMORY, Berkeley DB)
  • 日志模块 (Logging Module)
  • 复制主库模块 (Replication Master Module)
  • 复制从库模块 (Replication Slave Module)
  • 客户端/服务器协议API (Client/Server Protocol API)
  • 低层网络I/O API (Low-Level Network I/O API)
  • 内核API (Core API)

内核的交互 (Interaction of the Core Modules)

当服务以命令行模式启动时,初始化模块开始起作用。它解析配置文件和命令行参数,申请全局的内存,初始化全局全量和结构体,加载访问控制表,并执行大量其它的初始化任务。一旦初始化的工作完成,初始化模块把控制权交给连接管理器,连接管理器开始用轮询的方式监控听从客端户过来的连接。

当一个客户端连接数据库服务器时,连接管理器执行一个低层网络协议的任务,然后把控制权交给线程管理器,它依次提供线程来处理连接(从现在开始我们把他称作连接线程了)。连接线程可能会被创建一个新的,或从线程缓存中重找一个并激。当连接线程拿到控制权,它首先调用用户权限模块。当前连接用户的身份得到证实,客户端就可以发出请求了。

连接线程把请求数据传递给命令调试器。有些在MySQL的代码中被称为命令的请求可以直接被命令调试器处理,而更复杂的请求需要转发到另外的模块。一个典型的命令可以是请求服务器执行一个查询,改变当前使用数据库(use),报告状态,发送一个复制更新的连续转储(也就是一组sql语句),或执行一些其它的操作。

在MySQL服务器的术语中,有两种客户端的请求:查询与命令。查询是要经过解析器的所有请求。而命令是不用调用解析器就能执行的请求。我们将在本书的内容中用到查询(query)这个术语,所以不仅仅是SELECT被叫做查询,DELETE或 UPDATE也被叫做查询。有时我们也把查询叫SQL语句。

如果所有的查询日志是打开的,命令调试器会要求日志模块在调度之前将查询和命令记录到纯文本日志中。所以在全日志配置的情况下所有的查询都将被记录下来,即使在语法错误根本没有并执行并随后立即返回一个错误这种情况下。

命令调度器经由查询缓存模块将请求转发给分析器。 查询缓存模块检查查询是否是可以被缓存的类型,并检查是否存在仍然有效的之前运算并缓存下来的结果。在命中的情况下,执行在此刻发生短路,被缓存的结果被返回给用户,连接线程接到控制权并开始准备处理另一个命令。如果查询缓存模块没有命中,查询将转到解析器,它将根据查询的类型判断控制权将如何跳转。

我们可以确认下列的几种模块可以接着继续前进:优化器,表更新模块,表维护模块,复制模块,状态报告模块。Select查询被转化到优化器;updates, inserts, deletes 和创建表及更改表结构的查询跳转到各自的表更新模块;检查, 修复,更新索引统计和整理表碎片的查询跳转到表维护模块;复制相关的语句跳转到复制模块;查询状态的语句跳转到状态报告模块。还有大量的表更新模块:Delete模块,Create模块,Update模块,Insert模块和Alter模块。

在这个时候,每个从解析器获得控制权的模块将查询中涉及到的表单列表传递给访问控制模块,然后在可以访问的情况下将列表传到表管理器,表管理器打开表并获取到必要的锁。现在表操作模块将继续进行它的任务并将向抽象存储引擎模块发送大量的请求以完成低层操作,如插入或更新一条记录,根据关键字获取记录行,或执行一个表级操作,例如修复表或更新索引统计信息。

抽象存储引擎模块将利用对象的多态性自动将请求转化为具体存储引擎的对应的方法。换句话说,在处理一个存储引擎对象时,调用者以为处理一个抽象类型,而实际上处理的是一个更具体的类型。换句话说,当处理一个存储引擎对象时,调用者以为在处理一个抽象类型,而实际上这个对象是更具体的类型:即给定表类型所对应的那个存储引擎对象。接口方法是个虚拟的,用来创建透明的效果。正确的方法将被调用,而调用者并不需要关心存储引擎的具体类型。

一旦查询或命令开始被处理,相关的模块可能会将一部分有效的结果发送到客户端。也有可能会发送一些警告信息或是一条错误信息。如果一个错误发生了,服务器端和客户端都会明白这个查询或命令失败了,并会采取合适的措施。客户端不会再接受任何与该查询的有关结果集、警告或错误消息,但当接服务器端发送错误(信号)之后还会一直接受对连接线程控制。注意当MySQL为了稳定性和扩展性而没有使用异常的话,在所有层级的所有调用都必须采用适当的传输控制手段来检查针对这种情况的错误。

如果低层的模块用某种方式更改了数据,并且二进制更新日志也是激活的话,这个模块也将负责请求日志模块将更新事件记录到二进制更新文件中去,除了MySQL开发者和高级用户把它叫作二进制日志外,有时它还叫复制日志。

一旦任务完成,执行流程将回到连接线程,连接线程执行必要的清理工作并等待另一个来自客户端的查询或命令。会话将会在一直保持,直到客户端发送退出命令。

除了与常规的客户端交互以外,服务器端还会从一个复制从服务器上接受连续读取二进制更新日志的命令。这个命令被复制主库模块处理。

如果服务器被配置为复制从库,初始化模块将调用复制从库模块,它将依次起动两个线程,SQL线程和I/O线程。它们负责将主库上发生的更新传递到从库上。一台机器有可能被同时定义为主库和从库。 带有客户端的网络通信将会通过客户端/服务器协议模块,它负责对数据按一定的格式打包,并根据连接设置进行压缩。客户端/服务器协议依次使用低层的网络I/O模块,网络I/O模块可以在不同的平台之间在socket级接发数据。当连接设置了安全相关参数时,客户端/服务器协议也负责用OpenSSL包对数据进行加密。 当在执行各模块任务的时候,核心组件严重依赖核心API。核心API提供丰富的函数集,包括文件I/O、内在管理、字符串操作、各种数据结构与算法的实现以及很多其它有用的功能。应该鼓励MySQL开发者避免直接调用libc包,而是使用核心API使得将来的平台移植及代码优化变得更容易。

图1-1 阐明了核心模块与其它模块的交互

"高层MySQL模块视图" 图1-1 高层MySQL模块视图

详述内核模块

现在,我们将更近距离地接触每个组件。讨论的目的之是把之前提到的概念与实际的代码关联起来。另外,我们将会提及每个组件的部分历史并试着评估其将来的发展之路。

经常提到的源码将会被编译,其实你会发现将提及的文件用文本编辑器打开并定位到函数引用的位置是很有帮助的。调试器也会做同样的事,第3章会提到,那章也会告诉你如何拿到源码。

服务初始化模块

服务初始化模块负责服务在启动时的初始化工作,大多数代码可以在sql/mysqld.cc中找到。入口就是C/C++程序员所期望的:main()。下面是其它一些有趣的函数。如果所有文件没有标明,就在sql/mysqld.cc中。

  • init_common_variables()
  • init_thread_environment()
  • init_server_components()
  • grant_init() 在sql/sql_acl.cc
  • init_slave() 在sql/slave.cc
  • get_options()

虽然这些代码在3.22版中被创建之后就再也没有被从头重写过,但它被很具有意义地重构为将其做为新功能加入到MySQL中。一个大的初始化代码的变化是过去在main()下的函数在代码的生命周期中渐渐被重组为大量的辅助函数了。此外,当4.0版本的核心模块API模块的选项解析功能有效时,命令行和配置文件选项的解析也从GUN的getopt()转换为MySQL的核心API选项解析了。

在5.1版本中,一个具有重大意义的初始化插件的功能加到init_server_components()中。 总体来说,这段代码是相当稳定的。基于过去的历史,我们可以预言:将来在启动时需要特殊的初始化工作时,这段代码将要加入新的特性。无论如何,要重写这段代码是不太可能的。

连接管理器

连接管理器监听来自客户端的连接,分派这些请求到线程管理器。该模块也就是sql/mysqld.cc中的一个函数:handle_connections_sockets()。尽管如些,由于它在服务运行中担当的决定性的角色,值得被划分为一个单独的模块。大量的#ifdef指令昭示着将网络代码移植到种种操作系统上的挑战。

随着时间的推移,这段代码或多或少都在慢慢进化以适应不同操作系统下的奇奇怪怪的网络系统的调用。将来更多的更改可能是尝试新的移植,或是不同的操作系统供应商引进新的接口到新版的产品中。

线程管理器

线程管理器负责保持跟踪线程并保证一个线程被分派去处理客户端连接。这是另一个小的模块。大多数代码在sql/mysqld.cc中。入口是create_new_thread()。另一个有意思的函数是同一文件中的start_cached_thread()。

要注意的是在sql/sql_class.sh中定义了THD类并在sql/sql_class_cc中实现了它,成为了这个模块中的一部分。THD类型的对象是线程描述符(THead Descriptor),在大多数的服务模块中是至关重要的。很多函数将第一个参数设置为THD的指针。

线程管理器的代码在3.23的版本中加入了线程缓存后进行了大量的重写,自那时起代码就没有被明显改动过。将来不愿再对代码做任何明显的改动自在情理之中。

然而在我的概念中,关于将THD类当成这个模块的一个部分这个改动还有另外一个不同的故事。诸如预编译语句(prepared statements)、服务器端的游标以及存储过程等新加的、特性导致了4.1版和5.0版中THD类代码的大量重写。它现在是Query_arena、Statement和Open_tables_state类的父类,这些类也被定义在sql/sql_class.h中。

连接线程模块

连接线程是在已建立的连接上处理客户端请求的重点。这个模块也相当的小。它只由一个函数组成:在sql/sql_parse.cc中的handle_one_connection()。尽管如此,不考虑它的大小,见于它在服务中起扮演的角色,它也值得被归类为一个模块。

代码随时间进化,当各种调用THD变量的初始化工作被移至THD类之后,它渐渐变得简洁且易读。这段代码在将来不要做太多修改的愿望也是合情合理的。

用户权限认证模块

用户权限认证模块验证来连接的用户并初始化该用户所在权限级别的结构体及变量。该模块的入口是sql/sql_parse.cc中的check_connection()。但这个功能的其它部分在sql/sql_acl.cc和sqlpassword.cc中。一些有意思的检查的函数包括:

  • acl_check_host() sql/sql_acl.cc中
  • check_random_string() sql/password.cc中
  • check_user() sql/sql_parse.cc中
  • acl_getroot() sql/sql_acl.cc中

代码只在4.1版本中被明显地重写过一次。由于这个改动可能会带来影响,MySQL开发者为更新需要一个更安全的认证机制的协议耽误过一阵子。

自那以后,他们没有对代码做太多的改动。但是,随着5.1版中plug-in新功能的加入,MySQL开发者打算加入可插拔的认识与角色功能,这将引起代码的改动。

访问控制模块

访问控制模块来验证客户端的用户是否有足够的权限来执行请求的操作。大多数代码都在sql/sql_acl.cc中,一个非常常用的函数check_access()却在sql/sql_parse.cc中。下面列了其它一些我们关心的函数,除了特别指出的外,都能在sql/sql_acl.cc中找到。

  • check_gran()
  • check_table_access() sql/sql_parse.cc
  • check_grant_column()
  • acl_get()

自从3.22版本之后这些代码就没怎么动过。但在4.0版中加入过新的权限类型,这稍微改变了其它代码的使用方法。MySQL开发者打算加入角色的支持,这也会起对该模块大的改动。

解析器

解析器负责解析请求并构造一颗解析树。模块的入口是sql/sql_parse.cc中的mysql_parse(),它执行一些初始化工作,然后调用sql/sql_yacc.cc中由GNU Bison生成的yypaser(),GNU Bison在sql/sql_yacc.yy中,包含了MySQL所能理解的SQL语言集合的定义。要注意的是,不像其它很多的开源项目,MySQL用自己的语法扫描器代替lex(一个词法分析器生成工具)。关于MySQL语法扫描器会在第九章详细谈到。除了刚刚提到的之外,另外一些相关的文件还包括:

  • sql/gen_lex_hash.cc
  • sql/lex.h
  • sql/lex_symbol.h
  • sql/lex_hash.h(生成的文件)
  • sql/sql_lex.h
  • sql/sql_lex.cc
  • sql/目录下以item_开头的,以.h或.cc为后缀的一件文件

当有新的SQL的特征增加时,解析器要同步变更以适应新的情况。当然,解析器的核心结构相当稳定,到目前为止还是能适应增长。预计当有新的元素加入时,核心代码不会变动太多。MySQL的开发者曾经,其实有时一直在讨论将核心代码重写并移出yacc/Bison以提高速度。就这样,他们已经讨论了至少7年了,到目前还没有被列为优先解决的问题。

命令调度器

命令调度器负责将请求分派到知道该如何处理它们的低层的模块中。它由sql/sql_parse.cc中的do_command()和dispatch_command()两个函数组成。

随着可被支持的命令的增加,这个模块也在变大。将来代可能会发生小幅的增大,但核心结构不太可能有变动。

查询缓存模块

查询缓存模块缓存查询结果,每当有缓存结果时,通过返回已经缓存的结果集来缩短查询周期。该模块的实现在sql/sql_cache.cc中。一些用到的方法包括:

Query_cache::store_query()
Query_cache::send_result_to_client()

该模块是在4.0版中被加入的。除了修正一些bug之外,将来很少会被改动。

优化器

优化器负责生成响应查询的最佳策略,执行并将结果返回给客户端。这可能是MySQL代码中最复杂的一个模块了。入口是sql/sql_select.cc中mysql_select()函数。这个函数将会在第九章中进行讨论。其它一些用到的函数和方法都在sql/sql_select.cc中,包括: * JOIN:prepare() * JOIN:optimize() * JOIN:exce() * make_join_statistics() * find_best_combination() * optimize_cond()

当你深入到优化器的深度时,你会发现这是一个值得一观的洞穴。范围优化器(range optimizer)是一个单独存放在sql/opt_range.cc中的完全与优化器隔离开并足够复杂的优化器,负责优化使用索引来检索位于一个或多个给定的范围内的值。范围优化器的入口是SQL_SELECT::test_quick_select()

优化器一直处理更改的状态。4.1版中在另一个复杂的层中加入了子查询。5.0中加入了贪婪搜索(greedy search)来优化表的关联顺序,并让每个表能用到多个索引(索引合并)。预计将来会有大量的修改。一个期待已久的改变就是子查询优化。

表单管理器

表单管理器负责创建、读取更新表定义文件(.frm后缀的)、维护被叫做table cache的表单描述符的缓存和管理表级锁。大多数据代码在sql/sql_ base.cc、sql/table.cc、sql/unireg.cc和sql/lock.cc中。这个模块将在第9章中详解。一些被用到的函数包括:

  • openfrm() sql/table.cc中
  • mysql_create_frm() sql/unireg.cc中
  • open_table() sql/sql_base.cc中
  • open_tables() sql/sql_base.cc中
  • open_ltable() sql/sql_base.cc中
  • mysql_lock_table() sql/lock.cc中

这些代码自3.22版之后就没怎么动过,除了4.1版本中加入新的表格式外。在过去,Monty曾表达过对这些低效的table cache代码的不满并打算重写。但此后并没有列入到最重要的事中。尽管如此,一些改进在5.1版中有所体现。

表单更新模块

这一系列模块负责对表的create/delete/rename/drop/update/insert等操作,在代码上也有着明显的区别,篇幅所限,恕不详叙。尽管如此,一旦你对其它的代码熟悉了,你可以阅读源码并通过以下的入口毫不费力地进行调试并找出它们之间的区别:

  • mysql_update()和mysql_multi_update() sql/sql_update.cc中
  • mysql_insert() sql/sql_insert.cc中
  • mysql_create_table() sql/sql_table.cc中
  • mysql_alter_table() sql/sql_table中
  • mysql_rm_table() sql/sql_table中
  • mysql_delete() sql/sql_delete.cc中

Update和Delete模块在4.0版中因加入多表更新和删除而做了大量改动。为了支持4.1版的预编译语句(prepared statement)和5.1中触发器,Update、Insert和Delete模块也做了一些重组。除了时不时有一些极小的性能提升外无其它太大的动作。将来很大的一部分代码将会保持原貌。

表单维护模块

表单维护模块负责如check、repair、backup、restore、optimize(defragment)和analyze(更新关键字分布统计)等操作。代码可以在sql/sql_table.cc中找到。核心函数是mysql_admin_table()以及下面的好使的封装: * mysql_check_table() * mysql_repair_table() * mysql_backup_table() * mysql_restore_table() * mysql_optimize_table() * mysql_analyze_table() * mysql_admin_table()将请求进一步转发给相应的存储引擎的方法。大量的工作产生在存储储引擎级别。

在3.23版本中引入该模块以提供表单维护的SQL接口,在此之前表单维护必须在线下执行。在4.1版本中对网络协议模块进行了大量的修改以支持预编译语句。它影响了所有的对客户端有回馈的模块,包括表单维护模块。其它方面自引入以来就没有太多的更动,将来也不会有。

状态报告模块

状态报告模块负责响应服务器配置、性能跟踪变量、表结构信息、复制(replication)进程及table cache的状态等等相关的请求。它处理以show开头的查询。大多数代码在sql/sql_show.cc中。一些有趣的函数除了指明的外都在sql/sql_show.cc中: * mysqld_list_processes() * mysqld_show() * mysqld_show_create() * mysqld_show_fields() * mysqld_show_open_tables() * mysqld_show_warnings() * show_master_info() sql/slave.cc中 * show_binlog_info() sql/sql_repl.cc中

该模块被持续地改进。加了新的状态报告功能,就会加入新的函数,这种模式会一直延续。

抽象存储引擎接口(表处理器)

该模块其实就是一个叫handler抽象类和一个叫handlerton的结构体。handerton结构体是在5.1中为plug-in集成加入的,它提供了一个标准化的接口来执行低层的存储与检索操作。

表处理器在sql/handler.h中定义,部分在sql/handler.cc中实现。从它派生的具体的存储引擎类必须实现所有父类的纯虚函数。这将在第9章更详细地讨论。

该模块在3.23版本为了帮助集成BerkeleyDB的表而引入。它产生了非常深远的影响:现在各种各样的低层的存储引擎可以相当轻易地放到MySQL下。在集成InnoDB的过程中,代码也得到了很好的提炼。在将来该模块在很大程序上依赖于什么样的存储引擎将被集成进来以及已经存在的存储引擎变化。比如,有时下面的一些存储引擎的新特性要求添加新接口使得对高层的模块可用。

存储引擎实现(MyISAM, InnoDB, MEMORY, Berkeley DB)

每一种存储引擎都通过继承前面提到的handler类提供了一个标准操作接口。继承类的方法依据具体存储引擎的低层调用定义了标准操作接口。这部分过程与单独的存储引擎将会在第10章中详解。为一睹为快起见,你可以先看一眼下面的少数有意思的文件与文件夹:

  • sql/ha_myisam.h与sql/ha_myisam.cc
  • sql/ha_innodb.h与sql/ha_innodb.cc
  • sql/ha_heap.h与sql/ha_heap.cc
  • sql/ha_ndbcluster.h与sql/ha_ndbcluster.cc
  • myisam/
  • innobase/
  • heap/
  • ndb/

自打在3.23版中存储引擎被第一次抽象后,只有三个完整的引擎:MyISAM,ISAM(老版的MyISAM)和MEMORY。(注意MEMORY存储引擎早期叫HEAP,在源码中一些文件和文件夹中还能反映出早期名字的痕迹)。然而随着BerkeleyDB,MERGE,InnoDB以及近期涌显出来的NDB MySQL集群等引擎的加入,这个列表飞速增大。大多数存储引擎仍然在开发阶段,我们将来会看到更多的新的引擎加入。

日志模块

日志模块负责维护更高层的(逻辑层)的日志。一个存储引擎出于自身目的可以另外管理自身更低层的(物理层或逻辑层)的日志,但日志模块不涉及这些,而由存储引擎自己掌管。这里所指的逻辑日志乃是二进制更新日志(也可用来做复制)、命令日志(用来系统监视与应用调试)和慢查询(用来跟踪不优化的查询)。

5.1版本之前,该模块大部分在MYSQL_LOG这个类中,该类在sql/sql_class.h中定义,在sql/log.cc中实现。5.1版对该模块进行了重写。现在有一系各层级的管理类,MYSQL_LOG是TC_LOG的父类,它们都在sql/log.h中。

当然,绝大多数日志的工作是在记录二进制复制日志。用来记录事件的创建与读取二进制复制日志的类在sql/log_event.h中定义,在sql/log_event.cc中实现。复制主库模块与复制从库模块也在很大程度上依赖日志模块的功能。

当复制引入进来后模块被大量修改过。5.0中分布式事务导致一些修改。5.1版中加入了像操作SQL表单那样搜索日志的功能,这导致了大量代码重构。二进制日志部分也做了大量的修改以适应基于行的复制。此时些刻很难预估这些代码将来会有什么改变。

复制主库模块

复制主库模块负责主库上的复制功能。该模块最常用的功能就是发送持续的复制日志事件反馈到请求的从库。大部分代码在sql/sql_repl.cc中。核心函数是mysql_binlog_send()。

该模块在3.23版中加入,它除了一次将大量代码从函数中隔离出的的彻底的清理外没有经历任何大的更动。起初,这些代码都有着雄心勃勃的自动防止故障的开发计划。但是当这些计划要实施的时候,MySQL从爱立信拿到了NDB集群的代码,并开始走上自动故障处理终极目标的另一条路。根据那些开发情况,此时本地的MySQL在复制如何发展这一点上还不明朗。

这个模块将会在12章中进行更详细的讲解。

复制从库模块

复制从库模块负责从库上的复制功能。从库的职责就是从主库上获取更新,然后在从库上再应用一遍。在4.0版中,从库起动两个线程。网络I/O线程向主库请求并接收持续的更新反馈,并将他它记录为本地中继日志。SQL线程将在将他们从中继日志中读出来之后再应用。这些代码在sql/slave.cc中。要学习的最重要的函是handle_slave_io()和handle_slave_sql()。

该模块在3.23中与复制主库模块一同加入。在4.0中在一起的从库线程分解为SQL线程与I/O线程时这部分代码经过一次大量的改动。

该模块将在第12章有更详细的讨论。

客户端/服务器协议API

MySQL客户端/服务器通信协议在协议栈中位于操作系统协议(TCP/IP或本地socket)之上。该模块实现了跨平台创建、读到、解读和发送协议包的API。代码在sql/protocol.cc,sql/protocol.h和sql/net_serv.cc中

sql/protocol.h和sql/protocol.cc定义和实现了一系列的类。Protocol是基类,Protocol_simple、Protocol_prep和Protocol_cursor继承了他。模块中一些有趣的函数如下:

  • my_net_read() 在sql/net_serv.cc中
  • my_net_write() 在sql/net_serv.cc中
  • net_store_data() 在sql/protocol.cc中
  • send_ok() 在sql/protocol.cc中
  • send_error() 在sql/protocol.cc中

4.0版中该模块为支持4GB的包而被修改过,在此之前,包的上限是24MB。该模块的类层次结构是在4.1中为处理预编译语句而加入的。看起来该层面的大多数问题已经被解决了,将来也不会有太多的代码改动。但开发者在考虑加入消息支持。

该模块会在第5章做更详细的讨论。

低层网络I/O API

低层网络I/O API提供一个低层网络I/O和SSL会话的抽象。代码在vio/文件夹下,该模块的甩的函数都是在vio_开头了。 在需要支持SSL连接的需求刺激下,该模块在3.23中被引入。抽象低层的网络I/O也更方便了往新操作系统上的移植与维护老的端口。

核心API

核心API是MySQL的瑞士军刀。它提供了可移植的文件I/O、内存管理、字符串操作、文件系统导航、格式化打印,一个数据结构与算法的集合和大量的其它的功能。如果遇到一个问题,经常会在核心API中能找到解决方法,如果没有,那它正在开发中。该模块在很大程度上是Monty能力的体现,也不是为了仅仅去解决一个问题,它可能是MySQL神器的核心组件。

代码在mysys/和strings/文件夹下。大多数核心API的函数是my_开头。 这个模块已经在壮大与提高的路上。当有新的功能加入时,关注点是保持它的稳定性及高层的性能上。这种风格将一如既往。

该模块会在第3章详述。

MySQL的历史

MySQL的历史始于1979年,那时Monty Widenius在一个叫TcX的小公司工作,他用BASIC编写了一个运在4M主频、16K内存计算机上的报表工具。随着时间的推移,这个小工具被用C重写并迁移到Unix上运行。它还仅仅是一个低级的存储引擎再配上一个报表前端。它就是大家知道的Unireg。

或许是需要在比较烂的计算机硬件上工作,抑或是依靠天生的聪明才智,Monty练就了编写非常高效代码的习惯与能力。同时他还练就了,或者是天赋了具备常人所不能及的前瞻能力,使他能在不知道未来太多开发细节的情况下,怎样让现在的代码对以后的开发有所帮助。

除了以上原因之外,因TcX是一个小公司且Monty是其合伙人之一,所以代码如何编写,Monty相当有发言权。尽管有着为数不少跟Monty一样有天赋与才华的人,但因种种原因,很少有人能左右自己的代码20年以上,但Monty做到了。

Monty的作品、天赋以及他的这种对代码的所有权为MySQL这一奇迹的出现奠定了基础。

上世纪90年代某年某月某日,TcX的客户急切要求访问其数据的SQL接口,TcX出了好几种方案,一种是把数据都加载进一个商用数据库,但Monty对这个方案的执行速度并不满意。他尝试将mSQL的代码用做SQL层,并用自己的低层级的存储引擎集成进来,但效果也不好。然后一场由压抑的天才的程序员举旗的运动爆发了:“别人不靠谱,我要自己搞!”

就这样,1996年五月份,MySQL 1.0发布给一小撮人,同年10月,MySQL 3.11.1正式发布。这个发布版只提供一个Solaris的二进制发布版,一个月后,源码和Linux二进制包也发布了。

接下来的两年中,随着新的特性逐步增加,MySQL被移植到其它的操作系统上。起初MySQL发布的时候遵循一种特殊许可证:允许MySQL可以商用,但利用MySQL所开发的软件不能再次发布。特殊许可证可以颁给那些想销售捆绑MySQL的产品的人。另外,TcX也提供商业支持有偿服务。尽管MySQL最初的目标早已实现,但这些举措为TcX对MySQL的深入开发指明了方向。

在这一段时期中MySQL发展到3.22,它支持了适度的SQL集,还带有一个比我们想象的可以由一个人可以搞定的那种要更复杂的优化器,新版本速度很快,且非常稳定。API的大量提供,让你几乎可以用任何一种地球上的编程语言编来写它的客户端。尽管如此,它还是不支持事务、子查询、外键、存储过程和视图。只在表级别存在的锁,时不时会造成响应缓慢更甚挂机。很多不得要领的MySQL爱好者只是把它拿来玩玩而已,而高级用户则乐呵呵地把数据从Oracle和SQL Server上往MySQL上迁移,以解决性能问题和降低成本,并编写代码弥补MySQL自身的缺陷不足。

大约在1999-2000,一个名叫MySQL AB的独立公司成立了,它聘请了几位开发者并与Sleepycat公司建立合作伙伴关系,提供访问Berkeley DB数据文件的SQL接口。自从Berkeley DB具备事务功能之后,它就赋于了MySQL支持事务的特质,补上了它的短板。在为集成Berkeley DB做了一些修改之后,MySQL 3.23发布了。

尽管MySQL的开发者没能完全解决掉Berkeley DB接口的所有缺点,且Berkeley DB的表也不甚稳定,但是,功夫没白费。MySQL的源码中加入的钩子,这样就可以加入任何类型的存储引擎了,当然也就可以加入支持事务的了。

到了2000年四月,有了Slashdot的支持与赞助,master/slave机制加入。不支持事务的老存储引擎ISAM被重写了,并以MyISAM的形式发布。经过大量的改良,当前版本也支持了全文搜索。MySQL AB还曾有一个跟NuShpere短期的合作,在MySQL中加入了一个支持行级锁和事务的引擎Gemini,但因法律问题于2001年结束了。也就在这时,Heikki Tuuri提议用自己的引擎集成到其中,并接手MySQL AB的工作,这个引擎就是同样支持行级锁和事务的InnoDB。

因为之前Berkeley DB已经完成了新的表处理接口,所以Heikki的集成工作变得更加的顺畅。2001年10月,MySQL/InnoDB版发布了,版本号是4.0 alpha。2002年初,MySQL/InnoDB组合已经是非常快速稳定了,但在2003年3月份,4.0版才正式宣布。

值得一提的是,版本号的变更并是不因为加入了InnoDB。MySQL的开发者只是把InnoDB当成是一个插件,完全没把它当成是他们赖以成功的法宝。从过去到现在,一个新的存储引擎的加入也不值得发布一个版本号来庆贺。实际上,跟以前的版本比较起来,4.0并没加太多的新东西。也许新加的最有意义就数查询缓存了(Query Cache),它大大提升了众多应用的性能。用来在slave上复制数据的代码也重写了,变成了两个线程,一个用来处理master上的网络IO,另一个用来处理更新。另一些是关于优化器的改良。客户端/服务器间的协议也可以用SSL加密。

2003年4月4.1 alpha版本发布,2004年6月4.1 beta版发布。这可不像4.0,这次加入了很多有意义的功能。最有意义的就是子查询,一个大家期待已久的功能。空间索引支持也加到了MyISAM引擎中。Unicode支持也被实现了。客户端/服务器协议也有了大量的改动,面对黑客的攻击它更安全,而且支持预处理语句(prepared statement)。

与4.1同步进行的还有另外一个分支版本5.0,它加入了存储过程,服务器端游标,触发器,视图,分布式事务(Xa transactions),查询优化器的显著改进以及其它的一些特性。MySQL的开发者发现如果要让4.1 稳定下就要花很大的工夫,如果他们要把新特性都加进去,就必须先搞定存储过程,这让他们痛下决心另创了一个分枝来开发5.0。5.0 alpha版最终于2003年12月发布了。曾经一度因这个造成了一些混乱——两个分枝同时处在alpha 阶段,后来(2004年10月)4.1稳定了,这个尴尬也就随之而解了。

5.0也在一年之后,也就是2005年10月,也稳定下来了。

5.1 alpha版紧随其后,于2005年11月发布,其中加了好多的新的改进,如表分区(table data partitioning),基于行的复制(row-based replication),事件调度器(event schedular),和让新存储引擎和其它插件轻松集成的标准化插件API。

2008年1月16号 MySQL被Sun公司收购。

2009年04月20日Oracle收购Sun,MySQL归Oracle麾下。

2010年04月22日发布MySQL 5.5。

2013年02年05日发布MySQL 5.6.10 GA。

计算一个月里有多少天

MySQL并没有提供直接计算某个月里有多少天的函数,但是我们可以用last_dayday这两个函数来配合达到目的。

函数解释

last_day(date)

返回date所在月的最后一天

MariaDB [test]> select last_day('2013-08-14');
+------------------------+
| last_day('2013-08-14') |
+------------------------+
| 2013-08-31             |
+------------------------+
1 row in set (0.00 sec)

MariaDB [test]> select last_day('2012-02-01');    
+------------------------+
| last_day('2012-02-01') |
+------------------------+
| 2012-02-29             |
+------------------------+
1 row in set (0.00 sec)

day(date)

返回date中的天,它与dayofmonth有一样的作用。

MariaDB [test]> select day('2013-08-14');     
+-------------------+
| day('2013-08-14') |
+-------------------+
|                14 |
+-------------------+
1 row in set (0.00 sec)

MariaDB [test]> select dayofmonth('2013-08-14');
+--------------------------+
| dayofmonth('2013-08-14') |
+--------------------------+
|                       14 |
+--------------------------+
1 row in set (0.00 sec)

搭配效果

MariaDB [test]> select day(last_day('2013-08-14')); 
+-----------------------------+
| day(last_day('2013-08-14')) |
+-----------------------------+
|                          31 |
+-----------------------------+
1 row in set (0.00 sec)

扩展阅读

Group中的最大值

问题描述

某年某月某日某MySQL DBA的Q群中,说出一道面试题,求根据列A聚集(Aggregate)后B列中最大(或最小)值所对应的C列,为了提高难度,要求不要用关联和子查询。

场景举例

求用户最后登录IP:表login_log有三个字段(user, login_time, login_ip),记录用户登录log,求每个用户最后一次登录的IP,即按user聚集后最大的login_time所对应的login_ip。

解决方案

数据准备

我用的是MariaDB,在使用上,跟MySQL区别不大,命令提示符是MariaDB [test]>,test是数据库名。

执行以下SQL:

MariaDB [test]> create table login_log (user int not null, login_time datetime, login_ip varchar(15) ); 
Query OK, 0 rows affected (0.02 sec)

MariaDB [test]> desc login_log;
+------------+-------------+------+-----+---------+-------+
| Field      | Type        | Null | Key | Default | Extra |
+------------+-------------+------+-----+---------+-------+
| user       | int(11)     | NO   |     | NULL    |       |
| login_time | datetime    | YES  |     | NULL    |       |
| login_ip   | varchar(15) | YES  |     | NULL    |       |
+------------+-------------+------+-----+---------+-------+
3 rows in set (0.00 sec)

MariaDB [test]> insert into login_log 
 (user, login_time, login_ip) values 
 (1, '2013-01-01', '192.168.1.110'), 
 (1, '2013-06-05', '192.168.1.119'), 
 (2, '2013-01-01', '192.168.1.112'), 
 (2, '2013-06-06', '192.168.1.120'); 
 uery OK, 4 rows affected (0.00 sec)
 Records: 4  Duplicates: 0  Warnings: 0

 MariaDB [test]> select * from login_log;
+------+---------------------+---------------+
| user | login_time          | login_ip      |
+------+---------------------+---------------+
|    1 | 2013-01-01 00:00:00 | 192.168.1.110 |
|    1 | 2013-06-05 00:00:00 | 192.168.1.119 |
|    2 | 2013-01-01 00:00:00 | 192.168.1.112 |
|    2 | 2013-06-06 00:00:00 | 192.168.1.120 |
+------+---------------------+---------------+
4 rows in set (0.00 sec)

错误的做法

MariaDB [test]> select user, max(login_time), login_ip from login_log group by user;
+------+---------------------+---------------+
| user | max(login_time)     | login_ip      |
+------+---------------------+---------------+
|    1 | 2013-06-05 00:00:00 | 192.168.1.110 |
|    2 | 2013-06-06 00:00:00 | 192.168.1.112 |
+------+---------------------+---------------+
2 rows in set (0.02 sec)

没有出现我们想要的结果,严格来讲,这个SQL是不合格的,没有在group by中列出的列是不能出现在select list中,只是MySQL默认的sql_mode ONLY_FULL_GROUP_BY 没有打开,允许可以这么写而,不然

select max(login_time), min(login_time), login_ip from login_log group by user;

这个SQL你期望login_ip是最大值还是最小值呢?

正确的做法

MariaDB [test]> select user, substring_index(group_concat(login_ip order by login_time desc),',',1) last_ip from login_log group by 1;
+------+---------------+
| user | last_ip       |
+------+---------------+
|    1 | 192.168.1.119 |
|    2 | 192.168.1.120 |
+------+---------------+
2 rows in set (0.02 sec)

看起来不错,确实是我们所想要的数据。这条sql使用了group_concat函数将所有的login_ip用’,’拼成一个字符串,并按最后登录时间排倒序,最后用substring_index将第一个’,’前的字符串提取出来。

函数解释

group_concat

一个聚集函数,可以把某个列或某几列中的数据连接到一起,可以根据其它字段排序,可以指定连接符号。

MariaDB [test]> select user, group_concat(login_ip order by login_time desc) all_ip from login_log group by 1;
+------+-----------------------------+
| user | all_ip                      |
+------+-----------------------------+
|    1 | 192.168.1.119,192.168.1.110 |
|    2 | 192.168.1.120,192.168.1.112 |
+------+-----------------------------+
2 rows in set (0.01 sec)

MariaDB [test]> select user, group_concat(login_ip order by login_time desc SEPARATOR '##') all_ip from login_log group by 1;
+------+------------------------------+
| user | all_ip                       |
+------+------------------------------+
|    1 | 192.168.1.119##192.168.1.110 |
|    2 | 192.168.1.120##192.168.1.112 |
+------+------------------------------+
2 rows in set (0.00 sec)

substring_index

一个子字符串提取函数,有三个参数,用法是substring_index(str,delim,count),提取str中由第count个分割符delim前的字符串。如果count为负数,则从右往左截取。

MariaDB [test]> select substring_index('a,b,c,d', ',', 2);/*第2个,前的字符串。*/
+------------------------------------+
| substring_index('a,b,c,d', ',', 2) |
+------------------------------------+
| a,b                                |
+------------------------------------+
1 row in set (0.00 sec)

MariaDB [test]> select substring_index('user@renren.com', '@', -1);/*从右往左第1个@后的字符串。*/
+---------------------------------------------+
| substring_index('user@renren.com', '@', -1) |
+---------------------------------------------+
| renren.com                                  |
+---------------------------------------------+
1 row in set (0.00 sec)

扩展阅读