File Coverage

blib/lib/Pg/Explain/Hinter.pm
Criterion Covered Total %
statement 110 110 100.0
branch 37 50 74.0
condition n/a
subroutine 18 18 100.0
pod 8 8 100.0
total 173 186 93.0


line stmt bran cond sub pod time code
1             package Pg::Explain::Hinter;
2              
3             # UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
4 5     5   3079 use v5.18;
  5         20  
5 5     5   32 use strict;
  5         51  
  5         119  
6 5     5   82 use warnings;
  5         13  
  5         178  
7 5     5   33 use warnings qw( FATAL utf8 );
  5         10  
  5         171  
8 5     5   25 use utf8;
  5         11  
  5         279  
9 5     5   169 use open qw( :std :utf8 );
  5         178  
  5         51  
10 5     5   741 use Unicode::Normalize qw( NFC );
  5         15  
  5         256  
11 5     5   29 use Unicode::Collate;
  5         10  
  5         133  
12 5     5   28 use Encode qw( decode );
  5         10  
  5         352  
13              
14             if ( grep /\P{ASCII}/ => @ARGV ) {
15             @ARGV = map { decode( 'UTF-8', $_ ) } @ARGV;
16             }
17              
18             # UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/
19              
20 5     5   3289 use Pg::Explain::Hinter::Hint;
  5         14  
  5         7438  
21              
22             =head1 NAME
23              
24             Pg::Explain::Hinter - Review Pg::Explain plans and return hints, if there are any
25              
26             =head1 VERSION
27              
28             Version 2.3
29              
30             =cut
31              
32             our $VERSION = '2.3';
33              
34             =head1 SYNOPSIS
35              
36             Given Pg::Explain plan, it will look at its nodes, and suggest what can be improved, if anything.
37              
38             my $hinter = Pg::Explain::Hinter->new( $plan );
39             if ( $hinter->any_hints ) {
40             print Dumper( $hinter->hints );
41             } else {
42             print "There are no hints for this plan.\n";
43             }
44              
45             Hints are Pg::Explain::Hinter::Hint objects.
46              
47             =head1 FUNCTIONS
48              
49             =head2 new
50              
51             Object constructor.
52              
53             =cut
54              
55             sub new {
56 43     43 1 208 my $class = shift;
57 43         62 my $plan = shift;
58 43 50       112 croak( 'Given parameter is not Pg::Explain object!' ) unless 'Pg::Explain' eq ref $plan;
59 43         104 my $self = bless {}, $class;
60 43         138 $self->plan( $plan );
61 43         117 $self->hints( [] );
62 43         111 $self->calculate_hints();
63 43         98 return $self;
64             }
65              
66             =head2 plan
67              
68             Accessor for plan inside hinter.
69              
70             =cut
71              
72             sub plan {
73 211     211 1 306 my $self = shift;
74 211 100       410 $self->{ 'plan' } = $_[ 0 ] if 0 < scalar @_;
75 211         510 return $self->{ 'plan' };
76             }
77              
78             =head2 hints
79              
80             Accessor for hints inside hinter.
81              
82             =cut
83              
84             sub hints {
85 170     170 1 278 my $self = shift;
86 170 100       421 $self->{ 'hints' } = $_[ 0 ] if 0 < scalar @_;
87 170         574 return $self->{ 'hints' };
88             }
89              
90             =head2 any_hints
91              
92             Returns 1 if there are any hints for provided plan, and undef if there are none.
93              
94             =cut
95              
96             sub any_hints {
97 43     43 1 162 my $self = shift;
98 43 100       61 return 1 if 0 < scalar @{ $self->hints };
  43         78  
99 1         5 return;
100             }
101              
102             =head2 calculate_hints
103              
104             Main function checking if there are any things that could be hinted on.
105              
106             =cut
107              
108             sub calculate_hints {
109 43     43 1 64 my $self = shift;
110 43 100       76 return unless $self->plan->top_node->is_analyzed;
111              
112 42         97 for my $node ( $self->plan->top_node, $self->plan->top_node->all_recursive_subnodes ) {
113              
114             # If this node was not ran, we can't do anything about it.
115 43 50       92 next unless $node->actual_loops;
116              
117 43         133 $self->check_hint_disk_sort( $node );
118 43         115 $self->check_hint_indexable_seqscan_simple( $node );
119 43         128 $self->check_hint_indexable_seqscan_multi_equal_and( $node );
120              
121             }
122              
123 42         79 return;
124             }
125              
126             =head2 check_hint_disk_sort
127              
128             Check if given node matches criteria for DISK_SORT hint
129              
130             =cut
131              
132             sub check_hint_disk_sort {
133 43     43 1 61 my $self = shift;
134 43         58 my $node = shift;
135 43 100       91 return unless $node->type eq 'Sort';
136 1 50       4 return unless $node->extra_info;
137 1         2 for my $info ( @{ $node->extra_info } ) {
  1         11  
138 2 100       15 next unless $info =~ m{\ASort Method:.*Disk:\s*(\d+)kB\s*\z};
139 1         3 my $disk_used = $1;
140 1         2 push @{ $self->{ 'hints' } }, Pg::Explain::Hinter::Hint->new(
  1         11  
141             'node' => $node,
142             'type' => 'DISK_SORT',
143             'details' => [ $disk_used ],
144             );
145 1         2 last;
146             }
147 1         2 return;
148             }
149              
150             =head2 check_hint_indexable_seqscan_simple
151              
152             Check if given node matches criteria for INDEXABLE_SEQSCAN_SIMPLE hint
153              
154             =cut
155              
156             sub check_hint_indexable_seqscan_simple {
157 43     43 1 66 my $self = shift;
158 43         64 my $node = shift;
159              
160 43 100       87 return unless $node->type =~ m{\A(?:Parallel )?Seq Scan\z};
161 42 50       139 return unless $node->estimated_row_width;
162 42 100       119 return unless $node->total_rows_removed;
163 41 50       96 return unless $node->extra_info;
164              
165             # At least 3 pages worth of data is processed
166 41 50       116 return unless ( $node->total_rows + $node->total_rows_removed ) * $node->estimated_row_width > 3 * 8192;
167              
168             # At least 2/3rd of rows were removed
169 41 50       99 return unless $node->total_rows_removed > $node->total_rows * 2;
170              
171 41         65 for my $line ( @{ $node->extra_info } ) {
  41         83  
172 42 100       266 next unless $line =~ m{
173             \A
174             Filter: \s+ \(
175             ("[^"]+"|[a-z0-9_]+)
176             \s
177             (=|<|>|>=|<=)
178             \s
179             (?:
180             ' (?: [^'] | '' ) * '
181             (?: :: (?: "[^"]+" | [a-z0-9_ ]+ ) )?
182             |
183             \d+
184             )
185             \)
186             \z
187             }xms;
188 40         143 my ( $column_used, $operator ) = ( $1, $2 );
189 40         61 push @{ $self->{ 'hints' } }, Pg::Explain::Hinter::Hint->new(
  40         99  
190             'plan' => $self->plan,
191             'node' => $node,
192             'type' => 'INDEXABLE_SEQSCAN_SIMPLE',
193             'details' => [ $column_used, $operator ],
194             );
195 40         78 last;
196              
197             }
198 41         63 my @filter_lines = grep { /^Filter:/ } @{ $node->extra_info };
  82         275  
  41         82  
199 41 50       134 return if 1 != scalar @filter_lines;
200              
201             }
202              
203             =head2 check_hint_indexable_seqscan_multi_equal_and
204              
205             Check if given node matches criteria for INDEXABLE_SEQSCAN_MULTI_EQUAL_AND hint
206              
207             =cut
208              
209             sub check_hint_indexable_seqscan_multi_equal_and {
210 43     43 1 70 my $self = shift;
211 43         60 my $node = shift;
212              
213 43 100       91 return unless $node->type =~ m{\A(?:Parallel )?Seq Scan\z};
214 42 50       127 return unless $node->estimated_row_width;
215 42 100       103 return unless $node->total_rows_removed;
216 41 50       91 return unless $node->extra_info;
217              
218             # At least 3 pages worth of data is processed
219 41 50       109 return unless ( $node->total_rows + $node->total_rows_removed ) * $node->estimated_row_width > 3 * 8192;
220              
221             # At least 2/3rd of rows were removed
222 41 50       99 return unless $node->total_rows_removed > $node->total_rows * 2;
223              
224             # Filter: ((projet = 10317) AND (section = 29) AND (zone = 4))
225 41         132 my $single_condition = qr{
226             \(
227             ("[^"]+"|[a-z0-9_]+)
228             \s+
229             =
230             \s+
231             (
232             ' (?: [^'] | '' ) * '
233             (?: :: (?: "[^"]+" | [a-z0-9_ ]+ ) )?
234             |
235             \d+
236             )
237             \)
238             }xmso;
239              
240 41         59 for my $line ( @{ $node->extra_info } ) {
  41         83  
241 81 100       688 next unless $line =~ m{
242             \A
243             Filter: \s+ \(
244             (
245             ${single_condition}
246             (?:
247             \s+
248             AND
249             \s+
250             ${single_condition}
251             )+
252             )
253             \)
254             \z
255             }xms;
256 1         7 my $all_conditions = $1;
257 1         5 my @cols = ();
258 1         44 while ( $all_conditions =~ m{ ${single_condition} (?= \s+ AND \s+ | \z ) }xg ) {
259 3         26 push @cols, { 'column' => $1, 'value' => $2 };
260             }
261 1         7 push @{ $self->{ 'hints' } }, Pg::Explain::Hinter::Hint->new(
262             'plan' => $self->plan,
263             'node' => $node,
264             'type' => 'INDEXABLE_SEQSCAN_MULTI_EQUAL_AND',
265 1         3 'details' => [ sort { $a->{ 'column' } cmp $b->{ 'column' } } @cols ],
  3         25  
266             );
267 1         4 last;
268              
269             }
270 41         76 my @filter_lines = grep { /^Filter:/ } @{ $node->extra_info };
  82         270  
  41         85  
271 41 50       166 return if 1 != scalar @filter_lines;
272              
273             }
274              
275             =head1 AUTHOR
276              
277             hubert depesz lubaczewski, C<< >>
278              
279             =head1 BUGS
280              
281             Please report any bugs or feature requests to C.
282              
283             =head1 SUPPORT
284              
285             You can find documentation for this module with the perldoc command.
286              
287             perldoc Pg::Explain::Node
288              
289             =head1 COPYRIGHT & LICENSE
290              
291             Copyright 2008-2021 hubert depesz lubaczewski, all rights reserved.
292              
293             This program is free software; you can redistribute it and/or modify it
294             under the same terms as Perl itself.
295              
296             =cut
297              
298             1; # End of Pg::Explain::Hinter