Skip to content

Double类型

概述

Double(双精度浮点数)类型是MongoDB中用于存储小数和浮点数值的数据类型。Double类型遵循IEEE 754标准的64位双精度浮点数格式,提供大约15-17位有效数字的精度。在价格、评分、坐标、科学计算等需要小数精度的场景中广泛应用。

理解Double类型的精度特性、存储机制和最佳实践,对于避免精度丢失、正确处理数值计算和优化存储空间至关重要。本章节将全面介绍MongoDB中Double类型的使用方法和注意事项。

基本概念

Double类型特性

MongoDB的Double类型具有以下核心特性:

1. IEEE 754标准

  • 64位双精度浮点数
  • 约15-17位有效数字
  • 支持特殊值:Infinity、-Infinity、NaN

2. 数值范围

  • 最大值:约±1.8 × 10^308
  • 最小正数:约±5.0 × 10^-324
  • 存储空间:8字节

3. 精度特点

  • 二进制浮点数表示
  • 存在精度问题(如0.1 + 0.2 ≠ 0.3)
  • 不适合精确的货币计算

4. 查询能力

  • 支持精确匹配
  • 支持范围查询
  • 支持数值比较

Double类型语法

php
<?php
// 场景说明:演示MongoDB Double类型的基本语法和使用方式

// 1. 连接MongoDB数据库
$client = new MongoDB\Client("mongodb://localhost:27017");
$database = $client->selectDatabase("testdb");
$collection = $database->selectCollection("double_examples");

// 2. 插入不同类型的浮点数数据
$document = [
    'price' => 99.99,                                    // 商品价格
    'rating' => 4.5,                                     // 评分
    'temperature' => 36.5,                               // 温度
    'coordinate_x' => 116.404,                          // 经度
    'coordinate_y' => 39.915,                           // 纬度
    'percentage' => 0.85,                               // 百分比
    'scientific' => 1.23e-10,                           // 科学计数法
    'negative' => -123.456,                             // 负数
    'zero' => 0.0,                                      // 零值
    'infinity' => INF,                                  // 无穷大
    'nan' => NAN                                        // 非数字
];

// 关键行注释:插入包含各种浮点数的文档
$result = $collection->insertOne($document);
echo "插入成功,文档ID: " . $result->getInsertedId() . "\n";

// 3. 浮点数查询操作
// 精确匹配(注意精度问题)
$exactMatch = $collection->findOne(['price' => 99.99]);
echo "精确匹配结果: " . json_encode($exactMatch) . "\n";

// 范围查询
$rangeQuery = $collection->find([
    'rating' => ['$gte' => 4.0, '$lte' => 5.0]
])->toArray();
echo "范围查询结果数量: " . count($rangeQuery) . "\n";

// 大于查询
$greaterThan = $collection->find([
    'price' => ['$gt' => 50.0]
])->toArray();
echo "大于查询结果数量: " . count($greaterThan) . "\n";

// 4. 浮点数运算
// 使用$inc操作符增加数值
$collection->updateOne(
    ['rating' => 4.5],
    ['$inc' => ['rating' => 0.1]]
);
echo "评分已增加\n";

// 运行结果展示:
// 插入成功,文档ID: 507f1f77bcf86cd799439011
// 精确匹配结果: {"_id":"507f1f77bcf86cd799439011","price":99.99,...}
// 范围查询结果数量: 1
// 大于查询结果数量: 1
// 评分已增加
?>

常见改法对比

php
<?php
// 错误示例:浮点数精度问题
$price1 = 0.1;
$price2 = 0.2;
$total = $price1 + $price2;  // 错误:结果可能是0.30000000000000004
$collection->insertOne(['total' => $total]);

// 正确示例:使用Decimal128处理精确计算
$price1 = new MongoDB\BSON\Decimal128('0.1');
$price2 = new MongoDB\BSON\Decimal128('0.2');
$total = bcadd('0.1', '0.2', 2);  // 正确:结果为0.30
$collection->insertOne(['total' => new MongoDB\BSON\Decimal128($total)]);

// 错误示例:直接比较浮点数
if ($total == 0.3) {  // 错误:可能不相等
    echo "相等\n";
}

// 正确示例:使用精度范围比较
$epsilon = 0.0001;
if (abs($total - 0.3) < $epsilon) {  // 正确:考虑精度误差
    echo "相等\n";
}
?>

Double类型规范

1. 使用场景规范

  • 适用于不需要精确计算的场景
  • 科学计算、统计分析
  • 地理坐标存储
  • 不适用于货币计算

2. 存储规范

  • 统一使用浮点数类型
  • 避免整数和浮点数混用
  • 注意特殊值处理

3. 查询规范

  • 为常用查询字段创建索引
  • 使用范围查询优化性能
  • 注意精度比较问题

原理深度解析

Double存储机制

MongoDB使用BSON格式存储Double,遵循IEEE 754标准:

存储结构

[类型标识(1字节)] + [符号位(1位) + 指数位(11位) + 尾数位(52位)]

IEEE 754表示

  • 符号位:1位,表示正负
  • 指数位:11位,表示指数
  • 尾数位:52位,表示有效数字
php
<?php
// 场景说明:深入分析Double的存储机制和精度特性

class DoubleStorageAnalyzer {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    // 分析浮点数精度
    public function analyzePrecision() {
        $testCases = [
            'simple_decimal' => 0.1,
            'repeating' => 1/3,
            'large_number' => 1e20,
            'small_number' => 1e-20,
            'negative' => -123.456,
            'zero' => 0.0,
            'infinity' => INF,
            'nan' => NAN
        ];
        
        $analysis = [];
        foreach ($testCases as $name => $value) {
            $analysis[$name] = [
                'value' => $value,
                'type' => gettype($value),
                'is_finite' => is_finite($value),
                'is_infinite' => is_infinite($value),
                'is_nan' => is_nan($value),
                'precision' => $this->getPrecision($value)
            ];
        }
        
        // 关键行注释:插入分析结果到数据库
        $collection = $this->database->selectCollection('double_analysis');
        $collection->insertOne([
            'analysis' => $analysis,
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ]);
        
        return $analysis;
    }
    
    // 获取精度信息
    private function getPrecision($value) {
        if (!is_finite($value)) {
            return 'N/A';
        }
        
        $str = (string)$value;
        $parts = explode('.', $str);
        
        if (count($parts) === 2) {
            return strlen($parts[1]);
        }
        
        return 0;
    }
    
    // 演示精度问题
    public function demonstratePrecisionIssues() {
        $issues = [];
        
        // 问题1:0.1 + 0.2 ≠ 0.3
        $sum = 0.1 + 0.2;
        $issues['addition_issue'] = [
            'expected' => 0.3,
            'actual' => $sum,
            'difference' => abs($sum - 0.3),
            'equal' => $sum === 0.3
        ];
        
        // 问题2:浮点数累加误差
        $total = 0.0;
        for ($i = 0; $i < 10; $i++) {
            $total += 0.1;
        }
        $issues['accumulation_issue'] = [
            'expected' => 1.0,
            'actual' => $total,
            'difference' => abs($total - 1.0)
        ];
        
        // 问题3:大数精度丢失
        $large = 1e16;
        $result = $large + 1;
        $issues['large_number_issue'] = [
            'large' => $large,
            'plus_one' => $result,
            'equal' => $large === $result
        ];
        
        return $issues;
    }
    
    // 比较不同存储方式
    public function compareStorageMethods() {
        $collection = $this->database->selectCollection('storage_comparison');
        
        $value = 99.99;
        
        // 方法1:直接存储Double
        $collection->insertOne([
            'method' => 'double',
            'value' => $value,
            'storage_bytes' => 8
        ]);
        
        // 方法2:存储为整数(分)
        $collection->insertOne([
            'method' => 'integer_cents',
            'value' => (int)($value * 100),
            'original' => $value,
            'storage_bytes' => 4
        ]);
        
        // 方法3:存储为Decimal128
        $collection->insertOne([
            'method' => 'decimal128',
            'value' => new MongoDB\BSON\Decimal128((string)$value),
            'storage_bytes' => 16
        ]);
        
        // 方法4:存储为字符串
        $collection->insertOne([
            'method' => 'string',
            'value' => (string)$value,
            'storage_bytes' => strlen((string)$value)
        ]);
        
        return $collection->find()->toArray();
    }
}

// 使用示例
$analyzer = new DoubleStorageAnalyzer('testdb');

$precision = $analyzer->analyzePrecision();
print_r($precision);

$issues = $analyzer->demonstratePrecisionIssues();
print_r($issues);

$comparison = $analyzer->compareStorageMethods();
print_r($comparison);
?>

浮点数运算原理

php
<?php
// 场景说明:演示浮点数的运算机制和注意事项

class DoubleOperations {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    // 安全的浮点数比较
    public function safeCompare($a, $b, $epsilon = 1e-10) {
        // 关键行注释:使用epsilon进行安全的浮点数比较
        return abs($a - $b) < $epsilon;
    }
    
    // 精确的浮点数运算
    public function preciseOperation($a, $b, $operation) {
        switch ($operation) {
            case 'add':
                return bcadd((string)$a, (string)$b, 10);
            case 'sub':
                return bcsub((string)$a, (string)$b, 10);
            case 'mul':
                return bcmul((string)$a, (string)$b, 10);
            case 'div':
                return bcdiv((string)$a, (string)$b, 10);
            default:
                throw new Exception("未知操作");
        }
    }
    
    // 聚合统计
    public function aggregateStatistics($collectionName, $field) {
        $collection = $this->database->selectCollection($collectionName);
        
        $pipeline = [
            [
                '$group' => [
                    '_id' => null,
                    'total' => ['$sum' => '$' . $field],
                    'average' => ['$avg' => '$' . $field],
                    'max' => ['$max' => '$' . $field],
                    'min' => ['$min' => '$' . $field],
                    'stdDev' => ['$stdDevPop' => '$' . $field]
                ]
            ]
        ];
        
        return $collection->aggregate($pipeline)->toArray();
    }
    
    // 地理坐标处理
    public function handleGeoCoordinates() {
        $collection = $this->database->selectCollection('locations');
        
        // 创建地理空间索引
        $collection->createIndex(['location' => '2dsphere']);
        
        // 插入位置数据
        $collection->insertOne([
            'name' => 'Beijing',
            'location' => [
                'type' => 'Point',
                'coordinates' => [116.404, 39.915]  // [经度, 纬度]
            ]
        ]);
        
        // 查询附近的位置
        $nearby = $collection->find([
            'location' => [
                '$near' => [
                    '$geometry' => [
                        'type' => 'Point',
                        'coordinates' => [116.404, 39.915]
                    ],
                    '$maxDistance' => 10000  // 10公里
                ]
            ]
        ])->toArray();
        
        return $nearby;
    }
}

// 使用示例
$ops = new DoubleOperations('testdb');

// 安全比较
$equal = $ops->safeCompare(0.1 + 0.2, 0.3);
echo "0.1 + 0.2 是否等于 0.3: " . ($equal ? '是' : '否') . "\n";

// 精确运算
$sum = $ops->preciseOperation('0.1', '0.2', 'add');
echo "精确加法: 0.1 + 0.2 = $sum\n";
?>

常见错误与踩坑点

错误1:浮点数精度丢失

错误表现

  • 0.1 + 0.2 ≠ 0.3
  • 累加结果不准确
  • 比较操作失败

产生原因

  • 二进制浮点数表示限制
  • 无法精确表示某些十进制小数
  • 累加误差放大

解决方案

php
<?php
// 场景说明:演示浮点数精度问题的正确处理

class PrecisionHandler {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    // 错误示例:直接比较
    public function wrongComparison() {
        $a = 0.1 + 0.2;
        $b = 0.3;
        
        if ($a == $b) {
            echo "相等\n";
        } else {
            echo "不相等: $a != $b\n";
        }
    }
    
    // 正确示例:使用epsilon比较
    public function correctComparison() {
        $a = 0.1 + 0.2;
        $b = 0.3;
        $epsilon = 1e-10;
        
        // 关键行注释:使用epsilon进行安全的浮点数比较
        if (abs($a - $b) < $epsilon) {
            echo "相等(考虑精度误差)\n";
        } else {
            echo "不相等\n";
        }
    }
    
    // 错误示例:货币计算
    public function wrongMonetaryCalculation() {
        $collection = $this->database->selectCollection('wrong_monetary');
        
        $price = 19.99;
        $quantity = 3;
        $total = $price * $quantity;  // 错误:可能不等于59.97
        
        $collection->insertOne([
            'price' => $price,
            'quantity' => $quantity,
            'total' => $total,
            'expected' => 59.97,
            'error' => '精度丢失'
        ]);
        
        echo "错误:$price * $quantity = $total (期望: 59.97)\n";
    }
    
    // 正确示例:使用整数或Decimal128
    public function correctMonetaryCalculation() {
        $collection = $this->database->selectCollection('correct_monetary');
        
        // 方法1:使用整数(分)
        $priceCents = 1999;  // 19.99元 = 1999分
        $quantity = 3;
        $totalCents = $priceCents * $quantity;  // 5997分
        
        $collection->insertOne([
            'method' => 'integer_cents',
            'price_cents' => $priceCents,
            'quantity' => $quantity,
            'total_cents' => $totalCents,
            'total_yuan' => $totalCents / 100
        ]);
        
        // 方法2:使用Decimal128
        $price = new MongoDB\BSON\Decimal128('19.99');
        $quantity = 3;
        $total = bcmul('19.99', '3', 2);
        
        $collection->insertOne([
            'method' => 'decimal128',
            'price' => $price,
            'quantity' => $quantity,
            'total' => new MongoDB\BSON\Decimal128($total)
        ]);
        
        echo "正确:使用整数或Decimal128进行货币计算\n";
    }
    
    // 错误示例:浮点数累加
    public function wrongAccumulation() {
        $total = 0.0;
        for ($i = 0; $i < 10; $i++) {
            $total += 0.1;
        }
        
        echo "错误:累加结果 = $total (期望: 1.0)\n";
    }
    
    // 正确示例:使用Kahan算法
    public function correctAccumulation() {
        $total = 0.0;
        $compensation = 0.0;
        
        for ($i = 0; $i < 10; $i++) {
            $y = 0.1 - $compensation;
            $t = $total + $y;
            $compensation = ($t - $total) - $y;
            $total = $t;
        }
        
        echo "正确:Kahan算法累加结果 = $total\n";
    }
}

// 使用示例
$handler = new PrecisionHandler('testdb');
$handler->wrongComparison();
$handler->correctComparison();
$handler->wrongMonetaryCalculation();
$handler->correctMonetaryCalculation();
$handler->wrongAccumulation();
$handler->correctAccumulation();
?>

错误2:特殊值处理不当

错误表现

  • NaN导致查询失败
  • Infinity引起计算错误
  • 未正确处理特殊值

产生原因

  • 未检查特殊值
  • 缺少异常处理
  • 不了解特殊值特性

解决方案

php
<?php
// 场景说明:演示特殊值的正确处理

class SpecialValueHandler {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    // 错误示例:未处理特殊值
    public function wrongSpecialValueHandling() {
        $collection = $this->database->selectCollection('wrong_special');
        
        // 错误:直接插入NaN和Infinity
        $collection->insertOne([
            'value' => NAN,
            'description' => 'NaN value'
        ]);
        
        $collection->insertOne([
            'value' => INF,
            'description' => 'Infinity'
        ]);
        
        // 查询NaN会失败
        $result = $collection->findOne(['value' => NAN]);
        if (!$result) {
            echo "错误:无法查询到NaN值\n";
        }
    }
    
    // 正确示例:正确处理特殊值
    public function correctSpecialValueHandling() {
        $collection = $this->database->selectCollection('correct_special');
        
        // 检查并处理特殊值
        $values = [1.5, NAN, INF, -INF, 0.0];
        
        foreach ($values as $value) {
            $document = [
                'original_value' => $value,
                'is_nan' => is_nan($value),
                'is_infinite' => is_infinite($value),
                'is_finite' => is_finite($value),
                'type' => $this->classifyValue($value)
            ];
            
            $collection->insertOne($document);
        }
        
        // 查询特殊值
        $nanDocs = $collection->find(['is_nan' => true])->toArray();
        $infDocs = $collection->find(['is_infinite' => true])->toArray();
        
        echo "NaN文档数量: " . count($nanDocs) . "\n";
        echo "Infinity文档数量: " . count($infDocs) . "\n";
    }
    
    // 分类值类型
    private function classifyValue($value) {
        if (is_nan($value)) return 'NaN';
        if (is_infinite($value)) return $value > 0 ? 'PositiveInfinity' : 'NegativeInfinity';
        if ($value === 0.0) return 'Zero';
        return 'Normal';
    }
    
    // 安全的数学运算
    public function safeMathOperation($a, $b, $operation) {
        // 检查特殊值
        if (!is_finite($a) || !is_finite($b)) {
            return [
                'success' => false,
                'error' => '输入包含特殊值',
                'a_type' => $this->classifyValue($a),
                'b_type' => $this->classifyValue($b)
            ];
        }
        
        $result = null;
        switch ($operation) {
            case 'add':
                $result = $a + $b;
                break;
            case 'sub':
                $result = $a - $b;
                break;
            case 'mul':
                $result = $a * $b;
                break;
            case 'div':
                if ($b == 0) {
                    return [
                        'success' => false,
                        'error' => '除零错误'
                    ];
                }
                $result = $a / $b;
                break;
        }
        
        // 检查结果是否有效
        if (!is_finite($result)) {
            return [
                'success' => false,
                'error' => '结果为特殊值',
                'result_type' => $this->classifyValue($result)
            ];
        }
        
        return [
            'success' => true,
            'result' => $result
        ];
    }
}

// 使用示例
$handler = new SpecialValueHandler('testdb');
$handler->wrongSpecialValueHandling();
$handler->correctSpecialValueHandling();

$safe = $handler->safeMathOperation(1.5, 0.0, 'div');
print_r($safe);
?>

错误3:类型混淆

错误表现

  • 整数和浮点数混淆
  • 查询不到预期结果
  • 类型比较错误

产生原因

  • 未进行类型转换
  • 数据来源不一致
  • 缺少类型验证

解决方案

php
<?php
// 场景说明:演示浮点数类型混淆的正确处理

class TypeConfusionHandler {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    // 错误示例:类型混淆
    public function wrongTypeExample() {
        $collection = $this->database->selectCollection('wrong_type');
        
        // 插入整数
        $collection->insertOne(['value' => 100]);
        
        // 错误:使用浮点数查询整数
        $result = $collection->findOne(['value' => 100.0]);
        
        if ($result) {
            echo "找到结果\n";
        } else {
            echo "错误:类型不匹配导致查询失败\n";
        }
    }
    
    // 正确示例:统一类型
    public function correctTypeExample() {
        $collection = $this->database->selectCollection('correct_type');
        
        // 统一使用浮点数
        $collection->insertOne(['value' => 100.0]);
        
        // 使用相同类型查询
        $result = $collection->findOne(['value' => 100.0]);
        
        if ($result) {
            echo "正确:类型匹配查询成功\n";
        }
    }
    
    // 类型转换工具
    public function ensureFloat($value) {
        if (is_int($value)) {
            return (float)$value;
        } elseif (is_string($value)) {
            return (float)$value;
        } elseif (is_bool($value)) {
            return $value ? 1.0 : 0.0;
        } elseif (is_null($value)) {
            return 0.0;
        }
        
        return (float)$value;
    }
    
    // 类型检查
    public function checkType($value) {
        return [
            'value' => $value,
            'php_type' => gettype($value),
            'is_float' => is_float($value),
            'is_int' => is_int($value),
            'is_numeric' => is_numeric($value)
        ];
    }
}

// 使用示例
$handler = new TypeConfusionHandler('testdb');
$handler->wrongTypeExample();
$handler->correctTypeExample();

$floatValue = $handler->ensureFloat('123.45');
print_r($handler->checkType($floatValue));
?>

常见应用场景

场景1:商品价格系统

场景描述:实现商品价格的存储和计算。

使用方法

  • 使用整数存储(分为单位)
  • 或使用Decimal128
  • 避免使用Double

示例代码

php
<?php
// 场景说明:商品价格系统实现

class ProductPriceSystem {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    // 创建商品(使用整数存储价格)
    public function createProduct($name, $priceYuan) {
        $collection = $this->database->selectCollection('products');
        
        // 关键行注释:将价格转换为分存储,避免浮点数精度问题
        $priceCents = (int)($priceYuan * 100);
        
        $product = [
            'name' => $name,
            'price_cents' => $priceCents,
            'price_yuan' => $priceYuan,
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $collection->insertOne($product);
        
        return (string)$result->getInsertedId();
    }
    
    // 计算总价
    public function calculateTotal($productId, $quantity) {
        $collection = $this->database->selectCollection('products');
        
        $product = $collection->findOne(['_id' => new MongoDB\BSON\ObjectId($productId)]);
        
        if (!$product) {
            throw new Exception("商品不存在");
        }
        
        // 使用整数计算,避免精度问题
        $totalCents = $product['price_cents'] * $quantity;
        $totalYuan = $totalCents / 100;
        
        return [
            'product_name' => $product['name'],
            'unit_price' => $product['price_yuan'],
            'quantity' => $quantity,
            'total_cents' => $totalCents,
            'total_yuan' => $totalYuan
        ];
    }
    
    // 应用折扣
    public function applyDiscount($productId, $discountPercent) {
        $collection = $this->database->selectCollection('products');
        
        $product = $collection->findOne(['_id' => new MongoDB\BSON\ObjectId($productId)]);
        
        if (!$product) {
            throw new Exception("商品不存在");
        }
        
        // 计算折扣价格
        $originalCents = $product['price_cents'];
        $discountCents = (int)($originalCents * $discountPercent / 100);
        $finalCents = $originalCents - $discountCents;
        
        $collection->updateOne(
            ['_id' => new MongoDB\BSON\ObjectId($productId)],
            [
                '$set' => [
                    'discounted_price_cents' => $finalCents,
                    'discount_percent' => $discountPercent
                ]
            ]
        );
        
        return [
            'original_price' => $originalCents / 100,
            'discount_percent' => $discountPercent,
            'final_price' => $finalCents / 100
        ];
    }
}

// 使用示例
$priceSystem = new ProductPriceSystem('price_db');

// 创建商品
$productId = $priceSystem->createProduct('iPhone 15', 6999.99);
echo "商品ID: $productId\n";

// 计算总价
$total = $priceSystem->calculateTotal($productId, 3);
print_r($total);

// 应用折扣
$discount = $priceSystem->applyDiscount($productId, 10);
print_r($discount);
?>

场景2:评分系统

场景描述:实现产品评分和统计功能。

示例代码

php
<?php
// 场景说明:评分系统实现

class RatingSystem {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    // 添加评分
    public function addRating($productId, $userId, $rating) {
        $collection = $this->database->selectCollection('ratings');
        
        // 验证评分范围
        if ($rating < 0.0 || $rating > 5.0) {
            throw new Exception("评分必须在0-5之间");
        }
        
        $collection->insertOne([
            'product_id' => $productId,
            'user_id' => $userId,
            'rating' => (float)$rating,
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ]);
    }
    
    // 计算平均评分
    public function calculateAverageRating($productId) {
        $collection = $this->database->selectCollection('ratings');
        
        $pipeline = [
            [
                '$match' => ['product_id' => $productId]
            ],
            [
                '$group' => [
                    '_id' => '$product_id',
                    'average_rating' => ['$avg' => '$rating'],
                    'total_ratings' => ['$sum' => 1],
                    'rating_distribution' => [
                        '$push' => '$rating'
                    ]
                ]
            ]
        ];
        
        $result = $collection->aggregate($pipeline)->toArray();
        
        if (empty($result)) {
            return [
                'product_id' => $productId,
                'average_rating' => 0.0,
                'total_ratings' => 0
            ];
        }
        
        $data = $result[0];
        
        // 四舍五入到小数点后一位
        $averageRating = round($data['average_rating'], 1);
        
        return [
            'product_id' => $productId,
            'average_rating' => $averageRating,
            'total_ratings' => $data['total_ratings'],
            'distribution' => $this->calculateDistribution($data['rating_distribution'])
        ];
    }
    
    // 计算评分分布
    private function calculateDistribution($ratings) {
        $distribution = [
            '5_star' => 0,
            '4_star' => 0,
            '3_star' => 0,
            '2_star' => 0,
            '1_star' => 0
        ];
        
        foreach ($ratings as $rating) {
            if ($rating >= 4.5) $distribution['5_star']++;
            elseif ($rating >= 3.5) $distribution['4_star']++;
            elseif ($rating >= 2.5) $distribution['3_star']++;
            elseif ($rating >= 1.5) $distribution['2_star']++;
            else $distribution['1_star']++;
        }
        
        return $distribution;
    }
}

// 使用示例
$ratingSystem = new RatingSystem('rating_db');

// 添加评分
$ratingSystem->addRating('PROD001', 'USER001', 4.5);
$ratingSystem->addRating('PROD001', 'USER002', 5.0);
$ratingSystem->addRating('PROD001', 'USER003', 4.0);

// 计算平均评分
$stats = $ratingSystem->calculateAverageRating('PROD001');
print_r($stats);
?>

场景3:地理坐标系统

场景描述:存储和查询地理位置信息。

示例代码

php
<?php
// 场景说明:地理坐标系统实现

class GeoLocationSystem {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    // 初始化地理索引
    public function initializeGeoIndex() {
        $collection = $this->database->selectCollection('locations');
        
        // 关键行注释:创建2dsphere索引支持地理空间查询
        $collection->createIndex(['location' => '2dsphere']);
    }
    
    // 添加位置
    public function addLocation($name, $longitude, $latitude, $description = '') {
        $collection = $this->database->selectCollection('locations');
        
        $document = [
            'name' => $name,
            'location' => [
                'type' => 'Point',
                'coordinates' => [(float)$longitude, (float)$latitude]
            ],
            'description' => $description,
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $result = $collection->insertOne($document);
        
        return (string)$result->getInsertedId();
    }
    
    // 查询附近位置
    public function findNearby($longitude, $latitude, $maxDistance = 1000) {
        $collection = $this->database->selectCollection('locations');
        
        $query = [
            'location' => [
                '$near' => [
                    '$geometry' => [
                        'type' => 'Point',
                        'coordinates' => [(float)$longitude, (float)$latitude]
                    ],
                    '$maxDistance' => $maxDistance
                ]
            ]
        ];
        
        return $collection->find($query)->toArray();
    }
    
    // 查询范围内的位置
    public function findWithinPolygon($polygon) {
        $collection = $this->database->selectCollection('locations');
        
        $query = [
            'location' => [
                '$geoWithin' => [
                    '$polygon' => $polygon
                ]
            ]
        ];
        
        return $collection->find($query)->toArray();
    }
    
    // 计算两点距离
    public function calculateDistance($lon1, $lat1, $lon2, $lat2) {
        // 使用Haversine公式计算球面距离
        $earthRadius = 6371; // 地球半径(公里)
        
        $dLat = deg2rad($lat2 - $lat1);
        $dLon = deg2rad($lon2 - $lon1);
        
        $a = sin($dLat / 2) * sin($dLat / 2) +
             cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
             sin($dLon / 2) * sin($dLon / 2);
        
        $c = 2 * atan2(sqrt($a), sqrt(1 - $a));
        
        $distance = $earthRadius * $c;
        
        return round($distance, 2);
    }
}

// 使用示例
$geoSystem = new GeoLocationSystem('geo_db');
$geoSystem->initializeGeoIndex();

// 添加位置
$geoSystem->addLocation('北京天安门', 116.397128, 39.916527, '中国北京');
$geoSystem->addLocation('北京故宫', 116.397026, 39.918058, '中国北京');
$geoSystem->addLocation('北京王府井', 116.410889, 39.915599, '中国北京');

// 查询附近位置
$nearby = $geoSystem->findNearby(116.397128, 39.916527, 2000);
echo "附近位置数量: " . count($nearby) . "\n";

// 计算距离
$distance = $geoSystem->calculateDistance(
    116.397128, 39.916527,  // 天安门
    116.397026, 39.918058   // 故宫
);
echo "天安门到故宫距离: {$distance} 公里\n";
?>

企业级进阶应用场景

场景1:科学数据计算

场景描述:处理科学实验数据和统计分析。

示例代码

php
<?php
// 场景说明:科学数据计算系统

class ScientificDataSystem {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    // 存储实验数据
    public function storeExperimentData($experimentId, $measurements) {
        $collection = $this->database->selectCollection('experiments');
        
        $document = [
            'experiment_id' => $experimentId,
            'measurements' => $measurements,
            'statistics' => $this->calculateStatistics($measurements),
            'created_at' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $collection->insertOne($document);
    }
    
    // 计算统计信息
    private function calculateStatistics($data) {
        $count = count($data);
        $sum = array_sum($data);
        $mean = $sum / $count;
        
        // 计算标准差
        $variance = 0.0;
        foreach ($data as $value) {
            $variance += pow($value - $mean, 2);
        }
        $stdDev = sqrt($variance / $count);
        
        // 排序计算中位数
        $sorted = $data;
        sort($sorted);
        $median = $count % 2 === 0 ?
            ($sorted[$count / 2 - 1] + $sorted[$count / 2]) / 2 :
            $sorted[floor($count / 2)];
        
        return [
            'count' => $count,
            'sum' => $sum,
            'mean' => round($mean, 6),
            'median' => round($median, 6),
            'std_dev' => round($stdDev, 6),
            'min' => min($data),
            'max' => max($data)
        ];
    }
    
    // 聚合分析
    public function aggregateAnalysis($experimentIds) {
        $collection = $this->database->selectCollection('experiments');
        
        $pipeline = [
            [
                '$match' => ['experiment_id' => ['$in' => $experimentIds]]
            ],
            [
                '$group' => [
                    '_id' => null,
                    'avg_mean' => ['$avg' => '$statistics.mean'],
                    'avg_std_dev' => ['$avg' => '$statistics.std_dev'],
                    'total_measurements' => ['$sum' => '$statistics.count']
                ]
            ]
        ];
        
        return $collection->aggregate($pipeline)->toArray();
    }
}

// 使用示例
$scienceSystem = new ScientificDataSystem('science_db');

// 存储实验数据
$measurements = [1.234, 1.235, 1.233, 1.236, 1.234, 1.235, 1.237, 1.234, 1.235, 1.236];
$scienceSystem->storeExperimentData('EXP001', $measurements);

// 聚合分析
$analysis = $scienceSystem->aggregateAnalysis(['EXP001']);
print_r($analysis);
?>

场景2:传感器数据监控

场景描述:实时监控传感器数据。

示例代码

php
<?php
// 场景说明:传感器数据监控系统

class SensorMonitoringSystem {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    // 记录传感器数据
    public function recordSensorData($sensorId, $value, $unit) {
        $collection = $this->database->selectCollection('sensor_data');
        
        $document = [
            'sensor_id' => $sensorId,
            'value' => (float)$value,
            'unit' => $unit,
            'timestamp' => new MongoDB\BSON\UTCDateTime()
        ];
        
        $collection->insertOne($document);
    }
    
    // 查询时间范围内的数据
    public function queryTimeRange($sensorId, $startTime, $endTime) {
        $collection = $this->database->selectCollection('sensor_data');
        
        return $collection->find([
            'sensor_id' => $sensorId,
            'timestamp' => [
                '$gte' => $startTime,
                '$lte' => $endTime
            ]
        ], [
            'sort' => ['timestamp' => 1]
        ])->toArray();
    }
    
    // 计算移动平均
    public function calculateMovingAverage($sensorId, $windowSize = 10) {
        $collection = $this->database->selectCollection('sensor_data');
        
        $data = $collection->find([
            'sensor_id' => $sensorId
        ], [
            'sort' => ['timestamp' => -1],
            'limit' => $windowSize
        ])->toArray();
        
        if (count($data) < $windowSize) {
            return null;
        }
        
        $sum = 0.0;
        foreach ($data as $doc) {
            $sum += $doc['value'];
        }
        
        return $sum / $windowSize;
    }
    
    // 异常检测
    public function detectAnomalies($sensorId, $threshold) {
        $collection = $this->database->selectCollection('sensor_data');
        
        return $collection->find([
            'sensor_id' => $sensorId,
            '$or' => [
                ['value' => ['$gt' => $threshold['max']]],
                ['value' => ['$lt' => $threshold['min']]]
            ]
        ])->toArray();
    }
}

// 使用示例
$sensorSystem = new SensorMonitoringSystem('sensor_db');

// 记录传感器数据
for ($i = 0; $i < 20; $i++) {
    $temperature = 20.0 + rand(-50, 50) / 10.0;
    $sensorSystem->recordSensorData('TEMP001', $temperature, '°C');
}

// 计算移动平均
$movingAvg = $sensorSystem->calculateMovingAverage('TEMP001', 10);
echo "移动平均温度: {$movingAvg}°C\n";

// 异常检测
$anomalies = $sensorSystem->detectAnomalies('TEMP001', ['min' => 15.0, 'max' => 25.0]);
echo "异常数据数量: " . count($anomalies) . "\n";
?>

行业最佳实践

实践1:选择合适的数据类型

实践内容

  • 货币计算使用Decimal128或整数
  • 科学计算使用Double
  • 地理坐标使用Double

推荐理由

  • 避免精度问题
  • 提高计算准确性
  • 优化存储空间
php
<?php
// 好的实践:根据场景选择数据类型
class DataTypeSelector {
    public static function selectForMoney($amount) {
        // 使用Decimal128处理货币
        return new MongoDB\BSON\Decimal128((string)$amount);
    }
    
    public static function selectForScience($value) {
        // 使用Double处理科学数据
        return (float)$value;
    }
    
    public static function selectForGeo($coordinate) {
        // 使用Double处理地理坐标
        return (float)$coordinate;
    }
}
?>

实践2:使用epsilon比较

实践内容

  • 不直接比较浮点数
  • 使用epsilon进行范围比较
  • 根据场景选择合适的epsilon

推荐理由

  • 避免精度问题
  • 提高比较准确性
  • 减少错误
php
<?php
// 好的实践:使用epsilon比较
class FloatComparator {
    const DEFAULT_EPSILON = 1e-10;
    
    public static function equal($a, $b, $epsilon = self::DEFAULT_EPSILON) {
        return abs($a - $b) < $epsilon;
    }
    
    public static function greaterThan($a, $b, $epsilon = self::DEFAULT_EPSILON) {
        return $a - $b > $epsilon;
    }
    
    public static function lessThan($a, $b, $epsilon = self::DEFAULT_EPSILON) {
        return $b - $a > $epsilon;
    }
}

// 使用示例
$a = 0.1 + 0.2;
$b = 0.3;

if (FloatComparator::equal($a, $b)) {
    echo "相等\n";
}
?>

实践3:创建合适的索引

实践内容

  • 为常用查询字段创建索引
  • 使用复合索引优化多字段查询
  • 为地理坐标创建2dsphere索引

推荐理由

  • 提高查询性能
  • 减少资源消耗
  • 优化用户体验
php
<?php
// 好的实践:创建索引
$collection->createIndex(['price' => 1]);
$collection->createIndex(['rating' => -1]);
$collection->createIndex(['location' => '2dsphere']);
?>

常见问题答疑(FAQ)

问题1:为什么0.1 + 0.2不等于0.3?

问题描述:在编程中,0.1 + 0.2的结果不等于0.3,这是为什么?

回答内容

这是由于浮点数的二进制表示限制导致的:

php
<?php
// 场景说明:解释浮点数精度问题

$a = 0.1;
$b = 0.2;
$sum = $a + $b;

echo "0.1 + 0.2 = " . $sum . "\n";
echo "期望结果: 0.3\n";
echo "实际结果: $sum\n";
echo "差值: " . ($sum - 0.3) . "\n";

// 原因:二进制表示
echo "\n二进制表示:\n";
echo "0.1: " . decbin(unpack('Q', pack('d', 0.1))[1]) . "\n";
echo "0.2: " . decbin(unpack('Q', pack('d', 0.2))[1]) . "\n";
echo "0.3: " . decbin(unpack('Q', pack('d', 0.3))[1]) . "\n";

// 解决方案:使用epsilon比较
$epsilon = 1e-10;
if (abs($sum - 0.3) < $epsilon) {
    echo "\n使用epsilon比较: 相等\n";
}

// 或使用Decimal128
$precise = bcadd('0.1', '0.2', 2);
echo "使用Decimal128: 0.1 + 0.2 = $precise\n";
?>

问题2:如何正确处理货币计算?

问题描述:在电商系统中如何正确处理价格计算?

回答内容

使用整数或Decimal128处理货币:

php
<?php
// 场景说明:货币计算的正确方法

class MoneyCalculator {
    // 方法1:使用整数(分为单位)
    public static function calculateWithInt($priceYuan, $quantity) {
        $priceCents = (int)($priceYuan * 100);
        $totalCents = $priceCents * $quantity;
        
        return [
            'total_cents' => $totalCents,
            'total_yuan' => $totalCents / 100
        ];
    }
    
    // 方法2:使用Decimal128
    public static function calculateWithDecimal($price, $quantity) {
        $total = bcmul((string)$price, (string)$quantity, 2);
        
        return new MongoDB\BSON\Decimal128($total);
    }
    
    // 方法3:使用bcmath函数
    public static function calculateWithBcmath($price, $quantity) {
        return bcmul($price, $quantity, 2);
    }
}

// 使用示例
$price = '19.99';
$quantity = 3;

$result1 = MoneyCalculator::calculateWithInt(19.99, 3);
echo "整数方法: {$result1['total_yuan']} 元\n";

$result2 = MoneyCalculator::calculateWithDecimal($price, $quantity);
echo "Decimal128方法: {$result2} 元\n";

$result3 = MoneyCalculator::calculateWithBcmath($price, $quantity);
echo "bcmath方法: {$result3} 元\n";
?>

问题3:如何处理特殊值(NaN、Infinity)?

问题描述:如何正确处理浮点数的特殊值?

回答内容

正确识别和处理特殊值:

php
<?php
// 场景说明:特殊值处理

class SpecialValueProcessor {
    // 检查并分类值
    public static function classify($value) {
        if (is_nan($value)) {
            return 'NaN';
        }
        
        if (is_infinite($value)) {
            return $value > 0 ? 'PositiveInfinity' : 'NegativeInfinity';
        }
        
        if ($value === 0.0) {
            return 'Zero';
        }
        
        return 'Normal';
    }
    
    // 安全的数学运算
    public static function safeDivide($a, $b) {
        if ($b == 0) {
            if ($a > 0) return INF;
            if ($a < 0) return -INF;
            return NAN;
        }
        
        return $a / $b;
    }
    
    // 过滤特殊值
    public static function filterSpecialValues($array) {
        return array_filter($array, function($value) {
            return is_finite($value);
        });
    }
}

// 使用示例
$values = [1.5, NAN, INF, -INF, 0.0, 2.5];

foreach ($values as $value) {
    echo "值: $value, 类型: " . SpecialValueProcessor::classify($value) . "\n";
}

$filtered = SpecialValueProcessor::filterSpecialValues($values);
echo "过滤后的值: " . implode(', ', $filtered) . "\n";
?>

实战练习

练习1:基础练习 - 温度转换

解题思路

  1. 存储温度数据
  2. 实现温度转换
  3. 计算统计信息

常见误区

  • 未处理精度问题
  • 未验证输入范围
  • 未处理特殊值

参考代码

php
<?php
// 练习1:温度转换系统

class TemperatureConverter {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    // 摄氏度转华氏度
    public function celsiusToFahrenheit($celsius) {
        return $celsius * 9 / 5 + 32;
    }
    
    // 华氏度转摄氏度
    public function fahrenheitToCelsius($fahrenheit) {
        return ($fahrenheit - 32) * 5 / 9;
    }
    
    // 存储温度数据
    public function storeTemperature($location, $celsius) {
        $collection = $this->database->selectCollection('temperatures');
        
        $fahrenheit = $this->celsiusToFahrenheit($celsius);
        
        $collection->insertOne([
            'location' => $location,
            'celsius' => (float)$celsius,
            'fahrenheit' => (float)$fahrenheit,
            'timestamp' => new MongoDB\BSON\UTCDateTime()
        ]);
    }
    
    // 查询温度统计
    public function getStatistics($location) {
        $collection = $this->database->selectCollection('temperatures');
        
        $pipeline = [
            ['$match' => ['location' => $location]],
            [
                '$group' => [
                    '_id' => '$location',
                    'avg_celsius' => ['$avg' => '$celsius'],
                    'max_celsius' => ['$max' => '$celsius'],
                    'min_celsius' => ['$min' => '$celsius']
                ]
            ]
        ];
        
        return $collection->aggregate($pipeline)->toArray();
    }
}

// 运行练习
$converter = new TemperatureConverter('temp_db');

// 存储温度
$converter->storeTemperature('Beijing', 25.5);
$converter->storeTemperature('Beijing', 26.3);
$converter->storeTemperature('Beijing', 24.8);

// 查询统计
$stats = $converter->getStatistics('Beijing');
print_r($stats);
?>

练习2:进阶练习 - 商品折扣计算

解题思路

  1. 设计价格存储结构
  2. 实现折扣计算
  3. 处理精度问题

参考代码

php
<?php
// 练习2:商品折扣计算

class DiscountCalculator {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    // 应用百分比折扣
    public function applyPercentageDiscount($productId, $percentage) {
        $collection = $this->database->selectCollection('products');
        
        $product = $collection->findOne(['_id' => new MongoDB\BSON\ObjectId($productId)]);
        
        $originalPrice = $product['price_cents'];
        $discountAmount = (int)($originalPrice * $percentage / 100);
        $finalPrice = $originalPrice - $discountAmount;
        
        $collection->updateOne(
            ['_id' => new MongoDB\BSON\ObjectId($productId)],
            [
                '$set' => [
                    'discounted_price_cents' => $finalPrice,
                    'discount_percentage' => $percentage
                ]
            ]
        );
        
        return [
            'original_price' => $originalPrice / 100,
            'discount_percentage' => $percentage,
            'final_price' => $finalPrice / 100,
            'saved_amount' => $discountAmount / 100
        ];
    }
}

// 运行练习
$calculator = new DiscountCalculator('shop_db');

// 创建商品
$collection = $calculator->database->selectCollection('products');
$result = $collection->insertOne([
    'name' => 'iPhone 15',
    'price_cents' => 699999
]);

$productId = (string)$result->getInsertedId();

// 应用折扣
$discount = $calculator->applyPercentageDiscount($productId, 15);
print_r($discount);
?>

练习3:挑战练习 - 科学数据分析

解题思路

  1. 存储实验数据
  2. 计算统计指标
  3. 实现异常检测

参考代码

php
<?php
// 练习3:科学数据分析

class DataAnalyzer {
    private $database;
    
    public function __construct($databaseName) {
        $client = new MongoDB\Client("mongodb://localhost:27017");
        $this->database = $client->selectDatabase($databaseName);
    }
    
    // 分析数据
    public function analyze($data) {
        $count = count($data);
        $mean = array_sum($data) / $count;
        
        $variance = 0.0;
        foreach ($data as $value) {
            $variance += pow($value - $mean, 2);
        }
        $stdDev = sqrt($variance / $count);
        
        return [
            'count' => $count,
            'mean' => round($mean, 6),
            'std_dev' => round($stdDev, 6),
            'variance' => round($variance / $count, 6)
        ];
    }
    
    // 异常检测(使用3σ原则)
    public function detectOutliers($data) {
        $stats = $this->analyze($data);
        $mean = $stats['mean'];
        $stdDev = $stats['std_dev'];
        
        $outliers = [];
        foreach ($data as $value) {
            $zScore = abs($value - $mean) / $stdDev;
            if ($zScore > 3) {
                $outliers[] = $value;
            }
        }
        
        return $outliers;
    }
}

// 运行练习
$analyzer = new DataAnalyzer('analysis_db');

$data = [1.2, 1.3, 1.4, 1.3, 1.5, 1.2, 1.4, 10.0];  // 10.0是异常值
$stats = $analyzer->analyze($data);
print_r($stats);

$outliers = $analyzer->detectOutliers($data);
echo "异常值: " . implode(', ', $outliers) . "\n";
?>

知识点总结

核心要点

  1. Double类型特性

    • IEEE 754标准64位浮点数
    • 约15-17位有效数字
    • 支持特殊值:NaN、Infinity
  2. 精度问题

    • 二进制浮点数表示限制
    • 0.1 + 0.2 ≠ 0.3
    • 累加误差放大
  3. 存储机制

    • 8字节存储
    • 符号位 + 指数位 + 尾数位
    • BSON格式存储
  4. 查询优化

    • 支持精确和范围查询
    • 创建索引提高性能
    • 使用epsilon比较

易错点回顾

  1. 精度丢失

    • 直接比较浮点数
    • 货币计算使用Double
    • 未考虑精度误差
  2. 特殊值处理

    • 未检查NaN和Infinity
    • 缺少异常处理
    • 查询特殊值失败
  3. 类型混淆

    • 整数和浮点数混用
    • 未进行类型转换
    • 查询类型不匹配

拓展参考资料

官方文档链接

进阶学习路径建议

本知识点承接:《Integer类型》→《数值类型概述》

后续延伸至:《Decimal128类型》→《数值运算优化》→《科学计算应用》

建议学习顺序

  1. Integer类型
  2. Double类型(本章节)
  3. Decimal128类型
  4. 数值运算优化
  5. 科学计算应用