ميزات لغة Q وKDB+ باستخدام مثال الخدمة في الوقت الفعلي

يمكنك أن تقرأ عن ماهية قاعدة KDB+ ولغة برمجة Q وما هي نقاط القوة والضعف في رسالتي السابقة مقالة وباختصار في المقدمة. في هذه المقالة، سنقوم بتنفيذ خدمة على Q تقوم بمعالجة دفق البيانات الواردة وحساب وظائف التجميع المختلفة كل دقيقة في وضع "الوقت الفعلي" (أي سيكون لديها الوقت لحساب كل شيء قبل الجزء التالي من البيانات). الميزة الرئيسية لـ Q هي أنها لغة متجهة تسمح لك بالعمل ليس مع كائنات مفردة، ولكن مع مصفوفاتها ومصفوفات المصفوفات والكائنات المعقدة الأخرى. تشتهر لغات مثل Q وأقاربها K، J، APL بإيجازها. في كثير من الأحيان، يمكن كتابة البرنامج الذي يشغل عدة شاشات من التعليمات البرمجية بلغة مألوفة مثل Java في بضعة أسطر. وهذا ما أريد أن أوضحه في هذا المقال.

ميزات لغة Q وKDB+ باستخدام مثال الخدمة في الوقت الفعلي

مقدمة

KDB+ هي قاعدة بيانات عمودية تركز على كميات كبيرة جدًا من البيانات، مرتبة بطريقة محددة (بشكل أساسي حسب الوقت). يتم استخدامه بشكل أساسي في المؤسسات المالية - البنوك وصناديق الاستثمار وشركات التأمين. لغة Q هي اللغة الداخلية لـ KDB+ والتي تتيح لك العمل بفعالية مع هذه البيانات. إن أيديولوجية Q هي الإيجاز والكفاءة، في حين يتم التضحية بالوضوح. يتم تبرير ذلك من خلال حقيقة أن لغة المتجهات سيكون من الصعب فهمها على أي حال، كما أن إيجاز وثراء التسجيل يسمح لك برؤية جزء أكبر بكثير من البرنامج على شاشة واحدة، مما يسهل فهمه في النهاية.

في هذه المقالة نقوم بتنفيذ برنامج كامل في Q وقد ترغب في تجربته. للقيام بذلك، ستحتاج إلى Q الفعلي. يمكنك تنزيل الإصدار المجاني 32 بت من موقع شركة kx – www.kx.com. هناك، إذا كنت مهتمًا، ستجد معلومات مرجعية عن الكتاب Q س للبشر ومقالات مختلفة حول هذا الموضوع.

صياغة المشكلة

يوجد مصدر يرسل جدولاً يحتوي على بيانات كل 25 مللي ثانية. نظرًا لأن KDB+ يستخدم بشكل أساسي في التمويل، فسوف نفترض أن هذا جدول المعاملات (الصفقات)، والذي يحتوي على الأعمدة التالية: الوقت (الوقت بالمللي ثانية)، Sym (تسمية الشركة في البورصة - IBM, AAPL،…)، السعر (السعر الذي تم شراء الأسهم به)، الحجم (حجم الصفقة). الفاصل الزمني البالغ 25 مللي ثانية هو فترة تعسفية، وليست صغيرة جدًا وليست طويلة جدًا. وجودها يعني أن البيانات تصل إلى الخدمة مخزنة بالفعل. سيكون من السهل تنفيذ التخزين المؤقت من جانب الخدمة، بما في ذلك التخزين المؤقت الديناميكي اعتمادًا على الحمل الحالي، ولكن للتبسيط، سنركز على فاصل زمني ثابت.

يجب أن تحسب الخدمة كل دقيقة لكل رمز وارد من عمود Sym مجموعة من الوظائف المجمعة - الحد الأقصى للسعر، ومتوسط ​​السعر، وحجم المبلغ، وما إلى ذلك. معلومات مفيدة. من أجل التبسيط، سنفترض أن جميع الوظائف يمكن حسابها بشكل تدريجي، أي. للحصول على قيمة جديدة، يكفي معرفة رقمين - القيم القديمة والقيمة الواردة. على سبيل المثال، تحتوي الدالات max و Average و sum على هذه الخاصية، لكن الدالة المتوسطة لا تمتلك هذه الخاصية.

سنفترض أيضًا أن تدفق البيانات الواردة مرتب بالوقت. وهذا سيتيح لنا الفرصة للعمل فقط في اللحظة الأخيرة. عمليا، يكفي أن تكون قادرا على العمل مع الدقائق الحالية والسابقة في حالة تأخر بعض التحديثات. من أجل البساطة، لن ننظر في هذه الحالة.

وظائف التجميع

وظائف التجميع المطلوبة مذكورة أدناه. أخذت أكبر عدد ممكن منهم لزيادة الحمل على الخدمة:

  • عالي - الحد الأقصى للسعر - الحد الأقصى لسعر الدقيقة.
  • منخفض – الحد الأدنى للسعر – الحد الأدنى للسعر في الدقيقة.
  • السعر الأول – السعر الأول – السعر الأول للدقيقة.
  • lastPrice – آخر سعر – آخر سعر للدقيقة.
  • firstSize - الحجم الأول - حجم التجارة الأول في الدقيقة.
  • lastSize – Last Size – آخر حجم تداول في دقيقة واحدة.
  • numTrades – العد i – عدد الصفقات في الدقيقة.
  • الحجم - مجموع الحجم - مجموع أحجام التداول في الدقيقة.
  • pvolume – مجموع السعر – مجموع الأسعار في الدقيقة، مطلوب لمتوسط ​​السعر.
  • - مجموع سعر التداول * الحجم - إجمالي حجم المعاملات في الدقيقة.
  • متوسط ​​السعر - pvolume%numTrades - متوسط ​​السعر في الدقيقة.
  • avgSize – حجم التداول%numTrades – متوسط ​​حجم التداول في الدقيقة.
  • vwap - حجم التداول٪ - متوسط ​​سعر الدقيقة مرجحًا بحجم المعاملة.
  • cumVolume - حجم المبلغ - الحجم المتراكم للمعاملات على مدار الوقت بأكمله.

دعونا نناقش على الفور نقطة واحدة غير واضحة - كيفية تهيئة هذه الأعمدة لأول مرة ولكل دقيقة لاحقة. يجب تهيئة بعض الأعمدة من النوع firstPrice لتصبح خالية في كل مرة؛ وقيمتها غير محددة. يجب دائمًا تعيين أنواع وحدات التخزين الأخرى على 0. هناك أيضًا أعمدة تتطلب نهجًا مدمجًا - على سبيل المثال، يجب نسخ cumVolume من الدقيقة السابقة، وتعيين الأول على 0. فلنقم بتعيين كل هذه المعلمات باستخدام بيانات القاموس النوع (مماثل للسجل):

// list ! list – создать словарь, 0n – float null, 0N – long null, `sym – тип символ, `sym1`sym2 – список символов
initWith:`sym`time`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover`avgPrice`avgSize`vwap`cumVolume!(`;00:00;0n;0n;0n;0n;0N;0N;0;0;0.0;0.0;0n;0n;0n;0);
aggCols:reverse key[initWith] except `sym`time; // список всех вычисляемых колонок, reverse объяснен ниже

لقد أضفتsym وtime إلى القاموس للراحة، والآن initWith عبارة عن سطر جاهز من الجدول المجمع النهائي، حيث يبقى تعيين Sym والوقت الصحيحين. يمكنك استخدامه لإضافة صفوف جديدة إلى الجدول.

سنحتاج إلى aggCols عند إنشاء وظيفة التجميع. يجب أن تكون القائمة مقلوبة بسبب الترتيب الذي يتم به تقييم التعبيرات في Q (من اليمين إلى اليسار). الهدف هو التأكد من أن الحساب ينتقل من الحجم الأعلى إلى حجم نائب الرئيس، نظرًا لأن بعض الأعمدة تعتمد على الأعمدة السابقة.

الأعمدة التي تحتاج إلى نسخها إلى دقيقة جديدة من الدقيقة السابقة، تتم إضافة عمود Sym للراحة:

rollColumns:`sym`cumVolume;

الآن دعونا نقسم الأعمدة إلى مجموعات وفقًا لكيفية تحديثها. ويمكن التمييز بين ثلاثة أنواع:

  1. المراكم (الحجم، الدوران،..) – يجب علينا إضافة القيمة الواردة إلى القيمة السابقة.
  2. مع نقطة خاصة (عالية، منخفضة، ..) - يتم أخذ القيمة الأولى في الدقيقة من البيانات الواردة، ويتم حساب الباقي باستخدام الدالة.
  3. استراحة. يتم حسابها دائمًا باستخدام دالة.

دعونا نحدد المتغيرات لهذه الفئات:

accumulatorCols:`numTrades`volume`pvolume`turnover;
specialCols:`high`low`firstPrice`firstSize;

ترتيب الحساب

سنقوم بتحديث الجدول المجمع على مرحلتين. لتحقيق الكفاءة، نقوم أولاً بتقليص الجدول الوارد بحيث يكون هناك صف واحد فقط لكل حرف ودقيقة. حقيقة أن جميع وظائفنا تزايدية وترابطية تضمن أن نتيجة هذه الخطوة الإضافية لن تتغير. يمكنك تقليص الجدول باستخدام تحديد:

select high:max price, low:min price … by sym,time.minute from table

هذه الطريقة لها عيب - مجموعة الأعمدة المحسوبة محددة مسبقًا. لحسن الحظ، في Q، يتم تنفيذ التحديد أيضًا كدالة حيث يمكنك استبدال الوسائط التي تم إنشاؤها ديناميكيًا:

?[table;whereClause;byClause;selectClause]

لن أصف بالتفصيل تنسيق الوسائط؛ في حالتنا، فقط بواسطة وحدد التعبيرات ستكون غير تافهة ويجب أن تكون قواميس لأعمدة النموذج!التعبيرات. وبالتالي يمكن تعريف دالة التقلص على النحو التالي:

selExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover!parse each ("max price";"min price";"first price";"last price";"first size";"last size";"count i";"sum size";"sum price";"sum price*size"); // each это функция map в Q для одного списка
preprocess:?[;();`sym`time!`sym`time.minute;selExpression];

من أجل الوضوح، استخدمت وظيفة التحليل، التي تحول سلسلة مع تعبير Q إلى قيمة يمكن تمريرها إلى وظيفة التقييم وهي مطلوبة في وظيفة التحديد. لاحظ أيضًا أن المعالجة المسبقة يتم تعريفها على أنها إسقاط (أي دالة ذات وسائط محددة جزئيًا) لوظيفة التحديد، وهناك وسيطة واحدة (الجدول) مفقودة. إذا قمنا بتطبيق المعالجة المسبقة على جدول، فسنحصل على جدول مضغوط.

المرحلة الثانية هي تحديث الجدول المجمع. لنكتب أولاً الخوارزمية بالكود الكاذب:

for each sym in inputTable
  idx: row index in agg table for sym+currentTime;
  aggTable[idx;`high]: aggTable[idx;`high] | inputTable[sym;`high];
  aggTable[idx;`volume]: aggTable[idx;`volume] + inputTable[sym;`volume];
  …

في Q، من الشائع استخدام وظائف الخريطة/التقليل بدلاً من الحلقات. ولكن نظرًا لأن Q هي لغة متجهة ويمكننا بسهولة تطبيق جميع العمليات على جميع الرموز في وقت واحد، فبالتقريب الأول يمكننا الاستغناء عن حلقة على الإطلاق، وتنفيذ العمليات على جميع الرموز في وقت واحد:

idx:calcIdx inputTable;
row:aggTable idx;
aggTable[idx;`high]: row[`high] | inputTable`high;
aggTable[idx;`volume]: row[`volume] + inputTable`volume;
…

ولكن يمكننا أن نذهب أبعد من ذلك، فـ Q لديه عامل فريد وقوي للغاية - عامل التعيين المعمم. يسمح لك بتغيير مجموعة من القيم في بنية بيانات معقدة باستخدام قائمة المؤشرات والوظائف والوسائط. في حالتنا يبدو مثل هذا:

idx:calcIdx inputTable;
rows:aggTable idx;
// .[target;(idx0;idx1;..);function;argument] ~ target[idx 0;idx 1;…]: function[target[idx 0;idx 1;…];argument], в нашем случае функция – это присваивание
.[aggTable;(idx;aggCols);:;flip (row[`high] | inputTable`high;row[`volume] + inputTable`volume;…)];

لسوء الحظ، لتعيين جدول، تحتاج إلى قائمة الصفوف، وليس الأعمدة، ويجب عليك تبديل المصفوفة (قائمة الأعمدة إلى قائمة الصفوف) باستخدام وظيفة الوجه. يعد هذا مكلفًا بالنسبة لجدول كبير، لذا بدلاً من ذلك نقوم بتطبيق مهمة عامة على كل عمود على حدة، باستخدام وظيفة الخريطة (التي تبدو كفاصلة عليا):

.[aggTable;;:;]'[(idx;)each aggCols; (row[`high] | inputTable`high;row[`volume] + inputTable`volume;…)];

نستخدم مرة أخرى إسقاط الوظيفة. لاحظ أيضًا أنه في Q، يعد إنشاء القائمة أيضًا وظيفة ويمكننا تسميتها باستخدام وظيفة every(map) للحصول على قائمة بالقوائم.

للتأكد من أن مجموعة الأعمدة المحسوبة غير ثابتة، سنقوم بإنشاء التعبير أعلاه ديناميكيًا. دعونا أولاً نحدد الوظائف لحساب كل عمود، باستخدام متغيرات الصف وinp للإشارة إلى البيانات المجمعة والمدخلة:

aggExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`avgPrice`avgSize`vwap`cumVolume!
 ("row[`high]|inp`high";"row[`low]&inp`low";"row`firstPrice";"inp`lastPrice";"row`firstSize";"inp`lastSize";"pvolume%numTrades";"volume%numTrades";"turnover%volume";"row[`cumVolume]+inp`volume");

بعض الأعمدة تكون خاصة، ولا ينبغي حساب قيمتها الأولى بواسطة الدالة. يمكننا تحديد أنها الأولى من خلال عمود الصف [`numTrades] - إذا كانت تحتوي على 0، فإن القيمة هي الأولى. لدى Q وظيفة تحديد - ?[قائمة منطقية;قائمة1;قائمة2] - والتي تحدد قيمة من القائمة 1 أو 2 اعتمادًا على الشرط في الوسيطة الأولى:

// high -> ?[isFirst;inp`high;row[`high]|inp`high]
// @ - тоже обобщенное присваивание для случая когда индекс неглубокий
@[`aggExpression;specialCols;{[x;y]"?[isFirst;inp`",y,";",x,"]"};string specialCols];

لقد قمت هنا باستدعاء مهمة معممة باستخدام وظيفتي (تعبير بين قوسين متعرجين). يتلقى القيمة الحالية (الوسيطة الأولى) ووسيطة إضافية، والتي أقوم بتمريرها في المعلمة الرابعة.

دعونا نضيف مكبرات صوت البطارية بشكل منفصل، لأن الوظيفة هي نفسها بالنسبة لهم:

// volume -> row[`volume]+inp`volume
aggExpression[accumulatorCols]:{"row[`",x,"]+inp`",x } each string accumulatorCols;

هذه مهمة عادية وفقًا لمعايير Q، لكنني أقوم بتعيين قائمة من القيم مرة واحدة. أخيرًا، لنقم بإنشاء الوظيفة الرئيسية:

// ":",/:aggExprs ~ map[{":",x};aggExpr] => ":row[`high]|inp`high" присвоим вычисленное значение переменной, потому что некоторые колонки зависят от уже вычисленных значений
// string[cols],'exprs ~ map[,;string[cols];exprs] => "high:row[`high]|inp`high" завершим создание присваивания. ,’ расшифровывается как map[concat]
// ";" sv exprs – String from Vector (sv), соединяет список строк вставляя “;” посредине
updateAgg:value "{[aggTable;idx;inp] row:aggTable idx; isFirst_0=row`numTrades; .[aggTable;;:;]'[(idx;)each aggCols;(",(";"sv string[aggCols],'":",/:aggExpression aggCols),")]}";

باستخدام هذا التعبير، أقوم بإنشاء دالة ديناميكيًا من سلسلة تحتوي على التعبير الذي ذكرته أعلاه. سوف تبدو النتيجة مثل هذا:

{[aggTable;idx;inp] rows:aggTable idx; isFirst_0=row`numTrades; .[aggTable;;:;]'[(idx;)each aggCols ;(cumVolume:row[`cumVolume]+inp`cumVolume;… ; high:?[isFirst;inp`high;row[`high]|inp`high])]}

يتم عكس ترتيب تقييم العمود لأنه في Q يكون ترتيب التقييم من اليمين إلى اليسار.

الآن لدينا وظيفتان رئيسيتان ضروريتان للحسابات، نحتاج فقط إلى إضافة القليل من البنية التحتية والخدمة جاهزة.

الخطوات النهائية

لدينا وظائف المعالجة المسبقة والتحديث Agg التي تقوم بكل العمل. ولكن لا يزال من الضروري التأكد من الانتقال الصحيح خلال الدقائق وحساب فهارس التجميع. أولاً، دعونا نحدد دالة init:

init:{
  tradeAgg:: 0#enlist[initWith]; // создаем пустую типизированную таблицу, enlist превращает словарь в таблицу, а 0# означает взять 0 элементов из нее
  currTime::00:00; // начнем с 0, :: означает, что присваивание в глобальную переменную
  currSyms::`u#`symbol$(); // `u# - превращает список в дерево, для ускорения поиска элементов
  offset::0; // индекс в tradeAgg, где начинается текущая минута 
  rollCache:: `sym xkey update `u#sym from rollColumns#tradeAgg; // кэш для последних значений roll колонок, таблица с ключом sym
 }

سنقوم أيضًا بتعريف وظيفة اللفة، والتي ستغير الدقيقة الحالية:

roll:{[tm]
  if[currTime>tm; :init[]]; // если перевалили за полночь, то просто вызовем init
  rollCache,::offset _ rollColumns#tradeAgg; // обновим кэш – взять roll колонки из aggTable, обрезать, вставить в rollCache
  offset::count tradeAgg;
  currSyms::`u#`$();
 }

سنحتاج إلى وظيفة لإضافة أحرف جديدة:

addSyms:{[syms]
  currSyms,::syms; // добавим в список известных
  // добавим в таблицу sym, time и rollColumns воспользовавшись обобщенным присваиванием.
  // Функция ^ подставляет значения по умолчанию для roll колонок, если символа нет в кэше. value flip table возвращает список колонок в таблице.
  `tradeAgg upsert @[count[syms]#enlist initWith;`sym`time,cols rc;:;(syms;currTime), (initWith cols rc)^value flip rc:rollCache ([] sym: syms)];
 }

وأخيرًا، وظيفة التحديث (الاسم التقليدي لهذه الوظيفة لخدمات Q)، والتي يستدعيها العميل لإضافة البيانات:

upd:{[tblName;data] // tblName нам не нужно, но обычно сервис обрабатывает несколько таблиц 
  tm:exec distinct time from data:() xkey preprocess data; // preprocess & calc time
  updMinute[data] each tm; // добавим данные для каждой минуты
};
updMinute:{[data;tm]
  if[tm<>currTime; roll tm; currTime::tm]; // поменяем минуту, если необходимо
  data:select from data where time=tm; // фильтрация
  if[count msyms:syms where not (syms:data`sym)in currSyms; addSyms msyms]; // новые символы
  updateAgg[`tradeAgg;offset+currSyms?syms;data]; // обновим агрегированную таблицу. Функция ? ищет индекс элементов списка справа в списке слева.
 };

هذا كل شئ. إليكم الكود الكامل لخدمتنا، كما وعدناكم، في بضعة أسطر فقط:

initWith:`sym`time`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover`avgPrice`avgSize`vwap`cumVolume!(`;00:00;0n;0n;0n;0n;0N;0N;0;0;0.0;0.0;0n;0n;0n;0);
aggCols:reverse key[initWith] except `sym`time;
rollColumns:`sym`cumVolume;

accumulatorCols:`numTrades`volume`pvolume`turnover;
specialCols:`high`low`firstPrice`firstSize;

selExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover!parse each ("max price";"min price";"first price";"last price";"first size";"last size";"count i";"sum size";"sum price";"sum price*size");
preprocess:?[;();`sym`time!`sym`time.minute;selExpression];

aggExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`avgPrice`avgSize`vwap`cumVolume!("row[`high]|inp`high";"row[`low]&inp`low";"row`firstPrice";"inp`lastPrice";"row`firstSize";"inp`lastSize";"pvolume%numTrades";"volume%numTrades";"turnover%volume";"row[`cumVolume]+inp`volume");
@[`aggExpression;specialCols;{"?[isFirst;inp`",y,";",x,"]"};string specialCols];
aggExpression[accumulatorCols]:{"row[`",x,"]+inp`",x } each string accumulatorCols;
updateAgg:value "{[aggTable;idx;inp] row:aggTable idx; isFirst_0=row`numTrades; .[aggTable;;:;]'[(idx;)each aggCols;(",(";"sv string[aggCols],'":",/:aggExpression aggCols),")]}"; / '

init:{
  tradeAgg::0#enlist[initWith];
  currTime::00:00;
  currSyms::`u#`symbol$();
  offset::0;
  rollCache:: `sym xkey update `u#sym from rollColumns#tradeAgg;
 };
roll:{[tm]
  if[currTime>tm; :init[]];
  rollCache,::offset _ rollColumns#tradeAgg;
  offset::count tradeAgg;
  currSyms::`u#`$();
 };
addSyms:{[syms]
  currSyms,::syms;
  `tradeAgg upsert @[count[syms]#enlist initWith;`sym`time,cols rc;:;(syms;currTime),(initWith cols rc)^value flip rc:rollCache ([] sym: syms)];
 };

upd:{[tblName;data] updMinute[data] each exec distinct time from data:() xkey preprocess data};
updMinute:{[data;tm]
  if[tm<>currTime; roll tm; currTime::tm];
  data:select from data where time=tm;
  if[count msyms:syms where not (syms:data`sym)in currSyms; addSyms msyms];
  updateAgg[`tradeAgg;offset+currSyms?syms;data];
 };

تجريب

دعونا نتحقق من أداء الخدمة. للقيام بذلك، لنقم بتشغيله في عملية منفصلة (ضع الكود في ملف Service.q) واستدعاء الدالة init:

q service.q –p 5566

q)init[]

في وحدة تحكم أخرى، ابدأ عملية Q الثانية واتصل بالأولى:

h:hopen `:host:5566
h:hopen 5566 // если оба на одном хосте

أولاً، دعونا ننشئ قائمة بالرموز - 10000 قطعة ونضيف دالة لإنشاء جدول عشوائي. في الكونسول الثاني:

syms:`IBM`AAPL`GOOG,-9997?`8
rnd:{[n;t] ([] sym:n?syms; time:t+asc n#til 25; price:n?10f; size:n?10)}

أضفت ثلاثة رموز حقيقية إلى القائمة لتسهيل البحث عنها في الجدول. تقوم الدالة rnd بإنشاء جدول عشوائي يحتوي على عدد n من الصفوف، حيث يختلف الوقت من t إلى t+25 مللي ثانية.

يمكنك الآن تجربة إرسال البيانات إلى الخدمة (أضف الساعات العشر الأولى):

{h (`upd;`trade;rnd[10000;x])} each `time$00:00 + til 60*10

يمكنك التحقق في الخدمة من تحديث الجدول:

c 25 200
select from tradeAgg where sym=`AAPL
-20#select from tradeAgg where sym=`AAPL

النتيجة:

sym|time|high|low|firstPrice|lastPrice|firstSize|lastSize|numTrades|volume|pvolume|turnover|avgPrice|avgSize|vwap|cumVolume
--|--|--|--|--|--------------------------------
AAPL|09:27|9.258904|9.258904|9.258904|9.258904|8|8|1|8|9.258904|74.07123|9.258904|8|9.258904|2888
AAPL|09:28|9.068162|9.068162|9.068162|9.068162|7|7|1|7|9.068162|63.47713|9.068162|7|9.068162|2895
AAPL|09:31|4.680449|0.2011121|1.620827|0.2011121|1|5|4|14|9.569556|36.84342|2.392389|3.5|2.631673|2909
AAPL|09:33|2.812535|2.812535|2.812535|2.812535|6|6|1|6|2.812535|16.87521|2.812535|6|2.812535|2915
AAPL|09:34|5.099025|5.099025|5.099025|5.099025|4|4|1|4|5.099025|20.3961|5.099025|4|5.099025|2919

لنجري الآن اختبار التحميل لمعرفة مقدار البيانات التي يمكن للخدمة معالجتها في الدقيقة. اسمحوا لي أن أذكرك أننا قمنا بتعيين الفاصل الزمني للتحديث على 25 مللي ثانية. وبناءً على ذلك، يجب أن تصل الخدمة (في المتوسط) إلى 20 مللي ثانية على الأقل لكل تحديث لمنح المستخدمين الوقت لطلب البيانات. أدخل ما يلي في العملية الثانية:

tm:10:00:00.000
stressTest:{[n] 1 string[tm]," "; times,::h ({st:.z.T; upd[`trade;x]; .z.T-st};rnd[n;tm]); tm+:25}
start:{[n] times::(); do[4800;stressTest[n]]; -1 " "; `min`avg`med`max!(min times;avg times;med times;max times)}

4800 دقيقتين. يمكنك تجربة التشغيل أولاً لـ 1000 صف كل 25 مللي ثانية:

start 1000

في حالتي، تكون النتيجة حوالي بضعة مللي ثانية لكل تحديث. لذلك سأزيد عدد الصفوف على الفور إلى 10.000:

start 10000

النتيجة:

min| 00:00:00.004
avg| 9.191458
med| 9f
max| 00:00:00.030

مرة أخرى، لا يوجد شيء مميز، ولكن هذا 24 مليون سطر في الدقيقة، 400 ألف في الثانية. ولأكثر من 25 مللي ثانية، تباطأ التحديث 5 مرات فقط، على ما يبدو عندما تغيرت الدقيقة. دعونا نزيد إلى 100.000:

start 100000

النتيجة:

min| 00:00:00.013
avg| 25.11083
med| 24f
max| 00:00:00.108
q)sum times
00:02:00.532

كما ترون، فإن الخدمة بالكاد يمكنها التعامل، لكنها مع ذلك تمكنت من البقاء واقفا على قدميه. مثل هذا الحجم من البيانات (240 مليون صف في الدقيقة) كبير للغاية، وفي مثل هذه الحالات، من الشائع إطلاق عدة نسخ (أو حتى عشرات النسخ) من الخدمة، كل منها يعالج جزءًا فقط من الأحرف. ومع ذلك، فإن النتيجة مثيرة للإعجاب بالنسبة للغة مفسرة تركز في المقام الأول على تخزين البيانات.

قد يطرح السؤال حول سبب نمو الوقت بشكل غير خطي مع حجم كل تحديث. والسبب هو أن وظيفة التقليص هي في الواقع وظيفة C، وهي أكثر كفاءة بكثير من UpdateAgg. بدءًا من حجم تحديث معين (حوالي 10.000)، يصل UpdateAgg إلى الحد الأقصى ومن ثم لا يعتمد وقت تنفيذه على حجم التحديث. بفضل الخطوة الأولية Q، تكون الخدمة قادرة على استيعاب مثل هذه الكميات من البيانات. وهذا يسلط الضوء على مدى أهمية اختيار الخوارزمية الصحيحة عند العمل مع البيانات الضخمة. نقطة أخرى هي التخزين الصحيح للبيانات في الذاكرة. إذا لم يتم تخزين البيانات بشكل عمودي أو لم يتم ترتيبها حسب الوقت، فسنصبح على دراية بشيء مثل ذاكرة التخزين المؤقت TLB المفقودة - غياب عنوان صفحة الذاكرة في ذاكرة التخزين المؤقت لعنوان المعالج. يستغرق البحث عن عنوان حوالي 30 مرة أطول إذا لم ينجح، وإذا كانت البيانات متناثرة، فقد يؤدي ذلك إلى إبطاء الخدمة عدة مرات.

اختتام

في هذه المقالة، أوضحت أن قاعدة بيانات KDB+ وQ مناسبة ليس فقط لتخزين البيانات الكبيرة والوصول إليها بسهولة من خلال التحديد، ولكن أيضًا لإنشاء خدمات معالجة البيانات القادرة على استيعاب مئات الملايين من الصفوف/الجيجابايت من البيانات حتى في عملية Q واحدة. تتيح لغة Q نفسها تنفيذًا موجزًا ​​وفعالًا للغاية للخوارزميات المتعلقة بمعالجة البيانات نظرًا لطبيعتها المتجهة ومترجم لهجة SQL المدمج ومجموعة ناجحة جدًا من وظائف المكتبة.

سأشير إلى أن ما ورد أعلاه هو مجرد جزء مما يمكن أن تفعله Q، ولديها ميزات فريدة أخرى أيضًا. على سبيل المثال، بروتوكول IPC بسيط للغاية، والذي يمحو الحدود بين عمليات Q الفردية ويسمح لك بدمج مئات من هذه العمليات في شبكة واحدة، والتي يمكن أن تكون موجودة على عشرات الخوادم في أجزاء مختلفة من العالم.

المصدر: www.habr.com

إضافة تعليق