Возможно, вы как раз их тех, кто, просыпаясь каждое утро, задаёт себе три самых вечных жизненных вопроса: 1) как мне сделать потоковый SQL‑движок? 2) Что это такое — потоковый SQL‑движок? 3) Способен ли Господь наш сбрасывать те таблицы, коими владеет иной пользователь?

Я тоже ловил себя на том, что задаю себе эти вопросы, и порой они не оставляют меня даже во сне. Мне снятся различные SQL‑операторы, которые тычут в меня пальцем, насмехаются над моей некомпетентностью, а я умоляю их, чтобы они ответили на эти вопросы.

Так вот, где‑то год назад я (довольно смело, если «смелость» — это вообще про меня) снарядился как следует и пустился в долгий и тернистый путь, искать ответы на эти вопросы. Я шёл от монаха к пресвитеру, а от того — к жрецу макаронного монстра, и только в ужасе осознавал, сколь жалкие вопросы их занимают — например, каков смысл жизни, и как обрести мир с самим собой. Но, в конце концов, потерявшись в глубочайших расщелинах моего разума, я набрёл на часовенку, над входом которой значилось: «Epsio Labs». Тут я преисполнился откровения и вошёл в двери этого храма.

Друзья, сегодня я поделюсь с вами теми таинствами, которые познал там (за исключением тех, что подпадают под многочисленные NDA).

Что такое потоковый SQL-движок

Потоковый SQL-движок обеспечивает, чтобы результаты SQL-запросов всегда оставались актуальны, и их не приходилось пересчитывать. Даже в тех случаях, когда меняются данные, на которых основан запрос. Давайте разберём ситуацию на примере. Представьте себе простой запрос, например, SELECT count(*) FROM humans . Обычному SQL-движку (например, такому, как работает в Postgres, либо такому, как в MySQL) требовалось бы заново пересматривать все отдельные экземпляры humans при каждом выполнении такого запроса. Эта операция была бы весьма затратной и длительной, учитывая, что население на планете постоянно меняется. Но, работая с потоковым SQL-движком, можно было бы определить этот запрос всего один раз, и движок постоянно обеспечивал бы актуальность результата, учитывая как рождаемость, так и смертность. Не потребовалось бы никакого пересчёта, тем более не пришлось бы заново считать всех землян.

Как создать потоковый SQL-движок

Итак, простой пример, приведённый выше, вполне позволяет составить впечатление о том, как работает SQL-движок. Для начала требуется знать, как сработал бы любой обычный SQL-движок, если бы с его помощью потребовалось подсчитать количество людей. Далее, всякий раз при рождении нового человека вы бы увеличивали ваш результат на единицу, а всякий раз, когда кто-нибудь умирает, вы уменьшали бы результат на единицу. Всё просто, правда?  

Давайте нарисуем схему, на которой изложим процедуру обработки такого запроса – например, как будет обрабатываться рождение нового человека. У нас есть ряд узлов, по одному на каждую операцию, а также «конечный» узел, в котором будет находиться таблица результатов. Поскольку мы имеем дело с потоковым движком, он работает именно с изменениями, и обмен сообщениями между нашими узлами будет представлен так:  

 где ключ – это та величина, которую мы хотим изменить, а модификация показывает, на какую величину мы хотим её изменить. Так, если бы мы хотели передать сообщение вида: «Эй, мистер Узел, тут добавилось 1,5 яблока», то оно выглядело бы так:

 Каждый узел отвечал бы за приём изменений, выполнение операций того или иного типа, а затем за вывод результатов как таковых. Здесь есть и другая важная концепция: операции можно суммировать, при условии, что ключ у них один и тот же. Так, два изменения apple: 1.5 и apple: 2 эквиваленты apple: 3.5 . Бывает, что суммирование модификаций даёт 0, то есть, результат таков, как будто ничего не произошло. Например, у нас может быть два изменения, apple: 3 и apple: -3 . В таком случае ситуация эквивалентна той, в которой движку на вход не поступило никаких изменений. (Можете считать, что я дал вам три яблока, глубоко раскаялся в собственной щедрости и забрал у вас три яблока. С вашей точки зрения тогда бы ничего не изменилось – не считая того, что я испортил вам настроение). Чтобы этот пример выглядел более осмысленно, давайте нарисуем узлы, которые участвовали бы в нашем первом запросе (SELECT count(*) FROM humans), а затем добавим в систему первого человека, Адама.

Как видите, родился новый человек по имени «Adam». Узел «Counter» (Счётчик), названный так, вероятно, за способность считать, содержит внутреннее состояние, присущее сейчас процессу подсчёта людей в мире. Всякий раз, когда этот узел получает изменение (количество людей увеличивается млм уменьшается на единицу), движок обновляет свой внутренний счётчик, а затем выводит актуальные изменения. В данном случае требовалось учесть всего одно изменение: сообщить следующему узлу о необходимости один раз добавить единицу (в результате имеем текущее число людей, живущих на Земле). В таком случае следующим шёл бы узел «Final Results Table» (Итоговая таблица результатов). Это была бы полноценная реляционная таблица (хранимая, например, в Postgres). Можно представить себе, как любое такое действие транслируется в операцию DELETE или INSERT, в зависимости от ключа и модификации.

Далее давайте добавим Еву:

На этот раз узел счётчика должен вывести два изменения. Одно из них будет отменять предыдущее изменение, которое он вывел ранее, а второе — выдавать новое, обновлённое значение. В сущности, если бы мы попробовали просмотреть все изменения, которые узел Счётчик вывел в течение некоторого времени, то получили бы:

Поскольку разрешено комбинировать изменения с одинаковым ключом, вышеприведённое выражение эквивалентно следующему:

Если модификация равна нулю, это означает, что данное изменение можно просто удалить. Тогда в таблице с окончательными результатами у нас останется только 2: +1 – именно это нам и было нужно.

Что, к вам уже подбирается этот сладостный катарсис? Ага, ко мне тоже.

Другой пример, немного поинтереснее

Теперь представьте, что вы дьявол, и работаете с двумя таблицами:

  1. «Таблица людей», в которой два столбца. В один столбец заносится уникальный идентификатор, а в другой — имя.

  2. «Таблица зла», соотносящая id конкретных людей с (не)принадлежностью конкретного человека к числу злодеев.

Вполне очевидно, что нам было бы интересно подсчитать всех злодеев, и при этом учесть их поимённо:

SELECT humans.name, count(*) FROM humans 
JOIN evil_humans ON humans.id = evil_humans.human_id 
WHERE is_evil IS true 
GROUP BY humans.name

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

Узел фильтрации

Такой узел позволяет фильтровать изменения по ключу, безотносительно их модификаций. Если изменение «проходит» фильтр, то он пропускает его как есть. Чтобы вам было проще составить впечатление о таком механизме, давайте изобразим такой узел, который будет пропускать лишь те изменения, ключ которых равен слову «cats» (кошки).

Как видите, мы подали на фильтр 3,4 кошки, и он, как ни удивительно, пропустил их без каких-либо изменений. Теперь давайте попробуем пропустить через этот фильтр «собак» (dogs):

Ого! На этот раз фильтр ничего не пропустил. Думаю, идею вы уловили. Идём дальше.

Узел объединения

Узел объединения (Join) отвечает за приём изменений от двух узлов и за вывод тех изменений, у которых «ключи объединения» совпадают. Для этого он поддерживает внутреннее состояние (хранилище данных) для всех проходящих через него изменений, отображая эти изменения на соответствующие им ключи объединения. Так, в нашем примере с

JOIN evil_humans ON humans.id = evil_humans.human_id

мы бы создали один узел Join с двумя соответствиями:

  • Одно — слева, для отображения id на name

  • Второе — справа, для отображения human_id на is_evil

На практике эти отображения будут выглядеть примерно так:

Всякий раз, когда на узел Join будет поступать изменение с одной из сторон, он будет искать на другой стороне ключ, подходящий к этому изменению. Если найдёт, то выведет комбинацию значений. Давайте попробуем изобразить простой пример, где на узел Join подаётся новый человек по имени Томми с id 232, а затем вносим в базу данных изменение, согласно которому человек с id 232 не злодей.

Сперва новый человек по имени Томми вступает в мир:

Ладно, наш поток подтянул изменение, сообщающее узлу Join, что в базу данных добавлен Томми с id 232. Узел Join подыскивает соответствие для указанного изменения по ключу 232 — и ничего не находит. Следовательно, он ничего не выводит, однако обновляет своё внутреннее отображение, чтобы отразить факт добавления Томми. Это поможет нам при следующей операции:

Здесь узел Join получил изменение, пришедшее справа и сообщающее, что человек с id 232 — не злодей. Затем узел Join поискал отображения слева, нашёл соответствующее изменение, о котором мы говорили выше (232: Tommy‑) — и вывел скомбинированное изменение.

Но на этом история Томми не заканчивается. В любой момент могут поступить и другие изменения. Например, Томми упадёт с лестницы и погибнет. В результате узел Join получит Tommy, 232: -1, в результате чего этот узел выведет (232, Tommy, false): -1 — тем самым отменив предыдущее изменение Join. Либо у Томми может измениться показатель «злодейства» — этот пример мы прибережём на будущее.

Замечание — как вы могли заметить, здесь мы сказали, что «ключ объединения может измениться», но не ведём счёт модификаций при отображении объединений. В реальной практике, конечно, ведём, а затем перемножаем счёт модификаций с обеих сторон, чтобы получить итоговое количество модификаций.

Узел Group By

Узел «Group By» (Сгруппировать по) очень похож на узел-счётчик, уже рассмотренный выше. На самом деле, они просто близнецы-братья, но это уже история для другой статьи. Узел Group By выводит информацию, агрегированную по «корзинам». Так гарантируется, что все выведенные изменения непременно будут скомибинированы, и у вас останется не более чем по одному изменению на корзину. Вспомните, как мы брали в одну партию все изменения, с течением времени поступавшие на узел счётчика, и видели, что в итоге остаётся всего одно изменение, а остальные обнуляются. Это достигается путём поддержания внутреннего соответствия (в хранилище) между каждой корзиной и её агрегированным значением. Так, в нашем примере

SELECT humans.name, count(*) ... GROUP BY humans.name

узел Group By будет хранить соответствие примерно следующим образом:

Давайте попробуем изобразить простой пример и продемонстрировать, что произойдёт, если добавить на узел Group By новое имя, которое он никогда не видел:

Так что же здесь произошло? Изменение, поступившее на Group By — это приказ, по которому Group By должен увеличить Richard на единицу. Узел Group By просматривает свои внутренние соответствия и видит, что записи для Richard там нет. Он добавляет эту запись, а затем выводит Richard со значением, равным единице (это и есть ключ изменения (Richard, 1) ). Давайте продолжим и добавим ещё два экземпляра Richard:

Становится немного интереснее. Теперь узел Group By получает ещё одно изменение, согласно которому к Richard нужно добавить двойку. Узел Group By обновляет собственное внутреннее состояние, а затем выводит два изменения. Первое из них удаляет предыдущее, ранее выведенное изменение, а второе выводит новое, обновлённое значение количества для Richard.

Подытожим

Вернёмся к нашему исходному запросу:

SELECT humans.name, count(*) FROM humans 
JOIN evil_humans ON humans.id = evil_humans.human_id 
WHERE is_evil IS true 
GROUP BY humans.name

Чтобы подготовить контекст для следующего примера, представьте молодого заправского программиста оп имени Томми, id 232 (возможно, вы помните его из давешнего примера, в котором объяснялись объединения). Томми — крутой чувак, который регулярно минусил негодяев на StackOverflow (злодейство=false).

Как‑то раз Томми лягнула лошадь, и именно поэтому он неосторожно залил в master‑ветку какую‑то ерунду, одновременно с этим удалив всю непрерывную интеграцию. Этот случай мы представим в виде двух изменений. Одно из них отменяет предыдущее изменение, согласно которому Томми — не злодей (232, false): -1 , а второе добавляет новое изменение, согласно которому Томми — злодей (232, true): +1 :

Давайте кратко разберём вышеприведённый пример. Здесь мы вывели два изменения, о которых говорили выше: (232, false): -1 и (232, true): +1. Узел Join получает эту информацию, проверяет соответствие с другой стороны (id → имена), находит имя (Tommy) и выводит внесённые изменения, сопроводив их именем «Tommy». Далее фильтрующий узел WHERE is_evil IS true отсеивает изменение (232, false): -1, и, поскольку значение злодейства равно false, выводит только (232, true): +1. Узел Group By принимает это изменение, смотрит свои соответствия, видит, что на Томми ранее уже была заведена запись. Следовательно, узел Group By отсылает одно изменение, отменяющее другое изменение, выданное ранее (Tommy, 7): +1 (это произошло, когда мы в прошлый раз добавляли злого Томми). Затем этот узел высылает ещё одно изменение, также изменяющее количество Томми.

Подождите, но зачем же нам все эти проблемы?

Итак, теперь в мире 8 Томми-злодеев, и, чтобы узнать этот результат, нам не приходится заново прогонять запрос. Вы, конечно, могли бы сказать: «Мистер Дьявол, да вам же не нужен потоковый SQL-движок для таких операций. Если у вас есть таблица со списком людей, в которой указано, кто из них злодеи, просто создайте индексные списки. Да, вам всё равно придётся перебирать все данные при каждом запросе, но эти операции поиска, как минимум, будут быстрыми. Узел Group By каждый раз всё равно должен будет начинать работу с нуля, но, всё-таки»…


Действительно, мы можем оптимизировать запросы так, чтобы они проверялись буквально на лету, но этот приём работает лишь до некоторого предела. По мере добавления новых операций объединения, группирования и (свят-свят!) WITH RECURSIVE, оптимизировать запросы становится всё сложнее. А по мере добавления в систему всяких Томми, Тимми, Эдвардов, Дженни (не говоря уже о Рикардо, Самуэлях, Джеффри и Бенни), даже этих оптимизаций может оказаться мало (причём, это я ещё далее не начал рассказывать о пороках, связанных с кэшированием данных на предприятии). Как сказал бы мудрец, потоковые SQL-движки, совершенно обалденны при решении таких проблем; собственно, именно для их решения они и созданы.

Заключение

Ну что, готовы создавать потоковый движок? Все необходимые для этого элементы мы уже разобрали, но пока не затронули некоторых важных концепций. Например, как обеспечивать постоянную согласованность (чтобы никогда не приходилось выводить фрагментарных результатов), как задействовать при этом всю пропускную способность системы (постоянно тягаясь с возникающими задержками), как нормально взаимодействовать с хранилищем данных (кто сказал async‑io?). Может быть, напишу об этом другой пост, а может, и нет.

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

С тех пор, как я опубликовал эту статью, Старейшины из Epsio порешили, что более нет смысла утаивать свои тайны, поэтому выложили свой потоковый SQL во всеобщее пользование. Если хотите, можете посмотреть его здесь — поверьте, он просто летает.

Комментарии (39)


  1. Tzimie
    20.11.2023 14:55
    +1

    Очень интересно. Но что-то мне говорит, что в реальном мире это не так радужно

    Каков overhead предвычислений тысяч кверей, если мы не знаем, какие понадобятся?


    1. Ivan22
      20.11.2023 14:55

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


      1. Tzimie
        20.11.2023 14:55

        Но для online OLAP должно зайти


        1. Ivan22
          20.11.2023 14:55

          ну я не встречал еще ни одной online OLAP базы. Видимо не так-то это просто


          1. rcrvk
            20.11.2023 14:55

            Clickhouse позиционирует себя как OLAP)


            1. Ivan22
              20.11.2023 14:55

              а где там онлайн??? Что там есть сохраненные запросы которые сами обновляются в реал-тайме???? (Да и с аналитическими запросами там все очень плохо - хуже чем у любой класической субд), так что ни онлайна, ни олапа. Просто фактически быстрый кэш для уже рассчитанных витрин.

              Так-то такой онлайн как тут описано - это только кафка-стримс


  1. Ivan22
    20.11.2023 14:55
    +11

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


  1. Ivan22
    20.11.2023 14:55

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


  1. Tzimie
    20.11.2023 14:55

    А это своя СУБД или надстройка над существующей?


  1. ncix
    20.11.2023 14:55
    +1

    До боли напоминает "регистры" в 1С


  1. alexhott
    20.11.2023 14:55

    Под каждый запрос храни Многа Многа. А где потоки в SQL, я так и не нашел.


  1. mikegordan
    20.11.2023 14:55
    -6

    Мне платят 19000$ в месяц за то что я первый начал шилить и внедрять стриминговые движки в РФ, в сложный сайтах у которых JOIN происходит на клиенте или partical join в api gateway - отклик повысился в разы, "скачки" сайтов при загрузки сведены к нулю, а продажи подросли в разы.

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


    1. Politura
      20.11.2023 14:55
      +1

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


      1. mikegordan
        20.11.2023 14:55
        -1

        отвечу после пересоздания очередного аккаунта на хабре. Заминусовали, появились ограничения.


  1. shasoftX
    20.11.2023 14:55
    +3

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

    Т.е. движок должен знать ВСЕ запросы которые ему придется выполнять?


  1. ptr128
    20.11.2023 14:55
    +1

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


    1. BogdanPetrov
      20.11.2023 14:55

      Судя по документации, не поддерживается: https://docs.epsio.io/sql-support/built-in-functions/


    1. Aquahawk
      20.11.2023 14:55

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


      1. ptr128
        20.11.2023 14:55
        +1

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

        Пример:


        1. Aquahawk
          20.11.2023 14:55
          +1

          Да, всё так, но много кого такие данные утраивают


          1. ptr128
            20.11.2023 14:55

            И у меня есть данные, где устраивает. Но так как такие данные, нередко, в тех же записях, но других колонках, где не устраивает - пользуемся ClickHouse. Который даже точную медиану считает очень быстро. За что его разработчикам премного благодарен )


            1. Aquahawk
              20.11.2023 14:55

              то что клик великолепен, тут даже вопросов нет, используем и в продакшне и дома даже у меня показания датчиков умного дома в клике лежат.


  1. RekGRpth
    20.11.2023 14:55

    Описание похоже на timescaledb


  1. akakoychenko
    20.11.2023 14:55
    +5

    Почитал доки. И опыт говорит, что лучше это в проде не использовать. Почему так думаю: пару лет назад я руководил продуктом, где мы создали свой потоково-аггрегационный костыль, чтобы на лету считать аггрегаты над 15ТБ базой в постгресе, куда за день вставлялось 2 млрд строк. Работало не то, чтобы очевидно - при вызове data api на модификацию данных динамически генерировался огромный SQL запрос, который и вставлял сырые данные, и обновлял аггрегаты одной транзакцией. Понятно, что data api старались дергать большими батчами, и понятно, что ряд операторов (например, argmax) не реализовывали из-за страха перед вычислительной сложностью или объёмом состояния, ибо эта штуковина блокировала всю систему. Но, зато, работало, как часы.

    Потом же, захотелось чего-то более гибкого. Чтобы и операторы можно было с O(N) сложностью внедрять, и, чтобы любой джун-говнокодер мог свой аггрегат написать и не увалить систему, когда там окажется O(N^2).

    Так что, когда один умный чел посмотрел на все это, и сказал, мол, не страдайте херней, и берите debezium, то это выглядело, как священный грааль. Слить с него в кафку, а там уже, в параллель ставь хоть 100 аггрегаторов, каждый со своим хранилищем и в своём контейнере. И, главное, все это гарантированно консистентно (сливать в кафку с data api была плохая идея, ведь была вероятность, что из-за нештатной ситуации состояние кафки и бд рассинхронизируются).

    Попробовали. На тестовой базе все круто. На проде под указанной выше нагрузкой debezium тупо не работает стабильно. Сколько девопсы не плясали с бубном, как мы не пытались его зашардировать и в много инстансов работать заставить. Поговорил с другими счастливыми пользователями - проблема не только у меня. А решение ведь куда более проверенное временем, чем этот стартапчик, основанный год назад.

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

    Мне могут возразить, что я наваливаю кейсы серьёзного хайлоада, а это решение для тех, у кого сотня юзеров в день. Но прикол в том, что им эта приблуда и не нужна. Даже, если они уже выросли с того, чтобы дергать оперативную базу на аггрегации на каждый чих. И с делания этого на реплике выросли тоже, можно near real-time лить в clickhouse/Redshift, и дергать уже их. Да, не О(1), но грубая сила скоростных olap движков свое дело сделает. А вот, когда уже и этого не хватит, то привет все радости жизни из предыдущего абзаца


    1. Politura
      20.11.2023 14:55

       Работало не то, чтобы очевидно - при вызове data api на модификацию данных динамически генерировался огромный SQL запрос, который и вставлял сырые данные, и обновлял аггрегаты одной транзакцией.

      А чем не устроил вариант с триггерами на таблицах для обновления этих агрегатов? Тоже было-бы одной транзакцией и выглядело-бы не слишком страшно.


      1. akakoychenko
        20.11.2023 14:55

        Хороший вопрос. Не факт, что вспомню прямо все причины сейчас.

        Но, кажется, основной причиной была моя личная непереносимость наличия исполняемого кода в БД. SQL запросы на 1000 строк норм, но вот наличие в БД исполняемой логики уже нет. Очень не люблю размазывание логики между репозиторием и БД. Да, оно синхронизируется на этапе миграции, но слишком много нюансов возникает. Команде было бы сложно продать мне наличие триггеров или хранимок;)

        Кроме этого, там была масса плюшек уровня целостной системы. Например, data api после обработки батча получал в ответ скрипта итоговые значения всех обновленных аггрегатов, и сразу же плевался ими в кафку, что позволяло системе быть реактивной и масштабируемой. Или, там была очень интересная система управления дневными партициями, чтобы при удалении партиции не считать все заново, а лишь переагрегировать агрегаты дней. Можно было принудительно забутстрапить новый агрегат из аналитического хранилища. Кажется, ещё что-то было из приколов, которые требовали больше контроля наружу


        1. Politura
          20.11.2023 14:55

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

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


          1. Ivan22
            20.11.2023 14:55

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


            1. asmm
              20.11.2023 14:55

              батч режим-то не применишь, чтобы не каждый инсерт отрабатывать, а сразу большую пачку

              В postgresql AFTER STATEMENT триггеры

              Примерчик в конце
              https://www.postgresql.org/docs/current/plpgsql-trigger.html

              даст большую фору логике в приложении


  1. domix32
    20.11.2023 14:55

    например, SELECT count(*) FROM humans . Обычному SQL-движку <...>
    требовалось бы заново пересматривать все отдельные экземпляры humans при каждом выполнении такого запроса.

    что-то мне кажется, что это неправда. Звёздные запросы по-идее должны сразу поле size у столбца забирать, а не пробегать по записям. Вот был бы там какой-нибудь DISTINCT, тогда б было понятно.


    1. Ivan22
      20.11.2023 14:55

      когда кажется - надо смотреть в план запроса. А в плане мы увидим full scan. Se la vi.


      1. gnomeby
        20.11.2023 14:55

        Зависит от движка DB, в некоторые метрики изначально встроены.


        1. Ivan22
          20.11.2023 14:55

          это в какие же?


          1. gnomeby
            20.11.2023 14:55

            MyISAM. В InnoDB есть примерный подсчёт, иногда точный не нужен.


  1. gnomeby
    20.11.2023 14:55
    +1

    Есть ещё ksqldb https://ksqldb.io/. Однако опыт использования показал 3 вещи:

    1. Иногда ты упираешься в тупик и дальше никак

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

    3. Некоторые вещи лучше сделать на атомарных счётчиках в Redis. А некоторые специальными движками в ClickHouse.


    1. ogregor
      20.11.2023 14:55

      Вот кстати то же прочитал про него книгу. И задумался о применении в качестве узла в системе SQRS для запросов. Или все таки он не подходит для больших нагрузок на чтение из его таблиц?


      1. gnomeby
        20.11.2023 14:55

        на проде не испытывали, не прошел R&D.


  1. sneg2015
    20.11.2023 14:55

    Не совсем понял. В начале где добавляли Еву к Адаму. Сперва удалили 1. Потом сказали что нужно к результату прибавить 1 и получили 2 человека. Вопрос если 1 удалили, где тогда содержалась удаленная единица чтобы к ней прибавить 1? Получилось, что мы к none прибавили 1 и получили 2 ?


  1. HADGEHOGs
    20.11.2023 14:55

    Человек изобрел регистры накоплений 1С. Мои поздравления.