STRING_AGG忽略PostgreSQL中的GROUP BY

时间:2018-03-16 10:28:10

标签: sql postgresql sql-order-by string-aggregation postgresql-10

我为我的问题准备了SQL Fiddle -

在2人游戏中,我将玩家及其游戏存储在2个表格中:

CREATE TABLE players (
    uid SERIAL PRIMARY KEY,
    name text NOT NULL
);

CREATE TABLE games (
    gid SERIAL PRIMARY KEY,
    player1 integer NOT NULL REFERENCES players ON DELETE CASCADE,
    player2 integer NOT NULL REFERENCES players ON DELETE CASCADE
);

字母拼贴放置移动以及生成的单词和分数存储在另外2个表中:

CREATE TABLE moves (
    mid BIGSERIAL PRIMARY KEY,
    uid integer NOT NULL REFERENCES players ON DELETE CASCADE,
    gid integer NOT NULL REFERENCES games ON DELETE CASCADE,
    played timestamptz NOT NULL,
    tiles jsonb NOT NULL
);

CREATE TABLE scores (
    mid     bigint  NOT NULL REFERENCES moves ON DELETE CASCADE,
    uid     integer NOT NULL REFERENCES players ON DELETE CASCADE,
    gid     integer NOT NULL REFERENCES games ON DELETE CASCADE,
    word    text    NOT NULL CHECK(word ~ '^[A-Z]{2,}$'),
    score   integer NOT NULL CHECK(score >= 0)
);

在这里,我在上面的表中填写了包含游戏和2名玩家(Alice和Bob)的测试数据:

INSERT INTO players (name) VALUES ('Alice'), ('Bob');
INSERT INTO games (player1, player2) VALUES (1, 2);

他们的交互动作在下面,有时单个动作可以产生2个单词:

INSERT INTO moves (uid, gid, played, tiles) VALUES
(1, 1, now() + interval '1 min', '[{"col": 7, "row": 12, "value": 3, "letter": "A"}, {"col": 8, "row": 12, "value": 10, "letter": "B"}, {"col": 9, "row": 12, "value": 1, "letter": "C"}, {"col": 10, "row": 12, "value": 2, "letter": "D"}]
'::jsonb), 
(2, 1, now() + interval '2 min', '[{"col": 7, "row": 12, "value": 3, "letter": "X"}, {"col": 8, "row": 12, "value": 10, "letter": "Y"}, {"col": 9, "row": 12, "value": 1, "letter": "Z"}]
'::jsonb), 
(1, 1, now() + interval '3 min', '[{"col": 7, "row": 12, "value": 3, "letter": "K"}, {"col": 8, "row": 12, "value": 10, "letter": "L"}, {"col": 9, "row": 12, "value": 1, "letter": "M"}, {"col": 10, "row": 12, "value": 2, "letter": "N"}]
'::jsonb), 
(2, 1, now() + interval '4 min', '[]'::jsonb), 
(1, 1, now() + interval '5 min', '[{"col": 7, "row": 12, "value": 3, "letter": "A"}, {"col": 8, "row": 12, "value": 10, "letter": "B"}, {"col": 9, "row": 12, "value": 1, "letter": "C"}, {"col": 10, "row": 12, "value": 2, "letter": "D"}]
'::jsonb), 
(2, 1, now() + interval '6 min', '[{"col": 7, "row": 12, "value": 3, "letter": "P"}, {"col": 8, "row": 12, "value": 10, "letter": "Q"}]
'::jsonb);

INSERT INTO scores (mid, uid, gid, word, score) VALUES
(1, 1, 1, 'ABCD', 40),
(2, 2, 1, 'XYZ', 30),
(2, 2, 1, 'XAB', 30),
(3, 1, 1, 'KLMN', 40),
(3, 1, 1, 'KYZ', 30),
(5, 1, 1, 'ABCD', 40),
(6, 2, 1, 'PQ', 20),
(6, 2, 1, 'PABCD', 50);

如上所示,tiles列始终是JSON对象列表。

但我只需要检索对象的单个属性:letter

所以这是我的SQL代码(用于在某个游戏中显示玩家移动的PHP脚本):

SELECT 
    STRING_AGG(x->>'letter', ''),
    STRING_AGG(y, ', ')
FROM (
    SELECT 
        JSONB_ARRAY_ELEMENTS(m.tiles) AS x,
        FORMAT('%s (%s)', s.word, s.score) AS y
    FROM moves m
    LEFT JOIN scores s
    USING (mid)
    WHERE m.gid = 1
    GROUP BY mid, s.word, s.score
    ORDER BY played ASC
) AS z;

不幸的是,它没有按预期工作。

两个STRING_AGG调用将所有内容放在一起,尽管我尝试GROUP BY mid

screenshot

有没有办法将结果字符串拆分为mid(又称移动ID)?

更新

我的问题不在于排序。我的问题是我得到2个巨大的字符串,而我期望多个字符串,每个移动id一对(aka mid)。

这是我的预期输出,是否有人建议如何实现它?

mid   "concatenated 'letter' from JSON"   "concatenated words and scores"
 1                  'ABCD'                       'ABCD (40)'
 2                  'XYZ'                        'XYZ (30), XAB (30)'               
 3                  'KLMN'                       'KLMN (40), KYZ (30)'
 5                  'ABCD'                       'ABCD (40)'
 6                  'PQ'                         'PQ (20), PABCD (50)'

更新#2:

我遵循了Laurenz的建议(谢谢!这里SQL Fiddle):

SELECT 
    mid,
    STRING_AGG(x->>'letter', '') AS tiles,
    STRING_AGG(y, ', ') AS words
FROM (
    SELECT 
        mid,
        JSONB_ARRAY_ELEMENTS(m.tiles) AS x,
        FORMAT('%s (%s)', s.word, s.score) AS y
    FROM moves m
    LEFT JOIN scores s
    USING (mid)
    WHERE m.gid = 1
) AS z
GROUP BY mid
ORDER BY mid;

但由于某种原因,"字(得分)"条目乘以:

screenshot 2

3 个答案:

答案 0 :(得分:2)

如果您希望结果按特定顺序排列,那么请使用聚合调用中的order by子句,如文档中所述:

SELECT STRING_AGG(x->>'letter', '' ORDER BY played),
       STRING_AGG(y, ', ' ORDER BY played)
FROM (SELECT JSONB_ARRAY_ELEMENTS(m.tiles) AS x,
             FORMAT('%s (%s)', s.word, s.score) AS y
      FROM moves m LEFT JOIN
           scores s
           USING (mid)
      WHERE m.gid = 1
      GROUP BY mid, s.word, s.score
     ) z;

至于使用子查询,请注意documentation

  

默认情况下,此排序未指定,但可以通过控制   在聚合调用中编写ORDER BY子句,如图所示   第4.2.7节。或者,提供已排序的输入值   子查询将通常工作。

我猜你发现了一个“通常”不适用的情况。更安全的方法是使用显式语法的方法。

编辑:

您的外部查询是一个返回一行的聚合查询。所以一切都汇集在一起​​。

如果您想要每mid行一行,则外部查询中需要GROUP BY

SELECT STRING_AGG(x->>'letter', '' ORDER BY played),
       STRING_AGG(y, ', ' ORDER BY played)
FROM (SELECT JSONB_ARRAY_ELEMENTS(m.tiles) AS x,
             FORMAT('%s (%s)', s.word, s.score) AS y
      FROM moves m LEFT JOIN
           scores s
           USING (mid)
      WHERE m.gid = 1
      GROUP BY mid, s.word, s.score
     ) z
GROUP BY mid;

答案 1 :(得分:1)

如果要按mid进行分组,则必须将该列添加到内部查询的SELECT列表中,并将GROUP BY mid添加到外部查询中。

您可以在聚合中使用DISTINCT来删除重复项:

SELECT 
    mid,
    STRING_AGG(DISTINCT x->>'letter', '') AS tiles,
    STRING_AGG(DISTINCT y, ', ') AS words
FROM (
    SELECT 
        mid,
        JSONB_ARRAY_ELEMENTS(m.tiles) AS x,
        FORMAT('%s (%s)', s.word, s.score) AS y
    FROM moves m
    LEFT JOIN scores s
    USING (mid)
    WHERE m.gid = 1
) AS z
GROUP BY mid;

中期订购;

答案 2 :(得分:0)

我已经能够通过使用CTE(这里是SQL Fiddle)摆脱DISTINCT:

WITH cte1 AS (
SELECT 
    mid,
    STRING_AGG(x->>'letter', '') AS tiles
FROM (
        SELECT 
            mid,
            JSONB_ARRAY_ELEMENTS(tiles) AS x
        FROM moves
        WHERE gid = 1
) AS z
GROUP BY mid),
cte2 AS (
        SELECT 
        mid,
        STRING_AGG(y, ', ') AS words
    FROM (
        SELECT 
            mid,
            FORMAT('%s (%s)', word, score) AS y
        FROM scores
        WHERE gid = 1
) AS z
GROUP BY mid)
SELECT 
    mid, 
    tiles, 
    words 
FROM cte1 
JOIN cte2 using (mid) 
ORDER BY mid ASC;

result