Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: [C++][Parquet] Add RowRanges API #45234

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

Conversation

wgtmac
Copy link
Member

@wgtmac wgtmac commented Jan 13, 2025

Rationale for this change

This is part of the effort to support efficient filtering to the Parquet reader. The overall design has been discussed at https://docs.google.com/document/d/1SeVcYudu6uD9rb9zRAnlLGgdauutaNZlAaS0gVzjkgM

What changes are included in this PR?

Add a new RowRanges API and support basic operations on it.

Are these changes tested?

TODO

Are there any user-facing changes?

No.

Copy link

Thanks for opening a pull request!

If this is not a minor PR. Could you open an issue for this pull request on GitHub? https://github.com/apache/arrow/issues/new/choose

Opening GitHub issues ahead of time contributes to the Openness of the Apache Arrow project.

Then could you also rename the pull request title in the following format?

GH-${GITHUB_ISSUE_ID}: [${COMPONENT}] ${SUMMARY}

or

MINOR: [${COMPONENT}] ${SUMMARY}

See also:

@wgtmac wgtmac marked this pull request as ready for review January 17, 2025 15:05
@wgtmac wgtmac marked this pull request as draft January 17, 2025 15:06
@wgtmac
Copy link
Member Author

wgtmac commented Jan 17, 2025

@emkornfield @mapleFU @pitrou Could you help check the API (which has been discussed in the Google doc from the PR description)? I will add the BitmapRange support as well as test cases if the interfaces are looking good. Thanks!

@wgtmac wgtmac marked this pull request as ready for review January 21, 2025 06:24
namespace parquet {

/// RowRanges is a collection of non-overlapping and ascendingly ordered row ranges.
class PARQUET_EXPORT RowRanges {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we mark internal or experimental first? I think this might change fastly

class Iterator {
public:
virtual ~Iterator() = default;
virtual std::variant<IntervalRange, BitmapRange, End> NextRange() = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this virtual function being slow?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that doesn't look terrific. Perhaps something like:

/// Return a batch of ranges. The returned batch has size 0 iff the iterator is exhausted.
virtual util::span<std::variant<IntervalRange, BitmapRange>> NextRanges() = 0;

}

private:
decltype(RowRanges::ranges_.cbegin()) iter_;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we using IterT = decltype(RowRanges::ranges_.cbegin()); here?

@github-actions github-actions bot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Feb 6, 2025
Copy link
Member

@pitrou pitrou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked at the implementation, just commenting on the API.

struct IntervalRange {
/// Start row of the range (inclusive).
int64_t start;
/// End row of the range (inclusive).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest either a length or a exclusive end. Inclusive is more confusing and error-prone IMHO.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We commonly use the (start, length) convention in other places.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(start, length) is good for API but not good for computation because we have to transform it back and forth to (start, start + length).

struct End {};

/// \brief An iterator for accessing row ranges in order.
class Iterator {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have iteration facilities in arrow/util/iterator.h, should they be reused instead of defining our own here?


#pragma once

#include <variant>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also need to include <cstdint>

namespace parquet {

/// RowRanges is a collection of non-overlapping and ascendingly ordered row ranges.
class PARQUET_EXPORT RowRanges {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plural class names are always a bit weird, should we perhaps name it RowSelection?

static RowRanges MakeSingle(int64_t start, int64_t end);

/// \brief Make a row range from a list of intervals.
static RowRanges MakeIntervals(const std::vector<IntervalRange>& intervals);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could take a span<const IntervalRange> instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, perhaps name this FromIntervals.

Comment on lines +73 to +77
/// \brief Make a single row range of [0, row_count - 1].
static RowRanges MakeSingle(int64_t row_count);

/// \brief Make a single row range of [start, end].
static RowRanges MakeSingle(int64_t start, int64_t end);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is basically a simpler version of the IntervalRange-taking constructor?

};

/// \brief A range of contiguous rows represented by a bitmap.
struct BitmapRange {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually convenient to use? I see it's not supported yet in the implementation...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are other possible representations, for example:

/// \brief A range of 8 rows represented by a start offset and small indices
struct IndexedRange {
  /// Start row of the range
  int64_t offset;
  /// Indices from `offset` that point to selected rows, in ascending order.
  std::array<uint8_t, 8> indices;
};

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, two comments:

  1. this shows that a Google Doc is really not the right medium to discuss API changes
  2. I would ask that any API comes with an actual implementation; if it's too complicated to make use of the proposed BitmapRange efficiently, then it shows that BitmapRange is not the right abstraction

Copy link
Member Author

@wgtmac wgtmac Feb 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also a little bit skeptical about the effectiveness of mixing BitmapRange and IntervalRange. According to my experience, I need either a list of IntervalRange (like the RowRanges API in parquet-java) or simply a roaring bitmap, but not mixed. The former is used for intermediate result of filter pushdown (e.g. compute selected row ranges of a row group when evaluating different filters) and the latter is for row-level selection/deselection (e.g. apply iceberg position delete file to a base file).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The former is used for intermediate result of filter pushdown (e.g. compute selected row ranges of a row group when evaluating different filters) and the latter is for row-level selection/deselection (e.g. apply iceberg position delete file to a base file).

Ok, so several questions follow:

  1. at which use case is this API targeted?
  2. what is the typical bitmap length this is meant to represent?
  3. do we want a structure to select across an entire row group, or is it better to have one selection per data page, or perhaps a two-level selection (with a page bitmap and then a per-page bitmap for those pages that haven't been pruned)?

For example, given the typical size of a page, an uncompressed bitmap might be fine if selecting inside a page.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. at which use case is this API targeted?

a. Specify the required row ranges when creating a Parquet reader (be it a ParquetFileReader or RowGroupReader). This applies to both interval and bitmap forms depending on the use case. Perhaps this is the main reason to use a mixed form.
b. Used internally when computing matched row ranges from filter pushdown evaluation result with the help of page index. This applies to the interval range because operations are usually based on the page level.
c. Advanced row-level filtering discussed in #37559. This should be bitmap range used by ColumnReader and Decoder.

  1. what is the typical bitmap length this is meant to represent?

It depends on the use case above and can be any of num_rows_in_file, num_rows_in_a_row_group and batch_size.

  1. do we want a structure to select across an entire row group, or is it better to have one selection per data page, or perhaps a two-level selection (with a page bitmap and then a per-page bitmap for those pages that haven't been pruned)?

I don't think we need an intra-page selection since page boundaries are usually not aligned across different columns. I think file-level selection is easy to use for users and row-group-level selection is required by the RowGroupReader implementation.

class Iterator {
public:
virtual ~Iterator() = default;
virtual std::variant<IntervalRange, BitmapRange, End> NextRange() = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that doesn't look terrific. Perhaps something like:

/// Return a batch of ranges. The returned batch has size 0 iff the iterator is exhausted.
virtual util::span<std::variant<IntervalRange, BitmapRange>> NextRanges() = 0;


~IteratorImpl() override = default;

std::variant<RowRanges::IntervalRange, RowRanges::BitmapRange, RowRanges::End>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking that would variant being slow in storage? What would an internal:

enum class Type {
   IntervalRange,
   BitmapRange
};

struct Range {
   int64_t begin;
   /// for Interval this means range
   /// for bitmap this is a 64-bits map
   int64_t payload; 
};
std::vector<Type> types_;
std::vector<Range> ranges_;

I think this is more space efficient than current storage api...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants