Browse Source

KIKIMR-19522 BTreeIndex Precharge Simplification (#682)

kungurtsev 1 year ago
parent
commit
33e5f8bc22

+ 45 - 108
ydb/core/tablet_flat/flat_part_charge.h

@@ -42,68 +42,8 @@ namespace NTable {
             }
         }
 
-        bool Do(const TRowId row1, const TRowId row2,
+        TResult Do(const TCells key1, const TCells key2, const TRowId row1, const TRowId row2, 
                 const TKeyCellDefaults &keyDefaults, ui64 itemsLimit, ui64 bytesLimit) const noexcept override
-        {
-            auto index = Index.TryLoadRaw();
-            if (!index) {
-                return false;
-            }
-
-            auto startRow = row1;
-            auto endRow = row2;
-
-            // Page that contains row1
-            auto first = index->LookupRow(row1);
-            if (Y_UNLIKELY(!first)) {
-                return true; // already out of bounds, nothing to precharge
-            }
-
-            // Page that contains row2
-            auto last = index->LookupRow(row2, first);
-            if (Y_UNLIKELY(last < first)) {
-                last = first;
-                endRow = Min(endRow, index->GetLastRowId(last));
-            }
-
-            return DoPrecharge(TCells{}, TCells{}, TIter{}, TIter{}, first, last, startRow, endRow, keyDefaults, itemsLimit, bytesLimit);
-        }
-
-        bool DoReverse(const TRowId row1, const TRowId row2, 
-                const TKeyCellDefaults &keyDefaults, ui64 itemsLimit, ui64 bytesLimit) const noexcept override
-        {
-            auto index = Index.TryLoadRaw();
-            if (!index) {
-                return false;
-            }
-
-            auto startRow = row1;
-            auto endRow = row2;
-
-            // Page that contains row1
-            auto first = index->LookupRow(row1);
-            if (Y_UNLIKELY(!first)) {
-                // Looks like row1 is out of bounds, start from the last row
-                startRow = Min(row1, index->GetEndRowId() - 1);
-                first = --(*index)->End();
-                if (Y_UNLIKELY(!first)) {
-                    return true; // empty index?
-                }
-            }
-
-            // Page that contains row2
-            auto last = index->LookupRow(row2, first);
-            if (Y_UNLIKELY(last > first)) {
-                last = first; // will not go past the first page
-                endRow = Max(endRow, last->GetRowId());
-            }
-
-            return DoPrechargeReverse(TCells{}, TCells{}, TIter{}, TIter{}, first, last, startRow, endRow, keyDefaults, itemsLimit, bytesLimit);
-        }
-
-        TResult Do(const TCells key1, const TCells key2, const TRowId row1,
-                const TRowId row2, const TKeyCellDefaults &keyDefaults, ui64 itemsLimit,
-                ui64 bytesLimit) const noexcept override
         {
             auto index = Index.TryLoadRaw();
             if (!index) {
@@ -164,14 +104,14 @@ namespace NTable {
                 }
             }
 
-            bool ready = DoPrecharge(key1, key2, key1Page, key2Page, first, last, startRow, endRow, keyDefaults, itemsLimit, bytesLimit);
+            bool ready = DoPrecharge(key1, key2, key1Page, key2Page, first, last, 
+                startRow, endRow, keyDefaults, itemsLimit, bytesLimit);
 
             return { ready, overshot };
         }
 
-        TResult DoReverse(const TCells key1, const TCells key2, const TRowId row1,
-                const TRowId row2, const TKeyCellDefaults &keyDefaults, ui64 itemsLimit,
-                ui64 bytesLimit) const noexcept override
+        TResult DoReverse(const TCells key1, const TCells key2, const TRowId row1, const TRowId row2, 
+                const TKeyCellDefaults &keyDefaults, ui64 itemsLimit, ui64 bytesLimit) const noexcept override
         {
             auto index = Index.TryLoadRaw();
             if (!index) {
@@ -237,7 +177,8 @@ namespace NTable {
                 }
             }
 
-            bool ready = DoPrechargeReverse(key1, key2, key1Page, key2Page, first, last, startRow, endRow, keyDefaults, itemsLimit, bytesLimit);
+            bool ready = DoPrechargeReverse(key1, key2, key1Page, key2Page, first, last, 
+                startRow, endRow, keyDefaults, itemsLimit, bytesLimit);
 
             return { ready, overshot };
         }
@@ -254,10 +195,8 @@ namespace NTable {
          *
          * If @param key1Page specified, @param first should be the same.
          */
-        bool DoPrecharge(const TCells key1, const TCells key2, 
-                const TIter key1Page, const TIter key2Page,
-                const TIter first, const TIter last,
-                TRowId startRowId, TRowId endRowId,
+        bool DoPrecharge(const TCells key1, const TCells key2, const TIter key1Page, const TIter key2Page,
+                const TIter first, const TIter last, TRowId startRowId, TRowId endRowId,
                 const TKeyCellDefaults &keyDefaults, ui64 itemsLimit, ui64 bytesLimit) const noexcept
         {
             bool ready = true;
@@ -352,10 +291,8 @@ namespace NTable {
          *
          * If @param key1Page specified, @param first should be the same.
          */
-        bool DoPrechargeReverse(const TCells key1, const TCells key2, 
-                const TIter key1Page, const TIter key2Page,
-                TIter first, TIter last,
-                TRowId startRowId, TRowId endRowId,
+        bool DoPrechargeReverse(const TCells key1, const TCells key2, const TIter key1Page, const TIter key2Page,
+                TIter first, TIter last, TRowId startRowId, TRowId endRowId,
                 const TKeyCellDefaults &keyDefaults, ui64 itemsLimit, ui64 bytesLimit) const noexcept
         {
             bool ready = true;
@@ -562,8 +499,8 @@ namespace NTable {
         /**
          * Precharges pages that contain row1 to row2 inclusive
          */
-        bool DoPrechargeGroup(TGroupState& g, TRowId row1, TRowId row2, ui64& bytes) const noexcept {
-            auto groupIndex = g.GroupIndex.TryLoadRaw();
+        bool DoPrechargeGroup(TGroupState& group, TRowId row1, TRowId row2, ui64& bytes) const noexcept {
+            auto groupIndex = group.GroupIndex.TryLoadRaw();
             if (!groupIndex) {
                 if (bytes) {
                     // Note: we can't continue if we have bytes limit
@@ -574,29 +511,29 @@ namespace NTable {
 
             bool ready = true;
 
-            if (!g.Index || row1 < g.Index->GetRowId() || row1 > g.LastRowId) {
-                g.Index = groupIndex->LookupRow(row1, g.Index);
-                if (Y_UNLIKELY(!g.Index)) {
+            if (!group.Index || row1 < group.Index->GetRowId() || row1 > group.LastRowId) {
+                group.Index = groupIndex->LookupRow(row1, group.Index);
+                if (Y_UNLIKELY(!group.Index)) {
                     // Looks like row1 doesn't even exist
-                    g.LastRowId = Max<TRowId>();
+                    group.LastRowId = Max<TRowId>();
                     return ready;
                 }
-                g.LastRowId = groupIndex->GetLastRowId(g.Index);
-                auto pageId = g.Index->GetPageId();
-                ready &= bool(Env->TryGetPage(Part, pageId, g.GroupId));
-                bytes += Part->GetPageSize(pageId, g.GroupId);
+                group.LastRowId = groupIndex->GetLastRowId(group.Index);
+                auto pageId = group.Index->GetPageId();
+                ready &= bool(Env->TryGetPage(Part, pageId, group.GroupId));
+                bytes += Part->GetPageSize(pageId, group.GroupId);
             }
 
-            while (g.LastRowId < row2) {
-                if (!++g.Index) {
+            while (group.LastRowId < row2) {
+                if (!++group.Index) {
                     // Looks like row2 doesn't even exist
-                    g.LastRowId = Max<TRowId>();
+                    group.LastRowId = Max<TRowId>();
                     return ready;
                 }
-                g.LastRowId = groupIndex->GetLastRowId(g.Index);
-                auto pageId = g.Index->GetPageId();
-                ready &= bool(Env->TryGetPage(Part, pageId, g.GroupId));
-                bytes += Part->GetPageSize(pageId, g.GroupId);
+                group.LastRowId = groupIndex->GetLastRowId(group.Index);
+                auto pageId = group.Index->GetPageId();
+                ready &= bool(Env->TryGetPage(Part, pageId, group.GroupId));
+                bytes += Part->GetPageSize(pageId, group.GroupId);
             }
 
             return ready;
@@ -605,8 +542,8 @@ namespace NTable {
         /**
          * Precharges pages that contain row1 to row2 inclusive in reverse
          */
-        bool DoPrechargeGroupReverse(TGroupState& g, TRowId row1, TRowId row2, ui64& bytes) const noexcept {
-            auto groupIndex = g.GroupIndex.TryLoadRaw();
+        bool DoPrechargeGroupReverse(TGroupState& group, TRowId row1, TRowId row2, ui64& bytes) const noexcept {
+            auto groupIndex = group.GroupIndex.TryLoadRaw();
             if (!groupIndex) {
                 if (bytes) {
                     // Note: we can't continue if we have bytes limit
@@ -617,29 +554,29 @@ namespace NTable {
 
             bool ready = true;
 
-            if (!g.Index || row1 < g.Index->GetRowId() || row1 > g.LastRowId) {
-                g.Index = groupIndex->LookupRow(row1, g.Index);
-                if (Y_UNLIKELY(!g.Index)) {
+            if (!group.Index || row1 < group.Index->GetRowId() || row1 > group.LastRowId) {
+                group.Index = groupIndex->LookupRow(row1, group.Index);
+                if (Y_UNLIKELY(!group.Index)) {
                     // Looks like row1 doesn't even exist
-                    g.LastRowId = Max<TRowId>();
+                    group.LastRowId = Max<TRowId>();
                     return ready;
                 }
-                g.LastRowId = groupIndex->GetLastRowId(g.Index);
-                auto pageId = g.Index->GetPageId();
-                ready &= bool(Env->TryGetPage(Part, pageId, g.GroupId));
-                bytes += Part->GetPageSize(pageId, g.GroupId);
+                group.LastRowId = groupIndex->GetLastRowId(group.Index);
+                auto pageId = group.Index->GetPageId();
+                ready &= bool(Env->TryGetPage(Part, pageId, group.GroupId));
+                bytes += Part->GetPageSize(pageId, group.GroupId);
             }
 
-            while (g.Index->GetRowId() > row2) {
-                if (g.Index.Off() == 0) {
+            while (group.Index->GetRowId() > row2) {
+                if (group.Index.Off() == 0) {
                     // This was the last page we could precharge
                     return ready;
                 }
-                g.LastRowId = g.Index->GetRowId() - 1;
-                --g.Index;
-                auto pageId = g.Index->GetPageId();
-                ready &= bool(Env->TryGetPage(Part, pageId, g.GroupId));
-                bytes += Part->GetPageSize(pageId, g.GroupId);
+                group.LastRowId = group.Index->GetRowId() - 1;
+                --group.Index;
+                auto pageId = group.Index->GetPageId();
+                ready &= bool(Env->TryGetPage(Part, pageId, group.GroupId));
+                bytes += Part->GetPageSize(pageId, group.GroupId);
             }
 
             return ready;

+ 5 - 24
ydb/core/tablet_flat/flat_part_charge_btree_index.h

@@ -15,29 +15,7 @@ namespace NKikimr::NTable {
             Y_UNUSED(includeHistory);
         }
 
-        virtual bool Do(const TRowId row1, const TRowId row2,
-                const TKeyCellDefaults &keyDefaults, ui64 itemsLimit, ui64 bytesLimit) const noexcept override {
-            // TODO: implement
-            Y_UNUSED(row1);
-            Y_UNUSED(row2);
-            Y_UNUSED(keyDefaults);
-            Y_UNUSED(itemsLimit);
-            Y_UNUSED(bytesLimit);
-            return true;
-        }
-
-        virtual bool DoReverse(const TRowId row1, const TRowId row2, 
-                const TKeyCellDefaults &keyDefaults, ui64 itemsLimit, ui64 bytesLimit) const noexcept override {
-            // TODO: implement
-            Y_UNUSED(row1);
-            Y_UNUSED(row2);
-            Y_UNUSED(keyDefaults);
-            Y_UNUSED(itemsLimit);
-            Y_UNUSED(bytesLimit);
-            return true;
-        }
-
-        virtual TResult Do(const TCells key1, const TCells key2, const TRowId row1,
+        TResult Do(const TCells key1, const TCells key2, const TRowId row1,
                 const TRowId row2, const TKeyCellDefaults &keyDefaults, ui64 itemsLimit,
                 ui64 bytesLimit) const noexcept override {
             // TODO: implement
@@ -51,7 +29,7 @@ namespace NKikimr::NTable {
             return {true, false};
         }
 
-        virtual TResult DoReverse(const TCells key1, const TCells key2, const TRowId row1,
+        TResult DoReverse(const TCells key1, const TCells key2, const TRowId row1,
                 const TRowId row2, const TKeyCellDefaults &keyDefaults, ui64 itemsLimit,
                 ui64 bytesLimit) const noexcept override {
             // TODO: implement
@@ -64,6 +42,9 @@ namespace NKikimr::NTable {
             Y_UNUSED(bytesLimit);
             return {true, false};
         }
+
+    private:
+
 };
 
 }

+ 16 - 10
ydb/core/tablet_flat/flat_part_charge_iface.h

@@ -17,30 +17,36 @@ namespace NKikimr::NTable {
          *
          * Important caveat: assumes iteration won't touch any row > row2
          */
-        virtual bool Do(const TRowId row1, const TRowId row2,
-                const TKeyCellDefaults &keyDefaults, ui64 itemsLimit, ui64 bytesLimit) const noexcept = 0;
+        bool Do(const TRowId row1, const TRowId row2,
+                const TKeyCellDefaults &keyDefaults, ui64 itemsLimit, ui64 bytesLimit) const noexcept
+        {
+            return Do(TCells{}, TCells{}, row1, row2, 
+                keyDefaults, itemsLimit, bytesLimit).Ready;
+        }
 
         /**
          * Precharges data for rows between row1 and row2 inclusive in reverse
          *
          * Important caveat: assumes iteration won't touch any row > row2
          */
-        virtual bool DoReverse(const TRowId row1, const TRowId row2, 
-                const TKeyCellDefaults &keyDefaults, ui64 itemsLimit, ui64 bytesLimit) const noexcept = 0;
+        bool DoReverse(const TRowId row1, const TRowId row2, 
+                const TKeyCellDefaults &keyDefaults, ui64 itemsLimit, ui64 bytesLimit) const noexcept
+        {
+            return DoReverse(TCells{}, TCells{}, row1, row2, 
+                keyDefaults, itemsLimit, bytesLimit).Ready;
+        }
 
         /**
          * Precharges data for rows between max(key1, row1) and min(key2, row2) inclusive
          */
-        virtual TResult Do(const TCells key1, const TCells key2, const TRowId row1,
-                const TRowId row2, const TKeyCellDefaults &keyDefaults, ui64 itemsLimit,
-                ui64 bytesLimit) const noexcept = 0;
+        virtual TResult Do(const TCells key1, const TCells key2, const TRowId row1, const TRowId row2, 
+                const TKeyCellDefaults &keyDefaults, ui64 itemsLimit, ui64 bytesLimit) const noexcept = 0;
 
         /**
          * Precharges data for rows between min(key1, row1) and max(key2, row2) inclusive in reverse
          */
-        virtual TResult DoReverse(const TCells key1, const TCells key2, const TRowId row1,
-                const TRowId row2, const TKeyCellDefaults &keyDefaults, ui64 itemsLimit,
-                ui64 bytesLimit) const noexcept = 0;
+        virtual TResult DoReverse(const TCells key1, const TCells key2, const TRowId row1, const TRowId row2, 
+                const TKeyCellDefaults &keyDefaults, ui64 itemsLimit, ui64 bytesLimit) const noexcept = 0;
 
         virtual ~ICharge() = default;
 };

+ 188 - 38
ydb/core/tablet_flat/ut/ut_charge.cpp

@@ -125,7 +125,7 @@ namespace {
 
             for (auto seq: xrange(Mass.Saved.Size())) {
                 /* ... but rows pack has 4 rows per each page, the first row in
-                    each pack is ommited and used as spacer between pages. */
+                    each pack is omitted and used as spacer between pages. */
 
                 if (seq % 4 > 0) {
                     if (history) {
@@ -161,8 +161,14 @@ namespace {
 
         void CheckByRows(TPageId row1, TPageId row2, ui64 items, const TMap<TGroupId, TArr>& shouldPrecharge) const
         {
-            CheckPrechargeByRows(row1, row2, items, false, shouldPrecharge);
-            CheckPrechargeByRows(row1, row2, items, true, shouldPrecharge);
+            CheckPrechargeByRows(row1, row2, items, false, shouldPrecharge, false);
+            CheckPrechargeByRows(row1, row2, items, true, shouldPrecharge, false);
+        }
+
+        void CheckByRowsReverse(TPageId row1, TPageId row2, ui64 items, const TMap<TGroupId, TArr>& shouldPrecharge) const
+        {
+            CheckPrechargeByRows(row1, row2, items, false, shouldPrecharge, true);
+            CheckPrechargeByRows(row1, row2, items, true, shouldPrecharge, true);
         }
 
         void CheckIndex(ui32 lower, ui32 upper, ui64 items, const TMap<TGroupId, TArr>& shouldPrecharge, TSet<TPageId> stickyIndex) const {
@@ -205,10 +211,8 @@ namespace {
             CheckPrecharged(env.Touched, shouldPrecharge, sticky, flags);
         }
 
-        void CheckPrechargeByRows(TPageId row1, TPageId row2, ui64 items, bool fail, TMap<TGroupId, TArr> shouldPrecharge) const
+        void CheckPrechargeByRows(TPageId row1, TPageId row2, ui64 items, bool fail, TMap<TGroupId, TArr> shouldPrecharge, bool reverse) const
         {
-            Y_ABORT_UNLESS(row1 <= row2 && row2 < 3 * 9);
-
             auto sticky = GetIndexPages();
             TTouchEnv env(fail, sticky);
 
@@ -225,10 +229,12 @@ namespace {
                 tags.push_back(c.Tag);
             }
 
-            UNIT_ASSERT_VALUES_EQUAL_C(
-                !fail,
-                CreateCharge(&env, *run.begin()->Part, tags, false)->Do(row1, row2, keyDefaults, items, Max<ui64>()),
-                AssertMessage(fail));
+            auto charge = CreateCharge(&env, *run.begin()->Part, tags, false);
+            bool result = reverse
+                ? charge->DoReverse(row1, row2, keyDefaults, items, Max<ui64>())
+                : charge->Do(row1, row2, keyDefaults, items, Max<ui64>());
+
+            UNIT_ASSERT_VALUES_EQUAL_C(!fail, result, AssertMessage(fail));
 
             CheckPrecharged(env.Touched, shouldPrecharge, sticky, fail ? TPageIdFlags::IfFail : TPageIdFlags::IfNoFail);
         }
@@ -256,7 +262,7 @@ namespace {
                     wrap.Next().Is(key > upper ? EReady::Gone : EReady::Data);
                 }
 
-                // forcebly touch the next stop element that is greater than upper
+                // forcibly touch the next stop element that is greater than upper
                 // because instead of having |1 2 3| and stopping as soon as we see 2
                 // we may have |1*2 2*2 3*2| = |2 4 6| and be requested with upper = 5 (not 4)
                 if (key > upper) {
@@ -292,7 +298,7 @@ namespace {
                     wrap.Next().Is(key < upper || key == (ui32)-1 ? EReady::Gone : EReady::Data);
                 }
 
-                // forcebly touch the next stop element that is greater than upper
+                // forcibly touch the next stop element that is greater than upper
                 // because instead of having |1 2 3| and stopping as soon as we see 2
                 // we may have |1*2 2*2 3*2| = |2 4 6| and be requested with upper = 2 (not 4)
                 if (key < upper || key == (ui32)-1) {
@@ -358,7 +364,7 @@ namespace {
                         expectedValue.insert(absoluteId.Value(p.Page, p.Page));
                     }
                 }
-                UNIT_ASSERT_VALUES_EQUAL_C(expectedValue, actualValue, AssertMesage(groupId, flags));
+                UNIT_ASSERT_VALUES_EQUAL_C(expectedValue, actualValue, AssertMessage(groupId, flags));
             }
         }
 
@@ -367,12 +373,12 @@ namespace {
                 "Seq: " + std::to_string(CurrentStep()) + 
                 " Fail: " + (fail ? "Yes" : "No");
         }
-        std::string AssertMesage(TGroupId group) const {
+        std::string AssertMessage(TGroupId group) const {
             return 
                 "Seq: " + std::to_string(CurrentStep()) + 
                 " Group: " + std::to_string(group.Index) + "," + std::to_string(group.IsHistoric());
         }
-        std::string AssertMesage(TGroupId group, TPageIdFlags flags) const {
+        std::string AssertMessage(TGroupId group, TPageIdFlags flags) const {
             auto result = 
                 "Seq: " + std::to_string(CurrentStep()) + 
                 " Group: " + std::to_string(group.Index) + "," + std::to_string(group.IsHistoric());
@@ -604,7 +610,7 @@ Y_UNIT_TEST_SUITE(Charge) {
 
             me.To(209).CheckByKeys(5, 13, 0, TMap<TGroupId, TArr>{
                 {TGroupId{0}, {1, 2, 3, 4_I}},
-                {TGroupId{1}, {1_g, 2_g, 3, 4}}, // pages 3, 4 are always neded
+                {TGroupId{1}, {1_g, 2_g, 3, 4}}, // pages 3, 4 are always needed
                 {TGroupId{2}, {3_g, 4_g, 5_g, 6, 7, 8, 9_g}} // pages 6, 7, 8 are always needed
             });
 
@@ -628,7 +634,7 @@ Y_UNIT_TEST_SUITE(Charge) {
 
             me.To(213).CheckByKeys(6, 13, 0, TMap<TGroupId, TArr>{
                 {TGroupId{0}, {1, 2, 3, 4_I}},
-                {TGroupId{1}, {2_g, 3, 4}}, // pages 3, 4 are always neded
+                {TGroupId{1}, {2_g, 3, 4}}, // pages 3, 4 are always needed
                 {TGroupId{2}, {4_g, 5_g, 6, 7, 8, 9_g}} // pages 6, 7, 8 are always needed
             });
 
@@ -646,7 +652,7 @@ Y_UNIT_TEST_SUITE(Charge) {
 
             me.To(216).CheckByKeys(7, 13, 0, TMap<TGroupId, TArr>{
                 {TGroupId{0}, {1, 2, 3, 4_I}},
-                {TGroupId{1}, {2_g, 3, 4}}, // pages 3, 4 are always neded
+                {TGroupId{1}, {2_g, 3, 4}}, // pages 3, 4 are always needed
                 {TGroupId{2}, {5_g, 6, 7, 8, 9_g}} // pages 6, 7, 8 are always needed
             });
 
@@ -664,7 +670,7 @@ Y_UNIT_TEST_SUITE(Charge) {
 
             me.To(219).CheckByKeys(8, 13, 0, TMap<TGroupId, TArr>{
                 {TGroupId{0}, {1, 2, 3, 4_I}},
-                {TGroupId{1}, {3, 4}}, // pages 3, 4 are always neded
+                {TGroupId{1}, {3, 4}}, // pages 3, 4 are always needed
                 {TGroupId{2}, {6, 7, 8, 9_g}} // pages 6, 7, 8 are always needed
             });
 
@@ -747,13 +753,13 @@ Y_UNIT_TEST_SUITE(Charge) {
 
         me.To(101).CheckByKeys(5, 13, 999, TMap<TGroupId, TArr>{
             {TGroupId{0}, {1, 2, 3, 4_I}},
-            {TGroupId{1}, {1_g, 2_g, 3, 4}}, // pages 3, 4 are always neded
+            {TGroupId{1}, {1_g, 2_g, 3, 4}}, // pages 3, 4 are always needed
             {TGroupId{2}, {3_g, 4_g, 5_g, 6, 7, 8, 9_g}} // pages 6, 7, 8 are always needed
         });
 
         me.To(102).CheckByKeys(5, 13, 7, TMap<TGroupId, TArr>{
             {TGroupId{0}, {1, 2, 3, 4_I}},
-            {TGroupId{1}, {1_g, 2_g, 3, 4}}, // pages 3, 4 are always neded
+            {TGroupId{1}, {1_g, 2_g, 3, 4}}, // pages 3, 4 are always needed
             {TGroupId{2}, {3_g, 4_g, 5_g, 6, 7, 8, 9_g}} // pages 6, 7, 8 are always needed
         });
 
@@ -771,8 +777,8 @@ Y_UNIT_TEST_SUITE(Charge) {
 
         me.To(105).CheckByKeys(5, 13, 4, TMap<TGroupId, TArr>{
             {TGroupId{0}, {1, 2, 3_f, 4_f}},
-            {TGroupId{1}, {1_g, 2_g, 3, 4_f}}, // here we touh extra pages, but it's fine
-            {TGroupId{2}, {3_g, 4_g, 5_g, 6, 7, 8_f}} // here we touh extra pages, but it's fine
+            {TGroupId{1}, {1_g, 2_g, 3, 4_f}}, // here we touch extra pages, but it's fine
+            {TGroupId{2}, {3_g, 4_g, 5_g, 6, 7, 8_f}} // here we touch extra pages, but it's fine
         });
 
         me.To(106).CheckByKeys(7, 13, 3, TMap<TGroupId, TArr>{
@@ -911,12 +917,12 @@ Y_UNIT_TEST_SUITE(Charge) {
 
         me.To(200).CheckByKeysReverse(15, 3, 6, TMap<TGroupId, TArr>{
             {TGroupId{0}, {3, 2, 1, 0_f}},
-            {TGroupId{2}, {11_g, 10_g, 9_g, 8, 7, 6, 5, 4_f, 3_f}} // here we touh extra pages, but it's fine
+            {TGroupId{2}, {11_g, 10_g, 9_g, 8, 7, 6, 5, 4_f, 3_f}} // here we touch extra pages, but it's fine
         });
 
         me.To(201).CheckByKeysReverse(15, 3, 5, TMap<TGroupId, TArr>{
             {TGroupId{0}, {3, 2, 1_f}},
-            {TGroupId{2}, {11_g, 10_g, 9_g, 8, 7, 6, 5_f, 4_f, 3_f}} // here we touh extra pages, but it's fine
+            {TGroupId{2}, {11_g, 10_g, 9_g, 8, 7, 6, 5_f, 4_f, 3_f}} // here we touch extra pages, but it's fine
         });
 
         me.To(202).CheckByKeysReverse(15, 5, 5, TMap<TGroupId, TArr>{
@@ -926,22 +932,22 @@ Y_UNIT_TEST_SUITE(Charge) {
 
         me.To(203).CheckByKeysReverse(13, 3, 4, TMap<TGroupId, TArr>{
             {TGroupId{0}, {3, 2, 1}},
-            {TGroupId{2}, {9_g, 8, 7, 6, 5, 4_f}} // here we touh extra pages, but it's fine
+            {TGroupId{2}, {9_g, 8, 7, 6, 5, 4_f}} // here we touch extra pages, but it's fine
         });
 
         me.To(204).CheckByKeysReverse(13, 3, 3, TMap<TGroupId, TArr>{
             {TGroupId{0}, {3, 2, 1_f}},
-            {TGroupId{2}, {9_g, 8, 7, 6, 5_f}} // here we touh extra pages, but it's fine
+            {TGroupId{2}, {9_g, 8, 7, 6, 5_f}} // here we touch extra pages, but it's fine
         });
 
         me.To(205).CheckByKeysReverse(13, 3, 2, TMap<TGroupId, TArr>{
             {TGroupId{0}, {3, 2}},
-            {TGroupId{2}, {9_g, 8, 7, 6_f}} // here we touh extra pages, but it's fine
+            {TGroupId{2}, {9_g, 8, 7, 6_f}} // here we touch extra pages, but it's fine
         });
 
         me.To(206).CheckByKeysReverse(13, 3, 1, TMap<TGroupId, TArr>{
             {TGroupId{0}, {3, 2}},
-            {TGroupId{2}, {9_g, 8, 7_f}} // here we touh extra pages, but it's fine
+            {TGroupId{2}, {9_g, 8, 7_f}} // here we touch extra pages, but it's fine
         });
     }
 
@@ -1207,10 +1213,102 @@ Y_UNIT_TEST_SUITE(Charge) {
             {TGroupId{2}, {2, 3, 4, 5, 6}}
         });
 
-        me.To(107).CheckByRows(4, 8, 0, TMap<TGroupId, TArr>{
-            {TGroupId{0}, {1, 2}},
-            {TGroupId{1}, {2, 3, 4}},
-            {TGroupId{2}, {4, 5, 6, 7, 8}}
+        me.To(107).CheckByRows(3, 5, 0, TMap<TGroupId, TArr>{
+            {TGroupId{0}, {1}},
+            {TGroupId{1}, {1, 2}},
+            {TGroupId{2}, {3, 4, 5}}
+        });
+
+        me.To(200).CheckByRows(8, 7, 0, TMap<TGroupId, TArr>{
+            {TGroupId{0}, {2}},
+            {TGroupId{1}, {}},
+            {TGroupId{2}, {}}
+        });
+
+        me.To(201).CheckByRows(7, 1, 0, TMap<TGroupId, TArr>{
+            {TGroupId{0}, {2}},
+            {TGroupId{1}, {}},
+            {TGroupId{2}, {}}
+        });
+    }
+
+    Y_UNIT_TEST(ByRowsReverse)
+    {
+        TModel me(true);
+
+        /*
+        
+        row ids by pages:
+        group0 = ..|18 19 20|21 22 23|24 25 26|
+        group1 = ..|18 19|20 21|22 23|24 25|26|
+        group2 = ..|18|19|20|21|22|23|24|25|26|
+
+        */
+
+        me.To(100).CheckByRowsReverse(26, 26, 0, TMap<TGroupId, TArr>{
+            {TGroupId{0}, {8}},
+            {TGroupId{1}, {13}},
+            {TGroupId{2}, {26}}
+        });
+
+        me.To(101).CheckByRowsReverse(26, 25, 0, TMap<TGroupId, TArr>{
+            {TGroupId{0}, {8}},
+            {TGroupId{1}, {13, 12}},
+            {TGroupId{2}, {26, 25}}
+        });
+
+        me.To(102).CheckByRowsReverse(26, 24, 0, TMap<TGroupId, TArr>{
+            {TGroupId{0}, {8}},
+            {TGroupId{1}, {13, 12}},
+            {TGroupId{2}, {26, 25, 24}}
+        });
+
+        me.To(103).CheckByRowsReverse(26, 23, 0, TMap<TGroupId, TArr>{
+            {TGroupId{0}, {8, 7}},
+            {TGroupId{1}, {13, 12, 11}},
+            {TGroupId{2}, {26, 25, 24, 23}}
+        });
+
+        me.To(104).CheckByRowsReverse(26, 22, 0, TMap<TGroupId, TArr>{
+            {TGroupId{0}, {8, 7}},
+            {TGroupId{1}, {13, 12, 11}},
+            {TGroupId{2}, {26, 25, 24, 23, 22}}
+        });
+
+        me.To(105).CheckByRowsReverse(25, 19, 0, TMap<TGroupId, TArr>{
+            {TGroupId{0}, {8, 7, 6}},
+            {TGroupId{1}, {12, 11, 10, 9}},
+            {TGroupId{2}, {25, 24, 23, 22, 21, 20, 19}}
+        });
+
+        me.To(106).CheckByRowsReverse(24, 20, 0, TMap<TGroupId, TArr>{
+            {TGroupId{0}, {8, 7, 6}},
+            {TGroupId{1}, {12, 11, 10}},
+            {TGroupId{2}, {24, 23, 22, 21, 20}}
+        });
+
+        me.To(107).CheckByRowsReverse(23, 21, 0, TMap<TGroupId, TArr>{
+            {TGroupId{0}, {7}},
+            {TGroupId{1}, {11, 10}},
+            {TGroupId{2}, {23, 22, 21}}
+        });
+
+        me.To(200).CheckByRowsReverse(8, 9, 0, TMap<TGroupId, TArr>{
+            {TGroupId{0}, {2}},
+            {TGroupId{1}, {}},
+            {TGroupId{2}, {}}
+        });
+
+        me.To(201).CheckByRowsReverse(9, 15, 0, TMap<TGroupId, TArr>{
+            {TGroupId{0}, {3}},
+            {TGroupId{1}, {}},
+            {TGroupId{2}, {}}
+        });
+
+        me.To(202).CheckByRowsReverse(100, 23, 0, TMap<TGroupId, TArr>{
+            {TGroupId{0}, {8, 7}},
+            {TGroupId{1}, {13, 12, 11}},
+            {TGroupId{2}, {26, 25, 24, 23}}
         });
     }
 
@@ -1229,36 +1327,88 @@ Y_UNIT_TEST_SUITE(Charge) {
 
         // Precharge touches [row1 .. Min(row2, row1 + itemsLimit)] rows (1 extra row)
         
-        me.To(101).CheckByRows(1, 7, 6, TMap<TGroupId, TArr>{
+        me.To(100).CheckByRows(1, 7, 6, TMap<TGroupId, TArr>{
             {TGroupId{0}, {0, 1, 2}},
             {TGroupId{1}, {0, 1, 2, 3}},
             {TGroupId{2}, {1, 2, 3, 4, 5, 6, 7}}
         });
 
-        me.To(102).CheckByRows(1, 7, 5, TMap<TGroupId, TArr>{
+        me.To(101).CheckByRows(1, 7, 5, TMap<TGroupId, TArr>{
             {TGroupId{0}, {0, 1, 2}},
             {TGroupId{1}, {0, 1, 2, 3}},
             {TGroupId{2}, {1, 2, 3, 4, 5, 6}}
         });
 
-        me.To(103).CheckByRows(1, 7, 4, TMap<TGroupId, TArr>{
+        me.To(102).CheckByRows(1, 7, 4, TMap<TGroupId, TArr>{
             {TGroupId{0}, {0, 1}},
             {TGroupId{1}, {0, 1, 2}},
             {TGroupId{2}, {1, 2, 3, 4, 5}}
         });
 
-        me.To(104).CheckByRows(1, 7, 1, TMap<TGroupId, TArr>{
+        me.To(103).CheckByRows(1, 7, 1, TMap<TGroupId, TArr>{
             {TGroupId{0}, {0}},
             {TGroupId{1}, {0, 1}},
             {TGroupId{2}, {1, 2}}
         });
 
-        me.To(105).CheckByRows(3, 20, 5, TMap<TGroupId, TArr>{
+        me.To(104).CheckByRows(3, 20, 5, TMap<TGroupId, TArr>{
             {TGroupId{0}, {1, 2}},
             {TGroupId{1}, {1, 2, 3, 4}},
             {TGroupId{2}, {3, 4, 5, 6, 7, 8}}
         });
     }
+
+    Y_UNIT_TEST(ByRowsLimitsReverse)
+    {
+        TModel me(true);
+
+        /*
+        
+        row ids by pages:
+        group0 = ..|18 19 20|21 22 23|24 25 26|
+        group1 = ..|18 19|20 21|22 23|24 25|26|
+        group2 = ..|18|19|20|21|22|23|24|25|26|
+
+        */
+
+        // Precharge touches [Max(row2, row1 - itemsLimit) .. row1] rows (1 extra row)
+        
+        me.To(100).CheckByRowsReverse(25, 19, 6, TMap<TGroupId, TArr>{
+            {TGroupId{0}, {8, 7, 6}},
+            {TGroupId{1}, {12, 11, 10, 9}},
+            {TGroupId{2}, {25, 24, 23, 22, 21, 20, 19}}
+        });
+
+        me.To(101).CheckByRowsReverse(25, 19, 5, TMap<TGroupId, TArr>{
+            {TGroupId{0}, {8, 7, 6}},
+            {TGroupId{1}, {12, 11, 10}},
+            {TGroupId{2}, {25, 24, 23, 22, 21, 20}}
+        });
+
+        me.To(102).CheckByRowsReverse(25, 19, 4, TMap<TGroupId, TArr>{
+            {TGroupId{0}, {8, 7}},
+            {TGroupId{1}, {12, 11, 10}},
+            {TGroupId{2}, {25, 24, 23, 22, 21}}
+        });
+
+        me.To(103).CheckByRowsReverse(25, 19, 1, TMap<TGroupId, TArr>{
+            {TGroupId{0}, {8}},
+            {TGroupId{1}, {12}},
+            {TGroupId{2}, {25, 24}}
+        });
+
+        me.To(104).CheckByRowsReverse(23, 3, 5, TMap<TGroupId, TArr>{
+            {TGroupId{0}, {7, 6}},
+            {TGroupId{1}, {11, 10, 9}},
+            {TGroupId{2}, {23, 22, 21, 20, 19, 18}}
+        });
+
+        me.To(200).CheckByRowsReverse(100, 3, 5, TMap<TGroupId, TArr>{
+            {TGroupId{0}, {8, 7}},
+            {TGroupId{1}, {13, 12, 11, 10}},
+            {TGroupId{2}, {26, 25, 24, 23, 22, 21}}
+        });
+    }
 }
 
 }