Browse Source

KIKIMR-19139 Load index in Precharge

kungasc 1 year ago
parent
commit
55d8ae0f29

+ 91 - 43
ydb/core/tablet_flat/flat_part_charge.h

@@ -4,6 +4,7 @@
 #include "flat_table_part.h"
 #include "flat_part_iface.h"
 #include "flat_part_slice.h"
+#include "flat_part_index_iter.h"
 
 #include <util/generic/bitmap.h>
 
@@ -15,7 +16,8 @@ namespace NTable {
         using TCells = NPage::TCells;
         using TIter = NPage::TIndex::TIter;
         using TDataPage = NPage::TDataPage;
-
+        using TGroupId = NPage::TGroupId;
+        
         struct TResult {
             bool Ready;     /* All required pages are already in memory */
             bool Overshot;  /* Search may start outside of bounds */
@@ -25,21 +27,21 @@ namespace NTable {
             : Env(env)
             , Part(&part)
             , Scheme(*Part->Scheme)
-            , Index(Part->Index)
-            , HistoryIndex(
-                    includeHistory && Part->HistoricIndexes
-                    ? &Part->GetGroupIndex(NPage::TGroupId(0, true))
-                    : nullptr)
+            , Index(Part, Env, TGroupId())
         {
+            if (includeHistory && Part->IndexPages.Historic) {
+                HistoryIndex.emplace(Part, Env, TGroupId(0, true));
+            }
+
             TDynBitMap seen;
             for (TTag tag : tags) {
                 if (const auto* col = Scheme.FindColumnByTag(tag)) {
                     if (col->Group != 0 && !seen.Get(col->Group)) {
                         NPage::TGroupId groupId(col->Group);
-                        Groups.emplace_back(Part->GetGroupIndex(groupId), groupId);
+                        Groups.emplace_back(TPartIndexIt(Part, Env, groupId), groupId);
                         if (HistoryIndex) {
                             NPage::TGroupId historyGroupId(col->Group, true);
-                            HistoryGroups.emplace_back(Part->GetGroupIndex(historyGroupId), historyGroupId);
+                            HistoryGroups.emplace_back(TPartIndexIt(Part, Env, historyGroupId), historyGroupId);
                         }
                         seen.Set(col->Group);
                     }
@@ -178,16 +180,21 @@ namespace NTable {
             Y_VERIFY_DEBUG(beginRowId < endRowId, "Unexpected empty row range");
             Y_VERIFY_DEBUG(!Groups, "Unexpected column groups during SplitKey precharge");
 
+            auto index = Index.TryLoadRaw();
+            if (!index) {
+                return false;
+            }
+
             bool ready = true;
 
             // The first page that may contain splitKey
-            auto found = Index.LookupKey(splitKey, Scheme.Groups[0], ESeek::Lower, &keyDefaults);
+            auto found = index->LookupKey(splitKey, Scheme.Groups[0], ESeek::Lower, &keyDefaults);
 
             // Note: as we may have cut index key we may both need prev and next pages
 
             if (auto prev = found; prev.Off() && --prev) {
                 TRowId pageBegin = prev->GetRowId();
-                TRowId pageEnd = found ? found->GetRowId() : Index.GetEndRowId();
+                TRowId pageEnd = found ? found->GetRowId() : index->GetEndRowId();
                 if (pageBegin < endRowId && beginRowId < pageEnd) {
                     ready &= bool(Env->TryGetPage(Part, prev->GetPageId()));
                 }
@@ -197,7 +204,7 @@ namespace NTable {
                 bool needNext = true;
                 if (found->GetRowId() < beginRowId) {
                     // iterator may re-seek to the first page that's in range
-                    auto adjusted = Index.LookupRow(beginRowId, found);
+                    auto adjusted = index->LookupRow(beginRowId, found);
                     if (found != adjusted) {
                         found = adjusted;
                         needNext = false;
@@ -227,20 +234,25 @@ namespace NTable {
         bool Do(const TRowId row1, const TRowId row2,
                     const TKeyCellDefaults &keyDefaults, ui64 itemsLimit, ui64 bytesLimit) const noexcept
         {
+            auto index = Index.TryLoadRaw();
+            if (!index) {
+                return false;
+            }
+
             auto startRow = row1;
             auto endRow = row2;
 
             // Page that contains row1
-            auto first = Index.LookupRow(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);
+            auto last = index->LookupRow(row2, first);
             if (Y_UNLIKELY(last < first)) {
                 last = first;
-                endRow = Min(endRow, Index.GetLastRowId(last));
+                endRow = Min(endRow, index->GetLastRowId(last));
             }
 
             return DoPrecharge(TCells{}, TCells{}, TIter{}, TIter{}, first, last, startRow, endRow, keyDefaults, itemsLimit, bytesLimit);
@@ -254,22 +266,27 @@ namespace NTable {
         bool DoReverse(const TRowId row1, const TRowId row2, 
                 const TKeyCellDefaults &keyDefaults, ui64 itemsLimit, ui64 bytesLimit) const noexcept
         {
+            auto index = Index.TryLoadRaw();
+            if (!index) {
+                return false;
+            }
+
             auto startRow = row1;
             auto endRow = row2;
 
             // Page that contains row1
-            auto first = Index.LookupRow(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();
+                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);
+            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());
@@ -285,12 +302,17 @@ namespace NTable {
                 const TRowId row2, const TKeyCellDefaults &keyDefaults, ui64 itemsLimit,
                 ui64 bytesLimit) const noexcept
         {
+            auto index = Index.TryLoadRaw();
+            if (!index) {
+                return { false, false };
+            }
+
             auto startRow = row1;
             auto endRow = row2;
             bool overshot = !key2; // +inf is always on the next slice
 
             // First page to precharge (contains row1)
-            auto first = Index.LookupRow(row1);
+            auto first = index->LookupRow(row1);
             if (Y_UNLIKELY(!first)) {
                 return { true, true }; // already out of bounds, nothing to precharge
             }
@@ -299,16 +321,16 @@ namespace NTable {
             auto firstExt = first;
 
             // Last page to precharge (contains row2)
-            auto last = Index.LookupRow(row2, first);
+            auto last = index->LookupRow(row2, first);
             if (Y_UNLIKELY(last < first)) {
                 last = first; // will not go past the first page
-                endRow = Min(endRow, Index.GetLastRowId(last));
+                endRow = Min(endRow, index->GetLastRowId(last));
             }
 
             TIter key1Page;
             if (key1) {
                 // First page to precharge (may contain key >= key1)
-                key1Page = Index.LookupKey(key1, Scheme.Groups[0], ESeek::Lower, &keyDefaults);
+                key1Page = index->LookupKey(key1, Scheme.Groups[0], ESeek::Lower, &keyDefaults);
                 if (!key1Page || key1Page > last) {
                     return { true, true }; // first key is outside of bounds
                 }
@@ -329,7 +351,7 @@ namespace NTable {
             if (key2) {
                 // Last page to precharge (may contain key >= key2)
                 // We actually use the next page since lookup is not exact
-                key2Page = Index.LookupKey(key2, Scheme.Groups[0], ESeek::Lower, &keyDefaults);
+                key2Page = index->LookupKey(key2, Scheme.Groups[0], ESeek::Lower, &keyDefaults);
                 auto key2PageExt = key2Page + 1;
                 if (key2PageExt && key2PageExt <= last) {
                     last = Max(key2PageExt, firstExt);
@@ -351,16 +373,21 @@ namespace NTable {
                 const TRowId row2, const TKeyCellDefaults &keyDefaults, ui64 itemsLimit,
                 ui64 bytesLimit) const noexcept
         {
+            auto index = Index.TryLoadRaw();
+            if (!index) {
+                return { false, false };
+            }
+
             auto startRow = row1;
             auto endRow = row2;
             bool overshot = !key2; // +inf is always on the next slice
 
             // First page to precharge (contains row1)
-            auto first = Index.LookupRow(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();
+                startRow = Min(row1, index->GetEndRowId() - 1);
+                first = --(*index)->End();
                 if (Y_UNLIKELY(!first)) {
                     return { true, true }; // empty index?
                 }
@@ -370,7 +397,7 @@ namespace NTable {
             auto firstExt = first;
 
             // Last page to precharge (contains row2)
-            auto last = Index.LookupRow(row2, first);
+            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());
@@ -379,14 +406,14 @@ namespace NTable {
             TIter key1Page;
             if (key1) {
                 // First page to precharge (may contain key <= key1)
-                key1Page = Index.LookupKeyReverse(key1, Scheme.Groups[0], ESeek::Lower, &keyDefaults);
+                key1Page = index->LookupKeyReverse(key1, Scheme.Groups[0], ESeek::Lower, &keyDefaults);
                 if (!key1Page || key1Page < last) {
                     return { true, true }; // first key is outside of bounds
                 }
                 if (first >= key1Page) {
                     first = key1Page; // use the minimum
                     firstExt = key1Page - 1; // first key <= key1 might be on the next page
-                    startRow = Min(startRow, Index.GetLastRowId(first));
+                    startRow = Min(startRow, index->GetLastRowId(first));
                     if (key1Page.Off() == 0 || last > firstExt) {
                         firstExt = last; // never precharge past the last page
                         overshot = true; // may have to touch the next slice
@@ -400,11 +427,11 @@ namespace NTable {
             if (key2) {
                 // Last page to precharge (may contain key <= key2)
                 // We actually use the next page since lookup is not exact
-                key2Page = Index.LookupKeyReverse(key2, Scheme.Groups[0], ESeek::Lower, &keyDefaults);
+                key2Page = index->LookupKeyReverse(key2, Scheme.Groups[0], ESeek::Lower, &keyDefaults);
                 auto key2PageExt = key2Page - 1;
                 if (key2Page && key2Page.Off() != 0 && key2PageExt >= last) {
                     last = Min(key2PageExt, firstExt);
-                    endRow = Max(endRow, Index.GetLastRowId(last)); // may load the last row of key2PageExt
+                    endRow = Max(endRow, index->GetLastRowId(last)); // may load the last row of key2PageExt
                 } else {
                     overshot = true; // may find first key < key2 on row < row2
                 }
@@ -617,7 +644,10 @@ namespace NTable {
 
     private:
         bool DoPrechargeHistory(TRowId startRowId, TRowId endRowId) const noexcept {
-            using TCells = NPage::TCells;
+            auto index = HistoryIndex->TryLoadRaw();
+            if (!index) {
+                return false;
+            }
 
             if (endRowId < startRowId) {
                 using std::swap;
@@ -651,12 +681,12 @@ namespace NTable {
             // Directly use the histroy key defaults with correct sort order
             const TKeyCellDefaults* keyDefaults = Part->Scheme->HistoryKeys.Get();
 
-            auto first = HistoryIndex->LookupKey(startKey, scheme, ESeek::Lower, keyDefaults);
+            auto first = index->LookupKey(startKey, scheme, ESeek::Lower, keyDefaults);
             if (!first) {
                 return true;
             }
 
-            auto last = HistoryIndex->LookupKey(endKey, scheme, ESeek::Lower, keyDefaults);
+            auto last = index->LookupKey(endKey, scheme, ESeek::Lower, keyDefaults);
 
             bool ready = true;
             bool hasItems = false;
@@ -717,12 +747,12 @@ namespace NTable {
 
     private:
         struct TGroupState {
-            const NPage::TIndex& GroupIndex;
+            TPartIndexIt GroupIndex;
             TIter Index;
             TRowId LastRowId = Max<TRowId>();
             const NPage::TGroupId GroupId;
 
-            TGroupState(const NPage::TIndex& groupIndex, NPage::TGroupId groupId)
+            TGroupState(TPartIndexIt&& groupIndex, NPage::TGroupId groupId)
                 : GroupIndex(groupIndex)
                 , GroupId(groupId)
             { }
@@ -733,16 +763,25 @@ 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();
+            if (!groupIndex) {
+                if (bytes) {
+                    // Note: we can't continue if we have bytes limit
+                    bytes = Max<ui64>();
+                }
+                return false;
+            }
+
             bool ready = true;
 
             if (!g.Index || row1 < g.Index->GetRowId() || row1 > g.LastRowId) {
-                g.Index = g.GroupIndex.LookupRow(row1, g.Index);
+                g.Index = groupIndex->LookupRow(row1, g.Index);
                 if (Y_UNLIKELY(!g.Index)) {
                     // Looks like row1 doesn't even exist
                     g.LastRowId = Max<TRowId>();
                     return ready;
                 }
-                g.LastRowId = g.GroupIndex.GetLastRowId(g.Index);
+                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);
@@ -754,7 +793,7 @@ namespace NTable {
                     g.LastRowId = Max<TRowId>();
                     return ready;
                 }
-                g.LastRowId = g.GroupIndex.GetLastRowId(g.Index);
+                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);
@@ -767,16 +806,25 @@ 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();
+            if (!groupIndex) {
+                if (bytes) {
+                    // Note: we can't continue if we have bytes limit
+                    bytes = Max<ui64>();
+                }
+                return false;
+            }
+
             bool ready = true;
 
             if (!g.Index || row1 < g.Index->GetRowId() || row1 > g.LastRowId) {
-                g.Index = g.GroupIndex.LookupRow(row1, g.Index);
+                g.Index = groupIndex->LookupRow(row1, g.Index);
                 if (Y_UNLIKELY(!g.Index)) {
                     // Looks like row1 doesn't even exist
                     g.LastRowId = Max<TRowId>();
                     return ready;
                 }
-                g.LastRowId = g.GroupIndex.GetLastRowId(g.Index);
+                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);
@@ -826,8 +874,8 @@ namespace NTable {
         IPages * const Env = nullptr;
         const TPart * const Part = nullptr;
         const TPartScheme &Scheme;
-        const NPage::TIndex &Index;
-        const NPage::TIndex* const HistoryIndex;
+        mutable TPartIndexIt Index;
+        mutable std::optional<TPartIndexIt> HistoryIndex;
         mutable TSmallVec<TGroupState> Groups;
         mutable TSmallVec<TGroupState> HistoryGroups;
     };

+ 7 - 1
ydb/core/tablet_flat/flat_part_index_iter.h

@@ -67,6 +67,7 @@ public:
             return EReady::Page;
         }
         if (Iter.Off() == 0) {
+            Iter = { };
             return EReady::Gone;
         }
         Iter--;
@@ -77,6 +78,11 @@ public:
         return bool(Iter);
     }
 
+    // for precharge only
+    TIndex* TryLoadRaw() {
+        return TryGetIndex();
+    }
+
 public:
     TRowId GetEndRowId() {
         return EndRowId;
@@ -91,7 +97,7 @@ public:
         Y_VERIFY(Index);
         return Iter->GetRowId();
     }
-    
+
     TRowId GetNextRowId() {
         Y_VERIFY(Index);
         auto next = Iter + 1;

+ 253 - 26
ydb/core/tablet_flat/ut/ut_charge.cpp

@@ -17,11 +17,12 @@ namespace {
     enum TPageIdFlags {
         IfIter = 1,
         IfFail = 2,
-        IfNoFail = 4
+        IfNoFail = 4,
+        IfSticky = 8
     };
     struct TFlaggedPageId {
         TPageId Page;
-        TPageIdFlags Flags = static_cast<TPageIdFlags>(TPageIdFlags::IfIter | TPageIdFlags::IfFail | TPageIdFlags::IfNoFail);
+        TPageIdFlags Flags = static_cast<TPageIdFlags>(TPageIdFlags::IfIter | TPageIdFlags::IfFail | TPageIdFlags::IfNoFail | TPageIdFlags::IfSticky);
 
         TFlaggedPageId(TPageId page)
             : Page(page) {}
@@ -46,21 +47,27 @@ namespace {
     }
 
     struct TTouchEnv : public NTest::TTestEnv {
-        TTouchEnv(bool fail) : Fail(fail) { }
+        TTouchEnv(bool fail, TSet<std::pair<TGroupId, TPageId>> sticky) 
+            : Fail(fail)
+            , Sticky(std::move(sticky))
+            { }
 
         const TSharedData* TryGetPage(const TPart *part, TPageId id, TGroupId groupId) override
         {
-            if (part->IndexPages.Has(groupId, id)) {
-                // TODO: delete after index precharge
+            Touched[groupId].insert(id);
+            
+            if (!Fail || Sticky.contains({groupId, id})) {
                 return NTest::TTestEnv::TryGetPage(part, id, groupId);
             }
-            
-            Touched[groupId].insert(id);
-            return Fail ? nullptr : NTest::TTestEnv::TryGetPage(part, id, groupId);
+
+            ToLoad[groupId].insert(id);
+            return nullptr;
         }
 
         const bool Fail = false;
+        TSet<std::pair<TGroupId, TPageId>> Sticky;
         TMap<TGroupId, TSet<TPageId>> Touched;
+        TMap<TGroupId, TSet<TPageId>> ToLoad;
     };
 
     struct TCooker {
@@ -141,29 +148,39 @@ namespace {
 
         void CheckByKeys(ui32 lower, ui32 upper, ui64 items, const TMap<TGroupId, TArr>& shouldPrecharge) const
         {
-            CheckPrechargeByKeys(lower, upper, items, false, shouldPrecharge, false);
-            CheckPrechargeByKeys(lower, upper, items, true, shouldPrecharge, false);
+            CheckPrechargeByKeys(lower, upper, items, TPageIdFlags::IfNoFail, shouldPrecharge, false, GetIndexPages());
+            CheckPrechargeByKeys(lower, upper, items, TPageIdFlags::IfFail, shouldPrecharge, false, GetIndexPages());
             CheckIterByKeys(lower, upper, items ? items : Max<ui32>(), shouldPrecharge);
         }
 
         void CheckByKeysReverse(ui32 lower, ui32 upper, ui64 items, const TMap<TGroupId, TArr>& shouldPrecharge) const
         {
-            CheckPrechargeByKeys(lower, upper, items, false, shouldPrecharge, true);
-            CheckPrechargeByKeys(lower, upper, items, true, shouldPrecharge, true);
+            CheckPrechargeByKeys(lower, upper, items, TPageIdFlags::IfNoFail, shouldPrecharge, true, GetIndexPages());
+            CheckPrechargeByKeys(lower, upper, items, TPageIdFlags::IfFail, shouldPrecharge, true, GetIndexPages());
             CheckIterByKeysReverse(lower, upper, items ? items : Max<ui32>(), shouldPrecharge);
         }
 
-        void CheckByRows(TPageId row1, TPageId row2, ui64 items, TMap<TGroupId, TArr> shouldPrecharge) const
+        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);
         }
 
-        void CheckPrechargeByKeys(ui32 lower, ui32 upper, ui64 items, bool fail, const TMap<TGroupId, TArr>& shouldPrecharge, bool reverse) const
+        void CheckIndex(ui32 lower, ui32 upper, ui64 items, const TMap<TGroupId, TArr>& shouldPrecharge, TSet<TPageId> stickyIndex) const {
+            TSet<std::pair<TGroupId, TPageId>> sticky;
+            for (auto x : stickyIndex) {
+                sticky.insert({TGroupId{}, x});
+            }
+
+            CheckPrechargeByKeys(lower, upper, items, static_cast<TPageIdFlags>(TPageIdFlags::IfFail | TPageIdFlags::IfSticky), shouldPrecharge, false, sticky);
+        }
+
+        void CheckPrechargeByKeys(ui32 lower, ui32 upper, ui64 items, TPageIdFlags flags, const TMap<TGroupId, TArr>& shouldPrecharge, bool reverse, TSet<std::pair<TGroupId, TPageId>> sticky) const
         {
             Y_VERIFY(lower < Mass.Saved.Size() && upper < Mass.Saved.Size());
 
-            TTouchEnv env(fail);
+            bool fail(flags & TPageIdFlags::IfFail);
+            TTouchEnv env(fail, sticky);
 
             const auto &keyDefaults = *Tool.Scheme.Keys;
             const auto from = Tool.KeyCells(Mass.Saved[lower]);
@@ -184,16 +201,17 @@ namespace {
                 ? TCharge::Range(&env, from, to, run, keyDefaults, tags, items, Max<ui64>(), true)
                 : TCharge::RangeReverse(&env, from, to, run, keyDefaults, tags, items, Max<ui64>(), true);
 
-            UNIT_ASSERT_VALUES_EQUAL_C(!fail || env.Touched.empty(), ready, AssertMesage(fail));
+            UNIT_ASSERT_VALUES_EQUAL_C(!fail || env.ToLoad.empty(), ready, AssertMesage(fail));
 
-            AssertEqual(env.Touched, shouldPrecharge, fail ? TPageIdFlags::IfFail : TPageIdFlags::IfNoFail);
+            CheckPrecharged(env.Touched, shouldPrecharge, sticky, flags);
         }
 
         void CheckPrechargeByRows(TPageId row1, TPageId row2, ui64 items, bool fail, TMap<TGroupId, TArr> shouldPrecharge) const
         {
             Y_VERIFY(row1 <= row2 && row2 < 3 * 9);
 
-            TTouchEnv env(fail);
+            auto sticky = GetIndexPages();
+            TTouchEnv env(fail, sticky);
 
             const auto &keyDefaults = *Tool.Scheme.Keys;
             
@@ -213,14 +231,15 @@ namespace {
                 TCharge(&env, *run.begin()->Part, tags, false).Do(row1, row2, keyDefaults, items, Max<ui64>()),
                 AssertMesage(fail));
 
-            AssertEqual(env.Touched, shouldPrecharge, fail ? TPageIdFlags::IfFail : TPageIdFlags::IfNoFail);
+            CheckPrecharged(env.Touched, shouldPrecharge, sticky, fail ? TPageIdFlags::IfFail : TPageIdFlags::IfNoFail);
         }
 
         void CheckIterByKeys(ui32 lower, ui32 upper, ui64 items, const TMap<TGroupId, TArr>& precharged) const
         {
             Y_VERIFY(lower < Mass.Saved.Size() && upper < Mass.Saved.Size());
 
-            NTest::TCheckIt wrap(Eggs, { new TTouchEnv(false) });
+            auto sticky = GetIndexPages();
+            NTest::TCheckIt wrap(Eggs, { new TTouchEnv(false, sticky) });
 
             wrap.To(CurrentStep());
             wrap.StopAfter(Tool.KeyCells(Mass.Saved[upper]));
@@ -248,14 +267,15 @@ namespace {
 
             auto env = wrap.Displace<TTouchEnv>(nullptr);
 
-            AssertEqual(env->Touched, precharged, TPageIdFlags::IfIter);
+            CheckPrecharged(env->Touched, precharged, sticky, TPageIdFlags::IfIter);
         }
 
         void CheckIterByKeysReverse(ui32 lower, ui32 upper, ui64 items, const TMap<TGroupId, TArr>& precharged) const
         {
             Y_VERIFY(lower < Mass.Saved.Size() && upper < Mass.Saved.Size());
 
-            NTest::TCheckReverseIt wrap(Eggs, { new TTouchEnv(false) });
+            auto sticky = GetIndexPages();
+            NTest::TCheckReverseIt wrap(Eggs, { new TTouchEnv(false, sticky) });
 
             wrap.To(CurrentStep());
             wrap.StopAfter(Tool.KeyCells(Mass.Saved[upper]));
@@ -283,7 +303,7 @@ namespace {
 
             auto env = wrap.Displace<TTouchEnv>(nullptr);
 
-            AssertEqual(env->Touched, precharged, TPageIdFlags::IfIter);
+            CheckPrecharged(env->Touched, precharged, sticky, TPageIdFlags::IfIter);
         }
 
         const NTest::TMass Mass;
@@ -291,7 +311,23 @@ namespace {
         const NTest::TRowTool Tool;
 
     private:
-        void AssertEqual(const TMap<TGroupId, TSet<TPageId>>& actual, const TMap<TGroupId, TArr>& expected, TPageIdFlags flags) const {
+        TSet<std::pair<TGroupId, TPageId>> GetIndexPages() const {
+            TSet<std::pair<TGroupId, TPageId>> result;
+
+            auto &pages = Eggs.Lone()->IndexPages;
+            TGroupId mainGroupId{};
+            
+            for (auto x : pages.Groups) {
+                result.insert({mainGroupId, x});
+            }
+            for (auto x : pages.Historic) {
+                result.insert({mainGroupId, x});
+            }
+
+            return result;
+        }
+
+        void CheckPrecharged(const TMap<TGroupId, TSet<TPageId>>& actual, const TMap<TGroupId, TArr>& expected, TSet<std::pair<TGroupId, TPageId>> sticky, TPageIdFlags flags) const {
             for (auto [groupId, arr] : expected) {
                 if (groupId.IsHistoric() && flags == TPageIdFlags::IfIter) {
                     // isn't supported
@@ -304,11 +340,17 @@ namespace {
                     absoluteId[absoluteId.size()] = it->GetPageId();
                 }
 
-                auto actualValue = actual.Value(groupId, TSet<TPageId>());
+                TSet<TPageId> actualValue;
+                for (auto p : actual.Value(groupId, TSet<TPageId>())) {
+                    if (flags & TPageIdFlags::IfSticky || !sticky.contains({groupId, p})) {
+                        actualValue.insert(p);
+                    }
+                }
+
                 auto expectedValue = TSet<TPageId>{};
                 for (auto p  : arr) {
                     if (flags & p.Flags) {
-                        expectedValue.insert(absoluteId[p.Page]);
+                        expectedValue.insert(absoluteId.Value(p.Page, p.Page));
                     }
                 }
                 UNIT_ASSERT_VALUES_EQUAL_C(expectedValue, actualValue, AssertMesage(groupId, flags));
@@ -920,6 +962,191 @@ Y_UNIT_TEST_SUITE(Charge) {
         });
     }
 
+    Y_UNIT_TEST(ByKeysIndex)
+    {
+        { // index
+            TModel me(false, false);
+            auto &pages = me.Eggs.Lone()->IndexPages;
+
+            // no index => touch index
+            me.To(100).CheckIndex(6, 22, 0, TMap<TGroupId, TArr>{
+                {TGroupId{0}, {pages.Groups[0]}},
+            }, TSet<TPageId> {
+
+            });
+
+            // index => touch pages + index
+            me.To(101).CheckIndex(6, 22, 0, TMap<TGroupId, TArr>{
+                {TGroupId{0}, {1, 2, 3, 4, 5, 6, pages.Groups[0]}}
+            },
+            TSet<TPageId> {
+                pages.Groups[0]
+            });
+        }
+
+        { // index + history index
+            TModel me(false, true);
+            auto &pages = me.Eggs.Lone()->IndexPages;
+
+            // no index => touch index
+            me.To(200).CheckIndex(6, 22, 0, TMap<TGroupId, TArr>{
+                {TGroupId{0}, {pages.Groups[0]}},
+                {TGroupId{0, true}, {}}
+            }, TSet<TPageId> {
+
+            });
+
+            // no history index => touch main pages + index + history index
+            me.To(201).CheckIndex(6, 22, 0, TMap<TGroupId, TArr>{
+                {TGroupId{0}, {1, 2, 3, 4, 5, 6, pages.Groups[0], pages.Historic[0]}},
+                {TGroupId{0, true}, {}}
+            },
+            TSet<TPageId> {
+                pages.Groups[0]
+            });
+
+            // history index => touch main pages + history pages + index + history index
+            me.To(202).CheckIndex(6, 22, 0, TMap<TGroupId, TArr>{
+                {TGroupId{0}, {1, 2, 3, 4, 5, 6, pages.Groups[0], pages.Historic[0]}},
+                {TGroupId{0, true}, {1, 2, 3, 4}}
+            },
+            TSet<TPageId> {
+                pages.Groups[0], pages.Historic[0]
+            });
+        }
+        
+        { // index + groups
+            TModel me(true, false);
+            auto &pages = me.Eggs.Lone()->IndexPages;
+
+            // no index => touch index
+            me.To(300).CheckIndex(6, 22, 0, TMap<TGroupId, TArr>{
+                {TGroupId{0}, {pages.Groups[0]}},
+                {TGroupId{1}, {}}
+            }, TSet<TPageId> {
+
+            });
+
+            // no groups index => touch main pages + index + all groups indexes
+            me.To(301).CheckIndex(6, 22, 0, TMap<TGroupId, TArr>{
+                {TGroupId{0}, {1, 2, 3, 4, 5, 6, pages.Groups[0], pages.Groups[1], pages.Groups[2], pages.Groups[3]}},
+                {TGroupId{1}, {}}
+            },
+            TSet<TPageId> {
+                pages.Groups[0]
+            });
+
+            // groups index => touch all pages + index + all groups indexes
+            me.To(302).CheckIndex(6, 22, 0, TMap<TGroupId, TArr>{
+                {TGroupId{0}, {1, 2, 3, 4, 5, 6, pages.Groups[0], pages.Groups[1], pages.Groups[2], pages.Groups[3]}},
+                {TGroupId{1}, {3, 4, 5, 6, 7}}
+            },
+            TSet<TPageId> {
+                pages.Groups[0], pages.Groups[1]
+            });
+        }
+
+        { // index + groups + history
+            TModel me(true, true);
+            auto &pages = me.Eggs.Lone()->IndexPages;
+
+            // no index => touch index
+            me.To(400).CheckIndex(6, 22, 0, TMap<TGroupId, TArr>{
+                {TGroupId{0}, {pages.Groups[0]}},
+                {TGroupId{0, true}, {}},
+                {TGroupId{1}, {}},
+                {TGroupId{1, true}, {}},
+                {TGroupId{2}, {}},
+                {TGroupId{2, true}, {}}
+            }, TSet<TPageId> {
+
+            });
+
+            // only index => touch main pages + index + all groups indexes + history index
+            me.To(401).CheckIndex(6, 22, 0, TMap<TGroupId, TArr>{
+                {TGroupId{0}, {1, 2, 3, 4, 5, 6, pages.Groups[0], pages.Historic[0], pages.Groups[1], pages.Groups[2], pages.Groups[3]}},
+                {TGroupId{0, true}, {}},
+                {TGroupId{1}, {}},
+                {TGroupId{1, true}, {}},
+                {TGroupId{2}, {}},
+                {TGroupId{2, true}, {}}
+            },
+            TSet<TPageId> {
+                pages.Groups[0]
+            });
+
+            // history index => touch main pages + index + all groups indexes + main history pages
+            me.To(402).CheckIndex(6, 22, 0, TMap<TGroupId, TArr>{
+                {TGroupId{0}, {1, 2, 3, 4, 5, 6, pages.Groups[0], 
+                    pages.Historic[0], pages.Historic[1], pages.Historic[2], pages.Historic[3], pages.Groups[1], pages.Groups[2], pages.Groups[3]}},
+                {TGroupId{0, true}, {1, 2, 3, 4}},
+                {TGroupId{1}, {}},
+                {TGroupId{1, true}, {}},
+                {TGroupId{2}, {}},
+                {TGroupId{2, true}, {}}
+            },
+            TSet<TPageId> {
+                pages.Groups[0], pages.Historic[0]
+            });
+
+            // main history and history => touch main pages + index + all groups indexes + history pages + history groups pages
+            me.To(403).CheckIndex(6, 22, 0, TMap<TGroupId, TArr>{
+                {TGroupId{0}, {1, 2, 3, 4, 5, 6, pages.Groups[0], 
+                    pages.Historic[0], pages.Historic[1], pages.Historic[2], pages.Historic[3], pages.Groups[1], pages.Groups[2], pages.Groups[3]}},
+                {TGroupId{0, true}, {1, 2, 3, 4}},
+                {TGroupId{1}, {}},
+                {TGroupId{1, true}, {3, 4, 5}},
+                {TGroupId{2}, {}},
+                {TGroupId{2, true}, {}}
+            },
+            TSet<TPageId> {
+                pages.Groups[0], pages.Historic[0], pages.Historic[1] 
+            });
+
+            // groups index => touch main pages + index + history index + all groups indexes + groups pages
+            me.To(404).CheckIndex(6, 22, 0, TMap<TGroupId, TArr>{
+                {TGroupId{0}, {1, 2, 3, 4, 5, 6, pages.Groups[0], 
+                    pages.Historic[0], pages.Groups[1], pages.Groups[2], pages.Groups[3]}},
+                {TGroupId{0, true}, {}},
+                {TGroupId{1}, {3, 4, 5, 6, 7}},
+                {TGroupId{1, true}, {}},
+                {TGroupId{2}, {}},
+                {TGroupId{2, true}, {}}
+            },
+            TSet<TPageId> {
+                pages.Groups[0], pages.Groups[1]
+            });
+
+            // main history and groups => touch main pages + index + all groups indexes + groups pages + history main pages
+            me.To(405).CheckIndex(6, 22, 0, TMap<TGroupId, TArr>{
+                {TGroupId{0}, {1, 2, 3, 4, 5, 6, pages.Groups[0], 
+                    pages.Historic[0], pages.Historic[1], pages.Historic[2], pages.Historic[3], pages.Groups[1], pages.Groups[2], pages.Groups[3]}},
+                {TGroupId{0, true}, {1, 2, 3, 4}},
+                {TGroupId{1}, {3, 4, 5, 6, 7}},
+                {TGroupId{1, true}, {}},
+                {TGroupId{2}, {}},
+                {TGroupId{2, true}, {}}
+            },
+            TSet<TPageId> {
+                pages.Groups[0], pages.Historic[0], pages.Groups[1]
+            });
+
+            // all indexes
+            me.To(406).CheckIndex(6, 22, 0, TMap<TGroupId, TArr>{
+                {TGroupId{0}, {1, 2, 3, 4, 5, 6, pages.Groups[0], 
+                    pages.Historic[0], pages.Historic[1], pages.Historic[2], pages.Historic[3], pages.Groups[1], pages.Groups[2], pages.Groups[3]}},
+                {TGroupId{0, true}, {1, 2, 3, 4}},
+                {TGroupId{1}, {3, 4, 5, 6, 7}},
+                {TGroupId{1, true}, {3, 4, 5}},
+                {TGroupId{2}, {}},
+                {TGroupId{2, true}, {}}
+            },
+            TSet<TPageId> {
+                pages.Groups[0], pages.Historic[0], pages.Historic[1], pages.Groups[1]
+            });
+        }
+    }
+
     Y_UNIT_TEST(ByRows)
     {
         TModel me(true);

+ 31 - 13
ydb/core/tablet_flat/ut/ut_comp_shard.cpp

@@ -43,11 +43,6 @@ namespace {
             
             info.Touched.insert(token);
 
-            if (part->IndexPages.Has(groupId, ref)) {
-                // TODO: delete after index precharge
-                return NTest::TTestEnv::TryGetPage(part, ref, groupId);
-            }
-
             if (info.Loaded.contains(token)) {
                 return TTestEnv::TryGetPage(part, ref, groupId);
             }
@@ -286,12 +281,18 @@ Y_UNIT_TEST_SUITE(TShardedCompaction) {
                             TSimpleConsumer consumer;
                             TSliceSplitOp op(&consumer, &table, pshards, partView.Part, slice);
 
+                            // load index
                             bool ok1 = op.Execute(&env);
                             UNIT_ASSERT_VALUES_EQUAL(ok1, false);
-
                             env.Load();
+
+                            // load data
                             bool ok2 = op.Execute(&env);
-                            UNIT_ASSERT_VALUES_EQUAL(ok2, true);
+                            UNIT_ASSERT_VALUES_EQUAL(ok2, false);
+                            env.Load();
+
+                            bool ok3 = op.Execute(&env);
+                            UNIT_ASSERT_VALUES_EQUAL(ok3, true);
 
                             auto& result = consumer.Result.value();
                             size_t pos = 0;
@@ -370,12 +371,18 @@ Y_UNIT_TEST_SUITE(TShardedCompaction) {
         TSimpleConsumer consumer;
         TSliceSplitOp op(&consumer, &table, pshards, partView.Part, slice);
 
+        // load index
         bool ok1 = op.Execute(&env);
         UNIT_ASSERT_VALUES_EQUAL(ok1, false);
-
         env.Load();
+
+        // load data
         bool ok2 = op.Execute(&env);
-        UNIT_ASSERT_VALUES_EQUAL(ok2, true);
+        UNIT_ASSERT_VALUES_EQUAL(ok2, false);
+        env.Load();
+
+        bool ok3 = op.Execute(&env);
+        UNIT_ASSERT_VALUES_EQUAL(ok3, true);
 
         auto& result = consumer.Result.value();
         UNIT_ASSERT_VALUES_EQUAL(result.NewSlices.size(), 2u);
@@ -436,12 +443,18 @@ Y_UNIT_TEST_SUITE(TShardedCompaction) {
         TSimpleConsumer consumer;
         TSliceSplitOp op(&consumer, &table, pshards, partView.Part, slice);
 
+        // load index
         bool ok1 = op.Execute(&env);
         UNIT_ASSERT_VALUES_EQUAL(ok1, false);
-
         env.Load();
+
+        // load data
         bool ok2 = op.Execute(&env);
-        UNIT_ASSERT_VALUES_EQUAL(ok2, true);
+        UNIT_ASSERT_VALUES_EQUAL(ok2, false);
+        env.Load();
+
+        bool ok3 = op.Execute(&env);
+        UNIT_ASSERT_VALUES_EQUAL(ok3, true);
 
         auto& result = consumer.Result.value();
         UNIT_ASSERT_VALUES_EQUAL(result.NewSlices.size(), 2u);
@@ -1239,11 +1252,16 @@ Y_UNIT_TEST_SUITE(TShardedCompactionScenarios) {
         UNIT_ASSERT_VALUES_EQUAL(backend.PendingReads.size(), 2u);
         while (backend.PendingReads) {
             TStrictEnv env;
+            // load index
             auto first = backend.RunRead(&env);
             UNIT_ASSERT(!first.Completed);
             env.Load();
-            auto second = backend.RunRead(first.ReadId, &env);
-            UNIT_ASSERT(second.Completed);
+            // load data
+            auto second = backend.RunRead(&env);
+            UNIT_ASSERT(!second.Completed);
+            env.Load();
+            auto third = backend.RunRead(first.ReadId, &env);
+            UNIT_ASSERT(third.Completed);
         }
 
         UNIT_ASSERT(backend.CheckChangesFlag());

+ 0 - 5
ydb/core/tablet_flat/ut/ut_part.cpp

@@ -59,11 +59,6 @@ namespace {
     struct TTouchEnv : public NTest::TTestEnv {
         const TSharedData* TryGetPage(const TPart *part, TPageId id, TGroupId groupId) override
         {
-            if (part->IndexPages.Has(groupId, id)) {
-                // TODO: delete after index precharge
-                return NTest::TTestEnv::TryGetPage(part, id, groupId);
-            }
-
             if (PrechargePhase) {
                 Precharged[groupId].insert(id);
                 return NTest::TTestEnv::TryGetPage(part, id, groupId);