Привет, Хабр!

В этой статье мы попробуем взглянуть на архитектуру учетных систем (ERP, CRM, WMS, MES, B2B, ...) с позиций функционального программирования. Существующие системы сложны. Они базируются на реляционной схеме данных, и имеют огромный мутабельный стейт в виде сотен связаных таблиц. При этом единственным «источником правды» в таких системах является хронологически-упорядоченный журнал первичных документов (отпечатков событий реального мира), которые, очевидно, должны быть иммутабельными (и это правило соблюдается в аудируемых системах, где корректировки «задним числом» запрещены). Журнал документов составляет от силы 20% объема БД, а все остальное — промежуточные абстракции и агрегаты, с которыми удобно работать на языке SQL, но которые требуют постоянной синхронизации с документами, и между собой.

Если вернуться к истокам (устранить избыточность данных и отказаться от хранения агрегатов), а все бизнес-алгоритмы реализовать в виде функций, применяемых непосредственно к потоку первичных документов — мы получим функциональную СУБД, и построенную на ней функциональную ERP. Проблема производительности решается благодаря мемоизации, а объем функционального кода будет вполне соизмерим с объемом декларативного SQL, и не сложнее для понимания. В данной статье мы продемонстрируем подход, разработав простейшую файловую СУБД на языке TypeScript и рантайме Deno (аналог Node.js), а также протестируем производительность сверток на примере типичных бизнес-задач.

Почему это актуально


1) Мутабельный стейт + избыточность данных — это плохо, особенно когда необходимо обеспечивать его постоянную синхронизацию с потоком документов. Это источник потенциальных расхождений учетных данных (баланс не сходится) и трудно обнаруживаемых побочных эффектов.

2) Жесткая реляционная схема хранения исходных и промежуточных данных дорого обходится в Big Data, гетерогенных системах, и в условиях быстрых изменений — то есть по сути везде. Мы предлагаем хранить документы в исходном виде, упорядочив по времени, разрешив связи «от новых к старым» и никогда наоборот. Это позволит рассчитывать большинство агрегатов однопроходными алгоритмами прямо из документов, а все остальные таблицы — не нужны.

3) SQL устарел, так как предполагает доступность любых данных в любой момент, а в распределенных системах это очевидно не так — при разработке алгоритмов Big Data нужно быть готовым к тому, что часть необходимых данных появится позже, а часть уже появлялась раньше. Это требует небольшого переосмысления языка запросов, и сознательной заботы о кэшировании.

4) Современные ЯП позволяют создать отзывчивую систему, оперирующую миллионами записей локально на ноутбуке, где РСУБД просто не установится. А если говорить о серверах — предлагаемая схема имеет больше возможностей для параллелизма, в том числе на кластерах типа SPARK.

Предыстория вопроса


Проработав достаточно долго с различным бизнес-ПО (системы учета, планирования, WMS), практически везде сталкивался с двумя проблемами — сложность внесения изменений в схему данных, и частое падение производительности, когда эти изменения таки вносились. Вообще, эти системы имеют сложную структуру, поскольку к ним предъявляются противоречивые требования:

1) Аудируемость. Нужно хранить все первичные документы в неизменном виде. Разделение на справочники и операции весьма условно, во взрослых системах справочники огранизованы с версионированием, где каждое изменение оформляется специальным документом. Таким образом, исходные документы — это иммутабельная часть системы, и она является единственным «источником правды», а все остальные данные могут быть восстановлены из нее.

2) Производительность запросов. Например, при создании строки заказа на продажу система должна рассчитать цену товара с учетом скидок, для чего необходимо извлечь статус клиента, его текущий баланс, историю покупок, текущие акции в регионе, и т.д. Естественно, вся необходимая информация не может быть вычислена «на лету», а должна быть доступной в полу-готовом виде. Поэтому существующие системы хранят удобные абстракции над строками документов (проводки), а также заранее рассчитанные агрегаты (регистры накопления, временные срезы, текущие остатки, сводные проводки, и т.д.). Их объем составляет до 80% размера БД, структура таблиц жестко фиксирована, при любых изменениях в алгоритмах — программист должен позаботиться о правильном обновлении агрегатов. По сути агрегаты это и есть мутабельное состояние системы.

3) Транзакционная производительность. При проведении любого документа нужно пересчитать все агретаты, а это обычно блокирующая операция. Поэтому алгоритмы обновления агрегатов — самая болезненная точка системы, и при внесении большого количества изменений имеется существенный риск что-то сломать, и тогда данные «разъедутся», то есть агрегаты перестанут соответствовать документам. Эта ситуация — бич всех проектов внедрения и последующей поддержки.

Устанавливаем основы новой архитектуры


1) Хранение. Основа БД — хронологически упорядоченный журнал документов, отражающих свершившиеся факты реального мира. Справочники это тоже документы, просто длительного действия. И документ, и каждая версия записи справочника — иммутабельны. Никаких других данных в виде проводок / регистров / остатков в системе не хранится (сильное провокационное утверждение, в жизни бывает по разному, но нужно стремиться к совершенству). Документ имеет ряд системных атрибутов:

{
    "sys": {
        "code": "partner.1",  // человеко-читаемый код, идентификатор группы
        "ts": 1578263624612,  // временная метка записи
        "id": "partner.1^1578263624612",  // составной уникальный глобальный ID
        "cache": 1  // признак необходимости принудительного кэширования
    },
    ...
}

Документы с одинаковым code и разными ts образуют историческую группу, где актуальной считается последняя запись, остальные — историческими. Если установлен атрибут cache, последняя запись из группы попадают в так называемый топ-кэш, и одновременно все записи попадают в фулл-кэш, таким образом мы можем быстро извлечь запись справочника как по id, так и по code.

Документы могут дописываться в конец журнала, и никогда в середину. Изменение или удаление (отмена) старого документа — это всегда новый документ, записываемый в журнал с текущим ts. Таким образом, причинно-следственная связь определяется положением документа в журнале, скачки в прошлое или будущее запрещены (при этом даты принятия к учету, даты исполнения планов могут быть любыми, но с точки зрения ядра системы это просто атрибуты).

2) Связи. Документы могут ссылаться друг на друга по id. В отличие от «sql foreign key» — указывать тип сущности, на которую ссылаемся, нет необходимости, так как сущности лежат вперемешку, а id уникален. Связи от ранних документов к более поздним запрещены. Это означает, что в любом пользовательском алгоритме при обработке текущего документа могут быть востребованы связанные документы, уже встречавшиеся в выборке ранее (и по идее они должны быть кэшированы — либо ядром, либо пользовательским алгоритмом).

3) Горизонт иммутабельности. Часть документов, с которыми ведется активная работа (т.н. открытые документы) не может быть зафиксирована, поэтому вводится понятие горизонта иммутабельности, а база данных разделяется на 2 физических хранилища — иммутабельное хранилище и текущее хранилище. Все документы в первом хранилище имеют временную метку меньше горизонта, они неизменны, а результаты всех сверток кэшируются и переиспользуются. Все что позднее — называется текущим периодом, и при каждом запросе второе хранилище сканируется заново. Такая схема дает линейное время. Горизонт иммутабельности — термин, хорошо знакомый коллегам из 1С, и бухгалтерам. Производительность системы зависит исключительно от размера бардака текущего периода, и в этом вопросе мировая бизнес-практика беспощадна — чем он меньше, тем лучше.

4) Алгоритмы. Журнал документов может храниться в любом виде — последовательный файл, документная БД, таблица РСУБД, поступать из внешнего стрима — главное чтобы они были извлекаемы в прямом либо обратном хронологическом порядке. Любой бизнес-алгоритм — это композиция функций filter(), reduce(), get(), gettop(), примененная к потоку документов. Ввиду отсутствия семантики JOIN, у пользователя остается возможность использовать вложенные подзапросы к БД, либо пытаться ограничиться одним проходом, помещая в пользовательский кэш все, что может потребоваться в будущем. Естественно, помогает системный кэш, хранящий как отдельные документы, к которым уже были запросы по id / code, так и результаты расчетов, выполненных ранее (при полном совпадении входных параметров этих расчетов).

5) Мемоизация, или кэширование. Результаты запросов и расчетов попадают в кэш в случаях:

  • документ имеет атрибут cache, при первом reduce() он записывается в фулл-кэш, и обновляет запись в топ-кэше;
  • документ извлечен запросом по id / code, и он находится в иммутабельном хранилище;
  • reduce() завершил расчет по иммутабельному хранилищу, промежуточный результат клонируется и записывается в кэш, а расчет продолжается по текущему хранилищу.

Мы видим, что в отличие от жестко-структурированного «кэша» в системах, основанных на РСУБД, мы имеем адаптивный кэш, наполняемый по мере работы системы. Необходимость экономить память заставляет ограничивать объем кэшируемой информации, поэтому, например, результат функции filter() не кэшируется, а результат reduce() — обязательно. Пользователю даются ограниченные инструменты управления кэшем.

6) Поиск бывает 3-х видов. Первый — когда при обработке текущего документа нам нужно найти несколько связанных документов по неточным критериям. В этом случае либо подзапрос, либо в своем reduce() заранее сохраням все что может потенциально потребоваться, и когда потребовалось — ищем в этой выборке. Второй вид поиска — когда нам нужно получить актуальные элементы справочника, без исторических данных (т.н. текущий срез). В этом случае используется топ-кэш, который как раз хранит такие элементы. В третьем случае это fullscan по базе в обратной хронологической последовательности. Насколько целесообразно кэшировать результаты пользовательских поисков — вопрос дискуссионный, в какой-то мере очевидно необходимо, например с ограничением объема выборки.

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

Простая функциональная СУБД


Итак, попробуем что-нибудь написать. Язык TypeScript выбран за идеальное сочетание скриптового динамизма и типизации, рантайм Deno — за удобную поддержку TypeScript и WASM, а также наличия Rust API, что теоретически дает нам шанс ускорить некоторые алгоритмы (хотя это неточно).
Документы в нашей СУБД будут храниться в виде 2-х последовательных файлов, содержащих объекты JSON, разделенные символом "\x01", так как это позволяет написать быстрый потоковый парсер. API чтения состоит пока всего из 3-х функций:

type Document = any
type Result = any

public async get(id: string): Promise<Document | undefined>

public async gettop(code: string): Promise<Document | undefined>

public async reduce(
    filter: (result: Result, doc: Document) => Promise<boolean>, 
    reducer: (result: Result, doc: Document) => Promise<void>,
    result: Result
): Promise<Result>

Первая функция возвращает документ по id, вторая возвращает последний документ с заданным code, третья осуществляет фильтрацию и свертку, принимая на вход функцию фильтрации, функцию свертки и начальное значение аккумулятора. Мы сознательно не использовуем цепочку filter().reduce(), так как хотим кэшировать итоговый результат, а в случае цепочки — кэшировать отдельно результат фильтрации расточительно, а кэшировать результат свертки без знания условий фильтрации — бессмысленно. Поэтому reduce() получает на вход сразу все необходимое для расчета, и использует составной хэш от трех параметров в качестве ключа мемоизации.

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

Исходные данные


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

Партнеры и номенклатуры

{
    "sys": {
        "code": "partner.1",
        "ts": 1578263624612,
        "id": "partner.1^1578263624612",
        "cache": 1     
    },
    "type": "partner.retail",
    "name": "Рога и копыта ООО"
}
{
    "sys": {
        "code": "invent.1",
        "ts": 1578263624612,
        "id": "invent.1^1578263624612",
        "cache": 1     
    },
    "type": "invent.material",
    "name": "Гвоздь строительный 20мм"
}

Атрибут type — пользовательский, его иерархия никак не используется ядром, а лишь в пользовательских алгоритмах. Также не имеет значение семантика атрибута code — для ядра это просто строка.

Покупки и продажи

{
    "sys": {
        "code": "purch.1",
        "ts": 1578263624613,
        "id": "purch.1^1578263624613"  
    },
    "type": "purch",
    "date": "2020-01-07",
    "partner": "partner.3^1578263624612",
    "lines": [
        {
            "invent": "invent.0^1578263624612",
            "qty": 2,
            "price": 232.62838134273366
        },
        {
            "invent": "invent.1^1578263624917",
            "qty": 24,
            "price": 174.0459600393788
        }
    ]
}

Документы отличаются только типом (purch | sale), cтроки хранятся прямо в документе (в реляционной схеме они лежали бы в отдельной таблице).

Реализация алгоритмов


Анализ продаж
Считаем общую сумму продаж, средний чек, и среднее количество строк на документ.

import { FuncDB } from "./FuncDB.ts"
const db = FuncDB.open('./sample_database/')

let res = await db.reduce(
    async (_, doc) => doc.type == 'sale',  // фильтруем только продажи
    async (result, doc) => {
        result.doccou++
        doc.lines.forEach(line => {  // цикл по строкам документа
            result.linecou++
            result.amount += line.price * line.qty
        })
    },
    {amount: 0, doccou: 0, linecou: 0}  // инициализируем аккумулятор
)

console.log(`
    amount total = ${res.amount}
    amount per document = ${res.amount / res.doccou}
    lines per document = ${res.linecou / res.doccou}`
)

Обороты в разрезе номенклатур и партнеров
По сути это сводная таблица, поэтому в качестве аккумулятора используем Map.

class ResultRow { // строка результирующей таблицы
    invent_name = ''
    partner_name = ''
    debit_qty = 0
    debit_amount = 0
    credit_qty = 0
    credit_amount = 0
}

let res = await db.reduce(
    async (_, doc) => doc.type == 'purch' || doc.type == 'sale',
    async (result, doc) => {
        // поскольку внутри цикла у нас await - параллелим обработку строк
        const promises = doc.lines.map(async (line) => {
            const key = line.invent + doc.partner
            let row = result.get(key)
            if (row === undefined) {
                row = new ResultRow()
                // наименования получаем подзапросами к базе (они кэшируются)
                row.invent_name = (await db.get(line.invent)).name ?? ' not found'
                row.partner_name = (await db.get(doc.partner)).name ?? ' not found'
                result.set(key, row)
            }
            if (doc.type == 'purch') {
                row.debit_qty += line.qty
                row.debit_amount += line.qty * line.price
            } else if (doc.type == 'sale') {
                row.credit_qty += line.qty
                row.credit_amount += line.qty * line.price
            }
        })
        await Promise.all(promises)
    },
    new Map<string, ResultRow>() // результирующая таблица (аккумулятор)
)

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

Бенчмаркинг


Тестируем на сгенерированных данных:
?иммутабельное хранилище: 100 номенклатур + 100 контрагентов + 100 тыс. документов
?текущее хранилище: 10 номенклатур + 10 контрагентов + 10 тыс. документов
Использую доисторический ноутбук с процессором Intel Celeron CPU N2830 @ 2.16 GHz

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

Результаты - 100 тыс. документов за 11.1 секунды:
file: database_immutable.json:
?100200 docs parsed (0 errors)
?50018 docs processed (0 errors)
?11.098s elapsed
file: database_current.json:
?10020 docs parsed (0 errors)
?4987 docs processed (0 errors)
?1.036s elapsed
amount total = 623422871.2641689
amount per document = 11389.839613851627
lines per document = 3.6682561432355896

file: database_current.json:
?10021 docs parsed (0 errors)
?4988 docs processed (0 errors)
?1.034s elapsed
amount total = 623433860.2641689
amount per document = 11389.832290707558
lines per document = 3.6682073954983925

Если честно, я рассчитывал минимум на миллион документов в секунду. Разберемся, где у нас основная задержка на примере обработки первого файла:
?8.8s — чтение файла и извлечение строковых JSON, разделенных символом "\x01"
?1.9s — парсинг JSON в объекты
?0.4s — кэширование + пользовательский алгоритм
Заглянув в исходники Deno, я понял, основная задержка возникает при декодировании unicode, ведь V8 в качестве байто-дробилки подходит плохо. Это значит, что переписать критические куски на WASM/Rust будет очень просто, а если в качестве хранилища использовать нормальную объектную БД, то и парсинга JSON можно избежать, и тогда достичь миллиона записей в секунду — более чем реально. И это я не говорю про нормальное железо.

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

Результаты - 100 тыс. документов за 13.3 секунды:
file: database_immutable.json:
?100200 docs parsed (0 errors)
?100000 docs processed (0 errors)
?13.307s elapsed
file: database_current.json:
?10020 docs parsed (0 errors)
?10000 docs processed (0 errors)
?1.296s elapsed

invent name | partner name | debet qty | debet amount | credit qty | credit amount | balance amount
===========================================================================
invent 92 | partner 50 | 164 | 34795.53690513125 | 338 | 64322.24591475369 | -29526.709009622435
invent 44 | partner 32 | 285 | 57382.115033253926 | 209 | 43572.164405352596 | 13809.95062790133
invent 95 | partner 32 | 340 | 73377.08274368728 | 205 | 42007.69685305944 | 31369.38589062784
invent 73 | partner 32 | 325 | 57874.269249290744 | 300 | 58047.56414301135 | -173.29489372060198
invent 39 | partner 32 | 333 | 69749.88883753444 | 415 | 86369.07805766111 | -16619.189220126675
invent 80 | partner 49 | 388 | 74965.03809449819 | 279 | 51438.03787960939 | 23527.0002148888
invent 99 | partner 49 | 337 | 69360.84770099446 | 292 | 58521.2605634746 | 10839.587137519862
invent 38 | partner 45 | 302 | 63933.21291162898 | 217 | 44866.95192796074 | 19066.26098366824
invent 34 | partner 45 | 343 | 69539.75051653324 | 205 | 41155.65340219566 | 28384.09711433758
invent 41 | partner 45 | 278 | 63474.209440233106 | 258 | 45246.446456763035 | 18227.76298347007
< tail skipped >

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

Резюме


В целом я доволен результатом, схема получилась вполне рабочая, проект можно развивать, если у кого есть мысли — пишите. Буду благодарен за ссылку на публичные обфусцированные данные, приближенные к реальности (счета-фактуры, EDI, или что-то подобное), необходимые для полноценного тестирования.

Полный код на гитхабе

UPD
1) Статья в тему, благодарность VolCh за наводку.
2) Аналогичный подход реализован в CouchDB, благодарность apapacy за наводку.

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


  1. BugM
    09.01.2020 01:21

    Делать фулл скан всего и вся для любой простейшей аналитики такое себе решение…
    Да что там аналитика, простейший поиск не по ключу это фулл скан всего.
    Про join я даже не вспоминаю. Там пачка фуллсканов вылезет. Ну или надо гиганские кеши в памяти держать.
    Система умрет при любой минимальной нагрузке.

    На что только не идут люди лишь бы нормальную sql БД не проектировать.

    И в конце-концов есть Монга. Если уж очень хранилище разных документов хочется. Но метаданные справочники и прочее подобное все равно в sql лучше.

    PS: Все, или почти все sql бд поддерживают mater-slave из коробки и переживают выпадение мастера. При падении мастера база ненадолго в ридонли падает. А потом сама поднимается на запись и работает дальше.


    1. balajahe Автор
      09.01.2020 01:28

      У меня на проекте была система (DAX) которая в месяц набирала от силы 10 миллионов транзакций, закрытие периода — от 8 часов на кластере MSSQL. Основные причины — блокировки в базе при разносках коррекций + обновление кучи индексов. У меня фулсканом будет быстрее, и главное — блокировок нет, распараллелить возможно руками. Не от хорошей жизни придумали map/reduce на всяких хадупах, просто SQL не тянет бигдату, хотя его иногда сверху наворачивают на тот же SPARK.


      1. BugM
        09.01.2020 01:31
        +1

        Мапредьюсы и подобное нужно когда у нас петабайт данных.

        10 миллионов в месяц это даже смешно. Оно должно считаться на чем угодно без проблем. Данные даже в память влезут. Проверяйте свои алгоритмы рассчета. Там явно какие-то проблемы с ними.


        1. balajahe Автор
          09.01.2020 01:41

          Я уже там не работаю, да и не в этом дело. Идея — отказаться от мутабельного стейта в принципе. Пока функциональные языки слабо используются в проде, но тот же хаскель показывает какую-то сказочную производительность на иммутабельных коллекциях (тоже фулскан кстати), а если так посмотреть — чем обработка коллекций в памяти отличается от обработки первичных документов на диске? Только их количеством. Я бы посоревновался с тем же MSSQL, только мне нужна реальная бигдата, хотя-бы десятки миллиардов объектов, не знаю уж сколько это будет в байтах. Я как-то считал — одна сделка в ERP разносится по 60 таблицам где-то, а в источнике — просто документ со строками.


          1. BugM
            09.01.2020 01:50

            Тем что диск это дорого. Его медленно читать и еще медленее писать. Если у нас МНОГО данных и соотвенно hdd еще и random read очень дорогим становится.
            С памятью никакого сравнения. Память, с учетом кеша проца, линейно читается просто замечательно.

            Так в чем проблема? Проверить идею легко же. Основные сценарии чтения: поиск, group by с простенькими аггрегатами, join, order by. И пейджинг какой-нибуд сверху. Можно просто взять и написать. Ноута хватит. Нагенерить гигов 300 данных вообще не проблема, так чтобы в память точно не влазило.


            1. balajahe Автор
              09.01.2020 01:56

              Уже все проверено. Интернет-биллинг на оракле писал, 100 миллиардов записей, составные индексы вешали вставку, методом проб и ошибок пришли к той же схеме — большая таблица, организованная по 1 индексу, однопроходный скан с хинтом index_asc, и собственным кэшем в виде временных таблиц. Проблем с большими реляционными системами две:
              — необходимость обновлять все индексы во всех связанных таблицах при каждой транзакции
              — необходимость предоставления многопользовательского доступа к данным
              В системах map/reduce вторая проблема не стоит в принципе, так как данные «только для чтения». В результате на SQL вы ничего не распараллелите, надежда лишь на движок, а при потоковой обработке — вы сами пишете аддитивные алгоритмы, хотя это непривычно.
              PS
              random read это дорого, не спорю, поэтому и фуллскан.


              1. BugM
                09.01.2020 02:04
                +1

                Для одной большой таблицы с быстрыми вставками есть Кликхаус. Отлично работает в таком сценарии.

                Не надо Оракл (да и sql базы вообще) грузить на запись. Они для чтения. В sql кладем все что условно постоянно. В том же биллинге всех юзеров со всеми миллионами их свойств и связей кладем в sql и обвешиваем со всех сторон индексами. А транзакции пишем в Кликхаус.

                Не нужны вам паралельность и мапредьюс. Они не для этого. Они для данных не влазящих на один компьютер. И если там данных на грани влезания, то лучше уместить. Так проще и быстрее в среднем будет. Ключевое слово — Петабайт.


                1. balajahe Автор
                  09.01.2020 02:10

                  Столбцовая база с быстрой вставкой? Хм, спасибо, не знал, посмотрю. С OLAP работал конечно, но обновление кубов у нас обычно по ночам. Хотя это не отменяет идеи функционального программирования, которое постепенно становится хайповой темой :)


                1. XelaVopelk
                  09.01.2020 11:06

                  Если мне память не изменяет, в Кликхаусе отсутствуют транзакции, что ставит на ней крест для использования в ERP для регистрации потока документов.


                  1. Fragster
                    09.01.2020 18:59
                    +1

                    Как будто в подходе автора они есть


                1. XelaVopelk
                  09.01.2020 12:32

                  Яндекс в качестве oltp базы не так давно начала предлагать вот это: ru.bmstu.wiki/Yandex_Database
                  но:
                  0) она, ИМХО, сырая ещё
                  1) там весьма суровые ограничения


                  1. balajahe Автор
                    09.01.2020 12:46

                    Похоже, их newSQL — это ровно обратная концепция — все in memory, упор на распределенные транзакции, и т.д. Но вообще они молодцы, непонятно что теперь со всем этим будет, выйдет ли в жизнь. Наверно, сейчас чтобы выжить, нужно все в опенсорс выкладывать.


                1. somurzakov
                  09.01.2020 18:22

                  SQL смело выдержит биллинг, если грамотно продумать схему таблиц и использовать комбинацию partitioned таблиц, кластерных индексов, in-memory таблиц и не использовать долгие и сложные транзакции на SQL.
                  все банковские АБСки есть суть ERP и написаны на SQL — например ЦФТ из Новосиба


              1. XelaVopelk
                09.01.2020 11:15

                "— необходимость обновлять все индексы во всех связанных таблицах при каждой транзакции
                — необходимость предоставления многопользовательского доступа к данным"
                Не все, а только индексы для полей, которые участвуют в изменений обычно, (хотя если в вашей ERP, к примеру, тупо без разбора обновляются все поля документа при изменении статуса его, то это ваша «прикладная» печаль).
                — Раз тут Вы упомянули о MS SQL, то там есть optimistic locking и inmemory database

                «random read это дорого, не спорю, поэтому и фуллскан.»
                Фулл-скан всегда это очень дорого, даже если табличка помещается в памяти — ядра ЦПу будут только и заниматься тем, что перелопачивать десятки миллионов строк (больше не видел на OLTP базах, мы ж о ERP говорим использования «такой схемы», обычно колом становится раньше )


                1. balajahe Автор
                  09.01.2020 11:23

                  Если реляционная схема простая, то фулскан дороже. Если 30 таблиц для регистрации одной сделки — уже не факт. 10 миллионов JSON — это 10 секунд на мобильнике фулсканом, но в ERP это действительно долго получается. Тут нужен реальный проект чтобы нас рассудить.


                  1. XelaVopelk
                    09.01.2020 12:07

                    10 секунд на выборку по списку документов для любой выборки по списку документов это не просто долго, это безумно долго. Вот недавний случай «из жизни»: есть 250 «точек» которые грузят клиентам «что-то» (предположим что таблица у нас всего одна «Заказы на отгрузку»). До недавнего времени «точка» список своих документов на отгрузку обновляла примерно раз в минуту (взяли товар, отдали клиенту, показали, выбили чек). Положим у нас 40 ядерный процессор на СУБД выделен (по факту на месте стоял более слабый) и памяти хватает, чтобы табличка целиком лежала в памяти. Таким образом имеем (грубый подсчёт), получается что 60% процессорного времени (10 расчётов в минуту, укладывались сканом в 5-6 секунд на ядро * 40 ядер) СУБД будет поддерживать только эту операцию — выборки списка документов на «отгрузку» для каждой точки (в реальности больше, т.к. тут задачи на расчёт для простоты не мешают друг другу и не ждут ничего, а культурно раскладываются по ядрам последовательно). «Примерно так» и было в действительности «на месте» до ухода «в индекс».


                    1. balajahe Автор
                      09.01.2020 12:32

                      С поправкой на мое железо, неоптимизированный JavaScript все не так плохо. В реальности на одном приличном ядре и том же Rust я могу сделать примерно 10 миллионов объектов в секунду. Для бигдаты это ничто, но для ERP уже близко. Но, вообще-то нужен реальный проект.


      1. sshikov
        09.01.2020 08:15
        +1

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


        1. balajahe Автор
          09.01.2020 08:22

          Дать статистику, то есть сделать один полный проход по данным, правильно я понимаю?


          1. sshikov
            09.01.2020 13:34
            +1

            Ну, не обязательно сделать один (дополнительный) проход — можно собрать ее в процессе предыдущего прохода, например. Кто-то же эти данные записал — вот он в принципе может эту статистику знать. А иначе откуда спарк допустим узнает, сколько строк у нас в таблице? Там же внутри паркет, или вообще CSV, а у этих форматов нет таких метаданных (и потом, это может быть много файлов паркет).

            В общем-то, это и для обычных СУБД все ровно так же — если вы попросите Оракла собрать статистику по таблице, он точно также сделает фуллскан, скорее всего.


  1. apapacy
    09.01.2020 06:14

    Идёт вцелом правильные. Надо сказать что такая реализация с иммутабельностью и реализацией map/reduce уже существует это couchdb. Правда она явно не для highload. Как сказал ее автор, вы должны думать что couchdb делает всё очень плохо кроме быть может репликации.


    1. balajahe Автор
      09.01.2020 08:29

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


      1. apapacy
        09.01.2020 10:18

        В couchdb версии документа хранятся вечно. В этом смысле они иммутабельны. То есть нельзя изменить что-то и не оставитььследов.


        1. balajahe Автор
          09.01.2020 10:25

          Читаю про couchbase, собственно их view это и есть кэш, определяемый пользователями. Непонятно что со связями, если разрешить связь от любого к любому — рано или поздно это кончится хаосом. Надо изучать.


          1. apapacy
            09.01.2020 10:36
            +1

            Couchbase это синтаксический сахар вокруг couchdb и при этом с какой-то сложной лицензией. В couchdb однажды созданный map/reduce создаёт что-то вроде индекса который пересччитывается только по изменившимся документам.
            Проблема в том что она не поддерживается в актуальном состоянии при записи как например регистры в 1с а пересчитывается только по мере необходимости при доступе. Поэтому иногда создают кроны которые эти Вью периодически вызывают


            1. balajahe Автор
              09.01.2020 10:40

              Понятно, ход мысли аналогичный, правда с разницей в 15 лет )


            1. balajahe Автор
              09.01.2020 11:40

              Проблема в том что она не поддерживается в актуальном состоянии при записи как например регистры в 1с а пересчитывается только по мере необходимости при доступе.
              Это ж не проблема, а наоборот тренд — ленивые вычисления. Проблема понятна — мы не контролируем момент запуска тяжелой операции. С другой стороны жизненно — кому данные нужны, тот и подождет )


  1. qw1
    09.01.2020 09:00
    +1

    Блокировки при обновлении агрегатов не просто так.
    В схеме без блокировок, как я понимаю, нельзя сделать правило «запрещены отрицательные складские остатки», или «этот документ только что изменён юзером А, откройте его заново и вносите свои изменения».


    1. balajahe Автор
      09.01.2020 09:53

      Я согласен. В данном случае проблемы блокировок коснутся не всей базы, а только ее мутабельной части (текущего периода), потому что остатки по иммутабельной части лежат в кэше в готовом виде, если их в таком разрезе хоть кто-то когда-то запросил.
      Понимаю, что в современных СУБД есть партиционирование таблиц, текущий период в отдельном файле, и т.д. Проблема в том, что эту логику реализует дорогое ядро, а на уровне семантики прикладного программирования — пиши в любое место, блокируй что хочешь. Идея заключается в том, что если мы хотим настоящую бигдату, прикладных программистов придется ограничивать более строгой семантикой, но тогда радикально упростится ядро самой СУБД, и можно будет параллелить расчеты.


      1. SergeyUstinov
        09.01.2020 10:54

        :)))
        Где вы видели в ERP бигдату?
        Биллинг сотовых — это не ERP, на всякий случай уточняю. )))


        То, что Аксапта долго период закрывает — это не проблема SQL. Это проблема не оптимальных алгоритмов.


        1. balajahe Автор
          09.01.2020 11:47

          Согласен. Я ж не покушаюсь вообще на SQL, а только на ERP )) В той же аксапте — сначала мы исходные документы преобразовываем в проводки / регистры / сопоставления, а потом в сводных отчетах руководство хочет расшифровать любую сумму до строки первичного документа — и начинаем заново джойнить — от регистров к проводкам, от проводок к документам и т.д… А если на входе и на выходе по сути нужны только строки документов — зачем тогда все остальное промежуточное хранить?


          1. SergeyUstinov
            09.01.2020 12:55

            Во первых, OLTP и отчёты — это две сильно разные задачи.


            Во вторых, без ледгеров (проводки модуля / проводки ГК) создать гибкую тиражируемую систему не получится.
            Простой пример. Вчера обсуждали с репортерами (которые консолидированную отчётность готовят), что за 2019 год будем делать отчётность по сегментам (по странам продаж). Причем эти страны продаж не сильно пересекаются с нашими юр. лицами.
            При наличии ГК просто сгенерируем кучу проводок для каждой компании и перераспределим по сегментам текущие операции на основе неких баз распределения. Например амортизация ноутбука за январь была одна сумма, а станет 6. Причем на момент начисления амортизации или учёта расходов эти базы распределения неизвестны. Собственно говоря, что будем делить по сегментам — тоже не было известно. :)))
            И, главное, мы вообще никак код ЕРП трогать не будем (у нас Навик).
            А теперь представьте, как решение подобной задачи будет выглядеть в вашей системе.


      1. DrunkBear
        09.01.2020 11:46

        Если хочется чего-то странного — идём к классическим data lake /data vault: есть быстрая реляционная БД транзакций, есть бигдата с архивом транзакций за хх лет, с блекджеком, ML и агрегатами, есть ETL, которые грузят каждые n часов данные из реляционной Бд в кластер бигдаты — и все довольны.
        Если не хочется реляционной БД и допускается отставание на пару минут от реального времени — льём JSON пакетами, а дальше — impala умеет и с партициями работать, и с корзинами, и быстро сканить документы.
        PS тестил второй сценарий для мобильного биллинга — запросы к 1.5Tb сырых csv типа «кто в центре города за последние 15 дней чаще всего ходил на сайты типа порносайты» выполнялись ~100 секунд. Если хранить эти же данные в avro / parquet — ~30 секунд.
        PPS А всё же, зачем в ERP bigdata?


        1. balajahe Автор
          09.01.2020 11:58

          Первый вариант с блэкджеком — сложновато будет, и дорого, и специалисты нужны разнопрофильные. Второй сценарий — цифры ваши подтверждаю, все действительно летает, но это отчеты, а хочется полноценную БД.
          PS
          Зачем в ERP бигдата. Мне ставили как-то задачу — рассчитать полную себестоимость каждого поддона товара с учетом всех логистических затрат на перевозку и хранение (много транспортных плеч, на каждом складе товар сколько-то лежит, а это тоже затраты). Получился справочник только на 10 миллионов записей, а операций (по сути обычных распределений) уже ближе к миллиарду. И это всего-лишь опт, у ритейла данных поболее будет. Поэтому сейчас все ERP считают приблизительно, по номенклатурам, максимум по партиям, а вдруг менеджеры начитаются статей на хабре про интернет вещей, прицепят радиометку на каждый поддон, и захотят писать историю его жизни — вот вам и бигдата )


          1. DrunkBear
            09.01.2020 12:56

            Все хранилища данных под IoT, которые видел — или облака с bigdata бэкэндом, или локальная bigdata с kafka, поэтому использование IoT сводится или к варианту с блэкджеком и разнопрофильными спецами, или к чугуневому велосипедостроению (чугуневому — чтоб не падало).
            PS для бизнеса действительно будет различаться 40 поддонов в 1 партии, которые будут ехать одним контейнером по одинаковым складам?


            1. balajahe Автор
              09.01.2020 13:00

              Статья — как раз про чугуниевый )

              для бизнеса действительно будет различаться 40 поддонов в 1 партии, которые будут ехать одним контейнером по одинаковым складам?
              Партии разделяются, потом могут перебрасываться из филиала в филиал для пополнения страховых или под заказ, и сколько раз их туда-сюда гоняли — вообще-то интересная статистика. А если прицепить сюда себестоимость — можно поставить KPI менеджеру, который компонует маршруты, то есть задача вполне себе востребованная.


              1. SergeyUstinov
                09.01.2020 13:09

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


                1. balajahe Автор
                  09.01.2020 13:11

                  А мы отказались, и так производительность хромала.


                  1. SergeyUstinov
                    09.01.2020 13:15

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


                    1. balajahe Автор
                      09.01.2020 13:28

                      данные копировались в отдельную базу, там считалась детальная себестоимость
                      Ну вот в том и вопрос, зачем нужны базовые механизмы ERP, если такую очевидную работу нужно выполнять за бортом. В 1С-ERP в итоге систему разделили на 2 части — оперативный учет и регламентный учет, и обмены между ними. Но тоже не выход, так как в оперативном учете тоже попадаем в ловушку производительности.


                      1. DrunkBear
                        09.01.2020 13:41

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


                        1. balajahe Автор
                          09.01.2020 13:50

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


                          1. SergeyUstinov
                            09.01.2020 14:00

                            Себестоимость поддона рассчитала ERP. А детализацию этой себестоимости, которая нужна для отдельных отчетов — рассчитали на стороне.
                            Когда ERP досчитывает себестоимость — она делает записи в базе данных (в книге модуля, от которых вы хотите избавится). Ничто нам не мешает эти записи о себестоимости регулярно копировать и рассчитывать для них расшифровку себестоимости (что мы собственно говоря и делали).


                            1. balajahe Автор
                              09.01.2020 14:02

                              Мне это эстетически не нравится, хотя так в основном и делают.


                              1. SergeyUstinov
                                09.01.2020 14:23

                                А что не нравится?
                                Есть экскаваторы, которые хорошо умеют копать. Есть самосвалы, которые хорошо умеют возить.
                                Пытаться сделать одно универсальное решение… Так себе идея, прямо скажем. Требования то РАЗНЫЕ, следовательно оптимальная реализация в каждом случае тоже будет разной.


                          1. DrunkBear
                            09.01.2020 14:05

                            Ок, допустим, у нас горячая пора: близок конец квартала, текущее железо забито отчётами, кубами, планированием и сверками — встала задача посчитать, к примеру, историю поддонов — забрали срез данных, положили в облако (если все локальные ресурсы заняты), посчитали сутки, загрузили результат обратно в куб — все довольны.
                            А натягивать сову ERP на все возможные случаи применения: от транзакций и OLAP до отчётов, ML и архивов — немного странная идея.
                            PS помнится, кто-то вообще купил пару стоек RAM-дисков и перенёс БД на эти RAM-диски для скорости — тоже неплохой вариант, но не для всех случаев.


                            1. NitroJunkie
                              09.01.2020 14:07

                              Ну кстати 1С к примеру так со своим СКД по сути и предлагает делать. И многие 1С разработчики считают что так и надо. Впрочем если объемы не огромные, то что-то в этом есть.


                              1. SergeyUstinov
                                09.01.2020 14:33
                                +1

                                Если у тебя есть только молоток, то во всем видишь гвозди. :)
                                Когда 1Сникам говорю, что я успешно РЕШИЛ задачу с детальной себестоимостью (с разделяющимися партиями, транспортными расходами на перевозки туда-сюда и т.п.) — у них подгорать обычно начинает. :)))


                                1. gennayo
                                  09.01.2020 14:48

                                  1С в своей нише малого и среднего бизнеса хороша, но всегда удивлялся, чем руководствуются люди, которые на платформе 1С реализуют работу 3500 розничных магазинов или работу 5000 пользователей онлайн.


                                  1. DrunkBear
                                    10.01.2020 11:06

                                    Так Сложилось Исторически, а миграцию на подходящие инструменты никто даже согласовывать не станет — дорого, долго и непонятно.
                                    Увы.


                          1. XelaVopelk
                            09.01.2020 14:52

                            "… А в вашем случае как дооценить поддон, если вы его обсчитали на стороне?..."
                            Более того если склад у вас большой (или вообще на аутсорсе) и им рулит WMS, в документе ЕРП будут позиции сгруппированные по номенклатуре, далее вы пошлёте их в WMS и она даст вам реальное товародвижение (какие партии пошли по вашему документу).


                            1. balajahe Автор
                              09.01.2020 14:54

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


                              1. XelaVopelk
                                09.01.2020 15:09

                                0) Внутри нет, но партии разные с разной себестоимостью и какие партии пойдут по накладной перемещения согласно бизнес-логике WMS
                                1) Во вторых то что склад это отдельная группа зданий это ваша гипотеза, которая в реальной жизни может и не реализоваться. Я вот к примеру наблюдал реализацию, когда ЕРП система видя (WMS подсказала), что на основном складе не хватает части товара автоматически сформировать связанные документы на подвоз из других складов и документ отгрузки клиенту по факту обрабатывали 3 разных склада (разные партии, разные себестоимости транспортировки/ хранения по понятным причинам) с 2мя типами WMS один из которых склад-аутсорс.


                                1. balajahe Автор
                                  09.01.2020 15:36

                                  Накладные перемещения — если они наружу, то инициировать их может и WMS, а обсчитывать-то придется в ERP, которая должна знать про партии. Нет, ну я допускаю, что к WMS тоже можно прикрутить расчет себестоимости, но по моему так не делают.


                              1. SergeyUstinov
                                09.01.2020 15:14

                                Предположим, есть свой автопарк и свой склад.
                                В чем принципиальная разница в расходах на зарплату водителю, амортизацию грузовика и бензин при перевозке со склада на склад и зарплатой кладовщика, амортизацией погрузчика и электричество для погрузчика при перемещении из ячейки в ячейку?
                                И там и там себестоимость. И структура расходов очень похожи. :)


                                1. XelaVopelk
                                  09.01.2020 15:33

                                  Если вы учитываете расходы электрической энергии погрузчика на доставку каждого конкретного заказа, из пункта хранения склада А в пункт Б — С — Д — (там может быть сформирована цепочка), учитываются трудовые ресурсы «Грузчик Иванов тащил 15 минут двигатель ЯМЗ сер. номер ...» и т.д. равно как вы выписываете путевой лист на конкретную доставку товара со склада А в склад Б (пробег, расход топлива), командировочных расходов (склады в разных городах), то наверное разницы нет.


                                1. balajahe Автор
                                  09.01.2020 15:39

                                  Стоп. Затраты внутри склада рассчитываются помесячно, и потом распределяются на все что лежало. Конечно, можно считать пробег электрокара и привязывать его к поддону, но это уже сюрр. Поэтому затраты на хранение — это ERP, там зарплата, амортизация и прочий газ. А перевозка дальше километра — это отдельная накладная, сумма известна сразу, и складывать ее в котел чтобы потом распределять — не всегда гуд.


                                  1. XelaVopelk
                                    09.01.2020 15:44

                                    Так я про то же. разный принцип расчёта себестоимости, в одном месте распределение по всему складу, а в другом, по тому что ехало в машине в конкретное время из пункта А в пункт Б. И такого рода затраты по факту учитываются обычно, т.к. до того как оно приехало это всё «прогноз» (а там машина сломалась, встали в пробку, метель — вот и поплыли командировочные расходы на ГСМ и т.д.)


                                  1. SergeyUstinov
                                    09.01.2020 16:02

                                    На перевозки тоже в конце месяца считают. :)
                                    Я ведь и говорю — свой автопарк и свой склад. Зарплаты, амортизация и т.п. — это в конце месяца.

                                    А распределять по всему что лежало иногда не очень правильно. Если заняться оптимизацией складских операций — очень даже полезными будут детальные данные.
                                    Когда размазываем затраты поровну — не заметно, что один и тот же поддон десять раз за месяц с места на место перетащили.

                                    Мы не считали внутрискладские (все склады чужие), но если бы было надо — вполне можно.
                                    Более того, такой расчет явно бы делался за пределами ЕРП (распределение по складским операциям), а в ЕРП уже бы учитывались результаты расчетов — документы расходов, распределенные по партиям.

                                    То есть это пример того, что есть ТРИ разные системы (WMS, ERP, и процедуры расчета распределения затрат на складские операции), которые работают совместно — и это в некоторых случаях будет оптимальным решением.
                                    Пытаться все сделать в одной системе (ERP) очень часто не оптимально.


                      1. SergeyUstinov
                        09.01.2020 13:45
                        +1

                        Я выше написал — отчеты и OLTP — разные задачи. Нет смысла их смешивать.
                        И на самом деле нет никакой ловушки производительности для ERP систем. Ну просто напросто нет такого объема операций в принципе ни у одной фирмы, чтобы их не могла обработать стандартная SQL база данных.
                        Любая проблема c ERP системами, о которой начинают говорить — при ближайшем рассмотрении оказывается проблемой не технологии как таковой, а ошибок проектирования.
                        Например, начинают в одну кучу сваливать задачи OLTP системы и формирования отчетов. :)))

                        ERP — это OLTP система. Не надо пытаться повесить на неё «чужие» задачи — и сразу будет вам счастье.


      1. qw1
        09.01.2020 13:21

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

        Дорогое ядро — это достоинство. Потому как пишется и тестируется 1 раз квалифицированными людьми. А писать сложные функции с учётом архивного хвоста на каждую хотелку пользователя, которых возникает по 10 штук в день, слишком дорого.

        Особенная боль всё это отлаживать. А потом выбрасывать в помойку, как 95% ф-ций, которые при постановке нужны ещё вчера, а после сдачи вообще никому не нужны.


        1. balajahe Автор
          09.01.2020 13:31

          Функции писать не сложнее SQL при наличии удобного API, с остальным согласен.


          1. qw1
            09.01.2020 14:40

            Проблема не в переходе с SQL на сервер приложений, а в том, что данные предлагается искусственно порвать на 2 части (иммутабельная и оперативная), и прям вообще во всех операциях держать это в уме и склеивать эти части.


            1. balajahe Автор
              09.01.2020 14:45

              Нет, вы пишете один редьюссор в расчете на один поток документов. Рвет на части уже реализация, и склеивает она же, и кэш автоматически до-обновляет при фиксации очередной пачки документов. Проблемы начинаются, если мы хотим распараллелить алгоритм. В этом случае, он (алгоритм) должен быть аддитивным, либо вам придется писать 2 редьюссора — один агрегирует документы, второй агрегирует редьюссоры. В аддитивном алгоритме типа сводной таблицы с суммированием и мин/максом — редьюссоры склеиваются той же функцией, что и документы, но в общем случае это не так, например если мы считаем count distinct.


              1. qw1
                09.01.2020 17:57

                То есть, когда считаем оборот за январь 2020 и за январь 2014, можно написать код, в котором явно не указано, где проходит граница между активным и архивным периодами?


                1. balajahe Автор
                  09.01.2020 18:33

                  Да, дата это пользовательский атрибут, а таймштамп системный. Учетных периодов может быть несколько, вложенных один в другой, например по НДС квартал, про РСБУ месяц, по УУ — неделя. С российской практикой править все задним числом это мало согласуется, но всеж сейчас и у нас многие стали работать по белому, жизнь заставила.


  1. mazzy-ax
    09.01.2020 09:23
    +1

    концепт — хорош. «справочник — это документ» — блестяще.
    .
    что хотелось бы уточнить:
    gettop должен получать top на момент документа, которому нужна выборка. Чтобы выборка всегда давала одинаковый результат при разных запросах.
    .
    но так становится уж слишком похоже на пресловую Точку Актуальности (ТА) в 1С.


    1. balajahe Автор
      09.01.2020 09:28

      Нет, топ — это всегда последняя запись. «Версию на дату» получать в большинстве случае не нужно, так как в документе есть прямая ссылка на нужную версию справочника (которая была топовой на момент разноски). Если для каких-то алгоритмов нужна версия на произвольную дату — значит придется запускать поиск и кэшировать. Скорее всего, придется добавить функцию search() и кэшировать к примеру первых 100 найденных записей. Точка актуальности — да, конечно, это по сути оно.
      PS
      Вы тот самый легендарный mazzy? Респект, читал Вас с удовольствием пока с AX работал )


      1. mazzy-ax
        09.01.2020 10:01
        +1

        в этом случае придется делать два набора запросов — один в момент разноски документа, другой для повторного запроса в уже созданном документе. например, вам нужно распечатать документ (для определенности, ПКО) в момент разноски, а также потом в любой момент. Какая фамилия кассира должна появится в распечатке?
        .
        Спасибо. Спасибо за классную статью. Прежде всего за идею «справочник — это тоже документ». Похоже идею можно развить «в учетной системе все — это документ».


        1. balajahe Автор
          09.01.2020 10:06

          Да, запросы будут разные, первый условно по code (+ доп. критерии поиска), а второй — строго по ID.


          1. mazzy-ax
            09.01.2020 11:11

            разные запросы — это плохо. это снова ошибки при разработке.

            С другой стороны, в статье есть запрос «Обороты в разрезе номенклатур и партнеров»
            где идет запрос наименований номенклатуры и партнера
            const invent = await db.get(line.invent)
            const partner = await db.get(doc.partner)

            если аналогичный код находится не в отчете, а в методе печати документа (предположим, Расходая накладная)

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

            кроме того, переразноска все равно должна взять старые реквизиты в некоторых местах (название компании, директор, кассир и прочее)
            А если в этих реквизитах тоже были валидные исправления?

            в общем, приходим к извечному вопросу учетных систем: как отличить исправления и измененения :)


            1. balajahe Автор
              09.01.2020 11:36

              К сожалению, все так, и полной иммутабельности не получится. Мы же документы не в файле будем хранить, а в какой-нибудь NoSQL, а там можно править конкретный объект. Конечно, возникнет геморрой по описанию правил доступа к атрибутам — какие, в каких случаях и кому можно править. Но в случае с наименованием — правим непосредственно в той версии, где ошибка, или во всех версиях. Тут мне подсказали, что 10 лет назад все это уже было изобретено )


  1. gennayo
    09.01.2020 09:58
    +1

    Вот в чём-то похожий подход у ребят habr.com/ru/company/lsfusion/blog/458376


    1. balajahe Автор
      09.01.2020 10:02

      Спасибо, я как-раз хотел их еще раз подробно перечитать, может и возникнет повод позвонить.


  1. VolCh
    09.01.2020 10:35
    +1

    Как-то очень похоже на event sourcing с измененными терминами типа event у нас документ, а snapshot — кэш


    1. balajahe Автор
      09.01.2020 10:43

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


      1. lair
        09.01.2020 11:50

        Event Sourcing — это не журналы логов, совсем нет.


        1. balajahe Автор
          09.01.2020 12:20

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


          1. lair
            09.01.2020 12:20

            До кого "до нас"?


            И нет, это было "популярной темой" не только в 2005.


            1. balajahe Автор
              09.01.2020 12:39

              До российских компаний, не входящих в топ-10, я пока тут живу )


          1. EvgeniiR
            09.01.2020 14:01
            +1

            Интересно, что именно в 2005 это было популярной темой, вот и Фаулер писал

            Не только писал, но и говорил, и не только в 2005, а ещё и 2017, например.
            Да и вообще, не только Фаулер.

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

            Смелое заявление. Предлагаю вариант 3 — «Я пишу процедуры и запросы на 1С-бейсике, поэтому нахожусь в схожем по интересам комьюнити».

            А оглядываться по сторонам вообще полезно. Хотя бы чтобы не переизобретать давно известное заново.
            Продукты где ES под капотом существуют и в России.


            1. balajahe Автор
              09.01.2020 14:09

              Предлагаю четвертый вариант — вы рекомендуете мне работодателя, кому интересен event-sourcing подход в проде, я попрошу комьюнити рекомендовать ему меня — как аналитика, программиста и технического писателя, и все будут удовлетворены :)


              1. EvgeniiR
                09.01.2020 14:38
                +1

                Предлагаю четвертый вариант — вы рекомендуете мне работодателя, кому интересен event-sourcing подход в проде, я попрошу комьюнити рекомендовать ему меня — как аналитика, программиста и технического писателя, и все будут удовлетворены :)

                Я к комьюнити близком к использованию ES в проде тоже не отношусь(увы), и поиском таковых компаний целенаправленно не занимался.
                Людей с таким опытом, приобретённым в Российских компаниях видно периодически. Может быть кто-то ещё тут отпишется, или у lair есть примеры.
                Так сходу помню только что www.aeon.world искали людей на Elixir/ES/CQRS.


  1. Naves
    09.01.2020 10:51

    Не совсем понятно, как будет работать механизм редактирования документов, например того же перемещения товаров со склада на склад.
    Сначала неподтвержденного документа, когда оператор в час, натурально, по чайной ложке добавляет позиции и редактирует их количество.
    Если каждая редакция будет отдельной записью, то база будет примерно на 90% занята по факту мусором. Хотя иногда приходится выяснять и такое, когда же именно оператор добавил очередную чайную ложку. Но этот отдельный лог всегда чистится, хотя бы через месяц-полгода.
    И потом, когда в нашей реальности нужно срочно отредактировать очередной документ, забитый неделю назад, и нужно снова распечатать правильную накладную.


    1. balajahe Автор
      09.01.2020 11:14

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


      1. XelaVopelk
        09.01.2020 11:29

        Пользователи ERP они хитрые люди: они знают, что их комп может повиснуть, может лагнуть сеть, могут удалить элемент номенклатуры который они хотят использовать в документе или ликвидировать склад, с которой они пытались отгружать товар. Поэтому если документ относительно большой: ну так к примеру заявка поставщику на несколько сот позиций, которые менеджер может не торопясь пару недель (как нельзя изменить документ недельной давности????!!!) согласовывать и с закупками, и с продажами, и с «финиками», и с… — старается сохранять частями.


        1. AndreySu
          09.01.2020 11:58

          Мне кажется тут речь идет о документе уже закомиченном в систему, а вы говорите о временном документе, который еще на стадии формирования.


          1. XelaVopelk
            09.01.2020 12:13

            Что значит «закоммиченом»? Документ лежит в базе, его может посмотреть начальник менеджера («Чем этот балбес занимается весь день?»), по нему строятся отчёты на предмет как идёт процесс работы с закупками (менеджер ведь не одной этой поставкой занимается), по нему можно посмотреть историю изменений и т.д. и т.п.


            1. AndreySu
              10.01.2020 16:50
              +1

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


        1. balajahe Автор
          09.01.2020 12:07

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


      1. gennayo
        09.01.2020 12:00

        «Строки документа являются частью документа» — почему? В WMS, например, в общем случае операции атомарны и нет смысла менять весь документ при изменении конкретной строки.
        Кстати, в упомянутой мной lsfusion именно так всё и устроено.


        1. balajahe Автор
          09.01.2020 12:25

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


          1. gennayo
            09.01.2020 12:40

            Связывает, например, вид операции, исполнитель и т.п. А в общем случае, количественные остатки на складе, например, изменяет конкретная строка документа, а сумму взаиморасчетов с контрагентами — документ в целом. И ничего не мешает разделить эти изменения на 2 потока, например.


            1. balajahe Автор
              09.01.2020 12:49

              Согласен, можно и разделить.


      1. Naves
        09.01.2020 13:55

        >Совместное редактирование одного документа — согласен, проблема, но такого известные мне системы и сейчас не позволяют.
        В смысле не позволяют? А где же хранится текущая версия, в кеше клиента?
        Обычно не требуется одновременная работа, но текущая версия из базы доступна для чтения остальных.
        Вот на одном компьютере человек создал документ в базе, забил его позициями, сохранил, но не подтвердил. Потом пошел к начальнику/работнику отгрузки, спросил что-нибудь, и сразу там же добили что надо (Если у другого пользователя есть права на редактирование чужого неподписанного документа). Потом с любого рабочего места авторизовался под собой и подписал документ. Именно после подписи начинаются движения всяких счетчиков по складам и лицевым счетам.

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


        1. balajahe Автор
          09.01.2020 13:59

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


  1. mazzy-ax
    09.01.2020 11:15

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


    1. balajahe Автор
      09.01.2020 12:01

      Согласен, мне он больше для тестов был нужен.


  1. D01
    09.01.2020 12:22

    Я наверное что-то пропустил, но почему итоги считаются не по топам, а по всем документам?


    1. balajahe Автор
      09.01.2020 12:28

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


      1. D01
        09.01.2020 12:35

        Т.е. ввод новой версии документа, это ввод не полной информации, а только изменений?


        1. 10 шт. Товар 1 Клиент 1
        2. -1 шт. Товар 1 Клиент 1
          — удаление — т.к. клиент был указан не верно
        3. -9 шт. Товар 1 Клиент 1
        4. 9 шт. Товар 1 Клиент 2

        Так получается?


        1. balajahe Автор
          09.01.2020 12:51

          Да, конечно. Но обычно в ERP делают полное сторно, и следом вводят правильный (получается всего 3 документа вместо одного). Есть функционал частичного сторнирования строки, которым не любят пользоваться из-за худшей аудируемости.


  1. muhaa
    09.01.2020 13:01
    +1

    Интересная тема. Когда-то мне приходилось решать MES-подобные задачи, я тоже «изобрел» метод имутабельных документов. При этом удивлялся, почему я здесь один и где вообще теория, обсуждения, фреймворки и прочее.
    ИМХО, за имутабельными документами будущее. Потому что это единственная абсолютно строгая форма описания чего-либо. Даже если у нас есть какое-то полностью автоматизированное предприятие с роботами и дронами, все равно нужно: 1. Собирать информацию о состоянии производства, неисправностях и др. (документы о событиях) 2. Получать по истории, отраженной в документах состояние на анализируемый момент времени (на текущий и иногда на другой). 3. Корректировать ошибки в истории или дополнять информацию, информацией, полученной позже (документы об исправлении ошибочных документов или уточнении информации).
    Традиционный SQL подход здесь не работает, потому что информация по которой нужно делать запросы (состояние) виртуальна и вычисляется по истории документов.
    Каждый раз когда я пытался запустить систему на принципах имутабельных документов основной проблемой была реакция программистов и пользователей.
    Программист: «как теперь быть с привычными приемами типа SQL запросов, фильтров, постраничного показа в UI… Зачем все так сложно?»
    Пользователь: «Значит, если я вижу на экране неверно внесенный параметр, то я не могу его просто поправить. Вместо этого я должен добавить документ о том, что я хочу исправить информацию, содержащуюся в ранее внесенном документе о том, что параметр имел некое значение в некий момент… хм, в старой программе, которую делал наш вася все было проще, вы не пробовали сделать как у него?»


    1. balajahe Автор
      09.01.2020 13:22

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


    1. BugM
      09.01.2020 13:42

      А ведь пользователь прав. Все ошибаются. Если ошибка не критична для бизнес процесса, то почему ее нельзя просто исправить? Зачем вся эта бюрократия на пустом месте?


      Строить любой документ по всей истории медленно. Актуальные версии смотрят минимум в 95% случаев. И они должны работать быстро.


      1. muhaa
        09.01.2020 15:02

        Всякая информация по своей природе имеет источник во внешнем мире, который определяет достоверность информации и ответственность за нее. Если просто исправить параметр в атрибутах доменного объекта или в документе, то источника не будет. Позже, если окажется что эта информация не верна или противоречит другой, установить суть проблемы уже будет нельзя.
        В качестве полу-меры для решения проблемы обычно логируют изменения в атрибутах доменных объектов. Но тут возникают сложности. Например, если пользователь меняет атрибут некого доменного объекта, это что это означает? Что соответствующий параметр действительно изменился где-то в реальном мире примерно в момент внесения изменений, или это означает что пользователь исправил ошибочно внесенное значение? В итоге начинают плодиться дополнительные поля, для внесения исправленных значений, дополнительные журналы для логирования изменений.
        В принципе, можно разрешить непосредственные изменения только в документах (не в д. объектах) и логировать эти изменения. При этом доменные объекты формируются и корректируются по истории документов с хранением их в БД. Я примерно так и делал. Здесь запись в логе — по сути документ о корректировке другого документа.
        Окончательное и строгое решение — опираться только на имутабельные документы. В реализации сложно, но задача становится чисто математической и если она решена то получается заманчиво строгая платформа для прикладных задач.


        1. BugM
          09.01.2020 22:29

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

          С иммутабельными документами все хорошо до тех пор пока не выяснится что последняя версия каждого документа собирается из пары сотен предыдущих версий. А какая-нибудь парочка особо важных и нужных документов собирается из 10к предыдущих. И когда приходит хотя бы сотня пользователей с простейшим запросом «Покажи мне актуальную версию документа» все начинает тормозить. Очень тормозить. А они еще f5 любят жать.
          Объяснить мол пусть тормозит, зато тут математически все прекрасно не выйдет.


          1. balajahe Автор
            09.01.2020 22:52

            Большим ERP часто делают периодическое обрезание, то есть удаление истории с определенным горизонтом, а вместо нее запись аналога «входящего сальдо» на момент обрезания. По-моему даже штатную процедуру такую видел. Скорее всего также и тут придется делать, вопрос лишь выбора этого периода. Только в нашем случае можно не обрезать, а просто партиционировать иммутабельное хранилище по периодам, записывая в начало следующего последний срез предыдущего.
            PS
            В моем случае актуальная версия документа — это последняя версия, ее не нужно ниоткуда собирать, это самодостаточный документ, со строчной частью если надо. Собирать нужно только агрегаты, которые, согласен, пользователи тоже иногда захотят получать быстро. В общем, выхода нет, все лучшее уже изобретено до нас.


          1. muhaa
            10.01.2020 15:20

            Бывают разные задачи. У меня была задача учета электрических счетчиков. На балансе имеется с пол миллиона счетчиков, по каждому ведется история — снятие показаний, кому поставили (в аренду), ремонты, поверки. Основная задача — выявлять хищения электроэнергии за счет манипуляций со счетчиками. Здесь кррме имутабельных документов, описывающих события со счетчиками наверно вообще другого решения нет.


            1. SergeyUstinov
              10.01.2020 15:48

              Сама по себе идея с неизменными документами — полезная.
              Но пытаться для ЕРП прикрутить другую среду хранения вместо стандартных РСУБД… С моей точки зрения — очень сомнительно.
              Что нам мешает для «неизменных» документов использовать для табличек только инсерты и селекты, без апдейтов и делитов?
              Если правильно спроектировать систему и базу данных — все прекрасно будет работать. И я уверен, что работать будет лучше, чем если мы будем пытаться прикрутить инструменты от БигДата к ЕРП. Ну нет в ЕРП больших данных, нет.


              1. balajahe Автор
                10.01.2020 16:26

                Можно и в табличках хранить. Только для нестрогого JSON документные БД будут быстрее и дешевле. По поводу бигдаты и ERP — например в масштабах РФ действует перекрестная проверка сделок, (НДС, аффилированность, обоснованность цен и т.д.) База ЭСФ по всей стране положим за год — это уже бигдата. А учетные задачи все те же — обороты, сальдо, себестоимость. Я эту задачу решу минимальными средствами, а грузить все это добро в РСУБД — полезно только для бюджета проекта.


                1. SergeyUstinov
                  10.01.2020 17:21

                  Да, для документов без жесткой структуры объектные базы лучше подойдут.

                  И проверки сделок в рамках страны — это уже действительно ближе к биг дате. Хотя и не уверен — все зависит от структуры данных. Если там просто текстовые документы — это скорее биг дата (но их и обрабатывать сложно). А если там есть жесткая структура данных — вопрос…

                  Но вот задачи в таком проекте сильно отличаются от ЕРП. Скидки не надо считать, актуальные остатки товаров на складе или актуальное состояние взаиморасчетов никому не нужны и т.п.
                  Нужны относительно простые отчеты, но по огромному объему данных.
                  И вот для таких применений как раз и можно (нужно) использовать инструменты из БигДаты.


                  1. balajahe Автор
                    10.01.2020 17:36

                    По идее там стандартная XML, определенная в ФЗ, но на практике бывают ньюансы, закон ведь тоже меняется, поэтому желательно не отвергать документ формально не прошедший валидацию, а попытаться что-то полезное из него извлечь. Пока эта проверка проводится пост-фактум (тьфу-тьфу), согласен — ERP отдельно, а бигдата отдельно. Но если у меня будет свободное время, я все-таки попытаюсь выжать максимум из мап/редюс, хотя бы в самообразовательных целях. Если считать что датафайл влезет на мой диск целиком — заморачиваться на хадуп/спарк смысла нет — потоковый парсер пишется пару дней, распараллеливание расчета положим неделя, все остальное пользовательские алгоритмы.


            1. BugM
              10.01.2020 22:18

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

              Обычная sql база, возможно рядом не менее обычная Монга с джейсонами.
              Нагрузка смешная. Пусть даже Москва: 15кк счетчиков и 15кк событий в месяц. Работать на калькуляторе будет.

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


              1. muhaa
                11.01.2020 01:38
                +2

                Там весь бюджет разработки был несколько тыщ. $, срок разработки 3 месяца а пользователи — женщины в халатах, которые протирают счетчики, сканируют штрих-коды и электрики в поле. Поэтому конечно пользователям разрешалось править «имутабельные» документы с логированием изменений и красной галочкой на каждом исправленном документе. Но итоговое текущее состояние счетчика таки строилось и перестраивалось по документам а не велось непосредственно.
                Работал сервер действительно на обычной SQL базе и обычной персоналке. Документов набиралось несколько миллионов в год вроде (Минская область). Отдал с исходниками, согласно договору. Честно, не знаю выжила ли эта система в итоге.


    1. balajahe Автор
      10.01.2020 09:09

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


      1. DrunkBear
        10.01.2020 11:22

        Есть и обратная сторона: весь страх и ужас можно спрятать, и тогда появляются вопросы «чегойто наш отчёт тормозит?».
        Смотрим: ага, тормозит, потому что повис на 1 ядре, а оставшиеся 63 курят в сторонке, pushdown на storage cell тоже отдыхает. А почему?
        А потому что в where функция, которая тоже подзапрос с функцией, который тоже вызывает 2 фукнции, каждая из которых… Ну вы поняли.
        И в итоге оптимизатор смотрит на это горячее бразильское портанго и вешает всё на 1 ядро без распараллеливания — когда-нибудь да отработает.


        1. balajahe Автор
          10.01.2020 11:36

          Чтение иммутабельных документов ничего не может блокировать. Если нужны тяжелые вычисления в триггерах, значит нужно позаботиться о кэшировании результатов. В предлагаемой мной схеме (она же в couchdb) кэширование простое как валенок. А в пользовательских реляционных схемах оно становится неочевидным и запутанным уже после сотни таблиц. Это же обычные весы — сложность разработки vs сложность поддержки.


        1. balajahe Автор
          10.01.2020 11:39

          Я соглашусь лишь с тем, что при высокой транзакционной плотности вся эта схема непригодна, там нужна in memory db, а не какие-то там чистые фукнции. Как и драйвера на хаскеле никто не пишет.


  1. bgnx
    09.01.2020 13:11
    +1

    Интересно как с этой концепцией иммутабельной бд вы собираетесь поддерживать консистентность для различной бизнес-логики? Допустим юзер хочет перевести деньги другому юзеру. При параллельной обработке таких запросов может возникнуть ситуация гонки (race-condition) когда параллельный запрос увидит только только часть изменений другого запроса и таким образом перезапишет значения тем самым нарушив консистентность (не сойдутся остатки по счетам)
    Ок, с предлагаемым вами иммутабельным подходом можно не изменять данные в разных местах а просто запушить в лог некую запись об операции перевода а потом при чтении выполнить reduce и получить остатки по счету.
    Но стоит только добавить условие что юзер может выполнить перевод только при положительном остатке так вся эта идея с иммутабельностью проваливается потому что уже не получится запушить в лог факт операции так как клиенту нужно вернуть либо успех либо ошибку а значит проверку положительного остатка при переводе (редюс по всему списку переводов) нужно выполнить в самом запросе
    И таким образом получаем на порядок худший вариант чем изначально "мутабельная" версия так как редюс списка истории переводов (чтобы проверить остаток) будет занимать больше времени что увеличивает время конфликта с другими параллельными запросами (не говоря уже про саму неэффективность редюса при каждом запросе)
    И в конечном счете получается что мутабельность (точечные изменения в разных местах) и поддержка нужных индексов вместо пуша в лог и свертки (или всяких там подходов разделения чтения и записи а-ля cqrs) это самый эффективный способ реализации serializable-транзакций в базах данных (если конечно вам нужна консистентность чтобы сошлись остатки по счетам). Кстати советую посмотреть хороший доклад про уровни изоляции транзакций https://www.youtube.com/watch?v=5ZjhNTM8XU8
    Ну а хранения данных полнотью в оперативке (с консистентным сохранением на диск в режиме append-log) позволит выполнять несколько сот тысяч таких serializable-транзакций в секунду.
    Правда sql-базы под это не заточены но яркий пример базы данных которая умеет в больше 100к serializable-транзакций в секунду это Tarantool (https://habr.com/ru/company/oleg-bunin/blog/340062/, https://habr.com/ru/company/oleg-bunin/blog/310690/).
    Если вкратце то отличия подобных тарантулу in-memory баз от sql-баз (когда просто увеличиваем кеш) заключаются в следующем:
    а) не нужно поддерживать индексы на диске — так как используется только последовательная запись в конец файла то получаем скорость больше 100мб в секунду даже на крутящихся дисках а это позволяет записать больше сотни тысяч транзакций в секунду в бинарном формате
    б) изначально заточена под хранение в оперативке архитектура которая намного эффективнее кеша sql-баз — подробности тут — https://habr.com/ru/company/oleg-bunin/blog/310560/)
    в) при переходе с клиентских транзакций на серверные уменьшается время конфликта параллельных запросов и вообще необходимость в mvcc так как можно тупо выполнять транзакции в одном потоке что гарантирует serializable. Серверные транзакции отличаются от клиентских тем что вместо того чтобы стартовать транзакцию на клиенте и посылать отдельные запросы а потом посылать коммит (как это традиционно делают c sql-базами) — в базу одним запросом сразу передается вся логика обработки данных включая if-ы и разные циклы и вся эта логика уже выполняется на самой бд максимально близко к данным


    1. balajahe Автор
      09.01.2020 13:19

      Комментарий тянет на статью, спасибо!

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


    1. balajahe Автор
      09.01.2020 15:31

      Перечитал. Вы конечно правы, но тут задачи разные, у тарантула фокус на транзакционность, у CouchDB на извлечение, а я всего-лишь говорил о ERP, где из действительно важного — только регистр мгновенных остатков (резервы это тоже остатки, только с другим статусом наличия), а все остальные данные не являются мгновенно-критичными. Раньше была привычка на любую потребность создавать таблицу, а сейчас можно создать вместо таблицы — функцию. Этот подход имеет будущее в определенных кейсах, потому что функция чистая, она не изменяет хранимые данные, и другие функции не изменяют, таким образом все функции всегда ведут себя предсказуемо. Как только у вас появляется мутабельный стейт — поддерживать его консистентность самим с собой бывает труднее, чем выучить вместо SQL какую-нибудь Scala или эликсир. Но за повышение надежности и уменьшение количества таблиц тоже нужно платить, например снижением скорости отклика.


      1. XelaVopelk
        09.01.2020 15:40

        "… потому что функция чистая, она не изменяет хранимые данные..."
        Как это не изменяет? В вашем случае вы инсертите в базу — это уже «побочный эффект», а значит функциональной чистоты вы так не добьётесь.


        1. balajahe Автор
          09.01.2020 15:46

          Возможно (даже скорее всего) я просто видел плохие реализации ERP, где часть агрегатов лучше было бы выкинуть. Например в 1С остатки хранят срезами, а в DAX — только мгновенные, а срезы считают на лету. То есть на оси «чистые функции — грязный стейт» где-то есть оптимум, и он точно не справа.


  1. XelaVopelk
    09.01.2020 16:25

    "… Поэтому существующие системы хранят удобные абстракции над строками документов (проводки)..."
    Кстати, существуют системы, работающие «от проводки», а не «от документа», например ноне практически покойный «Инфин».


  1. GeorgWarden
    09.01.2020 23:52

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