Оригинал на GitHub
Вопрос
Начал писать небольшой свой блок управленческого учета. В первую очередь стоит задача фиксации движения денежных средств.
Как правильнее это реализовать с точки зрения скорости и т.п.?
Изначально была идея создать некую таблицу - регистр, в которой будут висеть остатки по всем счетам и кассам. А изменения чтобы в ней проводились одновременно с добавлением записи в таблице операций прихода-расхода. Однако увидел, что в демо-сервере это реализовано по другому - в таблице движения по складу постоянно подсчитывается остаток, и это значение потом уже подставляется через выборку с сортировкой.
Как правильнее это реализовать, какие подводные камни могут быть в обоих вариантах?
Ответ
Проблема расчета в том, что нам надо взять список операций и его просуммировать. Если у нас будет 100 млн строк — это будет очень долго.
Так как остаток по кассе/счету/складу обычно используется достаточно часто — идеологически правильно иметь такую архитектуру, что бы скорость его расчета не замедлялась со временем эксплуатации.
Поэтому мы почти везде используем расчет текущего остатка в строке операции на основе предыдущего значения.
Код в поле balance c использованием параметра Исполнять только при добавлении:
=: #sum + $prev
prev: select(table: $#ntn; field: $#nf; where: 'type' = #type; order: 'id' desc)
Параметр Исполнять только при добавлении нужен в этом случае, что бы не происходило дальнейших пересчетов этого кода после добавления, потому что мы берем значение подходящего нам баланса из последней существующей строки, а если пересчитывать после, то предыдущая строка уже не будет последней.
Если нужно иметь сводную таблицу с остатками по каждому типу, то можно сделать ее разными способами:
В которой мы через автозаполнение создаем строки с типами и в balance берем значение из таблицы операций из последней строки.
Код в поле balance в таблице отчета:
=: select(table: 'operations'; field: 'balance'; where: 'type' = #type; order: 'id' desc)
Временная таблица пересчитывается при открытии и возьмет актуальные данные.
Список с типами по которым нужно получить отчет может содержаться и в простой таблице с аналогичным кодом, который берет последнее значение balance по нужному типу из таблицы операций.
Но так как простая таблица сама по себе не пересчитывает строки, то нужно пересчитывать все строки типов раз в какой-то период, или, если нужна 100% актуальность, то делать это нужно в момент добавления строки в таблицу операций.
Код действия в поле balance в таблице operationsс триггером выполнения при добавлении:
=: recalculate(table: 'types'; where: 'type' = #type)
Аналогичным решением вместо recalculate
является set
значения остатка в простую таблицу с типами. В этом случае, не нужно никакого кода в таблице с типами. Код действия при добавлении в поле balance таблицы operations:
=: set(table: 'types'; field: 'balance' = $#nfv; where: 'type' = #type)
Развитием этого подхода является не считать balance в таблице операций, а изменять в таблице со списком типов. В этом случае не будет поля balance в таблице operations, а код действия при добавлении будет в поле sum:
=: set(table: 'types'; field: 'balance' + $#nfv; where: 'type' = #type)
Использование +
в set позволяет делать относительные изменения.
Проблема описанных выше подходов (она же их преимущества) — иммутабельность данных, те невозможность провести изменение в добавленной строке, так как они строго последовательные. Поэтому все поля в строках в таблице операций должны быть заблокированы на изменение — разрешено только добавление. Если произошла ошибка, то проводится строка с корректировкой.