|
| 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 | + and pa.atttypid in (20,16,1082,1184,1114,701,23,21,700,1083,2950,1700,25,18,1042,1043) |
| 100 | + where |
| 101 | + pc.relnamespace::regnamespace::text not in ( -- ignore schema list |
| 102 | + 'pg_catalog', 'pg_toast', 'information_schema' |
| 103 | + ) |
| 104 | + and er.oid is null -- ignore entities owned by extensions |
| 105 | + and pc.relkind in ('r', 'm') -- regular tables, and materialized views |
| 106 | + and pc.relpersistence = 'p' -- permanent tables (not unlogged or temporary) |
| 107 | + and pa.attnum > 0 |
| 108 | + and not pa.attisdropped |
| 109 | + and pi.indrelid is null |
| 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 | +$$; |
0 commit comments