perf: SqlHashJoin Go RTL — 3-way JOIN 4.2s→61ms (69x)

Go-native multi-table hash join bypasses per-row PRG overhead.
TryGoJoin detects equi-join + plain-col SELECT, aggregate cols
get placeholder. 2-way 73→3ms, 3-way 3.9s→61ms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 07:16:09 +09:00
parent 53aaa4b69a
commit 5fc9c3bbea
4 changed files with 353 additions and 1 deletions

View File

@@ -70,6 +70,7 @@ CLASS TSqlExecutor
METHOD ApplyWindowFunctions( aRows, aFN, aCols )
METHOD RunMerge()
METHOD RunTruncate()
METHOD TryGoJoin( aJoins, aResultExprs, nOuterWA )
METHOD TryBuildFieldPositions( aExprs )
METHOD TryCompileWhere( xWhere )
METHOD SqlExprToPrg( xNode )
@@ -1388,6 +1389,19 @@ METHOD RunSelect() CLASS TSqlExecutor
hJoinHash := { => }
/* === GO NATIVE JOIN FAST PATH ===
* Multi-table equi-join with all SELECT columns being plain
* field refs → hand the entire join to Go's SqlHashJoin.
* Bypasses per-row PRG JoinRecurse/FetchRow/dbSelectArea. */
IF Len( aJoins ) > 0 .AND. xWhere == NIL .AND. aGoRows == NIL
aGoRows := ::TryGoJoin( aJoins, aResultExprs, nWA )
IF aGoRows != NIL
FOR i := 1 TO Len( aGoRows )
AAdd( aRows, aGoRows[ i ] )
NEXT
ENDIF
ENDIF
/* === GO NATIVE FAST PATH ===
* Single-table, no joins, no aggregates, all SELECT exprs
* simple field refs, WHERE is NIL or compilable to pcode.
@@ -3781,6 +3795,133 @@ RETURN aResult
* SqlScan RTL. Any complexity (expressions, functions, joins,
* parameters in WHERE) → return NIL so the PRG loop takes over.
* -------------------------------------------------------------- */
/* TryGoJoin — attempt to hand a multi-table equi-join to Go's
* SqlHashJoin RTL. Returns the result array on success, NIL if the
* query shape doesn't fit (non-equi ON, complex SELECT exprs, etc.)
* and the caller should fall back to the PRG JoinRecurse path.
*
* Conditions for the fast path:
* - All joins are equi-joins on single columns (ND_BIN "=")
* - All SELECT columns are plain ND_COL field refs
* - No WHERE clause (WHERE is NIL)
*/
METHOD TryGoJoin( aJoins, aResultExprs, nOuterWA ) CLASS TSqlExecutor
LOCAL i, xE, xOnCond, cInnerAlias, cInnerField, cOuterField
LOCAL nInnerWA, nInnerFPos, nOuterFPos, nWA
LOCAL aJoinSpecs := {}, aSelectFields := {}
LOCAL cRef, nDot, cAlias, cField
LOCAL aGoRows
/* Build join specs: { nInnerWA, nInnerKeyField, nOuterKeyField } */
FOR i := 1 TO Len( aJoins )
xOnCond := aJoins[ i ][ 4 ]
/* Only support simple equi-join */
IF xOnCond == NIL .OR. xOnCond[ 1 ] != ND_BIN .OR. xOnCond[ 2 ] != "="
RETURN NIL
ENDIF
IF xOnCond[ 3 ] == NIL .OR. xOnCond[ 3 ][ 1 ] != ND_COL .OR. ;
xOnCond[ 4 ] == NIL .OR. xOnCond[ 4 ][ 1 ] != ND_COL
RETURN NIL
ENDIF
/* Determine which side is inner vs outer */
cInnerAlias := aJoins[ i ][ 3 ]
IF Empty( cInnerAlias )
cInnerAlias := aJoins[ i ][ 2 ]
ENDIF
IF ::ColBelongsTo( xOnCond[ 4 ][ 2 ], cInnerAlias )
cInnerField := xOnCond[ 4 ][ 2 ]
cOuterField := xOnCond[ 3 ][ 2 ]
ELSEIF ::ColBelongsTo( xOnCond[ 3 ][ 2 ], cInnerAlias )
cInnerField := xOnCond[ 3 ][ 2 ]
cOuterField := xOnCond[ 4 ][ 2 ]
ELSE
RETURN NIL
ENDIF
/* Resolve workarea + field positions */
nInnerWA := ::FindWA( Upper( cInnerAlias ) )
IF nInnerWA <= 0
RETURN NIL
ENDIF
dbSelectArea( nInnerWA )
cField := Upper( cInnerField )
IF "." $ cField
cField := SubStr( cField, At( ".", cField ) + 1 )
ENDIF
nInnerFPos := FieldPos( cField )
IF nInnerFPos == 0
RETURN NIL
ENDIF
/* Outer field — resolve in parent table */
cField := Upper( cOuterField )
nDot := At( ".", cField )
IF nDot > 0
cAlias := Left( cField, nDot - 1 )
cField := SubStr( cField, nDot + 1 )
nWA := ::FindWA( cAlias )
ELSE
nWA := nOuterWA
ENDIF
IF nWA <= 0
RETURN NIL
ENDIF
dbSelectArea( nWA )
nOuterFPos := FieldPos( cField )
IF nOuterFPos == 0
RETURN NIL
ENDIF
AAdd( aJoinSpecs, { nInnerWA, nInnerFPos, nOuterFPos } )
NEXT
/* Build select field specs: { nWA, nFieldPos } for each result column.
* Aggregate columns (ND_FN) get a {0, 0} placeholder — their values
* will be filled later by ComputeAgg during GROUP BY processing.
* This lets the Go fast path handle aggregate queries where the
* raw data columns (hidden) are plain ND_COL refs. */
FOR i := 1 TO Len( aResultExprs )
xE := aResultExprs[ i ][ 1 ]
IF xE == NIL .OR. xE[ 2 ] == "*"
RETURN NIL
ENDIF
IF xE[ 1 ] == ND_FN .OR. xE[ 1 ] == ND_WINDOW
/* Aggregate/window placeholder — Go returns 0, PRG fills later */
AAdd( aSelectFields, { 0, 0 } )
LOOP
ENDIF
IF xE[ 1 ] != ND_COL
RETURN NIL
ENDIF
cRef := xE[ 2 ]
nDot := At( ".", cRef )
IF nDot > 0
cAlias := Upper( Left( cRef, nDot - 1 ) )
cField := Upper( SubStr( cRef, nDot + 1 ) )
nWA := ::FindWA( cAlias )
ELSE
cField := Upper( cRef )
nWA := nOuterWA
ENDIF
IF nWA <= 0
RETURN NIL
ENDIF
dbSelectArea( nWA )
nOuterFPos := FieldPos( cField )
IF nOuterFPos == 0
RETURN NIL
ENDIF
AAdd( aSelectFields, { nWA, nOuterFPos } )
NEXT
/* Call Go-native hash join */
aGoRows := SqlHashJoin( aJoinSpecs, aSelectFields, nOuterWA )
RETURN aGoRows
METHOD TryBuildFieldPositions( aExprs ) CLASS TSqlExecutor
LOCAL aPositions := {}, i, xE, cRef, nDot, cField, nFPos