STRING_AGG 忽略 PostgreSQL 中的 GROUP BY
STRING_AGG ignores GROUP BY in PostgreSQL
我已经为我的问题准备了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 个玩家(爱丽丝和鲍勃)的测试数据填充上面的表格:
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
:
有没有办法用 mid
(又名移动 ID)拆分结果字符串?
更新:
我的问题不在于排序。我的问题是我得到 2 个巨大的字符串,而我希望有多个字符串,每个移动 ID 一对(又名 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;
但由于某种原因,"word (score)" 个条目成倍增加:
如果您想按 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;
按中期排序;
如果您想要特定顺序的结果,请在聚合调用中使用 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:
This ordering is unspecified by default, but can be controlled by
writing an ORDER BY clause within the aggregate call, as shown in
Section 4.2.7. Alternatively, supplying the input values from a sorted
subquery will usually work.
我猜你发现了 "usually" 不适用的情况。更安全的方法是使用显式语法。
编辑:
您的外部查询是 returns 一行的聚合查询。所以一切都汇集在一起。
如果你想要每个 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;
我已经能够通过使用 CTE 摆脱 DISTINCT(这里 SQL Fiddle):
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;
我已经为我的问题准备了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 个玩家(爱丽丝和鲍勃)的测试数据填充上面的表格:
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
:
有没有办法用 mid
(又名移动 ID)拆分结果字符串?
更新:
我的问题不在于排序。我的问题是我得到 2 个巨大的字符串,而我希望有多个字符串,每个移动 ID 一对(又名 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;
但由于某种原因,"word (score)" 个条目成倍增加:
如果您想按 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;
按中期排序;
如果您想要特定顺序的结果,请在聚合调用中使用 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:
This ordering is unspecified by default, but can be controlled by writing an ORDER BY clause within the aggregate call, as shown in Section 4.2.7. Alternatively, supplying the input values from a sorted subquery will usually work.
我猜你发现了 "usually" 不适用的情况。更安全的方法是使用显式语法。
编辑:
您的外部查询是 returns 一行的聚合查询。所以一切都汇集在一起。
如果你想要每个 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;
我已经能够通过使用 CTE 摆脱 DISTINCT(这里 SQL Fiddle):
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;