PHP 和 MySQL 固定点数值运算的挑战与技巧
处理固定点数值时,需要格外小心,尤其是在使用 PHP 和 MySQL 进行开发时。本文将探讨使用 PHP BCMath 扩展、MySQL 固定点表达式处理以及将固定点数据从 PHP 持久化到 MySQL 时遇到的障碍和细节问题。尽管存在一些挑战,我们将尝试找出如何处理固定点数值并避免精度损失的方法。
BCMath 文档指出:
为了进行任意精度数学运算,PHP 提供了二进制计算器,它支持任何大小和精度的数字,这些数字以字符串表示。
因此,BCMath 函数参数应以字符串表示。将数值型变量传递给 bcmath 函数可能导致错误的结果,与将双精度值视为字符串时出现的精度损失相同。
<code class="language-php">echo bcmul(776.210000, '100', 10) . PHP_EOL; echo bcmul(776.211000, '100', 10) . PHP_EOL; echo bcmul(776.210100, '100', 10) . PHP_EOL; echo bcmul(50018850776.210000, '100', 10) . PHP_EOL; echo bcmul(50018850776.211000, '100', 10) . PHP_EOL; echo bcmul(50018850776.210100, '100', 10) . PHP_EOL;</code>
结果是:
<code>77621.00 77621.100 77621.0100 5001885077621.00 5001885077621.100 5001885077621.00 //此处可见精度损失</code>
切勿将数值型变量传递给 BCMath 函数,只能传递表示数字的字符串值。即使不处理浮点数,BCMath 也会输出奇怪的结果:
<code class="language-php">echo bcmul('10', 0.0001, 10) . PHP_EOL; echo bcmul('10', 0.00001, 10) . PHP_EOL; echo 10*0.00001 . PHP_EOL;</code>
结果是:
<code>0.0010 0 // 这真的很奇怪!!! 0.0001</code>
其原因是 BCMath 将其参数转换为字符串,并且在某些情况下,数字的字符串表示形式具有指数表示法。
<code class="language-php">echo bcmul('10', '1e-4', 10) . PHP_EOL; // 也输出 0</code>
PHP 是一种弱类型语言,在某些情况下,无法严格控制输入——希望处理尽可能多的请求。
例如,我们可以通过应用 sprintf 转换来“修复”案例 2 和 案例 3:
<code class="language-php">$val = sprintf("%.10f", '1e-5'); echo bcmul('10', $val, 10) . PHP_EOL; // 给我们 0.0001000000</code>
但是,应用相同的转换会破坏案例 1 的“正确”行为:
<code class="language-php">$val = sprintf("%.10f", '50018850776.2100000000'); echo bcmul('10', $val, 10) . PHP_EOL; echo bcmul('10', 50018850776.2100000000, 10) . PHP_EOL; 500188507762.0999908450 // 错误 500188507762.10 // 正确</code>
因此,sprintf 解决方案不适用于 BCmath。假设所有用户输入都是字符串,我们可以实现一个简单的验证器,捕获所有指数表示法的数字并正确转换它们。此技术在 php-bignumbers 中已实现,因此我们可以安全地传入类似 1e-20 和 50018850776.2101 的参数,而不会丢失精度。
切勿在 BCMath PHP 扩展函数中将浮点数用作固定点运算参数。仅使用字符串。
使用 BCMath 扩展运算时,请注意指数表示法的参数。BCMath 函数无法正确处理指数参数(例如“1e-8”),因此应手动转换它们。注意,不要使用 sprintf 或类似的转换技术,因为这会导致精度损失。
可以使用 php-bignumbers 库,它可以处理指数形式的输入参数,并为用户提供固定点数学运算函数。但是,其性能不如 BCMath 扩展,因此它是在健壮的包和性能之间的一种折衷方案。
在 MySQL 中,固定点数字使用 DECIMAL 列类型处理。您可以阅读 MySQL 官方文档了解数据类型和精确数学运算。
最有趣的部分是 MySQL 如何处理表达式:
数值表达式的处理取决于表达式包含的值的类型:
如果存在任何近似值,则表达式为近似值,并使用浮点运算进行计算。
如果不存在近似值,则表达式仅包含精确值。如果任何精确值包含小数部分(小数点后的值),则使用 DECIMAL 精确算术计算表达式,精度为 65 位数字。“精确”一词受限于可以在二进制中表示的内容。例如,1.0/3.0 可以用十进制表示法近似为 .333…,但不能写成精确数字,因此 (1.0/3.0)*3.0 不精确地计算为 1.0。
否则,表达式仅包含整数值。表达式是精确的,并使用整数算术进行计算,精度与 BIGINT 相同(64 位)。
如果数值表达式包含任何字符串,则将其转换为双精度浮点值,表达式为近似值。
这是一个简短的示例,演示了小数部分的情况:
<code class="language-php">echo bcmul(776.210000, '100', 10) . PHP_EOL; echo bcmul(776.211000, '100', 10) . PHP_EOL; echo bcmul(776.210100, '100', 10) . PHP_EOL; echo bcmul(50018850776.210000, '100', 10) . PHP_EOL; echo bcmul(50018850776.211000, '100', 10) . PHP_EOL; echo bcmul(50018850776.210100, '100', 10) . PHP_EOL;</code>
这看起来很简单,但让我们看看如何在 PHP 中处理它。
因此,现在我们必须将固定点值从 PHP 持久化到 MySQL。正确的方法是在查询中使用预处理语句和占位符。然后我们进行参数绑定,一切都是安全可靠的。
<code>77621.00 77621.100 77621.0100 5001885077621.00 5001885077621.100 5001885077621.00 //此处可见精度损失</code>
当我们将值绑定到语句占位符时,我们可以通过 bindValue 的第三个参数指定其类型。可能的类型由常量 PDO::PARAM_BOOL、PDO::PARAM_NULL、PDO::PARAM_INT、PDO::PARAM_STR、PDO::PARAM_LOB 和 PDO::PARAM_STMT 表示。所以问题是 PHP PDO 扩展没有用于绑定的十进制参数类型。结果,查询中的所有数学表达式都被视为浮点表达式,而不是固定点表达式。
如果我们想利用预处理语句并使用固定点数字,最好的方法是在 PHP 中执行所有数学运算并将结果保存到 MySQL。
<code class="language-php">echo bcmul('10', 0.0001, 10) . PHP_EOL; echo bcmul('10', 0.00001, 10) . PHP_EOL; echo 10*0.00001 . PHP_EOL;</code>
我们得出以下结论:
我个人最喜欢的方法是第一种:在 PHP 中进行所有数学运算。我同意 PHP 和 MySQL 可能不是需要精确数学运算的应用程序的最佳选择,但是,如果您选择了这种技术堆栈,那么了解如何正确处理它是很好的。
(由于篇幅限制,FAQs 部分被省略。如果需要,可以单独生成FAQs部分。)
以上是BCMATH,PHP中的固定点数学,精确损失案例的详细内容。更多信息请关注PHP中文网其他相关文章!