# Why rows, measured costs and refusals `dau-scheduler` is small on purpose. It makes one decision, keeps one promise, and refuses what it cannot keep. This page explains the three choices that shape it. ## Rows, not stages A query can be split two ways: by stage, where one engine runs the scan and filter and the other the aggregation, or by rows, where both run the whole query over their own slice. Splitting by stage ships every intermediate across the link between the engines, and for the workloads this library serves the link is the bottleneck. So the split is always over rows: each engine runs the same query, and only partial results cross. That choice is also what makes the executor engine-agnostic. An arm is a callable handed the query and the rows it owns; nothing here knows what is on the other end, which is how the same executor serves two processors, a processor and an accelerator, or a local worker and a remote one. ## Cost is measured The collaborative share has a closed form, but its inputs are each engine's time for the whole query, and a fabricated number under a real decision is worse than no decision: the share changes which engine runs which rows, so an error in it reinforces itself. `plan_split` therefore refuses to guess. The host figure is measured from the query's first run, which has nothing to split onto anyway, keyed by plan and host, and re-measured when the row count or the engine has moved too far from what was measured. A speedup without the machine it was measured on is not a figure, it is a story. The device side of the balance includes a fixed cost per job. One measurement put it at a few milliseconds beside a 12.3 ms host time, which is enough to rule a split out entirely for small queries. When that happens the answer is host-only and says why. ## Merging is default-deny Not every result can be reassembled from two partial answers. Sums and counts add; a mean needs its sum and count carried separately; a top-k merges two k-lists; a grouped aggregation needs the boundary group folded; an exact distinct count cannot be combined without the elements themselves. The library writes that taxonomy down, and a `MergeRule` declares which kinds its shape can combine. Anything not declared is refused before either engine starts, by output name and with the fix in the message. A silently mis-merged answer is the failure this library exists to prevent, so the default is to refuse. ## What overlap measures `SplitExecution.overlap_seconds` is the intersection of the two arms' wall-clock intervals. It establishes that the arms were in flight together, which is the property a split needs and the one a serialized executor loses. It does not claim that two Python callables made parallel progress inside one interpreter; a real second engine runs outside it, and the figure is honest about what it can see.