如何构造SQL - 为列的每个值选择前X行?

时间:2012-10-03 09:45:30

标签: sql postgresql sorting query-optimization filtering

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

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排序的前十行(这是变得更复杂的地方)。
  3. 在一个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秒。考虑到之后,这个结果集需要join到其他3个表,并且应用了一系列附加条件并进行了进一步排序,这显然是不可接受的。

    好吧,我仍然需要获取这些数据,所以要么我缺少函数中的东西,要么我需要重新考虑整个算法。无论哪种方式,我都需要帮助。

3 个答案:

答案 0 :(得分: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
        ;

答案 1 :(得分:1)

经过多次努力,我设法让存储的函数在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秒。就我的目的而言,这已经足够好了,虽然我不确定随着数据量的增长会发生什么。

答案 2 :(得分:0)

作为一个简单的建议,我喜欢做这种故障排除的方法是构建一个查询,让我在那里大部分时间,并正确优化它,然后在它周围添加必要的pl / pgsql。这种方法的主要优点是您可以根据查询计划进行优化。

此外,如果你没有处理很多行,array_agg()和unnest()是你的朋友,因为他们允许你(在Pg 8.4及更高版本!)免除临时表管理开销并简单地构造和查询内存中的元组数组作为关系。如果你只是在内存而不是临时表中访问一个数组(计划开销更少,查询开销也更少),它也可能表现更好。

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