Обновить

Как читать таблицу на 100 млн строк и не положить ни память, ни базу

Уровень сложностиСредний
Время на прочтение10 мин
Охват и читатели7.6K
Всего голосов 4: ↑2 и ↓2+2
Комментарии6

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

Мне одному кажется, что необходимость делать выборки по 10М строк есть признак оч. хренового дизайна всей системы? Ну допустим, удалось допилить базу так, что она отдаст и не грохнется – дальше-то что? Потом это ведь и в коде как-то прожевать надо будет. Как, и главное – зачем? В нормально спроектированной системе такая задача не должна появляться в принципе.

Для бэкенда приложения согласен — там 10 млн строк в одном запросе означают проблему. Но это ETL: задача — перенести данные из рабочей базы в аналитическое хранилище. Первичная загрузка, бэкфилл, полная перезагрузка из источников вроде 1С, где нет надёжного updated_at, — всё это неизбежно требует прочитать таблицу целиком.

И «прожёвывать» её целиком никто не будет: она идёт кусками по ~29 МБ через чтение → преобразование → запись, и память воркера зависит от числа потоков, а не от размера таблицы. Об этом, собственно, и статья.

Если интересно, как это устроено в коде, можно посмотреть в репозитории: https://github.com/Denvic-Tech/dvt

задача — перенести данные из рабочей базы в аналитическое хранилище

Честно говоря, не вижу существенной разницы: если об аналитике задумались только когда в базе скопилось 10М заказов – это и есть хреновое проектирование. А хоть репликация там есть? А бэкапы?

Целая статья для описания как читать базу по страницам?

Сдается мне, Вы двигаетесь к Трино или Спарку.

По открытому вопросу про границы без OFFSET. Квадратичная стоимость у вас не из-за OFFSET как такового, а из-за того, что каждый запрос стартует с начала индекса. Два варианта, которые ложатся на все шесть диалектов без ветвления по СУБД.

Первый — один проход с оконной функцией: SELECT id FROM (SELECT id, ROW_NUMBER() OVER (ORDER BY id) AS rn FROM orders) t WHERE rn % 76924 = 0. Это один скан индекса за O(N) вместо N²/2, и ROW_NUMBER есть везде из вашего списка: PostgreSQL, MySQL 8+, MS SQL, Oracle, SQLite 3.25+, ClickHouse. На 10 млн строк это секунды, а не минуты.

Второй, если оконные функции по какой-то причине нельзя — keyset-цепочка: SELECT id FROM orders WHERE id > :prev ORDER BY id LIMIT 1 OFFSET 76923. Запросов по-прежнему 130, но каждый пропускает ровно k строк от предыдущей границы, а не от начала, итого те же O(N). Минус — последовательность, плюс — никаких окон и один SQL на всех.

И третье, уже чисто для PostgreSQL: раз вы и так берёте reltuples вместо COUNT(*), границы можно брать из pg_stats.histogram_bounds — они бесплатны и приближённы, но у вас уже есть предохранитель max_rows_per_partition, который ловит перекос. Для задачи «кусок влезает в память» точность квантилей до строки и не нужна.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации