Таблица (грид) с вертикальной полосой прокрутки — наиболее распространённый элемент пользовательского интерфейса для работы с данными реляционной БД. Однако известны сложности, с которыми приходится сталкиваться, когда таблица содержит так много записей, что тактика их полной вычитки и сохранения в оперативной памяти становится неразумной.

Какие-то приложения на большие таблицы не рассчитаны и «зависают» при попытке открыть на просмотр/редактирование таблицу с миллионами записей. Иные отказываются от использования грида с вертикальной полосой прокрутки в пользу постраничного отображения или предлагают пользователю лишь иллюзию, что при помощи полосы прокрутки можно быстро перейти к нужной (хотя бы к самой последней) записи.



Мы расскажем об одном из возможных методов реализации табличного элемента управления, обладающего свойствами Log(N)-быстрого 1) первоначального отображения 2) прокрутки на всём диапазоне записей 3) перехода к записи с заданным уникальным ключом. Всё это — при двух ограничениях: 1) записи могут быть отсортированы только по индексированному набору полей и 2) collation-правила базы данных должны быть известны алгоритму.

Изложенные в статье принципы были реализованы автором в проекте с его участием на языке Java.



Основной принцип



Двумя ключевыми функциональными возможностями табличного элемента управления (грида) являются
  1. прокрутка — отображение записей, соответствующих положению бегунка полосы прокрутки,
  2. переход к записи, заданной по комбинации ключевых полей.

Сперва рассмотрим простейшие подходы к их реализации и покажем, почему они непригодны в случае большого числа записей.

Наиболее прямолинейный подход заключается в считывании в оперативную память всех записей (или хотя бы их первичных ключей), после чего прокрутка осуществляется выборкой сегмента массива, а позиционирование — поиском записи в массиве. Но при большом количестве записей, во-первых, слишком много времени занимает процесс изначальной загрузки массива и, как следствие, задерживается отображение грида на экране, а во-вторых, оперативную память начинают занимать данные таблицы, что снижает общее быстродействие.

Другой подход заключается в «перекладывании» вычислений на СУБД: выборку записей для отображения в окне прокрутки осуществлять при помощи конструкций вида select… limit… offset ..., позиционирование — сводить к select… и count(*). Однако и offset, и count(*) связаны с перебором записей внутри сервера, они имеют в общем случае скорость выполнения O(N), и потому неэффективны при большом числе записей. Частый их вызов приводит к перегрузке сервера БД.

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

При условии, что по полю k построен индекс, быстрыми являются следующие запросы (они используют index lookup и выполняются за время Log(N)):
  1. Запрос A. Найти первую и последнюю запись в наборе данныx:
    select ... order by k [desc] limit 1
  2. Запрос B. Найти h первых записей с ключом, большим или равным данному значению K:
    select ... order by k where k >= K limit h
Замечательным свойством запроса B является то, что в качестве параметра ему необязательно подставлять ключ существующей в базе данных записи: он одинаково быстро находит как запись по её первичному ключу, так и ближайшую к заданному ключу запись. Этим мы будем активно пользоваться.

Не являются быстрыми следующие запросы:
  1. Запрос C Подсчитать общее количество записей в наборе данных:
    select count(*) ...
  2. Запрос D. Подсчитать число записей, предшествующих записи с ключом, большим или равным данному:
    select count(*) ... where key < K
Запросы B и D можно обобщить на случай сортировки по набору из нескольких полей order by k1, k2, k3… при условии, что на этих полях построен составной индекс. Условие k >= K должно быть обобщено на логическое условие сравнения нескольких значений в лексикографическом порядке:
where k1 >=  K1 and (k1 > K1 or (k2 >= K_2  and (k2 > K2 or ...)))
Основной принцип заключается в том, чтобы в процедурах, требующих быстрого отклика для пользователя, обходиться только быстрыми запросами к БД.


«Угадывание» зависимости первичного ключа от номера записи



На время представим себе, что первичный ключ нашей таблицы имеет всего одно целочисленное поле (далее мы снимем это ограничение).

Пусть каждому положению бегунка полосы прокрутки соответствует целое число от 0 до , где — общее число записей в таблице, — число строк, умещающихся на одной странице в гриде. Если бегунок полосы прокрутки выставлен в положение , то это означает, что при отрисовке грида необходимо пропустить первых записей в таблице, а затем вывести записей. Это как раз то, за что отвечают ключевые слова offset и limit в PostgreSQL, и при большом числе записей мы не можем ими пользоваться. Но если бы некоторым «волшебным» образом мы бы могли за быстрое время уметь вычислять функцию и обратную ей функцию , связывающую первичный ключ и число записей с ключом, меньшим данного, то тогда при помощи запроса B, подставляя туда значение , мы могли бы быстро получать набор записей для отображения пользователю. Задача перехода к нужной записи решалась бы таким образом: т. к. первичный ключ заранее известен, его сразу можно использовать в качестве параметра быстрого запроса B, а полосу прокрутки следовало бы установить в положение . Поставленная задача была бы решена!

(Заметим, что запрос D, вызываемый с параметром k, как раз возвращает значение , но 1) он медленный и 2) нам нужно уметь вычислять как , так и обратную ей.)

Естественно, функция целиком определяется данными в таблице, поэтому, чтобы точно восстановить взаимозависимость значений ключа и номера записи, необходимо прочесть из базы в оперативную память все ключи через одного: для корректного срабатывания запроса B в промежуточных точках можно считать, что (вспомним замечательное свойство запроса B). Это уже лучше, чем запоминать все подряд ключи, но всё ещё недостаточно хорошо, т. к. требует O(N) времени и оперативной памяти для первоначального отображения таблицы. К счастью, можно обойтись гораздо меньшим значением точно известных точек для функции , и чтобы понять это, нам потребуется небольшой комбинаторный анализ возможных значений .

Пусть мы выполнили запрос
select min(k), max(k), count(*) from foo
и получили результаты: 0, 60, 7. Т. е. теперь мы знаем, что в нашей таблице всего 7 записей (и значит, максимальное значение равно 6), первая из записей имеет ключ 0, а последняя — ключ 60. Одним таким запросом мы фактически узнали значения в трёх точках: , и . И это ещё не всё, ведь из самого определения следует, что:
  1. монотонно растёт,
  2. при увеличении на единицу, увеличивается на единицу или не увеличивается, поэтому график функции , кроме точки , целиком лежит в параллелограмме , , , ,
  3. общее число возможных функций равно числу способов распределения записей по значениям ключа (позиции первой и последней записи фиксированы), т. е.


  4. число возможных функций , которые в точке принимают значение , определяется произведением количества комбинаций записей с ключом, меньшим , и большим или равным :




Таким образом, если каждый из возможных вариантов считать равновероятным, то вероятность того, что для заданного значения inline_formula имеется ровно inline_formula записей с ключом, строго меньшим inline_formula, задаётся отношением inline_formula, известным как гипергеометрическое распределение. Для inline_formula, inline_formula картина выглядит вот таким образом:



Свойства гипергеометрического распределения хорошо известны. В случае inline_formula всегда inline_formula, а на отрезке inline_formula среднее значение inline_formula линейно зависит от k:


Дисперсия значения имеет форму перевернутой параболы с нулями на краях отрезка и максимумом посередине
некрасивая формула

(Грубо можно считать, что средняя ошибка меньше, чем inline_formula.)

Расчёт значений по вышеприведённым формулам для , показывает такую картину для возможных минимальных и максимальных (красные линии), среднего (голубая линия) значений , а также границ среднеквадратичного отклонения (серая область):



Итак, ломаная, построенная между точками , , и является статистически обоснованным приближением для вычисления функции и обратной , и это приближение даёт нам возможность довольно точно «угадать» значения функции даже если всё, что мы о ней знаем — это её значения в трёх точках, полученные при помощи одного запроса. По сути, мы для «угадывания» выполняем первую итерацию алгоритма интерполяционного поиска, только аккуратно определив граничные значения.

Если теперь для какого-то , станет известно новое точное значение , мы можем добавить пару к интерполяционной таблице и при помощи кусочной интерполяции получить уточнённый расчёт значения для решения задачи прокрутки, а применяя обратную кусочную интерполяцию, вычислять уточнённое значение при решении задачи позиционирования. С каждой накопленной точкой дипазон возможных расхождений будет сужаться, и мы будем получать всё более и более точную картину для функции .

Если ключевых полей несколько и типы данных не INT



На практике дело не ограничивается единственным целочисленным полем для сортировки набора данных. Во-первых, тип данных может быть другим: строка, дата и прочее. Во-вторых, сортируемых полей может быть несколько. Это затруднение устраняется, если мы умеем вычислять

  1. функцию-нумератор inline_formula, переводящую набор значений полей произвольных типов в натуральное число,
  2. обратную ей функцию inline_formula, переводящую натуральное число обратно в набор значений полей, inline_formula.

Функция-нумератор должна обладать тем свойством, что если набор inline_formula меньше набора inline_formula в смысле лексикографического порядка, то должно быть .

Говоря языком математики, биекция inline_formula должна устанавливать изоморфизм порядка между множеством возможных значений наборов полей и множеством натуральных чисел. Заметим: для того, чтобы задача поиска подходящей inline_formula была математически разрешима, важно, чтобы множества возможных значений полей inline_formula были конечными. К примеру, между множеством всех лексикографически упорядоченных пар натуральных (пар действительных) чисел и множеством натуральных (действительных) чисел невозможно построить изоморфизм порядка. Разумеется, множество всех возможных значений любого машинного типа данных является конечным, хотя и очень большим.

Для представления значений inline_formula не подходят стандартные 32- и 64-битовые целочисленные типы: так, чтобы перенумеровать одни лишь всевозможные 10-байтовые строки, уже не хватит 64-битового (8-байтового) целого. В своей реализации мы использовали класс java.math.BigInteger, способный представлять целые числа произвольной величины. При этом объём оперативной памяти, занимаемой значением inline_formula, примерно равен объёму, занимаемому набором значений .

При наличии обратимой функции-нумератора inline_formula и обратимой функции-интерполятора inline_formula,

  • прокрутка грида сводится к вычислению значений ключевых полей inline_formula, где inline_formula — положение вертикальной полосы прокрутки, после чего быстрый запрос к БД находит inline_formula первых записей, больших или равных inline_formula,
  • позиционирование сводится к считыванию inline_formula первых записей из БД по заранее известным значениям inline_formula, и к вычислению положения бегунка полосы прокрутки inline_formula.


Схема взаимодействия процедур



На этом этапе мы можем отвлечься от математики и разобраться собственно с алгоритмической частью. Общая схема взаимодействия процедур системы показана на рисунке ниже. Использована «приблизительная UML Activity» нотация, при этом сплошной стрелкой показана последовательность выполнения процедур, а пунктирной стрелкой — асинхронный вызов в отдельном потоке выполнения:



Допустим, что пользователь изменил положение бегунка вертикальной полосы прокрутки (см. в левый нижний угол диаграммы). Интерполятор вычисляет значение номера комбинации значений ключевых полей () с типом BigInteger. На основе этого значения нумератор восстанавливает комбинацию ключевых полей .

Здесь важно понимать, что на данном этапе в полях не обязательно будут находиться значения, действительно присутствующие в базе данных: там будут лишь приближения. В строковых полях, например, будет бессмысленный набор символов. Тем не менее, благодаря замечательному свойству запроса B, вывод из базы данных строк с ключами, большими или равными набору , окажется приблизительно верным результатом для данного положения полосы прокрутки.

Если пользователь отпустил полосу прокрутки и какое-то время не трогал её, асинхронно (в отдельном потоке выполнения) запускается запрос D, определяющий порядковый номер записи, а значит, и точное положение полосы прокрутки, которое соответствует тому, что отображено пользователю. Когда запрос будет завершён, на основе полученных данных будет пополнена интерполяционная таблица. Если к этому моменту пользователь не начал снова прокручивать таблицу, вертикальный бегунок «отскочит» на новое, уточнённое положение.

При переходе к записи последовательность вызовов процедур происходит в обратную сторону. Т. к. значения ключевых полей уже известны, для пользователя запросом B сразу извлекаются данные из базы. Нумератор вычисляет , и затем интерполятор определяет приблизительное положение полосы прокрутки как . Параллельно, в асинхронном потоке выполнения, выполняется уточняющий запрос, по результатам которого в интерполяционную таблицу добавляется новая точка. Через секунду-другую бегунок полосы прокрутки «отскочит» на новое, уточнённое положение.

Чем больше пользователь будет «бродить» по данным вертикальным бегунком, тем больше точек будет собираться в интерполяционной таблице и тем меньше будут «отскоки». Практика показывает, что достаточно всего 4-5 удачных точек в интерполяционной таблице, чтобы «отскоки» стали очень малы.

Экземпляр класса-интерполятора должен хранить в себе промежуточные точки монотонно растущей функции между множеством 32-битных целых чисел (номеров записей в таблице) и множеством объектов типа BigInteger (порядковых номеров комбинаций значений ключевых полей).

Сразу же после инициализации грида необходимо в отдельном потоке выполнения запросить общее количество записей в таблице, чтобы получить корректное значение . До того момента, как это значение будет получено при помощи выполняющегося в параллельном потоке запроса, можно использовать некоторое значение по умолчанию (например, 1000) — это не повлияет на корректность работы.

Интерполятор должен уметь за быстрое по количеству интерполяционных точек время вычислять значение как в одну, так и в другую сторону. Однако заметим, что чаще требуется вычислять значение порядкового номера комбинации по номеру записи: такие вычисления производятся много раз за секунду в процессе прокрутки грида пользователем. Поэтому за основу реализации модуля интерполятора удобно взять словарь на основе бинарного дерева, ключами которого являются номера записей, а значениями — порядковые номера комбинаций (класс TreeMap<Integer, BigInteger> в языке Java).

Ясно, что по заданному номеру такой словарь за логарифмическое время находит две точки (), между которыми строит прямую интерполяцию функции . Но тот факт, что растёт монотонно, позволяет за быстрое время производить и обратное вычисление. В самом деле: если дан номер комбинации , , поиск кусочного сегмента, в котором лежит , можно произвести в словаре методом дихотомии. Отыскав нужный сегмент, мы производим обратную интерполяцию и находим номер , соответствующий .

При пополнении словаря интерполяционными точками необходимо следить за тем, чтобы интерполируемая функция оставалась монотонной. Так как другие пользователи могут удалять и добавлять записи в просматриваемую таблицу, актуальность известных словарю интерполяционных точек может утратиться, а вновь добавляемая точка может нарушить монотонность. Поэтому метод добавления новой интерполяционной точки должен проверять, что «точке слева» от только что добавленной соответствует меньшее, а «точке справа» — большее значение. Если оказывается, что это не так, следует исходить из предположения, что последняя добавленная точка соответствует актуальному положению вещей, а некоторые из старых точек утратили свою актуальность. По отношению к вновь добавленной точке следует удалять все точки слева, содержащие большее значение, и все точки справа, содержащие меньшее значение. Процесс «срезания выбивающейся точки» показан на рисунке:



Также интерполятор должен содержать в себе механизм, в целях экономии памяти защищающий словарь от переполнения излишними точками, и отбрасывающий наименее существенные из них (как мы помним, не имеет смысла хранить все точки подряд — достаточно только через одну).

Заключение к первой части


Итак, мы разобрались, как в целом устроена наша система: основными её частями являются интерполятор и нумератор, а также полностью обсудили реализацию интерполятора. Чтобы завершить решение задачи, необходимо теперь реализовать нумератор — биекцию inline_formula, увязывающую наборы значений ключевых полей, возможно, состоящих из дат, строки, отсортированных при помощи Unicode collation rules и т. п. с растущими в том же порядке числами.

Этому посвящена вторая часть статьи.

Ещё я готовлю научную публикацию, и вы можете также ознакомиться с препринтом научной версии этой статьи на arxiv.org.

Комментарии (21)


  1. auine
    14.03.2016 10:25

    Таблица (грид) с вертикальной полосой прокрутки — наиболее распространённый элемент пользовательского интерфейса для работы с данными реляционной БД. Однако известны сложности, с которыми приходится сталкиваться, когда таблица содержит так много записей, что тактика их полной вычитки и сохранения в оперативной памяти становится неразумной.

    Какие-то приложения на большие таблицы не рассчитаны и «зависают» при попытке открыть на просмотр/редактирование таблицу с миллионами записей. Иные отказываются от использования грида с вертикальной полосой прокрутки в пользу постраничного отображения или предлагают пользователю лишь иллюзию, что при помощи полосы прокрутки можно быстро перейти к нужной (хотя бы к самой последней) записи.

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


    1. IvanPonomarev
      14.03.2016 10:49
      +2

      Не говорите "никто", всех вы знать не можете :-) Меня лично вдохновил классический интерфейс Microsoft Navision версии до 2009, который прекрасно справлялся с этой задачей по-видимому похожими методами. Мы создаём платформу для бизнес-решений, а бизнес-программисту надо дать простой универсальный инструмент, отвечающий на запрос "В этом месте формы я хочу грид для таблицы. В таблице может быть 5 записей, а может — 5 миллионов, ничего не хочу знать, меня волнуют бизнес-процессы, а не техническая сторона".

      На новизну идеи с интерполяцией я также не претендую. Однако мне пока что не удалось разыскать в интернете работу, которая бы увязывала интерполяцию "ключ-номер записи" с гипергеометрическим распределением и тем самым давала бы точную оценку среднеквадратичной погрешности, так что в этой части, возможно, есть и новизна.


      1. AndreyDmitriev
        14.03.2016 13:03
        +2

        Делал примерно такое лет десять назад (на эзотерическом LabVIEW). Задача стояла именно такая — отображать таблицу с сотнями тысяч записей с возможностью скроллинга. "Прямая" загрузка таблицы в многоколонный листбокс приводила к диким тормозам и бешеному расходу памяти. Вместо это собственный нативный скролл у таблицы был выключен, к ней был бесшовно приставлен собственный скролл бар, при перемещении которого делалась выборка из базы и показывались лишь "видимые" строки. Пришлось немного повозиться с "балансировкой" нагрузки, если пользователь дёргал скролл туда-сюда, чтобы не перегружать базу запросами (часть изменённых значений незаметно для пользователя пропускались, если следовали слишком часто). Кроме того, добавили "упреждающее" чтение, на тот случай, если пользователь "примерно" выставляет скролл, думает некоторое время, а затем "доводит" его до нужного значения, щёлкая по крайним стрелочкам, в промежутки ну или нажимая клавиши на клавиатуре — тут ему в таблицу выгоняются "кешированные" значения, полученные во время "простоя" без запросов к базе. Ну и с динамическим изменением размера формы пришлось чуть повозиться (когда пользователь разворачивает приложение на весь экран — в таблице больше записей помещается, ну и скролл надо синхронно с таблицей ресайзить и позиционировать). На всё про всё недели полторы потратил, пока оно не заработало "гладко". В интерполяцию особо не упирался — если пользователь перемещал скролл, скажем, в позицию 357650, то из базы это значение и выбиралось. Понятно, что небольшое смещение скролл-бара, скажем на несколько пикселей, легко приводит к скроллингу нескольких тысяч записей. С другой стороны в моём случае пользователь редко когда пользовался скроллбаром для вот такой "сплошной" навигации — как правило он сначала фильтрует базу по какому-либо полю (при этом в выборку попадает от силы несколько сот записей), и лишь затем пользуется скролл баром, чтобы найти конкретную запись.
        На современном этапе развития средств разработки я б первым делом проверил, нет ли такой функциональности "из коробки".


        1. IvanPonomarev
          14.03.2016 17:49

          В интерполяцию особо не упирался — если пользователь перемещал скролл, скажем, в позицию 357650, то из базы это значение и выбиралось.

          Означает ли это, что у вас первичный ключ состоял только из одного числового поля?


          1. AndreyDmitriev
            14.03.2016 20:54

            Да, именно так. Впрочем я выше неточно выразился — при перемещении скроллбара в позицию N я устанавливаю курсор в рекордсете в эту самую позицию и начинаю получать через fetch данные, заполняя видимую часть таблицы. Если пользователь хочет отфильтровать базу по какому-то праметру, то выполняется SELECT * FROM bla bla WHERE bla bla, затем я опрашиваю recordset — он мне отдаёт количество записей в фильтрованной выдаче, на основе этого количества я пересчитываю скроллбар (ну чтобы движок по высоте соответствовал количеству записей и инкремент был правильный), дальше ставлю курсор на запись в соответствии с новым положением скроллбара, если его юзер подёргал, ну и fetch рекордсета начиная с новой позиции. Это не самый оптимальный способ, но для моей задачи он оказался более чем достаточен.


            1. IvanPonomarev
              14.03.2016 21:01

              Я понял. Да, на самом деле некоторое загрубление допустимо, тут зависит от конкретной задачи, конечно.

              У нас ключ может быть составным и включать в себя в том числе и строки (о чём поведу речь во второй части статьи)


  1. aionin
    14.03.2016 12:27

    "Диссертабельно".
    Много математики. Действительно, сегодня мало кто так глубоко задумывается над эффективностью зачастую готовых элементов интерфейса.


    1. IvanPonomarev
      14.03.2016 16:12

      Ну, диссертацию я давно уж защитил, поэтому никуда не спешу — важна суть) На очереди вторая часть: там будет про преобразование строковых значений в числа с учётом collation-правил.


  1. Rupper
    14.03.2016 14:18

    Я правильно понимаю, что вы еще неявно предполагаете, что выполнение запроса практически бесплатно? Имею ввиду что несколько раз выполнить запрос не отличается от выполнить запрос один раз и зачитать все данные.


    1. IvanPonomarev
      14.03.2016 17:53

      Уточните, что для вас значит "зачитать все данные". Вообще все данные из таблицы? Я об этом пишу в самом начале статьи. Подход "зачитать всё" годится лишь до тех пор, пока записей достаточно мало, чтобы это можно было сделать за малое время. В противном случае требуется слишком много времени при каждом открытии таблицы (хранить в памяти долго мы данные тоже не можем: таблицу ведь редактируют другие пользователи, надо всё время выдавать свежее состояние данных).

      "Запрос B" действительно почти "бесплатен". И если записей очень много, то пользователь при одном сеансе "браузинга" таблицы в любом случае увидит лишь их малую часть (и запросить придётся лишь их малую часть).


  1. scumware
    15.03.2016 13:02

    Делал нечто подобное в реальном проекте (ушло в продакшен).
    У меня первый запрос считал кол-во результирующих записей, при скроллинге ListView заполнялся пустыми строками, и через таймаут (в районе полусекунды) отдельный поток подтягивал данные.

    Однако я не считаю хорошей идеей показывать много записей на экране: пользователю это не нужно. Ему нужно знать примерное кол-во записей результирующей выборки, а так же возможность пролистать вниз (до первого элемента), в середину (посмотреть набор записей из середины выборки), и в начало выборки. Вот эти кейсы и надо реализовывать.


    1. IvanPonomarev
      15.03.2016 15:06

      через таймаут (в районе полусекунды) отдельный поток подтягивал данные.

      Вы применяли явный запрос к СУБД вида "выдать данные, начиная с N-й записи" или интерполяцию?


      1. scumware
        15.03.2016 19:47

        Выдать данные начиная с N-й записи, лимитированной L'ным кол-вом строк. При этом стандартного функционала виндового ScrollBar'а мне было достаточно, и высчитывать самостоятельно индекс начала выборки мне не приходилось.


  1. Holms
    15.03.2016 20:39

    Когда была такая проблема решил с использованием ODBC курсора, для любого запроса он может двигаться вперед/назад по записям. Как такое сделать через ADO.NET так и не нашел.
    Если у вас получиться сделать что-то подобное которое можно будет использовать для HTML/JS таблиц (либо через WebSocket, либо через AJAX запросы), то я готов даже купить лицензию за такое.


    1. scumware
      15.03.2016 21:03

      Самостоятельно реализовать курсор. В частном случае это не очень сложно, а в общем эта задача не имеет приемлемого решения.
      К тому же если данные приходят, например, из хранимки, то ODBC'шный курсор ничем не поможет: если это серверный курсор, то вы сожрёте всю память на сервере, а если клиентский, то на клиенте (но перед этим вы сожрёте всю всю ширину сетевого канала).


    1. IvanPonomarev
      16.03.2016 14:42

      Мы всё это это делаем в рамках проекта разработки своей OpenSource платформы (https://share.curs.ru/wiki). В конце марта должен появиться билд с гридом для веб-интерфейса. Но там всё сильно завязано на серверную часть: есть у нас и реализованные нами курсоры. Поэтому как-то отделить в компоненту вряд ли получится)


  1. Holms
    16.03.2016 02:32

    мне в принципе все равно как реализовать, текущая версия это десктопная программа которая работает без проблем с ODBC курсором.
    У нас много таблиц у которых виртуальный скролл, без страниц, пользователь может просто нажать Page Down или Arrow Down и прокрутить все 10 миллионов записей если надо. Запросы очень сложные и полагаться на первичные ключ никак не получится.

    Теперь пришло время все это показывать в Вебе, вот и думаем как имитировать то-же поведение таблиц.


    1. IvanPonomarev
      16.03.2016 14:46

      Хм, хм, у вас "очень сложные запросы", и при этом они быстро возвращают 10 млн. записей? Позвольте усомниться: тут должны быть или запросы не очень сложные, или работают они не быстро, или записей не 10 млн. :-)) В предложенном мною методе довольно жёсткое ограничение: сортироваться можно только по индексированному набору полей. Иначе на миллионах записей возникает тормоз. Поэтому функциональности "сортировка столбца грида по щелчку на заголовок" при таком подходе нет и не будет… только выбор из списка заранее предложенных сортировок


      1. scumware
        17.03.2016 02:16

        Поэтому функциональности «сортировка столбца грида по щелчку на заголовок» при таком подходе нет и не будет… только выбор из списка заранее предложенных сортировок

        Странное решение.
        Вы вполне можете сортировать при щелчке по заголовку, но не по всем полям. Я, кстати, именно так и сделал.


        1. IvanPonomarev
          17.03.2016 23:00

          Окэй, допустим, у таблицы есть два составных индекса: по полям A, B, C и по полям A, D, E. Пользователь щёлкнул по столбцу A. Как будем сортировать?


          1. scumware
            18.03.2016 05:01

            Никак: просто не меняем порядок сортировки при щелчке по заголовкам полей, для которых сортировка не разрешена явным образом.

            Кстати, пользователи плохо воспринимают сложные сортировки. Даже начинающие программисты, до этого не сталкивавшиеся с SQL, с трудом понимают что значит order by A, B, C — они просто не понимают: мы тут по A сортируем, по B, или по C?
            И, кстати, не все знаю что такое составной индекс, и зачем он нужен. Пользователи вашей библиотеки могут даже не догадываться об их существовании.