唐突にSQLのGROUP BYの処理をPHPで実装してみた
SQLのGROUP BYは、とても強力な機能であるにもかかわらず、記述はとてもシンプル。
SELECT num , MAX(score) AS c_max , MIN(score) AS c_min , AVG(score) AS c_avg , SUM(score) AS c_sum FROM hoge GROUP BY num ORDER BY num
なぜか唐突に、これと同じ処理をPHPで書いてみようと思ったらしい。(エラーチェックちゃんとやってないのであしからず(謎))
<?php class SqlFeature { /** * SQLのGROUP BYと同じグルーピング&集計処理を行う。 * * 処理対象のデータは次の形式の配列を指定する。 * array( * array( * <column_name1> => <column_value1>, * <column_name2> => <column_value2>, * <column_name3> => <column_value3>, * : * ), * : * ) * * グループ化対象列の名前は次の形式の配列を指定する。 * array( * <column_name1>, * <column_name2>, * : * ) * * 集計関数は次の形式の配列を指定する。 * array( * <agg_result_column_name1> * => array('name' => <function_name1>, 'column' => <agg_column_name1>), * <agg_result_column_name2> * => array('name' => <function_name2>, 'column' => <agg_column_name2>), * : * ) * * @param $rows 処理対象のデータ * @param $groupColumnNames グループ化対象列の名前 * @param $aggFuncs 集計関数 * @return 集計結果のデータ */ public static function groupBy($rows, $groupColumnNames, $aggFuncs) { if (!is_array($groupColumnNames)) { $groupColumnNames = array($groupColumnNames); } $tmpRows = array(); // グルーピング処理 foreach ($rows as $row) { // グループ化対象列の値を抽出 $keyVals = array(); foreach ($groupColumnNames as $colName) { $keyVals[$colName] = $row[$colName]; } // グループを特定するハッシュキーを生成 $hashKey = self::genKey($keyVals); $tmpRows[$hashKey][':key:'] = $keyVals; // 各集計結果列用の集計対象値を抽出(配列として保持) foreach ($aggFuncs as $colName => $func) { $tmpRows[$hashKey][$colName][] = $row[$func['column']]; } } // 集計処理 foreach ($tmpRows as & $t) { foreach ($aggFuncs as $colName => $func) { // 集計元データを、集計関数での集計結果に置き換え $t[$colName] = call_user_func($func['name'], $t[$colName]); } } // 返却用データ配列の生成 $res = array(); foreach ($tmpRows as $tmpRow) { $row = array(); // グループ化対象列の値の平坦化 foreach ($groupColumnNames as $colName) { $row[$colName] = $tmpRow[':key:'][$colName]; } // 集計結果の値をコピー foreach ($aggFuncs as $colName => $func) { $row[$colName] = $tmpRow[$colName]; } $res[] = $row; } return $res; } /** * 値の配列から、連想配列のキーにするための文字列を生成する。 */ /* * 配列は連想配列のキーにできないので、代わりとなる文字列を生成するために使用。 * なんでもいいんだけど、とりあえずMD5値をコロン(':')で囲ったものを連結。 */ private static function genKey($values) { $res = ""; foreach ($values as $v) { $res .= ':' . md5($v) . ':'; } return $res; } }
使い方は以下のような感じ。
<?php ini_set('display_errors', 'on'); require_once('SqlFeature.php'); /** * 平均を求めるための集計関数。 */ function avg(array $vals) { return array_sum($vals) / count($vals); } /** * 集計結果を整形して出力する。 */ function printResult($result) { foreach ($result as $r) { printf("%3d %15.5lf %15.5lf %15.5lf %15.5lf\n", $r["num"], $r["c_max"], $r["c_min"], $r["c_avg"], $r["c_sum"]); } } $data = array ( array ( 'num' => '1', 'score' => '25'), array ( 'num' => '2', 'score' => '35'), array ( 'num' => '3', 'score' => '55'), array ( 'num' => '3', 'score' => '65'), array ( 'num' => '1', 'score' => '45'), array ( 'num' => '3', 'score' => '75'), array ( 'num' => '4', 'score' => '5'), array ( 'num' => '4', 'score' => '10'), ); $aggFuncs = array( 'c_max' => array('name' => 'max', 'column' => 'score'), 'c_min' => array('name' => 'min', 'column' => 'score'), 'c_avg' => array('name' => 'avg', 'column' => 'score'), 'c_sum' => array('name' => 'array_sum', 'column' => 'score'), ); $result = SqlFeature::groupBy($data, 'num', $aggFuncs); usort($result, function($a, $b) { return $a['num'] - $b['num']; }); printResult($result);
結果はこんな感じ。
1 45.00000 25.00000 35.00000 70.00000 2 35.00000 35.00000 35.00000 35.00000 3 75.00000 55.00000 65.00000 195.00000 4 10.00000 5.00000 7.50000 15.00000
ここでは、データをべた書きしているが、以下のような処理でDBMSから取得したデータもそのまま利用可能。
$conn = pg_connect('host=localhost port=5432 dbname=postgres user=postgres password=postgres'); $sql = "select num, score" . " from hoge", $res = pg_query($conn, $sql); $data = array(); while ($row = pg_fetch_assoc($res)) { $data[] = $row; } pg_close($conn);