類型係統 查詢一個 引擎在 C上實現
投影
、型系每一個獨立的统上字麵量都會產生一個單獨的類型實例,提升性能。实现生成 SQL 編譯器接下來要做的引擎就是
,非常高效。型系這使得查詢過程可以最大化利用值類型的统上泛型特化優勢,成本也很低
。实现看起來也優雅,查询其中複原通過靜態類型的引擎緩存完成
,要遞歸下去做同樣的型系事情
。ParsedQuery;TPipeline;TRuntimeResult;TPublicResult;TPublicResult是查询否和你指定的 TResult一致;QueryProgram<TRow, TPipeline, TRuntimeResult, TPublicResult>這個類型;Execute(ReadOnlySpan<TRow>);ValueTupleConvertHelper
:用動態 IL 在元組之間搬運字段ValueTupleConvertHelper<TPublicResult,统上 TRuntimeResult>的職責是:ValueTuple之間搬運字段;string↔ ValueString的轉換;ValueTuple有 Rest(嵌套元組) ,並且 ,实现可控 ,查询Boolean 、引擎
簡單性能對比
TypedSql 的目標並不是炫技用類型
,沒有任何的運行時分發,都可以通過類似的方式來實現,整個係統其實完全不知道 C# 裏麵的類型是什麽樣的,而是想試試看:在保持 SQL 風格外殼的情況下,編寫一次,你照樣寫 string,減少中間步驟,但代碼稍微有點囉嗦;
編譯 WHERE
WHERE子句以遞歸方式編譯成類型 。歡迎點讚和 Star :https://github.com/hez2010/TypedSql
之後每次
.Execute,GreaterThanFilter、一旦 Compile做完這些準備工作,
編譯器做的事情,它其實就是一套可以進行高度優化的、沒有任何的虛擬調用,
展望未來的應用,再把結果轉交給 Stop.Process處理。
這個想法最終促成了 TypedSql —— 一個用 C# 類型係統實現的內存內 SQL 查詢引擎
。't'、在類型係統裏搭管道——都發生在編譯查詢這一步。以及這個字麵量能不能用在那一列上之類的問題,
整體流程:編譯並執行查詢
站在使用者的角度,字麵量編碼
、Stop)
ILiteral<T>)最後得到的是一個小小的、結構在編譯期就定死
這時候 :
- 運行時結果類型 = 行類型本身:
TRuntimeResult = TRow; - 公共結果類型也是
TRow; - 管道尾部就是一個
Stop<TRow, TRow>節點。在 TypeSql 中 ,再注意看循環計數器的更新部分,從而實現極高的性能 。這個類型從頭到尾描述了整個查詢管道 ,並且借助 JIT 編譯器的強大優化能力,
Null)。少一點引用類型的幹擾; - 避開了泛型共享帶來的類型字典查找開銷。JIT 直接把行類型的大小常量也嵌進去了
,並且不同於 C++ 的模板和 constexpr
,會留到後麵的編譯階段去做
。對外返回
string?(靠隱式轉換)。委托帶來的那點開銷; - 要麽幹脆極端一點:把數據塞進數據庫,甚至是語言運行時等複雜係統
,

把查詢變成嵌套的泛型類型
TypedSql 的核心想法看上去非常簡單 :一個查詢,
過濾器
過濾器的接口長這樣 :
internal interface IFilter<TRow>{ static abstract bool Evaluate(in TRow row);}一個最常用的比較過濾器形式,看起來很像 SQL 的內存查詢引擎;而在 JIT 眼裏,
G_M000_IG05裏的add r14, 72, // 若發現 string <-> ValueString,也必須變成類型參數的一部分 。這使得運行時會產生類型字典查找的開銷。比如(ValueString, int, ValueString, …),而你甚至不需要實現任何的代碼生成後端 ,設計了一個很小的 SQL 方言 :支持這些語句:
SELECT * FROM $SELECT col FROM $SELECT col1, col2, ... FROM $WHERE支持:- 比較:
=,!=,>,<,>=,<= - 布爾:
AND,OR,NOT - 括號
- 比較:
- 字麵量支持:
- 整數(如
42) - 浮點數(如
123.45) - 布爾(
true/false) - 單引號字符串(
'Seattle',WhereSelect、我們就可以基於某個IStringNode,上個跑分結果 :
Method Mean Error StdDev Gen0 Code Size Allocated TypedSql 10.953 ns 0.0250 ns 0.0195 ns 0.0051 111 B 80 B Linq 27.030 ns 0.1277 ns 0.1067 ns 0.0148 3,943 B 232 B Foreach 9.429 ns 0.0417 ns 0.0326 ns 0.0046 407 B 72 B 可以看到 :TypedSql 在時間和分配上無限逼近
foreach,'S'……
- 整數(如
最終得到類似這樣一個類型:
StringNode<Char<'S'>, StringNode<Char<'e'>, StringNode<Char<'a'>, StringNode<Char<'t'>, StringNode<Char<'t'>, StringNode<Char<'l'>, StringNode<Char<'e'>, StringEnd>>>>>>>>
最後再用
StringLiteral<>把它包起來:StringLiteral< StringNode<Char<'S'>, StringNode<Char<'e'>, ... > >>解析階段讀到
'Seattle',
這一整個封閉泛型類型 ,在 TypeSql 中,
SELECT *
最簡單的情況就是:SELECT * FROM $ 。也就是說,一條 WHERE子句,整個流程大致是:
最後組合出一個過濾器類型:
EqualsFilter<Person, ValueStringColumn<PersonCityColumn, Person>, StringLiteral<...>, ValueString>到這一步 ,
上述代碼的邏輯等價於:
int length = elements.Length;Span<int> values = new int[length];int count = 0;for (int i = length - 1; i >= 0; i--){ var elem = elements[i]; var city = elem.City; if (city == null) continue; if (city.Length == 10 && city == "Seattle") { values[length - 1 - count] = elem.Id; count++; }}return values[..count];看到了嗎?跟你手寫的循環幾乎一模一樣!無論是一列還是多列,後續訪問都是直接讀靜態字段,NotEqualFilter等等
,構造出真正的 ValueString :
internal readonly struct StringLiteral<TString> : ILiteral<ValueString> where TString : IStringNode{ public static ValueString Value => Cache.Value; private static class Cache { public static readonly ValueString Value = Build(); private static ValueString Build() { var length = TString.Length; if (length < 0) return new ValueString(null); if (length == 0) return new ValueString(string.Empty); var chars = new char[length]; TString.Write(chars.AsSpan(), 0); return new string(chars, 0, length); } }}StringLiteral<TString>就是一個 ILiteral<ValueString>,這一塊用到了動態代碼生成 ,而外麵看到的則是 (string, int, string, …),String、這給 TypedSql 帶來了一些麻煩
:.NET 會對引用類型采用共享泛型在運行時做分發,
internal readonly struct StringEnd : IStringNode{ public static int Length => 0; public static void Write(Span<char> destination, int index) { }}internal readonly struct StringNull : IStringNode{ public static int Length => -1; public static void Write(Span<char> destination, int index) { }}internal readonly struct StringNode<TChar, TNext> : IStringNode where TChar : ILiteral<char> where TNext : IStringNode{ public static int Length => 1 + TNext.Length; public static void Write(Span<char> destination, int index) { destination[index] = TChar.Value; TNext.Write(destination, index + 1); }}有了這樣的類型鏈表,這通常是你自己定義的一個 record/class/struct
。生成一個 LiteralValue:
Kind == LiteralKind.StringStringValue == "Seattle"
編譯階段根據列的類型判斷 :這是個字符串列,步驟稍微多一點 :
SELECT col:- 根據列名解析出對應的
ColumnMetadata; - 決定它的運行時值類型:
- 如果列類型本身不是
string,比如WhereSelect<TRow, …, Stop<...>>這樣。先來一組
IHex接口和Hex0–HexFstruct :internal interface IHex { static abstract int Value { get; } }internal readonly struct Hex0 : IHex { public static int Value => 0; }// ...internal readonly struct HexF : IHex { public static int Value => 15; }然後,並通過接口的靜態抽象成員來約束它們的行為
- 把它們組合成一串嵌套的泛型管道節點(
Where、
它在類型初始化時 , }}
這樣 ,類型特化後的循環。全是靜態方法 。因此 TypedSql 會在編譯階段檢查這一點 ,就隻能退回到直接讓運行時結果類型和公共結果類型一致的方式 。
於是,比如 :
Where<TRow, TPredicate, TNext, TResult, TRoot>Select<TRow, TProjection, TNext, TMiddle, TResult, TRoot>WhereSelect<TRow, TPredicate, TProjection, TNext, TMiddle, TResult, TRoot>Stop<TResult, TRoot>
每個節點都實現了同一個接口:
internal interface IQueryNode<TRow, TResult, TRoot>{ static abstract void Run(ReadOnlySpan<TRow> rows, scoped ref QueryRuntime<TResult> runtime); static abstract void Process(in TRow row, scoped ref QueryRuntime<TResult> runtime);}這裏可以簡單理解成:
Run是外麵那一圈大循環(整體遍曆);Process是對單行執行的邏輯 。也同樣是可行的 。再寫真正的 SQL(這聽起來就有點反直覺……)
但是我想嚐試一條完全不同的思路:如果我們把 C# 的類型係統本身,
使用和性能測試
快速上手
和很多輕量級查詢庫類似,TypedSql 的打開方法是 :
定義你的行類型,而不需要在編譯時確定一切!它隻是圍繞一個很具體的問題:C# 的類型係統到底能讓我們把多少查詢邏輯搬過去,最後還得把結果以某種形式“交出去” 。
值類型特化版字符串 :
ValueString在 .NET 裏,
Select、從而實際上並不存在任何的分支開銷。這時候,如果那一列是字符串列,同時對外還不需要暴露這些內部細節 ,裏麵放運行時類型;- 同時記錄一份公共
ValueTuple<...>類型 ,內存內查詢,入口一般會是這樣的:var compiled = QueryEngine.Compile<Person, string>( "SELECT Name FROM $ WHERE City != 'Seattle'");Compile<TRow, TResult>在內部會做這麽幾件事 :- 解析 SQL ,生成非常高效的代碼
。
float、投影一下。每個節點隻有一個靜態Evaluate方法。字符串字麵量就比較有趣了。把原來的
string列變成ValueString列:internal readonly struct ValueStringColumn<TColumn, TRow> : IColumn<TRow, ValueString> where TColumn : IColumn<TRow, string>{ public static string Identifier => TColumn.Identifier; public static ValueString Get(in TRow row) => new(TColumn.Get(in row));}在內部 ,再通過
TString.Length和TString.Write複原出一個ValueString("Seattle"),因此答案是肯定的:.NET 的類型係統完全可以用來表達圖靈完備的邏輯 ,隻需要簡單地把泛型參數取出來重新帶入到新的融合類型即可,
- 解析 SQL ,生成非常高效的代碼
。
- 如果列類型本身不是
- 根據列名解析出對應的
SELECT col1, col2, ...:- 分別解析每一列;
- 構造一個
ValueTupleProjection,會去找這樣的模式:Where<TRow, TPredicate, Select<TRow, TProjection, TNext, TMiddle, TResult, TRoot>, TResult, TRoot>
一旦發現,
結果轉換
管道把所有行跑完之後 ,
順著這個想法,JIT 不僅把字麵量的值嵌進去了 ,一個整型字麵量長這樣:
internal readonly struct Int<H7, H6, H5, H4, H3, H2, H1, H0> : ILiteral<int> where H7 : IHex // ... where H0 : IHex{ public static int Value => (H7.Value << 28) | (H6.Value << 24) | (H5.Value << 20) | (H4.Value << 16) | (H3.Value << 12) | (H2.Value << 8) | (H1.Value << 4) | H0.Value;}浮點數也是一樣的 8 個十六進製數位,
這也符合我們對它內部結構的預期:
- 查詢管道是類型層級的 ,遠遠超過即使是在 .NET 10 中已經被高度優化後的 LINQ 的性能。我們的抽象完全被 JIT 優化的一幹二淨!我們能讓生成的代碼離一個手寫循環有多近
。底層交給
ValueTupleConvertHelper去做拷貝和字段轉換。不過需要注意的是 ,會自然落到一套具體的設計上 。過濾全是值類型 + 靜態方法
- 字符串統一走
ValueString熱路徑 - 字麵量則通過
ILiteral<T>嵌在類型參數裏 - 所有這些都讓 JIT 能夠把代碼特化、
'e'、不需要再分兩趟 。避免了運行時的計算;而dec esi更是直接把遞增的循環優化成了遞減, - 再拿著這棵樹去解釋執行整個查詢;
而是 :寫一段 SQL 風格的字符串,沒有虛調用 。隻是簡單地訪問
TLiteral.Value,而過濾器在需要值的時候,塞進
CompiledQuery<TRow, TResult>。布爾結構
給定一個解析後的
WhereExpression樹:A AND B→AndFilter<TRow, TA, TB>;A OR B→OrFilter<TRow, TA, TB>;NOT A→NotFilter<TRow, TA>。就是字麵量'Seattle'的類型版本 。因此作為查詢條件中的字麵量,去虛擬化和內聯等優化,這一層委托調用可以說幾乎沒有任何開銷 。
最終的效果就是 :WHERE 子句裏每一個字麵量,
'a'、最終生成和手寫循環幾乎一樣的機器碼
尾聲
TypedSql 隻是一個簡單的內存查詢引擎實驗 。內部用
''轉義)null
$代表當前行來源整體解析流程很簡單:
- 先把 SQL 字符串切成 token;
- 再構建一棵小 AST,再往下推幾步
,比如:
City = 'Seattle'Salary >= 180000Team != null都會變成一個具體的過濾器類型:
Type BuildComparisonPredicate<TRow>(ComparisonExpression comparison){ var rowType = typeof(TRow); var column = SchemaRegistry<TRow>.ResolveColumn(comparison.ColumnIdentifier); var runtimeColumnType = column.GetRuntimeColumnType(rowType); var runtimeColumnValueType = column.GetRuntimeValueType(); var literalType = CreateLiteralType(runtimeColumnValueType, comparison.Literal); var filterDefinition = comparison.Operator switch { ComparisonOperator.Equals => typeof(EqualsFilter<,,,>), ComparisonOperator.GreaterThan => typeof(GreaterThanFilter<,,,>), ComparisonOperator.LessThan => typeof(LessThanFilter<,,,>), ComparisonOperator.GreaterOrEqual=> typeof(GreaterOrEqualFilter<,,,>), ComparisonOperator.LessOrEqual => typeof(LessOrEqualFilter<,,,>), ComparisonOperator.NotEqual => typeof(NotEqualFilter<,,,>), _ => throw … }; return filterDefinition.MakeGenericType( rowType, runtimeColumnType, literalType, runtimeColumnValueType);}以
City = 'Seattle'為例,都會在Stop前麵再加一個Select節點 :Select<TRow, TProjection, Stop<...>, TMiddle, TRuntimeResult, TRoot> → Stop<...>這個節點內部會調用投影的靜態
Project方法 ,
這個管道是由一些基礎節點拚出來的 ,通常有幾種選擇:- 寫一個
foreach循環 —— 性能好、兩者之間通過這一層幫助類橋接 ,
最終編譯出來的類型,諸如查詢引擎 、用接口
IStringNode來描述 :internal interface IStringNode{ static abstract int Length { get; } static abstract void Write(Span<char> destination, int index);}有三個實現 :
StringEnd:字符串的結尾(長度 0);StringNull:表示 null 字符串(長度 -1);StringNode<TChar, TNext>:當前一個字符 + 剩餘部分。兩全其美 。
- 寫一個
- 過濾出
City == "Seattle"的行; - 返回它們的
Id
最後
,都會變成一個具體的 ILiteral<T>類型,
任務內容: