line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Pg::Explain; |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
# UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/ |
4
|
74
|
|
|
74
|
|
7394032
|
use v5.18; |
|
74
|
|
|
|
|
1251
|
|
5
|
74
|
|
|
74
|
|
433
|
use strict; |
|
74
|
|
|
|
|
166
|
|
|
74
|
|
|
|
|
1813
|
|
6
|
74
|
|
|
74
|
|
407
|
use warnings; |
|
74
|
|
|
|
|
172
|
|
|
74
|
|
|
|
|
2327
|
|
7
|
74
|
|
|
74
|
|
370
|
use warnings qw( FATAL utf8 ); |
|
74
|
|
|
|
|
191
|
|
|
74
|
|
|
|
|
3265
|
|
8
|
74
|
|
|
74
|
|
45411
|
use utf8; |
|
74
|
|
|
|
|
1142
|
|
|
74
|
|
|
|
|
420
|
|
9
|
74
|
|
|
74
|
|
35109
|
use open qw( :std :utf8 ); |
|
74
|
|
|
|
|
91523
|
|
|
74
|
|
|
|
|
494
|
|
10
|
74
|
|
|
74
|
|
53719
|
use Unicode::Normalize qw( NFC ); |
|
74
|
|
|
|
|
162840
|
|
|
74
|
|
|
|
|
5887
|
|
11
|
74
|
|
|
74
|
|
53899
|
use Unicode::Collate; |
|
74
|
|
|
|
|
649300
|
|
|
74
|
|
|
|
|
3082
|
|
12
|
74
|
|
|
74
|
|
43300
|
use Encode qw( decode ); |
|
74
|
|
|
|
|
775932
|
|
|
74
|
|
|
|
|
7868
|
|
13
|
|
|
|
|
|
|
|
14
|
74
|
|
|
74
|
|
548
|
if ( grep /\P{ASCII}/ => @ARGV ) { |
|
74
|
|
|
|
|
157
|
|
|
74
|
|
|
|
|
992
|
|
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
|
74
|
|
|
74
|
|
1571800
|
use Carp; |
|
74
|
|
|
|
|
185
|
|
|
74
|
|
|
|
|
5114
|
|
21
|
74
|
|
|
74
|
|
31314
|
use Clone qw( clone ); |
|
74
|
|
|
|
|
157299
|
|
|
74
|
|
|
|
|
4737
|
|
22
|
74
|
|
|
74
|
|
5360
|
use autodie; |
|
74
|
|
|
|
|
159341
|
|
|
74
|
|
|
|
|
649
|
|
23
|
74
|
|
|
74
|
|
391548
|
use List::Util qw( sum ); |
|
74
|
|
|
|
|
168
|
|
|
74
|
|
|
|
|
7322
|
|
24
|
74
|
|
|
74
|
|
37052
|
use Pg::Explain::StringAnonymizer; |
|
74
|
|
|
|
|
194
|
|
|
74
|
|
|
|
|
2593
|
|
25
|
74
|
|
|
74
|
|
32797
|
use Pg::Explain::FromText; |
|
74
|
|
|
|
|
221
|
|
|
74
|
|
|
|
|
3275
|
|
26
|
74
|
|
|
74
|
|
30768
|
use Pg::Explain::FromYAML; |
|
74
|
|
|
|
|
188
|
|
|
74
|
|
|
|
|
2756
|
|
27
|
74
|
|
|
74
|
|
31246
|
use Pg::Explain::FromJSON; |
|
74
|
|
|
|
|
206
|
|
|
74
|
|
|
|
|
2763
|
|
28
|
74
|
|
|
74
|
|
30891
|
use Pg::Explain::FromXML; |
|
74
|
|
|
|
|
205
|
|
|
74
|
|
|
|
|
345301
|
|
29
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
=head1 NAME |
31
|
|
|
|
|
|
|
|
32
|
|
|
|
|
|
|
Pg::Explain - Object approach at reading explain analyze output |
33
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
=head1 VERSION |
35
|
|
|
|
|
|
|
|
36
|
|
|
|
|
|
|
Version 2.4 |
37
|
|
|
|
|
|
|
|
38
|
|
|
|
|
|
|
=cut |
39
|
|
|
|
|
|
|
|
40
|
|
|
|
|
|
|
our $VERSION = '2.4'; |
41
|
|
|
|
|
|
|
|
42
|
|
|
|
|
|
|
=head1 SYNOPSIS |
43
|
|
|
|
|
|
|
|
44
|
|
|
|
|
|
|
Quick summary of what the module does. |
45
|
|
|
|
|
|
|
|
46
|
|
|
|
|
|
|
Perhaps a little code snippet. |
47
|
|
|
|
|
|
|
|
48
|
|
|
|
|
|
|
use Pg::Explain; |
49
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
my $explain = Pg::Explain->new('source_file' => 'some_file.out'); |
51
|
|
|
|
|
|
|
... |
52
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
my $explain = Pg::Explain->new( |
54
|
|
|
|
|
|
|
'source' => 'Seq Scan on tenk1 (cost=0.00..333.00 rows=10000 width=148)' |
55
|
|
|
|
|
|
|
); |
56
|
|
|
|
|
|
|
... |
57
|
|
|
|
|
|
|
|
58
|
|
|
|
|
|
|
|
59
|
|
|
|
|
|
|
=head1 FUNCTIONS |
60
|
|
|
|
|
|
|
|
61
|
|
|
|
|
|
|
=head2 source_format |
62
|
|
|
|
|
|
|
|
63
|
|
|
|
|
|
|
What is the detected format of source plan. One of: TEXT, JSON, YAML, OR XML. |
64
|
|
|
|
|
|
|
|
65
|
|
|
|
|
|
|
=head2 planning_time |
66
|
|
|
|
|
|
|
|
67
|
|
|
|
|
|
|
How much time PostgreSQL spent planning the query. In milliseconds. |
68
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
=head2 total_buffers |
70
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
All buffers used by query - for planning and execution. Mathematically: sum of planning_buffers and top_level->buffers. |
72
|
|
|
|
|
|
|
|
73
|
|
|
|
|
|
|
=head2 planning_buffers |
74
|
|
|
|
|
|
|
|
75
|
|
|
|
|
|
|
How much buffers PostgreSQL used for planning. Either undef or object of Pg::Explain::Buffers class. |
76
|
|
|
|
|
|
|
|
77
|
|
|
|
|
|
|
=head2 execution_time |
78
|
|
|
|
|
|
|
|
79
|
|
|
|
|
|
|
How much time PostgreSQL spent executing the query. In milliseconds. |
80
|
|
|
|
|
|
|
|
81
|
|
|
|
|
|
|
=head2 total_runtime |
82
|
|
|
|
|
|
|
|
83
|
|
|
|
|
|
|
How much time PostgreSQL spent working on this query. This was part of EXPLAIN OUTPUT only for PostgreSQL 9.3 or older. |
84
|
|
|
|
|
|
|
|
85
|
|
|
|
|
|
|
=head2 trigger_times |
86
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
Information about triggers that were called during execution of this query. Array of hashes, where each hash can contains: |
88
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
=over |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
=item * name - name of the trigger |
92
|
|
|
|
|
|
|
|
93
|
|
|
|
|
|
|
=item * calls - how many times it was called |
94
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
=item * time - total time spent in all executions of this trigger |
96
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
=back |
98
|
|
|
|
|
|
|
|
99
|
|
|
|
|
|
|
=head2 jit |
100
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
Contains information about JIT timings, as object of Pg::Explain::JIT class. |
102
|
|
|
|
|
|
|
|
103
|
|
|
|
|
|
|
If there was no JIT info, it will return undef. |
104
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
=head2 query |
106
|
|
|
|
|
|
|
|
107
|
|
|
|
|
|
|
What query this explain is for. This is available only for auto-explain plans. If not available, it will be undef. |
108
|
|
|
|
|
|
|
|
109
|
|
|
|
|
|
|
=head2 settings |
110
|
|
|
|
|
|
|
|
111
|
|
|
|
|
|
|
If explain contains information about specific settings that were changed in Pg, this hashref will contain it. |
112
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
If there are none - if will be undef. |
114
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
=cut |
116
|
|
|
|
|
|
|
|
117
|
168
|
50
|
|
168
|
1
|
53345
|
sub source_format { my $self = shift; $self->{ 'source_format' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'source_format' }; } |
|
168
|
|
|
|
|
500
|
|
|
168
|
|
|
|
|
806
|
|
118
|
470
|
100
|
|
470
|
1
|
5686
|
sub planning_time { my $self = shift; $self->{ 'planning_time' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'planning_time' }; } |
|
470
|
|
|
|
|
1511
|
|
|
470
|
|
|
|
|
1344
|
|
119
|
190
|
100
|
|
190
|
1
|
309
|
sub planning_buffers { my $self = shift; $self->{ 'planning_buffers' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'planning_buffers' }; } |
|
190
|
|
|
|
|
481
|
|
|
190
|
|
|
|
|
524
|
|
120
|
484
|
100
|
|
484
|
1
|
791
|
sub execution_time { my $self = shift; $self->{ 'execution_time' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'execution_time' }; } |
|
484
|
|
|
|
|
1321
|
|
|
484
|
|
|
|
|
1295
|
|
121
|
239
|
100
|
|
239
|
1
|
388
|
sub total_runtime { my $self = shift; $self->{ 'total_runtime' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'total_runtime' }; } |
|
239
|
|
|
|
|
625
|
|
|
239
|
|
|
|
|
674
|
|
122
|
213
|
100
|
|
213
|
1
|
316
|
sub trigger_times { my $self = shift; $self->{ 'trigger_times' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'trigger_times' }; } |
|
213
|
|
|
|
|
468
|
|
|
213
|
|
|
|
|
726
|
|
123
|
198
|
100
|
|
198
|
1
|
4659
|
sub jit { my $self = shift; $self->{ 'jit' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'jit' }; } |
|
198
|
|
|
|
|
470
|
|
|
198
|
|
|
|
|
592
|
|
124
|
104
|
100
|
|
104
|
1
|
203
|
sub query { my $self = shift; $self->{ 'query' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'query' }; } |
|
104
|
|
|
|
|
343
|
|
|
104
|
|
|
|
|
257
|
|
125
|
72
|
100
|
|
72
|
1
|
157
|
sub settings { my $self = shift; $self->{ 'settings' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'settings' }; } |
|
72
|
|
|
|
|
196
|
|
|
72
|
|
|
|
|
242
|
|
126
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
sub total_buffers { |
128
|
3
|
|
|
3
|
1
|
12
|
my $self = shift; |
129
|
3
|
50
|
|
|
|
9
|
if ( $self->top_node->buffers ) { |
130
|
3
|
100
|
|
|
|
10
|
return $self->top_node->buffers + $self->planning_buffers if $self->planning_buffers; |
131
|
1
|
|
|
|
|
6
|
return $self->top_node->buffers; |
132
|
|
|
|
|
|
|
} |
133
|
0
|
0
|
|
|
|
0
|
return $self->planning_buffers if $self->planning_buffers; |
134
|
0
|
|
|
|
|
0
|
return; |
135
|
|
|
|
|
|
|
} |
136
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
=head2 add_trigger_time |
138
|
|
|
|
|
|
|
|
139
|
|
|
|
|
|
|
Adds new information about trigger time. |
140
|
|
|
|
|
|
|
|
141
|
|
|
|
|
|
|
It will be available at $node->trigger_times (returns arrayref) |
142
|
|
|
|
|
|
|
|
143
|
|
|
|
|
|
|
=cut |
144
|
|
|
|
|
|
|
|
145
|
|
|
|
|
|
|
sub add_trigger_time { |
146
|
26
|
|
|
26
|
1
|
46
|
my $self = shift; |
147
|
26
|
100
|
|
|
|
56
|
if ( $self->trigger_times ) { |
148
|
16
|
|
|
|
|
27
|
push @{ $self->trigger_times }, @_; |
|
16
|
|
|
|
|
32
|
|
149
|
|
|
|
|
|
|
} |
150
|
|
|
|
|
|
|
else { |
151
|
10
|
|
|
|
|
40
|
$self->trigger_times( [ @_ ] ); |
152
|
|
|
|
|
|
|
} |
153
|
26
|
|
|
|
|
73
|
return; |
154
|
|
|
|
|
|
|
} |
155
|
|
|
|
|
|
|
|
156
|
|
|
|
|
|
|
=head2 runtime |
157
|
|
|
|
|
|
|
|
158
|
|
|
|
|
|
|
How long did the query run. Tries to get the value from various sources (total_runtime, execution_time, or top_node->actual_time_last). |
159
|
|
|
|
|
|
|
|
160
|
|
|
|
|
|
|
=cut |
161
|
|
|
|
|
|
|
|
162
|
|
|
|
|
|
|
sub runtime { |
163
|
5
|
|
|
5
|
1
|
80
|
my $self = shift; |
164
|
|
|
|
|
|
|
|
165
|
5
|
|
100
|
|
|
13
|
return $self->total_runtime // $self->execution_time // $self->top_node->actual_time_last; |
|
|
|
100
|
|
|
|
|
166
|
|
|
|
|
|
|
} |
167
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
=head2 node |
169
|
|
|
|
|
|
|
|
170
|
|
|
|
|
|
|
Returns node with given id from current explain. |
171
|
|
|
|
|
|
|
|
172
|
|
|
|
|
|
|
If there is second argument present, and it's Pg::Explain::Node object, it sets internal cache for this id and this node. |
173
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
=cut |
175
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
sub node { |
177
|
1575
|
|
|
1575
|
1
|
2462
|
my $self = shift; |
178
|
1575
|
|
|
|
|
2462
|
my $id = shift; |
179
|
1575
|
50
|
|
|
|
3314
|
return unless defined $id; |
180
|
1575
|
|
|
|
|
2276
|
my $node = shift; |
181
|
1575
|
100
|
|
|
|
5684
|
$self->{ 'node_by_id' }->{ $id } = $node if defined $node; |
182
|
1575
|
|
|
|
|
3482
|
return $self->{ 'node_by_id' }->{ $id }; |
183
|
|
|
|
|
|
|
} |
184
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
=head2 source |
186
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
Returns original source (text version) of explain. |
188
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
=cut |
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
sub source { |
192
|
512
|
|
|
512
|
1
|
18673
|
return shift->{ 'source' }; |
193
|
|
|
|
|
|
|
} |
194
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
=head2 source_filtered |
196
|
|
|
|
|
|
|
|
197
|
|
|
|
|
|
|
Returns filtered source explain. |
198
|
|
|
|
|
|
|
|
199
|
|
|
|
|
|
|
Currently there are only two filters: |
200
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
=over |
202
|
|
|
|
|
|
|
|
203
|
|
|
|
|
|
|
=item * remove quotes added by pgAdmin3 |
204
|
|
|
|
|
|
|
|
205
|
|
|
|
|
|
|
=item * remove + character at the end of line, added by default psql config. |
206
|
|
|
|
|
|
|
|
207
|
|
|
|
|
|
|
=back |
208
|
|
|
|
|
|
|
|
209
|
|
|
|
|
|
|
=cut |
210
|
|
|
|
|
|
|
|
211
|
|
|
|
|
|
|
sub source_filtered { |
212
|
512
|
|
|
512
|
1
|
832
|
my $self = shift; |
213
|
|
|
|
|
|
|
|
214
|
512
|
|
|
|
|
1303
|
my $filtered = ''; |
215
|
|
|
|
|
|
|
|
216
|
|
|
|
|
|
|
# use default variable, to avoid having to type variable name in all regexps below |
217
|
512
|
|
|
|
|
1306
|
for ( split /\r?\n/, $self->source ) { |
218
|
|
|
|
|
|
|
|
219
|
|
|
|
|
|
|
# Remove separator lines from various types of borders |
220
|
13720
|
100
|
|
|
|
26278
|
next if /^\+-+\+\z/; |
221
|
13678
|
100
|
|
|
|
31083
|
next if /^[-─═]+\z/; |
222
|
13533
|
100
|
|
|
|
24761
|
next if /^(?:├|╟|╠|╞)[─═]+(?:┤|╢|╣|╡)\z/; |
223
|
|
|
|
|
|
|
|
224
|
|
|
|
|
|
|
# Remove more horizontal lines |
225
|
13501
|
50
|
|
|
|
22055
|
next if /^\+-+\+\z/; |
226
|
13501
|
100
|
|
|
|
21912
|
next if /^└─+┘\z/; |
227
|
13485
|
100
|
|
|
|
21658
|
next if /^╚═+╝\z/; |
228
|
13469
|
100
|
|
|
|
21766
|
next if /^┌─+┐\z/; |
229
|
13453
|
100
|
|
|
|
21265
|
next if /^╔═+╗\z/; |
230
|
|
|
|
|
|
|
|
231
|
|
|
|
|
|
|
# Remove frames around, handles |, ║, │ |
232
|
13437
|
|
|
|
|
28469
|
s/^(\||║|│)(.*)\1\z/$2/; |
233
|
|
|
|
|
|
|
|
234
|
|
|
|
|
|
|
# Remove quotes around lines, both ' and " |
235
|
13437
|
|
|
|
|
25797
|
s/^(["'])(.*)\1\z/$2/; |
236
|
|
|
|
|
|
|
|
237
|
|
|
|
|
|
|
# Remove "+" line continuations |
238
|
13437
|
|
|
|
|
29591
|
s/\s*\+\z//; |
239
|
|
|
|
|
|
|
|
240
|
|
|
|
|
|
|
# Remove "↵" line continuations |
241
|
13437
|
|
|
|
|
32392
|
s/\s*↵\z//; |
242
|
|
|
|
|
|
|
|
243
|
|
|
|
|
|
|
# Remove "query plan" header |
244
|
13437
|
100
|
|
|
|
25490
|
next if /^\s*QUERY PLAN\s*\z/; |
245
|
|
|
|
|
|
|
|
246
|
|
|
|
|
|
|
# Remove rowcount |
247
|
13233
|
100
|
|
|
|
23685
|
next if /^\(\d+ rows?\)\z/; |
248
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
# Accumulate filtered source |
250
|
13034
|
|
|
|
|
27258
|
$filtered .= $_ . "\n"; |
251
|
|
|
|
|
|
|
} |
252
|
|
|
|
|
|
|
|
253
|
512
|
|
|
|
|
3115
|
return $filtered; |
254
|
|
|
|
|
|
|
} |
255
|
|
|
|
|
|
|
|
256
|
|
|
|
|
|
|
=head2 new |
257
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
Object constructor. |
259
|
|
|
|
|
|
|
|
260
|
|
|
|
|
|
|
Takes one of (only one!) (source, source_file) parameters, and either parses it from given source, or first reads given file. |
261
|
|
|
|
|
|
|
|
262
|
|
|
|
|
|
|
=cut |
263
|
|
|
|
|
|
|
|
264
|
|
|
|
|
|
|
sub new { |
265
|
516
|
|
|
516
|
1
|
1651785
|
my $class = shift; |
266
|
516
|
|
|
|
|
1405
|
my $self = bless {}, $class; |
267
|
516
|
|
|
|
|
967
|
my %args; |
268
|
516
|
100
|
|
|
|
1775
|
if ( 0 == scalar @_ ) { |
269
|
1
|
|
|
|
|
23
|
croak( 'One of (source, source_file) parameters has to be provided)' ); |
270
|
|
|
|
|
|
|
} |
271
|
515
|
50
|
|
|
|
2302
|
if ( 1 == scalar @_ ) { |
|
|
50
|
|
|
|
|
|
272
|
0
|
0
|
|
|
|
0
|
if ( 'HASH' eq ref $_[ 0 ] ) { |
273
|
0
|
|
|
|
|
0
|
%args = @{ $_[ 0 ] }; |
|
0
|
|
|
|
|
0
|
|
274
|
|
|
|
|
|
|
} |
275
|
|
|
|
|
|
|
else { |
276
|
0
|
|
|
|
|
0
|
croak( 'One of (source, source_file) parameters has to be provided)' ); |
277
|
|
|
|
|
|
|
} |
278
|
|
|
|
|
|
|
} |
279
|
|
|
|
|
|
|
elsif ( 1 == ( scalar( @_ ) % 2 ) ) { |
280
|
0
|
|
|
|
|
0
|
croak( 'One of (source, source_file) parameters has to be provided)' ); |
281
|
|
|
|
|
|
|
} |
282
|
|
|
|
|
|
|
else { |
283
|
515
|
|
|
|
|
1615
|
%args = @_; |
284
|
|
|
|
|
|
|
} |
285
|
|
|
|
|
|
|
|
286
|
515
|
100
|
|
|
|
1710
|
if ( $args{ 'source_file' } ) { |
|
|
50
|
|
|
|
|
|
287
|
179
|
100
|
|
|
|
530
|
croak( 'Only one of (source, source_file) parameters has to be provided)' ) if $args{ 'source' }; |
288
|
178
|
|
|
|
|
497
|
$self->{ 'source_file' } = $args{ 'source_file' }; |
289
|
178
|
|
|
|
|
559
|
$self->_read_source_from_file(); |
290
|
|
|
|
|
|
|
} |
291
|
|
|
|
|
|
|
elsif ( $args{ 'source' } ) { |
292
|
336
|
100
|
|
|
|
1607
|
if ( Encode::is_utf8( $args{ 'source' } ) ) { |
293
|
47
|
|
|
|
|
132
|
$self->{ 'source' } = $args{ 'source' }; |
294
|
|
|
|
|
|
|
} |
295
|
|
|
|
|
|
|
else { |
296
|
289
|
|
|
|
|
1213
|
$self->{ 'source' } = decode( 'UTF-8', $args{ 'source' } ); |
297
|
|
|
|
|
|
|
} |
298
|
|
|
|
|
|
|
} |
299
|
|
|
|
|
|
|
else { |
300
|
0
|
|
|
|
|
0
|
croak( 'One of (source, source_file) parameters has to be provided)' ); |
301
|
|
|
|
|
|
|
} |
302
|
|
|
|
|
|
|
|
303
|
|
|
|
|
|
|
# Initialize jit to undef |
304
|
513
|
|
|
|
|
30117
|
$self->{ 'jit' } = undef; |
305
|
|
|
|
|
|
|
|
306
|
|
|
|
|
|
|
# Initialize node_by_id hash to empty |
307
|
513
|
|
|
|
|
1151
|
$self->{ 'node_by_id' } = {}; |
308
|
|
|
|
|
|
|
|
309
|
513
|
|
|
|
|
1805
|
return $self; |
310
|
|
|
|
|
|
|
} |
311
|
|
|
|
|
|
|
|
312
|
|
|
|
|
|
|
=head2 top_node |
313
|
|
|
|
|
|
|
|
314
|
|
|
|
|
|
|
This method returns the top node of parsed plan. |
315
|
|
|
|
|
|
|
|
316
|
|
|
|
|
|
|
For example - in this plan: |
317
|
|
|
|
|
|
|
|
318
|
|
|
|
|
|
|
QUERY PLAN |
319
|
|
|
|
|
|
|
-------------------------------------------------------------- |
320
|
|
|
|
|
|
|
Limit (cost=0.00..0.01 rows=1 width=4) |
321
|
|
|
|
|
|
|
-> Seq Scan on test (cost=0.00..14.00 rows=1000 width=4) |
322
|
|
|
|
|
|
|
|
323
|
|
|
|
|
|
|
top_node is Pg::Explain::Node element with type set to 'Limit'. |
324
|
|
|
|
|
|
|
|
325
|
|
|
|
|
|
|
Generally every output of plans should start with ->top_node(), and descend |
326
|
|
|
|
|
|
|
recursively in it, using subplans(), initplans() and sub_nodes() methods. |
327
|
|
|
|
|
|
|
|
328
|
|
|
|
|
|
|
=cut |
329
|
|
|
|
|
|
|
|
330
|
|
|
|
|
|
|
sub top_node { |
331
|
4594
|
|
|
4594
|
1
|
181626
|
my $self = shift; |
332
|
4594
|
100
|
|
|
|
9421
|
$self->parse_source() unless $self->{ 'top_node' }; |
333
|
4594
|
|
|
|
|
12440
|
return $self->{ 'top_node' }; |
334
|
|
|
|
|
|
|
} |
335
|
|
|
|
|
|
|
|
336
|
|
|
|
|
|
|
=head2 parse_source |
337
|
|
|
|
|
|
|
|
338
|
|
|
|
|
|
|
Internally (from ->BUILD()) called function which checks which parser to use |
339
|
|
|
|
|
|
|
(text, json, xml, yaml), runs appropriate function, and stores top level |
340
|
|
|
|
|
|
|
node in $self->top_node. |
341
|
|
|
|
|
|
|
|
342
|
|
|
|
|
|
|
=cut |
343
|
|
|
|
|
|
|
|
344
|
|
|
|
|
|
|
sub parse_source { |
345
|
512
|
|
|
512
|
1
|
168096
|
my $self = shift; |
346
|
|
|
|
|
|
|
|
347
|
512
|
|
|
|
|
1431
|
my $source = $self->source_filtered; |
348
|
|
|
|
|
|
|
|
349
|
512
|
|
|
|
|
962
|
my $parser; |
350
|
|
|
|
|
|
|
|
351
|
512
|
100
|
|
|
|
4932
|
if ( $source =~ m{^\s*}m ) { |
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
352
|
|
|
|
|
|
|
|
353
|
|
|
|
|
|
|
# Format used by both explain command and autoexplain module |
354
|
67
|
|
|
|
|
263
|
$self->{ 'source_format' } = 'XML'; |
355
|
67
|
|
|
|
|
705
|
$parser = Pg::Explain::FromXML->new(); |
356
|
|
|
|
|
|
|
} |
357
|
|
|
|
|
|
|
elsif ( $source =~ m{ ^ \s* \[ \s* \{ \s* "Plan" \s* : \s* \{ }xms ) { |
358
|
|
|
|
|
|
|
|
359
|
|
|
|
|
|
|
# Format used by explain command |
360
|
70
|
|
|
|
|
232
|
$self->{ 'source_format' } = 'JSON'; |
361
|
70
|
|
|
|
|
630
|
$parser = Pg::Explain::FromJSON->new(); |
362
|
|
|
|
|
|
|
} |
363
|
|
|
|
|
|
|
elsif ( $source =~ m{ ^ \s* \{ \s* "Query \s+ Text" \s* : \s* ".*", \s* "Plan" \s* : \s* \{ .* \} \s* \z }xms ) { |
364
|
|
|
|
|
|
|
|
365
|
|
|
|
|
|
|
# Format used by autoexplain module |
366
|
4
|
|
|
|
|
14
|
$self->{ 'source_format' } = 'JSON'; |
367
|
4
|
|
|
|
|
56
|
$parser = Pg::Explain::FromJSON->new(); |
368
|
|
|
|
|
|
|
} |
369
|
|
|
|
|
|
|
elsif ( $source =~ m{ ^ \s* - \s+ Plan: \s* \n }xms ) { |
370
|
|
|
|
|
|
|
|
371
|
|
|
|
|
|
|
# Format used by explain command |
372
|
66
|
|
|
|
|
218
|
$self->{ 'source_format' } = 'YAML'; |
373
|
66
|
|
|
|
|
563
|
$parser = Pg::Explain::FromYAML->new(); |
374
|
|
|
|
|
|
|
} |
375
|
|
|
|
|
|
|
elsif ( $source =~ m{ ^ \s* Query \s+ Text: \s+ ".*" \s+ Plan: \s* \n }xms ) { |
376
|
|
|
|
|
|
|
|
377
|
|
|
|
|
|
|
# Format used by autoexplain module |
378
|
4
|
|
|
|
|
15
|
$self->{ 'source_format' } = 'YAML'; |
379
|
4
|
|
|
|
|
47
|
$parser = Pg::Explain::FromYAML->new(); |
380
|
|
|
|
|
|
|
} |
381
|
|
|
|
|
|
|
else { |
382
|
|
|
|
|
|
|
# Format used by both explain command and autoexplain module |
383
|
301
|
|
|
|
|
783
|
$self->{ 'source_format' } = 'TEXT'; |
384
|
301
|
|
|
|
|
2141
|
$parser = Pg::Explain::FromText->new(); |
385
|
|
|
|
|
|
|
} |
386
|
|
|
|
|
|
|
|
387
|
512
|
|
|
|
|
2037
|
$parser->explain( $self ); |
388
|
|
|
|
|
|
|
|
389
|
512
|
|
|
|
|
1952
|
$self->{ 'top_node' } = $parser->parse_source( $source ); |
390
|
|
|
|
|
|
|
|
391
|
512
|
|
|
|
|
1914
|
$self->check_for_parallelism(); |
392
|
|
|
|
|
|
|
|
393
|
512
|
|
|
|
|
1634
|
$self->check_for_exclusive_time_fixes(); |
394
|
|
|
|
|
|
|
|
395
|
512
|
|
|
|
|
2838
|
return; |
396
|
|
|
|
|
|
|
} |
397
|
|
|
|
|
|
|
|
398
|
|
|
|
|
|
|
=head2 check_for_exclusive_time_fixes |
399
|
|
|
|
|
|
|
|
400
|
|
|
|
|
|
|
Certain types of nodes (CTE Scans, and InitPlans) can cause issues with "naive" calculations of node exclusive time. |
401
|
|
|
|
|
|
|
|
402
|
|
|
|
|
|
|
To fix that whole tree will be scanned, and, if neccessary, node->exclusive_fix will be modified. |
403
|
|
|
|
|
|
|
|
404
|
|
|
|
|
|
|
=cut |
405
|
|
|
|
|
|
|
|
406
|
|
|
|
|
|
|
sub check_for_exclusive_time_fixes { |
407
|
512
|
|
|
512
|
1
|
867
|
my $self = shift; |
408
|
512
|
|
|
|
|
1471
|
$self->check_for_exclusive_time_fixes_cte(); |
409
|
512
|
|
|
|
|
1397
|
$self->check_for_exclusive_time_fixes_init(); |
410
|
|
|
|
|
|
|
} |
411
|
|
|
|
|
|
|
|
412
|
|
|
|
|
|
|
=head2 check_for_exclusive_time_fixes_cte |
413
|
|
|
|
|
|
|
|
414
|
|
|
|
|
|
|
Modifies node->exclusive_fix according to times that were used by CTEs. |
415
|
|
|
|
|
|
|
|
416
|
|
|
|
|
|
|
=cut |
417
|
|
|
|
|
|
|
|
418
|
|
|
|
|
|
|
sub check_for_exclusive_time_fixes_cte { |
419
|
512
|
|
|
512
|
1
|
817
|
my $self = shift; |
420
|
|
|
|
|
|
|
|
421
|
|
|
|
|
|
|
# Safeguard against endless loop in some edge cases. |
422
|
512
|
50
|
|
|
|
1245
|
return unless defined $self->{ 'top_node' }; |
423
|
|
|
|
|
|
|
|
424
|
|
|
|
|
|
|
# There is no point in checking if the plan is not analyzed. |
425
|
512
|
100
|
|
|
|
1213
|
return unless $self->top_node->is_analyzed; |
426
|
|
|
|
|
|
|
|
427
|
|
|
|
|
|
|
# Find nodes that have any ctes in them |
428
|
413
|
100
|
|
|
|
1252
|
my @nodes_with_cte = grep { $_->ctes && 0 < scalar keys %{ $_->ctes } } ( $self->top_node, $self->top_node->all_recursive_subnodes ); |
|
1291
|
|
|
|
|
2727
|
|
|
22
|
|
|
|
|
71
|
|
429
|
|
|
|
|
|
|
|
430
|
|
|
|
|
|
|
# For each node with cte in it... |
431
|
413
|
|
|
|
|
1148
|
for my $node ( @nodes_with_cte ) { |
432
|
|
|
|
|
|
|
|
433
|
|
|
|
|
|
|
# Find all nodes that are 'CTE Scan' - from given node, and all of its subnodes (recursively) |
434
|
22
|
|
|
|
|
79
|
my @cte_scans = grep { $_->type eq 'CTE Scan' } ( $node, $node->all_recursive_subnodes ); |
|
190
|
|
|
|
|
378
|
|
435
|
22
|
50
|
|
|
|
74
|
next if 0 == scalar @cte_scans; |
436
|
|
|
|
|
|
|
|
437
|
|
|
|
|
|
|
# Iterate over defined ctes |
438
|
22
|
|
|
|
|
49
|
while ( my ( $cte_name, $cte_node ) = each %{ $node->ctes } ) { |
|
53
|
|
|
|
|
127
|
|
439
|
|
|
|
|
|
|
|
440
|
|
|
|
|
|
|
# Find all CTE Scans that were scanning current CTE |
441
|
31
|
|
|
|
|
66
|
my @matching_cte_scans = grep { $_->scan_on->{ 'cte_name' } eq $cte_name } @cte_scans; |
|
58
|
|
|
|
|
124
|
|
442
|
31
|
50
|
|
|
|
79
|
next if 0 == scalar @matching_cte_scans; |
443
|
|
|
|
|
|
|
|
444
|
|
|
|
|
|
|
# How much time did Pg spend in given CTE itself |
445
|
31
|
|
|
|
|
111
|
my $cte_total_time = $cte_node->total_inclusive_time; |
446
|
|
|
|
|
|
|
|
447
|
|
|
|
|
|
|
# How much time did all the CTE Scans used |
448
|
31
|
|
50
|
|
|
64
|
my $total_time_of_scans = sum( map { $_->total_inclusive_time // 0 } @matching_cte_scans ); |
|
36
|
|
|
|
|
100
|
|
449
|
|
|
|
|
|
|
|
450
|
|
|
|
|
|
|
# Don't fail on divide by 0, and don't warn on undef |
451
|
31
|
50
|
|
|
|
77
|
next unless $total_time_of_scans; |
452
|
31
|
100
|
|
|
|
123
|
next unless $cte_total_time; |
453
|
|
|
|
|
|
|
|
454
|
|
|
|
|
|
|
# Subtract exclusive time proportionally. |
455
|
23
|
|
|
|
|
54
|
for my $scan ( grep { $_->total_inclusive_time } @matching_cte_scans ) { |
|
28
|
|
|
|
|
69
|
|
456
|
28
|
|
|
|
|
98
|
$scan->exclusive_fix( $scan->exclusive_fix - ( $scan->total_inclusive_time / $total_time_of_scans ) * $cte_total_time ); |
457
|
|
|
|
|
|
|
} |
458
|
|
|
|
|
|
|
} |
459
|
|
|
|
|
|
|
} |
460
|
413
|
|
|
|
|
751
|
return; |
461
|
|
|
|
|
|
|
} |
462
|
|
|
|
|
|
|
|
463
|
|
|
|
|
|
|
=head2 check_for_exclusive_time_fixes_init |
464
|
|
|
|
|
|
|
|
465
|
|
|
|
|
|
|
Modifies node->exclusive_fix according to times that were used by InitScans. |
466
|
|
|
|
|
|
|
|
467
|
|
|
|
|
|
|
=cut |
468
|
|
|
|
|
|
|
|
469
|
|
|
|
|
|
|
sub check_for_exclusive_time_fixes_init { |
470
|
512
|
|
|
512
|
1
|
876
|
my $self = shift; |
471
|
|
|
|
|
|
|
|
472
|
|
|
|
|
|
|
# Safeguard against endless loop in some edge cases. |
473
|
512
|
50
|
|
|
|
1354
|
return unless defined $self->{ 'top_node' }; |
474
|
|
|
|
|
|
|
|
475
|
|
|
|
|
|
|
# There is no point in checking if the plan is not analyzed. |
476
|
512
|
100
|
|
|
|
1086
|
return unless $self->top_node->is_analyzed; |
477
|
|
|
|
|
|
|
|
478
|
|
|
|
|
|
|
# Find nodes that have any init plans in them |
479
|
413
|
100
|
|
|
|
1217
|
my @nodes_with_init = grep { $_->initplans && 0 < scalar @{ $_->initplans } } ( $self->top_node, $self->top_node->all_recursive_subnodes ); |
|
1291
|
|
|
|
|
2593
|
|
|
34
|
|
|
|
|
88
|
|
480
|
|
|
|
|
|
|
|
481
|
|
|
|
|
|
|
# Check them all, one by one, to build "init-plan-visibility" info |
482
|
413
|
|
|
|
|
1097
|
for my $parent ( @nodes_with_init ) { |
483
|
|
|
|
|
|
|
|
484
|
|
|
|
|
|
|
# Nodes that see what initplan returned even if they don't refer to returned $* |
485
|
34
|
|
|
|
|
137
|
my @all_implicits = map { $_->id } ( $parent, $parent->all_recursive_subnodes ); |
|
113
|
|
|
|
|
242
|
|
486
|
34
|
|
|
|
|
81
|
my %skip_self_implicit = (); |
487
|
|
|
|
|
|
|
|
488
|
|
|
|
|
|
|
# Scan all initplans |
489
|
34
|
|
|
|
|
75
|
for my $idx ( 0 .. $#{ $parent->initplans } ) { |
|
34
|
|
|
|
|
90
|
|
490
|
37
|
|
|
|
|
106
|
my $initnode = $parent->initplans->[ $idx ]; |
491
|
|
|
|
|
|
|
|
492
|
|
|
|
|
|
|
# There is no point in adjusting things for no-time. |
493
|
37
|
100
|
|
|
|
137
|
next unless $initnode->total_inclusive_time; |
494
|
|
|
|
|
|
|
|
495
|
|
|
|
|
|
|
# Place to store implicit and explicit nodes |
496
|
36
|
|
|
|
|
107
|
my @implicitnodes = (); |
497
|
36
|
|
|
|
|
79
|
my @explicitnodes = (); |
498
|
|
|
|
|
|
|
|
499
|
36
|
|
|
|
|
137
|
my $explicit_re; |
500
|
|
|
|
|
|
|
|
501
|
|
|
|
|
|
|
# If there is metainfo, we can build regexp to find nodes explicitly using this init |
502
|
36
|
100
|
|
|
|
171
|
if ( $parent->initplans_metainfo->[ $idx ] ) { |
503
|
|
|
|
|
|
|
|
504
|
|
|
|
|
|
|
# List of $* variables that this initplan returns |
505
|
22
|
|
|
|
|
61
|
my $returns_string = $parent->initplans_metainfo->[ $idx ]->{ 'returns' }; |
506
|
22
|
|
|
|
|
52
|
my @returns_numbers = (); |
507
|
22
|
|
|
|
|
108
|
for my $element ( split /,/, $returns_string ) { |
508
|
30
|
50
|
|
|
|
263
|
push @returns_numbers, $element if $element =~ s/\A\$(\d+)\z/$1/; |
509
|
|
|
|
|
|
|
} |
510
|
22
|
|
|
|
|
97
|
my $returns = join( '|', @returns_numbers ); |
511
|
|
|
|
|
|
|
|
512
|
|
|
|
|
|
|
# Regular expression to check in extra-info for nodes. |
513
|
22
|
|
|
|
|
501
|
$explicit_re = qr{\$(?:${returns})(?!\d)}; |
514
|
|
|
|
|
|
|
} |
515
|
|
|
|
|
|
|
|
516
|
|
|
|
|
|
|
# Add current node, and it's kids to skip list |
517
|
36
|
|
|
|
|
151
|
for my $skip_node ( $initnode, $initnode->all_recursive_subnodes ) { |
518
|
61
|
|
|
|
|
170
|
$skip_self_implicit{ $skip_node->id } = 1; |
519
|
|
|
|
|
|
|
} |
520
|
|
|
|
|
|
|
|
521
|
|
|
|
|
|
|
# Iterate over all nodes that could have used data from this initplan |
522
|
36
|
|
|
|
|
90
|
for my $user_id ( grep { !$skip_self_implicit{ $_ } } @all_implicits ) { |
|
118
|
|
|
|
|
296
|
|
523
|
|
|
|
|
|
|
|
524
|
54
|
|
|
|
|
163
|
my $user = $self->node( $user_id ); |
525
|
|
|
|
|
|
|
|
526
|
|
|
|
|
|
|
# Add node to implicit ones,always |
527
|
54
|
|
|
|
|
118
|
push @implicitnodes, $user; |
528
|
|
|
|
|
|
|
|
529
|
|
|
|
|
|
|
# If there is explicit_re, try to find what is using this int explicitly |
530
|
54
|
100
|
|
|
|
172
|
next unless $explicit_re; |
531
|
32
|
100
|
|
|
|
102
|
next unless $user->extra_info; |
532
|
23
|
|
|
|
|
49
|
my $full_extra_info = join( "\n", @{ $user->extra_info } ); |
|
23
|
|
|
|
|
60
|
|
533
|
23
|
100
|
|
|
|
250
|
push @explicitnodes, $user if $full_extra_info =~ $explicit_re; |
534
|
|
|
|
|
|
|
} |
535
|
|
|
|
|
|
|
|
536
|
|
|
|
|
|
|
# Total times |
537
|
36
|
|
50
|
|
|
88
|
my $implicittime = sum( map { $_->total_exclusive_time // 0 } @implicitnodes ) // 0; |
|
54
|
|
50
|
|
|
157
|
|
538
|
36
|
|
50
|
|
|
152
|
my $explicittime = sum( map { $_->total_exclusive_time // 0 } @explicitnodes ) // 0; |
|
18
|
|
100
|
|
|
48
|
|
539
|
|
|
|
|
|
|
|
540
|
|
|
|
|
|
|
# Where to adjusct exclusive time |
541
|
36
|
|
|
|
|
93
|
my @adjust_these = (); |
542
|
36
|
|
|
|
|
73
|
my $ratio; |
543
|
36
|
100
|
66
|
|
|
233
|
if ( ( 0 < scalar @explicitnodes ) |
|
|
50
|
|
|
|
|
|
544
|
|
|
|
|
|
|
&& ( $explicittime > $initnode->total_inclusive_time ) ) |
545
|
|
|
|
|
|
|
{ |
546
|
17
|
|
|
|
|
51
|
@adjust_these = @explicitnodes; |
547
|
17
|
|
|
|
|
48
|
$ratio = $initnode->total_inclusive_time / $explicittime; |
548
|
|
|
|
|
|
|
} |
549
|
|
|
|
|
|
|
elsif ( $implicittime > $initnode->total_inclusive_time ) { |
550
|
19
|
|
|
|
|
42
|
@adjust_these = @implicitnodes; |
551
|
19
|
|
|
|
|
55
|
$ratio = $initnode->total_inclusive_time / $implicittime; |
552
|
|
|
|
|
|
|
} |
553
|
|
|
|
|
|
|
|
554
|
|
|
|
|
|
|
# Actually adjust exclusive times |
555
|
36
|
|
|
|
|
129
|
for my $node ( @adjust_these ) { |
556
|
48
|
50
|
|
|
|
137
|
next unless $node->total_exclusive_time; |
557
|
48
|
|
|
|
|
132
|
my $adjust = $ratio * $node->total_exclusive_time; |
558
|
48
|
|
|
|
|
149
|
$node->exclusive_fix( $node->exclusive_fix - $adjust ); |
559
|
|
|
|
|
|
|
} |
560
|
|
|
|
|
|
|
} |
561
|
|
|
|
|
|
|
} |
562
|
|
|
|
|
|
|
|
563
|
413
|
|
|
|
|
771
|
return; |
564
|
|
|
|
|
|
|
} |
565
|
|
|
|
|
|
|
|
566
|
|
|
|
|
|
|
=head2 check_for_parallelism |
567
|
|
|
|
|
|
|
|
568
|
|
|
|
|
|
|
Handles parallelism by setting "force_loops" if plan is analyzed and there are gather nodes. |
569
|
|
|
|
|
|
|
|
570
|
|
|
|
|
|
|
Generally, for each |
571
|
|
|
|
|
|
|
|
572
|
|
|
|
|
|
|
=cut |
573
|
|
|
|
|
|
|
|
574
|
|
|
|
|
|
|
sub check_for_parallelism { |
575
|
512
|
|
|
512
|
1
|
870
|
my $self = shift; |
576
|
|
|
|
|
|
|
|
577
|
|
|
|
|
|
|
# Safeguard against endless loop in some edge cases. |
578
|
512
|
50
|
|
|
|
1493
|
return unless defined $self->{ 'top_node' }; |
579
|
|
|
|
|
|
|
|
580
|
|
|
|
|
|
|
# There is no point in checking if the plan is not analyzed. |
581
|
512
|
100
|
|
|
|
1387
|
return unless $self->top_node->is_analyzed; |
582
|
|
|
|
|
|
|
|
583
|
|
|
|
|
|
|
# @nodes will contain list of nodes to check if they are Gather |
584
|
413
|
|
|
|
|
1110
|
my @nodes = ( [ 1, $self->top_node ] ); |
585
|
|
|
|
|
|
|
|
586
|
413
|
|
|
|
|
1412
|
while ( my $node_info = shift @nodes ) { |
587
|
|
|
|
|
|
|
|
588
|
1291
|
|
|
|
|
2123
|
my $workers = $node_info->[ 0 ]; |
589
|
1291
|
|
|
|
|
1763
|
my $node = $node_info->[ 1 ]; |
590
|
|
|
|
|
|
|
|
591
|
|
|
|
|
|
|
# Set workers. |
592
|
1291
|
|
|
|
|
3551
|
$node->workers( $workers ); |
593
|
|
|
|
|
|
|
|
594
|
|
|
|
|
|
|
# These sub-nodes don't get workers. |
595
|
1291
|
100
|
|
|
|
2812
|
push @nodes, map { [ $workers, $_ ] } @{ $node->initplans } if $node->initplans; |
|
37
|
|
|
|
|
118
|
|
|
34
|
|
|
|
|
103
|
|
596
|
1291
|
100
|
|
|
|
2971
|
push @nodes, map { [ $workers, $_ ] } @{ $node->subplans } if $node->subplans; |
|
29
|
|
|
|
|
83
|
|
|
24
|
|
|
|
|
90
|
|
597
|
1291
|
100
|
|
|
|
2841
|
push @nodes, map { [ $workers, $_ ] } values %{ $node->ctes } if $node->ctes; |
|
31
|
|
|
|
|
106
|
|
|
22
|
|
|
|
|
70
|
|
598
|
|
|
|
|
|
|
|
599
|
|
|
|
|
|
|
# If there are workers launched, set it as new workers value for recursive set. |
600
|
1291
|
100
|
|
|
|
2890
|
$workers = 1 + $node->workers_launched if defined $node->workers_launched; |
601
|
|
|
|
|
|
|
|
602
|
|
|
|
|
|
|
# These things get new workers |
603
|
1291
|
100
|
|
|
|
2687
|
push @nodes, map { [ $workers, $_ ] } @{ $node->sub_nodes } if $node->sub_nodes; |
|
781
|
|
|
|
|
2615
|
|
|
544
|
|
|
|
|
1233
|
|
604
|
|
|
|
|
|
|
} |
605
|
413
|
|
|
|
|
844
|
return; |
606
|
|
|
|
|
|
|
} |
607
|
|
|
|
|
|
|
|
608
|
|
|
|
|
|
|
=head2 _read_source_from_file |
609
|
|
|
|
|
|
|
|
610
|
|
|
|
|
|
|
Helper function to read source from file. |
611
|
|
|
|
|
|
|
|
612
|
|
|
|
|
|
|
=cut |
613
|
|
|
|
|
|
|
|
614
|
|
|
|
|
|
|
sub _read_source_from_file { |
615
|
178
|
|
|
178
|
|
304
|
my $self = shift; |
616
|
|
|
|
|
|
|
|
617
|
178
|
|
|
|
|
786
|
open my $fh, '<', $self->{ 'source_file' }; |
618
|
177
|
|
|
|
|
72117
|
local $/ = undef; |
619
|
177
|
|
|
|
|
9469
|
my $content = <$fh>; |
620
|
177
|
|
|
|
|
1174
|
close $fh; |
621
|
|
|
|
|
|
|
|
622
|
177
|
|
|
|
|
30030
|
delete $self->{ 'source_file' }; |
623
|
177
|
|
|
|
|
487
|
$self->{ 'source' } = $content; |
624
|
|
|
|
|
|
|
|
625
|
177
|
|
|
|
|
963
|
return; |
626
|
|
|
|
|
|
|
} |
627
|
|
|
|
|
|
|
|
628
|
|
|
|
|
|
|
=head2 as_text |
629
|
|
|
|
|
|
|
|
630
|
|
|
|
|
|
|
Returns parsed plan back as plain text format (regenerated from in-memory structure). |
631
|
|
|
|
|
|
|
|
632
|
|
|
|
|
|
|
This is mostly useful for (future at the moment) anonymizations. |
633
|
|
|
|
|
|
|
|
634
|
|
|
|
|
|
|
=cut |
635
|
|
|
|
|
|
|
|
636
|
|
|
|
|
|
|
sub as_text { |
637
|
91
|
|
|
91
|
1
|
1928
|
my $self = shift; |
638
|
|
|
|
|
|
|
|
639
|
91
|
|
|
|
|
355
|
my $textual = $self->top_node->as_text(); |
640
|
|
|
|
|
|
|
|
641
|
91
|
100
|
|
|
|
397
|
if ( $self->planning_buffers ) { |
642
|
8
|
|
|
|
|
45
|
$textual .= "Planning:\n"; |
643
|
8
|
|
|
|
|
33
|
my $buf_info = $self->planning_buffers->as_text; |
644
|
8
|
|
|
|
|
67
|
$buf_info =~ s/^/ /gm; |
645
|
8
|
|
|
|
|
27
|
$textual .= $buf_info . "\n"; |
646
|
|
|
|
|
|
|
|
647
|
|
|
|
|
|
|
} |
648
|
91
|
100
|
|
|
|
332
|
if ( $self->planning_time ) { |
649
|
25
|
|
|
|
|
86
|
$textual .= "Planning time: " . $self->planning_time . " ms\n"; |
650
|
|
|
|
|
|
|
} |
651
|
91
|
100
|
|
|
|
295
|
if ( $self->trigger_times ) { |
652
|
5
|
|
|
|
|
9
|
for my $t ( @{ $self->trigger_times } ) { |
|
5
|
|
|
|
|
13
|
|
653
|
13
|
|
|
|
|
91
|
$textual .= sprintf( "Trigger %s: time=%.3f calls=%d\n", $t->{ 'name' }, $t->{ 'time' }, $t->{ 'calls' } ); |
654
|
|
|
|
|
|
|
} |
655
|
|
|
|
|
|
|
} |
656
|
91
|
100
|
|
|
|
328
|
if ( $self->jit ) { |
657
|
4
|
|
|
|
|
15
|
$textual .= $self->jit->as_text(); |
658
|
|
|
|
|
|
|
} |
659
|
91
|
100
|
|
|
|
352
|
if ( $self->execution_time ) { |
660
|
34
|
|
|
|
|
127
|
$textual .= "Execution time: " . $self->execution_time . " ms\n"; |
661
|
|
|
|
|
|
|
} |
662
|
91
|
100
|
|
|
|
285
|
if ( $self->total_runtime ) { |
663
|
17
|
|
|
|
|
49
|
$textual .= "Total runtime: " . $self->total_runtime . " ms\n"; |
664
|
|
|
|
|
|
|
} |
665
|
|
|
|
|
|
|
|
666
|
91
|
|
|
|
|
640
|
return $textual; |
667
|
|
|
|
|
|
|
} |
668
|
|
|
|
|
|
|
|
669
|
|
|
|
|
|
|
=head2 get_struct |
670
|
|
|
|
|
|
|
|
671
|
|
|
|
|
|
|
Function which returns simple, not blessed, hashref with all information about the explain. |
672
|
|
|
|
|
|
|
|
673
|
|
|
|
|
|
|
This can be used for debug purposes, or as a base to print information to user. |
674
|
|
|
|
|
|
|
|
675
|
|
|
|
|
|
|
Output looks like this: |
676
|
|
|
|
|
|
|
|
677
|
|
|
|
|
|
|
{ |
678
|
|
|
|
|
|
|
'top_node' => {...} |
679
|
|
|
|
|
|
|
'planning_time' => '12.44', |
680
|
|
|
|
|
|
|
'planning_buffers' => {...}, |
681
|
|
|
|
|
|
|
'execution_time' => '12.44', |
682
|
|
|
|
|
|
|
'total_runtime' => '12.44', |
683
|
|
|
|
|
|
|
'trigger_times' => [ |
684
|
|
|
|
|
|
|
{ 'name' => ..., 'time' => ..., 'calls' => ... }, |
685
|
|
|
|
|
|
|
... |
686
|
|
|
|
|
|
|
], |
687
|
|
|
|
|
|
|
} |
688
|
|
|
|
|
|
|
|
689
|
|
|
|
|
|
|
=cut |
690
|
|
|
|
|
|
|
|
691
|
|
|
|
|
|
|
sub get_struct { |
692
|
50
|
|
|
50
|
1
|
22775
|
my $self = shift; |
693
|
50
|
|
|
|
|
111
|
my $reply = {}; |
694
|
50
|
|
|
|
|
147
|
$reply->{ 'top_node' } = $self->top_node->get_struct; |
695
|
50
|
100
|
|
|
|
161
|
$reply->{ 'planning_time' } = $self->planning_time if $self->planning_time; |
696
|
50
|
100
|
|
|
|
162
|
$reply->{ 'planning_buffers' } = $self->planning_buffers->get_struct if $self->planning_buffers; |
697
|
50
|
50
|
|
|
|
166
|
$reply->{ 'execution_time' } = $self->execution_time if $self->execution_time; |
698
|
50
|
50
|
|
|
|
178
|
$reply->{ 'total_runtime' } = $self->total_runtime if $self->total_runtime; |
699
|
50
|
100
|
|
|
|
136
|
$reply->{ 'trigger_times' } = clone( $self->trigger_times ) if $self->trigger_times; |
700
|
50
|
50
|
|
|
|
186
|
$reply->{ 'query' } = $self->query if $self->query; |
701
|
50
|
50
|
|
|
|
140
|
$reply->{ 'settings' } = $self->settings if $self->settings; |
702
|
|
|
|
|
|
|
|
703
|
50
|
100
|
|
|
|
169
|
if ( $self->jit ) { |
704
|
8
|
|
|
|
|
27
|
$reply->{ 'jit' } = {}; |
705
|
8
|
|
|
|
|
19
|
$reply->{ 'jit' }->{ 'functions' } = $self->jit->functions; |
706
|
8
|
|
|
|
|
16
|
$reply->{ 'jit' }->{ 'options' } = clone( $self->jit->options ); |
707
|
8
|
|
|
|
|
24
|
$reply->{ 'jit' }->{ 'timings' } = clone( $self->jit->timings ); |
708
|
|
|
|
|
|
|
} |
709
|
50
|
|
|
|
|
199
|
return $reply; |
710
|
|
|
|
|
|
|
} |
711
|
|
|
|
|
|
|
|
712
|
|
|
|
|
|
|
=head2 anonymize |
713
|
|
|
|
|
|
|
|
714
|
|
|
|
|
|
|
Used to remove all individual values from the explain, while still retaining |
715
|
|
|
|
|
|
|
all values that are needed to see what's wrong. |
716
|
|
|
|
|
|
|
|
717
|
|
|
|
|
|
|
If there are any arguments, these are treated as strings, anonymized using |
718
|
|
|
|
|
|
|
anonymizer used for plan, and are returned in the same order. |
719
|
|
|
|
|
|
|
|
720
|
|
|
|
|
|
|
This is mainly useful to anonymize queries. |
721
|
|
|
|
|
|
|
|
722
|
|
|
|
|
|
|
=cut |
723
|
|
|
|
|
|
|
|
724
|
|
|
|
|
|
|
sub anonymize { |
725
|
17
|
|
|
17
|
1
|
634
|
my $self = shift; |
726
|
17
|
|
|
|
|
46
|
my @extra_args = @_; |
727
|
|
|
|
|
|
|
|
728
|
17
|
|
|
|
|
49
|
my $anonymizer = $self->{ 'anonymizer' }; |
729
|
17
|
100
|
|
|
|
65
|
if ( !$anonymizer ) { |
730
|
15
|
|
|
|
|
138
|
$anonymizer = Pg::Explain::StringAnonymizer->new(); |
731
|
15
|
|
|
|
|
47
|
$self->top_node->anonymize_gathering( $anonymizer ); |
732
|
15
|
|
|
|
|
94
|
$anonymizer->finalize(); |
733
|
15
|
|
|
|
|
78
|
$self->top_node->anonymize_substitute( $anonymizer ); |
734
|
15
|
|
|
|
|
74
|
$self->{ 'anonymizer' } = $anonymizer; |
735
|
|
|
|
|
|
|
} |
736
|
|
|
|
|
|
|
|
737
|
17
|
100
|
|
|
|
158
|
return if 0 == scalar @extra_args; |
738
|
|
|
|
|
|
|
|
739
|
3
|
50
|
|
|
|
24
|
return $anonymizer->anonymize_text( $extra_args[ 0 ] ) if 1 == scalar @extra_args; |
740
|
|
|
|
|
|
|
|
741
|
0
|
|
|
|
|
|
return map { $anonymizer->anonymize_text( $_ ) } @extra_args; |
|
0
|
|
|
|
|
|
|
742
|
|
|
|
|
|
|
} |
743
|
|
|
|
|
|
|
|
744
|
|
|
|
|
|
|
=head1 AUTHOR |
745
|
|
|
|
|
|
|
|
746
|
|
|
|
|
|
|
hubert depesz lubaczewski, C<< >> |
747
|
|
|
|
|
|
|
|
748
|
|
|
|
|
|
|
=head1 BUGS |
749
|
|
|
|
|
|
|
|
750
|
|
|
|
|
|
|
Please report any bugs or feature requests to C. |
751
|
|
|
|
|
|
|
|
752
|
|
|
|
|
|
|
=head1 SUPPORT |
753
|
|
|
|
|
|
|
|
754
|
|
|
|
|
|
|
You can find documentation for this module with the perldoc command. |
755
|
|
|
|
|
|
|
|
756
|
|
|
|
|
|
|
perldoc Pg::Explain |
757
|
|
|
|
|
|
|
|
758
|
|
|
|
|
|
|
=head1 COPYRIGHT & LICENSE |
759
|
|
|
|
|
|
|
|
760
|
|
|
|
|
|
|
Copyright 2008-2021 hubert depesz lubaczewski, all rights reserved. |
761
|
|
|
|
|
|
|
|
762
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify it |
763
|
|
|
|
|
|
|
under the same terms as Perl itself. |
764
|
|
|
|
|
|
|
|
765
|
|
|
|
|
|
|
|
766
|
|
|
|
|
|
|
=cut |
767
|
|
|
|
|
|
|
|
768
|
|
|
|
|
|
|
1; # End of Pg::Explain |