Repository: modin-project/modin

Issue:
`DataFrame.join` benchmarks are not representative
### The actual problem

Current ASV benchmarks for `DataFrame.join` are not representative since they're testing a trivial case only: joining one table on itself using the same columns, SQL query for this would look like: `SELECT * FROM table1 INNER JOIN table1 ON table1.index = table1.index`, OmniSci backend [replaces cases like this](https://github.com/modin-project/modin/blob/643596d5f9e519358fe785ccd081ba05edb624ee/modin/experimental/core/execution/native/implementations/omnisci_on_native/dataframe/dataframe.py#L1148-L1170) with a simple projection that even doesn't involve OmniSci, so no JOIN is actually happening.

The reason for this behaviour is caching mechanism in data-generation in benchmarks: it always [returns the same <i>object</i>](https://github.com/modin-project/modin/blob/643596d5f9e519358fe785ccd081ba05edb624ee/asv_bench/benchmarks/utils/common.py#L372-L373) (not different objects with the same data) for the same arguments. This makes `self.df1` and `self.df2` pointing to the same DataFrame object (and so to the same table in OmniSci):
https://github.com/modin-project/modin/blob/643596d5f9e519358fe785ccd081ba05edb624ee/asv_bench/benchmarks/omnisci/benchmarks.py#L41-L43
```
(Pdb) self.df1._query_compiler._modin_frame._partitions[0][0].frame_id
'frame_b498605088d54bc6b653b021785bff72'
(Pdb) self.df2._query_compiler._modin_frame._partitions[0][0].frame_id
'frame_b498605088d54bc6b653b021785bff72'
(Pdb) self.df1 is self.df2
True
```

In advance of fixing this bug, we should add cases where the indices of joining frames (by default pandas joins frames by their indices) are not equal.

### Timings of different cases
Here are the timings of the fixed the-same-tables issue and the different-indices case of `DataFrame.join` on OmniSci backend in comparison with the benchmarks in master: 
```
================ ======= ==================================== ==========
    shape         how                test_case                   
---------------- ------- ------------------------------------ ----------
[15000000, 10]    left       joining_on_the_same_table        11.0±1ms
[15000000, 10]    left     different_tables_equal_indices     4.91±0s
[15000000, 10]    left   different_tables_different_indices   5.77±0s
[15000000, 10]   inner       joining_on_the_same_table        6.30±3ms
[15000000, 10]   inner     different_tables_equal_indices     4.93±0s
[15000000, 10]   inner   different_tables_different_indices   6.03±0s
================ ======= ==================================== ==========
```
@Garra1980 


Discussion:

Resolving patch:
diff --git a/asv_bench/benchmarks/omnisci/benchmarks.py b/asv_bench/benchmarks/omnisci/benchmarks.py
--- a/asv_bench/benchmarks/omnisci/benchmarks.py
+++ b/asv_bench/benchmarks/omnisci/benchmarks.py
@@ -28,21 +28,63 @@
     trigger_import,
     get_benchmark_shapes,
 )
+from ..utils.common import random_state
+import numpy as np
+import pandas
 
 
 class TimeJoin:
-    param_names = ["shape", "how"]
+    param_names = ["shape", "how", "is_equal_keys"]
     params = [
         get_benchmark_shapes("omnisci.TimeJoin"),
         ["left", "inner"],
+        [True, False],
     ]
 
-    def setup(self, shape, how):
-        self.df1 = generate_dataframe(ASV_USE_IMPL, "int", *shape, RAND_LOW, RAND_HIGH)
-        self.df2 = generate_dataframe(ASV_USE_IMPL, "int", *shape, RAND_LOW, RAND_HIGH)
+    def setup(self, shape, how, is_equal_keys):
+        self.df1, self.df2 = (
+            generate_dataframe(
+                ASV_USE_IMPL,
+                "int",
+                *frame_shape,
+                RAND_LOW,
+                RAND_HIGH,
+                cache_prefix=f"{i}-th_frame_to_join",
+            )
+            for i, frame_shape in enumerate((shape, shape))
+        )
+
+        if is_equal_keys:
+            # When the frames have default indices to join on: RangeIndex(frame_length),
+            # OmniSci backend performs join on the internal meta-column called 'rowid'.
+            # There is a bug in the engine that makes such joins fail. To avoid joining
+            # on the meta-column we explicitly specify a non-default index to join on.
+            # https://github.com/modin-project/modin/issues/3740
+            # Generating a new object for every index to avoid shared index objects:
+            self.df1.index = pandas.RangeIndex(1, len(self.df1) + 1)
+            self.df2.index = pandas.RangeIndex(1, len(self.df2) + 1)
+        else:
+            # Intersection rate indicates how many common join-keys `self.df1`
+            # and `self.df2` have in terms of percentage.
+            indices_intersection_rate = 0.5
+
+            frame_length = len(self.df1)
+            intersect_size = int(frame_length * indices_intersection_rate)
+
+            intersect_part = random_state.choice(
+                self.df1.index, size=intersect_size, replace=False
+            )
+            non_intersect_part = np.arange(
+                start=frame_length, stop=frame_length + (frame_length - intersect_size)
+            )
+            new_index = np.concatenate([intersect_part, non_intersect_part])
+
+            random_state.shuffle(new_index)
+            self.df1.index = new_index
+
         trigger_import(self.df1, self.df2)
 
-    def time_join(self, shape, how):
+    def time_join(self, shape, how, is_equal_keys):
         # join dataframes on index to get the predictable shape
         execute(self.df1.join(self.df2, how=how, lsuffix="left_"))
 
@@ -57,7 +99,7 @@ class TimeMerge:
     def setup(self, shapes, how):
         gen_unique_key = how == "inner"
         self.dfs = []
-        for shape in shapes:
+        for i, shape in enumerate(shapes):
             self.dfs.append(
                 generate_dataframe(
                     ASV_USE_IMPL,
@@ -66,6 +108,7 @@ def setup(self, shapes, how):
                     RAND_LOW,
                     RAND_HIGH,
                     gen_unique_key=gen_unique_key,
+                    cache_prefix=f"{i}-th_frame_to_merge",
                 )
             )
         trigger_import(*self.dfs)
@@ -86,11 +129,16 @@ class TimeAppend:
     params = [get_benchmark_shapes("omnisci.TimeAppend")]
 
     def setup(self, shapes):
-        self.df1 = generate_dataframe(
-            ASV_USE_IMPL, "int", *shapes[0], RAND_LOW, RAND_HIGH
-        )
-        self.df2 = generate_dataframe(
-            ASV_USE_IMPL, "int", *shapes[1], RAND_LOW, RAND_HIGH
+        self.df1, self.df2 = (
+            generate_dataframe(
+                ASV_USE_IMPL,
+                "int",
+                *shape,
+                RAND_LOW,
+                RAND_HIGH,
+                cache_prefix=f"{i}-th_frame_to_append",
+            )
+            for i, shape in enumerate(shapes)
         )
         trigger_import(self.df1, self.df2)
 
diff --git a/asv_bench/benchmarks/utils/common.py b/asv_bench/benchmarks/utils/common.py
--- a/asv_bench/benchmarks/utils/common.py
+++ b/asv_bench/benchmarks/utils/common.py
@@ -309,6 +309,7 @@ def generate_dataframe(
     groupby_ncols: Optional[int] = None,
     count_groups: Optional[int] = None,
     gen_unique_key: bool = False,
+    cache_prefix: str = None,
 ) -> Union[pd.DataFrame, pandas.DataFrame]:
     """
     Generate DataFrame with caching.
@@ -341,6 +342,8 @@ def generate_dataframe(
         Count of groups in groupby columns.
     gen_unique_key : bool, default: False
         Generate `col1` column where all elements are unique.
+    cache_prefix : str, optional
+        Prefix to add to the cache key of the requested frame.
 
     Returns
     -------
@@ -369,6 +372,9 @@ def generate_dataframe(
         gen_unique_key,
     )
 
+    if cache_prefix is not None:
+        cache_key = (cache_prefix, *cache_key)
+
     if cache_key in dataframes_cache:
         return dataframes_cache[cache_key]
 


Test patch: