【问题标题】:Merging Complicated Tables合并复杂表
【发布时间】:2016-07-13 04:20:54
【问题描述】:

我正在尝试将行与“真实”事物对应的多对一关系的表合并。

我正在编写一个二十一点模拟器,它将游戏历史存储在数据库中,每次运行都会生成一组新表。这些表实际上更像是模板,因为每个游戏都有自己的一组 3 个可变表(玩家、手牌和比赛)。这是布局,其中 suff 是用户指定的用于当前运行的后缀:

 - cards
     - id INTEGER PRIMARY KEY
     - cardValue INTEGER NOT NULL 
     - suit INTEGER NOT NULL
 - players_suff
     - whichPlayer INTEGER PRIMARY KEY
     - aiType TEXT NOT NULL
 - hands_suff
     - id BIGSERIAL PRIMARY KEY
     - whichPlayer INTEGER REFERENCES players_suff(whichPlayer) *
     - whichHand BIGINT NOT NULL
     - thisCard INTEGER REFERENCES cards(id)
 - matches_suff
     - id BIGSERIAL PRIMARY KEY
     - whichGame INTEGER NOT NULL
     - dealersHand BIGINT NOT NULL
     - whichPlayer INTEGER REFERENCES players_suff(whichPlayer)
     - thisPlayersHand BIGINT NOT NULL **
     - playerResult INTEGER NOT NULL --AKA who won

只创建一张卡片表,因为它的值是恒定的。

所以在运行模拟器两次后你可能会:

hands_firstrun
players_firstrun
matches_firstrun
hands_secondrun
players_secondrun
matches_secondrun

如果你对这两个运行使用相同的 AI 参数(即 player_firstrun 和 player_secondrun 完全相同),我希望能够组合这些表。问题是我插入手的方式让这变得非常混乱:whichHand 不能是 BIGSERIAL,因为 hands_suff 行与“实际手”的关系很多:1。 match_suff 的处理方式相同,因为二十一点“游戏”实际上由一组游戏组成:每个玩家与庄家的配对。所以对于 3 名玩家,你实际上每轮有 3 行。

目前我选择表中最大的 whichHand,将其加 1,然后插入一只手的所有行。如果我要合并 2 个可能都非常大的表,我担心这种“查询和插入”会非常慢。

当我合并表时,我觉得我应该能够(完全在 SQL 中)查询 whichHand 和 whichGame 中的最大值,然后使用它们合并表,为表中每个唯一的 whichHand 和 whichGame 递增它们正在合并。

(我看到了this question,但它无法在 2 个不同的地方使用生成的 ID)。我正在使用 Postgres,如果答案是特定的就可以了。

* 遗憾的是 postgres 不允许参数化的表名,所以这必须通过手动字符串替换来完成。不是世界末日,因为该程序不是面向网络的,除了我之外没有人可能会打扰它,但是 SQL 注入漏洞并没有让我高兴。

** matches_suff(whichPlayersHand) 原本要引用 hands_suff(whichHand) 但 foreign keys must reference unique values。 whichHand 不是唯一的,因为一手牌由多行组成,每行“持有”一张牌。要查询一手牌,您选择所有那些在 whichHand 中具有相同值的行。如果不使用数组,我想不出更优雅的方式来做到这一点。

编辑:

这就是我现在拥有的:

thomas=# \dt
            List of relations
 Schema |      Name      | Type  | Owner
--------+----------------+-------+--------
 public | cards          | table | thomas
 public | hands_first    | table | thomas
 public | hands_second   | table | thomas
 public | matches_first  | table | thomas
 public | matches_second | table | thomas
 public | players_first  | table | thomas
 public | players_second | table | thomas
(7 rows)

thomas=# SELECT * FROM hands_first
thomas-# \g
 id | whichplayer | whichhand | thiscard
----+-------------+-----------+----------
  1 |           0 |         0 |        6
  2 |           0 |         0 |       63
  3 |           0 |         0 |       41
  4 |           1 |         1 |       76
  5 |           1 |         1 |       23
  6 |           0 |         2 |       51
  7 |           0 |         2 |       29
  8 |           0 |         2 |        2
  9 |           0 |         2 |       92
 10 |           0 |         2 |        6
 11 |           1 |         3 |      101
 12 |           1 |         3 |        8
(12 rows)

thomas=# SELECT * FROM hands_second
thomas-# \g
 id | whichplayer | whichhand | thiscard
----+-------------+-----------+----------
  1 |           0 |         0 |       78
  2 |           0 |         0 |       38
  3 |           1 |         1 |       24
  4 |           1 |         1 |       18
  5 |           1 |         1 |       95
  6 |           1 |         1 |       40
  7 |           0 |         2 |       13
  8 |           0 |         2 |       84
  9 |           0 |         2 |       41
 10 |           1 |         3 |       29
 11 |           1 |         3 |       34
 12 |           1 |         3 |       56
 13 |           1 |         3 |       52



thomas=# SELECT * FROM matches_first
thomas-# \g
 id | whichgame | dealershand | whichplayer | thisplayershand | playerresult
----+-----------+-------------+-------------+-----------------+--------------
  1 |         0 |           0 |           1 |               1 |            1
  2 |         1 |           2 |           1 |               3 |            2
(2 rows)

thomas=# SELECT * FROM matches_second
thomas-# \g
 id | whichgame | dealershand | whichplayer | thisplayershand | playerresult
----+-----------+-------------+-------------+-----------------+--------------
  1 |         0 |           0 |           1 |               1 |            0
  2 |         1 |           2 |           1 |               3 |            2
(2 rows)

我想把它们结合起来:

hands_combined table:
 id | whichplayer | whichhand | thiscard
----+-------------+-----------+----------
  1 |           0 |         0 |        6 --Seven of Spades
  2 |           0 |         0 |       63 --Queen of Spades
  3 |           0 |         0 |       41 --Three of Clubs
  4 |           1 |         1 |       76
  5 |           1 |         1 |       23
  6 |           0 |         2 |       51
  7 |           0 |         2 |       29
  8 |           0 |         2 |        2
  9 |           0 |         2 |       92
 10 |           0 |         2 |        6
 11 |           1 |         3 |      101
 12 |           1 |         3 |        8
 13 |           0 |         4 |       78
 14 |           0 |         4 |       38
 15 |           1 |         5 |       24
 16 |           1 |         5 |       18
 17 |           1 |         5 |       95
 18 |           1 |         5 |       40
 19 |           0 |         6 |       13
 20 |           0 |         6 |       84
 21 |           0 |         6 |       41
 22 |           1 |         7 |       29
 23 |           1 |         7 |       34
 24 |           1 |         7 |       56
 25 |           1 |         7 |       52

matches_combined table:
 id | whichgame | dealershand | whichplayer | thisplayershand | playerresult
----+-----------+-------------+-------------+-----------------+--------------
  1 |         0 |           0 |           1 |               1 |            1
  2 |         1 |           2 |           1 |               3 |            2
  3 |         2 |           4 |           1 |               5 |            0
  4 |         3 |           6 |           1 |               7 |            2

"thiscard" 的每个值都表示 [1..104]--52 范围内的扑克牌,并带有一个额外的位来表示它是面朝上还是面朝下。由于空间原因,我没有发布实际的表格。 所以玩家 0(又名庄家)在第一局中有一手牌(黑桃七,空间皇后,梅花三)。

【问题讨论】:

  • 有人介意解释否决票以便我编辑问题吗?
  • 你能写出你拥有的数据的例子,以及你需要的数据吗?你也可以给出你现有的查询。
  • 生成的表格应该看起来像什么(也就是 combine 的结果)?顺便说一句,你为什么想出一个表格动物园而不添加game_id(你可以使用你当前的suff值作为那里的值)来区分相关的条目。目前的方式是对SQL数据库逻辑的严重滥用。
  • 正如 rpy 也提到的,这是一种非常奇怪的数据库结构方式。您通过在表名中存储记录信息(游戏)来模糊结构和数据之间的界限。正如我所看到的,“suff”的信息会更合适地存储在可以正确查询的列中。
  • @AnthonyE 我认为这会引发区分哪些球员参与每场比赛的问题。当前方法的优点(尽管相信我,我希望有更好的方法)是匹配表代表一组球员的比赛,这些球员可以通过球员表中的 ID 引用。下一轮可能会有其他玩家(以及不同数量的玩家)。编辑:重要的原因是因为重点是设置不同类型的 AI 相互对抗。如果这变得混乱,整个事情就会崩溃。

标签: sql database postgresql merge


【解决方案1】:

您的数据库结构不是很好,而且我确信它不是可扩展的方法,即动态创建表。创建物理表而不是使用现有结构存在性能缺陷。如果可以的话,我建议你重构你的数据库结构。

但是,您可以使用 UNION 运算符来合并您的数据。

【讨论】:

    【解决方案2】:

    我认为您没有按照预期的方式使用 PostgreSQL,而且您的表设计可能不适合您想要实现的目标。虽然很难理解您希望您的解决方案实现什么,但我写了这个,它似乎只使用少数表解决了您想要的所有问题,以及返回记录集以模拟您对单个运行的要求的函数。我使用枚举和复杂类型来说明您可能希望从 PostgreSQL 的强大功能中利用的一些特性。

    另外,我不确定参数化表名是什么(我从未在任何 RDBMS 中看到过类似的名称),但 PostgreSQL 确实允许一些非常合适的东西:记录集返回函数。

    CREATE TYPE card_value AS ENUM ('1', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K');
    CREATE TYPE card_suit AS ENUM ('Clubs', 'Diamonds', 'Hearts', 'Spades');
    CREATE TYPE card AS (value card_value, suit card_suit, face_up bool);
    
    CREATE TABLE runs (
      run_id bigserial NOT NULL PRIMARY KEY,
      run_date timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP
      );
    
    CREATE TABLE players (
      run_id bigint NOT NULL REFERENCES runs,
      player_no int NOT NULL, -- 0 can be assumed as always the dealer
      ai_type text NOT NULL,
      PRIMARY KEY (run_id, player_no)
      );
    
    CREATE TABLE matches (
      run_id bigint NOT NULL REFERENCES runs,
      match_no int NOT NULL,
      PRIMARY KEY (run_id, match_no)
      );
    
    CREATE TABLE hands (
      hand_id bigserial NOT NULL PRIMARY KEY,
      run_id bigint NOT NULL REFERENCES runs,
      match_no int NOT NULL,
      hand_no int NOT NULL,
      player_no int NOT NULL,
      UNIQUE (run_id, match_no, hand_no),
      FOREIGN KEY (run_id, match_no) REFERENCES matches,
      FOREIGN KEY (run_id, player_no) REFERENCES players
      );
    
    CREATE TABLE deals (
      deal_id bigserial NOT NULL PRIMARY KEY,
      hand_id bigint NOT NULL REFERENCES hands,
      card card NOT NULL
      );
    
    CREATE OR REPLACE FUNCTION players(int) RETURNS SETOF players AS $$
      SELECT * FROM players WHERE run_id = $1 ORDER BY player_no;
    $$ LANGUAGE SQL;
    
    CREATE OR REPLACE FUNCTION matches(int) RETURNS SETOF matches AS $$
      SELECT * FROM matches WHERE run_id = $1 ORDER BY match_no;
    $$ LANGUAGE SQL;
    
    CREATE OR REPLACE FUNCTION hands(int) RETURNS SETOF hands AS $$
      SELECT * FROM hands WHERE run_id = $1 ORDER BY match_no, hand_no;
    $$ LANGUAGE SQL;
    
    CREATE OR REPLACE FUNCTION hands(int, int) RETURNS SETOF hands AS $$
      SELECT * FROM hands WHERE run_id = $1 AND match_no = $2 ORDER BY hand_no;
    $$ LANGUAGE SQL;
    
    CREATE OR REPLACE FUNCTION winner_player (int, int) RETURNS int AS $$
      SELECT player_no
      FROM hands
      WHERE run_id = $1 AND match_no = $2
      ORDER BY hand_no DESC
      LIMIT 1
    $$ LANGUAGE SQL;
    
    CREATE OR REPLACE FUNCTION next_player_no (int) RETURNS int AS $$
      SELECT CASE WHEN EXISTS (SELECT 1 FROM runs WHERE run_id = $1) THEN
             COALESCE((SELECT MAX(player_no) FROM players WHERE run_id = $1), 0) + 1 END
    $$ LANGUAGE SQL;
    
    CREATE OR REPLACE FUNCTION next_match_no (int) RETURNS int AS $$
      SELECT CASE WHEN EXISTS (SELECT 1 FROM runs WHERE run_id = $1) THEN
             COALESCE((SELECT MAX(match_no) FROM matches WHERE run_id = $1), 0) + 1 END
    $$ LANGUAGE SQL;
    
    CREATE OR REPLACE FUNCTION next_hand_no (int) RETURNS int AS $$
      SELECT CASE WHEN EXISTS (SELECT 1 FROM runs WHERE run_id = $1) THEN
             COALESCE((SELECT MAX(hand_no) + 1 FROM hands WHERE run_id = $1), 0) END
    $$ LANGUAGE SQL;
    
    CREATE OR REPLACE FUNCTION card_to_int (card) RETURNS int AS $$
      SELECT ((SELECT enumsortorder::int-1 FROM pg_enum WHERE enumtypid = 'card_suit'::regtype AND enumlabel = ($1).suit::name) * 13 +
              (SELECT enumsortorder::int-1 FROM pg_enum WHERE enumtypid = 'card_value'::regtype AND enumlabel = ($1).value::name) + 1) *
             CASE WHEN ($1).face_up THEN 2 ELSE 1 END
    $$ LANGUAGE SQL; -- SELECT card_to_int(('3', 'Spades', false))
    
    CREATE OR REPLACE FUNCTION int_to_card (int) RETURNS card AS $$
      SELECT ((SELECT enumlabel::card_value FROM pg_enum WHERE enumtypid = 'card_value'::regtype AND enumsortorder = ((($1-1)%13)+1)::real),
              (SELECT enumlabel::card_suit  FROM pg_enum WHERE enumtypid = 'card_suit'::regtype  AND enumsortorder = (((($1-1)/13)::int%4)+1)::real),
              $1 > (13*4))::card
    $$ LANGUAGE SQL; -- SELECT i, int_to_card(i) FROM generate_series(1, 13*4*2) i
    
    CREATE OR REPLACE FUNCTION deal_cards(int, int, int, int[]) RETURNS TABLE (player_no int, hand_no int, card card) AS $$
      WITH
        hand AS (
          INSERT INTO hands (run_id, match_no, player_no, hand_no)
          VALUES ($1, $2, $3, next_hand_no($1))
          RETURNING hand_id, player_no, hand_no),
        mydeals AS (
          INSERT INTO deals (hand_id, card)
          SELECT hand_id, int_to_card(card_id)::card AS card
          FROM hand, UNNEST($4) card_id
          RETURNING hand_id, deal_id, card
          )
        SELECT h.player_no, h.hand_no, d.card
        FROM hand h, mydeals d
    $$ LANGUAGE SQL;
    
    CREATE OR REPLACE FUNCTION deals(int) RETURNS TABLE (deal_id bigint, hand_no int, player_no int, card int) AS $$
      SELECT d.deal_id, h.hand_no, h.player_no, card_to_int(d.card)
      FROM hands h
      JOIN deals d ON (d.hand_id = h.hand_id)
      WHERE h.run_id = $1
      ORDER BY d.deal_id;
    $$ LANGUAGE SQL;
    
    INSERT INTO runs DEFAULT VALUES; -- Add first run
    INSERT INTO players VALUES (1, 0, 'Dealer'); -- dealer always zero
    INSERT INTO players VALUES (1, next_player_no(1), 'Player 1');
    
    INSERT INTO matches VALUES (1, next_match_no(1)); -- First match
    SELECT * FROM deal_cards(1, 1, 0, ARRAY[6, 63, 41]);
    SELECT * FROM deal_cards(1, 1, 1, ARRAY[76, 23]);
    SELECT * FROM deal_cards(1, 1, 0, ARRAY[51, 29, 2, 92, 6]);
    SELECT * FROM deal_cards(1, 1, 1, ARRAY[101, 8]);
    
    INSERT INTO matches VALUES (1, next_match_no(1)); -- Second match
    SELECT * FROM deal_cards(1, 2, 0, ARRAY[78, 38]);
    SELECT * FROM deal_cards(1, 2, 1, ARRAY[24, 18, 95, 40]);
    SELECT * FROM deal_cards(1, 2, 0, ARRAY[13, 84, 41]);
    SELECT * FROM deal_cards(1, 2, 1, ARRAY[29, 34, 56, 52]);
    
    SELECT * FROM deals(1); -- This is the output you need (hands_combined table)
    
    -- This view can be used to retrieve the list of all winning hands
    CREATE OR REPLACE VIEW winning_hands AS
      SELECT DISTINCT ON (run_id, match_no) *
      FROM hands
      ORDER BY run_id, match_no, hand_no DESC;
    
    SELECT * FROM winning_hands;
    

    【讨论】:

    • 我认为一组可立即运行的功能示例正是操作所需要的。这个问题的答案很难“正确”,但这对于有类似问题的人来说绝对是一个很好的参考。
    【解决方案3】:

    使用 UNION 运算符不起作用吗?

    对于手的关系:

    SELECT * FROM hands_first
    UNION ALL
    SELECT * FROM hands_second
    

    对于匹配关系:

    SELECT * FROM matches_first
    UNION ALL
    SELECT * FROM matches_second
    

    作为更长期的解决方案,我会考虑重组数据库,因为使用此架构很快就会变得无法管理。为什么不通过引入游戏桌来提高标准化?

    换句话说,游戏有很多比赛比赛每场比赛都有很多玩家玩家每场比赛都有很多手牌。

    我建议在纸上绘制实体关系的 UML (http://dawgsquad.googlecode.com/hg/docs/database_images/Database_Model_Diagram(Title).png),然后改进架构以便可以使用普通 SQL 运算符进行查询。

    希望这会有所帮助。

    编辑:

    在这种情况下,您可以使用rownumber() PG 函数对两个表的并集使用子查询来表示行号:

    SELECT 
      row_number() AS id,
      whichplayer,
      whichhand,
      thiscard
    FROM
    (
      SELECT * FROM hands_first
      UNION ALL
      SELECT * FROM hands_second
    );
    

    同样的原则也适用于匹配表。显然,即使是少量的表,这也不能很好地扩展,因此会优先考虑规范化您的架构。

    一些PG功能的文档:http://www.postgresql.org/docs/current/interactive/functions-window.html

    【讨论】:

    • 问题在于 whichHand 的值重叠。我需要第二个表中的 whichHand 列来继续第一个表开始的序列。
    • (你有一个很好的重构数据库的观点。我会研究一下)
    • 查看我的编辑。没有亲自测试过,但它应该可以工作。
    【解决方案4】:

    要使用两个表的所有行构建新表,请执行以下操作:

    CREATE TABLE hands AS 
      select 1 as hand, id, whichplayer, whichhand, thiscard
      from hands_first
      union all
      select 2 as hand, id, whichplayer, whichhand, thiscard
      from hands_second
    

    之后,要插入新匹配的数据,创建从当前最后一个开始 + 1 的序列

    CREATE SEQUENCE matche START 3;
    

    在插入前读取序列值,并在插入中使用:

    SELECT nextval('matche');
    

    【讨论】:

      猜你喜欢
      • 2012-03-26
      • 2011-11-21
      • 2019-08-18
      • 2013-10-21
      • 2013-05-05
      • 2020-09-15
      • 1970-01-01
      • 2012-08-19
      • 2021-05-27
      相关资源
      最近更新 更多