sql - 在单个查询中获取分页行和总数
问题描述
核心要求:为
指定过滤条件、、. 可能有更多这样的过滤器,但无论如何,按提交日期返回最新的逻辑是相同的。两种主要用途,一种用于在 UI 中进行分页查看,另一种用于生成报告。person_id
submission_date
type
plan
status
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 。计数查询中包含 all 的联合null
可能适用于组合,但不确定。
我想将这两者组合成 1 个查询的主要原因是表很大并且窗口函数很昂贵。目前我使用单独的查询,它们基本上都运行相同的查询两次。
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
附加信息areEN
和CR
with的可能值EN
约占数据的 70%。表格列宽select avg_width from pg_stats where tablename='mytable';
共有 374 列,共 41 列,因此每列约 9。
这个想法是向用户预先显示一些页面,然后他们可以通过其他参数进行过滤,例如file_name
(每个文件通常有大约 5k 个条目)、type
(非常低的基数)、member_add_id
(高基数)、plan_id
(低基数,每 500k 到一百万个条目将与计划 ID 相关联)。在所有情况下,业务要求是只显示某组计划 ID 的最新记录submission_date
(对于每年完成的报告)。按 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
解决方案
首先要做的事情是:您可以在同一个查询中多次使用 CTE 的结果,这是 CTE 的主要功能。)您所拥有的将像这样工作(同时仍然只使用一次 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_id
with可以返回多行rank = 1
。 DISTINCT ON (person_id)
(如 Gordon 提供的)是适用的替代品row_number()
- 它适用于您,因为附加信息已澄清。看:
警告 2:ORDER BY submission_date DESC
既没有submission_date
也没有last_updated
定义NOT NULL
。ORDER BY submission_date DESC, last_updated DESC ...
See可能是个问题:
这些列真的应该是NOT NULL
吗?
你回复:
是的,所有这些列都应该是非空的。我可以添加该约束。我把它设置为可为空的,因为我们在文件中获取的数据并不总是完美的。但这是非常罕见的情况,我可以输入空字符串。
type 不允许使用空字符串date
。保持列可以为空。NULL
是这些情况下的正确值。按照演示使用NULLS LAST
以避免NULL
被排在最前面。
警告 3:OFFSET
如果OFFSET
等于或大于 CTE 返回的行数,则没有 row,因此也没有总计数。看:
临时解决方案
解决到目前为止的所有警告,并根据添加的信息,我们可能会得出以下查询:
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
保证,无论OFFSET
. DISTINCT ON
应该对基本查询中仅有的几行执行 OK-ish (person_id)
。
但是你有很宽的行。平均多宽?该查询可能会导致对整个表进行顺序扫描。索引无济于事(很多)。所有这些对于分页来说仍然非常低效。看:
您不能涉及用于分页的索引,因为它基于来自 CTE 的派生表。而且您的实际分页排序标准仍不清楚(ORDER BY id
?)。如果分页是目标,那么您迫切需要不同的查询样式。如果您只对前几页感兴趣,则需要不同的查询样式。最好的解决方案取决于问题中仍然缺少的信息......
从根本上更快
对于您更新的目标:
查找作者的最新
person_id
条目submission_date
(为简单起见,忽略“针对指定的过滤条件、类型、计划、状态”。)
和:
person_id
仅在具有的情况下查找最新行status IN ('ACCEPTED','CORRECTED')
基于这两个专业指标:
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
应该很可能是 type timestamptz
or date
,而不是- 在任何情况下,这在 Postgres 中都是一个奇怪的类型定义。看:character varying(255)
可能会优化更多细节,但这已经失控了。你可以考虑专业咨询。
推荐阅读
- machine-learning - 产品名称识别/分类
- ruby - 如何在 ruby 脚本中使用 awscli?
- c# - 调用 REST API,我在控制台内收到“未收到数据”消息,但能够进行身份验证
- asp.net-mvc - @Html.DisplayNameFor 子字符串?
- reactjs - 用于动态设置 css-classes 的自定义 react-hook
- powershell - 如何将 Device\HarddiskVolume[n] 与驱动器号相关联
- python - Python 中如何定义分类变量的级别?
- swift - 为什么这在一个地方有效,而在另一个地方无效?
- python - 尝试在 Azure 中使用 matplotlib 时出现“ModuleNotFoundError: No module named 'tkinter'”
- 7zip - 如何解压缩到包含名称中的点的文件夹中