PgSQL · 实战经验 · 分组TOP性能提升44倍

简介: 业务背景按分组取出TOP值,是非常常见的业务需求。比如提取每位歌手的下载量TOP 10的曲目、提取每个城市纳税前10的人或企业。传统方法传统的方法是使用窗口查询,PostgreSQL是支持窗口查询的。例子测试表和测试数据,生成10000个分组,1000万条记录。postgres=# create table tbl(c1 int, c2 int, c3 int);CREA

业务背景

按分组取出TOP值,是非常常见的业务需求。

比如提取每位歌手的下载量TOP 10的曲目、提取每个城市纳税前10的人或企业。

传统方法

传统的方法是使用窗口查询,PostgreSQL是支持窗口查询的。

例子

测试表和测试数据,生成10000个分组,1000万条记录。

postgres=# create table tbl(c1 int, c2 int, c3 int);
CREATE TABLE
postgres=# create index idx1 on tbl(c1,c2);
CREATE INDEX
postgres=# insert into tbl select mod(trunc(random()*10000)::int, 10000), trunc(random()*10000000) from generate_series(1,10000000);
INSERT 0 10000000

使用窗口查询的执行计划

postgres=# explain select * from (select row_number() over(partition by c1 order by c2) as rn,* from tbl) t where t.rn<=10;
                                       QUERY PLAN                                       
----------------------------------------------------------------------------------------
 Subquery Scan on t  (cost=0.43..770563.03 rows=3333326 width=20)
   Filter: (t.rn <= 10)
   ->  WindowAgg  (cost=0.43..645563.31 rows=9999977 width=12)
         ->  Index Scan using idx1 on tbl  (cost=0.43..470563.72 rows=9999977 width=12)
(4 rows)

使用窗口查询的结果举例

postgres=# select * from (select row_number() over(partition by c1 order by c2) as rn,* from tbl) t where t.rn<=10;
 rn |  c1  |   c2   | c3 
----+------+--------+----
  1 |    0 |   1657 |   
  2 |    0 |   3351 |   
  3 |    0 |   6347 |   
  4 |    0 |  12688 |   
  5 |    0 |  16991 |   
  6 |    0 |  19584 |   
  7 |    0 |  24694 |   
  8 |    0 |  36646 |   
  9 |    0 |  40882 |   
 10 |    0 |  41599 |   
  1 |    1 |  14465 |   
  2 |    1 |  29032 |   
  3 |    1 |  39969 |   
  4 |    1 |  41094 |   
  5 |    1 |  69481 |   
  6 |    1 |  70919 |   
  7 |    1 |  75575 |   
  8 |    1 |  81102 |   
  9 |    1 |  87496 |   
 10 |    1 |  90603 |   
......

使用窗口查询的效率,20.1秒

postgres=# explain (analyze,verbose,costs,timing,buffers) select * from (select row_number() over(partition by c1 order by c2) as rn,* from tbl) t where t.rn<=10;
                                                                     QUERY PLAN                                                                     
----------------------------------------------------------------------------------------------------------------------------------------------------
 Subquery Scan on t  (cost=0.43..770563.03 rows=3333326 width=20) (actual time=0.040..20813.469 rows=100000 loops=1)
   Output: t.rn, t.c1, t.c2, t.c3
   Filter: (t.rn <= 10)
   Rows Removed by Filter: 9900000
   Buffers: shared hit=10035535
   ->  WindowAgg  (cost=0.43..645563.31 rows=9999977 width=12) (actual time=0.035..18268.027 rows=10000000 loops=1)
         Output: row_number() OVER (?), tbl.c1, tbl.c2, tbl.c3
         Buffers: shared hit=10035535
         ->  Index Scan using idx1 on public.tbl  (cost=0.43..470563.72 rows=9999977 width=12) (actual time=0.026..11913.677 rows=10000000 loops=1)
               Output: tbl.c1, tbl.c2, tbl.c3
               Buffers: shared hit=10035535
 Planning time: 0.110 ms
 Execution time: 20833.747 ms
(13 rows)

雕虫小技

如何优化?

可以参考我之前写的,使用递归查询,优化count distinct的方法

本文同样需要用到递归查询,获得分组ID

postgres=# with recursive t1 as (
postgres(#  (select min(c1) c1 from tbl )
postgres(#   union all
postgres(#  (select (select min(tbl.c1) c1 from tbl where tbl.c1>t.c1) c1 from t1 t where t.c1 is not null)
postgres(# )
postgres-# select * from t1;

写成SRF函数,如下

postgres=# create or replace function f() returns setof tbl as $$
postgres$# declare
postgres$#   v int;
postgres$# begin
postgres$#   for v in with recursive t1 as (                                                                           
postgres$#    (select min(c1) c1 from tbl )                                                                   
postgres$#     union all                                                                                      
postgres$#    (select (select min(tbl.c1) c1 from tbl where tbl.c1>t.c1) c1 from t1 t where t.c1 is not null) 
postgres$#   )                                                                                                
postgres$#   select * from t1
postgres$#   LOOP
postgres$#     return query select * from tbl where c1=v order by c2 limit 10;
postgres$#   END LOOP;
postgres$# return;
postgres$# 
postgres$# end;
postgres$# $$ language plpgsql strict;
CREATE FUNCTION

优化后的查询结果例子

postgres=# select * from f();
  c1  |   c2   | c3 
------+--------+----
    0 |   1657 |   
    0 |   3351 |   
    0 |   6347 |   
    0 |  12688 |   
    0 |  16991 |   
    0 |  19584 |   
    0 |  24694 |   
    0 |  36646 |   
    0 |  40882 |   
    0 |  41599 |   
    1 |  14465 |   
    1 |  29032 |   
    1 |  39969 |   
    1 |  41094 |   
    1 |  69481 |   
    1 |  70919 |   
    1 |  75575 |   
    1 |  81102 |   
    1 |  87496 |   
    1 |  90603 |   
......

优化后,只需要464毫秒返回10000个分组的TOP 10。

postgres=# explain (analyze,verbose,timing,costs,buffers) select * from f();
                                                     QUERY PLAN                                                      
---------------------------------------------------------------------------------------------------------------------
 Function Scan on public.f  (cost=0.25..10.25 rows=1000 width=12) (actual time=419.218..444.810 rows=100000 loops=1)
   Output: c1, c2, c3
   Function Call: f()
   Buffers: shared hit=170407, temp read=221 written=220
 Planning time: 0.037 ms
 Execution time: 464.257 ms
(6 rows)

小结

  1. 传统的方法使用窗口查询,输出多个每个分组的TOP 10,需要扫描所有的记录。效率较低。

  2. 由于分组不是非常多,只有10000个,所以可以选择使用递归的方法,用上索引取TOP 10,速度非常快。

  3. 目前PostgreSQL的递归语法不支持递归的启动表写在subquery里面,也不支持启动表在递归查询中使用order by,所以不能直接使用递归得出结果,目前需要套一层函数。

目录
相关文章
|
网络协议
HTTP的连接方式——持久连接和非持久连接
HTTP的连接方式——持久连接和非持久连接
2201 0
|
10月前
|
应用服务中间件 PHP nginx
今日小结通过aliyun的本地容器镜像部署我的nginx和php环境
简介: 本教程介绍如何基于 Dragonwell 的 Ubuntu 镜像创建一个运行 Nginx 的 Docker 容器。首先从阿里云容器镜像服务拉取基础镜像,然后编写 Dockerfile 确保 Nginx 作为主进程运行,并暴露 80 端口。最后,在包含 Dockerfile 的目录下构建自定义镜像并启动容器,确保 Nginx 在前台运行,避免容器启动后立即退出。通过 `docker build` 和 `docker run` 命令完成整个流程。
396 25
今日小结通过aliyun的本地容器镜像部署我的nginx和php环境
|
5月前
|
机器学习/深度学习 人工智能 PyTorch
零基础入门CNN:聚AI卷积神经网络核心原理与工业级实战指南
卷积神经网络(CNN)通过局部感知和权值共享两大特性,成为计算机视觉的核心技术。本文详解CNN的卷积操作、架构设计、超参数调优及感受野计算,结合代码示例展示其在图像分类、目标检测等领域的应用价值。
320 7
|
人工智能 搜索推荐
PersonaCraft:首尔国立大学推出的单参考图像生成多身份全身图像技术
PersonaCraft是由首尔国立大学推出的创新技术,能够从单一参考图像生成多个人物的逼真全身图像。该技术结合了扩散模型和3D人类建模,有效处理人物间的遮挡问题,并支持用户自定义身体形状调整,为多人图像合成树立了新标准。
317 9
PersonaCraft:首尔国立大学推出的单参考图像生成多身份全身图像技术
|
应用服务中间件 Linux nginx
nginx + geoip2简明配置
有时候需要nginx结合IP地址库做一些事情,比如过滤掉某个国家的IP,该如何做呢?
|
存储 关系型数据库 MySQL
mysql 8.0 的 建表 和八种 建表引擎实例
mysql 8.0 的 建表 和八种 建表引擎实例
218 0
|
安全 算法 开发工具
【C 言专栏】基于 C 语言的嵌入式系统开发
【5月更文挑战第1天】本文探讨了C语言在嵌入式系统开发中的核心作用。嵌入式系统作为专用计算机系统广泛应用于家电、汽车、医疗等领域,具备实时性、低功耗等特点。C语言因其高效性、可移植性和灵活性成为开发首选。文章介绍了开发流程,包括需求分析、硬件选型、软件设计至部署维护,并强调中断处理、内存管理等关键技术。C语言在智能家居、汽车电子和医疗设备等领域的应用实例展示了其广泛影响力。面对硬件限制、实时性要求和安全挑战,开发者需不断优化和适应新技术趋势,以推动嵌入式系统创新发展。
483 0
【C 言专栏】基于 C 语言的嵌入式系统开发
|
jenkins Java 持续交付
实战指南:运用Jenkins实现Java项目的持续集成与自动化部署
【4月更文挑战第17天】Jenkins是一款强大的开源CI/CD工具,广泛用于Java项目的自动化构建、测试和部署。通过配置Jenkins环境、创建Job、设定构建触发器和步骤,实现持续集成和部署。集成Maven、Git等工具,确保代码质量并加速上线。持续部署阶段,Jenkins可将工件发布至远程服务器或云环境,实现高效、稳定的自动化流程。实践过程中,关注工作流优化、配置备份和服务器状态监控,以提升整体开发效率和软件质量。
872 1
|
存储 缓存 负载均衡
阿里云DNS常见问题之域名DNS跳转有问题如何解决
阿里云DNS(Domain Name System)服务是一个高可用和可扩展的云端DNS服务,用于将域名转换为IP地址,从而让用户能够通过域名访问云端资源。以下是一些关于阿里云DNS服务的常见问题合集:
|
存储 网络协议 安全
C/C++网络编程基础知识超详细讲解第一部分(系统性学习day11)
C/C++网络编程基础知识超详细讲解第一部分(系统性学习day11)