Skip to content

Commit 4421c56

Browse files
committed
return errors as array rather than raising to enable bulk usage
1 parent 141eb94 commit 4421c56

15 files changed

+255
-87
lines changed

README.md

Lines changed: 27 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -9,39 +9,6 @@
99

1010
---
1111

12-
```sql
13-
select
14-
*
15-
from
16-
index_advisor('
17-
select
18-
book.id,
19-
book.title,
20-
publisher.name as publisher_name,
21-
author.name as author_name,
22-
review.body review_body
23-
from
24-
book
25-
join publisher
26-
on book.publisher_id = publisher.id
27-
join author
28-
on book.author_id = author.id
29-
join review
30-
on book.id = review.book_id
31-
where
32-
author.id = $1
33-
and publisher.id = $2
34-
');
35-
36-
startup_cost_before | startup_cost_after | total_cost_before | total_cost_after | index_statements
37-
---------------------+--------------------+-------------------+------------------+----------------------------------------------------------
38-
27.26 | 12.77 | 68.48 | 42.37 | {"CREATE INDEX ON public.book USING btree (author_id)",
39-
"CREATE INDEX ON public.book USING btree (publisher_id)",
40-
"CREATE INDEX ON public.review USING btree (book_id)"}
41-
(1 row)
42-
```
43-
44-
4512
A PostgreSQL extension for recommending indexes to improve query performance.
4613

4714
Features:
@@ -52,21 +19,21 @@ Features:
5219

5320
## API
5421

55-
```sql
56-
index_advisor(query text) returns table( index_statement text )
57-
```
58-
5922
#### Description
6023
For a given *query*, searches for a set of SQL DDL `create index` statements that improve the query's execution time;
6124

6225
#### Signature
6326
```sql
64-
index_advisor(
65-
query text
66-
)
67-
returns table( index_statement text )
68-
volatile
69-
language plpgsql
27+
index_advisor(query text)
28+
returns
29+
table (
30+
startup_cost_before jsonb,
31+
startup_cost_after jsonb,
32+
total_cost_before jsonb,
33+
total_cost_after jsonb,
34+
index_statements text[],
35+
errors text[]
36+
)
7037
```
7138

7239
## Usage
@@ -76,17 +43,20 @@ For a minimal example, the `index_advisor` function can be given a single table
7643
```sql
7744
create extension if not exists index_advisor cascade;
7845

79-
create table if not exists public.book(
80-
id int,
81-
name text
46+
create table book(
47+
id int primary key,
48+
title text not null
8249
);
8350

8451
select
85-
index_advisor($$ select * from book where id = $1 $$);
52+
*
53+
from
54+
index_advisor('select book.id from book where title = $1');
8655

87-
index_advisor
88-
----------------------------------------------
89-
CREATE INDEX ON public.book USING btree (id)
56+
startup_cost_before | startup_cost_after | total_cost_before | total_cost_after | index_statements | errors
57+
---------------------+--------------------+-------------------+------------------+-----------------------------------------------------+--------
58+
0.00 | 1.17 | 25.88 | 6.40 | {"CREATE INDEX ON public.book USING btree (title)"},| {}
59+
(1 row)
9060
```
9161

9262
More complex queries may generate additional suggested indexes
@@ -119,6 +89,8 @@ create table review(
11989
);
12090

12191
select
92+
*
93+
from
12294
index_advisor('
12395
select
12496
book.id,
@@ -139,11 +111,11 @@ select
139111
and publisher.id = $2
140112
');
141113

142-
index_advisor
143-
--------------------------------------------------------
144-
CREATE INDEX ON public.review USING btree (book_id)
145-
CREATE INDEX ON public.book USING btree (author_id)
146-
CREATE INDEX ON public.book USING btree (publisher_id)
114+
startup_cost_before | startup_cost_after | total_cost_before | total_cost_after | index_statements | errors
115+
---------------------+--------------------+-------------------+------------------+-----------------------------------------------------------+--------
116+
27.26 | 12.77 | 68.48 | 42.37 | {"CREATE INDEX ON public.book USING btree (author_id)", | {}
117+
"CREATE INDEX ON public.book USING btree (publisher_id)",
118+
"CREATE INDEX ON public.review USING btree (book_id)"}
147119
(3 rows)
148120
```
149121

index_advisor--0.2.0.sql

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

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.2'
2+
default_version = '0.2.0'
33
relocatable = true
44
requires = hypopg
Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
begin;
2-
create extension index_advisor version '0.1.2' cascade;
2+
create extension index_advisor version '0.2.0' cascade;
33
NOTICE: installing required extension "hypopg"
4-
select index_advisor($$ select 1; $$);
5-
ERROR: query must not contain a semicolon
6-
CONTEXT: PL/pgSQL function index_advisor(text) line 24 at RAISE
4+
-- This is okay because semicolon gets stripped from the end of the statement
5+
select * from index_advisor($$ select 1; $$);
6+
startup_cost_before | startup_cost_after | total_cost_before | total_cost_after | index_statements | errors
7+
---------------------+--------------------+-------------------+------------------+------------------+--------
8+
0.00 | 0.00 | 0.01 | 0.01 | {} | {}
9+
(1 row)
10+
11+
-- This is not okay because it contains multiple statements
12+
select * from index_advisor($$ select 1; select 1 $$);
13+
startup_cost_before | startup_cost_after | total_cost_before | total_cost_after | index_statements | errors
14+
---------------------+--------------------+-------------------+------------------+------------------+----------------------------------------
15+
| | | | {} | {"Query must not contain a semicolon"}
16+
(1 row)
17+
718
rollback;

test/expected/integration.out

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
begin;
2-
create extension index_advisor version '0.1.2' cascade;
2+
create extension index_advisor version '0.2.0' cascade;
33
NOTICE: installing required extension "hypopg"
44
create table public.book(
55
id int,
@@ -8,9 +8,9 @@ NOTICE: installing required extension "hypopg"
88
select index_advisor($$
99
select * from book where id = $1
1010
$$);
11-
index_advisor
12-
------------------------------------------------------------------------------
13-
(0.00,4.07,25.88,13.54,"{""CREATE INDEX ON public.book USING btree (id)""}")
11+
index_advisor
12+
---------------------------------------------------------------------------------
13+
(0.00,4.07,25.88,13.54,"{""CREATE INDEX ON public.book USING btree (id)""}",{})
1414
(1 row)
1515

1616
rollback;

test/expected/issue_1.out

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
begin;
2-
create extension index_advisor version '0.1.2' cascade;
2+
create extension index_advisor version '0.2.0' cascade;
33
NOTICE: installing required extension "hypopg"
44
create table public.book(
55
id int,
@@ -10,9 +10,9 @@ NOTICE: installing required extension "hypopg"
1010
select index_advisor($$
1111
select * from book where id = $1
1212
$$);
13-
index_advisor
14-
------------------------------------------------------------------------------
15-
(0.00,4.07,25.88,13.54,"{""CREATE INDEX ON public.book USING btree (id)""}")
13+
index_advisor
14+
---------------------------------------------------------------------------------
15+
(0.00,4.07,25.88,13.54,"{""CREATE INDEX ON public.book USING btree (id)""}",{})
1616
(1 row)
1717

1818
rollback;

test/expected/multi_index.out

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
begin;
2-
create extension index_advisor version '0.1.2' cascade;
2+
create extension index_advisor version '0.2.0' cascade;
33
NOTICE: installing required extension "hypopg"
44
create table author(
55
id serial primary key,
@@ -43,9 +43,9 @@ NOTICE: installing required extension "hypopg"
4343
author.id = $1
4444
and publisher.id = $2
4545
');
46-
startup_cost_before | startup_cost_after | total_cost_before | total_cost_after | index_statements
47-
---------------------+--------------------+-------------------+------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------
48-
27.26 | 12.77 | 68.48 | 42.37 | {"CREATE INDEX ON public.book USING btree (author_id)","CREATE INDEX ON public.book USING btree (publisher_id)","CREATE INDEX ON public.review USING btree (book_id)"}
46+
startup_cost_before | startup_cost_after | total_cost_before | total_cost_after | index_statements | errors
47+
---------------------+--------------------+-------------------+------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------
48+
27.26 | 12.77 | 68.48 | 42.37 | {"CREATE INDEX ON public.book USING btree (author_id)","CREATE INDEX ON public.book USING btree (publisher_id)","CREATE INDEX ON public.review USING btree (book_id)"} | {}
4949
(1 row)
5050

5151
rollback;

0 commit comments

Comments
 (0)