如何使用这种不寻常的匹配条件编写连接?

时间:2013-04-07 10:50:34

标签: sql postgresql join greatest-n-per-group

我希望“左连接”一个表,以便不仅将值连接到匹配的行,还连接到任何后续的不匹配行,直到下一个匹配的行。换句话说,我想用前一个非空值填充空值。

样本数据和所需结果:

x

 id 
----
  1
  2
  3
  4
  5

y

 id | val 
----+-----
  1 | a
  4 | b

select x.id, y.val from x left join y on x.id=y.id order by x.id;的结果:

 id | val 
----+-----
  1 | a
  2 | 
  3 | 
  4 | b
  5 | 

期望的结果:

 id | val 
----+-----
  1 | a
  2 | a
  3 | a
  4 | b
  5 | b

7 个答案:

答案 0 :(得分:6)

指数

x.idy.id上创建索引 - 如果这些是您的主键,您可能已经拥有索引。
多列索引也可能有所帮助,尤其是第9 + 2行中的index only scans

CREATE INDEX y_mult_idx ON y (id DESC, val)

但是,在我的测试中,最初并未使用此索引。不得不向val添加(否则无意义)ORDER BY以说服查询计划程序排序顺序匹配。请参阅查询 3

该索引在此合成设置中几乎没有差别。但对于包含更多列的表格,从表格中检索val变得越来越昂贵,使“覆盖”索引更具吸引力。

查询

1)简单

SELECT DISTINCT ON (x.id)
       x.id, y.val
FROM   x
JOIN   y ON y.id <= x.id
ORDER  BY x.id, y.id DESC;

SQL Fiddle.

在此相关答案中使用DISTINCT的技术的更多解释:

我进行了一些测试,因为我怀疑第一个查询不能很好地扩展。它有一个小桌子很快,但没有更大的桌子。 Postgres不会优化计划,而是以(有限的)交叉加入开始,费用为O(N²)

2)快速

此查询仍然相当简单,并且可以很好地扩展:

SELECT x.id, y.val
FROM   x
JOIN  (SELECT *, lead(id, 1, 2147483647) OVER (ORDER BY id) AS next_id FROM y) y
       ON  x.id >= y.id
       AND x.id <  y.next_id
ORDER  BY 1;

窗口函数lead()是有用的。我使用该选项提供默认值来覆盖最后一行的角落情况:2147483647biggest possible integer。适应您的数据类型。

3)非常简单,几乎一样快

SELECT x.id
     ,(SELECT val FROM y WHERE id <= x.id ORDER BY id DESC, val LIMIT 1) AS val
FROM   x;

通常,相关子查询往往很慢。但是这个可以从(覆盖)指数中选择一个值,否则它就会变得如此简单以至于它可以竞争。

额外的ORDER BYval(大胆强调)似乎毫无意义。但添加它会使查询计划程序相信可以使用上面的多列索引y_mult_idx,因为排序顺序匹配。注意

  

仅使用y_mult_idx进行扫描..

<{1>}输出中的

测试用例

经过热烈的辩论和多次更新后,我收集了到目前为止发布的所有查询,并制作了一个快速概述的测试用例。我只使用1000行,因此SQLfiddle没有超时查询速度。但是前四名(Erwin 2,Clodoaldo,a_horse,Erwin 3)在我所有的本地测试中都是线性缩放的。 再次更新以包含我的最新添加内容,现在按性能改进格式和顺序:

Big SQL Fiddle comparing performance.

答案 1 :(得分:4)

select id,
       first_value(t.val) over (partition by group_flag order by t.id) as val
from (
  select x.id, 
         y.val, 
         sum(case
            when y.val is null then 0
            else 1
          end) over (order by x.id) as group_flag
  from x 
    left join y on x.id=y.id
) t    
order by id;

SQLFiddle示例:http://sqlfiddle.com/#!12/38903/1

答案 2 :(得分:4)

SQL Fiddle

select
    id, 
    first_value(val) over(partition by g order by id) val
from (
    select
        x.id, val, count(val) over(order by x.id) g
    from
        x
        left join
        y on x.id=y.id
) s
order by id

答案 3 :(得分:2)

SELECT
  m.id,
  y.val
FROM (
  SELECT
    x.id,
    MAX(y.id) id_y
  FROM
    x INNER JOIN y ON x.id >= y.id
  GROUP BY
    x.id
  ) m INNER JOIN y ON m.id_y = y.id
ORDER BY
  m.id

请参阅小提琴here

答案 4 :(得分:2)

我喜欢用(NOT)EXISTS表示集合函数MIN(),MAX()或closer_to()

SELECT x.id, y.val
FROM   x
JOIN   y ON y.id <= x.id
WHERE NOT EXISTS (SELECT *
        FROM y y2
        WHERE y2.id <= x.id -- same condition AS main query
        AND y2.id > y.id    -- but closer to x.id
        )
        ;

我的直觉是,这将产生与Erwin的答案完全相同的查询计划。

答案 5 :(得分:1)

使用COALESCE和子查询作为逻辑。

子查询将检索最后一个val值。

试试这个:

SELECT x1.id,
       coalesce(y1.val, (SELECT val
                       FROM   y
                       WHERE  id = (SELECT Max(x2.id)
                                    FROM   x AS x2
                                           JOIN y AS y2
                                             ON x2.id = y2.id
                                    WHERE  x2.id < x1.id)))
FROM   x AS x1
       LEFT JOIN y AS y1
              ON x1.id = y1.id
ORDER  BY x1.id;  

sqlfiddle:http://www.sqlfiddle.com/#!12/42526/1

答案 6 :(得分:0)

我不确定如何使用单个存储过程实现此目的。类似于以下的逻辑将返回所需的结果

create PROCEDURE GetData
AS
BEGIN
    Declare @resultTable TABLE(
    id int,
    value varchar(10))

    Declare @id int
    Declare @previousValue varchar(10)
    Declare @currentValue varchar(10)

    DECLARE x_cursor CURSOR
    FOR SELECT id FROM x order by id  

    OPEN x_cursor
    FETCH NEXT FROM x_cursor into @id;
    WHILE (@@FETCH_STATUS = 0)
    BEGIN
        select @currentValue = isnull(val,@previousValue)  from Y where id = @id
        insert into @resultTable values(@id,@currentValue)
        set @previousValue = @currentValue 
        FETCH NEXT FROM x_cursor into @id;
    END


    Close x_cursor
    Deallocate x_cursor

    select * from @resultTable
END
GO