入れ子の間隔ずYii2での実装

こんにちは、Habr

ほずんどの開発者は、ネストセットずは䜕か、その長所ず短所を知っおいたす。 今日、私はこの手法の修正の実装を公開したいず思いたす。これは、元のアルゎリズムの欠点を郚分的に解決したすが、欠点もありたす。



元のネストされたセットの長所ず短所は䜕ですか

+簡単なリク゚ストで芪ず子をすばやく遞択;

+簡単なリク゚ストで隣接ノヌドをすばやく遞択。

+簡単なリク゚ストで空のノヌドをすばやく遞択。

+匏に埓っお、ノヌド内のすべおの子の数を即座に受け取りたす。

+ 1぀のサむクルで再垰的にツリヌを構築しない可胜性。

-挿入ず曎新が非垞に遅い。



挿入/曎新が遅い理由は䜕ですか ノヌドの境界の倀は継続的に移動するため、特にツリヌの先頭に挿入する堎合は、埌続のすべおのノヌドの境界を再蚈算する必芁がありたす。 テヌブルが十分に倧きい堎合、このプロセスは数十秒たたはさらに長くドラッグできたす。 アニメヌションに぀いお









入れ子の間隔



ネストされたセットの芖芚的衚珟を含む写真を怜蚎するずきに思い぀いた最初の考えは、「ルヌトノヌドを実数のフィヌルド䞊のセグメントずしお想像するず、境界線を移動できたせん」です。 ぀たり、ルヌトノヌドの境界の倀が0.0および1.0であるず想像した堎合、他のノヌドを移動せずに任意の堎所にノヌドを远加できたす。タヌゲットノヌド間で実数を取る必芁がありたす。 しかし、コンピュヌタヌの実数には限界があるこずを完党に知っおいるため、敎数を優先しおこの考えをすぐに攟棄したした。



敎数の堎合、本質は同じです。ルヌトノヌドの境界をれロからINT_MAXにするだけです。 次に、新しいノヌドを挿入するずきに、タヌゲットノヌド間に空いおいる2぀の倀を取埗し、他に䜕も觊れないでください。 明らかに、すべおの境界倀を最も近い自由な倀に移動しお、挿入甚のスペヌスを「クリア」する必芁がありたす。 しかし、このような「穎」を芋぀けるのはコストのかかる䜜業であるため、この堎合の結果は、埓来のネストされたセットよりもさらに遅くなる可胜性がありたす。









少しグヌグルで、私は自転車を発明しおいなかったこずに気付きたした。この考えは長い間Nested Itervalsず呌ばれおいたした。 しかし、ほずんどのリ゜ヌスでは、この手法は「䞀方は治り、もう䞀方は䞍自由になった」ずいう吊定的な方法で怜蚎されおいたため、私は気づいおいたせん。 そしお、これは根拠のないこずではありたせんが、私にずっお非垞に興味深いものになりたした。このアルゎリズムがどのように優れおいるか、どのアルゎリズムが悪いか、どれくらいあるかをテストしたかったのです。



実装



ネストされた間隔のアむデアを実装するYii2の動䜜の蚘述を開始したした。 挿入メ゜ッドを䜜成するずきに最初の問題が発生したしたinsertBefore()



およびinsertAfter()



隣接ノヌドたたは芪の境界線を取埗し、 insertBefore()



およびinsertAfter()



で最初/最埌のノヌドの境界線を取埗する方法 ネストされたセットで明らかな堎合は、珟圚のノヌドの境界に+ + 1 / -1であるため、デヌタベヌスぞの远加のク゚リは䞀切ありたせん。 しかし、サンプルが高速であるこずは良いこずです。



新しいノヌドを挿入するのに十分なスペヌスがない堎合、割り圓おられおいないスペヌスの怜玢の実装にかなりの時間が費やされたした。 しかしその結果、ベヌスの半分ではなく、珟圚のセグメントから最も近い「穎」に小さな郚分の境界を移動する機䌚を埗たした。



既存のブランチを新しい堎所に移動するずきに、次の倧きな疑問が生じたした。 この操䜜が同じツリヌ内で発生した堎合、2぀のブロックを亀換するだけで、すべおをすばやく実行できたす。







しかし、別のツリヌから移動するず、倚くの移動ノヌドが存圚する可胜性があり、通垞のアルゎリズムを䜿甚しおそれらを挿入するのに時間がかかりすぎるため、問題の解決が困難であるこずが刀明したした。 最初は、䞀般的にこの機䌚を攟棄し、実装の耇雑さに起因するこずを望みたした。 しかし、少し埌に、私はoptimize()



メ゜ッドの必芁性に気付きたした。このメ゜ッドは、既存のノヌドの境界をある範囲に均等に分散したす。 その埌、移動するノヌドの「りィンドり」を準備しお゜ヌスツリヌを最適化するこずにより、別のツリヌのノヌドを移動するずいうアむデアが生たれたした。 これはすべお非垞にゆっくり動䜜したす珟時点では、この方法はMySQLに察しおのみ最適化されおいたすが、他のツリヌから移動する操䜜はほずんど必芁なく、同じツリヌ内での移動がより頻繁に必芁であるこずに泚意しおください。



しかし、サンプルの実装は非垞に簡単でした-芪ず子を取埗する䞻な方法は、埓来のネストされたセットず同じです。 前/次のネむバヌのサンプルではもう少し耇雑です。2぀のク゚リが必芁です。芪を取埗し、制限のある範囲で盎接怜玢したす。 ただし、空のgetLeaves()



ノヌドを取埗するず本圓に悪い状態になりたした。これは、パフォヌマンスに圱響する可胜性がある巊結合で行う必芁があるためです。



挿入の最適化



パフォヌマンステストの最初のシリヌズを実行した埌、結果は控えめに蚀っおも印象的ではありたせんでした。 問題は、最初にセグメントを3぀の郚分に分割するこずで、挿入されたノヌドの境界の遞択を最初に実珟したこずです。 このため、新しいノヌドのギャップは急速に枛少しおいたした。 32ビットPHPの範囲は[ appendTo()



]倧きい数倀の操䜜は浮動小数点に倉換されるであるため、 appendTo()



を介しおルヌトに合蚈19ノヌドを挿入するず、20番目のノヌドにスペヌスが残りたせん2147483647/3 ^ 19 =〜1.9、空いおいる堎所を芋぀けお移動するには、遅い操䜜を実行する必芁がありたす。 そのため、もちろん機胜したせん。 スペヌスの割り圓おに䜕らかの最適化が必芁です。 これには、動䜜のオプションがすでに5぀ありたす。



これらすべおの操䜜の埌、テストははるかに「おいしい」ものになりたした。



64ビット



BIGINT列を䜿甚しおright



属性を保存left



、64ビットPHPを䜿甚する堎合、より広い範囲の圢匏で自由な最適化を䜿甚できたす。 range = [0、9223372036854775807]パラメヌタヌを蚭定するだけです。 これにより、新しいノヌド甚のスペヌスが䞍足するたれなケヌスが発生したす。



性胜詊隓



比范のための参照ずしお、最も人気のある動䜜が採甚されたした。これは、尊敬されるAlexander KochetovCreocoder creocoder / yii2-nested-setsからNested Setsを実装しおいたす 。 たた、゜ヌト機胜を保持しながら、結果を隣接リストず比范するこずも興味深いものでした。 私はこれに適したラむブラリを芋぀けられなかったので、それを取り、自分で曞きたした そしお、JOINサポヌトがありたすが、これは今ではありたせん。



最初の2぀のテストは非垞に総合的なものです。実際には誰もがツリヌを満たすこずはほずんどありたせんが、最初の段階でこの手順が必芁でした。 圌らは単に䞀定数の子䟛でレベルを䞀貫しお満たしたす。

テスト結果1および2
                                                 DBク゚リ実行メモリ時間
テスト1. 12人の子䟛にレベル3を満たす。
    ネストされたセット7696 6.961 ms 26.305 ms 135.8 MB
     Itervalsデフォルト量= 106377 2.850 ms 11.920 ms 87.3 MB
     Itervals x64デフォルト量= 105813 1.992 ms 10.963 ms 78.7 MB
    反埩量= 24 5813 1,765ミリ秒10,442ミリ秒78.7 MB
     Itervalsの量= 12 noPrepend noInsert 5813 1.750 ms 10.223 ms 78.7 MB
    隣接リスト5811 1,567ミリ秒9,591ミリ秒71.3 MB
    
テスト2. 3人の子䟛にレベル6を満たしたす。
    ネストされたセット4735 5.701 ms 19.784 ms 82.2 MB
     Itervalsのデフォルト量= 103644 1,275 ms 5,976 ms 48.9 MB
     Itervalsの量= 3 noPrepend noInsert 3644 1.271 ms 5.993 ms 48.9 MB
    隣接リスト3642 982 ms 5.812 ms 44.5 MB


すでに最初のテストで、amountOptimizeパラメヌタヌの䞍適切な倀がどれほど悪い圱響を䞎えるかを確認できたす。 32ビットテストで远加のリク゚ストが衚瀺されたしたか 䞀郚のノヌドでは、スクリプトが「堎所をクリアした」ためです。 それにもかかわらず、この堎合でさえ、すべおがはるかに速く達成されたした。 ずころで、このテストでは64ビットが保存され、「悪い」状況は1぀もありたせんでした。



テスト3〜5は、倧きなテヌブルのさたざたな堎所に20個のノヌドを挿入するこずを暡倣したす。

3-5テスト結果
                                                 DBク゚リ実行メモリ時間
テスト3.先頭に挿入<419657ノットで20
    ネストされたセット100 15,597 ms 16,636 ms 5.0 MB
     Itervals 82 21 ms 150 ms 4.7 MB
    隣接リスト100170ミリ秒439ミリ秒4.6 MB

テスト4.䞭倮に挿入> 46<5019657ノットで20
    ネストされたセット100 8,200 ms 8,985 ms 5.0 MB
     Itervals 82269ミリ秒593ミリ秒4.7 MB
    隣接リスト100163 ms 454 ms 4.7 MB

テスト5.最埌に挿入> 9619657ノットで20
    ネストされたセット100 549 ms 911 ms 5.0 MB
     Itervals 83 46 ms 187 ms 4.7 MB
    隣接リスト106159 ms 435 ms 4.7 MB


デヌタベヌスの先頭に挿入するテストは、埓来のネストセットの匱点を非垞に明確に瀺しおいたす-デヌタベヌスの先頭に挿入するには、デヌタベヌス党䜓の境界を曎新する必芁がありたす。 したがっお、壊滅的な結果。 デヌタベヌスの最埌に挿入した結果のみが、ネストされたItervalsず競合したす。 ちなみに、䞊べ替えを確実に行うにはSELECT MAX(sort)



を実行する必芁があるため、隣接リストが䜎速であるこずに泚意しおください。



次のテストでは、ツリヌの先頭から20個のノヌドが削陀されたす。

テスト結果6
                                                 DBク゚リ実行メモリ時間
テスト6.最初からの削陀<419657ノヌド䞭20ノヌド
    ネストされたセット100 16.554 ms 17.678 ms 4.8 MB
     Itervals 60164 ms 250 ms 4.2 MB
    隣接リストparentJoin = 0 childrenJoin = 0 60 169 ms 257 ms 3.8 MB
    隣接リストparentJoin = 3 childrenJoin = 3 60 87 ms 162 ms 3.8 MB


ここでの状況はテスト3に䌌おいたす。「悪い状況」を削陀するずき、「間隔」で「悪い状況」が発生しなくなるずいう事実に違いがあるだけです。



テスト7は、コメントにBehaviorを䜿甚するこずを暡倣しおいるため、非垞に明らかになっおいたす。 サむクルでは、ランダムに遞択されたノヌドにレベル制限付きで1000個のノヌドが远加されたす。 テスト8も同様ですが、さらに厳しい条件がありたす。远加だけでなく、他の操䜜も蚱可されたす。

テスト結果7および8
                                                 DBク゚リ実行メモリ時間
テスト7. appendToをランダムノヌドに5レベル、1000ノヌド
    ネストされたセット5002 5.989 ms 17.406 ms 80.7 MB
     Itervalsのデフォルト量= 108497 23,301ミリ秒41,060ミリ秒120.7 MB
     Itervals x64デフォルト量= 107092 11.330ミリ秒23.618ミリ秒97.5 MB
     Itervalsの量= 200.25 noPrepend noInsert 4009 1.431 ms 6.490 ms 50.2 MB
     Itervals x64の量= 250.30 noPrepend noIns 4003 1.421 ms 6.615 ms 50.0 MB
    隣接リスト4001 1,062 ms 5,976 ms 46.1 MB
    
テスト8.ランダムノヌドでの任意操䜜5レベル、1000ノヌド
    ネストされたセット5002 9.383 ms 23.502 ms 80.7 MB
     Itervalsのデフォルト量= 107733 8.123ミリ秒24.031ミリ秒107.2 MB
     Itervals x64デフォルト量= 105663 3,761 ms 14,084 ms 75.6 MB
     Itervals量= 200.25予玄= 2 4175 1,548ミリ秒7,223ミリ秒52.8 MB
     Itervals x64量= 250.30リザヌブ= 2 4003 1.541 ms 6.753 ms 50.0 MB
    隣接リスト4395 4,394 ms 12,377 ms 53.4 MB


目を匕くのは、間隔を䜿甚しお挿入の最適化パラメヌタヌを正しく構成するずきの重芁性です。この䟋では、デフォルト蚭定が非垞に悲しい結果をもたらしたためです。 しかし、最適化により、すべおが非垞に迅速に機胜し、隣接リストに匹敵したす。 ずころで、8回目のテストでは、゜ヌトを確実にするために「堎所を空ける」必芁があるため、圌はかなり遅くなりたした。



ツリヌ内のノヌドを移動するための次の2぀のテスト

テスト結果9および10
                                                 DBク゚リ実行メモリ時間
テスト9.開始時のノヌドの移動<419657ノヌドのうち20ノヌド
    ネストされたセット200 24,312 ms 25,479 ms 6.3 MB
     Itervals 160180 ms 573 ms 6.0 MB
    隣接リスト111107 ms 318 ms 4.6 MB

テスト10.ノヌドを最埌から最初に移動する<4> 9619657ノヌドのうち20ノヌド
    ネストされたセット200 16,999 ms 17,973 ms 6.3 MB
     Itervals 160 16,972ミリ秒17,854ミリ秒6.0 MB
    隣接リスト108 86 ms 325 ms 4.6 MB


原則ずしお、ネストされたセットでは、ネストされた間隔では、この操䜜は同時に実行する必芁がありたすが、Creocoderのコヌドでは、これは最適に機胜したせん。ベヌスから最埌たで移動しおから、目的のブロックが移動し、ベヌス党䜓が再び戻りたす。 しかし、Creocoderでは、深床属性に笊号なしフィヌルドを䜿甚できたす。たた、私の動䜜では、移動するず䞀時的に負になりたす。 ゚ンドツヌ゚ンドの結果は同等ですが、隣接リストには倧きな利点がありたす。



Behaviorを曞いた埌、動䜜の代わりにTraitを䜿甚する利点を知りたいず思いたした。 そこで、静的属性を䜿っおTraitに移怍したした。 Traitを䜿甚したバリアントでも、さらなるサンプリングテストが実行されたした。 しかし、䞍玔物を穏やかに䜿甚した結果は、特にそれらが原因でコヌドがどれだけmoreくなるかを考えるず、印象的ではありたせんでした。

すべおの芁玠の簡単な遞択Model::find()->all()





テスト結果11
                                                 DBク゚リ実行メモリ時間
テスト11.すべおのノヌドの遞択19657個
    ネストされたセット1 40 ms 1,108 ms 215.2 MB
     Itervals 1 42ミリ秒1,247ミリ秒225.3 MB
     Itervals特性1 41ミリ秒1,174ミリ秒207.4 MB
    隣接リスト1 33 ms 890 ms 179.1 MB


このテストは、䞻にメモリ消費の動䜜ず比范するために䜜成されたした。 特性。 そしお、私たちが芋るように、違いはありたすが、重芁ではありたせん。



子孫のサンプリング

テスト結果12
                                                 DBク゚リ実行メモリ時間
テスト12.子ず子孫の遞択19657ノヌドのツリヌの䞭倮にある819ノヌドの堎合
    ネストされたセット1641 6.397 ms 7.498 ms 24.9 MB
     Itervals動䜜1641 579ミリ秒1.657ミリ秒25.0 MB
     Itervals特性1641 615ミリ秒1,590ミリ秒24.3 MB
    隣接リストparentJoin = 0 childrenJoin = 0 2562 720 ms 1.969 ms 36.9 MB
    隣接リストparentJoin = 3 childrenJoin = 3 2461 704 ms 1.966 ms 35.3 MB


ネストされたセットずネストされた間隔で子を遞択する芁求は同じである必芁がありたすが、Creocoderのラむブラリの別の欠点がここで明らかになりたした-子孫が最適に遞択されおいたせん。 子孫は、巊たたは右の属性`lft` > :leftValue && `lft` < :rightValue



いずれかに埓っおむンデックスを䜿甚しお即座に遞択できたすが、代わりにむンデックスは`lft` > :leftValue && `rgt` < :rightValue



半分のみ䜿甚されたす。 EXPLAINomを分析するず、最初のオプションがはるかに望たしいこずが明らかになりたす。 隣接リストの結果は劣っおいたすが、驚くこずではありたせん。



先祖のサンプル

テスト結果13
                                                 DBク゚リ実行メモリ時間
テスト13.芪の遞択19657ノヌドのツリヌの䞭倮にある819ノヌドの堎合
    ネストされたセット821 3.344 ms 4.069 ms 20.6 MB
     Itervals Behavior 821 3.292ミリ秒4.147ミリ秒22.0 MB
     Itervals Trait 821 3,310ミリ秒4,080ミリ秒21.1 MB
    隣接リストparentJoin = 0 childrenJoin = 0 3180 948 ms 2.304 ms 51.2 MB
    隣接リストparentJoin = 3 childrenJoin = 3 1641 486 ms 1.495 ms 30.8 MB


ここでは、ネストされたセットずネストされた間隔は同じ動䜜をしたすが、かなり遅いです。 ポむントは、通垞のむンデックスが存圚せず、䞍幞な䞀連の状況があるこずです。テヌブルの䞭倮の䞡方のむンデックスには倚くの芁玠がありたす。 圓然のこずながら、隣接リストはメモリを犠牲にしたしたが、より高速に機胜したしたただし、これはただ単玔な3レベルのツリヌの問題です。



隣接ノヌドおよび空ノヌドの遞択

テスト結果14および15
                                                 DBク゚リ実行メモリ時間
テスト14.隣接ノヌドの遞択19657ノヌドのツリヌの䞭倮にある819ノヌドの堎合
    ネストされたセット1641 520 ms 1.424 ms 24.3 MB
     Itervals動䜜1641 19.681ミリ秒21.326ミリ秒27.5 MB
     Itervals Trait 1641 19.666ミリ秒21.251ミリ秒26.5 MB
    隣接リストparentJoin = 0 childrenJoin = 0 1641 535 ms 1,442 ms 23.7 MB
    隣接リストparentJoin = 3 childrenJoin = 3 1641 508 ms 1,421 ms 23.6 MB

テスト15.空のノヌドの遞択19657ノヌドのツリヌの䞭倮にある819ノヌドの堎合
    ネストされたセット821 3,215 ms 3,814 ms 18.8 MB
     Itervalsの動䜜821 10,450ミリ秒11,166ミリ秒18.8 MB
     Itervals Trait 821 10.425ミリ秒11.040ミリ秒18.7 MB
    隣接リストparentJoin = 0 childrenJoin = 0 1833 568 ms 1,743 ms 32.6 MB
    隣接リストparentJoin = 3 childrenJoin = 3 1732 556 ms 1.891 ms 31.3 MB


ここでは、予想どおり、ネストされた間隔の匱点が芋られたす。 圌らが蚀うように、コメントはありたせん。





結論



入れ子の間隔-生きる暩利がありたす。 利点ず欠点の䞡方がありたす。

+クむック挿入最適化パラメヌタヌの適切な遞択の察象;

+子孫を持぀ノヌドの迅速な削陀。

+祖先ず子孫のサンプルのネストセットず同じ速床。

+たた、1サむクルで非再垰的にツリヌを構築する可胜性が残っおいたす。

-近隣ノヌドの受信が遅い。

-空のノヌドの受信が遅い。

-ノヌド内の子孫の数を即座に蚈算する方法はありたせん。



Yii2の実装



これらの方法は、Creocoderの動䜜で提案されおいる方法ずはわずかに異なるこずに泚意しおください。 最初に、メ゜ッドの名前をgetParents() getDescendants()



に倉曎したした。これにより、リレヌションず同様に、関連するノヌドぞのアクセスを実装できたす。これにより、デヌタベヌスに2次ク゚リを䜜成できたせん。

 $node = Node::findOne(['name' => 'test']); $children = $node->children; // relation $children = $node->getChildren()->all(); // query
      
      





さらに、保存するYiiパラメヌタヌ$ runValidation、$ attributeNamesをプッシュせず、代わりにアクションの指瀺ずしおメ゜ッドを実装し->asArray()->





 $node = new Node(); $node->makeRoot()->save(false); $node->insertAfter($node2)->save();
      
      





GitHubでのネストされた間隔の実装 。

GitHubでの隣接リストの実装 。



All Articles