【问题标题】:Offset/limit to page/size conversion偏移量/限制到页面/大小转换
【发布时间】:2014-08-23 08:49:03
【问题描述】:

这应该是一个有趣的挑战。 我正在寻找一种尚不存在的算法(据我所知)

  • 我们有一个数据库访问函数,可以一次读取多页记录,使用页码和页大小作为参数。让我们调用这个函数getFromDatabase(int page, int size)
  • 我们希望提供一个 REST API,它应该根据偏移量和限制返回记录。让我们将其包装在一个函数 getRecords(int offset, int limit) 中。

不知何故,我们必须使用给定的offsetlimit 来检索只能由pagesize 访问的匹配数据库记录。显然,偏移量/限制不会总是映射到单个页面/大小。挑战在于找到一种算法,使getFromDatabase 调用的“理想”数量能够检索所有记录。该算法应考虑几个因素:

  • 每次调用getFromDatabase都有一定的间接费用;尽量减少来电。
  • 检索到的每条记录都会增加额外的开销;检索尽可能少的记录(如果可以减少总开销,则可以检索“浪费”)。
  • 算法本身也有间接成本;显然,它们不应该超过任何好处。

我想出了以下算法:http://jsfiddle.net/mwvdlee/A7J9C/ (JS 代码,但算法与语言无关)。本质上是下面的伪代码:

do {
    do {
        try to convert (offset,limit) to (page,size)
        if too much waste
            lower limit by some amount
        else
            call `getDatabaseRecords()`
            filter out waste records
            increase offset to first record not yet retrieved
            lower limit to last records not yet retrieved              
    } until some records were retrieved
} until all records are retrieved from database

该算法的关键在于确定too much wastesome amount。但是这个算法不是最优的,也不能保证是完整的(很可能是这样,我只是无法证明)。

有没有更好的(已知的?)算法,或者我可以做的改进? 有人对如何解决这个问题有好的想法吗?

【问题讨论】:

  • 可能,迄今为止最便宜的选择是只调用一次外部服务,因为按偏移量返回行通常需要扫描所有先前的行。这太贵了。与此相比,传输单行非常便宜。
  • @usr;也许我误解了;如果调用者没有该行的识别信息,为什么转移单行会更便宜?这不就像用limit 1 检索一样有效吗?
  • 我的意思是传输 11 行可能几乎与传输 10 行一样便宜,只要它作为一个查询的一部分发生。它并不贵 10%。更像是 0.1% 以上。
  • 我假设在 DB 存储中分配了连续数量的记录(首先在 offset 处,最后在 offset+limit-1 处),但通常这不需要是从一些 DB 存储内存开始的块一些地址 a 并以 offset*recordsize 结尾。那么你怎么能希望用单个页面(无论如何给出这个值的“单位”)和任意大小(以字节为单位)来检索它?
  • @Martijn 如果有人向您请求 { offset = 25, limit = 10 },您不能只调用getFromDatabase(page: 2, size: 20) 并在一次调用中获取所有数据(并丢弃一些行)吗?这听起来不太难解决,所以我可能错过了一些要求。总是有可能构造一个最多浪费 50% 的调用。

标签: database algorithm limit offset


【解决方案1】:

正如@usr 所指出的,在这个问题的大多数变体中(无论是查询数据库、API 还是其他实体),最好尽可能减少调用次数,因为返回一些额外的行是几乎总是比发出单独的电话便宜。以下 PageSizeConversion 算法将始终找到返回尽可能少的记录的单个调用(这正是它执行搜索的方式)。在数据集的开头 (headWaste) 或结尾 (tailWaste) 可能会返回一些额外的记录,以使数据集适合单个页面。该算法在此处用 Javascript 实现,但很容易移植到任何语言。

function PageSizeConversion(offset, limit) {
  var window, leftShift;
  for (window = limit; window <= offset + limit; window++) {
    for (leftShift = 0; leftShift <= window - limit; leftShift++) {
      if ((offset - leftShift) % window == 0) {
        this.pageSize = window;
        this.page = (offset - leftShift) / this.pageSize;

        this.headWaste = leftShift;
        this.tailWaste = ((this.page + 1) * this.pageSize) - (offset + limit);
        return;
      }
    }
  }
}

var testData = [
  {"offset": 0,"limit": 10,"expectedPage": 0,"expectedSize": 10,"expectedHeadWaste": 0,"expectedTailWaste": 0},
  {"offset": 2,"limit": 1,"expectedPage": 2,"expectedSize": 1,"expectedHeadWaste": 0,"expectedTailWaste": 0},
  {"offset": 2,"limit": 2,"expectedPage": 1,"expectedSize": 2,"expectedHeadWaste": 0,"expectedTailWaste": 0},
  {"offset": 5,"limit": 3,"expectedPage": 1,"expectedSize": 4,"expectedHeadWaste": 1,"expectedTailWaste": 0},
  {"offset": 3,"limit": 5,"expectedPage": 0,"expectedSize": 8,"expectedHeadWaste": 3,"expectedTailWaste": 0},
  {"offset": 7,"limit": 3,"expectedPage": 1,"expectedSize": 5,"expectedHeadWaste": 2,"expectedTailWaste": 0},
  {"offset": 1030,"limit": 135,"expectedPage": 7,"expectedSize": 146,"expectedHeadWaste": 8,"expectedTailWaste": 3},
];

describe("PageSizeConversion Tests", function() {
  testData.forEach(function(testItem) {
    it("should return correct conversion for offset " + testItem.offset + " limit " + testItem.limit, function() {
      conversion = new PageSizeConversion(testItem.offset, testItem.limit);
      expect(conversion.page).toEqual(testItem.expectedPage);
      expect(conversion.pageSize).toEqual(testItem.expectedSize);
      expect(conversion.headWaste).toEqual(testItem.expectedHeadWaste);
      expect(conversion.tailWaste).toEqual(testItem.expectedTailWaste);
    });
  });
});

// load jasmine htmlReporter
(function() {
  var env = jasmine.getEnv();
  env.addReporter(new jasmine.HtmlReporter());
  env.execute();
}());
<script src="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine.js"></script>
<script src="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine-html.js"></script>
<link href="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine.css" rel="stylesheet" />
<title>Jasmine Spec Runner</title>

这可能不是 完全@Martijn 正在寻找的,因为它偶尔会产生大量浪费的结果。但在大多数情况下,这似乎是解决一般问题的好方法。

【讨论】:

    【解决方案2】:

    偏移/限制到页面/页面大小分页的原则(精确,不浪费)

    如果页面大小 == 限制或(偏移百分比限制)== 0,则页面 ==(整数) else page == (float)。

    在命令行运行类型'node convertOffsetToPage.js'

    function convertOffsetToPage(offset, limit) {
        const precision = 1000000;
        const pageSize = limit;
        let page = (offset + limit) / limit;
        page = Math.round(page * precision) / precision;
        return { page, pageSize };
    }
    
    function dbServiceSimulation(page, pageSize, items) {
        const start = Math.round((page - 1) * pageSize);
        const end = Math.round(page * pageSize);
        return items.slice(start, end);
    }
    
    function getDataItems(itemCount) {
        return Array.from(Array(itemCount), (_, x) => x);
    }
    
    const dataItems = getDataItems(1000000);
    
    console.log('\ndata items: ', dataItems);
    
    let offset = parseInt(process.argv[2], 10) || 0;
    let limit = parseInt(process.argv[3], 10) || 1;
    
    console.log('\ninput offset: ', offset);
    console.log('\ninput limit: ', limit);
    
    const { page, pageSize } = convertOffsetToPage(offset, limit);
    
    console.log('\npage = ', page);
    
    console.log('\npageSize = ', pageSize);
    
    const result = dbServiceSimulation(page, pageSize, dataItems);
    
    console.log('\nresult after core Service call');
    
    console.log('\nresult: ', result)
    
    console.log('\n');
    

    【讨论】:

      【解决方案3】:

      我也遇到了这个问题。我的解决方案是(在java中)简而言之。首先,琐碎的案例被关闭,然后是棘手的部分:)。它试图找到最佳的页面大小、页码并计算从页面开始的新偏移量。保留原始限制:

      public static PageSizeOffsetLimit toPageSize(int offset, int limit) {
          if (offset < limit) {
              return new PageSizeOffsetLimit(0, offset + limit, offset, limit);
          }
          if (offset == limit) {
              return new PageSizeOffsetLimit(1, limit, 0, limit);
          }
      
          for (int size = limit; size < 2 * limit; size++) {
              int newOffset = offset % size;
              if ((size - limit) >= newOffset) {
                  return new PageSizeOffsetLimit(offset / size, size, newOffset, limit);
              }
          }
          throw new RuntimeException(String.format(
              "Cannot determinate page and size from offset and limit (offset: %s, limit: %s)",
              offset,
              limit));
      }
      

      玩得开心:)

      【讨论】:

        猜你喜欢
        • 2018-01-16
        • 2019-09-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-02-25
        • 2016-12-15
        • 2018-06-23
        • 1970-01-01
        相关资源
        最近更新 更多