PostgreSQL:根据排序顺序选择最近的行

Posted

技术标签:

【中文标题】PostgreSQL:根据排序顺序选择最近的行【英文标题】:PostgreSQL: select nearest rows according to sort order 【发布时间】:2012-03-24 13:38:26 【问题描述】:

我有一张这样的桌子:

     a    |  user_id
----------+-------------
  0.1133  |  2312882332
  4.3293  |  7876123213
  3.1133  |  2312332332
  1.3293  |  7876543213
  0.0033  |  2312222332
  5.3293  |  5344343213
  3.2133  |  4122331112
  2.3293  |  9999942333

我想找到一个特定的行 - 例如1.3293 | 7876543213 - 并选择最近的 4 行。 2 上,2 下,如果可能的话。 排序顺序是ORDER BY a ASC

在这种情况下,我会得到:

  0.0033  |  2312222332
  0.1133  |  2312882332
  2.3293  |  9999942333
  3.1133  |  2312332332

如何使用 PostgreSQL 实现这一点? (顺便说一句,我正在使用 php。)

P.S.:对于最后一行或第一行,最近的行将高于 4 或低于 4。

【问题讨论】:

您是否尝试过在窗口上进行自联接->排名? 【参考方案1】:

测试用例:

CREATE TEMP TABLE tbl(a float, user_id bigint);
INSERT INTO tbl VALUES
 (0.1133, 2312882332)
,(4.3293, 7876123213)
,(3.1133, 2312332332)
,(1.3293, 7876543213)
,(0.0033, 2312222332)
,(5.3293, 5344343213)
,(3.2133, 4122331112)
,(2.3293, 9999942333);

查询:

WITH x AS (
    SELECT a
          ,user_id
          ,row_number() OVER (ORDER BY a, user_id) AS rn
    FROM   tbl
    ), y AS (
    SELECT rn, LEAST(rn - 3, (SELECT max(rn) - 5 FROM x)) AS min_rn
    FROM   x
    WHERE  (a, user_id) = (1.3293, 7876543213)
    )
SELECT *
FROM   x, y
WHERE  x.rn  > y.min_rn
AND    x.rn <> y.rn
ORDER  BY x.a, x.user_id
LIMIT  4;

返回问题中描述的结果。假设(a, user_id) 是唯一的。

尚不清楚a 是否应该是唯一的。这就是为什么我另外按user_id 排序以打破平局。这也是我使用window function row_number() 的原因,而不是not rank()row_number() 在任何情况下都是正确的工具。我们想要 4 行。如果排序顺序中有对等点,rank() 将给出未定义的行数。

只要表中至少有 5 行,此总是会返回 4 行。接近第一行/最后一行,返回前/后4行。在所有其他情况下,之前/之后的两行。条件行本身被排除在外。


提高性能

这是@Tim Landscheidt 所发布内容的改进版本。如果您喜欢索引的想法,请投票支持他的答案。不要打扰小桌子。但会提高 大表 的性能 - 只要您有合适的索引。最佳选择是multicolumn index 上的(a, user_id)

WITH params(_a, _user_id) AS (SELECT 5.3293, 5344343213) -- enter params once
    ,x AS  (
    (
    SELECT a
          ,user_id
          ,row_number() OVER (ORDER BY a DESC, user_id DESC) AS rn
    FROM   tbl, params p
    WHERE  a < p._a
       OR  a = p._a AND user_id < p._user_id -- a is not defined unique
    ORDER  BY a DESC, user_id DESC
    LIMIT  5  -- 4 + 1: including central row
    )
    UNION ALL -- UNION right away, trim one query level
    (
    SELECT a
          ,user_id
          ,row_number() OVER (ORDER BY a ASC, user_id ASC) AS rn
    FROM   tbl, params p
    WHERE  a > p._a
       OR  a = p._a AND user_id > p._user_id
    ORDER  BY a ASC, user_id ASC
    LIMIT  5
    )
    )
    , y AS (
    SELECT a, user_id
    FROM   x, params p
    WHERE (a, user_id) <> (p._a, p._user_id) -- exclude central row
    ORDER  BY rn  -- no need to ORDER BY a
    LIMIT  4
    )
SELECT *
FROM   y
ORDER  BY a, user_id   -- ORDER result as requested

与@Tim 版本的主要区别:

根据问题(a, user_id) 形成搜索条件,而不仅仅是a。这会以略微不同的方式更改窗口框架、ORDER BYWHERE 子句。

UNION 立即,无需额外的查询级别。您需要在两个 UNION 查询周围加上括号,以允许单独的 ORDER BY

按要求对结果进行排序。需要另一个查询级别(几乎没有任何成本)。

由于参数在多个地方使用,我将输入集中在一个领先的 CTE 中。 对于重复使用,您可以几乎“按原样”将此查询包装到 SQL 或 plpgsql 函数中。

【讨论】:

@wildplasser:美在于旁观者的眼中。 :) 不过,您的美丽有一些瑕疵:1) 返回 4,而不是 5 行,排除标准行。 2) 使用row_number(),而不是rank()。我为我的答案添加了解释。 3)您的查询因第一行/最后一行的极端情况而失败。试试WHERE this.val = 5.3293 4) 你的WHERE 子句只过滤a,问题将(a, user_id) 定义为过滤器。目前尚不清楚这是否会有所作为。 我没有看到 OP 不想要中间行。您对 row_number 与 rank 的事情是对的,当然,如果有关系,它们的行为会有所不同。 (需要在排序中增加一列来确定性地解决这个问题)我会修复它。 WITH 位会不会查询整个表? @TimLandscheidt:它会而且它需要。最终选择将仅返回 4 行。如果我们有启发式信息,例如“在 +/- 0.1 的范围内总是有 4 行”,那么我们可以在 WITH 子句中使用索引和预选 - 可能会更快...... @ErwinBrandstetter 但是为什么不在 a 上使用索引呢?有关示例,请参见我的答案。想要的行必须是选择行的前四行和后四行的并集的一部分,不必查询整个表。【参考方案2】:

还有一个:

WITH prec_rows AS
  (SELECT a,
          user_id,
          ROW_NUMBER() OVER (ORDER BY a DESC) AS rn
   FROM tbl
   WHERE a < 1.3293
   ORDER BY a DESC LIMIT 4),
     succ_rows AS
  (SELECT a,
          user_id,
          ROW_NUMBER() OVER (ORDER BY a ASC) AS rn
   FROM tbl
   WHERE a > 1.3293
   ORDER BY a ASC LIMIT 4)
SELECT a, user_id
FROM
  (SELECT a,
          user_id,
          rn
   FROM prec_rows
   UNION ALL SELECT a,
                    user_id,
                    rn
   FROM succ_rows) AS s
ORDER BY rn, a LIMIT 4;

AFAIR WITH 将实例化一个内存表,因此该解决方案的重点是尽可能限制其大小(在本例中为八行)。

【讨论】:

+1 好点。您的解决方案在使用大表和合适的索引时会表现得更好。我对您的查询有一些不适合发表评论的问题。我在答案中添加了一个版本。 @TimLandscheidt 我使用了SELECT a, user_id FROM prec_rows LIMIT 4-(SELECT count(1) FROM succ_rows)/2 UNION ALL [same, but for succ_rows]。但你的可能更快。【参考方案3】:
set search_path='tmp';

DROP TABLE lutser;
CREATE TABLE lutser
        ( val float
        , num bigint
        );
INSERT INTO lutser(val, num)
VALUES ( 0.1133  ,  2312882332  )
      ,( 4.3293  ,  7876123213  )
      ,( 3.1133  ,  2312332332  )
      ,( 1.3293  ,  7876543213  )
      ,( 0.0033  ,  2312222332  )
      ,( 5.3293  ,  5344343213  )
      ,( 3.2133  ,  4122331112  )
      ,( 2.3293  ,  9999942333  )
        ;

WITH ranked_lutsers AS (
        SELECT val, num
        ,rank() OVER (ORDER BY val) AS rnk
        FROM lutser
        )
SELECT that.val, that.num
        , (that.rnk-this.rnk) AS relrnk
FROM ranked_lutsers that
JOIN ranked_lutsers this ON (that.rnk BETWEEN this.rnk-2 AND this.rnk+2)
WHERE this.val = 1.3293
        ;

结果:

DROP TABLE
CREATE TABLE
INSERT 0 8
  val   |    num     | relrnk 
--------+------------+--------
 0.0033 | 2312222332 |     -2
 0.1133 | 2312882332 |     -1
 1.3293 | 7876543213 |      0
 2.3293 | 9999942333 |      1
 3.1133 | 2312332332 |      2
(5 rows)

正如 Erwin 指出的,输出中不需要中间行。此外,应该使用 row_number() 而不是 rank()。

WITH ranked_lutsers AS (
        SELECT val, num
        -- ,rank() OVER (ORDER BY val) AS rnk
        , row_number() OVER (ORDER BY val, num) AS rnk
        FROM lutser
) SELECT that.val, that.num
        , (that.rnk-this.rnk) AS relrnk
FROM ranked_lutsers that
JOIN ranked_lutsers this ON (that.rnk BETWEEN this.rnk-2 AND this.rnk+2 )
WHERE this.val = 1.3293
AND that.rnk <> this.rnk
        ;

结果2:

  val   |    num     | relrnk 
--------+------------+--------
 0.0033 | 2312222332 |     -2
 0.1133 | 2312882332 |     -1
 2.3293 | 9999942333 |      1
 3.1133 | 2312332332 |      2
(4 rows)

UPDATE2:始终选择四个,即使我们位于列表的顶部或底部。这使得查询有点难看。 (但不像欧文那样丑 ;-)

WITH ranked_lutsers AS (
        SELECT val, num
        -- ,rank() OVER (ORDER BY val) AS rnk
        , row_number() OVER (ORDER BY val, num) AS rnk
        FROM lutser
) SELECT that.val, that.num
        , ABS(that.rnk-this.rnk) AS srtrnk
        , (that.rnk-this.rnk) AS relrnk
FROM ranked_lutsers that
JOIN ranked_lutsers this ON (that.rnk BETWEEN this.rnk-4 AND this.rnk+4 )
-- WHERE this.val = 1.3293
WHERE this.val = 0.1133
AND that.rnk <> this.rnk
ORDER BY srtrnk ASC
LIMIT 4
        ;

输出:

  val   |    num     | srtrnk | relrnk 
--------+------------+--------+--------
 0.0033 | 2312222332 |      1 |     -1
 1.3293 | 7876543213 |      1 |      1
 2.3293 | 9999942333 |      2 |      2
 3.1133 | 2312332332 |      3 |      3
(4 rows)

更新:具有嵌套 CTE 的版本(具有外连接!!!)。为方便起见,我在表中添加了一个主键,恕我直言,这听起来是个好主意。

WITH distance AS (
        WITH ranked_lutsers AS (
        SELECT id
        , row_number() OVER (ORDER BY val, num) AS rnk
        FROM lutser
        ) SELECT l0.id AS one
        ,l1.id AS two
        , ABS(l1.rnk-l0.rnk) AS dist
        -- Warning: Cartesian product below
        FROM ranked_lutsers l0
        , ranked_lutsers l1 WHERE l0.id <> l1.id

        )
SELECT lu.*
FROM lutser lu
JOIN distance di
ON lu.id = di.two
WHERE di.one= 1
ORDER by di.dist
LIMIT 4 
        ;

【讨论】:

您仍然缺少角落案例。试试WHERE this.val = 5.3293 看看问题的最后一行.. Tnx。反正我从不喜欢花车上的选择......但是额外的条件会让我的查询和你的一样丑! 尽可能的美丽,但也必须尽可能的丑陋。很像生活。 ;)

以上是关于PostgreSQL:根据排序顺序选择最近的行的主要内容,如果未能解决你的问题,请参考以下文章

PostgreSQL 行排序详解

如何编辑我的 postgreSQL 查询以按日期选择几列的最新行

根据最近的日期从多个相似的行中选择一行

SQL Server - 如何将 RANK 函数插入到已按排名顺序排序的行中?

如何选择在两列中具有相同值集的行,从而连接第三列中的值?

R语言选择特定的行,对某一列排序