mirror of
https://gitlab.com/depesz/explain.depesz.com.git
synced 2024-11-30 09:06:42 +02:00
163 lines
4.9 KiB
Perl
Executable File
163 lines
4.9 KiB
Perl
Executable File
#!/usr/bin/env perl
|
|
|
|
# UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
|
|
use v5.14;
|
|
use strict;
|
|
use warnings;
|
|
use warnings qw( FATAL utf8 );
|
|
use utf8;
|
|
use open qw( :std :utf8 );
|
|
use Unicode::Normalize qw( NFC );
|
|
use Unicode::Collate;
|
|
use Encode qw( decode );
|
|
|
|
if ( grep /\P{ASCII}/ => @ARGV ) {
|
|
@ARGV = map { decode( 'UTF-8', $_ ) } @ARGV;
|
|
}
|
|
|
|
# If there is __DATA__,then uncomment next line:
|
|
# binmode( DATA, ':encoding(UTF-8)' );
|
|
# UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
|
|
|
|
# Useful common code
|
|
use autodie;
|
|
use Carp qw( carp croak confess cluck );
|
|
use English qw( -no_match_vars );
|
|
use Data::Dumper qw( Dumper );
|
|
|
|
# give a full stack dump on any untrapped exceptions
|
|
local $SIG{ __DIE__ } = sub {
|
|
confess "Uncaught exception: @_" unless $^S;
|
|
};
|
|
|
|
# now promote run-time warnings into stackdumped exceptions
|
|
# *unless* we're in an try block, in which
|
|
# case just generate a clucking stackdump instead
|
|
local $SIG{ __WARN__ } = sub {
|
|
if ( $^S ) { cluck "Trapped warning: @_" }
|
|
else { confess "Deadly warning: @_" }
|
|
};
|
|
|
|
# Useful common code
|
|
|
|
use DBI;
|
|
use Parallel::ForkManager;
|
|
use List::Util qw( any );
|
|
use Sys::Info;
|
|
use Pg::Explain;
|
|
use File::Temp qw( tempdir );
|
|
|
|
my $partitions = get_list_of_partitions();
|
|
my $cpus = Sys::Info->new->device( 'CPU' )->count;
|
|
my $output = tempdir( 'interesting-explains.XXXXXX', TMPDIR => 1 );
|
|
printf "Processing %d partitions in %d workers, output goes to %s\n", scalar @{ $partitions }, $cpus, $output;
|
|
|
|
my $pm = Parallel::ForkManager->new( $cpus );
|
|
|
|
PARTITION:
|
|
for my $partno ( 0 .. $#{ $partitions } ) {
|
|
my $part = $partitions->[ $partno ];
|
|
$pm->start and next PARTITION;
|
|
printf "Starting partition #%d\n", 1 + $partno;
|
|
|
|
open my $out, '>', "${output}/${part}.txt";
|
|
|
|
my $dbh = get_dbh();
|
|
|
|
my $seen = 0;
|
|
my $errors = 0;
|
|
my $interesting = 0;
|
|
|
|
my $query = sprintf "select id, plan FROM %s WHERE is_public and NOT is_deleted and NOT is_anonymized and plan ~ 'actual time=[0-9]'", $part;
|
|
$dbh->do( "DECLARE csr CURSOR FOR $query" );
|
|
while ( 1 ) {
|
|
my $rows = $dbh->selectall_arrayref( 'FETCH 100 FROM csr', { 'Slice' => {} } );
|
|
last if 0 == scalar @{ $rows };
|
|
for my $row ( @{ $rows } ) {
|
|
$seen++;
|
|
my $explain;
|
|
eval {
|
|
$explain = Pg::Explain->new( 'source' => $row->{ 'plan' } );
|
|
$explain->parse_source();
|
|
};
|
|
if ( $EVAL_ERROR ) {
|
|
$errors++;
|
|
next;
|
|
}
|
|
next unless $explain->top_node;
|
|
|
|
my $is_interesting = 0;
|
|
|
|
for my $line ( @{ output_lines( $row->{'id'}, $explain ) } ) {
|
|
print $out join("\t", @{$line}) . "\n";
|
|
$is_interesting = 1;
|
|
}
|
|
|
|
$interesting++ if $is_interesting;
|
|
}
|
|
}
|
|
|
|
$dbh->rollback();
|
|
$dbh->disconnect();
|
|
close $out;
|
|
printf "Partition %d done. %d plans scanned, %d errored out, %d interesting saved.\n", 1 + $partno, $seen, $errors, $interesting;
|
|
$pm->finish;
|
|
}
|
|
$pm->wait_all_children;
|
|
|
|
printf "All done, output in %s\n", $output;
|
|
|
|
exit;
|
|
|
|
sub output_lines {
|
|
my ( $id, $plan ) = @_;
|
|
my $ret = [];
|
|
for my $node ( $plan->top_node, $plan->top_node->all_recursive_subnodes ) {
|
|
|
|
next unless $node->estimated_row_width;
|
|
next unless $node->total_rows;
|
|
next unless $node->total_rows_removed;
|
|
next unless $node->extra_info;
|
|
|
|
# At least 3 pages worth of data is returned
|
|
next unless $node->total_rows * $node->estimated_row_width > 3 * 8192;
|
|
|
|
# At least 90% of rows were removed
|
|
next unless $node->total_rows_removed > 9 * $node->total_rows;
|
|
|
|
my @filter_lines = grep { /^Filter:/ } @{ $node->extra_info };
|
|
# There are filter expressions
|
|
next if 0 == scalar @filter_lines;
|
|
|
|
push @{ $ret }, map { [ $id, $node->id, $node->type, $node->estimated_row_width, $node->total_rows, $node->total_rows_removed, $_ ] } @filter_lines;
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
sub get_dbh {
|
|
my $dsn = sprintf 'dbi:Pg:dbname=%s', $ENV{ 'PGDATABASE' } || 'depesz_explain';
|
|
$dsn .= sprintf ';host=%s', $ENV{ 'PGHOST' } if $ENV{ 'PGHOST' };
|
|
$dsn .= sprintf ';port=%s', $ENV{ 'PGPORT' } if $ENV{ 'PGPORT' };
|
|
|
|
return DBI->connect( $dsn, undef, undef, { 'AutoCommit' => 0, 'PrintError' => 1, 'RaiseError' => 1 } );
|
|
}
|
|
|
|
sub get_list_of_partitions {
|
|
my $dbh = get_dbh();
|
|
my $parts = $dbh->selectcol_arrayref( "
|
|
SELECT
|
|
c.oid::regclass
|
|
FROM
|
|
pg_catalog.pg_class c
|
|
JOIN pg_catalog.pg_inherits i ON c.oid = i.inhrelid
|
|
WHERE
|
|
i.inhparent = 'public.plans'::regclass
|
|
AND c.relkind = 'r'
|
|
ORDER BY
|
|
c.relpages DESC
|
|
" );
|
|
$dbh->rollback();
|
|
$dbh->disconnect();
|
|
return $parts;
|
|
}
|