PostgreSQLの「人生」

最近Habréで、PostgreSQLでの海の戦いに関する記事が公開されました私は認めなければなりません:私はSQLを対象としていないSQLの問題を解決するのが大好きです。特に1つのSQLステートメントで。そして、私は著者に完全に同意します:



他の目的で特別なツールを使用すると、専門家から否定的なフィードバックが生じることがよくあります。ただし、意味のない興味深いタスクを解決すると、横方向の思考が訓練され、適切な解決策を探すためにさまざまな観点からツールを探索できます。



そしてさらに。正直に言うと、SQLを本来の目的で使用することは、常に退屈なことです。Coddによる同じ記事から始めて、すべての教科書にどのような例が示されているか覚えていますか?サプライヤーと部品、従業員と部門...喜びはどこにあり、楽しみはどこにありますか?私にとって、インスピレーションの源の1つは、手続き型ソリューションと宣言型ソリューションを比較することです。ジョン・コンウェイ



生涯とは何かを説明しないでください。 -私はと言うことができる、それが判明 -のセルオートマトン使って生活を、あなたはユニバーサルチューリングマシンを構築することができます。これは壮大な事実のように私には思えます。



では、1つのSQLステートメントでゲームLifeを実装することは可能ですか?



さて、始めましょう。



私たちのフィールドは、生きている細胞の座標を含むテーブルであり、あなたが激怒しているように、2次元の配列ではありません。このビューは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


隣人を数えるために、手続き型ループを回す代わりに、「マトリックス」を8方向すべてに1つのセルに移動し、各位置にある生きているセルの数を合計してみましょう。



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;


ここでは完全な接続が必要です。そうすることで、一方では空のセルで新しい生命が生まれ、他方では「郊外」の生きているセルを破壊することができます。サンプルに入るには、3つの条件があります。セルが空で、隣接するセルが3つだけある(その後、有効になり、新しいステータスを取得する必要があります)、または有効で、隣接するものが2つまたは3つある(その後、存続してSTAYステータスを取得する)、または有効ですが、ネイバーが2つ未満または3つを超える(その後、死ぬ運命にあり、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');


実際、ゲームのすべてのロジックが書かれています!



このアルゴリズムに、目に馴染みのある2次元マトリックスの形式で結果を表示することはすでに簡単です。そして、ケーキの上のアイシングのように、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