Постраничная выборка для динамических данных

Я уже давно внес в свой стиль программирования негласное правило о том, что все перечисления, будь это таблицы или списки, картинки или комментарии, должны загружаться порциями и поддерживать постраничную загрузку. Дело в том, что потом добавить этот функционал бывает гораздо сложнее, нежели чем сделать это сразу. Особенно на этапе разработки этим многие пренебрегают. Основным способом сделать постраничную загрузку является имплементация limit-offset подхода. Сейчас мы будем говорить о данных, возвращаемых из базы, соответственно, примеры я тоже буду строить отталкиваясь от этого. Но limit-offset, естественно, применим и для других случаев. Но что будет, если у нас высока вероятность появления новых данных? Возьмем для примера новости. Например, у нас на данный момент 20 новостей, а мы выводим по 10. Вывели первые 10 новостей, но еще до того как мы перешли к следующим десяти новостям, у нас появилась еще одна новость. Каждая новая запись будет смещать новости, и в результатах 2-й страницы мы увидим дубликат последней записи из первой выборки.

Думаю, наглядно будет представить пример на Zend Framework 1.x. У нас есть наипростейшая таблица NEWS с полями: ID, TIME, TEXT.

public function getNews($offset = 0)
{
    $select = $this->select()
        ->order('time DESC')
        ->limit(10, $offset);

    return $this->fetchAll($select);
}

Это обычный limit-offset. Как же избежать того случая, когда появляются дубликаты? Я для этого запоминаю какое-нибудь уникальное значение последней записи. Это может быть ID, какой-то хэш, пара значений и т.д. Главное — нам нужно отличить последний объект, и в следующей выборке вернуть N элементов после него. А еще очень важно — это то, что это значение должно быть привязано к сортировке.

Вернемся к нашим новостям. Уникальным значением для каждой новости будет ее ID. Этот ID у нас является первичным ключом и имеет опцию auto_increment. Чем новее новость, тем выше ID. В этом случае можно запоминать ID последней новости из выборки, и вместо offset передавать его в функцию.

public function getNews($lastId = 0)
{
    $select = $this->select()
        ->order('time DESC')
        ->limit(10);

    if ($lastId) {
        $select->where($this->getAdapter()->quoteInto('id < ?', $lastId));
    }

    return $this->fetchAll($select);
}

Конечно, в статье разобран очень простой пример, где ID связан с сортировкой. Но, повторюсь, это не обязательно будет ID, найдите такое уникальное значение, которое соответствует сортировке, и используйте его.

Еще такой подход называют постраничной выборкой по курсору. Используется он не везде и не всегда. Но в комментариях, сообщениях, ленте он может быть очень полезен.