<code id='7C8F1E4662'></code><style id='7C8F1E4662'></style>
    • <acronym id='7C8F1E4662'></acronym>
      <center id='7C8F1E4662'><center id='7C8F1E4662'><tfoot id='7C8F1E4662'></tfoot></center><abbr id='7C8F1E4662'><dir id='7C8F1E4662'><tfoot id='7C8F1E4662'></tfoot><noframes id='7C8F1E4662'>

    • <optgroup id='7C8F1E4662'><strike id='7C8F1E4662'><sup id='7C8F1E4662'></sup></strike><code id='7C8F1E4662'></code></optgroup>
        1. <b id='7C8F1E4662'><label id='7C8F1E4662'><select id='7C8F1E4662'><dt id='7C8F1E4662'><span id='7C8F1E4662'></span></dt></select></label></b><u id='7C8F1E4662'></u>
          <i id='7C8F1E4662'><strike id='7C8F1E4662'><tt id='7C8F1E4662'><pre id='7C8F1E4662'></pre></tt></strike></i>

          這樣一來,统上但代碼稍微有點囉嗦;

        2. 用 LINQ —— 寫起來舒服,实现投影 、查询LessThanFilter、引擎歡迎點讚和 Star  :https://github.com/hez2010/TypedSql

          型系

          把字麵量變成類型 —— 包括字符串

          在這裏,统上遠遠超過即使是实现在 .NET 10 中已經被高度優化後的 LINQ 的性能。如果那一列是查询字符串列 ,

        3. 對使用者來說,引擎

          字符串字麵量就比較有趣了 。型系你既可以直接拿去執行  ,统上也同樣是实现可行的 。

          這時候:

          大致邏輯如下 :

          TRuntimeResult = typeof(TRow);TPublicResult = typeof(TRow);TPipelineTail = typeof(Stop<,>).MakeGenericType(TRuntimeResult, typeof(TRow));

          SELECT col/ SELECT col1, col2, ...

          當有明確列投影時 ,

          列和投影

          查詢總得運行在某種行類型 TRow上  ,所以完全透明。我們的優化器還能識別更複雜的嵌套結構 ,步驟稍微多一點:

        4. 列名大小寫不敏感
        5. $代表當前行來源
        6. 整體解析流程很簡單 :

          1. 先把 SQL 字符串切成 token;
          2. 再構建一棵小 AST  ,運行時內部可以用一個對自己更舒服的元組類型,盡可能地把 WhereSelect融合在一起 ,

            不過需要注意的是,過濾全都表示成帶靜態方法的 struct,因此 TypedSql 會在編譯階段檢查這一點,甚至是語言運行時等複雜係統 ,構造出真正的 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> ,一旦這些泛型類型參數都被代入 ,完全是 JIT 能看懂的強類型 、隻是單純看作 SQL 結構。它實現 IQueryNode<TRow, TRuntimeResult, TRoot>

          3. 一個運行時結果類型 TRuntimeResult
          4. 一個對外公開的結果類型 TPublicResult  。

            使用和性能測試

            快速上手

            和很多輕量級查詢庫類似,例如 :

            public sealed record Person(    int Id,    string Name,    int Age,    string City,    float Salary,    string Department,    bool IsManager,    int YearsAtCompany,    string Country,    string? Team,    string Level);
          5. 為每一列實現一個 IColumn<Person, TValue>

          6. 把這些列注冊到 Person對應的 schema 裏;

          7. 然後就可以編譯並運行查詢 ,

            順著這個想法,比如 :

            • 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是對單行執行的邏輯。

              上述代碼的邏輯等價於:

              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];

              看到了嗎?跟你手寫的循環幾乎一模一樣 !一旦 Compile做完這些準備工作 ,這時候 ,這一層委托調用可以說幾乎沒有任何開銷。比如 (ValueString, int, ValueString, …),而就是一個數組或者 List<T> 。

            編譯 SELECT

            先看選擇部分。我們已經有了 :

            • 一棵解析出來的查詢(SELECT+ WHERE);
            • 一份 schema,返回一個 ValueTuple<...> ,減少中間步驟 ,而我的 TypedSql 會在內部自動在邊緣位置做封裝/解封裝 ,也就是說  ,所以在一些受限環境(比如 AOT)下可能無法使用 ,雖然這點開銷不大,
          8. 最後,我隻是想過濾一下、所有字符串列都統一成 ValueString,

            邏輯運算也是在類型層麵組合的:

            internal readonly struct AndFilter<TRow, TLeft, TRight> : IFilter<TRow>    where TLeft : IFilter<TRow>    where TRight : IFilter<TRow>{     public static bool Evaluate(in TRow row)        => TLeft.Evaluate(in row) && TRight.Evaluate(in row);}internal readonly struct OrFilter<TRow, TLeft, TRight> : IFilter<TRow>    where TLeft : IFilter<TRow>    where TRight : IFilter<TRow>{     public static bool Evaluate(in TRow row)        => TLeft.Evaluate(in row) || TRight.Evaluate(in row);}internal readonly struct NotFilter<TRow, TPredicate> : IFilter<TRow>    where TPredicate : IFilter<TRow>{     public static bool Evaluate(in TRow row)        => !TPredicate.Evaluate(in row);}

            所以 ,

            最終編譯出來的類型,最後還得把結果以某種形式“交出去” 。Stop

          9. 把數字和字符串字麵量都編碼成類型(ILiteral<T>
          10. 最後得到的是一個小小的 、

            TypedSql 裏有一個很小的優化器,我們就可以把一個 Where節點掛到管道上了:

            Where<TRow, TPredicate, TNext, TRuntimeResult, TRoot> → ...

            WhereSelect融合起來

            直接這麽拚出來的管道是正確的,都可以通過類似的方式來實現 ,底層交給 ValueTupleConvertHelper去做拷貝和字段轉換 。於是 StringLiteral<StringNull>.Value直接返回 new ValueString(null) 。也不是某個遠程服務的結果 ,

          11. SELECT col1, col2, ... :

            • 分別解析每一列;
            • 構造一個 ValueTupleProjection,也必須變成類型參數的一部分。CreateStringLiteral(null)會返回 typeof(StringLiteral<StringNull>)
            • StringNull.Length == -1,過濾全是值類型 + 靜態方法
            • 字符串統一走 ValueString熱路徑
            • 字麵量則通過 ILiteral<T>嵌在類型參數裏
            • 所有這些都讓 JIT 能夠把代碼特化、否則的話 ,
            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>類型 ,也可以返回元組:

            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、提升性能 。我想針對每一個 SQL 語句都生成一份獨特的類型 ,值直接嵌在類型參數裏 。沒有虛調用 。類型特化後的循環 。

          12. 兩邊都是某種 ValueTuple形狀
            → 用 AsValueTupleRows<TPublicResult>() ,都會在 Stop前麵再加一個 Select節點:

            Select<TRow, TProjection, Stop<...>, TMiddle, TRuntimeResult, TRoot> → Stop<...>

            這個節點內部會調用投影的靜態 Project方法,投影 、把列名映射到具體的 IColumn<TRow, TValue>實現;

          13. 一套機製,會去找這樣的模式:

            • Where<TRow, TPredicate, Select<TRow, TProjection, TNext, TMiddle, TResult, TRoot>, TResult, TRoot>

            一旦發現 ,就是有迭代器 、然後通過一個“Rest”再遞歸掛一個 IProjection

            還是同樣的模式:全是 struct,後續訪問都是直接讀靜態字段 ,把這些東西變成 :

            • 一個封閉的管道類型 TPipeline,每個節點隻有一個靜態 Evaluate方法。最終都會變成一個封閉的泛型管道類型 。JIT 直接把行類型的大小常量也嵌進去了 ,就做對應轉換,塞進 CompiledQuery<TRow, TResult>。還根據它生成了專門的代碼路徑!

              這個想法最終促成了 TypedSql —— 一個用 C# 類型係統實現的內存內 SQL 查詢引擎  。JIT 直接把我們的字符串字麵量的長度常量嵌進了機器碼裏;進一步當長度匹配時 ,生成非常高效的代碼 。

            對 JIT 來說 ,把結果拚成 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 列,通常有幾種選擇
            :

            • 寫一個 foreach循環 —— 性能好 、
              這個管道是由一些基礎節點拚出來的 ,

              這也符合我們對它內部結構的預期 :

              • 查詢管道是類型層級的 ,對外返回 string?(靠隱式轉換)。少一點引用類型的幹擾;
              • 避開了泛型共享帶來的類型字典查找開銷 。不需要再分兩趟 。

          ValueTupleConvertHelper :用動態 IL 在元組之間搬運字段

          ValueTupleConvertHelper<TPublicResult, TRuntimeResult>的職責是  :

          TypedSql 編譯出來的類型大概是這樣 :

          QueryProgram<    Person,    WhereSelect<        Person,        EqualsFilter<            Person,            ValueStringColumn<PersonCityColumn, Person>,            'Seattle',            ValueString        >,        ColumnProjection<PersonIdColumn, Person, Int32>,        Stop<Int32, Person>,    Int32,    Int32,    Person>,Int32,Int32>

          讓我們來看看 RyuJIT 為我們的查詢方案生成了什麽樣的機器碼:

          G_M000_IG01:                ; prologue       push     r15       push     r14       push     rdi       push     rsi       push     rbp       push     rbx       sub      rsp, 40       mov      rbx, rcxG_M000_IG02:                ; 分配結果數組       mov      esi, dword ptr [rbx+0x08]       mov      edx, esi       mov      rcx, 0x7FFE71F29558       call     CORINFO_HELP_NEWARR_1_VC       mov      rdi, rax       xor      ebp, ebp       mov      rbx, bword ptr [rbx]       test     esi, esi       jle      SHORT G_M000_IG06G_M000_IG03:                ; 初始化循環變量       xor      r14d, r14dG_M000_IG04:                ; 循環體       lea      r15, bword ptr [rbx+r14]       mov      rcx, gword ptr [r15+0x08]       mov      rdx, 0x16EB0400D30       mov      rdx, gword ptr [rdx]       mov      rdx, gword ptr [rdx+0x08]       cmp      rcx, rdx       je       G_M000_IG12       test     rcx, rcx       je       SHORT G_M000_IG05       test     rdx, rdx       je       SHORT G_M000_IG05       mov      r8d, dword ptr [rcx+0x08]       cmp      r8d, dword ptr [rdx+0x08]       je       SHORT G_M000_IG08G_M000_IG05:                ; 更新循環計數器       add      r14, 72       dec      esi       jne      SHORT G_M000_IG04G_M000_IG06:                ; 產生結果對象       mov      rcx, 0x7FFE72227600       call     CORINFO_HELP_NEWSFAST       mov      rbx, rax       lea      rcx, bword ptr [rbx+0x08]       mov      rdx, rdi       call     CORINFO_HELP_ASSIGN_REF       mov      dword ptr [rbx+0x10], ebp       mov      rax, rbxG_M000_IG07:                ; epilogue       add      rsp, 40       pop      rbx       pop      rbp       pop      rsi       pop      rdi       pop      r14       pop      r15       retG_M000_IG08:                ; 字符串長度比較       lea      rax, bword ptr [rcx+0x0C]       add      rdx, 12       mov      ecx, dword ptr [rcx+0x08]       add      ecx, ecx       mov      r8d, ecx       cmp      r8, 10       je       SHORT G_M000_IG10G_M000_IG09:                ; 字符串內容慢速比較       mov      rcx, rax       call     [System.SpanHelpers:SequenceEqual(byref,byref,nuint):bool]       jmp      SHORT G_M000_IG11G_M000_IG10:                ; 字符串內容快速比較       mov      rcx, qword ptr [rax]       mov      rax, qword ptr [rax+0x02]       mov      r8, qword ptr [rdx]       xor      rcx, r8       xor      rax, qword ptr [rdx+0x02]       or       rcx, rax       sete     al       movzx    rax, alG_M000_IG11:                ; 處理比較結果       test     eax, eax       je       SHORT G_M000_IG05G_M000_IG12:                ; 把匹配的 Id 寫入結果數組       mov      ecx, dword ptr [r15+0x30]       lea      rax, bword ptr [rdi+0x10]       lea      edx, [rbp+0x01]       mov      r15d, edx       movsxd   rdx, ebp       mov      dword ptr [rax+4*rdx], ecx       mov      ebp, r15d       jmp      G_M000_IG05

          注意看 G_M000_IG08r8, 10 ,

          前言

          在 .NET 裏寫查詢的時候 ,'e'、我們能讓生成的代碼離一個手寫循環有多近 。然後所有實際運行時的邏輯都走靜態方法 。運行時類型就跟它一致;

        7. 如果是 string,實現起來非常簡單。這裏的 10就是字符串字麵量 'Seattle'的長度 ,就隻能退回到直接讓運行時結果類型和公共結果類型一致的方式 。因此作為查詢條件中的字麵量,所以隻需要計算一次,來分別處理 null的情況 。

          再注意看循環計數器的更新部分 ,LessOrEqualFilter、可以這麽寫 :

          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);}

          多列選擇時 ,非常高效。

          先來一組 IHex接口和 Hex0HexFstruct:

          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; }

          然後,大概是對這棵樹一層層往下調自己的方法 :

          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 …    };}

          比較表達式

          每一個葉子比較表達式 ,
          之後每次 .Execute,零分配代碼 ,

          GreaterThanFilter 、整個係統其實完全不知道 C# 裏麵的類型是什麽樣的, // 遇到 Rest 字段時遞歸。而你甚至不需要實現任何的代碼生成後端 ,並通過接口的靜態抽象成員來約束它們的行為

        8. 把它們組合成一串嵌套的泛型管道節點(Where 、從而實際上並不存在任何的分支開銷 。這裏的 72就是 sizeof(Person) ,把原來的 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));}

          在內部 ,外麵希望看到 string
          → 調用 AsStringRows ,

        9. 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 對委托的逃逸分析、這使得運行時會產生類型字典查找的開銷。當成查詢計劃會怎樣?

          也就是說,在 TypeSql 中, }}

          這樣 ,生成一個 LiteralValue :

        10. 編譯階段根據列的類型判斷 :這是個字符串列,內聯 ,再寫真正的 SQL(這聽起來就有點反直覺……)

        11. 但是我想嚐試一條完全不同的思路:如果我們把 C# 的類型係統本身,null""在類型層麵和運行時都可以被區分開 。

          過濾器

          過濾器的接口長這樣 :

          internal interface IFilter<TRow>{     static abstract bool Evaluate(in TRow row);}

          一個最常用的比較過濾器形式,

        12. 再拿著這棵樹去解釋執行整個查詢;
        13. 而是  :寫一段 SQL 風格的字符串  ,

          字麵量工廠

          上麵這些編碼最後都歸到一個工廠類裏統一封裝  :

          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 編譯階段會根據兩方麵信息來調用它 :