Обновить
2K+
219
Боровиков Кирилл@Kilor

Архитектура ИС: PostgreSQL, Node.js и highload

632
Подписчики
Отправить сообщение

Ну, как-то странно уже получить из базы 1000 записей, а вернуть клиенту только 25 из них - зачем тогда сразу читать в разы больше? А если клиент хотел всего 25 - зачем мы стали столько читать?

"Последовательный перебор" - это как, через JOIN?

  1. Правильно, sale_id - уникальный ключ (поскольку PK), поэтому (sale_dt, sale_id) - тоже уникальный ключ, который позволяет вычитывать блок записей, без оглядки на сайд-эффекты возможной неуникальности ключа.

  2. Потому что никто не гарантирует монотонного возрастания sale_dt вместе с sale_id. Наоборот, в учетных системах регулярно возникает задача отразить что-то "задним числом", завести документ за прошлую дату.

  3. Как составной индекс поможет при фильтрации по полю из связанной таблицы конкретно в приведенном примере?

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

Сотни тысяч / миллионы записей в таких таблицах обычно. Если бизнес крупный и существующий долго - сотни миллионов, но все равно единицы TPS.

64K - 51ms, 1M - 437ms, 8M - 3458ms - везде не лучший результат

Полный код
const objs = require('./objs-64K.json');

const keys = Array(8).fill()
  .map((_, i) => String.fromCharCode('a'.charCodeAt() + i)); // ключи ['a', .., 'h']

const hrtime = process.hrtime.bigint;

const copy = keys.map(key => `obj['${key}-copy'] = obj['${key}']`); // copy-сегменты

const caseText = Array(1 << keys.length).fill() // copy-блоки для каждой маски
  .map((_, mask) => {
    const res = [];
    const orig = mask;
    for (let i = 0; mask; i++, mask >>= 1) {
      if (mask & 1) {
        res.push(copy[i]);
      }
    }
    return orig ? `case ${orig}:` + res.join(',') + `;break;\n` : '';
  })
  .join('');
const fnCopy = new Function('obj', `switch (obj['mask']) {${caseText}}`)

const ts = hrtime();
objs.forEach(fnCopy);
console.log(Number(hrtime() - ts)/1e6, 'ms');

Ну, в случае indirect call он доступа туда не имеет.

return new Function('obj', res.join(','));

У меня получились ровно те же результаты, что и для eval.

"Не пропускает пуши с eval" - это такая же паранойя, как и "goto - зло". То есть грамотный разработчик хоть на лету файл сгенерирует и сделает require, но это вынуждает его искать лишние лазейки.

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

А в чем для транслятора должна быть принципиальная разница между текстом функции из файла и текстом, сгенерированным в рантайме?

Если немного модифицировать, то можно, да, хоть это и не совсем фильтрация:

SELECT DISTINCT
  (first_value(T) OVER(PARTITION BY client_id ORDER BY dt DESC)).*
FROM
  orders T;

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

А как first_value поможет отфильтровать остальные записи по ключу?

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

Конечно, тут можно начать вводить флаги на записи клиента "были заказы", "взят в работу", "выставлено КП", ... - но это имеет косвенное отношение к задаче, потому что условия отбора заказов могут оказаться нефиксированными.

Тут нас может поджидать проблема, если в clients лежат не только "активные" клиенты, уже делавшие заказы, но и все потенциальные, коих может быть и миллион - а такой запрос масштабируется линейно именно по ним.

Посмотрите все серии статей от @erogov, там много полезного найдется.

Вот join-узлы обеспечивают только способ соединения самих записей, а чтение для них осуществляют нижележащие Scan-узлы.

В целом - так. У нас 92 страницы индекса - их мы вычитаем каждую однократно. В таблице - 521, но каждую из них мы читаем многократно, но ровно одну для каждой записи из индекса.

В моем профиле есть ссылки на статьи о многих способах оптимизаций для PG, и в рассылке Planet PostgreSQL интересное встречается.

Ага, индекс и Incremental Sort в памяти помогли, но все равно не лучше JOIN.

Даже если предположить, что при увеличении work_mem время на сортировку обнулится, все равно останется еще 15мс.

Я так понимаю по WindowAgg / Run Condition, что план из PG15, а на более ранних версиях было бы еще дольше. Хотя сравнивать при дисковой сортировке, съевшей 37мс не особо корректно.

Seq Scan настолько быстр, поскольку читает всю относительно небольшую таблицу как раз последовательно, а вот Index Scan после прочтения каждой записи индекса вынужден прочитать запись heap таблицы - ровно против этого придумали Index Only Scan.

Причем чтение с диска и pagecache тут у нас не играли, поскольку везде только shared hit - то есть было чтение страницы только из памяти (иначе было бы shared read).

Вот тут я рассказываю подробнее, как разные методы доступа работают.

Информация

В рейтинге
Не участвует
Откуда
Ярославль, Ярославская обл., Россия
Дата рождения
Зарегистрирован
Активность