EatSmartシステム部ブログ

ウェブサイトの開発や運営に関する情報です。

subqueryで複数項目を取得したい場合(PostgreSQLでLATERALを使う)

SQLで複雑なクエリを作っていると、メイン・テーブルの1行に対して、サブ・テーブルの集計した値を使用したい場合に、

SELECT
  main.id,
  main.name,
  AVG(sub.value) as average_value
FROM main_table main
INNER JOIN sub_table sub
ON main.id = sub.id
GROUP BY
  main.id,
  main.name

などと書いたりすると思います。

このときに、メイン・テーブルから取ってくる項目が多い場合

-- SQL1
SELECT
  main.id,
  main.name,
  main.column1,
  main.column2,
  main.column3,
  main.column4,
  main.column5,
  main.column6,
  main.column7,
  main.column8,
  main.column9,
  AVG(sub.value) as average_value
FROM main_table main
INNER JOIN sub_table sub
ON main.id = sub.id
GROUP BY
  main.id,
  main.name,
  main.column1,
  main.column2,
  main.column3,
  main.column4,
  main.column5,
  main.column6,
  main.column7,
  main.column8,
  main.column9

など、集計関数があるせいでGROUP BY項目が多くなってしまい、またSELECT項目を増やすたびにGROUP BY項目を追加しないといけなくなります。

それを嫌がり

SELECT
  main.id,
  main.name,
  (
    SELECT AVG(sub.value) 
    FROM sub_table sub 
    WHERE main.id = sub.id
  ) as average_value
FROM main_table main

とSELECT句のsubqueryとして書いた場合、集計項目が多くなると、

-- SQL2
SELECT
  main.id,
  main.name,
  (
    SELECT AVG(sub.value) 
    FROM sub_table sub 
    WHERE main.id = sub.id
  ) as average_value,
  (
    SELECT MAX(sub.value) 
    FROM sub_table sub 
    WHERE main.id = sub.id
  ) as max_value,
  (
    SELECT MIN(sub.value) 
    FROM sub_table sub 
    WHERE main.id = sub.id
  ) as min_value,
  (
    SELECT SUM(sub.value) 
    FROM sub_table sub 
    WHERE main.id = sub.id
  ) as sum_value,
  (
    SELECT COUNT(sub.value) 
    FROM sub_table sub 
    WHERE main.id = sub.id
  ) as count_value
FROM main_table main

のように、SELECT句の見通しがとても悪くなります。 こんなとき、LATERAL句(PostgreSQL9.3以上)を使うと見通しの良いSQLを作ることができます。

LATERAL句はテーブル(サブクエリ)のJOINとして働くのですが、結合対象のサブクエリ内で結合元の値を指定することができ、その値が遅延評価されます。(文章だけ見ても、意味が分からない…)

具体的には、

SELECT
  main.id,
  main.name,
  sub.average_value,
  sub.max_value,
  sub.min_value,
  sub.sum_value,
  sub.count_value
FROM main_table main
LEFT JOIN LATERAL (
  SELECT
    AVG(sub.value) as average_value,
    MAX(sub.value) as max_value,
    MIN(sub.value) as min_value,
    SUM(sub.value) as sum_value,
    COUNT(sub.value) as count_value
  FROM sub_table sub 
  WHERE main.id = sub.id
) as sub
ON TRUE

というSQLで、サブクエリ内のWHERE main.id = sub.idが、メイン・テーブルの1行ごとに評価され、集計関数の値を取得することができます。実行計画的にはSQL2と同等になるようです。

この程度であれば

SELECT
  main.id,
  main.name,
  sub.average_value,
  sub.max_value,
  sub.min_value,
  sub.sum_value,
  sub.count_value
FROM main_table main
LEFT OUTER JOIN (
  SELECT
    sub.id,
    AVG(sub.value) as average_value,
    MAX(sub.value) as max_value,
    MIN(sub.value) as min_value,
    SUM(sub.value) as sum_value,
    COUNT(sub.value) as count_value
  FROM sub_table sub 
  GROUP BY sub.id
) as sub
ON main.id = sub.id

と、サブ・テーブルをまるっとidのGROUP BYで集計してしまうこともできますが、もう少し複雑なSQLでも見通しを良くしようと思ったら、LATERALが有用なことがあると思います。