ndscoder opened a new issue, #27379:
URL: https://github.com/apache/datafusion/issues/27379

   ### Describe the bug
   
   A `FULL` or `LEFT` join planned as `HashJoinExec: mode=CollectLeft` whose 
right (probe) side yields no row emits every unmatched row of its left (build) 
side as a single batch, whatever `datafusion.execution.batch_size` is. With one 
probe row, matching or not, the same rows come in batches of `batch_size`.
   
   This matters under a memory pool. A `SortExec` above the join reserves about 
twice the batch's size (9.2 MB below, for a batch of about 4.6 MB) before it 
holds anything it could spill, so the statement fails with `Resources exhausted 
... for ExternalSorter[0] with 0.0 B already allocated`, where the same rows in 
batches of 8192 sort and spill within the pool.
   
   Settings that did not change it: `datafusion.execution.batch_size = 1000` 
(still one batch of 200000), `enforce_batch_size_in_joins = true`, 
`coalesce_batches = false`, `hash_join_buffering_capacity = 0` and `1048576`.
   
   ### To Reproduce
   
   With datafusion-python 55.0.0:
   
   ```python
   import pyarrow as pa
   from datafusion import RuntimeEnvBuilder, SessionConfig, SessionContext
   
   rows = 200_000
   table = pa.table({"k": pa.array(range(rows), pa.int64()), "v": 
pa.array(range(rows), pa.float64())})
   
   
   def context(pool_bytes=None):
       config = 
SessionConfig().with_target_partitions(1).set("datafusion.execution.sort_spill_reservation_bytes",
 str(2**20))
       runtime = RuntimeEnvBuilder().with_disk_manager_os()
       if pool_bytes:
           runtime = runtime.with_fair_spill_pool(pool_bytes)
       ctx = SessionContext(config, runtime)
       ctx.register_record_batches("a", [table.to_batches(max_chunksize=8192)])
       ctx.register_record_batches("b", [table.to_batches(max_chunksize=8192)])
       return ctx
   
   
   for where in ("k < 0", "k < 1"):  # the probe side gives no row, then one row
       sql = f"SELECT a.k, a.v, b.v AS w FROM a FULL JOIN (SELECT * FROM b 
WHERE {where}) b ON a.k = b.k"
       sizes = [batch.to_pyarrow().num_rows for batch in 
context().sql(sql).execute_stream()]
       print(f"WHERE {where}: {len(sizes)} batches, largest {max(sizes)}")
       try:
           sorted_rows = sum(b.to_pyarrow().num_rows for b in context(8 * 
2**20).sql(sql + " ORDER BY a.v DESC").execute_stream())
           print(f"  ORDER BY under an 8 MB fair spill pool: sorted 
{sorted_rows} rows")
       except Exception as error:
           print(f"  ORDER BY under an 8 MB fair spill pool: 
{str(error).splitlines()[-1]}")
   ```
   
   Output:
   
   ```
   WHERE k < 0: 1 batches, largest 200000
     ORDER BY under an 8 MB fair spill pool: Resources exhausted: Failed to 
allocate additional 9.2 MB for ExternalSorter[0] with 0.0 B already allocated 
for this reservation - 5.3 MB remain available for the total memory pool: 
fair(pool_size: 8.0 MB)
   WHERE k < 1: 25 batches, largest 8192
     ORDER BY under an 8 MB fair spill pool: sorted 200000 rows
   ```
   
   The full error starts with `Not enough memory to continue external sort. 
Consider increasing the memory limit config ...`. The plan:
   
   ```
   SortExec: expr=[v@1 DESC], preserve_partitioning=[false]
     ProjectionExec: expr=[k@0 as k, v@1 as v, v@2 as w]
       HashJoinExec: mode=CollectLeft, join_type=Full, on=[(k@0, k@0)], 
projection=[k@0, v@1, v@3]
         DataSourceExec: partitions=1, partition_sizes=[25]
         FilterExec: k@0 < 0
           DataSourceExec: partitions=1, partition_sizes=[25]
   ```
   
   `LEFT JOIN` gives the same batches (`[200000]` against batches of 8192). 
Reading the two tables from Parquet does too; there, with `collect_statistics = 
true`, the planner makes the side without rows the build side and the rows come 
in batches.
   
   ### Expected behavior
   
   The unmatched build rows emitted in batches of `batch_size`, as they are 
once the probe side has given a row, so the `ORDER BY` above sorts and spills 
within the pool in both cases.
   
   ### Additional context
   
   The probe side giving no row is common in practice: a filter on the joined 
table that matches nothing, such as a key range one table holds no row of. A 
query then fails or succeeds depending on whether one table happens to be empty 
for it, with the same memory pool.
   
   ### Willingness to contribute
   
   I cannot contribute a fix for this bug at this time


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to