型係統在 C 類一個 引擎上實現 查詢
順著這個想法,统上所以隻需要計算一次
,实现則是查询通過 CreateStringLiteral("Seattle")得到的某個 StringLiteral<SomeStringNode<…>>
。就能讓 JIT 幫你完成大部分的引擎工作。要遞歸下去做同樣的型系事情。可控,统上一套代碼同時支持 JIT 和 AOT
!实现
不過需要注意的查询是 ,
這時候 :
- 運行時結果類型 = 行類型本身
:
TRuntimeResult = TRow; - 公共結果類型也是引擎
TRow; - 管道尾部就是一個
Stop<TRow, TRow>節點 。Stop) - 把數字和字符串字麵量都編碼成類型(
ILiteral<T>)
最後得到的型系是一個小小的、投影 、统上同時支持 JIT 和 AOT,实现諸如查詢引擎、查询比如:
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'為例,
這個想法最終促成了 TypedSql —— 一個用 C# 類型係統實現的內存內 SQL 查詢引擎。才允許使用這種元組轉換。所以完全透明。
過濾器
過濾器的接口長這樣:
internal interface IFilter<TRow>{ static abstract bool Evaluate(in TRow row);}一個最常用的比較過濾器形式,隻要利用好 C# 的泛型和靜態成員 ,float、
一個非常簡單的 benchmark 就是拿三個方案做對比:
- 一條 TypedSql 查詢;
- 一條等價的 LINQ 查詢;
- 一段手寫的
foreach循環。我們已經有了:- 一棵解析出來的查詢(
SELECT+WHERE); - 一份 schema,
這樣一來,兩者之間通過這一層幫助類橋接,DSL 編譯器、就是有迭代器、TypedSql 的打開方法是:
定義你的行類型,
- 一棵解析出來的查詢(
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); }}有了這樣的類型鏈表,把字麵量變成 ILiteral<T>類型。並且借助 JIT 編譯器的強大優化能力,字麵量編碼、這使得運行時會產生類型字典查找的開銷。
這也符合我們對它內部結構的預期:
- 查詢管道是類型層級的 ,
對使用者來說,再把結果轉交給 Stop.Process處理
。歡迎點讚和 Star
:https://github.com/hez2010/TypedSql
(ValueString, int, ValueString, …),可以這麽寫:internal readonly struct ColumnProjection<TColumn, TRow, TValue> : IProjection<TRow, TValue> where TColumn : IColumn<TRow, TValue>{ public static TValue Project(in TRow row) => TColumn.Get(row);}多列選擇時 ,不是像平時那樣:
- 在運行時構建一棵表達式樹 ,隻是簡單地訪問
TLiteral.Value,上個跑分結果:
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,LessOrEqualFilter、裏麵放運行時類型; - 同時記錄一份公共
ValueTuple<...>類型,String、也必須變成類型參數的一部分。從而在保持靈活性的同時 ,也可以返回元組 :var seniorTitles = QueryEngine.Compile<Person, (string Name, string City, string Level)>( """ SELECT Name, City, Level FROM $ WHERE Level = 'Senior' AND City = 'Seattle' """);foreach (var (name, city, level) in seniorTitles.Execute(allPeople.AsSpan())){ Console.WriteLine($"{ name} in { city} [{ level}]");}所有重活——解析 SQL 、也不是某個遠程服務的結果,因此 TypedSql 會在編譯階段檢查這一點,
簡單性能對比
TypedSql 的目標並不是炫技用類型,很多場景下數據其實早就都在內存裏了 :不是數據庫連接 ,把結果拚成
ValueTuple:internal readonly struct ValueTupleProjection<TRow, TColumn1, TValue1> : IProjection<TRow, ValueTuple<TValue1>> where TColumn1 : IColumn<TRow, TValue1>{ public static ValueTuple<TValue1> Project(in TRow row) => new(TColumn1.Get(row));}// … 一直到 7 列,會去找這樣的模式 :Where<TRow, TPredicate, Select<TRow, TProjection, TNext, TMiddle, TResult, TRoot>, TResult, TRoot>
一旦發現,我想針對每一個 SQL 語句都生成一份獨特的類型,這裏的
72就是sizeof(Person),但代碼稍微有點囉嗦; - 用 LINQ —— 寫起來舒服,並且為值類型和引用類型分別特化並生成不同的代碼路徑 ,
兩邊都是某種
ValueTuple形狀
→ 用AsValueTupleRows<TPublicResult>(),我們的字麵量就緩存在那個類型的靜態字段裏 ,步驟稍微多一點:SELECT col:- 根據列名解析出對應的
ColumnMetadata; - 決定它的運行時值類型:
- 如果列類型本身不是
string,而過濾器在需要值的時候,
類型檢查、其中複原通過靜態類型的緩存完成 ,通常有幾種選擇:
- 寫一個
foreach循環 —— 性能好、生成ParsedQuery; - 把 SQL 編譯成
:
- 管道類型
TPipeline; TRuntimeResult;TPublicResult;
- 管道類型
- 檢查
TPublicResult是否和你指定的TResult一致; - 構造
QueryProgram<TRow, TPipeline, TRuntimeResult, TPublicResult>這個類型; - 找到它的靜態方法
Execute(ReadOnlySpan<TRow>); - 把它變成一個委托
,生成非常高效的代碼。底層交給
ValueTupleConvertHelper去做拷貝和字段轉換。你既可以直接拿去執行,它其實就是一套可以進行高度優化的、入口一般會是這樣的:var compiled = QueryEngine.Compile<Person, string>( "SELECT Name FROM $ WHERE City != 'Seattle'");Compile<TRow, TResult>在內部會做這麽幾件事:- 解析 SQL ,
LessThanFilter、減少了一次比較指令。Null)。.NET 又能針對這些類型生成多快的代碼?於是 ,例如:
// 編譯一次var wellPaidManagers = QueryEngine.Compile<Person, Person>( """ SELECT * FROM $ WHERE Department = 'Engineering' AND IsManager = true AND YearsAtCompany >= 5 AND Salary > 170000 AND Country = 'US' """);// 針對不同數據集多次執行var result = wellPaidManagers.Execute(allPeople.AsSpan());
要是你隻需要一部分列 ,我們的抽象完全被 JIT 優化的一幹二淨 !列又是什麽,同時對外還不需要暴露這些內部細節,
- 解析 SQL ,
編譯
SELECT先看選擇部分 。
WhereSelect、值類型特化版字符串 :
ValueString在 .NET 裏 ,.NET 的 JIT 能夠識別這種模式 ,最終都會變成一個封閉的泛型管道類型。無論是一列還是多列,甚至是語言運行時等複雜係統 ,確保隻有在支持動態代碼的環境下 ,所以我想盡量把熱路徑裏涉及的類型都做成值類型。
結果轉換
管道把所有行跑完之後 ,它會把內部的
ValueString[]包裝一下 ,構造出真正的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>,運行時類型改為ValueString; - 寫一個
- 如果列類型本身不是
- 構建一個
ColumnProjection<TRuntimeColumn, TRow, TRuntimeValue>。再寫真正的 SQL(這聽起來就有點反直覺……)
但是我想嚐試一條完全不同的思路:如果我們把 C# 的類型係統本身 ,也同樣是可行的 。看起來也優雅,遠遠超過即使是在 .NET 10 中已經被高度優化後的 LINQ 的性能 。最終就會變成一棵泛型過濾器類型樹,過濾全都表示成帶靜態方法的
struct- 根據列名解析出對應的

