Skip to main content

mz_compute/render/
top_k.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! TopK execution logic.
11//!
12//! Consult [TopKPlan] documentation for details.
13
14use std::cell::RefCell;
15use std::collections::BTreeMap;
16use std::rc::Rc;
17
18use differential_dataflow::AsCollection;
19use differential_dataflow::hashable::Hashable;
20use differential_dataflow::lattice::Lattice;
21use differential_dataflow::operators::arrange::{Arranged, TraceAgent};
22use differential_dataflow::operators::iterate::Variable as SemigroupVariable;
23use differential_dataflow::trace::cursor::{BatchCursor, BatchValOwn};
24use differential_dataflow::trace::{Builder, Cursor, Navigable, Trace};
25use differential_dataflow::{Data, VecCollection};
26use mz_compute_types::dyncfgs::{ENABLE_COMPUTE_TEMPORAL_BUCKETING, TEMPORAL_BUCKETING_SUMMARY};
27use mz_compute_types::plan::ArrangementStrategy;
28use mz_compute_types::plan::scalar::LirScalarExpr;
29use mz_compute_types::plan::top_k::{
30    BasicTopKPlan, MonotonicTop1Plan, MonotonicTopKPlan, TopKPlan,
31};
32use mz_expr::func::CastUint64ToInt64;
33use mz_expr::{BinaryFunc, Columns, Eval, EvalError, UnaryFunc, func, permutation_for_arrangement};
34use mz_ore::cast::CastFrom;
35use mz_ore::soft_assert_or_log;
36use mz_repr::fixed_length::ExtendDatums;
37use mz_repr::{Datum, DatumVec, Diff, ReprScalarType, Row, SharedRow};
38use mz_timely_util::columnation::ColumnationChunker;
39use mz_timely_util::operator::CollectionExt;
40use timely::Container;
41use timely::container::{CapacityContainerBuilder, PushInto};
42use timely::dataflow::channels::pact::Pipeline;
43use timely::dataflow::operators::Operator;
44
45use crate::extensions::arrange::{ArrangementSize, KeyCollection, MzArrange};
46use crate::extensions::reduce::{ClearContainer, MzReduce};
47use crate::render::Pairer;
48use crate::render::context::{ArrangementFlavor, CollectionBundle, Context};
49use crate::render::errors::DataflowErrorSer;
50use crate::render::errors::MaybeValidatingRow;
51use crate::typedefs::{ErrBatcher, ErrBuilder, KeyBatcher, MzTimestamp, RowRowSpine, RowSpine};
52use mz_row_spine::{
53    DatumContainer, DatumSeq, RowBatcher, RowBuilder, RowRowBatcher, RowRowBuilder, RowValBuilder,
54    RowValSpine,
55};
56
57// The implementation requires integer timestamps to be able to delay feedback for monotonic inputs.
58impl<'scope, T: crate::render::RenderTimestamp + crate::render::MaybeBucketByTime>
59    Context<'scope, T>
60{
61    pub(crate) fn render_topk(
62        &self,
63        input: CollectionBundle<'scope, T>,
64        top_k_plan: TopKPlan,
65        temporal_bucketing_strategy: ArrangementStrategy,
66    ) -> CollectionBundle<'scope, T> {
67        let (ok_input, err_input) = input.as_specific_collection(None, &self.config_set);
68
69        // Bucket the per-row input stream when lowering chose `TemporalBucketing`.
70        // `TopK` builds its own arrangement(s) inside the variants below, bypassing
71        // `ensure_collections`, so the strategy is plumbed through `LirRelationNode::TopK`
72        // rather than inferred at the arrangement site. `apply_bucketing_strategy`
73        // is a no-op for `Direct`.
74        //
75        // Note: a `MonotonicTop1Plan`/`MonotonicTopKPlan` with `must_consolidate =
76        // false` together with `TemporalBucketing` here would mean we install a
77        // bucket operator with no downstream consolidator -- pure overhead. That
78        // combination cannot actually occur: `RelaxMustConsolidate` (which is the
79        // only writer of `must_consolidate = false`) runs only on single-time
80        // dataflows (one-shot peeks / `COPY TO`), and in single-time dataflows
81        // `ExprPrepOneShot` constant-folds `mz_now()` to the dataflow `as_of`
82        // before lowering, so no temporal predicates survive into LIR and
83        // `has_future_updates` is `false` everywhere -- meaning no operator (TopK
84        // included) is ever lowered with `TemporalBucketing`. The assertion below
85        // pins down this invariant.
86        if matches!(
87            temporal_bucketing_strategy,
88            ArrangementStrategy::TemporalBucketing
89        ) {
90            let must_consolidate = match &top_k_plan {
91                TopKPlan::MonotonicTop1(p) => p.must_consolidate,
92                TopKPlan::MonotonicTopK(p) => p.must_consolidate,
93                TopKPlan::Basic(_) => true,
94            };
95            soft_assert_or_log!(
96                must_consolidate,
97                "TopK with `TemporalBucketing` should not have `must_consolidate = false`; \
98                 `RelaxMustConsolidate` only runs on single-time dataflows where \
99                 `mz_now()` has been const-folded and no temporal bucketing is set",
100            );
101        }
102        let ok_input = if matches!(
103            temporal_bucketing_strategy,
104            ArrangementStrategy::TemporalBucketing
105        ) && ENABLE_COMPUTE_TEMPORAL_BUCKETING.get(&self.config_set)
106        {
107            let summary: mz_repr::Timestamp = TEMPORAL_BUCKETING_SUMMARY
108                .get(&self.config_set)
109                .try_into()
110                .expect("must fit");
111            T::maybe_apply_temporal_bucketing(ok_input.inner, self.as_of_frontier.clone(), summary)
112        } else {
113            ok_input
114        };
115
116        // We create a new region to compartmentalize the topk logic.
117        let outer_scope = ok_input.scope();
118        let bundle = outer_scope.clone().region_named("TopK", |inner| {
119            let ok_input = ok_input.enter_region(inner);
120            let mut err_collection = err_input.enter_region(inner);
121
122            // Determine if there should be errors due to limit evaluation; update `err_collection`.
123            // TODO(vmarcos): We evaluate the limit expression below for each input update. There
124            // is an opportunity to do so for every group key instead if the error handling is
125            // integrated with: 1. The intra-timestamp thinning step in monotonic top-k, e.g., by
126            // adding an error output there; 2. The validating reduction on basic top-k
127            // (database-issues#7108).
128
129            match top_k_plan.limit().map(|l| (l.as_literal(), l)) {
130                None => {}
131                Some((Some(Ok(literal)), _))
132                    if literal == Datum::Null || literal.unwrap_int64() >= 0 => {}
133                Some((_, expr)) => {
134                    // Produce errors from limit selectors that error or are
135                    // negative, and nothing from limit selectors that do
136                    // not. Note that even if expr.could_error() is false,
137                    // the expression might still return a negative limit and
138                    // thus needs to be checked.
139                    let expr = expr.clone();
140                    let mut datum_vec = mz_repr::DatumVec::new();
141                    let errors = ok_input.clone().flat_map(move |row| {
142                        let temp_storage = mz_repr::RowArena::new();
143                        let datums = datum_vec.borrow_with(&row);
144                        match expr.eval(&datums[..], &temp_storage) {
145                            Ok(l) if l != Datum::Null && l.unwrap_int64() < 0 => {
146                                Some(EvalError::NegLimit.into())
147                            }
148                            Ok(_) => None,
149                            Err(e) => Some(e.into()),
150                        }
151                    });
152                    err_collection = err_collection.concat(errors);
153                }
154            }
155
156            let bundle = match top_k_plan {
157                TopKPlan::MonotonicTop1(MonotonicTop1Plan {
158                    group_key,
159                    order_key,
160                    arity,
161                    must_consolidate,
162                }) => {
163                    let (arrangement, errs) = self.render_top1_monotonic(
164                        ok_input,
165                        group_key.clone(),
166                        order_key,
167                        arity,
168                        must_consolidate,
169                    );
170                    err_collection = err_collection.concat(errs);
171
172                    // Lowering advertises this group-key arrangement (see the
173                    // `MirRelationExpr::TopK` arm in `lowering.rs`), so deliver it alone,
174                    // mirroring `render_reduce_plan`'s `ArrangementFlavor::Local`. A consumer
175                    // that needs the raw collection reconstructs it from the arrangement via
176                    // the advertised permutation, exactly as for an index arrangement.
177                    let errs: KeyCollection<_, _, _> = err_collection.clone().into();
178                    let err_arrangement = errs
179                        .mz_arrange::<ColumnationChunker<_>, ErrBatcher<_, _>, ErrBuilder<_, _>, _>(
180                            "Arrange bundle err",
181                        );
182                    CollectionBundle::from_columns(
183                        group_key.iter().copied(),
184                        ArrangementFlavor::Local(arrangement, err_arrangement),
185                    )
186                }
187                TopKPlan::MonotonicTopK(MonotonicTopKPlan {
188                    order_key,
189                    group_key,
190                    arity,
191                    mut limit,
192                    must_consolidate,
193                }) => {
194                    // Must permute `limit` to reference `group_key` elements as if in order.
195                    if let Some(expr) = limit.as_mut() {
196                        let mut map = BTreeMap::new();
197                        for (index, column) in group_key.iter().enumerate() {
198                            map.insert(*column, index);
199                        }
200                        expr.permute_map(&map);
201                    }
202
203                    // Map the group key along with the row and consolidate if required to do so.
204                    let mut datum_vec = mz_repr::DatumVec::new();
205                    let ok_scope = ok_input.scope();
206                    let collection = ok_input
207                        .map(move |row| {
208                            let group_row = {
209                                let datums = datum_vec.borrow_with(&row);
210                                SharedRow::pack(group_key.iter().map(|i| datums[*i]))
211                            };
212                            (group_row, row)
213                        })
214                        .consolidate_named_if::<KeyBatcher<_, _, _>>(
215                            must_consolidate,
216                            "Consolidated MonotonicTopK input",
217                        );
218
219                    // It should be now possible to ensure that we have a monotonic collection.
220                    let error_logger = self.error_logger();
221                    let (collection, errs) = collection.ensure_monotonic(move |data, diff| {
222                        error_logger.log(
223                            "Non-monotonic input to MonotonicTopK",
224                            &format!("data={data:?}, diff={diff}"),
225                        );
226                        let m = "tried to build monotonic top-k on non-monotonic input".into();
227                        (DataflowErrorSer::from(EvalError::Internal(m)), Diff::ONE)
228                    });
229                    err_collection = err_collection.concat(errs);
230
231                    // For monotonic inputs, we are able to thin the input relation in two stages:
232                    // 1. First, we can do an intra-timestamp thinning which has the advantage of
233                    //    being computed in a streaming fashion, even for the initial snapshot.
234                    // 2. Then, we can do inter-timestamp thinning by feeding back negations for
235                    //    any records that have been invalidated.
236                    let collection = if let Some(limit) = limit.clone() {
237                        render_intra_ts_thinning(collection, order_key.clone(), limit)
238                    } else {
239                        collection
240                    };
241
242                    let pairer = Pairer::new(1);
243                    let collection = collection.map(move |(group_row, row)| {
244                        let hash = row.hashed();
245                        let hash_key = pairer.merge(std::iter::once(Datum::from(hash)), &group_row);
246                        (hash_key, row)
247                    });
248
249                    // For monotonic inputs, we are able to retract inputs that can no longer be produced
250                    // as outputs. Any inputs beyond `offset + limit` will never again be produced as
251                    // outputs, and can be removed. The simplest form of this is when `offset == 0` and
252                    // these removable records are those in the input not produced in the output.
253                    // TODO: consider broadening this optimization to `offset > 0` by first filtering
254                    // down to `offset = 0` and `limit = offset + limit`, followed by a finishing act
255                    // of `offset` and `limit`, discarding only the records not produced in the intermediate
256                    // stage.
257                    let delay = std::time::Duration::from_secs(10);
258                    let (retractions_var, retractions) = SemigroupVariable::new(
259                        ok_scope,
260                        <T as crate::render::RenderTimestamp>::system_delay(
261                            delay.try_into().expect("must fit"),
262                        ),
263                    );
264                    let thinned = collection.clone().concat(retractions.negate());
265
266                    // As an additional optimization, we can skip creating the full topk hierachy
267                    // here since we now have an upper bound on the number records due to the
268                    // intra-ts thinning. The maximum number of records per timestamp is
269                    // (num_workers * limit), which we expect to be a small number and so we render
270                    // a single topk stage.
271                    let (result, errs) =
272                        self.build_topk_stage(thinned, order_key, 1u64, 0, limit, arity, false);
273                    // Consolidate the output of `build_topk_stage` because it's not guaranteed to be.
274                    let result = CollectionExt::consolidate_named::<KeyBatcher<_, _, _>>(
275                        result,
276                        "Monotonic TopK final consolidate",
277                    );
278                    retractions_var.set(collection.concat(result.clone().negate()));
279                    soft_assert_or_log!(
280                        errs.is_none(),
281                        "requested no validation, but received error collection"
282                    );
283
284                    CollectionBundle::from_collections(
285                        result.map(|(_key_hash, row)| row),
286                        err_collection,
287                    )
288                }
289                TopKPlan::Basic(BasicTopKPlan {
290                    group_key,
291                    order_key,
292                    offset,
293                    mut limit,
294                    arity,
295                    buckets,
296                }) => {
297                    // Must permute `limit` to reference `group_key` elements as if in order.
298                    if let Some(expr) = limit.as_mut() {
299                        let mut map = BTreeMap::new();
300                        for (index, column) in group_key.iter().enumerate() {
301                            map.insert(*column, index);
302                        }
303                        expr.permute_map(&map);
304                    }
305
306                    let (oks, errs) = self.build_topk(
307                        ok_input, group_key, order_key, offset, limit, arity, buckets,
308                    );
309                    err_collection = err_collection.concat(errs);
310                    CollectionBundle::from_collections(oks, err_collection)
311                }
312            };
313
314            // Extract the results from the region.
315            bundle.leave_region(outer_scope)
316        });
317
318        bundle
319    }
320
321    /// Constructs a TopK dataflow subgraph.
322    fn build_topk<'s>(
323        &self,
324        collection: VecCollection<'s, T, Row, Diff>,
325        group_key: Vec<usize>,
326        order_key: Vec<mz_expr::ColumnOrder>,
327        offset: usize,
328        limit: Option<LirScalarExpr>,
329        arity: usize,
330        buckets: Vec<u64>,
331    ) -> (
332        VecCollection<'s, T, Row, Diff>,
333        VecCollection<'s, T, DataflowErrorSer, Diff>,
334    ) {
335        let pairer = Pairer::new(1);
336        let mut datum_vec = mz_repr::DatumVec::new();
337        let mut collection = collection.map({
338            move |row| {
339                let group_row = {
340                    let row_hash = row.hashed();
341                    let datums = datum_vec.borrow_with(&row);
342                    let iterator = group_key.iter().map(|i| datums[*i]);
343                    pairer.merge(std::iter::once(Datum::from(row_hash)), iterator)
344                };
345                (group_row, row)
346            }
347        });
348
349        let mut validating = true;
350        let mut err_collection: Option<VecCollection<'s, T, _, _>> = None;
351
352        if let Some(mut limit) = limit.clone() {
353            // We may need a new `limit` that reflects the addition of `offset`.
354            // Ideally we compile it down to a literal if at all possible.
355            if offset > 0 {
356                let new_limit = (|| {
357                    let limit = limit.as_literal_int64()?;
358                    let offset = i64::try_from(offset).ok()?;
359                    limit.checked_add(offset)
360                })();
361
362                if let Some(new_limit) = new_limit {
363                    limit =
364                        LirScalarExpr::literal_ok(Datum::Int64(new_limit), ReprScalarType::Int64);
365                } else {
366                    limit = limit.call_binary(
367                        LirScalarExpr::literal_ok(
368                            Datum::UInt64(u64::cast_from(offset)),
369                            ReprScalarType::UInt64,
370                        )
371                        .call_unary(UnaryFunc::CastUint64ToInt64(CastUint64ToInt64)),
372                        BinaryFunc::AddInt64(func::AddInt64),
373                    );
374                }
375            }
376
377            // These bucket values define the shifts that happen to the 64 bit hash of the
378            // record, and should have the properties that 1. there are not too many of them,
379            // and 2. each has a modest difference to the next.
380            for bucket in buckets.into_iter() {
381                // here we do not apply `offset`, but instead restrict ourself with a limit
382                // that includes the offset. We cannot apply `offset` until we perform the
383                // final, complete reduction.
384                let (oks, errs) = self.build_topk_stage(
385                    collection,
386                    order_key.clone(),
387                    bucket,
388                    0,
389                    Some(limit.clone()),
390                    arity,
391                    validating,
392                );
393                collection = oks;
394                if validating {
395                    err_collection = errs;
396                    validating = false;
397                }
398            }
399        }
400
401        // We do a final step, both to make sure that we complete the reduction, and to correctly
402        // apply `offset` to the final group, as we have not yet been applying it to the partially
403        // formed groups.
404        let (oks, errs) = self.build_topk_stage(
405            collection, order_key, 1u64, offset, limit, arity, validating,
406        );
407        // Consolidate the output of `build_topk_stage` because it's not guaranteed to be.
408        let oks =
409            CollectionExt::consolidate_named::<KeyBatcher<_, _, _>>(oks, "TopK final consolidate");
410        collection = oks;
411        if validating {
412            err_collection = errs;
413        }
414        (
415            collection.map(|(_key_hash, row)| row),
416            err_collection.expect("at least one stage validated its inputs"),
417        )
418    }
419
420    /// To provide a robust incremental orderby-limit experience, we want to avoid grouping *all*
421    /// records (or even large groups) and then applying the ordering and limit. Instead, a more
422    /// robust approach forms groups of bounded size and applies the offset and limit to each,
423    /// and then increases the sizes of the groups.
424    ///
425    /// Builds a "stage", which uses a finer grouping than is required to reduce the volume of
426    /// updates, and to reduce the amount of work on the critical path for updates. The cost is
427    /// a larger number of arrangements when this optimization does nothing beneficial.
428    ///
429    /// The function accepts a collection of the form `(hash_key, row)`, a modulus it applies to the
430    /// `hash_key`'s hash datum, an `offset` for returning results, and a `limit` to restrict the
431    /// output size. `arity` represents the number of columns in the input data, and
432    /// if `validating` is true, we check for negative multiplicities, which indicate
433    /// an error in the input data.
434    ///
435    /// The output of this function is _not consolidated_.
436    ///
437    /// The dataflow fragment has the following shape:
438    /// ```text
439    ///     | input
440    ///     |
441    ///   arrange
442    ///     |\
443    ///     | \
444    ///     |  reduce
445    ///     |  |
446    ///     concat
447    ///     |
448    ///     | output
449    /// ```
450    /// There are additional map/flat_map operators as well as error demuxing operators, but we're
451    /// omitting them here for the sake of simplicity.
452    fn build_topk_stage<'s>(
453        &self,
454        collection: VecCollection<'s, T, (Row, Row), Diff>,
455        order_key: Vec<mz_expr::ColumnOrder>,
456        modulus: u64,
457        offset: usize,
458        limit: Option<LirScalarExpr>,
459        arity: usize,
460        validating: bool,
461    ) -> (
462        VecCollection<'s, T, (Row, Row), Diff>,
463        Option<VecCollection<'s, T, DataflowErrorSer, Diff>>,
464    ) {
465        // Form appropriate input by updating the `hash` column (first datum in `hash_key`) by
466        // applying `modulus`.
467        let input = collection.map(move |(hash_key, row)| {
468            let mut hash_key_iter = hash_key.iter();
469            let hash = hash_key_iter.next().unwrap().unwrap_uint64() % modulus;
470            let hash_key = SharedRow::pack(std::iter::once(hash.into()).chain(hash_key_iter));
471            (hash_key, row)
472        });
473
474        // If validating: demux errors, otherwise we cannot produce errors.
475        let (input, oks, errs) = if validating {
476            // Build topk stage, produce errors for invalid multiplicities.
477            let (input, stage) = build_topk_negated_stage::<
478                T,
479                RowValBuilder<_, _, _>,
480                RowValSpine<Result<Row, Row>, _, _>,
481            >(&input, order_key, offset, limit, arity);
482            let stage = stage.as_collection(|k, v| (k.to_row(), v.clone()));
483
484            // Demux oks and errors.
485            let error_logger = self.error_logger();
486            type CB<C> = CapacityContainerBuilder<C>;
487            let (oks, errs) = stage.map_fallible::<CB<_>, CB<_>, _, _, _>(
488                "Demuxing Errors",
489                move |(hk, result)| match result {
490                    Err(v) => {
491                        let mut hk_iter = hk.iter();
492                        let h = hk_iter.next().unwrap().unwrap_uint64();
493                        let k = SharedRow::pack(hk_iter);
494                        let message = "Negative multiplicities in TopK";
495                        error_logger.log(message, &format!("k={k:?}, h={h}, v={v:?}"));
496                        Err(EvalError::Internal(message.into()).into())
497                    }
498                    Ok(t) => Ok((hk, t)),
499                },
500            );
501            (input, oks, Some(errs))
502        } else {
503            // Build non-validating topk stage.
504            let (input, stage) =
505                build_topk_negated_stage::<T, RowRowBuilder<_, _>, RowRowSpine<_, _>>(
506                    &input, order_key, offset, limit, arity,
507                );
508            // Turn arrangement into collection.
509            let stage = stage.as_collection(|k, v| (k.to_row(), v.to_row()));
510
511            (input, stage, None)
512        };
513        let input = input.as_collection(|k, v| (k.to_row(), v.to_row()));
514        (oks.concat(input), errs)
515    }
516
517    fn render_top1_monotonic<'s>(
518        &self,
519        collection: VecCollection<'s, T, Row, Diff>,
520        group_key: Vec<usize>,
521        order_key: Vec<mz_expr::ColumnOrder>,
522        arity: usize,
523        must_consolidate: bool,
524    ) -> (
525        Arranged<'s, TraceAgent<RowRowSpine<T, Diff>>>,
526        VecCollection<'s, T, DataflowErrorSer, Diff>,
527    ) {
528        // The arrangement we build below is keyed by `group_key` and its value is the winning
529        // row thinned to `thinning`, following the layout `permutation_for_arrangement`
530        // dictates for `Reduce`-style group-key arrangements. A top-1 winner's group-key
531        // columns equal the key by construction, so dropping them from the value is lossless;
532        // consumers reconstruct the full row from key and value via the (unused here)
533        // permutation.
534        let key: Vec<LirScalarExpr> = group_key
535            .iter()
536            .map(|c| LirScalarExpr::column(*c))
537            .collect();
538        let (_permutation, thinning) = permutation_for_arrangement(&key, arity);
539
540        // We can place our rows directly into the diff field, and only keep the relevant one
541        // corresponding to evaluating our aggregate, instead of having to do a hierarchical
542        // reduction. We start by mapping the group key along with the row and consolidating
543        // if required to do so.
544        let collection = collection
545            .map({
546                let mut datum_vec = mz_repr::DatumVec::new();
547                move |row| {
548                    // Scoped to allow borrow of `row` to drop.
549                    let group_key = {
550                        let datums = datum_vec.borrow_with(&row);
551                        SharedRow::pack(group_key.iter().map(|i| datums[*i]))
552                    };
553                    (group_key, row)
554                }
555            })
556            .consolidate_named_if::<KeyBatcher<_, _, _>>(
557                must_consolidate,
558                "Consolidated MonotonicTop1 input",
559            );
560
561        // It should be now possible to ensure that we have a monotonic collection and process it.
562        let error_logger = self.error_logger();
563        let (partial, errs) = collection.ensure_monotonic(move |data, diff| {
564            error_logger.log(
565                "Non-monotonic input to MonotonicTop1",
566                &format!("data={data:?}, diff={diff}"),
567            );
568            let m = "tried to build monotonic top-1 on non-monotonic input".into();
569            (EvalError::Internal(m).into(), Diff::ONE)
570        });
571        let partial: KeyCollection<_, _, _> = partial
572            .explode_one(move |(group_key, row)| {
573                (
574                    group_key,
575                    monoids::Top1Monoid {
576                        row,
577                        order_key: order_key.clone(),
578                    },
579                )
580            })
581            .into();
582        let result = partial
583            .mz_arrange::<
584                ColumnationChunker<_>,
585                RowBatcher<_, _>,
586                RowBuilder<_, _>,
587                RowSpine<_, _>,
588            >(
589                "Arranged MonotonicTop1 partial [val: empty]",
590            )
591            .mz_reduce_abelian::<_, RowRowBuilder<_, _>, RowRowSpine<_, _>, _>(
592                "MonotonicTop1",
593                {
594                    let mut datum_vec = mz_repr::DatumVec::new();
595                    move |_key, input, output| {
596                        let accum: &monoids::Top1Monoid = &input[0].1;
597                        let datums = datum_vec.borrow_with(&accum.row);
598                        let value = SharedRow::pack(thinning.iter().map(|i| datums[*i]));
599                        output.push((value, Diff::ONE));
600                    }
601                },
602            );
603        (result, errs)
604    }
605}
606
607/// Build a stage of a topk reduction. Maintains the _retractions_ of the output instead of emitted
608/// rows. This has the benefit that we have to maintain state proportionally to size of the output
609/// instead of the size of the input.
610///
611/// Returns two arrangements:
612/// * The arranged input data without modifications, and
613/// * the maintained negated output data.
614fn build_topk_negated_stage<'s, T, Bu, Tr>(
615    input: &VecCollection<'s, T, (Row, Row), Diff>,
616    order_key: Vec<mz_expr::ColumnOrder>,
617    offset: usize,
618    limit: Option<LirScalarExpr>,
619    arity: usize,
620) -> (
621    Arranged<'s, TraceAgent<RowRowSpine<T, Diff>>>,
622    Arranged<'s, TraceAgent<Tr>>,
623)
624where
625    T: MzTimestamp,
626    Bu: Builder<
627            Time = T,
628            Input: Container + ClearContainer + PushInto<((Row, BatchValOwn<Tr>), T, Diff)>,
629            Output = Tr::Batch,
630        > + 'static,
631    Tr: Trace<Batch: Navigable, Time = T> + 'static,
632    for<'a> BatchCursor<Tr>: Cursor<
633            Key<'a> = DatumSeq<'a>,
634            KeyContainer = DatumContainer,
635            ValOwn: Data + MaybeValidatingRow<Row, Row>,
636            Time = T,
637            Diff = Diff,
638        >,
639    Arranged<'s, TraceAgent<Tr>>: ArrangementSize,
640{
641    let mut datum_vec = mz_repr::DatumVec::new();
642
643    // We only want to arrange parts of the input that are not part of the actual output
644    // such that `input.concat(&negated_output)` yields the correct TopK
645    // NOTE(vmarcos): The arranged input operator name below is used in the tuning advice
646    // built-in view mz_introspection.mz_expected_group_size_advice.
647    let arranged = input
648        .clone()
649        .mz_arrange::<
650            ColumnationChunker<_>,
651            RowRowBatcher<_, _>,
652            RowRowBuilder<_, _>,
653            RowRowSpine<_, _>,
654        >(
655            "Arranged TopK input",
656        );
657
658    // Eagerly evaluate literal limits.
659    let limit = limit.map(|l| match l.as_literal() {
660        Some(Ok(Datum::Null)) => Ok(Diff::MAX),
661        Some(Ok(d)) => Ok(Diff::from(d.unwrap_int64())),
662        _ => Err(l),
663    });
664
665    let reduced = arranged
666        .clone()
667        .mz_reduce_abelian::<_, Bu, Tr, _>("Reduced TopK input", {
668            move |hash_key, source, target: &mut Vec<(BatchValOwn<Tr>, Diff)>| {
669                // Unpack the limit, either into an integer literal or an expression to evaluate.
670                let limit = match &limit {
671                    Some(Ok(lit)) => Some(*lit),
672                    Some(Err(expr)) => {
673                        // Unpack `key` after skipping the hash and determine the limit.
674                        // If the limit errors, use a zero limit; errors are surfaced elsewhere.
675                        let temp_storage = mz_repr::RowArena::new();
676                        let mut key_datums = datum_vec.borrow();
677                        hash_key.extend_datums(&temp_storage, &mut key_datums, None);
678                        // `key_datums[0]` is the hash; the key columns follow it.
679                        let datum_limit = expr
680                            .eval(&key_datums[1..], &temp_storage)
681                            .unwrap_or(Datum::Int64(0));
682                        Some(match datum_limit {
683                            Datum::Null => Diff::MAX,
684                            d => Diff::from(d.unwrap_int64()),
685                        })
686                    }
687                    None => None,
688                };
689
690                if let Some(err) = BatchValOwn::<Tr>::into_error() {
691                    for (datums, diff) in source.iter() {
692                        if diff.is_positive() {
693                            continue;
694                        }
695                        target.push((err((*datums).to_row()), Diff::ONE));
696                        return;
697                    }
698                }
699
700                // Determine if we must actually shrink the result set.
701                let must_shrink = offset > 0
702                    || limit
703                        .map(|l| source.iter().map(|(_, d)| *d).sum::<Diff>() > l)
704                        .unwrap_or(false);
705                if !must_shrink {
706                    return;
707                }
708
709                // First go ahead and emit all records. Note that we ensure target
710                // has the capacity to hold at least these records, and avoid any
711                // dependencies on the user-provided (potentially unbounded) limit.
712                target.reserve(source.len());
713                for (datums, diff) in source.iter() {
714                    target.push((BatchValOwn::<Tr>::ok((*datums).to_row()), -diff));
715                }
716                // local copies that may count down to zero.
717                let mut offset = offset;
718                let mut limit = limit;
719
720                // The order in which we should produce rows.
721                let mut indexes = (0..source.len()).collect::<Vec<_>>();
722                // We decode the datums once, into a common buffer for efficiency.
723                // Each row should contain `arity` columns; we should check that.
724                let temp_storage = mz_repr::RowArena::new();
725                let mut buffer = datum_vec.borrow();
726                for (index, (datums, _)) in source.iter().enumerate() {
727                    datums.extend_datums(&temp_storage, &mut buffer, None);
728                    assert_eq!(buffer.len(), arity * (index + 1));
729                }
730                let width = buffer.len() / source.len();
731
732                //todo: use arrangements or otherwise make the sort more performant?
733                indexes.sort_by(|left, right| {
734                    let left = &buffer[left * width..][..width];
735                    let right = &buffer[right * width..][..width];
736                    // Note: source was originally ordered by the u8 array representation
737                    // of rows, but left.cmp(right) uses Datum::cmp.
738                    mz_expr::compare_columns(&order_key, left, right, || left.cmp(right))
739                });
740
741                // We now need to lay out the data in order of `buffer`, but respecting
742                // the `offset` and `limit` constraints.
743                for index in indexes.into_iter() {
744                    let (datums, mut diff) = source[index];
745                    if !diff.is_positive() {
746                        continue;
747                    }
748                    // If we are still skipping early records ...
749                    if offset > 0 {
750                        let to_skip =
751                            std::cmp::min(offset, usize::try_from(diff.into_inner()).unwrap());
752                        offset -= to_skip;
753                        diff -= Diff::try_from(to_skip).unwrap();
754                    }
755                    // We should produce at most `limit` records.
756                    if let Some(limit) = &mut limit {
757                        diff = std::cmp::min(diff, Diff::from(*limit));
758                        *limit -= diff;
759                    }
760                    // Output the indicated number of rows.
761                    if diff.is_positive() {
762                        // Emit retractions for the elements actually part of
763                        // the set of TopK elements.
764                        target.push((BatchValOwn::<Tr>::ok(datums.to_row()), diff));
765                    }
766                }
767            }
768        });
769    (arranged, reduced)
770}
771
772fn render_intra_ts_thinning<'s, T>(
773    collection: VecCollection<'s, T, (Row, Row), Diff>,
774    order_key: Vec<mz_expr::ColumnOrder>,
775    limit: LirScalarExpr,
776) -> VecCollection<'s, T, (Row, Row), Diff>
777where
778    T: timely::progress::Timestamp + Lattice,
779{
780    let mut datum_vec = mz_repr::DatumVec::new();
781
782    let mut aggregates = BTreeMap::new();
783    let shared = Rc::new(RefCell::new(monoids::Top1MonoidShared {
784        order_key,
785        left: DatumVec::new(),
786        right: DatumVec::new(),
787    }));
788    collection
789        .inner
790        .unary_notify(
791            Pipeline,
792            "TopKIntraTimeThinning",
793            [],
794            move |input, output, notificator| {
795                input.for_each_time(|time, data| {
796                    let agg_time = aggregates
797                        .entry(time.time().clone())
798                        .or_insert_with(BTreeMap::new);
799                    for ((grp_row, row), record_time, diff) in data.flat_map(|data| data.drain(..))
800                    {
801                        let monoid = monoids::Top1MonoidLocal {
802                            row,
803                            shared: Rc::clone(&shared),
804                        };
805
806                        // Evalute the limit, first as a constant and then against the key if needed.
807                        let limit = if let Some(l) = limit.as_literal_int64() {
808                            l
809                        } else {
810                            let temp_storage = mz_repr::RowArena::new();
811                            let key_datums = datum_vec.borrow_with(&grp_row);
812                            // Unpack `key` and determine the limit.
813                            // If the limit errors, use a zero limit; errors are surfaced elsewhere.
814                            let datum_limit = limit
815                                .eval(&key_datums, &temp_storage)
816                                .unwrap_or(mz_repr::Datum::Int64(0));
817                            if datum_limit == Datum::Null {
818                                i64::MAX
819                            } else {
820                                datum_limit.unwrap_int64()
821                            }
822                        };
823
824                        let topk = agg_time
825                            .entry((grp_row, record_time))
826                            .or_insert_with(move || topk_agg::TopKBatch::new(limit));
827                        topk.update(monoid, diff.into_inner());
828                    }
829                    notificator.notify_at(time.retain(0));
830                });
831
832                notificator.for_each(|time, _, _| {
833                    if let Some(aggs) = aggregates.remove(time.time()) {
834                        let mut session = output.session(&time);
835                        for ((grp_row, record_time), topk) in aggs {
836                            session.give_iterator(topk.into_iter().map(|(monoid, diff)| {
837                                (
838                                    (grp_row.clone(), monoid.into_row()),
839                                    record_time.clone(),
840                                    diff.into(),
841                                )
842                            }))
843                        }
844                    }
845                });
846            },
847        )
848        .as_collection()
849}
850
851/// Types for in-place intra-ts aggregation of monotonic streams.
852pub mod topk_agg {
853    use differential_dataflow::consolidation;
854    use smallvec::SmallVec;
855
856    // TODO: This struct looks a lot like ChangeBatch and indeed its code is a modified version of
857    // that. It would be nice to find a way to reuse some or all of the code from there.
858    //
859    // Additionally, because we're calling into DD's consolidate method we are forced to work with
860    // the `Ord` trait which for the usage we do above means that we need to clone the `order_key`
861    // for each record. It would be nice to also remove the need for cloning that piece of data
862    pub struct TopKBatch<T> {
863        updates: SmallVec<[(T, i64); 16]>,
864        clean: usize,
865        limit: i64,
866    }
867
868    impl<T: Ord> TopKBatch<T> {
869        pub fn new(limit: i64) -> Self {
870            Self {
871                updates: SmallVec::new(),
872                clean: 0,
873                limit,
874            }
875        }
876
877        /// Adds a new update, for `item` with `value`.
878        ///
879        /// This could be optimized to perform compaction when the number of "dirty" elements exceeds
880        /// half the length of the list, which would keep the total footprint within reasonable bounds
881        /// even under an arbitrary number of updates. This has a cost, and it isn't clear whether it
882        /// is worth paying without some experimentation.
883        #[inline]
884        pub fn update(&mut self, item: T, value: i64) {
885            self.updates.push((item, value));
886            self.maintain_bounds();
887        }
888
889        /// Compact the internal representation.
890        ///
891        /// This method sort `self.updates` and consolidates elements with equal item, discarding
892        /// any whose accumulation is zero. It is optimized to only do this if the number of dirty
893        /// elements is non-zero.
894        #[inline]
895        pub fn compact(&mut self) {
896            if self.clean < self.updates.len() && self.updates.len() > 1 {
897                let len = consolidation::consolidate_slice(&mut self.updates);
898                self.updates.truncate(len);
899
900                // We can now retain only the first K records and throw away everything else
901                let mut limit = self.limit;
902                self.updates.retain(|x| {
903                    if limit > 0 {
904                        limit -= x.1;
905                        true
906                    } else {
907                        false
908                    }
909                });
910                // By the end of the loop above `limit` will either be:
911                // (a) Positive, in which case all updates were retained;
912                // (b) Zero, in which case we discarded all updates after limit became zero;
913                // (c) Negative, in which case the last record we retained had more copies
914                // than necessary. In this latter case, we need to do one final adjustment
915                // of the diff field of the last record so that the total sum of the diffs
916                // in the batch is K.
917                if limit < 0 {
918                    if let Some(item) = self.updates.last_mut() {
919                        // We are subtracting the limit *negated*, therefore we are subtracting a value
920                        // that is *greater* than or equal to zero, which represents the excess.
921                        item.1 -= -limit;
922                    }
923                }
924            }
925            self.clean = self.updates.len();
926        }
927
928        /// Maintain the bounds of pending (non-compacted) updates versus clean (compacted) data.
929        /// This function tries to minimize work by only compacting if enough work has accumulated.
930        fn maintain_bounds(&mut self) {
931            // if we have more than 32 elements and at least half of them are not clean, compact
932            if self.updates.len() > 32 && self.updates.len() >> 1 >= self.clean {
933                self.compact()
934            }
935        }
936    }
937
938    impl<T: Ord> IntoIterator for TopKBatch<T> {
939        type Item = (T, i64);
940        type IntoIter = smallvec::IntoIter<[(T, i64); 16]>;
941
942        fn into_iter(mut self) -> Self::IntoIter {
943            self.compact();
944            self.updates.into_iter()
945        }
946    }
947}
948
949/// Monoids for in-place compaction of monotonic streams.
950pub mod monoids {
951    use std::cell::RefCell;
952    use std::cmp::Ordering;
953    use std::hash::{Hash, Hasher};
954    use std::rc::Rc;
955
956    use columnation::{Columnation, Region};
957    use differential_dataflow::difference::{IsZero, Multiply, Semigroup};
958    use mz_expr::ColumnOrder;
959    use mz_repr::{DatumVec, Diff, Row};
960    use serde::{Deserialize, Serialize};
961
962    /// A monoid containing a row and an ordering.
963    #[derive(Eq, PartialEq, Debug, Serialize, Deserialize, Hash, Default)]
964    pub struct Top1Monoid {
965        pub row: Row,
966        pub order_key: Vec<ColumnOrder>,
967    }
968
969    impl Clone for Top1Monoid {
970        #[inline]
971        fn clone(&self) -> Self {
972            Self {
973                row: self.row.clone(),
974                order_key: self.order_key.clone(),
975            }
976        }
977
978        #[inline]
979        fn clone_from(&mut self, source: &Self) {
980            self.row.clone_from(&source.row);
981            self.order_key.clone_from(&source.order_key);
982        }
983    }
984
985    impl Multiply<Diff> for Top1Monoid {
986        type Output = Self;
987
988        fn multiply(self, factor: &Diff) -> Self {
989            // Multiplication in Top1Monoid is idempotent, and its
990            // users must ascertain its monotonicity beforehand
991            // (typically with ensure_monotonic) since it has no zero
992            // value for us to use here.
993            assert!(factor.is_positive());
994            self
995        }
996    }
997
998    impl Ord for Top1Monoid {
999        fn cmp(&self, other: &Self) -> Ordering {
1000            debug_assert_eq!(self.order_key, other.order_key);
1001
1002            // It might be nice to cache this row decoding like the non-monotonic codepath, but we'd
1003            // have to store the decoded Datums in the same struct as the Row, which gets tricky.
1004            let left: Vec<_> = self.row.unpack();
1005            let right: Vec<_> = other.row.unpack();
1006            mz_expr::compare_columns(&self.order_key, &left, &right, || left.cmp(&right))
1007        }
1008    }
1009    impl PartialOrd for Top1Monoid {
1010        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1011            Some(self.cmp(other))
1012        }
1013    }
1014
1015    impl Semigroup for Top1Monoid {
1016        fn plus_equals(&mut self, rhs: &Self) {
1017            let cmp = (*self).cmp(rhs);
1018            // NB: Reminder that TopK returns the _minimum_ K items.
1019            if cmp == Ordering::Greater {
1020                self.clone_from(rhs);
1021            }
1022        }
1023    }
1024
1025    impl IsZero for Top1Monoid {
1026        fn is_zero(&self) -> bool {
1027            false
1028        }
1029    }
1030
1031    impl Columnation for Top1Monoid {
1032        type InnerRegion = Top1MonoidRegion;
1033    }
1034
1035    #[derive(Default)]
1036    pub struct Top1MonoidRegion {
1037        row_region: <Row as Columnation>::InnerRegion,
1038        order_key_region: <Vec<ColumnOrder> as Columnation>::InnerRegion,
1039    }
1040
1041    impl Region for Top1MonoidRegion {
1042        type Item = Top1Monoid;
1043
1044        unsafe fn copy(&mut self, item: &Self::Item) -> Self::Item {
1045            let row = unsafe { self.row_region.copy(&item.row) };
1046            let order_key = unsafe { self.order_key_region.copy(&item.order_key) };
1047            Self::Item { row, order_key }
1048        }
1049
1050        fn clear(&mut self) {
1051            self.row_region.clear();
1052            self.order_key_region.clear();
1053        }
1054
1055        fn reserve_items<'a, I>(&mut self, items1: I)
1056        where
1057            Self: 'a,
1058            I: Iterator<Item = &'a Self::Item> + Clone,
1059        {
1060            let items2 = items1.clone();
1061            self.row_region
1062                .reserve_items(items1.into_iter().map(|s| &s.row));
1063            self.order_key_region
1064                .reserve_items(items2.into_iter().map(|s| &s.order_key));
1065        }
1066
1067        fn reserve_regions<'a, I>(&mut self, regions1: I)
1068        where
1069            Self: 'a,
1070            I: Iterator<Item = &'a Self> + Clone,
1071        {
1072            let regions2 = regions1.clone();
1073            self.row_region
1074                .reserve_regions(regions1.into_iter().map(|s| &s.row_region));
1075            self.order_key_region
1076                .reserve_regions(regions2.into_iter().map(|s| &s.order_key_region));
1077        }
1078
1079        fn heap_size(&self, mut callback: impl FnMut(usize, usize)) {
1080            self.row_region.heap_size(&mut callback);
1081            self.order_key_region.heap_size(callback);
1082        }
1083    }
1084
1085    /// A shared portion of a thread-local top-1 monoid implementation.
1086    #[derive(Debug)]
1087    pub struct Top1MonoidShared {
1088        pub order_key: Vec<ColumnOrder>,
1089        pub left: DatumVec,
1090        pub right: DatumVec,
1091    }
1092
1093    /// A monoid containing a row and a shared pointer to a shared structure.
1094    /// Only suitable for thread-local aggregations.
1095    #[derive(Debug, Clone)]
1096    pub struct Top1MonoidLocal {
1097        pub row: Row,
1098        pub shared: Rc<RefCell<Top1MonoidShared>>,
1099    }
1100
1101    impl Top1MonoidLocal {
1102        pub fn into_row(self) -> Row {
1103            self.row
1104        }
1105    }
1106
1107    impl PartialEq for Top1MonoidLocal {
1108        fn eq(&self, other: &Self) -> bool {
1109            self.row.eq(&other.row)
1110        }
1111    }
1112
1113    impl Eq for Top1MonoidLocal {}
1114
1115    impl Hash for Top1MonoidLocal {
1116        fn hash<H: Hasher>(&self, state: &mut H) {
1117            self.row.hash(state);
1118        }
1119    }
1120
1121    impl Ord for Top1MonoidLocal {
1122        fn cmp(&self, other: &Self) -> Ordering {
1123            debug_assert!(Rc::ptr_eq(&self.shared, &other.shared));
1124            let Top1MonoidShared {
1125                left,
1126                right,
1127                order_key,
1128            } = &mut *self.shared.borrow_mut();
1129
1130            let left = left.borrow_with(&self.row);
1131            let right = right.borrow_with(&other.row);
1132            mz_expr::compare_columns(order_key, &left, &right, || left.cmp(&right))
1133        }
1134    }
1135
1136    impl PartialOrd for Top1MonoidLocal {
1137        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1138            Some(self.cmp(other))
1139        }
1140    }
1141
1142    impl Semigroup for Top1MonoidLocal {
1143        fn plus_equals(&mut self, rhs: &Self) {
1144            let cmp = (*self).cmp(rhs);
1145            // NB: Reminder that TopK returns the _minimum_ K items.
1146            if cmp == Ordering::Greater {
1147                self.clone_from(rhs);
1148            }
1149        }
1150    }
1151
1152    impl IsZero for Top1MonoidLocal {
1153        fn is_zero(&self) -> bool {
1154            false
1155        }
1156    }
1157}