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

   ## Describe the bug
   
   A recursive CTE that joins on string keys read from Parquet fails with 
`Failed to allocate
   additional ... for RecursiveQuery` even when the pool is several times 
larger than what the process
   actually uses. In the example below the query fails with a 4 GB pool and 
completes with 16 GB, while
   peaking at 1.2 GB of RSS; the same query over `Utf8` columns runs in a 1 GB 
pool.
   
   The cause is in `RecursiveQueryStream::push_batch` 
(`datafusion/physical-plan/src/recursive_query.rs`).
   It reserves `batch.get_array_memory_size()` for every batch it keeps for the 
next iteration:
   
   ```rust
   if let Err(e) = self.reservation.try_grow(batch.get_array_memory_size()) {
       return Poll::Ready(Some(Err(e)));
   }
   ```
   
   The recursive term here is a hash join, and its output is built with `take`. 
For a
   `StringViewArray`, `take` copies the views but keeps references to all of 
the build side's data
   buffers. So every output batch reports the build side's string data as its 
own, and the reservation
   counts the same buffers again for each buffered batch.
   
   I ran into this with a benchmark of mine: a span-tree walk over traces fails 
at SF 10 with a 64 GB
   pool, while DuckDB runs it in 1 GB.
   
   ## To Reproduce
   
   `datafusion-cli` from `main` (f058e2d), default settings. An org chart of 2M 
employees with
   string ids:
   
   ```sql
   COPY (
     SELECT 'emp-' || md5(CAST(value AS VARCHAR)) AS employee_id,
            CASE WHEN value = 0 THEN NULL ELSE 'emp-' || md5(CAST((value - 1) / 
10 AS VARCHAR)) END AS manager_id
     FROM range(2000000)
   ) TO 'employees.parquet' STORED AS PARQUET;
   
   CREATE EXTERNAL TABLE employees STORED AS PARQUET LOCATION 
'employees.parquet';
   
   WITH RECURSIVE chain AS (
       SELECT employee_id, 0 AS level FROM employees WHERE manager_id IS NULL
       UNION ALL
       SELECT e.employee_id, c.level + 1
       FROM employees e JOIN chain c ON e.manager_id = c.employee_id
   )
   SELECT level, count(*) AS headcount FROM chain GROUP BY level ORDER BY level;
   
   -- the same ids as Utf8; run the same query over employees_utf8
   CREATE VIEW employees_utf8 AS
   SELECT arrow_cast(employee_id, 'Utf8') AS employee_id, 
arrow_cast(manager_id, 'Utf8') AS manager_id
   FROM employees;
   ```
   
   Run with `datafusion-cli -m <pool> -f repro.sql` (Apple M2 Pro, 10 
partitions):
   
   | columns | pool | result | peak RSS |
   |---|---|---|---|
   | Utf8View (default) | 4 GB | `Failed to allocate additional 24.1 MB for 
RecursiveQuery with 579.0 MB already allocated` | 1.1 GB |
   | Utf8View | 16 GB | OK | 1.2 GB |
   | Utf8 (`employees_utf8`) | 4 GB | OK | 0.8 GB |
   | Utf8 | 1 GB | OK | 0.8 GB |
   
   It gets much worse when the planner picks a `CollectLeft` join, since then 
every buffered batch
   references the whole build side. On 4M spans in chains of 8, each buffered 
batch was charged about
   555 MB, and the query failed with every pool I tried up to 16 GB, at under 
800 MB of RSS.
   
   Setting `datafusion.execution.parquet.schema_force_view_types = false` works 
around it.
   
   ## Expected behavior
   
   `RecursiveQueryExec` reserves each buffer shared by its buffered batches 
once per iteration. The
   hash join, asof join and symmetric hash join already do this for their 
buffered batches with `RecordBatchMemoryCounter` 
(`datafusion/common/src/utils/memory.rs`).
   
   ## Additional context
   
   Related issues, none of which covers this:
   
   - #25712: `CollectLeft` joins multiply buffer references within one array. 
Deduplicating within an
     array doesn't stop the same allocation from being counted again in the 
next batch.
   - #13377: deduplicates buffers within one batch, not across batches.
   - #16841: the general discussion of accounting for shared buffers.
   - #25637: `RecursiveQueryExec` can't spill. That's a separate problem.
   
   I have a fix ready (PR to follow): keep a `RecordBatchMemoryCounter` in 
`RecursiveQueryStream`,
   reserve `count_batch_with_array_overhead(&batch)` in `push_batch`, and start 
a new counter for each
   iteration. With it the `CollectLeft` example passes with a 1 GB pool (peak 
RSS 1.16 GB) and the
   org chart with 2 GB. The org chart still fails at 1 GB; the error reports 
409 MB across
   RecursiveQuery's reservations and 59.1 MB each for two `HashJoinInput` 
partitions (only the top
   three consumers are shown).
   


-- 
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