alamb commented on code in PR #280:
URL: https://github.com/apache/arrow-site/pull/280#discussion_r1036478717


##########
_posts/2022-11-30-querying-parquet-with-millisecond-latency-part-3.md:
##########
@@ -0,0 +1,322 @@
+---
+layout: post
+title: "Querying Parquet with Millisecond Latency, Part 3"
+date: "2022-11-30 00:00:00"
+author: "tustvold and alamb"
+categories: [arrow]
+---
+<!--
+{% comment %}
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to you under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+{% endcomment %}
+-->
+
+## Introduction
+
+
+In [Part 1]({% post_url 
2022-11-30-querying-parquet-with-millisecond-latency-part-1 %}) of this series, 
reviews Parquet file structure, and [Part 2]({% post_url 
2022-11-30-querying-parquet-with-millisecond-latency-part-1 %}) explains 
commonly implemented decode optimizations and projection pushdown. In this 
part, we explain several forms of predicate pushdown and additional IO pushdown.
+
+
+# Predicate Pushdown
+
+Similar to projection pushdown, **predicate** pushdown also avoids fetching 
and decoding data from Parquet files, but does so using filter expressions. 
This technique typically requires closer integration with a query engine such 
as [DataFusion](https://arrow.apache.org/datafusion/), to determine valid 
predicates and evaluate them during the scan. Unfortunately without careful API 
design, the Parquet decoder and query engine can end up tightly coupled, 
preventing reuse (e.g. there are different Impala and Spark implementations in 
[Cloudera Parquet Predicate Pushdown 
docs](https://docs.cloudera.com/documentation/enterprise/6/6.3/topics/cdh_ig_predicate_pushdown_parquet.html#concept_pgs_plb_mgb)).
 The Rust Parquet reader uses the 
[RowSelection](https://docs.rs/parquet/27.0.0/parquet/arrow/arrow_reader/struct.RowSelector.html)
 API to avoid this coupling.
+
+
+## RowGroup Pruning
+
+The simplest form of predicate pushdown, supported by many Parquet based query 
engines, uses the statistics stored in the footer to skip entire RowGroups. We 
call this operation RowGroup _pruning_, and it is analogous to [partition 
pruning](https://docs.oracle.com/database/121/VLDBG/GUID-E677C85E-C5E3-4927-B3DF-684007A7B05D.htm#VLDBG00401)
 in many classical data warehouse systems.
+
+For the example query above, if the maximum value for A in a particular 
RowGroup is less than 35, the decoder can skip fetching and decoding any 
ColumnChunks from that **entire** RowGroup.
+
+
+```
+┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+┃Row Group 1 Metadata                      ┃
+┃ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃
+┃ ┃Column "A" Metadata    Min:0 Max:15   ┃◀╋ ┐
+┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃
+┃ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ │
+┃ ┃Column "B" Metadata                   ┃ ┃
+┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃ │
+┃ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃
+┃ ┃Column "C" Metadata                   ┃ ┃ │     Using the min and max values
+┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃       from the metadata, RowGroup
+┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ├ ─ ─ 1  can be entirely skipped
+┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓       (pruned) when searching for
+┃Row Group 2 Metadata                      ┃ │     rows with A > 35,
+┃ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃
+┃ ┃Column "A" Metadata   Min:10 Max:50   ┃◀╋ ┘
+┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃
+┃ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃
+┃ ┃Column "B" Metadata                   ┃ ┃
+┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃
+┃ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃
+┃ ┃Column "C" Metadata                   ┃ ┃
+┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┃
+┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+```
+
+
+Note that pruning on minimum and maximum values is effective for many data 
layouts and column types, but not all. Specifically, it is not as effective for 
columns with many distinct pseudo random values (e.g. identifiers or uuids). 
Thankfully for this use case Parquet also supports per ColumnChunk [Bloom 
Filters](https://github.com/apache/parquet-format/blob/master/BloomFilter.md). 
We are actively working on[ adding bloom 
filter](https://github.com/apache/arrow-rs/issues/3023) support in Apache 
Rust’s implementation.
+
+
+## Page Pruning
+
+A more sophisticated form of predicate pushdown uses the optional [page 
index](https://github.com/apache/parquet-format/blob/master/PageIndex.md) in 
the footer metadata to rule out entire Data Pages. The decoder decodes only the 
corresponding rows from other columns, often skipping entire pages.
+
+This optimization is complicated by the fact that pages in different 
ColumnChunks often contain different numbers of rows due to various reasons. 
While the page index may identify what pages are needed from one column, 
pruning a page from one column doesn’t immediately rule out entire pages in 
other columns.
+
+Page pruning proceeds as follows::
+
+
+
+* Use the predicates in combination with the page index to identify pages to 
skip
+* Use the offset index to determine what row ranges correspond to non-skipped 
pages
+* Computes the intersection of ranges across non-skipped pages, and decodes 
only those rows
+
+This last point is highly non-trivial to implement, especially for nested 
lists where [a single row may correspond to multiple 
values](https://arrow.apache.org/blog/2022/10/08/arrow-parquet-encoding-part-2/).
 Fortunately, the Rust Parquet reader hides this complexity internally, and can 
decode arbitrary 
[RowSelection](https://docs.rs/parquet/27.0.0/parquet/arrow/arrow_reader/struct.RowSelection.html)s.
+
+For example, to scan Columns A and B,  stored in 5 Data Pages as shown in the 
figure below:
+
+If the predicate is `A > 35`,
+* Page 1 is pruned using the page index (max value is 20), leaving a 
RowSelection of  [200->onwards],
+* Parquet reader skips Page 3 entirely (as its last row index is 99)
+* (Only) the relevant rows are read by reading pages 2, 4 and 5.
+
+If the predicate is instead `A > 35 AND B = "F"` the page index is even more 
effective
+* Using A > 35, yields a RowSelection of [200->onwards] as before
+* Using B = "F" on the remaining Page 4 and Page 5 of B, yields a RowSelection 
of [100-244]
+* Intersecting the two RowSelections leaves a combined RowSelection [200-244]
+* Parquet reader only decodes those 50 rows from Page 2 and Page 4.
+
+
+```
+┏━━ ━━━ ━━━ ━━━ ━━━ ━━━ ━━━ ━━━ ━━━ ━━━ ━━━ ━━━ ━━━
+   ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   ┃
+┃     ┌──────────────┐  │     ┌──────────────┐  │  ┃
+┃  │  │              │     │  │              │     ┃
+┃     │              │  │     │     Page     │  │
+   │  │              │     │  │      3       │     ┃
+┃     │              │  │     │   min: "A"   │  │  ┃
+┃  │  │              │     │  │   max: "C"   │     ┃
+┃     │     Page     │  │     │ first_row: 0 │  │
+   │  │      1       │     │  │              │     ┃
+┃     │   min: 10    │  │     └──────────────┘  │  ┃
+┃  │  │   max: 20    │     │  ┌──────────────┐     ┃
+┃     │ first_row: 0 │  │     │              │  │
+   │  │              │     │  │     Page     │     ┃
+┃     │              │  │     │      4       │  │  ┃
+┃  │  │              │     │  │   min: "D"   │     ┃
+┃     │              │  │     │   max: "G"   │  │
+   │  │              │     │  │first_row: 100│     ┃
+┃     └──────────────┘  │     │              │  │  ┃
+┃  │  ┌──────────────┐     │  │              │     ┃
+┃     │              │  │     └──────────────┘  │
+   │  │     Page     │     │  ┌──────────────┐     ┃
+┃     │      2       │  │     │              │  │  ┃
+┃  │  │   min: 30    │     │  │     Page     │     ┃
+┃     │   max: 40    │  │     │      5       │  │
+   │  │first_row: 200│     │  │   min: "H"   │     ┃
+┃     │              │  │     │   max: "Z"   │  │  ┃
+┃  │  │              │     │  │first_row: 250│     ┃
+┃     └──────────────┘  │     │              │  │
+   │                       │  └──────────────┘     ┃
+┃   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘  ┃
+┃       ColumnChunk            ColumnChunk         ┃
+┃            A                      B
+ ━━━ ━━━ ━━━ ━━━ ━━━ ━━━ ━━━ ━━━ ━━━ ━━━ ━━━ ━━━ ━━┛
+```
+
+
+Support for reading and writing these indexes from Arrow C++, and by extension 
pyarrow/pandas, is tracked in 
[PARQUET-1404](https://issues.apache.org/jira/browse/PARQUET-1404)
+
+
+## Late Materialization
+
+The two previous forms of predicate pushdown have only operated on metadata 
stored for RowGroups, ColumnChunks, and Data Pages prior to decoding values. 
However, the same techniques also extend to values of one or more columns 
*after* they have been decoded but prior to decoding other columns,  which is 
often called “late materialization”.
+
+This technique is especially effective when:
+* The predicate is very selective, i.e. filters out large numbers of rows
+* Each row is large, either due to wide rows (e.g. JSON blobs) or many columns
+* The selected data is clustered together
+* The columns required by the predicate are relatively inexpensive to decode, 
e.g. PrimitiveArray / DictionaryArray
+
+There is additional discussion in 
[SPARK-36527](https://issues.apache.org/jira/browse/SPARK-36527) and[ 
Impala](https://docs.cloudera.com/cdw-runtime/cloud/impala-reference/topics/impala-lazy-materialization.html).
+
+For example, given the predicate A > 35 AND B = "F" from above where the 
engine has used the page index to determine only 50 rows within RowSelection of 
[100-244] could match, using late materialization, the Parquet decoder:
+
+
+
+* Decodes the 50 values of Column A
+* Evaluates  A > 35 on those 50 values
+* In this case, only 5 rows pass, resulting in the RowSelection:
+    * RowSelection[205-206]
+    * RowSelection[238-240]
+* Only decodes the 5 rows for column B for those selections
+
+
+
+
+```
+
+
+  Row Index
+             ┌────────────────────┐            ┌────────────────────┐
+       200   │         30         │            │        "F"         │
+             └────────────────────┘            └────────────────────┘
+                      ...                               ...
+             ┌────────────────────┐            ┌────────────────────┐
+       205   │         37         │─ ─ ─ ─ ─ ─▶│        "F"         │
+             ├────────────────────┤            ├────────────────────┤
+       206   │         36         │─ ─ ─ ─ ─ ─▶│        "G"         │
+             └────────────────────┘            └────────────────────┘
+                      ...                               ...
+             ┌────────────────────┐            ┌────────────────────┐
+       238   │         36         │─ ─ ─ ─ ─ ─▶│        "F"         │
+             ├────────────────────┤            ├────────────────────┤
+       239   │         36         │─ ─ ─ ─ ─ ─▶│        "G"         │
+             ├────────────────────┤            ├────────────────────┤
+       240   │         40         │─ ─ ─ ─ ─ ─▶│         40         │
+             └────────────────────┘            └────────────────────┘
+                      ...                               ...
+             ┌────────────────────┐            ┌────────────────────┐
+      244    │         26         │            │        "D"         │
+             └────────────────────┘            └────────────────────┘
+
+
+                   Column A                          Column B
+                    Values                            Values
+
+
+```
+
+
+In certain cases, such as our example where B stores single character values, 
the cost of late materialization machinery can outweigh the savings in 
decoding. However, the savings can be substantial when some of the conditions 
listed above are fulfilled. The query engine must decide which predicates to 
push down and in which order to apply them for optimal results.
+
+While it is outside the scope of this document, the same technique can be 
applied for multiple predicates as well as predicates on multiple columns.  See 
the 
[RowFilter](https://docs.rs/parquet/latest/parquet/arrow/arrow_reader/struct.RowFilter.html)
  interface in the Parquet crate for more information and the 
[row_filter](https://github.com/apache/arrow-datafusion/blob/58b43f5c0b629be49a3efa0e37052ec51d9ba3fe/datafusion/core/src/physical_plan/file_format/parquet/row_filter.rs#L40-L70)
 implementation in DataFusion a corresponding query engine implementation.
+
+
+# IO Pushdown
+
+While Parquet was designed for efficient access on the [HDFS distributed file 
system](https://hadoop.apache.org/docs/r1.2.1/hdfs_design.html), it works very 
well with commodity blob storage systems such as AWS S3 as they have very 
similar characteristics:
+
+
+
+* **Relatively slow “random access” reads**: it is much more efficient to read 
large (MBs) sections of data in each request than issue many requests for 
smaller portions
+* **Significant latency to before the first byte is retrieved**
+* **High per-request cost: **Often billed per request, regardless of number of 
bytes read, which incentivizes fewer requests which each read a large 
contiguous section of data.
+
+To read optimally from such systems, a Parquet reader must:
+
+
+
+1. Minimize the number of I/O requests, while also applying the various 
pushdown techniques to avoid fetching large amounts of unused data.
+2. Integrate with the appropriate task scheduling mechanism to interleave IO 
and processing on the data that is fetched to avoid pipeline bottlenecks.
+
+As these are substantial engineering and integration challenges, many Parquet 
readers still require the files to be fetched in their entirety to local 
storage.
+
+Fetching the entire files in order to process them is not ideal for several 
reasons:
+
+
+
+1. **High Latency**: Decoding can not begin until the entire file to be 
fetched (Parquet metadata is at the end of the file, so the decoder must see 
the end prior to decoding the rest)
+2. **Wasted work**: Fetching the entire file fetches all necessary data, but 
also potentially lots of unnecessary data that will be skipped after reading 
the footer. This increases the cost unnecessarily.
+3. **Requires costly “locally attached” storage (or memory)**: Many cloud 
environments do not offer computing resources with locally attached storage – 
they either rely on expensive network block storage such as AWS EBS or else 
restrict local storage to certain classes of VMs.
+
+Avoiding the need to buffer the entire file requires a sophisticated Parquet 
decoder, integrated with the I/O subsystem, that can initially fetch and decode 
the metadata followed by ranged fetches for the relevant data blocks, 
interleaved with the decoding of Parquet data. This optimization requires 
careful engineering to fetch large enough blocks of data from the object store 
that the per request overhead doesn’t dominate gains from reducing the bytes 
transferred. [SPARK-36529](https://issues.apache.org/jira/browse/SPARK-36529) 
describes the challenges of sequential processing in more detail.
+
+
+```
+                       ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
+                                                                          │
+                       │
+               Step 1: Fetch                                              │
+ Parquet       Parquet metadata
+ file on 
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━▼━━━━━┓
+ Remote  ┃          ▒▒▒▒▒▒▒▒▒▒            ▒▒▒▒▒▒▒▒▒▒                 
░░░░░░░░░░ ┃
+ Object  ┃          ▒▒▒data▒▒▒            ▒▒▒data▒▒▒                 
░metadata░ ┃
+  Store  ┃          ▒▒▒▒▒▒▒▒▒▒            ▒▒▒▒▒▒▒▒▒▒                 
░░░░░░░░░░ ┃
+         
┗━━━━━━━━━━━━━━━▲━━━━━━━━━━━━━━━━━━━━━▲━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+                         │                     └ ─ ─ ─
+                                                      │
+                         │                   Step 2: Fetch only
+                          ─ ─ ─ ─ ─ ─ ─ ─ ─ relevant data blocks
+
+
+
+
+```
+
+
+Not included in this diagram picture are details like coalescing requests and 
ensuring minimum request sizes needed for an actual implementation.
+
+The Rust Parquet crate provides an async Parquet reader, to efficiently read 
from any 
[AsyncFileReader](https://docs.rs/parquet/latest/parquet/arrow/async_reader/trait.AsyncFileReader.html)
 that:
+
+
+
+* Efficiently reads from any storage medium that supports range requests
+* Integrates with Rust’s futures ecosystem to avoid blocking threads waiting 
on network IO [and easily can interleave CPU and network 
](https://www.influxdata.com/blog/using-rustlangs-async-tokio-runtime-for-cpu-bound-tasks/)
+* Requests multiple ranges simultaneously, to allow the implementation to 
coalesce adjacent ranges, fetch ranges in parallel, etc…
+* Uses the pushdown techniques described previously to eliminate fetching data 
where possible
+* Integrates easily with the Apache Arrow 
[object_store](https://docs.rs/object_store/latest/object_store/) crate which 
you can read more about 
[here](https://www.influxdata.com/blog/rust-object-store-donation/)
+
+To give a sense of what is possible, the following picture shows a timeline of 
fetching the footer metadata from remote files, using that metadata to 
determine what DataPages to read, and then fetching data and decoding 
simultaneously. This process often must be done for more than one file at a 
time in order to match network latency, bandwidth and available CPU.
+
+
+```

Review Comment:
   Note to myself that I hope to clean up this diagram



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to