• Глава 1. Введение в документы

    Содержание

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

    Табличное мышление

    Если вы бэкенд-разработчик, то скорее всего работали с реляционными базами данных. Наиболее известные их представители — это MySQL, ее форк MariaDB, PostgreSQL, Microsoft SQL Server, Oracle DB. Реляционные базы называются так из-за лежащей в их основе реляционной алгебры (она же алгебра отношений). Это математический аппарат, который строится на отношениях (множествах кортежей) и операций над ними (проекция, объединение, пересечение и другие).

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

    • любой массив данных является таблицей;
    • любая операция над таблицей порождает таблицу.

    В каждой базе найдется техника, которая не подходит под эти правила. Однако общую картину это не меняет: в основе реляционных баз лежат таблицы и операции над ними.

    Read more →

  • Совет дня №15

    Пагинация, продолжение.

    Вернемся к случаю, когда нужна пагинация по убыванию времени (created_at, updated_at) и так далее. Этот критерий встречается так часто, что для него есть лазейка.

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

    От кортежа можно избавиться, если в качестве ID используется UUID v7. Дело в том, что v7 совмещает в себе эти два свойства: время и уникальность. Существуют разные подвиды UUID v7, но не зависимо от них пагинация будет:

    • точно попадать в границы;
    • использовать btree индекс.

    Итак, если у вас API и нужна пагинация, ваши варианты:

    • limit и offset; убедитесь, что offset не превышает какого-то разумного числа, например тысячи. Иначе вас будут парсить.

    • keyset: комбинация полей (some_field + id) для уникальности. Требует отдельного индекса.

    • UUIDv7 – если требуется пагинация по created_at. Это частый случай, поэтому рассмотрите его.

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

    Один из способов – использовать курсор и FETCH API. Апишка у него довольно простая:

    BEGIN;
    
    DECLARE cur_foo CURSOR FOR SELECT * FROM items;
    
    FETCH FORWARD 100 FROM cur_foo;
    FETCH FORWARD 100 FROM cur_foo;
    FETCH FORWARD 100 FROM cur_foo;
    ...
    CLOSE cur_foo;
    COMMIT;
    

    Объявляем курсор, затем в цикле вытягиваем по 100 записей, пока результат не пустой. В конце закрываем курсор.

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

    Кроме того, курсор привязан к конкретному соединению с БД. Использовать их в HTTP API невозможно.

    Другой способ обойти большую таблицу – сдампить ее в файл при помощи COPY (см. прошлый совет). Чтобы таблица не заняла весь диск, ее сжимают в gzip. В результате у вас оказывается файл my_table.gzip, вы отрываете его и спокойно парсите. Идея в том, чтобы забрать данные из базы как можно скорее и потом не мучить ее пагинацией. Если скрипт упадет, не придется насиловать базу снова – у вас уже есть файл.

    Третий способ – использовать драйвер, который позволяет обрабатывать записи в полете. Например, мой pg2. Функция execute принимает запрос и всякие опции. Среди прочих можно передать редьюсер – функцию трех тел:

    (fn
      ([]
       (make-acc ...))
      ([acc row]
       (conj acc row))
      ([acc]
       (finalize acc)))
    

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

    Полагаю, это все, что можно сказать про пагинацию.

  • Совет дня №14

    Итак, пагинация. Прежде всего, совет такой: если можете избежать пагинации, сделайте это. Пагинация — это состояние и его проброс. Лишние заморочки, пространство для багов.

    Пример, когда пагинации можно избежать — поиск. Как правило, релевантность поиска резко снижается с продвижением по выдаче. Условно, первые 10 позиций точные, еще десять — более-менее, остальное — просто чтобы заполнить выдачу. Вспомните, как давно вы были на пятой странице Гугла? Если нужной информации нет, лучше отправить другой запрос, чем скроллить страницы. Поэтому договоритесь: в поиске выдаем 50-100 позиций и никаких пагинаций. Живые люди не будут ей пользоваться, а вы только откроете ворота различным ботам и парсерам.

    Конечно, иногда пагинация необходима. Простой способ ее добавить — это выражения LIMIT и OFFSET. Дешево и сердито, но с недостатками. Во-первых, на больших смещениях выборка линейно замедляется. Это все равно что перематывать аудиокассету каждый раз с начала. В моей таблице миллион документов, и выражение

    select * from table limit 1 offset 500000
    

    занимает 7 секунд. Целая вечность!

    Второй минус — LIMIT-OFFSET неконсистентны. В перерывах между запросами может добавиться новая запись, и произойдет наслоение. Я видел такое много раз: листаешь первую страницу, переходишь на вторую и видишь наверху заголовок, который был внизу первой страницы.

    Дело в том, что пока я мотал первую страницу, добавили новую статью, и окно сместилось. Для новостного сайта это не страшно, но представьте, что у нас аналог Твиттера. Пока мы читали первые 20 твитов, кто-то наспамил еще двадцать. В результате вторая страница может полностью состоять из твитов, которые мы только что видели на первой! Это никуда не годится.

    Чтобы этого избежать, используют пагинацию по уникальному btree-индексу. Такой индекс однозначно определяет позицию в таблице: новые записи не сместят окно. У каждой таблицы есть такой индекс — это первичный ключ (id). Если логика позволяет листать по id, этим нужно воспользоваться. Пример:

    select * from items where id < ?
    order by id (desc)
    limit 100
    

    Id последней записи запоминается и передается в следующий запрос. Продолжаем до тех пор, пока выборка не пустая.

    Как быть, если id случаен, и требуется сортировка по дате создания? Задача усложняется, потому что дата не уникальна. Записи могут быть вставлены импортом и поэтому иметь одну дату. Граница окна пагинации может попасть на серию записей с одинаковой датой. Если запомнить дату последней записи и выполнить запрос

    select * from items
    where created_at < ?
    order by created_at desc
    limit 100
    

    , мы пропустим записи, которые не показали.

    Решение в следующем: уникальный атрибут + неуникальный дают уникальное комбо. В самом деле: если поле created_at не уникальное, то пара (created_at, id) — уникальная. Поэтому заводим составной индекс на пару (created_at desc, id desc) и листаем по нему:

    select * from items
    where (created_at, id) < (?, ?)
    order by created_at desc, id desc
    limit 100
    

    Обратите внимание, мы сравниваем кортежи (они сравниваются поэлементно). Общий принцип такой, что создается уникальный индекс на пару (поле, id). Он называется keyset, потому что “набор ключей”.

    Каждая сотрировка (возраст, имя, зарплата) требует своей пары, поэтому договоритесь о них заранее.

  • Совет дня №13

    Работая с ORM, избегайте проблемы 1 + N. Это когда вы обращаетесь к ссылочным полям, и база подтягивает сущности штучно, а не разом.

    Пример: магазин товаров, сущность Order ссылается на User (кто заказал) и Item (что заказали). Модель выглядит так:

    class Order(model.Model):
      status = EnunField(active, cancelled, pending...)
      created_at = DateTimeFiled(now=True)
      user = ForeignField(class=User)
      item = ForeignField(class=Item)
    

    Типичная задача — вывести активные заказы с информацией о клиенте и товаре. Разработчик делает так:

    orders = Orders.filter(status=active) \
      .order_by(created_at, desc).all()
    

    Затем он строит таблицу:

    for order in orders:
      print order.id, order.user.name, order.item.title
    

    Что произойдет под капотом? Сначала выполнится запрос:

    select * from orders where status = 'active'
    order by created_at desc;
    

    Тут все в порядке. Однако в цикле, когда происходит обращение к полям user и item, выполняются запросы get-by-id:

    select * from users where id = 1
    select * from users where id = 2
    ...
    select * from items where id = 100
    select * from items where id = 200
    ...
    

    В среднем запросов будет 1 + 2N. На практике в одном заказе может быть много товаров, а кроме того, возможны подгрузки других сущностей. Скажем, товар хранит ссылку на продавца. Если в таблице должен быть продавец, это будет еще +N запросов.

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

    Проблема 1 + N лечится разными способами. Первый — ORM может джойнить сущности, то есть выполнить запрос:

    select * from orders
    left join users on orders.user_id = user.id
    left join items on order.item_id = item.id
    where status = 'active'
    order by created_at desc;
    

    Такой джоин может нарушить пагинацию по limit/offset, но это страшно. Можно либо не использовать ее вообще, либо взять пагинацию по keyset, либо заменить limit выражением fetch.

    Другой способ — вытянуть записи по слоям на уровне приложения. Первый слой — это orders. Как только происходит обращение к user, ORM собирает все user_id и выполнят запрос

    select * from users where id in (?, ?, ? ...)
    

    То же самое с items — выгребаются уникальные item_id, и по ним делается запрос с IN.

    Некоторые ORM действуют тоньше. Если айдишников много, они выгребают смежные сущности кусками по 10-30. Таким образом, если нужно пройти 100 заказов, мы совершим 4-10 запросов, что не так страшно.

    Проблема 1 + N — настоящий бич ORM. Сколько подобных ошибок я исправил — затрудняюсь припомнить (и конечно, совершил сам).

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

  • Совет дня №12

    Дополнение вчерашней заметки насчет запросов в базу. Смотреть запросы, которые выполняются во время тестов – это хорошо, можно поймать много кривых вещей. Но есть еще одна техника: считать запросы и проверять их количество. Этим вы защищаете код от ситуации, когда небольшое изменение накинуло +20 запросов. В ORM подобные вещи случаются часто. В основном они вызваны проблемой 1 + N, о которой будет следующий совет.

    Фреймворки-гиганты предлагают встроенный метод подсчета запросов. Например, в Django, если тестовый класс унаследован от TransactionTestCase, доступен метод assertNumQueries. В целом подобный тест выглядит так:

    class TestSomeFunc(TestCase):
        dеf test_func(self):
            with self.assertNumQueries(4, using="db1"):
                some_func()
    

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

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

    # 1 auth
    # 2 permission check
    # 4 fetch data
    # 2 update data
    # 2 send notifications
    

    В одном проекте выяснилось: удаление сущности порождало 400 с лишним запросов. Разумеется, почти все они удалялись поштучно, потому что разработчики не дружили с БД.

    Для Django написаны в том числе сторонние сборщики запросов, их легко нагуглить. Полагаю, то же самое есть в Руби, Spring Boot и прочих комбайнах. Подсчет легко сделать в Кложе: достаточно динамической переменной и макроса.

    Не обязательно считать запросы во всех апишках: скажем, для get-by-id подсчет избыточен. Но удаление сущностей, сложные перемещения из одной таблицы в другую – очень желательно. Все для этого есть, просто воспользуйтесь.

  • Совет дня №11

    Если вы работаете с базой, то должны видеть все запросы, которые в нее идут. Это абсолютно обязательное условие. В особой степени оно касается ORM. Когда я делаю ревью, и в коде используются абстракции над базой, то всегда спрашиваю: какие запросы производит этот код? При этом ожидаю не гадание, а лог терминала, приложенный PR.

    Увидеть запросы можно следующим образом. Если Postgres запущен в Докере, передайте в command ключ -E:

    services:
      postgres:
     image: postgres:17
     command: -E
    

    Если это ванильный Postgres, включите логирование всех запросов опцией:

    log_statement = 'all'
    

    После этого лог отслеживают командой tail:

    tail -f '/var/log/postgresql.log'
    

    Расположение лога зависит от операционной системы и менеджера пакетов.

    Третий способ — настроить логирование в приложении. Скажем, в Джанго все операции с базой идут в логгер django.db.backends. Нужно выставить ему уровень INFO и направить в консоль.

    Минус такого логирования в том, что оно учитывает только свои запросы. Если кто-то ходит в базу минуя ORM, этих запросов вы не увидите.

    С другой стороны, мы перекладываем бремя логирования с базы на приложение. База одна, и если логировать каждый запрос, это замедлит ее. А поскольку узлов приложения много, на них это не скажется.

    Вместе с запросами полезно логировать время их выполнения. Заведите дашбоард, который находит топ-100 медленных запросов. Раз в месяц заводите задачу на оптимизацию.

    Расширение pg_stat_statements (доступно из коробки) ведет статистику запросов: число вызовов, среднее, минимальное и максимальное времена, размер переданных строк и так далее. Очень помогает на проде.

    Если вы не видите, какие запросы идут в базу, разработка подобна блужданию в темноте.

  • Совет дня №10

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

    Проблема в следующем. В базе лежит огромный JSON с каталогом услуг. Его схема примерно такая:

    DOC = {
     "id": ID,
     "code": CODE,
     "children": [DOC]
    }
    

    Головной элемент содержит некоторые поля и children, в которых список элементов. У каждого элемента свои children и так далее. Вложенность значительна: иная ветка достигает 30 элементов.

    Уж не знаю, почему документ хранят в таком виде. Подозреваю, что это дамп какого-то виджета для редактирования дерева. Разработчик поленился разложить его на плоские записи и собрать обратно, поэтому он хранится как есть. Разумеется, услуги часто нужны в плоском виде. Разработчики вынимают JSON из базы и обходят в приложении.

    Есть, однако, простой способ привести JSON к плоскому виду – рекурсивный запрос:

    prepare flatten as
    with recursive step as (
        select
            jt.*,
            0 as level
        from
            json_table($1::jsonb, '$' columns(
                id int4 path '$.id',
                code text path '$.code',
                children jsonb path '$.children'
        )) as jt
        UNION ALL
        select
            jt.*,
            step.level + 1 as level
        from
            step,
            json_table(step.children, '$[*]' columns(
                id int4 path '$.id',
                code text path '$.code',
                children jsonb path '$.children'
        )) as jt)
    select id, code, level from step
    order by level;
    

    Первая часть производит строку с полями id, code и children. В нем – JSON-массив вложенных элементов. Рекурсивная часть берет каждый элемент и делает то же самое: расщепляет его на id, code и другие элементы. Все это колбасится, пока вложенные элементы не исчерпаются. А теперь – пример:

    execute flatten($${
        "id": 1,
        "code": "Product A",
        "children": [
            {"id": 2, "code": "Product B", "children": [
                {"id": 5, "code": "Product E", "children": [
                    {"id": 6, "code": "Product F", "children": []}
                ]}
            ]},
            {"id": 3, "code": "Product C", "children": [
                {"id": 7, "code": "Product F", "children": [
                    {"id": 8, "code": "Product G", "children": []}
                ]}
    
            ]},
            {"id": 4, "code": "Product D", "children": [
                {"id": 5, "code": "Product E", "children": [
                    {"id": 9, "code": "Product H", "children": []}
                ]}
            ]}
    ]}$$);
    

    Результат:

    ┌────┬───────────┬───────┐
    │ id │   code    │ level │
    ├────┼───────────┼───────┤
    │  1 │ Product A │     0 │
    │  2 │ Product B │     1 │
    │  3 │ Product C │     1 │
    │  4 │ Product D │     1 │
    │  5 │ Product E │     2 │
    │  7 │ Product F │     2 │
    │  5 │ Product E │     2 │
    │  6 │ Product F │     3 │
    │  8 │ Product G │     3 │
    │  9 │ Product H │     3 │
    └────┴───────────┴───────┘
    

    Такой обход очень полезен. Быстро выяснилось, что плоские продукты нужны аналитикам, которые не владеют Кложей или другим ЯП, чтобы их распутать. Я написал им запрос, и они чуть не заплакали от счастья. Во-вторых, продукты изменяются редко, поэтому из запроса легко сделать материализованную вьюху и обновлять ее каждый день. Для аналитиков это обычная таблица, которую можно джойнить в запросах.

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

  • Совет дня №9

    В комментарии к прошлой заметке прислали ссылку на Кирилла Макевнина. Он пишет о хранении деревьев в базе, и первый абзац такой:

    Обычно, для хранения деревьев в базе в таблицу добавляется поле parent_id. Это дубовое решение, которое работает хорошо в небольшом количестве ситуаций. Кому-то даже может хватить, но есть запросы, на которых такая схема не работает. Например если нам понадобится извлечь ветку этого дерева. В таком случае понадобится рекурсивно выполнять запрос с parent_id где для каждого нового запроса parent_id становится id записи из предыдущего запроса. Кто-то пытается решать эту задачу прямо в коде, создавая очень не эффективное решение, кто-то с помощью возможностей базы данных, что сильно привязывает к ней, плюс, страдает интеграция с ORM.

    На мой взгляд, это неверно. Подход с parent_id – не дубовое решение, а вполне даже рабочее. Просто мне кажется, на момент написания заметки Кирилл не знал о рекурсивных запросах в Postgres (под рекурсивными запросами он имеет в виду запросы в цикле из приложения, и это конечно же плохо).

    Приведу минимальный пример с деревом в базе. Предположим, мы храним вот такое дерево:

    ┌─────────────────────────────┐
    │            ┌───┐            │
    │       ┌────│ A │────┐       │
    │       ▼    └───┘    ▼       │
    │     ┌───┐         ┌───┐     │
    │   ┌─│ B │─┐     ┌─│ C │─┐   │
    │   ▼ └───┘ ▼     ▼ └───┘ ▼   │
    │ ┌───┐   ┌───┐ ┌───┐   ┌───┐ │
    │ │ D │   │ E │ │ F │   │ G │ │
    │ └───┘   └───┘ └───┘   └───┘ │
    └─────────────────────────────┘
    

    Запишем его в базу:

    create table tree(
        id text primary key,
        parent_id text null
    );
    
    insert into tree values
        ('a', null),
        ('b', 'a'),
        ('c', 'a'),
        ('d', 'b'),
        ('e', 'b'),
        ('f', 'c'),
        ('g', 'c');
    

    Напишем запрос, чтобы выгрести поддерево, зная вершину:

    prepare get_branch as
    with recursive sub as (
        select tree.*, 0 as level from tree where id = $1
        UNION ALL
        select tree.*, sub.level + 1
            from tree, sub
        where tree.parent_id = sub.id
    )
    select * from sub order by level;
    

    Проверка:

    execute get_branch('b');
    ┌────┬───────────┬───────┐
    │ id │ parent_id │ level │
    ├────┼───────────┼───────┤
    │ b  │ a         │     0 │
    │ d  │ b         │     1 │
    │ e  │ b         │     1 │
    └────┴───────────┴───────┘
    

    Второй запрос, чтобы найти путь к вершине:

    prepare get_path as
    with recursive sub as (
        select tree.*, 0 as level from tree where id = $1
        union all
        select tree.*, sub.level - 1
            from tree, sub
        where tree.id = sub.parent_id
    )
    select * from sub order by level;
    

    Проверка:

    execute get_path('f');
    ┌────┬───────────┬───────┐
    │ id │ parent_id │ level │
    ├────┼───────────┼───────┤
    │ a  │ <null>    │    -2 │
    │ c  │ a         │    -1 │
    │ f  │ c         │     0 │
    └────┴───────────┴───────┘
    

    Как видим, в обоих случаях был один запрос, циклы не понадобились, база все сделала сама. Напоминаю, что with recursive позволяет собрать вершины вглубь и вширь, находить циклы и многое другое.

    Материализованные пути, о которых пишет Кирилл, тоже бывают полезны. Скажем, если у вас система файлов и папок (виртуальное файловое хранилище), то иной раз это даже лучше, чем parent_id, потому что позволяет эффективно искать по префиксу. Но есть и недостатки: если каждый узел – UUID, путь становится огромным. Кроме того, если нужно переместить поддерево, то в случае с parent_id достаточно переключить ссылку, а materialized path требует пересчета всех путей поддерева.

    Ну а отказывать себе в мощных средствах только потому, что с ними не дружит ORM… это примерно как рушить свою жизнь из-за непутевого друга.

  • Совет дня №8

    Если в базе есть хоть какой-то намек на иерархию – parent_id, children_ids и так далее – вам не избежать рекурсивных запросов. Если точнее, избежать их можно: выбрать данные и обойти в приложении. Но зачем, если это может сделать база?

    Рекурсивный запрос на самом деле не имеет отношение к рекурсии. Это цикл или свертка. В целом он выглядит так:

    with recursive NAME as (
      init-part
      UNION (ALL)
      continuous-part
    )
    select * from NAME
    

    Часть init-part выполняется однажды. Часть continuous-part выполняется много раз, при этом псевдоним NAME ссылается на результат прошлой выборки. Например, когда continuous-part сработает в первый раз, в NAME будет то, что вернула init-part. Когда continuous-part сработает второй раз, в NAME будет то, что вернула continuous-part до этого и так далее.

    Цикл заканчивается, когда в очередной раз continuous-part вернула пустой результат. Должно быть ясное условие остановки, иначе запрос уйдет в бесконечный цикл. В итоговом SELECT алиас NAME содержит результат всех операций.

    При помощи рекурсивного запроса легко обойти дерево потомков, графы, отношения, различные связи. Более того – внутри рекурсивного запроса доступны формы SEARCH DEPTH FIRST и SEARCH BREADTH FIRST, выявление циклов по массиву вершин и другие приятные вещи. Так что когда на собеседовании скажут обойти граф, расчехляйте psql!

    Последнее, кстати, не такая уж и шутка. Скажем, дали вам файл с вершинами. А откуда взялся файл? Скорее всего, из базы. “Вот где графы храните, там и обходите.” (с) Поэтому связные структуры я обхожу в SQL без выгрузки в приложение. Это и быстрее, и результат виден сразу, и удобно коллегам-аналитикам, которые не знают Кложу/Питон.

  • Совет дня №7

    Коль скоро COPY – ваш лучший друг, присмотритесь к бинарному формату. В среднем он на 30% меньше, чем текстовый и CSV. Числа хранятся компактно, а не по принципу “байт на разряд”. Строки не экранируются. Это очень удобно: не нужно бежать по строке и проверять, есть ли обратный слэш и что-то за ним. Прочитал байтовый массив, обернул в (new String) и готово.

    Бинарный формат COPY в целом прост. Первые 19 байтов можно пропустить – это заголовок и резерв под флаги, которые в данный момент не используются. Далее идет набор строк. Каждая строка – это пара (int2, content), где int2 – число колонок. Оно одинаково для всех строк, но дублируется. Последняя строка содержит -1, а за ней ничего нет.

    С свою очередь content – это набор пар (int4, field) – длина поля и его содержимого. Если длина -1, то значение null, а содержимого нет. В зависимости от типа поле читают по-разному. Перечислять все типы и их особенности я не буду: для этого есть библиотеки. Радует, что библиотек для бинарного формата Postgres все больше: в Гугле находятся версии для Python, Node.js, Go и так далее. Среди прочих есть и мои реализации: pg-bin на Кложе и модуль org.pg.codec на Джаве в составе pg2.

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

Страница 2 из 106