From 56722e55afc2cf03213a6cda02fda3b4f40369be Mon Sep 17 00:00:00 2001
From: Andrey Strochuk <a.strochuk@picodata.io>
Date: Sun, 19 May 2024 12:14:00 +0300
Subject: [PATCH] feat: add one subarena

---
 sbroad-cartridge/src/cartridge/router.rs      |   7 +-
 sbroad-cartridge/src/cartridge/storage.rs     |   2 +-
 sbroad-core/src/backend/sql/ir.rs             |  14 +-
 sbroad-core/src/backend/sql/space.rs          |   3 +-
 sbroad-core/src/backend/sql/tree.rs           | 119 ++++----
 sbroad-core/src/executor.rs                   |  13 +-
 sbroad-core/src/executor/bucket.rs            |  16 +-
 sbroad-core/src/executor/engine.rs            |   6 +-
 sbroad-core/src/executor/engine/helpers.rs    |  35 ++-
 .../src/executor/engine/helpers/storage.rs    |   2 +-
 .../src/executor/engine/helpers/vshard.rs     |  27 +-
 sbroad-core/src/executor/engine/mock.rs       |  17 +-
 sbroad-core/src/executor/ir.rs                |  78 ++---
 sbroad-core/src/executor/protocol.rs          |   5 +-
 sbroad-core/src/executor/result.rs            |   5 +-
 sbroad-core/src/executor/tests.rs             |   4 +-
 sbroad-core/src/executor/tests/exec_plan.rs   |  83 +++---
 sbroad-core/src/executor/vtable.rs            |   9 +-
 sbroad-core/src/frontend/sql.rs               |  58 ++--
 sbroad-core/src/frontend/sql/ir.rs            |  85 +++---
 sbroad-core/src/frontend/sql/ir/tests.rs      |  62 ++--
 .../src/frontend/sql/ir/tests/global.rs       |  39 ++-
 .../src/frontend/sql/ir/tests/single.rs       |  10 +-
 sbroad-core/src/ir.rs                         | 256 +++++++++-------
 sbroad-core/src/ir/acl.rs                     |   8 +-
 sbroad-core/src/ir/aggregates.rs              |  16 +-
 sbroad-core/src/ir/api/children.rs            |  40 +--
 sbroad-core/src/ir/api/constant.rs            |  18 +-
 sbroad-core/src/ir/api/parameter.rs           |  62 ++--
 sbroad-core/src/ir/block.rs                   |   8 +-
 sbroad-core/src/ir/ddl.rs                     |   8 +-
 sbroad-core/src/ir/distribution.rs            |  48 +--
 sbroad-core/src/ir/distribution/tests.rs      | 257 +---------------
 sbroad-core/src/ir/explain.rs                 |  39 +--
 sbroad-core/src/ir/expression.rs              | 234 ++++++++-------
 sbroad-core/src/ir/expression/cast.rs         |   4 +-
 sbroad-core/src/ir/expression/concat.rs       |   4 +-
 sbroad-core/src/ir/expression/types.rs        |   4 +-
 sbroad-core/src/ir/function.rs                |  10 +-
 sbroad-core/src/ir/helpers.rs                 |  17 +-
 sbroad-core/src/ir/helpers/tests.rs           | 117 ++++----
 sbroad-core/src/ir/operator.rs                | 266 +++++++++--------
 sbroad-core/src/ir/operator/tests.rs          | 184 ++----------
 sbroad-core/src/ir/parameters.rs              |  10 +-
 sbroad-core/src/ir/relation/tests.rs          | 102 -------
 sbroad-core/src/ir/tests.rs                   |  38 +--
 sbroad-core/src/ir/transformation.rs          |  46 +--
 sbroad-core/src/ir/transformation/bool_in.rs  |   8 +-
 sbroad-core/src/ir/transformation/dnf.rs      |  26 +-
 .../ir/transformation/equality_propagation.rs |  26 +-
 .../equality_propagation/tests.rs             |   6 +-
 .../src/ir/transformation/merge_tuples.rs     |  49 +--
 .../src/ir/transformation/not_push_down.rs    |  23 +-
 .../src/ir/transformation/redistribution.rs   | 215 +++++++-------
 .../ir/transformation/redistribution/dml.rs   |  29 +-
 .../transformation/redistribution/eq_cols.rs  |  38 +--
 .../transformation/redistribution/groupby.rs  | 223 +++++++-------
 .../redistribution/left_join.rs               |   7 +-
 .../ir/transformation/redistribution/tests.rs | 281 +-----------------
 .../redistribution/tests/not_in.rs            |  14 +-
 .../redistribution/tests/segment.rs           |  95 +-----
 .../src/ir/transformation/split_columns.rs    |   8 +-
 .../ir/transformation/split_columns/tests.rs  |   4 +-
 sbroad-core/src/ir/tree.rs                    |  16 +-
 sbroad-core/src/ir/tree/and.rs                |  14 +-
 sbroad-core/src/ir/tree/expression.rs         |  22 +-
 sbroad-core/src/ir/tree/relation.rs           |  19 +-
 sbroad-core/src/ir/tree/subtree.rs            |  30 +-
 sbroad-core/src/ir/tree/tests.rs              |  82 ++---
 sbroad-core/src/ir/tree/traversal.rs          | 101 ++++---
 sbroad-core/src/ir/undo.rs                    |  10 +-
 71 files changed, 1598 insertions(+), 2243 deletions(-)

diff --git a/sbroad-cartridge/src/cartridge/router.rs b/sbroad-cartridge/src/cartridge/router.rs
index 89d7d4aaf..a5de1d2f6 100644
--- a/sbroad-cartridge/src/cartridge/router.rs
+++ b/sbroad-cartridge/src/cartridge/router.rs
@@ -3,6 +3,7 @@
 use sbroad::cbo::{ColumnStats, TableColumnPair, TableStats};
 use sbroad::executor::engine::helpers::vshard::{get_random_bucket, impl_exec_ir_on_buckets};
 use sbroad::executor::engine::{DispatchReturnFormat, Metadata, QueryCache, Vshard};
+use sbroad::ir::expression::NodeId;
 use sbroad::utils::MutexLike;
 use smol_str::{format_smolstr, SmolStr, ToSmolStr};
 use tarantool::fiber::Mutex;
@@ -247,7 +248,7 @@ impl Router for RouterRuntime {
     fn dispatch(
         &self,
         plan: &mut ExecutionPlan,
-        top_id: usize,
+        top_id: NodeId,
         buckets: &Buckets,
         return_format: DispatchReturnFormat,
     ) -> Result<Box<dyn Any>, SbroadError> {
@@ -263,10 +264,10 @@ impl Router for RouterRuntime {
     fn materialize_motion(
         &self,
         plan: &mut ExecutionPlan,
-        motion_node_id: usize,
+        motion_node_id: &NodeId,
         buckets: &Buckets,
     ) -> Result<VirtualTable, SbroadError> {
-        materialize_motion(self, plan, motion_node_id, buckets)
+        materialize_motion(self, plan, *motion_node_id, buckets)
     }
 
     fn extract_sharding_key_from_map<'rec>(
diff --git a/sbroad-cartridge/src/cartridge/storage.rs b/sbroad-cartridge/src/cartridge/storage.rs
index 076ff6540..50c55146a 100644
--- a/sbroad-cartridge/src/cartridge/storage.rs
+++ b/sbroad-cartridge/src/cartridge/storage.rs
@@ -16,7 +16,7 @@ use sbroad::executor::ir::{ExecutionPlan, QueryType};
 use sbroad::executor::lru::{Cache, LRUCache, DEFAULT_CAPACITY};
 use sbroad::executor::protocol::{RequiredData, SchemaInfo};
 use sbroad::ir::value::Value;
-use sbroad::ir::NodeId;
+use sbroad::ir::expression::NodeId;
 use sbroad::utils::MutexLike;
 use sbroad::{debug, error, warn};
 use smol_str::{format_smolstr, SmolStr, ToSmolStr};
diff --git a/sbroad-core/src/backend/sql/ir.rs b/sbroad-core/src/backend/sql/ir.rs
index 330fb4451..b033e3f71 100644
--- a/sbroad-core/src/backend/sql/ir.rs
+++ b/sbroad-core/src/backend/sql/ir.rs
@@ -2,7 +2,7 @@ use crate::debug;
 use crate::executor::protocol::VTablesMeta;
 use crate::ir::relation::Column;
 use crate::ir::transformation::redistribution::MotionPolicy;
-use crate::ir::tree::traversal::PostOrderWithFilter;
+use crate::ir::tree::traversal::{LevelNode, PostOrderWithFilter};
 use opentelemetry::Context;
 use serde::{Deserialize, Serialize};
 use smol_str::format_smolstr;
@@ -14,7 +14,7 @@ use tarantool::tuple::{FunctionArgs, Tuple};
 use crate::errors::{Action, Entity, SbroadError};
 use crate::executor::engine::helpers::table_name;
 use crate::executor::ir::ExecutionPlan;
-use crate::ir::expression::Expression;
+use crate::ir::expression::{Expression, NodeId};
 use crate::ir::operator::{OrderByType, Relational};
 use crate::ir::value::{LuaValue, Value};
 use crate::ir::Node;
@@ -161,8 +161,8 @@ impl ExecutionPlan {
     /// - IR plan is invalid
     pub fn to_params(&self) -> Result<Vec<Value>, SbroadError> {
         let plan = self.get_ir_plan();
-        let capacity = plan.next_id();
-        let filter = |id: usize| -> bool { matches!(plan.get_node(id), Ok(Node::Parameter(_))) };
+        let capacity = plan.nodes.len();
+        let filter = |id: NodeId| -> bool { matches!(plan.get_node(id), Ok(Node::Parameter(_))) };
         let mut tree = PostOrderWithFilter::with_capacity(
             |node| plan.flashback_subtree_iter(node),
             capacity,
@@ -172,7 +172,7 @@ impl ExecutionPlan {
         tree.populate_nodes(top_id);
         let nodes = tree.take_nodes();
         let mut params: Vec<Value> = Vec::with_capacity(nodes.len());
-        for (_, param_id) in nodes {
+        for LevelNode(_, param_id) in nodes {
             let Expression::Constant { value } = plan.get_expression_node(param_id)? else {
                 return Err(SbroadError::Invalid(
                     Entity::Plan,
@@ -428,7 +428,7 @@ impl ExecutionPlan {
                                         })?;
                                     }
                                     Expression::Reference { position, .. } => {
-                                        let rel_id: usize =
+                                        let rel_id =
                                             *ir_plan.get_relational_from_reference_node(*id)?;
                                         let rel_node = ir_plan.get_relation_node(rel_id)?;
 
@@ -553,7 +553,7 @@ impl ExecutionPlan {
     ///
     /// # Errors
     /// - If the subtree top is not a relational node.
-    pub fn subtree_modifies_data(&self, top_id: usize) -> Result<bool, SbroadError> {
+    pub fn subtree_modifies_data(&self, top_id: NodeId) -> Result<bool, SbroadError> {
         // Tarantool doesn't support `INSERT`, `UPDATE` and `DELETE` statements
         // with `RETURNING` clause. That is why it is enough to check if the top
         // node is a data modification statement or not.
diff --git a/sbroad-core/src/backend/sql/space.rs b/sbroad-core/src/backend/sql/space.rs
index 6342206ce..4807b384b 100644
--- a/sbroad-core/src/backend/sql/space.rs
+++ b/sbroad-core/src/backend/sql/space.rs
@@ -1,4 +1,5 @@
 use crate::executor::ir::ExecutionPlan;
+use crate::ir::expression::NodeId;
 use crate::ir::relation::SpaceEngine;
 use crate::{errors::SbroadError, executor::protocol::VTablesMeta};
 
@@ -133,7 +134,7 @@ pub const ADMIN_ID: u32 = 1;
 pub fn create_table(
     exec_plan: &ExecutionPlan,
     plan_id: &str,
-    motion_id: usize,
+    motion_id: NodeId,
     engine: &SpaceEngine,
     #[allow(unused_variables)] vtables_meta: Option<&VTablesMeta>,
 ) -> Result<TableGuard, SbroadError> {
diff --git a/sbroad-core/src/backend/sql/tree.rs b/sbroad-core/src/backend/sql/tree.rs
index 524448408..51954121c 100644
--- a/sbroad-core/src/backend/sql/tree.rs
+++ b/sbroad-core/src/backend/sql/tree.rs
@@ -4,10 +4,10 @@ use std::mem::take;
 
 use crate::errors::{Entity, SbroadError};
 use crate::executor::ir::ExecutionPlan;
-use crate::ir::expression::{Expression, FunctionFeature, TrimKind};
+use crate::ir::expression::{Expression, FunctionFeature, NodeId, TrimKind};
 use crate::ir::operator::{Bool, OrderByElement, OrderByEntity, OrderByType, Relational, Unary};
 use crate::ir::transformation::redistribution::{MotionOpcode, MotionPolicy};
-use crate::ir::tree::traversal::PostOrder;
+use crate::ir::tree::traversal::{LevelNode, PostOrder};
 use crate::ir::tree::Snapshot;
 use crate::ir::{Node, Plan};
 use crate::otm::child_span;
@@ -72,12 +72,12 @@ pub enum SyntaxData {
     /// "=, >, <, and, or, ..."
     Operator(SmolStr),
     /// plan node id
-    PlanId(usize),
+    PlanId(NodeId),
     /// parameter (a wrapper over a plan constants)
-    Parameter(usize),
+    Parameter(NodeId),
     /// virtual table (the key is a motion node id
     /// pointing to the execution plan's virtual table)
-    VTable(usize),
+    VTable(NodeId),
 }
 
 /// A syntax tree node.
@@ -300,7 +300,7 @@ impl SyntaxNode {
         }
     }
 
-    fn new_pointer(id: usize, left: Option<usize>, right: Vec<usize>) -> Self {
+    fn new_pointer(id: NodeId, left: Option<usize>, right: Vec<usize>) -> Self {
         SyntaxNode {
             data: SyntaxData::PlanId(id),
             left,
@@ -308,7 +308,7 @@ impl SyntaxNode {
         }
     }
 
-    fn new_parameter(id: usize) -> Self {
+    fn new_parameter(id: NodeId) -> Self {
         SyntaxNode {
             data: SyntaxData::Parameter(id),
             left: None,
@@ -326,7 +326,7 @@ impl SyntaxNode {
         }
     }
 
-    fn new_vtable(motion_id: usize) -> Self {
+    fn new_vtable(motion_id: NodeId) -> Self {
         SyntaxNode {
             data: SyntaxData::VTable(motion_id),
             left: None,
@@ -550,7 +550,7 @@ impl<'p> SyntaxPlan<'p> {
     /// # Panics
     /// - syntax node wraps non-plan node;
     /// - plan node is not the one we expect;
-    fn check_plan_node(&self, sn_id: usize, plan_id: usize) {
+    fn check_plan_node(&self, sn_id: usize, plan_id: NodeId) {
         let sn = self.nodes.get_sn(sn_id);
         let SyntaxNode {
             data: SyntaxData::PlanId(id) | SyntaxData::Parameter(id),
@@ -580,7 +580,7 @@ impl<'p> SyntaxPlan<'p> {
                 }
                 _ => {}
             }
-            panic!("Expected plan node {plan_id} but got {id}");
+            panic!("Expected plan node {plan_id:?} but got {id:?}");
         }
     }
 
@@ -591,7 +591,7 @@ impl<'p> SyntaxPlan<'p> {
 
     /// Pop syntax node identifier from the stack with a check, that the popped syntax
     /// node wraps an expected plan node.
-    fn pop_from_stack(&mut self, plan_id: usize) -> usize {
+    fn pop_from_stack(&mut self, plan_id: NodeId) -> usize {
         let sn_id = self.nodes.stack.pop().expect("stack must be non-empty");
         self.check_plan_node(sn_id, plan_id);
         sn_id
@@ -609,7 +609,7 @@ impl<'p> SyntaxPlan<'p> {
     /// - Failed to find a node in the plan.
     #[allow(clippy::too_many_lines)]
     #[allow(unused_variables)]
-    pub fn add_plan_node(&mut self, id: usize) {
+    pub fn add_plan_node(&mut self, id: NodeId) {
         let ir_plan = self.plan.get_ir_plan();
         let node = ir_plan
             .get_node(id)
@@ -668,7 +668,7 @@ impl<'p> SyntaxPlan<'p> {
         }
     }
 
-    fn prologue_rel(&self, id: usize) -> (&Plan, &Relational) {
+    fn prologue_rel(&self, id: NodeId) -> (&Plan, &Relational) {
         let plan = self.plan.get_ir_plan();
         let rel = plan
             .get_relation_node(id)
@@ -676,7 +676,7 @@ impl<'p> SyntaxPlan<'p> {
         (plan, rel)
     }
 
-    fn prologue_expr(&self, id: usize) -> (&Plan, &Expression) {
+    fn prologue_expr(&self, id: NodeId) -> (&Plan, &Expression) {
         let plan = self.plan.get_ir_plan();
         let expr = plan
             .get_expression_node(id)
@@ -686,7 +686,7 @@ impl<'p> SyntaxPlan<'p> {
 
     // Relational nodes.
 
-    fn add_cte(&mut self, id: usize) {
+    fn add_cte(&mut self, id: NodeId) {
         let (_, cte) = self.prologue_rel(id);
         let Relational::ScanCte { alias, child, .. } = cte else {
             panic!("expected CTE node");
@@ -704,7 +704,7 @@ impl<'p> SyntaxPlan<'p> {
         arena.push_sn_plan(sn);
     }
 
-    fn add_filter(&mut self, id: usize) {
+    fn add_filter(&mut self, id: NodeId) {
         let (plan, rel) = self.prologue_rel(id);
         let (Relational::Selection {
             children, filter, ..
@@ -726,7 +726,7 @@ impl<'p> SyntaxPlan<'p> {
         self.nodes.push_sn_plan(sn);
     }
 
-    fn add_group_by(&mut self, id: usize) {
+    fn add_group_by(&mut self, id: NodeId) {
         let (_, gb) = self.prologue_rel(id);
         let Relational::GroupBy {
             children, gr_cols, ..
@@ -736,16 +736,17 @@ impl<'p> SyntaxPlan<'p> {
         };
         let child_plan_id = *children.first().expect("GROUP BY child");
         // The columns on the stack are in reverse order.
-        let mut sn_gr_cols = gr_cols.iter().rev().copied().collect::<Vec<_>>();
+        let plan_gr_cols = gr_cols.iter().rev().copied().collect::<Vec<_>>();
+        let mut syntax_gr_cols = Vec::with_capacity(plan_gr_cols.len());
         // Reuse the same vector to avoid extra allocations
         // (replace plan node ids with syntax node ids).
-        for col_id in &mut sn_gr_cols {
-            *col_id = self.pop_from_stack(*col_id);
+        for col_id in &plan_gr_cols {
+            syntax_gr_cols.push(self.pop_from_stack(*col_id));
         }
         let child_sn_id = self.pop_from_stack(child_plan_id);
-        let mut sn_children = Vec::with_capacity(sn_gr_cols.len() * 2 - 1);
+        let mut sn_children = Vec::with_capacity(syntax_gr_cols.len() * 2 - 1);
         // The columns are in reverse order, so we need to reverse them back.
-        if let Some((first, others)) = sn_gr_cols.split_first() {
+        if let Some((first, others)) = syntax_gr_cols.split_first() {
             for id in others.iter().rev() {
                 sn_children.push(*id);
                 sn_children.push(self.nodes.push_sn_non_plan(SyntaxNode::new_comma()));
@@ -756,7 +757,7 @@ impl<'p> SyntaxPlan<'p> {
         self.nodes.push_sn_plan(sn);
     }
 
-    fn add_join(&mut self, id: usize) {
+    fn add_join(&mut self, id: NodeId) {
         let (plan, join) = self.prologue_rel(id);
         let Relational::Join {
             children,
@@ -791,7 +792,7 @@ impl<'p> SyntaxPlan<'p> {
         arena.push_sn_plan(sn);
     }
 
-    fn add_motion(&mut self, id: usize) {
+    fn add_motion(&mut self, id: NodeId) {
         let (plan, motion) = self.prologue_rel(id);
         let Relational::Motion {
             policy,
@@ -896,7 +897,7 @@ impl<'p> SyntaxPlan<'p> {
         arena.push_sn_plan(sn);
     }
 
-    fn add_order_by(&mut self, id: usize) {
+    fn add_order_by(&mut self, id: NodeId) {
         let (_, order_by) = self.prologue_rel(id);
         let Relational::OrderBy {
             order_by_elements,
@@ -952,7 +953,7 @@ impl<'p> SyntaxPlan<'p> {
         self.nodes.push_sn_plan(sn);
     }
 
-    fn add_proj(&mut self, id: usize) {
+    fn add_proj(&mut self, id: NodeId) {
         let (_, proj) = self.prologue_rel(id);
         let Relational::Projection {
             children, output, ..
@@ -985,7 +986,7 @@ impl<'p> SyntaxPlan<'p> {
         self.nodes.push_sn_plan(sn);
     }
 
-    fn add_scan_relation(&mut self, id: usize) {
+    fn add_scan_relation(&mut self, id: NodeId) {
         let (_, scan) = self.prologue_rel(id);
         let Relational::ScanRelation { alias, .. } = scan else {
             panic!("Expected SCAN node");
@@ -1001,7 +1002,7 @@ impl<'p> SyntaxPlan<'p> {
         arena.push_sn_plan(sn);
     }
 
-    fn add_set(&mut self, id: usize) {
+    fn add_set(&mut self, id: NodeId) {
         let (_, set) = self.prologue_rel(id);
         let (Relational::Except { left, right, .. }
         | Relational::Union { left, right, .. }
@@ -1017,7 +1018,7 @@ impl<'p> SyntaxPlan<'p> {
         self.nodes.push_sn_plan(sn);
     }
 
-    fn add_sq(&mut self, id: usize) {
+    fn add_sq(&mut self, id: NodeId) {
         let (_, sq) = self.prologue_rel(id);
         let Relational::ScanSubQuery {
             children, alias, ..
@@ -1042,7 +1043,7 @@ impl<'p> SyntaxPlan<'p> {
         arena.push_sn_plan(sn);
     }
 
-    fn add_values_row(&mut self, id: usize) {
+    fn add_values_row(&mut self, id: NodeId) {
         let (_, row) = self.prologue_rel(id);
         let Relational::ValuesRow { data, .. } = row else {
             panic!("Expected VALUES ROW node");
@@ -1052,7 +1053,7 @@ impl<'p> SyntaxPlan<'p> {
         self.nodes.push_sn_plan(sn);
     }
 
-    fn add_values(&mut self, id: usize) {
+    fn add_values(&mut self, id: NodeId) {
         let (_, values) = self.prologue_rel(id);
         let Relational::Values {
             children, output, ..
@@ -1062,24 +1063,25 @@ impl<'p> SyntaxPlan<'p> {
         };
         let output_plan_id = *output;
         // The syntax nodes on the stack are in the reverse order.
-        let mut sn_children = children.iter().rev().copied().collect::<Vec<_>>();
-        // Reuse the same vector to avoid extra allocations (replace plan node ids with syntax node ids).
-        for child_id in &mut sn_children {
-            *child_id = self.pop_from_stack(*child_id);
+        let plan_children = children.iter().rev().copied().collect::<Vec<_>>();
+        let mut syntax_children = Vec::with_capacity(plan_children.len());
+
+        for child_id in &plan_children {
+            syntax_children.push(self.pop_from_stack(*child_id));
         }
 
         // Consume the output from the stack.
         let _ = self.pop_from_stack(output_plan_id);
 
-        let mut nodes = Vec::with_capacity(sn_children.len() * 2 - 1);
+        let mut nodes = Vec::with_capacity(syntax_children.len() * 2 - 1);
         // Reverse the order of the children back.
         let arena = &mut self.nodes;
-        for child_id in sn_children.iter().skip(1).rev() {
+        for child_id in syntax_children.iter().skip(1).rev() {
             nodes.push(*child_id);
             nodes.push(arena.push_sn_non_plan(SyntaxNode::new_comma()));
         }
         nodes.push(
-            *sn_children
+            *syntax_children
                 .first()
                 .expect("values must have at least one child"),
         );
@@ -1087,7 +1089,7 @@ impl<'p> SyntaxPlan<'p> {
         arena.push_sn_plan(sn);
     }
 
-    fn add_limit(&mut self, id: usize) {
+    fn add_limit(&mut self, id: NodeId) {
         let (_, limit) = self.prologue_rel(id);
         let Relational::Limit { limit, child, .. } = limit else {
             panic!("expected LIMIT node");
@@ -1105,7 +1107,7 @@ impl<'p> SyntaxPlan<'p> {
 
     // Expression nodes.
 
-    fn add_alias(&mut self, id: usize) {
+    fn add_alias(&mut self, id: NodeId) {
         let (_, expr) = self.prologue_expr(id);
         let Expression::Alias { child, name } = expr else {
             panic!("Expected ALIAS node");
@@ -1132,7 +1134,7 @@ impl<'p> SyntaxPlan<'p> {
         self.nodes.push_sn_plan(sn);
     }
 
-    fn add_binary_op(&mut self, id: usize) {
+    fn add_binary_op(&mut self, id: NodeId) {
         let (_, expr) = self.prologue_expr(id);
         let (left_plan_id, right_plan_id, op_sn_id) = match expr {
             Expression::Bool {
@@ -1162,7 +1164,7 @@ impl<'p> SyntaxPlan<'p> {
         self.nodes.push_sn_plan(sn);
     }
 
-    fn add_case(&mut self, id: usize) {
+    fn add_case(&mut self, id: NodeId) {
         let (_, expr) = self.prologue_expr(id);
         let Expression::Case {
             search_expr,
@@ -1173,7 +1175,7 @@ impl<'p> SyntaxPlan<'p> {
             panic!("Expected CASE node");
         };
         let search_expr = *search_expr;
-        let when_blocks: Vec<(usize, usize)> = when_blocks.clone();
+        let when_blocks: Vec<(NodeId, NodeId)> = when_blocks.clone();
         let else_expr = *else_expr;
 
         let mut right_vec = Vec::with_capacity(1 + when_blocks.len() * 4 + 1);
@@ -1200,7 +1202,7 @@ impl<'p> SyntaxPlan<'p> {
         self.nodes.push_sn_plan(sn);
     }
 
-    fn add_cast(&mut self, id: usize) {
+    fn add_cast(&mut self, id: NodeId) {
         let (_, expr) = self.prologue_expr(id);
         let Expression::Cast { child, to } = expr else {
             panic!("Expected CAST node");
@@ -1220,7 +1222,7 @@ impl<'p> SyntaxPlan<'p> {
         arena.push_sn_plan(sn);
     }
 
-    fn add_concat(&mut self, id: usize) {
+    fn add_concat(&mut self, id: NodeId) {
         let (_, expr) = self.prologue_expr(id);
         let Expression::Concat { left, right } = expr else {
             panic!("Expected CONCAT node");
@@ -1236,7 +1238,7 @@ impl<'p> SyntaxPlan<'p> {
         self.nodes.push_sn_plan(sn);
     }
 
-    fn add_expr_in_parentheses(&mut self, id: usize) {
+    fn add_expr_in_parentheses(&mut self, id: NodeId) {
         let (_, expr) = self.prologue_expr(id);
         let Expression::ExprInParentheses { child } = expr else {
             panic!("Expected expression in parentheses node");
@@ -1252,7 +1254,7 @@ impl<'p> SyntaxPlan<'p> {
         arena.push_sn_plan(sn);
     }
 
-    fn add_row(&mut self, id: usize) {
+    fn add_row(&mut self, id: NodeId) {
         let plan = self.plan.get_ir_plan();
         let expr = plan
             .get_expression_node(id)
@@ -1376,7 +1378,7 @@ impl<'p> SyntaxPlan<'p> {
         self.nodes.push_sn_plan(sn);
     }
 
-    fn add_stable_func(&mut self, id: usize) {
+    fn add_stable_func(&mut self, id: NodeId) {
         let plan = self.plan.get_ir_plan();
         let expr = plan
             .get_expression_node(id)
@@ -1409,7 +1411,7 @@ impl<'p> SyntaxPlan<'p> {
         self.nodes.push_sn_plan(sn);
     }
 
-    fn add_trim(&mut self, id: usize) {
+    fn add_trim(&mut self, id: NodeId) {
         let (_, expr) = self.prologue_expr(id);
         let Expression::Trim {
             kind,
@@ -1457,7 +1459,7 @@ impl<'p> SyntaxPlan<'p> {
         self.nodes.push_sn_plan(sn);
     }
 
-    fn add_unary_op(&mut self, id: usize) {
+    fn add_unary_op(&mut self, id: NodeId) {
         let (plan, expr) = self.prologue_expr(id);
         let Expression::Unary { child, op } = expr else {
             panic!("Expected unary expression node");
@@ -1558,7 +1560,7 @@ impl<'p> SyntaxPlan<'p> {
         );
         dfs.populate_nodes(top);
         let nodes = dfs.take_nodes();
-        for (_, pos) in nodes {
+        for LevelNode(_, pos) in nodes {
             let node = self.nodes.get_sn(pos);
             if pos == top {
                 let select = Select::new(self, None, None, pos)?;
@@ -1603,7 +1605,7 @@ impl<'p> SyntaxPlan<'p> {
 
     pub(crate) fn empty(plan: &'p ExecutionPlan) -> Self {
         SyntaxPlan {
-            nodes: SyntaxNodes::with_capacity(plan.get_ir_plan().next_id() * 2),
+            nodes: SyntaxNodes::with_capacity(plan.get_ir_plan().nodes.len() * 2),
             top: None,
             plan,
             snapshot: Snapshot::Latest,
@@ -1619,7 +1621,7 @@ impl<'p> SyntaxPlan<'p> {
     #[otm_child_span("syntax.new")]
     pub fn new(
         plan: &'p ExecutionPlan,
-        top: usize,
+        top: NodeId,
         snapshot: Snapshot,
     ) -> Result<Self, SbroadError> {
         let mut sp = SyntaxPlan::empty(plan);
@@ -1627,12 +1629,13 @@ impl<'p> SyntaxPlan<'p> {
         let ir_plan = plan.get_ir_plan();
 
         // Wrap plan's nodes and preserve their ids.
-        let capacity = ir_plan.next_id();
+        let capacity = ir_plan.nodes.len();
         match snapshot {
             Snapshot::Latest => {
                 let mut dft_post =
                     PostOrder::with_capacity(|node| ir_plan.subtree_iter(node, false), capacity);
-                for (_, id) in dft_post.iter(top) {
+                for level_node in dft_post.iter(top) {
+                    let id = level_node.1;
                     // it works only for post-order traversal
                     sp.add_plan_node(id);
                     let sn_id = sp.nodes.next_id() - 1;
@@ -1644,7 +1647,8 @@ impl<'p> SyntaxPlan<'p> {
             Snapshot::Oldest => {
                 let mut dft_post =
                     PostOrder::with_capacity(|node| ir_plan.flashback_subtree_iter(node), capacity);
-                for (_, id) in dft_post.iter(top) {
+                for level_node in dft_post.iter(top) {
+                    let id = level_node.1;
                     // it works only for post-order traversal
                     sp.add_plan_node(id);
                     let sn_id = sp.nodes.next_id() - 1;
@@ -1787,6 +1791,3 @@ impl TryFrom<SyntaxPlan<'_>> for OrderedSyntaxNodes {
         Ok(Self { arena, positions })
     }
 }
-
-#[cfg(test)]
-mod tests;
diff --git a/sbroad-core/src/executor.rs b/sbroad-core/src/executor.rs
index d88f66beb..b708f8af1 100644
--- a/sbroad-core/src/executor.rs
+++ b/sbroad-core/src/executor.rs
@@ -33,6 +33,7 @@ use crate::executor::engine::{helpers::materialize_values, Router, TableVersionM
 use crate::executor::ir::ExecutionPlan;
 use crate::executor::lru::Cache;
 use crate::frontend::Ast;
+use crate::ir::expression::NodeId;
 use crate::ir::operator::Relational;
 use crate::ir::transformation::redistribution::MotionPolicy;
 use crate::ir::value::Value;
@@ -82,7 +83,7 @@ where
     coordinator: &'a C,
     /// Bucket map of view { plan output_id (Expression::Row) -> `Buckets` }.
     /// It's supposed to denote relational nodes' output buckets destination.
-    bucket_map: HashMap<usize, Buckets>,
+    bucket_map: HashMap<NodeId, Buckets>,
 }
 
 impl<'a, C> Query<'a, C>
@@ -93,7 +94,7 @@ where
         is_explain: bool,
         exec_plan: ExecutionPlan,
         coordinator: &'a C,
-        bucket_map: HashMap<usize, Buckets>,
+        bucket_map: HashMap<NodeId, Buckets>,
     ) -> Self {
         Self {
             is_explain,
@@ -201,7 +202,7 @@ where
                                 materialize_values(&mut self.exec_plan, *motion_id)?
                             {
                                 self.exec_plan.set_motion_vtable(
-                                    *motion_id,
+                                    motion_id,
                                     virtual_table,
                                     &vshard,
                                 )?;
@@ -227,11 +228,11 @@ where
                 let buckets = self.bucket_discovery(top_id)?;
                 let virtual_table = self.coordinator.materialize_motion(
                     &mut self.exec_plan,
-                    *motion_id,
+                    motion_id,
                     &buckets,
                 )?;
                 self.exec_plan
-                    .set_motion_vtable(*motion_id, virtual_table, &vshard)?;
+                    .set_motion_vtable(motion_id, virtual_table, &vshard)?;
             }
         }
 
@@ -273,7 +274,7 @@ where
                 return Err(err("no vtables in plan with motion top"));
             };
             let Some(mut vtable) = vtables.remove(&top_id) else {
-                return Err(err(&format!("no motion on top_id: {top_id}")));
+                return Err(err(&format!("no motion on top_id: {top_id:?}")));
             };
             let Some(v) = Rc::get_mut(&mut vtable) else {
                 return Err(err("there are other refs to vtable"));
diff --git a/sbroad-core/src/executor/bucket.rs b/sbroad-core/src/executor/bucket.rs
index 072ea8f8f..ebaae19bb 100644
--- a/sbroad-core/src/executor/bucket.rs
+++ b/sbroad-core/src/executor/bucket.rs
@@ -7,11 +7,11 @@ use crate::errors::{Action, Entity, SbroadError};
 use crate::executor::engine::{Router, Vshard};
 use crate::executor::Query;
 use crate::ir::distribution::Distribution;
-use crate::ir::expression::Expression;
+use crate::ir::expression::{Expression, NodeId};
 use crate::ir::helpers::RepeatableState;
 use crate::ir::operator::{Bool, JoinKind, Relational};
 use crate::ir::transformation::redistribution::MotionPolicy;
-use crate::ir::tree::traversal::{PostOrderWithFilter, REL_CAPACITY};
+use crate::ir::tree::traversal::{LevelNode, PostOrderWithFilter, REL_CAPACITY};
 use crate::ir::value::Value;
 use crate::ir::Node;
 use crate::otm::child_span;
@@ -88,7 +88,7 @@ where
     /// In general it returns `Buckets::All`, but in some cases (e.g. `Eq` and `In` operators) it
     /// will return `Buckets::Filtered` (if such a result is met in SELECT or JOIN filter, it means
     /// that we can execute the query only on some of the replicasets).
-    fn get_buckets_from_expr(&self, expr_id: usize) -> Result<Option<Buckets>, SbroadError> {
+    fn get_buckets_from_expr(&self, expr_id: NodeId) -> Result<Option<Buckets>, SbroadError> {
         // The only possible case there will be several `Buckets` in the vec is when we have `Eq`.
         // See the logic of its handling below.
         let mut buckets: Vec<Buckets> = vec![];
@@ -206,8 +206,8 @@ where
     /// In case there is just one expression (not `ANDed`) `chains` will remain empty.
     fn get_expression_tree_buckets(
         &self,
-        expr_id: usize,
-        rel_children: &[usize],
+        expr_id: NodeId,
+        rel_children: &[NodeId],
     ) -> Result<Buckets, SbroadError> {
         let ir_plan = self.exec_plan.get_ir_plan();
         let chains = ir_plan.get_dnf_chains(expr_id)?;
@@ -273,7 +273,7 @@ where
     /// - Relational nodes contain invalid children.
     #[allow(clippy::too_many_lines)]
     #[otm_child_span("query.bucket.discovery")]
-    pub fn bucket_discovery(&mut self, top_id: usize) -> Result<Buckets, SbroadError> {
+    pub fn bucket_discovery(&mut self, top_id: NodeId) -> Result<Buckets, SbroadError> {
         // if top's output has Distribution::Single then the whole subtree must executed only on
         // a single node, no need to traverse the subtree
         let top_output_id = self.exec_plan.get_ir_plan().get_relational_output(top_id)?;
@@ -291,7 +291,7 @@ where
         }
 
         let ir_plan = self.exec_plan.get_ir_plan();
-        let filter = |node_id: usize| -> bool {
+        let filter = |node_id: NodeId| -> bool {
             if let Ok(Node::Relational(_)) = ir_plan.get_node(node_id) {
                 return true;
             }
@@ -305,7 +305,7 @@ where
             Box::new(filter),
         );
 
-        for (_, node_id) in tree.iter(top_id) {
+        for LevelNode(_, node_id) in tree.iter(top_id) {
             if self.bucket_map.contains_key(&node_id) {
                 continue;
             }
diff --git a/sbroad-core/src/executor/engine.rs b/sbroad-core/src/executor/engine.rs
index 556bde45a..509a93a58 100644
--- a/sbroad-core/src/executor/engine.rs
+++ b/sbroad-core/src/executor/engine.rs
@@ -7,6 +7,7 @@ use tarantool::tuple::Tuple;
 
 use crate::cbo::histogram::Scalar;
 use crate::cbo::{ColumnStats, TableColumnPair, TableStats};
+use crate::ir::expression::NodeId;
 use crate::utils::MutexLike;
 use std::any::Any;
 
@@ -23,7 +24,6 @@ use crate::ir::function::Function;
 use crate::ir::relation::Table;
 use crate::ir::relation::Type;
 use crate::ir::value::Value;
-use crate::ir::NodeId;
 
 use super::result::ProducerResult;
 
@@ -277,7 +277,7 @@ pub trait Router: QueryCache {
     fn dispatch(
         &self,
         plan: &mut ExecutionPlan,
-        top_id: usize,
+        top_id: NodeId,
         buckets: &Buckets,
         return_format: DispatchReturnFormat,
     ) -> Result<Box<dyn Any>, SbroadError>;
@@ -289,7 +289,7 @@ pub trait Router: QueryCache {
     fn materialize_motion(
         &self,
         plan: &mut ExecutionPlan,
-        motion_node_id: usize,
+        motion_node_id: &NodeId,
         buckets: &Buckets,
     ) -> Result<VirtualTable, SbroadError>;
 
diff --git a/sbroad-core/src/executor/engine/helpers.rs b/sbroad-core/src/executor/engine/helpers.rs
index f29ba3c0a..77a7cc8a9 100644
--- a/sbroad-core/src/executor/engine/helpers.rs
+++ b/sbroad-core/src/executor/engine/helpers.rs
@@ -1,6 +1,6 @@
 use ahash::AHashMap;
 
-use crate::{error, utils::MutexLike};
+use crate::{error, ir::expression::NodeId, utils::MutexLike};
 use itertools::enumerate;
 use smol_str::{format_smolstr, SmolStr, ToSmolStr};
 use std::{
@@ -24,7 +24,6 @@ use crate::executor::engine::{QueryCache, StorageCache};
 use crate::executor::protocol::{EncodedTables, SchemaInfo};
 use crate::ir::operator::ConflictStrategy;
 use crate::ir::value::{EncodedValue, LuaValue, MsgPackValue};
-use crate::ir::NodeId;
 use crate::otm::child_span;
 use crate::utils::ByteCounter;
 use crate::{
@@ -436,7 +435,7 @@ pub type TupleBuilderPattern = Vec<TupleBuilderCommand>;
 pub fn init_local_update_tuple_builder(
     plan: &Plan,
     vtable: &VirtualTable,
-    update_id: usize,
+    update_id: NodeId,
 ) -> Result<TupleBuilderPattern, SbroadError> {
     if let Relational::Update {
         relation,
@@ -520,7 +519,7 @@ pub fn init_local_update_tuple_builder(
     }
     Err(SbroadError::Invalid(
         Entity::Node,
-        Some(format_smolstr!("expected Update on id ({update_id})")),
+        Some(format_smolstr!("expected Update on id ({update_id:?})")),
     ))
 }
 
@@ -530,7 +529,7 @@ pub fn init_local_update_tuple_builder(
 /// - plan top is not Delete
 pub fn init_delete_tuple_builder(
     plan: &Plan,
-    delete_id: usize,
+    delete_id: NodeId,
 ) -> Result<TupleBuilderPattern, SbroadError> {
     let table = plan.dml_node_table(delete_id)?;
     let mut commands = Vec::with_capacity(table.primary_key.positions.len());
@@ -547,7 +546,7 @@ pub fn init_delete_tuple_builder(
 pub fn init_insert_tuple_builder(
     plan: &Plan,
     vtable: &VirtualTable,
-    insert_id: usize,
+    insert_id: NodeId,
 ) -> Result<TupleBuilderPattern, SbroadError> {
     let columns = plan.insert_columns(insert_id)?;
     // Revert map of { pos_in_child_node -> pos_in_relation }
@@ -663,7 +662,7 @@ pub fn build_insert_args<'t>(
 fn init_sharded_update_tuple_builder(
     plan: &Plan,
     vtable: &VirtualTable,
-    update_id: usize,
+    update_id: NodeId,
 ) -> Result<TupleBuilderPattern, SbroadError> {
     let Relational::Update {
         update_columns_map, ..
@@ -672,7 +671,7 @@ fn init_sharded_update_tuple_builder(
         return Err(SbroadError::Invalid(
             Entity::Node,
             Some(format_smolstr!(
-                "update tuple builder: expected update node on id: {update_id}"
+                "update tuple builder: expected update node on id: {update_id:?}"
             )),
         ));
     };
@@ -825,7 +824,7 @@ fn has_zero_limit_clause(plan: &ExecutionPlan) -> Result<bool, SbroadError> {
 pub fn dispatch_impl(
     coordinator: &impl Router,
     plan: &mut ExecutionPlan,
-    top_id: usize,
+    top_id: NodeId,
     buckets: &Buckets,
     return_format: DispatchReturnFormat,
 ) -> Result<Box<dyn Any>, SbroadError> {
@@ -881,7 +880,7 @@ pub fn dispatch_by_buckets(
                     if !vtable.get_bucket_index().is_empty() {
                         return Err(SbroadError::Invalid(
                             Entity::Motion,
-                            Some(format_smolstr!("motion ({motion_id}) in subtree with distribution Single, but policy is not Full!")),
+                            Some(format_smolstr!("motion ({motion_id:?}) in subtree with distribution Single, but policy is not Full!")),
                         ));
                     }
                 }
@@ -897,7 +896,7 @@ pub fn dispatch_by_buckets(
 #[allow(clippy::too_many_lines)]
 pub(crate) fn materialize_values(
     plan: &mut ExecutionPlan,
-    motion_node_id: usize,
+    motion_node_id: NodeId,
 ) -> Result<Option<VirtualTable>, SbroadError> {
     // Check that the motion node has a local segment policy.
     let motion_node = plan.get_ir_plan().get_relation_node(motion_node_id)?;
@@ -1004,7 +1003,7 @@ pub(crate) fn materialize_values(
                 return Err(SbroadError::Invalid(
                     Entity::Node,
                     Some(format_smolstr!(
-                        "value node child ({child_id}) is not a values row node!"
+                        "value node child ({child_id:?}) is not a values row node!"
                     )),
                 ));
             }
@@ -1031,7 +1030,7 @@ pub(crate) fn materialize_values(
                 return Err(SbroadError::Invalid(
                     Entity::Node,
                     Some(format_smolstr!(
-                        "output column ({column_id}) is not an alias node!"
+                        "output column ({column_id:?}) is not an alias node!"
                     )),
                 ));
             }
@@ -1051,7 +1050,7 @@ pub(crate) fn materialize_values(
 pub fn materialize_motion(
     runtime: &impl Router,
     plan: &mut ExecutionPlan,
-    motion_node_id: usize,
+    motion_node_id: NodeId,
     buckets: &Buckets,
 ) -> Result<VirtualTable, SbroadError> {
     let top_id = plan.get_motion_subtree_root(motion_node_id)?;
@@ -1548,7 +1547,7 @@ fn materialize_vtable_locally<R: Vshard + QueryCache>(
     runtime: &R,
     optional: &mut OptionalData,
     required: &mut RequiredData,
-    child_id: usize,
+    child_id: NodeId,
 ) -> Result<(), SbroadError>
 where
     R::Cache: StorageCache,
@@ -1565,7 +1564,7 @@ where
         SbroadError::FailedTo(
             Action::Deserialize,
             Some(Entity::Tuple),
-            format_smolstr!("motion node {child_id}. {e:?}"),
+            format_smolstr!("motion node {child_id:?}. {e:?}"),
         )
     })?;
     let mut reader = bytes.as_ref().as_slice();
@@ -1573,7 +1572,7 @@ where
         SbroadError::FailedTo(
             Action::Decode,
             Some(Entity::Tuple),
-            format_smolstr!("motion node {child_id}. {e}"),
+            format_smolstr!("motion node {child_id:?}. {e}"),
         )
     })?;
     let vtable = data
@@ -1584,7 +1583,7 @@ where
         .as_virtual_table(column_names, true)?;
     optional
         .exec_plan
-        .set_motion_vtable(child_id, vtable, runtime)?;
+        .set_motion_vtable(&child_id, vtable, runtime)?;
     Ok(())
 }
 
diff --git a/sbroad-core/src/executor/engine/helpers/storage.rs b/sbroad-core/src/executor/engine/helpers/storage.rs
index a0556cb8f..c1756ffb9 100644
--- a/sbroad-core/src/executor/engine/helpers/storage.rs
+++ b/sbroad-core/src/executor/engine/helpers/storage.rs
@@ -17,7 +17,7 @@ use crate::executor::engine::helpers::proxy::sql_cache_proxy;
 use crate::executor::engine::helpers::table_name;
 use crate::executor::lru::DEFAULT_CAPACITY;
 use crate::ir::value::{EncodedValue, Value};
-use crate::ir::NodeId;
+use crate::ir::expression::NodeId;
 use crate::otm::child_span;
 use crate::utils::ByteCounter;
 
diff --git a/sbroad-core/src/executor/engine/helpers/vshard.rs b/sbroad-core/src/executor/engine/helpers/vshard.rs
index 6dfedda56..87302d9d5 100644
--- a/sbroad-core/src/executor/engine/helpers/vshard.rs
+++ b/sbroad-core/src/executor/engine/helpers/vshard.rs
@@ -19,14 +19,15 @@ use crate::{
         result::ProducerResult,
     },
     ir::{
+        expression::NodeId,
         helpers::RepeatableState,
         operator::Relational,
         transformation::redistribution::{MotionOpcode, MotionPolicy},
         tree::{
             relation::RelationalIterator,
-            traversal::{PostOrderWithFilter, REL_CAPACITY},
+            traversal::{LevelNode, PostOrderWithFilter, REL_CAPACITY},
         },
-        Node, NodeId, Plan,
+        Node, Plan,
     },
     otm::child_span,
 };
@@ -748,7 +749,7 @@ struct SerializeAsEmptyInfo {
 impl Plan {
     // return true if given node is Motion containing seriliaze as empty
     // opcode. If `check_enabled` is true checks that the opcode is enabled.
-    fn is_serialize_as_empty_motion(&self, node_id: usize, check_enabled: bool) -> bool {
+    fn is_serialize_as_empty_motion(&self, node_id: NodeId, check_enabled: bool) -> bool {
         if let Ok(Node::Relational(Relational::Motion { program, .. })) = self.get_node(node_id) {
             if let Some(op) = program
                 .0
@@ -761,8 +762,8 @@ impl Plan {
         false
     }
 
-    fn collect_top_ids(&self) -> Result<Vec<usize>, SbroadError> {
-        let mut stop_nodes: HashSet<usize> = HashSet::new();
+    fn collect_top_ids(&self) -> Result<Vec<NodeId>, SbroadError> {
+        let mut stop_nodes: HashSet<NodeId> = HashSet::new();
         let iter_children = |node_id| -> RelationalIterator<'_> {
             if self.is_serialize_as_empty_motion(node_id, true) {
                 stop_nodes.insert(node_id);
@@ -773,17 +774,17 @@ impl Plan {
             }
             self.nodes.rel_iter(node_id)
         };
-        let filter = |node_id: usize| -> bool { self.is_serialize_as_empty_motion(node_id, true) };
+        let filter = |node_id: NodeId| -> bool { self.is_serialize_as_empty_motion(node_id, true) };
         let mut dfs = PostOrderWithFilter::with_capacity(iter_children, 4, Box::new(filter));
 
-        Ok(dfs.iter(self.get_top()?).map(|(_, id)| id).collect())
+        Ok(dfs.iter(self.get_top()?).map(|id| id.1).collect())
     }
 
     fn serialize_as_empty_info(&self) -> Result<Option<SerializeAsEmptyInfo>, SbroadError> {
         let top_ids = self.collect_top_ids()?;
 
         let mut motions_ref_count: AHashMap<NodeId, usize> = AHashMap::new();
-        let filter = |node_id: usize| -> bool {
+        let filter = |node_id: NodeId| -> bool {
             matches!(
                 self.get_node(node_id),
                 Ok(Node::Relational(Relational::Motion { .. }))
@@ -791,7 +792,7 @@ impl Plan {
         };
         let mut dfs =
             PostOrderWithFilter::with_capacity(|x| self.nodes.rel_iter(x), 0, Box::new(filter));
-        for (_, motion_id) in dfs.iter(self.get_top()?) {
+        for LevelNode(_, motion_id) in dfs.iter(self.get_top()?) {
             motions_ref_count
                 .entry(motion_id)
                 .and_modify(|cnt| *cnt += 1)
@@ -805,7 +806,7 @@ impl Plan {
         // all motion nodes that are inside the subtrees
         // defined by `top_ids`
         let all_motion_nodes = {
-            let is_motion = |node_id: usize| -> bool {
+            let is_motion = |node_id: NodeId| -> bool {
                 matches!(
                     self.get_node(node_id),
                     Ok(Node::Relational(Relational::Motion { .. }))
@@ -818,7 +819,7 @@ impl Plan {
                     REL_CAPACITY,
                     Box::new(is_motion),
                 );
-                all_motions.extend(dfs.iter(*top_id).map(|(_, id)| id));
+                all_motions.extend(dfs.iter(*top_id).map(|id| id.1));
             }
             all_motions
         };
@@ -890,7 +891,7 @@ fn apply_serialize_as_empty_opcode(
         for motion_id in &unused_motions {
             let Some(use_count) = info.motions_ref_count.get_mut(motion_id) else {
                 return Err(SbroadError::UnexpectedNumberOfValues(format_smolstr!(
-                    "no ref count for motion={motion_id}"
+                    "no ref count for motion={motion_id:?}"
                 )));
             };
             if *use_count > 1 {
@@ -925,7 +926,7 @@ fn disable_serialize_as_empty_opcode(
         } else {
             return Err(SbroadError::Invalid(
                 Entity::Node,
-                Some(format_smolstr!("expected motion node on id {motion_id}")),
+                Some(format_smolstr!("expected motion node on id {motion_id:?}")),
             ));
         };
         for op in &mut program.0 {
diff --git a/sbroad-core/src/executor/engine/mock.rs b/sbroad-core/src/executor/engine/mock.rs
index 7a4336aac..900d90740 100644
--- a/sbroad-core/src/executor/engine/mock.rs
+++ b/sbroad-core/src/executor/engine/mock.rs
@@ -30,6 +30,7 @@ use crate::executor::result::ProducerResult;
 use crate::executor::vtable::VirtualTable;
 use crate::executor::Cache;
 use crate::frontend::sql::ast::AbstractSyntaxTree;
+use crate::ir::expression::NodeId;
 use crate::ir::function::Function;
 use crate::ir::relation::{Column, ColumnRole, SpaceEngine, Table, Type};
 use crate::ir::tree::Snapshot;
@@ -826,7 +827,7 @@ pub struct RouterRuntimeMock {
     // It's based on the RefCells instead of tarantool mutexes,
     // so it could be used in unit tests - they won't compile otherwise due to missing tarantool symbols.
     metadata: RefCell<RouterConfigurationMock>,
-    virtual_tables: RefCell<HashMap<usize, VirtualTable>>,
+    virtual_tables: RefCell<HashMap<NodeId, VirtualTable>>,
     ir_cache: Rc<RefCell<LRUCache<SmolStr, Plan>>>,
     table_statistics_cache: RefCell<HashMap<SmolStr, Rc<TableStats>>>,
     initial_column_statistics_cache: RefCell<HashMap<TableColumnPair, Rc<Box<dyn Any>>>>,
@@ -1299,7 +1300,7 @@ impl RouterRuntimeMock {
     }
 
     #[allow(dead_code)]
-    pub fn add_virtual_table(&self, id: usize, table: VirtualTable) {
+    pub fn add_virtual_table(&self, id: NodeId, table: VirtualTable) {
         self.virtual_tables.borrow_mut().insert(id, table);
     }
 }
@@ -1316,15 +1317,15 @@ impl Router for RouterRuntimeMock {
     fn materialize_motion(
         &self,
         _plan: &mut ExecutionPlan,
-        motion_node_id: usize,
+        motion_node_id: &NodeId,
         _buckets: &Buckets,
     ) -> Result<VirtualTable, SbroadError> {
-        if let Some(virtual_table) = self.virtual_tables.borrow().get(&motion_node_id) {
+        if let Some(virtual_table) = self.virtual_tables.borrow().get(motion_node_id) {
             Ok(virtual_table.clone())
         } else {
             Err(SbroadError::NotFound(
                 Entity::VirtualTable,
-                format_smolstr!("for motion node {motion_node_id}"),
+                format_smolstr!("for motion node {motion_node_id:?}"),
             ))
         }
     }
@@ -1332,7 +1333,7 @@ impl Router for RouterRuntimeMock {
     fn dispatch(
         &self,
         plan: &mut ExecutionPlan,
-        top_id: usize,
+        top_id: NodeId,
         buckets: &Buckets,
         _return_format: DispatchReturnFormat,
     ) -> Result<Box<dyn Any>, SbroadError> {
@@ -1488,7 +1489,7 @@ pub struct ReplicasetDispatchInfo {
     pub rs_id: usize,
     pub pattern: String,
     pub params: Vec<Value>,
-    pub vtables_map: HashMap<usize, Rc<VirtualTable>>,
+    pub vtables_map: HashMap<NodeId, Rc<VirtualTable>>,
 }
 
 impl ReplicasetDispatchInfo {
@@ -1502,7 +1503,7 @@ impl ReplicasetDispatchInfo {
         let (pattern_with_params, _) = exec_plan
             .to_sql(&syntax_data_nodes, TEMPLATE, None)
             .unwrap();
-        let mut vtables: HashMap<usize, Rc<VirtualTable>> = HashMap::new();
+        let mut vtables: HashMap<NodeId, Rc<VirtualTable>> = HashMap::new();
         if let Some(vtables_map) = exec_plan.get_vtables() {
             vtables.clone_from(vtables_map);
         }
diff --git a/sbroad-core/src/executor/ir.rs b/sbroad-core/src/executor/ir.rs
index 1b6888d7b..5a4fb3743 100644
--- a/sbroad-core/src/executor/ir.rs
+++ b/sbroad-core/src/executor/ir.rs
@@ -8,12 +8,12 @@ use smol_str::{format_smolstr, SmolStr, ToSmolStr};
 use crate::errors::{Action, Entity, SbroadError};
 use crate::executor::engine::Vshard;
 use crate::executor::vtable::{VirtualTable, VirtualTableMap};
-use crate::ir::expression::Expression;
+use crate::ir::expression::{Expression, NodeId};
 use crate::ir::operator::{OrderByElement, OrderByEntity, Relational};
 use crate::ir::relation::SpaceEngine;
 use crate::ir::transformation::redistribution::{MotionOpcode, MotionPolicy};
-use crate::ir::tree::traversal::PostOrder;
-use crate::ir::{ExecuteOptions, Node, Plan};
+use crate::ir::tree::traversal::{LevelNode, PostOrder};
+use crate::ir::{ArenaType, ExecuteOptions, Node, Plan};
 use crate::otm::child_span;
 use sbroad_proc::otm_child_span;
 
@@ -63,7 +63,7 @@ impl From<Plan> for ExecutionPlan {
 
 /// Translates the original plan's node id to the new sub-plan one.
 struct SubtreeMap {
-    inner: AHashMap<usize, usize>,
+    inner: AHashMap<NodeId, NodeId>,
 }
 
 impl SubtreeMap {
@@ -73,18 +73,18 @@ impl SubtreeMap {
         }
     }
 
-    fn get_id(&self, expr_id: usize) -> usize {
+    fn get_id(&self, expr_id: NodeId) -> NodeId {
         *self
             .inner
             .get(&expr_id)
-            .unwrap_or_else(|| panic!("Could not find expr with id {expr_id} in subtree map"))
+            .unwrap_or_else(|| panic!("Could not find expr with id {expr_id:?} in subtree map"))
     }
 
-    fn contains_key(&self, expr_id: usize) -> bool {
+    fn contains_key(&self, expr_id: NodeId) -> bool {
         self.inner.contains_key(&expr_id)
     }
 
-    fn insert(&mut self, old_id: usize, new_id: usize) {
+    fn insert(&mut self, old_id: NodeId, new_id: NodeId) {
         self.inner.insert(old_id, new_id);
     }
 }
@@ -111,15 +111,15 @@ impl ExecutionPlan {
     }
 
     #[must_use]
-    pub fn get_vtables(&self) -> Option<&HashMap<usize, Rc<VirtualTable>>> {
+    pub fn get_vtables(&self) -> Option<&HashMap<NodeId, Rc<VirtualTable>>> {
         self.vtables.as_ref().map(VirtualTableMap::map)
     }
 
-    pub fn get_mut_vtables(&mut self) -> Option<&mut HashMap<usize, Rc<VirtualTable>>> {
+    pub fn get_mut_vtables(&mut self) -> Option<&mut HashMap<NodeId, Rc<VirtualTable>>> {
         self.vtables.as_mut().map(VirtualTableMap::mut_map)
     }
 
-    pub fn set_vtables(&mut self, vtables: HashMap<usize, Rc<VirtualTable>>) {
+    pub fn set_vtables(&mut self, vtables: HashMap<NodeId, Rc<VirtualTable>>) {
         self.vtables = Some(VirtualTableMap::new(vtables));
     }
 
@@ -127,7 +127,7 @@ impl ExecutionPlan {
     ///
     /// # Errors
     /// - Failed to find a virtual table for the motion node.
-    pub fn get_motion_vtable(&self, motion_id: usize) -> Result<Rc<VirtualTable>, SbroadError> {
+    pub fn get_motion_vtable(&self, motion_id: NodeId) -> Result<Rc<VirtualTable>, SbroadError> {
         if let Some(vtable) = self.get_vtables() {
             if let Some(result) = vtable.get(&motion_id) {
                 return Ok(Rc::clone(result));
@@ -138,7 +138,7 @@ impl ExecutionPlan {
         Err(SbroadError::NotFound(
             Entity::VirtualTable,
             format_smolstr!(
-                "for Motion node ({motion_id}): {motion_node:?}. Plan: {:?}",
+                "for Motion node ({motion_id:?}): {motion_node:?}. Plan: {:?}",
                 self
             ),
         ))
@@ -151,13 +151,13 @@ impl ExecutionPlan {
     #[otm_child_span("query.motion.add")]
     pub fn set_motion_vtable(
         &mut self,
-        motion_id: usize,
+        motion_id: &NodeId,
         vtable: VirtualTable,
         runtime: &impl Vshard,
     ) -> Result<(), SbroadError> {
         let mut vtable = vtable;
         let program_len = if let Relational::Motion { program, .. } =
-            self.get_ir_plan().get_relation_node(motion_id)?
+            self.get_ir_plan().get_relation_node(*motion_id)?
         {
             program.0.len()
         } else {
@@ -168,7 +168,7 @@ impl ExecutionPlan {
         };
         for op_idx in 0..program_len {
             let plan = self.get_ir_plan();
-            let opcode = plan.get_motion_opcode(motion_id, op_idx)?;
+            let opcode = plan.get_motion_opcode(*motion_id, op_idx)?;
             match opcode {
                 MotionOpcode::RemoveDuplicates => {
                     vtable.remove_duplicates();
@@ -177,7 +177,7 @@ impl ExecutionPlan {
                     // Resharding must be done before applying projection
                     // to the virtual table. Otherwise projection can
                     // remove sharding columns.
-                    match plan.get_motion_policy(motion_id)? {
+                    match plan.get_motion_policy(*motion_id)? {
                         MotionPolicy::Segment(shard_key)
                         | MotionPolicy::LocalSegment(shard_key) => {
                             vtable.reshard(shard_key, runtime)?;
@@ -213,7 +213,7 @@ impl ExecutionPlan {
                     };
                     let Some(from_vtable) = vtables.map().get(&motion_id) else {
                         return Err(SbroadError::UnexpectedNumberOfValues(format_smolstr!(
-                            "expected virtual table for motion {motion_id}"
+                            "expected virtual table for motion {motion_id:?}"
                         )));
                     };
                     vtable.add_missing_rows(from_vtable)?;
@@ -228,7 +228,7 @@ impl ExecutionPlan {
         }
 
         if let Some(vtables) = self.get_mut_vtables() {
-            vtables.insert(motion_id, Rc::new(vtable));
+            vtables.insert(*motion_id, Rc::new(vtable));
         }
 
         Ok(())
@@ -272,7 +272,7 @@ impl ExecutionPlan {
     /// # Errors
     /// - node is not `Relation` type
     /// - node is not `Motion` type
-    pub fn get_motion_policy(&self, node_id: usize) -> Result<MotionPolicy, SbroadError> {
+    pub fn get_motion_policy(&self, node_id: NodeId) -> Result<MotionPolicy, SbroadError> {
         if let Relational::Motion { policy, .. } = &self.plan.get_relation_node(node_id)? {
             return Ok(policy.clone());
         }
@@ -287,7 +287,7 @@ impl ExecutionPlan {
     ///
     /// # Errors
     /// - node is not valid
-    pub fn get_motion_alias(&self, node_id: usize) -> Result<Option<SmolStr>, SbroadError> {
+    pub fn get_motion_alias(&self, node_id: NodeId) -> Result<Option<SmolStr>, SbroadError> {
         let child_id = &self.get_motion_child(node_id)?;
         let child_rel = self.get_ir_plan().get_relation_node(*child_id)?;
         match child_rel {
@@ -301,7 +301,7 @@ impl ExecutionPlan {
     ///
     /// # Errors
     /// - node is not valid
-    pub fn get_motion_subtree_root(&self, node_id: usize) -> Result<usize, SbroadError> {
+    pub fn get_motion_subtree_root(&self, node_id: NodeId) -> Result<NodeId, SbroadError> {
         let top_id = &self.get_motion_child(node_id)?;
         let rel = self.get_ir_plan().get_relation_node(*top_id)?;
         match rel {
@@ -337,12 +337,12 @@ impl ExecutionPlan {
     /// # Errors
     /// - node is not `Relation` type
     /// - node does not contain children
-    pub(crate) fn get_motion_child(&self, node_id: usize) -> Result<usize, SbroadError> {
+    pub(crate) fn get_motion_child(&self, node_id: NodeId) -> Result<NodeId, SbroadError> {
         let node = self.get_ir_plan().get_relation_node(node_id)?;
         if !node.is_motion() {
             return Err(SbroadError::Invalid(
                 Entity::Relational,
-                Some(format_smolstr!("current node ({node_id}) is not motion")),
+                Some(format_smolstr!("current node ({node_id:?}) is not motion")),
             ));
         }
 
@@ -350,7 +350,7 @@ impl ExecutionPlan {
 
         assert!(
             children.len() == 1,
-            "Motion node ({node_id}:{node:?}) must have a single child only (actual {})",
+            "Motion node ({node_id:?}:{node:?}) must have a single child only (actual {})",
             children.len()
         );
 
@@ -366,7 +366,7 @@ impl ExecutionPlan {
     ///
     /// # Errors
     /// - not a motion node
-    pub fn unlink_motion_subtree(&mut self, motion_id: usize) -> Result<(), SbroadError> {
+    pub fn unlink_motion_subtree(&mut self, motion_id: NodeId) -> Result<(), SbroadError> {
         let motion = self.get_mut_ir_plan().get_mut_relation_node(motion_id)?;
         if let Relational::Motion {
             ref mut children, ..
@@ -376,7 +376,7 @@ impl ExecutionPlan {
         } else {
             return Err(SbroadError::Invalid(
                 Entity::Relational,
-                Some(format_smolstr!("node ({motion_id}) is not motion")),
+                Some(format_smolstr!("node ({motion_id:?}) is not motion")),
             ));
         }
         Ok(())
@@ -391,11 +391,11 @@ impl ExecutionPlan {
     /// # Panics
     /// - Plan is in invalid state
     #[allow(clippy::too_many_lines)]
-    pub fn take_subtree(&mut self, top_id: usize) -> Result<Self, SbroadError> {
+    pub fn take_subtree(&mut self, top_id: NodeId) -> Result<Self, SbroadError> {
         // Get the subtree nodes indexes.
         let plan = self.get_ir_plan();
         let mut subtree =
-            PostOrder::with_capacity(|node| plan.exec_plan_subtree_iter(node), plan.next_id());
+            PostOrder::with_capacity(|node| plan.exec_plan_subtree_iter(node), plan.nodes.len());
         subtree.populate_nodes(top_id);
         let nodes = subtree.take_nodes();
 
@@ -404,7 +404,7 @@ impl ExecutionPlan {
         // as a set to avoid their removal.
         let cte_scans = nodes
             .iter()
-            .map(|(_, id)| *id)
+            .map(|LevelNode(_, id)| *id)
             .filter(|id| {
                 matches!(
                     plan.get_node(*id),
@@ -424,9 +424,11 @@ impl ExecutionPlan {
                 unreachable!("Expected CTE scan node.");
             };
             let child_id = *child;
-            if all_cte_nodes_capacity < child_id {
-                all_cte_nodes_capacity = child_id;
+            let offset = usize::try_from(child_id.offset).unwrap();
+            if all_cte_nodes_capacity < offset {
+                all_cte_nodes_capacity = offset;
             }
+
             cte_amount += 1;
         }
         all_cte_nodes_capacity += 1;
@@ -436,7 +438,7 @@ impl ExecutionPlan {
             all_cte_nodes_capacity / cte_amount * 2
         };
 
-        let mut cte_ids: AHashSet<usize> = AHashSet::new();
+        let mut cte_ids: AHashSet<NodeId> = AHashSet::new();
         let mut is_reserved = false;
         for cte_id in cte_scans {
             if !is_reserved {
@@ -447,7 +449,7 @@ impl ExecutionPlan {
                 |node| plan.exec_plan_subtree_iter(node),
                 single_cte_capacity,
             );
-            for (_, id) in cte_subtree.iter(cte_id) {
+            for LevelNode(_, id) in cte_subtree.iter(cte_id) {
                 cte_ids.insert(id);
             }
         }
@@ -455,12 +457,12 @@ impl ExecutionPlan {
         let mut subtree_map = SubtreeMap::with_capacity(nodes.len());
         let vtables_capacity = self.get_vtables().map_or_else(|| 1, HashMap::len);
         // Map of { plan node_id -> virtual table }.
-        let mut new_vtables: HashMap<usize, Rc<VirtualTable>> =
+        let mut new_vtables: HashMap<NodeId, Rc<VirtualTable>> =
             HashMap::with_capacity(vtables_capacity);
 
         let mut new_plan = Plan::new();
         new_plan.nodes.reserve(nodes.len());
-        for (_, node_id) in nodes {
+        for LevelNode(_, node_id) in nodes {
             // We have already processed this node (sub-queries in BETWEEN
             // and CTEs can be referred twice).
             if subtree_map.contains_key(node_id) {
@@ -469,7 +471,7 @@ impl ExecutionPlan {
 
             // Node from original plan that we'll take and replace with mock parameter node.
             let dst_node = self.get_mut_ir_plan().get_mut_node(node_id)?;
-            let next_id = new_plan.nodes.next_id();
+            let next_id = new_plan.nodes.next_id(ArenaType::Default);
 
             // Replace the node with some invalid value.
             // TODO: introduce some new enum variant for this purpose.
@@ -534,7 +536,7 @@ impl ExecutionPlan {
                             }
                         }
                         Relational::GroupBy { gr_cols, .. } => {
-                            let mut new_cols: Vec<usize> = Vec::with_capacity(gr_cols.len());
+                            let mut new_cols: Vec<NodeId> = Vec::with_capacity(gr_cols.len());
                             for col_id in gr_cols.iter() {
                                 let new_col_id = subtree_map.get_id(*col_id);
                                 new_plan.replace_parent_in_subtree(
diff --git a/sbroad-core/src/executor/protocol.rs b/sbroad-core/src/executor/protocol.rs
index d635841c1..3905635d1 100644
--- a/sbroad-core/src/executor/protocol.rs
+++ b/sbroad-core/src/executor/protocol.rs
@@ -11,17 +11,18 @@ use crate::debug;
 use crate::errors::{Action, Entity, SbroadError};
 use crate::executor::ir::{ExecutionPlan, QueryType};
 use crate::ir::value::Value;
+use crate::ir::Options;
 use crate::otm::{current_id, extract_context, inject_context};
 
 use crate::executor::engine::TableVersionMap;
-use crate::ir::{NodeId, Options};
+use crate::ir::expression::NodeId;
 #[cfg(not(feature = "mock"))]
 use opentelemetry::trace::TraceContextExt;
 
 use super::engine::helpers::vshard::CacheInfo;
 use super::vtable::VirtualTableMeta;
 
-pub type VTablesMeta = HashMap<usize, VirtualTableMeta>;
+pub type VTablesMeta = HashMap<NodeId, VirtualTableMeta>;
 
 #[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
 pub struct Binary(Vec<u8>);
diff --git a/sbroad-core/src/executor/result.rs b/sbroad-core/src/executor/result.rs
index bbc2d8f77..49a8bffa2 100644
--- a/sbroad-core/src/executor/result.rs
+++ b/sbroad-core/src/executor/result.rs
@@ -20,6 +20,7 @@ use tarantool::tuple::Encode;
 use crate::debug;
 use crate::errors::SbroadError;
 use crate::executor::vtable::{VTableTuple, VirtualTable};
+use crate::ir::expression::NodeId;
 use crate::ir::operator::Relational;
 use crate::ir::relation::{Column, ColumnRole, Type};
 use crate::ir::tree::traversal::{PostOrderWithFilter, REL_CAPACITY};
@@ -257,8 +258,8 @@ impl Plan {
     ///
     /// # Errors
     /// - If relational iterator fails to return a correct node.
-    pub fn subtree_contains_values(&self, top_id: usize) -> Result<bool, SbroadError> {
-        let filter = |node_id: usize| -> bool {
+    pub fn subtree_contains_values(&self, top_id: NodeId) -> Result<bool, SbroadError> {
+        let filter = |node_id: NodeId| -> bool {
             if let Ok(Node::Relational(Relational::Values { .. })) = self.get_node(node_id) {
                 return true;
             }
diff --git a/sbroad-core/src/executor/tests.rs b/sbroad-core/src/executor/tests.rs
index 4ceca6001..71f536340 100644
--- a/sbroad-core/src/executor/tests.rs
+++ b/sbroad-core/src/executor/tests.rs
@@ -1,6 +1,6 @@
 use pretty_assertions::assert_eq;
 
-use crate::backend::sql::ir::PatternWithParams;
+use crate::{backend::sql::ir::PatternWithParams, ir::expression::NodeId};
 
 use crate::executor::engine::mock::RouterRuntimeMock;
 use crate::executor::result::ProducerResult;
@@ -914,7 +914,7 @@ fn virtual_table_23(alias: Option<&str>) -> VirtualTable {
     virtual_table
 }
 
-fn get_motion_policy(plan: &Plan, motion_id: usize) -> &MotionPolicy {
+fn get_motion_policy(plan: &Plan, motion_id: NodeId) -> &MotionPolicy {
     let motion = plan.get_relation_node(motion_id).unwrap();
     if let Relational::Motion { policy, .. } = motion {
         policy
diff --git a/sbroad-core/src/executor/tests/exec_plan.rs b/sbroad-core/src/executor/tests/exec_plan.rs
index ecd8d0dfb..6234727e1 100644
--- a/sbroad-core/src/executor/tests/exec_plan.rs
+++ b/sbroad-core/src/executor/tests/exec_plan.rs
@@ -13,7 +13,7 @@ use crate::ir::relation::Type;
 use crate::ir::tests::{column_integer_user_non_null, column_user_non_null};
 use crate::ir::transformation::redistribution::MotionPolicy;
 use crate::ir::tree::Snapshot;
-use crate::ir::{Node, Slice};
+use crate::ir::{ArenaType, Node, Slice};
 
 use super::*;
 
@@ -30,7 +30,7 @@ fn f_sql(s: &str) -> String {
 /// Used for testing.
 fn get_sql_from_execution_plan(
     exec_plan: &mut ExecutionPlan,
-    top_id: usize,
+    top_id: NodeId,
     snapshot: Snapshot,
     name_base: &str,
 ) -> PatternWithParams {
@@ -63,7 +63,7 @@ fn exec_plan_subtree_test() {
     {
         virtual_table.reshard(key, &query.coordinator).unwrap();
     }
-    let mut vtables: HashMap<usize, Rc<VirtualTable>> = HashMap::new();
+    let mut vtables: HashMap<NodeId, Rc<VirtualTable>> = HashMap::new();
     vtables.insert(motion_id, Rc::new(virtual_table));
 
     let exec_plan = query.get_mut_exec_plan();
@@ -116,7 +116,7 @@ fn exec_plan_subtree_two_stage_groupby_test() {
         virtual_table.reshard(key, &query.coordinator).unwrap();
     }
 
-    let mut vtables: HashMap<usize, Rc<VirtualTable>> = HashMap::new();
+    let mut vtables: HashMap<NodeId, Rc<VirtualTable>> = HashMap::new();
     vtables.insert(motion_id, Rc::new(virtual_table));
 
     let exec_plan = query.get_mut_exec_plan();
@@ -181,7 +181,7 @@ fn exec_plan_subtree_two_stage_groupby_test_2() {
         virtual_table.reshard(key, &query.coordinator).unwrap();
     }
 
-    let mut vtables: HashMap<usize, Rc<VirtualTable>> = HashMap::new();
+    let mut vtables: HashMap<NodeId, Rc<VirtualTable>> = HashMap::new();
     vtables.insert(motion_id, Rc::new(virtual_table));
 
     let exec_plan = query.get_mut_exec_plan();
@@ -198,12 +198,11 @@ fn exec_plan_subtree_two_stage_groupby_test_2() {
     assert_eq!(
         sql,
         PatternWithParams::new(
-            f_sql(
-                r#"SELECT "T1"."FIRST_NAME" as "column_12",
-"T1"."sysFrom" as "column_14",
-"T1"."sys_op" as "column_13"
-FROM "test_space" as "T1"
-GROUP BY "T1"."FIRST_NAME", "T1"."sys_op", "T1"."sysFrom""#
+            format!(
+                "{} {} {}",
+                r#"SELECT "T1"."FIRST_NAME" as "column_12", "T1"."sys_op" as "column_13","#,
+                r#""T1"."sysFrom" as "column_14" FROM "test_space" as "T1""#,
+                r#"GROUP BY "T1"."FIRST_NAME", "T1"."sys_op", "T1"."sysFrom""#,
             ),
             vec![]
         )
@@ -261,7 +260,7 @@ fn exec_plan_subtree_aggregates() {
         virtual_table.reshard(key, &query.coordinator).unwrap();
     }
 
-    let mut vtables: HashMap<usize, Rc<VirtualTable>> = HashMap::new();
+    let mut vtables: HashMap<NodeId, Rc<VirtualTable>> = HashMap::new();
     vtables.insert(motion_id, Rc::new(virtual_table));
 
     let exec_plan = query.get_mut_exec_plan();
@@ -278,16 +277,14 @@ fn exec_plan_subtree_aggregates() {
     assert_eq!(
         sql,
         PatternWithParams::new(
-            f_sql(
-                r#"SELECT "T1"."sys_op" as "column_12",
-("T1"."id") * ("T1"."sys_op") as "column_49",
-"T1"."id" as "column_46",
-group_concat ("T1"."FIRST_NAME", ?) as "group_concat_58",
-count ("T1"."sysFrom") as "count_37", total ("T1"."id") as "total_64",
-min ("T1"."id") as "min_67", count ("T1"."id") as "count_61",
-max ("T1"."id") as "max_70", sum ("T1"."id") as "sum_42"
-FROM "test_space" as "T1"
-GROUP BY "T1"."sys_op", ("T1"."id") * ("T1"."sys_op"), "T1"."id""#
+            format!(
+                "{} {} {} {} {} {}",
+                r#"SELECT ("T1"."id") * ("T1"."sys_op") as "column_49", "T1"."sys_op" as "column_12","#,
+                r#""T1"."id" as "column_46", group_concat ("T1"."FIRST_NAME", ?) as "group_concat_58", count ("T1"."sysFrom") as "count_37","#,
+                r#"total ("T1"."id") as "total_64","#,
+                r#"min ("T1"."id") as "min_67", count ("T1"."id") as "count_61", max ("T1"."id") as "max_70","#,
+                r#"sum ("T1"."id") as "sum_42" FROM "test_space" as "T1""#,
+                r#"GROUP BY "T1"."sys_op", ("T1"."id") * ("T1"."sys_op"), "T1"."id""#,
             ),
             vec![Value::from("o")]
         )
@@ -336,7 +333,7 @@ fn exec_plan_subtree_aggregates_no_groupby() {
         virtual_table.reshard(key, &query.coordinator).unwrap();
     }
 
-    let mut vtables: HashMap<usize, Rc<VirtualTable>> = HashMap::new();
+    let mut vtables: HashMap<NodeId, Rc<VirtualTable>> = HashMap::new();
     vtables.insert(motion_id, Rc::new(virtual_table));
 
     let exec_plan = query.get_mut_exec_plan();
@@ -392,7 +389,7 @@ fn exec_plan_subquery_under_motion_without_alias() {
     {
         virtual_table.reshard(key, &query.coordinator).unwrap();
     }
-    let mut vtables: HashMap<usize, Rc<VirtualTable>> = HashMap::new();
+    let mut vtables: HashMap<NodeId, Rc<VirtualTable>> = HashMap::new();
     vtables.insert(motion_id, Rc::new(virtual_table));
 
     let exec_plan = query.get_mut_exec_plan();
@@ -433,7 +430,7 @@ fn exec_plan_subquery_under_motion_with_alias() {
     {
         virtual_table.reshard(key, &query.coordinator).unwrap();
     }
-    let mut vtables: HashMap<usize, Rc<VirtualTable>> = HashMap::new();
+    let mut vtables: HashMap<NodeId, Rc<VirtualTable>> = HashMap::new();
     vtables.insert(motion_id, Rc::new(virtual_table));
 
     let exec_plan = query.get_mut_exec_plan();
@@ -468,7 +465,7 @@ fn exec_plan_motion_under_in_operator() {
     {
         virtual_table.reshard(key, &query.coordinator).unwrap();
     }
-    let mut vtables: HashMap<usize, Rc<VirtualTable>> = HashMap::new();
+    let mut vtables: HashMap<NodeId, Rc<VirtualTable>> = HashMap::new();
     vtables.insert(motion_id, Rc::new(virtual_table));
 
     let exec_plan = query.get_mut_exec_plan();
@@ -507,7 +504,7 @@ fn exec_plan_motion_under_except() {
     {
         virtual_table.reshard(key, &query.coordinator).unwrap();
     }
-    let mut vtables: HashMap<usize, Rc<VirtualTable>> = HashMap::new();
+    let mut vtables: HashMap<NodeId, Rc<VirtualTable>> = HashMap::new();
     vtables.insert(motion_id, Rc::new(virtual_table));
 
     let exec_plan = query.get_mut_exec_plan();
@@ -544,7 +541,7 @@ fn exec_plan_subtree_count_asterisk() {
         virtual_table.reshard(key, &query.coordinator).unwrap();
     }
 
-    let mut vtables: HashMap<usize, Rc<VirtualTable>> = HashMap::new();
+    let mut vtables: HashMap<NodeId, Rc<VirtualTable>> = HashMap::new();
     vtables.insert(motion_id, Rc::new(virtual_table));
 
     let exec_plan = query.get_mut_exec_plan();
@@ -607,7 +604,7 @@ fn exec_plan_subtree_having() {
         virtual_table.reshard(key, &query.coordinator).unwrap();
     }
 
-    let mut vtables: HashMap<usize, Rc<VirtualTable>> = HashMap::new();
+    let mut vtables: HashMap<NodeId, Rc<VirtualTable>> = HashMap::new();
     vtables.insert(motion_id, Rc::new(virtual_table));
 
     let exec_plan = query.get_mut_exec_plan();
@@ -631,7 +628,7 @@ fn exec_plan_subtree_having() {
         PatternWithParams::new(
             format!(
                 "{} {} {}",
-                r#"SELECT "T1"."sys_op" as "column_12", ("T1"."sys_op") * (?) as "column_64","#,
+                r#"SELECT ("T1"."sys_op") * (?) as "column_64", "T1"."sys_op" as "column_12","#,
                 r#"count (("T1"."sys_op") * (?)) as "count_60" FROM "test_space" as "T1""#,
                 r#"GROUP BY "T1"."sys_op", ("T1"."sys_op") * (?)"#,
             ),
@@ -689,7 +686,7 @@ fn exec_plan_subtree_having_without_groupby() {
         virtual_table.reshard(key, &query.coordinator).unwrap();
     }
 
-    let mut vtables: HashMap<usize, Rc<VirtualTable>> = HashMap::new();
+    let mut vtables: HashMap<NodeId, Rc<VirtualTable>> = HashMap::new();
     vtables.insert(motion_id, Rc::new(virtual_table));
 
     let exec_plan = query.get_mut_exec_plan();
@@ -837,7 +834,7 @@ fn global_union_all2() {
     virtual_table.add_tuple(vec![Value::Integer(1)]);
     let exec_plan = query.get_mut_exec_plan();
     exec_plan
-        .set_motion_vtable(motion_id, virtual_table.clone(), &coordinator)
+        .set_motion_vtable(&motion_id, virtual_table.clone(), &coordinator)
         .unwrap();
 
     let top_id = exec_plan.get_ir_plan().get_top().unwrap();
@@ -863,6 +860,10 @@ fn global_union_all2() {
         .unwrap();
 
     let actual_dispatch = coordinator.detailed_dispatch(sub_plan, &buckets);
+    let motion_id = NodeId {
+        offset: motion_id as u32,
+        arena_type: ArenaType::Default,
+    };
 
     let expected = vec![
         ReplicasetDispatchInfo {
@@ -918,7 +919,7 @@ fn global_union_all3() {
     sq_vtable.add_tuple(vec![Value::Integer(1)]);
     query
         .exec_plan
-        .set_motion_vtable(sq_motion_id, sq_vtable.clone(), &coordinator)
+        .set_motion_vtable(&sq_motion_id, sq_vtable.clone(), &coordinator)
         .unwrap();
 
     let groupby_motion_id = *slices.slice(1).unwrap().position(0).unwrap();
@@ -942,7 +943,7 @@ fn global_union_all3() {
     }
     query
         .exec_plan
-        .set_motion_vtable(groupby_motion_id, groupby_vtable.clone(), &coordinator)
+        .set_motion_vtable(&groupby_motion_id, groupby_vtable.clone(), &coordinator)
         .unwrap();
 
     let top_id = query.exec_plan.get_ir_plan().get_top().unwrap();
@@ -994,6 +995,16 @@ fn global_union_all3() {
 
     let actual_dispatch = coordinator.detailed_dispatch(sub_plan, &buckets);
 
+    let groupby_motion_id = NodeId {
+        offset: groupby_motion_id as u32,
+        arena_type: ArenaType::Default,
+    };
+
+    let sq_motion_id = NodeId {
+        offset: sq_motion_id as u32,
+        arena_type: ArenaType::Default,
+    };
+
     let expected = vec![
         ReplicasetDispatchInfo {
             rs_id: 0,
@@ -1096,7 +1107,7 @@ fn global_except() {
         virtual_table.add_tuple(vec![Value::Integer(1)]);
         query
             .get_mut_exec_plan()
-            .set_motion_vtable(intersect_motion_id, virtual_table.clone(), &coordinator)
+            .set_motion_vtable(&intersect_motion_id, virtual_table.clone(), &coordinator)
             .unwrap();
     }
 
@@ -1138,7 +1149,7 @@ fn exec_plan_order_by() {
     {
         virtual_table.reshard(key, &query.coordinator).unwrap();
     }
-    let mut vtables: HashMap<usize, Rc<VirtualTable>> = HashMap::new();
+    let mut vtables: HashMap<NodeId, Rc<VirtualTable>> = HashMap::new();
     vtables.insert(motion_id, Rc::new(virtual_table));
 
     let exec_plan = query.get_mut_exec_plan();
diff --git a/sbroad-core/src/executor/vtable.rs b/sbroad-core/src/executor/vtable.rs
index c5cafa405..6a83ebe87 100644
--- a/sbroad-core/src/executor/vtable.rs
+++ b/sbroad-core/src/executor/vtable.rs
@@ -13,6 +13,7 @@ use crate::errors::{Entity, SbroadError};
 use crate::executor::engine::helpers::{TupleBuilderCommand, TupleBuilderPattern};
 use crate::executor::protocol::{Binary, EncodedRows, EncodedTables};
 use crate::executor::{bucket::Buckets, Vshard};
+use crate::ir::expression::NodeId;
 use crate::ir::helpers::RepeatableState;
 use crate::ir::relation::Column;
 use crate::ir::transformation::redistribution::{ColumnPosition, MotionKey, Target};
@@ -686,20 +687,20 @@ impl ExecutionPlan {
 }
 
 #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
-pub struct VirtualTableMap(HashMap<usize, Rc<VirtualTable>>);
+pub struct VirtualTableMap(HashMap<NodeId, Rc<VirtualTable>>);
 
 impl VirtualTableMap {
     #[must_use]
-    pub fn new(map: HashMap<usize, Rc<VirtualTable>>) -> Self {
+    pub fn new(map: HashMap<NodeId, Rc<VirtualTable>>) -> Self {
         Self(map)
     }
 
     #[must_use]
-    pub fn map(&self) -> &HashMap<usize, Rc<VirtualTable>> {
+    pub fn map(&self) -> &HashMap<NodeId, Rc<VirtualTable>> {
         &self.0
     }
 
-    pub fn mut_map(&mut self) -> &mut HashMap<usize, Rc<VirtualTable>> {
+    pub fn mut_map(&mut self) -> &mut HashMap<NodeId, Rc<VirtualTable>> {
         &mut self.0
     }
 }
diff --git a/sbroad-core/src/frontend/sql.rs b/sbroad-core/src/frontend/sql.rs
index 90ad8e6ef..337ac62aa 100644
--- a/sbroad-core/src/frontend/sql.rs
+++ b/sbroad-core/src/frontend/sql.rs
@@ -31,7 +31,7 @@ use crate::ir::ddl::{Language, ParamDef};
 use crate::ir::expression::cast::Type as CastType;
 use crate::ir::expression::{
     ColumnPositionMap, ColumnWithScan, ColumnsRetrievalSpec, Expression, ExpressionId,
-    FunctionFeature, Position, TrimKind,
+    FunctionFeature, NodeId, Position, TrimKind,
 };
 use crate::ir::operator::{
     Arithmetic, Bool, ConflictStrategy, JoinKind, OrderByElement, OrderByEntity, OrderByType,
@@ -40,7 +40,7 @@ use crate::ir::operator::{
 use crate::ir::relation::{Column, ColumnRole, TableKind, Type as RelationType};
 use crate::ir::tree::traversal::{PostOrder, EXPR_CAPACITY};
 use crate::ir::value::Value;
-use crate::ir::{Node, NodeId, OptionKind, OptionParamValue, OptionSpec, Plan};
+use crate::ir::{Node, OptionKind, OptionParamValue, OptionSpec, Plan};
 use crate::otm::child_span;
 
 use crate::errors::Entity::AST;
@@ -75,13 +75,13 @@ fn get_default_auth_method() -> SmolStr {
 /// `left >= center AND left <= right`.
 struct Between {
     /// Left node id.
-    left_id: usize,
+    left_id: NodeId,
     /// Less or equal node id (`left <= right`)
-    less_eq_id: usize,
+    less_eq_id: NodeId,
 }
 
 impl Between {
-    fn new(left_id: usize, less_eq_id: usize) -> Self {
+    fn new(left_id: NodeId, less_eq_id: NodeId) -> Self {
         Self {
             left_id,
             less_eq_id,
@@ -962,7 +962,7 @@ fn parse_select_statement(
     node_id: usize,
     map: &mut Translation,
     plan: &mut Plan,
-) -> Result<usize, SbroadError> {
+) -> Result<NodeId, SbroadError> {
     let node = ast.nodes.get_node(node_id)?;
     assert_eq!(node.rule, Rule::SelectStatement);
     let mut top_id = None;
@@ -1227,7 +1227,7 @@ fn parse_grant_revoke(
 
 fn parse_trim<M: Metadata>(
     pair: Pair<Rule>,
-    referred_relation_ids: &[usize],
+    referred_relation_ids: &[NodeId],
     worker: &mut ExpressionsWorker<M>,
     plan: &mut Plan,
 ) -> Result<ParseExpression, SbroadError> {
@@ -1424,7 +1424,7 @@ fn parse_param<M: Metadata>(
     param: &ParameterSource,
     worker: &mut ExpressionsWorker<M>,
     plan: &mut Plan,
-) -> Result<usize, SbroadError> {
+) -> Result<NodeId, SbroadError> {
     let param_index = param.get_param_index()?;
     let parameter = match param_index {
         None => {
@@ -1504,14 +1504,14 @@ struct ExpressionsWorker<'worker, M>
 where
     M: Metadata,
 {
-    subquery_ids_queue: VecDeque<usize>,
+    subquery_ids_queue: VecDeque<NodeId>,
     betweens: Vec<Between>,
     metadata: &'worker M,
     /// Map of { reference plan_id -> (it's column name, whether it's covered with row)}
     /// We have to save column name in order to use it later for alias creation.
     /// We use information about row coverage later when handling Row expression and need to know
     /// whether we should uncover our reference.
-    pub reference_to_name_map: HashMap<usize, (SmolStr, bool)>,
+    pub reference_to_name_map: HashMap<NodeId, (SmolStr, bool)>,
     /// Flag indicating whether parameter in Tarantool (? mark) style was met.
     met_tnt_param: bool,
     /// Flag indicating whether parameter in Postgres ($<index>) style was met.
@@ -1519,7 +1519,7 @@ where
     /// Map of (relational_node_id, columns_position_map).
     /// As `ColumnPositionMap` is used for parsing references and as it may be shared for the same
     /// relational node we cache it so that we don't have to recreate it every time.
-    column_positions_cache: HashMap<usize, ColumnPositionMap>,
+    column_positions_cache: HashMap<NodeId, ColumnPositionMap>,
     /// Time at the start of the plan building stage without timezone.
     /// It is used to replace CURRENT_DATE to actual value.
     current_time: OffsetDateTime,
@@ -1542,7 +1542,7 @@ where
         }
     }
 
-    fn build_columns_map(&mut self, plan: &Plan, rel_id: usize) -> Result<(), SbroadError> {
+    fn build_columns_map(&mut self, plan: &Plan, rel_id: NodeId) -> Result<(), SbroadError> {
         if self.column_positions_cache.get(&rel_id).is_none() {
             let new_map = ColumnPositionMap::new(plan, rel_id)?;
             self.column_positions_cache.insert(rel_id, new_map);
@@ -1552,7 +1552,7 @@ where
 
     fn columns_map_get_positions(
         &self,
-        rel_id: usize,
+        rel_id: NodeId,
         col_name: &str,
         scan_name: Option<&str>,
     ) -> Result<Position, SbroadError> {
@@ -1579,7 +1579,7 @@ enum ParseExpressionInfixOperator {
 #[derive(Clone, Debug)]
 enum ParseExpression {
     PlanId {
-        plan_id: usize,
+        plan_id: NodeId,
     },
     Parentheses {
         child: Box<ParseExpression>,
@@ -1660,7 +1660,7 @@ pub enum SelectOp {
 #[derive(Clone)]
 pub enum SelectExpr {
     PlanId {
-        plan_id: usize,
+        plan_id: NodeId,
     },
     Infix {
         op: SelectOp,
@@ -1670,7 +1670,7 @@ pub enum SelectExpr {
 }
 
 impl SelectExpr {
-    fn populate_plan(&self, plan: &mut Plan) -> Result<usize, SbroadError> {
+    fn populate_plan(&self, plan: &mut Plan) -> Result<NodeId, SbroadError> {
         match self {
             SelectExpr::PlanId { plan_id } => Ok(*plan_id),
             SelectExpr::Infix { op, left, right } => {
@@ -1697,7 +1697,7 @@ impl ParseExpression {
         &self,
         plan: &mut Plan,
         worker: &mut ExpressionsWorker<M>,
-    ) -> Result<usize, SbroadError>
+    ) -> Result<NodeId, SbroadError>
     where
         M: Metadata,
     {
@@ -1749,7 +1749,7 @@ impl ParseExpression {
                             (res.populate_plan(plan, worker)?),
                         ))
                     })
-                    .collect::<Result<Vec<(usize, usize)>, SbroadError>>()?;
+                    .collect::<Result<Vec<(NodeId, NodeId)>, SbroadError>>()?;
                 let else_expr_id = if let Some(else_expr) = else_expr {
                     Some(else_expr.populate_plan(plan, worker)?)
                 } else {
@@ -2074,7 +2074,7 @@ fn cast_type_from_pair(type_pair: Pair<Rule>) -> Result<CastType, SbroadError> {
 #[allow(clippy::too_many_lines)]
 fn parse_expr_pratt<M>(
     expression_pairs: Pairs<Rule>,
-    referred_relation_ids: &[usize],
+    referred_relation_ids: &[NodeId],
     worker: &mut ExpressionsWorker<M>,
     plan: &mut Plan,
 ) -> Result<ParseExpression, SbroadError>
@@ -2556,10 +2556,10 @@ fn parse_select_pratt(
 /// * Return `plan_id` of root Expression node
 fn parse_expr<M>(
     expression_pairs: Pairs<Rule>,
-    referred_relation_ids: &[usize],
+    referred_relation_ids: &[NodeId],
     worker: &mut ExpressionsWorker<M>,
     plan: &mut Plan,
-) -> Result<usize, SbroadError>
+) -> Result<NodeId, SbroadError>
 where
     M: Metadata,
 {
@@ -2572,7 +2572,7 @@ fn parse_select(
     pos_to_ast_id: &SelectChildPairTranslation,
     ast_to_plan: &Translation,
     plan: &mut Plan,
-) -> Result<usize, SbroadError> {
+) -> Result<NodeId, SbroadError> {
     let select_expr = parse_select_pratt(select_pairs, pos_to_ast_id, ast_to_plan)?;
     select_expr.populate_plan(plan)
 }
@@ -2769,7 +2769,8 @@ impl AbstractSyntaxTree {
                         let mut expr_tree =
                             PostOrder::with_capacity(|node| plan.nodes.expr_iter(node, false), EXPR_CAPACITY);
                         let mut reference_met = false;
-                        for (_, node_id) in expr_tree.iter(expr_plan_node_id) {
+                        for level_node in expr_tree.iter(expr_plan_node_id) {
+                            let node_id = level_node.1;
                             if let Expression::Reference { .. } = plan.get_expression_node(node_id)? {
                                 reference_met = true;
                                 break;
@@ -2848,7 +2849,8 @@ impl AbstractSyntaxTree {
         let mut worker = ExpressionsWorker::new(metadata);
         let mut ctes = CTEs::new();
 
-        for (_, id) in dft_post.iter(top) {
+        for level_node in dft_post.iter(top) {
+            let id = level_node.1;
             let node = self.nodes.get_node(id)?;
             match &node.rule {
                 Rule::Scan => {
@@ -2936,7 +2938,7 @@ impl AbstractSyntaxTree {
                 }
                 Rule::GroupBy => {
                     // Reminder: first GroupBy child in `node.children` is always a relational node.
-                    let mut children: Vec<usize> = Vec::with_capacity(node.children.len());
+                    let mut children: Vec<NodeId> = Vec::with_capacity(node.children.len());
                     let first_relational_child_ast_id =
                         node.children.first().expect("GroupBy has no children");
                     let first_relational_child_plan_id = map.get(*first_relational_child_ast_id)?;
@@ -3068,7 +3070,7 @@ impl AbstractSyntaxTree {
                     }
 
                     let plan_rel_child_id = map.get(*rel_child_id)?;
-                    let mut proj_columns: Vec<usize> = Vec::with_capacity(ast_columns_ids.len());
+                    let mut proj_columns: Vec<NodeId> = Vec::with_capacity(ast_columns_ids.len());
 
                     let mut unnamed_col_pos = 0;
                     for ast_column_id in ast_columns_ids {
@@ -3151,7 +3153,7 @@ impl AbstractSyntaxTree {
                     map.add(id, projection_id);
                 }
                 Rule::Values => {
-                    let mut plan_value_row_ids: Vec<usize> =
+                    let mut plan_value_row_ids: Vec<NodeId> =
                         Vec::with_capacity(node.children.len());
                     for ast_child_id in &node.children {
                         let row_pair = pairs_map.remove_pair(*ast_child_id);
@@ -3832,7 +3834,7 @@ impl Plan {
     /// Leave other nodes (e.g. rows) unchanged.
     ///
     /// Used for unification of expression nodes transformations (e.g. dnf).
-    fn row(&mut self, expr_id: usize) -> Result<usize, SbroadError> {
+    fn row(&mut self, expr_id: NodeId) -> Result<NodeId, SbroadError> {
         let row_id = if let Node::Expression(
             Expression::Reference { .. }
             | Expression::Constant { .. }
diff --git a/sbroad-core/src/frontend/sql/ir.rs b/sbroad-core/src/frontend/sql/ir.rs
index 284e966c0..9f437ddfa 100644
--- a/sbroad-core/src/frontend/sql/ir.rs
+++ b/sbroad-core/src/frontend/sql/ir.rs
@@ -7,14 +7,14 @@ use tarantool::decimal::Decimal;
 
 use crate::errors::{Action, Entity, SbroadError};
 use crate::frontend::sql::ast::Rule;
-use crate::ir::expression::Expression;
+use crate::ir::expression::{Expression, NodeId};
 use crate::ir::helpers::RepeatableState;
 use crate::ir::operator::{OrderByElement, OrderByEntity, Relational};
 use crate::ir::transformation::redistribution::MotionOpcode;
-use crate::ir::tree::traversal::{PostOrder, EXPR_CAPACITY};
+use crate::ir::tree::traversal::{LevelNode, PostOrder, EXPR_CAPACITY};
 use crate::ir::value::double::Double;
 use crate::ir::value::Value;
-use crate::ir::{Node, NodeId, Plan};
+use crate::ir::{ArenaType, Node, Plan};
 
 use super::Between;
 
@@ -82,7 +82,7 @@ impl Value {
 #[derive(Debug)]
 /// Helper struct representing map of { `ParseNode` id -> `Node` id }
 pub(super) struct Translation {
-    map: HashMap<usize, usize>,
+    map: HashMap<usize, NodeId>,
 }
 
 impl Translation {
@@ -92,11 +92,11 @@ impl Translation {
         }
     }
 
-    pub(super) fn add(&mut self, parse_id: usize, plan_id: usize) {
+    pub(super) fn add(&mut self, parse_id: usize, plan_id: NodeId) {
         self.map.insert(parse_id, plan_id);
     }
 
-    pub(super) fn get(&self, old: usize) -> Result<usize, SbroadError> {
+    pub(super) fn get(&self, old: usize) -> Result<NodeId, SbroadError> {
         self.map.get(&old).copied().ok_or_else(|| {
             SbroadError::NotFound(
                 Entity::Node,
@@ -111,17 +111,17 @@ impl Translation {
 struct SubQuery {
     /// Relational operator that is a parent of current SubQuery.
     /// E.g. Selection (in case SubQuery is met in WHERE expression).
-    relational: usize,
+    relational: NodeId,
     /// Expression operator in which this SubQuery is met.
     /// E.g. `Exists`.
-    operator: usize,
+    operator: NodeId,
     /// SubQuery id in plan.
-    sq: usize,
+    sq: NodeId,
 }
 impl Eq for SubQuery {}
 
 impl SubQuery {
-    fn new(relational: usize, operator: usize, sq: usize) -> SubQuery {
+    fn new(relational: NodeId, operator: NodeId, sq: NodeId) -> SubQuery {
         SubQuery {
             relational,
             operator,
@@ -142,20 +142,20 @@ impl CloneExprSubtreeMap {
         }
     }
 
-    fn insert(&mut self, old_id: usize, new_id: usize) {
+    fn insert(&mut self, old_id: NodeId, new_id: NodeId) {
         self.inner.insert(old_id, new_id);
     }
 
-    fn replace(&self, id: &mut usize) {
+    fn replace(&self, id: &mut NodeId) {
         let new_id = self.get(*id);
         *id = new_id;
     }
 
-    fn get(&self, id: usize) -> usize {
+    fn get(&self, id: NodeId) -> NodeId {
         *self
             .inner
             .get(&id)
-            .unwrap_or_else(|| panic!("Node with id {id} not found in the cloning subtree map."))
+            .unwrap_or_else(|| panic!("Node with id {id:?} not found in the cloning subtree map."))
     }
 }
 
@@ -164,7 +164,7 @@ impl Plan {
         let mut set: HashSet<SubQuery, RepeatableState> = HashSet::with_hasher(RepeatableState);
         // Traverse expression trees of the selection and join nodes.
         // Gather all sub-queries in the boolean expressions there.
-        for (relational_id, node) in self.nodes.iter().enumerate() {
+        for (offset, node) in self.nodes.iter().enumerate() {
             match node {
                 Node::Relational(
                     Relational::Selection { filter: tree, .. }
@@ -178,7 +178,12 @@ impl Plan {
                         |node| self.nodes.expr_iter(node, false),
                         capacity,
                     );
-                    for (_, op_id) in expr_post.iter(*tree) {
+                    let relational_id = NodeId {
+                        offset: u32::try_from(offset).unwrap(),
+                        arena_type: ArenaType::Default,
+                    };
+                    for level_node in expr_post.iter(*tree) {
+                        let op_id = level_node.1;
                         let expression_node = self.get_node(op_id)?;
                         if let Node::Expression(Expression::Bool { left, right, .. }) =
                             expression_node
@@ -211,9 +216,9 @@ impl Plan {
     /// Replace sub-queries with references to the sub-query.
     pub(super) fn replace_sq_with_references(
         &mut self,
-    ) -> Result<AHashMap<usize, usize>, SbroadError> {
+    ) -> Result<AHashMap<NodeId, NodeId>, SbroadError> {
         let set = self.gather_sq_for_replacement()?;
-        let mut replaces: AHashMap<usize, usize> = AHashMap::with_capacity(set.len());
+        let mut replaces: AHashMap<NodeId, NodeId> = AHashMap::with_capacity(set.len());
         for sq in set {
             // Append sub-query to relational node if it is not already there (can happen with BETWEEN).
             match self.get_mut_node(sq.relational)? {
@@ -298,10 +303,10 @@ impl Plan {
     pub(super) fn fix_betweens(
         &mut self,
         betweens: &[Between],
-        replaces: &AHashMap<usize, usize>,
+        replaces: &AHashMap<NodeId, NodeId>,
     ) -> Result<(), SbroadError> {
         for between in betweens {
-            let left_id: usize = if let Some(id) = replaces.get(&between.left_id) {
+            let left_id: NodeId = if let Some(id) = replaces.get(&between.left_id) {
                 self.clone_expr_subtree(*id)?
             } else {
                 self.clone_expr_subtree(between.left_id)?
@@ -319,14 +324,15 @@ impl Plan {
         Ok(())
     }
 
-    pub(crate) fn clone_expr_subtree(&mut self, top_id: usize) -> Result<usize, SbroadError> {
+    pub(crate) fn clone_expr_subtree(&mut self, top_id: NodeId) -> Result<NodeId, SbroadError> {
         let mut subtree =
             PostOrder::with_capacity(|node| self.nodes.expr_iter(node, false), EXPR_CAPACITY);
         subtree.populate_nodes(top_id);
         let nodes = subtree.take_nodes();
         let mut map = CloneExprSubtreeMap::with_capacity(nodes.len());
-        for (_, id) in nodes {
-            let next_id = self.nodes.next_id();
+        let mut expr_id = NodeId::default();
+        for LevelNode(_, id) in nodes {
+            let next_id = self.nodes.next_id(ArenaType::Default);
             let mut expr = self.get_expression_node(id)?.clone();
             match expr {
                 Expression::Constant { .. }
@@ -392,18 +398,18 @@ impl Plan {
                     }
                 }
             }
-            self.nodes.push(Node::Expression(expr));
+            expr_id = self.nodes.push(Node::Expression(expr));
             map.insert(id, next_id);
         }
-        Ok(self.nodes.next_id() - 1)
+        Ok(expr_id)
     }
 }
 
 /// Helper struct to clone plan's subtree.
 /// Assumes that all parameters are bound.
 pub struct SubtreeCloner {
-    old_new: AHashMap<usize, usize>,
-    nodes_with_backward_references: Vec<usize>,
+    old_new: AHashMap<NodeId, NodeId>,
+    nodes_with_backward_references: Vec<NodeId>,
 }
 
 impl SubtreeCloner {
@@ -414,19 +420,19 @@ impl SubtreeCloner {
         }
     }
 
-    fn get_new_id(&self, old_id: usize) -> Result<usize, SbroadError> {
+    fn get_new_id(&self, old_id: NodeId) -> Result<NodeId, SbroadError> {
         self.old_new
             .get(&old_id)
             .ok_or_else(|| {
                 SbroadError::Invalid(
                     Entity::Plan,
-                    Some(format_smolstr!("new node not found for old id: {old_id}")),
+                    Some(format_smolstr!("new node not found for old id: {old_id:?}")),
                 )
             })
             .copied()
     }
 
-    fn copy_list(&self, list: &[usize]) -> Result<Vec<usize>, SbroadError> {
+    fn copy_list(&self, list: &[NodeId]) -> Result<Vec<NodeId>, SbroadError> {
         let mut new_list = Vec::with_capacity(list.len());
         for id in list {
             new_list.push(self.get_new_id(*id)?);
@@ -525,7 +531,7 @@ impl SubtreeCloner {
     fn clone_relational(
         &mut self,
         old_relational: &Relational,
-        id: usize,
+        id: NodeId,
     ) -> Result<Relational, SbroadError> {
         let mut copied = old_relational.clone();
 
@@ -737,14 +743,15 @@ impl SubtreeCloner {
     fn clone(
         &mut self,
         plan: &mut Plan,
-        top_id: usize,
+        top_id: NodeId,
         capacity: usize,
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         let mut dfs = PostOrder::with_capacity(|x| plan.subtree_iter(x, true), capacity);
         dfs.populate_nodes(top_id);
         let nodes = dfs.take_nodes();
         drop(dfs);
-        for (_, id) in nodes {
+        for level_node in nodes {
+            let id = level_node.1;
             let node = plan.get_node(id)?;
             let new_node = match node {
                 Node::Relational(rel) => Node::Relational(self.clone_relational(rel, id)?),
@@ -753,7 +760,7 @@ impl SubtreeCloner {
                     return Err(SbroadError::Invalid(
                         Entity::Node,
                         Some(format_smolstr!(
-                            "clone: expected relational or expression on id: {id}"
+                            "clone: expected relational or expression on id: {id:?}"
                         )),
                     ))
                 }
@@ -764,7 +771,7 @@ impl SubtreeCloner {
                 return Err(SbroadError::Invalid(
                     Entity::Plan,
                     Some(format_smolstr!(
-                        "clone: node with id {id} was mapped twice: {old_new_id}, {new_id}"
+                        "clone: node with id {id:?} was mapped twice: {old_new_id:?}, {new_id:?}"
                     )),
                 ));
             }
@@ -779,7 +786,7 @@ impl SubtreeCloner {
                 SbroadError::Invalid(
                     Entity::Plan,
                     Some(format_smolstr!(
-                        "invalid subtree traversal with top: {top_id}"
+                        "invalid subtree traversal with top: {top_id:?}"
                     )),
                 )
             })
@@ -796,9 +803,9 @@ impl SubtreeCloner {
     /// - parameters/ddl/acl nodes are found in subtree
     pub fn clone_subtree(
         plan: &mut Plan,
-        top_id: usize,
+        top_id: NodeId,
         subtree_capacity: usize,
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         let mut helper = Self::new(subtree_capacity);
         helper.clone(plan, top_id, subtree_capacity)
     }
diff --git a/sbroad-core/src/frontend/sql/ir/tests.rs b/sbroad-core/src/frontend/sql/ir/tests.rs
index 1122ef67f..a4a43cf03 100644
--- a/sbroad-core/src/frontend/sql/ir/tests.rs
+++ b/sbroad-core/src/frontend/sql/ir/tests.rs
@@ -1,6 +1,7 @@
 use crate::errors::SbroadError;
 use crate::frontend::sql::ast::AbstractSyntaxTree;
 use crate::frontend::Ast;
+use crate::ir::expression::NodeId;
 use crate::ir::operator::Relational;
 use crate::ir::transformation::helpers::{sql_to_ir, sql_to_optimized_ir};
 use crate::ir::tree::traversal::PostOrder;
@@ -940,7 +941,7 @@ vtable_max_rows = 5000
 }
 
 impl Plan {
-    fn get_positions(&self, node_id: usize) -> Option<Positions> {
+    fn get_positions(&self, node_id: NodeId) -> Option<Positions> {
         let mut context = self.context_mut();
         context
             .get_shard_columns_positions(node_id, self)
@@ -959,7 +960,8 @@ fn track_shard_col_pos() {
     let plan = sql_to_optimized_ir(input, vec![]);
     let top = plan.get_top().unwrap();
     let mut dfs = PostOrder::with_capacity(|x| plan.nodes.rel_iter(x), 10);
-    for (_, node_id) in dfs.iter(top) {
+    for level_node in dfs.iter(top) {
+        let node_id = level_node.1;
         let node = plan.get_relation_node(node_id).unwrap();
         match node {
             Relational::ScanRelation { .. } | Relational::Selection { .. } => {
@@ -980,7 +982,8 @@ fn track_shard_col_pos() {
     let plan = sql_to_optimized_ir(input, vec![]);
     let top = plan.get_top().unwrap();
     let mut dfs = PostOrder::with_capacity(|x| plan.nodes.rel_iter(x), 10);
-    for (_, node_id) in dfs.iter(top) {
+    for level_node in dfs.iter(top) {
+        let node_id = level_node.1;
         let node = plan.get_relation_node(node_id).unwrap();
         if let Relational::Join { .. } = node {
             assert_eq!(
@@ -1002,7 +1005,8 @@ fn track_shard_col_pos() {
     let plan = sql_to_optimized_ir(input, vec![]);
     let top = plan.get_top().unwrap();
     let mut dfs = PostOrder::with_capacity(|x| plan.nodes.rel_iter(x), 10);
-    for (_, node_id) in dfs.iter(top) {
+    for level_node in dfs.iter(top) {
+        let node_id = level_node.1;
         let node = plan.get_relation_node(node_id).unwrap();
         if let Relational::Join { .. } = node {
             assert_eq!([Some(4_usize), None], plan.get_positions(node_id).unwrap());
@@ -1378,10 +1382,10 @@ fn front_sql_groupby_join_1() {
     println!("{}", plan.as_explain().unwrap());
     let expected_explain = String::from(
         r#"projection ("column_63"::string -> "product_code", "column_64"::boolean -> "product_units")
-    group by ("column_63"::string, "column_64"::boolean) output: ("column_64"::boolean -> "column_64", "column_63"::string -> "column_63")
+    group by ("column_63"::string, "column_64"::boolean) output: ("column_63"::string -> "column_63", "column_64"::boolean -> "column_64")
         motion [policy: segment([ref("column_63"), ref("column_64")])]
             scan
-                projection ("t2"."product_units"::boolean -> "column_64", "t2"."product_code"::string -> "column_63")
+                projection ("t2"."product_code"::string -> "column_63", "t2"."product_units"::boolean -> "column_64")
                     group by ("t2"."product_code"::string, "t2"."product_units"::boolean) output: ("t2"."product_units"::boolean -> "product_units", "t2"."product_code"::string -> "product_code", "t2"."identification_number"::integer -> "identification_number", "t"."id"::unsigned -> "id")
                         join on ROW("t2"."identification_number"::integer) = ROW("t"."id"::unsigned)
                             scan "t2"
@@ -1493,10 +1497,10 @@ fn front_sql_groupby_insert() {
         r#"insert "t" on conflict: fail
     motion [policy: segment([value(NULL), ref("d")])]
         projection ("column_12"::unsigned -> "b", "column_13"::unsigned -> "d")
-            group by ("column_13"::unsigned, "column_12"::unsigned) output: ("column_12"::unsigned -> "column_12", "column_13"::unsigned -> "column_13")
+            group by ("column_13"::unsigned, "column_12"::unsigned) output: ("column_13"::unsigned -> "column_13", "column_12"::unsigned -> "column_12")
                 motion [policy: segment([ref("column_13"), ref("column_12")])]
                     scan
-                        projection ("t"."b"::unsigned -> "column_12", "t"."d"::unsigned -> "column_13")
+                        projection ("t"."d"::unsigned -> "column_13", "t"."b"::unsigned -> "column_12")
                             group by ("t"."d"::unsigned, "t"."b"::unsigned) output: ("t"."a"::unsigned -> "a", "t"."b"::unsigned -> "b", "t"."c"::unsigned -> "c", "t"."d"::unsigned -> "d", "t"."bucket_id"::unsigned -> "bucket_id")
                                 scan "t"
 execution options:
@@ -1808,10 +1812,10 @@ fn front_sql_aggregates_with_distinct2() {
 
     let expected_explain = String::from(
         r#"projection ("column_12"::unsigned -> "b", sum(distinct ("column_34"::decimal))::decimal -> "col_1")
-    group by ("column_12"::unsigned) output: ("column_12"::unsigned -> "column_12", "column_34"::unsigned -> "column_34")
+    group by ("column_12"::unsigned) output: ("column_34"::unsigned -> "column_34", "column_12"::unsigned -> "column_12")
         motion [policy: segment([ref("column_12")])]
             scan
-                projection ("t"."b"::unsigned -> "column_12", ROW("t"."a"::unsigned) + ROW("t"."b"::unsigned) + ROW(3::unsigned) -> "column_34")
+                projection (ROW("t"."a"::unsigned) + ROW("t"."b"::unsigned) + ROW(3::unsigned) -> "column_34", "t"."b"::unsigned -> "column_12")
                     group by ("t"."b"::unsigned, ROW("t"."a"::unsigned) + ROW("t"."b"::unsigned) + ROW(3::unsigned)) output: ("t"."a"::unsigned -> "a", "t"."b"::unsigned -> "b", "t"."c"::unsigned -> "c", "t"."d"::unsigned -> "d", "t"."bucket_id"::unsigned -> "bucket_id")
                         scan "t"
 execution options:
@@ -1820,7 +1824,7 @@ vtable_max_rows = 5000
 "#,
     );
 
-    assert_eq!(expected_explain, plan.as_explain().unwrap());
+    assert_eq!(plan.as_explain().unwrap(), expected_explain);
 }
 
 #[test]
@@ -2384,10 +2388,10 @@ fn front_sql_groupby_expression3() {
     println!("{}", plan.as_explain().unwrap());
     let expected_explain = String::from(
         r#"projection ("column_16"::unsigned -> "col_1", "column_27"::unsigned * ROW(sum(("sum_59"::decimal))::decimal) / ROW(sum(("count_65"::integer))::decimal) -> "col_2")
-    group by ("column_16"::unsigned, "column_27"::unsigned) output: ("column_27"::unsigned -> "column_27", "column_16"::unsigned -> "column_16", "count_65"::integer -> "count_65", "sum_59"::decimal -> "sum_59")
+    group by ("column_16"::unsigned, "column_27"::unsigned) output: ("column_16"::unsigned -> "column_16", "column_27"::unsigned -> "column_27", "count_65"::integer -> "count_65", "sum_59"::decimal -> "sum_59")
         motion [policy: segment([ref("column_16"), ref("column_27")])]
             scan
-                projection ((ROW("t"."c"::unsigned) * ROW("t"."d"::unsigned)) -> "column_27", ROW("t"."a"::unsigned) + ROW("t"."b"::unsigned) -> "column_16", count((ROW("t"."a"::unsigned) * ROW("t"."b"::unsigned)))::integer -> "count_65", sum((ROW("t"."c"::unsigned) * ROW("t"."d"::unsigned)))::decimal -> "sum_59")
+                projection (ROW("t"."a"::unsigned) + ROW("t"."b"::unsigned) -> "column_16", (ROW("t"."c"::unsigned) * ROW("t"."d"::unsigned)) -> "column_27", count((ROW("t"."a"::unsigned) * ROW("t"."b"::unsigned)))::integer -> "count_65", sum((ROW("t"."c"::unsigned) * ROW("t"."d"::unsigned)))::decimal -> "sum_59")
                     group by (ROW("t"."a"::unsigned) + ROW("t"."b"::unsigned), (ROW("t"."c"::unsigned) * ROW("t"."d"::unsigned))) output: ("t"."a"::unsigned -> "a", "t"."b"::unsigned -> "b", "t"."c"::unsigned -> "c", "t"."d"::unsigned -> "d", "t"."bucket_id"::unsigned -> "bucket_id")
                         scan "t"
 execution options:
@@ -2408,10 +2412,10 @@ fn front_sql_groupby_expression4() {
     println!("{}", plan.as_explain().unwrap());
     let expected_explain = String::from(
         r#"projection ("column_16"::unsigned -> "col_1", "column_17"::unsigned -> "a")
-    group by ("column_16"::unsigned, "column_17"::unsigned) output: ("column_17"::unsigned -> "column_17", "column_16"::unsigned -> "column_16")
+    group by ("column_16"::unsigned, "column_17"::unsigned) output: ("column_16"::unsigned -> "column_16", "column_17"::unsigned -> "column_17")
         motion [policy: segment([ref("column_16"), ref("column_17")])]
             scan
-                projection ("t"."a"::unsigned -> "column_17", ROW("t"."a"::unsigned) + ROW("t"."b"::unsigned) -> "column_16")
+                projection (ROW("t"."a"::unsigned) + ROW("t"."b"::unsigned) -> "column_16", "t"."a"::unsigned -> "column_17")
                     group by (ROW("t"."a"::unsigned) + ROW("t"."b"::unsigned), "t"."a"::unsigned) output: ("t"."a"::unsigned -> "a", "t"."b"::unsigned -> "b", "t"."c"::unsigned -> "c", "t"."d"::unsigned -> "d", "t"."bucket_id"::unsigned -> "bucket_id")
                         scan "t"
 execution options:
@@ -2437,19 +2441,19 @@ fn front_sql_groupby_with_aggregates() {
     join on ROW("t1"."a"::unsigned, "t2"."g"::unsigned) = ROW("t2"."e"::unsigned, "t1"."b"::unsigned)
         scan "t1"
             projection ("column_12"::unsigned -> "a", "column_13"::unsigned -> "b", sum(("sum_31"::decimal))::decimal -> "c")
-                group by ("column_13"::unsigned, "column_12"::unsigned) output: ("column_12"::unsigned -> "column_12", "column_13"::unsigned -> "column_13", "sum_31"::decimal -> "sum_31")
+                group by ("column_13"::unsigned, "column_12"::unsigned) output: ("column_13"::unsigned -> "column_13", "column_12"::unsigned -> "column_12", "sum_31"::decimal -> "sum_31")
                     motion [policy: segment([ref("column_13"), ref("column_12")])]
                         scan
-                            projection ("t"."a"::unsigned -> "column_12", "t"."b"::unsigned -> "column_13", sum(("t"."c"::unsigned))::decimal -> "sum_31")
+                            projection ("t"."b"::unsigned -> "column_13", "t"."a"::unsigned -> "column_12", sum(("t"."c"::unsigned))::decimal -> "sum_31")
                                 group by ("t"."b"::unsigned, "t"."a"::unsigned) output: ("t"."a"::unsigned -> "a", "t"."b"::unsigned -> "b", "t"."c"::unsigned -> "c", "t"."d"::unsigned -> "d", "t"."bucket_id"::unsigned -> "bucket_id")
                                     scan "t"
         motion [policy: full]
             scan "t2"
                 projection ("column_55"::unsigned -> "g", "column_56"::unsigned -> "e", sum(("sum_74"::decimal))::decimal -> "f")
-                    group by ("column_55"::unsigned, "column_56"::unsigned) output: ("column_56"::unsigned -> "column_56", "column_55"::unsigned -> "column_55", "sum_74"::decimal -> "sum_74")
+                    group by ("column_55"::unsigned, "column_56"::unsigned) output: ("column_55"::unsigned -> "column_55", "column_56"::unsigned -> "column_56", "sum_74"::decimal -> "sum_74")
                         motion [policy: segment([ref("column_55"), ref("column_56")])]
                             scan
-                                projection ("t2"."e"::unsigned -> "column_56", "t2"."g"::unsigned -> "column_55", sum(("t2"."f"::unsigned))::decimal -> "sum_74")
+                                projection ("t2"."g"::unsigned -> "column_55", "t2"."e"::unsigned -> "column_56", sum(("t2"."f"::unsigned))::decimal -> "sum_74")
                                     group by ("t2"."g"::unsigned, "t2"."e"::unsigned) output: ("t2"."e"::unsigned -> "e", "t2"."f"::unsigned -> "f", "t2"."g"::unsigned -> "g", "t2"."h"::unsigned -> "h", "t2"."bucket_id"::unsigned -> "bucket_id")
                                         scan "t2"
 execution options:
@@ -2636,10 +2640,10 @@ fn front_sql_having1() {
     let expected_explain = String::from(
         r#"projection ("column_12"::unsigned -> "a", sum(("sum_52"::decimal))::decimal -> "col_1")
     having ROW("column_12"::unsigned) > ROW(1::unsigned) and ROW(sum(distinct ("column_27"::decimal))::decimal) > ROW(1::unsigned)
-        group by ("column_12"::unsigned) output: ("column_12"::unsigned -> "column_12", "column_27"::unsigned -> "column_27", "sum_52"::decimal -> "sum_52")
+        group by ("column_12"::unsigned) output: ("column_27"::unsigned -> "column_27", "column_12"::unsigned -> "column_12", "sum_52"::decimal -> "sum_52")
             motion [policy: segment([ref("column_12")])]
                 scan
-                    projection ("t"."a"::unsigned -> "column_12", "t"."b"::unsigned -> "column_27", sum(("t"."b"::unsigned))::decimal -> "sum_52")
+                    projection ("t"."b"::unsigned -> "column_27", "t"."a"::unsigned -> "column_12", sum(("t"."b"::unsigned))::decimal -> "sum_52")
                         group by ("t"."a"::unsigned, "t"."b"::unsigned) output: ("t"."a"::unsigned -> "a", "t"."b"::unsigned -> "b", "t"."c"::unsigned -> "c", "t"."d"::unsigned -> "d", "t"."bucket_id"::unsigned -> "bucket_id")
                             scan "t"
 execution options:
@@ -2778,10 +2782,10 @@ fn front_sql_having_with_sq_segment_motion() {
     let expected_explain = String::from(
         r#"projection ("column_12"::unsigned -> "sysFrom", "column_13"::unsigned -> "sys_op", sum(distinct ("column_70"::decimal))::decimal -> "sum", count(distinct ("column_70"::integer))::integer -> "count")
     having ROW("column_12"::unsigned, "column_13"::unsigned) in ROW($0, $0)
-        group by ("column_13"::unsigned, "column_12"::unsigned) output: ("column_12"::unsigned -> "column_12", "column_70"::unsigned -> "column_70", "column_13"::unsigned -> "column_13")
+        group by ("column_13"::unsigned, "column_12"::unsigned) output: ("column_70"::unsigned -> "column_70", "column_13"::unsigned -> "column_13", "column_12"::unsigned -> "column_12")
             motion [policy: segment([ref("column_13"), ref("column_12")])]
                 scan
-                    projection ("test_space"."sysFrom"::unsigned -> "column_12", "test_space"."id"::unsigned -> "column_70", "test_space"."sys_op"::unsigned -> "column_13")
+                    projection ("test_space"."id"::unsigned -> "column_70", "test_space"."sys_op"::unsigned -> "column_13", "test_space"."sysFrom"::unsigned -> "column_12")
                         group by ("test_space"."sys_op"::unsigned, "test_space"."sysFrom"::unsigned, "test_space"."id"::unsigned) output: ("test_space"."id"::unsigned -> "id", "test_space"."sysFrom"::unsigned -> "sysFrom", "test_space"."FIRST_NAME"::string -> "FIRST_NAME", "test_space"."sys_op"::unsigned -> "sys_op", "test_space"."bucket_id"::unsigned -> "bucket_id")
                             scan "test_space"
 subquery $0:
@@ -2814,10 +2818,10 @@ fn front_sql_having_with_sq_segment_local_motion() {
     let expected_explain = String::from(
         r#"projection ("column_12"::unsigned -> "sysFrom", "column_13"::unsigned -> "sys_op", sum(distinct ("column_70"::decimal))::decimal -> "sum", count(distinct ("column_70"::integer))::integer -> "count")
     having ROW("column_12"::unsigned, "column_13"::unsigned) in ROW($0, $0)
-        group by ("column_13"::unsigned, "column_12"::unsigned) output: ("column_12"::unsigned -> "column_12", "column_70"::unsigned -> "column_70", "column_13"::unsigned -> "column_13")
+        group by ("column_13"::unsigned, "column_12"::unsigned) output: ("column_70"::unsigned -> "column_70", "column_13"::unsigned -> "column_13", "column_12"::unsigned -> "column_12")
             motion [policy: segment([ref("column_13"), ref("column_12")])]
                 scan
-                    projection ("test_space"."sysFrom"::unsigned -> "column_12", "test_space"."id"::unsigned -> "column_70", "test_space"."sys_op"::unsigned -> "column_13")
+                    projection ("test_space"."id"::unsigned -> "column_70", "test_space"."sys_op"::unsigned -> "column_13", "test_space"."sysFrom"::unsigned -> "column_12")
                         group by ("test_space"."sys_op"::unsigned, "test_space"."sysFrom"::unsigned, "test_space"."id"::unsigned) output: ("test_space"."id"::unsigned -> "id", "test_space"."sysFrom"::unsigned -> "sysFrom", "test_space"."FIRST_NAME"::string -> "FIRST_NAME", "test_space"."sys_op"::unsigned -> "sys_op", "test_space"."bucket_id"::unsigned -> "bucket_id")
                             scan "test_space"
 subquery $0:
@@ -2933,10 +2937,10 @@ fn front_sql_select_distinct() {
     // here we must compute only two groupby columns at local stage: a, b
     let expected_explain = String::from(
         r#"projection ("column_22"::unsigned -> "a", "column_27"::unsigned -> "col_1")
-    group by ("column_27"::unsigned, "column_22"::unsigned) output: ("column_22"::unsigned -> "column_22", "column_27"::unsigned -> "column_27")
+    group by ("column_27"::unsigned, "column_22"::unsigned) output: ("column_27"::unsigned -> "column_27", "column_22"::unsigned -> "column_22")
         motion [policy: segment([ref("column_27"), ref("column_22")])]
             scan
-                projection ("t"."a"::unsigned -> "column_22", ROW("t"."a"::unsigned) + ROW("t"."b"::unsigned) -> "column_27")
+                projection (ROW("t"."a"::unsigned) + ROW("t"."b"::unsigned) -> "column_27", "t"."a"::unsigned -> "column_22")
                     group by (ROW("t"."a"::unsigned) + ROW("t"."b"::unsigned), "t"."a"::unsigned) output: ("t"."a"::unsigned -> "a", "t"."b"::unsigned -> "b", "t"."c"::unsigned -> "c", "t"."d"::unsigned -> "d", "t"."bucket_id"::unsigned -> "bucket_id")
                         scan "t"
 execution options:
@@ -2956,10 +2960,10 @@ fn front_sql_select_distinct_asterisk() {
     println!("{}", plan.as_explain().unwrap());
     let expected_explain = String::from(
         r#"projection ("column_23"::unsigned -> "a", "column_24"::unsigned -> "b", "column_25"::unsigned -> "c", "column_26"::unsigned -> "d")
-    group by ("column_24"::unsigned, "column_23"::unsigned, "column_25"::unsigned, "column_26"::unsigned) output: ("column_24"::unsigned -> "column_24", "column_23"::unsigned -> "column_23", "column_25"::unsigned -> "column_25", "column_26"::unsigned -> "column_26")
+    group by ("column_24"::unsigned, "column_23"::unsigned, "column_25"::unsigned, "column_26"::unsigned) output: ("column_23"::unsigned -> "column_23", "column_26"::unsigned -> "column_26", "column_25"::unsigned -> "column_25", "column_24"::unsigned -> "column_24")
         motion [policy: segment([ref("column_24"), ref("column_23"), ref("column_25"), ref("column_26")])]
             scan
-                projection ("t"."b"::unsigned -> "column_24", "t"."a"::unsigned -> "column_23", "t"."c"::unsigned -> "column_25", "t"."d"::unsigned -> "column_26")
+                projection ("t"."a"::unsigned -> "column_23", "t"."d"::unsigned -> "column_26", "t"."c"::unsigned -> "column_25", "t"."b"::unsigned -> "column_24")
                     group by ("t"."b"::unsigned, "t"."a"::unsigned, "t"."c"::unsigned, "t"."d"::unsigned) output: ("t"."a"::unsigned -> "a", "t"."b"::unsigned -> "b", "t"."c"::unsigned -> "c", "t"."d"::unsigned -> "d", "t"."bucket_id"::unsigned -> "bucket_id")
                         scan "t"
 execution options:
diff --git a/sbroad-core/src/frontend/sql/ir/tests/global.rs b/sbroad-core/src/frontend/sql/ir/tests/global.rs
index a3d2f7aa6..e067aed43 100644
--- a/sbroad-core/src/frontend/sql/ir/tests/global.rs
+++ b/sbroad-core/src/frontend/sql/ir/tests/global.rs
@@ -1,7 +1,8 @@
 use crate::ir::distribution::Distribution;
+use crate::ir::expression::NodeId;
 use crate::ir::operator::Relational;
 use crate::ir::transformation::helpers::sql_to_optimized_ir;
-use crate::ir::tree::traversal::{FilterFn, PostOrderWithFilter, REL_CAPACITY};
+use crate::ir::tree::traversal::{FilterFn, LevelNode, PostOrderWithFilter, REL_CAPACITY};
 use crate::ir::value::Value;
 use crate::ir::{Node, Plan};
 use pretty_assertions::assert_eq;
@@ -25,7 +26,7 @@ impl From<&Distribution> for DistMock {
     }
 }
 
-fn collect_relational(plan: &Plan, predicate: FilterFn<'_>) -> Vec<(usize, usize)> {
+fn collect_relational(plan: &Plan, predicate: FilterFn<'_, NodeId>) -> Vec<LevelNode<NodeId>> {
     let mut rel_tree = PostOrderWithFilter::with_capacity(
         |node| plan.nodes.rel_iter(node),
         REL_CAPACITY,
@@ -37,23 +38,29 @@ fn collect_relational(plan: &Plan, predicate: FilterFn<'_>) -> Vec<(usize, usize
     nodes
 }
 
-fn check_distributions(plan: &Plan, nodes: &[(usize, usize)], expected_distributions: &[DistMock]) {
+fn check_distributions(
+    plan: &Plan,
+    nodes: &[LevelNode<NodeId>],
+    expected_distributions: &[DistMock],
+) {
     assert_eq!(
         expected_distributions.len(),
         nodes.len(),
         "different number of nodes"
     );
-    for ((level, id), expected) in nodes.iter().zip(expected_distributions.iter()) {
-        let actual: DistMock = plan.get_rel_distribution(*id).unwrap().into();
+    for (level_node, expected) in nodes.iter().zip(expected_distributions.iter()) {
+        let level = level_node.0;
+        let id = level_node.1;
+        let actual: DistMock = plan.get_rel_distribution(id).unwrap().into();
         assert_eq!(
             expected, &actual,
-            "wrong distribution for node ({id}) at level {level}"
+            "wrong distribution for node ({id:?}) at level {level}"
         );
     }
 }
 
 fn check_selection_dist(plan: &Plan, expected_dist: DistMock) {
-    let filter = |id: usize| -> bool {
+    let filter = |id: NodeId| -> bool {
         matches!(
             plan.get_node(id),
             Ok(Node::Relational(Relational::Selection { .. }))
@@ -77,20 +84,20 @@ fn front_sql_global_tbl_sq1() {
     let plan = sql_to_optimized_ir(input, vec![]);
     let expected_explain = String::from(
         r#"projection ("global_t"."a"::integer -> "a", "global_t"."b"::integer -> "b")
-    selection ROW("global_t"."a"::integer) in ROW($1) or ROW("global_t"."a"::integer) in ROW($0)
+    selection ROW("global_t"."a"::integer) in ROW($0) or ROW("global_t"."a"::integer) in ROW($1)
         scan "global_t"
 subquery $0:
+motion [policy: full]
+            scan
+                projection ("t"."a"::unsigned -> "a1")
+                    scan "t"
+subquery $1:
 scan
             projection (sum(("sum_39"::decimal))::decimal -> "col_1")
                 motion [policy: full]
                     scan
                         projection (sum(("t"."a"::unsigned))::decimal -> "sum_39")
                             scan "t"
-subquery $1:
-motion [policy: full]
-            scan
-                projection ("t"."a"::unsigned -> "a1")
-                    scan "t"
 execution options:
 sql_vdbe_max_steps = 45000
 vtable_max_rows = 5000
@@ -213,7 +220,7 @@ fn front_sql_global_tbl_sq3() {
     let plan = sql_to_optimized_ir(input, vec![]);
     let expected_explain = String::from(
         r#"projection ("global_t"."a"::integer -> "a", "global_t"."b"::integer -> "b")
-    selection not ROW("global_t"."a"::integer, "global_t"."b"::integer) in ROW($1, $1) or ROW("global_t"."a"::integer, "global_t"."b"::integer) < ROW($0, $0)
+    selection not ROW("global_t"."a"::integer, "global_t"."b"::integer) in ROW($0, $0) or ROW("global_t"."a"::integer, "global_t"."b"::integer) < ROW($1, $1)
         scan "global_t"
 subquery $0:
 motion [policy: full]
@@ -384,7 +391,7 @@ vtable_max_rows = 5000
 }
 
 fn check_join_dist(plan: &Plan, expected_distributions: &[DistMock]) {
-    let filter = |id: usize| -> bool {
+    let filter = |id: NodeId| -> bool {
         matches!(
             plan.get_node(id),
             Ok(Node::Relational(Relational::Join { .. }))
@@ -981,7 +988,7 @@ vtable_max_rows = 5000
 }
 
 fn check_union_dist(plan: &Plan, expected_distributions: &[DistMock]) {
-    let filter = |id: usize| -> bool {
+    let filter = |id: NodeId| -> bool {
         matches!(
             plan.get_node(id),
             Ok(Node::Relational(Relational::UnionAll { .. }))
diff --git a/sbroad-core/src/frontend/sql/ir/tests/single.rs b/sbroad-core/src/frontend/sql/ir/tests/single.rs
index 412d2bc72..798f10e8b 100644
--- a/sbroad-core/src/frontend/sql/ir/tests/single.rs
+++ b/sbroad-core/src/frontend/sql/ir/tests/single.rs
@@ -1,5 +1,6 @@
 use smol_str::{SmolStr, ToSmolStr};
 
+use crate::ir::expression::NodeId;
 use crate::ir::operator::Relational;
 use crate::ir::transformation::helpers::sql_to_optimized_ir;
 use crate::ir::transformation::redistribution::{MotionKey, MotionPolicy, Target};
@@ -30,7 +31,7 @@ impl Policy {
 }
 
 impl Plan {
-    fn to_test_motion(&self, node_id: usize) -> Policy {
+    fn to_test_motion(&self, node_id: NodeId) -> Policy {
         let node = self.get_relation_node(node_id).unwrap();
 
         match node {
@@ -76,13 +77,14 @@ fn check_join_motions(
 ) {
     let plan = sql_to_optimized_ir(sql, vec![]);
     let mut dfs = PostOrder::with_capacity(|x| plan.nodes.rel_iter(x), REL_CAPACITY);
-    let (_, join_id) = dfs
+    let level_node = dfs
         .iter(plan.get_top().unwrap())
-        .find(|(_, n)| -> bool {
-            let rel = plan.get_relation_node(*n).unwrap();
+        .find(|level_node| -> bool {
+            let rel = plan.get_relation_node(level_node.1).unwrap();
             matches!(rel, Relational::Join { .. })
         })
         .unwrap();
+    let join_id = level_node.1;
     let children = plan.get_relational_children(join_id).unwrap();
     let (left_id, right_id, sq_nodes_ids) = (children[0], children[1], &children[2..]);
     let (left_actual, right_actual) = (plan.to_test_motion(left_id), plan.to_test_motion(right_id));
diff --git a/sbroad-core/src/ir.rs b/sbroad-core/src/ir.rs
index 4a4047c52..10732d04a 100644
--- a/sbroad-core/src/ir.rs
+++ b/sbroad-core/src/ir.rs
@@ -9,6 +9,7 @@ use std::cell::{RefCell, RefMut};
 use std::collections::hash_map::IntoIter;
 use std::collections::{HashMap, HashSet};
 use std::fmt::{Display, Formatter};
+use tree::traversal::LevelNode;
 
 use std::slice::Iter;
 use tarantool::tlua;
@@ -35,7 +36,7 @@ use crate::ir::undo::TransformationLog;
 use crate::ir::value::Value;
 use crate::{collection, error, warn};
 
-use self::expression::Position;
+use self::expression::{NodeId, Position};
 use self::parameters::Parameters;
 use self::relation::Relations;
 use self::transformation::redistribution::MotionPolicy;
@@ -87,6 +88,17 @@ pub enum Node {
     Parameter(Option<Type>),
 }
 
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Hash, Copy)]
+pub enum ArenaType {
+    Default,
+}
+
+impl Display for ArenaType {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(f, "")
+    }
+}
+
 /// Plan nodes storage.
 #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
 pub struct Nodes {
@@ -98,23 +110,17 @@ pub struct Nodes {
 }
 
 impl Nodes {
-    pub(crate) fn get(&self, id: usize) -> Result<&Node, SbroadError> {
-        match self.arena.get(id) {
-            None => Err(SbroadError::NotFound(
-                Entity::Node,
-                format_smolstr!("from arena with index {id}"),
-            )),
-            Some(node) => Ok(node),
+    pub(crate) fn get(&self, id: NodeId) -> Option<&Node> {
+        let offset = usize::try_from(id.offset).unwrap();
+        match id.arena_type {
+            ArenaType::Default => self.arena.get(offset),
         }
     }
 
-    pub(crate) fn get_mut(&mut self, id: usize) -> Result<&mut Node, SbroadError> {
-        match self.arena.get_mut(id) {
-            None => Err(SbroadError::NotFound(
-                Entity::Node,
-                format_smolstr!("from arena with index {id}"),
-            )),
-            Some(node) => Ok(node),
+    pub(crate) fn get_mut(&mut self, id: NodeId) -> Option<&mut Node> {
+        let offset = usize::try_from(id.offset).unwrap();
+        match id.arena_type {
+            ArenaType::Default => self.arena.get_mut(offset),
         }
     }
 
@@ -152,31 +158,47 @@ impl Nodes {
 
     /// Add new node to arena.
     ///
+    /// # Panics
     /// Inserts a new node to the arena and returns its position,
     /// that is treated as a pointer.
-    pub fn push(&mut self, node: Node) -> usize {
+    pub fn push(&mut self, node: Node) -> NodeId {
         let position = self.arena.len();
         self.arena.push(node);
-        position
+
+        NodeId {
+            offset: u32::try_from(position).unwrap(),
+            arena_type: ArenaType::Default,
+        }
     }
 
+    /// # Panics
     /// Returns the next node position
     #[must_use]
-    pub fn next_id(&self) -> usize {
-        self.arena.len()
+    pub fn next_id(&self, arena_type: ArenaType) -> NodeId {
+        match arena_type {
+            ArenaType::Default => NodeId {
+                offset: u32::try_from(self.arena.len()).unwrap(),
+                arena_type: ArenaType::Default,
+            },
+        }
     }
 
     /// Replace a node in arena with another one.
     ///
     /// # Errors
     /// - The node with the given position doesn't exist.
-    pub fn replace(&mut self, id: usize, node: Node) -> Result<Node, SbroadError> {
-        if id >= self.arena.len() {
-            return Err(SbroadError::UnexpectedNumberOfValues(format_smolstr!(
-                "can't replace node with id {id} as it is out of arena bounds"
-            )));
-        }
-        let old_node = std::mem::replace(&mut self.arena[id], node);
+    pub fn replace(&mut self, id: NodeId, node: Node) -> Result<Node, SbroadError> {
+        match id.arena_type {
+            ArenaType::Default => {
+                if id.offset as usize >= self.arena.len() {
+                    return Err(SbroadError::UnexpectedNumberOfValues(format_smolstr!(
+                        "can't replace node with id {id:?} as it is out of arena bounds"
+                    )));
+                }
+            }
+        };
+
+        let old_node = std::mem::replace(&mut self.arena[id.offset as usize], node);
         Ok(old_node)
     }
 
@@ -201,23 +223,23 @@ impl<'nodes> IntoIterator for &'nodes Nodes {
 /// Element of `slice` vec is a `motion_id` to execute.
 #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
 pub struct Slice {
-    slice: Vec<usize>,
+    slice: Vec<NodeId>,
 }
 
-impl From<Vec<usize>> for Slice {
-    fn from(vec: Vec<usize>) -> Self {
+impl From<Vec<NodeId>> for Slice {
+    fn from(vec: Vec<NodeId>) -> Self {
         Self { slice: vec }
     }
 }
 
 impl Slice {
     #[must_use]
-    pub fn position(&self, index: usize) -> Option<&usize> {
+    pub fn position(&self, index: usize) -> Option<&NodeId> {
         self.slice.get(index)
     }
 
     #[must_use]
-    pub fn positions(&self) -> &[usize] {
+    pub fn positions(&self) -> &[NodeId] {
         &self.slice
     }
 }
@@ -235,8 +257,8 @@ impl From<Vec<Slice>> for Slices {
     }
 }
 
-impl From<Vec<Vec<usize>>> for Slices {
-    fn from(vec: Vec<Vec<usize>>) -> Self {
+impl From<Vec<Vec<NodeId>>> for Slices {
+    fn from(vec: Vec<Vec<NodeId>>) -> Self {
         Self {
             slices: vec.into_iter().map(Slice::from).collect(),
         }
@@ -263,7 +285,7 @@ impl Slices {
 #[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
 pub enum OptionParamValue {
     Value { val: Value },
-    Parameter { plan_id: usize },
+    Parameter { plan_id: NodeId },
 }
 
 #[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)]
@@ -409,7 +431,6 @@ impl Options {
     }
 }
 
-pub type NodeId = usize;
 pub type ValueIdx = usize;
 
 /// Logical plan tree structure.
@@ -430,7 +451,7 @@ pub struct Plan {
     /// The plan top is marked as optional for tree creation convenience.
     /// We build the plan tree in a bottom-up manner, so the top would
     /// be added last. The plan without a top should be treated as invalid.
-    top: Option<usize>,
+    top: Option<NodeId>,
     /// The flag is enabled if user wants to get a query plan only.
     /// In this case we don't need to execute query.
     is_explain: bool,
@@ -527,16 +548,22 @@ impl Plan {
                 return Err(SbroadError::Invalid(
                     Entity::Plan,
                     Some("plan tree top is None".into()),
-                ))
-            }
-            Some(top) => {
-                let _ = self.nodes.get(top)?;
+                ));
             }
-        }
-
-        //TODO: additional consistency checks
+            Some(top) => match top.arena_type {
+                ArenaType::Default => {
+                    if self.nodes.get(top).is_none() {
+                        return Err(SbroadError::Invalid(
+                            Entity::Plan,
+                            Some("plan tree top index is out of bouns".into()),
+                        ));
+                    }
+                }
+            },
+        };
 
         Ok(())
+        //TODO: additional consistency checks
     }
 
     /// Constructor for an empty plan structure.
@@ -577,7 +604,8 @@ impl Plan {
                 BreadthFirst::with_capacity(|x| self.nodes.rel_iter(x), REL_CAPACITY, REL_CAPACITY);
             bfs.populate_nodes(self.get_top()?);
             let nodes = bfs.take_nodes();
-            for (_, id) in nodes {
+            for level_node in nodes {
+                let id = level_node.1;
                 if let Relational::Insert { .. } = self.get_relation_node(id)? {
                     let child_id = self.get_relational_child(id, 0)?;
                     if let Relational::Values { children, .. } = self.get_relation_node(child_id)? {
@@ -657,13 +685,15 @@ impl Plan {
     /// # Errors
     /// Returns `SbroadError` when the node with requested index
     /// doesn't exist.
-    pub fn get_node(&self, id: usize) -> Result<&Node, SbroadError> {
-        match self.nodes.arena.get(id) {
-            None => Err(SbroadError::NotFound(
-                Entity::Node,
-                format_smolstr!("from arena with index {id}"),
-            )),
-            Some(node) => Ok(node),
+    pub fn get_node(&self, id: NodeId) -> Result<&Node, SbroadError> {
+        match id.arena_type {
+            ArenaType::Default => match self.nodes.arena.get(id.offset as usize) {
+                None => Err(SbroadError::NotFound(
+                    Entity::Node,
+                    format_smolstr!("from {:?} arena with index {}", id.arena_type, id.offset),
+                )),
+                Some(node) => Ok(node),
+            },
         }
     }
 
@@ -672,13 +702,19 @@ impl Plan {
     /// # Errors
     /// Returns `SbroadError` when the node with requested index
     /// doesn't exist.
-    pub fn get_mut_node(&mut self, id: usize) -> Result<&mut Node, SbroadError> {
-        match self.nodes.arena.get_mut(id) {
-            None => Err(SbroadError::NotFound(
-                Entity::Node,
-                format_smolstr!("(mutable) from arena with index {id}"),
-            )),
-            Some(node) => Ok(node),
+    pub fn get_mut_node(&mut self, id: NodeId) -> Result<&mut Node, SbroadError> {
+        match id.arena_type {
+            ArenaType::Default => match self.nodes.arena.get_mut(id.offset as usize) {
+                None => Err(SbroadError::NotFound(
+                    Entity::Node,
+                    format_smolstr!(
+                        "(mutable) from {:?} arena with index {}",
+                        id.arena_type,
+                        id.offset
+                    ),
+                )),
+                Some(node) => Ok(node),
+            },
         }
     }
 
@@ -686,7 +722,7 @@ impl Plan {
     ///
     /// # Errors
     /// - top node is None (i.e. invalid plan)
-    pub fn get_top(&self) -> Result<usize, SbroadError> {
+    pub fn get_top(&self) -> Result<NodeId, SbroadError> {
         self.top
             .ok_or_else(|| SbroadError::Invalid(Entity::Plan, Some("plan tree top is None".into())))
     }
@@ -714,7 +750,7 @@ impl Plan {
     ///
     /// # Errors
     /// - Given node is not a scan
-    pub fn get_scan_relation(&self, scan_id: usize) -> Result<&str, SbroadError> {
+    pub fn get_scan_relation(&self, scan_id: NodeId) -> Result<&str, SbroadError> {
         let node = self.get_relation_node(scan_id)?;
         if let Relational::ScanRelation { relation, .. } = node {
             return Ok(relation.as_str());
@@ -757,10 +793,10 @@ impl Plan {
     /// - invalid expression tree
     pub fn contains_aggregates(
         &self,
-        expr_id: usize,
+        expr_id: NodeId,
         check_top: bool,
     ) -> Result<bool, SbroadError> {
-        let filter = |id: usize| -> bool {
+        let filter = |id: NodeId| -> bool {
             matches!(
                 self.get_node(id),
                 Ok(Node::Expression(Expression::StableFunction { .. }))
@@ -771,7 +807,8 @@ impl Plan {
             EXPR_CAPACITY,
             Box::new(filter),
         );
-        for (_, id) in dfs.iter(expr_id) {
+        for level_node in dfs.iter(expr_id) {
+            let id = level_node.1;
             if !check_top && id == expr_id {
                 continue;
             }
@@ -815,11 +852,11 @@ impl Plan {
     /// # Errors
     /// - node is not relational
     /// - node's output is not a row of aliases
-    pub fn get_row_from_rel_node(&mut self, node: usize) -> Result<usize, SbroadError> {
+    pub fn get_row_from_rel_node(&mut self, node: NodeId) -> Result<NodeId, SbroadError> {
         let n = self.get_node(node)?;
         if let Node::Relational(rel) = n {
             if let Node::Expression(Expression::Row { list, .. }) = self.get_node(rel.output())? {
-                let mut cols: Vec<usize> = Vec::with_capacity(list.len());
+                let mut cols: Vec<NodeId> = Vec::with_capacity(list.len());
                 for alias in list {
                     if let Node::Expression(Expression::Alias { child, .. }) =
                         self.get_node(*alias)?
@@ -842,8 +879,8 @@ impl Plan {
     }
 
     #[must_use]
-    pub fn next_id(&self) -> usize {
-        self.nodes.next_id()
+    pub fn next_id(&self, arena_type: ArenaType) -> NodeId {
+        self.nodes.next_id(arena_type)
     }
 
     /// Add condition node to the plan.
@@ -852,10 +889,10 @@ impl Plan {
     /// Returns `SbroadError` when the condition node can't append'.
     pub fn add_cond(
         &mut self,
-        left: usize,
+        left: NodeId,
         op: operator::Bool,
-        right: usize,
-    ) -> Result<usize, SbroadError> {
+        right: NodeId,
+    ) -> Result<NodeId, SbroadError> {
         self.nodes.add_bool(left, op, right)
     }
 
@@ -863,7 +900,7 @@ impl Plan {
     ///
     /// # Errors
     /// Returns `SbroadError` when the condition node can't append'.
-    pub fn add_covered_with_parentheses(&mut self, child: usize) -> usize {
+    pub fn add_covered_with_parentheses(&mut self, child: NodeId) -> NodeId {
         self.nodes.add_covered_with_parentheses(child)
     }
 
@@ -873,10 +910,10 @@ impl Plan {
     /// Returns `SbroadError` when the condition node can't append'.
     pub fn add_arithmetic_to_plan(
         &mut self,
-        left: usize,
+        left: NodeId,
         op: Arithmetic,
-        right: usize,
-    ) -> Result<usize, SbroadError> {
+        right: NodeId,
+    ) -> Result<NodeId, SbroadError> {
         self.nodes.add_arithmetic_node(left, op, right)
     }
 
@@ -884,17 +921,17 @@ impl Plan {
     ///
     /// # Errors
     /// - Child node is invalid
-    pub fn add_unary(&mut self, op: operator::Unary, child: usize) -> Result<usize, SbroadError> {
+    pub fn add_unary(&mut self, op: operator::Unary, child: NodeId) -> Result<NodeId, SbroadError> {
         self.nodes.add_unary_bool(op, child)
     }
 
     /// Add CASE ... END operator to the plan.
     pub fn add_case(
         &mut self,
-        search_expr: Option<usize>,
-        when_blocks: Vec<(usize, usize)>,
-        else_expr: Option<usize>,
-    ) -> usize {
+        search_expr: Option<NodeId>,
+        when_blocks: Vec<(NodeId, NodeId)>,
+        else_expr: Option<NodeId>,
+    ) -> NodeId {
         self.nodes.push(Node::Expression(Expression::Case {
             search_expr,
             else_expr,
@@ -906,7 +943,12 @@ impl Plan {
     ///
     /// # Errors
     /// - Children node are invalid
-    pub fn add_bool(&mut self, left: usize, op: Bool, right: usize) -> Result<usize, SbroadError> {
+    pub fn add_bool(
+        &mut self,
+        left: NodeId,
+        op: Bool,
+        right: NodeId,
+    ) -> Result<NodeId, SbroadError> {
         self.nodes.add_bool(left, op, right)
     }
 
@@ -963,7 +1005,7 @@ impl Plan {
     /// Set top node of plan
     /// # Errors
     /// - top node doesn't exist in the plan.
-    pub fn set_top(&mut self, top: usize) -> Result<(), SbroadError> {
+    pub fn set_top(&mut self, top: NodeId) -> Result<(), SbroadError> {
         self.get_node(top)?;
         self.top = Some(top);
         Ok(())
@@ -974,7 +1016,7 @@ impl Plan {
     /// # Errors
     /// - node doesn't exist in the plan
     /// - node is not a relational type
-    pub fn get_relation_node(&self, node_id: usize) -> Result<&Relational, SbroadError> {
+    pub fn get_relation_node(&self, node_id: NodeId) -> Result<&Relational, SbroadError> {
         let node = self.get_node(node_id)?;
         match node {
             Node::Relational(rel) => Ok(rel),
@@ -996,7 +1038,7 @@ impl Plan {
     /// - node is not a relational type
     pub fn get_mut_relation_node(
         &mut self,
-        node_id: usize,
+        node_id: NodeId,
     ) -> Result<&mut Relational, SbroadError> {
         match self.get_mut_node(node_id)? {
             Node::Relational(rel) => Ok(rel),
@@ -1016,7 +1058,7 @@ impl Plan {
     /// # Errors
     /// - node doesn't exist in the plan
     /// - node is not expression type
-    pub fn get_expression_node(&self, node_id: usize) -> Result<&Expression, SbroadError> {
+    pub fn get_expression_node(&self, node_id: NodeId) -> Result<&Expression, SbroadError> {
         match self.get_node(node_id)? {
             Node::Expression(exp) => Ok(exp),
             Node::Parameter(..) => {
@@ -1043,7 +1085,7 @@ impl Plan {
     /// - node is not expression type
     pub fn get_mut_expression_node(
         &mut self,
-        node_id: usize,
+        node_id: NodeId,
     ) -> Result<&mut Expression, SbroadError> {
         let node = self.get_mut_node(node_id)?;
         match node {
@@ -1055,7 +1097,7 @@ impl Plan {
             | Node::Block(..) => Err(SbroadError::Invalid(
                 Entity::Node,
                 Some(format_smolstr!(
-                    "node ({node_id}) is not expression type: {node:?}"
+                    "node ({node_id:?}) is not expression type: {node:?}"
                 )),
             )),
         }
@@ -1065,7 +1107,7 @@ impl Plan {
     ///
     /// # Errors
     /// - supplied id does not correspond to `Row` node
-    pub fn get_row_list(&self, row_id: usize) -> Result<&[usize], SbroadError> {
+    pub fn get_row_list(&self, row_id: NodeId) -> Result<&[NodeId], SbroadError> {
         self.get_expression_node(row_id)?.get_row_list()
     }
 
@@ -1074,7 +1116,7 @@ impl Plan {
     ///
     /// # Errors
     /// - node is not an expression node
-    pub fn get_child_under_alias(&self, child_id: usize) -> Result<usize, SbroadError> {
+    pub fn get_child_under_alias(&self, child_id: NodeId) -> Result<NodeId, SbroadError> {
         match self.get_expression_node(child_id)? {
             Expression::Alias {
                 child: alias_child, ..
@@ -1087,7 +1129,7 @@ impl Plan {
     ///
     /// # Errors
     /// - supplied id does not correspond to `Row` node
-    pub fn get_mut_row_list(&mut self, row_id: usize) -> Result<&mut Vec<usize>, SbroadError> {
+    pub fn get_mut_row_list(&mut self, row_id: NodeId) -> Result<&mut Vec<NodeId>, SbroadError> {
         self.get_mut_expression_node(row_id)?.get_mut_row_list()
     }
 
@@ -1107,9 +1149,9 @@ impl Plan {
     /// children with the same id. So, if this happens, only one child will be replaced.
     pub fn replace_expression(
         &mut self,
-        parent_id: usize,
-        old_id: usize,
-        new_id: usize,
+        parent_id: NodeId,
+        old_id: NodeId,
+        new_id: NodeId,
     ) -> Result<(), SbroadError> {
         match self.get_mut_expression_node(parent_id)? {
             Expression::Unary { child, .. }
@@ -1190,7 +1232,7 @@ impl Plan {
         Err(SbroadError::FailedTo(
             Action::Replace,
             Some(Entity::Expression),
-            format_smolstr!("parent expression ({parent_id}) has no child with id {old_id}"),
+            format_smolstr!("parent expression ({parent_id:?}) has no child with id {old_id:?}"),
         ))
     }
 
@@ -1199,7 +1241,11 @@ impl Plan {
     /// # Errors
     /// - supplied index is out of range
     /// - node is not `GroupBy`
-    pub fn get_groupby_col(&self, groupby_id: usize, col_idx: usize) -> Result<usize, SbroadError> {
+    pub fn get_groupby_col(
+        &self,
+        groupby_id: NodeId,
+        col_idx: usize,
+    ) -> Result<NodeId, SbroadError> {
         let node = self.get_relation_node(groupby_id)?;
         if let Relational::GroupBy { gr_cols, .. } = node {
             let col_id = gr_cols.get(col_idx).ok_or_else(|| {
@@ -1220,7 +1266,7 @@ impl Plan {
     /// # Errors
     /// - supplied index is out of range
     /// - node is not `Projection`
-    pub fn get_proj_col(&self, proj_id: usize, col_idx: usize) -> Result<usize, SbroadError> {
+    pub fn get_proj_col(&self, proj_id: NodeId, col_idx: usize) -> Result<NodeId, SbroadError> {
         let node = self.get_relation_node(proj_id)?;
         if let Relational::Projection { output, .. } = node {
             let col_id = self.get_row_list(*output)?.get(col_idx).ok_or_else(|| {
@@ -1240,7 +1286,7 @@ impl Plan {
     ///
     /// # Errors
     /// - node is not `GroupBy`
-    pub fn get_grouping_cols(&self, groupby_id: usize) -> Result<&[usize], SbroadError> {
+    pub fn get_grouping_cols(&self, groupby_id: NodeId) -> Result<&[NodeId], SbroadError> {
         let node = self.get_relation_node(groupby_id)?;
         if let Relational::GroupBy { gr_cols, .. } = node {
             return Ok(gr_cols);
@@ -1257,8 +1303,8 @@ impl Plan {
     /// - node is not `GroupBy`
     pub fn set_grouping_cols(
         &mut self,
-        groupby_id: usize,
-        new_cols: Vec<usize>,
+        groupby_id: NodeId,
+        new_cols: Vec<NodeId>,
     ) -> Result<(), SbroadError> {
         let node = self.get_mut_relation_node(groupby_id)?;
         if let Relational::GroupBy { gr_cols, .. } = node {
@@ -1324,7 +1370,7 @@ impl Plan {
         let ref_node_target_child =
             ref_node_children
                 .get(*first_target)
-                .unwrap_or_else(|| panic!("Failed to get target index {first_target} for reference {node:?} and ref_node [id = {parent:?}] {ref_node:?}"));
+                .unwrap_or_else(|| panic!("Failed to get target index {first_target:?} for reference {node:?} and ref_node [id = {parent:?}] {ref_node:?}"));
 
         let column_rel_node = self.get_relation_node(*ref_node_target_child)?;
         let column_expr_node = self.get_expression_node(column_rel_node.output())?;
@@ -1339,19 +1385,19 @@ impl Plan {
     }
 
     /// Set slices of the plan.
-    pub fn set_slices(&mut self, slices: Vec<Vec<usize>>) {
+    pub fn set_slices(&mut self, slices: Vec<Vec<NodeId>>) {
         self.slices = slices.into();
     }
 
     /// # Errors
     /// - serialization error (to binary)
-    pub fn pattern_id(&self, top_id: usize) -> Result<SmolStr, SbroadError> {
-        let mut dfs =
-            PostOrder::with_capacity(|x| self.subtree_iter(x, false), self.nodes.next_id());
+    pub fn pattern_id(&self, top_id: NodeId) -> Result<SmolStr, SbroadError> {
+        let mut dfs = PostOrder::with_capacity(|x| self.subtree_iter(x, false), self.nodes.len());
         dfs.populate_nodes(top_id);
         let nodes = dfs.take_nodes();
         let mut plan_nodes: Vec<&Node> = Vec::with_capacity(nodes.len());
-        for (_, id) in nodes {
+        for level_node in nodes {
+            let id = level_node.1;
             plan_nodes.push(self.get_node(id)?);
         }
         let bytes: Vec<u8> = bincode::serialize(&plan_nodes).map_err(|e| {
@@ -1636,7 +1682,7 @@ impl ShardColumnsMap {
 
     fn update_subtree(&mut self, node_id: NodeId, plan: &Plan) -> Result<(), SbroadError> {
         let mut dfs = PostOrder::with_capacity(|x| plan.nodes.rel_iter(x), REL_CAPACITY);
-        for (_, id) in dfs.iter(node_id) {
+        for LevelNode(_, id) in dfs.iter(node_id) {
             self.update_node(id, plan)?;
             self.invalid_ids.remove(&id);
         }
diff --git a/sbroad-core/src/ir/acl.rs b/sbroad-core/src/ir/acl.rs
index adec244ce..54cf1e420 100644
--- a/sbroad-core/src/ir/acl.rs
+++ b/sbroad-core/src/ir/acl.rs
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
 use smol_str::{format_smolstr, SmolStr, ToSmolStr};
 use tarantool::decimal::Decimal;
 
-use super::ddl::ParamDef;
+use super::{ddl::ParamDef, expression::NodeId};
 
 ::tarantool::define_str_enum! {
     /// Revoked or granted privilege.
@@ -256,7 +256,7 @@ impl Plan {
     /// # Errors
     /// - the node index is absent in arena
     /// - current node is not of ACL type
-    pub fn get_acl_node(&self, node_id: usize) -> Result<&Acl, SbroadError> {
+    pub fn get_acl_node(&self, node_id: NodeId) -> Result<&Acl, SbroadError> {
         let node = self.get_node(node_id)?;
         match node {
             Node::Acl(acl) => Ok(acl),
@@ -272,7 +272,7 @@ impl Plan {
     /// # Errors
     /// - the node index is absent in arena
     /// - current node is not of ACL type
-    pub fn get_mut_acl_node(&mut self, node_id: usize) -> Result<&mut Acl, SbroadError> {
+    pub fn get_mut_acl_node(&mut self, node_id: NodeId) -> Result<&mut Acl, SbroadError> {
         let node = self.get_mut_node(node_id)?;
         match node {
             Node::Acl(acl) => Ok(acl),
@@ -287,7 +287,7 @@ impl Plan {
     ///
     /// # Errors
     /// - current node is not of ACL type
-    pub fn take_acl_node(&mut self, node_id: usize) -> Result<Acl, SbroadError> {
+    pub fn take_acl_node(&mut self, node_id: NodeId) -> Result<Acl, SbroadError> {
         // Check that node is ACL type (before making any distructive operations).
         let _ = self.get_acl_node(node_id)?;
         // Replace ACL with parameter node.
diff --git a/sbroad-core/src/ir/aggregates.rs b/sbroad-core/src/ir/aggregates.rs
index f1ecee6ce..7db5a343e 100644
--- a/sbroad-core/src/ir/aggregates.rs
+++ b/sbroad-core/src/ir/aggregates.rs
@@ -10,7 +10,7 @@ use std::collections::HashMap;
 use std::fmt::{Display, Formatter};
 use std::rc::Rc;
 
-use super::expression::{ColumnPositionMap, FunctionFeature};
+use super::expression::{ColumnPositionMap, FunctionFeature, NodeId};
 
 /// The kind of aggregate function
 ///
@@ -90,7 +90,7 @@ impl AggregateKind {
     ///
     /// # Panics
     /// - Invalid argument count for aggregate
-    pub fn check_args_types(&self, plan: &Plan, args: &[usize]) -> Result<(), SbroadError> {
+    pub fn check_args_types(&self, plan: &Plan, args: &[NodeId]) -> Result<(), SbroadError> {
         use crate::ir::relation::Type;
         let get_arg_type = |idx: usize| -> Result<Type, SbroadError> {
             let arg_id = *args.get(idx).expect("wrong agregate");
@@ -200,7 +200,7 @@ pub struct SimpleAggregate {
     /// map will contain: `avg` -> `l1`
     pub lagg_alias: HashMap<AggregateKind, Rc<String>>,
     /// id of aggregate function in IR
-    pub fun_id: usize,
+    pub fun_id: NodeId,
 }
 
 #[cfg(not(feature = "mock"))]
@@ -220,7 +220,7 @@ pub fn generate_local_alias_for_aggr(kind: &AggregateKind, suffix: &str) -> Stri
 
 impl SimpleAggregate {
     #[must_use]
-    pub fn new(name: &str, fun_id: usize) -> Option<SimpleAggregate> {
+    pub fn new(name: &str, fun_id: NodeId) -> Option<SimpleAggregate> {
         let kind = AggregateKind::new(name)?;
         let laggr_alias: HashMap<AggregateKind, Rc<String>> = HashMap::new();
         let aggr = SimpleAggregate {
@@ -297,14 +297,14 @@ impl SimpleAggregate {
     #[allow(clippy::too_many_lines)]
     pub(crate) fn create_final_aggregate_expr(
         &self,
-        parent: usize,
+        parent: NodeId,
         plan: &mut Plan,
         fun_type: &RelType,
         mut position_kinds: Vec<PositionKind>,
         is_distinct: bool,
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         // map local AggregateKind to finalised expression of that aggregate
-        let mut final_aggregates: HashMap<AggregateKind, usize> = HashMap::new();
+        let mut final_aggregates: HashMap<AggregateKind, NodeId> = HashMap::new();
         let mut create_final_aggr = |position: Position,
                                      local_kind: AggregateKind,
                                      final_func: AggregateKind|
@@ -338,7 +338,7 @@ impl SimpleAggregate {
                         return Err(SbroadError::Invalid(
                             Entity::Aggregate,
                             Some(format_smolstr!(
-                                "fun_id ({}) points to other expression node",
+                                "fun_id ({:?}) points to other expression node",
                                 self.fun_id
                             )),
                         ));
diff --git a/sbroad-core/src/ir/api/children.rs b/sbroad-core/src/ir/api/children.rs
index d3874c90c..2b004eac7 100644
--- a/sbroad-core/src/ir/api/children.rs
+++ b/sbroad-core/src/ir/api/children.rs
@@ -1,15 +1,17 @@
 use std::ops::{Index, Range, RangeFrom, RangeFull};
 
+use crate::ir::expression::NodeId;
+
 #[derive(Debug)]
 pub enum Children<'r> {
     None,
-    Single(&'r usize),
-    Couple(&'r usize, &'r usize),
-    Many(&'r [usize]),
+    Single(&'r NodeId),
+    Couple(&'r NodeId, &'r NodeId),
+    Many(&'r [NodeId]),
 }
 
 impl<'r> Index<usize> for Children<'r> {
-    type Output = usize;
+    type Output = NodeId;
 
     fn index(&self, idx: usize) -> &Self::Output {
         match self {
@@ -29,7 +31,7 @@ impl<'r> Index<usize> for Children<'r> {
 }
 
 impl<'r> Index<Range<usize>> for Children<'r> {
-    type Output = [usize];
+    type Output = [NodeId];
 
     fn index(&self, range: Range<usize>) -> &Self::Output {
         match self {
@@ -40,7 +42,7 @@ impl<'r> Index<Range<usize>> for Children<'r> {
 }
 
 impl<'r> Index<RangeFrom<usize>> for Children<'r> {
-    type Output = [usize];
+    type Output = [NodeId];
 
     fn index(&self, range: RangeFrom<usize>) -> &Self::Output {
         &self[range.start..self.len()]
@@ -48,7 +50,7 @@ impl<'r> Index<RangeFrom<usize>> for Children<'r> {
 }
 
 impl<'r> Index<RangeFull> for Children<'r> {
-    type Output = [usize];
+    type Output = [NodeId];
 
     fn index(&self, _: RangeFull) -> &Self::Output {
         &self[0..self.len()]
@@ -72,7 +74,7 @@ impl<'r> Children<'r> {
     }
 
     #[must_use]
-    pub fn to_vec(&self) -> Vec<usize> {
+    pub fn to_vec(&self) -> Vec<NodeId> {
         match self {
             Children::None => vec![],
             Children::Single(i) => vec![**i],
@@ -82,7 +84,7 @@ impl<'r> Children<'r> {
     }
 
     #[must_use]
-    pub fn get(&self, idx: usize) -> Option<&'r usize> {
+    pub fn get(&self, idx: usize) -> Option<&'r NodeId> {
         if idx >= self.len() {
             return None;
         }
@@ -109,7 +111,7 @@ pub struct ChildrenIter<'c> {
 }
 
 impl<'c> Iterator for ChildrenIter<'c> {
-    type Item = &'c usize;
+    type Item = &'c NodeId;
 
     fn next(&mut self) -> Option<Self::Item> {
         let child = match self.inner {
@@ -148,7 +150,7 @@ impl<'r> Children<'r> {
 }
 
 impl<'r> IntoIterator for &'r Children<'r> {
-    type Item = &'r usize;
+    type Item = &'r NodeId;
 
     type IntoIter = ChildrenIter<'r>;
 
@@ -161,9 +163,9 @@ impl<'r> IntoIterator for &'r Children<'r> {
 #[allow(clippy::module_name_repetitions)]
 pub enum MutChildren<'r> {
     None,
-    Single(&'r mut usize),
-    Couple(&'r mut usize, &'r mut usize),
-    Many(&'r mut [usize]),
+    Single(&'r mut NodeId),
+    Couple(&'r mut NodeId, &'r mut NodeId),
+    Many(&'r mut [NodeId]),
 }
 
 impl<'r> MutChildren<'r> {
@@ -183,7 +185,7 @@ impl<'r> MutChildren<'r> {
     }
 
     #[must_use]
-    pub fn to_vec(&self) -> Vec<usize> {
+    pub fn to_vec(&self) -> Vec<NodeId> {
         match self {
             MutChildren::None => vec![],
             MutChildren::Single(i) => vec![**i],
@@ -196,7 +198,7 @@ impl<'r> MutChildren<'r> {
     // that instance lifetime '1 must be bigger than 'r and we
     // return '1
     #[must_use]
-    pub fn get_mut(self, idx: usize) -> Option<&'r mut usize> {
+    pub fn get_mut(self, idx: usize) -> Option<&'r mut NodeId> {
         if idx >= self.len() {
             return None;
         }
@@ -215,7 +217,7 @@ impl<'r> MutChildren<'r> {
     }
 
     #[must_use]
-    pub fn split_first(self) -> Option<(&'r mut usize, MutChildren<'r>)> {
+    pub fn split_first(self) -> Option<(&'r mut NodeId, MutChildren<'r>)> {
         let res = match self {
             MutChildren::None => return None,
             MutChildren::Single(i) => (i, MutChildren::None),
@@ -236,7 +238,7 @@ pub struct MutChildrenIter<'c> {
 }
 
 impl<'c> Iterator for MutChildrenIter<'c> {
-    type Item = &'c mut usize;
+    type Item = &'c mut NodeId;
 
     #[must_use]
     fn next(&mut self) -> Option<Self::Item> {
@@ -261,7 +263,7 @@ impl<'r> MutChildren<'r> {
 }
 
 impl<'r> IntoIterator for MutChildren<'r> {
-    type Item = &'r mut usize;
+    type Item = &'r mut NodeId;
 
     type IntoIter = MutChildrenIter<'r>;
 
diff --git a/sbroad-core/src/ir/api/constant.rs b/sbroad-core/src/ir/api/constant.rs
index c9ce32ae4..50d841a0c 100644
--- a/sbroad-core/src/ir/api/constant.rs
+++ b/sbroad-core/src/ir/api/constant.rs
@@ -1,9 +1,9 @@
 use smol_str::format_smolstr;
 
 use crate::errors::{Entity, SbroadError};
-use crate::ir::expression::Expression;
+use crate::ir::expression::{Expression, NodeId};
 use crate::ir::value::Value;
-use crate::ir::{Node, Nodes, Plan};
+use crate::ir::{ArenaType, Node, Nodes, Plan};
 
 impl Expression {
     /// Gets value from const node
@@ -45,26 +45,30 @@ impl Expression {
 
 impl Nodes {
     /// Adds constant node.
-    pub fn add_const(&mut self, value: Value) -> usize {
+    pub fn add_const(&mut self, value: Value) -> NodeId {
         self.push(Node::Expression(Expression::Constant { value }))
     }
 }
 
 impl Plan {
     /// Add constant value to the plan.
-    pub fn add_const(&mut self, v: Value) -> usize {
+    pub fn add_const(&mut self, v: Value) -> NodeId {
         self.nodes.add_const(v)
     }
 
     #[must_use]
-    pub fn get_const_list(&self) -> Vec<usize> {
+    /// # Panics
+    pub fn get_const_list(&self) -> Vec<NodeId> {
         self.nodes
             .arena
             .iter()
             .enumerate()
             .filter_map(|(id, node)| {
                 if let Node::Expression(Expression::Constant { .. }) = node {
-                    Some(id)
+                    Some(NodeId {
+                        offset: u32::try_from(id).unwrap(),
+                        arena_type: ArenaType::Default,
+                    })
                 } else {
                     None
                 }
@@ -83,7 +87,7 @@ impl Plan {
                 return Err(SbroadError::Invalid(
                     Entity::Expression,
                     Some(format_smolstr!(
-                        "Restoring parameters filed: node {const_node:?} (id: {id}) is not of a constant type"
+                        "Restoring parameters filed: node {const_node:?} (id: {id:?}) is not of a constant type"
                     )),
                 ));
             }
diff --git a/sbroad-core/src/ir/api/parameter.rs b/sbroad-core/src/ir/api/parameter.rs
index fa5cd4e32..9875e3028 100644
--- a/sbroad-core/src/ir/api/parameter.rs
+++ b/sbroad-core/src/ir/api/parameter.rs
@@ -1,10 +1,10 @@
 use crate::errors::SbroadError;
 use crate::ir::block::Block;
-use crate::ir::expression::Expression;
+use crate::ir::expression::{Expression, NodeId};
 use crate::ir::operator::Relational;
 use crate::ir::tree::traversal::{LevelNode, PostOrder};
 use crate::ir::value::Value;
-use crate::ir::{Node, NodeId, OptionParamValue, Plan, ValueIdx};
+use crate::ir::{ArenaType, Node, OptionParamValue, Plan, ValueIdx};
 use crate::otm::child_span;
 use sbroad_proc::otm_child_span;
 use smol_str::format_smolstr;
@@ -16,7 +16,7 @@ use std::collections::HashMap;
 struct ParamsBinder<'binder> {
     plan: &'binder mut Plan,
     /// Plan nodes to traverse during binding.
-    nodes: Vec<LevelNode>,
+    nodes: Vec<LevelNode<NodeId>>,
     /// Number of parameters met in the OPTIONs.
     binded_options_counter: usize,
     /// Flag indicating whether we use Tarantool parameters notation.
@@ -34,16 +34,16 @@ struct ParamsBinder<'binder> {
     /// queries it is IR responsibility).
     ///
     /// Map of { param_id -> corresponding row }.
-    row_map: AHashMap<usize, usize, RandomState>,
+    row_map: AHashMap<NodeId, NodeId, RandomState>,
 }
 
 fn get_param_value(
     tnt_params_style: bool,
-    param_id: usize,
+    param_id: NodeId,
     param_index: usize,
-    value_ids: &[usize],
+    value_ids: &[NodeId],
     pg_params_map: &HashMap<NodeId, ValueIdx>,
-) -> usize {
+) -> NodeId {
     let value_index = if tnt_params_style {
         // In case non-pg params are used, index is the correct position
         param_index
@@ -51,7 +51,7 @@ fn get_param_value(
         value_ids.len()
             - 1
             - *pg_params_map.get(&param_id).unwrap_or_else(|| {
-                panic!("Value index not found for parameter with id: {param_id}.")
+                panic!("Value index not found for parameter with id: {param_id:?}.")
             })
     };
     let val_id = value_ids
@@ -62,7 +62,7 @@ fn get_param_value(
 
 impl<'binder> ParamsBinder<'binder> {
     fn new(plan: &'binder mut Plan, mut values: Vec<Value>) -> Result<Self, SbroadError> {
-        let capacity = plan.next_id();
+        let capacity = plan.nodes.len();
         let mut tree = PostOrder::with_capacity(|node| plan.subtree_iter(node, false), capacity);
         let top_id = plan.get_top()?;
         tree.populate_nodes(top_id);
@@ -100,20 +100,20 @@ impl<'binder> ParamsBinder<'binder> {
             // than once in order to get the same hash.
             // See https://git.picodata.io/picodata/picodata/sbroad/-/issues/583
             let mut used_values = vec![false; self.values.len()];
-            let invalid_idx = |param_id: usize, value_idx: usize| {
-                panic!("Out of bounds value index {value_idx} for pg parameter {param_id}.");
+            let invalid_idx = |param_id: NodeId, value_idx: usize| {
+                panic!("Out of bounds value index {value_idx} for pg parameter {param_id:?}.");
             };
 
             // NB: we can't use `param_node_ids`, we need to traverse
             // parameters in the same order they will be bound,
             // otherwise we may get different hashes for plans
             // with tnt and pg parameters. See `subtree_hash*` tests,
-            for (_, param_id) in &self.nodes {
+            for LevelNode(_, param_id) in &self.nodes {
                 if !matches!(self.plan.get_node(*param_id)?, Node::Parameter(..)) {
                     continue;
                 }
                 let value_idx = *self.pg_params_map.get(param_id).unwrap_or_else(|| {
-                    panic!("Value index not found for parameter with id: {param_id}.");
+                    panic!("Value index not found for parameter with id: {param_id:?}.");
                 });
                 if used_values.get(value_idx).copied().unwrap_or(true) {
                     let Some(value) = self.values.get(value_idx) else {
@@ -159,7 +159,7 @@ impl<'binder> ParamsBinder<'binder> {
     }
 
     /// Retrieve a corresponding value (plan constant node) for a parameter node.
-    fn get_param_value(&self, param_id: usize, param_index: usize) -> usize {
+    fn get_param_value(&self, param_id: NodeId, param_index: usize) -> NodeId {
         get_param_value(
             self.tnt_params_style,
             param_id,
@@ -173,10 +173,10 @@ impl<'binder> ParamsBinder<'binder> {
     /// 2.) In case `cover_with_row` is set to true, cover the param node with a row.
     fn cover_param_with_row(
         &self,
-        param_id: usize,
+        param_id: NodeId,
         cover_with_row: bool,
         param_index: &mut usize,
-        row_ids: &mut HashMap<usize, usize, RandomState>,
+        row_ids: &mut HashMap<NodeId, NodeId, RandomState>,
     ) {
         if self.param_node_ids.contains(&param_id) {
             if row_ids.contains_key(&param_id) {
@@ -198,7 +198,7 @@ impl<'binder> ParamsBinder<'binder> {
 
         let mut row_ids = HashMap::with_hasher(RandomState::new());
 
-        for (_, id) in &self.nodes {
+        for LevelNode(_, id) in &self.nodes {
             let node = self.plan.get_node(*id)?;
             match node {
                 // Note: Parameter may not be met at the top of relational operators' expression
@@ -353,7 +353,7 @@ impl<'binder> ParamsBinder<'binder> {
             }
         }
 
-        let fixed_row_ids: AHashMap<usize, usize, RandomState> = row_ids
+        let fixed_row_ids: AHashMap<NodeId, NodeId, RandomState> = row_ids
             .iter()
             .map(|(param_id, val_id)| {
                 let row_cover = self.plan.nodes.add_row(vec![*val_id], None);
@@ -368,9 +368,9 @@ impl<'binder> ParamsBinder<'binder> {
     /// Replace parameters in the plan.
     #[allow(clippy::too_many_lines)]
     fn bind_params(&mut self) -> Result<(), SbroadError> {
-        let mut exprs_to_set_ref_type: HashMap<usize, Type> = HashMap::new();
+        let mut exprs_to_set_ref_type: HashMap<NodeId, Type> = HashMap::new();
 
-        for (_, id) in &self.nodes {
+        for LevelNode(_, id) in &self.nodes {
             // Before binding, references that referred to
             // parameters had scalar type (by default),
             // but in fact they may refer to different stuff.
@@ -394,13 +394,13 @@ impl<'binder> ParamsBinder<'binder> {
         let value_ids = std::mem::take(&mut self.value_ids);
         let pg_params_map = std::mem::take(&mut self.pg_params_map);
 
-        let bind_param = |param_id: &mut usize, is_row: bool, param_index: &mut usize| {
+        let bind_param = |param_id: &mut NodeId, is_row: bool, param_index: &mut usize| {
             *param_id = if self.param_node_ids.contains(param_id) {
                 *param_index = param_index.saturating_sub(1);
                 let binding_node_id = if is_row {
                     *row_ids
                         .get(param_id)
-                        .unwrap_or_else(|| panic!("Row not found at position {param_id}"))
+                        .unwrap_or_else(|| panic!("Row not found at position {param_id:?}"))
                 } else {
                     get_param_value(
                         tnt_params_style,
@@ -416,7 +416,7 @@ impl<'binder> ParamsBinder<'binder> {
             }
         };
 
-        for (_, id) in &self.nodes {
+        for LevelNode(_, id) in &self.nodes {
             let node = self.plan.get_mut_node(*id)?;
             match node {
                 Node::Relational(rel) => match rel {
@@ -529,7 +529,7 @@ impl<'binder> ParamsBinder<'binder> {
     }
 
     fn update_value_rows(&mut self) -> Result<(), SbroadError> {
-        for (_, id) in &self.nodes {
+        for LevelNode(_, id) in &self.nodes {
             if let Ok(Node::Relational(Relational::ValuesRow { .. })) = self.plan.get_node(*id) {
                 self.plan.update_values_row(*id)?;
             }
@@ -539,7 +539,7 @@ impl<'binder> ParamsBinder<'binder> {
 }
 
 impl Plan {
-    pub fn add_param(&mut self) -> usize {
+    pub fn add_param(&mut self) -> NodeId {
         self.nodes.push(Node::Parameter(None))
     }
 
@@ -580,15 +580,19 @@ impl Plan {
 
     // Gather all parameter nodes from the tree to a hash set.
     #[must_use]
-    pub fn get_param_set(&self) -> AHashSet<usize> {
-        let param_set: AHashSet<usize> = self
+    /// # Panics
+    pub fn get_param_set(&self) -> AHashSet<NodeId> {
+        let param_set: AHashSet<NodeId> = self
             .nodes
             .arena
             .iter()
             .enumerate()
             .filter_map(|(id, node)| {
                 if let Node::Parameter(..) = node {
-                    Some(id)
+                    Some(NodeId {
+                        offset: u32::try_from(id).unwrap(),
+                        arena_type: ArenaType::Default,
+                    })
                 } else {
                     None
                 }
@@ -606,7 +610,7 @@ impl Plan {
     ///
     /// # Panics
     /// - Plan is inconsistent state
-    pub fn update_values_row(&mut self, id: usize) -> Result<(), SbroadError> {
+    pub fn update_values_row(&mut self, id: NodeId) -> Result<(), SbroadError> {
         let values_row = self.get_node(id)?;
         let (output_id, data_id) =
             if let Node::Relational(Relational::ValuesRow { output, data, .. }) = values_row {
diff --git a/sbroad-core/src/ir/block.rs b/sbroad-core/src/ir/block.rs
index b7313321c..5dbd9ce97 100644
--- a/sbroad-core/src/ir/block.rs
+++ b/sbroad-core/src/ir/block.rs
@@ -1,10 +1,12 @@
 //! IR nodes representing blocks of commands.
 
 use crate::errors::{Entity, SbroadError};
-use crate::ir::{Node, NodeId, Plan};
+use crate::ir::{Node, Plan};
 use serde::{Deserialize, Serialize};
 use smol_str::{format_smolstr, SmolStr};
 
+use super::expression::NodeId;
+
 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
 pub enum Block {
     /// Procedure body.
@@ -41,7 +43,7 @@ impl Plan {
             | Node::Parameter(..) => Err(SbroadError::Invalid(
                 Entity::Node,
                 Some(format_smolstr!(
-                    "node {node:?} (id {node_id}) is not Block type"
+                    "node {node:?} (id {node_id:?}) is not Block type"
                 )),
             )),
         }
@@ -62,7 +64,7 @@ impl Plan {
             | Node::Parameter(..) => Err(SbroadError::Invalid(
                 Entity::Node,
                 Some(format_smolstr!(
-                    "node {node:?} (id {node_id}) is not Block type"
+                    "node {node:?} (id {node_id:?}) is not Block type"
                 )),
             )),
         }
diff --git a/sbroad-core/src/ir/ddl.rs b/sbroad-core/src/ir/ddl.rs
index 55d173c17..5932af5fb 100644
--- a/sbroad-core/src/ir/ddl.rs
+++ b/sbroad-core/src/ir/ddl.rs
@@ -11,6 +11,8 @@ use tarantool::{
     index::{IndexType, RtreeIndexDistanceType},
 };
 
+use super::expression::NodeId;
+
 #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
 pub struct ColumnDef {
     pub name: SmolStr,
@@ -187,7 +189,7 @@ impl Plan {
     /// # Errors
     /// - the node index is absent in arena
     /// - current node is not of DDL type
-    pub fn get_ddl_node(&self, node_id: usize) -> Result<&Ddl, SbroadError> {
+    pub fn get_ddl_node(&self, node_id: NodeId) -> Result<&Ddl, SbroadError> {
         let node = self.get_node(node_id)?;
         match node {
             Node::Ddl(ddl) => Ok(ddl),
@@ -203,7 +205,7 @@ impl Plan {
     /// # Errors
     /// - the node index is absent in arena
     /// - current node is not of DDL type
-    pub fn get_mut_ddl_node(&mut self, node_id: usize) -> Result<&mut Ddl, SbroadError> {
+    pub fn get_mut_ddl_node(&mut self, node_id: NodeId) -> Result<&mut Ddl, SbroadError> {
         let node = self.get_mut_node(node_id)?;
         match node {
             Node::Ddl(ddl) => Ok(ddl),
@@ -218,7 +220,7 @@ impl Plan {
     ///
     /// # Errors
     /// - current node is not of DDL type
-    pub fn take_ddl_node(&mut self, node_id: usize) -> Result<Ddl, SbroadError> {
+    pub fn take_ddl_node(&mut self, node_id: NodeId) -> Result<Ddl, SbroadError> {
         // Check that node is DDL type (before making any distructive operations).
         let _ = self.get_ddl_node(node_id)?;
         // Replace DDL with parameter node.
diff --git a/sbroad-core/src/ir/distribution.rs b/sbroad-core/src/ir/distribution.rs
index 16bc3e4b6..6a20a724b 100644
--- a/sbroad-core/src/ir/distribution.rs
+++ b/sbroad-core/src/ir/distribution.rs
@@ -12,7 +12,7 @@ use crate::ir::helpers::RepeatableState;
 use crate::ir::transformation::redistribution::{MotionKey, Target};
 
 use super::api::children::Children;
-use super::expression::Expression;
+use super::expression::{Expression, NodeId};
 use super::operator::Relational;
 use super::relation::{Column, ColumnPositions};
 use super::{Node, Plan};
@@ -279,9 +279,9 @@ impl Distribution {
 
 enum ReferredNodes {
     None,
-    Single(usize),
-    Pair(usize, usize),
-    Multiple(Vec<usize>),
+    Single(NodeId),
+    Pair(NodeId, NodeId),
+    Multiple(Vec<NodeId>),
 }
 
 impl ReferredNodes {
@@ -289,7 +289,7 @@ impl ReferredNodes {
         ReferredNodes::None
     }
 
-    fn append(&mut self, node: usize) {
+    fn append(&mut self, node: NodeId) {
         match self {
             ReferredNodes::None => *self = ReferredNodes::Single(node),
             ReferredNodes::Single(n) => {
@@ -324,7 +324,7 @@ struct ReferenceInfo {
 
 impl ReferenceInfo {
     pub fn new(
-        row_id: usize,
+        row_id: NodeId,
         ir: &Plan,
         parent_children: &Children<'_>,
     ) -> Result<Self, SbroadError> {
@@ -365,7 +365,7 @@ impl ReferenceInfo {
 }
 
 impl Iterator for ReferredNodes {
-    type Item = usize;
+    type Item = NodeId;
 
     fn next(&mut self) -> Option<Self::Item> {
         match self {
@@ -396,13 +396,13 @@ impl Iterator for ReferredNodes {
 #[derive(Debug, Eq, Hash, PartialEq)]
 struct ChildColumnReference {
     /// Child node id.
-    node_id: usize,
+    node_id: NodeId,
     /// Column position in the child node.
     column_position: usize,
 }
 
-impl From<(usize, usize)> for ChildColumnReference {
-    fn from((node_id, column_position): (usize, usize)) -> Self {
+impl From<(NodeId, usize)> for ChildColumnReference {
+    fn from((node_id, column_position): (NodeId, usize)) -> Self {
         ChildColumnReference {
             node_id,
             column_position,
@@ -419,14 +419,14 @@ impl Plan {
     /// - node is not projection
     /// - invalid projection node (e.g. no children)
     /// - failed to get child distribution
-    pub fn set_projection_distribution(&mut self, proj_id: usize) -> Result<(), SbroadError> {
+    pub fn set_projection_distribution(&mut self, proj_id: NodeId) -> Result<(), SbroadError> {
         if !matches!(
             self.get_relation_node(proj_id)?,
             Relational::Projection { .. }
         ) {
             return Err(SbroadError::Invalid(
                 Entity::Node,
-                Some(format_smolstr!("expected projection on id: {proj_id}")),
+                Some(format_smolstr!("expected projection on id: {proj_id:?}")),
             ));
         };
 
@@ -478,7 +478,7 @@ impl Plan {
     /// # Panics
     /// - reference has invalid targets
     #[allow(clippy::too_many_lines)]
-    pub fn set_distribution(&mut self, row_id: usize) -> Result<(), SbroadError> {
+    pub fn set_distribution(&mut self, row_id: NodeId) -> Result<(), SbroadError> {
         let row_children = self.get_expression_node(row_id)?.get_row_list()?;
 
         let mut parent_node = None;
@@ -608,7 +608,7 @@ impl Plan {
     /// - missing Motion(Full) for sq with Any distribution
     pub(crate) fn dist_from_subqueries(
         &self,
-        node_id: usize,
+        node_id: NodeId,
     ) -> Result<Option<Distribution>, SbroadError> {
         let node = self.get_relation_node(node_id)?;
 
@@ -644,7 +644,7 @@ impl Plan {
                     return Err(SbroadError::Invalid(
                         Entity::Distribution,
                         Some(format_smolstr!(
-                            "expected Motion(Full) for subquery child ({sq_id})"
+                            "expected Motion(Full) for subquery child ({sq_id:?})"
                         )),
                     ));
                 }
@@ -659,7 +659,7 @@ impl Plan {
 
     fn dist_from_child(
         &self,
-        child_rel_node: usize,
+        child_rel_node: NodeId,
         child_pos_map: &AHashMap<ChildColumnReference, ParentColumnPosition>,
     ) -> Result<Distribution, SbroadError> {
         if let Node::Relational(relational_op) = self.get_node(child_rel_node)? {
@@ -724,7 +724,7 @@ impl Plan {
     ///
     /// # Errors
     /// - supplied node is `Row`
-    pub fn set_dist(&mut self, row_id: usize, dist: Distribution) -> Result<(), SbroadError> {
+    pub fn set_dist(&mut self, row_id: NodeId, dist: Distribution) -> Result<(), SbroadError> {
         if let Expression::Row {
             ref mut distribution,
             ..
@@ -742,10 +742,10 @@ impl Plan {
     fn set_two_children_node_dist(
         &mut self,
         child_pos_map: &AHashMap<ChildColumnReference, ParentColumnPosition>,
-        left_id: usize,
-        right_id: usize,
-        parent_id: usize,
-        row_id: usize,
+        left_id: NodeId,
+        right_id: NodeId,
+        parent_id: NodeId,
+        row_id: NodeId,
     ) -> Result<(), SbroadError> {
         let left_dist = self.dist_from_child(left_id, child_pos_map)?;
         let right_dist = self.dist_from_child(right_id, child_pos_map)?;
@@ -785,7 +785,7 @@ impl Plan {
     ///
     /// # Errors
     /// - Node is not of a row type.
-    pub fn get_distribution(&self, row_id: usize) -> Result<&Distribution, SbroadError> {
+    pub fn get_distribution(&self, row_id: NodeId) -> Result<&Distribution, SbroadError> {
         match self.get_node(row_id)? {
             Node::Expression(expr) => expr.distribution(),
             Node::Relational(_) => Err(SbroadError::Invalid(
@@ -819,7 +819,7 @@ impl Plan {
     /// # Errors
     /// - Node is not realtional
     /// - Node is not of a row type.
-    pub fn get_rel_distribution(&self, rel_id: usize) -> Result<&Distribution, SbroadError> {
+    pub fn get_rel_distribution(&self, rel_id: NodeId) -> Result<&Distribution, SbroadError> {
         self.get_distribution(self.get_relation_node(rel_id)?.output())
     }
 
@@ -831,7 +831,7 @@ impl Plan {
     /// - row contains broken references
     pub fn get_or_init_distribution(
         &mut self,
-        row_id: usize,
+        row_id: NodeId,
     ) -> Result<&Distribution, SbroadError> {
         if let Err(SbroadError::Invalid(Entity::Distribution, _)) = self.get_distribution(row_id) {
             self.set_distribution(row_id)?;
diff --git a/sbroad-core/src/ir/distribution/tests.rs b/sbroad-core/src/ir/distribution/tests.rs
index 70eee98b4..342a394e7 100644
--- a/sbroad-core/src/ir/distribution/tests.rs
+++ b/sbroad-core/src/ir/distribution/tests.rs
@@ -6,8 +6,6 @@ use crate::ir::tree::traversal::{PostOrder, REL_CAPACITY};
 use crate::ir::{Node, Plan};
 use pretty_assertions::assert_eq;
 use smol_str::SmolStr;
-use std::fs;
-use std::path::Path;
 
 #[test]
 fn proj_preserve_dist_key() {
@@ -33,7 +31,7 @@ fn proj_preserve_dist_key() {
 
     plan.top = Some(proj_id);
 
-    let scan_output: usize = if let Node::Relational(scan) = plan.get_node(scan_id).unwrap() {
+    let scan_output: NodeId = if let Node::Relational(scan) = plan.get_node(scan_id).unwrap() {
         scan.output()
     } else {
         panic!("Invalid plan!");
@@ -47,7 +45,7 @@ fn proj_preserve_dist_key() {
         );
     }
 
-    let proj_output: usize = if let Node::Relational(proj) = plan.get_node(proj_id).unwrap() {
+    let proj_output: NodeId = if let Node::Relational(proj) = plan.get_node(proj_id).unwrap() {
         proj.output()
     } else {
         panic!("Invalid plan!");
@@ -62,101 +60,6 @@ fn proj_preserve_dist_key() {
     }
 }
 
-#[test]
-fn proj_shuffle_dist_key() {
-    // Load a table "t (a, b, c, d)" distributed by ["b", "a"]
-    // with projection ["a", "b"].
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("distribution")
-        .join("shuffle_dist_key.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    let mut plan = Plan::from_yaml(&s).unwrap();
-
-    let scan_output = 8;
-    let proj_output = 14;
-
-    plan.set_distribution(scan_output).unwrap();
-    if let Node::Expression(scan_row) = plan.get_node(scan_output).unwrap() {
-        let keys: HashSet<_, RepeatableState> = collection! { Key::new(vec![1, 0]) };
-        assert_eq!(
-            &Distribution::Segment { keys: keys.into() },
-            scan_row.distribution().unwrap()
-        );
-    }
-
-    plan.set_distribution(proj_output).unwrap();
-    if let Node::Expression(proj_row) = plan.get_node(proj_output).unwrap() {
-        let keys: HashSet<_, RepeatableState> = collection! { Key::new(vec![0, 1]) };
-        assert_eq!(
-            &Distribution::Segment { keys: keys.into() },
-            proj_row.distribution().unwrap()
-        );
-    }
-}
-
-#[test]
-fn proj_shrink_dist_key_1() {
-    // Load a table "t (a, b, c, d)" distributed by ["b", "a"]
-    // with projection ["c", "a"].
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("distribution")
-        .join("shrink_dist_key_1.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    let mut plan = Plan::from_yaml(&s).unwrap();
-
-    let scan_output = 8;
-    let proj_output = 14;
-
-    plan.set_distribution(scan_output).unwrap();
-    let keys: HashSet<_, RepeatableState> = collection! { Key::new(vec![1, 0]) };
-    assert_eq!(
-        &Distribution::Segment { keys: keys.into() },
-        plan.get_distribution(scan_output).unwrap()
-    );
-
-    plan.set_distribution(proj_output).unwrap();
-    assert_eq!(
-        &Distribution::Any,
-        plan.get_distribution(proj_output).unwrap()
-    );
-}
-
-#[test]
-fn proj_shrink_dist_key_2() {
-    // Load a table "t (a, b, c, d)" distributed by ["b", "a"]
-    // with projection ["a"].
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("distribution")
-        .join("shrink_dist_key_2.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    let mut plan = Plan::from_yaml(&s).unwrap();
-
-    let scan_output = 8;
-    let proj_output = 12;
-
-    plan.set_distribution(scan_output).unwrap();
-    let keys: HashSet<_, RepeatableState> = collection! { Key::new(vec![1, 0]) };
-    assert_eq!(
-        &Distribution::Segment { keys: keys.into() },
-        plan.get_distribution(scan_output).unwrap()
-    );
-
-    plan.set_distribution(proj_output).unwrap();
-    assert_eq!(
-        &Distribution::Any,
-        plan.get_distribution(proj_output).unwrap()
-    );
-}
-
 #[test]
 fn projection_any_dist_for_expr() {
     let input = r#"select count("id") FROM "test_space""#;
@@ -182,9 +85,9 @@ vtable_max_rows = 5000
     let local_proj_id = {
         let mut dfs = PostOrder::with_capacity(|x| plan.nodes.rel_iter(x), REL_CAPACITY);
         dfs.iter(plan.top.unwrap())
-            .find(|(_, x)| {
+            .find(|level_node| {
                 matches!(
-                    plan.get_relation_node(*x).unwrap(),
+                    plan.get_relation_node(level_node.1).unwrap(),
                     Relational::Projection { .. }
                 )
             })
@@ -197,156 +100,4 @@ vtable_max_rows = 5000
             .unwrap()
     );
 }
-
-#[test]
-fn union_all_fallback_to_random() {
-    // Load table "t1 (a, b)" distributed by ["a"],
-    // table "t2 (a, b)" distributed by ["b"],
-    // union all (t1, t2)
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("distribution")
-        .join("union_fallback_to_random.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    let mut plan = Plan::from_yaml(&s).unwrap();
-
-    let scan_t1_output = 4;
-    let scan_t2_output = 10;
-    let union_output = 16;
-
-    plan.set_distribution(scan_t1_output).unwrap();
-    let keys: HashSet<_, RepeatableState> = collection! { Key::new(vec![0]) };
-    assert_eq!(
-        &Distribution::Segment { keys: keys.into() },
-        plan.get_distribution(scan_t1_output).unwrap()
-    );
-
-    plan.set_distribution(scan_t2_output).unwrap();
-    let keys: HashSet<_, RepeatableState> = collection! { Key::new(vec![1]) };
-    assert_eq!(
-        &Distribution::Segment { keys: keys.into() },
-        plan.get_distribution(scan_t2_output).unwrap()
-    );
-
-    plan.set_distribution(union_output).unwrap();
-    assert_eq!(
-        &Distribution::Any,
-        plan.get_distribution(union_output).unwrap()
-    );
-}
-
-#[test]
-fn union_preserve_dist() {
-    // Load table "t1 (a, b)" distributed by ["a"],
-    // table "t2 (a, b)" distributed by ["b"],
-    // union all (t1, t2)
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("distribution")
-        .join("union_preserve_dist.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    let mut plan = Plan::from_yaml(&s).unwrap();
-
-    let scan_t1_output = 4;
-    let scan_t2_output = 10;
-    let union_output = 16;
-
-    plan.set_distribution(scan_t1_output).unwrap();
-    if let Node::Expression(scan_row) = plan.get_node(scan_t1_output).unwrap() {
-        let keys: HashSet<_, RepeatableState> = collection! { Key::new(vec![0]) };
-        assert_eq!(
-            &Distribution::Segment { keys: keys.into() },
-            scan_row.distribution().unwrap()
-        );
-    }
-
-    plan.set_distribution(scan_t2_output).unwrap();
-    if let Node::Expression(scan_row) = plan.get_node(scan_t2_output).unwrap() {
-        let keys: HashSet<_, RepeatableState> = collection! { Key::new(vec![0]) };
-        assert_eq!(
-            &Distribution::Segment { keys: keys.into() },
-            scan_row.distribution().unwrap()
-        );
-    }
-
-    plan.set_distribution(union_output).unwrap();
-    if let Node::Expression(scan_row) = plan.get_node(union_output).unwrap() {
-        let keys: HashSet<_, RepeatableState> = collection! { Key::new(vec![0]) };
-        assert_eq!(
-            &Distribution::Segment { keys: keys.into() },
-            scan_row.distribution().unwrap()
-        );
-    }
-}
-
-#[test]
-fn join_unite_keys() {
-    // Load table "t1 (a, b)" distributed by ["a"],
-    // table "t2 (c, d)" distributed by ["d"],
-    // select * from t1 join t2 on t1.a = t2.d
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("distribution")
-        .join("join_unite_keys.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    let mut plan = Plan::from_yaml(&s).unwrap();
-    let scan_t1_output = 4;
-    let scan_t2_output = 10;
-    let join_output = 27;
-    let t1_a = 14;
-    let t2_d = 17;
-
-    plan.set_distribution(scan_t1_output).unwrap();
-    if let Node::Expression(scan_row) = plan.get_node(scan_t1_output).unwrap() {
-        let keys: HashSet<_, RepeatableState> = collection! { Key::new(vec![0]) };
-        assert_eq!(
-            &Distribution::Segment { keys: keys.into() },
-            scan_row.distribution().unwrap()
-        );
-    }
-
-    plan.set_distribution(scan_t2_output).unwrap();
-    if let Node::Expression(scan_row) = plan.get_node(scan_t2_output).unwrap() {
-        let keys: HashSet<_, RepeatableState> = collection! { Key::new(vec![1]) };
-        assert_eq!(
-            &Distribution::Segment { keys: keys.into() },
-            scan_row.distribution().unwrap()
-        );
-    }
-
-    plan.set_distribution(join_output).unwrap();
-    if let Node::Expression(scan_row) = plan.get_node(join_output).unwrap() {
-        let keys: HashSet<_, RepeatableState> =
-            collection! { Key::new(vec![0]), Key::new(vec![3]) };
-        assert_eq!(
-            &Distribution::Segment { keys: keys.into() },
-            scan_row.distribution().unwrap()
-        );
-    }
-
-    plan.set_distribution(t1_a).unwrap();
-    if let Node::Expression(scan_row) = plan.get_node(t1_a).unwrap() {
-        let keys: HashSet<_, RepeatableState> = collection! { Key::new(vec![0]) };
-        assert_eq!(
-            &Distribution::Segment { keys: keys.into() },
-            scan_row.distribution().unwrap()
-        );
-    }
-
-    plan.set_distribution(t2_d).unwrap();
-    if let Node::Expression(scan_row) = plan.get_node(t2_d).unwrap() {
-        let keys: HashSet<_, RepeatableState> = collection! { Key::new(vec![0]) };
-        assert_eq!(
-            &Distribution::Segment { keys: keys.into() },
-            scan_row.distribution().unwrap()
-        );
-    }
-}
-
 //TODO: add other distribution variants to the test cases.
diff --git a/sbroad-core/src/ir/explain.rs b/sbroad-core/src/ir/explain.rs
index 53c1ddffe..60c010450 100644
--- a/sbroad-core/src/ir/explain.rs
+++ b/sbroad-core/src/ir/explain.rs
@@ -19,7 +19,7 @@ use crate::ir::transformation::redistribution::{
 };
 use crate::ir::{OptionKind, Plan};
 
-use super::expression::FunctionFeature;
+use super::expression::{FunctionFeature, NodeId};
 use super::operator::{Arithmetic, Bool, Unary};
 use super::tree::traversal::{PostOrder, EXPR_CAPACITY, REL_CAPACITY};
 use super::value::Value;
@@ -123,14 +123,15 @@ impl ColExpr {
     #[allow(dead_code, clippy::too_many_lines)]
     fn new(
         plan: &Plan,
-        subtree_top: usize,
+        subtree_top: NodeId,
         sq_ref_map: &SubQueryRefMap,
     ) -> Result<Self, SbroadError> {
-        let mut stack: Vec<(ColExpr, usize)> = Vec::new();
+        let mut stack: Vec<(ColExpr, NodeId)> = Vec::new();
         let mut dft_post =
             PostOrder::with_capacity(|node| plan.nodes.expr_iter(node, false), EXPR_CAPACITY);
 
-        for (_, id) in dft_post.iter(subtree_top) {
+        for level_node in dft_post.iter(subtree_top) {
+            let id = level_node.1;
             let current_node = plan.get_expression_node(id)?;
 
             match &current_node {
@@ -192,7 +193,7 @@ impl ColExpr {
                 Expression::Reference { position, .. } => {
                     let mut col_name = String::new();
 
-                    let rel_id: usize = *plan.get_relational_from_reference_node(id)?;
+                    let rel_id = *plan.get_relational_from_reference_node(id)?;
                     let rel_node = plan.get_relation_node(rel_id)?;
 
                     if let Some(name) = rel_node.scan_name(plan, *position)? {
@@ -268,7 +269,7 @@ impl ColExpr {
                 }
                 Expression::Row { list, .. } => {
                     let mut len = list.len();
-                    let mut row: Vec<(ColExpr, usize)> = Vec::with_capacity(len);
+                    let mut row: Vec<(ColExpr, NodeId)> = Vec::with_capacity(len);
                     while len > 0 {
                         let expr = stack.pop().ok_or_else(|| {
                             SbroadError::UnexpectedNumberOfValues(
@@ -361,7 +362,7 @@ impl ColExpr {
 /// Alias for map of (`SubQuery` id -> it's offset).
 /// Offset = `SubQuery` index (e.g. in case there are several `SubQueries` in Selection WHERE condition
 /// index will indicate to which of them Reference is pointing).
-type SubQueryRefMap = HashMap<usize, usize>;
+type SubQueryRefMap = HashMap<NodeId, usize>;
 
 #[derive(Debug, PartialEq, Serialize)]
 struct Projection {
@@ -373,7 +374,7 @@ impl Projection {
     #[allow(dead_code)]
     fn new(
         plan: &Plan,
-        output_id: usize,
+        output_id: NodeId,
         sq_ref_map: &SubQueryRefMap,
     ) -> Result<Self, SbroadError> {
         let mut result = Projection { cols: vec![] };
@@ -416,8 +417,8 @@ impl GroupBy {
     #[allow(dead_code)]
     fn new(
         plan: &Plan,
-        gr_cols: &Vec<usize>,
-        output_id: usize,
+        gr_cols: &Vec<NodeId>,
+        output_id: NodeId,
         sq_ref_map: &SubQueryRefMap,
     ) -> Result<Self, SbroadError> {
         let mut result = GroupBy {
@@ -547,7 +548,7 @@ struct Update {
 
 impl Update {
     #[allow(dead_code)]
-    fn new(plan: &Plan, update_id: usize) -> Result<Self, SbroadError> {
+    fn new(plan: &Plan, update_id: NodeId) -> Result<Self, SbroadError> {
         if let Relational::Update {
             relation: ref rel,
             update_columns_map,
@@ -609,7 +610,7 @@ impl Update {
         Err(SbroadError::Invalid(
             Entity::Node,
             Some(format_smolstr!(
-                "explain: expected Update node on id: {update_id}"
+                "explain: expected Update node on id: {update_id:?}"
             )),
         ))
     }
@@ -714,7 +715,7 @@ impl Row {
 
     fn from_col_exprs_with_ids(
         plan: &Plan,
-        exprs_with_ids: &mut Vec<(ColExpr, usize)>,
+        exprs_with_ids: &mut Vec<(ColExpr, NodeId)>,
         sq_ref_map: &SubQueryRefMap,
     ) -> Result<Self, SbroadError> {
         let mut row = Row::new();
@@ -724,7 +725,7 @@ impl Row {
 
             match &current_node {
                 Expression::Reference { .. } => {
-                    let rel_id: usize = *plan.get_relational_from_reference_node(expr_id)?;
+                    let rel_id = *plan.get_relational_from_reference_node(expr_id)?;
 
                     let rel_node = plan.get_relation_node(rel_id)?;
                     if plan.is_additional_child(rel_id)? {
@@ -734,7 +735,7 @@ impl Row {
                             let sq_offset = sq_ref_map.get(&rel_id).ok_or_else(|| {
                                 SbroadError::NotFound(
                                     Entity::SubQuery,
-                                    format_smolstr!("with index {rel_id} in the map"),
+                                    format_smolstr!("with index {rel_id:?} in the map"),
                                 )
                             })?;
                             row.add_col(RowVal::SqRef(Ref::new(*sq_offset)));
@@ -742,7 +743,7 @@ impl Row {
                             return Err(SbroadError::Invalid(
                                 Entity::Plan,
                                 Some(format_smolstr!(
-                                    "additional child ({rel_id}) is not SQ or Motion: {rel_node:?}"
+                                    "additional child ({rel_id:?}) is not SQ or Motion: {rel_node:?}"
                                 )),
                             ));
                         }
@@ -1030,7 +1031,7 @@ impl Display for FullExplain {
 impl FullExplain {
     #[allow(dead_code)]
     #[allow(clippy::too_many_lines)]
-    pub fn new(ir: &Plan, top_id: usize) -> Result<Self, SbroadError> {
+    pub fn new(ir: &Plan, top_id: NodeId) -> Result<Self, SbroadError> {
         let mut stack: Vec<ExplainTreePart> = Vec::with_capacity(ir.nodes.relation_node_amount());
         let mut result = FullExplain::default();
         result
@@ -1042,7 +1043,9 @@ impl FullExplain {
         ));
 
         let mut dft_post = PostOrder::with_capacity(|node| ir.nodes.rel_iter(node), REL_CAPACITY);
-        for (level, id) in dft_post.iter(top_id) {
+        for level_node in dft_post.iter(top_id) {
+            let level = level_node.0;
+            let id = level_node.1;
             let mut current_node = ExplainTreePart::with_level(level);
             let node = ir.get_relation_node(id)?;
             current_node.current = match &node {
diff --git a/sbroad-core/src/ir/expression.rs b/sbroad-core/src/ir/expression.rs
index e9b9eeefe..df68c1940 100644
--- a/sbroad-core/src/ir/expression.rs
+++ b/sbroad-core/src/ir/expression.rs
@@ -10,6 +10,7 @@ use ahash::RandomState;
 use serde::{Deserialize, Serialize};
 use smol_str::{format_smolstr, SmolStr};
 use std::collections::{BTreeMap, HashSet};
+use std::fmt::{Display, Formatter};
 use std::hash::{Hash, Hasher};
 use std::ops::Bound::Included;
 
@@ -23,13 +24,34 @@ use crate::ir::Positions as Targets;
 use super::distribution::Distribution;
 use super::tree::traversal::{PostOrderWithFilter, EXPR_CAPACITY};
 use super::value::Value;
-use super::{operator, Node, Nodes, Plan};
+use super::{operator, ArenaType, Node, Nodes, Plan};
 
 pub mod cast;
 pub mod concat;
 pub mod types;
 
-pub(crate) type ExpressionId = usize;
+pub(crate) type ExpressionId = NodeId;
+
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, Hash, Copy)]
+pub struct NodeId {
+    pub offset: u32,
+    pub arena_type: ArenaType,
+}
+
+impl Display for NodeId {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}{}", self.offset, self.arena_type)
+    }
+}
+
+impl Default for NodeId {
+    fn default() -> Self {
+        NodeId {
+            offset: 0,
+            arena_type: ArenaType::Default,
+        }
+    }
+}
 
 /// Tuple tree build blocks.
 ///
@@ -51,18 +73,18 @@ pub enum Expression {
         /// Alias name.
         name: SmolStr,
         /// Child expression node index in the plan node arena.
-        child: usize,
+        child: NodeId,
     },
     /// Binary expression returning boolean result.
     ///
     /// Example: `a > 42`, `b in (select c from ...)`.
     Bool {
         /// Left branch expression node index in the plan node arena.
-        left: usize,
+        left: NodeId,
         /// Boolean operator.
         op: operator::Bool,
         /// Right branch expression node index in the plan node arena.
-        right: usize,
+        right: NodeId,
     },
     /// Binary expression returning row result.
     ///
@@ -71,18 +93,18 @@ pub enum Expression {
     /// TODO: always cover children with parentheses (in to_sql).
     Arithmetic {
         /// Left branch expression node index in the plan node arena.
-        left: usize,
+        left: NodeId,
         /// Arithmetic operator.
         op: operator::Arithmetic,
         /// Right branch expression node index in the plan node arena.
-        right: usize,
+        right: NodeId,
     },
     /// Type cast expression.
     ///
     /// Example: `cast(a as text)`.
     Cast {
         /// Target expression that must be casted to another type.
-        child: usize,
+        child: NodeId,
         /// Cast type.
         to: cast::Type,
     },
@@ -91,9 +113,9 @@ pub enum Expression {
     /// Example: `a || 'hello'`.
     Concat {
         /// Left expression node id.
-        left: usize,
+        left: NodeId,
         /// Right expression node id.
-        right: usize,
+        right: NodeId,
     },
     /// Constant expressions.
     ///
@@ -109,7 +131,7 @@ pub enum Expression {
     /// - column position in the child(ren) output tuple
     Reference {
         /// Relational node ID that contains current reference.
-        parent: Option<usize>,
+        parent: Option<NodeId>,
         /// Targets in the relational node children list.
         /// - Leaf nodes (relation scans): None.
         /// - Union nodes: two elements (left and right).
@@ -130,7 +152,7 @@ pub enum Expression {
     ///  Example: (a, b, 1).
     Row {
         /// A list of the alias expression node indexes in the plan node arena.
-        list: Vec<usize>,
+        list: Vec<NodeId>,
         /// Resulting data distribution of the tuple. Should be filled as a part
         /// of the last "add Motion" transformation.
         distribution: Option<Distribution>,
@@ -146,7 +168,7 @@ pub enum Expression {
         /// Function name.
         name: SmolStr,
         /// Function arguments.
-        children: Vec<usize>,
+        children: Vec<NodeId>,
         /// Optional function feature.
         feature: Option<FunctionFeature>,
         /// Function return type.
@@ -162,26 +184,26 @@ pub enum Expression {
         /// Trim kind.
         kind: Option<TrimKind>,
         /// Trim string pattern to remove (it can be an expression).
-        pattern: Option<usize>,
+        pattern: Option<NodeId>,
         /// Target expression to trim.
-        target: usize,
+        target: NodeId,
     },
     /// Unary expression returning boolean result.
     Unary {
         /// Unary operator.
         op: operator::Unary,
         /// Child expression node index in the plan node arena.
-        child: usize,
+        child: NodeId,
     },
     /// Argument of `count` aggregate in `count(*)` expression
     CountAsterisk,
     ExprInParentheses {
-        child: usize,
+        child: NodeId,
     },
     Case {
-        search_expr: Option<usize>,
-        when_blocks: Vec<(usize, usize)>,
-        else_expr: Option<usize>,
+        search_expr: Option<NodeId>,
+        when_blocks: Vec<(NodeId, NodeId)>,
+        else_expr: Option<NodeId>,
     },
 }
 
@@ -236,7 +258,7 @@ impl Expression {
     ///
     /// # Errors
     /// - node isn't `Row`
-    pub fn clone_row_list(&self) -> Result<Vec<usize>, SbroadError> {
+    pub fn clone_row_list(&self) -> Result<Vec<NodeId>, SbroadError> {
         match self {
             Expression::Row { list, .. } => Ok(list.clone()),
             _ => Err(SbroadError::Invalid(
@@ -264,7 +286,7 @@ impl Expression {
     ///
     /// # Errors
     /// - node isn't `Row`
-    pub fn get_row_list(&self) -> Result<&[usize], SbroadError> {
+    pub fn get_row_list(&self) -> Result<&[NodeId], SbroadError> {
         match self {
             Expression::Row { ref list, .. } => Ok(list),
             _ => Err(SbroadError::Invalid(
@@ -278,7 +300,7 @@ impl Expression {
     ///
     /// # Errors
     /// - node isn't `Row`
-    pub fn get_mut_row_list(&mut self) -> Result<&mut Vec<usize>, SbroadError> {
+    pub fn get_mut_row_list(&mut self) -> Result<&mut Vec<NodeId>, SbroadError> {
         match self {
             Expression::Row { ref mut list, .. } => Ok(list),
             _ => Err(SbroadError::Invalid(
@@ -292,7 +314,7 @@ impl Expression {
     ///
     /// # Errors
     /// - node isn't `Row`
-    pub fn get_row_list_mut(&mut self) -> Result<&mut Vec<usize>, SbroadError> {
+    pub fn get_row_list_mut(&mut self) -> Result<&mut Vec<NodeId>, SbroadError> {
         match self {
             Expression::Row { ref mut list, .. } => Ok(list),
             _ => Err(SbroadError::Invalid(
@@ -330,7 +352,7 @@ impl Expression {
     /// # Errors
     /// - node isn't reference type
     /// - reference doesn't have a parent
-    pub fn get_parent(&self) -> Result<usize, SbroadError> {
+    pub fn get_parent(&self) -> Result<NodeId, SbroadError> {
         if let Expression::Reference { parent, .. } = self {
             return parent.ok_or_else(|| {
                 SbroadError::Invalid(Entity::Expression, Some("Reference has no parent".into()))
@@ -353,7 +375,7 @@ impl Expression {
     }
 
     /// Replaces parent in the reference node with the new one.
-    pub fn replace_parent_in_reference(&mut self, from_id: Option<usize>, to_id: Option<usize>) {
+    pub fn replace_parent_in_reference(&mut self, from_id: Option<NodeId>, to_id: Option<NodeId>) {
         if let Expression::Reference { parent, .. } = self {
             if *parent == from_id {
                 *parent = to_id;
@@ -374,7 +396,7 @@ impl Nodes {
     ///
     /// # Errors
     /// - child node is invalid
-    pub(crate) fn add_covered_with_parentheses(&mut self, child: usize) -> usize {
+    pub(crate) fn add_covered_with_parentheses(&mut self, child: NodeId) -> NodeId {
         let covered_with_parentheses = Expression::ExprInParentheses { child };
         self.push(Node::Expression(covered_with_parentheses))
     }
@@ -384,7 +406,7 @@ impl Nodes {
     /// # Errors
     /// - child node is invalid
     /// - name is empty
-    pub fn add_alias(&mut self, name: &str, child: usize) -> Result<usize, SbroadError> {
+    pub fn add_alias(&mut self, name: &str, child: NodeId) -> Result<NodeId, SbroadError> {
         let alias = Expression::Alias {
             name: SmolStr::from(name),
             child,
@@ -398,17 +420,17 @@ impl Nodes {
     /// - when left or right nodes are invalid
     pub fn add_bool(
         &mut self,
-        left: usize,
+        left: NodeId,
         op: operator::Bool,
-        right: usize,
-    ) -> Result<usize, SbroadError> {
-        self.arena.get(left).ok_or_else(|| {
+        right: NodeId,
+    ) -> Result<NodeId, SbroadError> {
+        self.get(left).ok_or_else(|| {
             SbroadError::NotFound(
                 Entity::Node,
                 format_smolstr!("(left child of boolean node) from arena with index {left}"),
             )
         })?;
-        self.arena.get(right).ok_or_else(|| {
+        self.get(right).ok_or_else(|| {
             SbroadError::NotFound(
                 Entity::Node,
                 format_smolstr!("(right child of boolean node) from arena with index {right}"),
@@ -423,20 +445,20 @@ impl Nodes {
     /// - when left or right nodes are invalid
     pub fn add_arithmetic_node(
         &mut self,
-        left: usize,
+        left: NodeId,
         op: operator::Arithmetic,
-        right: usize,
-    ) -> Result<usize, SbroadError> {
-        self.arena.get(left).ok_or_else(|| {
+        right: NodeId,
+    ) -> Result<NodeId, SbroadError> {
+        self.get(left).ok_or_else(|| {
             SbroadError::NotFound(
                 Entity::Node,
-                format_smolstr!("(left child of Arithmetic node) from arena with index {left}"),
+                format_smolstr!("(left child of Arithmetic node) from arena with index {left:?}"),
             )
         })?;
-        self.arena.get(right).ok_or_else(|| {
+        self.get(right).ok_or_else(|| {
             SbroadError::NotFound(
                 Entity::Node,
-                format_smolstr!("(right child of Arithmetic node) from arena with index {right}"),
+                format_smolstr!("(right child of Arithmetic node) from arena with index {right:?}"),
             )
         })?;
         Ok(self.push(Node::Expression(Expression::Arithmetic { left, op, right })))
@@ -445,11 +467,11 @@ impl Nodes {
     /// Adds reference node.
     pub fn add_ref(
         &mut self,
-        parent: Option<usize>,
+        parent: Option<NodeId>,
         targets: Option<Vec<usize>>,
         position: usize,
         col_type: Type,
-    ) -> usize {
+    ) -> NodeId {
         let r = Expression::Reference {
             parent,
             targets,
@@ -460,7 +482,7 @@ impl Nodes {
     }
 
     /// Adds row node.
-    pub fn add_row(&mut self, list: Vec<usize>, distribution: Option<Distribution>) -> usize {
+    pub fn add_row(&mut self, list: Vec<NodeId>, distribution: Option<Distribution>) -> NodeId {
         self.push(Node::Expression(Expression::Row { list, distribution }))
     }
 
@@ -471,9 +493,9 @@ impl Nodes {
     pub fn add_unary_bool(
         &mut self,
         op: operator::Unary,
-        child: usize,
-    ) -> Result<usize, SbroadError> {
-        self.arena.get(child).ok_or_else(|| {
+        child: NodeId,
+    ) -> Result<NodeId, SbroadError> {
+        self.get(child).ok_or_else(|| {
             SbroadError::NotFound(
                 Entity::Node,
                 format_smolstr!("from arena with index {child}"),
@@ -487,13 +509,13 @@ impl Nodes {
 // plan for PlanExpression, try to put it into hasher? but what do
 // with equality?
 pub struct PlanExpr<'plan> {
-    pub id: usize,
+    pub id: NodeId,
     pub plan: &'plan Plan,
 }
 
 impl<'plan> PlanExpr<'plan> {
     #[must_use]
-    pub fn new(id: usize, plan: &'plan Plan) -> Self {
+    pub fn new(id: NodeId, plan: &'plan Plan) -> Self {
         PlanExpr { id, plan }
     }
 }
@@ -564,7 +586,7 @@ impl<'plan> Comparator<'plan> {
     /// - invalid [`Expression::Reference`]s in either of subtrees
     /// - invalid children in some expression
     #[allow(clippy::too_many_lines)]
-    pub fn are_subtrees_equal(&self, lhs: usize, rhs: usize) -> Result<bool, SbroadError> {
+    pub fn are_subtrees_equal(&self, lhs: NodeId, rhs: NodeId) -> Result<bool, SbroadError> {
         let l = self.plan.get_node(lhs)?;
         let r = self.plan.get_node(rhs)?;
         if let Node::Expression(left) = l {
@@ -776,7 +798,7 @@ impl<'plan> Comparator<'plan> {
         Ok(false)
     }
 
-    pub fn hash_for_child_expr(&mut self, child: usize, depth: usize) {
+    pub fn hash_for_child_expr(&mut self, child: NodeId, depth: usize) {
         self.hash_for_expr(child, depth - 1);
     }
 
@@ -786,7 +808,7 @@ impl<'plan> Comparator<'plan> {
     /// # Panics
     /// - Comparator hasher wasn't set.
     #[allow(clippy::too_many_lines)]
-    pub fn hash_for_expr(&mut self, top: usize, depth: usize) {
+    pub fn hash_for_expr(&mut self, top: NodeId, depth: usize) {
         if depth == 0 {
             return;
         }
@@ -948,7 +970,7 @@ pub(crate) struct ColumnPositionMap {
 }
 
 impl ColumnPositionMap {
-    pub(crate) fn new(plan: &Plan, rel_id: usize) -> Result<Self, SbroadError> {
+    pub(crate) fn new(plan: &Plan, rel_id: NodeId) -> Result<Self, SbroadError> {
         let rel_node = plan.get_relation_node(rel_id)?;
         let output = plan.get_expression_node(rel_node.output())?;
         let alias_ids = output.get_row_list()?;
@@ -1110,18 +1132,18 @@ pub enum JoinTargets<'targets> {
 #[derive(Debug)]
 pub enum NewColumnsSource<'targets> {
     Join {
-        outer_child: usize,
-        inner_child: usize,
+        outer_child: NodeId,
+        inner_child: NodeId,
         targets: JoinTargets<'targets>,
     },
     /// Enum variant used both for Except and UnionAll operators.
     ExceptUnion {
-        left_child: usize,
-        right_child: usize,
+        left_child: NodeId,
+        right_child: NodeId,
     },
     /// Other relational nodes.
     Other {
-        child: usize,
+        child: NodeId,
         columns_spec: Option<ColumnsRetrievalSpec<'targets>>,
     },
 }
@@ -1134,9 +1156,9 @@ pub struct NewColumnSourceIterator<'iter> {
 
 impl<'targets> Iterator for NewColumnSourceIterator<'targets> {
     // Pair of (relational node id, target id)
-    type Item = (usize, usize);
+    type Item = (NodeId, usize);
 
-    fn next(&mut self) -> Option<(usize, usize)> {
+    fn next(&mut self) -> Option<(NodeId, usize)> {
         let result = match &self.source {
             NewColumnsSource::Join {
                 outer_child,
@@ -1178,7 +1200,7 @@ impl<'targets> Iterator for NewColumnSourceIterator<'targets> {
 }
 
 impl<'iter, 'source: 'iter> IntoIterator for &'source NewColumnsSource<'iter> {
-    type Item = (usize, usize);
+    type Item = (NodeId, usize);
     type IntoIter = NewColumnSourceIterator<'iter>;
 
     fn into_iter(self) -> Self::IntoIter {
@@ -1226,7 +1248,7 @@ impl<'source> NewColumnsSource<'source> {
 
 impl Plan {
     /// Add `Row` to plan.
-    pub fn add_row(&mut self, list: Vec<usize>, distribution: Option<Distribution>) -> usize {
+    pub fn add_row(&mut self, list: Vec<NodeId>, distribution: Option<Distribution>) -> NodeId {
         self.nodes.add_row(list, distribution)
     }
 
@@ -1248,9 +1270,9 @@ impl Plan {
         source: &NewColumnsSource,
         need_aliases: bool,
         need_sharding_column: bool,
-    ) -> Result<Vec<usize>, SbroadError> {
+    ) -> Result<Vec<NodeId>, SbroadError> {
         // Vec of (column position in child output, column plan id, new_targets).
-        let mut filtered_children_row_list: Vec<(usize, usize, Vec<usize>)> = Vec::new();
+        let mut filtered_children_row_list: Vec<(usize, NodeId, Vec<usize>)> = Vec::new();
 
         // Helper lambda to retrieve column positions we need to exclude from child `rel_id`.
         let column_positions_to_exclude = |rel_id| -> Result<Targets, SbroadError> {
@@ -1332,7 +1354,7 @@ impl Plan {
         };
 
         // List of columns to be passed into `Expression::Row`.
-        let mut result_row_list: Vec<usize> = Vec::with_capacity(filtered_children_row_list.len());
+        let mut result_row_list: Vec<NodeId> = Vec::with_capacity(filtered_children_row_list.len());
         for (pos, alias_node_id, new_targets) in filtered_children_row_list {
             let alias_expr = self.get_expression_node(alias_node_id)?;
             let alias_name = SmolStr::from(alias_expr.get_alias_name()?);
@@ -1358,10 +1380,10 @@ impl Plan {
     /// - child is an inconsistent relational node
     pub fn add_row_by_indices(
         &mut self,
-        rel_node: usize,
+        rel_node: NodeId,
         indices: Vec<usize>,
         need_sharding_column: bool,
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         let list = self.new_columns(
             &NewColumnsSource::Other {
                 child: rel_node,
@@ -1382,10 +1404,10 @@ impl Plan {
     /// - column names don't exist
     pub fn add_row_for_output(
         &mut self,
-        rel_node: usize,
+        rel_node: NodeId,
         col_names: &[&str],
         need_sharding_column: bool,
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         let specific_columns = if col_names.is_empty() {
             None
         } else {
@@ -1414,9 +1436,9 @@ impl Plan {
     /// - children are inconsistent relational nodes
     pub fn add_row_for_union_except(
         &mut self,
-        left: usize,
-        right: usize,
-    ) -> Result<usize, SbroadError> {
+        left: NodeId,
+        right: NodeId,
+    ) -> Result<NodeId, SbroadError> {
         let list = self.new_columns(
             &NewColumnsSource::ExceptUnion {
                 left_child: left,
@@ -1435,7 +1457,7 @@ impl Plan {
     /// # Errors
     /// Returns `SbroadError`:
     /// - children are inconsistent relational nodes
-    pub fn add_row_for_join(&mut self, left: usize, right: usize) -> Result<usize, SbroadError> {
+    pub fn add_row_for_join(&mut self, left: NodeId, right: NodeId) -> Result<NodeId, SbroadError> {
         let list = self.new_columns(
             &NewColumnsSource::Join {
                 outer_child: left,
@@ -1458,9 +1480,9 @@ impl Plan {
     /// - column names don't exist
     pub fn add_row_from_child(
         &mut self,
-        child: usize,
+        child: NodeId,
         col_names: &[&str],
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         let specific_columns = if col_names.is_empty() {
             None
         } else {
@@ -1490,10 +1512,10 @@ impl Plan {
     /// - children nodes are inconsistent with the target position
     pub(crate) fn add_row_from_subquery(
         &mut self,
-        children: &[usize],
+        children: &[NodeId],
         target: usize,
-        parent: Option<usize>,
-    ) -> Result<usize, SbroadError> {
+        parent: Option<NodeId>,
+    ) -> Result<NodeId, SbroadError> {
         let sq_id = *children.get(target).ok_or_else(|| {
             SbroadError::UnexpectedNumberOfValues(format_smolstr!(
                 "invalid target index: {target} (children: {children:?})",
@@ -1528,10 +1550,10 @@ impl Plan {
     /// - column names don't exist
     pub fn add_row_from_left_branch(
         &mut self,
-        left: usize,
-        right: usize,
+        left: NodeId,
+        right: NodeId,
         col_names: &[ColumnWithScan],
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         let list = self.new_columns(
             &NewColumnsSource::Join {
                 outer_child: left,
@@ -1556,10 +1578,10 @@ impl Plan {
     /// - column names don't exist
     pub fn add_row_from_right_branch(
         &mut self,
-        left: usize,
-        right: usize,
+        left: NodeId,
+        right: NodeId,
         col_names: &[ColumnWithScan],
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         let list = self.new_columns(
             &NewColumnsSource::Join {
                 outer_child: left,
@@ -1581,7 +1603,10 @@ impl Plan {
     ///
     /// # Errors
     /// - reference is invalid
-    pub fn get_relational_from_reference_node(&self, ref_id: usize) -> Result<&usize, SbroadError> {
+    pub fn get_relational_from_reference_node(
+        &self,
+        ref_id: NodeId,
+    ) -> Result<&NodeId, SbroadError> {
         if let Node::Expression(Expression::Reference {
             targets, parent, ..
         }) = self.get_node(ref_id)?
@@ -1589,7 +1614,7 @@ impl Plan {
             let Some(referred_rel_id) = parent else {
                 return Err(SbroadError::NotFound(
                     Entity::Node,
-                    format_smolstr!("that is Reference ({ref_id}) parent"),
+                    format_smolstr!("that is Reference ({ref_id:?}) parent"),
                 ));
             };
             let rel = self.get_relation_node(*referred_rel_id)?;
@@ -1639,8 +1664,8 @@ impl Plan {
     /// - `relational_map` is not initialized
     pub fn get_relational_nodes_from_row(
         &self,
-        row_id: usize,
-    ) -> Result<HashSet<usize, RandomState>, SbroadError> {
+        row_id: NodeId,
+    ) -> Result<HashSet<NodeId, RandomState>, SbroadError> {
         let row = self.get_expression_node(row_id)?;
         let capacity = if let Expression::Row { list, .. } = row {
             list.len()
@@ -1650,7 +1675,7 @@ impl Plan {
                 Some("Node is not a row".into()),
             ));
         };
-        let filter = |node_id: usize| -> bool {
+        let filter = |node_id: NodeId| -> bool {
             if let Ok(Node::Expression(Expression::Reference { .. })) = self.get_node(node_id) {
                 return true;
             }
@@ -1664,9 +1689,10 @@ impl Plan {
         post_tree.populate_nodes(row_id);
         let nodes = post_tree.take_nodes();
         // We don't expect much relational references in a row (5 is a reasonable number).
-        let mut rel_nodes: HashSet<usize, RandomState> =
+        let mut rel_nodes: HashSet<NodeId, RandomState> =
             HashSet::with_capacity_and_hasher(5, RandomState::new());
-        for (_, id) in nodes {
+        for level_node in nodes {
+            let id = level_node.1;
             let reference = self.get_expression_node(id)?;
             if let Expression::Reference {
                 targets, parent, ..
@@ -1675,7 +1701,7 @@ impl Plan {
                 let referred_rel_id = parent.ok_or_else(|| {
                     SbroadError::NotFound(
                         Entity::Node,
-                        format_smolstr!("that is Reference ({id}) parent"),
+                        format_smolstr!("that is Reference ({id:?}) parent"),
                     )
                 })?;
                 let rel = self.get_relation_node(referred_rel_id)?;
@@ -1694,7 +1720,7 @@ impl Plan {
 
     /// Check that the node is a boolean equality and its children are both rows.
     #[must_use]
-    pub fn is_bool_eq_with_rows(&self, node_id: usize) -> bool {
+    pub fn is_bool_eq_with_rows(&self, node_id: NodeId) -> bool {
         let Ok(node) = self.get_expression_node(node_id) else {
             return false;
         };
@@ -1723,7 +1749,7 @@ impl Plan {
     ///
     /// # Errors
     /// - If node is not an expression.
-    pub fn is_trivalent(&self, expr_id: usize) -> Result<bool, SbroadError> {
+    pub fn is_trivalent(&self, expr_id: NodeId) -> Result<bool, SbroadError> {
         let expr = self.get_expression_node(expr_id)?;
         match expr {
             Expression::Bool { .. }
@@ -1748,7 +1774,7 @@ impl Plan {
     ///
     /// # Errors
     /// - If node is not an expression.
-    pub fn is_ref(&self, expr_id: usize) -> Result<bool, SbroadError> {
+    pub fn is_ref(&self, expr_id: NodeId) -> Result<bool, SbroadError> {
         let expr = self.get_expression_node(expr_id)?;
         match expr {
             Expression::Reference { .. } => return Ok(true),
@@ -1771,7 +1797,7 @@ impl Plan {
     #[allow(dead_code)]
     pub fn get_child_const_from_row(
         &self,
-        row_id: usize,
+        row_id: NodeId,
         child_num: usize,
     ) -> Result<Value, SbroadError> {
         let node = self.get_expression_node(row_id)?;
@@ -1797,11 +1823,11 @@ impl Plan {
     /// - node is not an expression
     pub fn replace_parent_in_subtree(
         &mut self,
-        node_id: usize,
-        from_id: Option<usize>,
-        to_id: Option<usize>,
+        node_id: NodeId,
+        from_id: Option<NodeId>,
+        to_id: Option<NodeId>,
     ) -> Result<(), SbroadError> {
-        let filter = |node_id: usize| -> bool {
+        let filter = |node_id: NodeId| -> bool {
             if let Ok(Node::Expression(Expression::Reference { .. })) = self.get_node(node_id) {
                 return true;
             }
@@ -1815,7 +1841,8 @@ impl Plan {
         subtree.populate_nodes(node_id);
         let references = subtree.take_nodes();
         drop(subtree);
-        for (_, id) in references {
+        for level_node in references {
+            let id = level_node.1;
             let node = self.get_mut_expression_node(id)?;
             node.replace_parent_in_reference(from_id, to_id);
         }
@@ -1827,8 +1854,8 @@ impl Plan {
     /// # Errors
     /// - node is invalid
     /// - node is not an expression
-    pub fn flush_parent_in_subtree(&mut self, node_id: usize) -> Result<(), SbroadError> {
-        let filter = |node_id: usize| -> bool {
+    pub fn flush_parent_in_subtree(&mut self, node_id: NodeId) -> Result<(), SbroadError> {
+        let filter = |node_id: NodeId| -> bool {
             if let Ok(Node::Expression(Expression::Reference { .. })) = self.get_node(node_id) {
                 return true;
             }
@@ -1842,7 +1869,8 @@ impl Plan {
         subtree.populate_nodes(node_id);
         let references = subtree.take_nodes();
         drop(subtree);
-        for (_, id) in references {
+        for level_node in references {
+            let id = level_node.1;
             let node = self.get_mut_expression_node(id)?;
             node.flush_parent_in_reference();
         }
diff --git a/sbroad-core/src/ir/expression/cast.rs b/sbroad-core/src/ir/expression/cast.rs
index 4005fad34..d4a07d55b 100644
--- a/sbroad-core/src/ir/expression/cast.rs
+++ b/sbroad-core/src/ir/expression/cast.rs
@@ -8,6 +8,8 @@ use crate::ir::{Node, Plan};
 use serde::{Deserialize, Serialize};
 use smol_str::{format_smolstr, SmolStr, ToSmolStr};
 
+use super::NodeId;
+
 #[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
 pub enum Type {
     Any,
@@ -130,7 +132,7 @@ impl Plan {
     ///
     /// # Errors
     /// - Child node is not of the expression type.
-    pub fn add_cast(&mut self, expr_id: usize, to_type: Type) -> Result<usize, SbroadError> {
+    pub fn add_cast(&mut self, expr_id: NodeId, to_type: Type) -> Result<NodeId, SbroadError> {
         let cast_expr = Expression::Cast {
             child: expr_id,
             to: to_type,
diff --git a/sbroad-core/src/ir/expression/concat.rs b/sbroad-core/src/ir/expression/concat.rs
index 021ee508c..09e637988 100644
--- a/sbroad-core/src/ir/expression/concat.rs
+++ b/sbroad-core/src/ir/expression/concat.rs
@@ -2,12 +2,14 @@ use crate::errors::SbroadError;
 use crate::ir::expression::Expression;
 use crate::ir::{Node, Plan};
 
+use super::NodeId;
+
 impl Plan {
     /// Add concatenation expression to the IR plan.
     ///
     /// # Errors
     /// - Left or right child nodes are not of the expression type.
-    pub fn add_concat(&mut self, left_id: usize, right_id: usize) -> Result<usize, SbroadError> {
+    pub fn add_concat(&mut self, left_id: NodeId, right_id: NodeId) -> Result<NodeId, SbroadError> {
         // Check that both children are of expression type.
         for child_id in &[left_id, right_id] {
             self.get_expression_node(*child_id)?;
diff --git a/sbroad-core/src/ir/expression/types.rs b/sbroad-core/src/ir/expression/types.rs
index b879baf06..0122fe56c 100644
--- a/sbroad-core/src/ir/expression/types.rs
+++ b/sbroad-core/src/ir/expression/types.rs
@@ -5,8 +5,10 @@ use crate::{
     ir::{expression::Expression, relation::Type, Node, Plan},
 };
 
+use super::NodeId;
+
 impl Plan {
-    fn get_node_type(&self, node_id: usize) -> Result<Type, SbroadError> {
+    fn get_node_type(&self, node_id: NodeId) -> Result<Type, SbroadError> {
         match self.get_node(node_id)? {
             Node::Expression(expr) => expr.calculate_type(self),
             Node::Relational(relational) => Err(SbroadError::Invalid(
diff --git a/sbroad-core/src/ir/function.rs b/sbroad-core/src/ir/function.rs
index c7bea0939..5732bef6d 100644
--- a/sbroad-core/src/ir/function.rs
+++ b/sbroad-core/src/ir/function.rs
@@ -7,7 +7,7 @@ use crate::ir::{Node, Plan};
 use serde::{Deserialize, Serialize};
 use smol_str::{format_smolstr, SmolStr, ToSmolStr};
 
-use super::expression::FunctionFeature;
+use super::expression::{FunctionFeature, NodeId};
 
 #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
 pub enum Behavior {
@@ -61,9 +61,9 @@ impl Plan {
     pub fn add_stable_function(
         &mut self,
         function: &Function,
-        children: Vec<usize>,
+        children: Vec<NodeId>,
         feature: Option<FunctionFeature>,
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         if !function.is_stable() {
             return Err(SbroadError::Invalid(
                 Entity::SQLFunction,
@@ -92,9 +92,9 @@ impl Plan {
         &mut self,
         function: &str,
         kind: AggregateKind,
-        children: Vec<usize>,
+        children: Vec<NodeId>,
         is_distinct: bool,
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         match kind {
             AggregateKind::GRCONCAT => {
                 if children.len() > 2 || children.is_empty() {
diff --git a/sbroad-core/src/ir/helpers.rs b/sbroad-core/src/ir/helpers.rs
index 1041a84b2..7c59ef618 100644
--- a/sbroad-core/src/ir/helpers.rs
+++ b/sbroad-core/src/ir/helpers.rs
@@ -12,6 +12,8 @@ use std::collections::hash_map::DefaultHasher;
 use std::fmt::Write;
 use std::hash::BuildHasher;
 
+use super::expression::NodeId;
+
 /// Helper macros to build a hash map or set
 /// from the list of arguments.
 #[macro_export]
@@ -75,7 +77,7 @@ impl Plan {
         &self,
         buf: &mut String,
         tabulation_number: i32,
-        node_id: usize,
+        node_id: NodeId,
     ) -> Result<(), std::fmt::Error> {
         let expr_try = self.get_expression_node(node_id);
         if let Ok(expr) = expr_try {
@@ -211,7 +213,7 @@ impl Plan {
         &self,
         buf: &mut String,
         tabulation_number: i32,
-        node_id: usize,
+        node_id: NodeId,
     ) -> Result<(), std::fmt::Error> {
         if tabulation_number == 0 {
             writeln!(buf, "---------------------------------------------")?;
@@ -293,7 +295,7 @@ impl Plan {
                         let text = if let Ok(gl_col_expr) = gl_col_expr {
                             format!("Gr_col: {gl_col_expr:?}")
                         } else {
-                            format!("Gr_col: {gr_col}")
+                            format!("Gr_col: {gr_col:?}")
                         };
                         writeln_with_tabulation(buf, tabulation_number + 2, text.as_str())?;
                     }
@@ -428,14 +430,15 @@ impl Plan {
     ///
     /// # Errors
     /// Fail to format one of the relational node.
-    pub fn formatted_arena_subtree(&self, node_id: usize) -> Result<String, SbroadError> {
+    pub fn formatted_arena_subtree(&self, node_id: NodeId) -> Result<String, SbroadError> {
         let mut ir_tree = PostOrder::with_capacity(|node| self.nodes.rel_iter(node), EXPR_CAPACITY);
         ir_tree.populate_nodes(node_id);
         let nodes = ir_tree.take_nodes();
 
         let mut buf = String::new();
-        for (_, id) in &nodes {
-            if self.formatted_arena_node(&mut buf, 0, *id).is_err() {
+        for level_node in &nodes {
+            let id = level_node.1;
+            if self.formatted_arena_node(&mut buf, 0, id).is_err() {
                 return Err(SbroadError::FailedTo(
                     Action::Serialize,
                     Some(Entity::Plan),
@@ -497,7 +500,7 @@ impl SyntaxPlan<'_> {
             if let SyntaxData::PlanId(id) = data {
                 let node = plan.get_node(*id);
                 if let Ok(node) = node {
-                    writeln!(buf, "{node:?} [id={id}]")?;
+                    writeln!(buf, "{node:?} [id={id:?}]")?;
                 }
             } else {
                 writeln!(buf, "{data:?}")?;
diff --git a/sbroad-core/src/ir/helpers/tests.rs b/sbroad-core/src/ir/helpers/tests.rs
index 8be08bb6a..a910c1fa6 100644
--- a/sbroad-core/src/ir/helpers/tests.rs
+++ b/sbroad-core/src/ir/helpers/tests.rs
@@ -1,10 +1,10 @@
-use crate::ir::transformation::helpers::sql_to_optimized_ir;
+use crate::ir::{expression::NodeId, transformation::helpers::sql_to_optimized_ir, ArenaType};
 use pretty_assertions::assert_eq;
 
 #[test]
 fn simple_select() {
     let query = r#"SELECT "product_code" FROM "hash_testing""#;
-    let mut plan = sql_to_optimized_ir(query, vec![]);
+    let plan = sql_to_optimized_ir(query, vec![]);
 
     let actual_arena = plan.formatted_arena().unwrap();
 
@@ -17,11 +17,11 @@ fn simple_select() {
 	Output_id: 10
 		[id: 10] expression: Row [distribution = Some(Segment { keys: KeySet({Key { positions: [0, 1] }}) })]
 			List:
-				[id: 1] expression: Alias [name = identification_number, child = Reference { parent: Some(11), targets: None, position: 0, col_type: Integer }]
-				[id: 3] expression: Alias [name = product_code, child = Reference { parent: Some(11), targets: None, position: 1, col_type: String }]
-				[id: 5] expression: Alias [name = product_units, child = Reference { parent: Some(11), targets: None, position: 2, col_type: Boolean }]
-				[id: 7] expression: Alias [name = sys_op, child = Reference { parent: Some(11), targets: None, position: 3, col_type: Unsigned }]
-				[id: 9] expression: Alias [name = bucket_id, child = Reference { parent: Some(11), targets: None, position: 4, col_type: Unsigned }]
+				[id: 1] expression: Alias [name = identification_number, child = Reference { parent: Some(NodeId { offset: 11, arena_type: Default }), targets: None, position: 0, col_type: Integer }]
+				[id: 3] expression: Alias [name = product_code, child = Reference { parent: Some(NodeId { offset: 11, arena_type: Default }), targets: None, position: 1, col_type: String }]
+				[id: 5] expression: Alias [name = product_units, child = Reference { parent: Some(NodeId { offset: 11, arena_type: Default }), targets: None, position: 2, col_type: Boolean }]
+				[id: 7] expression: Alias [name = sys_op, child = Reference { parent: Some(NodeId { offset: 11, arena_type: Default }), targets: None, position: 3, col_type: Unsigned }]
+				[id: 9] expression: Alias [name = bucket_id, child = Reference { parent: Some(NodeId { offset: 11, arena_type: Default }), targets: None, position: 4, col_type: Unsigned }]
 ---------------------------------------------
 ---------------------------------------------
 [id: 15] relation: Projection
@@ -30,7 +30,7 @@ fn simple_select() {
 	Output_id: 14
 		[id: 14] expression: Row [distribution = Some(Any)]
 			List:
-				[id: 13] expression: Alias [name = product_code, child = Reference { parent: Some(15), targets: Some([0]), position: 1, col_type: String }]
+				[id: 13] expression: Alias [name = product_code, child = Reference { parent: Some(NodeId { offset: 15, arena_type: Default }), targets: Some([0]), position: 1, col_type: String }]
 ---------------------------------------------
 "#);
 
@@ -44,7 +44,7 @@ fn simple_join() {
                         INNER JOIN
                         (SELECT "identification_number" FROM "hash_testing") as "t2"
                         ON "t1"."id" = "t2"."identification_number""#;
-    let mut plan = sql_to_optimized_ir(query, vec![]);
+    let plan = sql_to_optimized_ir(query, vec![]);
     let actual_arena = plan.formatted_arena().unwrap();
 
     let mut expected_arena = String::new();
@@ -56,11 +56,11 @@ fn simple_join() {
 	Output_id: 10
 		[id: 10] expression: Row [distribution = Some(Segment { keys: KeySet({Key { positions: [0] }}) })]
 			List:
-				[id: 1] expression: Alias [name = id, child = Reference { parent: Some(11), targets: None, position: 0, col_type: Unsigned }]
-				[id: 3] expression: Alias [name = sysFrom, child = Reference { parent: Some(11), targets: None, position: 1, col_type: Unsigned }]
-				[id: 5] expression: Alias [name = FIRST_NAME, child = Reference { parent: Some(11), targets: None, position: 2, col_type: String }]
-				[id: 7] expression: Alias [name = sys_op, child = Reference { parent: Some(11), targets: None, position: 3, col_type: Unsigned }]
-				[id: 9] expression: Alias [name = bucket_id, child = Reference { parent: Some(11), targets: None, position: 4, col_type: Unsigned }]
+				[id: 1] expression: Alias [name = id, child = Reference { parent: Some(NodeId { offset: 11, arena_type: Default }), targets: None, position: 0, col_type: Unsigned }]
+				[id: 3] expression: Alias [name = sysFrom, child = Reference { parent: Some(NodeId { offset: 11, arena_type: Default }), targets: None, position: 1, col_type: Unsigned }]
+				[id: 5] expression: Alias [name = FIRST_NAME, child = Reference { parent: Some(NodeId { offset: 11, arena_type: Default }), targets: None, position: 2, col_type: String }]
+				[id: 7] expression: Alias [name = sys_op, child = Reference { parent: Some(NodeId { offset: 11, arena_type: Default }), targets: None, position: 3, col_type: Unsigned }]
+				[id: 9] expression: Alias [name = bucket_id, child = Reference { parent: Some(NodeId { offset: 11, arena_type: Default }), targets: None, position: 4, col_type: Unsigned }]
 ---------------------------------------------
 ---------------------------------------------
 [id: 15] relation: Projection
@@ -69,7 +69,7 @@ fn simple_join() {
 	Output_id: 14
 		[id: 14] expression: Row [distribution = Some(Segment { keys: KeySet({Key { positions: [0] }}) })]
 			List:
-				[id: 13] expression: Alias [name = id, child = Reference { parent: Some(15), targets: Some([0]), position: 0, col_type: Unsigned }]
+				[id: 13] expression: Alias [name = id, child = Reference { parent: Some(NodeId { offset: 15, arena_type: Default }), targets: Some([0]), position: 0, col_type: Unsigned }]
 ---------------------------------------------
 ---------------------------------------------
 [id: 19] relation: ScanSubQuery
@@ -79,7 +79,7 @@ fn simple_join() {
 	Output_id: 18
 		[id: 18] expression: Row [distribution = Some(Segment { keys: KeySet({Key { positions: [0] }}) })]
 			List:
-				[id: 17] expression: Alias [name = id, child = Reference { parent: Some(19), targets: Some([0]), position: 0, col_type: Unsigned }]
+				[id: 17] expression: Alias [name = id, child = Reference { parent: Some(NodeId { offset: 19, arena_type: Default }), targets: Some([0]), position: 0, col_type: Unsigned }]
 ---------------------------------------------
 ---------------------------------------------
 [id: 31] relation: ScanRelation
@@ -88,11 +88,11 @@ fn simple_join() {
 	Output_id: 30
 		[id: 30] expression: Row [distribution = Some(Segment { keys: KeySet({Key { positions: [0, 1] }}) })]
 			List:
-				[id: 21] expression: Alias [name = identification_number, child = Reference { parent: Some(31), targets: None, position: 0, col_type: Integer }]
-				[id: 23] expression: Alias [name = product_code, child = Reference { parent: Some(31), targets: None, position: 1, col_type: String }]
-				[id: 25] expression: Alias [name = product_units, child = Reference { parent: Some(31), targets: None, position: 2, col_type: Boolean }]
-				[id: 27] expression: Alias [name = sys_op, child = Reference { parent: Some(31), targets: None, position: 3, col_type: Unsigned }]
-				[id: 29] expression: Alias [name = bucket_id, child = Reference { parent: Some(31), targets: None, position: 4, col_type: Unsigned }]
+				[id: 21] expression: Alias [name = identification_number, child = Reference { parent: Some(NodeId { offset: 31, arena_type: Default }), targets: None, position: 0, col_type: Integer }]
+				[id: 23] expression: Alias [name = product_code, child = Reference { parent: Some(NodeId { offset: 31, arena_type: Default }), targets: None, position: 1, col_type: String }]
+				[id: 25] expression: Alias [name = product_units, child = Reference { parent: Some(NodeId { offset: 31, arena_type: Default }), targets: None, position: 2, col_type: Boolean }]
+				[id: 27] expression: Alias [name = sys_op, child = Reference { parent: Some(NodeId { offset: 31, arena_type: Default }), targets: None, position: 3, col_type: Unsigned }]
+				[id: 29] expression: Alias [name = bucket_id, child = Reference { parent: Some(NodeId { offset: 31, arena_type: Default }), targets: None, position: 4, col_type: Unsigned }]
 ---------------------------------------------
 ---------------------------------------------
 [id: 35] relation: Projection
@@ -101,7 +101,7 @@ fn simple_join() {
 	Output_id: 34
 		[id: 34] expression: Row [distribution = Some(Any)]
 			List:
-				[id: 33] expression: Alias [name = identification_number, child = Reference { parent: Some(35), targets: Some([0]), position: 0, col_type: Integer }]
+				[id: 33] expression: Alias [name = identification_number, child = Reference { parent: Some(NodeId { offset: 35, arena_type: Default }), targets: Some([0]), position: 0, col_type: Integer }]
 ---------------------------------------------
 ---------------------------------------------
 [id: 39] relation: ScanSubQuery
@@ -111,7 +111,7 @@ fn simple_join() {
 	Output_id: 38
 		[id: 38] expression: Row [distribution = Some(Any)]
 			List:
-				[id: 37] expression: Alias [name = identification_number, child = Reference { parent: Some(39), targets: Some([0]), position: 0, col_type: Integer }]
+				[id: 37] expression: Alias [name = identification_number, child = Reference { parent: Some(NodeId { offset: 39, arena_type: Default }), targets: Some([0]), position: 0, col_type: Integer }]
 ---------------------------------------------
 ---------------------------------------------
 [id: 61] relation: Motion [policy = Segment(MotionKey { targets: [Reference(0)] })]
@@ -120,7 +120,7 @@ fn simple_join() {
 	Output_id: 60
 		[id: 60] expression: Row [distribution = Some(Segment { keys: KeySet({Key { positions: [0] }}) })]
 			List:
-				[id: 59] expression: Alias [name = identification_number, child = Reference { parent: Some(61), targets: Some([0]), position: 0, col_type: Integer }]
+				[id: 59] expression: Alias [name = identification_number, child = Reference { parent: Some(NodeId { offset: 61, arena_type: Default }), targets: Some([0]), position: 0, col_type: Integer }]
 ---------------------------------------------
 ---------------------------------------------
 [id: 50] relation: InnerJoin
@@ -132,7 +132,7 @@ fn simple_join() {
 					[id: 40] expression: Reference
 						Alias: id
 						Referenced table name (or alias): t1
-						Parent: Some(50)
+						Parent: Some(NodeId { offset: 50, arena_type: Default })
 						target_id: 0
 						Column type: unsigned
 			Right child
@@ -141,7 +141,7 @@ fn simple_join() {
 					[id: 42] expression: Reference
 						Alias: identification_number
 						Referenced table name (or alias): t2
-						Parent: Some(50)
+						Parent: Some(NodeId { offset: 50, arena_type: Default })
 						target_id: 1
 						Column type: integer
 	Children:
@@ -150,8 +150,8 @@ fn simple_join() {
 	Output_id: 49
 		[id: 49] expression: Row [distribution = Some(Segment { keys: KeySet({Key { positions: [1] }, Key { positions: [0] }}) })]
 			List:
-				[id: 46] expression: Alias [name = id, child = Reference { parent: Some(50), targets: Some([0]), position: 0, col_type: Unsigned }]
-				[id: 48] expression: Alias [name = identification_number, child = Reference { parent: Some(50), targets: Some([1]), position: 0, col_type: Integer }]
+				[id: 46] expression: Alias [name = id, child = Reference { parent: Some(NodeId { offset: 50, arena_type: Default }), targets: Some([0]), position: 0, col_type: Unsigned }]
+				[id: 48] expression: Alias [name = identification_number, child = Reference { parent: Some(NodeId { offset: 50, arena_type: Default }), targets: Some([1]), position: 0, col_type: Integer }]
 ---------------------------------------------
 ---------------------------------------------
 [id: 54] relation: Projection
@@ -160,7 +160,7 @@ fn simple_join() {
 	Output_id: 53
 		[id: 53] expression: Row [distribution = Some(Segment { keys: KeySet({Key { positions: [0] }}) })]
 			List:
-				[id: 52] expression: Alias [name = id, child = Reference { parent: Some(54), targets: Some([0]), position: 0, col_type: Unsigned }]
+				[id: 52] expression: Alias [name = id, child = Reference { parent: Some(NodeId { offset: 54, arena_type: Default }), targets: Some([0]), position: 0, col_type: Unsigned }]
 ---------------------------------------------
 "#);
 
@@ -174,10 +174,13 @@ fn simple_join_subtree() {
                         INNER JOIN
                         (SELECT "identification_number" FROM "hash_testing") as "t2"
                         ON "t1"."id" = "t2"."identification_number""#;
-    let mut plan = sql_to_optimized_ir(query, vec![]);
+    let plan = sql_to_optimized_ir(query, vec![]);
 
     // Taken from the expected arena output in the `simple_join` test.
-    let inner_join_inner_child_id = 61;
+    let inner_join_inner_child_id = NodeId {
+        offset: 61,
+        arena_type: ArenaType::Default,
+    };
 
     let actual_arena_subtree = plan
         .formatted_arena_subtree(inner_join_inner_child_id)
@@ -192,11 +195,11 @@ fn simple_join_subtree() {
 	Output_id: 30
 		[id: 30] expression: Row [distribution = Some(Segment { keys: KeySet({Key { positions: [0, 1] }}) })]
 			List:
-				[id: 21] expression: Alias [name = identification_number, child = Reference { parent: Some(31), targets: None, position: 0, col_type: Integer }]
-				[id: 23] expression: Alias [name = product_code, child = Reference { parent: Some(31), targets: None, position: 1, col_type: String }]
-				[id: 25] expression: Alias [name = product_units, child = Reference { parent: Some(31), targets: None, position: 2, col_type: Boolean }]
-				[id: 27] expression: Alias [name = sys_op, child = Reference { parent: Some(31), targets: None, position: 3, col_type: Unsigned }]
-				[id: 29] expression: Alias [name = bucket_id, child = Reference { parent: Some(31), targets: None, position: 4, col_type: Unsigned }]
+				[id: 21] expression: Alias [name = identification_number, child = Reference { parent: Some(NodeId { offset: 31, arena_type: Default }), targets: None, position: 0, col_type: Integer }]
+				[id: 23] expression: Alias [name = product_code, child = Reference { parent: Some(NodeId { offset: 31, arena_type: Default }), targets: None, position: 1, col_type: String }]
+				[id: 25] expression: Alias [name = product_units, child = Reference { parent: Some(NodeId { offset: 31, arena_type: Default }), targets: None, position: 2, col_type: Boolean }]
+				[id: 27] expression: Alias [name = sys_op, child = Reference { parent: Some(NodeId { offset: 31, arena_type: Default }), targets: None, position: 3, col_type: Unsigned }]
+				[id: 29] expression: Alias [name = bucket_id, child = Reference { parent: Some(NodeId { offset: 31, arena_type: Default }), targets: None, position: 4, col_type: Unsigned }]
 ---------------------------------------------
 ---------------------------------------------
 [id: 35] relation: Projection
@@ -205,7 +208,7 @@ fn simple_join_subtree() {
 	Output_id: 34
 		[id: 34] expression: Row [distribution = Some(Any)]
 			List:
-				[id: 33] expression: Alias [name = identification_number, child = Reference { parent: Some(35), targets: Some([0]), position: 0, col_type: Integer }]
+				[id: 33] expression: Alias [name = identification_number, child = Reference { parent: Some(NodeId { offset: 35, arena_type: Default }), targets: Some([0]), position: 0, col_type: Integer }]
 ---------------------------------------------
 ---------------------------------------------
 [id: 39] relation: ScanSubQuery
@@ -215,7 +218,7 @@ fn simple_join_subtree() {
 	Output_id: 38
 		[id: 38] expression: Row [distribution = Some(Any)]
 			List:
-				[id: 37] expression: Alias [name = identification_number, child = Reference { parent: Some(39), targets: Some([0]), position: 0, col_type: Integer }]
+				[id: 37] expression: Alias [name = identification_number, child = Reference { parent: Some(NodeId { offset: 39, arena_type: Default }), targets: Some([0]), position: 0, col_type: Integer }]
 ---------------------------------------------
 ---------------------------------------------
 [id: 61] relation: Motion [policy = Segment(MotionKey { targets: [Reference(0)] })]
@@ -224,7 +227,7 @@ fn simple_join_subtree() {
 	Output_id: 60
 		[id: 60] expression: Row [distribution = Some(Segment { keys: KeySet({Key { positions: [0] }}) })]
 			List:
-				[id: 59] expression: Alias [name = identification_number, child = Reference { parent: Some(61), targets: Some([0]), position: 0, col_type: Integer }]
+				[id: 59] expression: Alias [name = identification_number, child = Reference { parent: Some(NodeId { offset: 61, arena_type: Default }), targets: Some([0]), position: 0, col_type: Integer }]
 ---------------------------------------------
 "#
     );
@@ -235,7 +238,7 @@ fn simple_join_subtree() {
 #[test]
 fn simple_aggregation_with_group_by() {
     let query = r#"SELECT "product_code" FROM "hash_testing" GROUP BY "product_code""#;
-    let mut plan = sql_to_optimized_ir(query, vec![]);
+    let plan = sql_to_optimized_ir(query, vec![]);
 
     let actual_arena = plan.formatted_arena().unwrap();
     let mut expected_arena = String::new();
@@ -247,26 +250,26 @@ fn simple_aggregation_with_group_by() {
 	Output_id: 10
 		[id: 10] expression: Row [distribution = Some(Segment { keys: KeySet({Key { positions: [0, 1] }}) })]
 			List:
-				[id: 1] expression: Alias [name = identification_number, child = Reference { parent: Some(11), targets: None, position: 0, col_type: Integer }]
-				[id: 3] expression: Alias [name = product_code, child = Reference { parent: Some(11), targets: None, position: 1, col_type: String }]
-				[id: 5] expression: Alias [name = product_units, child = Reference { parent: Some(11), targets: None, position: 2, col_type: Boolean }]
-				[id: 7] expression: Alias [name = sys_op, child = Reference { parent: Some(11), targets: None, position: 3, col_type: Unsigned }]
-				[id: 9] expression: Alias [name = bucket_id, child = Reference { parent: Some(11), targets: None, position: 4, col_type: Unsigned }]
+				[id: 1] expression: Alias [name = identification_number, child = Reference { parent: Some(NodeId { offset: 11, arena_type: Default }), targets: None, position: 0, col_type: Integer }]
+				[id: 3] expression: Alias [name = product_code, child = Reference { parent: Some(NodeId { offset: 11, arena_type: Default }), targets: None, position: 1, col_type: String }]
+				[id: 5] expression: Alias [name = product_units, child = Reference { parent: Some(NodeId { offset: 11, arena_type: Default }), targets: None, position: 2, col_type: Boolean }]
+				[id: 7] expression: Alias [name = sys_op, child = Reference { parent: Some(NodeId { offset: 11, arena_type: Default }), targets: None, position: 3, col_type: Unsigned }]
+				[id: 9] expression: Alias [name = bucket_id, child = Reference { parent: Some(NodeId { offset: 11, arena_type: Default }), targets: None, position: 4, col_type: Unsigned }]
 ---------------------------------------------
 ---------------------------------------------
 [id: 24] relation: GroupBy [is_final = false]
 	Gr_cols:
-		Gr_col: Reference { parent: Some(24), targets: Some([0]), position: 1, col_type: String }
+		Gr_col: Reference { parent: Some(NodeId { offset: 24, arena_type: Default }), targets: Some([0]), position: 1, col_type: String }
 	Children:
 		Child_id = 11
 	Output_id: 23
 		[id: 23] expression: Row [distribution = Some(Segment { keys: KeySet({Key { positions: [0, 1] }}) })]
 			List:
-				[id: 14] expression: Alias [name = identification_number, child = Reference { parent: Some(24), targets: Some([0]), position: 0, col_type: Integer }]
-				[id: 16] expression: Alias [name = product_code, child = Reference { parent: Some(24), targets: Some([0]), position: 1, col_type: String }]
-				[id: 18] expression: Alias [name = product_units, child = Reference { parent: Some(24), targets: Some([0]), position: 2, col_type: Boolean }]
-				[id: 20] expression: Alias [name = sys_op, child = Reference { parent: Some(24), targets: Some([0]), position: 3, col_type: Unsigned }]
-				[id: 22] expression: Alias [name = bucket_id, child = Reference { parent: Some(24), targets: Some([0]), position: 4, col_type: Unsigned }]
+				[id: 14] expression: Alias [name = identification_number, child = Reference { parent: Some(NodeId { offset: 24, arena_type: Default }), targets: Some([0]), position: 0, col_type: Integer }]
+				[id: 16] expression: Alias [name = product_code, child = Reference { parent: Some(NodeId { offset: 24, arena_type: Default }), targets: Some([0]), position: 1, col_type: String }]
+				[id: 18] expression: Alias [name = product_units, child = Reference { parent: Some(NodeId { offset: 24, arena_type: Default }), targets: Some([0]), position: 2, col_type: Boolean }]
+				[id: 20] expression: Alias [name = sys_op, child = Reference { parent: Some(NodeId { offset: 24, arena_type: Default }), targets: Some([0]), position: 3, col_type: Unsigned }]
+				[id: 22] expression: Alias [name = bucket_id, child = Reference { parent: Some(NodeId { offset: 24, arena_type: Default }), targets: Some([0]), position: 4, col_type: Unsigned }]
 ---------------------------------------------
 ---------------------------------------------
 [id: 31] relation: Projection
@@ -275,7 +278,7 @@ fn simple_aggregation_with_group_by() {
 	Output_id: 30
 		[id: 30] expression: Row [distribution = Some(Any)]
 			List:
-				[id: 29] expression: Alias [name = column_12, child = Reference { parent: Some(24), targets: Some([0]), position: 1, col_type: String }]
+				[id: 29] expression: Alias [name = column_12, child = Reference { parent: Some(NodeId { offset: 24, arena_type: Default }), targets: Some([0]), position: 1, col_type: String }]
 ---------------------------------------------
 ---------------------------------------------
 [id: 35] relation: ScanSubQuery
@@ -284,7 +287,7 @@ fn simple_aggregation_with_group_by() {
 	Output_id: 34
 		[id: 34] expression: Row [distribution = Some(Any)]
 			List:
-				[id: 33] expression: Alias [name = column_12, child = Reference { parent: Some(35), targets: Some([0]), position: 0, col_type: String }]
+				[id: 33] expression: Alias [name = column_12, child = Reference { parent: Some(NodeId { offset: 35, arena_type: Default }), targets: Some([0]), position: 0, col_type: String }]
 ---------------------------------------------
 ---------------------------------------------
 [id: 45] relation: Motion [policy = Segment(MotionKey { targets: [Reference(0)] })]
@@ -293,18 +296,18 @@ fn simple_aggregation_with_group_by() {
 	Output_id: 44
 		[id: 44] expression: Row [distribution = Some(Segment { keys: KeySet({Key { positions: [0] }}) })]
 			List:
-				[id: 43] expression: Alias [name = column_12, child = Reference { parent: Some(45), targets: Some([0]), position: 0, col_type: String }]
+				[id: 43] expression: Alias [name = column_12, child = Reference { parent: Some(NodeId { offset: 45, arena_type: Default }), targets: Some([0]), position: 0, col_type: String }]
 ---------------------------------------------
 ---------------------------------------------
 [id: 40] relation: GroupBy [is_final = true]
 	Gr_cols:
-		Gr_col: Reference { parent: Some(40), targets: Some([0]), position: 0, col_type: String }
+		Gr_col: Reference { parent: Some(NodeId { offset: 40, arena_type: Default }), targets: Some([0]), position: 0, col_type: String }
 	Children:
 		Child_id = 45
 	Output_id: 39
 		[id: 39] expression: Row [distribution = Some(Segment { keys: KeySet({Key { positions: [0] }}) })]
 			List:
-				[id: 38] expression: Alias [name = column_12, child = Reference { parent: Some(40), targets: Some([0]), position: 0, col_type: String }]
+				[id: 38] expression: Alias [name = column_12, child = Reference { parent: Some(NodeId { offset: 40, arena_type: Default }), targets: Some([0]), position: 0, col_type: String }]
 ---------------------------------------------
 ---------------------------------------------
 [id: 28] relation: Projection
@@ -313,7 +316,7 @@ fn simple_aggregation_with_group_by() {
 	Output_id: 27
 		[id: 27] expression: Row [distribution = Some(Segment { keys: KeySet({Key { positions: [0] }}) })]
 			List:
-				[id: 26] expression: Alias [name = product_code, child = Reference { parent: Some(28), targets: Some([0]), position: 0, col_type: String }]
+				[id: 26] expression: Alias [name = product_code, child = Reference { parent: Some(NodeId { offset: 28, arena_type: Default }), targets: Some([0]), position: 0, col_type: String }]
 ---------------------------------------------
 "#);
 
diff --git a/sbroad-core/src/ir/operator.rs b/sbroad-core/src/ir/operator.rs
index 19973b548..44728f3ed 100644
--- a/sbroad-core/src/ir/operator.rs
+++ b/sbroad-core/src/ir/operator.rs
@@ -17,10 +17,10 @@ use std::fmt::{Display, Formatter};
 use crate::errors::{Action, Entity, SbroadError};
 
 use super::api::children::{Children, MutChildren};
-use super::expression::{ColumnPositionMap, Expression};
+use super::expression::{ColumnPositionMap, Expression, NodeId};
 use super::transformation::redistribution::{MotionPolicy, Program};
-use super::tree::traversal::{PostOrderWithFilter, EXPR_CAPACITY};
-use super::{Node, NodeId, Plan};
+use super::tree::traversal::{LevelNode, PostOrderWithFilter, EXPR_CAPACITY};
+use super::{ArenaType, Node, Plan};
 use crate::ir::distribution::{Distribution, Key, KeySet};
 use crate::ir::expression::{ExpressionId, PlanExpr};
 use crate::ir::helpers::RepeatableState;
@@ -287,7 +287,7 @@ pub enum UpdateStrategy {
 
 #[derive(Clone, Deserialize, Debug, PartialEq, Eq, Serialize)]
 pub enum OrderByEntity {
-    Expression { expr_id: usize },
+    Expression { expr_id: NodeId },
     Index { value: usize },
 }
 
@@ -322,25 +322,25 @@ pub enum Relational {
         /// CTE's name.
         alias: SmolStr,
         /// Contains exactly one single element (projection node index).
-        child: usize,
+        child: NodeId,
         /// An output tuple with aliases.
-        output: usize,
+        output: NodeId,
     },
     Except {
         /// Left child id
-        left: usize,
+        left: NodeId,
         /// Right child id
-        right: usize,
+        right: NodeId,
         /// Outputs tuple node index in the plan node arena.
-        output: usize,
+        output: NodeId,
     },
     Delete {
         /// Relation name.
         relation: SmolStr,
         /// Contains exactly one single element.
-        children: Vec<usize>,
+        children: Vec<NodeId>,
         /// The output tuple (reserved for `delete returning`).
-        output: usize,
+        output: NodeId,
     },
     Insert {
         /// Relation name.
@@ -349,23 +349,23 @@ pub enum Relational {
         /// the child's tuple.
         columns: Vec<usize>,
         /// Contains exactly one single element.
-        children: Vec<usize>,
+        children: Vec<NodeId>,
         /// The output tuple (reserved for `insert returning`).
-        output: usize,
+        output: NodeId,
         /// What to do in case there is a conflict during insert on storage
         conflict_strategy: ConflictStrategy,
     },
     Intersect {
-        left: usize,
-        right: usize,
+        left: NodeId,
+        right: NodeId,
         // id of the output tuple
-        output: usize,
+        output: NodeId,
     },
     Update {
         /// Relation name.
         relation: SmolStr,
         /// Children ids. Update has exactly one child.
-        children: Vec<usize>,
+        children: Vec<NodeId>,
         /// Maps position of column being updated in table to corresponding position
         /// in `Projection` below `Update`.
         ///
@@ -378,18 +378,18 @@ pub enum Relational {
         /// below `Update`.
         pk_positions: Vec<ColumnPosition>,
         /// Output id.
-        output: usize,
+        output: NodeId,
     },
     Join {
         /// Contains at least two elements: left and right node indexes
         /// from the plan node arena. Every element other than those
         /// two should be treated as a `SubQuery` node.
-        children: Vec<usize>,
+        children: Vec<NodeId>,
         /// Left and right tuple comparison condition.
         /// In fact it is an expression tree top index from the plan node arena.
-        condition: usize,
+        condition: NodeId,
         /// Outputs tuple node index from the plan node arena.
-        output: usize,
+        output: NodeId,
         /// inner or left
         kind: JoinKind,
     },
@@ -398,13 +398,13 @@ pub enum Relational {
         alias: Option<SmolStr>,
         /// Contains exactly one single element: child node index
         /// from the plan node arena.
-        children: Vec<usize>,
+        children: Vec<NodeId>,
         /// Motion policy - the amount of data to be moved.
         policy: MotionPolicy,
         /// A sequence of opcodes that transform the data.
         program: Program,
         /// Outputs tuple node index in the plan node arena.
-        output: usize,
+        output: NodeId,
         /// A helper field indicating whether first element of
         /// `children` vec is a `Relational::SubQuery`.
         /// We need it on the stage of translating Plan to SQL, because
@@ -417,9 +417,9 @@ pub enum Relational {
         /// from the plan node arena. Every element other than the
         /// first one should be treated as a `SubQuery` node from
         /// the output tree.
-        children: Vec<usize>,
+        children: Vec<NodeId>,
         /// Outputs tuple node index in the plan node arena.
-        output: usize,
+        output: NodeId,
         /// Wheter the select was marked with `distinct` keyword
         is_distinct: bool,
     },
@@ -427,7 +427,7 @@ pub enum Relational {
         // Scan name.
         alias: Option<SmolStr>,
         /// Outputs tuple node index in the plan node arena.
-        output: usize,
+        output: NodeId,
         /// Relation name.
         relation: SmolStr,
     },
@@ -436,81 +436,81 @@ pub enum Relational {
         alias: Option<SmolStr>,
         /// Contains exactly one single element: child node index
         /// from the plan node arena.
-        children: Vec<usize>,
+        children: Vec<NodeId>,
         /// Outputs tuple node index in the plan node arena.
-        output: usize,
+        output: NodeId,
     },
     Selection {
         /// Contains at least one single element: child node index
         /// from the plan node arena. Every element other than the
         /// first one should be treated as a `SubQuery` node from
         /// the filter tree.
-        children: Vec<usize>,
+        children: Vec<NodeId>,
         /// Filters expression node index in the plan node arena.
-        filter: usize,
+        filter: NodeId,
         /// Outputs tuple node index in the plan node arena.
-        output: usize,
+        output: NodeId,
     },
     GroupBy {
         /// The first child is a relational operator before group by
-        children: Vec<usize>,
-        gr_cols: Vec<usize>,
-        output: usize,
+        children: Vec<NodeId>,
+        gr_cols: Vec<NodeId>,
+        output: NodeId,
         is_final: bool,
     },
     Having {
-        children: Vec<usize>,
-        output: usize,
-        filter: usize,
+        children: Vec<NodeId>,
+        output: NodeId,
+        filter: NodeId,
     },
     OrderBy {
-        child: usize,
-        output: usize,
+        child: NodeId,
+        output: NodeId,
         order_by_elements: Vec<OrderByElement>,
     },
     UnionAll {
         /// Left child id
-        left: usize,
+        left: NodeId,
         /// Right child id
-        right: usize,
+        right: NodeId,
         /// Outputs tuple node index in the plan node arena.
-        output: usize,
+        output: NodeId,
     },
     Union {
         /// Left child id
-        left: usize,
+        left: NodeId,
         /// Right child id
-        right: usize,
+        right: NodeId,
         /// Outputs tuple node index in the plan node arena.
-        output: usize,
+        output: NodeId,
     },
     Values {
         /// Output tuple.
-        output: usize,
+        output: NodeId,
         /// Non-empty list of value rows.
-        children: Vec<usize>,
+        children: Vec<NodeId>,
     },
     ValuesRow {
         /// Output tuple of aliases.
-        output: usize,
+        output: NodeId,
         /// The data tuple.
-        data: usize,
+        data: NodeId,
         /// A list of children is required for the rows containing
         /// sub-queries. For example, the row `(1, (select a from t))`
         /// requires `children` to keep projection node. If the row
         /// contains only constants (i.e. `(1, 2)`), then `children`
         /// should be empty.
-        children: Vec<usize>,
+        children: Vec<NodeId>,
     },
     Limit {
         /// Output tuple.
-        output: usize,
+        output: NodeId,
         // The limit value constant that comes after LIMIT keyword.
         limit: u64,
         /// Select statement that is being limited.
         /// Note that it can be a complex statement, like SELECT .. UNION ALL SELECT .. LIMIT 100,
         /// in that case limit is applied to the result of union.
-        child: usize,
+        child: NodeId,
     },
 }
 
@@ -518,7 +518,7 @@ pub enum Relational {
 impl Relational {
     /// Gets an immutable id of the output tuple node of the plan's arena.
     #[must_use]
-    pub fn output(&self) -> usize {
+    pub fn output(&self) -> NodeId {
         match self {
             Relational::ScanCte { output, .. }
             | Relational::Except { output, .. }
@@ -545,7 +545,7 @@ impl Relational {
 
     /// Gets an immutable reference to the output tuple node id.
     #[must_use]
-    pub fn mut_output(&mut self) -> &mut usize {
+    pub fn mut_output(&mut self) -> &mut NodeId {
         match self {
             Relational::ScanCte { output, .. }
             | Relational::Except { output, .. }
@@ -688,7 +688,7 @@ impl Relational {
     ///
     /// # Panics
     /// - wrong number of children for the given node
-    pub fn set_children(&mut self, children: Vec<usize>) {
+    pub fn set_children(&mut self, children: Vec<NodeId>) {
         match self {
             Relational::Join {
                 children: ref mut old,
@@ -909,7 +909,7 @@ impl Plan {
     ///
     /// # Errors
     /// - failed to oupdate shard columns info due to invalid plan subtree
-    pub fn add_relational(&mut self, node: Relational) -> Result<usize, SbroadError> {
+    pub fn add_relational(&mut self, node: Relational) -> Result<NodeId, SbroadError> {
         let rel_id = self.nodes.push(Node::Relational(node));
         let mut context = self.context_mut();
         context.shard_col_info.update_node(rel_id, self)?;
@@ -920,7 +920,7 @@ impl Plan {
     ///
     /// # Errors
     /// - child id pointes to non-existing or non-relational node.
-    pub fn add_delete(&mut self, table: SmolStr, child_id: usize) -> Result<usize, SbroadError> {
+    pub fn add_delete(&mut self, table: SmolStr, child_id: NodeId) -> Result<NodeId, SbroadError> {
         let output = self.add_row_for_output(child_id, &[], true)?;
         let delete = Relational::Delete {
             relation: table,
@@ -938,8 +938,8 @@ impl Plan {
     /// - children nodes are not relational
     /// - children tuples are invalid
     /// - children tuples have mismatching structure
-    pub fn add_except(&mut self, left: usize, right: usize) -> Result<usize, SbroadError> {
-        let child_row_len = |child: usize, plan: &Plan| -> Result<usize, SbroadError> {
+    pub fn add_except(&mut self, left: NodeId, right: NodeId) -> Result<NodeId, SbroadError> {
+        let child_row_len = |child: NodeId, plan: &Plan| -> Result<usize, SbroadError> {
             let child_output = plan.get_relation_node(child)?.output();
             Ok(plan
                 .get_expression_node(child_output)?
@@ -1027,15 +1027,15 @@ impl Plan {
         &mut self,
         relation: &str,
         update_defs: &HashMap<ColumnPosition, ExpressionId, RepeatableState>,
-        rel_child_id: usize,
-    ) -> Result<usize, SbroadError> {
+        rel_child_id: NodeId,
+    ) -> Result<NodeId, SbroadError> {
         // Create Reference node from given table column.
         fn create_ref_from_column(
             plan: &mut Plan,
             relation: &str,
             table_position_map: &HashMap<ColumnPosition, ColumnPosition>,
             col_pos: usize,
-        ) -> Result<usize, SbroadError> {
+        ) -> Result<NodeId, SbroadError> {
             let table = plan.get_relation_or_error(relation)?;
             let col: &Column = table.columns.get(col_pos).ok_or_else(|| {
                 SbroadError::Invalid(
@@ -1076,7 +1076,7 @@ impl Plan {
                 .iter()
                 .any(|col| update_defs.contains_key(col));
         // Columns of Projection that will be created
-        let mut projection_cols: Vec<usize> = Vec::with_capacity(update_defs.len());
+        let mut projection_cols: Vec<NodeId> = Vec::with_capacity(update_defs.len());
         // Positions of columns in Projection that constitute the primary key
         let mut primary_key_positions: Vec<usize> =
             Vec::with_capacity(table.primary_key.positions.len());
@@ -1158,7 +1158,7 @@ impl Plan {
                 .clone()
                 .iter()
                 .map(|pos| create_ref_from_column(self, relation, &child_map, *pos))
-                .collect::<Result<Vec<usize>, SbroadError>>()?;
+                .collect::<Result<Vec<NodeId>, SbroadError>>()?;
 
             let mut pos = 0;
             // Add update_expressions
@@ -1237,10 +1237,10 @@ impl Plan {
     pub fn add_insert(
         &mut self,
         relation: &str,
-        child: usize,
+        child: NodeId,
         columns: &[SmolStr],
         conflict_strategy: ConflictStrategy,
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         let rel = self.relations.get(relation).ok_or_else(|| {
             SbroadError::NotFound(
                 Entity::Table,
@@ -1297,7 +1297,7 @@ impl Plan {
             )));
         }
 
-        let mut refs: Vec<usize> = Vec::with_capacity(rel.columns.len());
+        let mut refs: Vec<NodeId> = Vec::with_capacity(rel.columns.len());
         for (pos, col) in rel.columns.iter().enumerate() {
             let r_id = self.nodes.add_ref(None, None, pos, col.r#type.clone());
             let col_alias_id = self.nodes.add_alias(&col.name, r_id)?;
@@ -1329,11 +1329,11 @@ impl Plan {
     ///
     /// # Errors
     /// - relation is invalid
-    pub fn add_scan(&mut self, table: &str, alias: Option<&str>) -> Result<usize, SbroadError> {
+    pub fn add_scan(&mut self, table: &str, alias: Option<&str>) -> Result<NodeId, SbroadError> {
         let nodes = &mut self.nodes;
 
         if let Some(rel) = self.relations.get(table) {
-            let mut refs: Vec<usize> = Vec::with_capacity(rel.columns.len());
+            let mut refs: Vec<NodeId> = Vec::with_capacity(rel.columns.len());
             for (pos, col) in rel.columns.iter().enumerate() {
                 let r_id = nodes.add_ref(None, None, pos, col.r#type.clone());
                 let col_alias_id = nodes.add_alias(&col.name, r_id)?;
@@ -1365,11 +1365,11 @@ impl Plan {
     /// - children output tuples are invalid
     pub fn add_join(
         &mut self,
-        left: usize,
-        right: usize,
-        condition: usize,
+        left: NodeId,
+        right: NodeId,
+        condition: NodeId,
         kind: JoinKind,
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         if !self.is_trivalent(condition)? {
             return Err(SbroadError::Invalid(
                 Entity::Expression,
@@ -1384,7 +1384,7 @@ impl Plan {
         // As a side effect, we also need to update the references
         // to the child's output in the condition expression as
         // we have filtered out the sharding column.
-        let mut children: Vec<usize> = Vec::with_capacity(2);
+        let mut children: Vec<NodeId> = Vec::with_capacity(2);
         for (child, join_child) in &[(left, JoinChild::Outer), (right, JoinChild::Inner)] {
             let child_node = self.get_relation_node(*child)?;
             if let Relational::ScanRelation {
@@ -1418,7 +1418,7 @@ impl Plan {
                     JoinChild::Outer => Some(vec![0_usize]),
                 };
                 let mut refs = Vec::with_capacity(condition_nodes.len());
-                for (_, id) in condition_nodes {
+                for LevelNode(_, id) in condition_nodes {
                     let expr = self.get_expression_node(id)?;
                     if let Expression::Reference {
                         position, targets, ..
@@ -1491,10 +1491,10 @@ impl Plan {
     /// - child output tuple is invalid
     pub fn add_motion(
         &mut self,
-        child_id: usize,
+        child_id: NodeId,
         policy: &MotionPolicy,
         program: Program,
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         let alias = if let Node::Relational(rel) = self.get_node(child_id)? {
             rel.scan_name(self, 0)?.map(SmolStr::from)
         } else {
@@ -1507,7 +1507,7 @@ impl Plan {
                 return Err(SbroadError::Invalid(
                     Entity::Motion,
                     Some(format_smolstr!(
-                        "add_motion: got MotionPolicy::None for child_id: {child_id}"
+                        "add_motion: got MotionPolicy::None for child_id: {child_id:?}"
                     )),
                 ))
             }
@@ -1557,11 +1557,11 @@ impl Plan {
     /// - column name do not match the ones in the child output tuple
     pub fn add_proj(
         &mut self,
-        child: usize,
+        child: NodeId,
         col_names: &[&str],
         is_distinct: bool,
         needs_shard_col: bool,
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         let output = self.add_row_for_output(child, col_names, needs_shard_col)?;
         let proj = Relational::Projection {
             children: vec![child],
@@ -1582,10 +1582,10 @@ impl Plan {
     /// - columns are not aliases or have duplicate names
     pub fn add_proj_internal(
         &mut self,
-        child: usize,
-        columns: &[usize],
+        child: NodeId,
+        columns: &[NodeId],
         is_distinct: bool,
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         let output = self.nodes.add_row(columns.to_vec(), None);
         let proj = Relational::Projection {
             children: vec![child],
@@ -1608,8 +1608,12 @@ impl Plan {
     ///
     /// # Panics
     /// - `children` is empty
-    pub fn add_select(&mut self, children: &[usize], filter: usize) -> Result<usize, SbroadError> {
-        let first_child: usize = *children
+    pub fn add_select(
+        &mut self,
+        children: &[NodeId],
+        filter: NodeId,
+    ) -> Result<NodeId, SbroadError> {
+        let first_child: NodeId = *children
             .first()
             .expect("No children passed to `add_select`");
 
@@ -1640,8 +1644,12 @@ impl Plan {
     /// - filter expression is not boolean
     /// - children nodes are not relational
     /// - first child output tuple is not valid
-    pub fn add_having(&mut self, children: &[usize], filter: usize) -> Result<usize, SbroadError> {
-        let first_child: usize = match children.len() {
+    pub fn add_having(
+        &mut self,
+        children: &[NodeId],
+        filter: NodeId,
+    ) -> Result<NodeId, SbroadError> {
+        let first_child: NodeId = match children.len() {
             0 => {
                 return Err(SbroadError::UnexpectedNumberOfValues(
                     "children list is empty".into(),
@@ -1687,9 +1695,9 @@ impl Plan {
     /// - Relational node child not found.
     pub fn add_order_by(
         &mut self,
-        child: usize,
+        child: NodeId,
         order_by_elements: Vec<OrderByElement>,
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         let output = self.add_row_for_output(child, &[], true)?;
         let order_by = Relational::OrderBy {
             child,
@@ -1719,9 +1727,9 @@ impl Plan {
     /// - child node output is not a correct tuple
     pub fn add_sub_query(
         &mut self,
-        child: usize,
+        child: NodeId,
         alias: Option<&str>,
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         let name: Option<SmolStr> = alias.map(SmolStr::from);
 
         let output = self.add_row_for_output(child, &[], true)?;
@@ -1815,11 +1823,11 @@ impl Plan {
     /// - children tuples have mismatching structure
     pub fn add_union(
         &mut self,
-        left: usize,
-        right: usize,
+        left: NodeId,
+        right: NodeId,
         remove_duplicates: bool,
-    ) -> Result<usize, SbroadError> {
-        let child_row_len = |child: usize, plan: &Plan| -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
+        let child_row_len = |child: NodeId, plan: &Plan| -> Result<usize, SbroadError> {
             let child_output = plan.get_relation_node(child)?.output();
             Ok(plan
                 .get_expression_node(child_output)?
@@ -1859,7 +1867,7 @@ impl Plan {
     ///
     /// # Errors
     /// - Row node is not of a row type
-    pub fn add_limit(&mut self, select: usize, limit: u64) -> Result<usize, SbroadError> {
+    pub fn add_limit(&mut self, select: NodeId, limit: u64) -> Result<NodeId, SbroadError> {
         let output = self.add_row_for_output(select, &[], true)?;
         let limit = Relational::Limit {
             output,
@@ -1878,12 +1886,12 @@ impl Plan {
     /// - Row node is not of a row type
     pub fn add_values_row(
         &mut self,
-        row_id: usize,
+        row_id: NodeId,
         col_idx: &mut usize,
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         let row = self.get_expression_node(row_id)?;
         let columns = row.clone_row_list()?;
-        let mut aliases: Vec<usize> = Vec::with_capacity(columns.len());
+        let mut aliases: Vec<NodeId> = Vec::with_capacity(columns.len());
         for col_id in columns {
             // Generate a row of aliases for the incoming row.
             *col_idx += 1;
@@ -1909,7 +1917,7 @@ impl Plan {
     /// # Errors
     /// - No child nodes
     /// - Child node is not relational
-    pub fn add_values(&mut self, value_rows: Vec<usize>) -> Result<usize, SbroadError> {
+    pub fn add_values(&mut self, value_rows: Vec<NodeId>) -> Result<NodeId, SbroadError> {
         // In case we have several `ValuesRow` under `Values`
         // (e.g. VALUES (1, "test_1"), (2, "test_2")),
         // the list of alias column names for it will look like:
@@ -1954,7 +1962,7 @@ impl Plan {
         };
 
         // Generate a row of aliases referencing all the children.
-        let mut aliases: Vec<usize> = Vec::with_capacity(names.len());
+        let mut aliases: Vec<NodeId> = Vec::with_capacity(names.len());
         let columns = last_output.clone_row_list()?;
         for (pos, name) in names.iter().enumerate() {
             let col_id = *columns.get(pos).ok_or_else(|| {
@@ -1988,7 +1996,7 @@ impl Plan {
     ///
     /// # Errors
     /// - node is not relational
-    pub fn get_relational_output(&self, rel_id: usize) -> Result<usize, SbroadError> {
+    pub fn get_relational_output(&self, rel_id: NodeId) -> Result<NodeId, SbroadError> {
         if let Node::Relational(rel) = self.get_node(rel_id)? {
             Ok(rel.output())
         } else {
@@ -2002,7 +2010,7 @@ impl Plan {
     /// - node is not relational
     /// - output is not `Expression::Row`
     /// - any node in the output tuple is not `Expression::Alias`
-    pub fn get_relational_aliases(&self, rel_id: usize) -> Result<Vec<SmolStr>, SbroadError> {
+    pub fn get_relational_aliases(&self, rel_id: NodeId) -> Result<Vec<SmolStr>, SbroadError> {
         let output = self.get_relational_output(rel_id)?;
         if let Expression::Row { list, .. } = self.get_expression_node(output)? {
             return list
@@ -2017,7 +2025,7 @@ impl Plan {
         Err(SbroadError::Invalid(
             Entity::Node,
             Some(format_smolstr!(
-                "expected output of Relational node {rel_id} to be Row"
+                "expected output of Relational node {rel_id:?} to be Row"
             )),
         ))
     }
@@ -2026,7 +2034,7 @@ impl Plan {
     ///
     /// # Errors
     /// - node is not relational
-    pub fn get_relational_children(&self, rel_id: usize) -> Result<Children<'_>, SbroadError> {
+    pub fn get_relational_children(&self, rel_id: NodeId) -> Result<Children<'_>, SbroadError> {
         if let Node::Relational(rel) = self.get_node(rel_id)? {
             Ok(rel.children())
         } else {
@@ -2044,9 +2052,9 @@ impl Plan {
     /// - node does not have child with specified index
     pub fn get_relational_child(
         &self,
-        rel_id: usize,
+        rel_id: NodeId,
         child_idx: usize,
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         if let Node::Relational(rel) = self.get_node(rel_id)? {
             return Ok(*rel.children().get(child_idx).ok_or_else(|| {
                 SbroadError::Invalid(
@@ -2059,7 +2067,7 @@ impl Plan {
         }
         Err(SbroadError::NotFound(
             Entity::Relational,
-            format_smolstr!("with id ({rel_id})"),
+            format_smolstr!("with id ({rel_id:?})"),
         ))
     }
 
@@ -2070,7 +2078,7 @@ impl Plan {
     ///
     /// # Errors
     /// - Node is not Join, Having, Selection
-    pub fn get_required_children_len(&self, node_id: usize) -> Result<usize, SbroadError> {
+    pub fn get_required_children_len(&self, node_id: NodeId) -> Result<usize, SbroadError> {
         let len = match self.get_relation_node(node_id)? {
             Relational::Join { .. } => 2,
             Relational::Having { .. } | Relational::Selection { .. } => 1,
@@ -2078,7 +2086,7 @@ impl Plan {
                 return Err(SbroadError::Invalid(
                     Entity::Node,
                     Some(format_smolstr!(
-                        "expected Join, Having or Selection on id ({node_id})"
+                        "expected Join, Having or Selection on id ({node_id:?})"
                     )),
                 ))
             }
@@ -2088,17 +2096,25 @@ impl Plan {
 
     /// Finds the parent of the given relational node.
     ///
+    /// # Panics
     /// # Errors
     /// - node is not relational
     /// - Plan has no top
-    pub fn find_parent_rel(&self, target_id: usize) -> Result<Option<usize>, SbroadError> {
+    pub fn find_parent_rel(&self, target_id: NodeId) -> Result<Option<NodeId>, SbroadError> {
         for (id, node) in self.nodes.iter().enumerate() {
             if !matches!(node, Node::Relational(_)) {
                 continue;
             }
-            for child_id in self.nodes.rel_iter(id) {
+
+            let rel_id = NodeId {
+                offset: u32::try_from(id).unwrap(),
+                arena_type: ArenaType::Default,
+            };
+
+            // It will work with one arena but fails with many. Needs to be refactored.
+            for child_id in self.nodes.rel_iter(rel_id) {
                 if child_id == target_id {
-                    return Ok(Some(id));
+                    return Ok(Some(rel_id));
                 }
             }
         }
@@ -2112,9 +2128,9 @@ impl Plan {
     /// - node does not have child with specified id
     pub fn change_child(
         &mut self,
-        parent_id: usize,
-        old_child_id: usize,
-        new_child_id: usize,
+        parent_id: NodeId,
+        old_child_id: NodeId,
+        new_child_id: NodeId,
     ) -> Result<(), SbroadError> {
         let node = self.get_mut_relation_node(parent_id)?;
         let children = node.mut_children();
@@ -2128,7 +2144,7 @@ impl Plan {
         Err(SbroadError::Invalid(
             Entity::Node,
             Some(format_smolstr!(
-                "node ({parent_id}) has no child with id ({old_child_id})"
+                "node ({parent_id:?}) has no child with id ({old_child_id:?})"
             )),
         ))
     }
@@ -2145,7 +2161,7 @@ impl Plan {
     pub fn table_position_map(
         &self,
         table_name: &str,
-        rel_id: usize,
+        rel_id: NodeId,
     ) -> Result<HashMap<ColumnPosition, ColumnPosition>, SbroadError> {
         let table = self.get_relation_or_error(table_name)?;
         let alias_to_pos = ColumnPositionMap::new(self, rel_id)?;
@@ -2168,10 +2184,10 @@ impl Plan {
     /// - node is scan (always a leaf node)
     pub fn set_relational_children(
         &mut self,
-        rel_id: usize,
-        children: Vec<usize>,
+        rel_id: NodeId,
+        children: Vec<NodeId>,
     ) -> Result<(), SbroadError> {
-        if let Node::Relational(ref mut rel) = self.nodes.get_mut(rel_id)? {
+        if let Some(Node::Relational(ref mut rel)) = self.nodes.get_mut(rel_id) {
             rel.set_children(children);
             return Ok(());
         }
@@ -2186,7 +2202,7 @@ impl Plan {
     /// # Errors
     /// - Failed to get plan top
     /// - Node returned by the relational iterator is not relational (bug)
-    pub fn is_additional_child(&self, node_id: usize) -> Result<bool, SbroadError> {
+    pub fn is_additional_child(&self, node_id: NodeId) -> Result<bool, SbroadError> {
         for node in &self.nodes {
             match node {
                 Node::Relational(
@@ -2213,8 +2229,8 @@ impl Plan {
     /// - If the node is not relational.
     pub fn is_additional_child_of_rel(
         &self,
-        rel_id: usize,
-        sq_id: usize,
+        rel_id: NodeId,
+        sq_id: NodeId,
     ) -> Result<bool, SbroadError> {
         let children = self.get_relational_children(rel_id)?;
         match self.get_relation_node(rel_id)? {
@@ -2232,7 +2248,7 @@ impl Plan {
     ///
     /// # Errors
     /// - node is not motion
-    pub fn get_motion_policy(&self, motion_id: usize) -> Result<&MotionPolicy, SbroadError> {
+    pub fn get_motion_policy(&self, motion_id: NodeId) -> Result<&MotionPolicy, SbroadError> {
         let node = self.get_relation_node(motion_id)?;
         if let Relational::Motion { policy, .. } = node {
             return Ok(policy);
diff --git a/sbroad-core/src/ir/operator/tests.rs b/sbroad-core/src/ir/operator/tests.rs
index 3c7bd2fc8..6bc322422 100644
--- a/sbroad-core/src/ir/operator/tests.rs
+++ b/sbroad-core/src/ir/operator/tests.rs
@@ -1,6 +1,3 @@
-use std::fs;
-use std::path::Path;
-
 use pretty_assertions::assert_eq;
 
 use crate::collection;
@@ -8,7 +5,6 @@ use crate::errors::{Entity, SbroadError};
 use crate::ir::distribution::{Distribution, Key};
 use crate::ir::expression::ColumnWithScan;
 use crate::ir::relation::{Column, ColumnRole, SpaceEngine, Table, Type};
-use crate::ir::tests::column_integer_user_non_null;
 use crate::ir::tests::{column_user_non_null, sharding_column};
 use crate::ir::value::Value;
 use crate::ir::{Node, Plan};
@@ -34,8 +30,14 @@ fn scan_rel() {
     .unwrap();
     plan.add_rel(t);
 
-    let scan_output = 8;
-    let scan_node = 9;
+    let scan_output = NodeId {
+        offset: 8,
+        arena_type: ArenaType::Default,
+    };
+    let scan_node = NodeId {
+        offset: 9,
+        arena_type: ArenaType::Default,
+    };
 
     let scan_id = plan.add_scan("t", None).unwrap();
     assert_eq!(scan_node, scan_id);
@@ -53,44 +55,6 @@ fn scan_rel() {
     }
 }
 
-#[test]
-fn scan_rel_serialized() {
-    let mut plan = Plan::default();
-
-    let t = Table::new_sharded(
-        "t",
-        vec![
-            column_user_non_null(SmolStr::from("a"), Type::Boolean),
-            column_user_non_null(SmolStr::from("b"), Type::Number),
-            column_user_non_null(SmolStr::from("c"), Type::String),
-            column_user_non_null(SmolStr::from("d"), Type::String),
-        ],
-        &["b", "a"],
-        &["b", "a"],
-        SpaceEngine::Memtx,
-    )
-    .unwrap();
-    plan.add_rel(t);
-
-    let scan_id = plan.add_scan("t", None).unwrap();
-    plan.top = Some(scan_id);
-
-    let scan_output = scan_id - 1;
-
-    plan.set_distribution(scan_output).unwrap();
-
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("operator")
-        .join("scan_rel.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    // This field is not serialized, do not check it
-    plan.context = None;
-    assert_eq!(plan, Plan::from_yaml(&s).unwrap());
-}
-
 #[test]
 fn projection() {
     let mut plan = Plan::default();
@@ -119,34 +83,32 @@ fn projection() {
             .unwrap_err()
     );
 
+    let mut test_node = NodeId {
+        offset: 1,
+        arena_type: ArenaType::Default,
+    };
+
     // Expression node instead of relational one
     assert_eq!(
         SbroadError::Invalid(
             Entity::Node,
-            Some("node is not Relational type: Expression(Alias { name: \"a\", child: 0 })".into())
+            Some("node is not Relational type: Expression(Alias { name: \"a\", child: NodeId { offset: 0, arena_type: Default } })".into())
         ),
-        plan.add_proj(1, &["a"], false, false).unwrap_err()
+        plan.add_proj(test_node, &["a"], false, false).unwrap_err()
     );
 
+    test_node.offset = 42;
+
     // Try to build projection from the non-existing node
     assert_eq!(
-        SbroadError::NotFound(Entity::Node, "from arena with index 42".to_smolstr()),
-        plan.add_proj(42, &["a"], false, false).unwrap_err()
+        SbroadError::NotFound(
+            Entity::Node,
+            "from Default arena with index 42".to_smolstr()
+        ),
+        plan.add_proj(test_node, &["a"], false, false).unwrap_err()
     );
 }
 
-#[test]
-fn projection_serialize() {
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("operator")
-        .join("projection.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    Plan::from_yaml(&s).unwrap();
-}
-
 #[test]
 fn selection() {
     // select * from t where (a, b) = (1, 10)
@@ -189,18 +151,6 @@ fn selection() {
     );
 }
 
-#[test]
-fn selection_serialize() {
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("operator")
-        .join("selection.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    Plan::from_yaml(&s).unwrap();
-}
-
 #[test]
 fn except() {
     let mut valid_plan = Plan::default();
@@ -417,87 +367,19 @@ fn sub_query() {
     plan.add_sub_query(scan_id, Some("sq")).unwrap();
 
     // Non-relational child node
-    let a = 1;
+    let a = NodeId {
+        offset: 1,
+        arena_type: ArenaType::Default,
+    };
     assert_eq!(
         SbroadError::Invalid(
             Entity::Node,
-            Some("node is not Relational type: Expression(Alias { name: \"a\", child: 0 })".into())
+            Some("node is not Relational type: Expression(Alias { name: \"a\", child: NodeId { offset: 0, arena_type: Default } })".into())
         ),
         plan.add_sub_query(a, Some("sq")).unwrap_err()
     );
 }
 
-#[test]
-fn sub_query_serialize() {
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("operator")
-        .join("sub_query.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    Plan::from_yaml(&s).unwrap();
-}
-
-#[test]
-#[allow(clippy::similar_names)]
-fn selection_with_sub_query() {
-    // t1(a int) key [a]
-    // t2(b int) key [b]
-    // select * from t1 where a = (select b from t2)
-
-    let mut plan = Plan::default();
-    let mut children: Vec<usize> = Vec::new();
-
-    let t1 = Table::new_sharded(
-        "t1",
-        vec![column_integer_user_non_null(SmolStr::from("a"))],
-        &["a"],
-        &["a"],
-        SpaceEngine::Memtx,
-    )
-    .unwrap();
-    plan.add_rel(t1);
-    let scan_t1_id = plan.add_scan("t1", None).unwrap();
-    children.push(scan_t1_id);
-
-    let t2 = Table::new_sharded(
-        "t2",
-        vec![column_integer_user_non_null(SmolStr::from("b"))],
-        &["b"],
-        &["b"],
-        SpaceEngine::Memtx,
-    )
-    .unwrap();
-    plan.add_rel(t2);
-    let scan_t2_id = plan.add_scan("t2", None).unwrap();
-    let proj_id = plan.add_proj(scan_t2_id, &["b"], false, false).unwrap();
-    let sq_id = plan.add_sub_query(proj_id, None).unwrap();
-    children.push(sq_id);
-
-    let b_id = plan
-        .add_row_from_subquery(&children[..], children.len() - 1, None)
-        .unwrap();
-    let a_id = plan.add_row_from_child(scan_t1_id, &["a"]).unwrap();
-    let eq_id = plan.add_cond(a_id, Bool::Eq, b_id).unwrap();
-
-    let select_id = plan.add_select(&children[..], eq_id).unwrap();
-    plan.set_top(select_id).unwrap();
-
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("operator")
-        .join("selection_with_sub_query.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    let expected_plan = Plan::from_yaml(&s).unwrap();
-
-    // This field is not serialized, do not check it
-    plan.context = None;
-    assert_eq!(expected_plan, plan);
-}
-
 #[test]
 fn join() {
     // t1(a, b), t2(c, d)
@@ -550,18 +432,6 @@ fn join() {
     plan.top = Some(join);
 }
 
-#[test]
-fn join_serialize() {
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("operator")
-        .join("join.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    Plan::from_yaml(&s).unwrap();
-}
-
 #[test]
 fn join_duplicate_columns() {
     // t1(a, b), t2(a, d)
diff --git a/sbroad-core/src/ir/parameters.rs b/sbroad-core/src/ir/parameters.rs
index d9418ba7f..eb5e001fc 100644
--- a/sbroad-core/src/ir/parameters.rs
+++ b/sbroad-core/src/ir/parameters.rs
@@ -5,8 +5,10 @@ use std::collections::HashMap;
 
 use crate::ir::Node;
 
+use super::expression::NodeId;
+
 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
-pub struct Parameters(HashMap<usize, Node>);
+pub struct Parameters(HashMap<NodeId, Node>);
 
 impl Default for Parameters {
     fn default() -> Self {
@@ -20,16 +22,16 @@ impl Parameters {
         Self(HashMap::new())
     }
 
-    pub fn insert(&mut self, index: usize, node: Node) {
+    pub fn insert(&mut self, index: NodeId, node: Node) {
         self.0.insert(index, node);
     }
 
     #[must_use]
-    pub fn get(&self, index: usize) -> Option<&Node> {
+    pub fn get(&self, index: NodeId) -> Option<&Node> {
         self.0.get(&index)
     }
 
-    pub fn drain(&mut self) -> HashMap<usize, Node> {
+    pub fn drain(&mut self) -> HashMap<NodeId, Node> {
         std::mem::take(&mut self.0)
     }
 }
diff --git a/sbroad-core/src/ir/relation/tests.rs b/sbroad-core/src/ir/relation/tests.rs
index 7873286bf..1ad4f143c 100644
--- a/sbroad-core/src/ir/relation/tests.rs
+++ b/sbroad-core/src/ir/relation/tests.rs
@@ -1,6 +1,3 @@
-use std::fs;
-use std::path::Path;
-
 use pretty_assertions::{assert_eq, assert_ne};
 
 use super::*;
@@ -156,105 +153,6 @@ fn table_seg_compound_type_in_key() {
     );
 }
 
-#[test]
-fn table_seg_serialized() {
-    let t = Table::new_sharded(
-        "t",
-        vec![
-            column_user_non_null(SmolStr::from("a"), Type::Boolean),
-            column_user_non_null(SmolStr::from("b"), Type::Number),
-            column_user_non_null(SmolStr::from("c"), Type::String),
-            column_user_non_null(SmolStr::from("d"), Type::String),
-            column_user_non_null(SmolStr::from("e"), Type::Integer),
-            column_user_non_null(SmolStr::from("f"), Type::Unsigned),
-        ],
-        &["a", "d"],
-        &["a", "d"],
-        SpaceEngine::Memtx,
-    )
-    .unwrap();
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("relation")
-        .join("table_seg_serialized.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    let t_yaml = Table::seg_from_yaml(&s).unwrap();
-    assert_eq!(t, t_yaml);
-}
-
-#[test]
-fn table_seg_serialized_duplicate_columns() {
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("relation")
-        .join("table_seg_serialized_duplicate_columns.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    assert_eq!(
-        Table::seg_from_yaml(&s).unwrap_err(),
-        SbroadError::DuplicatedValue(
-            "Table contains duplicate columns. Unable to convert to YAML.".into()
-        )
-    );
-}
-
-#[test]
-fn table_seg_serialized_out_of_range_key() {
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("relation")
-        .join("table_seg_serialized_out_of_range_key.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    assert_eq!(
-        Table::seg_from_yaml(&s).unwrap_err(),
-        SbroadError::Invalid(
-            Entity::Value,
-            Some("key positions must be less than 1".into())
-        )
-    );
-}
-
-#[test]
-fn table_seg_serialized_no_key() {
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("relation")
-        .join("table_seg_serialized_no_key.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    let t = Table::seg_from_yaml(&s);
-    assert_eq!(t.unwrap_err(), SbroadError::FailedTo(
-        Action::Serialize,
-        Some(Entity::Table),
-        "Message(\"invalid type: unit value, expected struct Key\", Some(Pos { marker: Marker { index: 121, line: 10, col: 18 }, path: \"kind.ShardedSpace.sharding_key\" }))".into()),
-    );
-}
-
-#[test]
-fn table_seg_serialized_no_columns() {
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("relation")
-        .join("table_seg_serialized_no_columns.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    assert_eq!(
-        Table::seg_from_yaml(&s).unwrap_err(),
-        SbroadError::FailedTo(
-            Action::Serialize,
-            Some(Entity::Table),
-            "Message(\"invalid type: unit value, expected a sequence\", Some(Pos { marker: Marker { index: 9, line: 1, col: 9 }, path: \"columns\" }))".into()
-        )
-    );
-}
-
 #[test]
 fn column_msgpack_serialize() {
     let c = column_user_non_null(SmolStr::from("name"), Type::Boolean);
diff --git a/sbroad-core/src/ir/tests.rs b/sbroad-core/src/ir/tests.rs
index 0de64fd0f..b807ac665 100644
--- a/sbroad-core/src/ir/tests.rs
+++ b/sbroad-core/src/ir/tests.rs
@@ -2,8 +2,6 @@ use super::*;
 use crate::ir::relation::{Column, ColumnRole, SpaceEngine, Table, Type};
 use pretty_assertions::assert_eq;
 use smol_str::SmolStr;
-use std::fs;
-use std::path::Path;
 
 /// Helper function to create `Column` object with given name and default:
 ///
@@ -53,34 +51,6 @@ pub fn sharding_column() -> Column {
     }
 }
 
-#[test]
-fn plan_no_top() {
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("plan_no_top.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    assert_eq!(
-        SbroadError::Invalid(Entity::Plan, Some("plan tree top is None".into())),
-        Plan::from_yaml(&s).unwrap_err()
-    );
-}
-
-#[test]
-fn plan_oor_top() {
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("plan_oor_top.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    assert_eq!(
-        SbroadError::NotFound(Entity::Node, "from arena with index 42".into()),
-        Plan::from_yaml(&s).unwrap_err()
-    );
-}
-
 #[test]
 fn get_node() {
     let mut plan = Plan::default();
@@ -110,8 +80,12 @@ fn get_node() {
 fn get_node_oor() {
     let plan = Plan::default();
     assert_eq!(
-        SbroadError::NotFound(Entity::Node, "from arena with index 42".into()),
-        plan.get_node(42).unwrap_err()
+        SbroadError::NotFound(Entity::Node, "from Default arena with index 42".into()),
+        plan.get_node(NodeId {
+            offset: 42,
+            arena_type: ArenaType::Default
+        })
+        .unwrap_err()
     );
 }
 
diff --git a/sbroad-core/src/ir/transformation.rs b/sbroad-core/src/ir/transformation.rs
index c85d27d55..b7779f0e2 100644
--- a/sbroad-core/src/ir/transformation.rs
+++ b/sbroad-core/src/ir/transformation.rs
@@ -12,6 +12,7 @@ pub mod split_columns;
 
 use smol_str::format_smolstr;
 
+use super::expression::NodeId;
 use super::tree::traversal::{PostOrder, PostOrderWithFilter, EXPR_CAPACITY};
 use crate::errors::{Entity, SbroadError};
 use crate::ir::expression::Expression;
@@ -19,7 +20,7 @@ use crate::ir::operator::{Bool, Relational};
 use crate::ir::{Node, Plan};
 use std::collections::HashMap;
 
-pub type ExprId = usize;
+pub type ExprId = NodeId;
 /// Helper struct representing map of (`old_expr_id` -> `changed_expr_id`).
 struct OldNewExpressionMap {
     inner: HashMap<ExprId, ExprId>,
@@ -32,11 +33,11 @@ impl OldNewExpressionMap {
         }
     }
 
-    fn insert(&mut self, old_id: usize, new_id: usize) {
+    fn insert(&mut self, old_id: ExprId, new_id: ExprId) {
         self.inner.insert(old_id, new_id);
     }
 
-    fn replace(&self, child: &mut usize) {
+    fn replace(&self, child: &mut ExprId) {
         if let Some(new_id) = self.inner.get(child) {
             *child = *new_id;
         }
@@ -46,7 +47,7 @@ impl OldNewExpressionMap {
         self.inner.is_empty()
     }
 
-    fn get(&self, key: usize) -> Option<&ExprId> {
+    fn get(&self, key: ExprId) -> Option<&ExprId> {
         self.inner.get(&key)
     }
 
@@ -70,9 +71,9 @@ impl Plan {
     /// - If the left or right child is not a trivalent.
     pub fn concat_and(
         &mut self,
-        left_expr_id: usize,
-        right_expr_id: usize,
-    ) -> Result<usize, SbroadError> {
+        left_expr_id: NodeId,
+        right_expr_id: NodeId,
+    ) -> Result<NodeId, SbroadError> {
         if !self.is_trivalent(left_expr_id)? {
             return Err(SbroadError::Invalid(
                 Entity::Expression,
@@ -100,9 +101,9 @@ impl Plan {
     /// - If the left or right child is not a trivalent.
     pub fn concat_or(
         &mut self,
-        left_expr_id: usize,
-        right_expr_id: usize,
-    ) -> Result<usize, SbroadError> {
+        left_expr_id: NodeId,
+        right_expr_id: NodeId,
+    ) -> Result<NodeId, SbroadError> {
         if !self.is_trivalent(left_expr_id)? {
             return Err(SbroadError::Invalid(
                 Entity::Expression,
@@ -137,8 +138,9 @@ impl Plan {
         let mut ir_tree = PostOrder::with_capacity(|node| self.nodes.rel_iter(node), EXPR_CAPACITY);
         ir_tree.populate_nodes(top_id);
         let nodes = ir_tree.take_nodes();
-        for (_, id) in &nodes {
-            let rel = self.get_relation_node(*id)?;
+        for level_node in &nodes {
+            let id = level_node.1;
+            let rel = self.get_relation_node(id)?;
             let (old_tree_id, new_tree_id) = match rel {
                 Relational::Selection {
                     filter: tree_id, ..
@@ -151,7 +153,7 @@ impl Plan {
             if old_tree_id != new_tree_id {
                 self.undo.add(new_tree_id, old_tree_id);
             }
-            let rel = self.get_mut_relation_node(*id)?;
+            let rel = self.get_mut_relation_node(id)?;
             match rel {
                 Relational::Selection {
                     filter: tree_id, ..
@@ -175,7 +177,7 @@ impl Plan {
     #[allow(clippy::too_many_lines)]
     pub fn expr_tree_replace_bool(
         &mut self,
-        top_id: usize,
+        top_id: NodeId,
         f: TransformFunction,
         ops: &[Bool],
     ) -> Result<OldNewTopIdPair, SbroadError> {
@@ -183,7 +185,7 @@ impl Plan {
         // Note, that filter accepts nodes:
         // * On which we'd like to apply transformation
         // * That will contain transformed nodes as children
-        let filter = |node_id: usize| -> bool {
+        let filter = |node_id: NodeId| -> bool {
             if let Ok(Node::Expression(
                 Expression::Bool { .. }
                 | Expression::ExprInParentheses { .. }
@@ -208,13 +210,14 @@ impl Plan {
         subtree.populate_nodes(top_id);
         let nodes = subtree.take_nodes();
         drop(subtree);
-        for (_, id) in &nodes {
-            let expr = self.get_expression_node(*id)?;
+        for level_node in &nodes {
+            let id = level_node.1;
+            let expr = self.get_expression_node(id)?;
             if let Expression::Bool { op, .. } = expr {
                 if ops.contains(op) || ops.is_empty() {
-                    let (old_top_id, new_top_id) = f(self, *id)?;
+                    let (old_top_id, new_top_id) = f(self, id)?;
                     if old_top_id != new_top_id {
-                        map.insert(*id, new_top_id);
+                        map.insert(id, new_top_id);
                     }
                 }
             }
@@ -228,8 +231,9 @@ impl Plan {
                 self.clone_expr_subtree(top_id)?
             };
             let mut new_top_id = top_id;
-            for (_, id) in &nodes {
-                let expr = self.get_mut_expression_node(*id)?;
+            for level_node in &nodes {
+                let id = level_node.1;
+                let expr = self.get_mut_expression_node(id)?;
                 // For all expressions in the subtree tries to replace their children
                 // with the new nodes from the map.
                 //
diff --git a/sbroad-core/src/ir/transformation/bool_in.rs b/sbroad-core/src/ir/transformation/bool_in.rs
index 191cfbb7d..be1bb8c5b 100644
--- a/sbroad-core/src/ir/transformation/bool_in.rs
+++ b/sbroad-core/src/ir/transformation/bool_in.rs
@@ -11,7 +11,7 @@
 //! ```
 
 use crate::errors::{Entity, SbroadError};
-use crate::ir::expression::Expression;
+use crate::ir::expression::{Expression, NodeId};
 use crate::ir::operator::Bool;
 use crate::ir::transformation::OldNewTopIdPair;
 use crate::ir::Plan;
@@ -22,18 +22,18 @@ use smol_str::format_smolstr;
 /// Replace IN operator with the chain of the OR-ed equalities in the expression tree.
 fn call_expr_tree_replace_in(
     plan: &mut Plan,
-    top_id: usize,
+    top_id: NodeId,
 ) -> Result<OldNewTopIdPair, SbroadError> {
     plan.expr_tree_replace_bool(top_id, &call_from_in, &[Bool::In])
 }
 
-fn call_from_in(plan: &mut Plan, top_id: usize) -> Result<OldNewTopIdPair, SbroadError> {
+fn call_from_in(plan: &mut Plan, top_id: NodeId) -> Result<OldNewTopIdPair, SbroadError> {
     plan.in_to_or(top_id)
 }
 
 impl Plan {
     /// Convert the IN operator to the chain of the OR-ed equalities.
-    fn in_to_or(&mut self, top_id: usize) -> Result<OldNewTopIdPair, SbroadError> {
+    fn in_to_or(&mut self, top_id: NodeId) -> Result<OldNewTopIdPair, SbroadError> {
         let top_expr = self.get_expression_node(top_id)?;
         let (left_id, right_id) = match top_expr {
             Expression::Bool {
diff --git a/sbroad-core/src/ir/transformation/dnf.rs b/sbroad-core/src/ir/transformation/dnf.rs
index 6a53adc6d..0cc77cedd 100644
--- a/sbroad-core/src/ir/transformation/dnf.rs
+++ b/sbroad-core/src/ir/transformation/dnf.rs
@@ -71,7 +71,7 @@
 //! ```
 
 use crate::errors::{Entity, SbroadError};
-use crate::ir::expression::Expression;
+use crate::ir::expression::{Expression, NodeId};
 use crate::ir::operator::Bool;
 use crate::ir::transformation::OldNewTopIdPair;
 use crate::ir::{Node, Plan};
@@ -82,12 +82,12 @@ use std::collections::VecDeque;
 /// A chain of the trivalents (boolean or NULL expressions) concatenated by AND.
 #[derive(Clone, Debug)]
 pub struct Chain {
-    nodes: VecDeque<usize>,
+    nodes: VecDeque<NodeId>,
 }
 
 /// Helper function to identify whether we are dealing with AND/OR operator that
 /// may be covered with parentheses.
-fn optionally_covered_and_or(expr_id: usize, plan: &Plan) -> Result<Option<usize>, SbroadError> {
+fn optionally_covered_and_or(expr_id: NodeId, plan: &Plan) -> Result<Option<NodeId>, SbroadError> {
     let expr = plan.get_expression_node(expr_id)?;
     let and_or = match expr {
         Expression::Bool { op, .. } => {
@@ -120,7 +120,7 @@ impl Chain {
 
     /// Append a new node to the chain. Keep AND and OR nodes in the back,
     /// while other nodes in the front of the chain queue.
-    fn push(&mut self, expr_id: usize, plan: &Plan) -> Result<(), SbroadError> {
+    fn push(&mut self, expr_id: NodeId, plan: &Plan) -> Result<(), SbroadError> {
         let and_or = optionally_covered_and_or(expr_id, plan)?;
         if let Some(and_or_id) = and_or {
             self.nodes.push_back(and_or_id);
@@ -131,7 +131,7 @@ impl Chain {
     }
 
     /// Pop AND and OR nodes (we append them to the back).
-    fn pop_back(&mut self, plan: &Plan) -> Result<Option<usize>, SbroadError> {
+    fn pop_back(&mut self, plan: &Plan) -> Result<Option<NodeId>, SbroadError> {
         if let Some(expr_id) = self.nodes.back() {
             let expr = plan.get_expression_node(*expr_id)?;
             if let Expression::Bool {
@@ -146,13 +146,13 @@ impl Chain {
     }
 
     /// Pop trivalent nodes other than AND and OR (we keep then in the front).
-    fn pop_front(&mut self) -> Option<usize> {
+    fn pop_front(&mut self) -> Option<NodeId> {
         self.nodes.pop_front()
     }
 
     /// Convert a chain to a new expression tree (reuse trivalent expressions).
-    fn as_plan(&mut self, plan: &mut Plan) -> Result<usize, SbroadError> {
-        let mut top_id: Option<usize> = None;
+    fn as_plan(&mut self, plan: &mut Plan) -> Result<NodeId, SbroadError> {
+        let mut top_id: Option<NodeId> = None;
         while let Some(expr_id) = self.pop_front() {
             match top_id {
                 None => {
@@ -170,12 +170,12 @@ impl Chain {
     }
 
     /// Return a mutable reference to the chain nodes.
-    pub fn get_mut_nodes(&mut self) -> &mut VecDeque<usize> {
+    pub fn get_mut_nodes(&mut self) -> &mut VecDeque<NodeId> {
         &mut self.nodes
     }
 }
 
-fn call_expr_tree_to_dnf(plan: &mut Plan, top_id: usize) -> Result<OldNewTopIdPair, SbroadError> {
+fn call_expr_tree_to_dnf(plan: &mut Plan, top_id: NodeId) -> Result<OldNewTopIdPair, SbroadError> {
     plan.expr_tree_to_dnf(top_id)
 }
 
@@ -185,7 +185,7 @@ impl Plan {
     /// # Errors
     /// - If the expression tree is not a trivalent expression.
     /// - Failed to append node to the AND chain.
-    pub fn get_dnf_chains(&self, top_id: usize) -> Result<VecDeque<Chain>, SbroadError> {
+    pub fn get_dnf_chains(&self, top_id: NodeId) -> Result<VecDeque<Chain>, SbroadError> {
         let capacity: usize = self.nodes.arena.iter().fold(0_usize, |acc, node| {
             acc + match node {
                 Node::Expression(Expression::Bool {
@@ -244,10 +244,10 @@ impl Plan {
     /// - Failed to retrieve DNF chains.
     /// - Failed to convert the AND chain to a new expression tree.
     /// - Failed to concatenate the AND expression trees to the OR tree.
-    pub fn expr_tree_to_dnf(&mut self, top_id: usize) -> Result<OldNewTopIdPair, SbroadError> {
+    pub fn expr_tree_to_dnf(&mut self, top_id: NodeId) -> Result<OldNewTopIdPair, SbroadError> {
         let mut result = self.get_dnf_chains(top_id)?;
 
-        let mut new_top_id: Option<usize> = None;
+        let mut new_top_id: Option<NodeId> = None;
         while let Some(mut chain) = result.pop_front() {
             let ir_chain_top = chain.as_plan(self)?;
             new_top_id = match new_top_id {
diff --git a/sbroad-core/src/ir/transformation/equality_propagation.rs b/sbroad-core/src/ir/transformation/equality_propagation.rs
index 6ea30eb01..dc7132d93 100644
--- a/sbroad-core/src/ir/transformation/equality_propagation.rs
+++ b/sbroad-core/src/ir/transformation/equality_propagation.rs
@@ -90,7 +90,7 @@
 //!    to the plan tree.
 
 use crate::errors::{Entity, SbroadError};
-use crate::ir::expression::Expression;
+use crate::ir::expression::{Expression, NodeId};
 use crate::ir::helpers::RepeatableState;
 use crate::ir::operator::Bool;
 use crate::ir::relation::Type;
@@ -109,7 +109,7 @@ use std::collections::{HashMap, HashSet};
 struct EqClassRef {
     targets: Option<Vec<usize>>,
     position: usize,
-    parent: Option<usize>,
+    parent: Option<NodeId>,
     col_type: Type,
 }
 
@@ -132,7 +132,7 @@ impl EqClassRef {
         Err(SbroadError::Invalid(Entity::Expression, None))
     }
 
-    fn to_single_col_row(&self, plan: &mut Plan) -> usize {
+    fn to_single_col_row(&self, plan: &mut Plan) -> NodeId {
         let id = plan.nodes.add_ref(
             self.parent,
             self.targets.clone(),
@@ -168,7 +168,7 @@ impl EqClassConst {
         ))
     }
 
-    fn to_const(&self, plan: &mut Plan) -> usize {
+    fn to_const(&self, plan: &mut Plan) -> NodeId {
         let const_id = plan.add_const(self.value.clone());
         plan.nodes.add_row(vec![const_id], None)
     }
@@ -182,7 +182,7 @@ enum EqClassExpr {
 }
 
 impl EqClassExpr {
-    fn to_plan(&self, plan: &mut Plan) -> usize {
+    fn to_plan(&self, plan: &mut Plan) -> NodeId {
         match self {
             EqClassExpr::EqClassConst(ec_const) => ec_const.to_const(plan),
             EqClassExpr::EqClassRef(ec_ref) => ec_ref.to_single_col_row(plan),
@@ -353,15 +353,15 @@ impl EqClassChain {
 /// Replace IN operator with the chain of the OR-ed equalities in the expression tree.
 fn call_expr_tree_derive_equalities(
     plan: &mut Plan,
-    top_id: usize,
+    top_id: NodeId,
 ) -> Result<OldNewTopIdPair, SbroadError> {
     plan.expr_tree_modify_and_chains(top_id, &call_build_and_chains, &call_as_plan)
 }
 
 fn call_build_and_chains(
     plan: &mut Plan,
-    nodes: &[usize],
-) -> Result<HashMap<usize, Chain, RepeatableState>, SbroadError> {
+    nodes: &[NodeId],
+) -> Result<HashMap<NodeId, Chain, RepeatableState>, SbroadError> {
     let mut chains = plan.populate_and_chains(nodes)?;
     for chain in chains.values_mut() {
         chain.extend_equality_operator(plan)?;
@@ -369,7 +369,7 @@ fn call_build_and_chains(
     Ok(chains)
 }
 
-fn call_as_plan(chain: &Chain, plan: &mut Plan) -> Result<usize, SbroadError> {
+fn call_as_plan(chain: &Chain, plan: &mut Plan) -> Result<NodeId, SbroadError> {
     chain.as_plan_ecs(plan)
 }
 
@@ -409,7 +409,7 @@ impl Chain {
         Ok(())
     }
 
-    fn as_plan_ecs(&self, plan: &mut Plan) -> Result<usize, SbroadError> {
+    fn as_plan_ecs(&self, plan: &mut Plan) -> Result<NodeId, SbroadError> {
         let other_top_id = match self.get_other().split_first() {
             Some((first, other)) => {
                 let mut top_id = *first;
@@ -424,7 +424,7 @@ impl Chain {
         // Chain is grouped by the operators in the hash map.
         // To make serialization non-flaky, we extract operators
         // in a deterministic order.
-        let mut grouped_top_id: Option<usize> = None;
+        let mut grouped_top_id: Option<NodeId> = None;
         // No need for "And" and "Or" operators.
         let ordered_ops = &[
             Bool::Eq,
@@ -441,7 +441,7 @@ impl Chain {
                     .iter()
                     .zip(right_vec.iter())
                     .map(|(l, r)| (*l, *r))
-                    .collect::<Vec<(usize, usize)>>()
+                    .collect::<Vec<(NodeId, NodeId)>>()
                     .split_first()
                 {
                     let left_row_id = plan.nodes.add_row(vec![first.0], None);
@@ -481,7 +481,7 @@ impl Chain {
 impl Plan {
     // DoSkip is a special case of an error - nothing bad had happened, the target node doesn't contain
     // anything interesting for us, skip it without any serious error.
-    fn try_to_eq_class_expr(&self, expr_id: usize) -> Result<EqClassExpr, SbroadError> {
+    fn try_to_eq_class_expr(&self, expr_id: NodeId) -> Result<EqClassExpr, SbroadError> {
         let expr = self.get_expression_node(expr_id)?;
         match expr {
             Expression::Constant { .. } => {
diff --git a/sbroad-core/src/ir/transformation/equality_propagation/tests.rs b/sbroad-core/src/ir/transformation/equality_propagation/tests.rs
index ba9182fe2..83f6bfa30 100644
--- a/sbroad-core/src/ir/transformation/equality_propagation/tests.rs
+++ b/sbroad-core/src/ir/transformation/equality_propagation/tests.rs
@@ -18,7 +18,7 @@ fn equality_propagation1() {
             "{} {} {}",
             r#"SELECT "t"."a" FROM "t""#,
             r#"WHERE ("t"."c") = (?) and ("t"."a") = (?) and ("t"."b") = (?)"#,
-            r#"and ("t"."c") = ("t"."a") or ("t"."d") = (?)"#,
+            r#"and ("t"."a") = ("t"."c") or ("t"."d") = (?)"#,
         ),
         vec![
             Value::from(1_u64),
@@ -107,8 +107,8 @@ fn equality_propagation5() {
             r#"SELECT "t"."a" FROM "t""#,
             r#"WHERE ("t"."d") = (?) and ("t"."c") = (?)"#,
             r#"and ("t"."a") = (?) and ("t"."b") = (?)"#,
-            r#"and ("t"."c") = ("t"."a") and ("t"."a") = ("t"."d")"#,
-            r#"and ("t"."d") = ("t"."b")"#,
+            r#"and ("t"."a") = ("t"."b") and ("t"."b") = ("t"."c")"#,
+            r#"and ("t"."c") = ("t"."d")"#,
         ),
         vec![
             Value::from(1_u64),
diff --git a/sbroad-core/src/ir/transformation/merge_tuples.rs b/sbroad-core/src/ir/transformation/merge_tuples.rs
index bb2fb2f83..30f76a9de 100644
--- a/sbroad-core/src/ir/transformation/merge_tuples.rs
+++ b/sbroad-core/src/ir/transformation/merge_tuples.rs
@@ -11,7 +11,7 @@
 //! ```
 
 use crate::errors::{Entity, SbroadError};
-use crate::ir::expression::Expression;
+use crate::ir::expression::{Expression, NodeId};
 use crate::ir::helpers::RepeatableState;
 use crate::ir::operator::Bool;
 use crate::ir::transformation::OldNewTopIdPair;
@@ -25,19 +25,19 @@ use std::collections::{hash_map::Entry, HashMap, HashSet};
 
 fn call_expr_tree_merge_tuples(
     plan: &mut Plan,
-    top_id: usize,
+    top_id: NodeId,
 ) -> Result<OldNewTopIdPair, SbroadError> {
     plan.expr_tree_modify_and_chains(top_id, &call_build_and_chains, &call_as_plan)
 }
 
 fn call_build_and_chains(
     plan: &mut Plan,
-    nodes: &[usize],
-) -> Result<HashMap<usize, Chain, RepeatableState>, SbroadError> {
+    nodes: &[NodeId],
+) -> Result<HashMap<NodeId, Chain, RepeatableState>, SbroadError> {
     plan.populate_and_chains(nodes)
 }
 
-fn call_as_plan(chain: &Chain, plan: &mut Plan) -> Result<usize, SbroadError> {
+fn call_as_plan(chain: &Chain, plan: &mut Plan) -> Result<NodeId, SbroadError> {
     chain.as_plan(plan)
 }
 
@@ -46,9 +46,9 @@ fn call_as_plan(chain: &Chain, plan: &mut Plan) -> Result<usize, SbroadError> {
 pub struct Chain {
     // Left and right sides of the equality (and non-equality) expressions
     // grouped by the operator.
-    grouped: HashMap<Bool, (Vec<usize>, Vec<usize>)>,
+    grouped: HashMap<Bool, (Vec<NodeId>, Vec<NodeId>)>,
     // Other boolean expressions in the AND chain (true, false, null, Lt, GtEq, etc).
-    other: Vec<usize>,
+    other: Vec<NodeId>,
 }
 
 impl Chain {
@@ -67,7 +67,7 @@ impl Chain {
     /// - Failed if the node is not an expression.
     /// - Failed if expression is not an "AND" or "OR".
     /// - There is something wrong with our sub-queries.
-    pub fn insert(&mut self, plan: &mut Plan, expr_id: usize) -> Result<(), SbroadError> {
+    pub fn insert(&mut self, plan: &mut Plan, expr_id: NodeId) -> Result<(), SbroadError> {
         let bool_expr = plan.get_expression_node(expr_id)?;
         if let Expression::Bool { left, op, right } = bool_expr {
             if let Bool::And | Bool::Or = op {
@@ -143,7 +143,7 @@ impl Chain {
         Ok(())
     }
 
-    fn as_plan(&self, plan: &mut Plan) -> Result<usize, SbroadError> {
+    fn as_plan(&self, plan: &mut Plan) -> Result<NodeId, SbroadError> {
         let other_top_id = match self.other.split_first() {
             Some((first, other)) => {
                 let mut top_id = *first;
@@ -158,7 +158,7 @@ impl Chain {
         // Chain is grouped by the operators in the hash map.
         // To make serialization non-flaky, we extract operators
         // in a deterministic order.
-        let mut grouped_top_id: Option<usize> = None;
+        let mut grouped_top_id: Option<NodeId> = None;
         let ordered_ops = &[Bool::Eq, Bool::NotEq];
         for op in ordered_ops {
             if let Some((left, right)) = self.grouped.get(op) {
@@ -193,19 +193,19 @@ impl Chain {
 
     /// Return boolean expression nodes grouped by the operator.
     #[must_use]
-    pub fn get_grouped(&self) -> &HashMap<Bool, (Vec<usize>, Vec<usize>)> {
+    pub fn get_grouped(&self) -> &HashMap<Bool, (Vec<NodeId>, Vec<NodeId>)> {
         &self.grouped
     }
 
     /// Return "other" boolean expression nodes.
     #[must_use]
-    pub fn get_other(&self) -> &Vec<usize> {
+    pub fn get_other(&self) -> &Vec<NodeId> {
         &self.other
     }
 }
 
 impl Plan {
-    fn get_columns_or_self(&self, expr_id: usize) -> Result<Vec<usize>, SbroadError> {
+    fn get_columns_or_self(&self, expr_id: NodeId) -> Result<Vec<NodeId>, SbroadError> {
         let expr = self.get_expression_node(expr_id)?;
         match expr {
             Expression::Row { list, .. } => Ok(list.clone()),
@@ -220,10 +220,10 @@ impl Plan {
     /// - Failed to insert the node to the "And" chain.
     pub fn populate_and_chains(
         &mut self,
-        nodes: &[usize],
-    ) -> Result<HashMap<usize, Chain, RepeatableState>, SbroadError> {
-        let mut visited: HashSet<usize> = HashSet::with_capacity(self.nodes.next_id());
-        let mut chains: HashMap<usize, Chain, RepeatableState> =
+        nodes: &[NodeId],
+    ) -> Result<HashMap<NodeId, Chain, RepeatableState>, SbroadError> {
+        let mut visited: HashSet<NodeId> = HashSet::with_capacity(self.nodes.len());
+        let mut chains: HashMap<NodeId, Chain, RepeatableState> =
             HashMap::with_capacity_and_hasher(nodes.len(), RepeatableState);
 
         for id in nodes {
@@ -237,8 +237,9 @@ impl Plan {
                 EXPR_CAPACITY,
                 EXPR_CAPACITY,
             );
-            let nodes_and: Vec<usize> = tree_and.iter(*id).map(|(_, id)| id).collect();
-            let mut nodes_for_chain: Vec<usize> = Vec::with_capacity(nodes_and.len());
+            let nodes_and: Vec<NodeId> =
+                tree_and.iter(*id).map(|level_node| level_node.1).collect();
+            let mut nodes_for_chain: Vec<NodeId> = Vec::with_capacity(nodes_and.len());
             for and_id in nodes_and {
                 let expr = self.get_expression_node(and_id)?;
                 if let Expression::Bool {
@@ -285,20 +286,20 @@ impl Plan {
     #[allow(clippy::type_complexity, clippy::too_many_lines)]
     pub fn expr_tree_modify_and_chains(
         &mut self,
-        expr_id: usize,
+        expr_id: NodeId,
         f_build_chains: &dyn Fn(
             &mut Plan,
-            &[usize],
+            &[NodeId],
         )
-            -> Result<HashMap<usize, Chain, RepeatableState>, SbroadError>,
-        f_to_plan: &dyn Fn(&Chain, &mut Plan) -> Result<usize, SbroadError>,
+            -> Result<HashMap<NodeId, Chain, RepeatableState>, SbroadError>,
+        f_to_plan: &dyn Fn(&Chain, &mut Plan) -> Result<NodeId, SbroadError>,
     ) -> Result<OldNewTopIdPair, SbroadError> {
         let mut tree = BreadthFirst::with_capacity(
             |node| self.nodes.expr_iter(node, false),
             EXPR_CAPACITY,
             EXPR_CAPACITY,
         );
-        let nodes: Vec<usize> = tree.iter(expr_id).map(|(_, id)| id).collect();
+        let nodes: Vec<NodeId> = tree.iter(expr_id).map(|level_node| level_node.1).collect();
         let chains = f_build_chains(self, &nodes)?;
 
         // Replace nodes' children with the merged tuples.
diff --git a/sbroad-core/src/ir/transformation/not_push_down.rs b/sbroad-core/src/ir/transformation/not_push_down.rs
index 867fb970e..06a672118 100644
--- a/sbroad-core/src/ir/transformation/not_push_down.rs
+++ b/sbroad-core/src/ir/transformation/not_push_down.rs
@@ -5,7 +5,7 @@
 //! * To:   `select * from "t" where "a" = 1 and "b" = 2`
 
 use crate::errors::{Entity, SbroadError};
-use crate::ir::expression::Expression;
+use crate::ir::expression::{Expression, NodeId};
 use crate::ir::operator::{Bool, Unary};
 use crate::ir::transformation::{OldNewExpressionMap, OldNewTopIdPair};
 use crate::ir::tree::traversal::{PostOrderWithFilter, EXPR_CAPACITY};
@@ -36,19 +36,19 @@ enum NotState {
     /// * cast -> On { parent_not_op: None } -> don't change self, because we don't know, what
     ///           value will cast return. As soon as parent_not_op is None, create one as a parent
     ///           node.
-    On { parent_not_op: Option<usize> },
+    On { parent_not_op: Option<NodeId> },
 }
 
 impl NotState {
     /// Create new `NotState::On`.
-    fn on(parent_not_op: Option<usize>) -> Self {
+    fn on(parent_not_op: Option<NodeId>) -> Self {
         NotState::On { parent_not_op }
     }
 }
 
 fn call_expr_tree_not_push_down(
     plan: &mut Plan,
-    top_id: usize,
+    top_id: NodeId,
 ) -> Result<OldNewTopIdPair, SbroadError> {
     // Because of the borrow checker we can't change `Bool` and `Row` children during recursive
     // traversal and have to do it using this map after transformation.
@@ -60,7 +60,7 @@ fn call_expr_tree_not_push_down(
         (top_id, top_id)
     } else {
         let old_top_id = plan.clone_expr_subtree(top_id)?;
-        let filter = |node_id: usize| -> bool {
+        let filter = |node_id: NodeId| -> bool {
             matches!(
                 plan.get_node(node_id),
                 Ok(Node::Expression(
@@ -78,8 +78,9 @@ fn call_expr_tree_not_push_down(
         subtree.populate_nodes(top_id);
         let nodes = subtree.take_nodes();
         drop(subtree);
-        for (_, id) in &nodes {
-            let expr = plan.get_mut_expression_node(*id)?;
+        for level_node in &nodes {
+            let id = level_node.1;
+            let expr = plan.get_mut_expression_node(id)?;
             match expr {
                 Expression::ExprInParentheses { child } => {
                     old_new_expression_map.replace(child);
@@ -131,9 +132,9 @@ impl Plan {
     /// * `not_state` is on and parent `Not` operator is absent  -> create new not node.
     fn cover_with_not(
         &mut self,
-        expr_id: usize,
+        expr_id: NodeId,
         not_state: &NotState,
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         if let NotState::On { parent_not_op } = not_state {
             if let Some(parent_not_op) = parent_not_op {
                 let parent_not_expr = self.get_mut_expression_node(*parent_not_op)?;
@@ -166,10 +167,10 @@ impl Plan {
     /// Recursive push down of `Not` operator.
     fn push_down_not_for_expression(
         &mut self,
-        expr_id: usize,
+        expr_id: NodeId,
         not_state: NotState,
         map: &mut OldNewExpressionMap,
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         let expr = self.get_expression_node(expr_id)?;
         let new_expr_id = match expr {
             Expression::ExprInParentheses { child } => {
diff --git a/sbroad-core/src/ir/transformation/redistribution.rs b/sbroad-core/src/ir/transformation/redistribution.rs
index afd9e0f48..1832013da 100644
--- a/sbroad-core/src/ir/transformation/redistribution.rs
+++ b/sbroad-core/src/ir/transformation/redistribution.rs
@@ -10,8 +10,8 @@ use crate::errors::{Action, Entity, SbroadError};
 use crate::frontend::sql::ir::SubtreeCloner;
 use crate::ir::api::children::Children;
 use crate::ir::distribution::{Distribution, Key, KeySet};
-use crate::ir::expression::ColumnPositionMap;
 use crate::ir::expression::Expression;
+use crate::ir::expression::{ColumnPositionMap, NodeId};
 use crate::ir::operator::{Bool, JoinKind, Relational, Unary, UpdateStrategy};
 
 use crate::ir::transformation::redistribution::eq_cols::EqualityCols;
@@ -131,12 +131,12 @@ pub enum MotionOpcode {
     /// Call `rearrange_for_update` method on vtable and then call `set_update_delete_tuple_len`
     /// on `Update` relational node.
     RearrangeForShardedUpdate {
-        update_id: usize,
+        update_id: NodeId,
         old_shard_columns_len: usize,
         new_shard_columns_positions: Vec<ColumnPosition>,
     },
     AddMissingRowsForLeftJoin {
-        motion_id: usize,
+        motion_id: NodeId,
     },
     /// When set to `true` this opcode serializes motion subtree to sql that produces
     /// empty table.
@@ -154,13 +154,13 @@ pub enum MotionOpcode {
 
 /// Helper struct that unwraps `Expression::Bool` fields.
 struct BoolOp {
-    left: usize,
+    left: NodeId,
     op: Bool,
-    right: usize,
+    right: NodeId,
 }
 
 impl BoolOp {
-    fn from_expr(plan: &Plan, expr_id: usize) -> Result<Self, SbroadError> {
+    fn from_expr(plan: &Plan, expr_id: NodeId) -> Result<Self, SbroadError> {
         if let Expression::Bool {
             left, op, right, ..
         } = plan.get_expression_node(expr_id)?
@@ -197,19 +197,19 @@ impl Program {
     }
 }
 
-type ChildId = usize;
+type ChildId = NodeId;
 type DataTransformation = (MotionPolicy, Program);
 
 /// Helper struct to store motion policy for every child of
 /// relational node with `parent_id`.
 #[derive(Debug)]
 struct Strategy {
-    parent_id: usize,
+    parent_id: NodeId,
     children_policy: HashMap<ChildId, DataTransformation>,
 }
 
 impl Strategy {
-    fn new(parent_id: usize) -> Self {
+    fn new(parent_id: NodeId) -> Self {
         Self {
             parent_id,
             children_policy: HashMap::new(),
@@ -218,7 +218,7 @@ impl Strategy {
 
     /// Add motion policy for child node.
     /// Update policy in case `child_id` key is already in the `children_policy` map.
-    fn add_child(&mut self, child_id: usize, policy: MotionPolicy, program: Program) {
+    fn add_child(&mut self, child_id: NodeId, policy: MotionPolicy, program: Program) {
         self.children_policy.insert(child_id, (policy, program));
     }
 }
@@ -273,8 +273,8 @@ fn join_policy_for_and(
 
 impl Plan {
     /// Get unary NOT expression nodes.
-    pub(crate) fn get_not_unary_nodes(&self, top: usize) -> Vec<LevelNode> {
-        let filter = |node_id: usize| -> bool {
+    pub(crate) fn get_not_unary_nodes(&self, top: NodeId) -> Vec<LevelNode<NodeId>> {
+        let filter = |node_id: NodeId| -> bool {
             matches!(
                 self.get_node(node_id),
                 Ok(Node::Expression(Expression::Unary { op: Unary::Not, .. }))
@@ -296,8 +296,8 @@ impl Plan {
     ///
     /// # Errors
     /// - some of the expression nodes are invalid
-    pub(crate) fn get_bool_nodes_with_row_children(&self, top: usize) -> Vec<LevelNode> {
-        let filter = |node_id: usize| -> bool {
+    pub(crate) fn get_bool_nodes_with_row_children(&self, top: NodeId) -> Vec<LevelNode<NodeId>> {
+        let filter = |node_id: NodeId| -> bool {
             // Append only booleans with row children.
             if let Ok(Node::Expression(Expression::Bool { left, right, .. })) =
                 self.get_node(node_id)
@@ -332,8 +332,8 @@ impl Plan {
     ///
     /// # Errors
     /// - some of the expression nodes are invalid
-    pub(crate) fn get_unary_nodes_with_row_children(&self, top: usize) -> Vec<LevelNode> {
-        let filter = |node_id: usize| -> bool {
+    pub(crate) fn get_unary_nodes_with_row_children(&self, top: NodeId) -> Vec<LevelNode<NodeId>> {
+        let filter = |node_id: NodeId| -> bool {
             // Append only unaries with row children.
             if let Ok(Node::Expression(Expression::Unary { child, .. })) = self.get_node(node_id) {
                 let child_is_row = matches!(
@@ -360,7 +360,10 @@ impl Plan {
     /// # Errors
     /// - Row node is not of a row type
     /// There are more than one sub-queries in the row node.
-    pub fn get_sub_query_from_row_node(&self, row_id: usize) -> Result<Option<usize>, SbroadError> {
+    pub fn get_sub_query_from_row_node(
+        &self,
+        row_id: NodeId,
+    ) -> Result<Option<NodeId>, SbroadError> {
         let rel_ids = self.get_relational_nodes_from_row(row_id)?;
         self.get_sub_query_among_rel_nodes(&rel_ids)
     }
@@ -372,9 +375,9 @@ impl Plan {
     /// - There are more than one sub-query
     pub fn get_sub_query_among_rel_nodes(
         &self,
-        rel_nodes: &HashSet<usize, RandomState>,
-    ) -> Result<Option<usize>, SbroadError> {
-        let mut sq_set: HashSet<usize, RandomState> = HashSet::with_hasher(RandomState::new());
+        rel_nodes: &HashSet<NodeId, RandomState>,
+    ) -> Result<Option<NodeId>, SbroadError> {
+        let mut sq_set: HashSet<NodeId, RandomState> = HashSet::with_hasher(RandomState::new());
         for rel_id in rel_nodes {
             if let Node::Relational(Relational::ScanSubQuery { .. }) = self.get_node(*rel_id)? {
                 sq_set.insert(*rel_id);
@@ -398,7 +401,7 @@ impl Plan {
     /// # Errors
     /// - Row node is not of a row type
     /// - There are more than one motion nodes in the row node
-    pub fn get_motion_from_row(&self, node_id: usize) -> Result<Option<usize>, SbroadError> {
+    pub fn get_motion_from_row(&self, node_id: NodeId) -> Result<Option<NodeId>, SbroadError> {
         let rel_nodes = self.get_relational_nodes_from_row(node_id)?;
         self.get_motion_among_rel_nodes(&rel_nodes)
     }
@@ -409,9 +412,9 @@ impl Plan {
     /// - Some of the nodes are not relational
     pub fn get_motion_among_rel_nodes(
         &self,
-        rel_nodes: &HashSet<usize, RandomState>,
-    ) -> Result<Option<usize>, SbroadError> {
-        let mut motion_set: HashSet<usize> = HashSet::new();
+        rel_nodes: &HashSet<NodeId, RandomState>,
+    ) -> Result<Option<NodeId>, SbroadError> {
+        let mut motion_set: HashSet<NodeId> = HashSet::new();
 
         for child in rel_nodes {
             if self.get_relation_node(*child)?.is_motion() {
@@ -442,9 +445,9 @@ impl Plan {
     /// In such case join/selection can be done locally.
     fn has_eq_on_bucket_id(
         &self,
-        left_row_id: usize,
-        right_row_id: usize,
-        rel_id: usize,
+        left_row_id: NodeId,
+        right_row_id: NodeId,
+        rel_id: NodeId,
         op: &Bool,
     ) -> Result<bool, SbroadError> {
         if !(Bool::Eq == *op || Bool::In == *op) {
@@ -461,8 +464,8 @@ impl Plan {
         // Equality pair `a = b` allows us to do local join.
         //
         // position in row that refers to shard column -> child id
-        let mut memo: AHashMap<usize, usize> = AHashMap::new();
-        let mut search_row = |row_id: usize| -> Result<bool, SbroadError> {
+        let mut memo: AHashMap<usize, NodeId> = AHashMap::new();
+        let mut search_row = |row_id: NodeId| -> Result<bool, SbroadError> {
             let refs = self.get_row_list(row_id)?;
             for (pos_in_row, ref_id) in refs.iter().enumerate() {
                 let node @ Expression::Reference {
@@ -477,7 +480,7 @@ impl Plan {
                     SbroadError::Invalid(
                         Entity::Node,
                         Some(format_smolstr!(
-                            "ref ({ref_id}) in join condition with no targets: {node:?}"
+                            "ref ({ref_id:?}) in join condition with no targets: {node:?}"
                         )),
                     )
                 })?;
@@ -485,7 +488,7 @@ impl Plan {
                     SbroadError::Invalid(
                         Entity::Node,
                         Some(format_smolstr!(
-                            "ref ({ref_id}) in join condition with empty targets: {node:?}"
+                            "ref ({ref_id:?}) in join condition with empty targets: {node:?}"
                         )),
                     )
                 })?;
@@ -517,8 +520,8 @@ impl Plan {
     /// - uninitialized distribution for some row
     fn choose_strategy_for_bool_op_inner_sq(
         &self,
-        outer_id: usize,
-        inner_id: usize,
+        outer_id: NodeId,
+        inner_id: NodeId,
         op: &Bool,
     ) -> Result<MotionPolicy, SbroadError> {
         let outer_dist = self.get_distribution(outer_id)?;
@@ -574,7 +577,7 @@ impl Plan {
         }
 
         // Check that all children we need to add motions exist in the current relational node.
-        let children_set: HashSet<usize> = children.iter().copied().collect();
+        let children_set: HashSet<NodeId> = children.iter().copied().collect();
         if !strategy
             .children_policy
             .iter()
@@ -588,7 +591,7 @@ impl Plan {
         }
 
         // Add motions.
-        let mut children_with_motions: Vec<usize> = Vec::new();
+        let mut children_with_motions: Vec<NodeId> = Vec::new();
         let children_owned = children.to_vec();
         for child in children_owned {
             if let Some((policy, ref mut program)) = strategy.children_policy.get_mut(&child) {
@@ -610,9 +613,9 @@ impl Plan {
     /// Only returns `SubQuery` that is an additional child of passed `rel_id` node.
     fn get_additional_sq(
         &self,
-        rel_id: usize,
-        row_id: usize,
-    ) -> Result<Option<usize>, SbroadError> {
+        rel_id: NodeId,
+        row_id: NodeId,
+    ) -> Result<Option<NodeId>, SbroadError> {
         if self.get_expression_node(row_id)?.is_row() {
             if let Some(sq_id) = self.get_sub_query_from_row_node(row_id)? {
                 if self.is_additional_child_of_rel(rel_id, sq_id)? {
@@ -626,10 +629,10 @@ impl Plan {
     /// Get `SubQuery`s from passed boolean `op_id` node (e.g. `In`).
     fn get_sq_node_strategies_for_bool_op(
         &self,
-        rel_id: usize,
-        op_id: usize,
-    ) -> Result<Vec<(usize, MotionPolicy)>, SbroadError> {
-        let mut strategies: Vec<(usize, MotionPolicy)> = Vec::new();
+        rel_id: NodeId,
+        op_id: NodeId,
+    ) -> Result<Vec<(NodeId, MotionPolicy)>, SbroadError> {
+        let mut strategies: Vec<(NodeId, MotionPolicy)> = Vec::new();
         let bool_op = BoolOp::from_expr(self, op_id)?;
         let left = self.get_additional_sq(rel_id, bool_op.left)?;
         let right = self.get_additional_sq(rel_id, bool_op.right)?;
@@ -693,9 +696,9 @@ impl Plan {
     /// Get `SubQuery`s from passed unary `op_id` node (e.g. `Exists`).
     fn get_sq_node_strategy_for_unary_op(
         &self,
-        rel_id: usize,
-        op_id: usize,
-    ) -> Result<Option<(usize, MotionPolicy)>, SbroadError> {
+        rel_id: NodeId,
+        op_id: NodeId,
+    ) -> Result<Option<(NodeId, MotionPolicy)>, SbroadError> {
         let unary_op_expr = self.get_expression_node(op_id)?;
         let Expression::Unary { child, op } = unary_op_expr else {
             return Err(SbroadError::Invalid(
@@ -722,15 +725,16 @@ impl Plan {
     /// Resolve sub-query conflicts with motion policies.
     fn resolve_sub_query_conflicts(
         &mut self,
-        select_id: usize,
-        filter_id: usize,
+        select_id: NodeId,
+        filter_id: NodeId,
     ) -> Result<Strategy, SbroadError> {
         let mut strategy = Strategy::new(select_id);
 
         let not_nodes = self.get_not_unary_nodes(filter_id);
         let mut not_nodes_children = HashSet::with_capacity(not_nodes.len());
-        for (_, not_node_id) in &not_nodes {
-            let not_node = self.get_expression_node(*not_node_id)?;
+        for level_node in &not_nodes {
+            let not_node_id = level_node.1;
+            let not_node = self.get_expression_node(not_node_id)?;
             if let Expression::Unary { child, .. } = not_node {
                 not_nodes_children.insert(*child);
             } else {
@@ -742,13 +746,13 @@ impl Plan {
         }
 
         let bool_nodes = self.get_bool_nodes_with_row_children(filter_id);
-        for (_, bool_node) in &bool_nodes {
+        for LevelNode(_, bool_node) in &bool_nodes {
             let bool_op = BoolOp::from_expr(self, *bool_node)?;
             self.set_distribution(bool_op.left)?;
             self.set_distribution(bool_op.right)?;
         }
 
-        for (_, bool_node) in &bool_nodes {
+        for LevelNode(_, bool_node) in &bool_nodes {
             let strategies = self.get_sq_node_strategies_for_bool_op(select_id, *bool_node)?;
             for (id, policy) in strategies {
                 // In case we faced with `not ... in ...`, we
@@ -762,8 +766,9 @@ impl Plan {
         }
 
         let unary_nodes = self.get_unary_nodes_with_row_children(filter_id);
-        for (_, unary_node) in &unary_nodes {
-            let unary_strategy = self.get_sq_node_strategy_for_unary_op(select_id, *unary_node)?;
+        for level_node in &unary_nodes {
+            let unary_node = level_node.1;
+            let unary_strategy = self.get_sq_node_strategy_for_unary_op(select_id, unary_node)?;
             if let Some((id, policy)) = unary_strategy {
                 strategy.add_child(id, policy, Program::default());
             }
@@ -783,7 +788,7 @@ impl Plan {
     /// # Errors
     /// - If the node is not a join node.
     /// - Join node has no children.
-    fn get_join_children(&self, join_id: usize) -> Result<Children<'_>, SbroadError> {
+    fn get_join_children(&self, join_id: NodeId) -> Result<Children<'_>, SbroadError> {
         let join = self.get_relation_node(join_id)?;
         if let Relational::Join { .. } = join {
         } else {
@@ -799,10 +804,10 @@ impl Plan {
     fn get_join_child_by_key(
         &self,
         key: &Key,
-        row_map: &HashMap<usize, usize>,
+        row_map: &HashMap<usize, NodeId>,
         join_children: &Children<'_>,
-    ) -> Result<usize, SbroadError> {
-        let mut children_set: HashSet<usize> = HashSet::new();
+    ) -> Result<NodeId, SbroadError> {
+        let mut children_set: HashSet<NodeId> = HashSet::new();
         for pos in &key.positions {
             let column_id = *row_map.get(pos).ok_or_else(|| {
                 SbroadError::NotFound(
@@ -845,9 +850,9 @@ impl Plan {
     ///
     /// # Errors
     /// - If the node is not a row node.
-    fn build_row_map(&self, row_id: usize) -> Result<HashMap<usize, usize>, SbroadError> {
+    fn build_row_map(&self, row_id: NodeId) -> Result<HashMap<usize, NodeId>, SbroadError> {
         let columns = self.get_expression_node(row_id)?.get_row_list()?;
-        let mut map: HashMap<usize, usize> = HashMap::new();
+        let mut map: HashMap<usize, NodeId> = HashMap::new();
         for (pos, col) in columns.iter().enumerate() {
             map.insert(pos, *col);
         }
@@ -863,9 +868,9 @@ impl Plan {
     /// - Distribution keys do not refer to inner or outer children of the join.
     fn split_join_keys_to_inner_and_outer(
         &self,
-        join_id: usize,
+        join_id: NodeId,
         keys: &[Key],
-        row_map: &HashMap<usize, usize>,
+        row_map: &HashMap<usize, NodeId>,
     ) -> Result<(Vec<Key>, Vec<Key>), SbroadError> {
         let mut outer_keys: Vec<Key> = Vec::new();
         let mut inner_keys: Vec<Key> = Vec::new();
@@ -903,7 +908,7 @@ impl Plan {
     /// This function extracts only the positions of the references to the inner child.
     fn get_inner_positions_from_condition_row(
         &self,
-        row_map: &HashMap<usize, usize>,
+        row_map: &HashMap<usize, NodeId>,
     ) -> Result<AHashSet<usize>, SbroadError> {
         let mut inner_positions: AHashSet<usize> = AHashSet::with_capacity(row_map.len());
         for (pos, col) in row_map {
@@ -927,7 +932,7 @@ impl Plan {
     fn get_referred_inner_child_column_positions(
         &self,
         column_positions: &[usize],
-        condition_row_map: &HashMap<usize, usize>,
+        condition_row_map: &HashMap<usize, NodeId>,
     ) -> Result<Vec<usize>, SbroadError> {
         let mut referred_column_positions: Vec<usize> = Vec::with_capacity(column_positions.len());
         for pos in column_positions {
@@ -967,7 +972,7 @@ impl Plan {
     fn get_inner_policy_by_outer_segment(
         &self,
         outer_keys: &[Key],
-        inner_row_map: &HashMap<usize, usize>,
+        inner_row_map: &HashMap<usize, NodeId>,
     ) -> Result<MotionPolicy, SbroadError> {
         let inner_position_map = self.get_inner_positions_from_condition_row(inner_row_map)?;
         for outer_key in outer_keys {
@@ -1004,9 +1009,9 @@ impl Plan {
     /// - Failed to split distribution keys in the row to inner and outer keys.
     fn join_policy_for_eq(
         &self,
-        join_id: usize,
-        left_row_id: usize,
-        right_row_id: usize,
+        join_id: NodeId,
+        left_row_id: NodeId,
+        right_row_id: NodeId,
     ) -> Result<MotionPolicy, SbroadError> {
         if self.has_eq_on_bucket_id(left_row_id, right_row_id, join_id, &Bool::Eq)? {
             return Ok(MotionPolicy::None);
@@ -1086,10 +1091,11 @@ impl Plan {
         }
     }
 
-    fn set_rows_distributions_in_expr(&mut self, expr_id: usize) -> Result<(), SbroadError> {
+    fn set_rows_distributions_in_expr(&mut self, expr_id: NodeId) -> Result<(), SbroadError> {
         let nodes = self.get_bool_nodes_with_row_children(expr_id);
-        for (_, node) in &nodes {
-            let bool_op = BoolOp::from_expr(self, *node)?;
+        for level_node in &nodes {
+            let node = level_node.1;
+            let bool_op = BoolOp::from_expr(self, node)?;
             self.set_distribution(bool_op.left)?;
             self.set_distribution(bool_op.right)?;
         }
@@ -1103,8 +1109,8 @@ impl Plan {
     #[allow(clippy::too_many_lines)]
     fn resolve_join_conflicts(
         &mut self,
-        rel_id: usize,
-        cond_id: usize,
+        rel_id: NodeId,
+        cond_id: NodeId,
         join_kind: &JoinKind,
     ) -> Result<(), SbroadError> {
         // If one of the children has Distribution::Single, then we can't compute Distribution of
@@ -1154,9 +1160,9 @@ impl Plan {
             (inner, outer)
         };
 
-        let mut inner_map: HashMap<usize, MotionPolicy> = HashMap::new();
+        let mut inner_map: HashMap<NodeId, MotionPolicy> = HashMap::new();
         let mut new_inner_policy = MotionPolicy::Full;
-        let filter = |node_id: usize| -> bool {
+        let filter = |node_id: NodeId| -> bool {
             matches!(
                 self.get_node(node_id),
                 Ok(Node::Expression(
@@ -1172,7 +1178,8 @@ impl Plan {
         expr_tree.populate_nodes(cond_id);
         let nodes = expr_tree.take_nodes();
         drop(expr_tree);
-        for (_, node_id) in nodes {
+        for level_node in nodes {
+            let node_id = level_node.1;
             let expr = self.get_expression_node(node_id)?;
 
             // Under `not ... in ...` we should change the policy to `Full`
@@ -1345,12 +1352,12 @@ impl Plan {
     /// for `Single` no motion is needed.
     fn fix_sq_strategy_for_global_tbl(
         &self,
-        rel_id: usize,
-        cond_id: usize,
+        rel_id: NodeId,
+        cond_id: NodeId,
         strategy: &mut Strategy,
     ) -> Result<(), SbroadError> {
         let chains = self.get_dnf_chains(cond_id)?;
-        let mut subqueries: Vec<usize> = vec![];
+        let mut subqueries: Vec<NodeId> = vec![];
         let mut chain_count: usize = 0;
         for mut chain in chains {
             let nodes = chain.get_mut_nodes();
@@ -1485,8 +1492,8 @@ impl Plan {
     #[allow(clippy::too_many_lines)]
     fn calculate_strategy_for_single_distribution(
         &mut self,
-        join_id: usize,
-        condition_id: usize,
+        join_id: NodeId,
+        condition_id: NodeId,
         join_kind: &JoinKind,
     ) -> Result<Option<Strategy>, SbroadError> {
         let (outer_id, inner_id) = {
@@ -1494,12 +1501,12 @@ impl Plan {
             (
                 *children.get(0).ok_or_else(|| {
                     SbroadError::UnexpectedNumberOfValues(format_smolstr!(
-                        "join {join_id} has no children!"
+                        "join {join_id:?} has no children!"
                     ))
                 })?,
                 *children.get(1).ok_or_else(|| {
                     SbroadError::UnexpectedNumberOfValues(format_smolstr!(
-                        "join {join_id} has one child!"
+                        "join {join_id:?} has one child!"
                     ))
                 })?,
             )
@@ -1581,7 +1588,7 @@ impl Plan {
     }
 
     #[allow(clippy::too_many_lines)]
-    fn resolve_update_conflicts(&mut self, update_id: usize) -> Result<Strategy, SbroadError> {
+    fn resolve_update_conflicts(&mut self, update_id: NodeId) -> Result<Strategy, SbroadError> {
         if self.dml_node_table(update_id)?.is_global() {
             return self.resolve_dml_node_conflict_for_global_table(update_id);
         }
@@ -1597,7 +1604,7 @@ impl Plan {
                 return Err(SbroadError::Invalid(
                     Entity::Update,
                     Some(format_smolstr!(
-                        "expected Projection under Update ({update_id})"
+                        "expected Projection under Update ({update_id:?})"
                     )),
                 ));
             }
@@ -1728,12 +1735,12 @@ impl Plan {
         } else {
             Err(SbroadError::Invalid(
                 Entity::Node,
-                Some(format_smolstr!("expected Update node on id {update_id}")),
+                Some(format_smolstr!("expected Update node on id {update_id:?}")),
             ))
         }
     }
 
-    fn resolve_delete_conflicts(&mut self, rel_id: usize) -> Result<Strategy, SbroadError> {
+    fn resolve_delete_conflicts(&mut self, rel_id: NodeId) -> Result<Strategy, SbroadError> {
         if self.dml_node_table(rel_id)?.is_global() {
             return self.resolve_dml_node_conflict_for_global_table(rel_id);
         }
@@ -1766,7 +1773,7 @@ impl Plan {
 
     fn resolve_dml_node_conflict_for_global_table(
         &mut self,
-        rel_id: usize,
+        rel_id: NodeId,
     ) -> Result<Strategy, SbroadError> {
         let mut map = Strategy::new(rel_id);
         let child_id = self.dml_child_id(rel_id)?;
@@ -1777,7 +1784,7 @@ impl Plan {
         Ok(map)
     }
 
-    fn resolve_insert_conflicts(&mut self, rel_id: usize) -> Result<Strategy, SbroadError> {
+    fn resolve_insert_conflicts(&mut self, rel_id: NodeId) -> Result<Strategy, SbroadError> {
         if self.dml_node_table(rel_id)?.is_global() {
             return self.resolve_dml_node_conflict_for_global_table(rel_id);
         }
@@ -1812,7 +1819,7 @@ impl Plan {
         Ok(map)
     }
 
-    fn resolve_cte_conflicts(&mut self, cte_id: usize) -> Result<Strategy, SbroadError> {
+    fn resolve_cte_conflicts(&mut self, cte_id: NodeId) -> Result<Strategy, SbroadError> {
         // We always gather CTE data on the router node.
         let mut map = Strategy::new(cte_id);
         let child_id = self.get_relational_child(cte_id, 0)?;
@@ -1852,7 +1859,11 @@ impl Plan {
     // select "bucket_id" as a from t1
     // except
     // select "bucket_id" as b from t1
-    fn is_except_on_bucket_id(&self, left_id: usize, right_id: usize) -> Result<bool, SbroadError> {
+    fn is_except_on_bucket_id(
+        &self,
+        left_id: NodeId,
+        right_id: NodeId,
+    ) -> Result<bool, SbroadError> {
         let mut context = self.context_mut();
         let Some(left_shard_positions) =
             context.get_shard_columns_positions(left_id, self)?.copied()
@@ -1874,7 +1885,7 @@ impl Plan {
     }
 
     #[allow(clippy::too_many_lines)]
-    fn resolve_except_conflicts(&mut self, rel_id: usize) -> Result<Strategy, SbroadError> {
+    fn resolve_except_conflicts(&mut self, rel_id: NodeId) -> Result<Strategy, SbroadError> {
         if !matches!(self.get_relation_node(rel_id)?, Relational::Except { .. }) {
             return Err(SbroadError::Invalid(
                 Entity::Relational,
@@ -1940,7 +1951,7 @@ impl Plan {
                     SbroadError::Invalid(
                         Entity::Distribution,
                         Some(format_smolstr!(
-                            "{} {} {right_id}",
+                            "{} {} {right_id:?}",
                             "Segment distribution with no keys.",
                             "Except right child:"
                         )),
@@ -1998,7 +2009,7 @@ impl Plan {
     ///             Projection a
     ///                 scan g
     /// ```
-    fn resolve_except_global_vs_sharded(&mut self, except_id: usize) -> Result<bool, SbroadError> {
+    fn resolve_except_global_vs_sharded(&mut self, except_id: NodeId) -> Result<bool, SbroadError> {
         let left_id = self.get_relational_child(except_id, 0)?;
         let right_id = self.get_relational_child(except_id, 1)?;
         let left_dist = self.get_rel_distribution(left_id)?;
@@ -2013,7 +2024,7 @@ impl Plan {
             return Ok(false);
         }
 
-        let cloned_left_id = SubtreeCloner::clone_subtree(self, left_id, left_id)?;
+        let cloned_left_id = SubtreeCloner::clone_subtree(self, left_id, left_id.offset as usize)?;
         let right_output_id = self.get_relational_output(right_id)?;
         let intersect_output_id = self.clone_expr_subtree(right_output_id)?;
         let intersect = Relational::Intersect {
@@ -2032,7 +2043,7 @@ impl Plan {
         Ok(true)
     }
 
-    fn resolve_union_conflicts(&mut self, rel_id: usize) -> Result<Strategy, SbroadError> {
+    fn resolve_union_conflicts(&mut self, rel_id: NodeId) -> Result<Strategy, SbroadError> {
         if !matches!(
             self.get_relation_node(rel_id)?,
             Relational::UnionAll { .. } | Relational::Union { .. }
@@ -2125,8 +2136,8 @@ impl Plan {
     /// - failed to set distribution
     #[otm_child_span("plan.transformation.add_motions")]
     pub fn add_motions(&mut self) -> Result<(), SbroadError> {
-        type CteChildId = usize;
-        type MotionId = usize;
+        type CteChildId = ChildId;
+        type MotionId = ChildId;
         let mut cte_motions: AHashMap<CteChildId, MotionId> = AHashMap::with_capacity(CTE_CAPACITY);
         let top = self.get_top()?;
         let mut post_tree =
@@ -2134,9 +2145,9 @@ impl Plan {
         post_tree.populate_nodes(top);
         let nodes = post_tree.take_nodes();
         let mut visited = AHashSet::with_capacity(nodes.len());
-        let mut old_new: AHashMap<usize, usize> = AHashMap::new();
+        let mut old_new: AHashMap<NodeId, NodeId> = AHashMap::new();
 
-        for (_, id) in nodes {
+        for LevelNode(_, id) in nodes {
             if visited.contains(&id) {
                 continue;
             }
@@ -2350,8 +2361,8 @@ impl Plan {
     ///
     /// # Errors
     /// - failed to traverse plan
-    pub fn calculate_slices(&self, top_id: usize) -> Result<Vec<Vec<usize>>, SbroadError> {
-        let mut motions: Vec<Vec<usize>> = Vec::new();
+    pub fn calculate_slices(&self, top_id: NodeId) -> Result<Vec<Vec<NodeId>>, SbroadError> {
+        let mut motions: Vec<Vec<NodeId>> = Vec::new();
         let mut bft_tree = BreadthFirst::with_capacity(
             |node| self.nodes.rel_iter(node),
             REL_CAPACITY,
@@ -2359,7 +2370,7 @@ impl Plan {
         );
         let mut map: HashMap<usize, usize> = HashMap::new();
         let mut max_level: usize = 0;
-        for (level, id) in bft_tree.iter(top_id) {
+        for LevelNode(level, id) in bft_tree.iter(top_id) {
             if let Node::Relational(Relational::Motion { .. }) = self.get_node(id)? {
                 let key: usize = match map.entry(level) {
                     Entry::Occupied(o) => *o.into_mut(),
diff --git a/sbroad-core/src/ir/transformation/redistribution/dml.rs b/sbroad-core/src/ir/transformation/redistribution/dml.rs
index 29afb5da0..b29de812b 100644
--- a/sbroad-core/src/ir/transformation/redistribution/dml.rs
+++ b/sbroad-core/src/ir/transformation/redistribution/dml.rs
@@ -1,4 +1,5 @@
 use crate::errors::{Entity, SbroadError};
+use crate::ir::expression::NodeId;
 use crate::ir::operator::{ConflictStrategy, Relational, UpdateStrategy};
 use crate::ir::relation::{Column, Table};
 use crate::ir::transformation::redistribution::MotionOpcode;
@@ -14,7 +15,7 @@ impl Plan {
     /// # Errors
     /// - node is not `Insert`
     /// - `Insert` has 0 or more than 1 child
-    pub fn dml_child_id(&self, dml_node_id: usize) -> Result<usize, SbroadError> {
+    pub fn dml_child_id(&self, dml_node_id: NodeId) -> Result<NodeId, SbroadError> {
         let dml_node = self.get_relation_node(dml_node_id)?;
         if let Relational::Insert { children, .. }
         | Relational::Update { children, .. }
@@ -30,7 +31,7 @@ impl Plan {
         }
         Err(SbroadError::Invalid(
             Entity::Node,
-            Some(format_smolstr!("dml node with id {dml_node_id}")),
+            Some(format_smolstr!("dml node with id {dml_node_id:?}")),
         ))
     }
 
@@ -40,7 +41,7 @@ impl Plan {
     /// - node is not an `Insert`
     pub fn insert_conflict_strategy(
         &self,
-        insert_id: usize,
+        insert_id: NodeId,
     ) -> Result<&ConflictStrategy, SbroadError> {
         let insert = self.get_relation_node(insert_id)?;
         if let Relational::Insert {
@@ -52,12 +53,12 @@ impl Plan {
         Err(SbroadError::Invalid(
             Entity::Node,
             Some(format_smolstr!(
-                "INSERT with id {insert_id} (conflict strategy))"
+                "INSERT with id {insert_id:?} (conflict strategy))"
             )),
         ))
     }
 
-    pub(crate) fn insert_motion_key(&self, insert_id: usize) -> Result<MotionKey, SbroadError> {
+    pub(crate) fn insert_motion_key(&self, insert_id: NodeId) -> Result<MotionKey, SbroadError> {
         let columns = self.insert_columns(insert_id)?;
         // Revert map of { pos_in_child_node -> pos_in_relation }
         // into map of { pos_in_relation -> pos_in_child_node }.
@@ -76,7 +77,7 @@ impl Plan {
             return Err(SbroadError::Invalid(
                 Entity::Node,
                 Some(format_smolstr!(
-                    "INSERT with id {} has {} columns, but the child node with id {} has {}",
+                    "INSERT with id {:?} has {} columns, but the child node with id {:?} has {}",
                     insert_id,
                     columns.len(),
                     child_id,
@@ -114,14 +115,14 @@ impl Plan {
     ///
     /// # Errors
     /// - node is not `Insert`
-    pub(crate) fn insert_columns(&self, insert_id: usize) -> Result<&[usize], SbroadError> {
+    pub(crate) fn insert_columns(&self, insert_id: NodeId) -> Result<&[usize], SbroadError> {
         let insert = self.get_relation_node(insert_id)?;
         if let Relational::Insert { ref columns, .. } = insert {
             return Ok(columns);
         }
         Err(SbroadError::Invalid(
             Entity::Node,
-            Some(format_smolstr!("expected insert node on id {insert_id}")),
+            Some(format_smolstr!("expected insert node on id {insert_id:?}")),
         ))
     }
 
@@ -129,7 +130,7 @@ impl Plan {
     ///
     /// # Errors
     /// - Node is not an `Insert`
-    pub fn dml_node_table(&self, node_id: usize) -> Result<&Table, SbroadError> {
+    pub fn dml_node_table(&self, node_id: NodeId) -> Result<&Table, SbroadError> {
         let node = self.get_relation_node(node_id)?;
         if let Relational::Insert { relation, .. }
         | Relational::Update { relation, .. }
@@ -139,7 +140,7 @@ impl Plan {
         }
         Err(SbroadError::Invalid(
             Entity::Node,
-            Some(format_smolstr!("DML node with id {node_id}")),
+            Some(format_smolstr!("DML node with id {node_id:?}")),
         ))
     }
 
@@ -150,7 +151,7 @@ impl Plan {
     /// - Node is not an sharded `Update`
     pub fn set_update_delete_tuple_len(
         &mut self,
-        update_id: usize,
+        update_id: NodeId,
         len: usize,
     ) -> Result<(), SbroadError> {
         let node = self.get_mut_relation_node(update_id)?;
@@ -178,7 +179,7 @@ impl Plan {
     /// # Errors
     /// - Node is not an sharded `Update`
     /// - length not set on current `Update` node
-    pub fn get_update_delete_tuple_len(&self, update_id: usize) -> Result<usize, SbroadError> {
+    pub fn get_update_delete_tuple_len(&self, update_id: NodeId) -> Result<usize, SbroadError> {
         let node = self.get_relation_node(update_id)?;
         if let Relational::Update {
             strategy:
@@ -206,7 +207,7 @@ impl Plan {
     /// - invalid index
     pub fn get_motion_opcode(
         &self,
-        motion_id: usize,
+        motion_id: NodeId,
         opcode_idx: usize,
     ) -> Result<&MotionOpcode, SbroadError> {
         let node = self.get_relation_node(motion_id)?;
@@ -229,7 +230,7 @@ impl Plan {
     ///
     /// # Errors
     /// - Node is not an `Update`
-    pub fn is_sharded_update(&self, update_id: usize) -> Result<bool, SbroadError> {
+    pub fn is_sharded_update(&self, update_id: NodeId) -> Result<bool, SbroadError> {
         let node = self.get_relation_node(update_id)?;
         if let Relational::Update { strategy, .. } = node {
             return Ok(matches!(strategy, UpdateStrategy::ShardedUpdate { .. }));
diff --git a/sbroad-core/src/ir/transformation/redistribution/eq_cols.rs b/sbroad-core/src/ir/transformation/redistribution/eq_cols.rs
index 7cb632af0..67e531528 100644
--- a/sbroad-core/src/ir/transformation/redistribution/eq_cols.rs
+++ b/sbroad-core/src/ir/transformation/redistribution/eq_cols.rs
@@ -1,5 +1,5 @@
 use crate::errors::SbroadError;
-use crate::ir::expression::{Expression, ExpressionId};
+use crate::ir::expression::{Expression, ExpressionId, NodeId};
 use crate::ir::operator::Bool;
 use crate::ir::transformation::redistribution::BoolOp;
 use crate::ir::tree::traversal::{PostOrder, PostOrderWithFilter, EXPR_CAPACITY};
@@ -65,13 +65,14 @@ impl ReferredMap {
 
     pub fn new_from_join_condition(
         plan: &Plan,
-        condition_id: usize,
-        join_id: usize,
+        condition_id: NodeId,
+        join_id: NodeId,
     ) -> Result<Self, SbroadError> {
         let mut referred = ReferredMap::with_capacity(EXPR_CAPACITY);
         let mut expr_tree =
             PostOrder::with_capacity(|node| plan.nodes.expr_iter(node, false), EXPR_CAPACITY);
-        for (_, node_id) in expr_tree.iter(condition_id) {
+        for level_node in expr_tree.iter(condition_id) {
+            let node_id = level_node.1;
             let expr = plan.get_expression_node(node_id)?;
             let res = match expr {
                 Expression::Bool { left, right, .. }
@@ -225,7 +226,7 @@ impl EqualityCols {
     /// by returned `EqualityCols`.
     fn eq_cols_for_bool(
         op: &BoolOp,
-        node_id: usize,
+        node_id: NodeId,
         refers_to: &ReferredMap,
         node_eq_cols: &mut EqualityColsMap,
     ) -> Option<EqualityCols> {
@@ -276,10 +277,10 @@ impl EqualityCols {
     /// - non-empty `EqualityCols` in case the subtree is "good" and supports repartition join
     /// by returned `EqualityCols`.
     fn eq_cols_for_eq(
-        list_left: &[usize],
-        list_right: &[usize],
-        node_id: usize,
-        inner_id: usize,
+        list_left: &[NodeId],
+        list_right: &[NodeId],
+        node_id: NodeId,
+        inner_id: NodeId,
         plan: &Plan,
         refers_to: &ReferredMap,
     ) -> Result<Option<EqualityCols>, SbroadError> {
@@ -350,9 +351,9 @@ impl EqualityCols {
     /// by returned `EqualityCols`.
     fn eq_cols_for_rows(
         op: &BoolOp,
-        node_id: usize,
+        node_id: NodeId,
         refers_to: &ReferredMap,
-        inner_id: usize,
+        inner_id: NodeId,
         plan: &Plan,
     ) -> Result<Option<EqualityCols>, SbroadError> {
         let left_expr = plan.get_expression_node(op.left)?;
@@ -413,8 +414,8 @@ impl EqualityCols {
     /// - non-empty `EqualityCols` in case the subtree is "good" and supports repartition join
     /// by returned `EqualityCols`.
     fn eq_cols_for_and(
-        left: usize,
-        right: usize,
+        left: NodeId,
+        right: NodeId,
         refers_to: &ReferredMap,
         map: &mut EqualityColsMap,
     ) -> Option<EqualityCols> {
@@ -481,13 +482,13 @@ impl EqualityCols {
     /// - Otherwise, returns non-empty `EqualityCols` wrapped in `Option`
     pub fn from_join_condition(
         plan: &Plan,
-        join_id: usize,
-        inner_id: usize,
-        condition_id: usize,
+        join_id: NodeId,
+        inner_id: NodeId,
+        condition_id: NodeId,
     ) -> Result<Option<EqualityCols>, SbroadError> {
         let mut node_eq_cols: EqualityColsMap = EqualityColsMap::new();
         let refers_to = ReferredMap::new_from_join_condition(plan, condition_id, join_id)?;
-        let filter = |node_id: usize| -> bool {
+        let filter = |node_id: NodeId| -> bool {
             if let Ok(Node::Expression(Expression::Bool { .. })) = plan.get_node(node_id) {
                 return true;
             }
@@ -498,7 +499,8 @@ impl EqualityCols {
             EXPR_CAPACITY,
             Box::new(filter),
         );
-        for (_, node_id) in expr_tree.iter(condition_id) {
+        for level_node in expr_tree.iter(condition_id) {
+            let node_id = level_node.1;
             let bool_op = BoolOp::from_expr(plan, node_id)?;
             let left_expr = plan.get_expression_node(bool_op.left)?;
             let right_expr = plan.get_expression_node(bool_op.right)?;
diff --git a/sbroad-core/src/ir/transformation/redistribution/groupby.rs b/sbroad-core/src/ir/transformation/redistribution/groupby.rs
index bb87f2532..ac587a759 100644
--- a/sbroad-core/src/ir/transformation/redistribution/groupby.rs
+++ b/sbroad-core/src/ir/transformation/redistribution/groupby.rs
@@ -6,7 +6,8 @@ use crate::ir::aggregates::{generate_local_alias_for_aggr, AggregateKind, Simple
 use crate::ir::distribution::Distribution;
 use crate::ir::expression::Expression::StableFunction;
 use crate::ir::expression::{
-    ColumnPositionMap, Comparator, Expression, FunctionFeature, ReferencePolicy, EXPR_HASH_DEPTH,
+    ColumnPositionMap, Comparator, Expression, FunctionFeature, NodeId, ReferencePolicy,
+    EXPR_HASH_DEPTH,
 };
 use crate::ir::operator::Relational;
 use crate::ir::relation::Type;
@@ -14,7 +15,7 @@ use crate::ir::transformation::redistribution::{
     MotionKey, MotionPolicy, Program, Strategy, Target,
 };
 use crate::ir::tree::traversal::{BreadthFirst, PostOrderWithFilter, EXPR_CAPACITY};
-use crate::ir::{Node, Plan};
+use crate::ir::{ArenaType, Node, Plan};
 use std::collections::{HashMap, HashSet};
 
 use crate::ir::function::{Behavior, Function};
@@ -29,10 +30,10 @@ const AGGR_CAPACITY: usize = 10;
 struct AggrInfo {
     /// id of Relational node in which this aggregate is located.
     /// It can be located in `Projection`, `Having`, `OrderBy`
-    parent_rel: usize,
+    parent_rel: NodeId,
     /// id of parent expression of aggregate function,
     /// if there is no parent it's `None`
-    parent_expr: Option<usize>,
+    parent_expr: Option<NodeId>,
     /// info about what aggregate it is: sum, count, ...
     aggr: SimpleAggregate,
     /// whether this aggregate was marked distinct in original user query
@@ -42,7 +43,7 @@ struct AggrInfo {
 /// Helper struct to find aggregates in expressions of finals
 struct AggrCollector<'plan> {
     /// id of final node in which matches are searched
-    parent_rel: Option<usize>,
+    parent_rel: Option<NodeId>,
     /// collected aggregates
     infos: Vec<AggrInfo>,
     plan: &'plan Plan,
@@ -55,13 +56,13 @@ struct AggrCollector<'plan> {
 /// For example grouping expressions can appear
 /// in `Projection`, `Having`, `OrderBy`
 struct ExpressionLocationIds {
-    pub parent_expr: Option<usize>,
-    pub expr: usize,
-    pub rel: usize,
+    pub parent_expr: Option<NodeId>,
+    pub expr: NodeId,
+    pub rel: NodeId,
 }
 
 impl ExpressionLocationIds {
-    pub fn new(expr_id: usize, parent_expr_id: Option<usize>, rel_id: usize) -> Self {
+    pub fn new(expr_id: NodeId, parent_expr_id: Option<NodeId>, rel_id: NodeId) -> Self {
         ExpressionLocationIds {
             parent_expr: parent_expr_id,
             expr: expr_id,
@@ -80,7 +81,7 @@ impl ExpressionLocationIds {
 struct AggregateSignature<'plan, 'args> {
     pub kind: AggregateKind,
     /// ids of expressions used as arguments to aggregate
-    pub arguments: &'args Vec<usize>,
+    pub arguments: &'args Vec<NodeId>,
     pub plan: &'plan Plan,
     /// reference to local alias of this local aggregate
     pub local_alias: Option<Rc<String>>,
@@ -131,12 +132,12 @@ impl<'plan, 'args> PartialEq<Self> for AggregateSignature<'plan, 'args> {
 impl<'plan, 'args> Eq for AggregateSignature<'plan, 'args> {}
 
 struct GroupingExpression<'plan> {
-    pub id: usize,
+    pub id: NodeId,
     pub plan: &'plan Plan,
 }
 
 impl<'plan> GroupingExpression<'plan> {
-    pub fn new(id: usize, plan: &'plan Plan) -> Self {
+    pub fn new(id: NodeId, plan: &'plan Plan) -> Self {
         GroupingExpression { id, plan }
     }
 }
@@ -180,14 +181,18 @@ impl<'plan> AggrCollector<'plan> {
     ///
     /// # Errors
     /// - invalid expression tree pointed by `top`
-    pub fn collect_aggregates(&mut self, top: usize, parent_rel: usize) -> Result<(), SbroadError> {
+    pub fn collect_aggregates(
+        &mut self,
+        top: NodeId,
+        parent_rel: NodeId,
+    ) -> Result<(), SbroadError> {
         self.parent_rel = Some(parent_rel);
         self.find(top, None)?;
         self.parent_rel = None;
         Ok(())
     }
 
-    fn find(&mut self, current: usize, parent: Option<usize>) -> Result<(), SbroadError> {
+    fn find(&mut self, current: NodeId, parent: Option<NodeId>) -> Result<(), SbroadError> {
         let expr = self.plan.get_expression_node(current)?;
         if let StableFunction { name, feature, .. } = expr {
             let is_distinct = matches!(feature, Some(FunctionFeature::Distinct));
@@ -219,7 +224,7 @@ impl<'plan> AggrCollector<'plan> {
 /// For example:
 /// `select a from t group by a having a = 1`
 /// Here expression in `GroupBy` is mapped to `a` in `Projection` and `a` in `Having`
-type GroupbyExpressionsMap = HashMap<usize, Vec<ExpressionLocationIds>>;
+type GroupbyExpressionsMap = HashMap<NodeId, Vec<ExpressionLocationIds>>;
 /// Maps id of `GroupBy` expression used in `GroupBy` (from local stage)
 /// to corresponding local alias used in local Projection. Note:
 /// this map does not contain mappings between grouping expressions from
@@ -230,22 +235,22 @@ type GroupbyExpressionsMap = HashMap<usize, Vec<ExpressionLocationIds>>;
 /// initial query: `select a, count(distinct b) from t group by a`
 /// map query: `select a as l1, b group by a, b`
 /// Then this map will map id of `a` to `l1`
-type LocalAliasesMap = HashMap<usize, Rc<String>>;
-type LocalAggrInfo = (AggregateKind, Vec<usize>, Rc<String>);
+type LocalAliasesMap = HashMap<NodeId, Rc<String>>;
+type LocalAggrInfo = (AggregateKind, Vec<NodeId>, Rc<String>);
 
 /// Helper struct to map expressions used in `GroupBy` to
 /// expressions used in some other node (`Projection`, `Having`, `OrderBy`)
 struct ExpressionMapper<'plan> {
     /// List of expressions ids of `GroupBy`
-    gr_exprs: &'plan Vec<usize>,
+    gr_exprs: &'plan Vec<NodeId>,
     map: GroupbyExpressionsMap,
     plan: &'plan Plan,
     /// Id of relational node (`Projection`, `Having`, `OrderBy`)
-    node_id: Option<usize>,
+    node_id: Option<NodeId>,
 }
 
 impl<'plan> ExpressionMapper<'plan> {
-    fn new(gr_expressions: &'plan Vec<usize>, plan: &'plan Plan) -> ExpressionMapper<'plan> {
+    fn new(gr_expressions: &'plan Vec<NodeId>, plan: &'plan Plan) -> ExpressionMapper<'plan> {
         let map: GroupbyExpressionsMap = HashMap::new();
         ExpressionMapper {
             gr_exprs: gr_expressions,
@@ -270,7 +275,7 @@ impl<'plan> ExpressionMapper<'plan> {
     /// - invalid query: node expression contains references that are not
     /// found in `GroupBy` expression. The reason is that user specified expression in
     /// node that does not match any expression in `GroupBy`
-    fn find_matches(&mut self, expr_root: usize, node_id: usize) -> Result<(), SbroadError> {
+    fn find_matches(&mut self, expr_root: NodeId, node_id: NodeId) -> Result<(), SbroadError> {
         self.node_id = Some(node_id);
         self.find(expr_root, None)?;
         self.node_id = None;
@@ -279,7 +284,7 @@ impl<'plan> ExpressionMapper<'plan> {
 
     /// Helper function for `find_matches` which compares current node to `GroupBy` expressions
     /// and if no match is found recursively calls itself.
-    fn find(&mut self, current: usize, parent: Option<usize>) -> Result<(), SbroadError> {
+    fn find(&mut self, current: NodeId, parent: Option<NodeId>) -> Result<(), SbroadError> {
         let Some(node_id) = self.node_id else {
             return Err(SbroadError::Invalid(Entity::ExpressionMapper, None));
         };
@@ -346,7 +351,7 @@ impl<'plan> ExpressionMapper<'plan> {
 
 impl Plan {
     #[allow(unreachable_code)]
-    fn generate_local_alias(id: usize) -> String {
+    fn generate_local_alias(id: NodeId) -> String {
         #[cfg(feature = "mock")]
         {
             return format!("column_{id}");
@@ -379,8 +384,8 @@ impl Plan {
     /// - invalid [`Expression::Reference`]s in either of subtrees
     pub fn are_aggregate_subtrees_equal(
         &self,
-        lhs: usize,
-        rhs: usize,
+        lhs: NodeId,
+        rhs: NodeId,
     ) -> Result<bool, SbroadError> {
         let l = self.get_node(lhs)?;
         let r = self.get_node(rhs)?;
@@ -616,7 +621,7 @@ impl Plan {
     /// # Errors
     /// - invalid children count
     /// - failed to create output for `GroupBy`
-    pub fn add_groupby_from_ast(&mut self, children: &[usize]) -> Result<usize, SbroadError> {
+    pub fn add_groupby_from_ast(&mut self, children: &[NodeId]) -> Result<NodeId, SbroadError> {
         let Some((first_child, other)) = children.split_first() else {
             return Err(SbroadError::UnexpectedNumberOfValues(
                 "GroupBy ast has no children".into(),
@@ -627,7 +632,7 @@ impl Plan {
         // 1) aggregates are not allowed
         // 2) must contain at least one column (group by 1 - is not valid)
         for (pos, grouping_expr_id) in other.iter().enumerate() {
-            let filter = |node_id: usize| -> bool {
+            let filter = |node_id: NodeId| -> bool {
                 matches!(
                     self.get_node(node_id),
                     Ok(Node::Expression(
@@ -641,7 +646,8 @@ impl Plan {
                 Box::new(filter),
             );
             let mut contains_at_least_one_col = false;
-            for (_, node_id) in dfs.iter(*grouping_expr_id) {
+            for level_node in dfs.iter(*grouping_expr_id) {
+                let node_id = level_node.1;
                 let node = self.get_node(node_id)?;
                 match node {
                     Node::Expression(Expression::Reference { .. }) => {
@@ -661,7 +667,7 @@ impl Plan {
             if !contains_at_least_one_col {
                 return Err(SbroadError::Invalid(
                     Entity::Query,
-                    Some(format_smolstr!("grouping expression must contain at least one column. Invalid expression number: {pos}"))
+                    Some(format_smolstr!("grouping expression must contain at least one column. Invalid expression number: {pos:?}"))
                 ));
             }
         }
@@ -677,11 +683,11 @@ impl Plan {
     /// - `grouping_exprs` - contains non-expr id
     pub fn add_groupby(
         &mut self,
-        child_id: usize,
-        grouping_exprs: &[usize],
+        child_id: NodeId,
+        grouping_exprs: &[NodeId],
         is_final: bool,
-        expr_parent: Option<usize>,
-    ) -> Result<usize, SbroadError> {
+        expr_parent: Option<NodeId>,
+    ) -> Result<NodeId, SbroadError> {
         let final_output = self.add_row_for_output(child_id, &[], true)?;
         let groupby = Relational::GroupBy {
             children: [child_id].to_vec(),
@@ -708,7 +714,7 @@ impl Plan {
     /// [`finals`] - ids of nodes in final (reduce stage) before adding two stage aggregation.
     /// It may contain ids of `Projection`, `Having`, `Limit`, `OrderBy`.
     /// Note: final `GroupBy` is not present because it will be added later in 2-stage pipeline.
-    fn collect_aggregates(&self, finals: &Vec<usize>) -> Result<Vec<AggrInfo>, SbroadError> {
+    fn collect_aggregates(&self, finals: &Vec<NodeId>) -> Result<Vec<AggrInfo>, SbroadError> {
         let mut collector = AggrCollector::with_capacity(self, AGGR_CAPACITY);
         for node_id in finals {
             let node = self.get_relation_node(*node_id)?;
@@ -725,7 +731,7 @@ impl Plan {
                     return Err(SbroadError::Invalid(
                         Entity::Plan,
                         Some(format_smolstr!(
-                            "unexpected relational node ({node_id}): {node:?}"
+                            "unexpected relational node ({node_id:?}): {node:?}"
                         )),
                     ))
                 }
@@ -769,20 +775,23 @@ impl Plan {
     ///             Scan (4)
     /// ```
     /// Then this function will return `([1, 2], 4)`
-    fn split_reduce_stage(&self, final_proj_id: usize) -> Result<(Vec<usize>, usize), SbroadError> {
-        let mut finals: Vec<usize> = vec![];
-        let get_first_child = |rel_id: usize| -> Result<usize, SbroadError> {
+    fn split_reduce_stage(
+        &self,
+        final_proj_id: NodeId,
+    ) -> Result<(Vec<NodeId>, NodeId), SbroadError> {
+        let mut finals: Vec<NodeId> = vec![];
+        let get_first_child = |rel_id: NodeId| -> Result<NodeId, SbroadError> {
             let c = *self
                 .get_relational_children(rel_id)?
                 .get(0)
                 .ok_or_else(|| {
                     SbroadError::UnexpectedNumberOfValues(format_smolstr!(
-                        "expected relation node ({rel_id}) to have children!"
+                        "expected relation node ({rel_id:?}) to have children!"
                     ))
                 })?;
             Ok(c)
         };
-        let mut next: usize = final_proj_id;
+        let mut next = final_proj_id;
         let max_reduce_nodes = 2;
         for _ in 0..=max_reduce_nodes {
             match self.get_relation_node(next)? {
@@ -832,10 +841,10 @@ impl Plan {
     #[allow(clippy::too_many_lines)]
     fn collect_grouping_expressions(
         &mut self,
-        upper: usize,
-        finals: &Vec<usize>,
+        upper: NodeId,
+        finals: &Vec<NodeId>,
         has_aggregates: bool,
-    ) -> Result<(usize, Vec<usize>, GroupbyExpressionsMap), SbroadError> {
+    ) -> Result<(NodeId, Vec<NodeId>, GroupbyExpressionsMap), SbroadError> {
         let mut grouping_expr = vec![];
         let mut gr_expr_map: GroupbyExpressionsMap = HashMap::new();
         let mut upper = upper;
@@ -852,7 +861,7 @@ impl Plan {
                 {
                     if *is_distinct {
                         let proj_cols_len = self.get_row_list(*output)?.len();
-                        let mut grouping_exprs: Vec<usize> = Vec::with_capacity(proj_cols_len);
+                        let mut grouping_exprs: Vec<NodeId> = Vec::with_capacity(proj_cols_len);
                         for i in 0..proj_cols_len {
                             let aliased_col = self.get_proj_col(*proj_id, i)?;
                             let proj_col_id = if let Expression::Alias { child, .. } =
@@ -885,7 +894,7 @@ impl Plan {
             let grouping_exprs = unique_grouping_exprs
                 .into_iter()
                 .map(|x| x.id)
-                .collect::<Vec<usize>>();
+                .collect::<Vec<NodeId>>();
             grouping_expr.extend(grouping_exprs.iter());
             self.set_grouping_cols(upper, grouping_exprs)?;
 
@@ -913,7 +922,7 @@ impl Plan {
                 match node {
                     Relational::Projection { output, .. } => {
                         for col in self.get_row_list(*output)? {
-                            let filter = |node_id: usize| -> bool {
+                            let filter = |node_id: NodeId| -> bool {
                                 matches!(
                                     self.get_node(node_id),
                                     Ok(Node::Expression(Expression::Reference { .. }))
@@ -926,7 +935,8 @@ impl Plan {
                             );
                             dfs.populate_nodes(*col);
                             let nodes = dfs.take_nodes();
-                            for (_, id) in nodes {
+                            for level_node in nodes {
+                                let id = level_node.1;
                                 let n = self.get_expression_node(id)?;
                                 if let Expression::Reference { .. } = n {
                                     let alias = match self.get_alias_from_reference_node(n) {
@@ -947,7 +957,8 @@ impl Plan {
                         );
                         bfs.populate_nodes(*filter);
                         let nodes = bfs.take_nodes();
-                        for (_, id) in nodes {
+                        for level_node in nodes {
+                            let id = level_node.1;
                             if let Expression::Reference { .. } = self.get_expression_node(id)? {
                                 return Err(SbroadError::Invalid(
                                     Entity::Query,
@@ -972,9 +983,9 @@ impl Plan {
     /// Reduce: `select l1, sum(distinct l2), sum(l3) from tmp_space group by l1`
     fn add_distinct_aggregates_to_local_groupby(
         &mut self,
-        upper: usize,
-        additional_grouping_exprs: Vec<usize>,
-    ) -> Result<usize, SbroadError> {
+        upper: NodeId,
+        additional_grouping_exprs: Vec<NodeId>,
+    ) -> Result<NodeId, SbroadError> {
         let mut local_proj_child_id = upper;
         if !additional_grouping_exprs.is_empty() {
             if let Relational::GroupBy { gr_cols, .. } =
@@ -1050,10 +1061,10 @@ impl Plan {
     /// - map between `GroupBy` expression and corresponding local alias.
     fn add_local_projection(
         &mut self,
-        child_id: usize,
+        child_id: NodeId,
         aggr_infos: &mut Vec<AggrInfo>,
-        grouping_exprs: &Vec<usize>,
-    ) -> Result<(usize, Vec<usize>, LocalAliasesMap), SbroadError> {
+        grouping_exprs: &Vec<NodeId>,
+    ) -> Result<(NodeId, Vec<usize>, LocalAliasesMap), SbroadError> {
         let (child_id, proj_output_cols, groupby_local_aliases, grouping_positions) =
             self.create_columns_for_local_proj(aggr_infos, child_id, grouping_exprs)?;
         let proj_output = self.nodes.add_row(proj_output_cols, None);
@@ -1076,9 +1087,9 @@ impl Plan {
     fn create_local_aggregate(
         &mut self,
         kind: AggregateKind,
-        arguments: &[usize],
+        arguments: &[NodeId],
         local_alias: &str,
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         let fun = Function {
             name: kind.to_smolstr(),
             behavior: Behavior::Stable,
@@ -1110,10 +1121,10 @@ impl Plan {
     fn create_columns_for_local_proj(
         &mut self,
         aggr_infos: &mut [AggrInfo],
-        upper_id: usize,
-        grouping_exprs: &Vec<usize>,
-    ) -> Result<(usize, Vec<usize>, LocalAliasesMap, Vec<usize>), SbroadError> {
-        let mut output_cols: Vec<usize> = vec![];
+        upper_id: NodeId,
+        grouping_exprs: &Vec<NodeId>,
+    ) -> Result<(NodeId, Vec<NodeId>, LocalAliasesMap, Vec<usize>), SbroadError> {
+        let mut output_cols: Vec<NodeId> = vec![];
         let (local_aliases, child_id, grouping_positions) =
             self.add_grouping_exprs(aggr_infos, upper_id, grouping_exprs, &mut output_cols)?;
         self.add_local_aggregates(aggr_infos, &mut output_cols)?;
@@ -1145,10 +1156,10 @@ impl Plan {
     fn add_grouping_exprs(
         &mut self,
         aggr_infos: &mut [AggrInfo],
-        upper_id: usize,
-        grouping_exprs: &Vec<usize>,
-        output_cols: &mut Vec<usize>,
-    ) -> Result<(LocalAliasesMap, usize, Vec<usize>), SbroadError> {
+        upper_id: NodeId,
+        grouping_exprs: &Vec<NodeId>,
+        output_cols: &mut Vec<NodeId>,
+    ) -> Result<(LocalAliasesMap, NodeId, Vec<usize>), SbroadError> {
         let mut unique_grouping_exprs_for_local_stage: HashMap<
             GroupingExpression,
             Rc<String>,
@@ -1162,13 +1173,13 @@ impl Plan {
         }
 
         // add grouping expressions found from distinct aggregates to local groupby
-        let mut grouping_exprs_from_aggregates: Vec<usize> = vec![];
+        let mut grouping_exprs_from_aggregates: Vec<NodeId> = vec![];
         for info in aggr_infos.iter_mut().filter(|x| x.is_distinct) {
             let argument = {
                 let args = self
                     .nodes
                     .expr_iter(info.aggr.fun_id, false)
-                    .collect::<Vec<usize>>();
+                    .collect::<Vec<NodeId>>();
                 if args.len() > 1 && !matches!(info.aggr.kind, AggregateKind::GRCONCAT) {
                     return Err(SbroadError::UnexpectedNumberOfValues(format_smolstr!(
                         "aggregate ({info:?}) have more than one argument"
@@ -1194,11 +1205,14 @@ impl Plan {
         }
 
         // Because of borrow checker we need to remove references to Plan from map
-        let mut unique_grouping_exprs_for_local_stage: HashMap<usize, Rc<String>, RepeatableState> =
-            unique_grouping_exprs_for_local_stage
-                .into_iter()
-                .map(|(e, s)| (e.id, s))
-                .collect();
+        let mut unique_grouping_exprs_for_local_stage: HashMap<
+            NodeId,
+            Rc<String>,
+            RepeatableState,
+        > = unique_grouping_exprs_for_local_stage
+            .into_iter()
+            .map(|(e, s)| (e.id, s))
+            .collect();
 
         let mut alias_to_pos: HashMap<Rc<String>, usize> = HashMap::new();
         // add grouping expressions to local projection
@@ -1234,7 +1248,7 @@ impl Plan {
             } else {
                 return Err(SbroadError::Invalid(
                     Entity::Node,
-                    Some(format_smolstr!("invalid map with unique grouping expressions. Could not find grouping expression with id: {expr_id}"))));
+                    Some(format_smolstr!("invalid map with unique grouping expressions. Could not find grouping expression with id: {expr_id:?}"))));
             }
         }
         let child_id = self
@@ -1250,7 +1264,7 @@ impl Plan {
     fn add_local_aggregates(
         &mut self,
         aggr_infos: &mut [AggrInfo],
-        output_cols: &mut Vec<usize>,
+        output_cols: &mut Vec<NodeId>,
     ) -> Result<(), SbroadError> {
         // Aggregate expressions can appear in `Projection`, `Having`, `OrderBy`, if the
         // same expression appears in different places, we must not calculate it separately:
@@ -1275,7 +1289,7 @@ impl Plan {
                 } else {
                     return Err(SbroadError::Invalid(
                         Entity::Aggregate,
-                        Some(format_smolstr!("invalid fun_id: {}", info.aggr.fun_id)),
+                        Some(format_smolstr!("invalid fun_id: {:?}", info.aggr.fun_id)),
                     ));
                 }
             };
@@ -1308,7 +1322,7 @@ impl Plan {
                     })?;
                     let alias = Rc::new(generate_local_alias_for_aggr(
                         &kind,
-                        &info.aggr.fun_id.to_string(),
+                        &format_smolstr!("{}", info.aggr.fun_id),
                     ));
                     info.aggr.lagg_alias.insert(kind, alias.clone());
                     signature.local_alias = Some(alias);
@@ -1321,7 +1335,7 @@ impl Plan {
         let local_aggregates: Result<Vec<LocalAggrInfo>, SbroadError> = unique_local_aggregates
             .into_iter()
             .map(
-                |x| -> Result<(AggregateKind, Vec<usize>, Rc<String>), SbroadError> {
+                |x| -> Result<(AggregateKind, Vec<NodeId>, Rc<String>), SbroadError> {
                     match x.get_alias() {
                         Ok(s) => Ok((x.kind, x.arguments.clone(), s)),
                         Err(e) => Err(e),
@@ -1351,15 +1365,15 @@ impl Plan {
     /// - if `GroupBy` node was not created, return `child_id`
     fn add_final_groupby(
         &mut self,
-        child_id: usize,
-        grouping_exprs: &Vec<usize>,
+        child_id: NodeId,
+        grouping_exprs: &Vec<NodeId>,
         local_aliases_map: &LocalAliasesMap,
-    ) -> Result<usize, SbroadError> {
+    ) -> Result<NodeId, SbroadError> {
         if grouping_exprs.is_empty() {
             // no GroupBy in the original query, nothing to do
             return Ok(child_id);
         }
-        let mut gr_cols: Vec<usize> = Vec::with_capacity(grouping_exprs.len());
+        let mut gr_cols: Vec<NodeId> = Vec::with_capacity(grouping_exprs.len());
         let child_map = ColumnPositionMap::new(self, child_id)?;
         let mut nodes = Vec::with_capacity(grouping_exprs.len());
         for expr_id in grouping_exprs {
@@ -1367,7 +1381,7 @@ impl Plan {
                 return Err(SbroadError::Invalid(
                     Entity::Plan,
                     Some(format_smolstr!(
-                        "could not find local alias for GroupBy expr ({expr_id})"
+                        "could not find local alias for GroupBy expr ({expr_id:?})"
                     )),
                 ));
             };
@@ -1377,7 +1391,7 @@ impl Plan {
                 return Err(SbroadError::Invalid(
                     Entity::Type,
                     Some(format_smolstr!(
-                        "add_final_groupby: GroupBy expr ({expr_id}) is not scalar ({col_type})!"
+                        "add_final_groupby: GroupBy expr ({expr_id:?}) is not scalar ({col_type})!"
                     )),
                 ));
             }
@@ -1394,7 +1408,7 @@ impl Plan {
             gr_cols.push(new_col_id);
         }
         let output = self.add_row_for_output(child_id, &[], true)?;
-        let final_id = self.nodes.next_id();
+        let final_id = self.nodes.next_id(ArenaType::Default);
         for col in &gr_cols {
             self.replace_parent_in_subtree(*col, None, Some(final_id))?;
         }
@@ -1424,10 +1438,10 @@ impl Plan {
         local_aliases_map: &LocalAliasesMap,
         map: GroupbyExpressionsMap,
     ) -> Result<(), SbroadError> {
-        type RelationalID = usize;
-        type GroupByExpressionID = usize;
-        type ExpressionID = usize;
-        type ExpressionParent = Option<usize>;
+        type RelationalID = NodeId;
+        type GroupByExpressionID = NodeId;
+        type ExpressionID = NodeId;
+        type ExpressionParent = Option<NodeId>;
         type ParentExpressionMap =
             HashMap<RelationalID, Vec<(GroupByExpressionID, ExpressionID, ExpressionParent)>>;
         let map: ParentExpressionMap = {
@@ -1450,7 +1464,7 @@ impl Plan {
                 .get(0)
                 .ok_or_else(|| {
                     SbroadError::UnexpectedNumberOfValues(format_smolstr!(
-                        "expected relation node ({rel_id}) to have children!"
+                        "expected relation node ({rel_id:?}) to have children!"
                     ))
                 })?;
             let alias_to_pos_map = ColumnPositionMap::new(self, child_id)?;
@@ -1460,7 +1474,7 @@ impl Plan {
                     return Err(SbroadError::Invalid(
                         Entity::Plan,
                         Some(format_smolstr!(
-                            "failed to find local alias for groupby expression {gr_expr_id}"
+                            "failed to find local alias for groupby expression {gr_expr_id:?}"
                         )),
                     ));
                 };
@@ -1492,7 +1506,7 @@ impl Plan {
                             return Err(SbroadError::Invalid(
                                 Entity::Plan,
                                 Some(format_smolstr!(
-                                    "{} {gr_expr_id} {} {expr_id} {}",
+                                    "{} {gr_expr_id:?} {} {expr_id:?} {}",
                                     "invalid mapping between group by expression",
                                     "and projection one: expression",
                                     "has no parent",
@@ -1505,7 +1519,9 @@ impl Plan {
                         _ => {
                             return Err(SbroadError::Invalid(
                                 Entity::Plan,
-                                Some(format_smolstr!("unexpected node in Reduce stage: {rel_id}")),
+                                Some(format_smolstr!(
+                                    "unexpected node in Reduce stage: {rel_id:?}"
+                                )),
                             ))
                         }
                     }
@@ -1540,8 +1556,8 @@ impl Plan {
     /// used in `finals`.
     fn patch_finals(
         &mut self,
-        finals: &[usize],
-        finals_child_id: usize,
+        finals: &[NodeId],
+        finals_child_id: NodeId,
         local_aliases_map: &LocalAliasesMap,
         aggr_infos: &Vec<AggrInfo>,
         gr_expr_map: GroupbyExpressionsMap,
@@ -1564,7 +1580,7 @@ impl Plan {
                     let child_id = *children.first().ok_or_else(|| {
                         SbroadError::Invalid(
                             Entity::Node,
-                            Some(format_smolstr!("Having ({node_id}) has no children!")),
+                            Some(format_smolstr!("Having ({node_id:?}) has no children!")),
                         )
                     })?;
                     let output = self.add_row_for_output(child_id, &[], true)?;
@@ -1581,7 +1597,7 @@ impl Plan {
         }
 
         self.patch_grouping_expressions(local_aliases_map, gr_expr_map)?;
-        let mut parent_to_infos: HashMap<usize, Vec<AggrInfo>> =
+        let mut parent_to_infos: HashMap<NodeId, Vec<AggrInfo>> =
             HashMap::with_capacity(finals.len());
         for info in aggr_infos {
             if let Some(v) = parent_to_infos.get_mut(&info.parent_rel) {
@@ -1597,7 +1613,7 @@ impl Plan {
                     SbroadError::Invalid(
                         Entity::Node,
                         Some(format_smolstr!(
-                            "patch aggregates: rel node ({parent}) has no children!"
+                            "patch aggregates: rel node ({parent:?}) has no children!"
                         )),
                     )
                 })?
@@ -1639,8 +1655,8 @@ impl Plan {
     fn add_motion_to_2stage(
         &mut self,
         grouping_positions: &[usize],
-        motion_parent: usize,
-        finals: &[usize],
+        motion_parent: NodeId,
+        finals: &[NodeId],
     ) -> Result<(), SbroadError> {
         let proj_id = *finals.first().ok_or_else(|| {
             SbroadError::Invalid(Entity::Plan, Some("no nodes in Reduce stage!".into()))
@@ -1671,7 +1687,7 @@ impl Plan {
                     SbroadError::Invalid(
                         Entity::Node,
                         Some(format_smolstr!(
-                            "final GroupBy ({motion_parent}) has no children!"
+                            "final GroupBy ({motion_parent:?}) has no children!"
                         )),
                     )
                 })?
@@ -1679,7 +1695,7 @@ impl Plan {
                 return Err(SbroadError::Invalid(
                     Entity::Plan,
                     Some(format_smolstr!(
-                        "expected to have GroupBy under reduce nodes on id: {motion_parent}"
+                        "expected to have GroupBy under reduce nodes on id: {motion_parent:?}"
                     )),
                 ));
             };
@@ -1713,7 +1729,10 @@ impl Plan {
     /// - failed to create `SQ` node
     /// - failed to change final `GroupBy` child to `SQ`
     /// - failed to update expressions in final `Projection`
-    pub fn add_two_stage_aggregation(&mut self, final_proj_id: usize) -> Result<bool, SbroadError> {
+    pub fn add_two_stage_aggregation(
+        &mut self,
+        final_proj_id: NodeId,
+    ) -> Result<bool, SbroadError> {
         let (finals, upper) = self.split_reduce_stage(final_proj_id)?;
         let mut aggr_infos = self.collect_aggregates(&finals)?;
         let has_aggregates = !aggr_infos.is_empty();
@@ -1760,7 +1779,7 @@ impl Plan {
         )?;
         self.add_motion_to_2stage(&grouping_positions, finals_child_id, &finals)?;
 
-        let mut having_id: Option<usize> = None;
+        let mut having_id: Option<NodeId> = None;
         // skip Projection
         for node_id in finals.iter().skip(1).rev() {
             self.set_distribution(self.get_relational_output(*node_id)?)?;
diff --git a/sbroad-core/src/ir/transformation/redistribution/left_join.rs b/sbroad-core/src/ir/transformation/redistribution/left_join.rs
index ea0d83d0f..546f72040 100644
--- a/sbroad-core/src/ir/transformation/redistribution/left_join.rs
+++ b/sbroad-core/src/ir/transformation/redistribution/left_join.rs
@@ -7,6 +7,7 @@ use crate::{
     errors::{Entity, SbroadError},
     ir::{
         distribution::Distribution,
+        expression::NodeId,
         operator::{JoinKind, Relational},
         Plan,
     },
@@ -17,7 +18,7 @@ use super::{MotionOpcode, MotionPolicy, Program, Strategy};
 impl Plan {
     pub(super) fn calculate_strategy_for_left_join_with_global_tbl(
         &mut self,
-        join_id: usize,
+        join_id: NodeId,
         join_kind: &JoinKind,
     ) -> Result<Option<Strategy>, SbroadError> {
         let is_left_join = matches!(join_kind, JoinKind::LeftOuter);
@@ -41,7 +42,7 @@ impl Plan {
         let Some(parent_id) = self.find_parent_rel(join_id)? else {
             return Err(SbroadError::Invalid(
                 Entity::Plan,
-                Some(format_smolstr!("join ({join_id}) has no parent!")),
+                Some(format_smolstr!("join ({join_id:?}) has no parent!")),
             ));
         };
         let projection_id = create_projection(self, join_id)?;
@@ -89,7 +90,7 @@ impl Plan {
     }
 }
 
-fn create_projection(plan: &mut Plan, join_id: usize) -> Result<usize, SbroadError> {
+fn create_projection(plan: &mut Plan, join_id: NodeId) -> Result<NodeId, SbroadError> {
     let proj_id = plan.add_proj(join_id, &[], false, false)?;
     let output_id = plan.get_relational_output(proj_id)?;
     plan.replace_parent_in_subtree(output_id, Some(join_id), Some(proj_id))?;
diff --git a/sbroad-core/src/ir/transformation/redistribution/tests.rs b/sbroad-core/src/ir/transformation/redistribution/tests.rs
index 2771326d0..9a5ef710d 100644
--- a/sbroad-core/src/ir/transformation/redistribution/tests.rs
+++ b/sbroad-core/src/ir/transformation/redistribution/tests.rs
@@ -1,288 +1,9 @@
 use super::*;
 use crate::ir::operator::Relational;
-use crate::ir::relation::{SpaceEngine, Table};
-use crate::ir::tests::column_integer_user_non_null;
 use crate::ir::transformation::helpers::sql_to_ir;
 use crate::ir::Plan;
 use crate::ir::Slices;
 use pretty_assertions::assert_eq;
-use smol_str::SmolStr;
-use std::fs;
-use std::path::Path;
-
-#[test]
-fn full_motion_less_for_sub_query() {
-    // t1(a int) key [a]
-    // t2(a int, b int) key [a]
-    // select * from t1 where a < (select b from t2)
-    let mut plan = Plan::default();
-    let mut children: Vec<usize> = Vec::new();
-
-    let t1 = Table::new_sharded(
-        "t1",
-        vec![column_integer_user_non_null(SmolStr::from("a"))],
-        &["a"],
-        &["a"],
-        SpaceEngine::Vinyl,
-    )
-    .unwrap();
-    plan.add_rel(t1);
-    let scan_t1_id = plan.add_scan("t1", None).unwrap();
-    children.push(scan_t1_id);
-
-    let t2 = Table::new_sharded(
-        "t2",
-        vec![
-            column_integer_user_non_null(SmolStr::from("a")),
-            column_integer_user_non_null(SmolStr::from("b")),
-        ],
-        &["a"],
-        &["a"],
-        SpaceEngine::Vinyl,
-    )
-    .unwrap();
-    plan.add_rel(t2);
-    let scan_t2_id = plan.add_scan("t2", None).unwrap();
-    let proj_id = plan.add_proj(scan_t2_id, &["b"], false, false).unwrap();
-    let sq_id = plan.add_sub_query(proj_id, None).unwrap();
-    children.push(sq_id);
-
-    let b_id = plan
-        .add_row_from_subquery(&children[..], children.len() - 1, None)
-        .unwrap();
-    let a_id = plan.add_row_from_child(scan_t1_id, &["a"]).unwrap();
-    let less_id = plan.add_cond(a_id, Bool::Lt, b_id).unwrap();
-
-    let select_id = plan.add_select(&children[..], less_id).unwrap();
-    plan.set_top(select_id).unwrap();
-
-    plan.add_motions().unwrap();
-
-    // Check the modified plan
-    plan.derive_equalities().unwrap();
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("transformation")
-        .join("redistribution")
-        .join("full_motion_less_for_sub_query.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    let expected_plan = Plan::from_yaml(&s).unwrap();
-    // This field is not serialized, do not check it
-    plan.context = None;
-    assert_eq!(plan, expected_plan);
-}
-
-#[test]
-#[allow(clippy::similar_names)]
-fn full_motion_non_segment_outer_for_sub_query() {
-    // t1(a int, b int) key [a]
-    // t2(a int) key [a]
-    // select * from t1 where b = (select a from t2)
-    let mut plan = Plan::default();
-    let mut children: Vec<usize> = Vec::new();
-
-    let t1 = Table::new_sharded(
-        "t1",
-        vec![
-            column_integer_user_non_null(SmolStr::from("a")),
-            column_integer_user_non_null(SmolStr::from("b")),
-        ],
-        &["a"],
-        &["a"],
-        SpaceEngine::Vinyl,
-    )
-    .unwrap();
-    plan.add_rel(t1);
-    let scan_t1_id = plan.add_scan("t1", None).unwrap();
-    children.push(scan_t1_id);
-
-    let t2 = Table::new_sharded(
-        "t2",
-        vec![column_integer_user_non_null(SmolStr::from("a"))],
-        &["a"],
-        &["a"],
-        SpaceEngine::Vinyl,
-    )
-    .unwrap();
-    plan.add_rel(t2);
-    let scan_t2_id = plan.add_scan("t2", None).unwrap();
-    let proj_id = plan.add_proj(scan_t2_id, &["a"], false, false).unwrap();
-    let sq_id = plan.add_sub_query(proj_id, None).unwrap();
-    children.push(sq_id);
-
-    let a_id = plan
-        .add_row_from_subquery(&children[..], children.len() - 1, None)
-        .unwrap();
-    let b_id = plan.add_row_from_child(scan_t1_id, &["b"]).unwrap();
-    let eq_id = plan.add_cond(b_id, Bool::Eq, a_id).unwrap();
-
-    let select_id = plan.add_select(&children[..], eq_id).unwrap();
-    plan.set_top(select_id).unwrap();
-
-    plan.add_motions().unwrap();
-
-    // Check the modified plan
-    plan.derive_equalities().unwrap();
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("transformation")
-        .join("redistribution")
-        .join("full_motion_non_segment_outer_for_sub_query.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    let expected_plan = Plan::from_yaml(&s).unwrap();
-    // This field is not serialized, do not check it
-    plan.context = None;
-    assert_eq!(plan, expected_plan);
-}
-
-#[test]
-#[allow(clippy::similar_names)]
-fn local_sub_query() {
-    // t1(a int) key [a]
-    // t2(a int, b int) key [a]
-    // select * from t1 where a = (select a from t2)
-    let mut plan = Plan::default();
-    let mut children: Vec<usize> = Vec::new();
-
-    let t1 = Table::new_sharded(
-        "t1",
-        vec![column_integer_user_non_null(SmolStr::from("a"))],
-        &["a"],
-        &["a"],
-        SpaceEngine::Memtx,
-    )
-    .unwrap();
-    plan.add_rel(t1);
-    let scan_t1_id = plan.add_scan("t1", None).unwrap();
-    children.push(scan_t1_id);
-
-    let t2 = Table::new_sharded(
-        "t2",
-        vec![column_integer_user_non_null(SmolStr::from("a"))],
-        &["a"],
-        &["a"],
-        SpaceEngine::Memtx,
-    )
-    .unwrap();
-    plan.add_rel(t2);
-    let scan_t2_id = plan.add_scan("t2", None).unwrap();
-    let proj_id = plan.add_proj(scan_t2_id, &["a"], false, false).unwrap();
-    let sq_id = plan.add_sub_query(proj_id, None).unwrap();
-    children.push(sq_id);
-
-    let inner_a_id = plan
-        .add_row_from_subquery(&children[..], children.len() - 1, None)
-        .unwrap();
-    let outer_a_id = plan.add_row_from_child(scan_t1_id, &["a"]).unwrap();
-    let eq_id = plan.add_cond(outer_a_id, Bool::Eq, inner_a_id).unwrap();
-
-    let select_id = plan.add_select(&children[..], eq_id).unwrap();
-    plan.set_top(select_id).unwrap();
-
-    plan.add_motions().unwrap();
-
-    // Check the modified plan
-    plan.derive_equalities().unwrap();
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("transformation")
-        .join("redistribution")
-        .join("local_sub_query.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    let expected_plan = Plan::from_yaml(&s).unwrap();
-    // This field is not serialized, do not check it
-    plan.context = None;
-    assert_eq!(plan, expected_plan);
-}
-
-#[test]
-fn multiple_sub_queries() {
-    // t1(a int) key [a]
-    // t2(a int, b int) key [a]
-    // select * from t1 where a < (select a from t2) or a = (select b from t2)
-    let mut plan = Plan::default();
-    let mut children: Vec<usize> = Vec::new();
-
-    let t1 = Table::new_sharded(
-        "t1",
-        vec![column_integer_user_non_null(SmolStr::from("a"))],
-        &["a"],
-        &["a"],
-        SpaceEngine::Memtx,
-    )
-    .unwrap();
-    plan.add_rel(t1);
-    let scan_t1_id = plan.add_scan("t1", None).unwrap();
-    children.push(scan_t1_id);
-
-    let t2 = Table::new_sharded(
-        "t2",
-        vec![
-            column_integer_user_non_null(SmolStr::from("a")),
-            column_integer_user_non_null(SmolStr::from("b")),
-        ],
-        &["a"],
-        &["a"],
-        SpaceEngine::Memtx,
-    )
-    .unwrap();
-    plan.add_rel(t2);
-    let sq1_scan_t2_id = plan.add_scan("t2", None).unwrap();
-    let sq1_proj_id = plan.add_proj(sq1_scan_t2_id, &["a"], false, false).unwrap();
-    let sq1_id = plan.add_sub_query(sq1_proj_id, None).unwrap();
-    children.push(sq1_id);
-    let sq1_pos = children.len() - 1;
-
-    let sq2_scan_t2_id = plan.add_scan("t2", None).unwrap();
-    let sq2_proj_id = plan.add_proj(sq2_scan_t2_id, &["b"], false, false).unwrap();
-    let sq2_id = plan.add_sub_query(sq2_proj_id, None).unwrap();
-    children.push(sq2_id);
-    let sq2_pos = children.len() - 1;
-
-    let sq1_inner_a_id = plan
-        .add_row_from_subquery(&children[..], sq1_pos, None)
-        .unwrap();
-    let sq1_outer_a_id = plan.add_row_from_child(scan_t1_id, &["a"]).unwrap();
-    let less_id = plan
-        .add_cond(sq1_outer_a_id, Bool::Lt, sq1_inner_a_id)
-        .unwrap();
-
-    let sq2_inner_a_id = plan
-        .add_row_from_subquery(&children[..], sq2_pos, None)
-        .unwrap();
-    let sq2_outer_a_id = plan.add_row_from_child(scan_t1_id, &["a"]).unwrap();
-    let eq_id = plan
-        .add_cond(sq2_outer_a_id, Bool::Eq, sq2_inner_a_id)
-        .unwrap();
-
-    let or_id = plan.add_cond(less_id, Bool::Or, eq_id).unwrap();
-
-    let select_id = plan.add_select(&children[..], or_id).unwrap();
-    plan.set_top(select_id).unwrap();
-
-    plan.add_motions().unwrap();
-
-    // Check the modified plan
-    plan.derive_equalities().unwrap();
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("transformation")
-        .join("redistribution")
-        .join("multiple_sub_queries.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    let expected_plan = Plan::from_yaml(&s).unwrap();
-    // This field is not serialized, do not check it
-    plan.context = None;
-    assert_eq!(plan, expected_plan);
-}
 
 #[test]
 fn union_all_in_sq() {
@@ -475,7 +196,7 @@ fn join_inner_or_local_full_policies() {
 /// # Panics
 ///   Motion node does not found
 #[must_use]
-pub fn get_motion_id(plan: &Plan, slice_id: usize, motion_idx: usize) -> Option<&usize> {
+pub fn get_motion_id(plan: &Plan, slice_id: usize, motion_idx: usize) -> Option<&NodeId> {
     plan.slices.slice(slice_id).unwrap().position(motion_idx)
 }
 
diff --git a/sbroad-core/src/ir/transformation/redistribution/tests/not_in.rs b/sbroad-core/src/ir/transformation/redistribution/tests/not_in.rs
index cb396f44e..8593501b9 100644
--- a/sbroad-core/src/ir/transformation/redistribution/tests/not_in.rs
+++ b/sbroad-core/src/ir/transformation/redistribution/tests/not_in.rs
@@ -1,8 +1,8 @@
 use crate::ir::operator::Relational;
 use crate::ir::transformation::helpers::sql_to_ir;
-use crate::ir::transformation::redistribution::tests::get_motion_id;
+use crate::ir::transformation::redistribution::tests::{get_motion_id, NodeId};
 use crate::ir::transformation::redistribution::MotionPolicy;
-use crate::ir::{Slice, Slices};
+use crate::ir::{ArenaType, Slice, Slices};
 use pretty_assertions::assert_eq;
 
 #[test]
@@ -28,7 +28,15 @@ fn not_in2() {
 
     let mut plan = sql_to_ir(query, vec![]);
     plan.add_motions().unwrap();
-    assert_eq!(Slices::from(vec![Slice { slice: vec![65] }]), plan.slices);
+    assert_eq!(
+        Slices::from(vec![Slice {
+            slice: vec![NodeId {
+                offset: 65,
+                arena_type: ArenaType::Default,
+            }]
+        }]),
+        plan.slices
+    );
 }
 
 #[test]
diff --git a/sbroad-core/src/ir/transformation/redistribution/tests/segment.rs b/sbroad-core/src/ir/transformation/redistribution/tests/segment.rs
index fe89c1f62..a42085758 100644
--- a/sbroad-core/src/ir/transformation/redistribution/tests/segment.rs
+++ b/sbroad-core/src/ir/transformation/redistribution/tests/segment.rs
@@ -1,102 +1,13 @@
 use crate::collection;
-use crate::errors::{Entity, SbroadError};
 use crate::ir::distribution::{Distribution, Key};
 use crate::ir::helpers::RepeatableState;
-use crate::ir::operator::{Bool, Relational};
-use crate::ir::relation::{Column, SpaceEngine, Table};
-use crate::ir::tests::column_integer_user_non_null;
+use crate::ir::operator::Relational;
+use crate::ir::relation::Column;
 use crate::ir::transformation::helpers::sql_to_ir;
 use crate::ir::transformation::redistribution::{MotionKey, MotionPolicy, Target};
-use crate::ir::{Node, Plan};
-use ahash::RandomState;
+use crate::ir::Node;
 use pretty_assertions::assert_eq;
-use smol_str::SmolStr;
 use std::collections::HashSet;
-use std::fs;
-use std::path::Path;
-
-#[test]
-#[allow(clippy::similar_names)]
-fn sub_query1() {
-    // t1(a int) key [a]
-    // t2(a int, b int) key [a]
-    // select * from t1 where a = (select b from t2)
-    let mut plan = Plan::default();
-    let mut children: Vec<usize> = Vec::new();
-
-    let t1 = Table::new_sharded(
-        "t1",
-        vec![column_integer_user_non_null(SmolStr::from("a"))],
-        &["a"],
-        &["a"],
-        SpaceEngine::Memtx,
-    )
-    .unwrap();
-    plan.add_rel(t1);
-    let scan_t1_id = plan.add_scan("t1", None).unwrap();
-    children.push(scan_t1_id);
-
-    let t2 = Table::new_sharded(
-        "t2",
-        vec![
-            column_integer_user_non_null(SmolStr::from("a")),
-            column_integer_user_non_null(SmolStr::from("b")),
-        ],
-        &["a"],
-        &["a"],
-        SpaceEngine::Memtx,
-    )
-    .unwrap();
-    plan.add_rel(t2);
-    let scan_t2_id = plan.add_scan("t2", None).unwrap();
-    let proj_id = plan.add_proj(scan_t2_id, &["b"], false, false).unwrap();
-    let sq_id = plan.add_sub_query(proj_id, None).unwrap();
-    children.push(sq_id);
-
-    let b_id = plan
-        .add_row_from_subquery(&children[..], children.len() - 1, None)
-        .unwrap();
-    let a_id = plan.add_row_from_child(scan_t1_id, &["a"]).unwrap();
-    let eq_id = plan.add_cond(a_id, Bool::Eq, b_id).unwrap();
-
-    let select_id = plan.add_select(&children[..], eq_id).unwrap();
-    plan.set_top(select_id).unwrap();
-
-    let mut expected_rel_set: HashSet<usize, RandomState> =
-        HashSet::with_hasher(RandomState::new());
-    expected_rel_set.insert(sq_id);
-    assert_eq!(
-        expected_rel_set,
-        plan.get_relational_nodes_from_row(b_id).unwrap()
-    );
-    assert_eq!(Some(sq_id), plan.get_sub_query_from_row_node(b_id).unwrap());
-
-    assert_eq!(
-        SbroadError::Invalid(
-            Entity::Distribution,
-            Some("distribution is uninitialized".into()),
-        ),
-        plan.resolve_sub_query_conflicts(select_id, eq_id)
-            .unwrap_err()
-    );
-
-    plan.add_motions().unwrap();
-
-    // Check the modified plan
-    plan.derive_equalities().unwrap();
-    let path = Path::new("")
-        .join("tests")
-        .join("artifactory")
-        .join("ir")
-        .join("transformation")
-        .join("redistribution")
-        .join("segment_motion_for_sub_query.yaml");
-    let s = fs::read_to_string(path).unwrap();
-    let expected_plan = Plan::from_yaml(&s).unwrap();
-    // This field is not serialized, do not check it
-    plan.context = None;
-    assert_eq!(plan, expected_plan);
-}
 
 #[test]
 fn inner_join1() {
diff --git a/sbroad-core/src/ir/transformation/split_columns.rs b/sbroad-core/src/ir/transformation/split_columns.rs
index 4b707001c..4b9cfe655 100644
--- a/sbroad-core/src/ir/transformation/split_columns.rs
+++ b/sbroad-core/src/ir/transformation/split_columns.rs
@@ -13,7 +13,7 @@
 //! ```
 
 use crate::errors::{Entity, SbroadError};
-use crate::ir::expression::Expression;
+use crate::ir::expression::{Expression, NodeId};
 use crate::ir::operator::Bool;
 use crate::ir::transformation::OldNewTopIdPair;
 use crate::ir::Plan;
@@ -23,7 +23,7 @@ use smol_str::format_smolstr;
 
 fn call_expr_tree_split_columns(
     plan: &mut Plan,
-    top_id: usize,
+    top_id: NodeId,
 ) -> Result<OldNewTopIdPair, SbroadError> {
     plan.expr_tree_replace_bool(
         top_id,
@@ -39,7 +39,7 @@ fn call_expr_tree_split_columns(
     )
 }
 
-fn call_split_bool(plan: &mut Plan, top_id: usize) -> Result<OldNewTopIdPair, SbroadError> {
+fn call_split_bool(plan: &mut Plan, top_id: NodeId) -> Result<OldNewTopIdPair, SbroadError> {
     plan.split_bool(top_id)
 }
 
@@ -51,7 +51,7 @@ impl Plan {
     /// - If the operator is not a boolean operator.
     /// - If left and right tuples have different number of columns.
     /// - If the plan is invalid for some unknown reason.
-    fn split_bool(&mut self, top_id: usize) -> Result<OldNewTopIdPair, SbroadError> {
+    fn split_bool(&mut self, top_id: NodeId) -> Result<OldNewTopIdPair, SbroadError> {
         let top_expr = self.get_expression_node(top_id)?;
         let (left_id, right_id, op) = match top_expr {
             Expression::Bool {
diff --git a/sbroad-core/src/ir/transformation/split_columns/tests.rs b/sbroad-core/src/ir/transformation/split_columns/tests.rs
index 427f480d3..0c038731c 100644
--- a/sbroad-core/src/ir/transformation/split_columns/tests.rs
+++ b/sbroad-core/src/ir/transformation/split_columns/tests.rs
@@ -53,8 +53,8 @@ fn split_columns3() {
             "{} {} {} {}",
             r#"unexpected number of values:"#,
             r#"left and right rows have different number of columns:"#,
-            r#"Row { list: [12, 13, 14], distribution: None },"#,
-            r#"Row { list: [15, 16], distribution: None }"#,
+            r#"Row { list: [NodeId { offset: 12, arena_type: Default }, NodeId { offset: 13, arena_type: Default }, NodeId { offset: 14, arena_type: Default }], distribution: None },"#,
+            r#"Row { list: [NodeId { offset: 15, arena_type: Default }, NodeId { offset: 16, arena_type: Default }], distribution: None }"#,
         ),
         format!("{plan_err}")
     );
diff --git a/sbroad-core/src/ir/tree.rs b/sbroad-core/src/ir/tree.rs
index f301a2a99..761135770 100644
--- a/sbroad-core/src/ir/tree.rs
+++ b/sbroad-core/src/ir/tree.rs
@@ -1,15 +1,17 @@
 //! IR tree traversal module.
 
-use super::{Nodes, Plan};
-use crate::ir::expression::Expression;
+use super::{
+    expression::{Expression, NodeId},
+    Nodes, Plan,
+};
 use std::cell::RefCell;
 
 trait TreeIterator<'nodes> {
-    fn get_current(&self) -> usize;
+    fn get_current(&self) -> NodeId;
     fn get_child(&self) -> &RefCell<usize>;
     fn get_nodes(&self) -> &'nodes Nodes;
 
-    fn handle_trim(&mut self, expr: &'nodes Expression) -> Option<&'nodes usize> {
+    fn handle_trim(&mut self, expr: &'nodes Expression) -> Option<&'nodes NodeId> {
         let Expression::Trim {
             pattern, target, ..
         } = expr
@@ -36,7 +38,7 @@ trait TreeIterator<'nodes> {
         }
     }
 
-    fn handle_left_right_children(&mut self, expr: &'nodes Expression) -> Option<&'nodes usize> {
+    fn handle_left_right_children(&mut self, expr: &'nodes Expression) -> Option<&'nodes NodeId> {
         let (Expression::Bool { left, right, .. }
         | Expression::Arithmetic { left, right, .. }
         | Expression::Concat { left, right, .. }) = expr
@@ -54,7 +56,7 @@ trait TreeIterator<'nodes> {
         None
     }
 
-    fn handle_single_child(&mut self, expr: &'nodes Expression) -> Option<&'nodes usize> {
+    fn handle_single_child(&mut self, expr: &'nodes Expression) -> Option<&'nodes NodeId> {
         let (Expression::Alias { child, .. }
         | Expression::ExprInParentheses { child }
         | Expression::Cast { child, .. }
@@ -70,7 +72,7 @@ trait TreeIterator<'nodes> {
         None
     }
 
-    fn handle_case_iter(&mut self, expr: &'nodes Expression) -> Option<&'nodes usize> {
+    fn handle_case_iter(&mut self, expr: &'nodes Expression) -> Option<&'nodes NodeId> {
         let Expression::Case {
             search_expr,
             when_blocks,
diff --git a/sbroad-core/src/ir/tree/and.rs b/sbroad-core/src/ir/tree/and.rs
index 3b4a080c1..c3cfc3167 100644
--- a/sbroad-core/src/ir/tree/and.rs
+++ b/sbroad-core/src/ir/tree/and.rs
@@ -1,7 +1,7 @@
 use std::cell::RefCell;
 
 use super::TreeIterator;
-use crate::ir::expression::Expression;
+use crate::ir::expression::{Expression, NodeId};
 use crate::ir::operator::Bool;
 use crate::ir::{Node, Nodes};
 
@@ -13,13 +13,13 @@ trait AndTreeIterator<'nodes>: TreeIterator<'nodes> {}
 #[allow(clippy::module_name_repetitions)]
 #[derive(Debug)]
 pub struct AndIterator<'n> {
-    current: usize,
+    current: NodeId,
     child: RefCell<usize>,
     nodes: &'n Nodes,
 }
 
 impl<'nodes> TreeIterator<'nodes> for AndIterator<'nodes> {
-    fn get_current(&self) -> usize {
+    fn get_current(&self) -> NodeId {
         self.current
     }
 
@@ -36,7 +36,7 @@ impl<'nodes> AndTreeIterator<'nodes> for AndIterator<'nodes> {}
 
 impl<'n> Nodes {
     #[must_use]
-    pub fn and_iter(&'n self, current: usize) -> AndIterator<'n> {
+    pub fn and_iter(&'n self, current: NodeId) -> AndIterator<'n> {
         AndIterator {
             current,
             child: RefCell::new(0),
@@ -46,15 +46,15 @@ impl<'n> Nodes {
 }
 
 impl<'n> Iterator for AndIterator<'n> {
-    type Item = usize;
+    type Item = NodeId;
 
     fn next(&mut self) -> Option<Self::Item> {
         and_next(self).copied()
     }
 }
 
-fn and_next<'nodes>(iter: &mut impl AndTreeIterator<'nodes>) -> Option<&'nodes usize> {
-    let node = iter.get_nodes().arena.get(iter.get_current());
+fn and_next<'nodes>(iter: &mut impl AndTreeIterator<'nodes>) -> Option<&'nodes NodeId> {
+    let node = iter.get_nodes().get(iter.get_current());
     if let Some(Node::Expression(Expression::Bool {
         left, op, right, ..
     })) = node
diff --git a/sbroad-core/src/ir/tree/expression.rs b/sbroad-core/src/ir/tree/expression.rs
index 53737b4f5..faaac377a 100644
--- a/sbroad-core/src/ir/tree/expression.rs
+++ b/sbroad-core/src/ir/tree/expression.rs
@@ -1,7 +1,7 @@
 use std::cell::RefCell;
 
 use super::TreeIterator;
-use crate::ir::expression::Expression;
+use crate::ir::expression::{Expression, NodeId};
 use crate::ir::{Node, Nodes};
 
 trait ExpressionTreeIterator<'nodes>: TreeIterator<'nodes> {
@@ -15,7 +15,7 @@ trait ExpressionTreeIterator<'nodes>: TreeIterator<'nodes> {
 #[allow(clippy::module_name_repetitions)]
 #[derive(Debug)]
 pub struct ExpressionIterator<'n> {
-    current: usize,
+    current: NodeId,
     child: RefCell<usize>,
     nodes: &'n Nodes,
     make_row_leaf: bool,
@@ -28,7 +28,7 @@ pub struct AggregateIterator<'p> {
 
 impl<'n> Nodes {
     #[must_use]
-    pub fn expr_iter(&'n self, current: usize, make_row_leaf: bool) -> ExpressionIterator<'n> {
+    pub fn expr_iter(&'n self, current: NodeId, make_row_leaf: bool) -> ExpressionIterator<'n> {
         ExpressionIterator {
             current,
             child: RefCell::new(0),
@@ -38,9 +38,9 @@ impl<'n> Nodes {
     }
 
     #[must_use]
-    pub fn aggregate_iter(&'n self, current: usize, make_row_leaf: bool) -> AggregateIterator<'n> {
+    pub fn aggregate_iter(&'n self, current: NodeId, make_row_leaf: bool) -> AggregateIterator<'n> {
         let must_stop = if let Some(Node::Expression(Expression::StableFunction { name, .. })) =
-            self.arena.get(current)
+            self.get(current)
         {
             Expression::is_aggregate_name(name)
         } else {
@@ -59,7 +59,7 @@ impl<'n> Nodes {
 }
 
 impl<'nodes> TreeIterator<'nodes> for ExpressionIterator<'nodes> {
-    fn get_current(&self) -> usize {
+    fn get_current(&self) -> NodeId {
         self.current
     }
 
@@ -79,7 +79,7 @@ impl<'nodes> ExpressionTreeIterator<'nodes> for ExpressionIterator<'nodes> {
 }
 
 impl<'n> Iterator for ExpressionIterator<'n> {
-    type Item = usize;
+    type Item = NodeId;
 
     fn next(&mut self) -> Option<Self::Item> {
         expression_next(self).copied()
@@ -87,7 +87,7 @@ impl<'n> Iterator for ExpressionIterator<'n> {
 }
 
 impl<'n> Iterator for AggregateIterator<'n> {
-    type Item = usize;
+    type Item = NodeId;
 
     fn next(&mut self) -> Option<Self::Item> {
         if self.must_stop {
@@ -100,8 +100,8 @@ impl<'n> Iterator for AggregateIterator<'n> {
 #[allow(clippy::too_many_lines)]
 fn expression_next<'nodes>(
     iter: &mut impl ExpressionTreeIterator<'nodes>,
-) -> Option<&'nodes usize> {
-    let node = iter.get_nodes().arena.get(iter.get_current());
+) -> Option<&'nodes NodeId> {
+    let node = iter.get_nodes().get(iter.get_current());
     match node {
         Some(node) => {
             match node {
@@ -123,7 +123,7 @@ fn expression_next<'nodes>(
                                 is_leaf = true;
                                 for col in list {
                                     if !matches!(
-                                        iter.get_nodes().arena.get(*col),
+                                        iter.get_nodes().get(*col),
                                         Some(Node::Expression(
                                             Expression::Reference { .. }
                                                 | Expression::Constant { .. }
diff --git a/sbroad-core/src/ir/tree/relation.rs b/sbroad-core/src/ir/tree/relation.rs
index 40733e65e..cc14584d1 100644
--- a/sbroad-core/src/ir/tree/relation.rs
+++ b/sbroad-core/src/ir/tree/relation.rs
@@ -1,8 +1,9 @@
 use std::cell::RefCell;
 
 use super::TreeIterator;
+use crate::ir::expression::NodeId;
 use crate::ir::operator::Relational;
-use crate::ir::{Node, Nodes};
+use crate::ir::{ArenaType, Node, Nodes};
 
 trait RelationalTreeIterator<'nodes>: TreeIterator<'nodes> {}
 
@@ -11,14 +12,14 @@ trait RelationalTreeIterator<'nodes>: TreeIterator<'nodes> {}
 /// The iterator returns the next relational node in the plan tree.
 #[derive(Debug)]
 pub struct RelationalIterator<'n> {
-    current: usize,
+    current: NodeId,
     child: RefCell<usize>,
     nodes: &'n Nodes,
 }
 
 impl<'n> Nodes {
     #[must_use]
-    pub fn rel_iter(&'n self, current: usize) -> RelationalIterator<'n> {
+    pub fn rel_iter(&'n self, current: NodeId) -> RelationalIterator<'n> {
         RelationalIterator {
             current,
             child: RefCell::new(0),
@@ -29,15 +30,15 @@ impl<'n> Nodes {
     #[must_use]
     pub fn empty_rel_iter(&'n self) -> RelationalIterator<'n> {
         RelationalIterator {
-            current: self.next_id(),
-            child: RefCell::new(1000),
+            current: self.next_id(ArenaType::Default),
+            child: RefCell::new(0),
             nodes: self,
         }
     }
 }
 
 impl<'nodes> TreeIterator<'nodes> for RelationalIterator<'nodes> {
-    fn get_current(&self) -> usize {
+    fn get_current(&self) -> NodeId {
         self.current
     }
 
@@ -53,7 +54,7 @@ impl<'nodes> TreeIterator<'nodes> for RelationalIterator<'nodes> {
 impl<'nodes> RelationalTreeIterator<'nodes> for RelationalIterator<'nodes> {}
 
 impl<'n> Iterator for RelationalIterator<'n> {
-    type Item = usize;
+    type Item = NodeId;
 
     fn next(&mut self) -> Option<Self::Item> {
         relational_next(self).copied()
@@ -62,8 +63,8 @@ impl<'n> Iterator for RelationalIterator<'n> {
 
 fn relational_next<'nodes>(
     iter: &mut impl RelationalTreeIterator<'nodes>,
-) -> Option<&'nodes usize> {
-    match iter.get_nodes().arena.get(iter.get_current()) {
+) -> Option<&'nodes NodeId> {
+    match iter.get_nodes().get(iter.get_current()) {
         Some(Node::Relational(
             node @ (Relational::Except { .. }
             | Relational::Join { .. }
diff --git a/sbroad-core/src/ir/tree/subtree.rs b/sbroad-core/src/ir/tree/subtree.rs
index ce331404a..4612eae7d 100644
--- a/sbroad-core/src/ir/tree/subtree.rs
+++ b/sbroad-core/src/ir/tree/subtree.rs
@@ -2,7 +2,7 @@ use std::cell::RefCell;
 use std::cmp::Ordering;
 
 use super::{PlanTreeIterator, Snapshot, TreeIterator};
-use crate::ir::expression::Expression;
+use crate::ir::expression::{Expression, NodeId};
 use crate::ir::operator::{OrderByElement, OrderByEntity, Relational};
 use crate::ir::{Node, Nodes, Plan};
 
@@ -15,14 +15,14 @@ trait SubtreePlanIterator<'plan>: PlanTreeIterator<'plan> {
 #[allow(clippy::module_name_repetitions)]
 #[derive(Debug)]
 pub struct SubtreeIterator<'plan> {
-    current: usize,
+    current: NodeId,
     child: RefCell<usize>,
     plan: &'plan Plan,
     need_output: bool,
 }
 
 impl<'nodes> TreeIterator<'nodes> for SubtreeIterator<'nodes> {
-    fn get_current(&self) -> usize {
+    fn get_current(&self) -> NodeId {
         self.current
     }
 
@@ -52,7 +52,7 @@ impl<'plan> SubtreePlanIterator<'plan> for SubtreeIterator<'plan> {
 }
 
 impl<'plan> Iterator for SubtreeIterator<'plan> {
-    type Item = usize;
+    type Item = NodeId;
 
     fn next(&mut self) -> Option<Self::Item> {
         subtree_next(self, &Snapshot::Latest).copied()
@@ -61,7 +61,7 @@ impl<'plan> Iterator for SubtreeIterator<'plan> {
 
 impl<'plan> Plan {
     #[must_use]
-    pub fn subtree_iter(&'plan self, current: usize, need_output: bool) -> SubtreeIterator<'plan> {
+    pub fn subtree_iter(&'plan self, current: NodeId, need_output: bool) -> SubtreeIterator<'plan> {
         SubtreeIterator {
             current,
             child: RefCell::new(0),
@@ -77,13 +77,13 @@ impl<'plan> Plan {
 /// at the moment).
 #[derive(Debug)]
 pub struct FlashbackSubtreeIterator<'plan> {
-    current: usize,
+    current: NodeId,
     child: RefCell<usize>,
     plan: &'plan Plan,
 }
 
 impl<'nodes> TreeIterator<'nodes> for FlashbackSubtreeIterator<'nodes> {
-    fn get_current(&self) -> usize {
+    fn get_current(&self) -> NodeId {
         self.current
     }
 
@@ -113,7 +113,7 @@ impl<'plan> SubtreePlanIterator<'plan> for FlashbackSubtreeIterator<'plan> {
 }
 
 impl<'plan> Iterator for FlashbackSubtreeIterator<'plan> {
-    type Item = usize;
+    type Item = NodeId;
 
     fn next(&mut self) -> Option<Self::Item> {
         subtree_next(self, &Snapshot::Oldest).copied()
@@ -122,7 +122,7 @@ impl<'plan> Iterator for FlashbackSubtreeIterator<'plan> {
 
 impl<'plan> Plan {
     #[must_use]
-    pub fn flashback_subtree_iter(&'plan self, current: usize) -> FlashbackSubtreeIterator<'plan> {
+    pub fn flashback_subtree_iter(&'plan self, current: NodeId) -> FlashbackSubtreeIterator<'plan> {
         FlashbackSubtreeIterator {
             current,
             child: RefCell::new(0),
@@ -134,13 +134,13 @@ impl<'plan> Plan {
 /// An iterator used while copying and execution plan subtree.
 #[derive(Debug)]
 pub struct ExecPlanSubtreeIterator<'plan> {
-    current: usize,
+    current: NodeId,
     child: RefCell<usize>,
     plan: &'plan Plan,
 }
 
 impl<'nodes> TreeIterator<'nodes> for ExecPlanSubtreeIterator<'nodes> {
-    fn get_current(&self) -> usize {
+    fn get_current(&self) -> NodeId {
         self.current
     }
 
@@ -170,7 +170,7 @@ impl<'plan> SubtreePlanIterator<'plan> for ExecPlanSubtreeIterator<'plan> {
 }
 
 impl<'plan> Iterator for ExecPlanSubtreeIterator<'plan> {
-    type Item = usize;
+    type Item = NodeId;
 
     fn next(&mut self) -> Option<Self::Item> {
         subtree_next(self, &Snapshot::Oldest).copied()
@@ -179,7 +179,7 @@ impl<'plan> Iterator for ExecPlanSubtreeIterator<'plan> {
 
 impl<'plan> Plan {
     #[must_use]
-    pub fn exec_plan_subtree_iter(&'plan self, current: usize) -> ExecPlanSubtreeIterator<'plan> {
+    pub fn exec_plan_subtree_iter(&'plan self, current: NodeId) -> ExecPlanSubtreeIterator<'plan> {
         ExecPlanSubtreeIterator {
             current,
             child: RefCell::new(0),
@@ -192,8 +192,8 @@ impl<'plan> Plan {
 fn subtree_next<'plan>(
     iter: &mut impl SubtreePlanIterator<'plan>,
     snapshot: &Snapshot,
-) -> Option<&'plan usize> {
-    if let Some(child) = iter.get_nodes().arena.get(iter.get_current()) {
+) -> Option<&'plan NodeId> {
+    if let Some(child) = iter.get_nodes().get(iter.get_current()) {
         return match child {
             Node::Parameter(..) | Node::Ddl(..) | Node::Acl(..) | Node::Block(..) => None,
             Node::Expression(expr) => match expr {
diff --git a/sbroad-core/src/ir/tree/tests.rs b/sbroad-core/src/ir/tree/tests.rs
index cd1589acc..c188bbcce 100644
--- a/sbroad-core/src/ir/tree/tests.rs
+++ b/sbroad-core/src/ir/tree/tests.rs
@@ -1,9 +1,9 @@
 use crate::ir::operator::Bool;
 use crate::ir::relation::{SpaceEngine, Table, Type};
 use crate::ir::tests::column_user_non_null;
-use crate::ir::tree::traversal::{BreadthFirst, PostOrder, EXPR_CAPACITY, REL_CAPACITY};
+use crate::ir::tree::traversal::{BreadthFirst, LevelNode, PostOrder, EXPR_CAPACITY, REL_CAPACITY};
 use crate::ir::value::Value;
-use crate::ir::{Expression, Plan};
+use crate::ir::{ArenaType, Expression, Plan};
 use pretty_assertions::assert_eq;
 use smol_str::SmolStr;
 
@@ -33,17 +33,17 @@ fn expression_bft() {
         EXPR_CAPACITY,
     );
     let mut iter = bft_tree.iter(top);
-    assert_eq!(iter.next(), Some((0, top)));
-    assert_eq!(iter.next(), Some((1, c1c2_and_c2c3)));
-    assert_eq!(iter.next(), Some((1, c4_eq_c5)));
-    assert_eq!(iter.next(), Some((2, c1_eq_c2)));
-    assert_eq!(iter.next(), Some((2, c2_eq_c3)));
-    assert_eq!(iter.next(), Some((2, c4)));
-    assert_eq!(iter.next(), Some((2, c5)));
-    assert_eq!(iter.next(), Some((3, c1)));
-    assert_eq!(iter.next(), Some((3, c2)));
-    assert_eq!(iter.next(), Some((3, c2)));
-    assert_eq!(iter.next(), Some((3, c3)));
+    assert_eq!(iter.next(), Some(LevelNode(0, top)));
+    assert_eq!(iter.next(), Some(LevelNode(1, c1c2_and_c2c3)));
+    assert_eq!(iter.next(), Some(LevelNode(1, c4_eq_c5)));
+    assert_eq!(iter.next(), Some(LevelNode(2, c1_eq_c2)));
+    assert_eq!(iter.next(), Some(LevelNode(2, c2_eq_c3)));
+    assert_eq!(iter.next(), Some(LevelNode(2, c4)));
+    assert_eq!(iter.next(), Some(LevelNode(2, c5)));
+    assert_eq!(iter.next(), Some(LevelNode(3, c1)));
+    assert_eq!(iter.next(), Some(LevelNode(3, c2)));
+    assert_eq!(iter.next(), Some(LevelNode(3, c2)));
+    assert_eq!(iter.next(), Some(LevelNode(3, c3)));
     assert_eq!(iter.next(), None);
 }
 
@@ -89,10 +89,10 @@ fn relational_post() {
     // Traverse the tree
     let mut dft_post = PostOrder::with_capacity(|node| plan.nodes.rel_iter(node), REL_CAPACITY);
     let mut iter = dft_post.iter(top);
-    assert_eq!(iter.next(), Some((1, scan_t1_id)));
-    assert_eq!(iter.next(), Some((2, scan_t2_id)));
-    assert_eq!(iter.next(), Some((1, selection_id)));
-    assert_eq!(iter.next(), Some((0, union_id)));
+    assert_eq!(iter.next(), Some(LevelNode(1, scan_t1_id)));
+    assert_eq!(iter.next(), Some(LevelNode(2, scan_t2_id)));
+    assert_eq!(iter.next(), Some(LevelNode(1, selection_id)));
+    assert_eq!(iter.next(), Some(LevelNode(0, union_id)));
     assert_eq!(iter.next(), None);
 }
 
@@ -164,30 +164,30 @@ fn selection_subquery_dfs_post() {
     // Traverse relational nodes in the plan tree
     let mut dft_post = PostOrder::with_capacity(|node| plan.nodes.rel_iter(node), REL_CAPACITY);
     let mut iter = dft_post.iter(top);
-    assert_eq!(iter.next(), Some((1, scan_t1_id)));
-    assert_eq!(iter.next(), Some((4, scan_t2_id)));
-    assert_eq!(iter.next(), Some((3, selection_t2_id)));
-    assert_eq!(iter.next(), Some((2, proj_id)));
-    assert_eq!(iter.next(), Some((1, sq_id)));
-    assert_eq!(iter.next(), Some((0, selection_t1_id)));
+    assert_eq!(iter.next(), Some(LevelNode(1, scan_t1_id)));
+    assert_eq!(iter.next(), Some(LevelNode(4, scan_t2_id)));
+    assert_eq!(iter.next(), Some(LevelNode(3, selection_t2_id)));
+    assert_eq!(iter.next(), Some(LevelNode(2, proj_id)));
+    assert_eq!(iter.next(), Some(LevelNode(1, sq_id)));
+    assert_eq!(iter.next(), Some(LevelNode(0, selection_t1_id)));
     assert_eq!(iter.next(), None);
 
     // Traverse expression nodes in the selection t2 filter
     let mut dft_post =
         PostOrder::with_capacity(|node| plan.nodes.expr_iter(node, true), EXPR_CAPACITY);
     let mut iter = dft_post.iter(eq_op);
-    assert_eq!(iter.next(), Some((1, b)));
-    assert_eq!(iter.next(), Some((1, const1)));
-    assert_eq!(iter.next(), Some((0, eq_op)));
+    assert_eq!(iter.next(), Some(LevelNode(1, b)));
+    assert_eq!(iter.next(), Some(LevelNode(1, const1)));
+    assert_eq!(iter.next(), Some(LevelNode(0, eq_op)));
     assert_eq!(iter.next(), None);
 
     // Traverse expression nodes in the selection t1 filter
     let mut dft_post =
         PostOrder::with_capacity(|node| plan.nodes.expr_iter(node, true), EXPR_CAPACITY);
     let mut iter = dft_post.iter(in_op);
-    assert_eq!(iter.next(), Some((1, a)));
-    assert_eq!(iter.next(), Some((1, c)));
-    assert_eq!(iter.next(), Some((0, in_op)));
+    assert_eq!(iter.next(), Some(LevelNode(1, a)));
+    assert_eq!(iter.next(), Some(LevelNode(1, c)));
+    assert_eq!(iter.next(), Some(LevelNode(0, in_op)));
     assert_eq!(iter.next(), None);
 }
 
@@ -221,7 +221,7 @@ fn subtree_dfs_post() {
     .unwrap();
     plan.add_rel(t1);
     let scan_t1_id = plan.add_scan("t1", None).unwrap();
-    let a_ref = plan.nodes.next_id();
+    let a_ref = plan.nodes.next_id(ArenaType::Default);
     let a = plan.add_row_from_child(scan_t1_id, &["a"]).unwrap();
     let const1 = plan.add_const(Value::from(1_i64));
     let eq_op = plan.nodes.add_bool(a, Bool::Eq, const1).unwrap();
@@ -249,17 +249,17 @@ fn subtree_dfs_post() {
 
     // Traverse relational nodes in the plan tree
     let mut dft_post =
-        PostOrder::with_capacity(|node| plan.subtree_iter(node, false), plan.next_id());
+        PostOrder::with_capacity(|node| plan.subtree_iter(node, false), plan.nodes.len());
     let mut iter = dft_post.iter(top);
-    assert_eq!(iter.next(), Some((3, *c_ref_id)));
-    assert_eq!(iter.next(), Some((2, *alias_id)));
-    assert_eq!(iter.next(), Some((1, proj_row_id)));
-    assert_eq!(iter.next(), Some((2, scan_t1_id)));
-    assert_eq!(iter.next(), Some((4, a_ref)));
-    assert_eq!(iter.next(), Some((3, a)));
-    assert_eq!(iter.next(), Some((3, const1)));
-    assert_eq!(iter.next(), Some((2, eq_op)));
-    assert_eq!(iter.next(), Some((1, selection_t1_id)));
-    assert_eq!(iter.next(), Some((0, proj_id)));
+    assert_eq!(iter.next(), Some(LevelNode(3, *c_ref_id)));
+    assert_eq!(iter.next(), Some(LevelNode(2, *alias_id)));
+    assert_eq!(iter.next(), Some(LevelNode(1, proj_row_id)));
+    assert_eq!(iter.next(), Some(LevelNode(2, scan_t1_id)));
+    assert_eq!(iter.next(), Some(LevelNode(4, a_ref)));
+    assert_eq!(iter.next(), Some(LevelNode(3, a)));
+    assert_eq!(iter.next(), Some(LevelNode(3, const1)));
+    assert_eq!(iter.next(), Some(LevelNode(2, eq_op)));
+    assert_eq!(iter.next(), Some(LevelNode(1, selection_t1_id)));
+    assert_eq!(iter.next(), Some(LevelNode(0, proj_id)));
     assert_eq!(iter.next(), None);
 }
diff --git a/sbroad-core/src/ir/tree/traversal.rs b/sbroad-core/src/ir/tree/traversal.rs
index e07788711..40478b9f2 100644
--- a/sbroad-core/src/ir/tree/traversal.rs
+++ b/sbroad-core/src/ir/tree/traversal.rs
@@ -4,36 +4,41 @@ pub const EXPR_CAPACITY: usize = 64;
 pub const REL_CAPACITY: usize = 32;
 
 /// Pair of (Level of the node in traversal algorithm, `node_id`).
-pub type LevelNode = (usize, usize);
+#[derive(Debug, PartialEq)]
+pub struct LevelNode<T>(pub usize, pub T)
+where
+    T: Copy;
 
-pub struct PostOrder<F, I>
+pub struct PostOrder<F, I, T>
 where
-    F: FnMut(usize) -> I,
-    I: Iterator<Item = usize>,
+    F: FnMut(T) -> I,
+    I: Iterator<Item = T>,
+    T: Copy,
 {
-    inner: PostOrderWithFilter<'static, F, I>,
+    inner: PostOrderWithFilter<'static, F, I, T>,
 }
 
-impl<F, I> PostOrder<F, I>
+impl<F, I, T> PostOrder<F, I, T>
 where
-    F: FnMut(usize) -> I,
-    I: Iterator<Item = usize>,
+    F: FnMut(T) -> I,
+    I: Iterator<Item = T>,
+    T: Copy,
 {
-    pub fn iter(&mut self, root: usize) -> impl Iterator<Item = LevelNode> {
+    pub fn iter(&mut self, root: T) -> impl Iterator<Item = LevelNode<T>> {
         self.inner.iter(root)
     }
 
-    pub fn new(iter_children: F, nodes: Vec<LevelNode>) -> Self {
+    pub fn new(iter_children: F, nodes: Vec<LevelNode<T>>) -> Self {
         Self {
             inner: PostOrderWithFilter::new(iter_children, nodes, Box::new(|_| true)),
         }
     }
 
-    pub fn populate_nodes(&mut self, root: usize) {
+    pub fn populate_nodes(&mut self, root: T) {
         self.inner.populate_nodes(root);
     }
 
-    pub fn take_nodes(&mut self) -> Vec<LevelNode> {
+    pub fn take_nodes(&mut self) -> Vec<LevelNode<T>> {
         self.inner.take_nodes()
     }
 
@@ -44,29 +49,35 @@ where
     }
 }
 
-pub type FilterFn<'filter> = Box<dyn Fn(usize) -> bool + 'filter>;
+pub type FilterFn<'filter, T> = Box<dyn Fn(T) -> bool + 'filter>;
 
-pub struct PostOrderWithFilter<'filter, F, I>
+pub struct PostOrderWithFilter<'filter, F, I, T>
 where
-    F: FnMut(usize) -> I,
-    I: Iterator<Item = usize>,
+    F: FnMut(T) -> I,
+    I: Iterator<Item = T>,
+    T: Copy,
 {
     iter_children: F,
-    nodes: Vec<LevelNode>,
-    filter_fn: FilterFn<'filter>,
+    nodes: Vec<LevelNode<T>>,
+    filter_fn: FilterFn<'filter, T>,
 }
 
-impl<'filter, F, I> PostOrderWithFilter<'filter, F, I>
+impl<'filter, F, I, T> PostOrderWithFilter<'filter, F, I, T>
 where
-    F: FnMut(usize) -> I,
-    I: Iterator<Item = usize>,
+    F: FnMut(T) -> I,
+    I: Iterator<Item = T>,
+    T: Copy,
 {
-    pub fn iter(&mut self, root: usize) -> impl Iterator<Item = LevelNode> {
+    pub fn iter(&mut self, root: T) -> impl Iterator<Item = LevelNode<T>> {
         self.populate_nodes(root);
         self.take_nodes().into_iter()
     }
 
-    pub fn new(iter_children: F, nodes: Vec<LevelNode>, filter_fn: FilterFn<'filter>) -> Self {
+    pub fn new(
+        iter_children: F,
+        nodes: Vec<LevelNode<T>>,
+        filter_fn: FilterFn<'filter, T>,
+    ) -> Self {
         Self {
             iter_children,
             nodes,
@@ -74,25 +85,25 @@ where
         }
     }
 
-    pub fn populate_nodes(&mut self, root: usize) {
+    pub fn populate_nodes(&mut self, root: T) {
         self.nodes.clear();
         self.traverse(root, 0);
     }
 
-    pub fn take_nodes(&mut self) -> Vec<LevelNode> {
+    pub fn take_nodes(&mut self) -> Vec<LevelNode<T>> {
         std::mem::take(&mut self.nodes)
     }
 
-    fn traverse(&mut self, root: usize, level: usize) {
+    fn traverse(&mut self, root: T, level: usize) {
         for child in (self.iter_children)(root) {
             self.traverse(child, level + 1);
         }
         if (self.filter_fn)(root) {
-            self.nodes.push((level, root));
+            self.nodes.push(LevelNode(level, root));
         }
     }
 
-    pub fn with_capacity(iter_children: F, capacity: usize, filter: FilterFn<'filter>) -> Self {
+    pub fn with_capacity(iter_children: F, capacity: usize, filter: FilterFn<'filter, T>) -> Self {
         Self {
             iter_children,
             nodes: Vec::with_capacity(capacity),
@@ -101,27 +112,29 @@ where
     }
 }
 
-pub struct BreadthFirst<F, I>
+pub struct BreadthFirst<F, I, T>
 where
-    F: FnMut(usize) -> I,
-    I: Iterator<Item = usize>,
+    F: FnMut(T) -> I,
+    I: Iterator<Item = T>,
+    T: Copy,
 {
     iter_children: F,
-    queue: VecDeque<LevelNode>,
-    nodes: Vec<LevelNode>,
+    queue: VecDeque<LevelNode<T>>,
+    nodes: Vec<LevelNode<T>>,
 }
 
-impl<F, I> BreadthFirst<F, I>
+impl<F, I, T> BreadthFirst<F, I, T>
 where
-    F: FnMut(usize) -> I,
-    I: Iterator<Item = usize>,
+    F: FnMut(T) -> I,
+    I: Iterator<Item = T>,
+    T: Copy,
 {
-    pub fn iter(&mut self, root: usize) -> impl Iterator<Item = LevelNode> {
+    pub fn iter(&mut self, root: T) -> impl Iterator<Item = LevelNode<T>> {
         self.populate_nodes(root);
         self.take_nodes().into_iter()
     }
 
-    pub fn new(iter_children: F, queue: VecDeque<LevelNode>, nodes: Vec<LevelNode>) -> Self {
+    pub fn new(iter_children: F, queue: VecDeque<LevelNode<T>>, nodes: Vec<LevelNode<T>>) -> Self {
         Self {
             iter_children,
             queue,
@@ -129,17 +142,17 @@ where
         }
     }
 
-    pub fn populate_nodes(&mut self, root: usize) {
-        self.queue.push_back((0, root));
-        while let Some((level, node)) = self.queue.pop_front() {
-            self.nodes.push((level, node));
+    pub fn populate_nodes(&mut self, root: T) {
+        self.queue.push_back(LevelNode(0, root));
+        while let Some(LevelNode(level, node)) = self.queue.pop_front() {
+            self.nodes.push(LevelNode(level, node));
             for child in (self.iter_children)(node) {
-                self.queue.push_back((level + 1, child));
+                self.queue.push_back(LevelNode(level + 1, child));
             }
         }
     }
 
-    pub fn take_nodes(&mut self) -> Vec<LevelNode> {
+    pub fn take_nodes(&mut self) -> Vec<LevelNode<T>> {
         std::mem::take(&mut self.nodes)
     }
 
diff --git a/sbroad-core/src/ir/undo.rs b/sbroad-core/src/ir/undo.rs
index 3062be50c..828f5d67c 100644
--- a/sbroad-core/src/ir/undo.rs
+++ b/sbroad-core/src/ir/undo.rs
@@ -3,12 +3,14 @@
 use serde::{Deserialize, Serialize};
 use std::collections::HashMap;
 
+use super::expression::NodeId;
+
 /// Transformation log keep the history of the plan subtree modifications.
 /// When we modify the plan subtree, we add a new entry to the log, where
 /// the key is a new subtree top node and the value is the previous version.
 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
 pub struct TransformationLog {
-    log: HashMap<usize, usize>,
+    log: HashMap<NodeId, NodeId>,
 }
 
 impl Default for TransformationLog {
@@ -25,7 +27,7 @@ impl TransformationLog {
         }
     }
 
-    pub fn add(&mut self, new_id: usize, old_id: usize) {
+    pub fn add(&mut self, new_id: NodeId, old_id: NodeId) {
         match self.log.get_key_value(&new_id) {
             None => {
                 self.log.insert(new_id, old_id);
@@ -38,12 +40,12 @@ impl TransformationLog {
     }
 
     #[must_use]
-    pub fn get(&self, new_id: &usize) -> Option<&usize> {
+    pub fn get(&self, new_id: &NodeId) -> Option<&NodeId> {
         self.log.get(new_id)
     }
 
     #[must_use]
-    pub fn get_oldest(&self, new_id: &usize) -> Option<&usize> {
+    pub fn get_oldest(&self, new_id: &NodeId) -> Option<&NodeId> {
         match self.log.get_key_value(new_id) {
             None => None,
             Some((id, _)) => {
-- 
GitLab