【问题标题】:How to structure SQL - select first X rows for each value of a column?如何构造 SQL - 为列的每个值选择前 X 行?
【发布时间】:2012-09-24 05:24:08
【问题描述】:

我有一个包含以下类型数据的表:

create table store (
    n_id             serial not null primary key,
    n_place_id       integer not null references place(n_id),
    dt_modified      timestamp not null,
    t_tag            varchar(4),
    n_status         integer not null default 0
    ...
    (about 50 more fields)
);

在下面的查询中使用的 n_id、n_place_id、dt_modified 和所有其他字段都有索引。

此表目前包含大约 100,000 行,但可能会增长到接近一百万甚至更多。然而,现在让我们假设我们保持在 100K 左右。

我正在尝试从这些表中选择满足一个两个条件的行:

  1. n_place_id 在特定子集中的所有行(这部分很简单);或
  2. 对于所有其他n_place_id 值,前十行按dt_modified 排序(这会变得更加复杂)。

在一个 SQL 中执行此操作似乎太痛苦了,因此我对为此使用存储函数感到满意。我的函数是这样定义的:

create or replace function api2.fn_api_mobile_objects()
  returns setof store as
$body$
declare
    maxres_free integer := 10;
    resulter    store%rowtype;
    mcnt        integer := 0;
    previd      integer := 0;
begin
    create temporary table paid on commit drop as
    select n_place_id from payments where t_reference is not null and now()::date between dt_paid and dt_valid;

    for resulter in
        select * from store where n_status > 0 and t_tag is not null order by n_place_id, dt_modified desc
    loop
        if resulter.n_place_id in (select n_place_id from paid) then
            return next resulter;
        else
            if previd <> resulter.n_place_id then
                mcnt := 0;
                previd := resulter.n_place_id;
            end if;

            if mcnt < maxres_free then
                return next resulter;
                mcnt := mcnt + 1;
            end if;
        end if;
    end loop;
end;$body$
  language 'plpgsql' volatile;

问题是

select * from api2.fn_api_mobile_objects()

执行大约需要 6-7 秒。考虑到在那之后这个结果集需要joined 到其他 3 个表,并应用了一堆附加条件并应用了进一步的排序,这显然是不可接受的。

好吧,我仍然需要获取这些数据,所以要么我在函数中遗漏了一些东西,要么我需要重新考虑整个算法。不管怎样,我需要这方面的帮助。

【问题讨论】:

  • 这里不需要函数或光标,恕我直言。我会尝试在一个带有窗口函数的子查询上加入(自我)。
  • if resulter.n_place_id in (select n_place_id from paid) then 将导致二次行为(临时表没有结构,因此每个“if”都会导致 seqscan/线性搜索。

标签: sql postgresql sorting query-optimization filtering


【解决方案1】:
CREATE TABLE store
    ( n_id             serial not null primary key
    , n_place_id       integer not null -- references place(n_id)
    , dt_modified      timestamp not null
    , t_tag            varchar(4)
    , n_status         integer not null default 0
        );
INSERT INTO store(n_place_id,dt_modified,n_status)
SELECT n,d,n%4
FROM generate_series(1,100) n
, generate_series('2012-01-01'::date ,'2012-10-01'::date, '1 day'::interval ) d
        ;

WITH zzz AS (
        SELECT n_id AS n_id
        , rank() OVER (partition BY n_place_id ORDER BY dt_modified) AS rnk
        FROM store
        )
SELECT st.*
FROM store st
JOIN zzz ON zzz.n_id = st.n_id
WHERE st.n_place_id IN ( 1,22,333)
OR zzz.rnk <=10
        ;

更新:这里是与子查询相同的 selfjoin 构造(CTE 被规划器处理有点不同):

SELECT st.*
FROM store st
JOIN ( SELECT sx.n_id AS n_id
        , rank() OVER (partition BY sx.n_place_id ORDER BY sx.dt_modified) AS zrnk
        FROM store sx
        ) xxx ON xxx.n_id = st.n_id
WHERE st.n_place_id IN ( 1,22,333)
OR xxx.zrnk <=10
        ;

【讨论】:

  • 谢谢。不幸的是,这比我在问题中显示的存储函数花费的时间要长得多。存储函数需要 6-7 秒,您的查询需要 11-12 秒。
  • 再想一想,窗口函数不需要压缩成 CTE。
  • 我试过你更新的 SQL - 它仍然需要大约 10-11 秒。我设法将其缩短到大约 1.3 秒(请参阅我自己的答案)。
【解决方案2】:

经过一番努力,我设法让存储函数在 1 秒多的时间内返回结果(这是一个巨大的改进)。现在函数看起来是这样的(我添加了附加条件,对性能影响不大):

create or replace function api2.fn_api_mobile_objects(t_search varchar)
  returns setof store as
$body$
declare
    maxres_free integer := 10;
    resulter    store%rowtype;
    mid     integer := 0;
begin
    create temporary table paid on commit drop as
    select n_place_id from payments where t_reference is not null and now()::date between dt_paid and dt_valid
    union
    select n_place_id from store where n_status > 0 and t_tag is not null group by n_place_id having count(1) <= 10;

    for resulter in
        select * from store
        where n_status > 0 and t_tag is not null
        and (t_name ~* t_search or t_description ~* t_search)
        and n_place_id in (select n_place_id from paid)
    loop
        return next resulter;
    end loop;

    for mid in
        select distinct n_place_id from store where n_place_id not in (select n_place_id from paid)
    loop
        for resulter in
            select * from store where n_status > 0 and t_tag is not null and n_place_id = mid order by dt_modified desc limit maxres_free
        loop
            return next resulter;
        end loop;
    end loop;

end;$body$
  language 'plpgsql' volatile;

这在我的本地机器上运行只需 1 秒多一点,而在现场运行大约需要 0.8-1.0 秒。就我的目的而言,这已经足够了,尽管我不确定随着数据量的增长会发生什么。

【讨论】:

  • 如果我理解正确,您设法避免了愚蠢的IF()。我仍然认为纯 SQL 会比这个过程代码执行得更好,但是在缺失的细节中可能隐藏了一些东西。
  • @wildpasser 老实说,没有其他遗漏的细节。我用不同数量的数据对其进行计时。行数低(在 2-3K 以下,纯 SQL 更快(但只有一点点)。随着行数的增长,存储函数成为赢家。我什至用 50 万行对其进行了测试:存储函数给了我结果在大约 3.5 秒内;纯 sql - 大约 30 秒。我认为这样做的原因是数据分区很多但不均匀。
  • HAVING COUNT() &lt; 10 的作用与您原来的问题不同。
  • @wildplasser 确实如此,但我分两部分进行:首先我选择那些少于 10 行的行 - 并为它们使用所有行;然后在第二个循环中,我对那些超过 10 个的循环使用内部循环
【解决方案3】:

作为一个简单的建议,我喜欢进行此类故障排除的方式是构建一个查询,使我能够顺利完成大部分任务,并对其进行适当优化,然后在其周围添加必要的 pl/pgsql 内容。这种方法的主要优点是您可以根据查询计划进行优化。

此外,如果您不处理大量行,array_agg() 和 unnest() 是您的朋友,因为它们允许您(在 Pg 8.4 及更高版本上!)省去临时表管理开销并简单地构造和查询内存中的元组数组作为关系。如果您只是在内存中访问数组而不是临时表,它也可能会执行得更好(更少的计划开销和更少的查询开销)。

此外,在您更新后的查询中,我会考虑用子查询或连接替换最终循环,从而允许规划器决定何时进行嵌套循环查找或何时尝试找到更好的方法。

【讨论】:

  • 感谢您的回答。相信我,我已经以数百种不同的方式进行了尝试,然后才确定了我在所有者答案中添加的最终功能。使用数组可能会更快,但我确实有很多数据。在带有n_place_id in (...) 的部分中,内部选择最多可以返回 30,000 个条目,尽管在大多数情况下约为 3,000 个。再加上这个调用是针对移动应用程序的 API 的事实 - 您最终可能会同时运行一百个这样的 API。因此临时表是更好的方法。
  • 我会对此进行测试以确定。临时表的内容很可能无论如何都会进入缓存,因此不确定它是否会比仅保留在私有进程的内存中执行更好的内存或并发性。我习惯于传递一个 2x1000 的文本字符串数组,相比之下,这听起来可能不是那么多,但我很确定这些文本字符串占用的内存比 10 倍那么多的整数还要多。
  • 我肯定会测试它,但我还需要仔细分析内存。即使按照最低标准,对于 64 位整数(它是一个 64 位系统),一个进程也需要大约 30K 来存储 3,000 个值的数组。如果我有一百个同时处理的进程,那将变成 3M - 仍然很低。但如果价值增长呢?我不反对这个想法,只是想小心内存使用。
猜你喜欢
  • 2012-04-04
  • 1970-01-01
  • 1970-01-01
  • 2013-04-04
  • 2012-12-11
  • 2010-11-29
  • 2017-12-13
  • 2012-07-28
  • 1970-01-01
相关资源
最近更新 更多