Договоримся о терминах
*Шардинг БД (db sharding) — это метод горизонтального масштабирования, при котором большая база данных разбивается на более мелкие, независимые части (shards), размещаемые на разных физических или виртуальных серверах. Каждый шард содержит подмножество данных, что снижает нагрузку на отдельные узлы, ускоряет запросы и позволяет хранить большие объемы информации, преодолевая ограничения вертикального масштабирования
**Read consistency (согласованность чтения) в БД — это гарантия того, что транзакция видит согласованное состояние данных, соответствующее определенному моменту времени (обычно моменту начала транзакции или запроса).
Зачем так?
Обычно шардинг подразумевает что вы заранее знаете ключ партиционирования по которому однозначно можно определить какой шард выбрать 1) для добавления нового контента так и 2) при поиске данных;
Однако нередки ситуации когда невозможно выбрать правильный ключ (так как он зависит от бизнес роли пользователя) а что еще чаще самый правильный преправильный ключ перестает со временем перестает быть правильным с развитием системы, накоплением данных, из за изменяющихся внешних обстоятельств (конъюнктуры рынка) и/или решений руководства));
Таким образом единственным разумным критерием выбора шарда может стать загруженность экземпляров БД (шардов) или равномерность распределения записей.
Добавление данных
Равномерность можно обеспечить либо 1) выбирая шард случайно или 2) по признаку загруженности (количеству уже добавленных записей), время от времени запрашивая в каждом шарде размер ключевых таблиц и кэшируя его значение в сервисе;
У случайного распределения есть преимущество в простоте реализации, но нет возможности автоматически подравнивать размер шардов если исходное их состояние было неодинаковым;
Приоритет шардов при любой реализации будет полезен если вы хотите использовать разное оборудование в кластере; так же специальное значение приоритета 0, например позволит на время вывести шард из-под нагрузки на запись, для обслуживания или архивирования.
Получение объекта по ID
Включение ключа шардирования в ID объекта, например, как смещение или seed в целом числе, используемом как основной ключ в “шардируемой” таблички - хороший способ быстро определять в какой именно шард нужно идти именно за ним; это позволяет ускорить типичную OLTP обработку транзакций (то есть цикла: SELECT … WHERE ID=<id>; UPDATE … WHERE ID=<id>);
Если генерация DML (INSERT-ов) выполняется на стороне бизнес-сервиса, тогда должен быть способ получения очередного ключа для нового объекта с помощью DML запроса до вставки самого объекта;
Генерация ключей «по одному» (если это конечно не GUID) как правило работает слишком медленно, поэтому запрос на получение ключей в шарде должен возвращать бизнес-сервис-у выделенный ему интервал из NN ключей;
Сам “seed” для каждой шардирумой таблички можно хранить непосредственно в самом шарде в системной таблице; или в конфигурации подключения к шарду.
Референсные таблицы (Lookup tables)
Кроме шардируемых таблиц как правило существуют (референсные или справочные) таблицы, которые с одной стороны нужны чтобы все запросы выполнялись корректно, а с другой стороны они небольшие (не сотни миллионов записей) и растут незначительно, записи в них не добавляются каждую секунду;
Такие таблицы можно хранить в специальном экземпляре БД – референсном, обновлять только там, а в шарды переносить с помощью логической репликации - https://postgrespro.ru/docs/postgresql/current/logical-replication;
Поиск данных и пэйджинг (Search & paging)
При равномерном распределении данных по шардам, мы не знаем заранее ключ шардированя если только у нас уже нет его ID с seed-ом, который и является идентификатором шарда;
Предположим, такой бизнес-кейс: вам нужна третья страница из ленты заказов магазина, отсортированных по дате обратном порядке;
У вас два узла с данными и плюс узел, который играет роль координатора; координатор может быть специальным прокси или бизнес-сервисом, который сам по себе умеет в шардинг;
Координатор получив запрос пользователя транслирует его в каждый из шардов изменив границы страницы; Вы не знаете сколько данных у вас на каждом из узлов поэтому чтобы вырезать тр��тью страницу вам нужно запросить с первой строки по N, где N = размер страницы * номер страницы + 1 (чтобы знать есть ли еще данные соответствующие критерию поиска в этом узле с данными)
Получив результаты от всех шардов, координатор объединяет результаты одновременно выполняя сортировку в один проход (ведь данные с узлов с данными возвращаются уже локально, для узла отсортированными)
Получив общий и теперь глобально отсортированный список записей, полученных со всех узлов с данными, Координатор вырезает новую третью страницу и возвращает ее пользователю;
И все вполне консистентно, по крайней мере до той же степени, до которой консистентен read consistency**.
И пример
Предположим, у нас есть шардированная таблица t1, с одной колонкой id, данные хранятся в двух шардах (A и B).

Пользователь (Frontend) присылает запрос на первую страницу размером в 2 записи из таблицы t1 отсортированную по id в порядке возрастания.
Back конвертирует этот запрос в следующий SQL:
SELECT id FROM t1 ORDER BY id LIMIT 3 OFFSET 0
OFFSET всегда 0, LIMIT <размер страницы * номер страницы + 1>; +1 нужен чтобы определить, есть ли еще данные, и должен ли Frontend рисовать пользователю кнопку “Следующая страница”;
Отправляет в каждый из шардов и в результате получает:

Выполняет сортировку и склейку в один проход и возвращает результирующую страницу пользователю (Frontend):

И предположим что Пользователь, не найдя интересных записей на первой странице и увидев кнопку “Следующая”, жмет ее и Frontend присылает запрос на вторую страницу:
Back конвертирует этот запрос в следующий SQL:
SELECT id FROM t1 ORDER BY id LIMIT 5 OFFSET 0
Page = 2, Page size = 2 --> 2 * 2 + 1 = 5;
И отправляет в каждый из шардов:

А если пользователь, захочет пойти на страницу 3 тогда:
Шарды получат следующий запрос:
SELECT id FROM t1 ORDER BY id LIMIT 7 OFFSET 0;
А Back вернет после обработки такую страницу:

Как видно из примера, минусом такого подхода является то что «досортировка» на стороне бэка может приводить к относительно большому расходу памяти если пользователь будет ходить «глубоко» по страницам.
Однако 1) на том факте что пользователи делают это редко построен целый Гугл, 2) для экспорта «вот всех всех записей по этим во критериям» в XLSX/CSV файлик можно сделать реализацию с асинхронной генерацией файликов отдельным сервисом по очереди и выделить для этого сервиса побольше памяти.
