【问题标题】:Improving OFFSET performance in PostgreSQL提高 PostgreSQL 中的 OFFSET 性能
【发布时间】:2011-09-30 21:27:54
【问题描述】:

我有一个表,我在 LIMIT 和 OFFSET 之前执行 ORDER BY 以便分页。

在 ORDER BY 列上添加索引会对性能产生巨大影响(当与较小的 LIMIT 结合使用时)。在一个 500,000 行的表上,我看到添加索引的性能提高了 10,000 倍,只要有一个小的 LIMIT。

但是,索引对高偏移量(即我分页中的后面页面)没有影响。这是可以理解的:b-tree 索引可以很容易地从头开始按顺序迭代而不是找到第 n 项。

计数 b 树索引 似乎有帮助,但我不知道 PostgreSQL 支持这些。还有其他解决方案吗?似乎针对大型 OFFSET 进行优化(尤其是在分页用例中)并不罕见。

不幸的是,PostgreSQL 手册只是说“被 OFFSET 子句跳过的行仍然必须在服务器内部计算;因此大的 OFFSET 可能效率低下。”

【问题讨论】:

    标签: database postgresql query-optimization


    【解决方案1】:

    我对“计数 b 树索引”一无所知,但我们在应用程序中为帮助解决此问题所做的一件事是将查询分成两部分,可能使用子查询。如果您已经这样做了,我很抱歉浪费您的时间。

    SELECT *
    FROM massive_table
    WHERE id IN (
        SELECT id
        FROM massive_table
        WHERE ...
        LIMIT 50
        OFFSET 500000
    );
    

    这里的优点是,虽然它仍然需要计算所有内容的正确顺序,但它不会对整行进行排序——仅对 id 列进行排序。

    【讨论】:

    • 使用 crosstab() 函数是一个很好的解决方案。我的第一个查询(限制 100,偏移量 0)持续了 14 毫秒,但最后一个查询(限制 100,偏移量 14900)持续了近 3 秒。使用此解决方案,我的所有查询都在 12 毫秒以上(!)
    • 这实际上是一个很好的解决方案,因为您受限于 LIMITOFFSET 分页,因为 UI 或复杂查询无法覆盖键集分页。我在一个包含三列的1e7 行的表上对一个有点复杂的查询进行了快速测试,该查询的偏移量为9e6。这种方法的速度提高了大约 270%。
    【解决方案2】:

    似乎优化了大 偏移量(尤其是在分页中 用例)并不是那么不寻常。

    这对我来说似乎有点不寻常。大多数人,大多数时候,似乎不会浏览很多页面。这是我支持的东西,但不会努力优化。

    但无论如何。 . .

    由于您的应用程序代码知道它已经看到了哪些有序值,它应该能够通过在 WHERE 子句中排除这些值来减少结果集并减少偏移量。假设您订购了单个列,并且按升序排序,您的应用代码可以存储页面上的最后一个值,然后以某种适当的方式将 AND your-ordered-column-name > last-value-seen 添加到 WHERE 子句中。

    【讨论】:

    • 它不一定知道它已经看到了什么,因为分页需要能够跳转到例如第 1000 页
    • 这可能是特定于应用程序的。 Google 允许您向前跳 9 页或向后跳 9 页,但不允许您只跳到第 1000 页。Google 似乎还对 URL 中的起始项目编号进行了编码,我想这可以用来减小结果的大小设置和偏移量的大小。
    • 这种访问模式的一个常见示例是拥有数千个帖子的论坛主题。用户跳转到偏移量 0 来阅读原帖,然后一些大的偏移量来阅读最新的回复,然后一些随机的偏移量来查看讨论中的兴趣点(比如深层链接或对自己帖子的回复)
    【解决方案3】:

    您可能需要一个计算索引。

    让我们创建一个表:

    create table sales(day date, amount real);
    

    并用一些随机的东西填充它:

    insert into sales 
        select current_date + s.a as day, random()*100 as amount
        from generate_series(1,20);
    

    按天索引,这里没什么特别的:

    create index sales_by_day on sales(day);
    

    创建行位置函数。还有其他方法,这个是最简单的:

    create or replace function sales_pos (date) returns bigint 
       as 'select count(day) from sales where day <= $1;' 
       language sql immutable;
    

    检查它是否有效(但不要在大型​​数据集上这样称呼它):

    select sales_pos(day), day, amount from sales;
    
         sales_pos |    day     |  amount  
        -----------+------------+----------
                 1 | 2011-07-08 |  41.6135
                 2 | 2011-07-09 |  19.0663
                 3 | 2011-07-10 |  12.3715
        ..................
    

    现在棘手的部分:添加另一个根据 sales_pos 函数值计算的索引:

    create index sales_by_pos on sales using btree(sales_pos(day));
    

    这里是你如何使用它。 5 是您的“偏移量”,10 是“限制”:

    select * from sales where sales_pos(day) >= 5 and sales_pos(day) < 5+10;
    
            day     | amount  
        ------------+---------
         2011-07-12 | 94.3042
         2011-07-13 | 12.9532
         2011-07-14 | 74.7261
        ...............
    

    它很快,因为当你这样调用它时,Postgres 使用索引中预先计算的值:

    explain select * from sales 
      where sales_pos(day) >= 5 and sales_pos(day) < 5+10;
    
                                        QUERY PLAN                                
        --------------------------------------------------------------------------
         Index Scan using sales_by_pos on sales  (cost=0.50..8.77 rows=1 width=8)
           Index Cond: ((sales_pos(day) >= 5) AND (sales_pos(day) < 15))
    

    希望对你有帮助。

    【讨论】:

    • select * from depesz blog: Pagination with fixed order 有一篇关于这项技术的长篇非常详细的博文
    • @Tometzky - 好主意!作为一项改进,我建议在分组列上使用窗口函数(仅限 9.0+)。
    • 太棒了。那么,现在每次向表中插入单个值时,它都会为表中的每个项目重新计算?
    • @KonstantineRybnikov 嗯.. 不,但你真的不需要重新计算索引,只要你严格按照日期顺序插入条目并且从不删除它们(无论如何这是个好主意)。在这种情况下,记录位置永远不会改变。
    • @MikeIvanov PostgreSql 使用这种优化吗? (是否只重新计算需要的)
    【解决方案4】:

    最近我解决了一个这样的问题,我写了一篇关于如何面对这个问题的博客。非常喜欢,希望对大家有帮助。 我使用惰性列表方法进行部分获取。我将查询的限制和偏移量或分页替换为手动分页。 在我的示例中,选择返回 1000 万条记录,我获取它们并将它们插入到“时态表”中:

    create or replace function load_records ()
    returns VOID as $$
    BEGIN
    drop sequence if exists temp_seq;
    create temp sequence temp_seq;
    insert into tmp_table
    SELECT linea.*
    FROM
    (
    select nextval('temp_seq') as ROWNUM,* from table1 t1
     join table2 t2 on (t2.fieldpk = t1.fieldpk)
     join table3 t3 on (t3.fieldpk = t2.fieldpk)
    ) linea;
    END;
    $$ language plpgsql;
    

    之后,我可以不计算每一行而是使用分配的序列进行分页:

    select * from tmp_table where counterrow >= 9000000 and counterrow <= 9025000
    

    从 java 的角度来看,我通过使用惰性列表的部分获取来实现此分页。这是一个从抽象列表扩展并实现 get() 方法的列表。 get方法可以使用数据访问接口继续获取下一组数据并释放内存堆:

    @Override
    public E get(int index) {
      if (bufferParcial.size() <= (index - lastIndexRoulette))
      {
        lastIndexRoulette = index;
        bufferParcial.removeAll(bufferParcial);
        bufferParcial = new ArrayList<E>();
            bufferParcial.addAll(daoInterface.getBufferParcial());
        if (bufferParcial.isEmpty())
        {
            return null;
        }
    
      }
      return bufferParcial.get(index - lastIndexRoulette);<br>
    }
    

    另一方面,数据访问接口使用查询进行分页,并实现一种方法进行逐步迭代,每25000条记录完成。

    可以在此处查看此方法的结果 http://www.arquitecturaysoftware.co/2013/10/laboratorio-1-iterar-millones-de.html

    【讨论】:

      【解决方案5】:

      不使用OFFSET,一个非常有效的技巧是使用临时表:

      CREATE  TEMPORARY TABLE just_index AS
      SELECT ROW_NUMBER() OVER (ORDER BY myID), myID
      FROM mytable;
      

      对于 10 000 000 行,它需要大约 10 秒来创建。 然后您想使用 SELECT 或 UPDATE 您的表,您只需:

      SELECT * FROM mytable INNER JOIN (SELECT just_index.myId FROM just_index WHERE row_number >= *your offset* LIMIT 1000000) indexes ON mytable.myID = indexes.myID
      

      仅使用 just_index 过滤 mytable 比使用 WHERE myID IN (SELECT ...) 更有效(在我的情况下)使用 INNER JOIN

      这样您就不必存储最后一个 myId 值,只需将偏移量替换为使用索引的 WHERE 子句

      【讨论】:

      • 谢谢!我提高了将所有格式化信息直接放入临时表中的性能,因此我避免了 INNER JOIN 并直接在临时表上进行过滤
      猜你喜欢
      • 2016-10-12
      • 1970-01-01
      • 2011-01-06
      • 2021-08-03
      • 2013-02-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-11-23
      相关资源
      最近更新 更多