Draft

The Tuple That Waited

Conversation 3.2 — Iterators and the State Between Results

The tuple that waited

Ada · 01

Keep the cinema instance from 3.1. Here it is in physical arrays, with positions written beside the tuples.

PositionMovies: TitleDirectorActor
0filmABergmanactor1
1filmABergmanactor2
2filmBOtheractor3
PositionPariscope: TheaterTitleSchedule
0ChampofilmA18:00
1ChampofilmA21:00
2RialtofilmB20:00
PositionLocation: TheaterAddressPhone
0Champoaddr1phone1
1Rialtoaddr2phone2

This time ask for the actor, showing time, and address:

appearance(c, s, a) :-
    Movies(t, d, c),
    Pariscope(h, t, s),
    Location(h, a, p).

How many answers?

Alice

Five. Each of actor1 and actor2 has two showing times at addr1. Actor3 has one at addr2.

Changing the head made the showings visible. In theater_address, several of these witnesses gave the same answer.

Ada · 02

Use the plan from 3.1: scan Movies, look up Pariscope by Title, then look up Location by Theater. Call the first join $J_1$ and the second $J_2$.

The consumer wants to inspect one answer, then decide whether to ask for another. How would you give $J_2$ its input?

Alice

The algebra gives $J_1$ a relation as its result. So I would first put that relation in an array, then let $J_2$ read it.

Ada · 03

Suppose that intermediate has a million matches, and the consumer stops after the first answer. What did the array strategy commit us to?

Alice

Producing and storing all million intermediate tuples before $J_2$ could even start. Much of that work might never be used.

Could $J_2$ ask for just one match now, and another only if it needs it?

Ada · 04

To find the address for the first showing, which of the other four matches does $J_2$ need?

Alice

None. Once $J_1$ has (filmA,Bergman,actor1,Champo,18:00), $J_2$ has the Theater value it needs.

We could pass that tuple on. But where does $J_1$ wait while $J_2$ uses it? If I simply restart it on the next request, I will get the same match again.

Ada · 05

Start with an easier computation. A Movies scan has returned $M_0$. What must it remember to answer another request?

Alice

Position 1, the next unread position. It need not copy the whole relation.

Ada · 06

Write every transition for this three-element array. Let Some(u) mean one returned tuple and None mean exhaustion. Return the new position alongside the result.

Alice
Old positionResultNew position
0Some(M₀)1
1Some(M₁)2
2Some(M₂)3
3None3

Position 3 stays at 3. Another request cannot invent another movie.

Ada · 07

Replace Movies by a fixed array $R$ of length $n$, including $n=0$.

Alice

The state is $i\in\{0,\ldots,n\}$, initially 0, and

$$\operatorname{next}_R(i)= \begin{cases} (\operatorname{Some}(R_i),i+1),&i<n,\\ (\operatorname{None},n),&i=n. \end{cases} $$

With an empty array, the first request is already the second case.

Ada · 08

Now open the Title lookup for filmA. Its matching Pariscope positions are $K=[0,1]$. What is its state?

Alice

An offset $q$ in that sequence: 0, then 1, then 2 for exhaustion. At offset $q<2$, it fetches $P_{K[q]}$ and advances $q$.

Ada · 09

For filmB, $K=[2]$. Is its initial state 2?

Alice

No. Its initial offset is 0. The first fetch is $P_{K[0]}=P_2$.

The position inside the match list and the position inside Pariscope are different numbers.

Ada · 10

Does an arbitrary Pariscope iterator let us ask for filmB?

Alice

It only promises the next result of the computation it already represents. We need an additional operation that accepts a Title and starts the right computation.

Ada · 11

$J_1$ has just returned $M_0\bowtie P_0$. Can it now forget $M_0$?

Alice

Yes, the Movies scan has moved to position 1.

Ada · 12

Who will produce $M_0\bowtie P_1$?

Alice

Oh. $J_1$ still owes actor1 the 21:00 showing. It must keep $M_0$ while the Title lookup advances.

Its child scan is already past $M_0$, but its own work with $M_0$ is not finished.

Ada · 13

Give $J_1$ three possible situations: it needs an outer tuple, it is matching a retained outer tuple, or it is done. What happens in each?

Alice

In Need, request the next Movies tuple. If there is one, retain it and initialize its Title lookup. Otherwise become Done.

In Match, request the next tuple of that lookup. Return the combined tuple if there is one. Otherwise return internally to Need.

In Done, return None.

Ada · 14

Suppose an outer movie has no showing. Should that failed lookup make $J_1$ return None?

Alice

No. Other movies may still have showings. One call to $J_1$ must continue through empty lookups until it finds a result or exhausts its outer input.

Ada · 15

Now use $J_1$ as $J_2$'s outer iterator. What changes in that definition?

Alice

The outer child becomes $J_1$. The retained tuple now contains both movie and showing attributes. Its Theater initializes a Location lookup.

$J_2$ can use the same algorithm without knowing how $J_1$ produces tuples.

Ada · 16

Request the first answer, then the second. Record the source positions used and what remains pending.

Alice
RequestWitnessPending work after the return
1$(M_0,P_0,L_0)$$J_1$ retains $M_0$, next Title-match offset 1; $J_2$'s current Location lookup is exhausted
2$(M_0,P_1,L_0)$$J_1$ retains $M_0$, next Title-match offset 2; $J_2$'s new Location lookup is exhausted

The answers are (actor1,18:00,addr1) and (actor1,21:00,addr1). The second request did not advance the Movies scan.

Ada · 17

And the third?

Alice

The exhausted Location lookup sends $J_2$ back to $J_1$. Its exhausted Title lookup makes $J_1$ request $M_1$, then initialize another filmA lookup at offset 0.

The witness is $(M_1,P_0,L_0)$, giving (actor2,18:00,addr1). The same Title does not make the two actors interchangeable.

Ada · 18

What whole intermediate relation did we store?

Alice

None. Each join retained its current outer tuple and inner progress. The indexes and backing arrays already existed.

The relation denoted by $J_1$ is still well defined, even though we never put all of it in one array.

Ada · 19

Now the consumer wants all five matches. It calls $J_1$ five times for tuples and once more to discover exhaustion. Suppose each return crosses an operator boundary with some fixed overhead. How could we cross that boundary less often without collecting the entire result?

Alice

Return a bounded group of results per request. We would keep the ability to resume, but share that overhead among several tuples.

We would also make the caller wait for a group. The useful group size depends on the consumer and the implementation.

Ada · 20

Use a capacity of three. Where does the first group end?

Alice

Its matching source-position pairs are

batch 1: (0,0), (0,1), (1,0)
batch 2: (1,1), (2,2)

After batch 1, $M_1$ still has one showing left. The batch boundary must save that pending match. Two calls return data, and a third discovers exhaustion, under this same explicit end-of-stream protocol.

Ada · 21

Call such a group a batch. Did it reduce the number of movie–showing matches that the query requires?

Alice

No. All five remain. The change amortizes the boundary overhead; it does not remove a relational result or guarantee a lower total running time.

Ada · 22

Does collecting three scalar results already make the computation vectorized?

Alice

It changes the unit passed to the caller. To compute over column vectors, we must also change the work inside the batch.

And a final batch can be short. We cannot throw away its two results.

Ada · 23

Suppose Pariscope has no index. Could a hash join still offer next?

Alice

Yes, after building a temporary table. Its private state would include that table as well as its probe progress.

The interface does not promise that preparation is cheap, or that every join uses an index supplied beforehand.

Ada · 24

Our consumer stops after two answers. The operator still owns its temporary table and open input. If it releases them only when next reaches None, what happens?

Alice

The consumer will never make that call. We need a separate way to release resources when it stops early.

Preparation must happen before the first request, and cleanup must be possible without requesting another result.

Ada · 25

Volcano names those operations open and close. Does close mean “run the query to exhaustion”?

Alice

No. It means release the resources this instance owns. Exhaustion and cleanup are different events.

Ada · 26

One last change. Location gains another tuple for Champo, with a different address. What must $J_2$ do after returning the first address?

Alice

Keep its current $J_1$ tuple and finish that Location lookup before asking $J_1$ for another showing. Otherwise it loses the second address.

Every level has its own unfinished work.

Ada · 27

If we return to the old head (h,a), can projection simply return one pair for every witness?

Alice

That would repeat (Champo,addr1) four times. Our relations are sets, so the result still needs duplicate elimination.

Streaming the witnesses did not change what projection means.

Recap

Resumable evaluation

A pull plan lets a consumer request the next result of an operator. Each operator retains the progress needed to resume. A request may cause many internal reads and may pass through several children before producing one result. An empty inner lookup is not exhaustion of the enclosing join.

For the chosen indexed plan, keyed access initializes an inner iterator, and the join retains its outer tuple until that iterator ends. Composition does not require materializing each logical intermediate. Batch boundaries must preserve the same unfinished work. Access capabilities, join algorithms, processing units, and lifecycle protocols remain distinct choices, all subject to the query's original set semantics.