Comments 33
Однажды я поступил в универ на бюджет (математическая специальность), как потом выяснилось, решив задачу неправильно! После экзамена перерешал и понял где закралась ошибка. Сразу расстроился, думая что не поступил, т.к. без той задачи я уже не проходил по баллам. Но случилось странное как мне казалось тогда чудо - проверяющие почти не срезали за него баллы, т. к. в принципе правильных рассуждений в нём было достаточно много. Мораль, думаю, понятна.
вместе с заданием шел скрипт наполнения таблицы тестовыми данными, который я тут приводить не буду. Он основан на функции random и всё время будет выдавать разный набор.
Совершенно напрасно. Пусть не воспроизведётся один к одному ваш набор записей. но тенденции останутся практически неизменными. Миллиона записей достаточно, чтобы их выровнять. Опять же всегда можно перегенерировать набор (несколько раз, или расширить его) и повторить - общее останется, а случайные выбросы уйдут. А сейчас - голые слова.
Но постойте, ведь в задании сказано, что
Сообщения обрабатываются в порядке возрастания значения поля “process_date“.
Здесь же получается, что 3-е число еще не обработано, 4-ое обработано, а 5-ое снова не обработано.
Странно все это подумал я и решил, что данный факт просто опишу в решении.
Что вы нашли странного? То, что сообщение взято на обработку, не означает, что оно будет обработано ранее более позднего. Как и не означает, что оно будет обработано вообще. Вполне себе нормальная ситуация, которую вы почему-то решили проигнорировать. так что ответ про "несостыковки по части логики" - совершенно справедливо.
использование бесполезного rows only и fetch вместо limit.
Документация PostgreSQL практически открытым текстом говорит, что LIMIT и FETCH - это разные синтаксические формы одной и той же операции. SQL:2008 ввёл, Postgres взял под козырёк - и не более. В чём вы видите различие в полезности?
В задании ничего не сказано про специфику обработки. А я уж поверьте наелся очередей и прекрасно понимаю, что могут быть дырки.
Если читать мою статью внимательно, то у меня есть вариант, который это учитывает.
Про скрипт. Мне не хотелось выкладывать задание полностью. По понятным надеюсь причинам
FETCH в продакшене не видел ни разу, потому что есть новый limit
Это какой такой "новый"? Тот, который общеизвестен, появился гораздо раньше FETCH, уж в Постгрессе-то точно, так что это по сравнению с ним FETCH - новый.
Итак. fetch имеет смысл только with ties, который тут не нужен. Я, конечно, пошел и перечитал. Не уверен я, что даже вы прям все-все детали документации помните. Но суть моей статьи вообще не в этом. Вы б лучше, что-нибудь по делу сказали, чем цепляться за мелочи.
Не уверен я, что даже вы прям все-все детали документации помните.
Не поверите, но я, прежде, чем писать, тоже сначала освежил память чтением документации. И даже пробежался по изменениям в версиях.
Вы б лучше, что-нибудь по делу сказали
Вы проигнорировали особенности набора данных. У вас же вся статья построена на "у вас неправильный набор данных, я его поправлю, а потом буду решать". Я ничего не имею против ваших вариантов решения - кроме того, что они решают ДРУГУЮ задачу.
Опять же - вы смело вмешиваетесь в данные: "При этом сделал число 2026-01-04 не обработанным". Сразу возникает вопрос, почему вы столь же смело не вмешались в структуру? Так, как предложил @NeKukSA. На поверхности же плавает..
Нет, я понимаю, что вам обидно. Рассказываете в общем хорошую вещь, а вам тычут в несоблюдение условий. Но ведь оно - имеется, из песни слова не выбросишь.
Т.е. если запрос ничего не вернул, значит этот день полностью обработан и рекурсия останавливается.
А если имеется день без записей? Я не вижу в задании, что это невозможно. Этой возможности ваше первое решение, то, что с рекурсией, не учитывает.
Итак. fetch имеет смысл только with ties, который тут не нужен.
WITH TIES - это дополнительная, и да, весьма небесполезная, фича, но никто не обязывает ей пользоваться. А без неё LIMIT и FETCH - это по факту синонимы в PostgreSQL. И слышать, что решение, имеет или не имеет смысл конкретная операция, принимается на основании её синтаксической формы - несколько странно.
Вы проигнорировали особенности набора данных
Я хочу вас спросить. Вы видели запрос с рекурсией? Понимаете, чем он отличается от того для которого потребовалось изменить данные? Понимаете, что эти запросы вернут одно и то же? Я только до сих пор не понимаю, как действует мнимый обработчик. Поэтому варианта основных два.
А если имеется день без записей?
А если кто-то другой загребет ту же запись? Тоже как бы по заданию можно предположить?
А если обработает, а признак не вернёт? Тоже по заданию...
Можно вас попросить проголосовать за неточность ТЗ?
Но ведь оно - имеется, из песни слова не выбросишь.
Там надо ускорить запрос, а не писать реальную очередь.
Не делал дополнительных и супер очевидных индексов только потому что в задании сказано изменить запрос. Изменить САМ запрос, чтобы он стал быстрей. Да и как вы, вероятно уже поняли, индексы нужно было не создавать, а убивать :) И последнее. Работа была 100% такая, что решение ускорить запрос индексом железобетонно не прокатило бы. Слишком уж просто.
Ну и кстати, в решении которое я им послал в пунктах ниже, были и индексы, и инклуды и даже партиции. Согласитесь, с партициями и индексы не нужны. Но все это было проигнорировано.
WITH TIES
Бог с ним. По настоящему это отношение к делу не имеет.
А почему не создать подходящий для запроса индекс? Копия уже имеющегося с условием
where status <> '10782572'::numeric
Индекс всегда будет содержать ссылки только на актуальные для шедулера строки, не будет занимать много памяти и не будет распухать при условии, что строки будут своевременно обрабатываться и автовакуум на базе не выключен)
Я бы начал разговор именно с такого предложения, даже выдумывать ничего не стал бы с запросом. Индексы - такой же инструмент, чего бы им не пользоваться.
Вы, конечно совершенно правы! Такой индекс всегда будет "в форме".
Но, в задании написано изменить сам запрос. И я чуть с дуба не рухнул, когда узнал, что изменить запрос у них означает выключить индекс вообще :)
То есть правильный ответ, с их точки зрения, это предварительный SET LOCAL enable_indexscan = off; , что ли? Бре-е-ед..
да, бред. думаю правильно:
drop index msg_idx;
create index msg_idx on msg (process_date) where status <> '10782572'::numeric;
в запрос добавить for no key update skip locked
лучше так:
CREATE INDEX msg_unprocessed_idx ON msg (process_date, id)
WHERE status <> 10782572;-- для очередей явно задавать порядок
id в include?
вряд ли, увеличится размер индекса; более сложно; там всего 100 строк; спросят что вы знаете о BitmapAnd. Про важность unit of order при обработке сообщений в очередях можно просто упомянуть
вряд ли, увеличится размер индекса
В этом индексе лежат только необработанные строки. А вот батч-выборка ORDER BY process_date, id — стабильная.
Ну да. С таким индексом никаких дополнительных плясок в коде не нужно. Это намного больше похоже на обработку очереди. Только в задании сказано тупо поменять запрос. Ой. Индекс выключить. Ой. Бог знает что сделать с дырками после обработки.
Я на самом деле не знаю, что было правильным ответом. Вот честно. Все что они мне сказали я тут привел. Но вот про мешающий индекс точно звучало. А я со стула упал.
Угу, есть такая проблема с интервью, когда что-то не говоришь т.к. считаешь, что это очевидно всем и нет нужны проговаривать, а потом узнаешь, что тебя отсеяли т.к. ты якобы не знаешь очевидных вещей. Поэтому надо на интервью все рассуждения, что лезут в голову, учиться озвучивать не стесняясь. Первым делом увидев эту задачу сказать, что надо индекс менять, а не запрос, а потом сказать, что раз по-условию менять именно запрос, буду пытаться что-то с этим сделать, хоть и добиться результатов близких к правильному индексу будет невозможно.
Я делал подобные штуки для прода. И я вообще вижу кривое задание. То как описано не заработает в проде, будет race condition за необработанные записи, потому что между условным статусом "новый" и "обработано" пройдет некоторое время.
Нужен либо дополнительный статус "в обработке", либо булева колонка "в обработке", а еще лучше lockID и lockExpirationDate. Там надо апдейтить записи в транзакции с блокировкой по строкам и проставлением статуса "в обработке", чтобы параллельный поток не взял в обработку записи, которые уже взял другой поток.
И уже на основе этого построить сначала правильный запрос в зависимости от БД - надо смотреть документацию. Запрос, в том числе, должен исключать записи со статусом "в обработке", а если сделали с колонкой lockExpirationDate - то и фильтр по ней. А потом уже думать над индексами. Ну и нагрузочные тесты.
Разные же задачи. Там, где нужна обработка, запрашивается только одна запись, или, точнее, апдейтится статус одной записи с возвратом проапдейченой записи.
А здесь задача вернуть 100 первых необработанных записей. Может для какого-то дашборда, который показывает первые Х записей по каким-то критериям. Хотя обычно страница по умолчанию 20 записей.
Ну и в целом, для обработки лучше не реляционную базу дергать, сжигая ценный ресурс - количество writing транзакций в секунду, а очереди, у которых есть retry, dead letter queue, и прочие вкусности.
SELECT id ,process_date ,statusFROM msgWHERE status <> '10782572'::numericORDER BY process_date FETCH FIRST (100) ROWS ONLY;
А почему не выбирается сразу колонка some_text?
Сам уже сталкивался с ситуацией, когда тестовое задание дают исключительно с целью, чтобы появился формальный повод для отказа. В этом случае как раз могут не полностью описать условия задачи или проверять твое решение на совсем других данных или что-нибудь еще. Возможно, и вакансии-то и не было на самом деле: собес провели, потешили свое эго, списали время в таск трекере и выдали тестовое, чтобы потом слить кандидата
Не люблю тестовые задания ни писать ни делать.
У тебя два пути: придумать абстрактную задачу или выдать фрагмент своего приложения.
В абстрактных задачах я не силен, для меня это выглядит будто нужно напрячься и спрятать в простой задаче непростой подвох. Если плохо спрячешь - задача слишком простая. Если хорошо спрячешь - в половине (скорее 80 процентов) ты констатируешь невнимательность. Круто когда твоя задача нанять суперзвезду, но чаще ты ищешь толковых ребят, которые ещё не всё умеют.
Теперь, значит фрагмент. Найди такой, в утром не слишком много контекстных ограничений (так-то это поле тут не нужно, но мы его потом отдаем в третью систему, забей), в котором мало "исторически сложилось" (система уже работает лет десять, за десять лет подходы пересматривали много раз) и чтобы задача не выглядела глупой (очень длинный идентификатор статуса для отработанной заявки вместо булевых значений) и чтоб код поместился в два-три файла. После этого вырежь половину используемых классов, упрости до базовых типов данных, увидь, что треть кода реально не нужна, но это функции так называемого "ядра". В процессе, пожалуйста, не спрашивай "зачем эти люди сделали именно так" (а там даже фамилии зафиксированы. Часто это твоя фамилия).
После всей этой боли, ты увидишь скучную задачу с очевидным решением. Ты попробуешь немного замаскировать ответ, чтобы хоть какая-то интрига осталась.
После этого придет хороший (без иронии) парень Вячеслав. Наложит на твой текст свою картину мира, что-то предположит в своей голове и предложит своё вырви-мозг решение, которое в общем-то нормальное для абстрактной задачи... Но ты бы не хотел, чтобы у тебя работал человек который помешает фразы "рекурсия" и "10 миллионов строк" в одном предложении и пишет нечитаемый в стрессовой ситуации (то есть несопровождаемый) sql-запрос с cte и сложным условием.
Более того, после всей твоей боли... Хороший человек Вячеслав (без иронии) пойдёт на форум и расскажет о твоей компании свои выводы которые он сделал на примере вычурного тестового задания, которое ты сам считаешь не показательным. Оно не показывает ни как Вячеслав программирует, ни как Вячеслав рассуждает, ни какие у него ценности.
В итоге, два хороших человека неслабо вспотели просто ради того чтобы HR себе галочку поставил.
Дак, что да или нет? В целом спич понятен, но нога букав.
Я думаю, вы увлеклись математической задачей и потеряли из виду физический смысл. Из-за этого ваше решение отличное но совершенно неприменимо в реальной жизни.
Тут второй вопрос вынуждает вас ответить "нет нужного индекса, потому он работает медленно". А третий вопрос "как его изменить для максимальной производительности" в существующем контексте разрешает вам создать индекс и заменить бесящий вас fetch на limit.
Вы же испугали работодателя отсутствием лени: вы согласились вынести себе мозг делая глупую и трудоёмкую работу потому что, вами показалось, что текст задания от вас это требует.
Не понимаю почему вы подумали, что на базу нельзя влиять: в своей жизни я видел ситуации, когда ты не можешь изменить текст запроса, и всего один раз я был в ситуации, когда нельзя оказывать влияние на постгрес (добавить вьюху, например), но даже там не контролировалось наличие и отсутствие индексов.
Тест на адекватность (просто термин, я считаю вас адекватным) заключается в необходимости сказать "вы знаете, можно многократно усложнить этот запрос и добиться прироста производительности без изменения БД, но это будет сложно сопровождать. Вы уверены что надо менять запрос, может лучше бахнем индекс?". Это нормально, ведь задания тоже пишут люди, а люди могут ошибаться или быть неаккуратными в формулировках. Это так же показывает, что вы работаете в команде, а не "выполняете поставленную задачу".
Если так посмотреть, получается, вы сами запороли собеседование
Вот теперь я вас понял. Спасибо за ваше мнение.
Теперь я вас удивлю и надеюсь все встанет на свои места и будет понятно, почему я написал эту статью.
Итак. В файле, который содержал решение были еще несколько вариантов. Там были еще парочка индексов, партиционирование (о нем, кстати, тут никто не подумал) и другие предложения. Т.е. вместо этих всех рекурсией и т.д. был тупой индекс, который содержал только не обработанные элементы очереди. Партиции тут вообще лучше всего подходят, т.к. не требуют индексов. Но все это было проигнорировано, а мои решения были названы сумбурными.
Что я делал дак то, что в отсутствии четкого задания я накидывал разные варианты.
К сожалению, все ещё не понятно почему вы написали эту статью)
Для меня это выглядит будто, вы предложили много решений, по которым получили общий комментарий. А нам рассказали не самое оптимальное решение и ответ компании. Но Бог с ним.
Я больше хотел обсудить партиции, для меня это какая-то больная тема, к которой мне, похоже, скоро придётся возвращаться.
Как вы тут придумали партиции? В голову приходит только разбиение по статусам.
При апдейтах мы (могу ошибаться) вынуждаем систему переносить запись из одной партиции в другую, что сопровождается обновлением двух (грубо говоря) таблиц и индексов под первичными ключами двух партиций. Как вы правильно сказали, на кассе это не проблема, а на нагруженной системе мы потребляем IO.
В то же время частичный индекс бы просто уменьшился бы да и все. Партиции даже в таком контексте выглядят более проигрышной стратегией.
Плюс, если системе понадобится искать что-нибудь по ServiceDate, вы прозреете насколько партиции все испортят.
Решение с частичным индексом вы тоже предложили, я так понял. Это хорошо.
Честно говоря я уже запутался что Вам понятно, а что нет.
Коротко.
Мне дали запрос и предложили его изменить. Изменить ТОЛЬКО запрос. Поэтому я накрутил логики и заодно предложил, как добиться производительности другими способами.
На что получил, как я думаю, не адекватный ответ, про мешающий индекс.
Все мои способы были с комментариями. С плюсами и минусами. Так, например, глобального PK PostgreSQL делать не умеет.
Но это все было просто проигнорировано.
Плюс, если системе понадобится искать что-нибудь по ServiceDate, вы прозреете насколько партиции все испортят.
Не знаю про какой вы ServiceDate, но есть partition pruning. Да и задачи такой не было.
Я так понял вы рассматриваете мой код применительно к продакшену, не понятно к какому. Но делать этого не нужно, потому что это было странное тестовое задание не понятно что проверяющие.
Странное тестовое задание или как упустить работу мечты