diff --git a/_FiveSql2/src/TSqlExecutor.prg b/_FiveSql2/src/TSqlExecutor.prg index 8db3bab..b5c486f 100644 --- a/_FiveSql2/src/TSqlExecutor.prg +++ b/_FiveSql2/src/TSqlExecutor.prg @@ -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