限制在边界上的不重叠矩形

Sno*_*all 5 postgresql database-design referential-integrity spatial exclusion-constraint

我正在尝试对电路板上零件的放置进行建模。没有任何有意义的限制,我的基本架构如下所示:

create table part (
    part_id bigserial primary key,
    name text not null,
    width double precision not null,
    height double precision not null
);
create table board (
    board_id bigserial primary key,
    width double precision not null,
    height double precision not null
);
create table board_part (
    board_id bigint not null references board,
    part_id bigint not null references part,
    position point not null
);
Run Code Online (Sandbox Code Playgroud)

SQL Fiddle可视化

对于bb2any board_parts,我想强制执行以下约束:

  1. b位于黑板上:

    box(b.position, point(b.part.width,b.part.height))
        <@ box(point(0,0), point(b.board.width,b.board.height))
    
    Run Code Online (Sandbox Code Playgroud)
  2. bb2如果它们位于同一块板上,则不会重叠:

    b.board_id != b2.board_id or
    not (box(b.position, point(b.part.width,b.part.height))
            && box(b2.position, point(b2.part.width,b2.part.height)))
    
    Run Code Online (Sandbox Code Playgroud)

我怎样才能实现这一点(没有太多的数据重复)?改变架构就好了。

这是我的最佳尝试(SQL Fiddle),从埃尔文对我之前问题的回答中汲取灵感。它强制执行我想要的约束,但表中有很多重复数据board_part。我想我可以编写一个函数来自动填充board_widthboard_heightpart_widthpart_height字段,但周围有这么多重复数据仍然感觉不对。另外,键入width/height字段感觉就像黑客。

Erw*_*ter 5

基本答案

\n

我建议使用几何类型box排除约束(Postgres 9.2+)。应该是完美的解决您的问题。它隐式创建一个也支持某些查询的 GiST 索引。

\n

将其与 equal on 结合起来board_id以允许在一个表中包含多个板。您将需要附加模块btree_gist。每个数据库一次:

\n
CREATE EXTENSION btree_gist;\n
Run Code Online (Sandbox Code Playgroud)\n

要将零件限制在板上,请添加CHECK约束。您需要板子的限制盒board_part(冗余)。可能看起来像这样:

\n
CREATE TABLE board_part (\n  board_id bigint NOT NULL REFERENCES board\n, board_box box NOT NULL\n, part_id bigint NOT NULL REFERENCES part\n, part_box box NOT NULL\n, EXCLUDE USING gist (board_id WITH =, part_box WITH &&)\n, CHECK (part_box <@ board_box)\n);\n
Run Code Online (Sandbox Code Playgroud)\n

&&..“重叠”运算符
\n<@ ..“包含”运算符

\n

缺点:您需要表中每个零件和盒子的尺寸board_part多余的每个零件和盒子的尺寸。

\n

高级答案

\n

为了避免冗余存储,我更喜欢这个新想法:FakeIMMUTABLE函数返回 id 的框并在这些函数上构建约束。您的表格只需使用原始设计的列即可。

\n

我进行了完整的测试来验证它是否有效。

\n

完整架构

\n

基表:

\n
CREATE TABLE part (\n  part_id serial PRIMARY KEY\n, part text NOT NULL\n, wide int NOT NULL\n, high int NOT NULL\n);\n\nCREATE TABLE board (\n  board_id serial PRIMARY KEY\n, board text NOT NULL\n, wide int NOT NULL\n, high int NOT NULL\n);\n
Run Code Online (Sandbox Code Playgroud)\n

IMMUTABLE功能:

\n
CREATE OR REPLACE FUNCTION f_boardbox(_board_id int)\n  RETURNS box\n  LANGUAGE sql IMMUTABLE AS\n\'SELECT box(point(0,0), point (b.wide, b.high))\n FROM   public.board b WHERE board_id = $1\'; \n\nCREATE OR REPLACE FUNCTION f_partbox(_part_id int, _wide int, _high int)\n  RETURNS box\n  LANGUAGE sql IMMUTABLE AS\n\'SELECT box(point($2,$3), point($2 + p.wide, $3 + p.high))\n FROM   public.part p WHERE part_id = $1\';\n
Run Code Online (Sandbox Code Playgroud)\n

替换public为表的实际架构。
\n在 Postgres 9.6 或更高版本中添加PARALLEL SAFE. 看:

\n\n

主表:

\n
CREATE TABLE board_part (\n  board_id int NOT NULL REFERENCES board\n, part_id int NOT NULL REFERENCES part\n, wide int NOT NULL\n, high int NOT NULL\n, EXCLUDE USING gist (board_id WITH =, f_partbox(part_id, wide, high) WITH &&)\n, CHECK (f_partbox(part_id, wide, high) <@ f_boardbox(board_id))\n);\n
Run Code Online (Sandbox Code Playgroud)\n

当然,如果您更新 aboard_idpart_id正在使用的维度,则部分索引和/或约束无效。您需要重新创建任何索引或约束,其基础是以下承诺:IMMUTABLE函数返回值永不改变的承诺的任何索引或约束。看:

\n\n

但是,您可以使用触发器来避免这种昂贵的操作,以仅更新受影响的行:

\n

触发器

\n
CREATE OR REPLACE FUNCTION f_board_upaft()\n  RETURNS trigger\n  LANGUAGE plpgsql AS\n$func$\nBEGIN\n   UPDATE board_part\n   SET    board_id = board_id        -- enough to trigger CHECK constraint\n   WHERE  board_id = NEW.board_id;   -- limit to relevant rows\n\n   RETURN NULL;\nEND\n$func$;\n\nCREATE TRIGGER board_upaft\nAFTER UPDATE OF wide, high ON board  -- limit to relevant columns\nFOR EACH ROW EXECUTE PROCEDURE f_board_upaft();\n\n\nCREATE OR REPLACE FUNCTION f_part_upaft()\n  RETURNS trigger\n  LANGUAGE plpgsql AS\n$func$\nBEGIN\n   UPDATE board_part\n   SET    part_id = 0\n   WHERE  part_id = NEW.part_id;\n\n   UPDATE board_part\n   SET    part_id = NEW.part_id\n   WHERE  part_id = 0;               -- enough to update EXCL. constraint\n\n   RETURN NULL;\nEND\n$func$;\n\nCREATE TRIGGER part_upaft\nAFTER UPDATE OF wide, high ON part\nFOR EACH ROW EXECUTE PROCEDURE f_part_upaft();\n
Run Code Online (Sandbox Code Playgroud)\n

part_id = 0是表中的特殊行part。触发器需要。

\n

测试数据:

\n
INSERT INTO board (board, wide, high)\nVALUES (\'b1010\',10,10), (\'b2030\',20, 30);\n\nINSERT INTO part (part_id, part, wide, high)\nVALUES (0,\'p0\',0,0);          -- special row needed for trigger!\n\nINSERT INTO part (part, wide, high)\nVALUES (\'p11\',1,1), (\'p33\',3,3);\n\nINSERT INTO board_part (board_id, part_id, wide, high)\nVALUES (1,1,3,3)\n     , (1,1,5,5)   -- no overlap, inside board\n     , (2,1,3,3);  -- different board, no overlap\n
Run Code Online (Sandbox Code Playgroud)\n

测试

\n

如果触发器和约束完成其工作,这些必须引发异常:

\n
UPDATE part SET wide = 6 , high = 6\nWHERE  part_id = 1;            -- violates EXCL. & CHECK constraint\n\nUPDATE part SET wide = 3 , high = 3\nWHERE  part_id = 1;            -- violates EXCL. constraint\n\nUPDATE board SET wide = 2 , high = 2\nWHERE  board_id = 1;            -- violates CHECK constraint\n
Run Code Online (Sandbox Code Playgroud)\n

db<>在这里小提琴
\n旧的sqlfiddle

\n