line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Pg::Explain::Node; |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
# UTF8 boilerplace, per http://stackoverflow.com/questions/6162484/why-does-modern-perl-avoid-utf-8-by-default/ |
4
|
72
|
|
|
72
|
|
830
|
use v5.18; |
|
72
|
|
|
|
|
230
|
|
5
|
72
|
|
|
72
|
|
337
|
use strict; |
|
72
|
|
|
|
|
111
|
|
|
72
|
|
|
|
|
1211
|
|
6
|
72
|
|
|
72
|
|
277
|
use warnings; |
|
72
|
|
|
|
|
118
|
|
|
72
|
|
|
|
|
1769
|
|
7
|
72
|
|
|
72
|
|
283
|
use warnings qw( FATAL utf8 ); |
|
72
|
|
|
|
|
119
|
|
|
72
|
|
|
|
|
1877
|
|
8
|
72
|
|
|
72
|
|
296
|
use utf8; |
|
72
|
|
|
|
|
118
|
|
|
72
|
|
|
|
|
394
|
|
9
|
72
|
|
|
72
|
|
1433
|
use open qw( :std :utf8 ); |
|
72
|
|
|
|
|
126
|
|
|
72
|
|
|
|
|
353
|
|
10
|
72
|
|
|
72
|
|
8043
|
use Unicode::Normalize qw( NFC ); |
|
72
|
|
|
|
|
127
|
|
|
72
|
|
|
|
|
3104
|
|
11
|
72
|
|
|
72
|
|
438
|
use Unicode::Collate; |
|
72
|
|
|
|
|
125
|
|
|
72
|
|
|
|
|
1685
|
|
12
|
72
|
|
|
72
|
|
346
|
use Encode qw( decode ); |
|
72
|
|
|
|
|
142
|
|
|
72
|
|
|
|
|
3492
|
|
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
|
72
|
|
|
72
|
|
13043
|
use Clone qw( clone ); |
|
72
|
|
|
|
|
123
|
|
|
72
|
|
|
|
|
3549
|
|
21
|
72
|
|
|
72
|
|
30856
|
use HOP::Lexer qw( string_lexer ); |
|
72
|
|
|
|
|
176190
|
|
|
72
|
|
|
|
|
3747
|
|
22
|
72
|
|
|
72
|
|
481
|
use Carp; |
|
72
|
|
|
|
|
119
|
|
|
72
|
|
|
|
|
3483
|
|
23
|
|
|
|
|
|
|
|
24
|
|
|
|
|
|
|
# I'm reasonably sure that there are no infinite recusion paths, but in some cases the plan is just deep enough to cause Perl to |
25
|
|
|
|
|
|
|
# issue warning about it. Since the warnings don't bring anything good to the table, let's disable them. |
26
|
72
|
|
|
72
|
|
342
|
no warnings 'recursion'; |
|
72
|
|
|
|
|
121
|
|
|
72
|
|
|
|
|
464578
|
|
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
=head1 NAME |
29
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
Pg::Explain::Node - Class representing single node from query plan |
31
|
|
|
|
|
|
|
|
32
|
|
|
|
|
|
|
=head1 VERSION |
33
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
Version 2.2 |
35
|
|
|
|
|
|
|
|
36
|
|
|
|
|
|
|
=cut |
37
|
|
|
|
|
|
|
|
38
|
|
|
|
|
|
|
our $VERSION = '2.2'; |
39
|
|
|
|
|
|
|
|
40
|
|
|
|
|
|
|
# Start counter for all node ids. |
41
|
|
|
|
|
|
|
our $base_id = 1; |
42
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
=head1 SYNOPSIS |
44
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
Quick summary of what the module does. |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
Perhaps a little code snippet. |
48
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
use Pg::Explain::Node; |
50
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
my $foo = Pg::Explain::Node->new(); |
52
|
|
|
|
|
|
|
... |
53
|
|
|
|
|
|
|
|
54
|
|
|
|
|
|
|
=head1 FUNCTIONS |
55
|
|
|
|
|
|
|
|
56
|
|
|
|
|
|
|
=head2 id |
57
|
|
|
|
|
|
|
|
58
|
|
|
|
|
|
|
Unique identifier of this node in this explain. It's read-only, autoincrementing integer. |
59
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
=head2 actual_loops |
61
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
Returns number how many times current node has been executed. |
63
|
|
|
|
|
|
|
|
64
|
|
|
|
|
|
|
This information is available only when parsing EXPLAIN ANALYZE output - not in EXPLAIN output. |
65
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
|
=head2 actual_rows |
67
|
|
|
|
|
|
|
|
68
|
|
|
|
|
|
|
Returns amount of rows current node returnes in single execution (i.e. if given node was executed 10 times, you have to multiply actual_rows by 10, to get full number of returned rows. |
69
|
|
|
|
|
|
|
|
70
|
|
|
|
|
|
|
This information is available only when parsing EXPLAIN ANALYZE output - not in EXPLAIN output. |
71
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
=head2 actual_time_first |
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
Returns time (in miliseconds) how long it took PostgreSQL to return 1st row from given node. |
75
|
|
|
|
|
|
|
|
76
|
|
|
|
|
|
|
This information is available only when parsing EXPLAIN ANALYZE output - not in EXPLAIN output. |
77
|
|
|
|
|
|
|
|
78
|
|
|
|
|
|
|
=head2 actual_time_last |
79
|
|
|
|
|
|
|
|
80
|
|
|
|
|
|
|
Returns time (in miliseconds) how long it took PostgreSQL to return all rows from given node. This number represents single execution of the node, so if given node was executed 10 times, you have to multiply actual_time_last by 10 to get total time of running of this node. |
81
|
|
|
|
|
|
|
|
82
|
|
|
|
|
|
|
This information is available only when parsing EXPLAIN ANALYZE output - not in EXPLAIN output. |
83
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
=head2 estimated_rows |
85
|
|
|
|
|
|
|
|
86
|
|
|
|
|
|
|
Returns estimated number of rows to be returned from this node. |
87
|
|
|
|
|
|
|
|
88
|
|
|
|
|
|
|
=head2 estimated_row_width |
89
|
|
|
|
|
|
|
|
90
|
|
|
|
|
|
|
Returns estimated width (in bytes) of single row returned from this node. |
91
|
|
|
|
|
|
|
|
92
|
|
|
|
|
|
|
=head2 estimated_startup_cost |
93
|
|
|
|
|
|
|
|
94
|
|
|
|
|
|
|
Returns estimated cost of starting execution of given node. Some node types do not have startup cost (i.e., it is 0), but some do. For example - Seq Scan has startup cost = 0, but Sort node has |
95
|
|
|
|
|
|
|
startup cost depending on number of rows. |
96
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
This cost is measured in units of "single-page seq scan". |
98
|
|
|
|
|
|
|
|
99
|
|
|
|
|
|
|
=head2 estimated_total_cost |
100
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
Returns estimated full cost of given node. |
102
|
|
|
|
|
|
|
|
103
|
|
|
|
|
|
|
This cost is measured in units of "single-page seq scan". |
104
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
=head2 workers_launched |
106
|
|
|
|
|
|
|
|
107
|
|
|
|
|
|
|
How many worker processes this node launched. |
108
|
|
|
|
|
|
|
|
109
|
|
|
|
|
|
|
=head2 workers |
110
|
|
|
|
|
|
|
|
111
|
|
|
|
|
|
|
How many workers was this node processed on. Always set to at least 1. |
112
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
=head2 type |
114
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
Textual representation of type of current node. Some types for example: |
116
|
|
|
|
|
|
|
|
117
|
|
|
|
|
|
|
=over |
118
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
=item * Index Scan |
120
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
=item * Index Scan Backward |
122
|
|
|
|
|
|
|
|
123
|
|
|
|
|
|
|
=item * Limit |
124
|
|
|
|
|
|
|
|
125
|
|
|
|
|
|
|
=item * Nested Loop |
126
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
=item * Nested Loop Left Join |
128
|
|
|
|
|
|
|
|
129
|
|
|
|
|
|
|
=item * Result |
130
|
|
|
|
|
|
|
|
131
|
|
|
|
|
|
|
=item * Seq Scan |
132
|
|
|
|
|
|
|
|
133
|
|
|
|
|
|
|
=item * Sort |
134
|
|
|
|
|
|
|
|
135
|
|
|
|
|
|
|
=back |
136
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
=head2 buffers |
138
|
|
|
|
|
|
|
|
139
|
|
|
|
|
|
|
Information about inclusive buffers usage in given node. It's either undef, or object of Pg::Explain::Buffers class. |
140
|
|
|
|
|
|
|
|
141
|
|
|
|
|
|
|
=cut |
142
|
|
|
|
|
|
|
|
143
|
|
|
|
|
|
|
=head2 scan_on |
144
|
|
|
|
|
|
|
|
145
|
|
|
|
|
|
|
Hashref with extra information in case of table scans. |
146
|
|
|
|
|
|
|
|
147
|
|
|
|
|
|
|
For Seq Scan it contains always 'table_name' key, and optionally 'table_alias' key. |
148
|
|
|
|
|
|
|
|
149
|
|
|
|
|
|
|
For Index Scan and Backward Index Scan, it also contains (always) 'index_name' key. |
150
|
|
|
|
|
|
|
|
151
|
|
|
|
|
|
|
=head2 extra_info |
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
ArrayRef of strings, each contains textual information (leading and tailing spaces removed) for given node. |
154
|
|
|
|
|
|
|
|
155
|
|
|
|
|
|
|
This is not always filled, as it depends heavily on node type and PostgreSQL version. |
156
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
=head2 sub_nodes |
158
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
ArrayRef of Pg::Explain::Node objects, which represent sub nodes. |
160
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
For more details, check ->add_sub_node method description. |
162
|
|
|
|
|
|
|
|
163
|
|
|
|
|
|
|
=head2 initplans |
164
|
|
|
|
|
|
|
|
165
|
|
|
|
|
|
|
ArrayRef of Pg::Explain::Node objects, which represent init plan. |
166
|
|
|
|
|
|
|
|
167
|
|
|
|
|
|
|
For more details, check ->add_initplan method description. |
168
|
|
|
|
|
|
|
|
169
|
|
|
|
|
|
|
=head2 initplans_metainfo |
170
|
|
|
|
|
|
|
|
171
|
|
|
|
|
|
|
ArrayRef of Hashrefs, where each hashref can contains: |
172
|
|
|
|
|
|
|
|
173
|
|
|
|
|
|
|
=over |
174
|
|
|
|
|
|
|
|
175
|
|
|
|
|
|
|
=item * 'name' - name of the InitPlan, generally number |
176
|
|
|
|
|
|
|
|
177
|
|
|
|
|
|
|
=item * 'returns' - string listing what the initplan returns. Generally a list of $X values (where X is 0 or positive integer) separated by comma. |
178
|
|
|
|
|
|
|
|
179
|
|
|
|
|
|
|
=back |
180
|
|
|
|
|
|
|
|
181
|
|
|
|
|
|
|
For more details, check ->add_initplan method description. |
182
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
=head2 subplans |
184
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
ArrayRef of Pg::Explain::Node objects, which represent sub plan. |
186
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
For more details, check ->add_subplan method description. |
188
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
=head2 ctes |
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
HashRef of Pg::Explain::Node objects, which represent CTE plans. |
192
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
For more details, check ->add_cte method description. |
194
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
=head2 cte_order |
196
|
|
|
|
|
|
|
|
197
|
|
|
|
|
|
|
ArrayRef of names of CTE nodes in given node. |
198
|
|
|
|
|
|
|
|
199
|
|
|
|
|
|
|
For more details, check ->add_cte method description. |
200
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
=head2 never_executed |
202
|
|
|
|
|
|
|
|
203
|
|
|
|
|
|
|
Returns true if given node was not executed, according to plan. |
204
|
|
|
|
|
|
|
|
205
|
|
|
|
|
|
|
=head2 parent |
206
|
|
|
|
|
|
|
|
207
|
|
|
|
|
|
|
Parent node of current node, or undef if it's top node. |
208
|
|
|
|
|
|
|
|
209
|
|
|
|
|
|
|
=head2 exclusive_fix |
210
|
|
|
|
|
|
|
|
211
|
|
|
|
|
|
|
Numeric value that will be added to total_exclusive_time. It is set by Pg::Explain::check_for_exclusive_time_fixes method once after parsing the explain. |
212
|
|
|
|
|
|
|
|
213
|
|
|
|
|
|
|
=cut |
214
|
|
|
|
|
|
|
|
215
|
1693
|
|
|
1693
|
1
|
6623
|
sub id { my $self = shift; return $self->{ 'id' }; } |
|
1693
|
|
|
|
|
4768
|
|
216
|
5500
|
100
|
|
5500
|
1
|
6347
|
sub actual_loops { my $self = shift; $self->{ 'actual_loops' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_loops' }; } |
|
5500
|
|
|
|
|
8206
|
|
|
5500
|
|
|
|
|
14810
|
|
217
|
1377
|
50
|
|
1377
|
1
|
2509
|
sub actual_rows { my $self = shift; $self->{ 'actual_rows' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_rows' }; } |
|
1377
|
|
|
|
|
2087
|
|
|
1377
|
|
|
|
|
2712
|
|
218
|
1199
|
50
|
|
1199
|
1
|
1339
|
sub actual_time_first { my $self = shift; $self->{ 'actual_time_first' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_time_first' }; } |
|
1199
|
|
|
|
|
1837
|
|
|
1199
|
|
|
|
|
2428
|
|
219
|
2826
|
50
|
|
2826
|
1
|
3112
|
sub actual_time_last { my $self = shift; $self->{ 'actual_time_last' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'actual_time_last' }; } |
|
2826
|
|
|
|
|
4278
|
|
|
2826
|
|
|
|
|
6078
|
|
220
|
979
|
100
|
|
979
|
1
|
1153
|
sub cte_order { my $self = shift; $self->{ 'cte_order' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'cte_order' }; } |
|
979
|
|
|
|
|
1662
|
|
|
979
|
|
|
|
|
1857
|
|
221
|
6570
|
100
|
|
6570
|
1
|
7049
|
sub ctes { my $self = shift; $self->{ 'ctes' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'ctes' }; } |
|
6570
|
|
|
|
|
9225
|
|
|
6570
|
|
|
|
|
11873
|
|
222
|
1360
|
50
|
|
1360
|
1
|
1547
|
sub estimated_rows { my $self = shift; $self->{ 'estimated_rows' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_rows' }; } |
|
1360
|
|
|
|
|
2108
|
|
|
1360
|
|
|
|
|
2615
|
|
223
|
1526
|
50
|
|
1526
|
1
|
1712
|
sub estimated_row_width { my $self = shift; $self->{ 'estimated_row_width' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_row_width' }; } |
|
1526
|
|
|
|
|
2284
|
|
|
1526
|
|
|
|
|
5623
|
|
224
|
1360
|
50
|
|
1360
|
1
|
1498
|
sub estimated_startup_cost { my $self = shift; $self->{ 'estimated_startup_cost' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_startup_cost' }; } |
|
1360
|
|
|
|
|
2060
|
|
|
1360
|
|
|
|
|
3154
|
|
225
|
1360
|
50
|
|
1360
|
1
|
1503
|
sub estimated_total_cost { my $self = shift; $self->{ 'estimated_total_cost' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'estimated_total_cost' }; } |
|
1360
|
|
|
|
|
2090
|
|
|
1360
|
|
|
|
|
2782
|
|
226
|
4992
|
100
|
|
4992
|
1
|
5526
|
sub extra_info { my $self = shift; $self->{ 'extra_info' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'extra_info' }; } |
|
4992
|
|
|
|
|
8083
|
|
|
4992
|
|
|
|
|
10444
|
|
227
|
6993
|
100
|
|
6993
|
1
|
7558
|
sub initplans { my $self = shift; $self->{ 'initplans' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'initplans' }; } |
|
6993
|
|
|
|
|
9785
|
|
|
6993
|
|
|
|
|
12194
|
|
228
|
751
|
100
|
|
751
|
1
|
922
|
sub initplans_metainfo { my $self = shift; $self->{ 'initplans_metainfo' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'initplans_metainfo' }; } |
|
751
|
|
|
|
|
1142
|
|
|
751
|
|
|
|
|
1525
|
|
229
|
687
|
100
|
|
687
|
1
|
852
|
sub never_executed { my $self = shift; $self->{ 'never_executed' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'never_executed' }; } |
|
687
|
|
|
|
|
2466
|
|
|
687
|
|
|
|
|
2398
|
|
230
|
1098
|
100
|
|
1098
|
1
|
1373
|
sub parent { my $self = shift; $self->{ 'parent' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'parent' }; } |
|
1098
|
|
|
|
|
2463
|
|
|
1098
|
|
|
|
|
1596
|
|
231
|
2876
|
100
|
|
2876
|
1
|
3630
|
sub scan_on { my $self = shift; $self->{ 'scan_on' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'scan_on' }; } |
|
2876
|
|
|
|
|
5298
|
|
|
2876
|
|
|
|
|
8434
|
|
232
|
9915
|
100
|
|
9915
|
1
|
11044
|
sub sub_nodes { my $self = shift; $self->{ 'sub_nodes' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'sub_nodes' }; } |
|
9915
|
|
|
|
|
14507
|
|
|
9915
|
|
|
|
|
17809
|
|
233
|
5721
|
100
|
|
5721
|
1
|
6075
|
sub subplans { my $self = shift; $self->{ 'subplans' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'subplans' }; } |
|
5721
|
|
|
|
|
8100
|
|
|
5721
|
|
|
|
|
9407
|
|
234
|
10909
|
100
|
|
10909
|
1
|
80623
|
sub type { my $self = shift; $self->{ 'type' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'type' }; } |
|
10909
|
|
|
|
|
16744
|
|
|
10909
|
|
|
|
|
28369
|
|
235
|
1328
|
100
|
|
1328
|
1
|
1534
|
sub workers_launched { my $self = shift; $self->{ 'workers_launched' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'workers_launched' }; } |
|
1328
|
|
|
|
|
2128
|
|
|
1328
|
|
|
|
|
2528
|
|
236
|
2334
|
100
|
50
|
2334
|
1
|
3676
|
sub workers { my $self = shift; $self->{ 'workers' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'workers' } || 1; } |
|
2334
|
|
|
|
|
4572
|
|
|
2334
|
|
|
|
|
5730
|
|
237
|
1166
|
100
|
|
1166
|
1
|
1371
|
sub buffers { my $self = shift; $self->{ 'buffers' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'buffers' }; } |
|
1166
|
|
|
|
|
1920
|
|
|
1166
|
|
|
|
|
2233
|
|
238
|
402
|
100
|
100
|
402
|
1
|
501
|
sub exclusive_fix { my $self = shift; $self->{ 'exclusive_fix' } = $_[ 0 ] if 0 < scalar @_; return $self->{ 'exclusive_fix' } // 0; } |
|
402
|
|
|
|
|
651
|
|
|
402
|
|
|
|
|
1263
|
|
239
|
|
|
|
|
|
|
|
240
|
|
|
|
|
|
|
=head2 new |
241
|
|
|
|
|
|
|
|
242
|
|
|
|
|
|
|
Object constructor. |
243
|
|
|
|
|
|
|
|
244
|
|
|
|
|
|
|
=cut |
245
|
|
|
|
|
|
|
|
246
|
|
|
|
|
|
|
sub new { |
247
|
1509
|
|
|
1509
|
1
|
2641
|
my $class = shift; |
248
|
1509
|
|
|
|
|
4264
|
my $self = bless { 'id' => $base_id++ }, $class; |
249
|
|
|
|
|
|
|
|
250
|
1509
|
|
|
|
|
2180
|
my %args; |
251
|
1509
|
50
|
|
|
|
3186
|
if ( 0 == scalar @_ ) { |
252
|
0
|
|
|
|
|
0
|
croak( 'Args should be passed as either hash or hashref' ); |
253
|
|
|
|
|
|
|
} |
254
|
1509
|
50
|
|
|
|
3923
|
if ( 1 == scalar @_ ) { |
|
|
50
|
|
|
|
|
|
255
|
0
|
0
|
|
|
|
0
|
if ( 'HASH' eq ref $_[ 0 ] ) { |
256
|
0
|
|
|
|
|
0
|
%args = @{ $_[ 0 ] }; |
|
0
|
|
|
|
|
0
|
|
257
|
|
|
|
|
|
|
} |
258
|
|
|
|
|
|
|
else { |
259
|
0
|
|
|
|
|
0
|
croak( 'Args should be passed as either hash or hashref' ); |
260
|
|
|
|
|
|
|
} |
261
|
|
|
|
|
|
|
} |
262
|
|
|
|
|
|
|
elsif ( 1 == ( scalar( @_ ) % 2 ) ) { |
263
|
0
|
|
|
|
|
0
|
croak( 'Args should be passed as either hash or hashref' ); |
264
|
|
|
|
|
|
|
} |
265
|
|
|
|
|
|
|
else { |
266
|
1509
|
|
|
|
|
14259
|
%args = @_; |
267
|
|
|
|
|
|
|
} |
268
|
1509
|
50
|
|
|
|
3863
|
croak( 'type has to be passed to constructor of explain node' ) unless defined $args{ 'type' }; |
269
|
|
|
|
|
|
|
|
270
|
|
|
|
|
|
|
# Backfill costs if they are not given from plan |
271
|
1509
|
|
|
|
|
2637
|
for my $key ( qw( estimated_rows estimated_row_width estimated_startup_cost estimated_total_cost ) ) { |
272
|
6036
|
100
|
|
|
|
10148
|
$args{ $key } = 0 unless defined $args{ $key }; |
273
|
|
|
|
|
|
|
} |
274
|
|
|
|
|
|
|
|
275
|
1509
|
|
|
|
|
4687
|
@{ $self }{ keys %args } = values %args; |
|
1509
|
|
|
|
|
7967
|
|
276
|
|
|
|
|
|
|
|
277
|
1509
|
100
|
|
|
|
4005
|
if ( |
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
278
|
|
|
|
|
|
|
$self->type =~ m{ \A ( |
279
|
|
|
|
|
|
|
(?: Parallel \s+ )? |
280
|
|
|
|
|
|
|
(?: Seq \s Scan | Tid \s+ Scan | Bitmap \s+ Heap \s+ Scan | Foreign \s+ Scan | Update | Insert | Delete ) |
281
|
|
|
|
|
|
|
) \s on \s (\S+) (?: \s+ (\S+) ) ? \z }xms |
282
|
|
|
|
|
|
|
) |
283
|
|
|
|
|
|
|
{ |
284
|
314
|
|
|
|
|
821
|
$self->type( $1 ); |
285
|
314
|
|
|
|
|
1357
|
$self->scan_on( { 'table_name' => $2, } ); |
286
|
314
|
100
|
|
|
|
1002
|
$self->scan_on->{ 'table_alias' } = $3 if defined $3; |
287
|
|
|
|
|
|
|
} |
288
|
|
|
|
|
|
|
elsif ( $self->type =~ m{ \A ( Bitmap \s+ Index \s+ Scan) \s on \s (\S+) \z }xms ) { |
289
|
21
|
|
|
|
|
66
|
$self->type( $1 ); |
290
|
21
|
|
|
|
|
99
|
$self->scan_on( { 'index_name' => $2, } ); |
291
|
|
|
|
|
|
|
} |
292
|
|
|
|
|
|
|
elsif ( $self->type =~ m{ \A ( (?: Parallel \s+ )? Index (?: \s Only )? \s Scan (?: \s Backward )? ) \s using \s (\S+) \s on \s (\S+) (?: \s+ (\S+) ) ? \z }xms ) { |
293
|
119
|
|
|
|
|
314
|
$self->type( $1 ); |
294
|
119
|
|
|
|
|
580
|
$self->scan_on( |
295
|
|
|
|
|
|
|
{ |
296
|
|
|
|
|
|
|
'index_name' => $2, |
297
|
|
|
|
|
|
|
'table_name' => $3, |
298
|
|
|
|
|
|
|
} |
299
|
|
|
|
|
|
|
); |
300
|
119
|
100
|
|
|
|
392
|
$self->scan_on->{ 'table_alias' } = $4 if defined $4; |
301
|
|
|
|
|
|
|
} |
302
|
|
|
|
|
|
|
elsif ( $self->type =~ m{ \A ( CTE \s Scan ) \s on \s (\S+) (?: \s+ (\S+) ) ? \z }xms ) { |
303
|
29
|
|
|
|
|
88
|
$self->type( $1 ); |
304
|
29
|
|
|
|
|
113
|
$self->scan_on( { 'cte_name' => $2, } ); |
305
|
29
|
100
|
|
|
|
95
|
$self->scan_on->{ 'cte_alias' } = $3 if defined $3; |
306
|
|
|
|
|
|
|
} |
307
|
|
|
|
|
|
|
elsif ( $self->type =~ m{ \A ( WorkTable \s Scan ) \s on \s (\S+) (?: \s+ (\S+) ) ? \z }xms ) { |
308
|
3
|
|
|
|
|
11
|
$self->type( $1 ); |
309
|
3
|
|
|
|
|
18
|
$self->scan_on( { 'worktable_name' => $2, } ); |
310
|
3
|
50
|
|
|
|
15
|
$self->scan_on->{ 'worktable_alias' } = $3 if defined $3; |
311
|
|
|
|
|
|
|
} |
312
|
|
|
|
|
|
|
elsif ( $self->type =~ m{ \A ( Function \s Scan ) \s on \s (\S+) (?: \s+ (\S+) )? \z }xms ) { |
313
|
11
|
|
|
|
|
38
|
$self->type( $1 ); |
314
|
11
|
|
|
|
|
64
|
$self->scan_on( { 'function_name' => $2, } ); |
315
|
11
|
100
|
|
|
|
66
|
$self->scan_on->{ 'function_alias' } = $3 if defined $3; |
316
|
|
|
|
|
|
|
} |
317
|
|
|
|
|
|
|
elsif ( $self->type =~ m{ \A ( Subquery \s Scan ) \s on \s (.+) \z }xms ) { |
318
|
3
|
|
|
|
|
8
|
$self->type( $1 ); |
319
|
3
|
|
|
|
|
5
|
my $name = $2; |
320
|
3
|
|
|
|
|
15
|
$name =~ s{\A"(.*)"\z}{$1}; |
321
|
3
|
|
|
|
|
11
|
$self->scan_on( { 'subquery_name' => $name, } ); |
322
|
|
|
|
|
|
|
} |
323
|
1509
|
|
|
|
|
5339
|
return $self; |
324
|
|
|
|
|
|
|
} |
325
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
=head2 explain |
327
|
|
|
|
|
|
|
|
328
|
|
|
|
|
|
|
Returns/sets Pg::Explain for this node. |
329
|
|
|
|
|
|
|
|
330
|
|
|
|
|
|
|
Also, calls $explain->node( $id, $self ); |
331
|
|
|
|
|
|
|
|
332
|
|
|
|
|
|
|
=cut |
333
|
|
|
|
|
|
|
|
334
|
|
|
|
|
|
|
sub explain { |
335
|
1509
|
|
|
1509
|
1
|
1977
|
my $self = shift; |
336
|
1509
|
|
|
|
|
1765
|
my $explain = shift; |
337
|
1509
|
50
|
|
|
|
2792
|
if ( defined $explain ) { |
338
|
1509
|
|
|
|
|
2414
|
$self->{ 'explain' } = $explain; |
339
|
1509
|
|
|
|
|
3249
|
$explain->node( $self->id, $self ); |
340
|
|
|
|
|
|
|
} |
341
|
1509
|
|
|
|
|
2500
|
return $self->{ 'explain' }; |
342
|
|
|
|
|
|
|
} |
343
|
|
|
|
|
|
|
|
344
|
|
|
|
|
|
|
=head2 add_extra_info |
345
|
|
|
|
|
|
|
|
346
|
|
|
|
|
|
|
Adds new line of extra information to explain node. |
347
|
|
|
|
|
|
|
|
348
|
|
|
|
|
|
|
It will be available at $node->extra_info (returns arrayref) |
349
|
|
|
|
|
|
|
|
350
|
|
|
|
|
|
|
Extra_info is used by some nodes to provide additional information. For example |
351
|
|
|
|
|
|
|
- for Sort nodes, they usually contain informtion about used memory, used sort |
352
|
|
|
|
|
|
|
method and keys. |
353
|
|
|
|
|
|
|
|
354
|
|
|
|
|
|
|
=cut |
355
|
|
|
|
|
|
|
|
356
|
|
|
|
|
|
|
sub add_extra_info { |
357
|
1358
|
|
|
1358
|
1
|
1790
|
my $self = shift; |
358
|
1358
|
100
|
|
|
|
2422
|
if ( $self->extra_info ) { |
359
|
566
|
|
|
|
|
724
|
push @{ $self->extra_info }, @_; |
|
566
|
|
|
|
|
897
|
|
360
|
|
|
|
|
|
|
} |
361
|
|
|
|
|
|
|
else { |
362
|
792
|
|
|
|
|
1875
|
$self->extra_info( [ @_ ] ); |
363
|
|
|
|
|
|
|
} |
364
|
1358
|
|
|
|
|
3388
|
return; |
365
|
|
|
|
|
|
|
} |
366
|
|
|
|
|
|
|
|
367
|
|
|
|
|
|
|
=head2 add_trigger_time |
368
|
|
|
|
|
|
|
|
369
|
|
|
|
|
|
|
Adds new information about trigger time. |
370
|
|
|
|
|
|
|
|
371
|
|
|
|
|
|
|
It will be available at $node->trigger_times (returns arrayref) |
372
|
|
|
|
|
|
|
|
373
|
|
|
|
|
|
|
=cut |
374
|
|
|
|
|
|
|
|
375
|
|
|
|
|
|
|
sub add_trigger_time { |
376
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
377
|
0
|
0
|
|
|
|
0
|
if ( $self->trigger_times ) { |
378
|
0
|
|
|
|
|
0
|
push @{ $self->trigger_times }, @_; |
|
0
|
|
|
|
|
0
|
|
379
|
|
|
|
|
|
|
} |
380
|
|
|
|
|
|
|
else { |
381
|
0
|
|
|
|
|
0
|
$self->trigger_times( [ @_ ] ); |
382
|
|
|
|
|
|
|
} |
383
|
0
|
|
|
|
|
0
|
return; |
384
|
|
|
|
|
|
|
} |
385
|
|
|
|
|
|
|
|
386
|
|
|
|
|
|
|
=head2 add_subplan |
387
|
|
|
|
|
|
|
|
388
|
|
|
|
|
|
|
Adds new subplan node. |
389
|
|
|
|
|
|
|
|
390
|
|
|
|
|
|
|
It will be available at $node->subplans (returns arrayref) |
391
|
|
|
|
|
|
|
|
392
|
|
|
|
|
|
|
Example of plan with subplan: |
393
|
|
|
|
|
|
|
|
394
|
|
|
|
|
|
|
# explain select *, (select oid::int4 from pg_class c2 where c2.relname = c.relname) - oid::int4 from pg_class c; |
395
|
|
|
|
|
|
|
QUERY PLAN |
396
|
|
|
|
|
|
|
------------------------------------------------------------------------------------------------------ |
397
|
|
|
|
|
|
|
Seq Scan on pg_class c (cost=0.00..1885.60 rows=227 width=200) |
398
|
|
|
|
|
|
|
SubPlan |
399
|
|
|
|
|
|
|
-> Index Scan using pg_class_relname_nsp_index on pg_class c2 (cost=0.00..8.27 rows=1 width=4) |
400
|
|
|
|
|
|
|
Index Cond: (relname = $0) |
401
|
|
|
|
|
|
|
(4 rows) |
402
|
|
|
|
|
|
|
|
403
|
|
|
|
|
|
|
|
404
|
|
|
|
|
|
|
=cut |
405
|
|
|
|
|
|
|
|
406
|
|
|
|
|
|
|
sub add_subplan { |
407
|
35
|
|
|
35
|
1
|
56
|
my $self = shift; |
408
|
35
|
|
|
|
|
76
|
my @nodes = map { $_->parent( $self ); $_ } @_; |
|
35
|
|
|
|
|
108
|
|
|
35
|
|
|
|
|
91
|
|
409
|
35
|
100
|
|
|
|
99
|
if ( $self->subplans ) { |
410
|
5
|
|
|
|
|
11
|
push @{ $self->subplans }, @nodes; |
|
5
|
|
|
|
|
13
|
|
411
|
|
|
|
|
|
|
} |
412
|
|
|
|
|
|
|
else { |
413
|
30
|
|
|
|
|
93
|
$self->subplans( [ @nodes ] ); |
414
|
|
|
|
|
|
|
} |
415
|
35
|
|
|
|
|
129
|
return; |
416
|
|
|
|
|
|
|
} |
417
|
|
|
|
|
|
|
|
418
|
|
|
|
|
|
|
=head2 add_initplan |
419
|
|
|
|
|
|
|
|
420
|
|
|
|
|
|
|
Adds new initplan node. |
421
|
|
|
|
|
|
|
|
422
|
|
|
|
|
|
|
Expects to get node object and hashred with metainformation. |
423
|
|
|
|
|
|
|
|
424
|
|
|
|
|
|
|
It will be available at $node->initplans (returns arrayref) and $node->initplans_metainfo (also arrayref); |
425
|
|
|
|
|
|
|
|
426
|
|
|
|
|
|
|
Example of plan with initplan: |
427
|
|
|
|
|
|
|
|
428
|
|
|
|
|
|
|
# explain analyze select 1 = (select 1); |
429
|
|
|
|
|
|
|
QUERY PLAN |
430
|
|
|
|
|
|
|
-------------------------------------------------------------------------------------------- |
431
|
|
|
|
|
|
|
Result (cost=0.01..0.02 rows=1 width=0) (actual time=0.033..0.035 rows=1 loops=1) |
432
|
|
|
|
|
|
|
InitPlan |
433
|
|
|
|
|
|
|
-> Result (cost=0.00..0.01 rows=1 width=0) (actual time=0.003..0.005 rows=1 loops=1) |
434
|
|
|
|
|
|
|
Total runtime: 0.234 ms |
435
|
|
|
|
|
|
|
(4 rows) |
436
|
|
|
|
|
|
|
|
437
|
|
|
|
|
|
|
=cut |
438
|
|
|
|
|
|
|
|
439
|
|
|
|
|
|
|
sub add_initplan { |
440
|
37
|
|
|
37
|
1
|
73
|
my $self = shift; |
441
|
37
|
|
|
|
|
94
|
my ( $node, $node_info ) = @_; |
442
|
|
|
|
|
|
|
|
443
|
37
|
100
|
|
|
|
121
|
$self->initplans( [] ) unless $self->initplans; |
444
|
37
|
100
|
|
|
|
100
|
$self->initplans_metainfo( [] ) unless $self->initplans_metainfo; |
445
|
|
|
|
|
|
|
|
446
|
37
|
|
|
|
|
105
|
$node->parent( $self ); |
447
|
37
|
|
|
|
|
52
|
push @{ $self->initplans }, $node; |
|
37
|
|
|
|
|
75
|
|
448
|
37
|
|
|
|
|
118
|
push @{ $self->initplans_metainfo }, $node_info; |
|
37
|
|
|
|
|
74
|
|
449
|
37
|
|
|
|
|
130
|
return; |
450
|
|
|
|
|
|
|
} |
451
|
|
|
|
|
|
|
|
452
|
|
|
|
|
|
|
=head2 add_cte |
453
|
|
|
|
|
|
|
|
454
|
|
|
|
|
|
|
Adds new cte node. CTE has to be named, so this function requires 2 arguments: name, and cte object itself. |
455
|
|
|
|
|
|
|
|
456
|
|
|
|
|
|
|
It will be available at $node->cte( name ), or $node->ctes (returns hashref). |
457
|
|
|
|
|
|
|
|
458
|
|
|
|
|
|
|
Since we need order (ctes are stored unordered, in hash), there is also $node->cte_order() which returns arrayref of names. |
459
|
|
|
|
|
|
|
|
460
|
|
|
|
|
|
|
=cut |
461
|
|
|
|
|
|
|
|
462
|
|
|
|
|
|
|
sub add_cte { |
463
|
32
|
|
|
32
|
1
|
55
|
my $self = shift; |
464
|
32
|
|
|
|
|
89
|
my ( $name, $cte ) = @_; |
465
|
32
|
|
|
|
|
104
|
$cte->parent( $self ); |
466
|
|
|
|
|
|
|
|
467
|
32
|
100
|
|
|
|
90
|
if ( $self->ctes ) { |
468
|
9
|
|
|
|
|
22
|
$self->ctes->{ $name } = $cte; |
469
|
9
|
|
|
|
|
13
|
push @{ $self->cte_order }, $name; |
|
9
|
|
|
|
|
15
|
|
470
|
|
|
|
|
|
|
} |
471
|
|
|
|
|
|
|
else { |
472
|
23
|
|
|
|
|
115
|
$self->ctes( { $name => $cte } ); |
473
|
23
|
|
|
|
|
82
|
$self->cte_order( [ $name ] ); |
474
|
|
|
|
|
|
|
} |
475
|
32
|
|
|
|
|
75
|
return; |
476
|
|
|
|
|
|
|
} |
477
|
|
|
|
|
|
|
|
478
|
|
|
|
|
|
|
=head2 cte |
479
|
|
|
|
|
|
|
|
480
|
|
|
|
|
|
|
Returns CTE object that has given name. |
481
|
|
|
|
|
|
|
|
482
|
|
|
|
|
|
|
=cut |
483
|
|
|
|
|
|
|
|
484
|
|
|
|
|
|
|
sub cte { |
485
|
11
|
|
|
11
|
1
|
19
|
my $self = shift; |
486
|
11
|
|
|
|
|
18
|
my $name = shift; |
487
|
11
|
|
|
|
|
25
|
return $self->ctes->{ $name }; |
488
|
|
|
|
|
|
|
} |
489
|
|
|
|
|
|
|
|
490
|
|
|
|
|
|
|
=head2 add_sub_node |
491
|
|
|
|
|
|
|
|
492
|
|
|
|
|
|
|
Adds new sub node. |
493
|
|
|
|
|
|
|
|
494
|
|
|
|
|
|
|
It will be available at $node->sub_nodes (returns arrayref) |
495
|
|
|
|
|
|
|
|
496
|
|
|
|
|
|
|
Sub nodes are nodes that are used by given node as data sources. |
497
|
|
|
|
|
|
|
|
498
|
|
|
|
|
|
|
For example - "Join" node, has 2 sources (sub_nodes), which are table scans (Seq Scan, Index Scan or Backward Index Scan) over some tables. |
499
|
|
|
|
|
|
|
|
500
|
|
|
|
|
|
|
Example plan which contains subnode: |
501
|
|
|
|
|
|
|
|
502
|
|
|
|
|
|
|
# explain select * from test limit 1; |
503
|
|
|
|
|
|
|
QUERY PLAN |
504
|
|
|
|
|
|
|
-------------------------------------------------------------- |
505
|
|
|
|
|
|
|
Limit (cost=0.00..0.01 rows=1 width=4) |
506
|
|
|
|
|
|
|
-> Seq Scan on test (cost=0.00..14.00 rows=1000 width=4) |
507
|
|
|
|
|
|
|
(2 rows) |
508
|
|
|
|
|
|
|
|
509
|
|
|
|
|
|
|
Node 'Limit' has 1 sub_plan, which is "Seq Scan" |
510
|
|
|
|
|
|
|
|
511
|
|
|
|
|
|
|
=cut |
512
|
|
|
|
|
|
|
|
513
|
|
|
|
|
|
|
sub add_sub_node { |
514
|
895
|
|
|
895
|
1
|
1349
|
my $self = shift; |
515
|
895
|
|
|
|
|
1626
|
my @nodes = map { $_->parent( $self ); $_ } @_; |
|
895
|
|
|
|
|
2240
|
|
|
895
|
|
|
|
|
2133
|
|
516
|
895
|
100
|
|
|
|
1884
|
if ( $self->sub_nodes ) { |
517
|
247
|
|
|
|
|
378
|
push @{ $self->sub_nodes }, @nodes; |
|
247
|
|
|
|
|
441
|
|
518
|
|
|
|
|
|
|
} |
519
|
|
|
|
|
|
|
else { |
520
|
648
|
|
|
|
|
1442
|
$self->sub_nodes( [ @nodes ] ); |
521
|
|
|
|
|
|
|
} |
522
|
895
|
|
|
|
|
2740
|
return; |
523
|
|
|
|
|
|
|
} |
524
|
|
|
|
|
|
|
|
525
|
|
|
|
|
|
|
=head2 get_struct |
526
|
|
|
|
|
|
|
|
527
|
|
|
|
|
|
|
Function which returns simple, not blessed, hashref with all information about given explain node and it's children. |
528
|
|
|
|
|
|
|
|
529
|
|
|
|
|
|
|
This can be used for debug purposes, or as a base to print information to user. |
530
|
|
|
|
|
|
|
|
531
|
|
|
|
|
|
|
Output looks like this: |
532
|
|
|
|
|
|
|
|
533
|
|
|
|
|
|
|
{ |
534
|
|
|
|
|
|
|
'estimated_rows' => '10000', |
535
|
|
|
|
|
|
|
'estimated_row_width' => '148', |
536
|
|
|
|
|
|
|
'estimated_startup_cost' => '0', |
537
|
|
|
|
|
|
|
'estimated_total_cost' => '333', |
538
|
|
|
|
|
|
|
'scan_on' => { 'table_name' => 'tenk1', }, |
539
|
|
|
|
|
|
|
'type' => 'Seq Scan', |
540
|
|
|
|
|
|
|
} |
541
|
|
|
|
|
|
|
|
542
|
|
|
|
|
|
|
=cut |
543
|
|
|
|
|
|
|
|
544
|
|
|
|
|
|
|
sub get_struct { |
545
|
539
|
|
|
539
|
1
|
960
|
my $self = shift; |
546
|
539
|
|
|
|
|
725
|
my $reply = {}; |
547
|
|
|
|
|
|
|
|
548
|
539
|
50
|
|
|
|
940
|
$reply->{ 'estimated_row_width' } = $self->estimated_row_width if defined $self->estimated_row_width; |
549
|
539
|
50
|
|
|
|
922
|
$reply->{ 'estimated_rows' } = $self->estimated_rows if defined $self->estimated_rows; |
550
|
539
|
50
|
|
|
|
910
|
$reply->{ 'estimated_startup_cost' } = 0 + $self->estimated_startup_cost if defined $self->estimated_startup_cost; # "0+" to remove .00 in case of integers |
551
|
539
|
50
|
|
|
|
1010
|
$reply->{ 'estimated_total_cost' } = 0 + $self->estimated_total_cost if defined $self->estimated_total_cost; # "0+" to remove .00 in case of integers |
552
|
539
|
100
|
|
|
|
870
|
$reply->{ 'actual_loops' } = $self->actual_loops if defined $self->actual_loops; |
553
|
539
|
100
|
|
|
|
923
|
$reply->{ 'actual_rows' } = $self->actual_rows if defined $self->actual_rows; |
554
|
539
|
100
|
|
|
|
849
|
$reply->{ 'actual_time_first' } = 0 + $self->actual_time_first if defined $self->actual_time_first; # "0+" to remove .00 in case of integers |
555
|
539
|
100
|
|
|
|
892
|
$reply->{ 'actual_time_last' } = 0 + $self->actual_time_last if defined $self->actual_time_last; # "0+" to remove .00 in case of integers |
556
|
539
|
50
|
|
|
|
963
|
$reply->{ 'type' } = $self->type if defined $self->type; |
557
|
539
|
100
|
|
|
|
887
|
$reply->{ 'scan_on' } = clone( $self->scan_on ) if defined $self->scan_on; |
558
|
539
|
100
|
|
|
|
1082
|
$reply->{ 'extra_info' } = clone( $self->extra_info ) if defined $self->extra_info; |
559
|
539
|
100
|
|
|
|
938
|
$reply->{ 'initplans_metainfo' } = clone( $self->initplans_metainfo ) if defined $self->initplans_metainfo; |
560
|
|
|
|
|
|
|
|
561
|
539
|
|
|
|
|
807
|
$reply->{ 'is_analyzed' } = $self->is_analyzed; |
562
|
|
|
|
|
|
|
|
563
|
539
|
100
|
|
|
|
904
|
$reply->{ 'sub_nodes' } = [ map { $_->get_struct } @{ $self->sub_nodes } ] if defined $self->sub_nodes; |
|
292
|
|
|
|
|
655
|
|
|
234
|
|
|
|
|
325
|
|
564
|
539
|
100
|
|
|
|
898
|
$reply->{ 'initplans' } = [ map { $_->get_struct } @{ $self->initplans } ] if defined $self->initplans; |
|
33
|
|
|
|
|
86
|
|
|
30
|
|
|
|
|
51
|
|
565
|
539
|
100
|
|
|
|
791
|
$reply->{ 'subplans' } = [ map { $_->get_struct } @{ $self->subplans } ] if defined $self->subplans; |
|
24
|
|
|
|
|
48
|
|
|
21
|
|
|
|
|
39
|
|
566
|
|
|
|
|
|
|
|
567
|
539
|
100
|
|
|
|
820
|
$reply->{ 'buffers' } = $self->buffers->get_struct() if $self->buffers; |
568
|
|
|
|
|
|
|
|
569
|
539
|
100
|
|
|
|
865
|
$reply->{ 'cte_order' } = clone( $self->cte_order ) if defined $self->cte_order; |
570
|
539
|
100
|
|
|
|
831
|
if ( defined $self->ctes ) { |
571
|
9
|
|
|
|
|
18
|
$reply->{ 'ctes' } = {}; |
572
|
9
|
|
|
|
|
17
|
while ( my ( $key, $cte_node ) = each %{ $self->ctes } ) { |
|
27
|
|
|
|
|
39
|
|
573
|
18
|
|
|
|
|
31
|
my $struct = $cte_node->get_struct; |
574
|
18
|
|
|
|
|
46
|
$reply->{ 'ctes' }->{ $key } = $struct; |
575
|
|
|
|
|
|
|
} |
576
|
|
|
|
|
|
|
} |
577
|
539
|
|
|
|
|
1352
|
return $reply; |
578
|
|
|
|
|
|
|
} |
579
|
|
|
|
|
|
|
|
580
|
|
|
|
|
|
|
=head2 total_inclusive_time |
581
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
Method for getting total node time, summarized with times of all subnodes, subplans and initplans - which is basically ->actual_loops * ->actual_time_last. |
583
|
|
|
|
|
|
|
|
584
|
|
|
|
|
|
|
=cut |
585
|
|
|
|
|
|
|
|
586
|
|
|
|
|
|
|
sub total_inclusive_time { |
587
|
614
|
|
|
614
|
1
|
699
|
my $self = shift; |
588
|
614
|
100
|
|
|
|
869
|
return unless defined $self->actual_time_last; |
589
|
610
|
50
|
|
|
|
936
|
return unless defined $self->actual_loops; |
590
|
610
|
|
|
|
|
860
|
return $self->actual_loops * $self->actual_time_last / $self->workers; |
591
|
|
|
|
|
|
|
} |
592
|
|
|
|
|
|
|
|
593
|
|
|
|
|
|
|
=head2 total_rows |
594
|
|
|
|
|
|
|
|
595
|
|
|
|
|
|
|
Method for getting total number of rows returned by current node. This takes into account parallelization and multiple loops. |
596
|
|
|
|
|
|
|
|
597
|
|
|
|
|
|
|
=cut |
598
|
|
|
|
|
|
|
|
599
|
|
|
|
|
|
|
sub total_rows { |
600
|
167
|
|
|
167
|
1
|
227
|
my $self = shift; |
601
|
167
|
50
|
|
|
|
254
|
return unless defined $self->actual_time_last; |
602
|
167
|
50
|
|
|
|
242
|
return unless defined $self->actual_loops; |
603
|
167
|
100
|
|
|
|
252
|
return $self->actual_loops * $self->actual_rows if 1 == $self->workers; |
604
|
1
|
|
|
|
|
3
|
return $self->workers * $self->actual_rows; |
605
|
|
|
|
|
|
|
} |
606
|
|
|
|
|
|
|
|
607
|
|
|
|
|
|
|
=head2 total_rows_removed |
608
|
|
|
|
|
|
|
|
609
|
|
|
|
|
|
|
Sum of rows removed by: |
610
|
|
|
|
|
|
|
|
611
|
|
|
|
|
|
|
=over |
612
|
|
|
|
|
|
|
|
613
|
|
|
|
|
|
|
=item * Conflict Filter |
614
|
|
|
|
|
|
|
|
615
|
|
|
|
|
|
|
=item * Filter |
616
|
|
|
|
|
|
|
|
617
|
|
|
|
|
|
|
=item * Index Recheck |
618
|
|
|
|
|
|
|
|
619
|
|
|
|
|
|
|
=item * Join Filter |
620
|
|
|
|
|
|
|
|
621
|
|
|
|
|
|
|
=back |
622
|
|
|
|
|
|
|
|
623
|
|
|
|
|
|
|
in given node. |
624
|
|
|
|
|
|
|
|
625
|
|
|
|
|
|
|
=cut |
626
|
|
|
|
|
|
|
|
627
|
|
|
|
|
|
|
sub total_rows_removed { |
628
|
251
|
|
|
251
|
1
|
281
|
my $self = shift; |
629
|
251
|
100
|
|
|
|
349
|
return 0 unless $self->extra_info; |
630
|
249
|
|
|
|
|
285
|
my $removed = 0; |
631
|
249
|
|
|
|
|
247
|
for my $line ( @{ $self->extra_info } ) { |
|
249
|
|
|
|
|
322
|
|
632
|
502
|
100
|
|
|
|
1132
|
next unless $line =~ m{^Rows Removed by (?:Conflict Filter|Filter|Index Recheck|Join Filter): (\d+)$}; |
633
|
250
|
|
|
|
|
521
|
$removed += $1; |
634
|
|
|
|
|
|
|
} |
635
|
249
|
100
|
|
|
|
368
|
return $self->actual_loops * $removed if 1 == $self->workers; |
636
|
1
|
|
|
|
|
5
|
return $self->workers * $removed; |
637
|
|
|
|
|
|
|
} |
638
|
|
|
|
|
|
|
|
639
|
|
|
|
|
|
|
=head2 total_exclusive_time |
640
|
|
|
|
|
|
|
|
641
|
|
|
|
|
|
|
Method for getting total node time, without times of subnodes - which amounts to time PostgreSQL spent running this paricular node. |
642
|
|
|
|
|
|
|
|
643
|
|
|
|
|
|
|
=cut |
644
|
|
|
|
|
|
|
|
645
|
|
|
|
|
|
|
sub total_exclusive_time { |
646
|
251
|
|
|
251
|
1
|
452
|
my $self = shift; |
647
|
|
|
|
|
|
|
|
648
|
251
|
|
|
|
|
409
|
my $time = $self->total_inclusive_time; |
649
|
251
|
100
|
|
|
|
453
|
return unless defined $time; |
650
|
|
|
|
|
|
|
|
651
|
250
|
|
|
|
|
394
|
for my $node ( map { @{ $_ } } grep { defined $_ } ( $self->sub_nodes ) ) { |
|
63
|
|
|
|
|
79
|
|
|
63
|
|
|
|
|
184
|
|
|
250
|
|
|
|
|
521
|
|
652
|
84
|
|
50
|
|
|
126
|
$time -= ( $node->total_inclusive_time || 0 ); |
653
|
|
|
|
|
|
|
} |
654
|
|
|
|
|
|
|
|
655
|
250
|
|
|
|
|
439
|
for my $plan ( map { @{ $_ } } grep { defined $_ } ( $self->subplans ) ) { |
|
33
|
|
|
|
|
46
|
|
|
33
|
|
|
|
|
85
|
|
|
250
|
|
|
|
|
448
|
|
656
|
34
|
|
100
|
|
|
70
|
$time -= ( $plan->total_inclusive_time || 0 ); |
657
|
|
|
|
|
|
|
} |
658
|
|
|
|
|
|
|
|
659
|
|
|
|
|
|
|
# Apply fix from ->exclusive_fix |
660
|
250
|
|
|
|
|
435
|
$time += $self->exclusive_fix; |
661
|
|
|
|
|
|
|
|
662
|
|
|
|
|
|
|
# ignore negative times - these come from rounding errors on nodes with loops > 1. |
663
|
250
|
100
|
|
|
|
428
|
return 0 if $time < 0; |
664
|
|
|
|
|
|
|
|
665
|
249
|
|
|
|
|
767
|
return $time; |
666
|
|
|
|
|
|
|
} |
667
|
|
|
|
|
|
|
|
668
|
|
|
|
|
|
|
=head2 total_exclusive_buffers |
669
|
|
|
|
|
|
|
|
670
|
|
|
|
|
|
|
Method for getting total buffers used by node, without buffers used by subnodes. |
671
|
|
|
|
|
|
|
|
672
|
|
|
|
|
|
|
=cut |
673
|
|
|
|
|
|
|
|
674
|
|
|
|
|
|
|
sub total_exclusive_buffers { |
675
|
3
|
|
|
3
|
1
|
5
|
my $self = shift; |
676
|
|
|
|
|
|
|
|
677
|
3
|
50
|
|
|
|
8
|
return unless $self->buffers; |
678
|
|
|
|
|
|
|
|
679
|
3
|
|
|
|
|
9
|
my @nodes = grep { $_->buffers } $self->all_subnodes; |
|
4
|
|
|
|
|
9
|
|
680
|
3
|
50
|
|
|
|
9
|
return $self->buffers if 0 == scalar @nodes; |
681
|
|
|
|
|
|
|
|
682
|
3
|
|
|
|
|
40
|
my $sub_node_buffers = $nodes[ 0 ]->buffers; |
683
|
3
|
|
|
|
|
4
|
shift @nodes; |
684
|
3
|
|
|
|
|
8
|
for my $n ( @nodes ) { |
685
|
1
|
|
|
|
|
10
|
$sub_node_buffers = $sub_node_buffers + $n->buffers; |
686
|
|
|
|
|
|
|
} |
687
|
|
|
|
|
|
|
|
688
|
3
|
|
|
|
|
7
|
return $self->buffers - $sub_node_buffers; |
689
|
|
|
|
|
|
|
} |
690
|
|
|
|
|
|
|
|
691
|
|
|
|
|
|
|
=head2 all_subnodes |
692
|
|
|
|
|
|
|
|
693
|
|
|
|
|
|
|
Returns list of all subnodes of current node. |
694
|
|
|
|
|
|
|
|
695
|
|
|
|
|
|
|
=cut |
696
|
|
|
|
|
|
|
|
697
|
|
|
|
|
|
|
sub all_subnodes { |
698
|
57
|
|
|
57
|
1
|
71
|
my $self = shift; |
699
|
57
|
|
|
|
|
66
|
my @nodes = (); |
700
|
57
|
100
|
|
|
|
88
|
push @nodes, @{ $self->sub_nodes } if $self->sub_nodes; |
|
29
|
|
|
|
|
45
|
|
701
|
57
|
100
|
|
|
|
94
|
push @nodes, @{ $self->initplans } if $self->initplans; |
|
2
|
|
|
|
|
5
|
|
702
|
57
|
100
|
|
|
|
87
|
push @nodes, @{ $self->subplans } if $self->subplans; |
|
4
|
|
|
|
|
9
|
|
703
|
57
|
50
|
|
|
|
127
|
push @nodes, values %{ $self->ctes } if $self->ctes; |
|
0
|
|
|
|
|
0
|
|
704
|
57
|
|
|
|
|
140
|
return @nodes; |
705
|
|
|
|
|
|
|
} |
706
|
|
|
|
|
|
|
|
707
|
|
|
|
|
|
|
=head2 all_recursive_subnodes |
708
|
|
|
|
|
|
|
|
709
|
|
|
|
|
|
|
Returns list of all subnodes of current node and its subnodes, and their subnodes, and ... |
710
|
|
|
|
|
|
|
|
711
|
|
|
|
|
|
|
=cut |
712
|
|
|
|
|
|
|
|
713
|
|
|
|
|
|
|
sub all_recursive_subnodes { |
714
|
3124
|
|
|
3124
|
1
|
3557
|
my $self = shift; |
715
|
3124
|
|
|
|
|
3560
|
my @nodes = (); |
716
|
3124
|
100
|
|
|
|
3941
|
push @nodes, @{ $self->sub_nodes } if $self->sub_nodes; |
|
1308
|
|
|
|
|
1739
|
|
717
|
3124
|
100
|
|
|
|
4297
|
push @nodes, @{ $self->initplans } if $self->initplans; |
|
113
|
|
|
|
|
160
|
|
718
|
3124
|
100
|
|
|
|
4129
|
push @nodes, @{ $self->subplans } if $self->subplans; |
|
60
|
|
|
|
|
100
|
|
719
|
3124
|
100
|
|
|
|
4108
|
push @nodes, values %{ $self->ctes } if $self->ctes; |
|
75
|
|
|
|
|
128
|
|
720
|
|
|
|
|
|
|
|
721
|
3124
|
|
|
|
|
5508
|
return map { $_, $_->all_recursive_subnodes } @nodes; |
|
2156
|
|
|
|
|
3416
|
|
722
|
|
|
|
|
|
|
} |
723
|
|
|
|
|
|
|
|
724
|
|
|
|
|
|
|
=head2 all_parents |
725
|
|
|
|
|
|
|
|
726
|
|
|
|
|
|
|
Returns list of all nodes that are "above" given node in explain. |
727
|
|
|
|
|
|
|
|
728
|
|
|
|
|
|
|
List can be empty if it's top level node. |
729
|
|
|
|
|
|
|
|
730
|
|
|
|
|
|
|
=cut |
731
|
|
|
|
|
|
|
|
732
|
|
|
|
|
|
|
sub all_parents { |
733
|
20
|
|
|
20
|
1
|
10700
|
my $self = shift; |
734
|
20
|
|
|
|
|
26
|
my @nodes = (); |
735
|
20
|
|
|
|
|
26
|
my $current = $self; |
736
|
20
|
|
|
|
|
35
|
while ( my $next = $current->parent ) { |
737
|
79
|
|
|
|
|
85
|
unshift @nodes, $next; |
738
|
79
|
|
|
|
|
104
|
$current = $next; |
739
|
|
|
|
|
|
|
} |
740
|
20
|
|
|
|
|
38
|
return @nodes; |
741
|
|
|
|
|
|
|
} |
742
|
|
|
|
|
|
|
|
743
|
|
|
|
|
|
|
=head2 is_analyzed |
744
|
|
|
|
|
|
|
|
745
|
|
|
|
|
|
|
Returns 1 if the explain node it represents was generated by EXPLAIN ANALYZE. 0 otherwise. |
746
|
|
|
|
|
|
|
|
747
|
|
|
|
|
|
|
=cut |
748
|
|
|
|
|
|
|
|
749
|
|
|
|
|
|
|
sub is_analyzed { |
750
|
2402
|
|
|
2402
|
1
|
3006
|
my $self = shift; |
751
|
|
|
|
|
|
|
|
752
|
2402
|
100
|
66
|
|
|
3967
|
return defined $self->actual_loops || $self->never_executed ? 1 : 0; |
753
|
|
|
|
|
|
|
} |
754
|
|
|
|
|
|
|
|
755
|
|
|
|
|
|
|
=head2 as_text |
756
|
|
|
|
|
|
|
|
757
|
|
|
|
|
|
|
Returns textual representation of explain nodes from given node down. |
758
|
|
|
|
|
|
|
|
759
|
|
|
|
|
|
|
This is used to build textual explains out of in-memory data structures. |
760
|
|
|
|
|
|
|
|
761
|
|
|
|
|
|
|
=cut |
762
|
|
|
|
|
|
|
|
763
|
|
|
|
|
|
|
sub as_text { |
764
|
282
|
|
|
282
|
1
|
447
|
my $self = shift; |
765
|
282
|
|
|
|
|
374
|
my $prefix = shift; |
766
|
282
|
100
|
|
|
|
625
|
$prefix = '' unless defined $prefix; |
767
|
|
|
|
|
|
|
|
768
|
282
|
100
|
|
|
|
620
|
$prefix .= '-> ' if '' ne $prefix; |
769
|
282
|
|
|
|
|
465
|
my $prefix_on_spaces = $prefix . " "; |
770
|
282
|
|
|
|
|
1150
|
$prefix_on_spaces =~ s/[^ ]/ /g; |
771
|
|
|
|
|
|
|
|
772
|
282
|
|
|
|
|
673
|
my $heading_line = $self->type; |
773
|
|
|
|
|
|
|
|
774
|
282
|
100
|
|
|
|
528
|
if ( $self->scan_on ) { |
775
|
140
|
|
|
|
|
238
|
my $S = $self->scan_on; |
776
|
140
|
100
|
|
|
|
602
|
if ( $S->{ 'cte_name' } ) { |
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
777
|
11
|
|
|
|
|
33
|
$heading_line .= " on " . $S->{ 'cte_name' }; |
778
|
11
|
100
|
|
|
|
55
|
$heading_line .= " " . $S->{ 'cte_alias' } if $S->{ 'cte_alias' }; |
779
|
|
|
|
|
|
|
} |
780
|
|
|
|
|
|
|
elsif ( $S->{ 'function_name' } ) { |
781
|
3
|
|
|
|
|
13
|
$heading_line .= " on " . $S->{ 'function_name' }; |
782
|
3
|
50
|
|
|
|
13
|
$heading_line .= " " . $S->{ 'function_alias' } if $S->{ 'function_alias' }; |
783
|
|
|
|
|
|
|
} |
784
|
|
|
|
|
|
|
elsif ( $S->{ 'index_name' } ) { |
785
|
42
|
100
|
|
|
|
85
|
if ( $S->{ 'table_name' } ) { |
786
|
34
|
|
|
|
|
91
|
$heading_line .= " using " . $S->{ 'index_name' } . " on " . $S->{ 'table_name' }; |
787
|
34
|
100
|
|
|
|
99
|
$heading_line .= " " . $S->{ 'table_alias' } if $S->{ 'table_alias' }; |
788
|
|
|
|
|
|
|
} |
789
|
|
|
|
|
|
|
else { |
790
|
8
|
|
|
|
|
36
|
$heading_line .= " on " . $S->{ 'index_name' }; |
791
|
|
|
|
|
|
|
} |
792
|
|
|
|
|
|
|
} |
793
|
|
|
|
|
|
|
elsif ( $S->{ 'subquery_name' } ) { |
794
|
1
|
|
|
|
|
4
|
$heading_line .= " on " . $S->{ 'subquery_name' },; |
795
|
|
|
|
|
|
|
} |
796
|
|
|
|
|
|
|
elsif ( $S->{ 'worktable_name' } ) { |
797
|
4
|
|
|
|
|
12
|
$heading_line .= " on " . $S->{ 'worktable_name' },; |
798
|
4
|
50
|
|
|
|
22
|
$heading_line .= " " . $S->{ 'worktable_alias' } if $S->{ 'worktable_alias' }; |
799
|
|
|
|
|
|
|
} |
800
|
|
|
|
|
|
|
else { |
801
|
79
|
|
|
|
|
191
|
$heading_line .= " on " . $S->{ 'table_name' }; |
802
|
79
|
100
|
|
|
|
264
|
$heading_line .= " " . $S->{ 'table_alias' } if $S->{ 'table_alias' }; |
803
|
|
|
|
|
|
|
} |
804
|
|
|
|
|
|
|
} |
805
|
282
|
|
|
|
|
556
|
$heading_line .= sprintf ' (cost=%.2f..%.2f rows=%s width=%d)', $self->estimated_startup_cost, $self->estimated_total_cost, $self->estimated_rows, $self->estimated_row_width; |
806
|
282
|
100
|
|
|
|
829
|
if ( $self->is_analyzed ) { |
807
|
231
|
|
|
|
|
302
|
my $inner; |
808
|
231
|
100
|
|
|
|
579
|
if ( $self->never_executed ) { |
|
|
100
|
|
|
|
|
|
809
|
6
|
|
|
|
|
10
|
$inner = 'never executed'; |
810
|
|
|
|
|
|
|
} |
811
|
|
|
|
|
|
|
elsif ( defined $self->actual_time_last ) { |
812
|
217
|
|
|
|
|
374
|
$inner = sprintf 'actual time=%.3f..%.3f rows=%s loops=%d', $self->actual_time_first, $self->actual_time_last, $self->actual_rows, $self->actual_loops; |
813
|
|
|
|
|
|
|
} |
814
|
|
|
|
|
|
|
else { |
815
|
8
|
|
|
|
|
13
|
$inner = sprintf 'actual rows=%s loops=%d', $self->actual_rows, $self->actual_loops; |
816
|
|
|
|
|
|
|
} |
817
|
231
|
|
|
|
|
699
|
$heading_line .= " ($inner)"; |
818
|
|
|
|
|
|
|
} |
819
|
|
|
|
|
|
|
|
820
|
282
|
|
|
|
|
496
|
my @lines = (); |
821
|
|
|
|
|
|
|
|
822
|
282
|
|
|
|
|
574
|
push @lines, $prefix . $heading_line; |
823
|
282
|
100
|
|
|
|
476
|
if ( $self->extra_info ) { |
824
|
151
|
|
|
|
|
189
|
push @lines, $prefix_on_spaces . " " . $_ for @{ $self->extra_info }; |
|
151
|
|
|
|
|
232
|
|
825
|
|
|
|
|
|
|
} |
826
|
282
|
|
|
|
|
856
|
my $textual = join( "\n", @lines ) . "\n"; |
827
|
282
|
100
|
|
|
|
578
|
if ( $self->buffers ) { |
828
|
62
|
|
|
|
|
112
|
my $buf_info = $self->buffers->as_text; |
829
|
62
|
|
|
|
|
286
|
$buf_info =~ s/^/${prefix_on_spaces}/gm; |
830
|
62
|
|
|
|
|
226
|
$textual .= $buf_info . "\n"; |
831
|
|
|
|
|
|
|
} |
832
|
|
|
|
|
|
|
|
833
|
282
|
100
|
|
|
|
535
|
if ( $self->cte_order ) { |
834
|
9
|
|
|
|
|
16
|
for my $cte_name ( @{ $self->cte_order } ) { |
|
9
|
|
|
|
|
19
|
|
835
|
11
|
|
|
|
|
62
|
$textual .= $prefix_on_spaces . "CTE " . $cte_name . "\n"; |
836
|
11
|
|
|
|
|
38
|
$textual .= $self->cte( $cte_name )->as_text( $prefix_on_spaces . " " ); |
837
|
|
|
|
|
|
|
} |
838
|
|
|
|
|
|
|
} |
839
|
|
|
|
|
|
|
|
840
|
282
|
100
|
|
|
|
481
|
if ( $self->initplans ) { |
841
|
11
|
|
|
|
|
17
|
for my $i ( 0 .. $#{ $self->initplans } ) { |
|
11
|
|
|
|
|
25
|
|
842
|
12
|
|
|
|
|
20
|
my $ip = $self->initplans->[ $i ]; |
843
|
12
|
|
|
|
|
21
|
my $meta = $self->initplans_metainfo->[ $i ]; |
844
|
12
|
|
|
|
|
19
|
my $init_name; |
845
|
12
|
100
|
|
|
|
23
|
if ( $meta ) { |
846
|
8
|
|
|
|
|
34
|
$init_name = sprintf "InitPlan %d (returns %s)\n", $meta->{ 'name' }, $meta->{ 'returns' }; |
847
|
|
|
|
|
|
|
} |
848
|
|
|
|
|
|
|
else { |
849
|
4
|
|
|
|
|
5
|
$init_name = "InitPlan\n"; |
850
|
|
|
|
|
|
|
} |
851
|
12
|
|
|
|
|
43
|
$textual .= $prefix_on_spaces . $init_name; |
852
|
12
|
|
|
|
|
57
|
$textual .= $ip->as_text( $prefix_on_spaces . " " ); |
853
|
|
|
|
|
|
|
} |
854
|
|
|
|
|
|
|
} |
855
|
282
|
100
|
|
|
|
505
|
if ( $self->sub_nodes ) { |
856
|
133
|
|
|
|
|
169
|
$textual .= $_->as_text( $prefix_on_spaces ) for @{ $self->sub_nodes }; |
|
133
|
|
|
|
|
203
|
|
857
|
|
|
|
|
|
|
} |
858
|
282
|
100
|
|
|
|
512
|
if ( $self->subplans ) { |
859
|
6
|
|
|
|
|
7
|
for my $ip ( @{ $self->subplans } ) { |
|
6
|
|
|
|
|
86
|
|
860
|
7
|
|
|
|
|
22
|
$textual .= $prefix_on_spaces . "SubPlan\n"; |
861
|
7
|
|
|
|
|
104
|
$textual .= $ip->as_text( $prefix_on_spaces . " " ); |
862
|
|
|
|
|
|
|
} |
863
|
|
|
|
|
|
|
} |
864
|
282
|
|
|
|
|
1036
|
return $textual; |
865
|
|
|
|
|
|
|
} |
866
|
|
|
|
|
|
|
|
867
|
|
|
|
|
|
|
=head2 anonymize_gathering |
868
|
|
|
|
|
|
|
|
869
|
|
|
|
|
|
|
First stage of anonymization - gathering of all possible strings that could and should be anonymized. |
870
|
|
|
|
|
|
|
|
871
|
|
|
|
|
|
|
=cut |
872
|
|
|
|
|
|
|
|
873
|
|
|
|
|
|
|
sub anonymize_gathering { |
874
|
52
|
|
|
52
|
1
|
89
|
my $self = shift; |
875
|
52
|
|
|
|
|
73
|
my $anonymizer = shift; |
876
|
|
|
|
|
|
|
|
877
|
52
|
100
|
|
|
|
102
|
if ( $self->scan_on ) { |
878
|
26
|
|
|
|
|
49
|
$anonymizer->add( values %{ $self->scan_on } ); |
|
26
|
|
|
|
|
48
|
|
879
|
|
|
|
|
|
|
} |
880
|
|
|
|
|
|
|
|
881
|
52
|
100
|
|
|
|
127
|
if ( $self->cte_order ) { |
882
|
2
|
|
|
|
|
7
|
$anonymizer->add( $self->{ 'cte_order' } ); |
883
|
|
|
|
|
|
|
} |
884
|
|
|
|
|
|
|
|
885
|
52
|
100
|
|
|
|
108
|
if ( $self->extra_info ) { |
886
|
40
|
|
|
|
|
68
|
for my $line ( @{ $self->extra_info } ) { |
|
40
|
|
|
|
|
79
|
|
887
|
83
|
|
|
|
|
9220
|
my $copy = $line; |
888
|
83
|
100
|
|
|
|
238
|
if ( $copy =~ m{^Foreign File:\s+(\S.*?)\s*$} ) { |
889
|
3
|
|
|
|
|
8
|
$anonymizer->add( $1 ); |
890
|
3
|
|
|
|
|
5
|
next; |
891
|
|
|
|
|
|
|
} |
892
|
80
|
100
|
|
|
|
614
|
next unless $copy =~ s{^((?:Join Filter|Index Cond|Recheck Cond|Hash Cond|Merge Cond|Filter|Group Key|Sort Key|Output|One-Time Filter):\s+)(.*)$}{$2}; |
893
|
44
|
|
|
|
|
109
|
my $prefix = $1; |
894
|
44
|
|
|
|
|
107
|
my $lexer = $self->_make_lexer( $copy ); |
895
|
44
|
|
|
|
|
33494
|
while ( my $x = $lexer->() ) { |
896
|
1867
|
50
|
|
|
|
551567
|
next unless ref $x; |
897
|
1867
|
100
|
|
|
|
6590
|
$anonymizer->add( $x->[ 1 ] ) if $x->[ 0 ] =~ m{\A (?: STRING_LITERAL | QUOTED_IDENTIFIER | IDENTIFIER ) \z}x; |
898
|
|
|
|
|
|
|
} |
899
|
|
|
|
|
|
|
} |
900
|
|
|
|
|
|
|
} |
901
|
|
|
|
|
|
|
|
902
|
52
|
|
|
|
|
4187
|
for my $key ( qw( sub_nodes initplans subplans ) ) { |
903
|
156
|
100
|
|
|
|
326
|
next unless $self->{ $key }; |
904
|
30
|
|
|
|
|
69
|
$_->anonymize_gathering( $anonymizer ) for @{ $self->{ $key } }; |
|
30
|
|
|
|
|
169
|
|
905
|
|
|
|
|
|
|
} |
906
|
|
|
|
|
|
|
|
907
|
52
|
100
|
|
|
|
138
|
if ( $self->{ 'ctes' } ) { |
908
|
2
|
|
|
|
|
5
|
$_->anonymize_gathering( $anonymizer ) for values %{ $self->{ 'ctes' } }; |
|
2
|
|
|
|
|
13
|
|
909
|
|
|
|
|
|
|
} |
910
|
52
|
|
|
|
|
130
|
return; |
911
|
|
|
|
|
|
|
} |
912
|
|
|
|
|
|
|
|
913
|
|
|
|
|
|
|
=head2 _make_lexer |
914
|
|
|
|
|
|
|
|
915
|
|
|
|
|
|
|
Helper function which creates HOP::Lexer based lexer for given line of input |
916
|
|
|
|
|
|
|
|
917
|
|
|
|
|
|
|
=cut |
918
|
|
|
|
|
|
|
|
919
|
|
|
|
|
|
|
sub _make_lexer { |
920
|
88
|
|
|
88
|
|
111
|
my $self = shift; |
921
|
88
|
|
|
|
|
114
|
my $data = shift; |
922
|
|
|
|
|
|
|
|
923
|
|
|
|
|
|
|
## Got from PostgreSQL 9.2devel with: |
924
|
|
|
|
|
|
|
# SQL # with z as ( |
925
|
|
|
|
|
|
|
# SQL # select |
926
|
|
|
|
|
|
|
# SQL # typname::text as a, |
927
|
|
|
|
|
|
|
# SQL # oid::regtype::text as b |
928
|
|
|
|
|
|
|
# SQL # from |
929
|
|
|
|
|
|
|
# SQL # pg_type |
930
|
|
|
|
|
|
|
# SQL # where |
931
|
|
|
|
|
|
|
# SQL # typrelid = 0 |
932
|
|
|
|
|
|
|
# SQL # and typnamespace = 11 |
933
|
|
|
|
|
|
|
# SQL # ), |
934
|
|
|
|
|
|
|
# SQL # d as ( |
935
|
|
|
|
|
|
|
# SQL # select a from z |
936
|
|
|
|
|
|
|
# SQL # union |
937
|
|
|
|
|
|
|
# SQL # select b from z |
938
|
|
|
|
|
|
|
# SQL # ), |
939
|
|
|
|
|
|
|
# SQL # f as ( |
940
|
|
|
|
|
|
|
# SQL # select distinct |
941
|
|
|
|
|
|
|
# SQL # regexp_replace( |
942
|
|
|
|
|
|
|
# SQL # regexp_replace( |
943
|
|
|
|
|
|
|
# SQL # regexp_replace( a, '^_', '' ), |
944
|
|
|
|
|
|
|
# SQL # E'\\[\\]$', |
945
|
|
|
|
|
|
|
# SQL # '' |
946
|
|
|
|
|
|
|
# SQL # ), |
947
|
|
|
|
|
|
|
# SQL # '^"(.*)"$', |
948
|
|
|
|
|
|
|
# SQL # E'\\1' |
949
|
|
|
|
|
|
|
# SQL # ) as t |
950
|
|
|
|
|
|
|
# SQL # from |
951
|
|
|
|
|
|
|
# SQL # d |
952
|
|
|
|
|
|
|
# SQL # ) |
953
|
|
|
|
|
|
|
# SQL # select |
954
|
|
|
|
|
|
|
# SQL # t |
955
|
|
|
|
|
|
|
# SQL # from |
956
|
|
|
|
|
|
|
# SQL # f |
957
|
|
|
|
|
|
|
# SQL # order by |
958
|
|
|
|
|
|
|
# SQL # length(t) desc, |
959
|
|
|
|
|
|
|
# SQL # t asc; |
960
|
|
|
|
|
|
|
|
961
|
|
|
|
|
|
|
# Following regexp was generated by feeding list from above query to: |
962
|
|
|
|
|
|
|
# use Regexp::List; |
963
|
|
|
|
|
|
|
# my $q = Regexp::List->new(); |
964
|
|
|
|
|
|
|
# print = $q->list2re( @_ ); |
965
|
|
|
|
|
|
|
# It is faster than normal alternative regexp like: |
966
|
|
|
|
|
|
|
# (?:timestamp without time zone|timestamp with time zone|time without time zone|....|xid|xml) |
967
|
88
|
|
|
|
|
242
|
my $any_pgtype = |
968
|
|
|
|
|
|
|
qr{(?-xism:(?=[abcdfgilmnoprstuvx])(?:t(?:i(?:me(?:stamp(?:\ with(?:out)?\ time\ zone|tz)?|\ with(?:out)?\ time\ zone|tz)?|nterval|d)|s(?:(?:tz)?range|vector|query)|(?:xid_snapsho|ex)t|rigger)|c(?:har(?:acter(?:\ varying)?)?|i(?:dr?|rcle)|string)|d(?:ate(?:range)?|ouble\ precision)|l(?:anguage_handler|ine|seg)|re(?:g(?:proc(?:edure)?|oper(?:ator)?|c(?:onfig|lass)|dictionary|type)|fcursor|ltime|cord|al)|p(?:o(?:lygon|int)|g_node_tree|ath)|a(?:ny(?:e(?:lement|num)|(?:non)?array|range)?|bstime|clitem)|b(?:i(?:t(?:\ varying)?|gint)|o(?:ol(?:ean)?|x)|pchar|ytea)|f(?:loat[48]|dw_handler)|in(?:t(?:2(?:vector)?|4(?:range)?|8(?:range)?|e(?:r[nv]al|ger))|et)|o(?:id(?:vector)?|paque)|n(?:um(?:range|eric)|ame)|sm(?:allint|gr)|m(?:acaddr|oney)|u(?:nknown|uid)|v(?:ar(?:char|bit)|oid)|x(?:id|ml)|gtsvector))}; |
969
|
|
|
|
|
|
|
|
970
|
88
|
|
|
|
|
8883
|
my @input_tokens = ( |
971
|
|
|
|
|
|
|
[ 'STRING_LITERAL', qr{'(?:''|[^']+)+'} ], |
972
|
|
|
|
|
|
|
[ 'PGTYPECAST', qr{::"?_?$any_pgtype"?(?:\[\])?} ], |
973
|
|
|
|
|
|
|
[ 'QUOTED_IDENTIFIER', qr{"(?:""|[^"]+)+"} ], |
974
|
|
|
|
|
|
|
[ 'AND', qr{\bAND\b}i ], |
975
|
|
|
|
|
|
|
[ 'ANY', qr{\bANY\b}i ], |
976
|
|
|
|
|
|
|
[ 'ARRAY', qr{\bARRAY\b}i ], |
977
|
|
|
|
|
|
|
[ 'AS', qr{\bAS\b}i ], |
978
|
|
|
|
|
|
|
[ 'ASC', qr{\bASC\b}i ], |
979
|
|
|
|
|
|
|
[ 'CASE', qr{\bCASE\b}i ], |
980
|
|
|
|
|
|
|
[ 'CAST', qr{\bCAST\b}i ], |
981
|
|
|
|
|
|
|
[ 'CHECK', qr{\bCHECK\b}i ], |
982
|
|
|
|
|
|
|
[ 'COLLATE', qr{\bCOLLATE\b}i ], |
983
|
|
|
|
|
|
|
[ 'COLUMN', qr{\bCOLUMN\b}i ], |
984
|
|
|
|
|
|
|
[ 'CURRENT_CATALOG', qr{\bCURRENT_CATALOG\b}i ], |
985
|
|
|
|
|
|
|
[ 'CURRENT_DATE', qr{\bCURRENT_DATE\b}i ], |
986
|
|
|
|
|
|
|
[ 'CURRENT_ROLE', qr{\bCURRENT_ROLE\b}i ], |
987
|
|
|
|
|
|
|
[ 'CURRENT_TIME', qr{\bCURRENT_TIME\b}i ], |
988
|
|
|
|
|
|
|
[ 'CURRENT_TIMESTAMP', qr{\bCURRENT_TIMESTAMP\b}i ], |
989
|
|
|
|
|
|
|
[ 'CURRENT_USER', qr{\bCURRENT_USER\b}i ], |
990
|
|
|
|
|
|
|
[ 'DEFAULT', qr{\bDEFAULT\b}i ], |
991
|
|
|
|
|
|
|
[ 'DESC', qr{\bDESC\b}i ], |
992
|
|
|
|
|
|
|
[ 'DISTINCT', qr{\bDISTINCT\b}i ], |
993
|
|
|
|
|
|
|
[ 'DO', qr{\bDO\b}i ], |
994
|
|
|
|
|
|
|
[ 'ELSE', qr{\bELSE\b}i ], |
995
|
|
|
|
|
|
|
[ 'END', qr{\bEND\b}i ], |
996
|
|
|
|
|
|
|
[ 'EXCEPT', qr{\bEXCEPT\b}i ], |
997
|
|
|
|
|
|
|
[ 'FALSE', qr{\bFALSE\b}i ], |
998
|
|
|
|
|
|
|
[ 'FETCH', qr{\bFETCH\b}i ], |
999
|
|
|
|
|
|
|
[ 'FOR', qr{\bFOR\b}i ], |
1000
|
|
|
|
|
|
|
[ 'FOREIGN', qr{\bFOREIGN\b}i ], |
1001
|
|
|
|
|
|
|
[ 'FROM', qr{\bFROM\b}i ], |
1002
|
|
|
|
|
|
|
[ 'IN', qr{\bIN\b}i ], |
1003
|
|
|
|
|
|
|
[ 'INITIALLY', qr{\bINITIALLY\b}i ], |
1004
|
|
|
|
|
|
|
[ 'INTERSECT', qr{\bINTERSECT\b}i ], |
1005
|
|
|
|
|
|
|
[ 'INTO', qr{\bINTO\b}i ], |
1006
|
|
|
|
|
|
|
[ 'LEADING', qr{\bLEADING\b}i ], |
1007
|
|
|
|
|
|
|
[ 'LIMIT', qr{\bLIMIT\b}i ], |
1008
|
|
|
|
|
|
|
[ 'LOCALTIME', qr{\bLOCALTIME\b}i ], |
1009
|
|
|
|
|
|
|
[ 'LOCALTIMESTAMP', qr{\bLOCALTIMESTAMP\b}i ], |
1010
|
|
|
|
|
|
|
[ 'NOT', qr{\bNOT\b}i ], |
1011
|
|
|
|
|
|
|
[ 'NULL', qr{\bNULL\b}i ], |
1012
|
|
|
|
|
|
|
[ 'OFFSET', qr{\bOFFSET\b}i ], |
1013
|
|
|
|
|
|
|
[ 'ON', qr{\bON\b}i ], |
1014
|
|
|
|
|
|
|
[ 'ONLY', qr{\bONLY\b}i ], |
1015
|
|
|
|
|
|
|
[ 'OR', qr{\bOR\b}i ], |
1016
|
|
|
|
|
|
|
[ 'ORDER', qr{\bORDER\b}i ], |
1017
|
|
|
|
|
|
|
[ 'PLACING', qr{\bPLACING\b}i ], |
1018
|
|
|
|
|
|
|
[ 'PRIMARY', qr{\bPRIMARY\b}i ], |
1019
|
|
|
|
|
|
|
[ 'REFERENCES', qr{\bREFERENCES\b}i ], |
1020
|
|
|
|
|
|
|
[ 'RETURNING', qr{\bRETURNING\b}i ], |
1021
|
|
|
|
|
|
|
[ 'SESSION_USER', qr{\bSESSION_USER\b}i ], |
1022
|
|
|
|
|
|
|
[ 'SOME', qr{\bSOME\b}i ], |
1023
|
|
|
|
|
|
|
[ 'SYMMETRIC', qr{\bSYMMETRIC\b}i ], |
1024
|
|
|
|
|
|
|
[ 'THEN', qr{\bTHEN\b}i ], |
1025
|
|
|
|
|
|
|
[ 'TO', qr{\bTO\b}i ], |
1026
|
|
|
|
|
|
|
[ 'TRAILING', qr{\bTRAILING\b}i ], |
1027
|
|
|
|
|
|
|
[ 'TRUE', qr{\bTRUE\b}i ], |
1028
|
|
|
|
|
|
|
[ 'UNION', qr{\bUNION\b}i ], |
1029
|
|
|
|
|
|
|
[ 'UNIQUE', qr{\bUNIQUE\b}i ], |
1030
|
|
|
|
|
|
|
[ 'USER', qr{\bUSER\b}i ], |
1031
|
|
|
|
|
|
|
[ 'USING', qr{\bUSING\b}i ], |
1032
|
|
|
|
|
|
|
[ 'WHEN', qr{\bWHEN\b}i ], |
1033
|
|
|
|
|
|
|
[ 'WHERE', qr{\bWHERE\b}i ], |
1034
|
|
|
|
|
|
|
[ 'CAST:', qr{::}i ], |
1035
|
|
|
|
|
|
|
[ 'COMMA', qr{,}i ], |
1036
|
|
|
|
|
|
|
[ 'DOT', qr{\.}i ], |
1037
|
|
|
|
|
|
|
[ 'LEFT_PARENTHESIS', qr{\(}i ], |
1038
|
|
|
|
|
|
|
[ 'RIGHT_PARENTHESIS', qr{\)}i ], |
1039
|
|
|
|
|
|
|
[ 'DOT', qr{\.}i ], |
1040
|
|
|
|
|
|
|
[ 'STAR', qr{[*]} ], |
1041
|
|
|
|
|
|
|
[ 'OP', qr{[+=/<>!~@-]} ], |
1042
|
|
|
|
|
|
|
[ 'NUM', qr{-?(?:\d*\.\d+|\d+)} ], |
1043
|
|
|
|
|
|
|
[ 'IDENTIFIER', qr{[a-z_][a-z0-9_]*}i ], |
1044
|
|
|
|
|
|
|
[ 'SPACE', qr{\s+} ], |
1045
|
|
|
|
|
|
|
); |
1046
|
88
|
|
|
|
|
492
|
return string_lexer( $data, @input_tokens ); |
1047
|
|
|
|
|
|
|
} |
1048
|
|
|
|
|
|
|
|
1049
|
|
|
|
|
|
|
=head2 anonymize_substitute |
1050
|
|
|
|
|
|
|
|
1051
|
|
|
|
|
|
|
Second stage of anonymization - actual changing strings into anonymized versions. |
1052
|
|
|
|
|
|
|
|
1053
|
|
|
|
|
|
|
=cut |
1054
|
|
|
|
|
|
|
|
1055
|
|
|
|
|
|
|
sub anonymize_substitute { |
1056
|
52
|
|
|
52
|
1
|
87
|
my $self = shift; |
1057
|
52
|
|
|
|
|
70
|
my $anonymizer = shift; |
1058
|
|
|
|
|
|
|
|
1059
|
52
|
100
|
|
|
|
132
|
if ( $self->scan_on ) { |
1060
|
26
|
|
|
|
|
49
|
while ( my ( $key, $value ) = each %{ $self->scan_on } ) { |
|
67
|
|
|
|
|
102
|
|
1061
|
41
|
|
|
|
|
234
|
$self->scan_on->{ $key } = $anonymizer->anonymized( $value ); |
1062
|
|
|
|
|
|
|
} |
1063
|
|
|
|
|
|
|
} |
1064
|
|
|
|
|
|
|
|
1065
|
52
|
100
|
|
|
|
135
|
if ( $self->cte_order ) { |
1066
|
2
|
|
|
|
|
5
|
my @new_order = (); |
1067
|
2
|
|
|
|
|
3
|
for my $cte_name ( @{ $self->cte_order } ) { |
|
2
|
|
|
|
|
5
|
|
1068
|
2
|
|
|
|
|
8
|
my $new_name = $anonymizer->anonymized( $cte_name ); |
1069
|
2
|
|
|
|
|
6
|
push @new_order, $new_name; |
1070
|
2
|
|
|
|
|
7
|
$self->ctes->{ $new_name } = delete $self->{ 'ctes' }->{ $cte_name }; |
1071
|
|
|
|
|
|
|
} |
1072
|
2
|
|
|
|
|
7
|
$self->cte_order( \@new_order ); |
1073
|
|
|
|
|
|
|
} |
1074
|
|
|
|
|
|
|
|
1075
|
52
|
100
|
|
|
|
110
|
if ( $self->extra_info ) { |
1076
|
40
|
|
|
|
|
63
|
my @new_extra_info = (); |
1077
|
40
|
|
|
|
|
49
|
for my $line ( @{ $self->extra_info } ) { |
|
40
|
|
|
|
|
62
|
|
1078
|
83
|
100
|
|
|
|
243
|
if ( $line =~ m{^(Foreign File:\s+)(\S.*?)(\s*)$} ) { |
1079
|
3
|
|
|
|
|
8
|
push @new_extra_info, $1 . $anonymizer->anonymized( $2 ) . $3; |
1080
|
3
|
|
|
|
|
7
|
next; |
1081
|
|
|
|
|
|
|
} |
1082
|
80
|
100
|
|
|
|
581
|
unless ( $line =~ s{^((?:Join Filter|Index Cond|Recheck Cond|Hash Cond|Merge Cond|Filter|Group Key|Sort Key|Output|One-Time Filter):\s+)(.*)$}{$2} ) { |
1083
|
36
|
|
|
|
|
65
|
push @new_extra_info, $line; |
1084
|
36
|
|
|
|
|
168
|
next; |
1085
|
|
|
|
|
|
|
} |
1086
|
44
|
|
|
|
|
116
|
my $output = $1; |
1087
|
44
|
|
|
|
|
106
|
my $lexer = $self->_make_lexer( $line ); |
1088
|
44
|
|
|
|
|
30816
|
while ( my $x = $lexer->() ) { |
1089
|
1867
|
50
|
|
|
|
553321
|
if ( ref $x ) { |
1090
|
1867
|
100
|
|
|
|
4111
|
if ( $x->[ 0 ] eq 'STRING_LITERAL' ) { |
|
|
50
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
1091
|
17
|
|
|
|
|
71
|
$output .= "'" . $anonymizer->anonymized( $x->[ 1 ] ) . "'"; |
1092
|
|
|
|
|
|
|
} |
1093
|
|
|
|
|
|
|
elsif ( $x->[ 0 ] eq 'QUOTED_IDENTIFIER' ) { |
1094
|
0
|
|
|
|
|
0
|
$output .= '"' . $anonymizer->anonymized( $x->[ 1 ] ) . '"'; |
1095
|
|
|
|
|
|
|
} |
1096
|
|
|
|
|
|
|
elsif ( $x->[ 0 ] eq 'IDENTIFIER' ) { |
1097
|
604
|
|
|
|
|
1547
|
$output .= $anonymizer->anonymized( $x->[ 1 ] ); |
1098
|
|
|
|
|
|
|
} |
1099
|
|
|
|
|
|
|
else { |
1100
|
1246
|
|
|
|
|
2606
|
$output .= $x->[ 1 ]; |
1101
|
|
|
|
|
|
|
} |
1102
|
|
|
|
|
|
|
} |
1103
|
|
|
|
|
|
|
else { |
1104
|
0
|
|
|
|
|
0
|
$output .= $x; |
1105
|
|
|
|
|
|
|
} |
1106
|
|
|
|
|
|
|
} |
1107
|
44
|
|
|
|
|
13069
|
push @new_extra_info, $output; |
1108
|
|
|
|
|
|
|
} |
1109
|
40
|
|
|
|
|
131
|
$self->{ 'extra_info' } = \@new_extra_info; |
1110
|
|
|
|
|
|
|
} |
1111
|
|
|
|
|
|
|
|
1112
|
52
|
|
|
|
|
126
|
for my $key ( qw( sub_nodes initplans subplans ) ) { |
1113
|
156
|
100
|
|
|
|
301
|
next unless $self->{ $key }; |
1114
|
30
|
|
|
|
|
43
|
$_->anonymize_substitute( $anonymizer ) for @{ $self->{ $key } }; |
|
30
|
|
|
|
|
204
|
|
1115
|
|
|
|
|
|
|
} |
1116
|
|
|
|
|
|
|
|
1117
|
52
|
100
|
|
|
|
115
|
if ( $self->{ 'ctes' } ) { |
1118
|
2
|
|
|
|
|
6
|
$_->anonymize_substitute( $anonymizer ) for values %{ $self->{ 'ctes' } }; |
|
2
|
|
|
|
|
21
|
|
1119
|
|
|
|
|
|
|
} |
1120
|
52
|
|
|
|
|
145
|
return; |
1121
|
|
|
|
|
|
|
} |
1122
|
|
|
|
|
|
|
|
1123
|
|
|
|
|
|
|
=head1 AUTHOR |
1124
|
|
|
|
|
|
|
|
1125
|
|
|
|
|
|
|
hubert depesz lubaczewski, C<< >> |
1126
|
|
|
|
|
|
|
|
1127
|
|
|
|
|
|
|
=head1 BUGS |
1128
|
|
|
|
|
|
|
|
1129
|
|
|
|
|
|
|
Please report any bugs or feature requests to C. |
1130
|
|
|
|
|
|
|
|
1131
|
|
|
|
|
|
|
=head1 SUPPORT |
1132
|
|
|
|
|
|
|
|
1133
|
|
|
|
|
|
|
You can find documentation for this module with the perldoc command. |
1134
|
|
|
|
|
|
|
|
1135
|
|
|
|
|
|
|
perldoc Pg::Explain::Node |
1136
|
|
|
|
|
|
|
|
1137
|
|
|
|
|
|
|
=head1 COPYRIGHT & LICENSE |
1138
|
|
|
|
|
|
|
|
1139
|
|
|
|
|
|
|
Copyright 2008-2021 hubert depesz lubaczewski, all rights reserved. |
1140
|
|
|
|
|
|
|
|
1141
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify it |
1142
|
|
|
|
|
|
|
under the same terms as Perl itself. |
1143
|
|
|
|
|
|
|
|
1144
|
|
|
|
|
|
|
=cut |
1145
|
|
|
|
|
|
|
|
1146
|
|
|
|
|
|
|
1; # End of Pg::Explain::Node |