Как стать автором
Обновить

Реализация грида для работы с большими таблицами. Часть 1

Время на прочтение10 мин
Количество просмотров16K
Всего голосов 15: ↑14 и ↓1+13
Комментарии21

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

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

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

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

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

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

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

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

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

Вы применяли явный запрос к СУБД вида "выдать данные, начиная с N-й записи" или интерполяцию?
Выдать данные начиная с N-й записи, лимитированной L'ным кол-вом строк. При этом стандартного функционала виндового ScrollBar'а мне было достаточно, и высчитывать самостоятельно индекс начала выборки мне не приходилось.
Когда была такая проблема решил с использованием ODBC курсора, для любого запроса он может двигаться вперед/назад по записям. Как такое сделать через ADO.NET так и не нашел.
Если у вас получиться сделать что-то подобное которое можно будет использовать для HTML/JS таблиц (либо через WebSocket, либо через AJAX запросы), то я готов даже купить лицензию за такое.
Самостоятельно реализовать курсор. В частном случае это не очень сложно, а в общем эта задача не имеет приемлемого решения.
К тому же если данные приходят, например, из хранимки, то ODBC'шный курсор ничем не поможет: если это серверный курсор, то вы сожрёте всю память на сервере, а если клиентский, то на клиенте (но перед этим вы сожрёте всю всю ширину сетевого канала).
Мы всё это это делаем в рамках проекта разработки своей OpenSource платформы (https://share.curs.ru/wiki). В конце марта должен появиться билд с гридом для веб-интерфейса. Но там всё сильно завязано на серверную часть: есть у нас и реализованные нами курсоры. Поэтому как-то отделить в компоненту вряд ли получится)
мне в принципе все равно как реализовать, текущая версия это десктопная программа которая работает без проблем с ODBC курсором.
У нас много таблиц у которых виртуальный скролл, без страниц, пользователь может просто нажать Page Down или Arrow Down и прокрутить все 10 миллионов записей если надо. Запросы очень сложные и полагаться на первичные ключ никак не получится.

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

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

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

Публикации

Изменить настройки темы

Истории