Good evening, in PostgreSQL 11 I have a table holding player moves (could be: 'play', 'swap', 'skip', ...) in a word game:
# \d words_moves; Table "public.words_moves" Column | Type | Collation | Nullable | Default ---------+--------------------------+-----------+----------+------------------------------------------ mid | bigint | | not null | nextval('words_moves_mid_seq'::regclass) action | text | | not null | gid | integer | | not null | uid | integer | | not null | played | timestamp with time zone | | not null | tiles | jsonb | | | score | integer | | | letters | text | | | hand | text | | | puzzle | boolean | | not null | false Indexes: "words_moves_pkey" PRIMARY KEY, btree (mid) "words_moves_gid_played_idx" btree (gid, played DESC) "words_moves_uid_action_played_idx" btree (uid, action, played) "words_moves_uid_idx" btree (uid) Check constraints: "words_moves_score_check" CHECK (score >= 0) Foreign-key constraints: "words_moves_gid_fkey" FOREIGN KEY (gid) REFERENCES words_games(gid) ON DELETE CASCADE "words_moves_uid_fkey" FOREIGN KEY (uid) REFERENCES words_users(uid) ON DELETE CASCADE Referenced by: TABLE "words_scores" CONSTRAINT "words_scores_mid_fkey" FOREIGN KEY (mid) REFERENCES words_moves(mid) ON DELETE CASCADE When I search for "interesting moves" with score higher than 90 or all 7 tiles played, then the query takes a bit longer: EXPLAIN ANALYZE SELECT TO_CHAR(played, 'Mon YYYY'), COUNT(NULLIF(puzzle, FALSE)) OVER (PARTITION BY TO_CHAR(played, 'Mon YYYY')), puzzle, mid, MD5(mid || 'cookie'), gid, score FROM words_moves WHERE action = 'play' AND LENGTH(hand) = 7 AND (LENGTH(letters) = 7 OR score > 90) AND played > CURRENT_TIMESTAMP - interval '1 year' AND played < CURRENT_TIMESTAMP - interval '3 day' ORDER BY played DESC; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Sort (cost=168533.19..168533.30 rows=44 width=97) (actual time=2126.433..2126.541 rows=1036 loops=1) Sort Key: played DESC Sort Method: quicksort Memory: 194kB -> WindowAgg (cost=168530.56..168531.99 rows=44 width=97) (actual time=2122.991..2125.593 rows=1036 loops=1) -> Sort (cost=168530.56..168530.67 rows=44 width=57) (actual time=2122.934..2123.049 rows=1036 loops=1) Sort Key: (to_char(played, 'Mon YYYY'::text)) Sort Method: quicksort Memory: 129kB -> Gather (cost=1000.00..168529.36 rows=44 width=57) (actual time=287.461..2121.445 rows=1036 loops=1) Workers Planned: 2 Workers Launched: 2 -> Parallel Seq Scan on words_moves (cost=0.00..167524.96 rows=18 width=57) (actual time=207.234..2115.686 rows=345 loops=3) Filter: ((action = 'play'::text) AND (length(hand) = 7) AND ((length(letters) = 7) OR (score > 90)) AND (played > (CURRENT_TIMESTAMP - '1 year'::interval)) AND (played < (CURRENT_TIMESTAMP - '3 days'::interval))) Rows Removed by Filter: 1088073 Planning Time: 0.383 ms Execution Time: 2126.922 ms (15 rows) Here the link: https://explain.depesz.com/s/s3HF So I add an index with - CREATE INDEX ON words_moves(length(letters), score, action); That is because I have several such queries looking for interesting 'play' moves with high score or all 7 tiles played. This helps a bit: QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Sort (cost=126487.34..126487.45 rows=44 width=97) (actual time=219.649..219.744 rows=1036 loops=1) Sort Key: played DESC Sort Method: quicksort Memory: 194kB -> WindowAgg (cost=126484.71..126486.14 rows=44 width=97) (actual time=216.180..218.680 rows=1036 loops=1) -> Sort (cost=126484.71..126484.82 rows=44 width=57) (actual time=216.125..216.226 rows=1036 loops=1) Sort Key: (to_char(played, 'Mon YYYY'::text)) Sort Method: quicksort Memory: 129kB -> Bitmap Heap Scan on words_moves (cost=83892.69..126483.50 rows=44 width=57) (actual time=209.447..214.879 rows=1036 loops=1) Recheck Cond: (((length(letters) = 7) AND (action = 'play'::text)) OR ((score > 90) AND (action = 'play'::text))) Filter: ((length(hand) = 7) AND (played > (CURRENT_TIMESTAMP - '1 year'::interval)) AND (played < (CURRENT_TIMESTAMP - '3 days'::interval))) Rows Removed by Filter: 468 Heap Blocks: exact=1495 -> BitmapOr (cost=83892.69..83892.69 rows=15280 width=0) (actual time=209.083..209.083 rows=0 loops=1) -> Bitmap Index Scan on words_moves_length_score_action_idx (cost=0.00..419.69 rows=14838 width=0) (actual time=2.040..2.040 rows=912 loops=1) Index Cond: ((length(letters) = 7) AND (action = 'play'::text)) -> Bitmap Index Scan on words_moves_length_score_action_idx (cost=0.00..83472.98 rows=442 width=0) (actual time=207.038..207.038 rows=608 loops=1) Index Cond: ((score > 90) AND (action = 'play'::text)) Planning Time: 1.102 ms Execution Time: 220.676 ms (19 rows) Here the resulting link: https://explain.depesz.com/s/Pwbt Then I drop that index and create another one: CREATE INDEX ON words_moves(length(letters), score) WHERE action = 'play'; And it seems to have a better performance on the query: QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Sort (cost=97687.61..97687.72 rows=44 width=97) (actual time=175.832..175.927 rows=1036 loops=1) Sort Key: played DESC Sort Method: quicksort Memory: 194kB -> WindowAgg (cost=97684.98..97686.41 rows=44 width=97) (actual time=172.443..174.937 rows=1036 loops=1) -> Sort (cost=97684.98..97685.09 rows=44 width=57) (actual time=172.390..172.490 rows=1036 loops=1) Sort Key: (to_char(played, 'Mon YYYY'::text)) Sort Method: quicksort Memory: 129kB -> Bitmap Heap Scan on words_moves (cost=55092.96..97683.78 rows=44 width=57) (actual time=165.420..171.164 rows=1036 loops=1) Recheck Cond: (((length(letters) = 7) AND (action = 'play'::text)) OR ((score > 90) AND (action = 'play'::text))) Filter: ((length(hand) = 7) AND (played > (CURRENT_TIMESTAMP - '1 year'::interval)) AND (played < (CURRENT_TIMESTAMP - '3 days'::interval))) Rows Removed by Filter: 468 Heap Blocks: exact=1495 -> BitmapOr (cost=55092.96..55092.96 rows=15280 width=0) (actual time=165.036..165.036 rows=0 loops=1) -> Bitmap Index Scan on words_moves_length_score_idx (cost=0.00..275.71 rows=14838 width=0) (actual time=0.620..0.620 rows=912 loops=1) Index Cond: (length(letters) = 7) -> Bitmap Index Scan on words_moves_length_score_idx (cost=0.00..54817.23 rows=442 width=0) (actual time=164.413..164.413 rows=608 loops=1) Index Cond: (score > 90) Planning Time: 0.948 ms Execution Time: 177.604 ms (19 rows) Here the resulting link: https://explain.depesz.com/s/pmCw I still wonder, what am I missing, what could be improved there? Does anybody please have a hint? Thanks Alex