数据库内核月报 - 2015 / 03

MySQL · 答疑释惑· lower_case_table_names 使用问题

背景

在MySQL中,表是和操作系统中的文件对应的,而文件名在有的操作系统下是区分大小写的(比如linux),有的是不区分大小写(比如Windows),表名与文件名的大小写对应关系,MySQL 是通过lower_case_table_names 这个变量来控制的。

这个变量的有效取值是0,1,2,按照官方文档 的解释:

0表示,表在文件系统存储的时候,对应的文件名是按建表时指定的大小写存的,MySQL 内部对表名的比较也是区分大小写的; 1表示,表在文件系统存储的时候,对应的文件名都小写的,MySQL 内部对表名的比较是转成小写的,即不区分大小写; 2表示,表在文件系统存储的时候,对应的文件名是按建表时指定的大小写存的,但是 MySQL 内部对表名的比较是转成小写的,即不区分大小写。

0适用于区分大小写的系统,1都适用,2适用于不区分大小写的系统。

如果在开始使用MySQL选定了一个合适的值后,就不要改变,不然的话在之后使用中就会出现问题。

问题描述

这里给出一个在使用过程中改变 lower_case_table_names 导致 drop database 失败的案例。因为lower_case_table_names是个只读变量,只能在启动时指定参数设置值,或者 gdb 挂上去直接改内存。

首先在启动 mysqld 的时候,指定 lower_case_table_names = 0,我们执行这样的语句:

create database db1;
use db1;
create table t1(a int) engine = InnoDB; 
create table t2(a int) engine = MyISAM; 
create table T3(a int) engine = InnoDB; 
create table T4(a int) engine = MyISAM;

查看对应数据库目录下的表文件:

$ls db1
db.opt  t1.frm  t1.ibd  t2.frm  t2.MYD  t2.MYI  T3.frm  T3.ibd  T4.frm  T4.MYD  T4.MYI

然后重启mysqld,指定 lower_case_table_names =1,执行删除db1

mysql> drop database db1;
ERROR 1010 (HY000): Error dropping database (can't rmdir './db1', errno: 39)

可以看到删库语句执行失败,我们再看下数据库目录下的表文件

$ls a
T3.frm  T4.frm  T4.MYD  T4.MYI

可以看到,大写的 T3 和 T4 表没有被删掉,为什么呢?

问题分析

mysqld 在执行 drop database 操作的时候,是调用 mysql_rm_db 这个函数,在删除时先把db下的所有表都删掉,然后再把db删掉。为了找出对应db下的所有表,mysqld 是通过遍历数据库目录下的文件来做的,具体是用 find_db_tables_and_rm_known_files 这个函数,遍历数据库目录下的所有文件,然后构造出要 drop 的table列表,然而在构造删除列表过程中,会有这样一个判断:

if (lower_case_table_names)
table_list->table_name_length= my_casedn_str(files_charset_info,
table_list->table_name);

意思就是如果lower_case_table_names非0的话,就把 table_name 转成小写的,T3 和 T4 就被转成 t3 和 t4,这样生成的 table_list 中的对应的表是 t1,t2,t3,t4。之后拿着这样的 table_list 通过 mysql_rm_table_no_locks 一个个删表,这样就只把t1,t2 给删了,t3和t4不存在,并且删表时的逻辑是带有 if exists 的,所以也不会报错。

在list表都删除完后,调用rm_dir_w_symlink来删除db目录,此时db1目录下还有 T3 T4 对应的文件,这个函数会调用系统的 rmdir 函数,而当目录非空的时候,rmdir是执行失败的。

所以我们看到最终的错误提示 Error dropping database (can't rmdir './db1', errno: 39)

建议

上面的问题是改变 lower_case_table_names 导致 drop database 失败,其实还有许多其它的因为lower_case_table_names值改变导致的问题,比如主备库本来这个值本来是一致的,如果只改主库的值的话,就会导致备库复制中断,报找不到表的问题,或者本来是不区分大小写的,应用里的写的SQL语句有大写表名,也有小写表名,之后改成区分大小写,就会导致应用出错。

所以建议是:

  • 不要轻易的改变lower_case_table_names的值,如果真要改的话,要先检查下已有的表是否有大小写的问题,保证目前的表名和要改的模式是一致的,比如从区分大小写改为不区分大小写,那就不应该有大写表存在,如果有的话,要先把大写表rename成小写的,如果本来有共存同名的大写表和小写表,就要想办法去掉一个。
  • 应用不要依赖于 mysql 的表名转换机制,应用里的sql语句应该和表名一致,在不区分大小写的时候,应用里对同一个表的使用,不要既有大写表名,也有小写表名。