web-dev-qa-db-ja.com

各項目のJPA Select最新インスタンス

ミーティングエンティティがあるとしましょう。各会議には、1人の出席者と会議の日付があります。会議テーブル内では、出席者ごとに複数の会議があり、それぞれ日付が異なります。すべての参加者に対して最新の会議のみを選択するJPAクエリが必要です。たとえば、テーブルが次のようになっている場合

Meeting ID | Attendee ID | Meeting Date
1          | 1           |  6/1/2011
2          | 2           |  6/1/2011
3          | 1           |  6/6/2011
4          | 3           |  6/6/2011

私の結果は

Meeting ID | Attendee ID | Meeting Date
2          | 2           |  6/1/2011
3          | 1           |  6/6/2011
4          | 3           |  6/6/2011

Postgresに対してJPA 2を使用します。会議には、出席者への1-1と単純なタイムスタンプの日付があります。私はgroup byとmax(blah)、そしておそらく自分自身への参加を行う必要があると思いますが、これに取り組むための最良の方法はわかりません。

更新:これで遊んで夜を過ごした後でも、これに対する許容できるJPQLソリューションがまだありません。ここに私がこれまでに持っているものがあります:

select m from Meeting m 
where m.meetingDate in 
    ( select max(meet.meetingDate) 
      from Meeting meet group by meet.attendee )

出席部門によるフィルタリングなど、この質問に関係のないさまざまな条件があります。これが機能する唯一の理由は、会議の日付を2番目(またはそれより細かい)に追跡しているため、2つの会議がまったく同時に発生する可能性が最小限であるためです。いくつかのJavaの周りにいくつかの要素を配置して、同時に2人になる場合に備えて、各出席者の最後の会議のみを保持しますが、それはかなり安っぽい解決策です。クエリですべてを取得するには難しすぎますが、まだわかりません。

pdate2: sqlタグを追加します。sqlを使用してビューを作成し、JPAオブジェクトを作成してビューにマップする必要がある場合は、それで問題ありません。

23
digitaljoel

私はこのクエリでそれを持っていると思います。

select m from Meeting m 
    where m.meetingDate = 
        (select max(m1.meetingDate) 
            from Meeting m1 
            where m1.attendee = m.attendee )
    and not exists 
        (select m2 from Meeting m2 
            where m2.attendee = m.attendee 
            and m2.meetingDate > m.meetingDate)
15
digitaljoel

SQLでは、解決策は非常に簡単です。テーブルをサブクエリで結合します。これにより、各出席者の最新の会議が得られます。

select * from Meeting ALL
join ( select max(meetingDate) as newest, attendee
from Meeting group by attendee ) LATEST
on ALL.meetingDate = LATEST.newest AND ALL.attendee = LATEST.attendee

これは動作し、高速に動作します!

JPAの問題は、それ(またはほとんどの実装)が結合のサブクエリを許可しないことです。最初に何をコンパイルするか、そしてそれがどれほど遅いかを試すために数時間を費やした後、私はJPAを嫌うと決めました。上記のようなソリューション(EXISTS(SELECT ..)またはIN(SELECT ..)など)は、実行に時間がかかり、必要以上に時間がかかります。

機能するソリューションがあるということは、JPAからそのソリューションにアクセスするだけでよいということです。 SQLには、まさにそのための2つの魔法の言葉があります。

CREATE VIEW

そして人生はとても単純になります...そのようなエンティティを定義してそれを使うだけです。注意:読み取り専用です。

もちろん、JPAの純粋主義者はあなたがそれをするときあなたを軽蔑します。ですから、誰かが純粋なJPAソリューションを持っているなら、私たちに知らせてください!

15
Bulba

SQLでは、これは非常に単純だと思うので、JPAにマップできると思います。

SELECT m.AttendeeId, MAX(m.MeetingDate) from Meeting m GROUP BY m.AttendeeId

編集:messageId自体も必要な場合は、他の2つの値が等しいメッセージのmessageIdを返す単純なサブクエリでそれを行うことができます。同じ出席者と日付に複数のmessageIdが存在するケースを処理することを確認してください(たとえば、最初の結果はすべて同じように良いはずなので、最初の結果を選択します-そのようなデータは会議でも意味があると思います)

9
Voo

プレーンSQL

Bulbaが言ったように 適切な方法は、サブクエリをgroup byで結合することです。

JPA、JPQL

問題は、サブクエリに参加できないことです。

これは回避策です。

Group byを使用してサブクエリで何が得られるかを見てみましょう。ペアのリスト_(attendee_id, max(meeting_date))_を取得します。このペアは、参加したい最大日付を持つ行の新しい一意のIDのようなものです。次に、テーブルの各行が_(attendee_id, meeting_date)_のペアを形成することに注意してください。したがって、すべての行には_(attendee_id, meeting_date)_というペアのIDがあります。サブクエリで受け取ったリストに属するIDを形成する場合にのみ行を取得できます。

簡単にするために、このIDペアを_attendee_id_と_meeting_date_の連結として表すことができます:concat(attendee_id, meeting_date)

次に、SQLでのクエリ(JPQLおよびJPA CriteriaBuilderの場合も同様)は次のようになります。

_SELECT * FROM meetings 
WHERE concat(attendee_id, meeting_date) IN
(SELECT concat(attendee_id, max(meeting_date)) FROM meetings GROUP BY attendee_id)
_

answers のように、クエリごとに1つのサブクエリしかなく、各行に1つのサブクエリがないことに注意してください。

文字列の比較を恐れていますか?

特別オファーがあります!

そのIDペアを数値にエンコードしましょう。 _attendee_id_と_meeting_date_の合計になりますが、コードの一意性を確保するための変更が加えられています。日付の数値表現をUnix時間として取得できます。最終的なコードに最大値の制限があるため、コードがキャプチャできる最大日付の値を修正します(例:bigint(int8)<263)。便宜上、2149-06-07 03:00:00として最大日付を取得します。秒で5662310400、日で65536です。ここでは、日数の精度が必要であると仮定します(そのため、時間とそれ以下は無視します)。一意のコードを構築するために、基数が65536の数値システムの数値として解釈できます。最後のシンボル(0から2までの数値)16-1)そのような数値システムのコードまたはコードは日数です。他のシンボルは_attendee_id_をキャプチャします。このような解釈では、コードはXXXXのようになり、各Xは[0,216-1](より正確に言うと、最初のXは[0,215-1]符号の1ビットのため)、最初の3つのXは_attendee_id_を表し、最後のXは_meeting_date_を表します。したがって、コードがキャプチャできる_attendee_id_の最大値は2です。47-1。コードは_attendee_id_ * 65536 + "日数"として計算できます。

Postgresqlでは次のようになります。

_attendee_id*65536 + date_part('Epoch', meeting_date)/(60*60*24)
_

ここで、 _date_part_ は秒で日付を返し、定数で除算することで日数に変換します。

そして、すべての参加者の最新の会議を取得するための最後のクエリ:

_SELECT * FROM meetings
WHERE attendee_id*65536 + date_part('Epoch', meeting_date)/(60*60*24)
IN (SELECT attendee_id*65536 + date_part('Epoch', max(meeting_date))/(60*60*24) from meetings GROUP BY attendee_id);
_

ベンチマーク

私は質問のような構造のテーブルを作成し、[1、10000]から_attendee_id_をランダムに選択し、範囲[1970-01-01、2017-09-16]からランダムに日付を選択して100000行を入力しました。次の手法で( EXPLAIN ANALYZE )クエリをベンチマークしました。

  1. 相関サブクエリ

    _SELECT * FROM meetings m1 WHERE m1.meeting_date=
    (SELECT max(m2.meeting_date) FROM meetings m2 WHERE m2.attendee_id=m1.attendee_id);
    _

    実行時間:873260.878 ms

  2. サブクエリをグループ化して結合する

    _SELECT * FROM meetings m
    JOIN (SELECT attendee_id, max(meeting_date) from meetings GROUP BY attendee_id) attendee_max_date
    ON attendee_max_date.attendee_id = m.attendee_id;</code>
    _

    実行時間:103.427ミリ秒

  3. ペア_(attendee_id, date)_をキーとして使用します

    • 文字列としての_attendee_id_および_meeting_date_の連結

      _SELECT * FROM meetings WHERE concat(attendee_id, meeting_date) IN
      (SELECT concat(attendee_id, max(meeting_date)) from meetings GROUP BY attendee_id);
      _

      実行時間:207.720 ms

    • _attendee_id_および_meeting_date_を単一の数値(コード)にエンコードする

      _SELECT * FROM meetings
      WHERE attendee_id*65536 + date_part('Epoch',meeting_date)/(60*60*24)
      IN (SELECT attendee_id*65536 + date_part('Epoch',max(meeting_date))/(60*60*24) from meetings GROUP BY attendee_id);
      _

      実行時間:127.595 ms

これは、テーブルスキーム、テーブルデータ(csvとして)、テーブルにデータを入力するためのコード、およびクエリを含む git です。

7
dimathe47