Skip to content

Commit 565bfcd

Browse files
committed
Fix missing inversion when the bounded ORDER plan is converted into the SORT one. Some code simplification.
1 parent 81d6d22 commit 565bfcd

File tree

4 files changed

+127
-124
lines changed

4 files changed

+127
-124
lines changed

src/jrd/optimizer/InnerJoin.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ void InnerJoin::estimateCost(unsigned position,
187187
fb_assert(!position || candidate->dependencies);
188188

189189
// Remember selectivity of this stream
190-
joinedStreams[position].selectivity = candidate->selectivity;
190+
joinedStreams[position].selectivity = candidate->matchSelectivity;
191191

192192
// Calculate the nested loop cost, it's our default option
193193
const auto loopCost = candidate->cost * cardinality;

src/jrd/optimizer/Optimizer.cpp

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2905,7 +2905,7 @@ RecordSource* Optimizer::generateRetrieval(StreamType stream,
29052905
inversion = candidate->inversion;
29062906
condition = candidate->condition;
29072907
dbkeyRanges.assign(candidate->dbkeyRanges);
2908-
scanSelectivity = candidate->selectivity;
2908+
scanSelectivity = candidate->matchSelectivity;
29092909

29102910
// Just for safety sake, this condition must be already checked
29112911
// inside OptimizerRetrieval::matchOnIndexes()
@@ -2920,9 +2920,7 @@ RecordSource* Optimizer::generateRetrieval(StreamType stream,
29202920
}
29212921
}
29222922

2923-
const auto navigation = retrieval.getNavigation(candidate);
2924-
2925-
if (navigation)
2923+
if (const auto navigation = retrieval.getNavigation())
29262924
{
29272925
if (sortClause)
29282926
*sortClause = nullptr;

src/jrd/optimizer/Optimizer.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,7 @@ struct InversionCandidate
685685
{}
686686

687687
double selectivity = MAXIMUM_SELECTIVITY;
688+
double matchSelectivity = MAXIMUM_SELECTIVITY;
688689
double cost = 0;
689690
unsigned nonFullMatchedSegments = MAX_INDEX_SEGMENTS + 1;
690691
unsigned matchedSegments = 0;
@@ -724,10 +725,11 @@ class Retrieval final : private Firebird::PermanentStorage
724725
}
725726

726727
InversionCandidate* getInversion();
727-
IndexTableScan* getNavigation(const InversionCandidate* candidate);
728+
IndexTableScan* getNavigation();
728729

729730
protected:
730731
void analyzeNavigation(const InversionCandidateList& inversions);
732+
void applyNavigation(InversionCandidate* candidate);
731733
bool betterInversion(const InversionCandidate* inv1, const InversionCandidate* inv2,
732734
bool navigation) const;
733735
bool checkIndexCondition(index_desc& idx, BooleanList& matches) const;

src/jrd/optimizer/Retrieval.cpp

Lines changed: 121 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -316,17 +316,7 @@ InversionCandidate* Retrieval::getInversion()
316316
invCandidate = FB_NEW_POOL(getPool()) InversionCandidate(getPool());
317317
}
318318

319-
if (invCandidate->unique)
320-
{
321-
// Set up the unique retrieval cost to be fixed and not dependent on
322-
// possibly outdated statistics. It includes N index scans plus one data page fetch.
323-
invCandidate->cost = DEFAULT_INDEX_COST * invCandidate->indexes + 1;
324-
}
325-
else
326-
{
327-
// Add the records retrieval cost to the priorly calculated index scan cost
328-
invCandidate->cost += cardinality * invCandidate->selectivity;
329-
}
319+
invCandidate->matchSelectivity = invCandidate->selectivity;
330320

331321
// Adjust the effective selectivity by treating computable but unmatched conjunctions
332322
// as filters. But consider only those local to our stream.
@@ -336,12 +326,15 @@ InversionCandidate* Retrieval::getInversion()
336326
{
337327
if (!(iter & Optimizer::CONJUNCT_USED))
338328
{
339-
const auto matched = invCandidate->matches.exist(iter);
329+
const auto matched = invCandidate->matches.exist(iter) ||
330+
(navigationCandidate && navigationCandidate->matches.exist(iter));
340331

341-
if (setConjunctionsMatched && matched)
342-
iter |= Optimizer::CONJUNCT_MATCHED;
343-
else if (!setConjunctionsMatched && !matched &&
344-
iter->computable(csb, stream, true) &&
332+
if (matched)
333+
{
334+
if (setConjunctionsMatched)
335+
iter |= Optimizer::CONJUNCT_MATCHED;
336+
}
337+
else if (iter->computable(csb, stream, true) &&
345338
iter->containsStream(stream))
346339
{
347340
selectivity *= Optimizer::getSelectivity(*iter);
@@ -357,6 +350,18 @@ InversionCandidate* Retrieval::getInversion()
357350

358351
Optimizer::adjustSelectivity(invCandidate->selectivity, selectivity);
359352

353+
// Apply navigational walk, if meaningful
354+
if (navigationCandidate)
355+
applyNavigation(invCandidate);
356+
357+
// Add the records retrieval cost to the priorly calculated index scan cost.
358+
// For unique retrievals, set up a fixed cost (independent from a possibly outdated statistics).
359+
// It includes DEFAULT_INDEX_COST index scans plus one data page fetch.
360+
if (invCandidate->unique)
361+
invCandidate->cost = DEFAULT_INDEX_COST * invCandidate->indexes + 1;
362+
else
363+
invCandidate->cost += cardinality * invCandidate->selectivity;
364+
360365
// Add the streams where this stream is depending on
361366
for (auto match : invCandidate->matches)
362367
{
@@ -376,94 +381,16 @@ InversionCandidate* Retrieval::getInversion()
376381
return invCandidate;
377382
}
378383

379-
IndexTableScan* Retrieval::getNavigation(const InversionCandidate* candidate)
384+
IndexTableScan* Retrieval::getNavigation()
380385
{
381386
if (!navigationCandidate)
382387
return nullptr;
383388

384-
const auto scratch = navigationCandidate->scratch;
385-
386-
const auto streamCardinality = csb->csb_rpt[stream].csb_cardinality;
387-
388-
// If the table looks like empty during preparation time, we cannot be sure about
389-
// its real cardinality during execution. So, unless we have some index-based
390-
// filtering applied, let's better be pessimistic and avoid external sorting
391-
// due to likely cardinality under-estimation.
392-
const bool avoidSorting = (streamCardinality <= MINIMUM_CARDINALITY && !candidate->inversion);
393-
394-
if (!(scratch->index->idx_runtime_flags & idx_plan_navigate) && !avoidSorting)
395-
{
396-
// Check whether the navigational index scan is cheaper than the external sort
397-
// and give up if it's not worth the efforts.
398-
//
399-
// We ignore candidate->cost in the calculations below as it belongs
400-
// to both parts being compared.
401-
402-
fb_assert(candidate);
403-
404-
// Restore the original selectivity of the inversion,
405-
// i.e. before the navigation candidate was accounted
406-
auto selectivity = candidate->selectivity / navigationCandidate->selectivity;
407-
408-
// Non-indexed booleans are checked before sorting, so they improve the selectivity
409-
410-
double factor = MAXIMUM_SELECTIVITY;
411-
for (auto iter = optimizer->getConjuncts(outerFlag, innerFlag); iter.hasData(); ++iter)
412-
{
413-
if (!(iter & Optimizer::CONJUNCT_USED) &&
414-
!candidate->matches.exist(iter) &&
415-
iter->computable(csb, stream, true) &&
416-
iter->containsStream(stream))
417-
{
418-
factor *= Optimizer::getSelectivity(*iter);
419-
}
420-
}
421-
422-
Optimizer::adjustSelectivity(selectivity, factor);
423-
424-
// Don't consider external sorting if optimization for first rows is requested
425-
// and we have no local filtering applied
426-
427-
if (!optimizer->favorFirstRows() || selectivity < MAXIMUM_SELECTIVITY)
428-
{
429-
// Estimate amount of records to be sorted
430-
const auto cardinality = streamCardinality * selectivity;
431-
432-
// We optimistically assume that records will be cached during sorting
433-
const auto sortCost =
434-
// record copying (to the sort buffer and back)
435-
cardinality * COST_FACTOR_MEMCOPY * 2 +
436-
// quicksort algorithm is O(n*log(n)) in average
437-
cardinality * log2(cardinality) * COST_FACTOR_QUICKSORT;
438-
439-
// During navigation we fetch an index leaf page per every record being returned,
440-
// thus add the estimated cardinality to the cost
441-
auto navigationCost = navigationCandidate->cost +
442-
streamCardinality * candidate->selectivity;
443-
444-
if (optimizer->favorFirstRows())
445-
{
446-
// Reset the cost to represent a single record retrieval
447-
navigationCost = DEFAULT_INDEX_COST;
448-
449-
// We know that some local filtering is applied, so we need
450-
// to adjust the cost as we need to walk the index
451-
// until the first matching record is found
452-
const auto fullIndexCost = navigationCandidate->scratch->cardinality;
453-
const auto ratio = MAXIMUM_SELECTIVITY / selectivity;
454-
const auto fraction = ratio / streamCardinality;
455-
const auto walkCost = fullIndexCost * fraction * navigationCandidate->selectivity;
456-
navigationCost += walkCost;
457-
}
458-
459-
if (sortCost < navigationCost)
460-
return nullptr;
461-
}
462-
}
463-
464389
// Looks like we can do a navigational walk. Flag that
465390
// we have used this index for navigation, and allocate
466391
// a navigational rsb for it.
392+
393+
const auto scratch = navigationCandidate->scratch;
467394
scratch->index->idx_runtime_flags |= idx_navigate;
468395

469396
const auto indexNode = makeIndexScanNode(scratch);
@@ -715,6 +642,103 @@ void Retrieval::analyzeNavigation(const InversionCandidateList& inversions)
715642
navigationCandidate = bestCandidate;
716643
}
717644

645+
void Retrieval::applyNavigation(InversionCandidate* candidate)
646+
{
647+
fb_assert(navigationCandidate && candidate);
648+
fb_assert(navigationCandidate != candidate);
649+
650+
candidate->navigated = true;
651+
652+
const auto scratch = navigationCandidate->scratch;
653+
const auto streamCardinality = csb->csb_rpt[stream].csb_cardinality;
654+
655+
// If the table looks like empty during preparation time, we cannot be sure about
656+
// its real cardinality during execution. So, unless we have some index-based
657+
// filtering applied, let's better be pessimistic and avoid external sorting
658+
// due to likely cardinality under-estimation.
659+
const bool avoidSorting =
660+
(streamCardinality <= MINIMUM_CARDINALITY && !candidate->inversion) ||
661+
// also don't consider external sorting if optimization for first rows is requested
662+
// and we have no local filtering applied
663+
(optimizer->favorFirstRows() && candidate->selectivity == MAXIMUM_SELECTIVITY) ||
664+
// and finally, skip it if the explicit plan specifies this index as navigated
665+
(scratch->index->idx_runtime_flags & idx_plan_navigate);
666+
667+
if (!avoidSorting)
668+
{
669+
// Check whether the navigational index scan is cheaper than the external sort
670+
// and give up if it's not worth the efforts.
671+
//
672+
// We ignore candidate->cost in the calculations below as it belongs
673+
// to both parts being compared.
674+
675+
// Estimate amount of records to be sorted
676+
const auto cardinality = streamCardinality * candidate->selectivity;
677+
const auto matchCardinality = streamCardinality * candidate->matchSelectivity;
678+
679+
// We optimistically assume that records will be cached during sorting
680+
const auto sortCost =
681+
// record copying (to the sort buffer and back)
682+
cardinality * COST_FACTOR_MEMCOPY * 2 +
683+
// quicksort algorithm is O(n*log(n)) in average
684+
cardinality * log2(cardinality) * COST_FACTOR_QUICKSORT;
685+
686+
// During navigation we fetch an index leaf page per every record being returned,
687+
// thus add the estimated cardinality to the cost
688+
auto navigationCost = navigationCandidate->cost +
689+
matchCardinality * navigationCandidate->selectivity;
690+
691+
if (optimizer->favorFirstRows())
692+
{
693+
// Reset the cost to represent a single record retrieval
694+
navigationCost = DEFAULT_INDEX_COST;
695+
696+
// We know that some local filtering is applied, so we need
697+
// to adjust the cost as we need to walk the index
698+
// until the first matching record is found
699+
const auto fullIndexCost = navigationCandidate->scratch->cardinality;
700+
const auto ratio = MAXIMUM_SELECTIVITY / candidate->selectivity;
701+
const auto fraction = ratio / streamCardinality;
702+
const auto walkCost = fullIndexCost * fraction * navigationCandidate->selectivity;
703+
navigationCost += walkCost;
704+
}
705+
706+
if (sortCost < navigationCost)
707+
{
708+
candidate->navigated = false;
709+
navigationCandidate->cost = 0;
710+
}
711+
}
712+
713+
// Update the final inversion candidate
714+
715+
candidate->unique = candidate->unique || navigationCandidate->unique;
716+
candidate->selectivity *= navigationCandidate->selectivity;
717+
candidate->cost += navigationCandidate->cost;
718+
candidate->indexes += navigationCandidate->indexes;
719+
720+
for (const auto navMatch : navigationCandidate->matches)
721+
{
722+
if (!candidate->matches.exist(navMatch))
723+
candidate->matches.add(navMatch);
724+
}
725+
726+
if (!candidate->navigated)
727+
{
728+
// If navigation is undesired, add its possible inversion to the final candidate
729+
730+
if (navigationCandidate->matches.hasData())
731+
{
732+
const auto inversionNode = (!navigationCandidate->inversion && navigationCandidate->scratch) ?
733+
makeIndexScanNode(navigationCandidate->scratch) : navigationCandidate->inversion;
734+
candidate->inversion = composeInversion(candidate->inversion,
735+
inversionNode, InversionNode::TYPE_AND);
736+
}
737+
738+
navigationCandidate.reset();
739+
}
740+
}
741+
718742
bool Retrieval::betterInversion(const InversionCandidate* inv1,
719743
const InversionCandidate* inv2,
720744
bool navigation) const
@@ -1755,27 +1779,6 @@ InversionCandidate* Retrieval::makeInversion(InversionCandidateList& inversions)
17551779
}
17561780
}
17571781

1758-
// If we have no index used for filtering, but there's a navigational walk,
1759-
// set up the inversion candidate appropriately.
1760-
1761-
if (navigationCandidate)
1762-
{
1763-
if (!invCandidate)
1764-
invCandidate = FB_NEW_POOL(getPool()) InversionCandidate(getPool());
1765-
1766-
invCandidate->unique = navigationCandidate->unique;
1767-
invCandidate->selectivity *= navigationCandidate->selectivity;
1768-
invCandidate->cost += navigationCandidate->cost;
1769-
++invCandidate->indexes;
1770-
invCandidate->navigated = true;
1771-
1772-
for (const auto navMatch : navigationCandidate->matches)
1773-
{
1774-
if (!invCandidate->matches.exist(navMatch))
1775-
invCandidate->matches.add(navMatch);
1776-
}
1777-
}
1778-
17791782
return invCandidate;
17801783
}
17811784

0 commit comments

Comments
 (0)