company_banner

PostgreSQL: как всего одно изменение привело к росту производительности в 9 раз

Автор оригинала: James Long
  • Перевод
В самом сердце проекта Actual, который предназначен для управления персональными финансами, лежит система синхронизации данных собственной разработки. Недавно я реализовал в проекте полное сквозное шифрование (оно, правда, пока не вышло в продакшн). Эта работа вдохновила меня на исследование производительности внутренних механизмов системы. Сегодня я хочу рассказать об одной возможности PostgreSQL, которая позволила добиться 9-10 кратного увеличения производительности проекта.



Проблема 169000 сообщений


Actual — это полностью локальное приложение, синхронизация выполняется в фоновом режиме с использованием бесконфликтных реплицированных типов данных (Conflict-free replicated data type, CRDT). Это означает, что серверная часть проекта очень проста. Её основная задача заключается в том, чтобы сохранять и загружать «сообщения» для клиентов. Весь код, касающийся синхронизации данных, занимает всего лишь примерно 200 строк JavaScript.

Нам нужно обрабатывать очень много сообщений для того чтобы синхронизация была бы быстрой. На самом деле, как-то раз произошла одна странность: новый пользователь за один день сгенерировал 169000 сообщений. Это — показатель, который очень резко отклоняется от обычных значений. Например, при импорте в систему 1000 транзакций генерируется примерно 6000 сообщений. И это — вполне нормальное значение, которое больше, чем среднее количество сообщений, генерируемых одним пользователем за день. Я так думаю, что эти 169000 сообщения были сгенерированы при попытке использования API для импорта в систему большого объёма данных. У нас есть разные API для подобных целей. Но такое количество сообщений — это очень много. Поэтому я решил превратить их в бенчмарк для исследования производительности Actual.

Я попытался пропустить через систему 169000 сообщений и «подвесил» сервер. Запрос был отклонён по тайм-ауту, а сервер всё ещё пытался разобраться с сообщениями, замедляя всё остальное. Я тут же понял, что проблема заключается именно в этом.

Сообщения хранятся в базе данных PostgreSQL. Соответствующая таблица выглядит так:

CREATE TABLE messages_binary
  (timestamp TEXT,
   group_id TEXT,
   is_encrypted BOOLEAN,
   content bytea,
   PRIMARY KEY(timestamp, group_id));

В таблице хранятся маленькие бинарные блобы, отметки времени и «группы синхронизации», к которым эти блобы принадлежат.

Сервер оказался перегруженным из-за того, что попытался добавить в базу данных огромное количество строк. К сожалению, мы, добавляя в базу сообщения, не можем просто выполнить один запрос, содержащий множество операторов INSERT. Особенности наших CRDT накладывают на работу с базой данных некоторые ограничения:

  1. Сообщение не может быть продублировано (оно идентифицируется полем timestamp).
  2. Нам нужно обновлять дерево хешей, эта процедура зависит от того, было ли сообщение добавлено в базу данных.

С первым ограничением справиться было легко. Так как мы сделали timestamp первичным ключом, мы можем выполнять запросы вида INSERT INTO messages_binary (...) VALUES (...) ON CONFLICT DO NOTHING. Выражение ON CONFLICT указывает системе на то, что при возникновении конфликта делать ничего не нужно. Конфликт первичных ключей возникает у дубликатов уже существующих записей.

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

if(inserted) {
  trie = merkle.insert(trie, Timestamp.parse(msg.timestamp));
}

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

Весь код обновления базы данных выглядит так, как показано ниже (тут используются некоторые абстракции над node-postgres):

await runQuery('BEGIN');
let trie = await getMerkle(runQuery, groupId);
for (let message of messages) {
  let timestamp = message.getTimestamp();
  let isEncrypted = message.getIsencrypted();
  let content = message.getContent();
  let { changes } = await runQuery(
    `INSERT INTO messages_binary (timestamp, group_id, is_encrypted, content)
       VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING`,
    [timestamp, groupId, isEncrypted, content]
  );
  if (changes === 1) {
    // Обновить дерево хешей
    trie = merkle.insert(trie, Timestamp.parse(timestamp));
  }
}
await runQuery(
  `INSERT INTO messages_merkles (group_id, merkle)
     VALUES ($1, $2)
         ON CONFLICT (group_id) DO UPDATE SET merkle = $2`,
  [groupId, JSON.stringify(trie)]
);
await runQuery('COMMIT');

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

Теперь суть проблемы стала мне совершенно понятна: мы выполняем запросы INSERT для каждого отдельного сообщения. В нашем примере, создающем огромную нагрузку на систему, мы пытаемся выполнить 169000 подобных запросов. PostgreSQL работает на отдельном сервере (правда, он расположен близко к серверу приложения). Выполнение такого огромного количества сетевых запросов и само по себе способно ухудшить производительность системы, не говоря уже о том, как подобная задача способна перегрузить PostgreSQL.

Какова реальная производительность системы?


Я знал о том, что данные в нашу базу добавляются медленно, но я не понимал того, насколько медленно. Поэтому я решил протестировать систему, использовав меньшее количество сообщений. Такое, чтобы их обработка могла бы быть нормально завершена. Оказалось, что для обработки 4000 сообщений нужно 6.9 секунды. Это — лишь результат профилирования вышеприведённого кода, затраты времени на передачу данных по сети не учитываются.

Это — колоссальная проблема UX проекта. В процессе обработки данных пользователю приходится сидеть перед экраном компьютера и наблюдать, как крутится, крутится и крутится значок синхронизации.

Если вернуться к вопросам архитектуры нашей системы, то вот что нам было нужно:

  • Выполнять как можно меньше запросов.
  • Знать о том, какие именно сообщения были добавлены в базу данных.
  • Атомарно выполнять операции по записи сообщений и по обновлению дерева хешей.

Мы могли бы узнать о том, какие сообщения уже существуют, и отфильтровать их, но это потребовало бы выполнения тяжёлых запросов SELECT (и одним запросом тут, вероятно, обойтись бы было нельзя, так как кого-нибудь вряд ли порадовала бы перспектива передачи в запрос 169000 параметров). У меня была ещё одна идея, которая заключалась в добавлении в базу сообщений с уникальным номером. Потом можно было бы выполнить запрос на получение сообщений с этим номером, так как этот номер имели бы только новые сообщения.

Оптимизация запросов


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

-- Передать несколько элементов в INSERT можно, по крайней мере, в PostgreSQL
INSERT INTO messages_binary (timestamp, group_id, content) VALUES
  ("1", "group1", "binary-blob1"),
  ("3", "group1", "binary-blobb6"),
  ("2", "group1", "binary-blobbbb");

Это — лучше чем объединять множество операторов INSERT в один запрос, так как такая конструкция, вероятно, будет работать быстрее. А ещё важнее то, что у нас была надежда на получение сведений о том, что было сделано.

Углубившись в документацию, я обнаружил выражение RETURNING оператора INSERT. По умолчанию PostgreSQL, добавляя данные в базу, не возвращает ничего кроме сведений о количестве изменённых строк. Но если выполняется запрос вида INSERT INTO table (value) VALUES (1) RETURNING id — система вернёт идентификатор новой строки.

Главный вопрос заключался в том, работает ли подобная конструкция так, как было нужно мне. А именно, если применяется оператор INSERT, рассчитанный на обработку нескольких элементов, и при этом в нём имеется выражение ON CONFLICT DO NOTHING, будет ли возвращён массив идентификаторов только для элементов, которые были реально добавлены в базу? Я подозревал, что подобная конструкция может вернуть идентификаторы всех элементов, даже тех, при записи которых произошёл конфликт, то есть таких, которые, в итоге, записаны не были.

Я, по-быстрому, написал скрипт для исследования вышеизложенной идеи. И тут мне повезло: выражение RETURNING работало именно так, как мне было нужно. Вот соответствующий запрос:

INSERT INTO messages_binary (timestamp, group_id, content) VALUES
  ('1', 'group5', '...'),
  ('2', 'group6', '...'),
  ('3', 'group7', '...')
ON CONFLICT DO NOTHING RETURNING timestamp;

Если, при выполнении этого запроса уже существовало сообщение, отметка времени которого равнялась 1, в базу записывались только сообщения 2 и 3. А возвращаемый массив выглядел как [{ id: '2' }, { id: '3' }]. Получилось!

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

Новый код выглядел примерно так, как показано ниже. Я всё ещё исследую безопасность вспомогательного механизма pg-promise:

// Используем вспомогательный механизм из библиотеки `pg-promise`
// для генерирования конструкций INSERT с множеством значений.
// Значения соответствующим образом форматируются.
// http://vitaly-t.github.io/pg-promise/helpers.html#.insert
let stmt = pgp.helpers.insert(
  messages.map(msg => ({
    timestamp: msg.getTimestamp(),
    group_id: groupId,
    is_encrypted: msg.getIsencrypted(),
    content: msg.getContent()
  })),
  ['timestamp', 'group_id', 'is_encrypted', 'content'],
  'messages_binary'
);
let { changes, rows } = await runQuery(
  stmt + ' ON CONFLICT DO NOTHING RETURNING timestamp'
);
rows.forEach(row => {
  trie = merkle.insert(trie, Timestamp.parse(row.timestamp));
});
// Запись дерева хешей…

Теперь взглянем на результаты:

4000 messages
Before: 6.9s
After: .75s
40000 messages
Before: 59s
After: 7.1s

Если вас это удивляет — знайте — всё выглядит именно так. Раньше на обработку 40000 сообщений нужно было 59 секунд. А теперь — всего 7.1 секунды. Это значит, что у нас появилась возможность обрабатывать в 10 раз больше сообщений за то же время, что и раньше.

Добавлю, что после выхода этого материала оказалось, что вышеприведённые данные были искажены из-за ошибки SQL. Каждый фрагмент данных оказывался большего размера, чем нужно (из-за неправильной кодировки бинарного блоба). После исправления этой ошибки сгенерированные INSERT-запросы стали примерно на 25% меньше. Это привело к тому, что 40000 сообщений теперь обрабатываются примерно за 5 секунд.

А как насчёт обработки 169000 сообщений?

Насколько большим может быть отдельный запрос?


Возможно, вас интересует то, как повёл себя в новых условиях наш бенчмарк — запись в базу 169000 сообщений. Ну, оказалось, что выполнить его всё равно нельзя из-за ограничений PostgreSQL. Эту проблему решить не так уж и легко.

Первой проблемой, с которой я столкнулся при попытке обработки 169000 записей, было падение Node.js. А именно, сбой вызвал вспомогательный метод pgp.helpers.insert из pg-promise при передаче ему настолько большого количества элементов. Я точно не знаю о том, почему это случилось, но я не вижу смысла выяснять причину этого, так как я столкнулся и с другими проблемами.

Для начала, обработать 169000 элементов — значит — выгрузить 21 Мб данных. Это неприемлемо из-за того, что слишком велики шансы того, что в ходе этой операции может произойти ошибка.

Если уменьшить бенчмарк до 100000 сообщений, то мы уже можем получить что-то рабочее. Запрос INSERT, в котором используется множество значений, представляет собой строку размером в 72 Мб. Попытка выполнить столь огромный запрос просто… подвешивает весь сервер. Я не знаю точно о том, в чём кроется источник проблемы. Не знаю я и о том, что, может быть, можно настроить PostgreSQL так, чтобы система могла бы обрабатывать подобные запросы. Но, опять же, запросы таких размеров мы обрабатывать не можем.

Более приемлемое решение заключается в разбиении запросов на части и в указании верхней границы количества сообщений для отдельного запроса. На роль подобного лимита, как кажется, хорошо подходят 40000 сообщений. Если говорить о размере, то объём данных составляет 5 Мб, на их обработку уходит 7 секунд (правда, и при таком количестве сообщений размер строки запроса составляет 40 Мб, но PostgreSQL без проблем обрабатывает такой запрос). Для обработки 169000 сообщений нужно отправить 5 запросов, в четырёх из которых содержится по 40000 сообщений, а в одном ещё 9000 сообщений. Общее время обработки таких запросов составит примерно 29.6 секунд (169000 / 40000 * 7). Это, при условии показа пользователям сведений о ходе проведения операции, не так уж и много для такого огромного объёма данных.

Но это — наихудший сценарий. Обычно нам не приходится работать с запросами, время обработки которых выражается в секундах. Чаще всего выполняются запросы на обработку 10-200 сообщений. На синхронизацию данных при таком подходе уходит что-то около 20 мс. 169000 сообщений — это, пожалуй, худший из худших сценариев, это ситуация, которая может возникнуть в том случае, если кто-то заваливает API тысячами запросов в секунду, а потом пытается синхронизировать изменения. Такое не случается практически никогда. Но мы должны быть готовы к ситуации, в которой нашим пользователям это может понадобиться.

Ещё одно улучшение


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

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

У меня есть такое ощущение, что использование для транзакций уровня изоляции Serializable может решить эту проблему. Транзакцию начинают, используя BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE. PostgreSQL отменит транзакцию в том случае, если обнаружит возможность возникновения состояния гонок. Я не полностью уверен в том, подойдёт ли это мне, учитывая то, что в рамках одной транзакции я что-то читаю из базы и что-то в неё пишу. Но если это мне подойдёт, то, при сбое транзакции, мне достаточно будет просто её перезапустить. В результате процессы синхронизации будут выполняться последовательно.

Что дальше?


Я пока не пробовал исследовать производительность синхронизации на 169000 сообщений. Клиент выполняет больше работы в ходе синхронизации из-за того, что в его системе присутствует немало дополнительных механизмов. Поэтому тут ещё достаточно места для оптимизации. Сомневаюсь, что в текущих условиях система справится с обработкой сразу 169000 сообщений, но вот в том, что она нормально обработает 40000 сообщений, я уверен. И я считаю удачной идеей разбиение больших запросов на запросы, рассчитанные на обработку 40000 сообщений. Это, в частности, упрощает обратную связь с пользователем, позволяя сообщать ему о ходе выполняемой работы.

Но, в целом, можно отметить, что оптимизация систем в расчёте на экстремально большие нагрузки — это весьма полезное занятие. Достигнутое улучшение производительности в 9-10 раз сказывается и на мелких запросах, которые составляют около 95% общей нагрузки на систему. Запрос, который раньше занимал 100 мс, теперь занимает всего около 10 мс. Это просто замечательно!

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



RUVDS.com
RUVDS – хостинг VDS/VPS серверов

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

    +24
    Странно, почему даже не попытались использовать Prepared statements для изначального INSERT-а, что было бы стандартным приемом для такого типа задач? В итоге все выродилось объединение/разбиение SQL-в в более длинные/короткие формы, и проблему так и не устранили…
    Ожидал увидеть что-нибудь похардкорнее, типа разбиение на партиции/подтаблицы, выключение индексов на время инсерта, анализ физического расположения записываемых данных с целью их максимального эффективного группирования, прединсертные сортировки и т.п.
    В общем странная статья, зачем ее было переводить…
      +4

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

        +1
        Не раз слышал про отключение индексов для ускорения вставки. Но вижу в этом следующие проблемы: при большом количестве записей в таблице, операция включения индексов будет занимать довольно значительное время. Что делать при параллельной вставке из нескольких потоков? Синхронизировать их?
        Вопросы не риторические и без сарказма, хотелось бы узнать, как решают при таком подходе описанные мной проблемы.
          0
          Вставка в таблицу без индексов обычно мгновенна и не зависит от размеров таблицы. Построение индекса на миллионах записей всегда быстрее, чем миллионы вставок в упорядоченную структуру. Я когда-то давно делал эти замеры на MySQL с количеством записей порядка миллионов, точных цифр у меня сейчас нет, но помню что разница по времени была в разы.
          Но тут могут повлиять внешние факторы. Например этих нескольких минут на построение индекса может просто не быть, так как блокировка недопустима. В этих случаях может помочь
          — разбиение на подтаблицы,
          — hot swap таблиц путем переименования,
          — доступ к данным через VIEW, который тоже можно переопределить мгновенно в зависимости от того, в каких таблицах в данный момент данные и индексы актуальны.
          Еще можно предварительно создавать записи-placeholder-ы в индексировнной таблице, задав тем самым значения индексов для непоступивших еще данных, а при поступлении уже апдейтить какие-надо неиндексированные поля. Такой прием позволяет физически группировать записи в тот же самый блоке на диске, и потом прочитать их горздо быстрее, чем если бы записи были созданы в разное время и в разных блоках.
          В общем поле извращений тут большое.
            0
            Вставка в таблицу без индексов обычно мгновенна и не зависит от размеров таблицы

            В постгре это точно не так и зависит от огромного количества факторов. В частности от интенсивности чтения, изменения и собственно самой записи.

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

            Миллион вставок всегда будут очень медленными. А если вставить батчем, то это утверждение может быть верным далеко не всегда.

            Остальные советы тоже хороши в ограниченном количестве случаев. Например, если работаете с финансами так лучше не делать :)
              0
              Остальные советы тоже хороши в ограниченном количестве случаев. Например, если работаете с финансами так лучше не делать :)
              «Остальные советы» реализовывались как раз в финансах в телеком-биллинге. Там вообще это стандартная практика — копить транзакции за разные месяцы в отдельных таблицах. Там же пробовали и записи-плейсхолдеры. Они во много раз увеличивали скорость формирования стейтментов в конце месяца, так как данные каждого клиента были уже сгруппированы на диске, но естественно пришлось заплатить цену: загрузка системы при записи возрасла в разы, ну и какое-то количество плейсхолдеров оставалось неиспользованными, и БД слегка увеличилась.
              Поэтому да, надо смотреть весь цикл жизни данных, в каком порядке ожидается поступление данных, когда, как часто и по каким критериям будет происходить чтение, насколько каждая из этих операций критична по времени, по целостности, какое есть железо, какая нужна надежность, требуемое время восстановления, и т.д. и т.п. И тогда уж принимать решение.
          0
          А еще для ноды можно использовать бинарный протокол PG и сетевые запросы уменьшатся в размерах

          Подробней в документации
            0
            У PG только один протокол, я в терминологии не силен — там передаются байты-комманды и си-строки с нулем в конце. Вероятно что бинарный.
            А pg и pg-native отличаются тем, что первый полностью на js написан, а второй обращается к сишной библиотеке, второй чуть-чуть быстрее.
            Если надо ещё ускорить попробуйте (мой) pg-adapter
              0
              Протокол один, а вот параметры запроса (например, вида INSERT… VALUES ($1, $2 ...)) могут передаваться в текстовом или бинарном виде.
                0
                Это как? Можно пример или ссылку?
                  0
                  Не знаю, как с этим дела в node.js. Как правило, любой pg драйвер написан на основе libpq, а там см. функции PQexecParams и PQexecPrepared:
                  paramFormats[]
                  Specifies whether parameters are text (put a zero in the array entry for the corresponding parameter) or binary (put a one in the array entry for the corresponding parameter). If the array pointer is null then all parameters are presumed to be text strings.

                  Values passed in binary format require knowledge of the internal representation expected by the backend. For example, integers must be passed in network byte order. Passing numeric values requires knowledge of the server storage format, as implemented in src/backend/utils/adt/numeric.c::numeric_send() and src/backend/utils/adt/numeric.c::numeric_recv().
                    0
                    Значит, все-таки, у PG один формат для передачи по сети, это библиотека libpq на стороне клиента может принимать в разном виде. Используется не в любом драйвере, в node по умолчанию pg без неё работает.
                      0
                      Значит, все-таки, у PG один формат для передачи по сети

                      Нет, именно что по сети гоняет по-разному.
                  0
                  Протоколов именно что два: версии 2 и версии 3.
              0
              согласен. статья написана чтобы похвалить штатную функцию библиотеки pg-promise, у которой этот метод вообще вписан в документацию. Где новости то?
              +28
              Статья уровня школьника, который узнал, что в базу можно вставлять несколько значений одновременно.
                +13
                +
                как всего одно изменение привело к росту производительности в 9 раз

                Ожидание — неожиданная конфигурация PSQL, интересный способ разбивки данных…
                Реальность — оказывается можно объединить несколько вставок в одну.
                  –4

                  Статья вполне ок — автор описал свой реальный опыт и грабли, на которые наступил.

                    0
                    Согласен. Вообще детские ошибки. А для таких множественных потоков и транзакций нужно использовать пуллер типа pgbouncer, т… к для postgres дорого создавать для каждого соединения отдельный процесс.
                    +5
                    Откройте для себя COPY.
                      0

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

                      +9
                      1. Как можно было изначально написать вставку единичными запросами, если на входе ожидалось тысячи сообщений? За такое джунам по рукам дают еще на стадии учебы.
                      2. Хранить метку времени в поле TEXT?? Почитайте про типы timestamp и timestamptz.
                      3. А для group_id зачем TEXT? Почти наверняка там строки вполне предсказуемой длины.
                        0
                        3. А для group_id зачем TEXT? Почти наверняка там строки вполне предсказуемой длины.

                        А это не BIGINT-FK должен быть вообще?
                        REFERENCES group(id), например

                        +6

                        Этот товарищ, кроме своего пет-проекта actual еще и работает в stripe.
                        Но это не главное.
                        Он тот, кто придумал prettier.


                        Это позор автора.
                        Статья не стоила времени, затраченного на перевод.

                          0
                          Спасибо за комментарий. Хоть понял что это перевод, т.к. в приложении хабра не видно, кто автор оригинала. Сходил на его сайт… мда… «и это люди запрещают мне ковыряться в носу» (с)
                          James Long is a developer & designer with over a decade of experience building large-scale applications.


                          P.S. Еще теплилась надежда, что в stripe он пришел на позиции джуна. Но нет "I talked to Alex Sexton 5 years ago about joining Stripe".
                            0
                            Почитал его блог. Понятно, почему его взяли в stripe. Ему больше нравиться заниматься продуктами. Т.е. да, технически он видимо слабоват (в пересчете на годы проведенные в разработке), но любит заниматься развитием и созданием продуктов. Тем более у него продукт в виде actual который можно пощупать и оценить.
                              0
                              Он сделал большой вклад в индустрию, сделав prettier.

                              Но это — позор, как он есть.

                              Такие статьи встречаются регулярно, и довольно часто в корпоративных блогах. Видимо, у людей какой-то план по публикациям и получаются такие материалы. Но его никто не заставлял в свой блог писать, я полагаю…
                              0
                              Если это он придумал prettier, то я готов ему все простить
                                0
                                Широкой вы души человек :)

                                Можно было бы это простить, если бы это на review нашлось :)
                              +1

                              Из текста следует что параметры запроса интерполируются в строку запроса.Если бы это не делалось и использовался бы параметрический запрос то можно было бы и на окна не разбивать.

                                +2
                                Автор какой-то агалтелый джун. Не понятно, как статья набрала +16.

                                К правильным комментариям выше я бы добавил UUID в качестве первичного ключа. И генерация его на клиенте. Ждать ответа от бека при этом не нужно. Можно на сервере реализовать вставку через очередь.
                                  +6
                                  Ждем от него цикл статей под общим заголовком «Читаем документацию к PostgreSQL.»
                                  0
                                  Хорошая статья, к тому же сагрила людей на полезные комментарии.
                                    +3
                                    А одному мне кажется, что использовать составной ключ с датой это не очень разумно?
                                      0
                                      С технической точки зрения структура базы в целом паршивенькая. А первичным там должен быть UUID с генерацией на клиенте.
                                        0
                                        честно говоря, никогда не сталкивался с генерацией uuid на клиенте, но уж что стараюсь всегда вбить в голову студентам, что первичный ключ ни в коем случае не должен быть составным.
                                          0
                                          Почему?
                                            0
                                            На моём небольшом опыте тут чаще всего возникает ситуация, что составные первичные ключи в какой-то момент могут по факту перестать быть первичными ключами (возможны задвоения). Используя же первичные ключи на основе автоинкремента или uuid всё таки исключаем такую возможность и если необходима функция, то её можно отдельно реализовать, а при необходимости отключить.
                                            При этом мы в целом уменьшаем размеры индекса по первичному ключу. Чем больше столбцов и чем более объёмные там типы данных, тем больше получается индекс, но это спорное утверждение, местами может быть и меньше.
                                              0
                                              Ну если составной ключ перестает быть первичным, значит изначально автор неправильно понял предметную область, т.е. допустил ошибку на этапе проектирования. В самом составном ключе ни чего криминального нет, особенно когда он естественный.
                                              Другой вопрос, что суррогатные ключ в виде автоинкрементных идентификаторов или UUID-ов удобным на практике в том числе и не разрабам. Второй мотив использования суррогатных ключей — зачастую сложность в выделении естественного ключа, а иногда и невозможности это сделать.
                                                +2
                                                На мой взгляд всё таки не всегда это происходит из-за ошибки. Иногда меняется область. При этом это может добавить некоторые трудности как раз разработчикам при изменении области, а суррогатный ключ минимизирует подобные проблемы.
                                                Другая же проблема на мой взгляд, которая как раз перекликается с невозможностью выделения естественного ключа — требование практического анализа большого объёма данных для проверки высказываний экспертов. Я очень часто сталкиваюсь с ситуацией, когда вроде бы эксперты говорят, что дублирования быть не может, но при анализе на предмет поиска как раз ситуаций, которых быть не может несколько таких не найдётся.
                                                Поэтому на мой взгляд суррогатный ключ это прям обязательная вещь, ведь нельзя спроектировать прям идеальную систему.
                                                  0
                                                  В составных первичных ключах есть еще один недостаток. Зачастую PK делаются не только для того чтобы обеспечить уникальность, но и для того чтобы сослаться на таблицу из дочерней foreign key-м. В таком случае в дочернюю таблицу придется тащить не одно, а два поля, что означает перерасход места под таблицу.
                                                  Ну и как заметил GooG2e, бывают ситуации когда естественные ключи перестают быть уникальными. Имхо, достаточно столкнуться с одним таким случаем на своей практике в проде на большой базе, чтоб перестать даже думать использовать естественные ключи.
                                                    0
                                                    Зато такие случае сразу позволяют выявить проблемы в бизнес процессах предметной области. Что очень полезно. Поэтому я тоже лично за использование суррогатных ключей (в том числе из-за FK), но при этом за уникальный индекс на естественном PK именна для отлова вышеприведенной ситуации.
                                        +1
                                        Кто возьмется написать расширение для браузера чтобы скрывать бесполезные статьи от ruvds? :)
                                          +2
                                          Мне кажется, тут же был вроде черный личный список авторов.
                                          +1
                                          Как всего одно изменение замедлило прод в 9 раз
                                          cool story
                                          :D
                                            0

                                            Вообще больше похоже на статью, как я делал и как делать не стоит.
                                            Если приложение предполагает высокую нагрузку то всё сделано не верно.
                                            На наших проектах мы прокачивам по несколько сотен тысяч записей в бд на обновление и вставку.
                                            При этом постгресс не испытывает видимых усилий.
                                            Любой проект и задача начинаеться с анализов механизмов и возможностей инструментов, а не со статьи на хабре как я открыл для себя америку.

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

                                            Самое читаемое