数据库内核月报 - 2015 / 07

MySQL · 答疑解惑 · 浮点型的显示问题

背景

我们打开MySQL客户端,执行下面的SQL语句:

drop table if exists t;
create table t(id double)engine=innodb;
insert into t values(1e-15),(1e-16);
select * from t;

select * from t出来的内容如下,我们看到浮点数1e-15用正常的数值来表示,1e-16用科学技术法来表示。

+-------------------+
| id                |
+-------------------+
| 0.000000000000001 |
|             1e-16 |
+-------------------+

我们知道在计算机中浮点数用来近似表示某个实数。浮点数有2种显示风格,一种是正常的表示(0.18, 2.345等),一种是科学技术法的表示(1.23e+12,2.45e-16等)。那么MySQL的浮点型在什么情况下表示成正常的实数(如0.18,2.345),什么情况下表示成科学计数法(如1.23e+12,2.45e-16)呢?下面我们进行更精确的实验以及从源码角度来解释MySQL对于浮点数的显示问题。

实验

我们用下面的SQL语句直接显示多个浮点数:

select (1e+14),(1e+15),(2.3e+14),(2.3e+15),(1e-15),(1e-16),(3.4e-15),(3.4e-16);

select出来的内容是:

+-----------------+-------+-----------------+---------+-------------------+-------+--------------------+---------+
| 1e+14           | 1e+15 | 2.3e+14         | 2.3e+15 | 1e-15             | 1e-16 | 3.4e-15            | 3.4e-16 |
+-----------------+-------+-----------------+---------+-------------------+-------+--------------------+---------+
| 100000000000000 |  1e15 | 230000000000000 |  2.3e15 | 0.000000000000001 | 1e-16 | 0.0000000000000034 | 3.4e-16 |
+-----------------+-------+-----------------+---------+-------------------+-------+--------------------+---------+

通过以上的例子再结合更多的实验我们可以看出这么一个规律:

  1. 在数值大于0时,科学计数法表示的指数小于或等于14时,select出来的是正常非科学计数法的数值;
  2. 在数值大于0时,科学计数法表示的指数大于14时,select出来的是科学计数法的数值;
  3. 当数值小于0时,科学计数法表示的指数大于或等于-15时,select出来的是正常非科学计数法的数值;
  4. 当数值小于0时,科学计数法表示的指数小于-15时,select出来的是科学计数法的数值。

另外由于上面的select并没有来自某个具体表,所以浮点数展示的规则是和存储引擎没有关系的,MySQL对于浮点数展示包装的逻辑是在server层完成的。

我们去代码里验证一下这个规律是否正确。

验证

我们可以用gdb跟到代码里面寻找这块逻辑,但是MySQL单单server层的代码也有好几万行,盲目的跟代码并不能很快的找到我们要找的位置。所以,跟代码前我们很有必要先分析一下这块逻辑会出现在什么位置。

我们知道MySQL对select的处理的大体过程是,客户端向服务端发送select,服务端解析select并把结果返回到客户端,那么这块逻辑就很有可能出现在服务端把结果送到客户端这个过程中。

最后通过跟踪代码我们发现了在MySQL将结果返回客户端的过程中,在下面这个位置的buffer->set_real对要显示的内容进行了包装,并把包装的结果放到buffer这个变量里。

sql/protocol.cc:
bool Protocol_text::store(double from, uint32 decimals, String *buffer)
{
#ifndef DBUG_OFF
  DBUG_ASSERT(field_types == 0 ||
	      field_types[field_pos] == MYSQL_TYPE_DOUBLE);
  field_pos++;
#endif
  buffer->set_real(from, decimals, thd->charset());
  return net_store_data((uchar*) buffer->ptr(), buffer->length());
}

在对set_real往更深的调用层次跟踪,我们找到了对浮点数的展示进行包装的位置:

strings/dtoa.c:
...
size_t my_gcvt(double x, my_gcvt_arg_type type, int width, char *to,
               my_bool *error)
...

通过分析my_gcvt这个函数,我们可以得出MySQL对于浮点数展示的规则。

首先我们必须知道以下这个事实(下面’f’format表示正常格式,’e’format表示科学计数法的格式):
MySQL对select出来的每一列占用的宽度是有要求的,如果浮点数在’f’format下的有效数字太多,就有可能超过最大宽度,这时若还想要用’f’format,就不得不丢失一些有效数字了。如果同样数值的’e’format不会丢失有效数字,MySQL就会把该浮点数从’f’format转为’e’format。

下面的这个if语句确定了用’f’format表示浮点数的条件。

strings/dtoa.c -> function my_gcvt

if ((have_space ||
    /*
      Not enough space, let's see if the 'f' format provides the most number
      of significant digits.
    */
     ((decpt <= width && (decpt >= -1 || (decpt == -2 &&
                                            (len > 1 || !force_e_format)))) &&
       !force_e_format)) &&

     /*
       Use the 'e' format in some cases even if we have enough space for the
       'f' one. See comment for MAX_DECPT_FOR_F_FORMAT.
     */
    (!have_space || (decpt >= -MAX_DECPT_FOR_F_FORMAT + 1 &&
                     (decpt <= MAX_DECPT_FOR_F_FORMAT || len > decpt))))

代码有点乱,但是通过看注释以及上下文,我们可以分析出用’f’format表示浮点数必须同时满足2个条件:
1. 用’f’format表示浮点数不会因为宽度限制造成精度丢失;
2. 浮点数用若用’e’format表示时的指数在一个临界值范围(-15,14)内,那么就用’f’format表示。

在前面的实验中,我们给出的几个浮点数若用’f’format并不会超过列的最大宽度,即满足条件1。那么这几个浮点数用’f’format还是’e’format表示就由条件2决定了,条件2和我们在实验中看到的规律相符。