GROUP BY 一列,同时在 PostgreSQL 中按另一列排序

Fak*_*ame 9 postgresql performance postgresql-9.3 greatest-n-per-group query-performance

我怎么能在GROUP BY一个列中排序,而按另一列排序。

我正在尝试执行以下操作:

SELECT dbId,retreivalTime 
    FROM FileItems 
    WHERE sourceSite='something' 
    GROUP BY seriesName 
    ORDER BY retreivalTime DESC 
    LIMIT 100 
    OFFSET 0;
Run Code Online (Sandbox Code Playgroud)

我要选择的最后一个从FileItems / N /项,按降序排列,与过滤行DISTINCT的值seriesName。上面的查询出错了ERROR: column "fileitems.dbid" must appear in the GROUP BY clause or be used in an aggregate function。我需要该dbid值以便然后获取此查询的输出,并将JOIN其放在源表上以获取我所在的其余列。

请注意,这基本上是以下问题的格式塔,为了清楚起见,删除了许多无关的细节。


原始问题

我有一个要从 sqlite3 迁移到 PostgreSQL 的系统,因为我已经在很大程度上超出了 sqlite:

    SELECT
            d.dbId,
            d.dlState,
            d.sourceSite,
        [snip a bunch of rows]
            d.note

    FROM FileItems AS d
        JOIN
            ( SELECT dbId
                FROM FileItems
                WHERE sourceSite='{something}'
                GROUP BY seriesName
                ORDER BY MAX(retreivalTime) DESC
                LIMIT 100
                OFFSET 0
            ) AS di
            ON  di.dbId = d.dbId
    ORDER BY d.retreivalTime DESC;
Run Code Online (Sandbox Code Playgroud)

基本上,我想选择数据库中的最后n 个DISTINCT项目,其中不同的约束在一列上,排序顺序在不同的列上。

不幸的是,上面的查询虽然在 sqlite 中运行良好,但在 PostgreSQL 中却出现了错误 psycopg2.ProgrammingError: column "fileitems.dbid" must appear in the GROUP BY clause or be used in an aggregate function.

不幸的是,虽然添加dbId到 GROUP BY 子句修复了问题(例如GROUP BY seriesName,dbId),但这意味着对查询结果的不同过滤不再起作用,因为dbid是数据库主键,因此所有值都是不同的。

从阅读Postgres 文档来看,有SELECT DISTINCT ON ({nnn}),但这要求返回的结果按 排序{nnn}

因此,要通过 做我想做的事情SELECT DISTINCT ON,我必须查询 allDISTINCT {nnn}和他们的MAX(retreivalTime),然后按而不是再次排序,然后取最大的 100 并使用那些对表进行查询以获取其余的行,我我想避免,因为数据库在列中有 ~175K 行和 ~14K 不同值,我只想要最新的 100,并且这个查询对性能有些关键(我需要查询时间 < 1/2 秒)。retreivalTime{nnn}seriesName

我在这里的天真假设基本上是 DB 只需要按 的降序遍历每一行,retreivalTime一旦看到LIMIT项目就停止,所以全表查询是不理想的,但我不假装真正了解数据库系统在内部进行了优化,我可能完全错误地处理了这个问题。

FWIW,我偶尔会用不同的OFFSET值,但是长的查询时间,其中偏移>〜500是完全可以接受的情况下。基本上,这OFFSET是一种蹩脚的分页机制,它让我无需将滚动游标专用于每个连接,我可能会在某个时候重新访问它。


参考问题我一个月前问的导致这个查询


好的,更多注意事项:

    SELECT
            d.dbId,
            d.dlState,
            d.sourceSite,
        [snip a bunch of rows]
            d.note

    FROM FileItems AS d
        JOIN
            ( SELECT seriesName, MAX(retreivalTime) AS max_retreivalTime
                FROM FileItems
                WHERE sourceSite='{something}'
                GROUP BY seriesName
                ORDER BY max_retreivalTime DESC
                LIMIT %s
                OFFSET %s
            ) AS di
            ON  di.seriesName = d.seriesName AND di.max_retreivalTime = d.retreivalTime
    ORDER BY d.retreivalTime DESC;
Run Code Online (Sandbox Code Playgroud)

如描述的那样对查询正常工作,但如果我删除GROUP BY子句,它将失败(它在我的应用程序中是可选的)。

psycopg2.ProgrammingError: column "FileItems.seriesname" must appear in the GROUP BY clause or be used in an aggregate function

我想我从根本上不理解子查询在 PostgreSQL 中是如何工作的。我哪里错了?我的印象是子查询基本上只是一个内联函数,结果只是输入到主查询中。

Erw*_*ter 10

一致的行

这似乎并不在你的雷达又重要的问题:
从每个组行对于同seriesName,做你想做的列一个排,或只是任何多行值(这可能会或可能不会一起去)?

您的答案是后者,您将最大值dbid与最大值结合起来retreivaltime,后者可能来自不同的行。

要获得一致的行,请使用DISTINCT ON并将其包装在子查询中以对结果进行不同的排序:

SELECT * FROM (
   SELECT DISTINCT ON (seriesName)
          dbid, seriesName, retreivaltime
   FROM   FileItems
   WHERE  sourceSite = 'mk' 
   ORDER  BY seriesName, retreivaltime DESC NULLS LAST  -- latest retreivaltime
   ) sub
ORDER BY retreivaltime DESC NULLS LAST
LIMIT  100;
Run Code Online (Sandbox Code Playgroud)

详情DISTINCT ON

旁白:应该是retrievalTime,或者更好:retrieval_time。不带引号的混合大小写标识符是 Postgres 中常见的混淆来源。

使用 rCTE 提高性能

由于我们在这里处理一个大表,我们需要一个可以使用索引的查询,而上面的查询不是这种情况(除了WHERE sourceSite = 'mk'

仔细检查后,您的问题似乎是松散索引扫描的特例。Postgres 本身不支持松散索引扫描,但可以使用递归 CTE进行模拟。Postgres Wiki 中有一个简单案例代码示例。

关于 SO 的相关答案以及更高级的解决方案、解释、小提琴:

不过,您的情况更复杂。但我想我找到了一个变体来让它为你工作。建立在这个索引上(没有WHERE sourceSite = 'mk'

CREATE INDEX mi_special_full_idx ON MangaItems
(retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST, dbid)
Run Code Online (Sandbox Code Playgroud)

或(与WHERE sourceSite = 'mk'

CREATE INDEX mi_special_granulated_idx ON MangaItems
(sourceSite, retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST, dbid)
Run Code Online (Sandbox Code Playgroud)

第一个索引可用于两个查询,但对于附加的 WHERE 条件并不完全有效。第二个索引对于第一个查询的用途非常有限。由于您拥有查询的两个变体,请考虑创建两个索引。

dbid在最后添加以允许仅索引扫描

这个带有递归 CTE 的查询使用了索引。我使用Postgres 9.3进行了测试,它对我有用:没有顺序扫描,所有仅索引扫描:

WITH RECURSIVE cte AS (
   (
   SELECT dbid, seriesName, retreivaltime, 1 AS rn, ARRAY[seriesName] AS arr
   FROM   MangaItems
   WHERE  sourceSite = 'mk'
   ORDER  BY retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST
   LIMIT  1
   )
   UNION ALL
   SELECT i.dbid, i.seriesName, i.retreivaltime, c.rn + 1, c.arr || i.seriesName
   FROM   cte c
   ,      LATERAL (
      SELECT dbid, seriesName, retreivaltime
      FROM   MangaItems
      WHERE (retreivaltime, seriesName) < (c.retreivaltime, c.seriesName)
      AND    sourceSite = 'mk'  -- repeat condition!
      AND    seriesName <> ALL(c.arr)
      ORDER  BY retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST
      LIMIT  1
      ) i
   WHERE  c.rn < 101
   )
SELECT dbid
FROM   cte
ORDER  BY rn;
Run Code Online (Sandbox Code Playgroud)

需要包含seriesName in ORDER BY,因为retreivaltime它不是唯一的。“几乎”唯一仍然不是唯一的。

解释

  • 非递归查询从最新的行开始。

  • 递归查询添加下一个最新行,其中 aseriesName不在列表中,等等,直到我们有 100 行。

  • 基本部分是JOIN条件(b.retreivaltime, b.seriesName) < (c.retreivaltime, c.seriesName)ORDER BY条款ORDER BY retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST。两者都匹配索引的排序顺序,这使得神奇的事情发生。

  • seriesName在数组中收集以排除重复项。的成本b.seriesName <> ALL(c.foo_arr)随着行数的增加而逐渐增加,但对于 100 行,它仍然很便宜。

  • 只是dbid按照评论中的说明返回。

部分索引的替代方案:

我们之前一直在处理类似的问题。这是一个基于部分索引和循环函数的高度优化的完整解决方案:

如果做得对,可能是最快的方式(物化视图除外)。但是比较复杂。

物化视图

由于您没有很多写入操作并且它们不是评论中所述的性能关键(应该在问题中),因此前 n 个预先计算的行保存在物化视图中,并在对底层表。而是将性能关键查询基于物化视图。

  • 可能只是最新 ​​1000 个dbid左右的“瘦”MV 。在查询中,连接到原始表。例如,如果内容有时会更新,但前 n 行可以保持不变。

  • 或者返回整行的“胖”mv。还更快。显然,需要更频繁地刷新。

此处此处的手册中的详细信息。


Fak*_*ame 6

好的,我已经阅读了更多文档,现在我至少对这个问题有了更好的理解。

基本上,正在发生的事情是聚合dbid的结果有多个可能的值GROUP BY seriesName。使用 SQLite 和 MySQL,显然数据库引擎只是随机选择一个(这在我的应用程序中绝对没问题)。

但是,PostgreSQL 更为保守,因此与其选择随机值,不如抛出错误。

使此查询工作的一种简单方法是将聚合函数应用于相关值:

SELECT MAX(dbid) AS mdbid, seriesName, MAX(retreivaltime) AS mrt
    FROM MangaItems 
    WHERE sourceSite='mk' 
    GROUP BY seriesName
    ORDER BY mrt DESC 
    LIMIT 100 
    OFFSET 0;
Run Code Online (Sandbox Code Playgroud)

这使得查询输出完全限定,并且查询现在可以工作。