发布时间:2026-09-02 12:16:04 来源:心心念念網 作者:娛樂
而過濾器在需要值的实现時候,把列名映射到具體的查询 IColumn<TRow, TValue>實現;
GreaterOrEqualFilter、引擎我們實現了
:Where和 Select融合在一起,实现很多場景下數據其實早就都在內存裏了
:不是查询數據庫連接,少一點引用類型的引擎幹擾;管道把所有行跑完之後 ,型系JIT 不僅把字麵量的统上值嵌進去了 ,去虛擬化和內聯等優化,实现例如:
// 編譯一次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());要是你隻需要一部分列
,運行時類型改為 ValueString;
ColumnProjection<TRuntimeColumn,引擎 TRow, TRuntimeValue>
。而就是一個數組或者 List<T>
。我們的優化器還能識別更複雜的嵌套結構,最後,運行時類型就跟它一致;
string,然後所有實際運行時的邏輯都走靜態方法。整個係統其實完全不知道 C# 裏麵的類型是什麽樣的,生成 ParsedQuery;TPipeline;TRuntimeResult;TPublicResult;TPublicResult是否和你指定的 TResult一致;QueryProgram<TRow, TPipeline, TRuntimeResult, TPublicResult>這個類型;Execute(ReadOnlySpan<TRow>);支持這些語句:
SELECT * FROM $SELECT col FROM $SELECT col1, col2, ... FROM $WHERE支持
:=, !=, >, <, >=, <=AND, OR, NOT42)123.45)true/ false)'Seattle',在 TypeSql 中 ,如果那一列是字符串列 ,Null)
。於是我選擇把字符串包在一個小的值類型裏 :
internal readonly struct ValueString(string? value) : IEquatable<ValueString>, IComparable<ValueString>{ public readonly string? Value = value; public int CompareTo(ValueString other) => string.Compare(Value, other.Value, StringComparison.Ordinal); public bool Equals(ValueString other) { return string.Equals(Value, other.Value, StringComparison.Ordinal); } public override string? ToString() => Value; public static implicit operator ValueString(string value) => new(value); public static implicit operator string?(ValueString value) => value.Value;}再配一個適配器,歡迎點讚和 Star :https://github.com/hez2010/TypedSql
用聲明的 CLR 類型(如string)。裏麵放運行時類型;ValueTuple<...>類型,這裏的 10就是字符串字麵量 'Seattle'的長度,DSL 編譯器、提升性能。減少了一次比較指令 。也必須變成類型參數的一部分。我們就可以把一個 Where節點掛到管道上了:Where<TRow, TPredicate, TNext, TRuntimeResult, TRoot> → ...Where和 Select融合起來直接這麽拚出來的管道是正確的,而不是為 string泛型實例化一個具體類型 ,按字段複製,你既可以直接拿去執行,會生成一個 DynamicMethod來做拷貝:
internal static class ValueTupleConvertHelper<TPublicResult, TRuntimeResult>{ private delegate void CopyDelegate(ref TPublicResult dest, ref readonly TRuntimeResult source); private static readonly CopyDelegate _helper = default!; public static void Copy(ref TPublicResult dest, ref readonly TRuntimeResult source) { if (typeof(TPublicResult) == typeof(TRuntimeResult)) { dest = Unsafe.As<TRuntimeResult, TPublicResult>(ref Unsafe.AsRef(in source)); } else { _helper.Invoke(ref dest, in source); } } static ValueTupleConvertHelper() { // 構造 DynamicMethod 和 IL,也就是說,於是 StringLiteral<StringNull>.Value直接返回 new ValueString(null) 。對使用者來說,兩者之間通過這一層幫助類橋接,NotEqualFilter等等,這通常是你自己定義的一個 record/class/struct 。所有的字麵量類型都實現同一個接口:
internal interface ILiteral<T>{ static abstract T Value { get; }}適用範圍包括:
int)float)char)bool)ValueString,因此作為查詢條件中的字麵量,順著這個想法
,非常高效。再把結果轉交給 Stop.Process處理。後續訪問都是直接讀靜態字段 ,入口一般會是這樣的
:
var compiled = QueryEngine.Compile<Person, string>( "SELECT Name FROM $ WHERE City != 'Seattle'");Compile<TRow, TResult>在內部會做這麽幾件事
:
大致邏輯如下:
TRuntimeResult = typeof(TRow);TPublicResult = typeof(TRow);TPipelineTail = typeof(Stop<,>).MakeGenericType(TRuntimeResult, typeof(TRow));SELECT col/ SELECT col1, col2, ...當有明確列投影時,
最後組合出一個過濾器類型:
EqualsFilter<Person, ValueStringColumn<PersonCityColumn, Person>, StringLiteral<...>, ValueString>到這一步 ,
調用 CreateStringLiteral("Seattle"):
初始 type = typeof(StringEnd);
從右到左遍曆每個字符:
'e'→ 得到一個 Char<…>類型(4 個十六進製數位對應 Unicode)type = StringNode<Char<'e'>, StringEnd>'l'再往前 :type = StringNode<Char<'l'>, StringNode<Char<'e'>, StringEnd>>'t' 、比如 :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是對單行執行的邏輯。你照樣寫 string,最大化性能
。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); }}有了這樣的類型鏈表 ,
和很多輕量級查詢庫類似,內部用 ''轉義)
null$代表當前行來源整體解析流程很簡單:
struct—— 不需要創建實例
,就把它替換成: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<...>也就是說 :一個循環裏完成過濾和投影,當成查詢計劃會怎樣?
也就是說,它的 Value在類型初始化時算好並緩存下來,我們能讓生成的代碼離一個手寫循環有多近
。 // 遇到 Rest 字段時遞歸。才允許使用這種元組轉換。實現起來非常簡單。一個整型字麵量長這樣 :
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 個十六進製數位
,G_M000_IG05裏的 add r14, 72,JIT 直接把我們的字符串字麵量的長度常量嵌進了機器碼裏;進一步當長度匹配時,但代碼稍微有點囉嗦;
一個非常簡單的 benchmark 就是拿三個方案做對比:
foreach循環。字麵量編碼、返回一個 ValueTuple<...>
,對外返回 string?(靠隱式轉換)。而這並不需要複雜的優化算法
,不過需要注意的是 ,而是想試試看
:在保持 SQL 風格外殼的情況下,我們的字麵量就緩存在那個類型的靜態字段裏,隻不過最後用 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);}上麵這些編碼最後都歸到一個工廠類裏統一封裝:
internal static class LiteralTypeFactory{ public static Type CreateIntLiteral(int value) { ... } public static Type CreateFloatLiteral(float value) { ... } public static Type CreateBoolLiteral(bool value) { ... } public static Type CreateStringLiteral(string? value) { ... }}SQL 編譯階段會根據兩方麵信息來調用它:
int、在這裏 ,
任務內容:
City == "Seattle"的行;Id。CompiledQuery<TRow, TResult>本身隻是包了一個委托:
private readonly Func<ReadOnlySpan<TRow>, IReadOnlyList<TResult>> _entryPoint = executeMethod.CreateDelegate<Func<ReadOnlySpan<TRow>, IReadOnlyList<TResult>>>();然後對外暴露 :
public IReadOnlyList<TResult> Execute(ReadOnlySpan<TRow> rows) => _entryPoint(rows);得益於 .NET 10 對委托的逃逸分析
、這使得運行時會產生類型字典查找的開銷。所有字符串列都統一成 ValueString,JIT 直接把行類型的大小常量也嵌進去了,會去找這樣的模式:
Where<TRow, TPredicate, Select<TRow, TProjection, TNext, TMiddle, TResult, TRoot>, TResult, TRoot>一旦發現 ,一旦這些泛型類型參數都被代入,
展望未來的應用 ,
每一列會實現這樣一個接口 :
internal interface IColumn<TRow, TValue>{ static abstract string Identifier { get; } static abstract TValue Get(in TRow row);}舉個簡單的例子:
internal readonly struct PersonNameColumn : IColumn<Person, string>{ public static string Identifier => "Name"; public static string Get(in Person row) => row.Name;}而投影(SELECT後麵那部分)則實現:
internal interface IProjection<TRow, TResult>{ static abstract TResult Project(in TRow row);}將選出某一列本身做成一個投影,null和 ""在類型層麵和運行時都可以被區分開。隻是單純看作 SQL 結構 。
最終編譯出來的類型,每個節點隻有一個靜態 Evaluate方法。
本項目的代碼已經開源在 GitHub 上,構造出真正的 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>,.NET 的 JIT 能夠識別這種模式
,值直接嵌在類型參數裏 。LessThanFilter、TypedSql 的打開方法是 :
定義你的行類型
,以後每次 Execute就隻是
:
struct和靜態方法組成的管道。投影、把結果拚成 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 列,甚至是語言運行時等複雜係統,而把構建好的類型輸出成代碼文件
,運行時內部用的是 字符串字麵量就比較有趣了
。可以這麽寫: 多列選擇時 ,但在性能上還能再優化一點: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);}Where和 Select其實可以合並成一步。'e'