为什么 MS SQL bigint 类型隐式映射到 float64 python 类型,处理它的最佳方法是什么?

ate*_*evm 5 sql-server python external-scripts

Python 整数类型具有无限精度,因此它能够容纳 MS SQL(64 位)的 bigint 值。当传递给外部脚本时,它仍然隐式映射到 float64 python 类型。

这可能会导致大整数的严重计算错误。

那么为什么它映射到 float64 呢?

我的猜测是:

R 是通过可扩展性架构在 Python 之前添加的,它具有固定精度的整数(32 位)。所以它不能容纳 bigint。所以也许这是一个兼容性问题。

确保精确计算的最佳做法是什么?

简单但可行的想法:将 bigint 作为字符串传递,然后将它们解析为 int。

我知道它在实践中引起问题的可能性很小,但还是很高兴知道。

怎么可能有问题:

我写了一个简单的例子来演示它怎么会是一个问题:

CREATE TABLE #test (
    big_integer BIGINT
);

INSERT INTO #test 
    (big_integer)
VALUES
    (36028797018963968),
    (36028797018963968 + 1);

EXECUTE sp_execute_external_script 
    @language = N'Python',
    @input_data_1 = N'SELECT big_integer FROM #test',
    @script = N'
print(InputDataSet.dtypes)
OutputDataSet = InputDataSet
'
Run Code Online (Sandbox Code Playgroud)

在 SQL Server 2019 上执行此代码将为您提供以下结果:

| | (No column name)  |
|---------------------|
|1| 36028797018963970 |
|2| 36028797018963970 |
Run Code Online (Sandbox Code Playgroud)

由于该print(InputDataSet.dtypes)声明,我们可以看到以下消息:

...
STDOUT message(s) from external script: 
big_integer    float64
dtype: object
...
Run Code Online (Sandbox Code Playgroud)

所以我们得到了一个浮点舍入错误。对于足够大的整数,此错误的值大于 1,这就是此问题的根源。

教授浮点算术超出了这个问题的范围,但如果你不明白发生了什么,我会链接一些很好的材料:

简单示例 - 堆栈溢出

浮点数 - Computerphile

IEEE 754 格式 - 牛津

如果您想对此进行试验,我还会分享一个小的 ipython 示例(这不能替代学习其背后的理论):

In [16]: import numpy as np

In [17]: a = 2**55

In [18]: a
Out[18]: 36028797018963968

In [19]: float(a) == float(a + 1)
Out[19]: True

In [20]: float(a)
Out[20]: 3.602879701896397e+16

In [21]: float(a + 1)
Out[21]: 3.602879701896397e+16

In [22]: np.nextafter(float(a), np.inf)
Out[22]: 3.6028797018963976e+16
Run Code Online (Sandbox Code Playgroud)

笔记

要运行我的示例 T-SQL,必须满足一些条件:

ate*_*evm 2

我写下我发现的最佳解决方案:

CREATE TABLE #test (
    big_integer BIGINT
);

INSERT INTO #test 
    (big_integer)
VALUES
    (36028797018963968),
    (36028797018963968 + 1);

CREATE TABLE #out (
    big_integer BIGINT
);

INSERT INTO #out
    EXECUTE sp_execute_external_script 
        @language = N'Python',
        @input_data_1 = N'SELECT CAST(big_integer AS VARCHAR(20)) AS big_integer FROM #test',
        @script = N'
import numpy as np
print(InputDataSet)
InputDataSet["big_integer"] = InputDataSet["big_integer"].astype(np.int64)
InputDataSet["big_integer"] = InputDataSet["big_integer"] + 1
InputDataSet["big_integer"] = InputDataSet["big_integer"].astype(str)
OutputDataSet = InputDataSet
';

SELECT big_integer FROM #out;
Run Code Online (Sandbox Code Playgroud)

我做了我在问题中的假设:

  1. big_integer列转换为VARCHAR(20)64 位有符号整数的字符串表示形式的最大长度:
In [34]: len(str(-2**63))
Out[34]: 20

In [35]: len(str(2**63-1))
Out[35]: 19
Run Code Online (Sandbox Code Playgroud)
  1. 在外部脚本中将其转换回numpy.int64 类型。

  2. 做了一个简单的计算:增加列中的所有值

  3. 将其转换回仍在 python 中的字符串。这一步也是必要的,因为隐式类型转换是双向的。

  4. 将值插入表big_integer的列中#out。它也有一个BIGINT类型,并且返回的字符串被隐式转换为BIGINT

笔记

您需要处理此问题的情况很少见。整数值必须大于 2^52,因此两个 float64 之间的距离将大于 1。

In [50]: def float_distance(x):
    ...:     x_float = float(x)
    ...:     x_next_float = np.nextafter(x_float, np.inf)
    ...:     x_float_diff = x_next_float - x_float
    ...:     return(x, x_float, x_next_float, x_float_diff)

In [51]: float_distance(2**52)
Out[51]: (4503599627370496, 4503599627370496.0, 4503599627370497.0, 1.0)

In [52]: float_distance(2**53)
Out[52]: (9007199254740992, 9007199254740992.0, 9007199254740994.0, 2.0)
Run Code Online (Sandbox Code Playgroud)

我想如果您存储物理学或生物信息学中高通量科学研究的结果,可能会发生这种情况。