perf(FiveSql2): fix O(N²) window-function regression for default frame

Q2 Running total regressed 100ms→6.7s from the frame-aware rewrite.
Default frame (UNBOUNDED PRECEDING to CURRENT ROW) now uses O(N)
incremental path; general per-row-frame loop only for custom frames.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 23:24:02 +09:00
parent c869a08365
commit 79e812a24e

View File

@@ -2955,7 +2955,7 @@ METHOD ApplyWindowFunctions( aRows, aFN, aCols ) CLASS TSqlExecutor
LOCAL nLagLead, nArgCol, xDefault
LOCAL nRunSum, nRunCount
LOCAL aWinCols, nWC
LOCAL hFrame, nFS, nFE, m, xVal, xMin, xMax
LOCAL hFrame, nFS, nFE, m, xVal, xMin, xMax, lDefaultFrame
/* Scan for window function columns */
aWinCols := {}
@@ -3114,19 +3114,73 @@ METHOD ApplyWindowFunctions( aRows, aFN, aCols ) CLASS TSqlExecutor
ENDIF
ENDIF
/* Detect default frame (UNBOUNDED PRECEDING to CURRENT ROW)
* which can use the O(N) incremental running-sum path instead
* of the O(N²) general per-row-frame-aggregate. */
/* Default frame = UNBOUNDED PRECEDING to CURRENT ROW.
* This covers: no frame spec, or explicit ROWS UNBOUNDED
* PRECEDING (without BETWEEN or with implied CURRENT ROW end).
* The incremental O(N) path handles this; the general frame
* loop is only needed for custom boundaries like
* ROWS BETWEEN 6 PRECEDING AND CURRENT ROW. */
lDefaultFrame := .T.
IF hFrame != NIL .AND. ValType( hFrame ) == "H"
IF hb_HHasKey( hFrame, "end" ) .AND. ;
! ( "CURRENT ROW" $ hFrame[ "end" ] )
lDefaultFrame := .F.
ENDIF
IF hb_HHasKey( hFrame, "start" ) .AND. ;
! ( "UNBOUNDED PRECEDING" $ hFrame[ "start" ] )
lDefaultFrame := .F.
ENDIF
ENDIF
IF lDefaultFrame
/* O(N) incremental path — original fast code */
nRunSum := 0
nRunCount := 0
xMin := NIL
xMax := NIL
FOR k := 1 TO Len( aPartIdx )
IF cFunc == "COUNT" .AND. nArgCol == 0
nRunCount++
ELSEIF nArgCol > 0 .AND. nArgCol <= Len( aRows[ aPartIdx[ k ] ] )
xVal := aRows[ aPartIdx[ k ] ][ nArgCol ]
IF xVal != NIL
nRunCount++
nRunSum += SqlCoerceNum( xVal )
IF xMin == NIL .OR. SqlCmpLt( xVal, xMin )
xMin := xVal
ENDIF
IF xMax == NIL .OR. SqlCmpLt( xMax, xVal )
xMax := xVal
ENDIF
ENDIF
ENDIF
IF nColIdx <= Len( aRows[ aPartIdx[ k ] ] )
DO CASE
CASE cFunc == "SUM"
aRows[ aPartIdx[ k ] ][ nColIdx ] := nRunSum
CASE cFunc == "AVG"
aRows[ aPartIdx[ k ] ][ nColIdx ] := iif( nRunCount > 0, nRunSum / nRunCount, NIL )
CASE cFunc == "COUNT"
aRows[ aPartIdx[ k ] ][ nColIdx ] := nRunCount
CASE cFunc == "MIN"
aRows[ aPartIdx[ k ] ][ nColIdx ] := xMin
CASE cFunc == "MAX"
aRows[ aPartIdx[ k ] ][ nColIdx ] := xMax
ENDCASE
ENDIF
NEXT
ELSE
/* General frame path — O(N*W) where W = frame width */
FOR k := 1 TO Len( aPartIdx )
/* Compute frame boundaries for this row */
nFS := 1 /* default: UNBOUNDED PRECEDING */
nFE := k /* default: CURRENT ROW */
IF hFrame != NIL .AND. ValType( hFrame ) == "H"
IF hb_HHasKey( hFrame, "start" )
nFS := SqlFrameOffset( hFrame[ "start" ], k, Len( aPartIdx ) )
ENDIF
IF hb_HHasKey( hFrame, "end" )
nFE := SqlFrameOffset( hFrame[ "end" ], k, Len( aPartIdx ) )
ELSE
nFE := k
ENDIF
nFS := 1
nFE := k
IF hb_HHasKey( hFrame, "start" )
nFS := SqlFrameOffset( hFrame[ "start" ], k, Len( aPartIdx ) )
ENDIF
IF hb_HHasKey( hFrame, "end" )
nFE := SqlFrameOffset( hFrame[ "end" ], k, Len( aPartIdx ) )
ENDIF
IF nFS < 1
nFS := 1
@@ -3135,7 +3189,6 @@ METHOD ApplyWindowFunctions( aRows, aFN, aCols ) CLASS TSqlExecutor
nFE := Len( aPartIdx )
ENDIF
/* Aggregate over the frame window */
nRunSum := 0
nRunCount := 0
xMin := NIL
@@ -3174,6 +3227,7 @@ METHOD ApplyWindowFunctions( aRows, aFN, aCols ) CLASS TSqlExecutor
ENDCASE
ENDIF
NEXT
ENDIF /* lDefaultFrame */
ENDCASE
NEXT