مرحبا يا هبر!
سنحاول في هذه المقالة إلقاء نظرة على بنية النظم المحاسبية (ERP ، CRM ، WMS ، MES ، B2B ، ...) من منظور البرمجة الوظيفية. النظم الحالية معقدة. وهي تستند إلى مخطط البيانات العلائقية ، ولها حالة قابلة للتغيير ضخمة في شكل مئات الجداول ذات الصلة. علاوة على ذلك ، فإن "مصدر الحقيقة" الوحيد في مثل هذه الأنظمة هو مجلة من الوثائق الأولية مرتبة ترتيبًا زمنيًا (مطبوعات لأحداث حقيقية) ، والتي من الواضح أنها يجب أن تكون غير قابلة للتغيير (وهذه القاعدة تتم ملاحظتها في الأنظمة المراجعة حيث يُحظر التصحيحات الخلفية). تشكل يومية المستندات 20٪ من حجم قاعدة البيانات بالقوة ، وكل شيء آخر هو عبارة عن مجاميع وسيطة ، وهي ملائمة للعمل بلغة SQL ، ولكنها تتطلب مزامنة ثابتة مع المستندات ومع بعضها البعض.
إذا عدنا إلى المصادر (تخلصنا من تكرار البيانات ونرفض تخزين المجاميع) ، وننفذ جميع خوارزميات الأعمال في شكل وظائف يتم تطبيقها مباشرة على دفق الوثائق الأساسية ، فسنحصل على نظام إدارة قواعد البيانات الوظيفية ونظام ERP وظيفي مبني عليه. تم حل مشكلة الأداء بفضل عملية
الحفظ ،
وستكون كمية التعليمات البرمجية الوظيفية متناسبة تمامًا مع مقدار لغة SQL
التوضيحية ، وليس من الصعب فهمها. في هذه المقالة ، سنعرض النهج من خلال تطوير أبسط الملفات DBMS في TypeScript و
Deno وقت التشغيل (التناظرية إلى Node.js) ، وكذلك اختبار أداء التلفيفات باستخدام مهام العمل النموذجية كمثال.
لماذا هذا ذات الصلة
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