2009年12月31日星期四

MySQL字符集III

今天继续研究字符集.

上次似乎的出了结论,但是后来测试又发现一些怪现象,所以今天继续测试.就是关于客户端字符集的,因为每一个客户端都有自己的字符集,当其与设置的character_set_client以及character_set_results不匹配的时候,其中的转换关系是否如我所预期的呢?

 

我使用我机器上的windows命令行,因为我确定其客户端使用的字符集是gbk

Create database tim character set utf8;

Use tim;

Create table test_gbk(t varchar(100)) character set gbk;

Create table test_utf8(t varchar(100)) character set utf8;

insert test_gbk values('阳光'),('太阳'),('济南'),('光明');

insert test_utf8 values('阳光'),('太阳'),('济南'),('光明');

select * from test_gbk;

select * from test_utf8;

都能正常显示中文

Select length(t) from test_gbk; 4

Select length(t) from test_utf8; 6

Select char_length(t) from test_gbk; 2

Select length(t) from test_utf8; 2

也是正常的存储了对应的gbk编码以及utf8编码

Set character_set_connection=’ascii’;

奇怪的事情出现了

Select * from test_gbk; select * from test_utf8;

都能够正常显示中文,并没有如我预期的一样从gbk,utf8转换到ascii出现数据丢失.

但是insert into test_gbk values(‘中国’);  insert into test_utf8 values(‘中国’);

Select * from test_gbk; select * from test_utf8;

我们刚插入的一行显示的是问号?

也就是说在insert的时候,是有一个character_set_clientcharacter_set_connect然后再到数据库字符集的转换过程的,但是在select的时候,这个转化过程是没有的.

 

然后我们来看一下character_set_clientcharacter_set_results起的作用

在一个utf8的客户端,set names gbk;

INSERT INTO test_gbk VALUES ('南方');

INSERT INTO test_utf8 VALUES ('南方');

然后在设置正常的命令行查看,得到的结果均是���

LENGTH函数查看,前者得到的结果是6,后者得到结果是9.

我们分析一下过程,因为是utf8的客户端,所以实际得到的均是utf8编码的南方

然而因为我们set names gbk, 也就是说character_set_clients, character_results, character_connection均是gbk,clientconnection不需要转换,存储到数据库的时候,当存储到test_gbk的时候,不需要转换,所以存储的是6个字节的utf8编码南方”;

当存储到test_utf8的时候,执行从gbkutf8的转换,所以引擎讲utf8编码的南方视为gbk编码的3个汉字, 也就是���”,然后再以utf8编码来编码这3个汉字,所以就变成了9个字节.

 

我前面谈到过phpmyadmin的字符集,也曾经在phpmyadminset names,结果不起作用.

经研究,发现phpmyadmin在连接mysql数据库的时候,character set connection是可以设置的,就是通过选择MySQL connection collation来设置的,character_set_client, character_set_results目前尚不清楚是怎样决定的,其客户端字符集则是跟登陆时候选择的语言所对应的php文件编码有关系.

我使用的phpmyadmin3.2.4, phpmyadminlibrary目录下有一个select_lang.lib.php的文件,其中定义了所有我们在登陆时候可以选择的语言列表:

return array(

        'af-utf-8'    => array('af|afrikaans', 'afrikaans-utf-8', 'af', ''),

        'ar-utf-8'    => array('ar|arabic', 'arabic-utf-8', 'ar', 'العربية'),

        'az-utf-8'    => array('az|azerbaijani', 'azerbaijani-utf-8', 'az', 'Azərbaycanca'),

        'bn-utf-8'    => array('bn|bangla', 'bangla-utf-8', 'bn', ''),

……

其中每一条都有类似于”afrikaans-utf-8”,”arabic-utf-8”,”azerbaijani-utf-8”,”bangla-utf-8”的字符串,phpmyadmin根据选择语言所对应的这一串字符串来寻找想对应的界面配置文件,这些文件存储在phpmyadminlang目录下,而这个文件中第一行定义的变量$charset则决定了登陆以后的页面字符集,也可以通过查看页面源码文件中的字符集来查看.

MySQL字符集II

最近发现RD发过来我这边checkSQL中有些数据是日文的, 但是我check了一下我们的production数据库, 发现数据库设的字符集是latin1, 但是我询问了下RD, 以前也一直这样, 但是从来没有报过错, 而且对应的系统上面也能正常显示出日文来.

我就开始纳闷了,latin1不是明明不能存储日文吗? 怎么这边就不会出问题呢.

 

研究之下, 还是有些发现的, 尽管我原来就知道MySQL中关于字符集的几个变量有些作用, 但是一直没有深究其中每一个的具体作用.

character_set_client指的是客户端的字符集, 一般在下SQL语句的时候, 最好设置成与客户端一致.

character_set_results指的是select结果显示的字符集, 一般情况下也设置成与客户端一致.

character_set_connection指的是连接的字符集.

然后剩下的就是数据库端的字符集了, 数据库端的自个字符集变量之间的关系这里先不讲, 因为数据库端字符集划分的最小单位是column, 所以我们这里指column的字符集.

 

我们在与MySQL进行交互的时候, 一种是客户端向数据库做请求. 这种交互第一步是character_set_clientcharacter_set_connection之间的转换, 如果不一致, 就需要进行转换. 而如果源字符集不是目标字符集的超集, 就可能出现数据丢失. 第二步就是character_set_connection与数据库之间字符集的转换, 同样, 如果不一致, 就有可能出现数据丢失.

 

而在我碰到的这个例子里面, 变量设置如下:

character_set_client latin1

character_set_connection utf8

character_set_database latin1

character_set_filesystem binary

character_set_results latin1

character_set_server latin1

character_set_system utf8

我们可以看到, 这中间经过了两层转换, 首先是character_set_clientcharacter_set_connection, 然后是character_set_connectioncharacter_set_database的转换, 但是可以看到这两种转换都不会损失数据.

也就是说我们下SQL的时候输入的是什么, 最终存入数据库的就是什么. 这也就是为什么我们的系统一直没有出现问题, 一直能够正常显示其中的日文.

从这个例子我们也可以看出, 只要我们保证这两个过程不需要进行字符集转换或者转换不会丢失数据, 我们的系统的数据就不会出现问题.

 

但是会出现问题的是COLLATE, 我这边做了如下测试:

CASE1: 数据表字符集gbk,set names 'utf8',客户端是utf8,

INSERT INTO test VALUES ('济南'),('大山'),('光明'),('阳光');

然后查看排序结果, 发现能够正常排序,结果为

大山

光明

济南

阳光

  

CASE2: 数据表字符集gbk,set names 'gbk',客户端是utf8

INSERT INTO test VALUES ('济南'),('大山'),('光明'),('阳光');

发现虽然还是能够正常看到了结果中文, 但是排序乱掉了, 变成了

阳光

光明

济南

大山

 

我们分析下

CASE1:clientconnection,不需要转换,connectiondatabase,需要进行转换,但因为我本来存储的是中文, gbk可以存储中文,所以能够正常转换,存入的也是gbk编码的中文,所以排序不会乱.

CASE2:clientconnection,不需要转换,connectiondatabase,也不需要进行转换. 所以没有数据丢失, 但是为什么排序就乱掉了呢, 原来我们存入的不是gbk编码的中文,而是我们客户端的utf8的中文, 而我们数据库的collate是中文排序,当然就不能正常排序啦.


2009年12月22日星期二

INNODB存储引擎中的AUTO_INCREMENT列

INNODB存储引擎中表AUTO_INCREMENT列的下一个值会存储在informationf_schem.TABLES表中, 每次往这个表中新插入数据, 如果对应列值比当前最大值要大, MYSQL都会更新informationf_schem.TABLES中的AUTO_INCREMENT, 使其等于最大值+1, 无论insert语句是在事务内还是自动COMMIT.

但是当事务内的INSERT语句ROLLBACK, informationf_schem.TABLES中的列值不会回滚. 也就是说这样就会造成跳号”.

 

也就是说, 我们可以通过查找informationf_schem.TABLES来确定某一TABLE AUTO_INCREMENT列的下一个值, 但是这一个值并不一定总是等于当前TABLE中最大值+1, 因为可能被其他connect连接所用, 但是还没有被COMMIT.

2009年12月17日星期四

有关auto_increment

MYSQL5.1中, 在MYISAM存储引擎下, 当auto_increment列与其他列组合作为主键, 且auto_increment列不作为第一列的时候, 能够实现auto_increment值根据第一列分组产生.

但是在INNO DB存储引擎下, 不允许这么设置, auto increment列必须为唯一主键或者组合主键的第一列.

2009年12月14日星期一

MySQL关于INNO DB参数

有用参数:

innodb_additional_mem_pool_size

存储数据字典的cache

innodb_autoextend_increment

auto extend table space file自增长幅度

innodb_buffer_pool_size

数据缓存

innodb_data_file_path

数据文件

innodb_data_home_dir

数据文件目录

innodb_fast_shutdown

0do a full purge and an insert buffer merge before a shutdown

1fast shutdown

2flush logs and then shut down cold

innodb_flush_log_at_trx_commit

0 每秒log buffer写到log file, 并更新数据文件

1 每次commit log buffer写到log file, 并更新数据文件

2 每次commit log buffer写到log file, 每秒更新数据文件

以上两个参数感觉对performance至关重要, 但其后台到底做了什么动作, 暂时还不晓得, 留待以后研究!!

innodb_log_buffer_size

innodb_log_file_size

innodb_log_files_in_group

innodb_log_group_home_dir

2009年12月8日星期二

MYSQL的字符集问题

MySQL的字符集支持(Character Set Support)有两个方面:
     字符集(Character set)和排序方式(Collation)
对于字符集的支持细化到四个层次:
     服务器(server),数据库(database),数据表(table)和连接(connection)

 

查看MYSQL支持的字符集:information_schema.CHARACTER_SETS

MySQL默认字符集

  MySQL对于字符集的指定可以细化到一个数据库,一张表,一列,应该用什么字符集。

但是,传统的 程序在创建数据库和数据表时并没有使用那么复杂的配置,它们用的是默认的配置,那么,默认的配置从何而来呢?

    (1)编译MySQL 时,指定了一个默认的字符集,这个字符集是 latin1
    (2)安装MySQL 时,可以在配置文件 (my.ini) 中指定一个默认的的字符集,如果没指定,这个值继承自编译时指定的;
    (3)启动mysqld 时,可以在命令行参数中指定一个默认的的字符集,如果没指定,这个值继承自配置文件中的配置,此时 character_set_server 被设定为这个默认的字符集;
    (4)当创建一个新的数据库时,除非明确指定,这个数据库的字符集被缺省设定为character_set_server
    (5)当选定了一个数据库时,character_set_database 被设定为这个数据库默认的字符集;
    (6)在这个数据库里创建一张表时,表默认的字符集被设定为 character_set_database,也就是这个数据库默认的字符集;
    (7)当在表内设置一栏时,除非明确指定,否则此栏缺省的字符集就是表默认的字符集;

简单的总结一下,如果什么地方都不修改,那么所有的数据库的所有表的所有栏位的都用 latin1 存储,不过我们如果安装 MySQL,一般都会选择多语言支持,也就是说,安装程序会自动在配置文件中把 default_character_set 设置为 UTF-8,这保证了缺省情况下,所有的数据库的所有表的所有栏位的都用 UTF-8 存储。

乱码问题一般是由于以上几个变量设置错误照成的,很多人在请教乱码问题的时候,一般都会得到一个这样的答案:你先set names一下

那么set names是什么呢? set names实际上就是同时设置了character_set_client, character_set_connection, character_set_results这三个系统变量。

 

因为character_set_client,character_set_connection这两个变量仅用与保证与 character_set_database编码的一致,而character_set_results则用与保证SELECT返回的结果与程序的编码 一致。

如,你的数据库(character_set_database)用的是utf8的字符集,那么你就要保证 character_set_client,character_set_connection也是utf8的字符集。而你的程序也许采用的并不是 utf8,比如你的程序用的是gbk,那么你若把character_set_results也设置为utf8的话就会出现乱码问题。此时你应该把 character_set_results设置为gbk。这样就能保证数据库返回的结果与你的程序的编码一致。

 

 

2009年12月3日星期四

关于MySQL FLOAT数据类型的误差

最近刚接触MySQL, 在设计表结构的时候大量的使用了FLOAT类型, 这个时候新公司的同事过来说不能用FLOAT, FLOAT的时候会带来误差, 并且举了个例子, 就是用FLOAT(8,3)存储99999.993的时候会变成99999.992, 我测试了一下, 果然.

我就纳闷了, 为什么会产生这种情况呢? 这么容易产生误差!!! 我在玩Oracle的时候使用NUMBER类型可从来没有出现过这种问题. 于是下决心研究以下这个问题, 找出问题的究竟.

通过翻阅资料以及自己的测试, 终于找出了结果.

自从 MySQL 3.23 版本以后,MySQL 的浮点型 FLOAT DOUBLE 就已经遵循了IEEE-754的单精度和双精度标准。MySQL FLOAT 类型占用4个字节,DOUBLE 类型占用8个字节.

IEEE 标准中,浮点数是将特定长度的连续字节的所有二进制位分割为特定宽度的符号域,指数域和尾数域三个域,其中保存的值分别用于表示给定二进制浮点数中的符 号,指数和尾数。这样,通过尾数和可以调节的指数(所以称为"浮点")就可以表达给定的数值了。具体的格式参见下面的图例:

第一个域为符号域。其中 0 表示数值为正数,而 1 则表示负数。

第二个域为指数域,对应 于我们之前介绍的二进制科学计数法中的指数部分。其中单精度数为 8 位,双精度数为 11 位。以单精度数为例,8 位的指数为可以表达 0 255 之间的 255 个指数值。但是,指数可以为正数,也可以为负数。为了处理负指数的情况,实际的指数值按要求需要加上一个偏差(Bias)值作为保存在指数域中的值,单精 度数的偏差值为 127,而双精度数的偏差值为 1023。比如,单精度的实际指数值 0 在指数域中将保存为 127;而保存在指数域中的 64 则表示实际的指数值 -63 偏差的引入使得对于单精度数,实际可以表达的指数值的范围就变成 -127 128 之间(包含两端)。我们不久还将看到,实际的指数值 -127(保存为 0)以及 +128(保存为全 1)保留用作特殊值的处理。这样,实际可以表达的有效指数范围就在 -127 127 之间。

第三个域为尾数域,其中 单精度数为 23 位长,双精度数为 52 位长。除了我们将要讲到的某些特殊值外,IEEE 标准要求浮点数必须是规范的。这意味着尾数的小数点左侧必须为 1,因此我们在保存尾数的时候,可以省略小数点前面这个 1,从而腾出一个二进制位来保存更多的尾数。这样我们实际上用 23 位长的尾数域表达了 24 位的尾数。比如对于单精度数而言,二进制的 1001.101(对应于十进制的 9.625)可以表达为 1.001101 × 23,所以实际保存在尾数域中的值为 00110100000000000000000,即去掉小数点左侧的 1,并用 0 在右侧补齐。

好了, 以上是背景知识介绍, 下面就来看我们这个例子:

FLOAT(8,3) 存储99999.993

我们首先规格化:

BIN(99999) = 11000011010011111

也就是说99999需要17, 省略掉第一个1, 需要耗费16, 也就是说剩下来的只有7位可以存储小数点后面的0.993

BIN(0.993 * POW(2,7))=1111111

也就是说最后规格化的结果为1.10000110100111111111111 * 216

但是我们来看一下这个存储的结果

CONV(110000110100111111111111,2,10) / POW(2,7)= 99999.9921875

也就是说我们存储的99999.993实际存储的是99999.9921875

我们知道FLOAT(M,D)中的D并不会影响, 只会映像查看的结果, 所以在SELECT的时候就四舍五入变成了99999.992

那我们来测试下,实际上是不是这样子的

CREAT TABLE TEST(T FLOAT(20,12));

INSERT INTO TEST VALUES(9999.993);

SELECT * FROM TEST;

果然, 结果是99999.9921875

我们接下来看下一系列的

INSERT INTO TEST VALUES

(99999.991),

(99999.992),

(99999.993),

(99999.994),

(99999.995),

(99999.996),

(99999.997),

(99999.998),

(99999.999),

我们发现从99999.991 - 99999.996存储的都是99999.9921875, 99999.997 - 99999.999我们QUERY出来的结果是100000

可是细心一点会发现, 我们在规格化小数部分的时候, 使用的是BIN(0.993 * POW(2,7)), 而如果按照这样子

Bin(0.991 * pow(2,7)) = bin(126.848) = 1111110

Bin(0.992 * pow(2,7)) = bin(126.976) = 1111110

Bin(0.993 * pow(2,7)) = bin(127.104) = 1111111

Bin(0.994 * pow(2,7)) = bin(127.232) = 1111111

Bin(0.995* pow(2,7)) = bin(127.36) = 1111111

Bin(0.996 * pow(2,7)) = bin(127.488) = 1111111

Bin(0.997 * pow(2,7)) = bin(127.616) = 1111111

Bin(0.998 * pow(2,7)) = bin(127.744) = 1111111

Bin(0.999 * pow(2,7)) = bin(127.872) = 1111111

我们存储99999.991, 99999.992的时候,后面7为就应该是1111110, 而不是1111111, 最终结果就应该是99999.984375

99999.997, 99999.998, 99999.999 3个数存储的也应该是99999.9921875 而不是100000.

这是为什么呢? 四舍五入? 前面两个入成127, 后面两个入成128?

我们来测试下, 我们存储99999.935

0.935 * POW(2,7) = 119.68 ≈ 120

后面7位就应该是BIN(120) = 1111000

CONV(110000110100111111111000,2,10) / POW(2,7)= 99999.9375

我们看以下存储结果, 果然是99999.9375.

现在, FLOAT型之所以产生误差的原因就已经清楚了, 是因为存储格式的局限性造成的. DOUBLE因为可以用来存储尾数的位更多, 所以相对来说会更精确一点, 但也是有误差的. 所以如果要严格的要求精确, 就只能采用DECIMAL.