1
0
mirror of https://gitlab.com/depesz/explain.depesz.com.git synced 2024-11-24 08:42:27 +02:00

Add hints from Pg::Explain::Hinter

This commit is contained in:
Hubert depesz Lubaczewski 2022-02-16 12:23:51 +01:00
parent eddd052137
commit 23f69af2d8
8 changed files with 112 additions and 8 deletions

View File

@ -18,7 +18,7 @@ requires 'Encode';
requires 'English';
requires 'File::Spec';
requires 'Mojolicious';
requires 'Pg::Explain', '>= 1.13';
requires 'Pg::Explain', '>= 2.0';
requires 'Number::Bytes::Human';
# vim: set ft=perl:

View File

@ -40,6 +40,9 @@ sub startup {
# load number_format plugin
$self->plugin( 'number_format' );
# Plugin to to smart quoting of DB identifiers
$self->plugin( 'DBIdentQuote' );
# Plugin to check if static file exists
$self->plugin( 'Explain::Plugin::StaticExists' );

View File

@ -5,6 +5,7 @@ use Mojo::Base 'Mojolicious::Controller';
use English -no_match_vars;
use Pg::Explain;
use Pg::Explain::Hinter;
use pgFormatter::Beautify;
use Encode;
use Email::Valid;
@ -268,7 +269,7 @@ sub delete {
my $key = defined $self->stash->{ key } ? $self->stash->{ key } : '';
# missing or invalid
return $self->redirect_to( 'new-explain' ) unless $id =~ m{\A[a-zA-Z0-9]+\z};
return $self->redirect_to( 'new-explain' ) unless $id =~ m{\A[a-zA-Z0-9]+\z};
return $self->redirect_to( 'new-explain' ) unless $key =~ m{\A[a-zA-Z0-9]+\z};
# delete plan in database
@ -356,6 +357,10 @@ sub show {
$self->stash->{ optimization_path } = $self->database->get_optimization_path( $id );
$self->stash->{ suboptimizations } = $self->database->get_optimizations_for( $id );
# Add hinter, if it makes sense
my $hinter = Pg::Explain::Hinter->new( $explain );
$self->stash->{ hinter } = $hinter if $hinter->any_hints;
# render will be called automatically
return $self->render( 'controller/iframe' ) if 'iframe' eq $self->match->endpoint->name;
@ -471,8 +476,8 @@ sub info {
my @versions = ();
for my $module ( sort keys %INC ) {
next if $module =~ m{^\.?/};
$module =~ s/\.pm$//;
$module =~ s#/#::#g;
$module =~ s/\.pm$//;
$module =~ s#/#::#g;
push @versions,
{
'module' => $module,

View File

@ -0,0 +1,21 @@
package Explain::Plugin::DBIdentQuote;
use Number::Bytes::Human qw(format_bytes);
use Mojo::Base 'Mojolicious::Plugin';
sub register {
my ( $self, $app ) = @_;
# register helpers
$app->helper( db_ident_quote => \&db_ident_quote );
}
sub db_ident_quote {
my $self = shift;
local $_ = shift;
return $_ if /\A[a-z][a-z0-9_]*\z/;
return sprintf '"%s"', $_;
}
1;

View File

@ -117,7 +117,7 @@ section p a:hover {background:#234;color:#fff;border:1px solid #123}
.result .tabs ul {margin:0;padding:0;list-style:none}
.result .tabs li {display:block;float:left;margin-right:1px}
.result .tabs a {display:block;padding:4px 12px;background:#cfcfcf;color:#fff;text-decoration:none;font-weight:bold}
.result .tabs a {display:block;padding:4px 12px;background:#ababab;color:#fff;text-decoration:none;font-weight:bold}
.result .tabs a:hover {color:#234}
.result .tabs a.current,
.result .tabs a.current:hover {background:#234;color:#fff}
@ -285,3 +285,19 @@ section p a:hover {background:#234;color:#fff;border:1px solid #123}
#plan-settings th { font-size: 1.2em; font-weight: bold; padding: 0.2em 0.5em;}
#plan-settings thead th {text-align: center; font-size: 1.5em;}
#plan-settings td {text-align: right; padding: 0.2em 0.5em;}
.result-hints h1 {
border: none;
padding: 10px 20px;
color: #444;
}
.result-hints ol {
padding-left: 20px;
}
.result-hints ol li {
margin-top: 20px;
}
.result-hints ol li p,
.result-hints ol li pre {
margin: 5px;
}

View File

@ -90,7 +90,7 @@ div.explain-form a:hover {background:#e5ecf9}
* html #explain-form input {border:0}
*:first-child+html #explain-form input {border:0}
.result-text, .result-source, .result-html, .result-stats {margin:0;padding:0;border:1px solid #ccc;overflow:auto}
.result-text, .result-source, .result-html, .result-stats, .result-hints {margin:0;padding:0;border:1px solid #ccc;overflow:auto}
/* all ico(ns) */
.result-html table#explain td.n div.ico {position:absolute;top:-4px;left:-22px;width:22px;height:22px;background:url('../img/sprite.png') no-repeat 0px -180px;text-indent:-1000px;overflow:hidden}
@ -205,3 +205,12 @@ button.copy {
background: #e5ecf9;
color: #444;
}
div.tabs span.newblink {
margin: 10px;
animation: newblink .3s alternate infinite;
}
@keyframes newblink {
0% { color: #ff0000; }
100% { color: #ffff00; }
}

View File

@ -340,7 +340,7 @@
return;
}
if(['html', 'source', 'text', 'query', 'bquery', 'stats'].indexOf(want) == -1 ) {
if(['html', 'source', 'text', 'hints', 'query', 'bquery', 'stats'].indexOf(want) == -1 ) {
return;
}

View File

@ -112,7 +112,8 @@
% }
% my $global_node_id = 0;
% my $table_node_id_to_explain_node_id = {};
% my $explain_node_id_to_table_node_id = {};
% my $prev_row_level = 0;
@ -212,6 +213,8 @@
% $row_class .= ' c-' . $row_color;
% my $node_id = $global_node_id++;
% $table_node_id_to_explain_node_id->{$global_node_id} = $node->id;
% $explain_node_id_to_table_node_id->{$node->id} = $global_node_id;
<tr id="l<%= $global_node_id =%>" class="n <%= $row_class %>" data-node_id="<%= $node_id =%>" data-node_parent="<%= $parent =%>" data-level="<%= $level =%>" data-e="<%= $exclusive_point =%>" data-i="<%= $inclusive_point =%>" data-x="<%= $rows_x_point =%>">
<td class="u <%= $cfg->{ vu } ? '' : ' tight' %>"><a href="#l<%= $global_node_id =%>"><%= $global_node_id =%>.</a></td>
@ -617,6 +620,14 @@
onkeypress="return this.onclick( );">TEXT</a>
</li>
% }
% if ( stash('hinter') ) {
<li class="hints">
<a href="#hints"
title="view plan hints"
onclick="$( this ).explain( 'toggleView', 'hints', this ); return false;"
onkeypress="return this.onclick( );">HINTS <span class="newblink">!NEW!</span></a>
</li>
% }
% if ( stash('query') ) {
<li class="query">
<a href="#query"
@ -824,6 +835,45 @@
</div>
% }
% if ( stash('hinter') ) {
% my $H = stash('hinter')->hints;
<div class="result-hints res-tab hidden">
% if ( 1 == scalar @{ $H } ) {
<h1>I have one hint for you:</h1>
% } else {
<h1>I have <%= scalar @{ $H } %> hints for you:</h1>
% }
<ol>
% for my $hint ( @{ $H }) {
% my $node_num = $explain_node_id_to_table_node_id->{ $hint->node->id };
<li>
% if ( $hint->type eq 'DISK_SORT' ) {
<p>You have <a href="#<%= $node_num %>" target="_new">sort node (#<%= $node_num %>)</a> that is using disk space to sort.</p>
<p>This is because your <a href="https://www.postgresql.org/docs/current/runtime-config-<%= $guc_docs->{ 'work_mem' } %>" target='_new'>work_mem</a> setting is too low.</p>
<p>Increasing it can make the sort run in memory, or, at least, use less of disk. Sort used <%= $hint->details->[0] %>kB, so you would need to set your work_mem to <em>at least</em> that much to have a chance at sorting in memory only.</p>
% } elsif ( $hint->type eq 'INDEXABLE_SEQSCAN_SIMPLE' ) {
% my $table_name = $hint->node->scan_on->{'table_name'};
% my $column_name = $hint->details->[0];
% my $operator = $hint->details->[1];
% my $fetched_rows = $hint->node->total_rows + $hint->node->total_rows_removed;
% my $dropped_rows = $hint->node->total_rows_removed;
<p>You have <a href="#<%= $node_num %>" target="_new"><%= $hint->node->type %> (#<%= $node_num %>)</a> that could use an index.</p>
<p>This node searches in table <em><%= $table_name %></em> using <em><%= $operator %></em> operator on column <em><%= $column_name %></em>. In process it fetches from disk <%= commify_number( $fetched_rows ) %> rows, just to drop
% if ( $fetched_rows == $dropped_rows ) {
<em>all</em>
% } else {
<%= commify_number( $dropped_rows ) %>
% }
of them!</p>
<p>This should be possible to speed up by creating this index:</p>
<pre><code class="sql">CREATE INDEX CONCURRENTLY <%= db_ident_quote( "explain_depesz_com_hint_${id}_${node_num}" ) %> ON <%= db_ident_quote($table_name) %> ( <%= db_ident_quote($column_name) %> );</code></pre>
% }
</li>
% }
</ol>
</div>
% }
% if ( stash('query') ) {
<div class="result-query res-tab hidden">
<pre id="query"><code class="sql"><%= stash('query') %></code></pre>