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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user