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