تنفيذ أجهزة الحالة في PostgreSQL · Felix Geisendörfer

تنفيذ أجهزة الحالة في PostgreSQL · Felix Geisendörfer


تنفيذ أجهزة الحالة في PostgreSQL · Felix Geisendörfer

تم النشر:

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

  • يجب ألا يتم شحن الطلبات قبل أن يتم دفعها.
  • يمكن إلغاء الطلبات، ولكن فقط إذا لم يتم شحنها بعد.
  • يجب استرداد الطلب الذي تم دفعه بالفعل بعد الإلغاء.

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

عمليًا يتم ذلك عادةً عن طريق رسم رسم بياني موجه يوضح كيفية ارتباط الحالات المختلفة ببعضها البعض عبر الأحداث:

آلة الحالة المحدودة لنظام إدارة الطلبات في PostgreSQL

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

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

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

لنبدأ بإنشاء ملف order_events الجدول الذي يتتبع جميع الأحداث ل معين order_id.

CREATE TABLE order_events (
  id serial PRIMARY KEY,
  order_id int NOT NULL,
  event text NOT NULL,
  time timestamp DEFAULT now() NOT NULL
);

بعد ذلك، دعونا ننفذ وظيفة النقل، والتي هي قلب كل ولايات ميكرونيزيا الموحدة:

CREATE FUNCTION order_events_transition(state text, event text) RETURNS text
LANGUAGE sql AS
$$
  SELECT CASE state
    WHEN 'start' THEN
      CASE event
        WHEN 'create' THEN 'awaiting_payment'
        ELSE 'error'
      END
    WHEN 'awaiting_payment' THEN
      CASE event
        WHEN 'pay' THEN 'awaiting_shipment'
        WHEN 'cancel' THEN 'canceled'
        ELSE 'error'
      END
    WHEN 'awaiting_shipment' THEN
      CASE event
        WHEN 'cancel' THEN 'awaiting_refund'
        WHEN 'ship' THEN 'shipped'
        ELSE 'error'
      END
    WHEN 'awaiting_refund' THEN
      CASE event
        WHEN 'refund' THEN 'canceled'
        ELSE 'error'
      END
    ELSE 'error'
  END
$$;

وقبل أن نواصل، دعونا نختبر بعض الأمثلة للتأكد من أن الوظيفة تعمل:

SELECT state, event, order_events_transition(state, event)
FROM (VALUES
  ('start', 'create'),
  ('awaiting_payment', 'pay'),
  ('awaiting_payment', 'cancel'),
  ('awaiting_payment', 'ship')
) AS examples(state, event);
      state       | event  | order_events_transition
------------------+--------+-------------------------
 start            | create | awaiting_payment
 awaiting_payment | pay    | awaiting_shipment
 awaiting_payment | cancel | canceled
 awaiting_payment | ship   | error

يبدو ما ورد أعلاه صحيحًا، ولكن ليس من الواضح على الفور كيف يمكننا استخدام هذه الوظيفة لفرض ولايات ميكرونيزيا الموحدة الخاصة بنا على جميع الصفوف لنفس الشيء order_id في
order_events طاولة.

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

باختصار، التجميع الذي يحدده المستخدم هو دالة لها قيمة داخلية (حالة) يتم تحديثها لكل قيمة إدخال يتم تمريرها إليها باستخدام دالة انتقال الحالة. وهذا يعني أنه يناسب نموذج FSM الخاص بنا مثل القفاز:

CREATE AGGREGATE order_events_fsm(text) (
  SFUNC = order_events_transition,
  STYPE = text,
  INITCOND = 'start'
);

ما ورد أعلاه يحدد مجموعا جديدا يسمى order_events_fsm الذي يأخذ أ
text الإدخال (أحد أحداثنا) ويستدعي order_events_transition وظيفة انتقال الحالة (FSUNC) لكل إدخال مع الحالة الحالية (STYPE) وهو أيضًا من النوع text. الحالة الأولية هي start
(INITCOND).

يُظهر الاختبار السريع أنه يعمل كما هو متوقع:

SELECT order_events_fsm(event ORDER BY id)
FROM (VALUES
  (1, 'create'),
  (2, 'pay'),
  (3, 'cancel')
) examples(id, event);
 order_events_fsm
------------------
 awaiting_refund

الآن دعونا نستخدم لدينا order_events_fsm لإنشاء BEFORE INSERT الزناد لدينا order_events الجدول الذي يتأكد من جميع أحداث معينة order_id
صالحة ولا تؤدي إلى حالة خطأ. نحن نفعل هذا باستخدام بسيطة plpgsql
الوظيفة التي تنفذ لدينا order_events_fsm ضد كل الأحداث الحالية order_id بالإضافة إلى الحدث الجديد. إذا كانت الحالة النهائية error، فإننا نثير استثناءً يؤدي إلى تراجع المعاملة الحالية. يبدو مثل هذا:

CREATE FUNCTION order_events_tigger_func() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
  new_state text;
BEGIN
  SELECT order_events_fsm(event ORDER BY id)
  FROM (
    SELECT id, event FROM order_events WHERE order_id = new.order_id
    UNION
    SELECT new.id, new.event
  ) s
  INTO new_state;
  
  IF new_state = 'error' THEN
    RAISE EXCEPTION 'invalid event';
  END IF;

  RETURN new;
END
$$;

CREATE TRIGGER order_events_trigger BEFORE INSERT ON order_events
FOR EACH ROW EXECUTE PROCEDURE order_events_tigger_func();

يمكننا التحقق من أن هذا يعمل عن طريق إدراج تسلسل صالح لأحداث الطلب:

INSERT INTO order_events (order_id, event) VALUES
  (1, 'create'),
  (1, 'pay'),
  (1, 'ship');
INSERT 0 3

بالإضافة إلى تسلسل غير صالح للأحداث:

INSERT INTO order_events (order_id, event) VALUES
  (2, 'create'),
  (2, 'ship');
psql:orders.sql:95: ERROR:  invalid event
CONTEXT:  PL/pgSQL function order_events_tigger_func() line 14 at RAISE

كما هو متوقع، لم تصل إلى طاولتنا سوى السلسلة الأولى من الإدخالات:

SELECT id, order_id, event FROM order_events;
 id | order_id | event
----+----------+--------
  1 |        1 | create
  2 |        1 | pay
  3 |        1 | ship

إذا كنت لا تزال على الحياد بشأن تضمين هذا النوع من المنطق في قاعدة البيانات الخاصة بك، فلنرى كيف يمنحنا نهجنا صلاحيات تحليلية متقدمة كمنتج ثانوي مجاني. لنفكر في مجموعة بيانات جديدة مكونة من 3 أوامر:

TRUNCATE order_events;
INSERT INTO order_events (order_id, event, time) VALUES
  (1, 'create', '2017-07-23 00:00:00'),
  (1, 'pay', '2017-07-23 12:00:00'),
  (1, 'ship', '2017-07-24 00:00:00'),

  (2, 'create', '2017-07-23 00:00:00'),
  (2, 'cancel', '2017-07-24 00:00:00'),

  (3, 'create', '2017-07-23 00:00:00'),
  (3, 'pay', '2017-07-24 00:00:00'),
  (3, 'cancel', '2017-07-25 00:00:00'),
  (3, 'refund', '2017-07-26 00:00:00');

باستخدام لدينا order_events_fsm كوظيفة نافذة، يمكننا بسهولة الحصول على سجل الحالة لأمر معين:

SELECT time, order_events_fsm(event) OVER (ORDER BY id)
FROM order_events
WHERE order_id = 3;
        time         | order_events_fsm
---------------------+-------------------
 2017-07-23 00:00:00 | awaiting_payment
 2017-07-24 00:00:00 | awaiting_shipment
 2017-07-25 00:00:00 | awaiting_refund
 2017-07-26 00:00:00 | canceled

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

SELECT date::date, state, count(1)
FROM
  generate_series('2017-07-23'::date, '2017-07-26', '1 day') date,
  LATERAL (
    SELECT order_id, order_events_fsm(event ORDER BY id) AS state
    FROM order_events
    WHERE time < date + '1 day'::interval
    GROUP BY 1
  ) orders
GROUP BY 1, 2
ORDER BY 1, 2;
    date    |       state       | count
------------+-------------------+-------
 2017-07-23 | awaiting_payment  |     2
 2017-07-23 | awaiting_shipment |     1
 2017-07-24 | awaiting_shipment |     1
 2017-07-24 | canceled          |     1
 2017-07-24 | shipped           |     1
 2017-07-25 | awaiting_refund   |     1
 2017-07-25 | canceled          |     1
 2017-07-25 | shipped           |     1
 2017-07-26 | canceled          |     2
 2017-07-26 | shipped           |     1

إليكم الأمر، تم تنفيذ FSM كتجميع محدد من قبل المستخدم في PostgreSQL مما يوفر تكامل البيانات والتحليلات المتقدمة.

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

على أي حال، أنا أتطلع حقًا إلى الحصول على تعليقات حول هذا الأمر، ويسعدني جدًا الإجابة على أي أسئلة، لذا يرجى التعليق.

انضم إلي في أبل: إذا أثارت المقالة أعلاه حماسك، فتفضل بالانضمام إلى فريقي في Apple. نحن نقوم الآن بتعيين مطوري Go وPostgreSQL في شنغهاي، الصين. النقل ممكن، فقط أرسل لي رسالة بالبريد الإلكتروني لمعرفة المزيد عن هذا الدور.

شكرًا لثورستن بول ويوهانس بوين على المراجعة.

– فيليكس جايسيندورفر


اشترك في هذه المدونة عبر RSS أو البريد الإلكتروني أو احصل على تحديثات صغيرة مني عبر تغريد.





Source link

Postagens Similares

  • Gracias por ser un amigo

    Ha sido uno de esos meses, y con eso me refiero a uno de los 663 meses desde que nací. Este no será un post largo porque sólo tengo dos cosas que decir. En primer lugar, me alegra mucho que hayamos reordenado los condados de estudio rural de GMI (Ingreso Mínimo Garantizado) para que el…

  • Instantané : Viaweb, juin 1998

    janvier 2012 Quelques heures avant l’annonce de l’acquisition de Yahoo en juin 1998, j’ai pris un instantané du site de Viaweb. J’ai pensé que cela pourrait être intéressant à regarder un jour. La première chose que l’on remarque, c’est la taille des pages. Les écrans étaient beaucoup plus petits en 1998. Si je me souviens…

  • నా కొత్త స్టార్టప్ చెక్‌లిస్ట్

    నేను ఇటీవల కొత్త కంపెనీని ప్రారంభించానని మీలో కొందరికి తెలిసి ఉండవచ్చు. నేను ఇంకా -దాని గురించి మాట్లాడటానికి సిద్ధంగా లేను, కానీ నేను మొదటి సారి వ్యవస్థాపకుడిగా రాంప్ చేయాల్సిన లాజిస్టికల్ విషయాలపై కొన్ని గమనికలను సంగ్రహించాలనుకుంటున్నాను. ఇది అక్కడ ఎవరికైనా సహాయపడుతుందని ఆశిస్తున్నాము. Source link

  • A Way to Detect Bias

    October 2015 This will come as a surprise to a lot of people, but in some cases it’s possible to detect bias in a selection process without knowing anything about the applicant pool. Which is exciting because among other things it means third parties can use this technique to detect bias whether those doing the…

  • Rec League – Neyle ilgilendiğinizi paylaşın

    Neyle ilgilendiğinizi paylaşın. Rec League sevdiğiniz şeyleri paylaşmaya yönelik bir uygulamadır. Önerilerinizi düzenli koleksiyonlar halinde kataloglayın (Roma rehberiniz, açık sekmeleriniz, açık artırma lotlarınız, kitap rafınız, elinizden bırakamayacağınız şeyler). Bakış açısına hayran olduğunuz kişileri takip edin, ortak ilgi alanlarınız üzerinden bağlantı kurun ve parlak, beklenmedik bilgiler bulun. “Rec League’deyken kortizolüm düşüyor” -Londra “Kullandıktan sonra kendimi daha…

  • Covid-19 预估更新 2020 年 8 月 23 日 – CDC 预测中包含预估

    估计感染死亡人数和死亡率 如果我们知道感染人数,并且知道感染死亡率(IFR),那么从感染人数预测死亡人数就会非常简单。然而,在整个大流行期间,我们一直在估计感染人数和 IFR,但在许多情况下做得很糟糕。各州的数字似乎也有很大差异。 IFR 约为 0.5% 我在 Medium 上的一篇文章中解释了我是如何得出这个结论的。超级快速的解释是,这个数字至少可以根据纽约州 4 月下旬进行的抗体测试,结合纽约州因 Covid-19 死亡的信息来计算出来。一旦我们知道了大概的 IFR,我们就可以引导根据死亡人数估计感染人数的方法,这有助于我们将阳性检测与感染之间的关系归零。 检测呈阳性并不等于感染 阳性检测和感染不是一回事。阳性检测是感染数量的有用指标,但它们并不相同。这个简单的概念似乎阻碍了许多所做的估计。 随着时间的推移,阳性检测与感染之间的关系一直在发生变化。在大流行初期,感染与阳性检测的比例似乎在 10:1 到 20:1 之间。现在这个比例似乎在 3:1 到 5:1 的范围内。 单纯病死率 我的估计方法的基石是跟踪 Naive CFR 的变化,作为感染与阳性检测比率的指标。 单纯病死率 计算方法是今天的死亡人数除以 14 天前的阳性检测人数。由于死亡滞后于阳性检测,因此阳性检测是未来死亡的有用先行指标。 我仍然对许多广为人知的短期死亡率估计的巨大错误感到惊讶。如果您仅采用过去 7 天的平均初始病死率 (naive CFR),并根据 14 天滞后的阳性检测结果,用它来估计未来 14 天的死亡人数,那么您可以对未来 14 天的死亡人数做出比已发布的绝大多数估计更准确的估计。 对于截至 8 月 1 日左右的估计,我使用了阳性测试百分比(阳性率)和幼稚 CFR 之间的强相关性。因为我们在报告测试数据时知道测试阳性率,所以它作为死亡率的领先指标非常有用,而且我使用最近的阳性率以及阳性测试的数量来估计近期未来的死亡率。 但自大流行开始以来,单纯病死率已大幅下降。一开始超过50%,8月初一度跌至1.4%。随着 Naive CFR…

Deixe um comentário

O seu endereço de email não será publicado. Campos obrigatórios marcados com *