哈Ha!
在本文中,我们将尝试从功能编程的角度来看会计系统(ERP,CRM,WMS,MES,B2B等)的体系结构。 现有系统很复杂。 它们基于关系数据模式,并且以数百个相关表的形式具有巨大的可变状态。 而且,在这种系统中,唯一的“真理来源”是按时间顺序排列的原始文档日志(现实事件的打印),显然,该日志必须是不可变的(并且在禁止回溯更正的已审计系统中遵守此规则)。 文档日志强制使用了20%的数据库量,其他所有内容都是中间的抽象和聚合,它们在SQL语言中使用起来很方便,但需要与文档以及彼此之间保持不断的同步。
如果我们回到源头(消除数据冗余并拒绝存储聚合),并以直接应用于主文档流的功能形式实施所有业务算法,我们将获得一个功能强大的DBMS和一个功能强大的ERP。 由于有了
备忘录 ,性能问题得以解决,功能代码的数量将与声明性SQL的数量相称,并且不难理解。 在本文中,我们将通过在TypeScript和
Deno运行时 (类似于Node.js)中开发最简单的文件DBMS来演示该方法,并以典型的业务任务为例来测试卷积的性能。
为什么这很重要
1)可变状态+数据冗余性很差,尤其是在有必要确保其与文档流的持续同步时。 这是潜在的凭证差异的来源(余额不收敛)并且很难检测到副作用。
2)在大数据,异构系统以及快速变化的条件下(即实际上在任何地方),用于存储源数据和中间数据的刚性关系方案都很昂贵。 我们建议以原始格式存储文档,并按时间排序,以允许“从新到旧”的连接,反之亦然。 这样一来,大多数聚合就可以直接通过单次算法从文档中进行计算,而
无需其他所有表格。
3) SQL已过时,因为它假定随时都有任何数据可用,并且在分布式系统中显然不是这种情况-开发大数据算法时,您需要为一些必要的数据做好准备以便稍后出现,而某些早已出现。 这就需要对查询语言进行一些重新思考,并有意识地关注缓存。
4) Modern PL使您可以创建一个响应式系统,该系统可在笔记本电脑上本地处理数百万条记录,而RDBMS根本不会在其中安装。 而且,如果我们谈论服务器-提出的方案在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, - ), .
UPD1) ,
VolCh .
2) CouchDB,
apapacy .
PS