WHERE … IN condition and multiple columns in subquery

喜你入骨 提交于 2019-12-25 07:59:23

问题


Is it please possible to rewrite the SQL query

SELECT DISTINCT ON (uid)
        uid,
        female,
        given,
        photo,
        place
FROM words_social
WHERE uid IN (SELECT player1 FROM games)
        OR uid IN (SELECT player2 FROM games)
ORDER BY uid, stamp DESC

where first column player1 is fetched in a subquery and then column player2 is fetched from the same table?

I've searched around and it seems that a JOIN should be used here instead of the 2 subqueries, but can not figure out exactly how.

Just to give more context - below are the 2 tables in question

CREATE TABLE words_social (
        sid varchar(255) NOT NULL,

        social integer NOT NULL CHECK (0 <= social AND social <= 6),
        female integer NOT NULL CHECK (female = 0 OR female = 1),
        given  varchar(255) NOT NULL CHECK (given ~ '\S'),
        family varchar(255),
        photo  varchar(255) CHECK (photo ~* '^https?://...'),
        place  varchar(255),
        stamp  integer NOT NULL,            /* Only the most recent stamp is used */

        uid integer NOT NULL REFERENCES words_users ON DELETE CASCADE,
        PRIMARY KEY(sid, social)
);

CREATE TABLE words_games (
        gid SERIAL PRIMARY KEY,

        created timestamptz NOT NULL,
        finished timestamptz,

        player1 integer REFERENCES words_users(uid) ON DELETE CASCADE NOT NULL,
        player2 integer REFERENCES words_users(uid) ON DELETE CASCADE,

        played1 timestamptz,
        played2 timestamptz,

        mid integer /* REFERENCES words_moves */,

        score1 integer NOT NULL CHECK (score1 >= 0),
        score2 integer NOT NULL CHECK (score2 >= 0),

        hand1 varchar[7] NOT NULL,
        hand2 varchar[7] NOT NULL,
        pile  varchar[116] NOT NULL,

        letters varchar[15][15] NOT NULL,
        values integer[15][15] NOT NULL,
        bid integer NOT NULL REFERENCES words_boards ON DELETE CASCADE
);

and the actual CTE query, which works well, but I would like to optimize it:

CREATE OR REPLACE FUNCTION words_get_games(in_uid integer)
        RETURNS TABLE (
                out_gid integer,
                out_created integer,
                out_finished integer,
                out_player1 integer,
                out_player2 integer,
                out_played1 integer,
                out_played2 integer,
                out_score1 integer,
                out_score2 integer,
                out_hand1 text,
                out_hand2 text,
                out_letters varchar[15][15],
                out_values integer[15][15],
                out_bid integer,
                out_last_tiles jsonb,
                out_last_score integer,
                out_female1 integer,
                out_female2 integer,
                out_given1 varchar,
                out_given2 varchar,
                out_photo1 varchar,
                out_photo2 varchar,
                out_place1 varchar,
                out_place2 varchar
        ) AS
$func$
        WITH games AS (
                SELECT
                        g.gid,
                        EXTRACT(EPOCH FROM g.created)::int AS created,
                        EXTRACT(EPOCH FROM g.finished)::int AS finished,
                        g.player1,
                        g.player2, -- can be NULL
                        EXTRACT(EPOCH FROM g.played1)::int AS played1,
                        EXTRACT(EPOCH FROM g.played2)::int AS played2,
                        g.score1,
                        g.score2,
                        ARRAY_TO_STRING(g.hand1, '') AS hand1,
                        REGEXP_REPLACE(ARRAY_TO_STRING(g.hand2, ''), '.', '?', 'g') AS hand2,
                        g.letters,
                        g.values,
                        g.bid,
                        m.tiles AS last_tiles,
                        m.score AS last_score
                FROM words_games g LEFT JOIN words_moves m USING(mid)
                WHERE g.player1 = in_uid
                AND (g.finished IS NULL OR g.finished > CURRENT_TIMESTAMP - INTERVAL '1 day')
                UNION SELECT
                        g.gid,
                        EXTRACT(EPOCH FROM g.created)::int AS created,
                        EXTRACT(EPOCH FROM g.finished)::int AS finished,
                        g.player2 AS player1,
                        g.player1 AS player2, -- can not be NULL
                        EXTRACT(EPOCH FROM g.played2)::int AS played1,
                        EXTRACT(EPOCH FROM g.played1)::int AS played2,
                        g.score2 AS score1,
                        g.score1 AS score2,
                        ARRAY_TO_STRING(g.hand2, '') AS hand1,
                        REGEXP_REPLACE(ARRAY_TO_STRING(g.hand1, ''), '.', '?', 'g') AS hand2,
                        g.letters,
                        g.values,
                        g.bid,
                        m.tiles AS last_tiles,
                        m.score AS last_score
                FROM words_games g LEFT JOIN words_moves m USING(mid)
                WHERE g.player2 = in_uid
                AND (g.finished IS NULL OR g.finished > CURRENT_TIMESTAMP - INTERVAL '1 day')
        ),
    social AS (
        SELECT DISTINCT ON (uid)
        uid,
        female,
        given,
                photo,
                place
        FROM words_social
        WHERE uid IN (SELECT player1 FROM games)             /* How to optimize? */
                OR uid IN (SELECT player2 FROM games)
        ORDER BY uid, stamp DESC
    )
    SELECT
                g.gid,
                g.created,
                g.finished,
                g.player1,
                g.player2,
                g.played1,
                g.played2,
                g.score1,
                g.score2,
                g.hand1,
                g.hand2,
                g.letters,
                g.values,
                g.bid,
                g.last_tiles,
                g.last_score,
                s1.female,
                s2.female,
                s1.given,
                s2.given,
                s1.photo,
                s2.photo,
                s1.place,
                s2.place
    FROM games g
    LEFT OUTER JOIN social s1 ON g.player1 = s1.uid
    LEFT OUTER JOIN social s2 ON g.player2 = s2.uid;

$func$ LANGUAGE sql;

回答1:


Just to prove that you don't need the CTEs, here is your query rewritten without them.

  • had to guess some table structures, because the question was incomplete
  • a prepared statement instead of a function(the subsynstax is similiar)
  • rewrote the player1<-->player2 duplication; this can easily be handled via a CASE expression or the like
  • rewrote the most recent social record using not exists (could also be done via a row_number() OVER (partition by uid ORDER BY tstamp DESC) rn ... where rn=1
  • removed some decorative FD fields

PREPARE rewrite2(integer) AS
        SELECT g.gid
            , EXTRACT(EPOCH FROM g.created)::int AS created
            , EXTRACT(EPOCH FROM g.finished)::int AS finished
            , g.player1
            , g.player2 -- can be NULL
            , EXTRACT(EPOCH FROM g.played1)::int AS played1
            , EXTRACT(EPOCH FROM g.played2)::int AS played2
            , g.score1
            , g.score2
            , ARRAY_TO_STRING(g.hand1, '') AS hand1
            , REGEXP_REPLACE(ARRAY_TO_STRING(g.hand2, ''), '.', '?', 'g') AS hand2
            , g.letters
            , g.values
            , g.bid
            , m.tiles AS last_tiles
            , m.score AS last_score
            , s1.female AS female1
            , s1.given AS given1
            , s2.female AS female2
            , s2.given AS given2
    FROM words_games g
    LEFT JOIN words_moves m USING(mid)
    LEFT JOIN words_social s1 ON s1.uid = g.player1
        AND NOT EXISTS( SELECT *
            FROM words_social nx WHERE s1.uid = nx.uid
            AND nx.stamp > s1.stamp)
    LEFT JOIN words_social s2 ON s2.uid = g.player2
        AND NOT EXISTS( SELECT *
            FROM words_social nx WHERE s2.uid = nx.uid
            AND nx.stamp > s2.stamp)
    WHERE (g.player1 = $1 OR g.player2 = $1)
    AND (g.finished IS NULL OR g.finished > CURRENT_TIMESTAMP - INTERVAL '1 day')
        ;

EXPLAIN EXECUTE rewrite2(1);



回答2:


SELECT DISTINCT ON (uid)
       uid,
       female,
       given,
       photo,
       place
FROM words_social ws 
INNER JOIN games g ON ws.uid = g.player1 OR ws.uid = g.player2 
ORDER BY ws.uid, stamp DESC



回答3:


Use EXISTS when IN doesn't suffice.

SELECT DISTINCT ON (uid)
        uid,
        female,
        given,
        photo,
        place
FROM words_social ws
WHERE EXISTS
(
  SELECT *
  FROM games g
  WHERE ws.uid IN (g.player1, g.player2)
)
ORDER BY uid, stamp DESC


来源:https://stackoverflow.com/questions/40304011/where-in-condition-and-multiple-columns-in-subquery

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!