【问题标题】:Get paginated rows and total count in single query在单个查询中获取分页行和总数
【发布时间】:2020-07-24 09:00:03
【问题描述】:

核心要求:
通过submission_date 为指定的过滤条件typeplanstatus 查找person_id 的最新条目。可能有更多这样的过滤器,但无论如何,按提交日期返回最新的逻辑是相同的。两种主要用途,一种用于在 UI 中进行分页查看,另一种用于生成报告。

WITH cte AS (
  SELECT * FROM (
    SELECT my_table.*, rank() OVER (PARTITION BY person_id ORDER BY submission_date DESC, last_updated DESC, id DESC) FROM my_table
    )  rank_filter 
      WHERE RANK=1 AND status in ('ACCEPTED','CORRECTED') AND type != 'CR' AND h_plan_id IN (10000, 20000)
)
SELECT
SELECT count(id) FROM cte group by id,
SELECT * FROM cte limit 10 offset 0;

group by 也不适用于 CTE。计数查询中所有 null 的联合可能适用于组合,但不确定。

我想将这两个组合成一个查询的主要原因是因为表很大并且窗口函数很昂贵。目前我使用单独的查询,它们基本上都运行相同的查询两次。

Postgres 版本 12。

\d my_table;
                               Table "public.my_table"
                 Column   |            Type             | Collation | Nullable 
--------------------------+-----------------------------+-----------+----------
 id                       | bigint                      |           | not null 
 h_plan_id                | bigint                      |           | not null 
 h_plan_submitter_id      | bigint                      |           |          
 last_updated             | timestamp without time zone |           |          
 date_created             | timestamp without time zone |           |          
 modified_by              | character varying(255)      |           |          
 segment_number           | integer                     |           |          

 -- <bunch of other text columns>

 submission_date          | character varying(255)      |           |          
 person_id                | character varying(255)      |           |          
 status                   | character varying(255)      |           |          
 file_id                  | bigint                      |           | not null 
Indexes:
    "my_table_pkey" PRIMARY KEY, btree (id)
    "my_table_file_idx" btree (file_id)
    "my_table_hplansubmitter_idx" btree (h_plan_submitter_id)
    "my_table_key_hash_idx" btree (key_hash)
    "my_table_person_id_idx" btree (person_id)
    "my_table_segment_number_idx" btree (segment_number)
Foreign-key constraints:
    "fk38njesaryvhj7e3p4thqkq7pb" FOREIGN KEY (h_plan_id) REFERENCES health_plan(id) ON UPDATE CASCADE ON DELETE CASCADE
    "fk6by9668sowmdob7433mi3rpsu" FOREIGN KEY (h_plan_submitter_id) REFERENCES h_plan_submitter(id) ON UPDATE CASCADE ON DELETE CASCADE
    "fkb06gpo9ng6eujkhnes0eco7bj" FOREIGN KEY (file_id) REFERENCES x12file(id) ON UPDATE CASCADE ON DELETE CASCADE

附加信息 type 的可能值为 ENCR,其中 EN 约占数据的 70%。 表格列宽select avg_width from pg_stats where tablename='mytable'; 共有 374 列,共 41 列,因此每列约 9 列。

这个想法是先向用户显示一些页面,然后他们可以通过其他参数进行过滤,例如file_name(每个文件通常有大约 5k 个条目),type(非常低的基数),member_add_id(高基数) )、plan_id(低基数,每 500k 到 100 万个条目将与一个计划 ID 相关联)。在所有情况下,业务要求是仅显示 submission_date 的一组特定计划 ID 的最新记录(对于每年完成的报告)。按 id 排序只是防御性编码,同一天可以有多个条目,即使有人编辑了倒数第二个条目因此触及了last_updated 时间戳,我们只想显示相同数据的最后一个条目。这可能永远不会发生,可以删除。

用户可以使用这些数据生成 csv 报告。

下面右连接查询的解释结果:

 Nested Loop Left Join  (cost=554076.32..554076.56 rows=10 width=17092) (actual time=4530.914..4530.922 rows=10 loops=1)
   CTE cte
     ->  Unique  (cost=519813.11..522319.10 rows=495358 width=1922) (actual time=2719.093..3523.029 rows=422638 loops=1)
           ->  Sort  (cost=519813.11..521066.10 rows=501198 width=1922) (actual time=2719.091..3301.622 rows=423211 loops=1)
                 Sort Key: mytable.person_id, mytable.submission_date DESC NULLS LAST, mytable.last_updated DESC NULLS LAST, mytable.id DESC
                 Sort Method: external merge  Disk: 152384kB
                 ->  Seq Scan on mytable  (cost=0.00..54367.63 rows=501198 width=1922) (actual time=293.953..468.554 rows=423211 loops=1)
                       Filter: (((status)::text = ANY ('{ACCEPTED,CORRECTED}'::text[])) AND (h_plan_id = ANY ('{1,2}'::bigint[])) AND ((type)::text <> 'CR'::text))
                       Rows Removed by Filter: 10158
   ->  Aggregate  (cost=11145.56..11145.57 rows=1 width=8) (actual time=4142.116..4142.116 rows=1 loops=1)
         ->  CTE Scan on cte  (cost=0.00..9907.16 rows=495358 width=0) (actual time=2719.095..4071.481 rows=422638 loops=1)
   ->  Limit  (cost=20611.67..20611.69 rows=10 width=17084) (actual time=388.777..388.781 rows=10 loops=1)
         ->  Sort  (cost=20611.67..21850.06 rows=495358 width=17084) (actual time=388.776..388.777 rows=10 loops=1)
               Sort Key: cte_1.person_id
               Sort Method: top-N heapsort  Memory: 30kB
               ->  CTE Scan on cte cte_1  (cost=0.00..9907.16 rows=495358 width=17084) (actual time=0.013..128.314 rows=422638 loops=1)
 Planning Time: 0.369 ms
 JIT:
   Functions: 9
   Options: Inlining true, Optimization true, Expressions true, Deforming true
   Timing: Generation 1.947 ms, Inlining 4.983 ms, Optimization 178.469 ms, Emission 110.261 ms, Total 295.660 ms
 Execution Time: 4587.711 ms

【问题讨论】:

  • 请(始终)声明您的 Postgres 版本,并更准确地定义“总数”。你的意思是rank(),而不是row_number()? (因此每个person_id 可能有多个行。)显示数据类型和约束的实际CREATE TABLE 语句可能会有所帮助。
  • submission_datelast_updated 未定义 NOT NULL。这可能是您当前的ORDER BY 的问题。这些真的应该是NOT NULL吗?
  • 我可以将它们设为不可为空的列。
  • 您要返回哪些行,以哪种方式排序,有多少?您的问题中有很多零碎的信息,但对核心要求没有明确的定义。如果您希望每个person_id 的最新行只有在具有status in ('ACCEPTED','CORRECTED') 的情况下,那么您当前的方法一开始就不正确。您必须按status 过滤 为每个person_id 提取最新行...
  • 我现在更新了核心要求。 “如果你想要每个 person_id 的最新行,只有在 ('ACCEPTED','CORRECTED') 中具有状态”是的,这是正确的。

标签: sql postgresql common-table-expression window-functions


【解决方案1】:

首先要做的事:您可以在同一个查询中多次使用 CTE 的结果,这是一个主要的 feature of CTEs。)您所拥有的会像这样工作(虽然仍然只使用一次 CTE):

WITH cte AS (
   SELECT * FROM (
      SELECT *, row_number()  -- see below
                OVER (PARTITION BY person_id
                      ORDER BY submission_date DESC NULLS LAST  -- see below
                             , last_updated DESC NULLS LAST  -- see below
                             , id DESC) AS rn
      FROM  tbl
      ) sub
   WHERE  rn = 1
   AND    status IN ('ACCEPTED', 'CORRECTED')
   )
SELECT *, count(*) OVER () AS total_rows_in_cte
FROM   cte
LIMIT  10
OFFSET 0;  -- see below

警告 1:rank()

rank() 可以为每个person_idrank = 1 返回多行。 DISTINCT ON (person_id)(如 Gordon 提供的)是 row_number() 的适用替代品 - 对您有用,如附加信息澄清。见:

注意事项 2:ORDER BY submission_date DESC

submission_datelast_updated 均未定义 NOT NULL。可能是ORDER BY submission_date DESC, last_updated DESC ... 的问题请参阅:

这些列真的应该是NOT NULL吗?

你回复了:

是的,所有这些列都应该是非空的。我可以添加该约束。我把它设置为可为空的,因为我们在文件中获取的数据并不总是完美的。但这是非常罕见的情况,我可以输入空字符串。

date 类型不允许使用空字符串。保持列可以为空。 NULL 是这些情况的正确值。使用 NULLS LAST 来避免 NULL 被排在最前面。

注意事项 3:OFFSET

如果 OFFSET 等于或大于 CTE 返回的行数,您会得到无行,因此也没有总计数。见:

临时解决方案

解决到目前为止的所有警告,并根据添加的信息,我们可能会得出以下查询:

WITH cte AS (
   SELECT DISTINCT ON (person_id) *
   FROM   tbl
   WHERE  status IN ('ACCEPTED', 'CORRECTED')
   ORDER  BY person_id, submission_date DESC NULLS LAST, last_updated DESC NULLS LAST, id DESC
   )
SELECT *
FROM  (
   TABLE  cte
   ORDER  BY person_id  -- ?? see below
   LIMIT  10
   OFFSET 0
   ) sub
RIGHT  JOIN (SELECT count(*) FROM cte) c(total_rows_in_cte) ON true;

现在 CTE 实际上使用了两次。 RIGHT JOIN 保证我们得到总数,不管OFFSETDISTINCT ON 应该对基本查询中每个 (person_id) 的仅有几行执行 OK-ish。

但是你的行很宽。平均多宽?该查询可能会导致对整个表进行顺序扫描。索引无济于事(很多)。所有这些都将保持分页效率极低。见:

您不能涉及分页索引,因为它基于 CTE 的派生表。而且您的分页的实际排序标准仍然不清楚(ORDER BY id ?)。如果分页是目标,那么您迫切需要不同的查询样式。如果您只对前几页感兴趣,则需要不同的查询样式。最佳解决方案取决于问题中仍然缺少的信息...

速度更快

对于您更新的目标:

查找person_idsubmission_date 的最新条目

(为了简单起见,忽略“指定过滤条件、类型、计划、状态”。)

还有:

仅当 person_id 具有 status IN ('ACCEPTED','CORRECTED') 时才查找每个 person_id 的最新行

基于这两个专门的指数

CREATE INDEX ON tbl (submission_date DESC NULLS LAST, last_updated DESC NULLS LAST, id DESC NULLS LAST)
WHERE  status IN ('ACCEPTED', 'CORRECTED'); -- optional

CREATE INDEX ON tbl (person_id, submission_date DESC NULLS LAST, last_updated DESC NULLS LAST, id DESC NULLS LAST);

运行此查询:

WITH RECURSIVE cte AS (
   (
   SELECT t  -- whole row
   FROM   tbl t
   WHERE  status IN ('ACCEPTED', 'CORRECTED')
   AND    NOT EXISTS (SELECT FROM tbl
                      WHERE  person_id = t.person_id 
                      AND   (  submission_date,   last_updated,   id)
                          > (t.submission_date, t.last_updated, t.id)  -- row-wise comparison
                      )
   ORDER  BY submission_date DESC NULLS LAST, last_updated DESC NULLS LAST, id DESC NULLS LAST
   LIMIT  1
   )

   UNION ALL
   SELECT (SELECT t1  -- whole row
           FROM   tbl t1
           WHERE ( t1.submission_date, t1.last_updated, t1.id)
               < ((t).submission_date,(t).last_updated,(t).id)  -- row-wise comparison
           AND    t1.status IN ('ACCEPTED', 'CORRECTED')
           AND    NOT EXISTS (SELECT FROM tbl
                              WHERE  person_id = t1.person_id 
                              AND   (   submission_date,    last_updated,    id)
                                  > (t1.submission_date, t1.last_updated, t1.id)  -- row-wise comparison
                              )
           ORDER  BY submission_date DESC NULLS LAST, last_updated DESC NULLS LAST, id DESC NULLS LAST
           LIMIT  1)
   FROM   cte c
   WHERE  (t).id IS NOT NULL
   )
SELECT (t).*
FROM   cte
LIMIT  10
OFFSET 0;

这里的每一组括号都是必需的。

这种复杂程度应该通过使用给定的索引而不是顺序扫描来从根本上更快地检索相对较小的一组顶行。见:

submission_date 很可能应该是 timestamptzdate,而不是 character varying(255) - 在任何情况下,这在 Postgres 中都是一个奇怪的类型定义。见:

可能会优化更多细节,但这已经失控了。您可以考虑专业咨询。

【讨论】:

  • 重要警告!很高兴知道。我也更新了表创建信息。如果您有任何其他见解将很高兴听到!
  • 是的 row_number() 应该同样有效。如果使用,查询将如何变化?
  • 这已经很有帮助了.. 如果性能优化是您的目标,我们还需要更多。请参阅此处的说明:stackoverflow.com/tags/postgresql-performance/info。最重要的是:基数和数据分布。粗略地说,你的表中有多少行,每个person_id 有多少行,有多少接受,更正,...
  • @Abe:现在没时间了,今晚晚些时候我会回来检查更新。我认为这里有更多的潜力......
  • 我又添加了一个解决方案。
【解决方案2】:

您可以尝试简化查询:

SELECT DISTINCT ON (person_id) t.*
FROM my_table t
WHERE status in ('ACCEPTED', 'CORRECTED')
ORDER BY person_id, submission_date DESC, last_updated DESC, id DESC

我不确定 Postgres 是否足够聪明,可以在这种情况下使用 (person_id, submission_date DESC, last_updated DESC, id DESC, status) 上的索引,但值得一试。

您可以使用表达式上的索引来加速此操作:(status in ('ACCEPTED', 'CORRECTED'), person_id, submission_date DESC, last_updated DESC, id DESC)

编辑:

如果要按另一列排序,可以使用子查询:

SELECT t.*
FROM (SELECT DISTINCT ON (person_id) t.*
      FROM my_table t
      WHERE status in ('ACCEPTED', 'CORRECTED')
      ORDER BY person_id, submission_date DESC, last_updated DESC, id DESC
     ) t
ORDER BY submission_date DESC

【讨论】:

  • 有没有办法通过 person_id 绕过订单。最好先显示提交日期的最新记录。我试着把它放在第二位,没有用。
猜你喜欢
  • 2013-10-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-30
  • 2015-10-23
  • 1970-01-01
  • 1970-01-01
  • 2021-07-17
相关资源
最近更新 更多