InnerJoinAutoDetect

概要

left outer join でも inner join でも結果が変わらない結合を自動で判別して、inner joinにします。

e.g. InnerJoinAutoDetect を使った ConditionBean による検索 @DisplaySql
select dfloc.MEMBER_ID, dfloc.MEMBER_NAME, ...
  from MEMBER dfloc
    inner join MEMBER_STATUS dfrel_0 on ...
    left outer join MEMBER_SECURITY dfrel_1 on ...
    left outer join MEMBER_SERVICE dfrel_2 on ...
    inner join MEMBER_WITHDRAWAL dfrel_3 on ...
 where dfrel_3.WITHDRAWAL_DATETIME >= '2014-08-08'

基本概念

そもそも ConditionBean は、外部結合(left outer join)的な挙動を基本にしています。 そこには明確なポリシーがあり、内部結合(inner join)はパフォーマンス考慮のための手段と捉えています。

InnerJoinAutoDetectは、手動の (Manual)InnerJoin とは別に、InnerJoin 可能な結合、つまり外部結合である必要のない外部結合を自動判別する機能です(@since 0.9.8.8)

判別ロジック

大きく二つのアプローチがあります。

構造的アプローチ
NotNullのカラムでFK制約のあるリレーション
検索条件アプローチ
where句で絞り込み条件として利用されているリレーション

ポイントとしては、相手(関連テーブル)側のデータが存在しないレコードが、確実に検索対象から外れてもいいのかどうかを判断することです。

構造的アプローチ

NotNullのカラムでFK制約のあるリレーションは、InlineView や OnClause で結合する前に絞り込みが発生していない限りは、相手側のデータは必ず存在します。

e.g. InnerJoinAutoDetect 構造的アプローチ @DisplaySql
select dfloc.MEMBER_ID, dfloc.MEMBER_NAME, ...
  from MEMBER dfloc
    inner join MEMBER_STATUS dfrel_0 on ...

ただし、AdditionalForeignKey (疑似FK) によるリレーションは対象外です。DBMS側でFK制約がないのであれば、不整合なリレーションが発生する可能性があるからです。 物理的なFK制約が存在していることが前提です。

検索条件アプローチ

(一部例外を除き) 相手側のカラムで where 句で絞り込み条件が指定されていれば、相手側のデータは必ず存在します。

e.g. InnerJoinAutoDetect 検索条件アプローチ @DisplaySql
select dfloc.MEMBER_ID, dfloc.MEMBER_NAME, ...
  from MEMBER dfloc
    left outer join MEMBER_SECURITY dfrel_1 on ...
    left outer join MEMBER_SERVICE dfrel_2 on ...
    inner join MEMBER_WITHDRAWAL dfrel_3 on ...
 where dfrel_3.WITHDRAWAL_DATETIME >= '2014-08-08'

絞り込み条件を指定していれば、自然と null のレコードは除外されます。 つまり、結合して相手が存在しないレコードは絞り込まれるので、inner join でも結果は変わりません。

ただし、その条件が、IsNull や coalesce() など null のデータを対象にする条件の場合は、判断の要素には含まれません。 また、OrScopeQueryで指定された絞り込み条件は、確実に相手側が存在するレコードだけを対象にすることが保証されないため、判断の要素には含まれません。 さらに、InlineView や OnClause の絞り込み条件は判断の要素には含まれません。

安全寄りのロジック

その他、AdditionalForeignKey の OverRelation を利用しているリレーションなど、判断がとても複雑になるケースにおいては、判断を諦めて inner join にはしません。 100% inner join にしてもOK、という判断が下せない限りは、left outer join を保ちます。

一律の設定

1.0.3からはデフォルトで、全ての ConditionBean が InnerJoinAutoDetect になっています。

それ以前のバージョンでは littleAdjustmentMap.dfprop の isInnerJoinAutoDetect を true にすることで、全ての ConditionBean で InnerJoinAutoDetect を有効にすることができます。

1.0.3からはデフォルトで有効

それまでは、ConditionBeanのメソッド呼び出しによるオプション、もしくは、dfpropによるオプションでしたが、 1.0.3からは DBFlute のデフォルトとして有効になっています。

(jflute確認ですが)実業務での実績が複数あること、その中の一つは300テーブルを越える大規模なシステムであること、 DBFlute内でのテストケースが十分に揃っていること、これらの要因から DBFlute のデフォルト機能として十分な品質に達していると判断しました。

また、この機能はとても魅力的なもので(少なくともjfluteはそう思っています)、 left outer join によるパフォーマンス劣化は多くのO/Rマッパーの悩みの一つで、 それが(多くのケースで)未然に防がれるというのは、システムの安定稼働につながるからです。 (大抵、そこが問題になるのはリリースしてからデータ量が多くなってきてから)

この機能を無効にしたい場合は、littleAdjustmentMap.dfprop の isInnerJoinAutoDetect を false にします。この検索だけ無効にしたい、という場合は、ConditionBean の suppressInnerJoinAutoDetect() を呼び出すことで、その検索だけ無効になります。

1.0.2以前:リリース前なら一律 @until 1.0.2

DBMSによっては left outer join がパフォーマンスのネックになることがあります。 DB側で自動判別してくれればとは思ってしまいますが、なかなかそうなってくれないこともあります。

そのアプリケーションがリリース前、つまり、運用開始前なのであれば、一律の設定で InnerJoin を自動判別させるようにすると良いでしょう。 幾つかのパフォーマンス問題が未然に防げればそれに越したことはありません。

未然に防がれてしまうと、実はDBFluteとしては、InnerJoinAutoDetect という機能がうれしいのかどうか、 どれほど効果がある機能なのか、ツールとしての価値がわかりにくくなってしまい、プロモーションがやりづらいのですが...まあそれは仕方のないことでしょう。

また、この機能がどれほど正確に作られているのか!? それが利用側としては不安に思うところでしょう。概要でも述べていますが、ちょっとでも不安要素がある場合は inner join にはしていません。「100% inner join でOK」という確証がある場合のみ、と安全側に寄せたロジックにしています。 この機能に関するテストケースも、他の機能よりもかなり多めに用意して確認をしています。 また、この機能を一律の設定で有効にした、テーブルの数が200以上規模の開発で利用されていて運用できているのを少なくともjfluteは知っています。

細かい仕様について

Union や SubQuery では

Union や SubQuery などの ConditionBean でも InnerJoinAutoDetect になります。 ただし、リレーションの判別処理はそれぞれ独立しているため、例えばメインの select で inner join になったリレーションも、Union 側の select ではそのようになるとは限りません。

ネストしたリレーションでは

ネストしたリレーションが "検索条件アプローチ" で内部結合対象となった場合、そのリレーションを辿る途中のリレーションも内部結合となります。 これは、SQLの文法的にそのように判断しても問題ないためです。 (A, B, C と結合していて、C のカラムで絞り込みをしていれば A と B も内部結合)

ただし、ネストしたリレーションが "構造的アプローチ" で内部結合対象であっても、そこまで辿る途中のリレーションが一つでも外部結合であれば、そのリレーションは特に内部結合の候補にはなりませんし、 そこまで辿る途中のリレーションはそのままです。これは、SQL文法的にこのケースで内部結合にすると、基点テーブルの件数が変わってしまうためです。 (A, B, C と結合していて、B と C が構造的に内部結合対象であっても、A と B が外部結合であれば A, B, C 全て外部結合)

1:必ず1リレーションでは

例えば会員と会員セキュリティのような、絶対に相手が存在する 1:必ず1 の関係であっても、これは構造的アプローチによる内部結合の候補にはなりません。 相手が絶対に存在するカージナリティかどうかというのは、DBMSの制約ではなく業務的な制約なので、スキーマのメタデータには含まれません。 つまり、DBFluteとしては、1:必ず1なのかどうかが判断できないのです。

内部結合したいのは主には検索条件アプローチ

dfprop で 1:必ず1 を指定するという機能もありません。 よって、会員と会員セキュリティを自動で内部結合にすることはありません。

これは追加するか迷ったところではありますが、もしDB変更で 1:必ず1 ではなくなった場合に、dfprop の設定だけが置き去りにされると、それは大変なデグレとなってしまいます(しかも、とても気付きにくいデグレに)。

また、パフォーマンス問題で内部結合で解決したケースというのは、どちらかというと検索条件アプローチの方でした。 相手側のカラムで絞り込み条件を指定すると、PostgreSQLやMySQLなどで遅い実行計画になる現象が報告されたことがありました。 検索結果は変わりませんが内部結合にすると速くなるのです。 (また、不思議なのは、バインド変数をやめて埋め込みにすると外部結合のままでも速くなると)

そういうことからも、構造的アプローチの方はどちらかといえば効用的には二番手のアプローチなので、 無理に会員と会員セキュリティを自動で内部結合にすることはしていません。 どうしてもというときは、(あまりお奨めではありませんが) (Manual)InnerJoin を利用します。