• SQL

    LATERALを使ったクエリ

    SQLのLATERALというのを知りました。個人的な理解としては効率的なCROSS JOIN、N+1クエリのSQL版といった感じです(N+1に関してはJOINしてればだいたいそうだが)。LATERALはサブクエリの中で外側のクエリの値を参照できるという特徴があります。

    具体例として下記のようなテーブルを考えてみます。アンケートと、その回答を格納するテーブルです。以降PostgreSQLを前提としています。

    CREATE TABLE surveys ( survey_id bigint primary key, first_served_campaign bool ); CREATE TABLE answers ( answer_id bigint primary key, survey_id bigint, user_id bigint, answered_at timestamptz );

    ここで、アンケートに回答したユーザーの中で、先着3名までにプレゼントをする企画があったとします。 first_served_campaign カラムがtrueの場合、そのアンケートが対象となります。このとき、先着3名のユーザーを取得するクエリを考えます。

    パッと思いつくのがウィンドウ関数を用いる関数です。

    WITH filtered_surveys AS ( SELECT survey_id FROM surveys WHERE first_served_campaign = true ), ranked_answers AS ( SELECT fs.survey_id, a.user_id, a.answered_at, ROW_NUMBER() OVER (PARTITION BY fs.survey_id ORDER BY a.answered_at) AS rank FROM filtered_surveys fs JOIN answers a ON fs.survey_id = a.survey_id ) SELECT survey_id, user_id FROM ranked_answers WHERE rank <= 3 ORDER BY survey_id;

    ただ、このクエリ、ちょっと遅い。ranked_answers で全ての回答を取得してから、その中から先着3名を取得しているためです。ここでLATERALを使うと、サブクエリの中で外側のクエリの値を参照できるため、効率的に取得できます。

    SELECT s.survey_id, a.user_id FROM surveys s, LATERAL ( SELECT user_id FROM answers WHERE answers.survey_id = s.survey_id ORDER BY answered_at LIMIT 3 ) a WHERE s.first_served_campaign = true ORDER BY s.survey_id;

    first_served_campaign がtrueなアンケートが5,000件、回答がそれぞれアンケート毎に10,000件つまり合わせて回答が合わせて50,000,000件ある場合、前者のクエリは50,000,000件の回答を取得してから先着3名を取得しますが、後者のクエリはアンケート毎に3名の回答を取得するため、つまり15,000件の回答を取得するだけで済みます。実際、実行計画を見ると、前者のクエリは Gather Merge が50,000,000行に対して、後者のクエリは Nested Loop が15,000行に対して行わるような計画になっています。結果コストが圧倒的に変わります。