]> granicus.if.org Git - postgresql/blob - src/backend/optimizer/README
Reimplement the linked list data structure used throughout the backend.
[postgresql] / src / backend / optimizer / README
1 Summary
2 -------
3
4 These directories take the Query structure returned by the parser, and
5 generate a plan used by the executor.  The /plan directory generates the
6 actual output plan, the /path code generates all possible ways to join the
7 tables, and /prep handles various preprocessing steps for special cases.
8 /util is utility stuff.  /geqo is the separate "genetic optimization" planner
9 --- it does a semi-random search through the join tree space, rather than
10 exhaustively considering all possible join trees.  (But each join considered
11 by /geqo is given to /path to create paths for, so we consider all possible
12 implementation paths for each specific join pair even in GEQO mode.)
13
14
15 Paths and Join Pairs
16 --------------------
17
18 During the planning/optimizing process, we build "Path" trees representing
19 the different ways of doing a query.  We select the cheapest Path that
20 generates the desired relation and turn it into a Plan to pass to the
21 executor.  (There is pretty much a one-to-one correspondence between the
22 Path and Plan trees, but Path nodes omit info that won't be needed during
23 planning, and include info needed for planning that won't be needed by the
24 executor.)
25
26 The optimizer builds a RelOptInfo structure for each base relation used in
27 the query.  Base rels are either primitive tables, or subquery subselects
28 that are planned via a separate recursive invocation of the planner.  A
29 RelOptInfo is also built for each join relation that is considered during
30 planning.  A join rel is simply a combination of base rels.  There is only
31 one join RelOptInfo for any given set of baserels --- for example, the join
32 {A B C} is represented by the same RelOptInfo no matter whether we build it
33 by joining A and B first and then adding C, or joining B and C first and
34 then adding A, etc.  These different means of building the joinrel are
35 represented as Paths.  For each RelOptInfo we build a list of Paths that
36 represent plausible ways to implement the scan or join of that relation.
37 Once we've considered all the plausible Paths for a rel, we select the one
38 that is cheapest according to the planner's cost estimates.  The final plan
39 is derived from the cheapest Path for the RelOptInfo that includes all the
40 base rels of the query.
41
42 Possible Paths for a primitive table relation include plain old sequential
43 scan, plus index scans for any indexes that exist on the table.  A subquery
44 base relation just has one Path, a "SubqueryScan" path (which links to the
45 subplan that was built by a recursive invocation of the planner).  Likewise
46 a function-RTE base relation has only one possible Path.
47
48 Joins always occur using two RelOptInfos.  One is outer, the other inner.
49 Outers drive lookups of values in the inner.  In a nested loop, lookups of
50 values in the inner occur by scanning the inner path once per outer tuple
51 to find each matching inner row.  In a mergejoin, inner and outer rows are
52 ordered, and are accessed in order, so only one scan is required to perform
53 the entire join: both inner and outer paths are scanned in-sync.  (There's
54 not a lot of difference between inner and outer in a mergejoin...)  In a
55 hashjoin, the inner is scanned first and all its rows are entered in a
56 hashtable, then the outer is scanned and for each row we lookup the join
57 key in the hashtable.
58
59 A Path for a join relation is actually a tree structure, with the top
60 Path node representing the join method.  It has left and right subpaths
61 that represent the scan or join methods used for the two input relations.
62
63
64 Join Tree Construction
65 ----------------------
66
67 The optimizer generates optimal query plans by doing a more-or-less
68 exhaustive search through the ways of executing the query.  The best Path
69 tree is found by a recursive process:
70
71 1) Take each base relation in the query, and make a RelOptInfo structure
72 for it.  Find each potentially useful way of accessing the relation,
73 including sequential and index scans, and make a Path representing that
74 way.  All the Paths made for a given relation are placed in its
75 RelOptInfo.pathlist.  (Actually, we discard Paths that are obviously
76 inferior alternatives before they ever get into the pathlist --- what
77 ends up in the pathlist is the cheapest way of generating each potentially
78 useful sort ordering of the relation.)  Also create RelOptInfo.joininfo
79 nodes that list all the join clauses that involve this relation.  For
80 example, the WHERE clause "tab1.col1 = tab2.col1" generates a JoinInfo
81 for tab1 listing tab2 as an unjoined relation, and also one for tab2
82 showing tab1 as an unjoined relation.
83
84 If we have only a single base relation in the query, we are done.
85 Otherwise we have to figure out how to join the base relations into a
86 single join relation.
87
88 2) If the query's FROM clause contains explicit JOIN clauses, we join
89 those pairs of relations in exactly the tree structure indicated by the
90 JOIN clauses.  (This is absolutely necessary when dealing with outer JOINs.
91 For inner JOINs we have more flexibility in theory, but don't currently
92 exploit it in practice.)  For each such join pair, we generate a Path
93 for each feasible join method, and select the cheapest Path.  Note that
94 the JOIN clause structure determines the join Path structure, but it
95 doesn't constrain the join implementation method at each join (nestloop,
96 merge, hash), nor does it say which rel is considered outer or inner at
97 each join.  We consider all these possibilities in building Paths.
98
99 3) At the top level of the FROM clause we will have a list of relations
100 that are either base rels or joinrels constructed per JOIN directives.
101 We can join these rels together in any order the planner sees fit.
102 The standard (non-GEQO) planner does this as follows:
103
104 Consider joining each RelOptInfo to each other RelOptInfo specified in its
105 RelOptInfo.joininfo, and generate a Path for each possible join method for
106 each such pair.  (If we have a RelOptInfo with no join clauses, we have no
107 choice but to generate a clauseless Cartesian-product join; so we consider
108 joining that rel to each other available rel.  But in the presence of join
109 clauses we will only consider joins that use available join clauses.)
110
111 If we only had two relations in the FROM list, we are done: we just pick
112 the cheapest path for the join RelOptInfo.  If we had more than two, we now
113 need to consider ways of joining join RelOptInfos to each other to make
114 join RelOptInfos that represent more than two FROM items.
115
116 The join tree is constructed using a "dynamic programming" algorithm:
117 in the first pass (already described) we consider ways to create join rels
118 representing exactly two FROM items.  The second pass considers ways
119 to make join rels that represent exactly three FROM items; the next pass,
120 four items, etc.  The last pass considers how to make the final join
121 relation that includes all FROM items --- obviously there can be only one
122 join rel at this top level, whereas there can be more than one join rel
123 at lower levels.  At each level we use joins that follow available join
124 clauses, if possible, just as described for the first level.
125
126 For example:
127
128     SELECT  *
129     FROM    tab1, tab2, tab3, tab4
130     WHERE   tab1.col = tab2.col AND
131         tab2.col = tab3.col AND
132         tab3.col = tab4.col
133
134     Tables 1, 2, 3, and 4 are joined as:
135     {1 2},{2 3},{3 4}
136     {1 2 3},{2 3 4}
137     {1 2 3 4}
138     (other possibilities will be excluded for lack of join clauses)
139
140     SELECT  *
141     FROM    tab1, tab2, tab3, tab4
142     WHERE   tab1.col = tab2.col AND
143         tab1.col = tab3.col AND
144         tab1.col = tab4.col
145
146     Tables 1, 2, 3, and 4 are joined as:
147     {1 2},{1 3},{1 4}
148     {1 2 3},{1 3 4},{1 2 4}
149     {1 2 3 4}
150
151 We consider left-handed plans (the outer rel of an upper join is a joinrel,
152 but the inner is always a single FROM item); right-handed plans (outer rel
153 is always a single item); and bushy plans (both inner and outer can be
154 joins themselves).  For example, when building {1 2 3 4} we consider
155 joining {1 2 3} to {4} (left-handed), {4} to {1 2 3} (right-handed), and
156 {1 2} to {3 4} (bushy), among other choices.  Although the jointree
157 scanning code produces these potential join combinations one at a time,
158 all the ways to produce the same set of joined base rels will share the
159 same RelOptInfo, so the paths produced from different join combinations
160 that produce equivalent joinrels will compete in add_path.
161
162 Once we have built the final join rel, we use either the cheapest path
163 for it or the cheapest path with the desired ordering (if that's cheaper
164 than applying a sort to the cheapest other path).
165
166
167 Pulling up subqueries
168 ---------------------
169
170 As we described above, a subquery appearing in the range table is planned
171 independently and treated as a "black box" during planning of the outer
172 query.  This is necessary when the subquery uses features such as
173 aggregates, GROUP, or DISTINCT.  But if the subquery is just a simple
174 scan or join, treating the subquery as a black box may produce a poor plan
175 compared to considering it as part of the entire plan search space.
176 Therefore, at the start of the planning process the planner looks for
177 simple subqueries and pulls them up into the main query's jointree.
178
179 Pulling up a subquery may result in FROM-list joins appearing below the top
180 of the join tree.  Each FROM-list is planned using the dynamic-programming
181 search method described above.
182
183 If pulling up a subquery produces a FROM-list as a direct child of another
184 FROM-list (with no explicit JOIN directives between), then we can merge the
185 two FROM-lists together.  Once that's done, the subquery is an absolutely
186 integral part of the outer query and will not constrain the join tree
187 search space at all.  However, that could result in unpleasant growth of
188 planning time, since the dynamic-programming search has runtime exponential
189 in the number of FROM-items considered.  Therefore, we don't merge
190 FROM-lists if the result would have too many FROM-items in one list.
191
192
193 Optimizer Functions
194 -------------------
195
196 The primary entry point is planner().
197
198 planner()
199  set up for recursive handling of subqueries
200  do final cleanup after planning.
201 -subquery_planner()
202  pull up subqueries from rangetable, if possible
203  canonicalize qual
204      Attempt to simplify WHERE clause to the most useful form; this includes
205      flattening nested AND/ORs and detecting clauses that are duplicated in
206      different branches of an OR.
207  simplify constant expressions
208  process sublinks
209  convert Vars of outer query levels into Params
210 --grouping_planner()
211   preprocess target list for non-SELECT queries
212   handle UNION/INTERSECT/EXCEPT, GROUP BY, HAVING, aggregates,
213         ORDER BY, DISTINCT, LIMIT
214 --query_planner()
215    pull out constant quals, which can be used to gate execution of the
216         whole plan (if any are found, we make a top-level Result node
217         to do the gating)
218    make list of base relations used in query
219    split up the qual into restrictions (a=1) and joins (b=c)
220    find qual clauses that enable merge and hash joins
221 ----make_one_rel()
222      set_base_rel_pathlist()
223       find scan and all index paths for each base relation
224       find selectivity of columns used in joins
225 -----make_one_rel_by_joins()
226       jump to geqo if needed
227       else call make_rels_by_joins() for each level of join tree needed
228       make_rels_by_joins():
229         For each joinrel of the prior level, do make_rels_by_clause_joins()
230         if it has join clauses, or make_rels_by_clauseless_joins() if not.
231         Also generate "bushy plan" joins between joinrels of lower levels.
232       Back at make_one_rel_by_joins(), apply set_cheapest() to extract the
233       cheapest path for each newly constructed joinrel.
234       Loop back if this wasn't the top join level.
235    Back at query_planner:
236     put back any constant quals by adding a Result node
237  Back at grouping_planner:
238  do grouping(GROUP)
239  do aggregates
240  make unique(DISTINCT)
241  make sort(ORDER BY)
242  make limit(LIMIT/OFFSET)
243
244
245 Optimizer Data Structures
246 -------------------------
247
248 RelOptInfo      - a relation or joined relations
249
250  RestrictInfo   - WHERE clauses, like "x = 3" or "y = z"
251                   (note the same structure is used for restriction and
252                    join clauses)
253  JoinInfo       - join clauses associated with a particular pair of relations
254
255  Path           - every way to generate a RelOptInfo(sequential,index,joins)
256   SeqScan       - a plain Path node with pathtype = T_SeqScan
257   IndexPath     - index scans
258   TidPath       - scan by CTID
259   AppendPath    - append multiple subpaths together
260   ResultPath    - a Result plan node (used for variable-free tlist or qual)
261   MaterialPath  - a Material plan node
262   UniquePath    - remove duplicate rows
263   NestPath      - nested-loop joins
264   MergePath     - merge joins
265   HashPath      - hash joins
266
267  PathKeys       - a data structure representing the ordering of a path
268
269 The optimizer spends a good deal of its time worrying about the ordering
270 of the tuples returned by a path.  The reason this is useful is that by
271 knowing the sort ordering of a path, we may be able to use that path as
272 the left or right input of a mergejoin and avoid an explicit sort step.
273 Nestloops and hash joins don't really care what the order of their inputs
274 is, but mergejoin needs suitably ordered inputs.  Therefore, all paths
275 generated during the optimization process are marked with their sort order
276 (to the extent that it is known) for possible use by a higher-level merge.
277
278 It is also possible to avoid an explicit sort step to implement a user's
279 ORDER BY clause if the final path has the right ordering already, so the
280 sort ordering is of interest even at the top level.  query_planner() will
281 look for the cheapest path with a sort order matching the desired order,
282 and grouping_planner() will compare its cost to the cost of using the
283 cheapest-overall path and doing an explicit sort.
284
285 When we are generating paths for a particular RelOptInfo, we discard a path
286 if it is more expensive than another known path that has the same or better
287 sort order.  We will never discard a path that is the only known way to
288 achieve a given sort order (without an explicit sort, that is).  In this
289 way, the next level up will have the maximum freedom to build mergejoins
290 without sorting, since it can pick from any of the paths retained for its
291 inputs.
292
293
294 PathKeys
295 --------
296
297 The PathKeys data structure represents what is known about the sort order
298 of a particular Path.
299
300 Path.pathkeys is a List of Lists of PathKeyItem nodes that represent
301 the sort order of the result generated by the Path.  The n'th sublist
302 represents the n'th sort key of the result.
303
304 In single/base relation RelOptInfo's, the Paths represent various ways
305 of scanning the relation and the resulting ordering of the tuples.
306 Sequential scan Paths have NIL pathkeys, indicating no known ordering.
307 Index scans have Path.pathkeys that represent the chosen index's ordering,
308 if any.  A single-key index would create a pathkey with a single sublist,
309 e.g. ( (tab1.indexkey1/sortop1) ).  A multi-key index generates a sublist
310 per key, e.g. ( (tab1.indexkey1/sortop1) (tab1.indexkey2/sortop2) ) which
311 shows major sort by indexkey1 (ordering by sortop1) and minor sort by
312 indexkey2 with sortop2.
313
314 Note that a multi-pass indexscan (OR clause scan) has NIL pathkeys since
315 we can say nothing about the overall order of its result.  Also, an
316 indexscan on an unordered type of index generates NIL pathkeys.  However,
317 we can always create a pathkey by doing an explicit sort.  The pathkeys
318 for a Sort plan's output just represent the sort key fields and the
319 ordering operators used.
320
321 Things get more interesting when we consider joins.  Suppose we do a
322 mergejoin between A and B using the mergeclause A.X = B.Y.  The output
323 of the mergejoin is sorted by X --- but it is also sorted by Y.  We
324 represent this fact by listing both keys in a single pathkey sublist:
325 ( (A.X/xsortop B.Y/ysortop) ).  This pathkey asserts that the major
326 sort order of the Path can be taken to be *either* A.X or B.Y.
327 They are equal, so they are both primary sort keys.  By doing this,
328 we allow future joins to use either var as a pre-sorted key, so upper
329 Mergejoins may be able to avoid having to re-sort the Path.  This is
330 why pathkeys is a List of Lists.
331
332 We keep a sortop associated with each PathKeyItem because cross-data-type
333 mergejoins are possible; for example int4 = int8 is mergejoinable.
334 In this case we need to remember that the left var is ordered by int4lt
335 while the right var is ordered by int8lt.  So the different members of
336 each sublist could have different sortops.
337
338 Note that while the order of the top list is meaningful (primary vs.
339 secondary sort key), the order of each sublist is arbitrary.  Each sublist
340 should be regarded as a set of equivalent keys, with no significance
341 to the list order.
342
343 With a little further thought, it becomes apparent that pathkeys for
344 joins need not only come from mergejoins.  For example, if we do a
345 nestloop join between outer relation A and inner relation B, then any
346 pathkeys relevant to A are still valid for the join result: we have
347 not altered the order of the tuples from A.  Even more interesting,
348 if there was a mergeclause (more formally, an "equijoin clause") A.X=B.Y,
349 and A.X was a pathkey for the outer relation A, then we can assert that
350 B.Y is a pathkey for the join result; X was ordered before and still is,
351 and the joined values of Y are equal to the joined values of X, so Y
352 must now be ordered too.  This is true even though we used neither an
353 explicit sort nor a mergejoin on Y.
354
355 More generally, whenever we have an equijoin clause A.X = B.Y and a
356 pathkey A.X, we can add B.Y to that pathkey if B is part of the joined
357 relation the pathkey is for, *no matter how we formed the join*.  It works
358 as long as the clause has been applied at some point while forming the
359 join relation.  (In the current implementation, we always apply qual
360 clauses as soon as possible, ie, as far down in the plan tree as possible.
361 So we can treat the pathkeys as equivalent everywhere.  The exception is
362 when the relations A and B are joined inside the nullable side of an
363 OUTER JOIN and the equijoin clause comes from above the OUTER JOIN.  In this
364 case we cannot apply the qual as soon as A and B are joined, so we do not
365 consider the pathkeys to be equivalent.  This could be improved if we wanted
366 to go to the trouble of making pathkey equivalence be context-dependent,
367 but that seems much more complex than it's worth.)
368
369 In short, then: when producing the pathkeys for a merge or nestloop join,
370 we can keep all of the keys of the outer path, since the ordering of the
371 outer path will be preserved in the result.  Furthermore, we can add to
372 each pathkey sublist any inner vars that are equijoined to any of the
373 outer vars in the sublist; this works regardless of whether we are
374 implementing the join using that equijoin clause as a mergeclause,
375 or merely enforcing the clause after-the-fact as a qpqual filter.
376
377 Although Hashjoins also work only with equijoin operators, it is *not*
378 safe to consider the output of a Hashjoin to be sorted in any particular
379 order --- not even the outer path's order.  This is true because the
380 executor might have to split the join into multiple batches.  Therefore
381 a Hashjoin is always given NIL pathkeys.  (Also, we need to use only
382 mergejoinable operators when deducing which inner vars are now sorted,
383 because a mergejoin operator tells us which left- and right-datatype
384 sortops can be considered equivalent, whereas a hashjoin operator
385 doesn't imply anything about sort order.)
386
387 Pathkeys are also useful to represent an ordering that we wish to achieve,
388 since they are easily compared to the pathkeys of a potential candidate
389 path.  So, SortClause lists are turned into pathkeys lists for use inside
390 the optimizer.
391
392 OK, now for how it *really* works:
393
394 We did implement pathkeys just as described above, and found that the
395 planner spent a huge amount of time comparing pathkeys, because the
396 representation of pathkeys as unordered lists made it expensive to decide
397 whether two were equal or not.  So, we've modified the representation
398 as described next.
399
400 If we scan the WHERE clause for equijoin clauses (mergejoinable clauses)
401 during planner startup, we can construct lists of equivalent pathkey items
402 for the query.  There could be more than two items per equivalence set;
403 for example, WHERE A.X = B.Y AND B.Y = C.Z AND D.R = E.S creates the
404 equivalence sets { A.X B.Y C.Z } and { D.R E.S } (plus associated sortops).
405 Any pathkey item that belongs to an equivalence set implies that all the
406 other items in its set apply to the relation too, or at least all the ones
407 that are for fields present in the relation.  (Some of the items in the
408 set might be for as-yet-unjoined relations.)  Furthermore, any multi-item
409 pathkey sublist that appears at any stage of planning the query *must* be
410 a subset of one or another of these equivalence sets; there's no way we'd
411 have put two items in the same pathkey sublist unless they were equijoined
412 in WHERE.
413
414 Now suppose that we allow a pathkey sublist to contain pathkey items for
415 vars that are not yet part of the pathkey's relation.  This introduces
416 no logical difficulty, because such items can easily be seen to be
417 irrelevant; we just mandate that they be ignored.  But having allowed
418 this, we can declare (by fiat) that any multiple-item pathkey sublist
419 must be "equal()" to the appropriate equivalence set.  In effect,
420 whenever we make a pathkey sublist that mentions any var appearing in an
421 equivalence set, we instantly add all the other vars equivalenced to it,
422 whether they appear yet in the pathkey's relation or not.  And we also
423 mandate that the pathkey sublist appear in the same order as the
424 equivalence set it comes from.
425
426 In fact, we can go even further, and say that the canonical representation
427 of a pathkey sublist is a pointer directly to the relevant equivalence set,
428 which is kept in a list of pathkey equivalence sets for the query.  Then
429 pathkey sublist comparison reduces to pointer-equality checking!  To do this
430 we also have to add single-element pathkey sublists to the query's list of
431 equivalence sets, but that's a small price to pay.
432
433 By the way, it's OK and even useful for us to build equivalence sets
434 that mention multiple vars from the same relation.  For example, if
435 we have WHERE A.X = A.Y and we are scanning A using an index on X,
436 we can legitimately conclude that the path is sorted by Y as well;
437 and this could be handy if Y is the variable used in other join clauses
438 or ORDER BY.  So, any WHERE clause with a mergejoinable operator can
439 contribute to an equivalence set, even if it's not a join clause.
440
441 As sketched so far, equijoin operators allow us to conclude that
442 A.X = B.Y and B.Y = C.Z together imply A.X = C.Z, even when different
443 datatypes are involved.  What is not immediately obvious is that to use
444 the "canonical pathkey" representation, we *must* make this deduction.
445 An example (from a real bug in Postgres 7.0) is a mergejoin for a query
446 like
447         SELECT * FROM t1, t2 WHERE t1.f2 = t2.f3 AND t1.f1 = t2.f3;
448 The canonical-pathkey mechanism is able to deduce that t1.f1 = t1.f2
449 (ie, both appear in the same canonical pathkey set).  If we sort t1
450 and then apply a mergejoin, we *must* filter the t1 tuples using the
451 implied qualification f1 = f2, because otherwise the output of the sort
452 will be ordered by f1 or f2 (whichever we sort on) but not both.  The
453 merge will then fail since (depending on which qual clause it applies
454 first) it's expecting either ORDER BY f1,f2 or ORDER BY f2,f1, but the
455 actual output of the sort has neither of these orderings.  The best fix
456 for this is to generate all the implied equality constraints for each
457 equijoin set and add these clauses to the query's qualification list.
458 In other words, we *explicitly* deduce f1 = f2 and add this to the WHERE
459 clause.  The constraint will be applied as a qpqual to the output of the
460 scan on t1, resulting in sort output that is indeed ordered by both vars.
461 This approach provides more information to the selectivity estimation
462 code than it would otherwise have, and reduces the number of tuples
463 processed in join stages, so it's a win to make these deductions even
464 if we weren't forced to.
465
466 When we generate implied equality constraints, we may find ourselves
467 adding redundant clauses to specific relations.  For example, consider
468         SELECT * FROM t1, t2, t3 WHERE t1.a = t2.b AND t2.b = t3.c;
469 We will generate the implied clause t1.a = t3.c and add it to the tree.
470 This is good since it allows us to consider joining t1 and t3 directly,
471 which we otherwise wouldn't do.  But when we reach the stage of joining
472 all three relations, we will have redundant join clauses --- eg, if we
473 join t1 and t2 first, then the path that joins (t1 t2) to t3 will have
474 both t2.b = t3.c and t1.a = t3.c as restriction clauses.  This is bad;
475 not only is evaluation of the extra clause useless work at runtime,
476 but the selectivity estimator routines will underestimate the number
477 of tuples produced since they won't know that the two clauses are
478 perfectly redundant.  We fix this by detecting and removing redundant
479 clauses as the restriction clause list is built for each join.  (We
480 can't do it sooner, since which clauses are redundant will vary depending
481 on the join order.)
482
483 Yet another implication of all this is that mergejoinable operators
484 must form closed equivalence sets.  For example, if "int2 = int4"
485 and "int4 = int8" are both marked mergejoinable, then there had better
486 be a mergejoinable "int2 = int8" operator as well.  Otherwise, when
487 we're given WHERE int2var = int4var AND int4var = int8var, we'll fail
488 while trying to create a representation of the implied clause
489 int2var = int8var.
490
491 An additional refinement we can make is to insist that canonical pathkey
492 lists (sort orderings) do not mention the same pathkey set more than once.
493 For example, a pathkey list ((A) (B) (A)) is redundant --- the second
494 occurrence of (A) does not change the ordering, since the data must already
495 be sorted by A.  Although a user probably wouldn't write ORDER BY A,B,A
496 directly, such redundancies are more probable once equijoin equivalences
497 have been considered.  Also, the system is likely to generate redundant
498 pathkey lists when computing the sort ordering needed for a mergejoin.  By
499 eliminating the redundancy, we save time and improve planning, since the
500 planner will more easily recognize equivalent orderings as being equivalent.
501
502 Though Bob Devine <bob.devine@worldnet.att.net> was not involved in the 
503 coding of our optimizer, he is available to field questions about
504 optimizer topics.
505
506 -- bjm & tgl
507