Skip to content

Commit 1aa1016

Browse files
authored
Merge pull request #4 from supabase/or/issue_1
Only consider indexes on primitive types
2 parents 7c0386f + 436451b commit 1aa1016

13 files changed

+363
-9
lines changed

index_advisor--0.1.0--0.1.1.sql

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
drop type index_advisor_output;
2+
3+
create or replace function index_advisor(
4+
query text
5+
)
6+
returns table (
7+
startup_cost_before jsonb,
8+
startup_cost_after jsonb,
9+
total_cost_before jsonb,
10+
total_cost_after jsonb,
11+
index_statements text[]
12+
)
13+
volatile
14+
language plpgsql
15+
as $$
16+
declare
17+
n_args int;
18+
prepared_statement_name text = 'index_advisor_working_statement';
19+
hypopg_schema_name text = (select extnamespace::regnamespace::text from pg_extension where extname = 'hypopg');
20+
explain_plan_statement text;
21+
rec record;
22+
plan_initial jsonb;
23+
plan_final jsonb;
24+
statements text[] = '{}';
25+
begin
26+
27+
-- Disallow multiple statements
28+
if query ilike '%;%' then
29+
raise exception 'query must not contain a semicolon';
30+
end if;
31+
32+
-- Hack to support PostgREST because the prepared statement for args incorrectly defaults to text
33+
query := replace(query, 'WITH pgrst_payload AS (SELECT $1 AS json_data)', 'WITH pgrst_payload AS (SELECT $1::json AS json_data)');
34+
35+
-- Create a prepared statement for the given query
36+
deallocate all;
37+
execute format('prepare %I as %s', prepared_statement_name, query);
38+
39+
-- Detect how many arguments are present in the prepared statement
40+
n_args = (
41+
select
42+
coalesce(array_length(parameter_types, 1), 0)
43+
from
44+
pg_prepared_statements
45+
where
46+
name = prepared_statement_name
47+
limit
48+
1
49+
);
50+
51+
-- Create a SQL statement that can be executed to collect the explain plan
52+
explain_plan_statement = format(
53+
'set local plan_cache_mode = force_generic_plan; explain (format json) execute %I%s',
54+
--'explain (format json) execute %I%s',
55+
prepared_statement_name,
56+
case
57+
when n_args = 0 then ''
58+
else format(
59+
'(%s)', array_to_string(array_fill('null'::text, array[n_args]), ',')
60+
)
61+
end
62+
);
63+
64+
-- Store the query plan before any new indexes
65+
execute explain_plan_statement into plan_initial;
66+
67+
-- Create possible indexes
68+
for rec in (
69+
with extension_regclass as (
70+
select
71+
distinct objid as oid
72+
from
73+
pg_depend
74+
where
75+
deptype = 'e'
76+
)
77+
select
78+
pc.relnamespace::regnamespace::text as schema_name,
79+
pc.relname as table_name,
80+
pa.attname as column_name,
81+
format(
82+
'select %I.hypopg_create_index($i$create index on %I.%I(%I)$i$)',
83+
hypopg_schema_name,
84+
pc.relnamespace::regnamespace::text,
85+
pc.relname,
86+
pa.attname
87+
) hypopg_statement
88+
from
89+
pg_catalog.pg_class pc
90+
join pg_catalog.pg_attribute pa
91+
on pc.oid = pa.attrelid
92+
left join extension_regclass er
93+
on pc.oid = er.oid
94+
left join pg_index pi
95+
on pc.oid = pi.indrelid
96+
and (select array_agg(x) from unnest(pi.indkey) v(x)) = array[pa.attnum]
97+
and pi.indexprs is null -- ignore expression indexes
98+
and pi.indpred is null -- ignore partial indexes
99+
where
100+
pc.relnamespace::regnamespace::text not in ( -- ignore schema list
101+
'pg_catalog', 'pg_toast', 'information_schema'
102+
)
103+
and er.oid is null -- ignore entities owned by extensions
104+
and pc.relkind in ('r', 'm') -- regular tables, and materialized views
105+
and pc.relpersistence = 'p' -- permanent tables (not unlogged or temporary)
106+
and pa.attnum > 0
107+
and not pa.attisdropped
108+
and pi.indrelid is null
109+
and pa.atttypid in (20,16,1082,1184,1114,701,23,21,700,1083,2950,1700,25,18,1042,1043)
110+
)
111+
loop
112+
-- Create the hypothetical index
113+
execute rec.hypopg_statement;
114+
end loop;
115+
116+
/*
117+
for rec in select * from hypopg()
118+
loop
119+
raise notice '%', rec;
120+
end loop;
121+
*/
122+
123+
-- Create a prepared statement for the given query
124+
-- The original prepared statement MUST be dropped because its plan is cached
125+
execute format('deallocate %I', prepared_statement_name);
126+
execute format('prepare %I as %s', prepared_statement_name, query);
127+
128+
-- Store the query plan after new indexes
129+
execute explain_plan_statement into plan_final;
130+
131+
--raise notice '%', plan_final;
132+
133+
-- Idenfity referenced indexes in new plan
134+
execute format(
135+
'select
136+
coalesce(array_agg(hypopg_get_indexdef(indexrelid) order by indrelid, indkey::text), $i${}$i$::text[])
137+
from
138+
%I.hypopg()
139+
where
140+
%s ilike ($i$%%$i$ || indexname || $i$%%$i$)
141+
',
142+
hypopg_schema_name,
143+
quote_literal(plan_final)::text
144+
) into statements;
145+
146+
-- Reset all hypothetical indexes
147+
perform hypopg_reset();
148+
149+
-- Reset prepared statements
150+
deallocate all;
151+
152+
return query values (
153+
(plan_initial -> 0 -> 'Plan' -> 'Startup Cost'),
154+
(plan_final -> 0 -> 'Plan' -> 'Startup Cost'),
155+
(plan_initial -> 0 -> 'Plan' -> 'Total Cost'),
156+
(plan_final -> 0 -> 'Plan' -> 'Total Cost'),
157+
statements::text[]
158+
);
159+
160+
end;
161+
$$;

index_advisor--0.1.1.sql

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
create or replace function index_advisor(
2+
query text
3+
)
4+
returns table (
5+
startup_cost_before jsonb,
6+
startup_cost_after jsonb,
7+
total_cost_before jsonb,
8+
total_cost_after jsonb,
9+
index_statements text[]
10+
)
11+
volatile
12+
language plpgsql
13+
as $$
14+
declare
15+
n_args int;
16+
prepared_statement_name text = 'index_advisor_working_statement';
17+
hypopg_schema_name text = (select extnamespace::regnamespace::text from pg_extension where extname = 'hypopg');
18+
explain_plan_statement text;
19+
rec record;
20+
plan_initial jsonb;
21+
plan_final jsonb;
22+
statements text[] = '{}';
23+
begin
24+
25+
-- Disallow multiple statements
26+
if query ilike '%;%' then
27+
raise exception 'query must not contain a semicolon';
28+
end if;
29+
30+
-- Hack to support PostgREST because the prepared statement for args incorrectly defaults to text
31+
query := replace(query, 'WITH pgrst_payload AS (SELECT $1 AS json_data)', 'WITH pgrst_payload AS (SELECT $1::json AS json_data)');
32+
33+
-- Create a prepared statement for the given query
34+
deallocate all;
35+
execute format('prepare %I as %s', prepared_statement_name, query);
36+
37+
-- Detect how many arguments are present in the prepared statement
38+
n_args = (
39+
select
40+
coalesce(array_length(parameter_types, 1), 0)
41+
from
42+
pg_prepared_statements
43+
where
44+
name = prepared_statement_name
45+
limit
46+
1
47+
);
48+
49+
-- Create a SQL statement that can be executed to collect the explain plan
50+
explain_plan_statement = format(
51+
'set local plan_cache_mode = force_generic_plan; explain (format json) execute %I%s',
52+
--'explain (format json) execute %I%s',
53+
prepared_statement_name,
54+
case
55+
when n_args = 0 then ''
56+
else format(
57+
'(%s)', array_to_string(array_fill('null'::text, array[n_args]), ',')
58+
)
59+
end
60+
);
61+
62+
-- Store the query plan before any new indexes
63+
execute explain_plan_statement into plan_initial;
64+
65+
-- Create possible indexes
66+
for rec in (
67+
with extension_regclass as (
68+
select
69+
distinct objid as oid
70+
from
71+
pg_depend
72+
where
73+
deptype = 'e'
74+
)
75+
select
76+
pc.relnamespace::regnamespace::text as schema_name,
77+
pc.relname as table_name,
78+
pa.attname as column_name,
79+
format(
80+
'select %I.hypopg_create_index($i$create index on %I.%I(%I)$i$)',
81+
hypopg_schema_name,
82+
pc.relnamespace::regnamespace::text,
83+
pc.relname,
84+
pa.attname
85+
) hypopg_statement
86+
from
87+
pg_catalog.pg_class pc
88+
join pg_catalog.pg_attribute pa
89+
on pc.oid = pa.attrelid
90+
left join extension_regclass er
91+
on pc.oid = er.oid
92+
left join pg_index pi
93+
on pc.oid = pi.indrelid
94+
and (select array_agg(x) from unnest(pi.indkey) v(x)) = array[pa.attnum]
95+
and pi.indexprs is null -- ignore expression indexes
96+
and pi.indpred is null -- ignore partial indexes
97+
where
98+
pc.relnamespace::regnamespace::text not in ( -- ignore schema list
99+
'pg_catalog', 'pg_toast', 'information_schema'
100+
)
101+
and er.oid is null -- ignore entities owned by extensions
102+
and pc.relkind in ('r', 'm') -- regular tables, and materialized views
103+
and pc.relpersistence = 'p' -- permanent tables (not unlogged or temporary)
104+
and pa.attnum > 0
105+
and not pa.attisdropped
106+
and pi.indrelid is null
107+
and pa.atttypid in (20,16,1082,1184,1114,701,23,21,700,1083,2950,1700,25,18,1042,1043)
108+
)
109+
loop
110+
-- Create the hypothetical index
111+
execute rec.hypopg_statement;
112+
end loop;
113+
114+
/*
115+
for rec in select * from hypopg()
116+
loop
117+
raise notice '%', rec;
118+
end loop;
119+
*/
120+
121+
-- Create a prepared statement for the given query
122+
-- The original prepared statement MUST be dropped because its plan is cached
123+
execute format('deallocate %I', prepared_statement_name);
124+
execute format('prepare %I as %s', prepared_statement_name, query);
125+
126+
-- Store the query plan after new indexes
127+
execute explain_plan_statement into plan_final;
128+
129+
--raise notice '%', plan_final;
130+
131+
-- Idenfity referenced indexes in new plan
132+
execute format(
133+
'select
134+
coalesce(array_agg(hypopg_get_indexdef(indexrelid) order by indrelid, indkey::text), $i${}$i$::text[])
135+
from
136+
%I.hypopg()
137+
where
138+
%s ilike ($i$%%$i$ || indexname || $i$%%$i$)
139+
',
140+
hypopg_schema_name,
141+
quote_literal(plan_final)::text
142+
) into statements;
143+
144+
-- Reset all hypothetical indexes
145+
perform hypopg_reset();
146+
147+
-- Reset prepared statements
148+
deallocate all;
149+
150+
return query values (
151+
(plan_initial -> 0 -> 'Plan' -> 'Startup Cost'),
152+
(plan_final -> 0 -> 'Plan' -> 'Startup Cost'),
153+
(plan_initial -> 0 -> 'Plan' -> 'Total Cost'),
154+
(plan_final -> 0 -> 'Plan' -> 'Total Cost'),
155+
statements::text[]
156+
);
157+
158+
end;
159+
$$;

index_advisor.control

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
comment = 'Query index advisor'
2-
default_version = '0.1.0'
2+
default_version = '0.1.1'
33
relocatable = true
44
requires = hypopg

test/expected/disallow_semicolon.out

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
begin;
2-
create extension index_advisor cascade;
2+
create extension index_advisor version '0.1.1' cascade;
33
NOTICE: installing required extension "hypopg"
44
select index_advisor($$ select 1; $$);
55
ERROR: query must not contain a semicolon

test/expected/integration.out

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
begin;
2-
create extension index_advisor cascade;
2+
create extension index_advisor version '0.1.1' cascade;
33
NOTICE: installing required extension "hypopg"
44
create table public.book(
55
id int,

test/expected/issue_1.out

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
begin;
2+
create extension index_advisor version '0.1.1' cascade;
3+
NOTICE: installing required extension "hypopg"
4+
create table public.book(
5+
id int,
6+
-- json type is not btree indexable. In version 0.1.1 this raises the error
7+
-- ERROR: data type json has no default operator class for access method "btree"
8+
meta json
9+
);
10+
select index_advisor($$
11+
select * from book where id = $1
12+
$$);
13+
index_advisor
14+
------------------------------------------------------------------------------
15+
(0.00,4.07,25.88,13.54,"{""CREATE INDEX ON public.book USING btree (id)""}")
16+
(1 row)
17+
18+
rollback;

test/expected/multi_index.out

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
begin;
2-
create extension index_advisor cascade;
2+
create extension index_advisor version '0.1.1' cascade;
33
NOTICE: installing required extension "hypopg"
44
create table author(
55
id serial primary key,

test/expected/postgrest_query.out

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
begin;
2-
create extension index_advisor cascade;
2+
create extension index_advisor version '0.1.1' cascade;
33
NOTICE: installing required extension "hypopg"
44
create function get_info(x int) returns text language sql as $$ select 'foo' $$;
55
select index_advisor($$

test/sql/disallow_semicolon.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
begin;
22

3-
create extension index_advisor cascade;
3+
create extension index_advisor version '0.1.1' cascade;
44

55
select index_advisor($$ select 1; $$);
66

test/sql/integration.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
begin;
22

3-
create extension index_advisor cascade;
3+
create extension index_advisor version '0.1.1' cascade;
44

55
create table public.book(
66
id int,

0 commit comments

Comments
 (0)