返回

PHP多维数组添加计算列(points)及SQL优化

php

PHP 多维数组中添加计算列

最近遇到一个需求, 需要给 PHP 从数据库查询出来的多维数组的每一行添加一个计算得来的新列 points 。 简单记录一下解决过程和思路。

问题

原本的 PHP 代码大概是这样:

$children = mysql_query("SELECT c.id, c.name, c.age, c.photoName, c.panelColor FROM children as c");
$temp = array();

while ($child = mysql_fetch_assoc($children)) { 

    // 获取孩子的评分
    $ratings = mysql_query('
        SELECT r.behaviourID, t.points, t.typeName 
        FROM  behaviourRatings as r 
        JOIN  behaviourTypes as t 
        ON    r.behaviourID = t.typeID 
        WHERE r.childID = ' . $child['id']);

    // 循环评分计算总分
     $totalPoints = 0;
     while ($childRatings = mysql_fetch_array($ratings)){
       $totalPoints = ($totalPoints + $childRatings['points']);
     }

     // 分数处理逻辑
     if(($totalPoints + $maxPoints) > $maxPoints) {
        $total = $maxPoints;
     } else if($totalPoints < 0){
        $total = ($maxPoints + $totalPoints);
     }else{
        $total = ($maxPoints - $totalPoints);
     }

     // 设置 child 数组
     $temp[] = $child;
 }

$response = array();
$response['timestamp'] = $currentmodif;
$response['children'] = $temp;
echo json_encode($response, JSON_PRETTY_PRINT);

这段代码从 children 表查询数据, 然后通过循环和计算, 得到每个 childtotalPoints,并经过处理,赋值到 $total 。目标是在 $temp 数组的每个元素(也就是每个 $child)里加一个 points 键, 值就是 $total

之前试过 $temp['points'] = $total; 这种方式, 但这会把 points 放到 $temp 数组的 外面,而不是每个 $child 数组里面。

现在的输出是这样的:

{
    "timestamp": 1482918104,
    "children": [
        {
            "id": "1",
            "name": "Maya",
            "age": "5",
            "photoName": "maya.png",
            "panelColor": ""
        },
        {
            "id": "2",
            "name": "Brynlee",
            "age": "3",
            "photoName": "brynlee.png",
            "panelColor": "green"
        }
    ]
}

需要把每个 child 的 points 加进去。

问题原因

问题的关键在于对 PHP 数组结构的理解, 以及赋值操作的位置。 $temp[] = $child; 是将 $child 数组作为 $temp 的一个新元素 追加 进去。如果在这个操作 之后 再用 $temp['points'] = $total, 那么 points 就成了 $temp 的一个键, 而不是各个 $child 的键。

解决方案

方案一: 直接在 $child 数组中添加

最直接的方法, 就是在把 $child 放入 $temp 之前, 直接给 $child 数组添加 points 键。

$children = mysql_query("SELECT c.id, c.name, c.age, c.photoName, c.panelColor FROM children as c");
$temp = array();

while ($child = mysql_fetch_assoc($children)) {

    $ratings = mysql_query('
        SELECT r.behaviourID, t.points, t.typeName
        FROM  behaviourRatings as r
        JOIN  behaviourTypes as t
        ON    r.behaviourID = t.typeID
        WHERE r.childID = ' . $child['id']);

     $totalPoints = 0;
     while ($childRatings = mysql_fetch_array($ratings)){
       $totalPoints = ($totalPoints + $childRatings['points']);
     }

     if(($totalPoints + $maxPoints) > $maxPoints) {
        $total = $maxPoints;
     } else if($totalPoints < 0){
        $total = ($maxPoints + $totalPoints);
     }else{
        $total = ($maxPoints - $totalPoints);
     }

     // 直接添加到 $child 数组
     $child['points'] = $total;
     $temp[] = $child;
 }

$response = array();
$response['timestamp'] = $currentmodif;
$response['children'] = $temp;
echo json_encode($response, JSON_PRETTY_PRINT);

原理:

$child 数组在循环中被创建, 我们在将它添加到 $temp 之前 , 用 $child['points'] = $total; 直接给 $child 添加了 points。 这样, $temp 中的每个元素都包含 points

输出结果:

{
    "timestamp": 1482918104,
    "children": [
        {
            "id": "1",
            "name": "Maya",
            "age": "5",
            "photoName": "maya.png",
            "panelColor": "",
            "points": 100
        },
        {
            "id": "2",
            "name": "Brynlee",
            "age": "3",
            "photoName": "brynlee.png",
            "panelColor": "green",
            "points": 85
        }
    ]
}

方案二:使用 array_map

如果不想修改原有的循环逻辑, 也可以用 array_map 函数来处理。

$children = mysql_query("SELECT c.id, c.name, c.age, c.photoName, c.panelColor FROM children as c");
$temp = array();

while ($child = mysql_fetch_assoc($children)) {

    $ratings = mysql_query('
        SELECT r.behaviourID, t.points, t.typeName
        FROM  behaviourRatings as r
        JOIN  behaviourTypes as t
        ON    r.behaviourID = t.typeID
        WHERE r.childID = ' . $child['id']);

     $totalPoints = 0;
     while ($childRatings = mysql_fetch_array($ratings)){
       $totalPoints = ($totalPoints + $childRatings['points']);
     }

     if(($totalPoints + $maxPoints) > $maxPoints) {
        $total = $maxPoints;
     } else if($totalPoints < 0){
        $total = ($maxPoints + $totalPoints);
     }else{
        $total = ($maxPoints - $totalPoints);
     }
      $child['total'] = $total; // 先临时存到 'total'里。
     $temp[] = $child;
 }

 // 使用 array_map 添加 'points'
$temp = array_map(function($item) {
    $item['points'] = $item['total'];
    unset($item['total']); // 不需要的话删掉
    return $item;
}, $temp);

$response = array();
$response['timestamp'] = $currentmodif;
$response['children'] = $temp;
echo json_encode($response, JSON_PRETTY_PRINT);

原理:

array_map$temp 数组的每个元素执行一个回调函数。 在回调函数里, 我们把每个元素的 'total' 的值赋给 'points', 然后把 'total' 删掉 (如果不需要的话)。

安全建议:

  • 弃用 mysql_* 函数: mysql_* 函数族已经在 PHP 5.5 中弃用,并在 PHP 7.0 中完全移除。 应该使用 MySQLiPDO 扩展。
  • SQL 注入防护: 代码中的 SQL 查询存在 SQL 注入的风险。务必使用预处理语句或者对输入进行转义。

使用 MySQLi 的改进示例:

$mysqli = new mysqli("localhost", "user", "password", "database");

$stmt = $mysqli->prepare("SELECT c.id, c.name, c.age, c.photoName, c.panelColor FROM children as c");
$stmt->execute();
$result = $stmt->get_result();

$temp = array();

while ($child = $result->fetch_assoc()) {

    $stmt2 = $mysqli->prepare('
        SELECT t.points
        FROM  behaviourRatings as r
        JOIN  behaviourTypes as t
        ON    r.behaviourID = t.typeID
        WHERE r.childID = ?');
    $stmt2->bind_param("i", $child['id']); // 'i' 表示 integer
    $stmt2->execute();
    $ratings = $stmt2->get_result();

     $totalPoints = 0;
     while ($childRatings = $ratings->fetch_assoc()){
       $totalPoints = ($totalPoints + $childRatings['points']);
     }

     if(($totalPoints + $maxPoints) > $maxPoints) {
        $total = $maxPoints;
     } else if($totalPoints < 0){
        $total = ($maxPoints + $totalPoints);
     }else{
        $total = ($maxPoints - $totalPoints);
     }

     $child['points'] = $total;
     $temp[] = $child;
 }

$response = array();
$response['timestamp'] = $currentmodif; // 假定 $currentmodif 已经设置
$response['children'] = $temp;
echo json_encode($response, JSON_PRETTY_PRINT);

$stmt->close();
$stmt2->close();
$mysqli->close();

代码优化及进阶使用技巧:

  1. 合并查询 : 可以考虑将获取 children 信息和计算 points 的两个查询合并成一个,减少数据库查询次数。

    SELECT
        c.id,
        c.name,
        c.age,
        c.photoName,
        c.panelColor,
        COALESCE(SUM(t.points), 0) AS points  -- 如果没有匹配的 ratings, 则 points 为 0
    FROM children AS c
    LEFT JOIN behaviourRatings AS r ON c.id = r.childID
    LEFT JOIN behaviourTypes AS t ON r.behaviourID = t.typeID
    GROUP BY c.id;
    

    PHP部分简化为:

   $mysqli = new mysqli("localhost", "user", "password", "database");

    $stmt = $mysqli->prepare('SELECT
    c.id,
    c.name,
    c.age,
    c.photoName,
    c.panelColor,
    COALESCE(SUM(t.points), 0) AS points
    FROM children AS c
    LEFT JOIN behaviourRatings AS r ON c.id = r.childID
    LEFT JOIN behaviourTypes AS t ON r.behaviourID = t.typeID
    GROUP BY c.id');

   $stmt->execute();
    $result = $stmt->get_result();
    $temp = array();
      while ($child = $result->fetch_assoc()){
         // 简化分数逻辑
         $total = min(max($maxPoints, $maxPoints + $child['points']), $maxPoints);
          $child['points'] = $total;

          $temp[] = $child;
      }

   $response = array();
   $response['timestamp'] = time();
   $response['children'] = $temp;
   echo json_encode($response, JSON_PRETTY_PRINT);
    $stmt->close();
    $mysqli->close();

  1. 分数计算逻辑优化 : $total 的计算逻辑可以用 minmax 函数简化:
$total = min(max($maxPoints, $maxPoints + $totalPoints), $maxPoints);

这样就不用 if...else if...else 了, 更简洁。

总结一下, 最好的解决方案是直接在获取 $child 数据后, 立刻计算并添加到 $child 数组里。 其次, 如果不想改动太大, 可以用 array_map。 当然, 最重要的还是换用 MySQLiPDO, 并使用预处理语句来防止 SQL 注入, 这比单纯加个字段要重要得多! 合并 SQL 更是能大幅提升性能。