Major changes since last commit: - FiveSql2 SQL:1999 engine (10,458 LOC) — 43/43 ALL PASS - 21 compiler/runtime bugs fixed (short-circuit AND/OR, FOR LOOP, etc.) - @byref pass-by-reference via RefCell pattern - Mutable closure capture (EnsureLocalRef + RefCell sharing) - RTL: 400 → 479 functions (+79: file, string, datetime, hash, UTF-8) - DateTime/Timestamp fully working (hb_DateTime, hb_Hour/Min/Sec, display) - Reserved word guard (39 keywords blocked from function calls) - AEval arg order fix (element before index) - Closure capture redecl fix (unique _cap_ names per block) - Hash/string indexing in ArrayPush/ArrayPop - Harbour compat test suite: 51/51 - 4 docs: Porting Report, Implementation Plan, Optimization Plan, Commercialization Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
516 lines
16 KiB
Markdown
516 lines
16 KiB
Markdown
# FiveSql2 최적화 계획서
|
||
|
||
**Date:** 2026-04-08
|
||
**Experts:** SQL Query Optimizer · Go I/O Architect · SQLite Internal Designer
|
||
**Baseline:** Home ext4, 100 emp + 200 orders, 1000 iterations
|
||
|
||
---
|
||
|
||
## 현재 성능 프로파일
|
||
|
||
| 쿼리 | us/query | 병목 |
|
||
|------|----------|------|
|
||
| SELECT * | 2,192 | Resolve per column per row |
|
||
| WHERE filter | 2,280 | EvalExpr per row |
|
||
| ORDER BY | 3,150 | ASort + SqlCoerceForCmp per comparison |
|
||
| GROUP BY HAVING | 4,135 | SqlValToStr + FindColIdx per row per col |
|
||
| DISTINCT | 1,019 | RowKey string concat |
|
||
| INNER JOIN | 9,291 | Hash join probe + dbGoto per match |
|
||
| CTE simple | 8,037 | Temp DBF create/write/read/delete |
|
||
| RECURSIVE CTE | 5,585 | In-memory (good) |
|
||
| ROW_NUMBER | 3,705 | Partition key + sort |
|
||
| RANK PARTITION | 4,748 | Partition key + sort |
|
||
| SUM OVER | 2,060 | Partition key |
|
||
| INSERT | 4,319 | dbAppend + FieldPut |
|
||
| UPDATE | 6,144 | Scan + replace + commit |
|
||
| COUNT(*) | 1,065 | Full scan |
|
||
| CTE+Win+JOIN | 18,395 | CTE file I/O + JOIN + Window |
|
||
|
||
---
|
||
|
||
## Expert 1: SQL Query Optimizer
|
||
|
||
> *"가장 빠른 행은 읽지 않는 행이다"*
|
||
|
||
### 1.1 Resolve 캐시 — Column Binding (Impact: ALL queries, -40%)
|
||
|
||
**현재:** 매 행의 매 컬럼마다 `Resolve()` 호출
|
||
|
||
```
|
||
FetchRow (per row):
|
||
FOR i := 1 TO Len(aExprs) // 4 columns
|
||
cField := Upper(SubStr(...)) // string op
|
||
nWA := ::FindWA(cTblAlias) // linear search in aTables
|
||
dbSelectArea(nWA) // workarea switch
|
||
nFPos := FieldPos(cField) // field name lookup
|
||
xVal := FieldGet(nFPos) // actual read
|
||
AllTrim(xVal) // string trim
|
||
NEXT
|
||
```
|
||
|
||
100 rows × 4 cols = 400 calls → 400× FindWA + 400× FieldPos + 400× dbSelectArea
|
||
|
||
**SQLite 방식: Compile-time column binding**
|
||
|
||
쿼리 실행 전에 각 column reference를 `{nWA, nFieldPos}` 쌍으로 미리 바인딩한다.
|
||
SQLite의 `OP_Column` opcode는 cursor + column_index 만 가지고 직접 읽는다.
|
||
|
||
```
|
||
// 구현: RunSelect에서 scan loop 진입 전
|
||
LOCAL aBindings := {} // pre-computed {nWA, nFieldPos} per result column
|
||
FOR i := 1 TO Len(aResultExprs)
|
||
xE := aResultExprs[i][1]
|
||
IF xE[1] == ND_COL .AND. xE[2] != "*"
|
||
// resolve once → store {nWA, nFPos}
|
||
AAdd(aBindings, {nWA, nFPos})
|
||
ELSE
|
||
AAdd(aBindings, NIL) // complex expr → fallback to EvalExpr
|
||
ENDIF
|
||
NEXT
|
||
|
||
// scan loop: use binding directly
|
||
WHILE ! Eof()
|
||
aRow := {}
|
||
FOR i := 1 TO Len(aBindings)
|
||
IF aBindings[i] != NIL
|
||
dbSelectArea(aBindings[i][1])
|
||
AAdd(aRow, AllTrim(FieldGet(aBindings[i][2])))
|
||
ELSE
|
||
AAdd(aRow, ::EvalExpr(aResultExprs[i][1]))
|
||
ENDIF
|
||
NEXT
|
||
...
|
||
ENDDO
|
||
```
|
||
|
||
**WHERE에도 동일 적용:** WHERE expr 안의 ND_COL도 바인딩하면 EvalExpr recursion 제거.
|
||
|
||
**예상 효과:**
|
||
- SELECT *: 2,192 → ~800us (-63%)
|
||
- WHERE: 2,280 → ~900us (-60%)
|
||
- 전체 쿼리 평균: -40%
|
||
|
||
### 1.2 GROUP BY — Hash Key 사전 계산 (Impact: GROUP BY, -50%)
|
||
|
||
**현재:**
|
||
```
|
||
FOR i := 1 TO Len(aRows) // 100 rows
|
||
cKey := ""
|
||
FOR j := 1 TO Len(aGroupBy) // 1-3 cols
|
||
nGCol := ::FindColIdx(aGroupBy[j], aFN) // O(n) linear search
|
||
cKey += SqlValToStr(aRows[i][nGCol]) + "|" // AllTrim + Str
|
||
NEXT
|
||
NEXT
|
||
```
|
||
|
||
**SQLite 방식: Column index pre-resolve + integer hash**
|
||
|
||
```
|
||
// Pre-resolve group column indices once
|
||
aGColIdx := {}
|
||
FOR j := 1 TO Len(aGroupBy)
|
||
AAdd(aGColIdx, ::FindColIdx(aGroupBy[j], aFN))
|
||
NEXT
|
||
|
||
// Use integer hash instead of string concat
|
||
FOR i := 1 TO Len(aRows)
|
||
nHash := 0
|
||
FOR j := 1 TO Len(aGColIdx)
|
||
nHash := nHash * 31 + SqlHashVal(aRows[i][aGColIdx[j]])
|
||
NEXT
|
||
// Collision check with actual values only on hash match
|
||
NEXT
|
||
```
|
||
|
||
### 1.3 Aggregate — SqlCoerceNum 중복 제거 (Impact: GROUP BY, -20%)
|
||
|
||
**현재 (TSqlAgg:199-200):**
|
||
```
|
||
IF xMin == NIL .OR. SqlCoerceNum(xVal) < SqlCoerceNum(xMin)
|
||
// SqlCoerceNum(xVal) called TWICE per comparison
|
||
```
|
||
|
||
**수정:**
|
||
```
|
||
nVal := SqlCoerceNum(xVal) // cache once
|
||
nSum += nVal
|
||
IF xMin == NIL .OR. nVal < nMinCached
|
||
xMin := xVal
|
||
nMinCached := nVal
|
||
ENDIF
|
||
```
|
||
|
||
### 1.4 WHERE Short-Circuit 강화 (Impact: WHERE, -15%)
|
||
|
||
**현재:** `AND`/`OR`는 short-circuit하지만, 비교 연산자 `=`, `<`, `>` 에서
|
||
양쪽 모두 `SqlCoerceForCmp()` (AllTrim + Upper)을 호출한다.
|
||
|
||
**수정:** 같은 타입이면 coercion 스킵
|
||
|
||
```
|
||
IF ValType(xL) == "N" .AND. ValType(xR) == "N"
|
||
RETURN xL < xR // direct comparison, no coercion
|
||
ENDIF
|
||
IF ValType(xL) == "C" .AND. ValType(xR) == "C"
|
||
RETURN xL < xR // Harbour string comparison
|
||
ENDIF
|
||
// Fallback: coerce
|
||
```
|
||
|
||
---
|
||
|
||
## Expert 2: Go I/O Architect
|
||
|
||
> *"시스템 콜 1회 = 유저 코드 10,000줄"*
|
||
|
||
### 2.1 CTE — Temp DBF 제거, In-Memory Table (Impact: CTE, -80%)
|
||
|
||
**현재 (MaterializeCTE):**
|
||
```
|
||
dbCreate("__cte_name.dbf", aStruct) // 1. 파일 생성 (syscall: open+write)
|
||
USE __cte_name.dbf NEW // 2. 파일 열기 (syscall: open+read)
|
||
FOR each row
|
||
dbAppend() // 3. 레코드 추가 (syscall: write)
|
||
NEXT
|
||
CLOSE // 4. 닫기 (syscall: close)
|
||
USE __cte_name.dbf NEW SHARED // 5. 다시 열기 (syscall: open+read)
|
||
... query uses it ...
|
||
CLOSE // 6. 닫기
|
||
FErase("__cte_name.dbf") // 7. 삭제 (syscall: unlink)
|
||
```
|
||
|
||
7번의 syscall이 CTE당 발생. ext4에서도 ~5ms.
|
||
|
||
**SQLite 방식: Ephemeral Table**
|
||
|
||
SQLite는 CTE를 `sqlite3_malloc` 기반 B-tree에 저장한다.
|
||
Five에서는 in-memory array를 직접 사용:
|
||
|
||
```
|
||
// MaterializeCTE → MaterializeCTEInMemory
|
||
aFN := {"COL1", "COL2", ...}
|
||
aDataRows := {}
|
||
// Execute anchor query, collect rows into aDataRows
|
||
// Store in executor context: ::hCTEData[cName] := {aFN, aDataRows}
|
||
// No dbCreate, no USE, no FErase
|
||
|
||
// When referenced:
|
||
// Instead of dbSelectArea + FieldGet, use direct array access
|
||
// aRow := ::hCTEData["name"][2][nRowIdx]
|
||
```
|
||
|
||
RECURSIVE CTE는 이미 이 방식이다 (TSqlExecutor:2062). Non-recursive도 동일 적용.
|
||
|
||
**예상 효과:** CTE: 8,037 → ~1,000us (-87%)
|
||
|
||
### 2.2 dbSelectArea 최소화 (Impact: ALL, -15%)
|
||
|
||
**현재:** scan loop 내에서 매 행마다 `dbSelectArea(nWA)` 호출
|
||
|
||
```
|
||
WHILE ! Eof()
|
||
... process row ...
|
||
dbSelectArea(nWA) // ← 매번 호출 (100행 = 100회)
|
||
dbSkip()
|
||
ENDDO
|
||
```
|
||
|
||
**Go 레벨:** `dbSelectArea`는 WorkAreaManager의 current area를 바꾸는 함수.
|
||
JOIN이 없는 단일 테이블 쿼리에서는 불필요.
|
||
|
||
**수정:**
|
||
```
|
||
dbSelectArea(nWA) // loop 전 1회
|
||
dbGoTop()
|
||
WHILE ! Eof()
|
||
... process row (no dbSelectArea needed if single table) ...
|
||
dbSkip() // same WA, no switch needed
|
||
ENDDO
|
||
```
|
||
|
||
JOIN이 있을 때만 `dbSelectArea` 유지.
|
||
|
||
### 2.3 FieldGet Batch — 행 단위 일괄 읽기 (Impact: SELECT *, -30%)
|
||
|
||
**현재:** 컬럼마다 `FieldGet(i)` 개별 호출
|
||
|
||
```
|
||
FOR i := 1 TO nCols
|
||
dbSelectArea(nWA)
|
||
AAdd(aRow, FieldGet(nFPos))
|
||
NEXT
|
||
```
|
||
|
||
**Go 레벨 최적화:** `hbrdd/dbf/dbf.go`에 `GetRecord() []Value` 메서드 추가
|
||
|
||
```go
|
||
// dbf.go — 레코드 버퍼에서 모든 필드를 한 번에 추출
|
||
func (a *Area) GetRecord() []Value {
|
||
fields := make([]Value, a.header.NumFields())
|
||
for i := range fields {
|
||
fields[i] = a.getFieldFromBuffer(i)
|
||
}
|
||
return fields
|
||
}
|
||
```
|
||
|
||
RTL에서 `dbGetRecord()` 함수로 노출 → FiveSql2의 FetchRow에서 사용.
|
||
|
||
**예상 효과:** SELECT *: 2,192 → ~1,400us (-36%)
|
||
|
||
### 2.4 AllTrim 지연 — Lazy Trim (Impact: ALL string ops, -10%)
|
||
|
||
**현재:** FieldGet 후 즉시 `AllTrim(xVal)` — 매 행 매 컬럼
|
||
|
||
DBF는 고정 길이 필드이므로 `"Alice "` 같은 공백 패딩이 있다.
|
||
하지만 모든 경우에 trim이 필요한 건 아니다:
|
||
- 비교: `WHERE name = 'Alice'` → `"Alice " = "Alice"` Harbour는 `SET EXACT OFF`에서 매칭
|
||
- 출력: 최종 결과에서만 trim 필요
|
||
|
||
**수정:** FetchRow에서 trim 제거, 최종 결과 반환 시에만 trim
|
||
|
||
---
|
||
|
||
## Expert 3: SQLite Internal Designer
|
||
|
||
> *"쿼리 컴파일은 1회, 실행은 N회"*
|
||
|
||
### 3.1 Prepared Statement Cache (Impact: 반복 쿼리, -60%)
|
||
|
||
**현재:** 매 `five_SQL()` 호출마다:
|
||
```
|
||
TSqlLexer():New(cSQL) → Tokenize() // O(SQL길이)
|
||
TSqlParser2():New() → Parse() // O(토큰수)
|
||
TSqlExecutor():New() → Run() // O(행수)
|
||
```
|
||
|
||
FiveSql2 벤치마크는 동일 쿼리 1000회 반복. 매번 lex+parse = ~300us 낭비.
|
||
|
||
**SQLite 방식: sqlite3_prepare_v2 + sqlite3_step**
|
||
|
||
```
|
||
// Phase 1: Prepare (1회)
|
||
oStmt := five_SQL_Prepare("SELECT * FROM emp WHERE salary > ?")
|
||
|
||
// Phase 2: Execute (N회)
|
||
oStmt:Bind(1, 5000)
|
||
aR := oStmt:Execute()
|
||
|
||
// 내부: token array + parse tree를 캐시
|
||
// Execute()는 TSqlExecutor():New(cachedQuery):Run()만 호출
|
||
```
|
||
|
||
**간단한 구현 — Query Plan Cache:**
|
||
```
|
||
STATIC s_hPlanCache := { => }
|
||
|
||
METHOD Execute(cSQL) CLASS TFiveSQL
|
||
LOCAL hQuery
|
||
|
||
IF hb_HHasKey(s_hPlanCache, cSQL)
|
||
hQuery := s_hPlanCache[cSQL]
|
||
ELSE
|
||
::oLexer := TSqlLexer():New(cSQL)
|
||
::oLexer:Tokenize()
|
||
::oParser := TSqlParser2():New(::oLexer:GetTokens(), ::aParams)
|
||
hQuery := ::oParser:Parse()
|
||
s_hPlanCache[cSQL] := hQuery
|
||
ENDIF
|
||
|
||
::oExec := TSqlExecutor():New(hQuery, ::aParams)
|
||
RETURN ::oExec:Run()
|
||
```
|
||
|
||
**주의:** hQuery가 mutable이면 deep clone 필요. 또는 immutable 보장.
|
||
|
||
**예상 효과:** 반복 쿼리: -300us/query (COUNT: 1,065 → ~765us)
|
||
|
||
### 3.2 Virtual Table for CTE (Impact: CTE, -90%)
|
||
|
||
SQLite의 ephemeral table 개념을 확장:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────┐
|
||
│ 현재: CTE → Temp DBF File │
|
||
│ │
|
||
│ five_SQL("WITH top AS (...) SELECT * FROM top") │
|
||
│ ↓ │
|
||
│ dbCreate("__cte_top.dbf") ← DISK I/O │
|
||
│ USE __cte_top.dbf ← DISK I/O │
|
||
│ ... insert rows ... ← DISK I/O │
|
||
│ CLOSE + reopen ← DISK I/O │
|
||
│ ... scan via dbGoTop/Skip ← DISK I/O │
|
||
│ FErase ← DISK I/O │
|
||
│ │
|
||
│ 제안: CTE → In-Memory Virtual Table │
|
||
│ │
|
||
│ ::hCTEData["TOP"] := {aFieldNames, aDataRows} │
|
||
│ ↓ │
|
||
│ Resolve() checks ::hCTEData first │
|
||
│ If CTE alias → direct array access (zero I/O) │
|
||
│ No dbCreate, no USE, no FErase │
|
||
└─────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**핵심 변경:**
|
||
1. `MaterializeCTE` → `MaterializeCTEInMemory`: 배열로 저장
|
||
2. `Resolve()` → CTE alias 감지 시 `::hCTEData`에서 직접 읽기
|
||
3. `FetchRow()` → CTE 테이블이면 배열 인덱싱
|
||
|
||
### 3.3 Register-Based Evaluation (Impact: 모든 expr, -30%)
|
||
|
||
**현재:** Stack-based AST walking (tree interpreter)
|
||
|
||
```
|
||
EvalExpr({ND_BIN, ">", {ND_COL, "SALARY"}, {ND_LIT, 5000}})
|
||
→ EvalExpr({ND_COL, "SALARY"}) // recursive call 1
|
||
→ Resolve("SALARY") // string lookup
|
||
→ EvalExpr({ND_LIT, 5000}) // recursive call 2
|
||
→ return 5000
|
||
→ Compare(7500, 5000)
|
||
→ return .T.
|
||
```
|
||
|
||
**SQLite 방식: Compiled bytecode**
|
||
|
||
```
|
||
// Compile phase (1회):
|
||
program := {
|
||
{OP_COLUMN, 4, 0}, // R0 = field[4] (SALARY, pre-bound)
|
||
{OP_INTEGER, 5000, 1}, // R1 = 5000
|
||
{OP_GT, 0, 1, 2}, // R2 = R0 > R1
|
||
}
|
||
|
||
// Execute phase (per row):
|
||
FOR each instruction
|
||
SWITCH op
|
||
CASE OP_COLUMN: regs[dst] = FieldGet(field_idx)
|
||
CASE OP_INTEGER: regs[dst] = value
|
||
CASE OP_GT: regs[dst] = regs[a] > regs[b]
|
||
END
|
||
NEXT
|
||
```
|
||
|
||
이것은 **Phase 2** 최적화 — 현재 아키텍처에서는 column binding으로 대부분의 효과를 얻을 수 있다.
|
||
|
||
### 3.4 ORDER BY — Pre-Coerced Sort Key (Impact: ORDER BY, -40%)
|
||
|
||
**현재 ASort 콜백:**
|
||
```
|
||
{|a, b| SqlRowCompare(a, b) < 0}
|
||
|
||
SqlRowCompare:
|
||
xA := aRowA[nCol]
|
||
xB := aRowB[nCol]
|
||
xA := SqlCoerceForCmp(xA) // AllTrim() + Upper() — per comparison
|
||
xB := SqlCoerceForCmp(xB) // AllTrim() + Upper() — per comparison
|
||
```
|
||
|
||
100행 정렬 = ~660 comparisons × 2 AllTrim = 1,320 AllTrim calls
|
||
|
||
**SQLite 방식: Sort key materialization**
|
||
|
||
```
|
||
// Pre-compute sort keys before ASort
|
||
FOR i := 1 TO Len(aRows)
|
||
aSortKeys[i] := {}
|
||
FOR j := 1 TO Len(aOrderBy)
|
||
AAdd(aSortKeys[i], SqlCoerceForCmp(aRows[i][nCol]))
|
||
NEXT
|
||
NEXT
|
||
|
||
// ASort using pre-computed keys (no AllTrim in comparator)
|
||
ASort(aRows,,, {|a, b|
|
||
FOR k := 1 TO Len(aSortKeys[...])
|
||
IF aSortKeys[idxA][k] < aSortKeys[idxB][k]
|
||
RETURN .T.
|
||
ENDIF
|
||
NEXT
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
## 구현 우선순위
|
||
|
||
### Phase A: Quick Wins (각각 1시간 이내, 즉시 효과)
|
||
|
||
| # | 항목 | 대상 파일 | 예상 효과 |
|
||
|---|------|----------|----------|
|
||
| A1 | **Query Plan Cache** | TFiveSQL.prg | 반복 쿼리 -300us |
|
||
| A2 | **FindColIdx 캐시** | TSqlAgg.prg, TSqlSort.prg | GROUP BY -30% |
|
||
| A3 | **SqlCoerceNum 중복 제거** | TSqlAgg.prg:199-200 | Aggregate -20% |
|
||
| A4 | **dbSelectArea 제거 (단일 테이블)** | TSqlExecutor.prg:1203 | ALL -15% |
|
||
| A5 | **비교 타입 매칭 fast-path** | TSqlExecutor.prg:486-490 | WHERE -15% |
|
||
|
||
### Phase B: Medium Effort (각각 반나절, 구조 변경)
|
||
|
||
| # | 항목 | 대상 파일 | 예상 효과 |
|
||
|---|------|----------|----------|
|
||
| B1 | **Column Binding (pre-resolve)** | TSqlExecutor.prg | ALL -40% |
|
||
| B2 | **CTE In-Memory Virtual Table** | TSqlExecutor.prg | CTE -80% |
|
||
| B3 | **Sort Key Materialization** | TSqlSort.prg | ORDER BY -40% |
|
||
| B4 | **AllTrim lazy (최종만)** | TSqlExecutor.prg | ALL strings -10% |
|
||
|
||
### Phase C: Deep Optimization (1일+, 아키텍처 변경)
|
||
|
||
| # | 항목 | 대상 파일 | 예상 효과 |
|
||
|---|------|----------|----------|
|
||
| C1 | **Register-based expr eval** | TSqlExpr.prg (new) | ALL expr -30% |
|
||
| C2 | **GetRecord batch read** | hbrdd/dbf/dbf.go + hbrtl | SELECT * -36% |
|
||
| C3 | **RIGHT JOIN hash** | TSqlExecutor.prg | RIGHT JOIN -90% |
|
||
| C4 | **Prepared Statement API** | TFiveSQL.prg (new) | API 수준 캐싱 |
|
||
|
||
---
|
||
|
||
## 예상 최종 성능 (Phase A+B 완료 시)
|
||
|
||
| 쿼리 | 현재 (us) | Phase A | Phase A+B | 목표 |
|
||
|------|-----------|---------|-----------|------|
|
||
| SELECT * | 2,192 | 1,800 | **700** | < 1ms |
|
||
| WHERE | 2,280 | 1,900 | **800** | < 1ms |
|
||
| ORDER BY | 3,150 | 2,700 | **1,500** | < 2ms |
|
||
| GROUP BY | 4,135 | 2,800 | **1,500** | < 2ms |
|
||
| DISTINCT | 1,019 | 800 | **500** | < 1ms |
|
||
| JOIN | 9,291 | 7,500 | **3,000** | < 5ms |
|
||
| CTE | 8,037 | 7,700 | **1,000** | < 2ms |
|
||
| Window | 3,705 | 3,000 | **1,500** | < 2ms |
|
||
| INSERT | 4,319 | 4,000 | **3,000** | < 4ms |
|
||
| COUNT | 1,065 | 765 | **500** | < 1ms |
|
||
| Complex | 18,395 | 14,000 | **5,000** | < 8ms |
|
||
|
||
---
|
||
|
||
## 검증 기준
|
||
|
||
1. FiveSql2 test_sql1999: **43/43 ALL PASS** (회귀 없음)
|
||
2. FiveSql2 test_basic_sql: **15/15 ALL PASS**
|
||
3. 벤치마크: Phase A → 전체 평균 **-25%**, Phase B → 전체 평균 **-60%**
|
||
4. Go test: **ALL PASS**
|
||
|
||
---
|
||
|
||
## Appendix: 파일별 변경 맵
|
||
|
||
```
|
||
_FiveSql2/src/
|
||
├── TFiveSQL.prg ← A1: plan cache
|
||
├── TSqlExecutor.prg ← A4: dbSelectArea, B1: column binding, B2: CTE in-mem
|
||
├── TSqlExpr.prg ← A5: type fast-path, C1: register eval
|
||
├── TSqlAgg.prg ← A2: FindColIdx cache, A3: SqlCoerceNum dedup
|
||
├── TSqlSort.prg ← A2: FindColIdx cache, B3: sort key materialization
|
||
├── TSqlFunc.prg ← helper optimizations
|
||
├── TSqlLexer.prg ← (A1에서 캐시되므로 변경 불필요)
|
||
├── TSqlParser2.prg ← (A1에서 캐시되므로 변경 불필요)
|
||
└── TSqlDDL.prg ← (DML/DDL, 우선순위 낮음)
|
||
|
||
hbrdd/dbf/
|
||
└── dbf.go ← C2: GetRecord batch read
|
||
|
||
hbrtl/
|
||
└── database.go ← C2: dbGetRecord RTL
|
||
```
|
||
|
||
*"Premature optimization is the root of all evil,
|
||
but late optimization is the root of all slowness." — SQLite source comment*
|