Skip to content

Commit 99c9f63

Browse files
committed
Implemented #7331: Cost-based choice between nested loop join and hash join
1 parent 4ab49be commit 99c9f63

File tree

5 files changed

+224
-73
lines changed

5 files changed

+224
-73
lines changed

src/jrd/optimizer/InnerJoin.cpp

Lines changed: 174 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,10 @@ void InnerJoin::calculateStreamInfo()
9797

9898
for (auto innerStream : innerStreams)
9999
{
100-
streams.add(innerStream->stream);
101-
csb->csb_rpt[innerStream->stream].activate();
100+
streams.add(innerStream->number);
101+
csb->csb_rpt[innerStream->number].activate();
102102

103-
Retrieval retrieval(tdbb, optimizer, innerStream->stream, false, false, sort, true);
103+
Retrieval retrieval(tdbb, optimizer, innerStream->number, false, false, sort, true);
104104
const auto candidate = retrieval.getInversion();
105105

106106
innerStream->baseCost = candidate->cost;
@@ -109,7 +109,7 @@ void InnerJoin::calculateStreamInfo()
109109
innerStream->baseUnique = candidate->unique;
110110
innerStream->baseNavigated = candidate->navigated;
111111

112-
csb->csb_rpt[innerStream->stream].deactivate();
112+
csb->csb_rpt[innerStream->number].deactivate();
113113
}
114114

115115
// Collect dependencies between every pair of streams
@@ -120,7 +120,7 @@ void InnerJoin::calculateStreamInfo()
120120

121121
for (const auto innerStream : innerStreams)
122122
{
123-
const StreamType testStream = innerStream->stream;
123+
const StreamType testStream = innerStream->number;
124124

125125
if (baseStream != testStream)
126126
{
@@ -145,7 +145,7 @@ void InnerJoin::calculateStreamInfo()
145145
}
146146

147147
// Unless PLAN is enforced, sort the streams based on independency and cost
148-
if (!plan && (innerStreams.getCount() > 1))
148+
if (!plan && innerStreams.getCount() > 1)
149149
{
150150
StreamInfoList tempStreams;
151151

@@ -172,23 +172,75 @@ void InnerJoin::calculateStreamInfo()
172172

173173
void InnerJoin::estimateCost(unsigned position,
174174
const StreamInfo* stream,
175-
double* cost,
176-
double* resultingCardinality) const
175+
double& cost,
176+
double& cardinality)
177177
{
178-
const auto sort = (position == 0 && sortPtr) ? *sortPtr : nullptr;
178+
fb_assert(joinedStreams[position].number == stream->number);
179+
180+
const auto sort = (!position && sortPtr) ? *sortPtr : nullptr;
179181

180182
// Create the optimizer retrieval generation class and calculate
181183
// which indexes will be used and the total estimated selectivity will be returned
182-
Retrieval retrieval(tdbb, optimizer, stream->stream, false, false, sort, true);
184+
Retrieval retrieval(tdbb, optimizer, stream->number, false, false, sort, true);
183185
const auto candidate = retrieval.getInversion();
186+
fb_assert(!position || candidate->dependencies);
187+
188+
// Calculate the relationship selectivity
189+
double selectivity = candidate->selectivity;
190+
if (selectivity < stream->baseSelectivity)
191+
selectivity /= stream->baseSelectivity;
192+
193+
joinedStreams[position].selectivity = selectivity;
194+
195+
// Get the stream cardinality
196+
const auto tail = &csb->csb_rpt[stream->number];
197+
const auto streamCardinality = tail->csb_cardinality;
184198

185-
*cost = candidate->cost;
199+
// Calculate the nested loop cost, it's our default option
200+
const auto loopCost = candidate->cost * cardinality;
201+
cost = loopCost;
186202

187-
// Calculate cardinality
188-
const auto tail = &csb->csb_rpt[stream->stream];
189-
const double cardinality = tail->csb_cardinality * candidate->selectivity;
203+
if (position)
204+
{
205+
// Calculate the hashing cost. It's estimated as the hashed stream retrieval cost
206+
// plus two cardinalities. Hashed stream cardinality means the cost of copying rows
207+
// into the hash table and the outer cardinality represents probing the hash table.
208+
const auto hashCardinality = stream->baseSelectivity * streamCardinality;
209+
const auto hashCost = stream->baseCost + hashCardinality + cardinality;
210+
211+
if (hashCost <= loopCost && hashCardinality <= HashJoin::maxCapacity())
212+
{
213+
auto& equiMatches = joinedStreams[position].equiMatches;
214+
fb_assert(!equiMatches.hasData());
190215

191-
*resultingCardinality = MAX(cardinality, MINIMUM_CARDINALITY);
216+
// Scan the matches for possible equi-join conditions
217+
for (const auto match : candidate->matches)
218+
{
219+
// Check whether we have an equivalence operation
220+
if (!optimizer->checkEquiJoin(match))
221+
continue;
222+
223+
// Check whether the match references priorly joined streams
224+
const auto end = joinedStreams.begin() + position;
225+
for (auto iter = joinedStreams.begin(); iter != end; ++iter)
226+
{
227+
if (match->containsStream(iter->number) &&
228+
equiMatches.getCount() < equiMatches.getCapacity())
229+
{
230+
equiMatches.add(match);
231+
break;
232+
}
233+
}
234+
}
235+
236+
// Adjust the actual cost value, if hash joining is both possible and preferrable
237+
if (equiMatches.hasData())
238+
cost = hashCost;
239+
}
240+
}
241+
242+
const auto resultingCardinality = streamCardinality * candidate->selectivity;
243+
cardinality = MAX(resultingCardinality, MINIMUM_CARDINALITY);
192244
}
193245

194246

@@ -231,7 +283,8 @@ bool InnerJoin::findJoinOrder()
231283
{
232284
if (!bestCount || innerStream->baseCost < bestCost)
233285
{
234-
joinedStreams[0].bestStream = innerStream->stream;
286+
bestStreams.resize(1);
287+
bestStreams.front().number = innerStream->number;
235288
bestCount = 1;
236289
bestCost = innerStream->baseCost;
237290
}
@@ -270,11 +323,10 @@ bool InnerJoin::findJoinOrder()
270323
}
271324

272325
// Mark streams as used
273-
for (unsigned i = 0; i < bestCount; i++)
326+
for (const auto& stream : bestStreams)
274327
{
275-
auto streamInfo = getStreamInfo(joinedStreams[i].bestStream);
328+
auto streamInfo = getStreamInfo(stream.number);
276329
streamInfo->used = true;
277-
bestStreams.add(joinedStreams[i].bestStream);
278330
}
279331

280332
#ifdef OPT_DEBUG
@@ -299,11 +351,11 @@ void InnerJoin::findBestOrder(unsigned position,
299351
double cost,
300352
double cardinality)
301353
{
302-
const auto tail = &csb->csb_rpt[stream->stream];
354+
const auto tail = &csb->csb_rpt[stream->number];
303355

304356
// Do some initializations
305357
tail->activate();
306-
joinedStreams[position].number = stream->stream;
358+
joinedStreams[position].reset(stream->number);
307359

308360
// Save the various flag bits from the optimizer block to reset its
309361
// state after each test
@@ -312,13 +364,13 @@ void InnerJoin::findBestOrder(unsigned position,
312364
streamFlags.add(innerStream->used);
313365

314366
// Compute delta and total estimate cost to fetch this stream
315-
double positionCost = 0, positionCardinality = 0, newCost = 0, newCardinality = 0;
367+
double positionCost = 0, positionCardinality = cardinality, newCost = 0, newCardinality = 0;
316368

317369
if (!plan)
318370
{
319-
estimateCost(position, stream, &positionCost, &positionCardinality);
320-
newCost = cost + cardinality * positionCost;
321-
newCardinality = positionCardinality * cardinality;
371+
estimateCost(position, stream, positionCost, positionCardinality);
372+
newCost = cost + positionCost;
373+
newCardinality = cardinality * positionCardinality;
322374
}
323375

324376
position++;
@@ -329,13 +381,7 @@ void InnerJoin::findBestOrder(unsigned position,
329381
{
330382
bestCount = position;
331383
bestCost = newCost;
332-
333-
const auto end = joinedStreams.begin() + position;
334-
for (auto iter = joinedStreams.begin(); iter != end; ++iter)
335-
{
336-
auto& joinedStream = *iter;
337-
joinedStream.bestStream = joinedStream.number;
338-
}
384+
bestStreams.assign(joinedStreams.begin(), position);
339385
}
340386

341387
#ifdef OPT_DEBUG
@@ -398,7 +444,7 @@ void InnerJoin::findBestOrder(unsigned position,
398444
IndexRelationship* processRelationship = processList.begin();
399445
for (FB_SIZE_T index = 0; index < processList.getCount(); index++)
400446
{
401-
if (relationStreamInfo->stream == processRelationship[index].stream)
447+
if (relationStreamInfo->number == processRelationship[index].stream)
402448
{
403449
// If the cost of this relationship is cheaper then remove the
404450
// old relationship and add this one
@@ -445,25 +491,103 @@ void InnerJoin::findBestOrder(unsigned position,
445491
River* InnerJoin::formRiver()
446492
{
447493
fb_assert(bestCount);
448-
fb_assert(bestStreams.hasData());
494+
fb_assert(bestStreams.getCount() == bestCount);
449495

450-
if (bestStreams.getCount() != innerStreams.getCount())
496+
const auto orgSortPtr = sortPtr;
497+
498+
if (bestCount != innerStreams.getCount())
451499
sortPtr = nullptr;
452500

501+
RecordSource* rsb;
502+
StreamList streams;
453503
HalfStaticArray<RecordSource*, OPT_STATIC_ITEMS> rsbs;
454504

455-
for (const auto stream : bestStreams)
505+
for (const auto& stream : bestStreams)
456506
{
457-
const auto rsb = optimizer->generateRetrieval(stream, sortPtr, false, false);
507+
// We use hash join instead of nested loop join if:
508+
// - stream has equivalence relationship(s) with the prior streams
509+
// (and hashing was estimated to be cheaper)
510+
// AND
511+
// - optimization for first rows is not requested
512+
// OR
513+
// - existing sort was not utilized using an index
514+
515+
if (rsbs.hasData() && // this is not the first stream
516+
stream.equiMatches.hasData() &&
517+
(!optimizer->favorFirstRows() || (orgSortPtr && *orgSortPtr)))
518+
{
519+
fb_assert(streams.hasData());
520+
521+
// Deactivate priorly joined streams
522+
StreamStateHolder stateHolder(csb, streams);
523+
stateHolder.deactivate();
524+
525+
// Create an independent retrieval
526+
rsb = optimizer->generateRetrieval(stream.number, sortPtr, false, false);
527+
528+
// Create a nested loop join from the priorly processed streams
529+
const auto priorRsb = (rsbs.getCount() == 1) ? rsbs[0] :
530+
FB_NEW_POOL(getPool()) NestedLoopJoin(csb, rsbs.getCount(), rsbs.begin());
531+
532+
const River priorRiver(csb, priorRsb, nullptr, streams);
533+
534+
// Prepare record sources and corresponding equivalence keys for hash-joining
535+
RecordSource* hashJoinRsbs[] = {priorRsb, rsb};
536+
537+
HalfStaticArray<NestValueArray*, OPT_STATIC_ITEMS> keys;
538+
539+
keys.add(FB_NEW_POOL(getPool()) NestValueArray(getPool()));
540+
keys.add(FB_NEW_POOL(getPool()) NestValueArray(getPool()));
541+
542+
for (const auto match : stream.equiMatches)
543+
{
544+
NestConst<ValueExprNode> node1;
545+
NestConst<ValueExprNode> node2;
546+
547+
if (!optimizer->getEquiJoinKeys(match, &node1, &node2))
548+
fb_assert(false);
549+
550+
if (!priorRiver.isReferenced(node1))
551+
{
552+
fb_assert(priorRiver.isReferenced(node2));
553+
554+
// Swap the sides
555+
std::swap(node1, node2);
556+
}
557+
558+
keys[0]->add(node1);
559+
keys[1]->add(node2);
560+
}
561+
562+
// Ensure the smallest stream is the one to be hashed
563+
if (rsb->getCardinality() > priorRsb->getCardinality())
564+
{
565+
// Swap the sides
566+
std::swap(hashJoinRsbs[0], hashJoinRsbs[1]);
567+
std::swap(keys[0], keys[1]);
568+
}
569+
570+
// Create a hash join
571+
rsb = FB_NEW_POOL(getPool())
572+
HashJoin(tdbb, csb, 2, hashJoinRsbs, keys.begin(), stream.selectivity);
573+
574+
// Clear priorly processed rsb's, as they're already incorporated into a hash join
575+
rsbs.clear();
576+
}
577+
else
578+
rsb = optimizer->generateRetrieval(stream.number, sortPtr, false, false);
579+
458580
rsbs.add(rsb);
581+
streams.add(stream.number);
459582
sortPtr = nullptr;
460583
}
461584

462-
const auto rsb = (rsbs.getCount() == 1) ? rsbs[0] :
585+
// Create a nested loop join from the processed streams
586+
rsb = (rsbs.getCount() == 1) ? rsbs[0] :
463587
FB_NEW_POOL(getPool()) NestedLoopJoin(csb, rsbs.getCount(), rsbs.begin());
464588

465589
// Allocate a river block and move the best order into it
466-
const auto river = FB_NEW_POOL(getPool()) River(csb, rsb, nullptr, bestStreams);
590+
const auto river = FB_NEW_POOL(getPool()) River(csb, rsb, nullptr, streams);
467591
river->deactivate(csb);
468592
return river;
469593
}
@@ -484,21 +608,21 @@ void InnerJoin::getIndexedRelationships(StreamInfo* testStream)
484608
testStream->stream, name.c_str());
485609
#endif
486610

487-
const auto tail = &csb->csb_rpt[testStream->stream];
611+
const auto tail = &csb->csb_rpt[testStream->number];
488612

489-
Retrieval retrieval(tdbb, optimizer, testStream->stream, false, false, nullptr, true);
613+
Retrieval retrieval(tdbb, optimizer, testStream->number, false, false, nullptr, true);
490614
const auto candidate = retrieval.getInversion();
491615

492616
for (const auto baseStream : innerStreams)
493617
{
494-
if (baseStream->stream != testStream->stream &&
495-
candidate->dependentFromStreams.exist(baseStream->stream))
618+
if (baseStream->number != testStream->number &&
619+
candidate->dependentFromStreams.exist(baseStream->number))
496620
{
497621
// If the base stream already depends on the testing stream, don't store it again
498622
bool found = false;
499623
for (const auto& relationship : baseStream->indexedRelationships)
500624
{
501-
if (relationship.stream == testStream->stream)
625+
if (relationship.stream == testStream->number)
502626
{
503627
found = true;
504628
break;
@@ -515,7 +639,7 @@ void InnerJoin::getIndexedRelationships(StreamInfo* testStream)
515639
// with the base stream active as without the base stream
516640
// then the test stream has a indexed relationship with the base stream.
517641
IndexRelationship indexRelationship;
518-
indexRelationship.stream = testStream->stream;
642+
indexRelationship.stream = testStream->number;
519643
indexRelationship.unique = candidate->unique;
520644
indexRelationship.cost = candidate->cost;
521645
indexRelationship.cardinality = candidate->unique ?
@@ -540,7 +664,7 @@ InnerJoin::StreamInfo* InnerJoin::getStreamInfo(StreamType stream)
540664
{
541665
for (FB_SIZE_T i = 0; i < innerStreams.getCount(); i++)
542666
{
543-
if (innerStreams[i]->stream == stream)
667+
if (innerStreams[i]->number == stream)
544668
return innerStreams[i];
545669
}
546670

@@ -561,8 +685,8 @@ void InnerJoin::printBestOrder() const
561685
const auto end = bestStreams.end();
562686
for (auto iter = bestStreams.begin(); iter != end; iter++)
563687
{
564-
const auto name = optimizer->getStreamName(*iter);
565-
optimizer->printf(" %u (%s)", *iter, name.c_str());
688+
const auto name = optimizer->getStreamName(iter->number);
689+
optimizer->printf(" %u (%s)", iter->number, name.c_str());
566690

567691
if (iter != end - 1)
568692
optimizer->printf(",");
@@ -616,9 +740,9 @@ void InnerJoin::printStartOrder() const
616740
const auto innerStream = *iter;
617741
if (!innerStream->used)
618742
{
619-
const auto name = optimizer->getStreamName(innerStream->stream);
743+
const auto name = optimizer->getStreamName(innerStream->number);
620744
optimizer->printf(" %u (%s) base cost (%1.2f)",
621-
innerStream->stream, name.c_str(), innerStream->baseCost);
745+
innerStream->number, name.c_str(), innerStream->baseCost);
622746

623747
if (iter != end - 1)
624748
optimizer->printf(",");

0 commit comments

Comments
 (0)