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   3389 use v5.18;
  5         22  
5 5     5   28 use strict;
  5         65  
  5         128  
6 5     5   62 use warnings;
  5         12  
  5         152  
7 5     5   25 use warnings qw( FATAL utf8 );
  5         10  
  5         155  
8 5     5   22 use utf8;
  5         11  
  5         211  
9 5     5   160 use open qw( :std :utf8 );
  5         164  
  5         39  
10 5     5   771 use Unicode::Normalize qw( NFC );
  5         24  
  5         279  
11 5     5   32 use Unicode::Collate;
  5         17  
  5         122  
12 5     5   24 use Encode qw( decode );
  5         9  
  5         359  
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   3215 use Pg::Explain::Hinter::Hint;
  5         12  
  5         7381  
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.4
29              
30             =cut
31              
32             our $VERSION = '2.4';
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 217 my $class = shift;
57 43         94 my $plan = shift;
58 43 50       120 croak( 'Given parameter is not Pg::Explain object!' ) unless 'Pg::Explain' eq ref $plan;
59 43         90 my $self = bless {}, $class;
60 43         122 $self->plan( $plan );
61 43         119 $self->hints( [] );
62 43         110 $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 273 my $self = shift;
74 211 100       448 $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 300 my $self = shift;
86 170 100       391 $self->{ 'hints' } = $_[ 0 ] if 0 < scalar @_;
87 170         602 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 186 my $self = shift;
98 43 100       69 return 1 if 0 < scalar @{ $self->hints };
  43         81  
99 1         4 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 57 my $self = shift;
110 43 100       116 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       96 next unless $node->actual_loops;
116              
117 43         147 $self->check_hint_disk_sort( $node );
118 43         124 $self->check_hint_indexable_seqscan_simple( $node );
119 43         116 $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 55 my $self = shift;
134 43         56 my $node = shift;
135 43 100       92 return unless $node->type eq 'Sort';
136 1 50       5 return unless $node->extra_info;
137 1         3 for my $info ( @{ $node->extra_info } ) {
  1         2  
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         3 push @{ $self->{ 'hints' } }, Pg::Explain::Hinter::Hint->new(
  1         10  
141             'node' => $node,
142             'type' => 'DISK_SORT',
143             'details' => [ $disk_used ],
144             );
145 1         2 last;
146             }
147 1         11 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 71 my $self = shift;
158 43         69 my $node = shift;
159              
160 43 100       86 return unless $node->type =~ m{\A(?:Parallel )?Seq Scan\z};
161 42 50       128 return unless $node->estimated_row_width;
162 42 100       119 return unless $node->total_rows_removed;
163 41 50       92 return unless $node->extra_info;
164              
165             # At least 3 pages worth of data is processed
166 41 50       108 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       102 return unless $node->total_rows_removed > $node->total_rows * 2;
170              
171 41         66 for my $line ( @{ $node->extra_info } ) {
  41         106  
172 42 100       252 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         134 my ( $column_used, $operator ) = ( $1, $2 );
189 40         74 push @{ $self->{ 'hints' } }, Pg::Explain::Hinter::Hint->new(
  40         117  
190             'plan' => $self->plan,
191             'node' => $node,
192             'type' => 'INDEXABLE_SEQSCAN_SIMPLE',
193             'details' => [ $column_used, $operator ],
194             );
195 40         81 last;
196              
197             }
198 41         56 my @filter_lines = grep { /^Filter:/ } @{ $node->extra_info };
  82         285  
  41         91  
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 75 my $self = shift;
211 43         57 my $node = shift;
212              
213 43 100       92 return unless $node->type =~ m{\A(?:Parallel )?Seq Scan\z};
214 42 50       131 return unless $node->estimated_row_width;
215 42 100       103 return unless $node->total_rows_removed;
216 41 50       94 return unless $node->extra_info;
217              
218             # At least 3 pages worth of data is processed
219 41 50       126 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       107 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         80  
241 81 100       701 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         5 my $all_conditions = $1;
257 1         4 my @cols = ();
258 1         55 while ( $all_conditions =~ m{ ${single_condition} (?= \s+ AND \s+ | \z ) }xg ) {
259 3         29 push @cols, { 'column' => $1, 'value' => $2 };
260             }
261 1         4 push @{ $self->{ 'hints' } }, Pg::Explain::Hinter::Hint->new(
262             'plan' => $self->plan,
263             'node' => $node,
264             'type' => 'INDEXABLE_SEQSCAN_MULTI_EQUAL_AND',
265 1         2 'details' => [ sort { $a->{ 'column' } cmp $b->{ 'column' } } @cols ],
  3         13  
266             );
267 1         4 last;
268              
269             }
270 41         75 my @filter_lines = grep { /^Filter:/ } @{ $node->extra_info };
  82         257  
  41         91  
271 41 50       158 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