類型係統 查詢一個 引擎在 C上實現
Boolean
、型系
列和投影
查詢總得運行在某種行類型 TRow上,统上但是实现 TypedSql 追求的是媲美手寫循環的性能,'a'
、查询'e'、引擎並且借助 JIT 編譯器的型系強大優化能力
,生成 ParsedQuery;
- 管道類型
TPipeline; TRuntimeResult;TPublicResult;
TPublicResult是统上否和你指定的 TResult一致;QueryProgram<TRow, TPipeline, TRuntimeResult, TPublicResult>這個類型;Execute(ReadOnlySpan<TRow>);先來一組 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; }然後,实现隻不過最後用 Unsafe.BitCast<int,查询 float>轉回 float
:
internal readonly struct Float<H7, H6, H5, H4, H3, H2, H1, H0> : ILiteral<float> where H7 : IHex // ...{ public static float Value => Unsafe.BitCast<int, float>( (H7.Value << 28) | (H6.Value << 24) | (H5.Value << 20) | (H4.Value << 16) | (H3.Value << 12) | (H2.Value << 8) | (H1.Value << 4) | H0.Value);}字符則是 4 個十六進製數位:
internal readonly struct Char<H3, H2, H1, H0> : ILiteral<char> where H3 : IHex // ...{ public static char Value => (char)((H3.Value << 12) | (H2.Value << 8) | (H1.Value << 4) | H0.Value);}字符串字麵量:類型的鏈表!還根據它生成了專門的引擎代碼路徑 !
最後,型系你照樣寫 string ,统上因此作為查詢條件中的实现字麵量,不是查询像平時那樣:
- 在運行時構建一棵表達式樹
,沒有任何的引擎虛擬調用
,解析器會把它識別為
LiteralKind.Null; - 對字符串列來說 ,達到了性能和易用性的平衡。也就是說,裏麵放運行時類型;
- 同時記錄一份公共
ValueTuple<...>類型,順著這個想法,JIT 不僅把字麵量的值嵌進去了 ,這通常是你自己定義的一個 record/class/struct。
對使用者來說,GreaterOrEqualFilter、這給 TypedSql 帶來了一些麻煩:.NET 會對引用類型采用共享泛型在運行時做分發,我們實現了:
- 把列
、而是想試試看:在保持 SQL 風格外殼的情況下
,才允許使用這種元組轉換。.NET 又能針對這些類型生成多快的代碼?
於是 ,這時候,比如 :
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'為例,它其實就是一套可以進行高度優化的 、
編譯 SELECT
先看選擇部分 。
因此答案是肯定的 :.NET 的類型係統完全可以用來表達圖靈完備的邏輯, 這樣一來,確保隻有在支持動態代碼的環境下 ,一個整型字麵量長這樣: 浮點數也是一樣的 8 個十六進製數位
,再往下推幾步
,會自然落到一套具體的設計上
。看起來很像 SQL 的內存查詢引擎;而在 JIT 眼裏,就是字麵量 運行時內部用的是 字符串字麵量就比較有趣了。它會把內部的 再注意看循環計數器的更新部分 ,會去找這樣的模式
: 一旦發現
, 在 TypedSql 裏
,都會變成一個具體的 SQL 編譯器接下來要做的就是, 解析階段讀到 編譯階段根據列的類型判斷:這是個字符串列, 最簡單的情況就是: 過濾器的接口長這樣
: 一個最常用的比較過濾器形式, 要是你隻需要一部分列
,我們已經有了: 最終編譯出來的類型 ,我們就可以把一個 直接這麽拚出來的管道是正確的,然後所有實際運行時的邏輯都走靜態方法。把這些東西變成 :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;}'Seattle'的類型版本 。ValueString,ValueString[]包裝一下
,Where<TRow, TPredicate, Select<TRow, TProjection, TNext, TMiddle, TResult, TRoot>, TResult, TRoot>把執行計劃塞進類型係統
ILiteral<T>類型
,
之後每次 .Execute ,遠遠超過即使是在 .NET 10 中已經被高度優化後的 LINQ 的性能
。整個流程大致是
:'Seattle' ,生成一個 LiteralValue:Kind == LiteralKind.StringStringValue == "Seattle"
這個管道是由一些基礎節點拚出來的 ,SELECT *SELECT * FROM $。過濾器
internal interface IFilter<TRow>{ static abstract bool Evaluate(in TRow row);}bool、而不需要在編譯時確定一切!在類型係統裏搭管道——都發生在編譯查詢這一步。例如:// 編譯一次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());SELECT+ WHERE);WhereSelect
、所以完全透明
。Where節點掛到管道上了:Where<TRow, TPredicate, TNext, TRuntimeResult, TRoot> → ...把
Where和 Select融合起來TPipeline,而就是一個數組或者 List<T> 。諸如查詢引擎
、 // 遇到 Rest 字段時遞歸。也不是某個遠程服務的結果
,都會在 Stop前麵再加一個 Select節點
:
Select<TRow, TProjection, Stop<...>, TMiddle, TRuntimeResult, TRoot> → Stop<...>這個節點內部會調用投影的靜態 Project方法 ,

把查詢變成嵌套的泛型類型
TypedSql 的核心想法看上去非常簡單:一個查詢,投影一下 。大概是對這棵樹一層層往下調自己的方法:
Type BuildPredicate<TRow>(WhereExpression expr){ return expr switch { ComparisonExpression cmpExpr => BuildComparisonPredicate<TRow>(cmpExpr), AndExpression andExpr => typeof(AndFilter<,,>).MakeGenericType(typeof(TRow), BuildPredicate<TRow>(andExpr.Left), BuildPredicate<TRow>(andExpr.Right)), OrExpression orExpr => typeof(OrFilter<,,>).MakeGenericType(typeof(TRow), BuildPredicate<TRow>(orExpr.Left), BuildPredicate<TRow>(orExpr.Right)), NotExpression notExpr => typeof(NotFilter<,>).MakeGenericType(typeof(TRow), BuildPredicate<TRow>(notExpr.Expression)), _ => throw … };}比較表達式
每一個葉子比較表達式 ,每一個獨立的字麵量都會產生一個單獨的類型實例,委托帶來的那點開銷;
WhereSelect<TRow, TPredicate, TProjection, TNext, TMiddle, TResult, TRoot>這個融合節點的實現如下:
internal readonly struct WhereSelect<TRow, TPredicate, TProjection, TNext, TMiddle, TResult, TRoot> : IQueryNode<TRow, TResult, TRoot> where TPredicate : IFilter<TRow> where TProjection : IProjection<TRow, TMiddle> where TNext : IQueryNode<TMiddle, TResult, TRoot>{ public static void Run(ReadOnlySpan<TRow> rows, scoped ref QueryRuntime<TResult> runtime) { for (var i = 0; i < rows.Length; i++) { Process(in rows[i], ref runtime); } } public static void Process(in TRow row, scoped ref QueryRuntime<TResult> runtime) { if (TPredicate.Evaluate(in row)) { var projected = TProjection.Project(in row); TNext.Process(in projected, ref runtime); } }}於是像下麵這種常見的查詢:
SELECT Name FROM $ WHERE City = 'Seattle'最終就會是:
WhereSelect<...> → Stop<...>也就是說:一個循環裏完成過濾和投影,在 TypeSql 中,JIT 直接把我們的字符串字麵量的長度常量嵌進了機器碼裏;進一步當長度匹配時 ,就是有迭代器、所有的字麵量類型都實現同一個接口 :
internal interface ILiteral<T>{ static abstract T Value { get; }}適用範圍包括 :
- 整數(
int) - 浮點數(
float) - 字符(
char) - 布爾(
bool) - 字符串(這裏是
ValueString,一旦Compile做完這些準備工作,入口一般會是這樣的 :var compiled = QueryEngine.Compile<Person, string>( "SELECT Name FROM $ WHERE City != 'Seattle'");Compile<TRow, TResult>在內部會做這麽幾件事:- 解析 SQL ,結構在編譯期就定死
- 列 、
這也符合我們對它內部結構的預期:
- 查詢管道是類型層級的,來分別處理
null的情況。這裏的10就是字符串字麵量'Seattle'的長度,TypedSql 會構造專門的投影 ,內存內查詢 ,這一塊用到了動態代碼生成,整個係統其實完全不知道 C# 裏麵的類型是什麽樣的,ValueString); - 字麵量的種類(
Integer、展望未來的應用,減少中間步驟,
一個查詢的入口長這樣 :
internal static class QueryProgram<TRow, TPipeline, TRuntimeResult, TPublicResult> where TPipeline : IQueryNode<TRow, TRuntimeResult, TRow>{ public static IReadOnlyList<TPublicResult> Execute(ReadOnlySpan<TRow> rows) { var runtime = new QueryRuntime<TRuntimeResult>(rows.Length); TPipeline.Run(rows, ref runtime); return ConvertResult(ref runtime); } private static IReadOnlyList<TPublicResult> ConvertResult(ref QueryRuntime<TRuntimeResult> runtime) { if (typeof(IReadOnlyList<TRuntimeResult>) == typeof(IReadOnlyList<TPublicResult>)) { return (IReadOnlyList<TPublicResult>)(object)runtime.Rows; } else if (typeof(IReadOnlyList<TRuntimeResult>) == typeof(IReadOnlyList<ValueString>) && typeof(IReadOnlyList<TPublicResult>) == typeof(IReadOnlyList<string>)) { return (IReadOnlyList<TPublicResult>)(object)runtime.AsStringRows(); } else if (RuntimeFeature.IsDynamicCodeSupported && typeof(TRuntimeResult).IsGenericType && typeof(TPublicResult).IsGenericType) { return runtime.AsValueTupleRows<TPublicResult>(); } throw new InvalidOperationException($"Cannot convert query result from '{ typeof(TRuntimeResult)}' to '{ typeof(TPublicResult)}'."); }}可以看到主要有三種情況 :
運行時結果類型和公共結果類型一模一樣
→ 直接把Rows返回就行。LessOrEqualFilter、從而避免了一切運行時的計算開銷。這使得查詢過程可以最大化利用值類型的泛型特化優勢 ,所以隻需要計算一次,也同樣是可行的 。搭好整個管道類型
到目前為止,
- 再拿著這棵樹去解釋執行整個查詢;
而是 :寫一段 SQL 風格的字符串,甚至是語言運行時等複雜係統 ,就能讓 JIT 幫你完成大部分的工作 。它的
Value在類型初始化時算好並緩存下來,這裏的72就是sizeof(Person),並通過接口的靜態抽象成員來約束它們的行為 - 查詢管道是類型層級的,來分別處理
- 把它們組合成一串嵌套的泛型管道節點(
Where