array_multisortの第二ソートキーの話


PHPの[array_multisort](http://php.net/manual/ja/function.array-multisort.php)といえば例3にありますようにデータベースの結果をソートするのに利用する機会が多いことかと思います。
例えばこんなデータ。

```php
$data = array(
  array('sortval' => 70,  'name' => 'データ1'),
  array('sortval' => 80,  'name' => 'データ2'),
  array('sortval' => 70,  'name' => 'データ3'),
  array('sortval' => 100, 'name' => 'データ4'),
  array('sortval' => 70,  'name' => 'データ5'),
  array('sortval' => 70,  'name' => 'データ6'),
);
```

RDBだったらsortして返せば済む話なのですが、例えば何かのマスタであるcsvとかだと、プログラム側で並び替えないといけない事情も出てくるかと思います。

これをsortvalキーの値を元に並べ替えたいというときに``array_multisort``を使います。
ドキュメントの例に倣ってやってみます。

```php
$sortKey1 = array();
foreach ($data as $index => $val) {
    $sortKey1[$index] = $val['sortval'];
}
array_multisort($sortKey1, SORT_ASC, $data);
var_dump($data);
// array(6) {
//   [0]=>
//   array(2) {
//     ["name"]=>
//     string(10) "データ1"
//     ["sortval"]=>
//     int(70)
//   }
//   [1]=>
//   array(2) {
//     ["name"]=>
//     string(10) "データ3"
//     ["sortval"]=>
//     int(70)
//   }
//   [2]=>
//   array(2) {
//     ["name"]=>
//     string(10) "データ5"
//     ["sortval"]=>
//     int(70)
//   }
//   [3]=>
//   array(2) {
//     ["name"]=>
//     string(10) "データ6"
//     ["sortval"]=>
//     int(70)
//   }
//   [4]=>
//   array(2) {
//     ["name"]=>
//     string(10) "データ2"
//     ["sortval"]=>
//     int(80)
//   }
//   [5]=>
//   array(2) {
//     ["name"]=>
//     string(10) "データ4"
//     ["sortval"]=>
//     int(100)
//   }
// }
```

はい。sortvalの値が少ない順、昇順に並び替わりました。
この結果を見て「sortvalの値が重複してる場合は元の配列の並び順を保持してくれてるのかー」と安易に判断してしまいましたが、実際はそうではありませんでした。

```php
$data = array(
  array( 'sortval' => 70,  'name' => 'データ9'), // データ9にリネーム
  array( 'sortval' => 80,  'name' => 'データ2'),
  array( 'sortval' => 70,  'name' => 'データ3'),
  array( 'sortval' => 100, 'name' => 'データ4'),
  array( 'sortval' => 70,  'name' => 'データ5'),
  array( 'sortval' => 70,  'name' => 'データ6'),
);
$sortKey1 = array();
foreach ($data as $index => $val) {
    $sortKey1[$index] = $val['sortval'];
}
array_multisort($sortKey1, SORT_ASC, $data);
var_dump($data);
// array(6) {
//   [0]=>
//   array(2) {
//     ["sortval"]=>
//     int(70)
//     ["name"]=>
//     string(10) "データ3"
//   }
//   [1]=>
//   array(2) {
//     ["sortval"]=>
//     int(70)
//     ["name"]=>
//     string(10) "データ5"
//   }
//   [2]=>
//   array(2) {
//     ["sortval"]=>
//     int(70)
//     ["name"]=>
//     string(10) "データ6"
//   }
//   [3]=>
//   array(2) {
//     ["sortval"]=>
//     int(70)
//     ["name"]=>
//     string(10) "データ9" ← 4番目に来てる
//   }
//   [4]=>
//   array(2) {
//     ["sortval"]=>
//     int(80)
//     ["name"]=>
//     string(10) "データ2"
//   }
//   [5]=>
//   array(2) {
//     ["sortval"]=>
//     int(100)
//     ["name"]=>
//     string(10) "データ4"
//   }
// }
```

はい。
元の順番を第二ソートキーとして使ってるとは限らなそう。
ちょっと簡略化したデータでキーを増やして検証。

```php
$data = array(
  array('sortval' => 70, 'name' => 'データ9', 'other' => 'fuga'),
  array('sortval' => 70, 'name' => 'データ9', 'other' => 'hoge')
);
$sortKey1 = array();
foreach ($data as $index => $val) {
    $sortKey1[$index] = $val['sortval'];
}
array_multisort($sortKey1, SORT_ASC, $data); // ← 昇順
var_dump($data);
// array(2) {
//   [0]=>
//   array(3) {
//     ["sortval"]=>
//     int(70)
//     ["name"]=>
//     string(10) "データ9"
//     ["other"]=>
//     string(4) "fuga" ← fugaの方が先にきてる
//   }
//   [1]=>
//   array(3) {
//     ["sortval"]=>
//     int(70)
//     ["name"]=>
//     string(10) "データ9"
//     ["other"]=>
//     string(4) "hoge"
//   }
// }

$data = array(
  array('sortval' => 70, 'name' => 'データ9', 'other' => 'fuga'),
  array('sortval' => 70, 'name' => 'データ9', 'other' => 'hoge')
);
$sortKey1 = array();
foreach ($data as $index => $val) {
    $sortKey1[$index] = $val['sortval'];
}
array_multisort($sortKey1, SORT_DESC, $data); // ← 降順
var_dump($data);
// array(2) {
//   [0]=>
//   array(3) {
//     ["sortval"]=>
//     int(70)
//     ["name"]=>
//     string(10) "データ9"
//     ["other"]=>
//     string(4) "fuga" ← 変わらずfugaの方が先にきてる
//   }
//   [1]=>
//   array(3) {
//     ["sortval"]=>
//     int(70)
//     ["name"]=>
//     string(10) "データ9"
//     ["other"]=>
//     string(4) "hoge"
//   }
// }
```

otherキーの値を参照して順番が変わるかと思いきや変わらない。なぞ。
（検証コードが雑すぎる感もありますが）

# まとめ
第二ソートキーとして何が使われるのか、正直言って分かりませんでした。
何回実行しても結果が変わらないのでランダムってことはないでしょう。
ひょっとしたらハッシュ値か何かで比較してるのかなぁ、どうなのかなぁ。
でもそれだったらotherキーの例で順番が変わってくれてもいいと思うんだけどなぁ。
[array_multisortの実装](https://github.com/php/php-src/blob/master/ext/standard/array.c#L4795)が読み解ければ分かるのかなぁ…と思ったけど私はCが読めませんでした。残念。


そんなわけで値が重複した時に順番が不定でもいいなら前述の実装でも良いですが、
元の配列の順番を保持していてほしい、というときは重複し得ない第二ソートキーも指定してあげないといけませんね、という話でした :open_mouth: 

```php
$data = array(
  array('sortval' => 70,  'name' => 'データ9'),
  array('sortval' => 80,  'name' => 'データ2'),
  array('sortval' => 70,  'name' => 'データ3'),
  array('sortval' => 100, 'name' => 'データ4'),
  array('sortval' => 70,  'name' => 'データ5'),
  array('sortval' => 70,  'name' => 'データ6'),
);
$sortKey1 = $sortKey2 = array();
foreach ($data as $index => $val) {
    $sortKey1[$index] = $val['sortval'];
    $sortKey2[$index] = $index; // ← 元の並び順もソートキーとして使う
}
array_multisort($sortKey1, SORT_ASC, $sortKey2, SORT_ASC, $data);
var_dump($data);
// array(6) {
//   [0]=>
//   array(2) {
//     ["sortval"]=>
//     int(70)
//     ["name"]=>
//     string(10) "データ9" ← 元の順番通り
//   }
//   [1]=>
//   array(2) {
//     ["sortval"]=>
//     int(70)
//     ["name"]=>
//     string(10) "データ3"
//   }
//   [2]=>
//   array(2) {
//     ["sortval"]=>
//     int(70)
//     ["name"]=>
//     string(10) "データ5"
//   }
//   [3]=>
//   array(2) {
//     ["sortval"]=>
//     int(70)
//     ["name"]=>
//     string(10) "データ6"
//   }
//   [4]=>
//   array(2) {
//     ["sortval"]=>
//     int(80)
//     ["name"]=>
//     string(10) "データ2"
//   }
//   [5]=>
//   array(2) {
//     ["sortval"]=>
//     int(100)
//     ["name"]=>
//     string(10) "データ4"
//   }
// }
```