`

Mysql源代码分析系列

阅读更多
Mysql源代码分析系列(2): 源代码结构


Mysql源代码主要包括客户端程序代码,服务器端代码,测试工具和一些库构成,下面我们对比较重要的目录做些介绍。



BUILD
这个目录在本系列的上篇文章中我们仔细看过,内含各种平台的编译脚本,这里就不仔细说了。


client
这个目录下有如下比较让人眼熟的文件: mysql.cc, mysqlcheck.c, mysqladmin.cc, mysqlshow.c,等等,如果你编译一下就会发现那些眼熟的程序也出现了,比如mysql。明白了吧,这个目录就是那些客户端程序所在的目录。这个目录的内容也比较少,而且也不是我们阅读的重点。


Docs
这个目录包含了文档。


storage
这个目录包含了所谓的Mysql存储引擎 (storage engine)。存储引擎是数据库系统的核心,封装了数据库文件的操作,是数据库系统是否强大最重要的因素。Mysql实现了一个抽象接口层,叫做handler(sql/handler.h),其中定义了接口函数,比如:ha_open, ha_index_end, ha_create等等,存储引擎需要实现这些接口才能被系统使用。这个接口定义超级复杂,有900多行 :-(,不过我们暂时知道它是干什么的就好了,没必要深究每行代码。对于具体每种引擎的特点,我推荐大家去看mysql的在线文档: http://dev.mysql.com/doc/refman/5.1/en/storage-engines.html


应该能看到如下的目录:

* innobase, innodb的目录,当前最流行的存储引擎

* myisam, 最早的Mysql存储引擎,一直到innodb出现以前,使用最广的引擎。

* heap, 基于内存的存储引擎

* federated, 一个比较新的存储引擎

* example, csv,这几个大家可以作为自己写存储引擎时的参考实现,比较容易读懂



mysys

包含了对于系统调用的封装,用以方便实现跨平台。大家看看文件名就大概知道是什么情况了。


sql

这个目录是另外一个大块头,你应该会看到mysqld.cc,没错,这里就是数据库主程序mysqld所在的地方。大部分的系统流程都发生在这里。你还能看到sql_insert.cc, sql_update.cc, sql_select.cc,等等,分别实现了对应的SQL命令。后面我们还要经常提到这个目录下的文件。


大概有如下及部分:

SQL解析器代码: sql_lex.cc, sql_yacc.yy, sql_yacc.cc, sql_parse.cc等,实现了对SQL语句的解析操作。

"handler"代码: handle.cc, handler.h,定义了存储引擎的接口。

"item"代码:item_func.cc, item_create.cc,定义了SQL解析后的各个部分。

SQL语句执行代码: sql_update.cc, sql_insert.cc sql_select.cc, sql_show.cc, sql_load.cc,执行SQL对应的语句。当你要看"SELECT ..."的执行的时候,直接到sql_select.cc去看就OK了。

辅助代码: net_serv.cc实现网络操作

还有其他很多代码。



vio

封装了virtual IO接口,主要是封装了各种协议的网络操作。


plugin

插件的目录,目前有一个全文搜索插件(只能用在myisam存储引擎)。


libmysqld

Mysql连接库源代码。


开源函数库目录

和所有的开源项目一样,Mysql也使用了一些开源的库,在其代码库中我们能看到dbug、pstack、strings、 zlib等。




多说无益,主要是对于mysql的代码目录有个概念,要找的时候也有个方向。万一要找某个东西找不到了就只能grep了...

Mysql源代码分析系列(3): 主要调用流程



引言

本文主要介绍Mysql主要的调用流程,将从代码的角度来看一个从用户发出的"select * from test" SQL命令在服务器内部是如何被执行的。从我个人的经验来看,阅读理解大规模项目的代码最重要的两个方面,一是了解主要的数据结构,二是了解数据流,在这里主要是调用流程。把这两个主线把握住以后,大部分代码都是比较容易阅读的,Mysql的源代码属于比较好读的类型,因为函数的调用关系比较明确。难读的代码一般都充斥着大量的回调、异步调用,很可能你极难找到某个函数在哪里或什么时候被调用了。当然,算法的实现代码也很难读。幸好Mysql不是那种难读的类型,所以我们也不要害怕,大步向前吧!

主要执行过程
从架构上来看,Mysql服务器对于一条SQL语句的执行过程可以分成如下几部分:

接受命令                 包括用户验证,资源申请等

    |

    V

命令解析                 解析SQL语句,生成语法树

    |

    V

寻找执行计划            根据解析出来的语法树,找到可能的执行计划。对于一条SQL语句,很可能会有多种执行方案,特别是在SQL语句比较复杂的时候。这里需要对于各种可能的方案进行代价评估,最快的找到最有的执行方案。

    |

    V

优化执行计划            优化执行计划。这是SQL执行中最复杂的部分之一,据说全都是由数学博士们写出来的,而且比较难懂。我目前还处于不懂的状态。

    |

    V

  执行                    没啥可说的,只剩执行及返回结果了




系统启动

所有的程序都从main开始,mysqld也不例外,打开sql/mysqld.cc,稍加搜索,你就能看到熟悉的main函数,我们可以将其进行如下简写:

int main(int argc, char* argv[]) {

  logger.init_base();


  init_common_variables(MYSQL_CONFIG_NAME, argc, argv, load_default_groups));                // 解析配置文件和命令行参数,将配置文件中的内容转行成命令行参数

   init_signals();   

   user_info= check_user(mysqld_user);

   set_user(mysqld_user, user_info);

   init_server_components();                    // 初始化服务器模块

   network_init();                                   // 初始化网络模块,根据配置,打开IP socket/unix socket/windows named pipe来进行监听。

   start_signal_handler();                        // 开始接收信号

   acl_init(...);                                      // 初始化ACL (Access Control List)

   servers_init(0);                                 // 服务器初始化


   init_status_vars();                             // 状态变量初始化

   create_shutdown_thread();                 // 创建关闭线程


   create_maintenance_thread();             // 创建维护线程


   sql_print_information(...);                   // 打印一些信息

   handle_connections_sockets(0);          // 主要的服务处理函数,循环等待并接受命令,进行查询,返回结果,也是我们要详细关注的函数

   wait for exit;                                    // 服务要退出

   cleanup;

   exit(0);

}




可以仔细的看看这个简写的main函数,逻辑很清楚,就算没有我的这些注释大部分人也能容易的理解整个系统的执行流程。其实完整的main函数有接近300行,但是中心思想已经被包含在这里简短的十几行代码中了。




通过看这些代码,读者会发现mysqld是通过多线程来处理任务的,这点和Apache服务器是不一样的。




等待命令

mysqld等待和处理命令主要在handle_connections_sockets(0);来完成,这里我们仔细看看这个函数调用发生了什么。该函数也在mysqld.cc中,也有大概300行,我们继续简写。

为了方便分析,这里我们假定配置服务器通过unix domain socket来监听接受命令,其他方式类同。

pthread_handler_t handle_connections_sockets(void *arg __attribute__((unused)))


{

  FD_ZERO(&clientFDs);

   FD_SET(unix_sock,&clientFDs);      // unix_socket在network_init中被打开


   socket_flags=fcntl(unix_sock, F_GETFL, 0);
   while (!abort_loop) {                   // abort_loop是全局变量,在某些情况下被置为1表示要退出。
     readFDs=clientFDs;                   // 需要监听的socket
     select((int) max_used_connection,&readFDs,0,0,0);     // select异步监听,当接收到时间以后返回。
     sock = unix_sock;
     flags= socket_flags;
     fcntl(sock, F_SETFL, flags | O_NONBLOCK);

     new_sock = accept(sock, my_reinterpret_cast(struct sockaddr *) (&cAddr),  &length);        // 接受请求
     getsockname(new_sock,&dummy, &dummyLen);
     thd= new THD;                     // 创建mysqld任务线程描述符,它封装了一个客户端连接请求的所有信息
     vio_tmp=vio_new(new_sock,  VIO_TYPE_SOCKET, VIO_LOCALHOST);     // 网络操作抽象层
     my_net_init(&thd->net,vio_tmp));       // 初始化任务线程描述符的网络操作

     create_new_thread(thd);                  // 创建任务线程

  }

}




看到这里,大家应该已经基本清楚mysqld如何启动并进入监听状态,真正的命令处理就是在create_new_thread里面,看名字也知道就是创建一个新线程来处理任务。




怎么样,是不是觉得mysql的代码很好懂呢?呵呵,更坚定了要继续读下去的信心。

一条语句的执行

下面具体看看服务器如何执行语句"insert"语句的。





上一节我们提到create_new_thread是所有处理的入口,这里我们仔细看看它到底干了什么。幸运的是,它也在mysqld.cc里面,我们不费吹灰之力就找他了它:

static void create_new_thread(THD *thd) {

  NET *net=&thd->net;

  if (connection_count >= max_connections + 1 || abort_loop) {         // 看看当前连接数是不是超过了系统配置允许的最大值,如果是就断开连接。

    close_connection(thd, ER_CON_COUNT_ERROR, 1);

    delete thd;

  }

  ++connection_count;


  thread_scheduler.add_connection(thd);       // 将新连接加入到thread_scheduler的连接队列中。


}





现在看来关键还是在thread_scheduler干了什么,现在打开sql/scheduler.cc文件:

void one_thread_per_connection_scheduler(scheduler_functions* func) {

  func->max_threads= max_connections;

  func->add_connection= create_thread_to_handle_connection;

  func->end_thread= one_thread_per_connection_end;

}





再看create_thread_to_handle_connection,它还是在mysqld.cc中,哈哈:

void create_thread_to_handle_connection(THD *thd) {

   if (cached_thread_count > wake_thread) {

       thread_cache.append(thd);

       pthread_cond_signal(&COND_thread_cache);

    } else {

     threads.append(thd);

     pthread_create(&thd->real_id,&connection_attrib,   handle_one_connection,  (void*) thd)));

   }

}





恩,看来先是看当前工作线程缓存(thread_cache)中有否空余的线程,有的话,让他们来处理,否则创建一个新的线程,该线程执行handle_one_connection函数




很好,继续往下看,到了sql/sql_connection.cc中。

pthread_handler_t handle_one_connection(void *arg) {

  thread_scheduler.init_new_connection_thread();

   setup_connection_thread_globals(thd);

   for (;;) {

     lex_start(thd);

     login_connection(thd);          // 进行连接身份验证

     prepare_new_connection_state(thd);

     do_command(thd);               // 处理命令
     end_connection(thd);
  }

}





do_command在sql/sql_parse.cc中。

bool do_command(THD *thd) {

  NET *net= &thd->net;

   packet_length= my_net_read(net);

   packet= (char*) net->read_pos;

   command= (enum enum_server_command) (uchar) packet[0];           // 解析客户端穿过来的命令类型

   dispatch_command(command, thd, packet+1, (uint) (packet_length-1));

}





再看dispatch_command:

bool dispatch_command(enum enum_server_command command, THD *thd, char* packet, uint packet_length) {

  NET *net= &thd->net;

  thd->command=command;

  switch (command) {

    case COM_INIT_DB: ...;

    case COM_TABLE_DUMP: ...;

    case COM_CHANGE_USER: ...;

    ...

    case COM_QUERY:

        alloc_query(thd, packet, packet_length);

        mysql_parse(thd, thd->query, thd->query_length, &end_of_stmt);

  }

}





进行sql语句解析

void mysql_parse(THD *thd, const char *inBuf, uint length, const char ** found_semicolon) {

   lex_start(thd);

    if (query_cache_send_result_to_client(thd, (char*) inBuf, length) <= 0) {         // 看query cache中有否命中,有就直接返回结果,否则进行查找

       Parser_state parser_state(thd, inBuf, length);                                          

       parse_sql(thd, & parser_state, NULL);                                                      // 解析sql语句

       mysql_execute_command(thd);                                                               // 执行

    }

}





总算开始执行了,mysql_execute_command函数超长,接近3k行:-(,我们还是按需分析吧。还是觉得这种代码不应该出现在这种高水平的开源软件里面,至少在linux kernel中很少看见这么长的函数,而在mysql里面确实是常常看到。

int mysql_execute_command(THD *thd) {

   LEX  *lex= thd->lex;            // 解析过后的sql语句的语法结构

   TABLE_LIST *all_tables = lex->query_tables;      // 该语句要访问的表的列表

   switch (lex->sql_command) {

       ...

       case SQLCOM_INSERT:

           insert_precheck(thd, all_tables);



           mysql_insert(thd, all_tables, lex->field_list, lex->many_values, lex->update_list, lex->value_list, lex->duplicates, lex->ignore);
           break;
       ...
       case SQLCOM_SELECT:

          check_table_access(thd, lex->exchange ? SELECT_ACL | FILE_ACL :  SELECT_ACL,  all_tables, UINT_MAX, FALSE);     // 检查用户对数据表的访问权限

          execute_sqlcom_select(thd, all_tables);                     // 执行select语句


          break;

   }

}

Mysql源代码分析系列(4): 主要调用流程(续)

在上一篇文章中我们讲到了的mysql_execute_command,这个函数根据解析出来的SQL命令分别调用不同的函数做进一步处理。我们这里先看"INSERT"命令的处理流程。其对应的处理函数是mysql_insert,在sql/sql_insert.cc中,还是很长,大概300多行。


bool mysql_insert(THD *thd,
                   TABLE_LIST *table_list,      // 该命令要用到的表
                   List<Item> &fields,             // 使用的域
                   List<List_item> &values_list,
                   List<Item> &update_fields,
                   List<Item> &update_values,
                   enum_duplicates duplic,
                   bool ignored) {
  open_and_lock_tables(thd, table_list);
  mysql_prepare_insert(...);

  foreach value in values_list {
    write_record(...);
  }
}
其实里面还有很多处理trigger,错误,view之类的,我们暂时都忽略。


// 写数据记录
int write_record(THD *thd, TABLE *table,COPY_INFO *info) {
  if (info->handle_duplicates == DUP_REPLACE || info->handle_duplicates == DUP_UPDATE) {
     table->file->ha_write_row(table->record[0]);
     table->file->ha_update_row(table->record[1], table->record[0]));

  } else {
     table->file->ha_write_row(table->record[0]);
  }
}



不用说,这里我们还是省略了好多东西,要注意的是这里调用的table->file->ha_write_row和table->file->ha_update_row。在sql/table.h可以看到table的定义,其中file被定义成handler *file; 那handler是什么?对了,这就是我们前面提到的数据存储抽象层,所有的存储引擎都必须事先这里定义的接口,才能被mysql使用。在这里使用的具体的接口操作是ha_write_row和ha_update_row。这两个函数可以在sql/handler.cc中看到。比如ha_write_row:
int handler::ha_write_row(uchar *buf) {
  write_row(buf);   // 调用具体的实现
  binlog_log_row(table, 0, buf, log_func));  // 写binlog
}



下面我们看看在myisam中是怎么实现的文件操作,代码在storage/myisam/ha_myisam.cc中。先看write_row:
723 int ha_myisam::write_row(uchar *buf)
724 {
725   ha_statistic_increment(&SSV::ha_write_count);
726
727   /* If we have a timestamp column, update it to the current time */
728   if (table->timestamp_field_type & TIMESTAMP_AUTO_SET_ON_INSERT)      // 如果有timestamp域,写入当前时间。
729     table->timestamp_field->set_time();
730
731   /*
732     If we have an auto_increment column and we are writing a changed row
733     or a new row, then update the auto_increment value in the record.
734   */
735   if (table->next_number_field && buf == table->record[0])                   // 更新auto_increment列
736   {
737     int error;
738     if ((error= update_auto_increment()))
739       return error;
740   }
741   return mi_write(file,buf);       // 真正写文件
742 }


再看mi_write函数,很好找,就在storage/myisam/mi_write.c,我们就不再分析下去了,具体实现和myisam使用的文件格式相关,有兴趣的可以参考myisam的相关文档。


结语

写到这里,我们对于mysql执行sql语句的主要调用流程就有了大概的了解,但是真正的难点往往在细节中,我也刻意逃避了这些细节,但是还是应该列一下相关的内容:
+ Sql语句的解析和相关数据结构
+ Sql语句的描述数据结构
+ 执行优化相关算法
+ 数据存储殷勤抽象层的定义和交互
+ 存储引擎的具体操作和文件格式



必须要提到的是,这些地方的代码都比较难懂,而且核心函数都很长,非常不便与理解,有需要的人可以选一些方面具体深入,但要面面俱到就需要很多时间了。
Mysql源代码分析(5): Plugin架构介绍
Mysql现在很多模块都是通过plugin的方式连接到Mysql核心中的,除了大家熟悉的存储引擎都是Plugin之外,Mysql还支持其他类型的plugin。本文将对相关内容做一些简单介绍。主要还是以架构性的介绍为主,具体细节会提到一点,但是肯定不会包括所有的细节。

主要数据结构和定义
大部分的数据接口,宏和常量都定义在include/mysql/plugin.h中,我们来慢慢看。

先看plugin的类型:


#define MYSQL_UDF_PLUGIN             0  /* User-defined function        */
#define MYSQL_STORAGE_ENGINE_PLUGIN  1  /* Storage Engine               */
#define MYSQL_FTPARSER_PLUGIN        2  /* Full-text parser plugin      */
#define MYSQL_DAEMON_PLUGIN          3  /* The daemon/raw plugin type */
#define MYSQL_INFORMATION_SCHEMA_PLUGIN  4  /* The I_S plugin type */
开发者开发的plugin必须指定上述类型之一。类型包括用户自定义函数,存储引擎,全文解析,原声plugin和information schema plugin。最常见的是前三个,daemon plugin一般用来在mysqld中启动一个线程,在某些时候干活儿。


一个plugin的描述数据接口是:
struct st_mysql_plugin
{
  int type;             /* the plugin type (a MYSQL_XXX_PLUGIN value)   */
  void *info;           /* pointer to type-specific plugin descriptor   */
  const char *name;     /* plugin name                                  */
  const char *author;   /* plugin author (for SHOW PLUGINS)             */
  const char *descr;    /* general descriptive text (for SHOW PLUGINS ) */
  int license;          /* the plugin license (PLUGIN_LICENSE_XXX)      */
  int (*init)(void *);  /* the function to invoke when plugin is loaded */
  int (*deinit)(void *);/* the function to invoke when plugin is unloaded */
  unsigned int version; /* plugin version (for SHOW PLUGINS)            */
  struct st_mysql_show_var *status_vars;
  struct st_mysql_sys_var **system_vars;
  void * __reserved1;   /* reserved for dependency checking             */
};
主要内容包括类型,名字,初始化/清理函数,状态变量和系统变量的定义等等。但是在使用的时候一般不是直接使用这个数据结构,而是使用大量的宏来辅助。


一个plugin的开始:
#define mysql_declare_plugin(NAME) \
__MYSQL_DECLARE_PLUGIN(NAME, \
                 builtin_ ## NAME ## _plugin_interface_version, \
                 builtin_ ## NAME ## _sizeof_struct_st_plugin, \
                 builtin_ ## NAME ## _plugin)
plugin定义结束:
#define mysql_declare_plugin_end ,{0,0,0,0,0,0,0,0,0,0,0,0}}


__MYSQL_DECLARE_PLUGIN根据plugin是动态链接plugin还是静态链接plugin有不同的定义:

#ifndef MYSQL_DYNAMIC_PLUGIN
#define __MYSQL_DECLARE_PLUGIN(NAME, VERSION, PSIZE, DECLS)                   \
int VERSION= MYSQL_PLUGIN_INTERFACE_VERSION;                                  \
int PSIZE= sizeof(struct st_mysql_plugin);                                    \
struct st_mysql_plugin DECLS[]= {
#else
#define __MYSQL_DECLARE_PLUGIN(NAME, VERSION, PSIZE, DECLS)                   \
int _mysql_plugin_interface_version_= MYSQL_PLUGIN_INTERFACE_VERSION;         \
int _mysql_sizeof_struct_st_plugin_= sizeof(struct st_mysql_plugin);          \
struct st_mysql_plugin _mysql_plugin_declarations_[]= {
#endif


特别要注意的是“#ifndef MYSQL_DYNAMIC_PLUGIN”,如果你要写的plugin是动态加载的话,需要在编译的时候定义这个宏。
总体而言,mysql_declare_plugin申明了一个struct st_mysql_plugin数组,开发者需要在该宏之后填写plugin自定义的st_mysql_plugin各个成员,并通过mysql_declare_plugin_end结束这个数组。


看个例子plugin/daemon_example/daemon_example.cc,这是个动态MYSQL_DAEMON_PLUGIN类型的plugin,注意到plugin/daemon_example/Makefile.am里面有-DMYSQL_DYNAMIC_PLUGIN。具体定义如下:
mysql_declare_plugin(daemon_example)
{
  MYSQL_DAEMON_PLUGIN,
  &daemon_example_plugin,
  "daemon_example",
  "Brian Aker",
  "Daemon example, creates a heartbeat beat file in mysql-heartbeat.log",
  PLUGIN_LICENSE_GPL,
  daemon_example_plugin_init, /* Plugin Init */                          // plugin初始化入口
  daemon_example_plugin_deinit, /* Plugin Deinit */                   // plugin清理函数
  0x0100 /* 1.0 */,
  NULL,                       /* status variables                */
  NULL,                       /* system variables                */
  NULL                        /* config options                  */
}
mysql_declare_plugin_end;


这个定义经过preprocess被展开后定义为:
int _mysql_plugin_interface_version_= MYSQL_PLUGIN_INTERFACE_VERSION;         \
int _mysql_sizeof_struct_st_plugin_= sizeof(struct st_mysql_plugin);          \
struct st_mysql_plugin _mysql_plugin_declarations_[]= {
  { MYSQL_DAEMON_PLUGIN,
  &daemon_example_plugin,
  "daemon_example",
  "Brian Aker",
  "Daemon example, creates a heartbeat beat file in mysql-heartbeat.log",
  PLUGIN_LICENSE_GPL,
  daemon_example_plugin_init, /* Plugin Init */                          // plugin初始化入口
  daemon_example_plugin_deinit, /* Plugin Deinit */                   // plugin清理函数
  0x0100 /* 1.0 */,
  NULL,                       /* status variables                */
  NULL,                       /* system variables                */
  NULL                        /* config options                  */
} , {0,0,0,0,0,0,0,0,0,0,0,0}};


静态链接plugin也类似,只不过plugin宏展开出来的变量都有自己的名字,对于myisam,生成了一个叫builtin_myisam_plugin的plugin数组。


plugin可以定义自己的变量,包括系统变量和状态变量。具体的例子可以看看storage/innobase/handler/ha_innodb.cc里面对于innodb插件的申明,结合plugin.h,还是比较容易看懂的。


在mysql的源代码里面grep一把mysql_declare_plugin,看看都有哪些plugin:
$grep "mysql_declare_plugin(" --include=*.cc -rni *

plugin/daemon_example/daemon_example.cc:187:mysql_declare_plugin(daemon_example)
sql/ha_partition.cc:6269:mysql_declare_plugin(partition)
sql/log.cc:5528:mysql_declare_plugin(binlog)
sql/ha_ndbcluster.cc:10533:mysql_declare_plugin(ndbcluster)
storage/csv/ha_tina.cc:1603:mysql_declare_plugin(csv)
storage/example/ha_example.cc:893:mysql_declare_plugin(example)
storage/myisam/ha_myisam.cc:2057:mysql_declare_plugin(myisam)
storage/heap/ha_heap.cc:746:mysql_declare_plugin(heap)
storage/innobase/handler/ha_innodb.cc:8231:mysql_declare_plugin(innobase)
storage/myisammrg/ha_myisammrg.cc:1186:mysql_declare_plugin(myisammrg)
storage/blackhole/ha_blackhole.cc:356:mysql_declare_plugin(blackhole)
storage/federated/ha_federated.cc:3368:mysql_declare_plugin(federated)
storage/archive/ha_archive.cc:1627:mysql_declare_plugin(archive)
呵呵,连binlog都是plugin哦,不过还是storage plugin占大多数。


Plugin初始化



在见面的介绍main函数的文章中我也提到了其中有个函数plugin_init()是初始化的一部分,这个东东就是所有静态链接初始化plugin的初始化入口。该函数定义在"sql/sql_plugin.cc"中。

int plugin_init(int *argc, char **argv, int flags) {

  // 初始化内存分配pool。

  init_alloc_root(&plugin_mem_root, 4096, 4096);

  init_alloc_root(&tmp_root, 4096, 4096);

  // hash结构初始化

  ...

  // 初始化运行时plugin数组,plugin_dl_array用来保存动态加载plugin,plugin_array保存静态链接plugin。而且最多各自能有16个plugin。

  my_init_dynamic_array(&plugin_dl_array, sizeof(struct st_plugin_dl *),16,16);

  my_init_dynamic_array(&plugin_array, sizeof(struct st_plugin_int *),16,16);
  // 初始化静态链接plugin
  for (builtins= mysqld_builtins; *builtins; builtins++) {
    // 每一个plugin还可以有多个子plugin,参见见面的plugin申明。
    for (plugin= *builtins; plugin->info; plugin++) {
      register_builtin(plugin, &tmp, &plugin_ptr); // 将plugin放到plugin_array和plugin_hash中。
      // 这个时候只初始化csv或者myisam plugin。
      plugin_initialize(plugin_ptr);    // 初始化plugin,调用plugin的初始化函数,将plugin的状态变量加入到状态变量列表中,将系统变量的plugin成员指向当前的活动plugin。
    }
  }
  // 根据用户选项初始化动态加载plugin
  if (!(flags & PLUGIN_INIT_SKIP_DYNAMIC_LOADING))
  {
    if (opt_plugin_load)
      plugin_load_list(&tmp_root, argc, argv, opt_plugin_load);        // 根据配置加载制定的plugin。包括找到dll,加载,寻找符号并设置plugin结构。
    if (!(flags & PLUGIN_INIT_SKIP_PLUGIN_TABLE))
      plugin_load(&tmp_root, argc, argv);          // 加载系统plugin table中的plugin。
  }
  // 初始化剩下的plugin。
  for (i= 0; i < plugin_array.elements; i++) {
    plugin_ptr= *dynamic_element(&plugin_array, i, struct st_plugin_int **);
    if (plugin_ptr->state == PLUGIN_IS_UNINITIALIZED)
    {
      if (plugin_initialize(plugin_ptr))
      {
        plugin_ptr->state= PLUGIN_IS_DYING;
        *(reap++)= plugin_ptr;
      }
    }
  }
  ...
}





这个函数执行结束以后,在plugin_array,plugin_dl_array,plugin_hash中保存了当前加载了的所有的plugin。到此plugin初始化结束。




在plugin_initialize函数里面,调用了每个plugin自己的init函数(参见前面的内容)。特别要提到的是对于各种不同类型的plugin,初始化函数的参数也不一样,这是通过一个全局的plugin_type_initialize间接层来实现的。这个数组对于每种类型的plugin定义了一个函数,比如对于storage plugin对应的是ha_initialize_handlerton,对于information scheme对应的是initialize_schema_table,然后在这些函数中再调用plugin的初始化函数。暂时对于其他类型的plugin没有定义这个中间层初始化函数,所以就直接调用了plugin的初始化函数。

Mysql源代码分析(6): Plugin架构介绍-续
上篇文章我们分析了Mysql的Plugin接口以及plugin的初始化过程,这里我们继续看plugin怎么被使用的。基本还是通过例子看问题,主要分析myisam如何通过plugin接口被调用的。





myisam是mysql最早的和默认的storage engine,前面我们也看到在plugin初始化的时候是优先初始化myisam,然后才初始化其他的存储引擎。这里我们假定用户要对一个myisam的表做操作,具体看看其中涉及的调用过程。

myisam的初始化

myisam plugin的定义可以在storage/myisam/ha_isam.cc中找到:


mysql_declare_plugin(myisam)


{


  MYSQL_STORAGE_ENGINE_PLUGIN,


  &myisam_storage_engine,


  "MyISAM",


  "MySQL AB",


  "Default engine as of MySQL 3.23 with great performance",


  PLUGIN_LICENSE_GPL,


  myisam_init, /* Plugin Init */


  NULL, /* Plugin Deinit */


  0x0100, /* 1.0 */


  NULL,                       /* status variables                */


  NULL,                       /* system variables                */


  NULL                        /* config options                  */


}


mysql_declare_plugin_end;





初始化函数是myisam_init。在前面文章中提到,storage engine类型的plugin均是通过ha_initialize_handlerton初始化。myisam_init的输入参数是void *p,实际上是handlerton*。handlerton在mysql中封装了访问一个存储引擎需要的接口,每个存储引擎在全局空间有一个handlerton对象,保存在对应的内存中plugin结构的data域中。该结构具体定义可以在sql/handler.h中找到。myisam_init做的事情很简单,设置handlerton中的各个域,其中最重要的域是create,被指向了一个函数myisam_create_handler,这个函数用来创建handler,用来对于数据库文件进行操作。

打开一个表
数据库表是数据库中所有操作的基础,我们看看打开一个表需要做些什么。当一个select命令进来的时候,sql_parse.cc中的execute_sqlcom_select被执行,并被传入parse出来的所有该命令要用的到表。它会调用open_and_lock_tables来打开指定的表,然后调用open_and_lock_tables_derived,再调用open_tables,再调用open_table(sql_base.cc)。一大堆调用之后真正开始干实事儿的是open_unireg_entry,名字很奇怪,但是确实就是它开始打开表了,我们仔细将仔细看这个函数,以及它调用的函数。这个函数很长,其实大部分都是在做错误处理,最重要的就以下几行:

static int open_unireg_entry(THD *thd, TABLE *entry, TABLE_LIST *table_list, const char *alias, char *cache_key, uint cache_key_length, MEM_ROOT *mem_root, uint flags) {

   ...

   share= get_table_share_with_create(thd, table_list, cache_key, cache_key_length, OPEN_VIEW |table_list->i_s_requested_object, &error);

   open_table_from_share(thd, share, alias, ...);


   ...

}


get_table_share_with_create是创建一个table_share结构,包括了同一个类型的表公用的数据结构,open_table_from_share则通过这个公用结构打开对应的要操作的表。





TABLE_SHARE *get_table_share(THD *thd, TABLE_LIST *table_list, ...) {

  share= alloc_table_share(table_list, key, key_length)); //分配内存

  my_hash_insert(&table_def_cache, (uchar*) share);  // 加入cache,以后可以直接用

  open_table_def(thd, share, db_flags); // 代开表的定义,需要读frm文件

}





open_table_def是用来打开存储表定义的文件。mysql中,每个表都有一个.frm文件,存储了表的定义,这个函数就是要打开表对应的frm文件,读入定义信息,填入TABLE_SHARE结构。

int open_table_def(THD *thd, TABLE_SHARE *share, uint db_flags) {

  file= my_open(path, O_RDONLY | O_SHARE, MYF(0))


  open_binary_frm(thd, share, head, file);

}





open_binary_frm读入二进制的frm文件信息。这个函数超长,但是我们暂时只是对与plugin相关的部分感兴趣。因为每个表的storage engine信息就是从frm文件中读出来的,我们看相关的代码片段:

open_binary_frm(...) {

  ...

  plugin_ref tmp_plugin= ha_resolve_by_name(thd, &name);   // name就是storage engine的名字,比如"myisam"。这里根据名字找到对应的plugin。

  share->db_plugin= my_plugin_lock(NULL, &tmp_plugin);  // 保存plugin的引用,供以后使用。plugin中的"data"域就是handlerton*,这将是主要的使用plugin的入口。

  ...

}





好了,TABLE_SHARE设置好了,我们回到open_unireg_entry中,继续看open_table_from_share。这才是真正打开表的地方。这个函数还是在sql/table.cc中。这个函数还是超长...,万幸的是我们还是只想关注plugin相关的内容。TABLE中有一个file结构,类型是handler*,我们以前提到过,handler就是一个打开的表的引用,显然open_table_from_share的责任之一就是要设置这个域。

int open_table_from_share(THD *thd, TABLE_SHARE *share, ... TABLE *outparam, ...) {  // outparam是打开后的表信息。

   ...

   outparam->file= get_new_handler(share, &outparam->mem_root, share->db_type()));  // 直奔主题,获取一个handler。 share->db_type()返回plugin对应的handlerton,其实就是将plugin->data强制转换成handlerton.

   ...

   outparam->file->ha_open(outparam, ...); // 调用plugin的handler定义的open函数,做自定义的open操作。

   ...

}





get_new_handler负责根据TABLE_SHARE的内容构造一个handler对象。这个函数在sql/handler.cc中。


handler *get_new_handler(TABLE_SHARE *share, MEM_ROOT *alloc, handlerton *db_type) {

  file= db_type->create(db_type, share, alloc);  // 调用plugin的create函数,来创建handler成员。

  file->init();

}


前面我们提到过对于myisam对应的create函数是myisam_create_handler,这个函数就是new了一个ha_myisam对象,而ha_myisam又是从handler继承下来的,重载了handler对应的函数。

这样一个对于应数据库表文件的handler就可以使用啦,它的第一个使用就是在open_table_from_share中被调用ha_open。ha_open在handler.cc中定义,其实就是调用了重载后了open函数。在ha_myisam中,我们可以看到open函数的定义,这里我们就不仔细看了,实现细节和myisam的文件结构相关。




看到这里一个"SELECT * from test"语句如何打开表的部分就基本清楚了,主要过程包括:


从frm文件寻找storage engine的名字,并获取对应的storage engine plugin的handlerton

调用handlerton的create函数生成handler

通过handler重载的open函数打开表文件


挺清楚的。




到了这里,我们就有了表的handler,以后凡是涉及到存储引擎的操作,都通过这个接口调用来做,这样,storage engine plugin就和mysql核心紧密结合到了一起,各司其事,共同完成复杂的sql操作。

Mysql源代码分析(7): MYISAM的数据文件处理
好久没写分析文章了,一个是比较忙,另一个是因为余下的内容都是硬骨头,需要花时间慢慢理解。剩下的比较有意思的内容有:

select语句的执行和优化过程。大家关心数据库的查询性能,主要是对着部分比较感兴趣,特别是其中的查询优化部分。
Mysql的replication。Mysql的master/slave架构是大部分使用mysql的高性能网站架构的不二选择,replication则是这个架构的基础。
具体数据库引擎的实现。这部分也是很多关心mysql性能的人会比较感兴趣的部分,不过这个工作比较复杂,特别是流行的innodb,这个工作量尤其浩大,而且难度颇高。其中涉及到transaction的部分,也是特别复杂。



另外,我发现我写的文章被一些地方转摘了,感谢大家的阅读,但是我也希望转摘要注明出处,至少给个原文链接吧,也不枉我幸苦一场。

今天主要写写Myisam的数据文件的处理。

Myisam是最早实现的Mysql数据库引擎,也是人们心中的性能最好的引擎(虽然不是功能最强的,没办法,现实往往要求性能和功能做权衡)。这里选择分析它,主要原因是其实现还算比较简单明了,而且最近我对数据文件的格式比较感兴趣,特别是变长数据的处理。要注意的是本文不会介绍myisam的索引文件格式。

基本知识
对于每一个以Myisam做数据引擎的表,在<%data_dir%>/<database>目录下会有如下几个文件来保存其相关信息:

.frm文件。 这个文件是跨引擎的,描述了该表的元信息,其中最重要的是表定义和表的数据库引擎。
.MYD文件。这是我们要看的重点文件,包含了数据库record信息,就是数据库中的每个行。
.MYI文件。索引文件,用来加速查找。

而对于MYD中的每个record,可以是fixed,dynamic以及packed三种类型之一。fixed表示record的大小是固定的,没有VARCHAR, blob之类的东东。dynamic则刚好相反,有变长数据类型。packed类型是通过myisampack处理过的record。参见:http://dev.mysql.com/doc/refman/5.1/en/myisam-table-formats.html。


需要注意的是record类型是针对表的设置,而不是对每个column的设置。

record处理接口
record的类型是表级别的设置,所以在一个表被打开的时候,myisam会检查元数据的选项,看该表的record是什么类型,然后设置对应的处理函数,具体处理在storage/myisam/mi_open.c的mi_setup_functions中,我们看其中的一个片段:
746 void mi_setup_functions(register MYISAM_SHARE *share)
747 {
         ....
759   else if (share->options & HA_OPTION_PACK_RECORD)
760   {
761     share->read_record=_mi_read_dynamic_record;
762     share->read_rnd=_mi_read_rnd_dynamic_record;
763     share->delete_record=_mi_delete_dynamic_record;
764     share->compare_record=_mi_cmp_dynamic_record;
765     share->compare_unique=_mi_cmp_dynamic_unique;
766     share->calc_checksum= mi_checksum;
767
768     /* add bits used to pack data to pack_reclength for faster allocation */
769     share->base.pack_reclength+= share->base.pack_bits;
770     if (share->base.blobs)
771     {
772       share->update_record=_mi_update_blob_record;
773       share->write_record=_mi_write_blob_record;
774     }
775     else
776     {
777       share->write_record=_mi_write_dynamic_record;
778       share->update_record=_mi_update_dynamic_record;
779     }
780   }
         ...


这是针对pack类型的处理函数设置。设置了share结构中的一堆函数接口。顺便说一句,这种方式是C语言编程中常用的实现”多态“的办法:申明函数接口,动态设置接口实现,思想上和C++的动态绑定是一致的。这段代码对于dynamic类型的表的record处理函数做了设置。比较有趣的是HA_OPTION_PACK_RECORD用来指定dynamic类型。


看到这些函数名大家可以猜想出他们都是干嘛的,下面主要看看fixed类型和dynamic类型的具体处理。
Fixed类型
顾名思义,fixed类型的表中的所有字段都是定长的,不能出现TEXT, VARCHAR之类的东东。这种严格限制带来的好处就是更快更直接的数据record操作,想想也知道,每个数据都是定长的,在文件操作的时候多方便啊。
看看一个数据的函数_mi_write_static_record,它在mi_statrec.c中,所有对于fixed record的操作的实现都定义在这个文件中。

21 int _mi_write_static_record(MI_INFO *info, const uchar *record)
22 {
       ...
24   if (info->s->state.dellink != HA_OFFSET_ERROR &&
25       !info->append_insert_at_end)
26   {
         检查dellink中是否有record。dellink是所有被删除的数据构成的链表。当一个record被删除的时候,它
所占的文件大小不是被马上释放,而是被放入dellink中,等候下次使用。
27     my_off_t filepos=info->s->state.dellink;
         读入dellink所指向的数据空间的信息。
33     更新dellink,将使用了的数据空间移除。

         将record写入找到的已删除的数据的空间中。

40   }

41   else
42   {
43     检查数据文件是否过大。
49     如果使用的写缓冲,则写入写缓冲。
         将新数据写入文件最后。
         更新元数据。
         ...
86 }



因为所有的数据都是一样大小,处理起来很简单。特别是当一个数据被删除的时候,它所占的空间被放入一个回收链表中,下次要写入新数据的时候,如果回收链表不为空,直接从其中找一个写入新数据即可,不用分配新的存储空间。


Fixed类型的其他处理也都很简单,这里不再多说了。需要提出的是,不管用的什么类型的数据,当数据被删除的时候,其所占的空间并不是马上被释放的,那样操作代价太大,要把该数据后面的所有数据向前移位,肯定无法忍受。一般的做法都是将这些空间用链表穿起来,供以后使用,所以数据文件一般是不会主动缩小的.....即使是innodb也是这样。


Dynamic类型

Dynamic类型是相对于fixed的类型而言,这种类型可以容忍变长数据类型的存在。随之而来的是更复杂的数据文件的操作。Dynamic类型中被删除的数据块也不是马上被释放,也被链表连起来。下次要写入新数据的时候,还是优先从这个链表中找。不同于fixed类型的处理在于新来的数据和链表中的空间的大小可能不一样。如果新数据大了,就会找好几个空余空间,将数据分散于多个数据块中,如果新数据小了,则会将空余数据块分成两个,一个写入新数据,一个还是放在空余链表中供后来者使用。


看一下mi_dynrec.c中的write_dynamic_record函数。
320 static int write_dynamic_record(MI_INFO *info, const uchar *record,
321         ulong reclength)
322 {
         检查是否有足够的空间来存放新数据,空间满了返回错误。
351
352   do
353   {
          // 找一个可以写入数据的地方。注意这里是在一个循环里面,也就是说每次找到的
          // 空间不一定能够写入整个数据,只能写入部分的话,剩下的还要继续找地方写。
354     if (_mi_find_writepos(info,reclength,&filepos,&length))
355       goto err;
          // 写入能够放入找到的空间的数据。
356     if (_mi_write_part_record(info,filepos,length,
357                               (info->append_insert_at_end ?
358                                HA_OFFSET_ERROR : info->s->state.dellink),
359             (uchar**) &record,&reclength,&flag))
360       goto err;
361   } while (reclength);
         ...
      }
其中的循环说明了一切,很有可能一个数据会被分成几块儿,写到不同的地方,但是他们合起来才构成了整个数据。


再看_mi_find_writepos。
371 static int _mi_find_writepos(MI_INFO *info,
372            ulong reclength, /* record length */
373            my_off_t *filepos, /* Return file pos */
374            ulong *length)   /* length of block at filepos */
375 {
376   MI_BLOCK_INFO block_info;
         ...
         // 先检查dellink中是否有空余的空间。
380   if (info->s->state.dellink != HA_OFFSET_ERROR &&
381       !info->append_insert_at_end)
382   {
383     /* Deleted blocks exists;  Get last used block */
           存在空余空间,那就把链表中的头找出来,把其中的空间用来写入新数据。
           将这块空间的描述返回给调用者。
           ....
398   }
399   else
400   {
401     /* No deleted blocks;  Allocate a new block */
           没有已删除的空间,那就在数据文件的最后分配空间,并返回给调用者。
421   }
         ...
      }
如果有已删除的空间的话,那就直接把链表头描述的空间返回。这个算法很简单,但是我觉得这样简单的算法可能会赵成一些问题,比如存储的碎片化,一块儿大空间被切的越来越小,到后来写入一个数据要使用好几个空间。这些问题在操作系统的内存管理中也同样存在,所以产生了大量的内存管理算法,这里也应该可以借用吧。


具体的写入是在_mi_write_part_record中完成的。这个函数比较长,我就直接简写如下了。
int _mi_write_part_record(MI_INFO *info,                                 
         my_off_t filepos, /* points at empty block */
         ulong length,   /* length of block */
         my_off_t next_filepos,/* Next empty block */
         uchar **record, /* pointer to record ptr */
         ulong *reclength, /* length of *record */
         int *flag)    /* *flag == 0 if header */
{
   如果给出的空间空间大于数据长度的话,计算填完数据后剩余的空间。
   如果空间刚好,准备一些元数据。
   如果空间太小,则找到下一个写入空间的位置(要么是下一个dellink,要么是文件末尾),并准备这些元数据。如果是第一部分的数据的话,要写入更多的信息。
   如果空间太大,有剩余空间的话,先看这个空间能否与和下一个空闲空间连接起来形成一个大空间,如果能的话就合并。将其相关的元数据,比如空间的位置,大小之类的,准备好。
   开始写数据罗,如果启用了写缓冲,则写入缓冲,否则写入找出来的空间。
   更新dellink的相关信息。
}


逻辑很清楚,主要是要处理空间过大或者过小带来的复杂性。


好了,到了这里大部分的处理都很清楚了,还是很直接的。剩下的就是在删除一个数据的时候,将其所占的空间放到dellink中,要注意的是,如果其数据块可以和dellink中的其他数据块合并,合并操作也是在删除数据的操作中调用的,而且合并出来的数据块还可能和其他数据块继续合并
分享到:
评论

相关推荐

    mysql5.7源代码

    通过分析和研究MySQL的源代码,我们可以深入了解数据库系统的内部机制,这对于数据库开发人员、系统管理员以及性能优化专家来说都是宝贵的资源。 1. **SQL解析与查询优化** MySQL的SQL解析器负责将用户输入的SQL...

    mysql源码(mysql-8.1.0.tar.gz)

    MySQL是世界上最受欢迎的开源关系型数据库管理系统之一,其源代码的分析和理解对于开发者和数据库管理员来说至关重要。这里我们讨论的是MySQL的8.1.0版本的源码,该版本通常包含了新的特性和性能优化。 MySQL 8.x...

    mysql-5.1.63源代码

    2. **SQL解析与执行**:MySQL的源代码中,SQL语句的解析和执行流程是核心部分。`sql`目录下的文件包含了许多处理SQL语句的组件,如`sql-parser.y`是SQL解析器的定义,`sql-execution.cc`涉及执行计划的生成和执行。 ...

    Mysql5.6.10源代码

    MySQL 5.6.10 是 MySQL 数据库管理系统的一个重要版本,它在 5.6 系列中引入了许多性能优化和新特性。这个源代码的发布为...总之,MySQL 5.6.10 源代码的分析和学习是一个深入了解数据库系统设计与实现的宝贵机会。

    mysql-5.1.58.zip_control_mysql 5.1.58_mysql 源代码_mysql5.1.58

    MySQL 5.1系列尤其注重在大型企业和互联网应用中的表现,它的源代码是开发者深入理解数据库工作原理和进行定制化开发的宝贵资源。 MySQL 5.1.58的源代码包含了多个组件和模块,如SQL解析器、存储引擎、查询优化器、...

    mysql源码(mysql-8.2.0.zip)

    MySQL的源码分析可以帮助我们深入了解其内部工作机制,包括查询优化、事务处理、存储引擎等核心功能。`mysql-8.2.0.zip`是MySQL的一个特定版本,以下是关于这个版本的一些关键知识点: 1. **版本更新**:MySQL ...

    MySQL的ODBC接口程序源代码.zip_MYSQL_mySQL ODBC_odbc mysql_odbc mysql_sq

    MySQL的ODBC接口程序源代码提供了连接MySQL数据库的开放式数据库连接(ODBC)方式,这是一种在不同操作系统和编程环境中访问数据库的标准API。ODBC允许应用程序通过一个统一的接口与多种数据库系统交互,包括MySQL。本...

    mysql源码

    MySQL是一种广泛使用的开源关系型数据库管理系统(RDBMS),其源码分析可以帮助我们深入理解数据库的工作原理、查询优化以及并发控制等核心概念。MySQL 5.5.58是该系统的一个稳定版本,包含了丰富的功能和性能改进。...

    mysql必知必会-表结构代码-源代码-分章节知识点总结.zip

    在这个压缩包中,你将找到与该书相关的三个关键文件,分别对应于创建表结构、填充数据以及书中源代码的整理。 `create.sql`文件是关于创建数据库表结构的SQL脚本。在MySQL中,表是存储数据的基本单元,表结构定义了...

    mysql源码包

    同时,`mysql5.6.25源码包.tar.gz`则包含所有源代码,是深入学习MySQL技术的宝贵资料。总的来说,MySQL源码分析对于数据库开发者、运维人员或希望深入学习数据库原理的人来说,都是一个极其有价值的实践项目。

    MySQL的ODBC接口程序源代码

    对于学习和研究,你可以通过阅读源代码了解MySQL与ODBC接口的交互细节,以及如何实现数据库连接、查询解析、结果集处理等核心功能。这对于数据库应用开发、数据库驱动程序开发,甚至是数据库性能优化等领域都有着...

    PHP+MySQL经典案例剖析_源代码

    《PHP+MySQL经典案例剖析》是一...通过分析和实践这些源代码,你可以深化对PHP和MySQL的了解,提升Web开发技能,同时也能学习到实际项目中的一些最佳实践。无论是初学者还是经验丰富的开发者,这些案例都极具参考价值。

    VC++ 6.0连接MySQL源代码案例,值得学习

    这个"VC++ 6.0连接MySQL源代码案例"为初学者提供了一个很好的学习平台,让我们深入探讨其中涉及的知识点。 首先,VC++ 6.0是微软公司推出的一款集成开发环境,主要用于编写基于C++语言的应用程序。尽管它已经不再...

    java mysql驱动源码

    总的来说,Java连接MySQL的实现涉及到网络通信、数据库协议、SQL解析等多个领域,源码分析可以帮助开发者提升对数据库操作的深入理解和优化技巧,进一步提高应用的性能和稳定性。通过研究`src`目录下的源码,可以...

    Mysql 5.5.62数据库源码

    MySQL 5.5.62 是一个开源的关系型数据库管理系统,由C++语言编写而成,其源码提供了深入了解数据库系统内部工作原理的机会。这个版本在MySQL的生命周期中扮演了重要角色,因为它包含了多个重要的功能改进和性能优化...

    PHP6与MySQL5基础教程第三版源代码(09.11.25更新)

    在本书的源代码包phpmyql3_scripts中,我们可以期待找到一系列的PHP脚本和MySQL查询,涵盖了从连接数据库、执行SQL语句、处理查询结果到创建动态网页的各种场景。通过分析和运行这些代码,读者可以学习如何使用PHP与...

    MySQL源码结构解析

    ### MySQL源码结构解析 #### MySQL基本架构 MySQL作为一个关系型数据库管理系统,其核心功能包括数据存储、查询处理以及事务管理等。为了更好地理解MySQL的工作原理,深入剖析其内部架构至关重要。 - **服务器层*...

    MySQL源代码

    MySQL源代码是一个深入理解数据库系统工作原理的重要资源,特别是对于那些想要自己开发小型关系数据库或者进行MySQL性能优化的研究者来说。MySQL是一个广泛使用的开源关系型数据库管理系统,其源代码提供了宝贵的...

    MySQL 源代码(2之2:Linux 版)

    在Linux环境下编译和使用MySQL源代码,能更好地理解和适应其与操作系统交互的方式,这对于系统级开发人员和数据库管理员来说至关重要。 MySQL 5.5.8是该系列的一个特定版本,它包含了从早期版本到5.5系列的一系列...

    ExcelVBA操作MySQL源代码

    标题中的“ExcelVBA操作MySQL源代码”是一个关于如何使用Excel的VBA(Visual Basic for Applications)编程语言来与MySQL数据库进行交互的教程或代码集合。这个主题涉及到的知识点包括: 1. **VBA基础**:VBA是...

Global site tag (gtag.js) - Google Analytics