Skip to content

Commit e8ec19a

Browse files
committed
Add pg_stash_advice contrib module.
This module allows plan advice strings to be provided automatically from an in-memory advice stash. Advice stashes are stored in dynamic shared memory and must be recreated and repopulated after a server restart. If pg_stash_advice.stash_name is set to the name of an advice stash, and if query identifiers are enabled, the query identifier for each query will be looked up in the advice stash and the associated advice string, if any, will be used each time that query is planned. Reviewed-by: Lukas Fittl <lukas@fittl.com> Reviewed-by: Alexandra Wang <alexandra.wang.oss@gmail.com> Reviewed-by: David G. Johnston <david.g.johnston@gmail.com> Reviewed-by: Jakub Wartak <jakub.wartak@enterprisedb.com> Discussion: http://postgr.es/m/CA+TgmoaeNuHXQ60P3ZZqJLrSjP3L1KYokW9kPfGbWDyt+1t=Ng@mail.gmail.com
1 parent 404a17c commit e8ec19a

18 files changed

Lines changed: 1869 additions & 0 deletions

contrib/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ SUBDIRS = \
3636
pg_overexplain \
3737
pg_plan_advice \
3838
pg_prewarm \
39+
pg_stash_advice \
3940
pg_stat_statements \
4041
pg_surgery \
4142
pg_trgm \

contrib/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ subdir('pg_overexplain')
5151
subdir('pg_plan_advice')
5252
subdir('pg_prewarm')
5353
subdir('pgrowlocks')
54+
subdir('pg_stash_advice')
5455
subdir('pg_stat_statements')
5556
subdir('pgstattuple')
5657
subdir('pg_surgery')

contrib/pg_stash_advice/Makefile

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# contrib/pg_stash_advice/Makefile
2+
3+
MODULE_big = pg_stash_advice
4+
OBJS = \
5+
$(WIN32RES) \
6+
pg_stash_advice.o \
7+
stashfuncs.o
8+
9+
EXTENSION = pg_stash_advice
10+
DATA = pg_stash_advice--1.0.sql
11+
PGFILEDESC = "pg_stash_advice - store and automatically apply plan advice"
12+
13+
REGRESS = pg_stash_advice pg_stash_advice_utf8
14+
EXTRA_INSTALL = contrib/pg_plan_advice
15+
16+
ifdef USE_PGXS
17+
PG_CPPFLAGS = -I$(includedir_server)/extension
18+
PG_CONFIG = pg_config
19+
PGXS := $(shell $(PG_CONFIG) --pgxs)
20+
include $(PGXS)
21+
else
22+
PG_CPPFLAGS = -I$(top_srcdir)/contrib/pg_plan_advice
23+
subdir = contrib/pg_stash_advice
24+
top_builddir = ../..
25+
include $(top_builddir)/src/Makefile.global
26+
include $(top_srcdir)/contrib/contrib-global.mk
27+
endif
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
CREATE EXTENSION pg_stash_advice;
2+
SET compute_query_id = on;
3+
SET max_parallel_workers_per_gather = 0;
4+
-- Helper: extract query identifier from EXPLAIN VERBOSE output.
5+
CREATE OR REPLACE FUNCTION get_query_id(query_text text) RETURNS bigint
6+
LANGUAGE plpgsql AS $$
7+
DECLARE
8+
line text;
9+
qid bigint;
10+
BEGIN
11+
FOR line IN EXECUTE 'EXPLAIN (VERBOSE, FORMAT TEXT) ' || query_text
12+
LOOP
13+
IF line ~ 'Query Identifier:' THEN
14+
qid := regexp_replace(line, '.*Query Identifier:\s*(-?\d+).*', '\1')::bigint;
15+
RETURN qid;
16+
END IF;
17+
END LOOP;
18+
RAISE EXCEPTION 'Query Identifier not found in EXPLAIN output';
19+
END;
20+
$$;
21+
CREATE TABLE aa_dim1 (id integer primary key, dim1 text, val1 int)
22+
WITH (autovacuum_enabled = false);
23+
INSERT INTO aa_dim1 (id, dim1, val1)
24+
SELECT g, 'some filler text ' || g, (g % 3) + 1
25+
FROM generate_series(1,100) g;
26+
VACUUM ANALYZE aa_dim1;
27+
CREATE TABLE aa_dim2 (id integer primary key, dim2 text, val2 int)
28+
WITH (autovacuum_enabled = false);
29+
INSERT INTO aa_dim2 (id, dim2, val2)
30+
SELECT g, 'some filler text ' || g, (g % 7) + 1
31+
FROM generate_series(1,1000) g;
32+
VACUUM ANALYZE aa_dim2;
33+
CREATE TABLE aa_fact (
34+
id int primary key,
35+
dim1_id integer not null references aa_dim1 (id),
36+
dim2_id integer not null references aa_dim2 (id)
37+
) WITH (autovacuum_enabled = false);
38+
INSERT INTO aa_fact
39+
SELECT g, (g%100)+1, (g%100)+1 FROM generate_series(1,100000) g;
40+
VACUUM ANALYZE aa_fact;
41+
-- Get the query identifier.
42+
SELECT get_query_id($$
43+
SELECT * FROM aa_fact f LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
44+
LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
45+
WHERE val1 = 1 AND val2 = 1;
46+
$$) AS qid \gset
47+
-- Create an advice stash and point pg_stash_advice at it.
48+
SELECT pg_create_advice_stash('regress_stash');
49+
pg_create_advice_stash
50+
------------------------
51+
52+
(1 row)
53+
54+
SET pg_stash_advice.stash_name = 'regress_stash';
55+
-- Run our test query for the first time with no stashed advice.
56+
EXPLAIN (COSTS OFF)
57+
SELECT * FROM aa_fact f LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
58+
LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
59+
WHERE val1 = 1 AND val2 = 1;
60+
QUERY PLAN
61+
------------------------------------------
62+
Hash Join
63+
Hash Cond: (f.dim1_id = d1.id)
64+
-> Hash Join
65+
Hash Cond: (f.dim2_id = d2.id)
66+
-> Seq Scan on aa_fact f
67+
-> Hash
68+
-> Seq Scan on aa_dim2 d2
69+
Filter: (val2 = 1)
70+
-> Hash
71+
-> Seq Scan on aa_dim1 d1
72+
Filter: (val1 = 1)
73+
(11 rows)
74+
75+
-- Force an index scan on dim1
76+
SELECT pg_set_stashed_advice('regress_stash', :'qid',
77+
'INDEX_SCAN(d1 aa_dim1_pkey)');
78+
pg_set_stashed_advice
79+
-----------------------
80+
81+
(1 row)
82+
83+
EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
84+
LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
85+
LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
86+
WHERE val1 = 1 AND val2 = 1;
87+
QUERY PLAN
88+
---------------------------------------------------------
89+
Hash Join
90+
Hash Cond: (f.dim1_id = d1.id)
91+
-> Hash Join
92+
Hash Cond: (f.dim2_id = d2.id)
93+
-> Seq Scan on aa_fact f
94+
-> Hash
95+
-> Seq Scan on aa_dim2 d2
96+
Filter: (val2 = 1)
97+
-> Hash
98+
-> Index Scan using aa_dim1_pkey on aa_dim1 d1
99+
Filter: (val1 = 1)
100+
Supplied Plan Advice:
101+
INDEX_SCAN(d1 aa_dim1_pkey) /* matched */
102+
(13 rows)
103+
104+
-- Force an alternative join order
105+
SELECT pg_set_stashed_advice('regress_stash', :'qid',
106+
'join_order(f d1 d2)');
107+
pg_set_stashed_advice
108+
-----------------------
109+
110+
(1 row)
111+
112+
EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
113+
LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
114+
LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
115+
WHERE val1 = 1 AND val2 = 1;
116+
QUERY PLAN
117+
------------------------------------------
118+
Hash Join
119+
Hash Cond: (f.dim2_id = d2.id)
120+
-> Hash Join
121+
Hash Cond: (f.dim1_id = d1.id)
122+
-> Seq Scan on aa_fact f
123+
-> Hash
124+
-> Seq Scan on aa_dim1 d1
125+
Filter: (val1 = 1)
126+
-> Hash
127+
-> Seq Scan on aa_dim2 d2
128+
Filter: (val2 = 1)
129+
Supplied Plan Advice:
130+
JOIN_ORDER(f d1 d2) /* matched */
131+
(13 rows)
132+
133+
-- Force an alternative join strategy
134+
SELECT pg_set_stashed_advice('regress_stash', :'qid',
135+
'NESTED_LOOP_PLAIN(d1)');
136+
pg_set_stashed_advice
137+
-----------------------
138+
139+
(1 row)
140+
141+
EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
142+
LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
143+
LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
144+
WHERE val1 = 1 AND val2 = 1;
145+
QUERY PLAN
146+
---------------------------------------------------
147+
Nested Loop
148+
-> Hash Join
149+
Hash Cond: (f.dim2_id = d2.id)
150+
-> Seq Scan on aa_fact f
151+
-> Hash
152+
-> Seq Scan on aa_dim2 d2
153+
Filter: (val2 = 1)
154+
-> Index Scan using aa_dim1_pkey on aa_dim1 d1
155+
Index Cond: (id = f.dim1_id)
156+
Filter: (val1 = 1)
157+
Supplied Plan Advice:
158+
NESTED_LOOP_PLAIN(d1) /* matched */
159+
(12 rows)
160+
161+
-- Add a useless extra entry to our test stash. Shouldn't change the result
162+
-- from the previous test.
163+
-- (If we're unlucky enough that this ever fails due to query ID actually
164+
-- being 1, then just put some other constant here. Seems unlikely.)
165+
SELECT pg_set_stashed_advice('regress_stash', 1, 'SEQ_SCAN(d1)');
166+
pg_set_stashed_advice
167+
-----------------------
168+
169+
(1 row)
170+
171+
EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
172+
LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
173+
LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
174+
WHERE val1 = 1 AND val2 = 1;
175+
QUERY PLAN
176+
---------------------------------------------------
177+
Nested Loop
178+
-> Hash Join
179+
Hash Cond: (f.dim2_id = d2.id)
180+
-> Seq Scan on aa_fact f
181+
-> Hash
182+
-> Seq Scan on aa_dim2 d2
183+
Filter: (val2 = 1)
184+
-> Index Scan using aa_dim1_pkey on aa_dim1 d1
185+
Index Cond: (id = f.dim1_id)
186+
Filter: (val1 = 1)
187+
Supplied Plan Advice:
188+
NESTED_LOOP_PLAIN(d1) /* matched */
189+
(12 rows)
190+
191+
-- Try an empty stash to be sure it does nothing
192+
SELECT pg_create_advice_stash('regress_empty_stash');
193+
pg_create_advice_stash
194+
------------------------
195+
196+
(1 row)
197+
198+
SET pg_stash_advice.stash_name = 'regress_empty_stash';
199+
EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
200+
LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
201+
LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
202+
WHERE val1 = 1 AND val2 = 1;
203+
QUERY PLAN
204+
------------------------------------------
205+
Hash Join
206+
Hash Cond: (f.dim1_id = d1.id)
207+
-> Hash Join
208+
Hash Cond: (f.dim2_id = d2.id)
209+
-> Seq Scan on aa_fact f
210+
-> Hash
211+
-> Seq Scan on aa_dim2 d2
212+
Filter: (val2 = 1)
213+
-> Hash
214+
-> Seq Scan on aa_dim1 d1
215+
Filter: (val1 = 1)
216+
(11 rows)
217+
218+
-- Test that we can list each stash individually and all of them together,
219+
-- but not a nonexistent stash.
220+
SELECT * FROM pg_get_advice_stashes() ORDER BY stash_name;
221+
stash_name | num_entries
222+
---------------------+-------------
223+
regress_empty_stash | 0
224+
regress_stash | 2
225+
(2 rows)
226+
227+
SELECT stash_name, advice_string
228+
FROM pg_get_advice_stash_contents('regress_stash') ORDER BY advice_string;
229+
stash_name | advice_string
230+
---------------+-----------------------
231+
regress_stash | NESTED_LOOP_PLAIN(d1)
232+
regress_stash | SEQ_SCAN(d1)
233+
(2 rows)
234+
235+
SELECT stash_name, advice_string
236+
FROM pg_get_advice_stash_contents('regress_empty_stash')
237+
ORDER BY advice_string;
238+
stash_name | advice_string
239+
------------+---------------
240+
(0 rows)
241+
242+
SELECT stash_name, advice_string
243+
FROM pg_get_advice_stash_contents(NULL) ORDER BY advice_string;
244+
stash_name | advice_string
245+
---------------+-----------------------
246+
regress_stash | NESTED_LOOP_PLAIN(d1)
247+
regress_stash | SEQ_SCAN(d1)
248+
(2 rows)
249+
250+
SELECT stash_name, advice_string
251+
FROM pg_get_advice_stash_contents('no_such_stash')
252+
ORDER BY advice_string;
253+
ERROR: advice stash "no_such_stash" does not exist
254+
-- Test that we can remove advice.
255+
SELECT pg_set_stashed_advice('regress_stash', :'qid', null);
256+
pg_set_stashed_advice
257+
-----------------------
258+
259+
(1 row)
260+
261+
SET pg_stash_advice.stash_name = 'regress_stash';
262+
EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
263+
LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
264+
LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
265+
WHERE val1 = 1 AND val2 = 1;
266+
QUERY PLAN
267+
------------------------------------------
268+
Hash Join
269+
Hash Cond: (f.dim1_id = d1.id)
270+
-> Hash Join
271+
Hash Cond: (f.dim2_id = d2.id)
272+
-> Seq Scan on aa_fact f
273+
-> Hash
274+
-> Seq Scan on aa_dim2 d2
275+
Filter: (val2 = 1)
276+
-> Hash
277+
-> Seq Scan on aa_dim1 d1
278+
Filter: (val1 = 1)
279+
(11 rows)
280+
281+
SELECT * FROM pg_get_advice_stashes() ORDER BY stash_name;
282+
stash_name | num_entries
283+
---------------------+-------------
284+
regress_empty_stash | 0
285+
regress_stash | 1
286+
(2 rows)
287+
288+
SELECT stash_name, advice_string
289+
FROM pg_get_advice_stash_contents('regress_stash') ORDER BY advice_string;
290+
stash_name | advice_string
291+
---------------+---------------
292+
regress_stash | SEQ_SCAN(d1)
293+
(1 row)
294+
295+
-- Can't create a stash that already exists, or drop one that doesn't.
296+
SELECT pg_create_advice_stash('regress_stash');
297+
ERROR: advice stash "regress_stash" already exists
298+
SELECT pg_drop_advice_stash('no_such_stash');
299+
ERROR: advice stash "no_such_stash" does not exist
300+
-- Can't add to or remove from a stash that does not exist.
301+
SELECT pg_set_stashed_advice('no_such_stash', 1, 'SEQ_SCAN(t)');
302+
ERROR: advice stash "no_such_stash" does not exist
303+
SELECT pg_set_stashed_advice('no_such_stash', 1, null);
304+
ERROR: advice stash "no_such_stash" does not exist
305+
-- Can't use query ID 0.
306+
SELECT pg_set_stashed_advice('regress_stash', 0, 'SEQ_SCAN(t)');
307+
ERROR: cannot set advice string for query ID 0
308+
-- Stash names must be non-empty, ASCII, and not too long, and must look
309+
-- like identifiers.
310+
SELECT pg_create_advice_stash('');
311+
ERROR: advice stash name may not be zero length
312+
SELECT pg_create_advice_stash('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
313+
ERROR: advice stash names may not be longer than 63 bytes
314+
SELECT pg_create_advice_stash(' ');
315+
ERROR: advice stash name must begin with a letter or underscore and contain only letters, digits, and underscores
316+
SET pg_stash_advice.stash_name = '99bottles';
317+
ERROR: invalid value for parameter "pg_stash_advice.stash_name": "99bottles"
318+
DETAIL: advice stash name must begin with a letter or underscore and contain only letters, digits, and underscores
319+
-- Clean up state in dynamic shared memory.
320+
SELECT pg_drop_advice_stash('regress_stash');
321+
pg_drop_advice_stash
322+
----------------------
323+
324+
(1 row)
325+
326+
SELECT pg_drop_advice_stash('regress_empty_stash');
327+
pg_drop_advice_stash
328+
----------------------
329+
330+
(1 row)
331+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* This test must be run in a database with UTF-8 encoding,
3+
* because other encodings don't support all the characters used.
4+
*/
5+
SELECT getdatabaseencoding() <> 'UTF8'
6+
AS skip_test \gset
7+
\if :skip_test
8+
\quit
9+
\endif
10+
SET client_encoding = utf8;
11+
-- Non-ASCII stash names should be rejected.
12+
SELECT pg_create_advice_stash('café');
13+
ERROR: advice stash name must not contain non-ASCII characters
14+
SET pg_stash_advice.stash_name = 'café';
15+
ERROR: invalid value for parameter "pg_stash_advice.stash_name": "café"
16+
DETAIL: advice stash name must not contain non-ASCII characters

0 commit comments

Comments
 (0)