GROUP BY での SUM に DISTINCT を適用
relation.distinct.sum が生成する SUM(DISTINCT column) は重複除去に有効ですが、group が付与されたときに DISTINCT が失われ、重複した行が合計に含まれる不整合がありました。この PR はその不具合を解消し、グループ化された集計でも正しく SUM(DISTINCT) が適用されるようにします。結果として、同一カラムの重複値を除いた正確な合計が取得可能になります。
背景
Account.where(firm_id: 1).distinct.sum(:credit_limit) は SUM(DISTINCT credit_limit) を生成し 110 を返す一方で、group を追加した Account.where(firm_id: 1).group(:firm_id).distinct.sum(:credit_limit) は DISTINCT が除外され、重複がカウントされた 160 を返していました。この差異は、未グループ化パスでは select_value.distinct = true if operation == "sum" && distinct が設定されているのに対し、グループ化パスでは同様のフラグ設定が抜けていたことが原因です。結果として、集計結果が数値的に誤ったものとなり、ビジネスロジック上の不整合を引き起こしていました。
技術的な変更
execute_grouped_calculation メソッド内に select_value.distinct = true if operation == "sum" && distinct を追加し、グループ化された計算でも select_value.distinct フラグがオンになるよう統一しました。この変更は既存のロジックに最小限の差分で挿入され、他の集計操作や非 DISTINCT のケースには影響を与えません。
変更前:
select_value = operation_over_aggregate_column(column, operation, distinct)
select_value = select_value.as(model.adapter_class.quote_column_name(column_alias))
変更後:
select_value = operation_over_aggregate_column(column, operation, distinct)
select_value.distinct = true if operation == "sum" && distinct
select_value = select_value.as(model.adapter_class.quote_column_name(column_alias))
テストスイートにも test_should_group_by_distinct_summed_field が追加され、未グループ化・グループ化の両ケースで期待通りに 110 と {1 => 110} が得られることを検証しています。このテストはデータをリセットし複数レコードを作成した上で、distinct.sum の結果を明示的に比較しています。
設計判断
修正は select_value.distinct の設定をグループ化パスへ移植するという、既存のフラグ設定ロジックを再利用する方針で実装されました。代替案として新しいメソッドや設定キーの導入が検討されましたが、既存コードとの互換性とシンプルさを保つために現在の実装が選択されました。
この変更は distinct が指定されていない従来の sum 動作を全く変えず、sum とともに distinct オプションが要求された場合のみフラグを付与します。そのため、既存アプリケーションの振る舞いは保持されつつ、期待通りの集計が可能になるという利点があります。
まとめ
本 PR は SUM(DISTINCT ...) が GROUP BY と併用されたときに失われていた問題を修正し、relation.distinct.sum が常に重複除去された合計を返すよう統一しました。変更点は execute_grouped_calculation へのフラグ追加と対応テストの追加のみで、既存コードへの影響は最小限です。これにより、Rails の集計 API がより一貫した動作を提供できるようになりました。