"الحياة" على PostgreSQL

نُشر مؤخرًا على حبري مقالًا بعنوان معركة البحر في PostgreSQL . يجب أن أعترف: أحب حل المشكلات في SQL غير المخصصة لـ SQL. خاصة مع بيان SQL واحد. وأنا أتفق تمامًا مع المؤلفين:



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



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



اسمحوا لي ألا أشرح ما هي حياة جون كونواي. سأقول فقط - اتضح  - باستخدام الجهاز الخلوي للحياة ، يمكنك بناء آلة تورينج عالمية. يبدو لي أن هذه حقيقة عظيمة.



إذن ، هل من الممكن تنفيذ لعبة Life بعبارة SQL واحدة؟



حسنًا ، لنبدأ.



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



الحديث عن المصفوفات
, :



CREATE TABLE matrix (
  rw  integer,
  cl  integer,
  val float
);


, , — . , A(L×M) B(M×N) (L×N), ci,j = ∑k = 1...M  ai,k × bk,j.



i, j, k. SQL- :



SELECT a.rw, b.cl, sum(a.val * b.val)
FROM a
    JOIN b ON a.cl = b.rw
GROUP BY a.rw, b.cl;


. . : . . .



, , , . .



لذا فإن المجال:



CREATE TABLE cells(
    x integer,
    y integer
);
INSERT INTO cells VALUES
    (0,2), (1,2), (2,2), (2,1), (1,0); -- glider


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



WITH shift(x,y) AS (
    VALUES (0,1), (0,-1), (1,0), (-1,0), (1,1), (1,-1), (-1,1), (-1,-1)
),
neighbors(x,y,cnt) AS (
    SELECT t.x, t.y, count(*)
    FROM (
        SELECT c.x + s.x, c.y + s.y
        FROM cells c
            CROSS JOIN shift s
    ) t(x,y)
    GROUP BY t.x, t.y 
)
SELECT * FROM neighbors;


يمكن أيضًا إنشاء التحولات باستخدام استعلام ، ولكن من المحتمل ألا تجعل الأمر أسهل.



بوجود جيران ، يبقى أن نقرر أي الخلايا يجب أن تموت وأي منها يجب أن يولد:



WITH shift(x,y) AS (
    ...
),
neighbors(x,y,cnt) AS (
    ...
),
generation(x,y,status) AS (
    SELECT coalesce(n.x,c.x),
           coalesce(n.y,c.y),
           CASE
                WHEN c.x IS NULL THEN 'NEW'
                WHEN n.cnt IN (2,3) THEN 'STAY'
                ELSE 'DIE'
           END
    FROM neighbors n
        FULL JOIN cells c ON c.x = n.x AND c.y = n.y
    WHERE (c.x IS NULL AND n.cnt = 3)
          OR
          (c.x IS NOT NULL)
)
SELECT * FROM generation;


الاتصال الكامل ضروري هنا ، من ناحية ، يمكن أن تنشأ حياة جديدة في خلية فارغة ، ومن ناحية أخرى لتدمير الخلايا الحية "في الضواحي". لدينا ثلاثة شروط للدخول في العينة: إما أن تكون الخلية فارغة ولديها ثلاثة جيران بالضبط (ثم يجب أن تعود إلى الحياة وتتلقى الحالة الجديدة) ، أو أنها على قيد الحياة ولديها اثنان أو ثلاثة من الجيران (ثم تعيش وتتلقى حالة البقاء) ، أو أنها على قيد الحياة ، ولكن لديه أقل من اثنين أو أكثر من ثلاثة جيران (ثم محكوم عليه بالموت ويتلقى حالة DIE).



نحتاج الآن إلى تحديث ساحة اللعب باستخدام معلومات حول الجيل الجديد من الخلايا. هذا هو المكان الذي تصبح فيه قدرات PostgreSQL مفيدة: سنفعل كل ما نحتاج إليه في نفس عبارة SQL.



WITH shift(x,y) AS (
    ...
),
neighbors(x,y,cnt) AS (
    ...
),
generation(x,y,status) AS (
    ...
),
del AS ( 
    DELETE FROM cells
    WHERE (x,y) IN (
        SELECT x, y FROM generation WHERE status = 'DIE'
  )
),
ins AS (
    INSERT INTO cells
        SELECT x, y FROM generation WHERE status = 'NEW'
)
SELECT *
FROM generation
WHERE status IN ('STAY','NEW');


في الواقع ، كل منطق اللعبة مكتوب!



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



هنا هو الاستعلام بأكمله ، مع الحد الأدنى من المخرجات القابلة للهضم. انسخ والصق واستمتع!



WITH shift(x,y) AS (
    VALUES (0,1), (0,-1), (1,0), (-1,0), (1,1), (1,-1), (-1,1), (-1,-1)
),
neighbors(x,y,cnt) AS (
    SELECT t.x, t.y, count(*)
    FROM (
        SELECT c.x + s.x, c.y + s.y
        FROM cells c
            CROSS JOIN shift s
    ) t(x,y)
    GROUP BY t.x, t.y 
),
generation(x,y,status) AS (
    SELECT coalesce(n.x,c.x),
           coalesce(n.y,c.y),
           CASE
                WHEN c.x IS NULL THEN 'NEW'
                WHEN n.cnt IN (2,3) THEN 'STAY'
                ELSE 'DIE'
           END
    FROM neighbors n
        FULL JOIN cells c ON c.x = n.x AND c.y = n.y
    WHERE (c.x IS NULL AND n.cnt = 3)
          OR
          (c.x IS NOT NULL)
),
del AS ( 
    DELETE FROM cells
    WHERE (x,y) IN (
        SELECT x, y FROM generation WHERE status = 'DIE'
  )
),
ins AS (
    INSERT INTO cells
        SELECT x, y FROM generation WHERE status = 'NEW'
),
dimensions(x1,x2,y1,y2) AS (
    SELECT min(x), max(x), min(y), max(y)
    FROM generation
    WHERE status IN ('STAY','NEW')
)
SELECT string_agg(CASE WHEN g.x IS NULL THEN ' ' ELSE '*' END, '' ORDER BY cols.x)
FROM dimensions d
    CROSS JOIN generate_series(d.x1,d.x2) cols(x)
    CROSS JOIN generate_series(d.y1,d.y2) lines(y)
    LEFT JOIN generation g ON g.x = cols.x AND g.y = lines.y AND g.status IN ('STAY','NEW')
GROUP BY lines.y
ORDER BY lines.y
\watch 1



All Articles