こんにちは!最近、実務でSQLの重複排除に頭を悩ませ、無事に数時間を溶かしたエンジニアです。
「このデータ、重複を排除してスマートに取得して!」と言われたものの、いざ書き始めてみると想定外のエラーや仕様の壁にぶち当たりまくりました……。
今回は、私が実際に直面した**「Oracle DBでの重複排除の2大パターン」と、それをPHP(PDO)に組み込む時に「本気で踏み抜いた罠」**を、自分の備忘録も兼ねて共有します!同じところで沼りかけている誰かの参考になれば幸いです。
🛠 パターン1:単純にユニークなリストが欲しかった(DISTINCT)
まず私がやろうとしたのは、全データの中から「存在する値の種類(バリエーション)」だけをシンプルに抜き出すことでした。
📝 解決したSQL
SELECT DISTINCT
EQUIPMENT_ID
FROM
INSPECTION_TABLE
WHERE
CONTRACT_ID = 'C001';
💡 最初に私がやらかしたミス
「IDだけじゃなくて、ついでに他のカラム(点検日)も一緒に取っちゃお!」と、欲張ってこう書きました。
SELECT DISTINCT
EQUIPMENT_ID,
INSPECTION_DATE -- 余計なカラムを増やした
FROM
INSPECTION_TABLE;
すると、「あれ?IDが重複して出てくるぞ…?」という現象が発生。
原因を調べて知ったのですが、DISTINCT は 「指定したすべてのカラムの組み合わせ」で重複を判断するため、「IDが同じでも日付が違えば別物」とみなされてしまうのですね。基本ですが、さっそくここで一回詰まりました。
🔥 パターン2:重複の中から「最新の1件」だけを絞り込みたかった(ROW_NUMBER)
次に直面したのが、実務でめちゃくちゃよくある「機器IDは重複させたくないけれど、一番新しい点検日の行(他のカラムも含めて丸ごと)が欲しい!」というケースです。
DISTINCT では太刀打ちできなかったので、分析関数(ウィンドウ関数) の ROW_NUMBER() を使って解決しました。
📝 解決したSQL(Oracle DB全バージョン対応)
SELECT
EQUIPMENT_ID,
INSPECTION_DATE,
STATUS
FROM (
SELECT
EQUIPMENT_ID,
INSPECTION_DATE,
STATUS,
-- 💡 これが救世主の1行
ROW_NUMBER() OVER (PARTITION BY EQUIPMENT_ID ORDER BY INSPECTION_DATE DESC) as RNUM
FROM
INSPECTION_TABLE
)
WHERE
RNUM = 1; -- 各グループの1位(最新)だけを召喚!
💡 どういう仕組みで解決したか?
-
PARTITION BY EQUIPMENT_IDで、機器IDごとにチーム(グループ)を作って、 -
ORDER BY INSPECTION_DATE DESCで、チーム内で日付が新しい順に並び替えます。 -
ROW_NUMBER()が、上から順に1, 2, 3...と背番号(連番)を振ってくれるので、 - 最後に外側で
WHERE RNUM = 1(=最新)の行だけをズバッと抜き出す、という流れです。
ちなみにこの ROW_NUMBER()、Oracle 11gや12c、現在の19cや23cなど、ほぼ全てのOracleバージョンでそのまま使えるので、環境を気にせずガシガシ使えるのが最高です。
🚨 PHP連携で本気で踏み抜いた罠:「Array to string conversion」
さて、SQLが無事に通ってIDの配列を取得できた私は、次のステップへ進みました。
「よし、このIDリストを使って、次のSQLの IN 句に流し込むぞ!」と、以下のようにコードを書いたのです。
❌ 盛大にエラーになったコード
$equipment_ids = [10, 20, 30]; // SQLで取ってきた配列
// 😱 ここでエラー発生!
$statement = "SELECT * FROM EQUIPMENT WHERE ID IN ({$equipment_ids})";
エラー内容:
Notice: Array to string conversion in ...
「あ、PHPの配列(Array)ってそのまま文字列の中に放り込めないじゃん……」とここで気づく。
⭕ プレースホルダを使ってスマートに解決
安全かつスマートに実装するなら、? を配列の数だけ作って、execute に配列を丸ごと渡すのがベストプラクティスでした。
$equipment_ids = [10, 20, 30];
// 1. 配列の数だけ 「?」 の文字列を作る (例: "?, ?, ?")
$placeholders = implode(',', array_fill(0, count($equipment_ids), '?'));
// 2. SQL文を組み立てる
$statement = "SELECT * FROM EQUIPMENT WHERE ID IN ({$placeholders})";
// 3. プリペアドステートメントで安全に実行!
$stmt = $pdo->prepare($statement);
$stmt->execute($equipment_ids); // ここで配列をガツッと渡す
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
こうすれば、SQLインジェクションというセキュリティの脆弱性も防げて一石二鳥です。
⚠️ 先輩に教えてもらった「Oracle 1,000件の壁」
ここで注意なのが、OracleはIN 句の中に指定できる値の数が最大1,000個までという強力な制限があるそうです。もしIDが1,000件を超える可能性があるなら、array_chunk で配列を分割して OR で繋ぐなどの工夫が必要になります(今回は大丈夫でしたが、危なかった……)。
※余裕があればここの解決策もしたかったところです💦
🤝 おまけ:2つの連想配列を「IDをキー」にして綺麗にマージする
最後に、別々の条件で取得した2つのリストを1つにまとめる必要がありました。
それぞれの配列を「IDをキーにした連想配列」にしていたので、PHPの + 演算子を使うことで、重複を防ぎつつ一発でマージできました。
// 条件Aで取ってきたデータ (IDがキー)
$list_a = [
10 => $dto1,
20 => $dto2
];
// 条件Bで取ってきたデータ (IDがキー)
$list_b = [
20 => $dto_old, // ID 20 が重複している
30 => $dto3
];
// 💡 優先したい方を左にしてマージ(同じキーがあれば左側が残る)
$merged_list = $list_a + $list_b;
// 結果:ID 10, 20(条件A側), 30 の3つが綺麗にまとまる!
普通の array_merge() を使うと、キーが数字の場合に連番(0, 1, 2...)に振り直されて自慢のIDキーが消滅してしまうので、連想配列のマージは + 演算子が安全、というのも良い学びでした。
📊 振り返りまとめ
- 単純な種類のリストアップなら
DISTINCT - 「〜の中で最新の1件」を丸ごと取るなら
ROW_NUMBER()(全バージョン対応) - Oracleの
IN句は上限1,000件の制限に注意 - PHPの配列を
IN句に入れるときはimplodeと?を使う
最初はかなり苦戦しましたが、一度仕組みを理解してしまえば次からは怖くありません。
もし同じような実装で「あれ?」となった時は、ぜひこのやり方を試してみてください!