
وحتى التطبيقات في الكود ، بما في ذلك JavaScript ، هناك الكثير منها - من "canonical" بواسطة John Resig والإصدارات المحسنة المختلفة إلى سلسلة من الوحدات النمطية في NPM .
لماذا احتجنا إلى استخدامه لخدمة جمع وتحليل خطط PostgreSQL ، وحتى "إعادة تدوير" بعض عمليات التنفيذ الجديدة؟ ..
سجلات ملتصقة
دعنا نلقي نظرة على جزء صغير من سجل خادم PostgreSQL:
2020-09-11 14:49:53.281 MSK [80927:619/4255507] [explain.tensor.ru] 10.76.182.154(59933) pgAdmin III - ???????????????????? ???????????????? LOG: duration: 0.016 ms plan:
Query Text: explain analyze
SELECT
*
FROM
pg_class
WHERE
relname = '
';
Index Scan using pg_class_relname_nsp_index on pg_class (cost=0.29..2.54 rows=1 width=265) (actual time=0.014..0.014 rows=0 loops=1)
Index Cond: (relname = '
'::name)
Buffers: shared hit=2
يمكننا استخدام التنسيق الذي تم تعيينه بواسطة متغير log_line_prefix لتحديد وقص سطر العنوان الذي يبدأ بتاريخ :
SHOW log_line_prefix;
-- "%m [%p:%v] [%d] %r %a "
يتطلب الأمر قدرًا كبيرًا من السحر
const reTS = "\\d{4}(?:-\\d{2}){2} \\d{2}(?::\\d{2}){2}"
, reTSMS = reTS + "\\.\\d{3}"
, reTZ = "(?:[A-Za-z]{3,5}|GMT[+\\-]\\d{1,2})";
var re = {
// : log_line_prefix
'%a' : "(?:[\\x20-\\x7F]{0,63})"
, '%u' : "(?:[\\x20-\\x7F]{0,63})"
, '%d' : "[\\x20-\\x7F]{0,63}?"
, '%r' : "(?:(?:\\d{1,3}(?:\\.\\d{1,3}){3}|[\\-\\.\\_a-z0-9])\\(\\d{1,5}\\)|\\[local\\]|)"
, '%h' : "(?:(?:\\d{1,3}(?:\\.\\d{1,3}){3}|[\\-\\.\\_a-z0-9])|\\[local\\]|)"
, '%p' : "\\d{1,5}"
, '%t' : reTS + ' ' + reTZ
, '%m' : reTSMS + ' ' + reTZ
, '%i' : "(?:SET|SELECT|DO|INSERT|UPDATE|DELETE|COPY|COMMIT|startup|idle|idle in transaction|streaming [0-9a-f]{1,8}\/[0-9a-f]{1,8}|)(?: waiting)?"
, '%e' : "[0-9a-z]{5}"
, '%c' : "[0-9a-f]{1,8}\\.[0-9a-f]{1,8}"
, '%l' : "\\d+"
, '%s' : "\\d{4}(?:-\\d{2}){2} \\d{2}(?::\\d{2}){2} [A-Z]{3}"
, '%v' : "(?:\\d{1,9}\\/\\d{1,9}|)"
, '%x' : "\\d+"
, '%q' : ""
, '%%' : "%"
// : log_min_messages
, '%!' : "(?:DEBUG[1-5]|INFO|NOTICE|WARNING|ERROR|LOG|FATAL|PANIC)"
// : log_error_verbosity
, '%@' : "(?:DETAIL|HINT|QUERY|CONTEXT|LOCATION|STATEMENT)"
};
re['%#'] = "(?:" + re['%!'] + "|" + re['%@'] + ")";
// log_line_prefix RegExp
let lre = self.settings['log_line_prefix'].replace(/([\[\]\(\)\{\}\|\?\$\\])/g, "\\\$1") + '%#: ';
self.tokens = lre.match(new RegExp('(' + Object.keys(re).join('|') + ')', 'g'));
let matcher = self.tokens.reduce((str, token) => str.replace(token, '(' + re[token] + ')'), lre);
self.matcher = new RegExp('^' + matcher, '');
ولكن بعد ذلك لدينا طلب مع خطة - وكيف نفهم أين ينتهي أحد ويبدأ الآخر؟ ..
يبدو أن الخطة عبارة عن تمثيل نصي لشجرة ، لذلك يجب أن يكون هناك "جذر" واحد؟ أي السطر الأول من الأسفل مع أدنى مسافة بادئة (حذف ،
Trigger ...) - الجذر المطلوب وبداية الخطة؟
للاسف لا. في مثالنا ، مثل هذه السلسلة ستكون "الذيل"
'::name)من الانقسام إلى أجزاء سلسلة متعددة الأسطر. كيف تكون؟
استخدم Trie ، Luke!
لكن لاحظ أن الخطة يجب أن تبدأ من إحدى العقد:
Seq Scan, Index Scan, Sort, Aggregate, ...- لا أكثر ولا أقل ، ولكن 133 خيارًا مختلفًا ، باستثناء CTE, InitPlan SubPlanتلك التي لا يمكن أن تكون جذرًا.
في الواقع ، لا نعرف أي من العقد التي نعرفها موجودة في بداية هذا السطر (وإن كانت موجودة أصلاً) ، لكننا نريد العثور عليها. هذا هو المكان الذي ستساعدنا فيه شجرة البادئة .
تراي ثابت
لكن شجرتنا التي نريد بناءها لها العديد من الميزات:
- الانضغاط
لدينا عشرات / مئات العناصر المحتملة بطول محدود للغاية ، لذلك لا يمكن أن يكون هناك موقف لعدد كبير من المفاتيح الطويلة جدًا المتطابقة تقريبًا والتي تختلف فقط في الحرف الأخير. أطول مفاتيح لدينا هو على الأرجح'Parallel Index Only Scan Backward'. -
. . -
. , . - -
, «» Garbage Collector'.
يرجع الشرط الأخير إلى حقيقة أن تحليل السجلات على أدوات التجميع لدينا يتم تنفيذه دون انقطاع في وضع التدفق. وكلما قلت قدرتنا على "التخلص من القمامة" ، زادت الموارد التي نوجهها إلى النشاط المفيد بدلاً من التنظيف وراء أنفسنا.
ستساعدنا وظيفتان مفيدتان في هذا:
String.prototype.charCodeAt(index)يسمح لك بمعرفة رمز الحرف في موضع معين في السلسلةString.prototype.startsWith(searchString[, position])يتحقق مما إذا كانت بداية سلسلة [من موضع معين] تطابق البحث
بناء الخريطة
لنلقِ نظرة على مثال لكيفية إنشاء خريطة للعثور بسرعة على العناصر التي تحتاجها من المجموعة الأصلية باستخدام هاتين العمليتين: Hmm ... لديهم نفس بادئة "In"!
Insert
Index Scan
Index Scan Backward
Index Only Scan
Index Only Scan Backward
// Longest Common Prefix
let len, lcp;
for (let key of keys) {
//
if (lcp === undefined) {
len = key.length;
lcp = key.slice(off);
continue;
}
len = Math.min(len, key.length);
// , "" LCP
if (lcp == '' || key.startsWith(lcp, off)) {
continue;
}
// LCP
for (let i = 0; i < lcp.length; i++) {
if (lcp.charCodeAt(i) != key.charCodeAt(off + i)) {
lcp = lcp.slice(0, i);
break;
}
}
}
ولأنها هي نفسها ، فعند التحقق من رموزها ، لن نتمكن من الحصول على معلومات جديدة بأي شكل من الأشكال - مما يعني أننا نحتاج فقط إلى التحقق من الرموز التي تذهب إلى أبعد من ذلك ، حتى طول أقصر عنصر . سوف يساعدوننا في تقسيم جميع العناصر إلى عدة مجموعات: في هذه الحالة ، لا يهم الرمز الذي نأخذه للقسم (الثالث أو الخامس ، على سبيل المثال) - سيظل تكوين المجموعات كما هو ، لذلك تتكرر نفس المجموعة بالضبط من التقسيم إلى مجموعات ليست هناك حاجة للمعالجة :
Insert
Index Scan
Index Scan Backward
Index Only Scan
Index Only Scan Backward
//
let grp = new Set();
res.pos = {};
for (let i = off + lcp.length; i < len; i++) {
// [i]-
let chr = keys.reduce((rv, key) => {
if (key.length < i) {
return rv;
}
let ch = key.charCodeAt(i);
rv[ch] = rv[ch] || [];
rv[ch].push(key);
return rv;
}, {});
//
let cmb = Object.values(chr)
.map(seg => seg.join('\t'))
.sort()
.join('\n');
if (grp.has(cmb)) {
continue;
}
else {
grp.add(cmb);
}
res.pos[i] = chr;
}
المقياس الأمثل
يبقى فقط أن نفهم - وإذا كانت المجموعات مختلفة في الرمزين الثالث والخامس - أي من فروع الأشجار هذه يجب أن تختار؟ للقيام بذلك ، نقدم مقياسًا يمنحنا إجابة على هذا السؤال - عدد المقارنات بين الأحرف الفردية للعثور على كل مفتاح من المفاتيح.
نحن هنا نهمل حقيقة أن بعض العقد توجد في الواقع في الخطط أكثر من غيرها ، ونعتبرها متكافئة.
, 3-'s',startsWith, , 6 , ,Insert.
: 1 (.charCodeAt(2)) + 6 (.startsWith('Insert')) = 7 .
'd', 7-, ,'O''S'. —'Index Scan Backward'(+19 )'Index Scan'(+10 ).
,'Index Scan Backward', 19 ,'Index Scan'— 19 + 10 = 29.
: 1 (.charCodeAt(2)) + 1 (.charCodeAt(6)) + 19 + 29 (.startsWith(...)) = 50 .
نتيجة لذلك ، على سبيل المثال لدينا ، ستبدو الخريطة المثالية كما يلي:
{
'$pos' : 2 // 3-
, '$chr' : Map {
100 => { // 'd'
'$pos' : 6 // 7-
, '$chr' : Map {
79 => [ 'Index Only Scan Backward', 'Index Only Scan' ] // 'O'
, 83 => [ 'Index Scan Backward', 'Index Scan' ] // 'S'
}
}
, 115 => 'Insert' // 's'
}
}
فزوحه!
الآن كل ما تبقى هو تجميع كل شيء معًا وإضافة وظيفة البحث وبعض التحسينات - ويمكنك استخدام:
//
const fill = (obj, off, hash) => {
off = off || 0;
hash = hash || {};
let keys = obj.src;
//
let H = keys.join('\n');
hash[off] = hash[off] || {};
if (hash[off][H]) {
obj.res = hash[off][H];
return;
}
obj.res = {};
hash[off][H] = obj.res;
let res = obj.res;
// -
if (keys.length == 1) {
res.lst = [...keys];
res.cmp = res.lst[0].length;
return;
}
// Longest Common Prefix
let len, lcp;
for (let key of keys) {
//
if (lcp == undefined) {
len = key.length;
lcp = key.slice(off);
continue;
}
len = Math.min(len, key.length);
// , "" LCP
if (lcp == '' || key.startsWith(lcp, off)) {
continue;
}
// LCP
for (let i = 0; i < lcp.length; i++) {
if (lcp.charCodeAt(i) != key.charCodeAt(off + i)) {
lcp = lcp.slice(0, i);
break;
}
}
}
//
if (off + lcp.length == len) {
let cmp = 0;
// -
if (keys.length == 2) {
res.lst = [...keys];
}
// " "
else {
res.src = keys.filter(key => key.length > off + lcp.length);
res.lst = keys.filter(key => key.length <= off + lcp.length);
}
// , ,
res.lst.sort((x, y) => y.length - x.length); // s.length DESC
cmp += res.lst.reduce((rv, key, idx, keys) => rv + (keys.length - idx + 1) * key.length, 0);
// -
if (res.src && res.src.length) {
fill(res, off + lcp.length + 1, hash);
cmp += res.res.cmp;
}
res.cmp = cmp + 1;
return;
}
//
let grp = new Set();
res.pos = {};
for (let i = off + lcp.length; i < len; i++) {
// [i]-
let chr = keys.reduce((rv, key) => {
if (key.length < i) {
return rv;
}
let ch = key.charCodeAt(i);
rv[ch] = rv[ch] || [];
rv[ch].push(key);
return rv;
}, {});
//
let cmb = Object.values(chr)
.map(seg => seg.join('\t'))
.sort()
.join('\n');
if (grp.has(cmb)) {
continue;
}
else {
grp.add(cmb);
}
let fl = true;
let cmp = 0;
for (let [ch, keys] of Object.entries(chr)) {
//
if (keys.length == 1) {
let key = keys[0];
chr[ch] = key;
cmp += key.length;
}
//
else {
fl = false;
chr[ch] = {src : keys};
fill(chr[ch], i + 1, hash);
cmp += chr[ch].res.cmp;
}
}
res.pos[i] = {
chr
, cmp
};
// ""
if (res.cmp === undefined || cmp + 1 < res.cmp) {
res.cmp = cmp + 1;
res.bst = i;
}
// ,
if (fl) {
res.bst = i;
for (let j = off; j < i; j++) {
delete res.pos[j];
}
break;
}
}
};
//
const comp = obj => {
//
delete obj.src;
delete obj.cmp;
if (obj.res) {
let res = obj.res;
if (res.pos !== undefined) {
//
obj.$pos = res.bst;
let $chr = res.pos[res.bst].chr;
Object.entries($chr).forEach(([key, val]) => {
//
comp(val);
// - ""
let keys = Object.keys(val);
if (keys.length == 1 && keys[0] == '$lst') {
$chr[key] = val.$lst;
}
});
// - Map -
obj.$chr = new Map(Object.entries($chr).map(([key, val]) => [Number(key), val]));
}
// ""
if (res.lst !== undefined) {
obj.$lst = res.lst;
delete res.lst;
if (res.res !== undefined) {
comp(res);
Object.assign(obj, res);
}
}
delete obj.res;
}
};
// -
const find = (str, off, map) => {
let curr = map;
do {
//
let $pos = curr.$pos;
if ($pos !== undefined) {
let next = curr.$chr.get(str.charCodeAt(off + $pos));
if (typeof next === 'string') { //
if (str.startsWith(next, off)) {
return next;
}
}
else if (next instanceof Array) { //
for (let key of next) {
if (str.startsWith(key, off)) {
return key;
}
}
}
else if (next !== undefined) { // map,
curr = next;
continue;
}
}
// ,
if (curr.$lst) {
for (let key of curr.$lst) {
if (str.startsWith(key, off)) {
return key;
}
}
}
return;
}
while (true);
};
function ImmutableTrie(keys) {
this.map = {src : keys.sort((x, y) => x < y ? -1 : +1)};
fill(this.map);
comp(this.map);
}
const p = ImmutableTrie.prototype;
p.get = function(line, off) {
return find(line, off || 0, this.map);
};
p.has = function(line, off) {
return this.get(line, off) !== undefined;
};
module.exports = ImmutableTrie;
كما ترون ، عند البحث في مثل هذه Trie الثابتة ،
المكافأة: يمكننا الآن الحصول على البادئة المرغوبة دون الحاجة إلى القيام بذلك
.sliceعلى السطر الأصلي ، حتى لو علمنا أنه في البداية ، كان هناك شيء غريب بالنسبة للخطة:
const nodeIT = new ImmutableTrie(...);
nodeIT.get(' -> Parallel Seq Scan on abc', 6); // 'Parallel Seq Scan'
حسنًا ، عندما قررنا بالفعل أين تبدأ الخطة ، بنفس الطريقة تمامًا (ولكن بمساعدة أسماء سمات Trie) ، نحدد الخطوط التي تمثل بداية سمة العقدة ، والتي تمثل استمرارًا للسلسلة متعددة الأسطر و "لصقها":
Index Scan using pg_class_relname_nsp_index on pg_class (cost=0.29..2.54 rows=1 width=265) (actual time=0.014..0.014 rows=0 loops=1)
Index Cond: (relname = '\n\n'::name)
Buffers: shared hit=2
حسنًا ، في هذا الشكل ، من الأسهل بكثير تفكيكه.