如何使用最有效的窗口函数在连接值上构建聚合?

时间:2018-03-23 11:32:21

标签: sql postgresql

我遇到了使用窗口函数在连接值上构建聚合的问题。简化它看起来像:

我收到了以下表格:

  CREATE TABLE movies (
    id SERIAL,
    name VARCHAR,
    year INT,
    genre VARCHAR,
    country VARCHAR
  );

  CREATE TABLE tags (
    id SERIAL,
    name VARCHAR
  );

  CREATE TABLE movies_tags (
    id SERIAL,
    movie_id INT,
    tag_id INT
  );

现在我想做以下声明:

  SELECT m.*, array_agg(t.name) AS tags
  FROM movies m
  LEFT JOIN movies_tags mt ON mt.movie_id = m.id
  LEFT JOIN tags t ON t.id = mt.tag_id
  ORDER BY m.name
  LIMIT 10

由于选择中的聚合,所有电影都会加入所有标签,然后再选择大型连接中的前10名。我的目标是出于性能原因仅在前10部电影上进行聚合。所以我做的是这个:

  WITH top_movies AS (
    SELECT m.*
    FROM movies m
    ORDER BY m.name
    LIMIT 10
  )
  SELECT tm.*, array_agg(t.name) AS tags
  FROM top_movies tm
  LEFT JOIN movies_tags mt ON mt.movie_id = tm.id
  LEFT JOIN tags t ON t.id = mt.tag_id

表现要好得多。但是我遇到了另一个问题。最终的目标是创建一个可重用的组件形式,如Postgres中的函数或ORM中的命名查询,如Rails的Active Record,我可以根据我的需要动态修改,例如:

  SELECT * FROM my_top_movies_with_tags() AS tm
  WHERE tm.country = 'USA' AND tm.year <= 1995
  LIMIT 10;

因此,我必须修改我的SQL语句,即电影选择是外部查询,但仍然限制了标签加入我想要的前n部电影。

为了做到这一点,我尝试了横向连接并做了这个:

  SELECT m.*, lat.tags FROM movies m
  LATERAL (
    SELECT array_agg(t.name) AS tags
    FROM movies_tags mt
    JOIN tags t ON t.id = mt.tag_id
    WHERE mt.movie_id = m.id
  ) AS lat
  ORDER BY m.name
  LIMIT 10;

这使我可以灵活地随后动态修改它,但性能明显更差。

还有其他方法可以实现我不了解的目标吗?

我的目标总结如下:

  1. 仅在array_agg电影集上制作聚合(LIMIT),而不是在整个电影桌上。
  2. 通过附加WHEREORDERLIMIT声明来保持可修改。
  3. 表现很好。

2 个答案:

答案 0 :(得分:1)

如何使用row_number模拟LIMIT

SELECT * FROM (
    SELECT 
        m.*, 
        array_agg(t.name) AS tags,
        row_number() OVER(ORDER BY m.name) AS rownum
    FROM 
        movies m
        LEFT JOIN movies_tags mt ON mt.movie_id = m.id
        LEFT JOIN tags t ON t.id = mt.tag_id
    --There're must be a GROUP BY here
    ) AS tmp
WHERE rownum <= 10;

此外,在使用CTE进行性能关键查询时,请考虑this article

答案 1 :(得分:0)

You can add form inputs to a temporary table and use your this table for filtering.

 CREATE TEMP TABLE temp_inputs
  (
     country VARCHAR(80),
     year int
  )
  ON COMMIT DELETE ROWS;


WITH top_movies AS (
    SELECT m.*
    FROM movies m
    ORDER BY m.name
    LIMIT 10
  )
  SELECT tm.*, array_agg(t.name) AS tags
  FROM tmovies tm, temp_inputs
  LEFT JOIN movies_tags mt ON mt.movie_id = tm.id
  LEFT JOIN tags t ON t.id = mt.tag_id
  and tm.country = temp_inputs.country AND tm.year <=  temp_inputs.year