项目中碰到一个bug,需要将MySQL表中的数据导出,字段中间用逗号隔开。
1、复现
步骤:
版本 5.1.48
a) 准备数据
CREATE TABLE `test` (
`id` int(11) DEFAULT NULL,
`data` char(10) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=gbk;
insert into tad2 values (1,'丁\\奇');
|
b) select concat(id, data) from test into outfile ‘/tmp/a’;
现象:
在生成的/tmp/a中,发现”丁”字乱码了,实际上是”丁”的第二个字节前面多了个 ‘\’。
2、相关现象
a) select concat(id, data) from test; --结果正常(否则早报了)
b) select id, data from test into outfile ‘/tmp/a’ –结果正常
c) select concat(data, data) from test into outfile ‘/tmp/a’;结果正常
d) MySQL官方 5.5版本; select concat(id, data) from test into outfile ‘/tmp/a’; 结果正常。
3、源码分析
先看出问题的代码位置select_export::send_data (sql/sql_class.cc),这个函数是将要输出到外部文件(outfile)的字符串作处理,比如将’\’转成’\\’. 这解释了上面的相关现象a), 不需要输出到外部文件,不调用这个函数。
字符转换代码逻辑如下
for (start=pos=(char*) res->ptr(),end=pos+used_length ;
pos != end ;
pos++)
{
if (use_mb(res_charset))
{
int l;
if ((l=my_ismbchar(res_charset, pos, end)))
{
pos += l-1;
continue;
}
}
if ((NEED_ESCAPING(*pos) ||
(check_second_byte &&
my_mbcharlen(character_set_client, (uchar) *pos) == 2 &&
pos + 1 < end &&
NEED_ESCAPING(pos[1]))) &&
(enclosed || !is_ambiguous_field_term ||
(int) (uchar) *pos != field_term_char))
{当前字符前加入 ‘\’}
}
|
说明:
a) 宏和注释删掉了。简单说来就是一个循环,判断需要转译的字符,前面加上’\’。
b) use_mb(res_charset)判断当前字符串所使用的字符集是否定义了ismbchar。若定义,则通过my_ismbchar判断每个单字的长度l,并直接跳过l个字节。
c) 对于不能跳的,进入if的判断,如果是需要转译的字符,则前面加上’\’.
这代码看上去似乎没什么问题,尤其是对于多字节字符还通过use_mb作了保护。不过这个if判断需要细细看一下。逻辑简单描述是这样的“若当前字符需要转译,则加’\’; 若客户端使用字符为2字节,且下一个字节需要转译,则当前字符也转译”。
于是这儿悲剧了,客户端使用gbk. 当我们使用concat(id, data)时,整个合并的字符串被当成什么字符来处理呢――binary。于是use_mb的多字符串保护都无效了。在处理“丁”的第二个字符时,它的下一个字符是’\’, 因此原本应该是转译成”B6 A1”的这个字,被转译成”B6 5C A1”, 在输出文本上就看到了乱码。之所以会变成binary,是因为bigint类型的id字段和gbk作合并判断的结果。
于是我们知道相关现象c), 由于是两个gbk合并,结果还是gbk,字符保护生效。
同时相关现象b), 由于没有concat,因此不需要类型合并,按照两个字符串来处理,data字段还是gbk,同样不会有问题。
从代码中可以发现,所有的数字相关字段与字符串字段作concat并outfile之后,都会有这个现象。
4、MySQL 5.5修复了?
在5.5版本中,数字字段不再是my_charset_binary, 而是my_charset_numeric, 而这个my_charset_numeric,其实就是my_charset_latin1. latin1与gbk合并的结果是gbk. 所以相关现象d),并不是处理了这个问题,而是刚好绕过。
完成上面的分析,要再复现并不难。把表中的id字段改成blob字段,5.5中“丁”字还是乱码了。
5、反思,这个算“bug”吗
我们回顾一下,发现之所以会出现文章开头说到的乱码,是因为concat(id, data)的合并结果字符串是binary,但是客户端使用的是gbk造成的。
如果在导出之前,先set names binary, 结果自然正常了。因为即使字符保护被跳过,在判断到“丁”字的第二个字符的时候,由于客户端也使用binary,就不会在这里加’\’, 而整个串中只有’\’会被转译成 ‘\\’。这个是我们要的结果。
似乎只是使用的错误?其实细想一下就知道了,为什么源码中要加第二个字符的判断?而且把gbk认定为是“需要检查下一个字符,以确定当前字符是否需要转译”的字符集?原因就是编码上,有些汉字的第二个字符就是需要转译的,如果按字符判断,问题就出现了。
一个例子就是” 盶”(B1 5C),在gbk下,需要转译成什么呢,直接输出”B1 5C”显然是不行的, 这个5C会把它后面的字符给转译了;转成B1 5C 5C也不行,会被理解成B1+”5C5C”。 实际上MySQL希望转成” 5C B1 5C 5C”, 所以代码就成了我们看到的这样。
所以我们的结论还是认为它是个bug,看下面这行数据
CREATE TABLE `test` (
`name`blob,
`data` char(10) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=gbk;
insert into tad2 values ('盶','丁\\奇');
|
还是那个需求,把表数据输出,中间用逗号隔开,此时客户端要使用什么字符集呢。实际上不论使用binary还是gbk,都会造成乱码。
6、简单修改
这个问题还挺复杂的,也许binary和gbk字段就不应该允许concat,也许即使是concat,也应该按照原来的字段类型先转译好了再直接拼接,这样就不会有用A字符集的规则解释B字符集的矛盾了。
简单验证一下上面的阐述,把5.1换成跟5.5相似的现象吧.(虽然只是把错100步改成错90步)
a) 在sql/mysql_priv.h的enum Derivation定义中增加DERIVATION_NUMERIC=5 (当然DERIVATION_IGNORABLE修改为=6)
b) 在sql/field.h的class Field_num声明中增加三行
enum Derivation derivation(void) const { return DERIVATION_NUMERIC; }
uint repertoire(void) const { return MY_REPERTOIRE_ASCII; }
CHARSET_INFO *charset(void) const { return &my_charset_latin1; }
c) 在sql/field.h的class Field声明中增加一行
virtual uint repertoire(void) const { }
d) 修改sql/item.cc的Item_field::set_field(Field *field_par)函数
collation.set(field_par->charset(), field_par->derivation(), field_par->repertoire());
好吧,这样所有的数字类型字段都用latin1编码了。
7、小结
对于支持多字符集的软件,字符串处理还是很复杂的。当然我们知道5.5还是有问题的,反馈一下,期待官方的方案吧。
目前碰到有这个需求的,还是先直接导出,在生成文件中自己作文本处理吧。
分享到:
相关推荐
mysql字符串比较函数:concat和regexp.pdf
gulp-concat 口香糖 gulp-image-data-uri 口香糖 gulp-minify-css 吞咽净化 手表 浏览器同步 Gulp任务 gulp css :将sass编译为css +缩小 gulp js :concat +缩小脚本 gulp img :压缩图像 gulp html :缩小html ...
然而,有时候为了学习、练习或者特定的需求,我们可能需要自定义一个字符串类来模拟`std::string`的行为。"String: C++字符串的个人实现"这个项目就是这样一个实践,它要求我们从零开始构建一个类似于`std::string`...
在MySQL数据库中,`CONCAT`函数用于将两个或更多的字符串连接成一个单一的字符串。这个函数非常实用,尤其是在处理涉及字符串拼接的查询时。`CONCAT`的基本语法如下: ```sql CONCAT(str1, str2, ..., str_n) ``` ...
Python基于机器学习的光伏功率预测项目源码+训练数据+测试数据 数据加载 使用pandas的read_csv()方法加载数据,然后使用concat()的方法分别将4个训练集和4个测试集合并到一起。 数据探索 使用head()查看测试集和...
在动态拼接字符串时,我们常会用到字符拼接,我对拼接的引号不理解,如: 1、”’+ id +”’ 为什么是3个引号,为什么左边一个加号右边一个加号(能不能着重帮我解释下这个,详细点) SQL code ...
JFramework:Meteor + React的快速原型制作 ...切穿CRUD 您想如何构建一个完全没有样板代码的Web应用程序? JFramework为您提供了以下快速原型制作功能: ... this.setState({books: this.state.books.concat([anot
作用: GROUP_CONCAT函数可以拼接某个字段值成字符串,默认的分隔符是 逗号,即”,” , 如果需要自定义分隔符可以使用 SEPARATOR 如: SELECT GROUP_CONCAT...(1)在MySQL配置文件中加入: 文件:my.ini
在SQL Server 2012中,引入了一个新的字符串函数——CONCAT,它的主要功能是方便地连接多个字符串。在CONCAT函数出现之前,我们通常使用 "+" 运算符来连接字符串,但这种方法存在一个问题,即如果其中任何一个字符串...
`GROUP_CONCAT` 是 MySQL 中一个非常实用的聚合函数,主要用于将来自同一分组的多个值连接成一个字符串。这一功能在许多场景下都非常有用,例如汇总数据、创建列表等。 #### 二、基本语法与用法 `GROUP_CONCAT` 的...
Shopify + Grunt +木材为下一个Shopify...特征Timber主timber.scss.css单独Sass文件使Sass进入资产文件夹压缩影像NPM软件包咕-咕tri的手表grunt-contrib-imagemin grunt-contrib-concat指示运行git clone git@github....
在这个场景下,错误信息提到的是"WM_CONCAT"函数,这表明在Oracle 19c数据库环境中,用户尝试使用WM_CONCAT函数,但系统无法识别该函数,提示其为无效的标识符。WM_CONCAT是Oracle 10g及更早版本中用于字符串合并的...
解决ORA-00904: "WMSYS"."WM_CONCAT": 标识符无效 在sqlplus中执行包里的owmctab.plb、owmaggrs.plb、owmaggrb.plb三个脚本即可。 简单来说,用PL/SQL执行下一下几个脚本就可以了。 特别要注意:PL/SQL登录时,要...
MySQL中的`GROUP_CONCAT`函数是一个非常实用的聚合函数,它允许你在分组查询中将一组行的某个列值合并成一个字符串,每个值之间由指定的分隔符隔开。这个函数对于数据汇总和报告生成特别有用,因为它可以把多行数据...
总结来说,`GROUP_CONCAT`是MySQL中一种强大的聚合工具,能够有效地将一组值合并为单个字符串,这对于数据报告和分析特别有用。理解并熟练运用`GROUP_CONCAT`,可以帮助我们编写出更加高效和简洁的SQL查询。在实际...
一、concat()函数 功能:将多个字符串连接成一个字符串 语法:concat(str1,str2,…) 其中的字符串既可以是数据表字段,也可以是指定的字符串 返回结果为连接参数产生的字符串,如果有任何一个参数为null,则该条记录...
在MySQL数据库中,`GROUP_CONCAT` 函数是一个非常实用的工具,它允许你在聚合查询中将多个行的某个列值合并成一个单一的字符串,每个值之间由默认的逗号分隔。然而,当你处理大量数据时,可能会遇到一个限制,即`...
前几篇文章给大家介绍了MySQL中的替换函数(Replace)、切分函数(SubString),今天我们一起来看看MySQL专业拼接“字符串”的函数:concat。老规矩,有好的建议和想法,记得写到评论中,等我上班摸鱼时,跟大家一起...