Skip to content

Commit d377235

Browse files
authored
Merge pull request #6 from supabase/or/issue_2
Error handling
2 parents 1aa1016 + c4d1ab3 commit d377235

18 files changed

+486
-82
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.1.2.sql

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

0 commit comments

Comments
 (0)