<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>InfluxData Blog - Andrew Lamb</title>
    <description>Posts by Andrew Lamb on the InfluxData Blog</description>
    <link>https://www.influxdata.com/blog/author/andrew-lamb/</link>
    <language>en-us</language>
    <lastBuildDate>Thu, 03 Apr 2025 07:00:00 +0000</lastBuildDate>
    <pubDate>Thu, 03 Apr 2025 07:00:00 +0000</pubDate>
    <ttl>1800</ttl>
    <item>
      <title>Optimizing SQL (and DataFrames) in DataFusion: Part 2</title>
      <description>&lt;p&gt;&lt;em&gt;Part 2: Optimizers in Apache DataFusion&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://www.influxdata.com/blog/optimizing-sql-dataframes-part-one/?utm_source=website&amp;amp;utm_medium=direct&amp;amp;utm_campaign=optimizing_sql_dataframes_part_two&amp;amp;utm_content=blog"&gt;first part of this post&lt;/a&gt;, we discussed what a Query Optimizer is and what role it plays and described how industrial optimizers are organized. In this second post, we describe various optimizations found in &lt;a href="https://datafusion.apache.org/"&gt;Apache DataFusion&lt;/a&gt; and other industrial systems in more detail.&lt;/p&gt;

&lt;p&gt;DataFusion contains high-quality, full-featured implementations for &lt;em&gt;Always Optimizations&lt;/em&gt; and &lt;em&gt;Engine Specific Optimizations&lt;/em&gt; (defined in Part 1). Optimizers are implemented as rewrites of &lt;code class="language-markup"&gt;LogicalPlan&lt;/code&gt; in the &lt;a href="https://github.com/apache/datafusion/tree/main/datafusion/optimizer"&gt;logical optimizer&lt;/a&gt; or rewrites of &lt;code class="language-markup"&gt;ExecutionPlan&lt;/code&gt; in the &lt;a href="https://github.com/apache/datafusion/tree/main/datafusion/physical-optimizer"&gt;physical optimizer&lt;/a&gt;. This design means the same optimizer passes are applied for SQL and DataFrame queries, as well as plans for other query language frontends such as &lt;a href="https://github.com/influxdata/influxdb3_core/tree/26a30bf8d6e2b6b3f1dd905c4ec27e3db6e20d5f/iox_query_influxql"&gt;InfluxQL&lt;/a&gt; in InfluxDB 3, &lt;a href="https://github.com/GreptimeTeam/greptimedb/blob/0bd322a078cae4f128b791475ec91149499de33a/src/query/src/promql/planner.rs#L1"&gt;PromQL&lt;/a&gt; in &lt;a href="https://greptime.com/"&gt;Greptime&lt;/a&gt;, and &lt;a href="https://github.com/vega/vegafusion/tree/dc15c1b9fc7d297f12bea919795d58cda1c88fcf/vegafusion-core/src/planning"&gt;Vega&lt;/a&gt; in &lt;a href="https://vegafusion.io/"&gt;VegaFusion&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id="always-optimizations"&gt;Always optimizations&lt;/h2&gt;

&lt;p&gt;Some optimizations are so important they are found in almost all query engines and are typically the first implemented as they provide the largest cost-benefit ratio (and performance is terrible without them).&lt;/p&gt;

&lt;h3 id="predicatefilter-pushdown"&gt;Predicate/Filter Pushdown&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt;: Avoids carrying unneeded &lt;em&gt;rows&lt;/em&gt; as soon as possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What&lt;/strong&gt;: Moves filters “down” in the plan so they run earlier in execution, as shown in Figure 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example Implementations&lt;/strong&gt;: &lt;a href="https://github.com/apache/datafusion/blob/main/datafusion/optimizer/src/push_down_filter.rs"&gt;DataFusion&lt;/a&gt;, &lt;a href="https://github.com/duckdb/duckdb/blob/main/src/optimizer/filter_pushdown.cpp"&gt;DuckDB&lt;/a&gt;, &lt;a href="https://github.com/ClickHouse/ClickHouse/blob/master/src/Processors/QueryPlan/Optimizations/filterPushDown.cpp"&gt;ClickHouse&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The earlier data is filtered out in the plan, the less work the rest of the plan has to do. Most mature databases aggressively use filter pushdown/early filtering combined with techniques such as partition and storage pruning (e.g., &lt;a href="https://blog.xiangpeng.systems/posts/parquet-to-arrow/"&gt;Parquet Row Group pruning&lt;/a&gt;) for performance.&lt;/p&gt;

&lt;p&gt;An extreme and somewhat contrived example query:&lt;/p&gt;

&lt;pre class=""&gt;&lt;code class="language-sql"&gt;SELECT city, COUNT(*) FROM population GROUP BY city HAVING city = ‘BOSTON’;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Semantically, HAVING is &lt;a href="https://www.datacamp.com/tutorial/sql-order-of-execution"&gt;evaluated after&lt;/a&gt; GROUP BY in SQL. However, computing the population of all cities and discarding everything except Boston is much slower than computing only the population for Boston, so most Query Optimizers will evaluate the filter before the aggregation.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/4RLCtj5BxYM6rMZEJaTWDL/db7a6d4d1edad92f39627efe95bd1fcd/Screenshot_2025-04-02_at_8.12.37_AM.png" alt="Figure 1" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 1&lt;/strong&gt;: Filter Pushdown. In (&lt;strong&gt;A&lt;/strong&gt;), without filter pushdown, the operator processes more rows, reducing efficiency. In (&lt;strong&gt;B&lt;/strong&gt;) with filter pushdown, the operator receives fewer rows, resulting in less overall work and a faster and more efficient query.&lt;/p&gt;

&lt;h3 id="projection-pushdown"&gt;Projection pushdown&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt;: Avoids carrying unneeded &lt;em&gt;columns&lt;/em&gt; as soon as possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What:&lt;/strong&gt; Pushes “projection” (keeping only certain columns) earlier in the plan, as shown in Figure 2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example Implementations:&lt;/strong&gt; &lt;a href="https://github.com/apache/datafusion/blob/main/datafusion/physical-optimizer/src/projection_pushdown.rs"&gt;DataFusion&lt;/a&gt;, &lt;a href="https://github.com/duckdb/duckdb/blob/a8a6a080c8809d5d4b3c955e9f113574f6f0bfe0/src/optimizer/pushdown/pushdown_projection.cpp"&gt;DuckDB&lt;/a&gt;, &lt;a href="https://github.com/ClickHouse/ClickHouse/blob/master/src/Processors/QueryPlan/Optimizations/optimizeUseNormalProjection.cpp"&gt;ClickHouse&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Similarly to the motivation for &lt;em&gt;Filter Pushdown&lt;/em&gt;, the earlier the plan stops doing something, the less work it does overall and the faster it runs. For Projection Pushdown, if columns are not needed later in a plan, copying the data to the output of other operators is unnecessary and the costs of copying can add up. For example, in Figure 3 of Part 1, the &lt;code class="language-markup"&gt;species&lt;/code&gt; column is only needed to evaluate the filter within the scan and &lt;code class="language-markup"&gt;notes&lt;/code&gt; are never used, so copying them through the rest of the plan is unnecessary.&lt;/p&gt;

&lt;p&gt;Projection Pushdown is especially effective and important for column store databases, where the storage format itself (such as &lt;a href="https://parquet.apache.org/"&gt;Apache Parquet&lt;/a&gt;) supports efficiently reading only a subset of required columns. It is &lt;a href="https://blog.xiangpeng.systems/posts/parquet-pushdown/"&gt;especially powerful in combination with filter pushdown&lt;/a&gt;. Projection Pushdown is still important but less effective for row-oriented formats such as JSON or CSV, where each column in each row must be parsed even if it is not used in the plan.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/dp3eJCvF5vzntCg9OzAFF/8cbbe5a600426493bf9085a6b4f8a0c5/image5.png" alt="image5" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 2:&lt;/strong&gt; In (&lt;strong&gt;A&lt;/strong&gt;), without projection pushdown, the operator receives more columns, reducing efficiency. In (&lt;strong&gt;B&lt;/strong&gt;), with projection pushdown, the operator receives fewer columns, leading to optimized execution.&lt;/p&gt;

&lt;h3 id="limit-pushdown"&gt;Limit Pushdown&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt;: The earlier the plan stops generating data, the less overall work it does, and some operators have more efficient limited implementations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What:&lt;/strong&gt; Pushes limits (maximum row counts) down in a plan as early as possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example Implementations:&lt;/strong&gt; &lt;a href="https://github.com/apache/datafusion/blob/main/datafusion/optimizer/src/push_down_limit.rs"&gt;DataFusion&lt;/a&gt;, &lt;a href="https://github.com/duckdb/duckdb/blob/main/src/optimizer/limit_pushdown.cpp"&gt;DuckDB&lt;/a&gt;, &lt;a href="https://github.com/ClickHouse/ClickHouse/blob/master/src/Processors/QueryPlan/Optimizations/limitPushDown.cpp"&gt;ClickHouse&lt;/a&gt;, Spark (&lt;a href="https://github.com/apache/spark/blob/7bc8e99cde424c59b98fe915e3fdaaa30beadb76/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/LimitPushDownThroughWindow.scala"&gt;Window&lt;/a&gt; and &lt;a href="https://github.com/apache/spark/blob/7bc8e99cde424c59b98fe915e3fdaaa30beadb76/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/PushProjectionThroughLimit.scala"&gt;Projection&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Often, queries have a &lt;code class="language-markup"&gt;LIMIT&lt;/code&gt; or other clause that allows them to stop generating results early, so the sooner they can stop execution, the more efficiently they will execute.&lt;/p&gt;

&lt;p&gt;In addition, DataFusion and other systems have more efficient implementations of some operators that can be used if there is a limit. The classic example is replacing a full sort + limit with a &lt;a href="https://docs.rs/datafusion/latest/datafusion/physical_plan/struct.TopK.html"&gt;TopK&lt;/a&gt; operator that only tracks the top values using a heap. Similarly,  DataFusion’s Parquet reader stops fetching and opening additional files once the limit is hit. 
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/f90969f55b454d92a6249446e0baeca1/fa8fce28e353df8cf056acc26c584f20/unnamed.png" alt="" /&gt;
&lt;strong&gt;Figure 3&lt;/strong&gt;: In (&lt;strong&gt;A&lt;/strong&gt;), without limit pushdown, all data is sorted and everything except the first few rows are discarded. In (&lt;strong&gt;B&lt;/strong&gt;), with limit pushdown, Sort is replaced with TopK operator which does much less work.&lt;/p&gt;

&lt;h3 id="expression-simplification--constant-folding"&gt;Expression Simplification / Constant Folding&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt;: Evaluating the same expression for each row when the value doesn’t change is wasteful&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What&lt;/strong&gt;: Partially evaluates and/or algebraically simplifies expressions&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example Implementations:&lt;/strong&gt; &lt;a href="https://github.com/apache/datafusion/tree/main/datafusion/optimizer/src/simplify_expressions"&gt;DataFusion&lt;/a&gt;, DuckDB (has several &lt;a href="https://github.com/duckdb/duckdb/tree/7b18f0f3691c1b6367cf68ed2598d7034e14f41b/src/optimizer/rule"&gt;rules&lt;/a&gt; such as &lt;a href="https://github.com/duckdb/duckdb/blob/7b18f0f3691c1b6367cf68ed2598d7034e14f41b/src/optimizer/rule/constant_folding.cpp"&gt;constant folding&lt;/a&gt;, and &lt;a href="https://github.com/duckdb/duckdb/blob/7b18f0f3691c1b6367cf68ed2598d7034e14f41b/src/optimizer/rule/comparison_simplification.cpp"&gt;comparison simplification&lt;/a&gt;), &lt;a href="https://github.com/apache/spark/blob/7bc8e99cde424c59b98fe915e3fdaaa30beadb76/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/expressions.scala"&gt;Spark&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If an expression doesn’t change from row to row, it is better to evaluate it &lt;strong&gt;once&lt;/strong&gt; during planning. This is a classic compiler technique used in database systems.&lt;/p&gt;

&lt;p&gt;For example, given a query that finds all values from the current year:&lt;/p&gt;

&lt;pre class=""&gt;&lt;code class="language-sql"&gt;SELECT … WHERE extract(year from time_column) = extract(year from now())&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Evaluating &lt;code class="language-markup"&gt;extract(year from now())&lt;/code&gt; on every row is much more expensive than evaluating it once during planning time, so the query becomes a constant.&lt;/p&gt;

&lt;pre class=""&gt;&lt;code class="language-sql"&gt;SELECT … WHERE extract(year from time_column) = 2025&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Furthermore, it is often possible to push such predicates &lt;strong&gt;into&lt;/strong&gt; scans.&lt;/p&gt;

&lt;h3 id="rewriting-code-classlanguage-markupouter-joincode--code-classlanguage-markupinner-joincode"&gt;Rewriting &lt;code class="language-markup"&gt;OUTER JOIN&lt;/code&gt; → &lt;code class="language-markup"&gt;INNER JOIN&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; &lt;code class="language-markup"&gt;INNER JOIN&lt;/code&gt; implementations are almost always faster (as they are simpler) than &lt;code class="language-markup"&gt;OUTER JOIN&lt;/code&gt; implementations. &lt;code class="language-markup"&gt;INNER JOIN&lt;/code&gt;s impose fewer restrictions on other optimizer passes (such as join reordering and additional filter pushdown).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What&lt;/strong&gt;: In cases where &lt;code class="language-markup"&gt;null&lt;/code&gt; rows introduced by an &lt;code class="language-markup"&gt;OUTER JOIN&lt;/code&gt; will not appear in the results, it can be rewritten to an &lt;code class="language-markup"&gt;INNER JOIN.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example Implementations:&lt;/strong&gt; &lt;a href="https://github.com/apache/datafusion/blob/6028474969f0bfead96eb7f413791470afb6bf82/datafusion/optimizer/src/eliminate_outer_join.rs"&gt;DataFusion&lt;/a&gt;, &lt;a href="https://github.com/apache/spark/blob/7bc8e99cde424c59b98fe915e3fdaaa30beadb76/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/joins.scala#L124-L158"&gt;Spark&lt;/a&gt;, &lt;a href="https://github.com/ClickHouse/ClickHouse/blob/master/src/Processors/QueryPlan/Optimizations/convertOuterJoinToInnerJoin.cpp"&gt;ClickHouse&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For example, given a query such as the following:&lt;/p&gt;

&lt;pre class=""&gt;&lt;code class="language-sql"&gt;SELECT …
FROM orders LEFT OUTER JOIN customer ON (orders.cid = customer.id)
WHERE customer.last_name = ‘Lamb’&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;code class="language-markup"&gt;LEFT OUTER JOIN&lt;/code&gt; keeps all rows in &lt;code class="language-markup"&gt;orders&lt;/code&gt; that don’t have a matching customer but fills in the fields with &lt;code class="language-markup"&gt;null&lt;/code&gt;. All such rows will be filtered out by &lt;code class="language-markup"&gt;customer.last_name = ‘Lamb’&lt;/code&gt; and thus an &lt;code class="language-markup"&gt;INNER JOIN&lt;/code&gt; produces the same answer. This is illustrated in Figure 4.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/1i0kseWtDBoBnvhVxTeAhy/7d28a9ad6902e3eaf6800524a79dcbda/Screenshot_2025-04-02_at_7.36.06_AM.png" alt="Figure 4" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 4&lt;/strong&gt;: Rewriting &lt;code class="language-markup"&gt;OUTER JOIN&lt;/code&gt; to &lt;code class="language-markup"&gt;INNER JOIN&lt;/code&gt;. In (A), the original query contains an &lt;code class="language-markup"&gt;OUTER JOIN&lt;/code&gt; and a filter on &lt;code class="language-markup"&gt;customer.last_name&lt;/code&gt;, which filters out all rows that might be introduced by the &lt;code class="language-markup"&gt;OUTER JOIN&lt;/code&gt;. In (B), the &lt;code class="language-markup"&gt;OUTER JOIN&lt;/code&gt; is converted to inner join and a more efficient implementation can be used.&lt;/p&gt;

&lt;h2 id="engine-specific-optimizations"&gt;Engine specific optimizations&lt;/h2&gt;

&lt;p&gt;As discussed in Part 1 of this blog, optimizers also contain a set of passes that are still always good to do but are closely tied to the specifics of the query engine. This section describes some common types&lt;/p&gt;

&lt;h3 id="subquery-rewrites"&gt;Subquery Rewrites&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt;: Implementing subqueries by running a query for each row of the outer query is very expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What&lt;/strong&gt;: It is possible to rewrite subqueries as joins, which often perform much better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example Implementations:&lt;/strong&gt; DataFusion (&lt;a href="https://github.com/apache/datafusion/blob/main/datafusion/optimizer/src/decorrelate.rs"&gt;one&lt;/a&gt;, &lt;a href="https://github.com/apache/datafusion/blob/main/datafusion/optimizer/src/decorrelate_predicate_subquery.rs"&gt;two&lt;/a&gt;, &lt;a href="https://github.com/apache/datafusion/blob/main/datafusion/optimizer/src/scalar_subquery_to_join.rs"&gt;three&lt;/a&gt;), &lt;a href="https://github.com/apache/spark/blob/7bc8e99cde424c59b98fe915e3fdaaa30beadb76/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/subquery.scala"&gt;Spark&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Evaluating subqueries a row at a time is so expensive that execution engines in high-performance analytic systems such as DataFusion and &lt;a href="https://vertica.com/"&gt;Vertica&lt;/a&gt; may not support row-at-a-time evaluation, given how terrible the performance would be. Instead, analytic systems rewrite such queries into joins, which can perform 100s or 1000s of times faster for large datasets. However, transforming subqueries to joins requires “exotic” join semantics such as &lt;code class="language-markup"&gt;SEMI JOIN&lt;/code&gt;, &lt;code class="language-markup"&gt;ANTI JOIN&lt;/code&gt;,  and variations on how to treat equality with null&lt;sup&gt;1&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;For a simple example, consider a query like this:&lt;/p&gt;

&lt;pre class=""&gt;&lt;code class="language-sql"&gt;SELECT customer.name 
FROM customer 
WHERE (SELECT sum(value) 
       FROM orders WHERE
       orders.cid = customer.id) &amp;gt; 10;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This can be rewritten into:&lt;/p&gt;

&lt;pre class=""&gt;&lt;code class="language-sql"&gt;SELECT customer.name 
FROM customer 
JOIN (
  SELECT customer.id as cid_inner, sum(value) s 
  FROM orders 
  GROUP BY customer.id
 ) ON (customer.id = cid_inner AND s &amp;gt; 10);&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We don’t have space to detail this transformation or explain why it is so much faster to run but using it and many other transformations allows efficient subquery evaluation.&lt;/p&gt;

&lt;h3 id="optimized-expression-evaluation"&gt;Optimized expression evaluation&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt;: The capabilities of expression evaluation vary from system to system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What&lt;/strong&gt;: Optimize expression evaluation for the particular execution environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example Implementations&lt;/strong&gt;: There are many examples of these type of optimizations, including DataFusion’s &lt;a href="https://github.com/apache/datafusion/blob/main/datafusion/optimizer/src/common_subexpr_eliminate.rs"&gt;Common Subexpression Elimination&lt;/a&gt;, &lt;a href="https://github.com/apache/datafusion/blob/8f3f70877febaa79be3349875e979d3a6e65c30e/datafusion/optimizer/src/simplify_expressions/unwrap_cast.rs#L70"&gt;unwrap_cast&lt;/a&gt;, and &lt;a href="https://github.com/apache/datafusion/blob/main/datafusion/optimizer/src/extract_equijoin_predicate.rs"&gt;identifying equality join predicates&lt;/a&gt;. DuckDB &lt;a href="https://github.com/duckdb/duckdb/blob/main/src/optimizer/in_clause_rewriter.cpp"&gt;rewrites IN clauses&lt;/a&gt;, and &lt;a href="https://github.com/duckdb/duckdb/blob/main/src/optimizer/sum_rewriter.cpp"&gt;SUM expressions&lt;/a&gt;. Spark also &lt;a href="https://github.com/apache/spark/blob/7bc8e99cde424c59b98fe915e3fdaaa30beadb76/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/UnwrapCastInBinaryComparison.scala"&gt;unwraps casts in binary comparisons&lt;/a&gt; and &lt;a href="https://github.com/apache/spark/blob/7bc8e99cde424c59b98fe915e3fdaaa30beadb76/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/InjectRuntimeFilter.scala"&gt;adds special runtime filters&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To give a specific example of what DataFusion’s common subexpression elimination does, consider this query that refers to a complex expression multiple times:&lt;/p&gt;

&lt;pre class=""&gt;&lt;code class="language-sql"&gt;SELECT date_bin('1 hour', time, '1970-01-01')
FROM table 
WHERE date_bin('1 hour', time, '1970-01-01') &amp;gt;= '2025-01-01 00:00:00'
ORDER BY date_bin('1 hour', time, '1970-01-01')&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Evaluating &lt;code class="language-markup"&gt;date_bin('1 hour', time, '1970-01-01')&lt;/code&gt; each time it is encountered is inefficient compared to calculating its result once and reusing that result when it is encountered again (similar to caching). This reuse is called &lt;em&gt;Common Subexpression Elimination&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Some execution engines implement this optimization internally to their expression evaluation engine, but DataFusion represents it explicitly using a separate Projection plan node, as illustrated in Figure 5.  Effectively, the query above is rewritten to the following:&lt;/p&gt;

&lt;pre class=""&gt;&lt;code class="language-sql"&gt;SELECT time_chunk 
FROM(SELECT date_bin('1 hour', time, '1970-01-01') as time_chunk 
     FROM table)
WHERE time_chunk &amp;gt;= '2025-01-01 00:00:00'
ORDER BY time_chunk&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/81dbadd751754e85a51e558f15090792/4f363fc8d296732756b9ca167e24b9a4/unnamed.png" alt="" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 5:&lt;/strong&gt; Adding a Projection to evaluate common complex subexpression decreases complexity for subsequent stages.&lt;/p&gt;

&lt;h3 id="algorithm-selection"&gt;Algorithm Selection&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt;: Different engines have different specialized operators for certain operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What:&lt;/strong&gt; Selects specific implementations from the available operators based on properties of the query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example Implementations:&lt;/strong&gt; DataFusion’s &lt;a href="https://github.com/apache/datafusion/blob/8f3f70877febaa79be3349875e979d3a6e65c30e/datafusion/physical-optimizer/src/enforce_sorting/mod.rs"&gt;EnforceSorting&lt;/a&gt; pass uses sort-optimized implementations, Spark’s &lt;a href="https://github.com/apache/spark/blob/7bc8e99cde424c59b98fe915e3fdaaa30beadb76/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/RewriteAsOfJoin.scala"&gt;rewrite useS a special operator for ASOF joins&lt;/a&gt;, and ClickHouse’s&lt;a href="https://github.com/ClickHouse/ClickHouse/blob/7d15deda4b33282f356bb3e40a190d005acf72f2/src/Interpreters/ExpressionAnalyzer.cpp#L1066-L1080"&gt;join algorithm selection&lt;/a&gt; (such as &lt;a href="https://github.com/ClickHouse/ClickHouse/blob/7d15deda4b33282f356bb3e40a190d005acf72f2/src/Interpreters/ExpressionAnalyzer.cpp#L1022"&gt;when to use MergeJoin&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;For example, DataFusion uses a &lt;code class="language-markup"&gt;TopK&lt;/code&gt; (&lt;a href="https://docs.rs/datafusion/latest/datafusion/physical_plan/struct.TopK.html"&gt;source&lt;/a&gt;) operator rather than a full &lt;code class="language-markup"&gt;Sort&lt;/code&gt; if there is also a limit on the query. Similarly, it may choose to use the more efficient &lt;code class="language-markup"&gt;PartialOrdered&lt;/code&gt; grouping operation when the data is sorted on group keys or a &lt;code class="language-markup"&gt;MergeJoin&lt;/code&gt;.
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/2bf3413ccff3466991828b72cf0a07d9/62399e70917e8653cf50fe2ee7e92b65/unnamed.png" alt="" /&gt;
&lt;strong&gt;Figure 6:&lt;/strong&gt; An example of a specialized operation for grouping. In (&lt;strong&gt;A&lt;/strong&gt;), input data has no specified ordering, and DataFusion uses a hashing-based grouping operator (&lt;a href="https://github.com/apache/datafusion/blob/main/datafusion/physical-plan/src/aggregates/row_hash.rs"&gt;source&lt;/a&gt;) to determine distinct groups. In (&lt;strong&gt;B&lt;/strong&gt;), when the input data is ordered by the group keys, DataFusion uses a specialized grouping operator (&lt;a href="https://github.com/apache/datafusion/tree/main/datafusion/physical-plan/src/aggregates/order"&gt;source&lt;/a&gt;) to find boundaries that separate groups.&lt;/p&gt;

&lt;h3 id="using-statistics-directly"&gt;Using Statistics Directly&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt;: Using pre-computed statistics from a table, without actually reading or opening files, is much faster than processing data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What&lt;/strong&gt;: Replace calculations on data with the value from statistics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example Implementations:&lt;/strong&gt; &lt;a href="https://github.com/apache/datafusion/blob/8f3f70877febaa79be3349875e979d3a6e65c30e/datafusion/physical-optimizer/src/aggregate_statistics.rs"&gt;DataFusion&lt;/a&gt;, &lt;a href="https://github.com/duckdb/duckdb/blob/main/src/optimizer/statistics_propagator.cpp"&gt;DuckDB&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Some queries, such as the classic &lt;code class="language-markup"&gt;COUNT(*) from my_table&lt;/code&gt; used for data exploration, can be answered using statistics only. Optimizers often have access to statistics for other reasons (such as Access Path and Join Order Selection) and statistics are commonly stored in analytic file formats. For example, the &lt;a href="https://docs.rs/parquet/latest/parquet/file/metadata/index.html"&gt;Metadata&lt;/a&gt; of Apache Parquet files stores MIN, MAX, and COUNT​ information. 
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/89e2ad4cee9d443cba1f166a8551641c/fbb25ac39187b1eaa9193621b8622e63/unnamed.png" alt="" /&gt;
&lt;strong&gt;Figure 7:&lt;/strong&gt; When the aggregation result is already stored in the statistics, the query can be evaluated using the values from statistics without looking at any compressed data. The Optimizer replaces the Aggregation operation with values from statistics.&lt;/p&gt;

&lt;h2 id="access-path-and-join-order-selection"&gt;Access path and join order selection&lt;/h2&gt;

&lt;h3 id="overview"&gt;Overview&lt;/h3&gt;

&lt;p&gt;Last but certainly not least are optimizations that choose between plans with potentially (very) different performance. The major options in this category are:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Join Order:&lt;/strong&gt; In what order should tables be combined using JOINs?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Access Paths:&lt;/strong&gt; Which copy of the data or index should be read to find matching tuples?&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Materialized_view"&gt;Materialized View&lt;/a&gt;: Can the query can be rewritten to use a materialized view (partially computed query results)? This topic deserves its own blog (or book); we don’t discuss it further here.
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/26Aq3hmRoZqPlT1BfGkc3Z/7d120feb39922c135909e936db832c77/Screenshot_2025-04-02_at_7.35.08_AM.png" alt="Figure 8" /&gt;
&lt;strong&gt;Figure 8:&lt;/strong&gt; Access Path and Join Order Selection Query Optimizers. Optimizers use heuristics to enumerate some subset of potential join orders (shape) and access paths (color). The plan with the lowest estimated cost is chosen according to some cost model. In this case, Plan 2, with a cost of 180,000, is chosen for execution as it has the lowest estimated cost.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This class of optimizations is a hard problem for at least the following reasons:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Exponential Search Space&lt;/strong&gt;: The number of potential plans increases exponentially as the number of joins and indexes increases.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Performance Sensitivity&lt;/strong&gt;: Often, different plans that are very similar in structure perform very differently. For example, swapping the input order to a hash join can result in 1000x or more (yes, thousandfold!) run time differences.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Cardinality Estimation Errors&lt;/strong&gt;: Determining the optimal plan relies on cardinality estimates (e.g., how many rows will come out of each join). Estimating this cardinality is a &lt;a href="https://www.vldb.org/pvldb/vol9/p204-leis.pdf"&gt;known hard problem&lt;/a&gt;, and in practice, queries with as few as three joins often have large cardinality estimation errors.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id="heuristics-and-cost-based-optimization"&gt;Heuristics and Cost-Based Optimization&lt;/h3&gt;

&lt;p&gt;Industrial optimizers handle these problems using a combination of:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Heuristics:&lt;/strong&gt; Prune the search space and avoid considering plans that are (almost) never good. Examples include considering left-deep trees or using &lt;code class="language-markup"&gt;Foreign Key&lt;/code&gt; / &lt;code class="language-markup"&gt;Primary Key&lt;/code&gt; relationships to pick the build size of a hash join.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Cost Model&lt;/strong&gt;: Given the smaller set of candidate plans, the Optimizer then estimates their cost and picks the one using the lowest cost.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For some examples, you can read about &lt;a href="https://docs.databricks.com/aws/en/optimizations/cbo"&gt;Spark’s cost-based optimizer&lt;/a&gt; or look at the code for DataFusion’s &lt;a href="https://github.com/apache/datafusion/blob/main/datafusion/physical-optimizer/src/join_selection.rs"&gt;join selection&lt;/a&gt; and DuckDB’s &lt;a href="https://github.com/duckdb/duckdb/blob/main/src/optimizer/join_order/cost_model.cpp"&gt;cost model&lt;/a&gt; and &lt;a href="https://github.com/duckdb/duckdb/blob/84c87b12fa9554a8775dc243b4d0afd5b407321a/src/optimizer/join_order/plan_enumerator.cpp#L469-L472"&gt;join-order enumeration&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However, the use of heuristics and (imprecise) cost models means optimizers must:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Make deep assumptions about the execution environment:&lt;/strong&gt; For example, the heuristics often include assumptions that joins implement &lt;a href="https://www.alibabacloud.com/blog/alibaba-cloud-analyticdb-for-mysql-create-ultimate-runtimefilter-capability_600228"&gt;sideways information passing (RuntimeFilters)&lt;/a&gt; or that Join operators always preserve a particular input.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Use one particular objective function:&lt;/strong&gt; There are almost always trade-offs between desirable plan properties, such as execution speed, memory use, and robustness in the face of cardinality estimation. Industrial optimizers typically have one cost function, which attempts to balance between the properties or a series of hard-to-use indirect tuning knobs to control the behavior.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Require statistics&lt;/strong&gt;: Typically cost models require up-to-date statistics, which can be expensive to compute, must be kept up to date as new data arrives, and often have trouble capturing the nonuniformity of real-world datasets.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id="join-ordering-in-datafusion"&gt;Join Ordering in DataFusion&lt;/h3&gt;

&lt;p&gt;DataFusion purposely does not include a sophisticated cost-based optimizer. Instead, in keeping with its &lt;a href="https://docs.rs/datafusion/latest/datafusion/#design-goals"&gt;design goals&lt;/a&gt; it provides a reasonable default implementation along with extension points to customize behavior.&lt;/p&gt;

&lt;p&gt;Specifically, DataFusion includes:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;“Syntactic Optimizer” (joins in the order listed in the query&lt;sup&gt;2&lt;/sup&gt;) with basic join reordering (&lt;a href="https://github.com/apache/datafusion/blob/main/datafusion/physical-optimizer/src/join_selection.rs"&gt;source&lt;/a&gt;) to prevent join disasters&lt;/li&gt;
  &lt;li&gt;Support for &lt;a href="https://docs.rs/datafusion/latest/datafusion/common/struct.ColumnStatistics.html"&gt;ColumnStatistics&lt;/a&gt; and &lt;a href="https://docs.rs/datafusion/latest/datafusion/common/struct.Statistics.html"&gt;Table Statistics&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;The framework for &lt;a href="https://docs.rs/datafusion/latest/datafusion/physical_expr/struct.AnalysisContext.html#structfield.selectivity"&gt;filter selectivity&lt;/a&gt; + join cardinality estimation&lt;/li&gt;
  &lt;li&gt;APIs for easily rewriting plans, such as the &lt;a href="https://docs.rs/datafusion/latest/datafusion/common/tree_node/trait.TreeNode.html#overview"&gt;TreeNode API&lt;/a&gt; and &lt;a href="https://docs.rs/datafusion/latest/datafusion/physical_plan/joins/struct.HashJoinExec.html#method.swap_inputs"&gt;reordering joins&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This combination of features, along with &lt;a href="https://docs.rs/datafusion/latest/datafusion/execution/session_state/struct.SessionStateBuilder.html#method.with_physical_optimizer_rule"&gt;custom optimizer passes&lt;/a&gt;, lets users customize the behavior to their use case, such as custom indexes like &lt;a href="https://uwheel.rs/post/datafusion_uwheel/"&gt;uWheel&lt;/a&gt; and &lt;a href="https://docs.google.com/presentation/d/1mHDw1uZcOwlpUO3mA8aqSyk7IqeovpSuXG27clowXWE/edit#slide=id.p"&gt;materialized views&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The rationale for including only a basic optimizer is that any particular set of heuristics and cost model is unlikely to work well for the wide variety of DataFusion users because they have different tradeoffs.&lt;/p&gt;

&lt;p&gt;For example, some users may always have access to adequate resources, want the fastest query execution, and are willing to tolerate runtime errors or a performance cliff when there is insufficient memory. Other users, however, may be willing to accept a slower maximum performance in return for a more predictable performance when running in a resource-constrained environment. This approach is not universally agreed. One of us has &lt;a href="https://www.researchgate.net/publication/269306314_The_Vertica_Query_Optimizer_The_case_for_specialized_query_optimizers"&gt;previously argued the case for specialized optimizers&lt;/a&gt; in a more academic paper, and the topic comes up regularly in the DataFusion community (e.g., &lt;a href="https://github.com/apache/datafusion/issues/9846#issuecomment-2566568654"&gt;this recent comment&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Note: We are &lt;a href="https://github.com/apache/datafusion/issues/3929"&gt;actively improving&lt;/a&gt; this part of the code to help people write their own optimizers (🎣 come help us define and implement it!)&lt;/p&gt;

&lt;h2 id="to-summarize"&gt;To summarize&lt;/h2&gt;

&lt;p&gt;Optimizers are awesome, and we hope these two posts have demystified what they are and how they are implemented in industrial systems. Like many modern query engine designs, the common techniques are well known, though require substantial effort to get right. DataFusion’s industrial strength optimizers can and do serve many real-world systems well and we expect that number to grow over time.&lt;/p&gt;

&lt;p&gt;We also think DataFusion provides interesting opportunities for optimizer research. As we discussed, there are still unsolved problems, such as optimal join ordering. Experiments in papers often use academic systems or modify optimizers in open source but tightly integrated systems (for example, the recent &lt;a href="https://www.vldb.org/pvldb/vol17/p1350-justen.pdf"&gt;POLARs paper&lt;/a&gt; uses DuckDB). However, this style means the research is constrained to the set of heuristics and structure provided by those particular systems. Hopefully DataFusion’s documentation, &lt;a href="https://dl.acm.org/doi/10.1145/3626246.3653368"&gt;newly citeable SIGMOD paper&lt;/a&gt;, and modular design will encourage more broadly applicable research in this area.&lt;/p&gt;

&lt;p&gt;And finally, as always, if you are interested in working on query engines and learning more about how they are designed and implemented, please &lt;a href="https://datafusion.apache.org/contributor-guide/communication.html"&gt;join our community&lt;/a&gt;. We welcome first-time contributors as well as long-time participants to the fun of building a database together.&lt;/p&gt;

&lt;hr /&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;See &lt;a href="https://btw-2015.informatik.uni-hamburg.de/res/proceedings/Hauptband/Wiss/Neumann-Unnesting_Arbitrary_Querie.pdf"&gt;Unnesting Arbitrary Queries&lt;/a&gt; from Neumann and Kemper for a more academic treatment.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;One of my favorite terms I learned from Andy Pavlo’s CMU online lectures.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;
</description>
      <pubDate>Thu, 03 Apr 2025 07:00:00 +0000</pubDate>
      <link>https://www.influxdata.com/blog/optimizing-sql-dataframes-part-two/</link>
      <guid isPermaLink="true">https://www.influxdata.com/blog/optimizing-sql-dataframes-part-two/</guid>
      <category>Developer</category>
      <author>Andrew Lamb, Mustafa Akur (InfluxData)</author>
    </item>
    <item>
      <title>Optimizing SQL (and DataFrames) in DataFusion: Part 1</title>
      <description>&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;

&lt;p&gt;Sometimes Query Optimizers are seen as a sort of black magic, &lt;a href="https://15799.courses.cs.cmu.edu/spring2025/"&gt;“the most challenging problem in computer science&lt;/a&gt;&lt;a href="https://15799.courses.cs.cmu.edu/spring2025/"&gt;,”&lt;/a&gt; according to Father Pavlo, or some behind-the-scenes player. We believe this perception is because:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;One must implement the rest of a database system (data storage, transactions, SQL parser, expression evaluation, plan execution, etc.) &lt;strong&gt;before&lt;/strong&gt; the optimizer becomes critical&lt;sup&gt;1&lt;/sup&gt;.&lt;/li&gt;
  &lt;li&gt;Some parts of the optimizer are tightly tied to the rest of the system (e.g., storage or indexes), so many classic optimizers are described with system-specific terminology.&lt;/li&gt;
  &lt;li&gt;Some optimizer tasks, such as access path selection and join order are known challenges and not yet solved (practically)—maybe they really do require black magic 🤔.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;However, Query Optimizers are no more complicated in theory or practice than other parts of a database system, as we will argue in a series of posts:&lt;/p&gt;

&lt;p&gt;Part 1:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Review what a Query Optimizer is, what it does, and why you need one for SQL and DataFrames.&lt;/li&gt;
  &lt;li&gt;Describe how industrial Query Optimizers are structured and standard optimization classes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Part 2:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Describe the optimization categories with examples and pointers to implementations.&lt;/li&gt;
  &lt;li&gt;Describe &lt;a href="https://datafusion.apache.org/"&gt;Apache DataFusion&lt;/a&gt;’s rationale and approach to query optimization, specifically for access path and join ordering.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After reading these blogs, we hope people will use DataFusion to:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Build their own system specific optimizers.&lt;/li&gt;
  &lt;li&gt;Perform practical academic research on optimization (especially researchers working on new optimizations / join ordering—looking at you &lt;a href="https://15799.courses.cs.cmu.edu/spring2025/"&gt;CMU 15-799&lt;/a&gt;, next year).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id="query-optimizer-background"&gt;Query Optimizer background&lt;/h2&gt;

&lt;p&gt;The key pitch for querying databases, and likely the key to the longevity of SQL (despite people’s love/hate relationship—see &lt;a href="https://db.cs.cmu.edu/seminar2025/"&gt;SQL or Death? Seminar Series – Spring 2025&lt;/a&gt;), is that it disconnects the &lt;code class="language-markup"&gt;WHAT&lt;/code&gt; you want to compute from the &lt;code class="language-markup"&gt;HOW&lt;/code&gt; to do it. SQL is a &lt;em&gt;declarative&lt;/em&gt; language—it describes what answers are desired rather than an &lt;em&gt;imperative&lt;/em&gt; language such as Python, where you describe how to do the computation as shown in Figure 1.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/4amC5zMhas941GjCbgiQvj/52d0c3963cf1544b0d278fbbd8d3fa1d/figure-1.png" alt="figure-1" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 1&lt;/strong&gt;: Query Execution: Users describe the answer they want using either a DataFrame or SQL. The query planner or DataFrame API translates that description into an &lt;em&gt;Initial Plan&lt;/em&gt;, which is correct but slow. The Query Optimizer then rewrites the initial plan to an &lt;em&gt;Optimized Plan&lt;/em&gt;, which computes the same results but faster and more efficiently. Finally, the Execution Engine executes the optimized plan producing results.&lt;/p&gt;

&lt;h2 id="sql-dataframes-logicalplan-equivalence"&gt;SQL, DataFrames, LogicalPlan equivalence&lt;/h2&gt;

&lt;p&gt;Given their name, it is not surprising that Query Optimizers can improve the performance of SQL queries. However, it is under-appreciated that this also applies to DataFrame style APIs.&lt;/p&gt;

&lt;p&gt;Classic DataFrame systems such as &lt;a href="https://pandas.pydata.org/"&gt;pandas&lt;/a&gt; and &lt;a href="https://pola.rs/"&gt;Polars&lt;/a&gt; (by default) execute eagerly and thus have limited opportunities for optimization. However, more modern APIs such as Polar’s &lt;a href="https://docs.pola.rs/user-guide/lazy/using/"&gt;lazy API&lt;/a&gt;, Apache Spark &lt;a href="https://spark.apache.org/docs/latest/sql-programming-guide.html#datasets-and-dataframes"&gt;DataFrame&lt;/a&gt;, and DataFusion’s &lt;a href="https://datafusion.apache.org/user-guide/dataframe.html"&gt;DataFrame&lt;/a&gt; are much faster as they use the design shown in Figure 1 and apply many query optimization techniques.&lt;/p&gt;

&lt;h2 id="example-of-query-optimizer"&gt;Example of Query Optimizer&lt;/h2&gt;

&lt;p&gt;This section motivates the value of a Query Optimizer with an example. Let’s say you have some observations of animal behavior, as illustrated in Table 1.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;&lt;!----&gt;&lt;/th&gt;
      &lt;th&gt;&lt;!----&gt;&lt;/th&gt;
      &lt;th&gt;&lt;!----&gt;&lt;/th&gt;
      &lt;th&gt;&lt;!----&gt;&lt;/th&gt;
      &lt;th&gt;&lt;!----&gt;&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Location&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;Species&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;Population&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;Observation Time&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;Notes&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;North&lt;/td&gt;
      &lt;td&gt;contrarian spider&lt;/td&gt;
      &lt;td&gt;100&lt;/td&gt;
      &lt;td&gt;2025-02-21T10:00:00Z&lt;/td&gt;
      &lt;td&gt;Watched Me&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;…&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;South&lt;/td&gt;
      &lt;td&gt;contrarian spider&lt;/td&gt;
      &lt;td&gt;234&lt;/td&gt;
      &lt;td&gt;2025-02-23T11:23:00Z&lt;/td&gt;
      &lt;td&gt;N/A&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;Table 1&lt;/strong&gt;: Example observational data.&lt;/p&gt;

&lt;p&gt;If the user wants to know the average population for some species in the last month, a user can write a SQL query or a DataFrame such as the following:&lt;/p&gt;

&lt;pre class=""&gt;&lt;code class="language-sql"&gt;SELECT location, AVG(population)
FROM observations
WHERE species = ‘contrarian spider’ AND 
  observation_time &amp;gt;= now() - interval '1 month'
GROUP BY location&lt;/code&gt;&lt;/pre&gt;

&lt;pre class=""&gt;&lt;code class="language-bash"&gt;df.scan("observations")
  .filter(col("species").eq("contrarian spider"))
  .filter(col("observation_time").ge(now()).sub(interval('1 month')))
  .agg(vec![col(location)], vec![avg(col("population")])&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Within DataFusion, both the SQL and DataFrame are translated into the same &lt;code class="language-markup"&gt;LogicalPlan&lt;/code&gt;, a “tree of relational operators.” This is a fancy way of saying data flow graphs where the edges represent tabular data (rows + columns) and the nodes represent a transformation (see &lt;a href="https://youtu.be/EzZTLiSJnhY"&gt;this DataFusion overview video&lt;/a&gt; for more details). The initial &lt;code class="language-markup"&gt;LogicalPlan&lt;/code&gt; for the queries above is shown in Figure 2.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/18Gsm7FSLzL9EY3Kpgps0f/10a18799cab00006012398fc8b927f2f/Part1-figure2.png" alt="Part1-figure2" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 2&lt;/strong&gt;: Example initial &lt;code class="language-markup"&gt;LogicalPlan&lt;/code&gt; for SQL and DataFrame query. The plan is read from bottom to top, computing the results in each step.&lt;/p&gt;

&lt;p&gt;The optimizer’s job is to take this query plan and rewrite it into an alternate plan that computes the same results but faster, such as the one shown in Figure 3.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/7fLcpx7snCKCfYEH7hDP4/2dec74ab814a9092e46d56a8a044fc6e/figure-3.png" alt="figure-3" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 3&lt;/strong&gt;: An example optimized plan that computes the same result as the plan in Figure 2 more efficiently. The diagram highlights where the optimizer has applied &lt;em&gt;Projection Pushdown&lt;/em&gt;, &lt;em&gt;Filter Pushdown&lt;/em&gt;, and &lt;em&gt;Constant Evaluation&lt;/em&gt;. Note that this is a simplified example for explanatory purposes, and actual optimizers such as the one in DataFusion perform additional tasks such as choosing specific aggregation algorithms.&lt;/p&gt;

&lt;h2 id="query-optimizer-implementation"&gt;Query Optimizer implementation&lt;/h2&gt;

&lt;p&gt;Industrial optimizers, such as DataFusion’s (&lt;a href="https://github.com/apache/datafusion/tree/334d6ec50f36659403c96e1bffef4228be7c458e/datafusion/optimizer/src"&gt;source&lt;/a&gt;), ClickHouse (&lt;a href="https://github.com/ClickHouse/ClickHouse/tree/master/src/Analyzer/Passes"&gt;source&lt;/a&gt;, &lt;a href="https://github.com/ClickHouse/ClickHouse/tree/master/src/Processors/QueryPlan/Optimizations"&gt;source&lt;/a&gt;), DuckDB (&lt;a href="https://github.com/duckdb/duckdb/tree/4afa85c6a4dacc39524d1649fd8eb8c19c28ad14/src/optimizer"&gt;source&lt;/a&gt;), and Apache Spark (&lt;a href="https://github.com/apache/spark/tree/7bc8e99cde424c59b98fe915e3fdaaa30beadb76/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer"&gt;source&lt;/a&gt;), are implemented as a series of passes or rules that rewrite a query plan. The overall optimizer is composed of a sequence of these rules,&lt;sup&gt;6&lt;/sup&gt; as shown in Figure 4. The specific order of the rules also often matters, but we will not discuss this detail in this post.&lt;/p&gt;

&lt;p&gt;A multi-pass design is standard because it helps:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Understand, implement, and test each pass in isolation&lt;/li&gt;
  &lt;li&gt;Easily extend the optimizer by adding new passes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/34WAXPaNlEDKt6UKd1UJJx/b0ccedad9126a8433f76440b3130be12/Screenshot_2025-03-20_at_2.26.08_PM.png" alt="Figure 4" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 4&lt;/strong&gt;: Query Optimizers are implemented as a series of rules that each rewrite the query plan. Each rule’s algorithm is expressed as a transformation of a previous plan.&lt;/p&gt;

&lt;p&gt;There are three major classes of optimizations in industrial optimizers:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Always Optimizations&lt;/strong&gt;: These are always good to do and thus are always applied. This class of optimization includes expression simplification, predicate pushdown, and limit pushdown. These optimizations are typically simple in theory, though they require nontrivial amounts of code and tests to implement in practice.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Engine Specific Optimizations:&lt;/strong&gt; These optimizations take advantage of specific engine features, such as how expressions are evaluated or what particular hash or join implementations are available.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Access Path and Join Order Selection&lt;/strong&gt;: These passes choose one access method per table and a join order for execution, typically using heuristics and a cost model to make tradeoffs between the options. Databases often have multiple ways to access the data (e.g., index scan or full-table scan), as well as many potential orders to combine (join) multiple tables. These methods compute the same result but can vary drastically in performance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This brings us to the end of Part 1. In &lt;a href="https://www.influxdata.com/blog/optimizing-sql-dataframes-part-two/"&gt;Part 2&lt;/a&gt;, we will explain these classes of optimizations in more detail and provide examples of how they are implemented in DataFusion and other systems.&lt;/p&gt;

&lt;hr /&gt;

&lt;ol&gt;
  &lt;li&gt;And thus in academic classes, by the time you get around to an optimizer the semester is over and everyone is ready for the semester to be done. Once industrial systems mature to the point where the optimizer is a bottleneck, the shiny newness of the &lt;a href="https://en.wikipedia.org/wiki/Gartner_hype_cycle"&gt;hype cycle&lt;/a&gt; has worn off and it is likely in the trough of disappointment.&lt;/li&gt;
&lt;/ol&gt;
</description>
      <pubDate>Mon, 31 Mar 2025 07:00:00 +0000</pubDate>
      <link>https://www.influxdata.com/blog/optimizing-sql-dataframes-part-one/</link>
      <guid isPermaLink="true">https://www.influxdata.com/blog/optimizing-sql-dataframes-part-one/</guid>
      <category>Developer</category>
      <author>Andrew Lamb, Mustafa Akur (InfluxData)</author>
    </item>
    <item>
      <title>2025: The Year of 1,000 DataFusion-Based Systems </title>
      <description>&lt;p&gt;&lt;a href="https://datafusion.apache.org/"&gt;Apache DataFusion&lt;/a&gt; has reached an inflection point. It has matured beyond early adopters and is now a viable choice for anyone building highly performant analytic systems. I predict 2025 will bring a significant acceleration in the number of systems built on DataFusion, and my focus this year is to help drive that growth.&lt;/p&gt;

&lt;h2 id="the-journey-from-0-to-1000-projects"&gt;&lt;strong&gt;The journey from 0 to 1,000 projects&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;Two years ago, when introducing DataFusion to VCs and early collaborators, I had an ambitious goal: 1,000 projects powered by DataFusion. That number was aspirational—bold enough to challenge but grounded enough to feel achievable. I think we may hit that goal in 2025.&lt;/p&gt;

&lt;p&gt;DataFusion achieved several key milestones in 2024 as it matured from a promising technology to a building block for highly performant systems:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://news.apache.org/foundation/entry/apache-software-foundation-announces-new-top-level-project-apache-datafusion"&gt;Elevated to a Top-Level Project within the Apache Software Foundation (ASF)&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Hosted the first in-person meetup in &lt;a href="https://github.com/apache/datafusion/discussions/8152"&gt;Austin, Texas&lt;/a&gt;, followed by others in San Francisco, Seattle, Belgrade, and more.&lt;/li&gt;
  &lt;li&gt;Published a research paper at &lt;a href="https://2024.sigmod.org/"&gt;ACM SIGMOD 2024&lt;/a&gt;&lt;u&gt;,&lt;/u&gt; one of the world’s leading database conferences.&lt;/li&gt;
  &lt;li&gt;Gained adoption by a growing number of &lt;a href="https://datafusion.apache.org/user-guide/introduction.html#known-users"&gt;database products and companies&lt;/a&gt;&lt;sup&gt;[1]&lt;/sup&gt;, with increased &lt;a href="https://datafusion.apache.org/user-guide/concepts-readings-events.html"&gt;media attention&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The year closed with a major breakthrough: DataFusion 43.0.0 became the &lt;a href="https://www.influxdata.com/blog/apache-datafusion-fastest-single-node-querying-engine/?utm_source=website&amp;amp;utm_medium=direct&amp;amp;utm_campaign=datafusion_2025_influxdb&amp;amp;utm_content=blog"&gt;fastest engine for querying Apache Parquet files in ClickBench&lt;/a&gt;, marking the first time a Rust-based engine surpassed traditional C/C++ engines.&lt;/p&gt;

&lt;p&gt;These milestones didn’t happen by chance—they are the result of eight years of relentless development from hundreds of individuals and countless engineering hours. Figure 1 shows my subjective appraisal of DataFusion’s timeline and my prediction of its acceleration over the next few years:
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/3s8NlVGxEIK0eVS04MhX8m/773d7eb0c10f6535c45985fab98716b3/02-a.png" alt="Figure 1" /&gt;
&lt;strong&gt;Figure 1:&lt;/strong&gt; Major milestones in the DataFusion project lifetime and my estimates of project adoption. I predict 2025 will be very exciting.&lt;/p&gt;

&lt;h2 id="early-adopters-including-influxdb-3"&gt;&lt;strong&gt;2020-2023 early adopters, including InfluxDB 3&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;InfluxData &lt;a href="https://www.influxdata.com/blog/apache-arrow-parquet-flight-and-their-ecosystem-are-a-game-changer-for-olap/?utm_source=website&amp;amp;utm_medium=direct&amp;amp;utm_campaign=datafusion_2025_influxdb&amp;amp;utm_content=blog"&gt;recognized DataFusion’s potential early on&lt;/a&gt; and bet on it for the rebuild of InfluxDB in Rust, along with the rest of the &lt;a href="https://www.influxdata.com/blog/flight-datafusion-arrow-parquet-fdap-architecture-influxdb/?utm_source=website&amp;amp;utm_medium=direct&amp;amp;utm_campaign=datafusion_2025_influxdb&amp;amp;utm_content=blog"&gt;FDAP stack&lt;/a&gt;—Apache Arrow Flight, Apache DataFusion, Apache Arrow, and Apache Parquet—all ASF technologies. At the time, DataFusion was still in its infancy, developed primarily by its creator, Andy Grove, during his spare time.&lt;/p&gt;

&lt;p&gt;Creating a high-performance time series engine using well-known columnar and vectorization techniques was central to the InfluxDB 3 design. Such an engine requires significant knowledge and investment and had previously been available only to a small number of companies and elite research institutions. We believed that the combination of being written in Rust, an ASF project, and part of the Arrow ecosystem would attract other users to DataFusion, who would both benefit and help provide the engineering needed. That bet has paid off, with over 94 individuals contributing to the &lt;a href="https://github.com/apache/datafusion/blob/main/dev/changelog/44.0.0.md"&gt;most recent release&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;InfluxData wasn’t alone in recognizing DataFusion’s potential. Companies like &lt;a href="https://coralogix.com/"&gt;Coralogix&lt;/a&gt;, &lt;a href="https://greptime.com/"&gt;Greptime&lt;/a&gt;, and &lt;a href="https://www.synnada.ai/"&gt;Synnada&lt;/a&gt; also embraced DataFusion, betting that building on its foundation and contributing to its development would allow them to deliver better products more quickly and cost-effectively than doing it entirely by themselves.&lt;/p&gt;

&lt;p&gt;This collective investment helped grow DataFusion and its community while delivering tangible benefits to early adopters. While the journey came with challenges, the returns have been undeniably high.&lt;/p&gt;

&lt;p&gt;Today, in InfluxDB 3, every aspect of data processing flows through a DataFusion plan after &lt;a href="https://docs.influxdata.com/influxdb/cloud/reference/syntax/line-protocol/?utm_source=website&amp;amp;utm_medium=direct&amp;amp;utm_campaign=datafusion_2025_influxdb&amp;amp;utm_content=blog"&gt;Line Protocol&lt;/a&gt; parsing. This includes writing and compacting Apache Parquet files and executing SQL, InfluxQL, and Flux queries. Our multi-tenant production systems alone execute 10s of millions of DataFusion plans daily. Improvements from the broader DataFusion community flow directly into InfluxDB 3, with &lt;a href="https://github.com/apache/datafusion/issues/10826"&gt;many&lt;/a&gt; &lt;a href="https://github.com/apache/datafusion/issues/10258"&gt;of&lt;/a&gt; &lt;a href="https://github.com/apache/datafusion/issues/11408"&gt;our&lt;/a&gt; &lt;a href="https://github.com/apache/arrow-rs/issues/6206"&gt;bug&lt;/a&gt; &lt;a href="https://github.com/apache/datafusion/issues/11032"&gt;reports&lt;/a&gt; or &lt;a href="https://github.com/apache/datafusion/pull/12400#pullrequestreview-2297246575"&gt;SQL feature requests&lt;/a&gt; from customers resolved upstream by other contributors—requiring only a version upgrade.&lt;/p&gt;

&lt;h2 id="gaining-momentum"&gt;&lt;strong&gt;2023-2025: gaining momentum&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;Major companies with dedicated engineering teams are now building and deploying DataFusion-based systems across diverse contexts while contributing back to the project. This virtuous cycle has driven rapid innovation in performance and features, with adoption still in its early stages. The past two years have been a turning point, with engineers from leading tech companies such as Apple, eBay, Kuaishou, Airbnb, TikTok, Huawei, and Alibaba contributing significantly to DataFusion.&lt;/p&gt;

&lt;p&gt;A key milestone came last year when Apple developers built a replacement for Spark query execution using DataFusion, which they &lt;a href="https://arrow.apache.org/blog/2024/03/06/comet-donation/"&gt;donated to ASF&lt;/a&gt; and is now developed as &lt;a href="https://datafusion.apache.org/comet/"&gt;Apache DataFusion Comet&lt;/a&gt;. This not only demonstrated Apple’s confidence in DataFusion but also inspired additional contributions from the broader open source community, accelerating its growth.&lt;/p&gt;

&lt;h3 id="integration-into-the-open-data-lake"&gt;&lt;strong&gt;Integration into the Open Data Lake&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;In 2025, adoption of DataFusion is set to surge as the industry embraces &lt;a href="https://sympathetic.ink/2024/11/07/The-Advent-Of-The-Open-Data-Lake.html"&gt;Open Data Lake&lt;/a&gt; architectures. The data landscape is evolving into a constellation of specialized processing systems, each tailored for unique use cases, as illustrated in Figure 2. 
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/Eiywu2wyKPhGAm40IMeDg/0b9f3d30ba94b64d7634119369043e2c/01-a.png" alt="Figure 2" /&gt;
&lt;strong&gt;Figure 2&lt;/strong&gt;: Next-generation analytics: a constellation of different tools with a shared storage layer based on the open Apache Parquet and Apache Iceberg formats stored on Object Storage such as AWS S3, &lt;a href="https://cloud.google.com/storage"&gt;GCP Cloud Storage&lt;/a&gt;, and &lt;a href="https://azure.microsoft.com/en-us/products/storage/blobs"&gt;Azure Blob Storage&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;These systems will share the same underlying data stored in the &lt;a href="https://www.influxdata.com/glossary/apache-parquet/?utm_source=website&amp;amp;utm_medium=direct&amp;amp;utm_campaign=datafusion_2025_influxdb&amp;amp;utm_content=blog"&gt;Apache Parquet&lt;/a&gt; open format, organized by &lt;a href="https://iceberg.apache.org/"&gt;Apache Iceberg&lt;/a&gt;, and tailored to different use cases. Achieving high performance in this architecture requires advanced, vectorized analytic technology—an area where DataFusion excels due to its &lt;a href="https://www.apache.org/licenses/LICENSE-2.0"&gt;permissive licensing&lt;/a&gt;, &lt;a href="https://docs.rs/datafusion/latest/datafusion/index.html#design-goals"&gt;extensible design&lt;/a&gt;, and &lt;a href="https://datafusion.apache.org/blog/2024/11/18/datafusion-fastest-single-node-parquet-clickbench/#:~:text=RSS-,Apache%20DataFusion%20is%20now%20the%20fastest%20single,for%20querying%20Apache%20Parquet%20files&amp;amp;text=I%20am%20extremely%20excited%20to,Clickhouse%20using%20the%20same%20hardware"&gt;exceptional Parquet performance&lt;/a&gt;. The Rust-based implementations of &lt;a href="https://github.com/delta-io/delta-rs"&gt;Delta Lake&lt;/a&gt;, &lt;a href="https://github.com/apache/iceberg-rust"&gt;Apache Iceberg&lt;/a&gt;, and &lt;a href="https://github.com/apache/iceberg-rust"&gt;Apache Hudi&lt;/a&gt;, all built using DataFusion, highlight its central role in the shift toward open, modular data architectures.&lt;/p&gt;

&lt;p&gt;To support this proliferation, I expect significant additional investment from the DataFusion community to improve the technological underpinnings of querying in this new architecture. Efforts include &lt;a href="https://github.com/apache/datafusion/issues/13456"&gt;simplifying and accelerating remote file queries&lt;/a&gt; and exploring &lt;a href="https://blog.haoxp.xyz/posts/caching-datafusion/"&gt;advanced caching strategies&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id="streamlining-adoption-for-downstream-users"&gt;&lt;strong&gt;Streamlining Adoption for Downstream Users&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;Another major theme for investment in 2025 will be reducing friction for downstream users when adopting new versions of DataFusion. Recent efforts in DataFusion to complete projects such as &lt;a href="https://datafusion.apache.org/blog/2024/09/13/string-view-german-style-strings-part-1/"&gt;StringView&lt;/a&gt; and &lt;a href="https://github.com/apache/datafusion/issues/8709"&gt;Window Function Migration&lt;/a&gt; solidified its foundation, but the velocity of changes also caused &lt;a href="https://github.com/apache/datafusion/issues/13525"&gt;challenges downstream for some upgrades&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As the ecosystem grows, ensuring the smooth adoption of updates becomes increasingly critical. We are &lt;a href="https://github.com/apache/datafusion/issues/13648"&gt;discussing ways to improve this process&lt;/a&gt; as well as clarify the &lt;a href="https://github.com/apache/datafusion/issues/12357"&gt;criteria for adding new features/what belongs in core DataFusion&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;By balancing innovation with stability, the DataFusion community aims to maintain its rapid velocity of improvements while making it easier for users and contributors to keep pace.&lt;/p&gt;

&lt;h3 id="next-level-quality-bashing-pesky-bugs"&gt;&lt;strong&gt;Next Level Quality: Bashing Pesky Bugs&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;As DataFusion matures, users tend to:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Expect more concerning the breadth and depth of functionality (e.g., SQL and type support)&lt;/li&gt;
  &lt;li&gt;Run increasingly complicated queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These trends naturally expose feature gaps and bugs. For example, given that InfluxDB 3 executes tens of millions of DataFusion plans per day on InfluxData production systems, we find occasional and increasingly &lt;a href="https://github.com/apache/datafusion/issues/13748"&gt;esoteric issues&lt;/a&gt; that we report and help fix.&lt;/p&gt;

&lt;p&gt;This “hardening” phase is a natural step for any successful software on its path to maturity and widespread adoption. While fixing these bugs can be tedious, it is a straightforward task requiring focused engineering effort. I am confident in our community’s ability to drive up the quality level.&lt;/p&gt;

&lt;p&gt;DataFusion already benefits from &lt;a href="https://datafusion.apache.org/contributor-guide/testing.html"&gt;extensive test coverage&lt;/a&gt;, and I predict we will see additional focus on automated industrial testing. Examples include &lt;a href="https://www.linkedin.com/in/bruceritchie"&gt;Bruce Ritchie’s&lt;/a&gt; work on &lt;a href="https://github.com/apache/datafusion/issues/13811"&gt;running DataFusion on the SQLite test corpus&lt;/a&gt; and &lt;a href="https://github.com/2010YOUY01"&gt;Yongting You’s&lt;/a&gt; efforts to &lt;a href="https://github.com/apache/datafusion/issues/11030"&gt;run SQLancer&lt;/a&gt; on Datafusion. InfluxData plans to contribute significantly to this area as well, and I hope other companies using DataFusion will do the same.&lt;/p&gt;

&lt;h3 id="pushing-the-limits-of-performance"&gt;&lt;strong&gt;Pushing the Limits of Performance&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;One of DataFusion’s core principles is world-class performance: applications built on DataFusion can focus mostly on their specific features and take advantage of DataFusion’s performance (like &lt;a href="https://llvm.org/"&gt;LLVM&lt;/a&gt;, my favorite, though very geeky, analogy).&lt;/p&gt;

&lt;p&gt;DataFusion already has optimized most “low-hanging fruit,” so continued performance improvements require careful and focused engineering. We continue to see performance projects such as &lt;a href="https://github.com/apache/datafusion/issues/12680"&gt;vectorized group keys&lt;/a&gt; and &lt;a href="https://github.com/apache/datafusion/pull/12978"&gt;improved pruning&lt;/a&gt;, but the quality bar gets higher. We will need continued, ongoing help from the community to find, implement, evaluate, and verify these improvements.&lt;/p&gt;

&lt;p&gt;I am particularly excited about the possibility of working with academic groups—there is a wealth of talent and focused time for low-level performance optimization among PhD students. Additional collaboration can accelerate the adoption of students’ work into real-world systems and make DataFusion faster, and I am excited to help make it happen.&lt;/p&gt;

&lt;h2 id="the-year-ahead"&gt;&lt;strong&gt;The year ahead&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;2025 will be very exciting as more DataFusion-based systems hit the market, solidifying its place as a foundational building block for analytic and data platforms. The future of the data stack is composable, and DataFusion will be one key component. While challenges are inevitable, the community (and I) will focus on driving it forward as fast as possible while maintaining a stable foundation, leading to a thriving ecosystem.&lt;/p&gt;

&lt;p&gt;I’ll close with my usual appeal (aka 🎣 attempt): DataFusion is an open source project driven by &lt;a href="https://datafusion.apache.org/contributor-guide/index.html#open-contribution-and-assigning-tickets"&gt;open contributions&lt;/a&gt;. We welcome and encourage &lt;em&gt;contributions from everyone&lt;/em&gt;. Review capacity remains our most limited, but impactful resource, and I encourage companies and individuals to dedicate time reviewing code, testing proposals, and helping maintain the project.&lt;/p&gt;

&lt;p&gt;Finally, I want to express my gratitude to InfluxData. It was InfluxData’s vision and early recognition of DataFusion’s potential that introduced me to the project and supported my contributions over the past 4.5 years. This has allowed me to engage deeply—reviewing &lt;a href="https://github.com/alamb"&gt;countless PRs&lt;/a&gt;, contributing more features (both directly and indirectly related to InfluxDB 3), writing &lt;a href="https://www.influxdata.com/blog/flight-datafusion-arrow-parquet-fdap-architecture-influxdb/?utm_source=website&amp;amp;utm_medium=direct&amp;amp;utm_campaign=datafusion_2025_influxdb&amp;amp;utm_content=blog"&gt;many&lt;/a&gt; &lt;a href="https://www.influxdata.com/blog/aggregating-millions-groups-fast-apache-arrow-datafusion/?utm_source=website&amp;amp;utm_medium=direct&amp;amp;utm_campaign=datafusion_2025_influxdb&amp;amp;utm_content=blog"&gt;blog&lt;/a&gt; &lt;a href="https://www.influxdata.com/blog/faster-queries-with-stringview-part-one-influxdb/?utm_source=website&amp;amp;utm_medium=direct&amp;amp;utm_campaign=datafusion_2025_influxdb&amp;amp;utm_content=blog"&gt;posts&lt;/a&gt;, traveling for meetups, and supporting my role as the project’s PMC.&lt;/p&gt;

&lt;p&gt;2025 will be a pivotal year for DataFusion, and I look forward to seeing the innovation this community will drive.&lt;/p&gt;

&lt;p&gt;&lt;sup&gt;[1]&lt;/sup&gt; The numbers on those lists seem modest, but they only include people who have written publicly about their use. I know of many internal projects/data systems not listed that also use DataFusion.&lt;/p&gt;

&lt;p&gt;&lt;sup&gt;[2]&lt;/sup&gt; I am a database internals developer, after all. How cool is that!!&lt;/p&gt;
</description>
      <pubDate>Wed, 08 Jan 2025 07:00:00 +0000</pubDate>
      <link>https://www.influxdata.com/blog/datafusion-2025-influxdb/</link>
      <guid isPermaLink="true">https://www.influxdata.com/blog/datafusion-2025-influxdb/</guid>
      <category>Developer</category>
      <author>Andrew Lamb (InfluxData)</author>
    </item>
    <item>
      <title>​Apache DataFusion Meetup: Chicago December 2024 Recap</title>
      <description>&lt;p&gt;This past week, I attended and spoke at the &lt;a href="https://datafusion.apache.org/"&gt;Apache DataFusion&lt;/a&gt; &lt;a href="https://lu.ma/eq5myc5i"&gt;Meetup&lt;/a&gt; in Chicago, Illinois. Inspired by &lt;a href="https://www.linkedin.com/in/samican-tandogdu"&gt;Sami Tandogdu&lt;/a&gt;&lt;a href="https://www.linkedin.com/in/samican-tandogdu"&gt;’s&lt;/a&gt; (Synnada) great &lt;a href="https://github.com/apache/datafusion/discussions/11431#discussioncomment-10832070"&gt;recap of the DataFusion Belgrade meetup&lt;/a&gt;, I figured I would try it myself.&lt;/p&gt;

&lt;p&gt;First of all, huge thanks to &lt;a href="https://1871.com"&gt;1871&lt;/a&gt;, &lt;a href="https://pydantic.dev/"&gt;Pydantic&lt;/a&gt;, and (of course) InfluxData for sponsoring the event; to Adrian who did much of the work organizing; and to Xiangpeng and Adrian for some of these pictures. 
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/7f45b4796dad4b44ad936157828123c3/1a6663e18426bcb3e89c49c97805c163/unnamed.jpg" alt="" /&gt;
Around 25 DataFusion enthusiasts attended, learned from talks hosted by project contributors, and discussed ideas for the future. The meetup felt somewhat unique as almost all attendees were using DataFusion in their products or projects. This led to some great discussions and a visceral feeling that the adoption of DataFusion is increasing. Below is a summary of the four featured talks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Building a Real-Time Data Lake with DataFusion”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.linkedin.com/in/adrian-garcia-badaracco/"&gt;Adrian Garcia Badaracco&lt;/a&gt; - Founding Engineer, Pydantic 
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/4f0360abb3a649ac91143d610307c4d6/4e027dc0cff91b21ff7506a1652d4b9d/unnamed.jpg" alt="" /&gt;
First up was Adrian, a founding engineer at Pydantic. His  team is building the database for &lt;a href="https://pydantic.dev/logfire"&gt;pydantic LogFire&lt;/a&gt;, an observability platform. Adrian gave an overview of how Pydantic uses DataFusion to build a near real-time data lake for observability data and some details of their indexing and metadata store. &lt;a href="https://youtu.be/XxEtOf-MzNA?si=CHSqzeRr1Sh7ZpD-"&gt;VIDEO&lt;/a&gt; / &lt;a href="https://docs.google.com/presentation/d/1BWuVyGzF1_iQRcmZDfoDg4V5kVlefw1fDGRt7TqxYsU/edit"&gt;SLIDES&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;”Practical Data Science in Robotics Using DataFusion”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.linkedin.com/in/timsaucer/"&gt;Tim Saucer&lt;/a&gt; - Director of Simulation &amp;amp; Infrastructure, &lt;a href="https://maymobility.com/?"&gt;May Mobility&lt;/a&gt;
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/9da08785e3b44833a24ba2844db288fa/9baf84dce90db239166d03a948674cc8/unnamed.jpg" alt="" /&gt;
Next up was Tim Saucer, a contributor and &lt;a href="https://datafusion.apache.org/contributor-guide/governance.html#roles"&gt;committer&lt;/a&gt; on DataFusion, who focused on the Python bindings. Tim spoke about data science in robotics and how DataFusion can be used to address some of the challenges particular to that field. &lt;a href="https://youtu.be/CsqWOxFWK9w?si=AsCUDxDWbKtBoA_F"&gt;VIDEO&lt;/a&gt; / &lt;a href="https://timsaucer.com/data/Practical_Robotics_with_DataFusion.pdf"&gt;SLIDES&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Practical Disaggregated Cache for DataFusion”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Xiangpeng Hao (&lt;a href="https://github.com/XiangpengHao"&gt;@XiangpengHao&lt;/a&gt;) - PhD Student, UW Madison
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/39cb285b68c241309fd2ee001fcf943b/8862112aaf377e0bb390f0dd18acdede/unnamed.jpg" alt="" /&gt;
The next speaker was Xiangpeng Hao, a fourth-year PhD student at the University of Wisconsin-Madison, studying and building database and storage systems. He spoke about his work building SplitSQL, a disaggregated cache for modern data analytics also built on DataFusion. He was a former intern at InfluxData and, in that role, contributed heavily to the &lt;a href="https://www.influxdata.com/blog/faster-queries-with-stringview-part-one-influxdb/"&gt;StringView integration in Apache DataFusion&lt;/a&gt; and Parquet Metadata. &lt;a href="https://youtu.be/woqXLE25gMc?si=1Irx1i70qjmujBj3"&gt;VIDEO&lt;/a&gt; / &lt;a href="https://github.com/user-attachments/files/18214371/datafusion-cache.pdf"&gt;SLIDES&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Building InfluxDB 3 with the FDAP Stack”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Andrew Lamb (&lt;a href="https://github.com/alamb"&gt;@alamb&lt;/a&gt;) - Staff Engineer, DataFusion, PMC chair, InfluxData 
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/2f22b33d182343e4beec6e791c91a81b/56994a92242832876051db844c978587/unnamed.jpg" alt="" /&gt;
Finally, it was my turn to speak about the rationale for why and how we built InfluxDB 3 using the FDAP stack, with a focus on the DataFusion aspects. Sorry for the somewhat goofy picture and the fact I forgot to turn on the microphone for the recording. &lt;a href="https://youtu.be/-1D25zAfB00?si=QY5EB5x9kKXGDVus"&gt;VIDEO&lt;/a&gt; (no sound 🤦 ) / &lt;a href="https://docs.google.com/presentation/d/1AiI5r8LtnbDkceBW1NSlqBmD07xxq1JJWQFkRZ35Ef8/edit#slide=id.g2e79518841d_0_95"&gt;SLIDES&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In addition to the speakers, it was great to meet &lt;a href="https://www.linkedin.com/in/alexwilcoxson/"&gt;Alex Wilcoxson&lt;/a&gt;, Michael Maletich, and others from &lt;a href="https://www.relativity.com/"&gt;Relativity Software&lt;/a&gt;, who are building a document discovery platform using DataFusion and &lt;a href="https://www.linkedin.com/in/michael-ward-5377859/"&gt;Michael Ward&lt;/a&gt; of DataFusion-Python fame. Also present were &lt;a href="https://www.linkedin.com/in/camuel/"&gt;Camuel Gilyadov&lt;/a&gt; and &lt;a href="https://www.linkedin.com/in/sergei-turukin/"&gt;Sergei Turukin&lt;/a&gt; from &lt;a href="https://www.embucket.com/"&gt;Embucket&lt;/a&gt;, who are working on a new DataFusion-powered project and &lt;a href="https://www.linkedin.com/in/devan-benz-b03a8894/"&gt;Devan Benz&lt;/a&gt;, a fellow Influxer working on database internals. After lunch, we had some informal conversations about topics such as the future of the project, building secondary indexes, performance, and the DataFusion-Python roadmap.&lt;/p&gt;

&lt;p&gt;While running around meeting other users is somewhat exhausting, I think it is important during this stage of the project’s growth. As its adoption takes off, building a community that can sustain the project over the long term is more important than ever, and I am very excited, as always, to be a part of that.&lt;/p&gt;
</description>
      <pubDate>Mon, 06 Jan 2025 07:00:00 +0000</pubDate>
      <link>https://www.influxdata.com/blog/apache-datafusion-meetup-recap-2024-influxdb/</link>
      <guid isPermaLink="true">https://www.influxdata.com/blog/apache-datafusion-meetup-recap-2024-influxdb/</guid>
      <category>Developer</category>
      <author>Andrew Lamb (InfluxData)</author>
    </item>
    <item>
      <title>Apache DataFusion is Now the Fastest Single Node Engine for Querying Apache Parquet Files</title>
      <description>&lt;p&gt;&lt;em&gt;This blog was originally published on &lt;a href="https://datafusion.apache.org/blog/2024/11/18/datafusion-fastest-single-node-parquet-clickbench/"&gt;Apache DataFusion Project News &amp;amp; Blog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I am extremely excited to announce that &lt;a href="https://datafusion.apache.org/"&gt;Apache DataFusion&lt;/a&gt;  &lt;a href="https://crates.io/crates/datafusion"&gt;43.0.0&lt;/a&gt;  is the fastest engine for querying Apache Parquet files in &lt;a href="https://benchmark.clickhouse.com/"&gt;ClickBench&lt;/a&gt;. It is faster than both &lt;a href="https://duckdb.org/"&gt;DuckDB&lt;/a&gt; and &lt;a href="https://clickhouse.com/chdb"&gt;chDB&lt;/a&gt;/&lt;a href="https://clickhouse.com/"&gt;Clickhouse&lt;/a&gt; using the same hardware. It also marks the first time a &lt;a href="https://www.rust-lang.org/"&gt;Rust&lt;/a&gt; based engine holds the top spot, which has previously been held by traditional C/C++ based engines. 
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/36c1a65a1a1f4db0ba64ba756d653290/adac1cbf6a10ed51a35403030af030d0/unnamed.png" alt="" /&gt;
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/c00c85a41fa04466b4a9c25f6b6b3e2a/fc899e5c6e5afb32bd1ed58c60022076/unnamed.png" alt="" /&gt;
&lt;strong&gt;Figure 1&lt;/strong&gt;: 2024-11-16 &lt;a href="https://benchmark.clickhouse.com/#eyJzeXN0ZW0iOnsiQWxsb3lEQiI6ZmFsc2UsIkFsbG95REIgKHR1bmVkKSI6ZmFsc2UsIkF0aGVuYSAocGFydGl0aW9uZWQpIjpmYWxzZSwiQXRoZW5hIChzaW5nbGUpIjpmYWxzZSwiQXVyb3JhIGZvciBNeVNRTCI6ZmFsc2UsIkF1cm9yYSBmb3IgUG9zdGdyZVNRTCI6ZmFsc2UsIkJ5Q29uaXR5IjpmYWxzZSwiQnl0ZUhvdXNlIjpmYWxzZSwiY2hEQiAoRGF0YUZyYW1lKSI6ZmFsc2UsImNoREIgKFBhcnF1ZXQsIHBhcnRpdGlvbmVkKSI6dHJ1ZSwiY2hEQiI6ZmFsc2UsIkNpdHVzIjpmYWxzZSwiQ2xpY2tIb3VzZSBDbG91ZCAoYXdzKSI6ZmFsc2UsIkNsaWNrSG91c2UgQ2xvdWQgKGF6dXJlKSI6ZmFsc2UsIkNsaWNrSG91c2UgQ2xvdWQgKGdjcCkiOmZhbHNlLCJDbGlja0hvdXNlIChkYXRhIGxha2UsIHBhcnRpdGlvbmVkKSI6ZmFsc2UsIkNsaWNrSG91c2UgKGRhdGEgbGFrZSwgc2luZ2xlKSI6ZmFsc2UsIkNsaWNrSG91c2UgKFBhcnF1ZXQsIHBhcnRpdGlvbmVkKSI6dHJ1ZSwiQ2xpY2tIb3VzZSAoUGFycXVldCwgc2luZ2xlKSI6ZmFsc2UsIkNsaWNrSG91c2UgKHdlYikiOmZhbHNlLCJDbGlja0hvdXNlIjpmYWxzZSwiQ2xpY2tIb3VzZSAodHVuZWQpIjpmYWxzZSwiQ2xpY2tIb3VzZSAodHVuZWQsIG1lbW9yeSkiOmZhbHNlLCJDbG91ZGJlcnJ5IjpmYWxzZSwiQ3JhdGVEQiI6ZmFsc2UsIkNydW5jaHkgQnJpZGdlIGZvciBBbmFseXRpY3MgKFBhcnF1ZXQpIjpmYWxzZSwiRGF0YWJlbmQiOmZhbHNlLCJEYXRhRnVzaW9uIChQYXJxdWV0LCBwYXJ0aXRpb25lZCkiOnRydWUsIkRhdGFGdXNpb24gKFBhcnF1ZXQsIHNpbmdsZSkiOmZhbHNlLCJBcGFjaGUgRG9yaXMiOmZhbHNlLCJEcnVpZCI6ZmFsc2UsIkR1Y2tEQiAoRGF0YUZyYW1lKSI6ZmFsc2UsIkR1Y2tEQiAoUGFycXVldCwgcGFydGl0aW9uZWQpIjp0cnVlLCJEdWNrREIiOmZhbHNlLCJFbGFzdGljc2VhcmNoIjpmYWxzZSwiRWxhc3RpY3NlYXJjaCAodHVuZWQpIjpmYWxzZSwiR2xhcmVEQiI6ZmFsc2UsIkdyZWVucGx1bSI6ZmFsc2UsIkhlYXZ5QUkiOmZhbHNlLCJIeWRyYSI6ZmFsc2UsIkluZm9icmlnaHQiOmZhbHNlLCJLaW5ldGljYSI6ZmFsc2UsIk1hcmlhREIgQ29sdW1uU3RvcmUiOmZhbHNlLCJNYXJpYURCIjpmYWxzZSwiTW9uZXREQiI6ZmFsc2UsIk1vbmdvREIiOmZhbHNlLCJNb3RoZXJEdWNrIjpmYWxzZSwiTXlTUUwgKE15SVNBTSkiOmZhbHNlLCJNeVNRTCI6ZmFsc2UsIk94bGEiOmZhbHNlLCJQYW5kYXMgKERhdGFGcmFtZSkiOmZhbHNlLCJQYXJhZGVEQiAoUGFycXVldCwgcGFydGl0aW9uZWQpIjp0cnVlLCJQYXJhZGVEQiAoUGFycXVldCwgc2luZ2xlKSI6ZmFsc2UsIlBpbm90IjpmYWxzZSwiUG9sYXJzIChEYXRhRnJhbWUpIjpmYWxzZSwiUG9zdGdyZVNRTCAodHVuZWQpIjpmYWxzZSwiUG9zdGdyZVNRTCI6ZmFsc2UsIlF1ZXN0REIgKHBhcnRpdGlvbmVkKSI6ZmFsc2UsIlF1ZXN0REIiOmZhbHNlLCJSZWRzaGlmdCI6ZmFsc2UsIlNpbmdsZVN0b3JlIjpmYWxzZSwiU25vd2ZsYWtlIjpmYWxzZSwiU1FMaXRlIjpmYWxzZSwiU3RhclJvY2tzIjpmYWxzZSwiVGFibGVzcGFjZSI6ZmFsc2UsIlRlbWJvIE9MQVAgKGNvbHVtbmFyKSI6ZmFsc2UsIlRpbWVzY2FsZURCIChubyBjb2x1bW5zdG9yZSkiOmZhbHNlLCJUaW1lc2NhbGVEQiI6ZmFsc2UsIlRpbnliaXJkIChGcmVlIFRyaWFsKSI6ZmFsc2UsIlVtYnJhIjpmYWxzZX0sInR5cGUiOnsiQyI6dHJ1ZSwiY29sdW1uLW9yaWVudGVkIjp0cnVlLCJQb3N0Z3JlU1FMIGNvbXBhdGlibGUiOnRydWUsIm1hbmFnZWQiOnRydWUsImdjcCI6dHJ1ZSwic3RhdGVsZXNzIjp0cnVlLCJKYXZhIjp0cnVlLCJDKysiOnRydWUsIk15U1FMIGNvbXBhdGlibGUiOnRydWUsInJvdy1vcmllbnRlZCI6dHJ1ZSwiQ2xpY2tIb3VzZSBkZXJpdmF0aXZlIjp0cnVlLCJlbWJlZGRlZCI6dHJ1ZSwic2VydmVybGVzcyI6dHJ1ZSwiZGF0YWZyYW1lIjp0cnVlLCJhd3MiOnRydWUsImF6dXJlIjp0cnVlLCJhbmFseXRpY2FsIjp0cnVlLCJSdXN0Ijp0cnVlLCJzZWFyY2giOnRydWUsImRvY3VtZW50Ijp0cnVlLCJzb21ld2hhdCBQb3N0Z3JlU1FMIGNvbXBhdGlibGUiOnRydWUsInRpbWUtc2VyaWVzIjp0cnVlfSwibWFjaGluZSI6eyIxNiB2Q1BVIDEyOEdCIjp0cnVlLCI4IHZDUFUgNjRHQiI6dHJ1ZSwic2VydmVybGVzcyI6dHJ1ZSwiMTZhY3UiOnRydWUsImM2YS40eGxhcmdlLCA1MDBnYiBncDIiOnRydWUsIkwiOnRydWUsIk0iOnRydWUsIlMiOnRydWUsIlhTIjp0cnVlLCJjNmEubWV0YWwsIDUwMGdiIGdwMiI6ZmFsc2UsIjE5MkdCIjp0cnVlLCIyNEdCIjp0cnVlLCIzNjBHQiI6dHJ1ZSwiNDhHQiI6dHJ1ZSwiNzIwR0IiOnRydWUsIjk2R0IiOnRydWUsImRldiI6dHJ1ZSwiNzA4R0IiOnRydWUsImM1bi40eGxhcmdlLCA1MDBnYiBncDIiOnRydWUsIkFuYWx5dGljcy0yNTZHQiAoNjQgdkNvcmVzLCAyNTYgR0IpIjp0cnVlLCJjNS40eGxhcmdlLCA1MDBnYiBncDIiOnRydWUsImM2YS40eGxhcmdlLCAxNTAwZ2IgZ3AyIjp0cnVlLCJjbG91ZCI6dHJ1ZSwiZGMyLjh4bGFyZ2UiOnRydWUsInJhMy4xNnhsYXJnZSI6dHJ1ZSwicmEzLjR4bGFyZ2UiOnRydWUsInJhMy54bHBsdXMiOnRydWUsIlMyIjp0cnVlLCJTMjQiOnRydWUsIjJYTCI6dHJ1ZSwiM1hMIjp0cnVlLCI0WEwiOnRydWUsIlhMIjp0cnVlLCJMMSAtIDE2Q1BVIDMyR0IiOnRydWUsImM2YS40eGxhcmdlLCA1MDBnYiBncDMiOnRydWV9LCJjbHVzdGVyX3NpemUiOnsiMSI6dHJ1ZSwiMiI6dHJ1ZSwiNCI6dHJ1ZSwiOCI6dHJ1ZSwiMTYiOnRydWUsIjMyIjp0cnVlLCI2NCI6dHJ1ZSwiMTI4Ijp0cnVlLCJzZXJ2ZXJsZXNzIjp0cnVlfSwibWV0cmljIjoiaG90IiwicXVlcmllcyI6W3RydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWVdfQ=="&gt;ClickBench Results&lt;/a&gt; for the  ‘hot’&lt;sup&gt;1&lt;/sup&gt; run against the partitioned 14 GB Parquet dataset (100 files, each ~140MB) on a &lt;code class="language-markup"&gt;c6a.4xlarge&lt;/code&gt; (16 CPU/32 GB  RAM) machine. Measurements are relative (&lt;code class="language-markup"&gt;1.x&lt;/code&gt;) to results using different hardware.
&lt;br /&gt;
&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Best in class performance on Parquet is now available to anyone. DataFusion’s open design lets you start quickly with a full-featured Query Engine, including SQL, data formats, catalogs, and more, and then customize any behavior you need. I predict the continued emergence of new classes of data systems now that creators can focus the bulk of their innovation on areas such as query languages, system integrations, and data formats rather than trying to play catchup with core engine performance.&lt;/p&gt;

&lt;p&gt;ClickBench also includes results for proprietary storage formats, which require costly load/export steps, making them useful in fewer use cases and thus much less important than open formats (though the idea of use case specific formats is interesting&lt;sup&gt;2&lt;/sup&gt;).&lt;/p&gt;

&lt;p&gt;This blog post highlights some of the techniques we used to achieve this performance, and celebrates the teamwork involved.&lt;/p&gt;

&lt;h2 id="a-strong-history-of-performance-improvements"&gt;A strong history of performance improvements&lt;/h2&gt;

&lt;p&gt;Performance has long been a core focus for DataFusion’s community, and speed attracts users and contributors. Recently, we seem to have been even more focused on performance, including in July, 2024 when &lt;a href="https://www.linkedin.com/in/mehmet-ozan-kabak/"&gt;Mehmet Ozan Kabak&lt;/a&gt;, CEO of &lt;a href="https://www.synnada.ai/"&gt;Synnada&lt;/a&gt;, again &lt;a href="https://github.com/apache/datafusion/issues/11442#issuecomment-2226834443"&gt;suggested focusing on performance&lt;/a&gt;. This got many of us excited (who doesn’t love a challenge!), and we have subsequently rallied to steadily improve the performance release on release as shown in Figure 2.
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/1b983da404424052b17a8b47291ab744/095e2c317a228d6f5f8e0ce99836df3e/unnamed.png" alt="" /&gt;
&lt;strong&gt;Figure 2&lt;/strong&gt;: ClickBench performance improved over 30% between DataFusion 34 (released Dec 2023) and DataFusion 43 (released Nov 2024). 
&lt;br /&gt;
&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Like all good optimization efforts, ours took sustained effort as DataFusion ran out &lt;a href="https://www.influxdata.com/blog/aggregating-millions-groups-fast-apache-arrow-datafusion/"&gt;single 2x performance improvements&lt;/a&gt; several years ago. Working together our community of engineers from around the world&lt;sup&gt;3&lt;/sup&gt; and all experience levels&lt;sup&gt;4&lt;/sup&gt; pulled it off (check out &lt;a href="https://github.com/apache/datafusion/issues/12821"&gt;this discussion&lt;/a&gt; to get a sense). It may be a &lt;a href="https://db.cs.cmu.edu/seminar2024/"&gt;“hobo sandwich”&lt;/a&gt;&lt;sup&gt;5&lt;/sup&gt; but it is a tasty one!&lt;/p&gt;

&lt;p&gt;Of course, most of these techniques have been implemented and described before, but until now they were only available in proprietary systems such as &lt;a href="https://www.vertica.com/"&gt;Vertica&lt;/a&gt;, &lt;a href="https://www.databricks.com/product/photon"&gt;DataBricks Photon&lt;/a&gt;, or &lt;a href="https://www.snowflake.com/en/"&gt;Snowflake&lt;/a&gt; or in tightly integrated open source systems such as &lt;a href="https://duckdb.org/"&gt;DuckDB&lt;/a&gt; or &lt;a href="https://clickhouse.com/"&gt;ClickHouse&lt;/a&gt; which were not designed to extend.&lt;/p&gt;

&lt;h3 id="stringview"&gt;StringView&lt;/h3&gt;

&lt;p&gt;Performance improved for all queries when DataFusion switched to using Arrow &lt;code class="language-markup"&gt;StringView&lt;/code&gt;. Using &lt;code class="language-markup"&gt;StringView&lt;/code&gt; “just” saves some copies and avoids one memory access for certain comparisons. However, these copies and comparisons happen to occur in many of the hottest loops during query processing, so optimizing them resulted in noticeable performance improvements.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/3f036fe937874c7fb7610a965471ee6a/ff20c6448250d0f769b277e9033492e0/unnamed.png" alt="" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 3:&lt;/strong&gt; Figure from &lt;a href="https://www.influxdata.com/blog/faster-queries-with-stringview-part-one-influxdb/"&gt;Using StringView / German Style Strings to Make Queries Faster: Part 1&lt;/a&gt; showing how StringView saves copying data in many cases. 
&lt;br /&gt;
&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Using StringView to make DataFusion faster for ClickBench required substantial careful, low level optimization work described in &lt;a href="https://www.influxdata.com/blog/faster-queries-with-stringview-part-one-influxdb/"&gt;Using StringView / German Style Strings to Make Queries Faster: Part 1&lt;/a&gt; and &lt;a href="https://www.influxdata.com/blog/faster-queries-with-stringview-part-two-influxdb/"&gt;Part 2&lt;/a&gt;. However, it &lt;em&gt;also&lt;/em&gt; required extending the rest of DataFusion’s operations to support the new type. You can get a sense of the magnitude of the work required by looking at the 100+ pull requests linked to the epic in arrow-rs (&lt;a href="https://github.com/apache/arrow-rs/issues/5374"&gt;here&lt;/a&gt;) and three major epics (&lt;a href="https://github.com/apache/datafusion/issues/10918"&gt;here&lt;/a&gt;, &lt;a href="https://github.com/apache/datafusion/issues/11790"&gt;here&lt;/a&gt;, and &lt;a href="https://github.com/apache/datafusion/issues/11752"&gt;here&lt;/a&gt;) in DataFusion.&lt;/p&gt;

&lt;p&gt;Here is a partial list of people involved in the project (I am sorry to those I forgot):&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Arrow&lt;/strong&gt;:  &lt;a href="https://github.com/XiangpengHao"&gt;Xiangpeng Hao&lt;/a&gt; (InfluxData’s amazing 2024 summer intern and UW Madison PhD), &lt;a href="https://github.com/ariesdevil"&gt;Yijun Zhao&lt;/a&gt; from DataBend Labs, and &lt;a href="https://github.com/tustvold"&gt;Raphael Taylor-Davies&lt;/a&gt; laid the foundation. &lt;a href="https://github.com/RinChanNOWWW"&gt;RinChanNOW&lt;/a&gt; from Tencent and &lt;a href="https://github.com/a10y"&gt;Andrew Duffy&lt;/a&gt; from SpiralDB helped push it along in the early days, and &lt;a href="https://github.com/viirya"&gt;Liang-Chi Hsieh&lt;/a&gt;, &lt;a href="https://github.com/Dandandan"&gt;Daniël Heres&lt;/a&gt; reviewed and provided guidance.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;DataFusion&lt;/strong&gt;:  &lt;a href="https://github.com/XiangpengHao"&gt;Xiangpeng Hao&lt;/a&gt;, again charted the initial path and &lt;a href="https://github.com/Weijun-H"&gt;Alex Huang&lt;/a&gt;, &lt;a href="https://github.com/dharanad"&gt;Dharan Aditya&lt;/a&gt; &lt;a href="https://github.com/Lordworms"&gt;Lordworms&lt;/a&gt;, &lt;a href="https://github.com/goldmedal"&gt;Jax Liu&lt;/a&gt;, &lt;a href="https://github.com/wiedld"&gt;wiedld&lt;/a&gt;, &lt;a href="https://github.com/tlm365"&gt;Tai Le Manh&lt;/a&gt;, &lt;a href="https://github.com/my-vegetable-has-exploded"&gt;yi wang&lt;/a&gt;, &lt;a href="https://github.com/doupache"&gt;doupache&lt;/a&gt;, &lt;a href="https://github.com/jayzhan211"&gt;Jay Zhan&lt;/a&gt;, &lt;a href="https://github.com/xinlifoobar"&gt;Xin Li&lt;/a&gt;, and &lt;a href="https://github.com/Kev1n8"&gt;Kaifeng Zheng&lt;/a&gt; made it real.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;DataFusion String Function Migration&lt;/strong&gt;:  &lt;a href="https://github.com/tshauck"&gt;Trent Hauck&lt;/a&gt; organized the effort and set the patterns, &lt;a href="https://github.com/goldmedal"&gt;Jax Liu&lt;/a&gt; made a clever testing framework, and &lt;a href="https://github.com/austin362667"&gt;Austin Liu&lt;/a&gt;, &lt;a href="https://github.com/demetribu"&gt;Dmitrii Bu&lt;/a&gt;, &lt;a href="https://github.com/tlm365"&gt;Tai Le Manh&lt;/a&gt;, &lt;a href="https://github.com/PsiACE"&gt;Chojan Shang&lt;/a&gt;, &lt;a href="https://github.com/devanbenz"&gt;WeblWabl&lt;/a&gt;, &lt;a href="https://github.com/Lordworms"&gt;Lordworms&lt;/a&gt;, &lt;a href="https://github.com/thinh2"&gt;iamthinh&lt;/a&gt;, &lt;a href="https://github.com/Omega359"&gt;Bruce Ritchie&lt;/a&gt; , &lt;a href="https://github.com/Kev1n8"&gt;Kaifeng Zheng&lt;/a&gt;, and &lt;a href="https://github.com/xinlifoobar"&gt;Xin Li&lt;/a&gt; bashed out the conversions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id="parquet"&gt;Parquet&lt;/h3&gt;

&lt;p&gt;Part of DataFusion’s speed in ClickBench is reading Parquet files (really) quickly, which reflects invested effort in the Parquet reading system (&lt;a href="https://www.influxdata.com/blog/querying-parquet-millisecond-latency/"&gt;see Querying Parquet with Millisecond Latency&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.rs/datafusion/latest/datafusion/datasource/physical_plan/parquet/struct.ParquetExec.html"&gt;DataFusion ParquetExec&lt;/a&gt; (built on &lt;a href="https://crates.io/crates/parquet"&gt;Rust Parquet&lt;/a&gt;) is now the most sophisticated open source Parquet reader I know of. It has every optimization we can think of for reading Parquet, including projection pushdown, predicate pushdown (row group metadata, page index, and bloom filters), limit pushdown, parallel reading, interleaved I/O, and late materialized filtering (coming soon by default). Some recent work from &lt;a href="https://github.com/itsjunetime"&gt;June&lt;/a&gt;  &lt;a href="https://github.com/apache/datafusion/pull/12135"&gt;recently unblocked a remaining hurdle&lt;/a&gt; for enabling late materialized filtering, and conveniently &lt;a href="https://github.com/XiangpengHao"&gt;Xiangpeng Hao&lt;/a&gt; is working on the &lt;a href="https://github.com/apache/arrow-datafusion/issues/3463"&gt;final piece&lt;/a&gt; (no pressure😅).&lt;/p&gt;

&lt;h3 id="skipping-partial-aggregation-when-it-doesnt-help"&gt;Skipping partial aggregation when it doesn’t help&lt;/h3&gt;

&lt;p&gt;Many ClickBench queries are aggregations that summarize millions of rows, a common task for reporting and dashboarding. DataFusion uses state of the art &lt;a href="https://docs.rs/datafusion/latest/datafusion/physical_plan/trait.Accumulator.html#tymethod.state"&gt;two phase aggregation&lt;/a&gt; plans. Normally, two phase aggregation works well as the first phase consolidates many rows immediately after reading, while the data is still in cache. However, for certain “high cardinality” aggregate queries (that have large numbers of groups), &lt;a href="https://github.com/apache/datafusion/issues/6937"&gt;the two phase aggregation strategy used in DataFusion was inefficient&lt;/a&gt;, manifesting in relatively slower performance compared to other engines for ClickBench queries such as&lt;/p&gt;

&lt;pre class=""&gt;&lt;code class="language-sql"&gt;SELECT "WatchID", "ClientIP", COUNT(*) AS c, SUM("IsRefresh"), AVG("ResolutionWidth") 
FROM hits 
GROUP BY "WatchID", "ClientIP" -- **** 13M distinct Groups  ****
ORDER BY c DESC 
LIMIT 10;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For such queries, the first first aggregation phase does not significantly reduce the number of rows, which wastes significant effort. &lt;a href="https://github.com/korowa"&gt;Eduard Karacharov&lt;/a&gt; solved this problem with a &lt;a href="https://github.com/apache/datafusion/pull/11627"&gt;dynamic strategy&lt;/a&gt; to bypass the first phase when it is not working efficiently, shown in Figure 4.
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/59fbiZTn9KWnnazzYcNkMH/46eac477f2d5a581388d38a838cf782c/skipping-partial-aggregation.png" alt="Figure 4" /&gt;
&lt;strong&gt;Figure 4&lt;/strong&gt;: Diagram from &lt;a href="https://docs.rs/datafusion/latest/datafusion/physical_plan/trait.Accumulator.html#tymethod.state"&gt;DataFusion API docs&lt;/a&gt; showing when the muti-phase grouping is not effective.&lt;/p&gt;

&lt;h3 id="optimized-multi-column-grouping"&gt;Optimized multi-column grouping&lt;/h3&gt;

&lt;p&gt;Another method for improving analytic database performance is specialized (aka highly optimized) versions of operations for different data types, which the system picks at runtime based on the query. Like other systems, DataFusion has specialized code for handling different types of group columns. For example, there is &lt;a href="https://github.com/apache/datafusion/blob/73507c307487708deb321e1ba4e0d302084ca27e/datafusion/physical-plan/src/aggregates/group_values/single_group_by/primitive.rs"&gt;special code&lt;/a&gt; that handles &lt;code class="language-markup"&gt;GROUP BY int_id&lt;/code&gt;  and different &lt;a href="https://github.com/apache/datafusion/blob/73507c307487708deb321e1ba4e0d302084ca27e/datafusion/physical-plan/src/aggregates/group_values/single_group_by/bytes.rs"&gt;specialized code&lt;/a&gt; that handles &lt;code class="language-markup"&gt;GROUP BY string_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When a query groups by multiple columns, it is tricker to apply this technique. For example &lt;code class="language-markup"&gt;GROUP BY string_id, int_id&lt;/code&gt; and &lt;code class="language-markup"&gt;GROUP BY int_id, string_id&lt;/code&gt; have different optimal structures, but it is not possible to include specialized versions for all possible combinations of group column types.&lt;/p&gt;

&lt;p&gt;DataFusion includes &lt;a href="https://github.com/apache/datafusion/blob/73507c307487708deb321e1ba4e0d302084ca27e/datafusion/physical-plan/src/aggregates/group_values/row.rs#L33-L39"&gt;a general Row based mechanism&lt;/a&gt; that works for any combination of column types, but this general mechanism copies each value twice as shown in Figure 5. The cost of this copy &lt;a href="https://github.com/apache/datafusion/issues/9403"&gt;is especially high for variable length strings and binary data&lt;/a&gt;.
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/71SnFPRebmOeHM6KEsy731/8a4f85077b3333aff3ab9692dc837af0/row-based-storage.png" alt="Figure 5" /&gt;
&lt;strong&gt;Figure 5&lt;/strong&gt;: Prior to DataFusion 43.0.0, queries with multiple group columns used Row based group storage and copied each group value twice. This copy consumes a substantial amount of the query time for queries with many distinct groups, such as several of the queries in ClickBench.
&lt;br /&gt;
&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Many optimizations in Databases boil down to simply avoiding copies, and this was no exception. The trick was to figure out how to avoid copies without causing per-column comparison overhead to dominate or complexity to get out of hand. In a great example of diligent and disciplined engineering, &lt;a href="http://jayzhan211"&gt;Jay Zhan&lt;/a&gt; tried &lt;a href="https://github.com/apache/datafusion/pull/10937"&gt;several&lt;/a&gt; &lt;a href="https://github.com/apache/datafusion/pull/10976"&gt;different&lt;/a&gt; approaches until arriving at the &lt;a href="https://github.com/apache/datafusion/pull/12269"&gt;one shipped in DataFusion 43.0.0&lt;/a&gt;, shown in Figure 6.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/4lFNUYxCJl7qjWI4L4PrcL/1807df43e659de337508bd47c614f412/column-based-storage.png" alt="Figure 6" /&gt;
&lt;strong&gt;Figure 6&lt;/strong&gt;: DataFusion 43.0.0’s new columnar group storage copies each group value exactly once, which is significantly faster when grouping by multiple columns.
&lt;br /&gt;
&lt;br /&gt;&lt;/p&gt;

&lt;p&gt;Huge thanks as well to &lt;a href="https://github.com/eejbyfeldt"&gt;Emil Ejbyfeldt&lt;/a&gt; and &lt;a href="https://github.com/Dandandan"&gt;Daniël Heres&lt;/a&gt; for their help reviewing and to &lt;a href="https://github.com/Rachelint"&gt;Rachelint (kamille&lt;/a&gt;) for reviewing and contributing a faster &lt;a href="https://github.com/apache/datafusion/pull/12996"&gt;vectorized append and compare for multi group&lt;/a&gt; which will be released in DataFusion 44. The discussion on &lt;a href="https://github.com/apache/datafusion/issues/9403"&gt;the ticket&lt;/a&gt; is another great example of the power of the DataFusion community working together to build great software.&lt;/p&gt;

&lt;h2 id="whats-next-"&gt;What’s next 🚀&lt;/h2&gt;

&lt;p&gt;Just as I expect the performance of other engines to improve, DataFusion has several more performance improvements lined up itself:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href="https://github.com/apache/datafusion/pull/11943#top"&gt;Intermediate results blocked management&lt;/a&gt; (thanks again &lt;a href="https://github.com/Rachelint"&gt;Rachelint Kamille&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://github.com/apache/datafusion/issues/3463"&gt;Enable parquet filter pushdown by default #3463&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We are also talking about what to focus on over the &lt;a href="https://github.com/apache/datafusion/issues/13274"&gt;next three months&lt;/a&gt; and are always looking for people to help! If you want to geek out (obsess??) about performance and other features with engineers from around the world, &lt;a href="https://datafusion.apache.org/contributor-guide/communication.html"&gt;we would love you to join us&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id="additional-thanks"&gt;Additional thanks&lt;/h2&gt;

&lt;p&gt;In addition to the people called out above, thanks:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href="https://github.com/pmcgleenon"&gt;Patrick McGleenon&lt;/a&gt; for running ClickBench and gathering this data (&lt;a href="https://github.com/apache/datafusion/issues/13099#issuecomment-2478314793"&gt;source&lt;/a&gt;).&lt;/li&gt;
  &lt;li&gt;Everyone I missed in the shoutouts—there are so many of you. We appreciate everyone.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;I have dreamed about DataFusion being at the top of the ClickBench leaderboard for several years. I often watched with envy improvements in systems backed by large VC investments, internet companies, or world class research institutions, and doubted that we could pull off something similar in an open source project with always limited time.&lt;/p&gt;

&lt;p&gt;The fact that we have now surpassed those other systems in query performance speaks to the power and possibility of focusing on community and aligning the collective enthusiasm and skills towards a common goal. Of course, being on the top in any particular benchmark is likely fleeting as other engines will improve, but so will DataFusion!&lt;/p&gt;

&lt;p&gt;I love working on DataFusion—the people,  the quality of the code, my interactions and the results we have achieved together far surpass my expectations as well as most of my other software development experiences. I can’t wait to see what people will build next, and hope to &lt;a href="https://github.com/apache/datafusion"&gt;see you online&lt;/a&gt;.&lt;/p&gt;

&lt;hr /&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Note that DuckDB is slightly faster on the ‘cold’ run.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Want to try your hand at a custom format for ClickBench fame/glory? &lt;a href="https://github.com/apache/datafusion/issues/13448"&gt;Make DataFusion the fastest engine in ClickBench with custom file format&lt;/a&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;We have contributors from North America, South American, Europe, Asia, Africa and Australia.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Undergraduates, PhD, Junior engineers, and getting-kind-of-crotchety experienced engineers.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Thanks to Andy Pavlo, I love that nomenclature.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;
</description>
      <pubDate>Mon, 25 Nov 2024 07:00:00 +0000</pubDate>
      <link>https://www.influxdata.com/blog/apache-datafusion-fastest-single-node-querying-engine/</link>
      <guid isPermaLink="true">https://www.influxdata.com/blog/apache-datafusion-fastest-single-node-querying-engine/</guid>
      <category>Developer</category>
      <author>Andrew Lamb (InfluxData)</author>
    </item>
    <item>
      <title>Using StringView / German Style Strings to Make Queries Faster: Part 2 - String Operations</title>
      <description>&lt;h2 id="section-3-faster-string-operations"&gt;Section 3: Faster String Operations&lt;/h2&gt;

&lt;p&gt;In the &lt;a href="https://www.influxdata.com/blog/faster-queries-with-stringview-part-one-influxdb/" title="Using StringView / German Style Strings to Make Queries Faster: Part 1 - Reading Parquet"&gt;first post&lt;/a&gt;, we discussed the nuances required to accelerate Parquet loading using StringViewArray by reusing buffers and reducing copies. In this second part of the post, we describe the rest of the journey: implementing additional efficient operations for real query processing.&lt;/p&gt;

&lt;h3 id="section-31-faster-comparison"&gt;Section 3.1 Faster comparison&lt;/h3&gt;

&lt;p&gt;String comparison is ubiquitous; it is the core of &lt;a href="https://docs.rs/arrow/latest/arrow/compute/kernels/cmp/index.html"&gt;cmp&lt;/a&gt;, &lt;a href="https://docs.rs/arrow/latest/arrow/compute/fn.min.html"&gt;min&lt;/a&gt;/&lt;a href="https://docs.rs/arrow/latest/arrow/compute/fn.max.html"&gt;max&lt;/a&gt;, and &lt;a href="https://docs.rs/arrow/latest/arrow/compute/kernels/comparison/fn.like.html"&gt;like&lt;/a&gt;/&lt;a href="https://docs.rs/arrow/latest/arrow/compute/kernels/comparison/fn.ilike.html"&gt;ilike&lt;/a&gt; kernels. StringViewArray is designed to accelerate such comparisons using the inlined prefix—the key observation is that, in many cases, only the first few bytes of the string determine the string comparison results.&lt;/p&gt;

&lt;p&gt;For example, to compare the strings InfluxDB with Apache DataFusion, we only need to look at the first byte to determine the string ordering or equality. In this case, since &lt;code class="language-markup"&gt;A&lt;/code&gt;
is earlier in the alphabet than &lt;code class="language-markup"&gt;
I&lt;/code&gt;, Apache DataFusion sorts first, and we know the strings are not equal. Despite only needing the first byte, comparing these strings when stored as a StringArray requires two memory accesses: 1) load the string offset and 2) use the offset to locate the string bytes. For low-level operations such as &lt;code class="language-markup"&gt;cmp&lt;/code&gt; that are invoked millions of times in the very hot paths of queries, avoiding this extra memory access can make a measurable difference in query performance.&lt;/p&gt;

&lt;p&gt;For StringViewArray, typically, only one memory access is needed to load the view struct. Only if the result can not be determined from the prefix is the second memory access required. For the example above, there is no need for the second access. This technique is very effective in practice: the second access is never necessary for the more than &lt;a href="https://www.vldb.org/pvldb/vol17/p148-zeng.pdf"&gt;60% of real-world strings which are shorter than 12 bytes&lt;/a&gt;, as they are stored completely in the prefix.&lt;/p&gt;

&lt;p&gt;However, functions that operate on strings must be specialized to take advantage of the inlined prefix. In addition to low-level comparison kernels, we implemented &lt;a href="https://github.com/apache/arrow-rs/issues/5374"&gt;a wide range&lt;/a&gt; of other StringViewArray operations that cover the functions and operations seen in ClickBench queries. Supporting StringViewArray in all string operations takes quite a bit of effort, and thankfully the Arrow and DataFusion communities are already hard at work doing so (see &lt;a href="https://github.com/apache/datafusion/issues/11752"&gt;https://github.com/apache/datafusion/issues/11752&lt;/a&gt; if you want to help out).&lt;/p&gt;

&lt;h3 id="section-32-faster-code-classlanguage-markuptakecode-and-code-classlanguage-markupfiltercode"&gt;Section 3.2: Faster &lt;code class="language-markup"&gt;take&lt;/code&gt; and &lt;code class="language-markup"&gt;filter&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;After a filter operation such as &lt;code class="language-markup"&gt;WHERE url &amp;lt;&amp;gt; ‘’&lt;/code&gt; to avoid processing empty urls, DataFusion will often &lt;em&gt;coalesce&lt;/em&gt; results to form a new array with only the passing elements. This coalescing ensures the batches are sufficiently sized to benefit from &lt;a href="https://www.vldb.org/pvldb/vol11/p2209-kersten.pdf"&gt;vectorized processing&lt;/a&gt; in subsequent steps.&lt;/p&gt;

&lt;p&gt;The coalescing operation is implemented using the &lt;a href="https://docs.rs/arrow/latest/arrow/compute/fn.take.html"&gt;take&lt;/a&gt; and &lt;a href="https://arrow.apache.org/rust/arrow/compute/kernels/filter/fn.filter.html"&gt;filter&lt;/a&gt; kernels in arrow-rs. For StringArray, these kernels require copying the string contents to a new buffer without “holes” in between. This copy can be expensive especially when the new array is large.&lt;/p&gt;

&lt;p&gt;However, &lt;code class="language-markup"&gt;take&lt;/code&gt; and &lt;code class="language-markup"&gt;filter&lt;/code&gt; for StringViewArray can avoid the copy by reusing buffers from the old array. The kernels only need to create a new list of  &lt;code class="language-markup"&gt;view&lt;/code&gt;s that point at the same strings within the old buffers.  Figure 1 illustrates the difference between the output of both string representations. StringArray creates two new strings at offsets 0-17 and 17-32, while StringViewArray simply points to the original buffer at offsets 0 and 25.
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/a34495da2cf8403fb5ee6f860c70dee1/c6725b9b608ea2699335bb9f2dd5ac76/unnamed.png" alt="" /&gt;
  Figure 1: Zero-copy &lt;code class="language-markup"&gt;take&lt;/code&gt;/&lt;code class="language-markup"&gt;filter&lt;/code&gt; for StringViewArray&lt;/p&gt;

&lt;h3 id="section-33-when-to-gc"&gt;Section 3.3: When to GC?&lt;/h3&gt;

&lt;p&gt;Zero-copy take/filter is great for generating large arrays quickly, but it is suboptimal for highly selective filters, where most of the strings are filtered out. When the cardinality drops, StringViewArray buffers become sparse—only a small subset of the bytes in the buffer’s memory are referred to by any &lt;code class="language-markup"&gt;view&lt;/code&gt;. This leads to excessive memory usage, especially in a &lt;a href="https://github.com/apache/datafusion/issues/11628"&gt;filter-then-coalesce scenario&lt;/a&gt;. For example, a StringViewArray with 10M strings may only refer to 1M strings after some filter operations; however, due to zero-copy take/filter, the (reused) 10M buffers can not be released/reused.&lt;/p&gt;

&lt;p&gt;To release unused memory, we implemented a &lt;a href="https://docs.rs/arrow/latest/arrow/array/struct.GenericByteViewArray.html#method.gc"&gt;garbage collection (GC)&lt;/a&gt; routine to consolidate the data into a new buffer to release the old sparse buffer(s). As the GC operation copies strings, similarly to StringArray, we must be careful about when to call it. If we call GC too early, we cause unnecessary copying, losing much of the benefit of StringViewArray. If we call GC too late, we hold large buffers for too long, increasing memory use and decreasing cache efficiency. The &lt;a href="https://pola.rs/posts/polars-string-type/"&gt;Polars blog&lt;/a&gt; on StringView also refers to the challenge presented by garbage collection timing.&lt;/p&gt;

&lt;p&gt;arrow-rs implements the GC process, but it is up to users to decide when to call it. We leverage the semantics of the query engine and observed that the &lt;a href="https://docs.rs/datafusion/latest/datafusion/physical_plan/coalesce_batches/struct.CoalesceBatchesExec.html"&gt;CoalseceBatchesExec&lt;/a&gt; operator, which merge smaller batches to a larger batch, is often used after the record cardinality is expected to shrink, which aligns perfectly with the scenario of GC in StringViewArray. We, therefore,&lt;a href="https://github.com/apache/datafusion/pull/11587"&gt;implemented the GC procedure&lt;/a&gt; inside CoalseceBatchesExec&lt;sup&gt;1&lt;/sup&gt;,with a heuristic that estimates when the buffers are too sparse.&lt;/p&gt;

&lt;h3 id="section-34-the-art-of-function-inlining-not-too-much-not-too-little"&gt;Section 3.4: The art of function inlining: not too much, not too little&lt;/h3&gt;

&lt;p&gt;Like string inlining, &lt;em&gt;function&lt;/em&gt; inlining is the process of embedding a short function into the caller to avoid the overhead of function calls (caller/callee save). Usually, the Rust compiler does a good job of deciding when to inline. However, it is possible to override its default using the &lt;a href="https://doc.rust-lang.org/reference/attributes/codegen.html#the-inline-attribute"&gt;#[inline(always)]&lt;/a&gt;&lt;a href="https://doc.rust-lang.org/reference/attributes/codegen.html#the-inline-attribute"&gt;directive&lt;/a&gt;. In performance-critical code, inlined code allows us to organize large functions into smaller ones without paying the runtime cost of function invocation.&lt;/p&gt;

&lt;p&gt;However, function inlining is &lt;strong&gt;&lt;em&gt;not&lt;/em&gt;&lt;/strong&gt; always better, as it leads to larger function bodies that are harder for LLVM to optimize (for example, suboptimal &lt;a href="https://en.wikipedia.org/wiki/Register_allocation"&gt;register spilling&lt;/a&gt;) and risk overflowing the CPU’s instruction cache. We observed several performance regressions where function inlining caused &lt;em&gt;slower&lt;/em&gt; performance when implementing the StringViewArray comparison kernels. Careful inspection and tuning of the code was required to aid the compiler in generating efficient code. More details can be found in this PR: &lt;a href="https://github.com/apache/arrow-rs/pull/5900"&gt;https://github.com/apache/arrow-rs/pull/5900&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id="section-35-buffer-size-tuning"&gt;Section 3.5: Buffer size tuning&lt;/h3&gt;

&lt;p&gt;StringViewArray permits multiple buffers, which enables a flexible buffer layout and potentially reduces the need to copy data. However, a large number of buffers slows down the performance of other operations. For example, &lt;a href="https://docs.rs/arrow/latest/arrow/array/trait.Array.html#tymethod.get_array_memory_size"&gt;get_array_memory_size&lt;/a&gt;() needs to sum the memory size of each buffer, which takes a long time with thousands of small buffers. In certain cases, we found that multiple calls to &lt;a href="https://docs.rs/arrow/latest/arrow/compute/fn.concat_batches.html"&gt;concat_batches&lt;/a&gt; lead to arrays with millions of buffers, which was prohibitively expensive.&lt;/p&gt;

&lt;p&gt;For example, consider a StringViewArray with the previous default buffer size of 8 KB. With this configuration, holding 4GB of string data requires almost half a million buffers! Larger buffer sizes are needed for larger arrays, but we cannot arbitrarily increase the default buffer size, as small arrays would consume too much memory (most arrays require at least one buffer). Buffer sizing is especially problematic in query processing, as we often need to construct small batches of string arrays, and the sizes are unknown at planning time.&lt;/p&gt;

&lt;p&gt;To balance the buffer size trade-off, we again leverage the query processing (DataFusion) semantics to decide when to use larger buffers. While coalescing batches, we combine multiple small string arrays and set a smaller buffer size to keep the total memory consumption low. In string aggregation, we aggregate over an entire Datafusion partition, which can generate a large number of strings, so we set a larger buffer size (2MB).&lt;/p&gt;

&lt;p&gt;To assist situations where the semantics are unknown, we also &lt;a href="https://github.com/apache/arrow-rs/pull/6136"&gt;implemented&lt;/a&gt; a classic dynamic exponential buffer size growth strategy, which starts with a small buffer size (8KB) and doubles the size of each new buffer up to 2MB. We implemented this strategy in arrow-rs and enabled it by default so that other users of StringViewArray can also benefit from this optimization. See this issue for more details: &lt;a href="https://github.com/apache/arrow-rs/issues/6094"&gt;https://github.com/apache/arrow-rs/issues/6094&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id="section-36-end-to-end-query-performance"&gt;Section 3.6: End-to-end query performance&lt;/h3&gt;

&lt;p&gt;We have made significant progress in optimizing StringViewArray filtering operations. Now, let’s test it in the real world to see how it works!&lt;/p&gt;

&lt;p&gt;Let’s consider ClickBench query 22, which selects multiple string fields (URL, Title, and SearchPhase) and applies several filters.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-java"&gt;SELECT 
  "SearchPhrase", 
  MIN("URL"), MIN("Title"), COUNT(\*) AS c, COUNT(DISTINCT "UserID") 
FROM hits 
WHERE 
  "Title" LIKE '%Google%' AND 
  "URL" NOT LIKE '%.google.%' AND 
  "SearchPhrase" &amp;lt;&amp;gt; '' 
GROUP BY "SearchPhrase" 
ORDER BY c DESC 
LIMIT 10;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We ran the benchmark using the following command in the DataFusion repo. Again, the –string-view option means we use StringViewArray instead of StringArray.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-java"&gt;cargo run --profile release-nonlto --bin dfbench -- clickbench --queries-path benchmarks/queries/clickbench/queries.sql --iterations 3 --query 22 --path benchmarks/data/hits.parquet --string-view&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;To eliminate the impact of the faster Parquet reading using StringViewArray (see the first part of this blog), Figure 2 plots only the time spent in FilterExec. Without StringViewArray, the filter takes 7.17s; with StringViewArray, the filter only takes 4.86s, a 32% reduction in time. Moreover, we see a 17% improvement in end-to-end query performance.
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/85d6c7df75864ae7ac09e428e1611576/d60513f4b56381b0814d0998378753bd/unnamed.png" alt="" /&gt;
Figure 2: StringViewArray reduces the filter time by 32% on ClickBench query 22.&lt;/p&gt;

&lt;h2 id="section-4-faster-string-aggregation"&gt;Section 4: Faster String Aggregation&lt;/h2&gt;

&lt;p&gt;So far, we have discussed how to exploit two StringViewArray features: reduced copy and faster filtering. This section focuses on reusing string bytes to repeat string values.&lt;/p&gt;

&lt;p&gt;As described in part one of this blog, if two strings have identical values, StringViewArray can use two different &lt;code class="language-markup"&gt;view&lt;/code&gt;s pointing at the same buffer range, thus avoiding repeating the string bytes in the buffer. This makes StringViewArray similar to an Arrow &lt;a href="https://docs.rs/arrow/latest/arrow/array/struct.DictionaryArray.html"&gt;DictionaryArray&lt;/a&gt; that stores Strings—both array types work well for strings with only a few distinct values.&lt;/p&gt;

&lt;p&gt;Deduplicating string values can significantly reduce memory consumption in StringViewArray. However, this process is expensive and involves hashing every string and maintaining a hash table, and so it cannot be done by default when creating a StringViewArray. We introduced an&lt;a href="https://docs.rs/arrow/latest/arrow/array/builder/struct.GenericByteViewBuilder.html#method.with_deduplicate_strings"&gt;opt-in string deduplication mode&lt;/a&gt; in arrow-rs for advanced users who know their data has a small number of distinct values, and where the benefits of reduced memory consumption outweigh the additional overhead of array construction.&lt;/p&gt;

&lt;p&gt;Once again, we leverage DataFusion query semantics to identify StringViewArray with duplicate values, such as aggregation queries with multiple group keys. For example, some &lt;a href="https://github.com/apache/datafusion/blob/main/benchmarks/queries/clickbench/queries.sql"&gt;ClickBench queries&lt;/a&gt; group by two columns:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;UserID (an integer with close to 1 M distinct values)&lt;/li&gt;
  &lt;li&gt;MobilePhoneModel (a string with less than a hundred distinct values)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this case, the output row count is &lt;code class="language-markup"&gt;count(distinct UserID) * count(distinct MobilePhoneModel&lt;/code&gt;,  which is 100M. Each string value of  MobilePhoneModel is repeated 1M times. With StringViewArray, we can save space by pointing the repeating values to the same underlying buffer.&lt;/p&gt;

&lt;p&gt;Faster string aggregation with StringView is part of a larger project to &lt;a href="https://github.com/apache/datafusion/issues/7000"&gt;improve DataFusion aggregation performance&lt;/a&gt;. We have a &lt;a href="https://github.com/apache/datafusion/pull/11794"&gt;proof of concept implementation&lt;/a&gt; with StringView that can improve the multi-column string aggregation by 20%. We would love your help to get it production ready!&lt;/p&gt;

&lt;h2 id="section-5-stringview-pitfalls"&gt;Section 5: StringView Pitfalls&lt;/h2&gt;

&lt;p&gt;Most existing blog posts (including this one) focus on the benefits of using StringViewArray over other string representations such as StringArray. As we have discussed, even though it requires a significant engineering investment to realize, StringViewArray is a major improvement over StringArray in many cases.&lt;/p&gt;

&lt;p&gt;However, there are several cases where StringViewArray is slower than StringArray. For completeness, we have listed those instances here:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Tiny strings (when strings are shorter than 8 bytes)&lt;/strong&gt;: every element of the StringViewArray consumes at least 16 bytes of memory—the size of the &lt;code&gt;view&lt;/code&gt; struct. For an array of tiny strings, StringViewArray consumes more memory than StringArray and thus can cause slower performance due to additional memory pressure on the CPU cache.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Many repeated short strings&lt;/strong&gt;: Similar to the first point, StringViewArray can be slower and require more memory than a DictionaryArray because 1) it can only reuse the bytes in the buffer when the strings are longer than 12 bytes and 2) 32-bit offsets are always used, even when a smaller size (8 bit or 16 bit) could represent all the distinct values.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Filtering:&lt;/strong&gt; As we mentioned above, StringViewArrays often consume more memory than the corresponding StringArray, and memory bloat quickly dominates the performance without GC. However, invoking GC also reduces the benefits of less copying so must be carefully tuned.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id="section-6-conclusion-and-takeaways"&gt;Section 6: Conclusion and Takeaways&lt;/h2&gt;

&lt;p&gt;In these two blog posts, we discussed what it takes to implement StringViewArray in arrow-rs and then integrate it into DataFusion. Our evaluations on ClickBench queries show that StringView can improve the performance of string-intensive workloads by up to 2x.&lt;/p&gt;

&lt;p&gt;Given that DataFusion already &lt;a href="https://benchmark.clickhouse.com/#eyJzeXN0ZW0iOnsiQWxsb3lEQiI6ZmFsc2UsIkF0aGVuYSAocGFydGl0aW9uZWQpIjpmYWxzZSwiQXRoZW5hIChzaW5nbGUpIjpmYWxzZSwiQXVyb3JhIGZvciBNeVNRTCI6ZmFsc2UsIkF1cm9yYSBmb3IgUG9zdGdyZVNRTCI6ZmFsc2UsIkJ5Q29uaXR5IjpmYWxzZSwiQnl0ZUhvdXNlIjpmYWxzZSwiY2hEQiAoUGFycXVldCwgcGFydGl0aW9uZWQpIjpmYWxzZSwiY2hEQiI6ZmFsc2UsIkNpdHVzIjpmYWxzZSwiQ2xpY2tIb3VzZSBDbG91ZCAoYXdzKSI6ZmFsc2UsIkNsaWNrSG91c2UgQ2xvdWQgKGF3cykgUGFyYWxsZWwgUmVwbGljYXMgT04iOmZhbHNlLCJDbGlja0hvdXNlIENsb3VkIChBenVyZSkiOmZhbHNlLCJDbGlja0hvdXNlIENsb3VkIChBenVyZSkgUGFyYWxsZWwgUmVwbGljYSBPTiI6ZmFsc2UsIkNsaWNrSG91c2UgQ2xvdWQgKEF6dXJlKSBQYXJhbGxlbCBSZXBsaWNhcyBPTiI6ZmFsc2UsIkNsaWNrSG91c2UgQ2xvdWQgKGdjcCkiOmZhbHNlLCJDbGlja0hvdXNlIENsb3VkIChnY3ApIFBhcmFsbGVsIFJlcGxpY2FzIE9OIjpmYWxzZSwiQ2xpY2tIb3VzZSAoZGF0YSBsYWtlLCBwYXJ0aXRpb25lZCkiOmZhbHNlLCJDbGlja0hvdXNlIChkYXRhIGxha2UsIHNpbmdsZSkiOmZhbHNlLCJDbGlja0hvdXNlIChQYXJxdWV0LCBwYXJ0aXRpb25lZCkiOmZhbHNlLCJDbGlja0hvdXNlIChQYXJxdWV0LCBzaW5nbGUpIjpmYWxzZSwiQ2xpY2tIb3VzZSAod2ViKSI6ZmFsc2UsIkNsaWNrSG91c2UiOmZhbHNlLCJDbGlja0hvdXNlICh0dW5lZCkiOmZhbHNlLCJDbGlja0hvdXNlICh0dW5lZCwgbWVtb3J5KSI6ZmFsc2UsIkNsb3VkYmVycnkiOmZhbHNlLCJDcmF0ZURCIjpmYWxzZSwiQ3J1bmNoeSBCcmlkZ2UgZm9yIEFuYWx5dGljcyAoUGFycXVldCkiOmZhbHNlLCJEYXRhYmVuZCI6ZmFsc2UsIkRhdGFGdXNpb24gKFBhcnF1ZXQsIHBhcnRpdGlvbmVkKSI6dHJ1ZSwiRGF0YUZ1c2lvbiAoUGFycXVldCwgc2luZ2xlKSI6ZmFsc2UsIkFwYWNoZSBEb3JpcyI6ZmFsc2UsIkRydWlkIjpmYWxzZSwiRHVja0RCIChQYXJxdWV0LCBwYXJ0aXRpb25lZCkiOnRydWUsIkR1Y2tEQiI6ZmFsc2UsIkVsYXN0aWNzZWFyY2giOmZhbHNlLCJFbGFzdGljc2VhcmNoICh0dW5lZCkiOmZhbHNlLCJHbGFyZURCIjpmYWxzZSwiR3JlZW5wbHVtIjpmYWxzZSwiSGVhdnlBSSI6ZmFsc2UsIkh5ZHJhIjpmYWxzZSwiSW5mb2JyaWdodCI6ZmFsc2UsIktpbmV0aWNhIjpmYWxzZSwiTWFyaWFEQiBDb2x1bW5TdG9yZSI6ZmFsc2UsIk1hcmlhREIiOmZhbHNlLCJNb25ldERCIjpmYWxzZSwiTW9uZ29EQiI6ZmFsc2UsIk1vdGhlcmR1Y2siOmZhbHNlLCJNeVNRTCAoTXlJU0FNKSI6ZmFsc2UsIk15U1FMIjpmYWxzZSwiT3hsYSI6ZmFsc2UsIlBhcmFkZURCIChQYXJxdWV0LCBwYXJ0aXRpb25lZCkiOmZhbHNlLCJQYXJhZGVEQiAoUGFycXVldCwgc2luZ2xlKSI6ZmFsc2UsIlBpbm90IjpmYWxzZSwiUG9zdGdyZVNRTCAodHVuZWQpIjpmYWxzZSwiUG9zdGdyZVNRTCI6ZmFsc2UsIlF1ZXN0REIgKHBhcnRpdGlvbmVkKSI6ZmFsc2UsIlF1ZXN0REIiOmZhbHNlLCJSZWRzaGlmdCI6ZmFsc2UsIlNlbGVjdERCIjpmYWxzZSwiU2luZ2xlU3RvcmUiOmZhbHNlLCJTbm93Zmxha2UiOmZhbHNlLCJTUUxpdGUiOmZhbHNlLCJTdGFyUm9ja3MiOmZhbHNlLCJUYWJsZXNwYWNlIjpmYWxzZSwiVGVtYm8gT0xBUCAoY29sdW1uYXIpIjpmYWxzZSwiVGltZXNjYWxlREIgKGNvbXByZXNzaW9uKSI6ZmFsc2UsIlRpbWVzY2FsZURCIjpmYWxzZSwiVW1icmEiOmZhbHNlfSwidHlwZSI6eyJDIjp0cnVlLCJjb2x1bW4tb3JpZW50ZWQiOnRydWUsIlBvc3RncmVTUUwgY29tcGF0aWJsZSI6dHJ1ZSwibWFuYWdlZCI6dHJ1ZSwiZ2NwIjp0cnVlLCJzdGF0ZWxlc3MiOnRydWUsIkphdmEiOnRydWUsIkMrKyI6dHJ1ZSwiTXlTUUwgY29tcGF0aWJsZSI6dHJ1ZSwicm93LW9yaWVudGVkIjp0cnVlLCJDbGlja0hvdXNlIGRlcml2YXRpdmUiOnRydWUsImVtYmVkZGVkIjp0cnVlLCJzZXJ2ZXJsZXNzIjp0cnVlLCJhd3MiOnRydWUsInBhcmFsbGVsIHJlcGxpY2FzIjp0cnVlLCJBenVyZSI6dHJ1ZSwiYW5hbHl0aWNhbCI6dHJ1ZSwiUnVzdCI6dHJ1ZSwic2VhcmNoIjp0cnVlLCJkb2N1bWVudCI6dHJ1ZSwic29tZXdoYXQgUG9zdGdyZVNRTCBjb21wYXRpYmxlIjp0cnVlLCJ0aW1lLXNlcmllcyI6dHJ1ZX0sIm1hY2hpbmUiOnsiMTYgdkNQVSAxMjhHQiI6dHJ1ZSwiOCB2Q1BVIDY0R0IiOnRydWUsInNlcnZlcmxlc3MiOnRydWUsIjE2YWN1Ijp0cnVlLCJjNmEuNHhsYXJnZSwgNTAwZ2IgZ3AyIjp0cnVlLCJMIjp0cnVlLCJNIjp0cnVlLCJTIjp0cnVlLCJYUyI6dHJ1ZSwiYzZhLm1ldGFsLCA1MDBnYiBncDIiOnRydWUsIjE5MkdCIjp0cnVlLCIyNEdCIjp0cnVlLCIzNjBHQiI6dHJ1ZSwiNDhHQiI6dHJ1ZSwiNzIwR0IiOnRydWUsIjk2R0IiOnRydWUsIjE0MzBHQiI6dHJ1ZSwiZGV2Ijp0cnVlLCI3MDhHQiI6dHJ1ZSwiYzVuLjR4bGFyZ2UsIDUwMGdiIGdwMiI6dHJ1ZSwiQW5hbHl0aWNzLTI1NkdCICg2NCB2Q29yZXMsIDI1NiBHQikiOnRydWUsImM1LjR4bGFyZ2UsIDUwMGdiIGdwMiI6dHJ1ZSwiYzZhLjR4bGFyZ2UsIDE1MDBnYiBncDIiOnRydWUsImNsb3VkIjp0cnVlLCJkYzIuOHhsYXJnZSI6dHJ1ZSwicmEzLjE2eGxhcmdlIjp0cnVlLCJyYTMuNHhsYXJnZSI6dHJ1ZSwicmEzLnhscGx1cyI6dHJ1ZSwiUzIiOnRydWUsIlMyNCI6dHJ1ZSwiMlhMIjp0cnVlLCIzWEwiOnRydWUsIjRYTCI6dHJ1ZSwiWEwiOnRydWUsIkwxIC0gMTZDUFUgMzJHQiI6dHJ1ZSwiYzZhLjR4bGFyZ2UsIDUwMGdiIGdwMyI6dHJ1ZX0sImNsdXN0ZXJfc2l6ZSI6eyIxIjp0cnVlLCIyIjp0cnVlLCI0Ijp0cnVlLCI4Ijp0cnVlLCIxNiI6dHJ1ZSwiMzIiOnRydWUsIjY0Ijp0cnVlLCIxMjgiOnRydWUsInNlcnZlcmxlc3MiOnRydWUsImRlZGljYXRlZCI6dHJ1ZX0sIm1ldHJpYyI6ImhvdCIsInF1ZXJpZXMiOlt0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLH"&gt;performs very well on ClickBench&lt;/a&gt;, the level of end-to-end performance improvement using StringViewArray shows the power of this technique and, of course, is a win for DataFusion and the systems that build upon it.&lt;/p&gt;

&lt;p&gt;StringView is a big project that has received tremendous community support. Specifically, we would like to thank &lt;a href="https://github.com/tustvold"&gt;@tustvold&lt;/a&gt;, &lt;a href="https://github.com/ariesdevil"&gt;@ariesdevil&lt;/a&gt;, &lt;a href="https://github.com/RinChanNOWWW"&gt;@RinChanNOWWW&lt;/a&gt;, &lt;a href="https://github.com/ClSlaid"&gt;@ClSlaid&lt;/a&gt;, &lt;a href="https://github.com/2010YOUY01"&gt;@2010YOUY01&lt;/a&gt;, &lt;a href="https://github.com/chloro-pn"&gt;@chloro-pn&lt;/a&gt;, &lt;a href="https://github.com/a10y"&gt;@a10y&lt;/a&gt;, &lt;a href="https://github.com/Kev1n8"&gt;@Kev1n8&lt;/a&gt;, &lt;a href="https://github.com/Weijun-H"&gt;@Weijun-H&lt;/a&gt;, &lt;a href="https://github.com/PsiACE"&gt;@PsiACE&lt;/a&gt;, &lt;a href="https://github.com/tshauck"&gt;@tshauck&lt;/a&gt;, and &lt;a href="https://github.com/xinlifoobar"&gt;@xinlifoobar&lt;/a&gt; for their valuable contributions!&lt;/p&gt;

&lt;p&gt;As the introduction states, “German Style Strings” is a relatively straightforward research idea that avoid some string copies and accelerates comparisons. However, applying this (great) idea in practice requires a significant investment in careful software engineering. Again, we encourage the research community to continue to help apply research ideas to industrial systems, such as DataFusion, as doing so provides valuable perspectives when evaluating future research questions for the greatest potential impact.&lt;/p&gt;

&lt;hr /&gt;

&lt;ol&gt;
  &lt;li&gt;There are additional optimizations possible in this operation that the community is working on, such as  &lt;a href="https://github.com/apache/datafusion/issues/7957"&gt;https://github.com/apache/datafusion/issues/7957&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
</description>
      <pubDate>Tue, 03 Sep 2024 08:00:00 +0000</pubDate>
      <link>https://www.influxdata.com/blog/faster-queries-with-stringview-part-two-influxdb/</link>
      <guid isPermaLink="true">https://www.influxdata.com/blog/faster-queries-with-stringview-part-two-influxdb/</guid>
      <category>Developer</category>
      <author>Andrew Lamb, Xiangpeng Hao (InfluxData)</author>
    </item>
    <item>
      <title>Using StringView / German Style Strings to Make Queries Faster: Part 1 - Reading Parquet</title>
      <description>&lt;p&gt;Editor’s Note: This is the first of a &lt;a href="https://www.influxdata.com/blog/faster-queries-with-stringview-part-two-influxdb/" title="Using StringView / German Style Strings to Make Queries Faster: Part 2 - String Operations"&gt;two part&lt;/a&gt; blog series.&lt;/p&gt;

&lt;p&gt;This blog describes our experience implementing &lt;a href="https://arrow.apache.org/docs/format/Columnar.html#variable-size-binary-view-layout"&gt;StringView&lt;/a&gt; in the &lt;a href="https://github.com/apache/arrow-rs"&gt;Rust implementation&lt;/a&gt; of &lt;a href="https://arrow.apache.org/"&gt;Apache Arrow&lt;/a&gt;, and integrating it into &lt;a href="https://datafusion.apache.org/"&gt;Apache DataFusion&lt;/a&gt;, significantly accelerating string-intensive queries in the &lt;a href="https://benchmark.clickhouse.com/"&gt;ClickBench&lt;/a&gt; benchmark by 20%- 200% (Figure 1&lt;sup&gt;1&lt;/sup&gt;).&lt;/p&gt;

&lt;p&gt;Getting significant end-to-end performance improvements was non-trivial. Implementing StringView itself was only a fraction of the effort required. Among other things, we had to optimize UTF-8 validation, implement unintuitive compiler optimizations, tune block sizes, and time GC to realize the &lt;a href="https://www.influxdata.com/blog/flight-datafusion-arrow-parquet-fdap-architecture-influxdb/"&gt;FDAP ecosystem&lt;/a&gt;’s benefit. With other members of the open source community, we were able to overcome performance bottlenecks that could have killed the project. We would like to contribute by explaining the challenges and solutions in more detail so that more of the community can learn from our experience.&lt;/p&gt;

&lt;p&gt;StringView is based on a simple idea: avoid some string copies and accelerate comparisons with inlined prefixes. Like most great ideas, it is “obvious” only after &lt;a href="https://db.in.tum.de/~freitag/papers/p29-neumann-cidr20.pdf"&gt;someone describes it clearly&lt;/a&gt;. Although simple, straightforward implementation actually &lt;em&gt;slows down performance for almost every query&lt;/em&gt;. We must, therefore, apply astute observations and diligent engineering to realize the actual benefits from StringView.&lt;/p&gt;

&lt;p&gt;Although this journey was successful, not all research ideas are as lucky. To accelerate the adoption of research into industry, it is valuable to integrate research prototypes with practical systems. Understanding the nuances of real-world systems makes it more likely that research designs&lt;sup&gt;2&lt;/sup&gt; will lead to practical system improvements.&lt;/p&gt;

&lt;p&gt;StringView support was released as part of arrow-rs &lt;a href="https://crates.io/crates/arrow/52.2.0"&gt;v52.2.0&lt;/a&gt; and DataFusion v41.0.0. You can try it by setting the &lt;code class=" language-markup"&gt;schema_force_string_view&lt;/code&gt; &lt;a href="https://datafusion.apache.org/user-guide/configs.html"&gt;DataFusion configuration option&lt;/a&gt;, and we are &lt;a href="https://github.com/apache/datafusion/issues/11682"&gt;hard at work with the community&lt;/a&gt; to make it the default. We invite everyone to try it out, take advantage of the effort invested so far, and contribute to making it better.
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/3c1e9348c4a044f28c4aabdee5ba37ad/8a56e70efc02df72d6b95dc2f7e361ca/unnamed.png" alt="" /&gt;
Figure 1: StringView improves string-intensive ClickBench query performance by 20% - 200%&lt;/p&gt;

&lt;h2 id="section-1-what-is-stringview"&gt;Section 1: What is StringView?&lt;/h2&gt;
&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/6fdc3d7bdc294c0bad9fe260491b8f45/8d837d2573050a6b229abed13e7a23fd/unnamed.png" alt="" /&gt;
Figure 2: Use StringArray and StringViewArray to represent the same string content.&lt;/p&gt;

&lt;p&gt;The concept of inlined strings with prefixes (called “German Strings” &lt;a href="https://x.com/andy_pavlo/status/1813258735965643203"&gt;by Andy Pavlo&lt;/a&gt;, in homage to &lt;a href="https://www.tum.de/"&gt;TUM&lt;/a&gt;, where the &lt;a href="https://db.in.tum.de/~freitag/papers/p29-neumann-cidr20.pdf"&gt;Umbra paper that describes&lt;/a&gt; them originated) has been used in many recent database systems (&lt;a href="https://engineering.fb.com/2024/02/20/developer-tools/velox-apache-arrow-15-composable-data-management/"&gt;Velox&lt;/a&gt;, &lt;a href="https://pola.rs/posts/polars-string-type/"&gt;Polars&lt;/a&gt;, &lt;a href="https://duckdb.org/2021/12/03/duck-arrow.html"&gt;DuckDB&lt;/a&gt;, &lt;a href="https://cedardb.com/blog/german_strings/"&gt;CedarDB&lt;/a&gt;, etc.) and was introduced to Arrow as a new &lt;a href="https://arrow.apache.org/docs/format/Columnar.html#variable-size-binary-view-layout"&gt;StringViewArray&lt;/a&gt;&lt;sup&gt;3&lt;/sup&gt; type. Arrow’s original &lt;a href="https://arrow.apache.org/docs/format/Columnar.html#variable-size-binary-layout"&gt;StringArray&lt;/a&gt; is very memory efficient but less effective for certain operations. StringViewArray accelerates string-intensive operations via prefix inlining and a more flexible and compact string representation.&lt;/p&gt;

&lt;p&gt;A StringViewArray consists of three components:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;The &lt;em&gt;view&lt;/em&gt; array&lt;/li&gt;
  &lt;li&gt;The buffers&lt;/li&gt;
  &lt;li&gt;The buffer pointers (IDs) that map buffer offsets to their physical locations&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each view is 16 bytes long, and its contents differ based on the string’s length:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;string length &amp;lt; 12 bytes: the first four bytes store the string length, and the remaining 12 bytes store the inlined string.&lt;/li&gt;
  &lt;li&gt;string length &amp;gt; 12 bytes: the string is stored in a separate buffer. The length is again stored in the first 4 bytes, followed by the buffer id (4 bytes), the buffer offset (4 bytes), and the prefix (first 4 bytes) of the string.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Figure 2 shows an example of the same logical content (left) using StringArray (middle) and StringViewArray (right):&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The first string – &lt;code class="language-markup"&gt;“Apache DataFusion”&lt;/code&gt; – is 17 bytes long, and both StringArray and StringViewArray store the string’s bytes at the beginning of the buffer. The StringViewArray also inlines the first 4 bytes – &lt;code class="language-markup"&gt;“Apac”&lt;/code&gt; – in the view.&lt;/li&gt;
  &lt;li&gt;The second string, &lt;code class=" language-markup"&gt;“InfluxDB”&lt;/code&gt; is only 8 bytes long, so StringViewArray completely inlines the string content in the view struct while StringArray stores the string in the buffer as well.&lt;/li&gt;
  &lt;li&gt;The third string &lt;code class="language-markup"&gt;“Arrow Rust Impl”&lt;/code&gt; is 15 bytes long and cannot be fully inlined. StringViewArray stores this in the same form as the first string.&lt;/li&gt;
  &lt;li&gt;The last string &lt;code class="language-markup"&gt;“Apache DataFusion”&lt;/code&gt; has the same content as the first string. It’s possible to use StringViewArray to avoid this duplication and reuse the bytes by pointing the view to the previous location.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;StringViewArray provides three opportunities for outperforming StringArray:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Less copying via the offset + buffer format&lt;/li&gt;
  &lt;li&gt;Faster comparisons using the inlined string prefix&lt;/li&gt;
  &lt;li&gt;Reusing repeated string values with the flexible view layout&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The rest of this blog post discusses how to apply these opportunities in real query scenarios to improve performance, what challenges we encountered along the way, and how we solved them.&lt;/p&gt;

&lt;h2 id="section-2-faster-parquet-loading"&gt;Section 2: Faster Parquet Loading&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://parquet.apache.org/"&gt;Apache&lt;/a&gt;&lt;a href="https://parquet.apache.org/"&gt;Parquet&lt;/a&gt; is the de facto format for storing large-scale analytical data commonly stored LakeHouse-style, such as &lt;a href="https://iceberg.apache.org"&gt;Apache Iceberg&lt;/a&gt; and &lt;a href="https://delta.io"&gt;Delta Lake&lt;/a&gt;. Efficiently loading data from Parquet is thus critical to query performance in many important real-world workloads.&lt;/p&gt;

&lt;p&gt;Parquet encodes strings (i.e., &lt;a href="https://docs.rs/parquet/latest/parquet/data_type/struct.ByteArray.html"&gt;byte array&lt;/a&gt;) in a slightly different format than required for the original Arrow StringArray. The string length is encoded inline with the actual string data (as shown in Figure 4 left). As mentioned previously, StringArray requires the data buffer to be continuous and compact—the strings have to follow one after another. This requirement means that reading Parquet string data into an Arrow StringArray requires copying and consolidating the string bytes to a new buffer and tracking offsets in a separate array. Copying these strings is often wasteful. Typical queries filter out most data immediately after loading, so most of the copied data is quickly discarded.&lt;/p&gt;

&lt;p&gt;On the other hand, reading Parquet data as a StringViewArray can re-use the same data buffer as storing the Parquet pages because StringViewArray does not require strings to be contiguous. For example, in Figure 4, the StringViewArray directly references the buffer with the decoded Parquet page. The string &lt;code class="language-markup"&gt;“Arrow Rust Impl”&lt;/code&gt; is represented by a &lt;code class="language-markup"&gt;view&lt;/code&gt; with offset 37 and length 15 into that buffer.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/cee09f1a659246fa8a1f2ef0249c2a9d/093af0e22a70a0e00686eb303e1dde4d/unnamed.png" alt="" /&gt;
Figure 4: StringViewArray avoids copying by reusing decoded Parquet pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mini benchmark&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Reusing Parquet buffers is great in theory, but how much does saving a copy actually matter? We can run the following benchmark in arrow-rs to find out:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-java"&gt;cargo bench --bench arrow_reader --features="arrow test_common experimental" "arrow_array_reader/Binary.*Array/plain encoded"&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Our benchmarking machine shows that loading &lt;em&gt;BinaryViewArray&lt;/em&gt; is almost 2x faster than loading BinaryArray (see next section about why this isn’t &lt;em&gt;String&lt;/em&gt;ViewArray).&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-java"&gt;arrow_array_reader/BinaryArray/plain encoded                        time:   [315.86 µs **317.47 µs** 319.00 µs]
arrow_array_reader/BinaryViewArray/plain encoded
time:   [162.08 µs **162.20 µs** 162.32 µs]&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can read more on this arrow-rs issue: &lt;a href="https://github.com/apache/arrow-rs/issues/5904"&gt;https://github.com/apache/arrow-rs/issues/5904&lt;/a&gt;&lt;/p&gt;

&lt;h3 id="section-21-from-binary-to-strings"&gt;Section 2.1: From binary to strings&lt;/h3&gt;

&lt;p&gt;You may wonder why we reported performance for BinaryViewArray when this post is about StringViewArray. Surprisingly, initially, our implementation to read StringViewArray from Parquet was much &lt;em&gt;slower&lt;/em&gt; than StringArray. Why? TLDR: Although reading StringViewArray copied less data, the initial implementation also spent much more time validating &lt;a href="https://en.wikipedia.org/wiki/UTF-8#:~:text=UTF%2D8%20is%20a%20variable,Unicode%20Standard"&gt;UTF-8&lt;/a&gt; (as shown in Figure 5).&lt;/p&gt;

&lt;p&gt;Strings are stored as byte sequences. When reading data from (potentially untrusted) Parquet files, a Parquet decoder must ensure those byte sequences are valid UTF-8 strings, and most programming languages, including Rust, include highly &lt;a href="https://doc.rust-lang.org/std/str/fn.from_utf8.html"&gt;optimized routines&lt;/a&gt; for doing so.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/2c6075e20695476eb5a073e363d6274f/85b87756429a0ede29a397ef8b4faca9/unnamed.png" alt="" /&gt;&lt;/p&gt;

&lt;p&gt;Figure 5: Time to load strings from Parquet. The UTF-8 validation advantage initially eliminates the advantage of reduced copying for StringViewArray.&lt;/p&gt;

&lt;p&gt;A StringArray can be validated in a single call to the UTF-8 validation function as it has a continuous string buffer. As long as the underlying buffer is UTF-8&lt;sup&gt;4&lt;/sup&gt;, all strings in the array must be UTF-8. The Rust parquet reader makes a single function call to validate the entire buffer.&lt;/p&gt;

&lt;p&gt;However, validating an arbitrary StringViewArray requires validating each string with a separate call to the validation function, as the underlying buffer may also contain non-string data (for example, the lengths in Parquet pages).&lt;/p&gt;

&lt;p&gt;UTF-8 validation in Rust is highly optimized and favors longer strings (as shown in Figure 6), likely because it leverages SIMD instructions to perform parallel validation. The benefit of a single function call to validate UTF-8 over a function call for each string more than eliminates the advantage of avoiding the copy for StringViewArray.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/ee7cb8a70aaf454f9f5fdb6622e7921e/e77ca6b97f81a08e627825b4b55a2309/unnamed.png" alt="" /&gt;&lt;/p&gt;

&lt;p&gt;Figure 6: UTF-8 validation throughput vs string length—StringArray’s contiguous buffer can be validated much faster than StringViewArray’s buffer.&lt;/p&gt;

&lt;p&gt;Does this mean we should only use StringArray? No! Thankfully, there’s a clever way out. The key observation is that in many real-world datasets, &lt;a href="https://www.vldb.org/pvldb/vol17/p148-zeng.pdf"&gt;99% of strings are shorter than 128 bytes&lt;/a&gt;, meaning the encoded length values are smaller than 128, &lt;strong&gt;in which case the length itself is also valid UTF-8&lt;/strong&gt; (in fact, it is &lt;a href="https://en.wikipedia.org/wiki/ASCII"&gt;ASCII&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;This observation means we can optimize validating UTF-8 strings in Parquet pages by treating the length bytes as part of a single large string as long as the length &lt;em&gt;value&lt;/em&gt; is less than 128. Put another way, prior to this optimization, the length bytes act as string boundaries, which require a UTF-8 validation on each string. After this optimization, only those strings with lengths larger than 128 bytes (less than 1% of the strings in the ClickBench dataset) are string boundaries, significantly increasing the UTF-8 validation chunk size and thus improving performance.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/apache/arrow-rs/pull/6009/files"&gt;actual implementation&lt;/a&gt; is only nine lines of Rust (with 30 lines of comments). You can find more details in the related arrow-rs issue:  &lt;a href="https://github.com/apache/arrow-rs/issues/5995"&gt;https://github.com/apache/arrow-rs/issues/5995&lt;/a&gt;. As expected, with this optimization, loading StringViewArray is almost 2x faster than loading StringArray.&lt;/p&gt;

&lt;h3 id="section-22-be-careful-about-implicit-copies"&gt;Section 2.2: Be careful about implicit copies&lt;/h3&gt;

&lt;p&gt;After all the work to avoid copying strings when loading from Parquet, performance was still not as good as expected. We tracked the problem to a few implicit data copies that we weren’t aware of, as described in &lt;a href="https://github.com/apache/arrow-rs/issues/6033"&gt;this issue&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The copies we eventually identified come from the following innocent-looking line of Rust code, where &lt;code class="language-markup"&gt;self.buf&lt;/code&gt; is a &lt;a href="https://en.wikipedia.org/wiki/Reference_counting"&gt;reference counted&lt;/a&gt; pointer that should transform without copying into a buffer for use in StringViewArray.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-java"&gt;let block_id = output.append_block(self.buf.clone().into());&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;However, Rust-type coercion rules favored a blanket implementation that &lt;em&gt;did&lt;/em&gt; copy data. This implementation is shown in the following code block where the &lt;code class="language-markup"&gt;impl&amp;lt;T: AsRef&amp;lt;[u8]&amp;gt;&amp;gt;&lt;/code&gt;will accept any type that implements &lt;code class="language-markup"&gt;AsRef&amp;lt;[u8]&amp;gt;&lt;/code&gt; and copies the data to create a new buffer. To avoid copying, users need to explicitly call from&lt;code class="language-markup"&gt;_vec&lt;/code&gt;, which consumes the &lt;code class="language-markup"&gt;Vec&lt;/code&gt; and transforms it into a buffer.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-java"&gt;impl&amp;lt;T: AsRef&amp;lt;[u8]&amp;gt;&amp;gt; From&amp;lt;T&amp;gt; for Buffer {
    fn from(p: T) -&amp;gt; Self {
        // copies data here
	 ...
    }
}
impl Buffer { 
  pub fn from_vec&amp;lt;T&amp;gt;(data: Vec&amp;lt;T&amp;gt;) -&amp;gt; Self {
// zero-copy transformation
...
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Diagnosing this implicit copy was time-consuming as it relied on subtle Rust language semantics. We needed to track every step of the data flow to ensure every copy was necessary. To help other users and prevent future mistakes, we also &lt;a href="https://github.com/apache/arrow-rs/pull/6043"&gt;removed&lt;/a&gt; the implicit API from arrow-rs in favor of an explicit API. Using this approach, we found and fixed several &lt;a href="https://github.com/apache/arrow-rs/pull/6039"&gt;other unintentional copies&lt;/a&gt; in the code base—hopefully, the change will help other &lt;a href="https://github.com/spiraldb/vortex/pull/504"&gt;downstream users&lt;/a&gt; avoid unnecessary copies.&lt;/p&gt;

&lt;h3 id="section-23-help-the-compiler-by-giving-it-more-information"&gt;Section 2.3: Help the compiler by giving it more information&lt;/h3&gt;

&lt;p&gt;The Rust compiler’s automatic optimizations mostly work very well for a wide variety of use cases, but sometimes, it needs additional hints to generate the most efficient code. When profiling the performance of &lt;code class="language-markup"&gt;view&lt;/code&gt; construction, we found, counterintuitively, that constructing &lt;strong&gt;long&lt;/strong&gt; strings was 10x faster than constructing &lt;strong&gt;short&lt;/strong&gt; strings, which made short strings slower on StringViewArray than on StringArray!&lt;/p&gt;

&lt;p&gt;As described in Section 1, StringViewArray treats long and short strings differently. Short strings (&amp;lt;12 bytes) directly inline to the view struct, while long strings only inline the first 4 bytes. The code to construct a &lt;code class="language-markup"&gt;view&lt;/code&gt; looks something like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-java"&gt;if len &amp;lt;= 12 {
   // Construct 16 byte view for short string
   let mut view_buffer = [0; 16];
   view_buffer[0..4].copy_from_slice(&amp;amp;len.to_le_bytes());
   view_buffer[4..4 + data.len()].copy_from_slice(data);
   ...
} else {      
   // Construct 16 byte view for long string
   ByteView {
       length: len,
       prefix: u32::from_le_bytes(data[0..4].try_into().unwrap()),
       buffer_index: block_id,
       offset,
   }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It appears that both branches of the code should be fast: they both involve copying at most 16 bytes of data and some memory shift/store operations. How could the branch for short strings be 10x slower?&lt;/p&gt;

&lt;p&gt;Looking at the assembly code using &lt;a href="https://godbolt.org/"&gt;godbolt&lt;/a&gt;, we (with help from &lt;a href="https://github.com/aoli-al"&gt;Ao Li&lt;/a&gt;) found the compiler used CPU &lt;strong&gt;load instructions&lt;/strong&gt; to copy the fixed-sized 4 bytes to the &lt;code class="language-markup"&gt;view&lt;/code&gt; for long strings, but it calls a function, &lt;a href="https://doc.rust-lang.org/std/ptr/fn.copy_nonoverlapping.html"&gt;ptr::copy_non_overlapping&lt;/a&gt;, to copy the inlined bytes to the &lt;code class="language-markup"&gt;view&lt;/code&gt; for short strings. The difference is that long strings have a prefix size (4 bytes) known at compile time, so the compiler directly uses efficient CPU instructions. But, since the size of the short string is unknown to the compiler, it has to call the general-purpose function &lt;code class="language-markup"&gt;ptr::copy_non_coverlapping&lt;/code&gt; . Making a function call is significant unnecessary overhead compared to a CPU copy instruction.&lt;/p&gt;

&lt;p&gt;However, we know something the compiler doesn’t know: the short string size is not arbitrary—it must be between 0 and 12 bytes, and we can leverage this information to avoid the function call. Our solution generates 13 copies of the function using generics, one for each of the possible prefix lengths. The code looks as follows, and &lt;a href="https://godbolt.org/z/685YPsd5G"&gt;checking the assembly code&lt;/a&gt;, we confirmed there are no calls to &lt;code class="language-markup"&gt;ptr::copy_non_overlapping&lt;/code&gt; , and only native CPU instructions are used. For more details, see &lt;a href="https://github.com/apache/arrow-rs/issues/6034"&gt;the ticket&lt;/a&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-java"&gt;fn make_inlined_view&amp;lt;const LEN: usize&amp;gt;(data: &amp;amp;[u8]) -&amp;gt; u128 {
     let mut view_buffer = [0; 16];
     view_buffer[0..4].copy_from_slice(&amp;amp;(LEN as u32).to_le_bytes());
     view_buffer[4..4 + LEN].copy_from_slice(&amp;amp;data[..LEN]);
     u128::from_le_bytes(view_buffer)
}
pub fn make_view(data: &amp;amp;[u8], block_id: u32, offset: u32) -&amp;gt; u128 {
     let len = data.len();
     // generate special code for each of the 13 possible lengths
     match len {
         0 =&amp;gt; make\_inlined\_view::&amp;lt;0&amp;gt;(data),
         1 =&amp;gt; make\_inlined\_view::&amp;lt;1&amp;gt;(data),
         2 =&amp;gt; make\_inlined\_view::&amp;lt;2&amp;gt;(data),
         3 =&amp;gt; make\_inlined\_view::&amp;lt;3&amp;gt;(data),
         4 =&amp;gt; make\_inlined\_view::&amp;lt;4&amp;gt;(data),
         5 =&amp;gt; make\_inlined\_view::&amp;lt;5&amp;gt;(data),
         6 =&amp;gt; make\_inlined\_view::&amp;lt;6&amp;gt;(data),
         7 =&amp;gt; make\_inlined\_view::&amp;lt;7&amp;gt;(data),
         8 =&amp;gt; make\_inlined\_view::&amp;lt;8&amp;gt;(data),
         9 =&amp;gt; make\_inlined\_view::&amp;lt;9&amp;gt;(data),
         10 =&amp;gt; make\_inlined\_view::&amp;lt;10&amp;gt;(data),
         11 =&amp;gt; make\_inlined\_view::&amp;lt;11&amp;gt;(data),
         12 =&amp;gt; make\_inlined\_view::&amp;lt;12&amp;gt;(data),
         _ =&amp;gt; {
           // handle long string
}}}&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id="section-24-end-to-end-query-performance"&gt;Section 2.4: End-to-end query performance&lt;/h3&gt;

&lt;p&gt;In the previous sections, we went out of our way to make sure loading StringViewArray is faster than StringArray. Before going further, we wanted to verify if obsessing about reducing copies and function calls has actually improved end-to-end performance in real-life queries. To do this, we evaluated a ClickBench query (Q20) in DataFusion that counts how many URLs contain the word &lt;code class="language-markup"&gt;"google"&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-java"&gt; SELECT COUNT(*) FROM hits WHERE "URL" LIKE '%google%'; &lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is a relatively simple query; most of the time is spent on loading the “URL” column to find matching rows. The query plan looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-java"&gt; Projection: COUNT(*) [COUNT(*):Int64;N]
  Aggregate: groupBy=[[]], aggr=[[COUNT(*)]] [COUNT(*):Int64;N]
    Filter: hits.URL LIKE Utf8("%google%")
      TableScan: hits &lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We ran the benchmark in the DataFusion repo like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-java"&gt;cargo run --profile release-nonlto --bin dfbench -- clickbench --queries-path benchmarks/queries/clickbench/queries.sql --iterations 3 --query 20 --path benchmarks/data/hits.parquet --string-view&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;With StringViewArray we saw a 24% end-to-end performance improvement, as shown in Figure 7. With the –string-view argument, the end-to-end query time is 944.3 ms, 869.6 ms, 861.9 ms (three iterations). Without –string-view, the end-to-end query time is 1186.1 ms, 1126.1 ms, 1138.3 ms.
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/8b513715c2c44d879b78c93587261a0e/384d6245cbbf9a3919f55b1ea251ca04/unnamed.png" alt="" /&gt;
Figure 7: StringView reduces end-to-end query time by 24% on ClickBench Q20.&lt;/p&gt;

&lt;p&gt;We also double-checked with detailed profiling and verified that the time reduction is indeed due to faster Parquet loading.&lt;/p&gt;

&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;

&lt;p&gt;In this first blog post, we have described what it took to improve the performance of simply reading strings from Parquet files using StringView. While this resulted in real end-to-end query performance improvements, in our next post, we explore additional optimizations enabled by StringView in DataFusion, along with some of the pitfalls we encountered while implementing them.&lt;/p&gt;

&lt;p&gt;Note: Thanks to InfluxData for sponsoring this work as a summer intern project&lt;/p&gt;
</description>
      <pubDate>Thu, 22 Aug 2024 08:00:00 +0000</pubDate>
      <link>https://www.influxdata.com/blog/faster-queries-with-stringview-part-one-influxdb/</link>
      <guid isPermaLink="true">https://www.influxdata.com/blog/faster-queries-with-stringview-part-one-influxdb/</guid>
      <category>Developer</category>
      <author>Andrew Lamb, Xiangpeng Hao (InfluxData)</author>
    </item>
    <item>
      <title>How Good is Parquet for Wide Tables (Machine Learning Workloads) Really?</title>
      <description>&lt;p&gt;In this blog post, we quantify the metadata overhead of &lt;a href="https://parquet.apache.org/"&gt;Apache Parquet&lt;/a&gt; files for storing thousands of columns, as well as space and decode time using &lt;a href="https://crates.io/crates/parquet"&gt;parquet-rs&lt;/a&gt;, implemented in Rust. We conclude that while technical concerns about Parquet metadata are valid, the actual overhead is smaller than generally recognized. In fact, optimizing writer settings and simple implementation tweaks can reduce overhead by 30-40%. With significant additional implementation optimization, decode speeds could improve by up to 4x.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/3bcf8d6b3394451aa2cf473b098edfdf/8c09495bed20a3cf13ae5472d4c009be/unnamed.png" alt="" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 1&lt;/strong&gt;: Metadata decode time for 1000 Parquet Float64 columns using &lt;a href="https://crates.io/crates/parquet"&gt;parquet-rs&lt;/a&gt;. Configuring the writer to omit statistics improves decode performance by 30% (9.1ms → 6.9 ms). Standard software engineering optimization techniques improve the decode performance by another 40% (6.9ms → 4.1ms / 9.1ms → 6.4ms)&lt;/p&gt;

&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;

&lt;p&gt;Recent assertions have suggested that Parquet is not suitable for wide tables with 1000s of columns, often found in &lt;a href="https://huggingface.co/docs/datasets-server/en/parquet#list-parquet-files"&gt;machine learning workloads&lt;/a&gt;. Proposals for new file formats, such as &lt;a href="https://www.cs.cit.tum.de/fileadmin/w00cfj/dis/papers/btrblocks.pdf"&gt;BtrBlocks&lt;/a&gt;, &lt;a href="https://blog.lancedb.com/lance-v2/"&gt;Lance V2&lt;/a&gt;, and &lt;a href="https://github.com/facebookincubator/nimble"&gt;Nimble&lt;/a&gt;&lt;sup&gt;1&lt;/sup&gt;, often accompany these assertions.&lt;/p&gt;

&lt;p&gt;Usually, &lt;a href="https://www.vldb.org/pvldb/vol17/p148-zeng.pdf"&gt;the stated rationale&lt;/a&gt; is that wide tables have “large” metadata, which takes a “long time” to decode, often longer than reading the data itself. Using &lt;a href="https://thrift.apache.org/"&gt;Apache Thrift&lt;/a&gt; to store the metadata means the &lt;a href="https://medium.com/pinterest-engineering/improving-data-processing-efficiency-using-partial-deserialization-of-thrift-16bc3a4a38b4"&gt;entire metadata&lt;/a&gt; payload must be decoded for each file, even when only a small subset of columns is required. It also appears to be common (though incorrect) to equate Parquet (the format) with a specific Parquet implementation (e.g., &lt;a href="https://github.com/apache/parquet-java"&gt;parquet-java&lt;/a&gt;) when evaluating performance.&lt;/p&gt;

&lt;p&gt;Leaving aside the fact that many query systems cache information from the Parquet metadata in a form suited for faster processing, we wanted quantitative information on how much of the purported metadata overhead is due to limitations in the Parquet format vs. how much is due to less optimized implementations or poorly configured settings of Parquet writers.&lt;/p&gt;

&lt;h2 id="background"&gt;Background&lt;/h2&gt;

&lt;p&gt;Parquet files include the metadata required to interpret the file. This metadata also instructs the reader to load only the portion of the file necessary to answer queries. More information on these techniques can be found in &lt;a href="https://www.influxdata.com/blog/querying-parquet-millisecond-latency/"&gt;Querying Parquet with Millisecond Latency&lt;/a&gt;. Typical Parquet files are GBs in size, but many queries read only a small portion, so the metadata is often critical to quickly finding the required data.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/4QBcJxLbh3Wmn57PhMK6QA/593454367e919fea392dc56f65d0d7e9/layout-of-parquet-files.png" alt="layout-of-parquet-files" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 2&lt;/strong&gt;: Layout of Parquet files. The metadata is stored in the footer (at the end of the file) and contains the location of pages within the file and optional statistics such as min/max/null counts for each column chunk.&lt;/p&gt;

&lt;p&gt;As shown in Figure 2, the structure of Parquet metadata mirrors that of the Parquet file: It contains entries for each row group, and each entry contains information for each column chunk within that row group. This means that the metadata size is O(row_group * column) and grows linearly with both the number of row groups and the number of columns.&lt;/p&gt;

&lt;p&gt;In addition to the information required to decode each column’s data, such as starting offset and encoding type, the metadata can optionally store min, max, and null counts for each column chunk. Query engines, such as &lt;a href="https://datafusion.apache.org/"&gt;Apache DataFusion&lt;/a&gt; and &lt;a href="https://github.com/duckdb/duckdb"&gt;DuckDB&lt;/a&gt;, can use these statistics to skip decoding row groups and data pages entirely.&lt;/p&gt;

&lt;p&gt;The metadata is encoded in the &lt;a href="https://thrift.apache.org/"&gt;Apache Thrift&lt;/a&gt; format, which is similar to &lt;a href="https://protobuf.dev/"&gt;protobuf&lt;/a&gt;. Thrift uses variable-length encoding to achieve high space efficiency. Still, the variable-length encoding requires Parquet readers to fetch and potentially examine the entire metadata footer before reading any content. For example, it is not possible to jump directly to the location in the metadata required to read a single-row group without starting at the beginning.&lt;/p&gt;

&lt;p&gt;Reading Parquet metadata in parquet-rs’s &lt;a href="https://docs.rs/parquet/latest/parquet/arrow/arrow_reader/index.html"&gt;ArrowReader&lt;/a&gt; requires three steps:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Load the metadata from storage to memory&lt;/li&gt;
  &lt;li&gt;Decode the thrift-formatted data into in-memory structures: &lt;a href="https://docs.rs/parquet/latest/parquet/file/metadata/struct.ParquetMetaData.html"&gt;ParquetMetadata&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Build the Arrow &lt;a href="https://docs.rs/arrow/latest/arrow/datatypes/struct.Schema.html"&gt;Schema&lt;/a&gt; from the Parquet &lt;a href="https://docs.rs/parquet/latest/parquet/schema/types/struct.SchemaDescriptor.html"&gt;SchemaDescriptor&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The time required to load the metadata from storage depends on the storage device and ranges from 100us (local SSD) to 200ms (S3)&lt;sup&gt;2&lt;/sup&gt;. As shown in Figures 4 and 6, decoding from Thrift into Rust structures is by far the most time-consuming activity once the data is in memory. This makes sense as the decoding inflates a tiny compact encoding (Thrift) into point-accessible in-memory Rust structures. Transforming the SchemaDescriptor into Arrow Schema also requires a small amount of CPU time.&lt;/p&gt;

&lt;h2 id="testbed"&gt;Testbed&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Implementation&lt;/strong&gt;: We experimented with parquet–rs&lt;sup&gt;3&lt;/sup&gt;, a Rust implementation of Parquet, version 51.0.0. We repeat each experiment five times for each file and report the average time of the last four executions to exclude the impact of caching. You can find the benchmark code &lt;a href="https://github.com/XiangpengHao/Parquet-Gym/blob/main/format-study/src/bin/wide_table_bench.rs"&gt;here&lt;/a&gt;. We ran the benchmark on an AMD 7600X processor clocked at 5.4 GHz with a 32 MB L3 cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workload&lt;/strong&gt;: We generated several Parquet files with between 10 to 100,000&lt;sup&gt;4&lt;/sup&gt; Float64 columns, mimicking machine learning workloads. As we focus on the metadata, we simply write the same repeated value multiple times for the data. Each Parquet file contains ten-row groups, and because each row group includes all columns, the Parquet metadata encodes 10 * column_count individual ColumnChunk structures. To study the impact of including statistics, we tested three configurations: no statistics, chunk-level statistics, and page-level statistics (the default in parquet-rs).&lt;/p&gt;

&lt;h2 id="results"&gt;Results&lt;/h2&gt;
&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/016af77435144b8d9281820a69f0dd95/894bb421b43eea14020722cdbcb9c4de/unnamed.png" alt="" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 3&lt;/strong&gt;:  Metadata decode time and size for parquet-rs vs the number of Float64 columns in the Parquet file. Note both x and y axes are log scale.&lt;/p&gt;

&lt;p&gt;Figure 3 plots the relationship between metadata size and decode time as the number of columns increases from 10 to 100,000. As expected, the metadata size and decode time are linearly proportional to the number of columns in the Parquet file.
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/0edc46b2f622419d9d52dadbc5ccf848/c481f32b333f96738fe28a1939d1538b/unnamed.png" alt="" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 4:&lt;/strong&gt; Metadata decode time and size for parquet-rs for different statistics levels. The metadata decode time chart (left) also illustrates the time breakdown between Thrift decoding and creating the Arrow Schema (see Figure 6 for a more detailed breakdown).
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/dc0bdef48fe0428b8307f0ac85b1675b/6de92725e5562e999f831c93c4284f4e/unnamed.png" alt="" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 5&lt;/strong&gt;: Average per-column decode time and metadata size. The x-axis shows the stats level; the y-axis shows the time and size per column.&lt;/p&gt;

&lt;p&gt;In Figures 4 and 5, we examined the impact of statistics on metadata decode speed and size. Specifically, we configured&lt;sup&gt;5&lt;/sup&gt; the parquet-rs writer in one of three modes:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;none&lt;/strong&gt;: No statistics (&lt;a href="https://docs.rs/parquet/latest/parquet/file/properties/enum.EnabledStatistics.html#variant.None"&gt;EnabledStatistics::None&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;chunk&lt;/strong&gt;: The writer stores min value, max value, and null count statistics for each column chunk, for each row group (&lt;a href="https://docs.rs/parquet/latest/parquet/file/properties/enum.EnabledStatistics.html#variant.Chunk"&gt;EnabledStatistics::Chunk&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;page&lt;/strong&gt;: (the default setting of parquet-rs). In addition to the statistics written at the chunk level, the writer also writes structures from the &lt;a href="https://github.com/apache/parquet-format/blob/master/PageIndex.md"&gt;Parquet Page Index&lt;/a&gt;, which can &lt;a href="https://blog.cloudera.com/speeding-up-select-queries-with-parquet-page-indexes"&gt;speed up query processing&lt;/a&gt;  (&lt;a href="https://docs.rs/parquet/latest/parquet/file/properties/enum.EnabledStatistics.html#variant.Page"&gt;EnabledStatistics::Page&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Figure 5 charts these settings’ average per-column decode time and metadata size impact. Note that we expect the impact of disabling statistics for string columns to be even more significant than our float-based measurements, as string statistics values are typically larger.&lt;/p&gt;

&lt;p&gt;Our findings are as follows:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;With no statistics, metadata decodes 30% faster and is 30% smaller than the default level.&lt;/li&gt;
  &lt;li&gt;Page-level statistics only add minor overhead on top of chunk-level stats&lt;sup&gt;6&lt;/sup&gt;.&lt;/li&gt;
  &lt;li&gt;Building the Arrow schema takes negligible time.&lt;/li&gt;
  &lt;li&gt;Decoding Thrift takes twice as long as transforming Thrift structs to parquet-rs structs.&lt;/li&gt;
  &lt;li&gt;With minimal metadata (stats level &lt;strong&gt;none&lt;/strong&gt;), each additional column adds 5us to decode time and 700 bytes to storage requirements.&lt;/li&gt;
  &lt;li&gt;Our measurements are consistent, and the error bars are small.&lt;/li&gt;
  &lt;li&gt;parquet-rs 51.0.0 can decode Parquet metadata at 100MB/s (10ms to decode each megabyte of metadata).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Our findings suggest that software optimization efforts focused on improving the efficiency of Thrift decoding and Thrift to parquet-rs struct transformation will directly translate to improving overall metadata decode speed. 
&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/73ffebc0d9324ec7952bdb6ca260c8d1/f8c498607b4ac14b6f22f3d38ef2f200/unnamed.png" alt="" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 6&lt;/strong&gt;: Detailed analysis of metadata decode time breakdown.&lt;/p&gt;

&lt;p&gt;Finally, we analyzed decoding using a profiler and plotted the results in Figure 6.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;61% of the time is spent decoding and building &lt;a href="https://docs.rs/parquet/latest/parquet/format/struct.FileMetaData.html"&gt;FileMetaData&lt;/a&gt;, which includes the Parquet schema.&lt;/li&gt;
  &lt;li&gt;31% of the time is spent building &lt;a href="https://docs.rs/parquet/latest/parquet/file/metadata/struct.RowGroupMetaData.html"&gt;RowGroupMetaData&lt;/a&gt;, which transforms decoded Thrift data structures into parquet-rs data structures.&lt;/li&gt;
  &lt;li&gt;7% of the time is spent building an Arrow schema.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/8e0eb8fdac274d288d2ba0df51759073/b599dde6f77ebc13520a3a36f228276b/unnamed.png" alt="" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Figure 7:&lt;/strong&gt; Simple software engineering optimizations (e.g., better allocator, optimized in-memory layout, and SIMD acceleration) improved the decoding throughput by up to 75%.&lt;/p&gt;

&lt;p&gt;Finally, we spent a few days prototyping simple &lt;a href="https://www.google.com/url?q=https://github.com/apache/arrow-rs/pull/5856&amp;amp;sa=D&amp;amp;source=docs&amp;amp;ust=1717815578904130&amp;amp;usg=AOvVaw08pM7y8EaIOv-GaJRFgHMr"&gt;engineering optimizations&lt;/a&gt; (e.g., better allocator, optimized in-memory layout, and SIMD acceleration) to improve the decoding performance. Figure 7 shows that with even minor code changes (less than 100 loc, no change in API), we could improve decode performance by up to 75%. Other community members have also discussed and prototyped several &lt;a href="https://github.com/apache/arrow-rs/issues/5853"&gt;more involved changes&lt;/a&gt;, such as &lt;a href="https://github.com/apache/arrow-rs/issues/5775"&gt;reducing allocations&lt;/a&gt; (~2x improvement) and a more &lt;a href="https://github.com/apache/arrow-rs/issues/5854"&gt;optimized thrift decoder&lt;/a&gt; (another ~2x improvement)&lt;/p&gt;

&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;For workloads where metadata size and decode speed are of utmost concern, configuring the Parquet writer not to write statistics&lt;sup&gt;7&lt;/sup&gt; improves speed and space by 30% with no other software changes.&lt;/p&gt;

&lt;p&gt;While the Rust Parquet implementation is already reasonably fast for metadata decoding, the potential for significant speed improvements is within reach. By applying straightforward software engineering techniques, decoding speed can be enhanced by around a factor of 4. This investment in existing decoders is likely to yield a larger payoff than the creation of entirely new formats.&lt;/p&gt;

&lt;p&gt;Finally, in a more extensive overall system, where it is common to read data from object storage such as S3, we believe that metadata fetch and parsing is unlikely to be a significant bottleneck. Given that first-byte access latencies of 100ms-200ms are expected in object stores, by appropriately interleaving fetch and decode, metadata parsing is likely to be a small part of the overall execution time.&lt;/p&gt;

&lt;h2 id="future-work"&gt;Future work&lt;/h2&gt;

&lt;p&gt;There are several areas that we did not explore that deserve additional attention:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A similar performance comparison for other open source Parquet implementations (e.g. &lt;a href="https://github.com/apache/parquet-java"&gt;parquet-java&lt;/a&gt; and &lt;a href="https://github.com/apache/arrow/tree/main/cpp/src/parquet"&gt;parquet-cpp&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;A similar study of Parquet metadata size and decode speed for String / Binary columns. We expect the benefits from disabling statistics and optimized decoder implementations to be substantially higher for such columns because the values stored in the statistics are significantly larger.&lt;/li&gt;
  &lt;li&gt;A similar study with ‌newly proposed formats like &lt;a href="https://blog.lancedb.com/lance-v2/"&gt;Lance V2&lt;/a&gt; &lt;a href="https://github.com/facebookincubator/nimble"&gt;Nimble&lt;/a&gt; would help us understand how much better they are at handling large numbers of columns and what other tradeoffs may exist. In particular, these new formats incorporate lightweight metadata/statistics (e.g., smaller, decoupled metadata) and/or allow partial decoding, i.e., decode only the projected column rather than the entire metadata, which should permit much faster decode times.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Acknowledgments&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We would like to thank Raphael Taylor-Davies, &lt;a href="https://x.com/jhorstmann23"&gt;Jörn Horstmann&lt;/a&gt;, and Paul Dix for their helpful comments on earlier versions of this post.&lt;/p&gt;

&lt;hr /&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;For more, see the &lt;a href="https://lists.apache.org/thread/8xmxc76nd00624qqps6s1qw6lhv1qwv5"&gt;discussion&lt;/a&gt; on the dev@parquet.apache.org mailing list.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;See charts from &lt;a href="https://www.vldb.org/pvldb/vol16/p2769-durner.pdf"&gt;Exploiting Cloud Object Storage for High-Performance Analytics&lt;/a&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Caveat: we are biased being contributors and maintainers of parquet-rs&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Note that with the default writer settings, our testbed ran out of memory when writing the 100,000 column Parquet file. We found that the issue was resolved by setting the &lt;a href="https://docs.rs/parquet/latest/parquet/file/properties/struct.WriterProperties.html#method.data_page_row_count_limit"&gt;data_page_row_count&lt;/a&gt; &lt;a href="https://github.com/XiangpengHao/Parquet-Gym/blob/a3f91bb2cc2b5cd11880d13641937529edfae3bf/format-study/src/bin/generator.rs#L89"&gt;to 10,000&lt;/a&gt;. With the default (unlimited) data page row count, we &lt;a href="https://github.com/apache/arrow-rs/issues/5828"&gt;found&lt;/a&gt; the Parquet writer consumed over 80GB of memory. We have started a &lt;a href="https://github.com/apache/arrow-rs/issues/5797"&gt;discussion&lt;/a&gt; about changing this default as another common criticism of using Parquet with wide tables is that writers require a large memory buffer, but we think this may be due to the default writer settings.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;By setting &lt;a href="https://docs.rs/parquet/latest/parquet/file/properties/struct.WriterProperties.html#method.statistics_enabled"&gt;WriterProperties::statistics_enabled&lt;/a&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Note that parquet-rs reader does not create Rust structs from the PageIndex structures by &lt;a href="https://docs.rs/parquet/latest/parquet/arrow/arrow_reader/struct.ArrowReaderOptions.html#method.with_page_index"&gt;default&lt;/a&gt;, so the decode overhead would likely be higher if we were decoding this as well..&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Though of course this may impact query performance for workloads that would benefit from statistics (e.g. they have predicates on the affected columns).&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;
</description>
      <pubDate>Tue, 18 Jun 2024 08:00:00 +0000</pubDate>
      <link>https://www.influxdata.com/blog/how-good-parquet-wide-tables/</link>
      <guid isPermaLink="true">https://www.influxdata.com/blog/how-good-parquet-wide-tables/</guid>
      <category>Developer</category>
      <author>Xiangpeng Hao, Andrew Lamb (InfluxData)</author>
    </item>
    <item>
      <title>Making Most Recent Value Queries Hundreds of Times Faster</title>
      <description>&lt;p&gt;This post explains how databases optimize queries, which can result in queries running hundreds of times faster. While we focus on one specific query type that is important to InfluxDB 3, the optimization process we describe is the same for any database.&lt;/p&gt;

&lt;h1 id="optimizing-a-query-is-like-playing-with-lego"&gt;Optimizing a query is like playing with Lego&lt;/h1&gt;

&lt;p&gt;You can come up with different structures when playing with the same set of Lego pieces, as shown in Figure 1. While you often use the same basic bricks to build whatever structure you want, there are times when you need a different type of shape (e.g., tiny star) for a specific project.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/2RtVIx96OBI6OvTvsRxNbn/48174cf3d415f81308ffb55676cd35b8/lego-structure.jpg" alt="lego-structure" /&gt;&lt;/p&gt;

&lt;p class="is-italic has-text-centered" style="font-size: 16px;"&gt;Figure 1: Two different structures built from the same basic Lego squares and rectangles&lt;/p&gt;

&lt;p&gt;In a database, running a query means running a &lt;strong&gt;query plan,&lt;/strong&gt; a tree of different operators that process and stream data. Each operator is like a Lego brick: depending on how they are connected, they compute the same result but with different performances. Much of query optimization involves swapping or moving existing operators around to form a better query plan, but on some rare occasions, a new special case operator is needed to do the job better.&lt;/p&gt;

&lt;p&gt;Let’s walk through an example of optimizing a query by creating a specialized operator and recombining existing operators to form a query plan with superior performance.&lt;/p&gt;

&lt;h1 id="querying-the-most-recent-values"&gt;Querying the most recent value(s)&lt;/h1&gt;

&lt;p&gt;As a time series database, one common use case is managing signal data from many devices. A common question is: “&lt;em&gt;What is the signal last sent by a specified device (e.g., device number 10)?”&lt;/em&gt;  The answer to this question (or variations of it) is often used to drive a UI or monitoring dashboard. Using SQL, a query that can answer this question is:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT   …
FROM     signal
WHERE    device = 10
       AND time BETWEEN now() - interval 'X days' and now()
ORDER BY time DESC
LIMIT    1;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The filter &lt;code&gt;time BETWEEN now() - interval 'X days' and now()&lt;/code&gt; narrows down the question a bit: “&lt;em&gt;What is the signal last sent by device number 10 for the last X days?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;While this query is simple, actual queries can be more complicated. For example, “find the average value over the last five values,” so our solution must be able to handle these more general queries as well.&lt;/p&gt;

&lt;p&gt;It is also important that these queries return results in milliseconds, because every device owner requests values frequently. One challenge is the owner does not know when the last signal happened—it could be five minutes ago or several months ago. Thus, the value of time range &lt;code&gt;X&lt;/code&gt; can be very large and the query runtime long. Unless users take great care writing their query, it will read and process substantial data, increasing the query return time.&lt;/p&gt;

&lt;p&gt;Unlike traditional relational or time series databases, InfluxDB 3.0 stores data in &lt;a href="https://parquet.apache.org/"&gt;Parquet&lt;/a&gt; files rather than custom file formats and specialized indexes. Our mission was to make this class of queries run in milliseconds regardless of how large the time range X is without introducing special indexes.&lt;/p&gt;

&lt;h2 id="runtimes-before-and-after-improvements"&gt;Runtimes before and after improvements&lt;/h2&gt;

&lt;p&gt;Before explaining our approach, let us look at the results in Figure 2. Blue represents the normalized runtimes of the queries in different time ranges before the improvements, and green represents the ones after. Queries timeout after running for 30 units, so the actual runtimes of queries that reached 30 units were even higher. As the chart shows, our improvements made large-time-range queries run hundreds of times faster and brought the runtimes of all queries down to the level requested by our customers.&lt;/p&gt;

&lt;p&gt;&lt;img src="//images.ctfassets.net/o7xu9whrs0u9/7IfaM65vUEb0rJPNqMInvW/f0a9ccc91b9ca6af6f2dc80c674a47e9/Before-after.jpg" alt="Before-after" width="650" height="auto" /&gt;&lt;/p&gt;

&lt;p class="is-italic has-text-centered" style="font-size: 16px;"&gt;Figure 2:  Query runtimes of before and after Improvements&lt;/p&gt;

&lt;p&gt;Let’s move on to how we achieved this.&lt;/p&gt;

&lt;h2 id="query-plan-before-improvements"&gt;Query plan before improvements&lt;/h2&gt;

&lt;p&gt;Figure 3 shows a simplified version of the query plan before the improvements using a &lt;strong&gt;sort merge&lt;/strong&gt; &lt;strong&gt;algorithm&lt;/strong&gt;. We read a query plan from the bottom up. The input includes four files that four corresponding &lt;strong&gt;scan&lt;/strong&gt; operators read in parallel. Each scan output goes through a corresponding &lt;strong&gt;sort&lt;/strong&gt; operator that orders the data by descending timestamp. Four sorted output streams are sent to a &lt;strong&gt;merge&lt;/strong&gt; operator that combines them into a single sorted stream and stops after the number of limit rows, which is &lt;code&gt;1&lt;/code&gt; in this example. There are many more files in the &lt;code&gt;signal&lt;/code&gt; table, but InfluxDB first prunes unnecessary files based on the filters of the query.&lt;/p&gt;

&lt;p&gt;&lt;img style="padding-top: 30px;" src="//images.ctfassets.net/o7xu9whrs0u9/7KskmsN8d9l2cvMnrmUcqg/90c20bd031cb0851c9f47535b9864689/Query_plan_using_sort_merge_algorithm.png" alt="Query plan using sort merge algorithm" /&gt;&lt;/p&gt;
&lt;p class="is-italic has-text-centered" style="font-size: 16px;"&gt;Figure 3: Query plan using sort merge algorithm&lt;/p&gt;

&lt;p&gt;When files overlap, InfluxDB may need to‌ &lt;a href="https://www.influxdata.com/blog/using-deduplication-eventually-consistent-transactions/"&gt;deduplicate&lt;/a&gt; data. Figure 4 shows a more accurate plan that sorts data of overlapped files, File 2 and File 3, together and deduplicates them before sending data to the sort and merge operators.&lt;/p&gt;

&lt;p&gt;&lt;img style="padding-top: 30px;" src="//images.ctfassets.net/o7xu9whrs0u9/7FP49Aj7Z2w6eXcXUupC53/a8840af34737b625e0d7d9613ac83b4c/InfluxDB_query_plan_using_sort_merge_algorithm_but_grouping_overlapped_files_first.png" alt="InfluxDB query plan using sort merge algorithm but grouping overlapped files first" /&gt;&lt;/p&gt;
&lt;p class="is-italic has-text-centered" style="font-size: 16px;"&gt;Figure 4: InfluxDB query plan using sort merge algorithm but grouping overlapped files first&lt;/p&gt;

&lt;p&gt;The optimization described in the next section only depends on the operators at the top of the plan, and thus, the simplified plan in Figure 5 more clearly illustrates the solution. Note that we omitted many other details of the plan—for example, the Sort operator does not sort the &lt;em&gt;entire&lt;/em&gt; file but simply retains the &lt;a href="https://docs.rs/datafusion/latest/datafusion/physical_plan/struct.TopK.html#background"&gt;Top “K” rows&lt;/a&gt; due to the &lt;code&gt;LIMIT 1&lt;/code&gt; in the query.&lt;/p&gt;

&lt;p&gt;Note that the data streams going into the merge operator do not overlap.&lt;/p&gt;

&lt;p&gt;&lt;img style="padding-top: 30px;" src="//images.ctfassets.net/o7xu9whrs0u9/3YzjJQXB2uW2ZJEh6N5T7F/5c62e5bbeb56e3dd0988be2cab18bcd6/Figure-5.png" alt="Figure-5" /&gt;&lt;/p&gt;
&lt;p class="is-italic has-text-centered" style="font-size: 16px;"&gt;Figure 5: The top part of the plan that includes non-overlapped data streams to merge operator&lt;/p&gt;

&lt;h2 id="analyzing-the-plan-and-identifying-improvements"&gt;Analyzing the plan and identifying improvements&lt;/h2&gt;

&lt;p&gt;Normally when merging multiple streams, all inputs must be known before producing any output (as the first row might come from any of the inputs). This implies in the above plan that we must read and sort all the input streams. However, if we know the time ranges of the streams do not overlap, we can simply read and sort the streams one by one, stopping once we find the required number of rows. Not only is this less work than‌ merging the data, but if the number of required rows is small, it is likely only a single stream must be read.&lt;/p&gt;

&lt;p&gt;Thankfully, InfluxDB has statistics about the time ranges of the data in each file before reading them and groups overlapped files to produce non-overlapped streams, as shown in Figure 5. So, we can apply this observation to make a faster query plan without additional indexes or statistics. However, the behavior of reading streams one by one, stopping when the limit is hit is no longer a &lt;strong&gt;merge&lt;/strong&gt;. We needed a new operator.&lt;/p&gt;

&lt;h2 id="new-query-plan"&gt;New query plan&lt;/h2&gt;

&lt;p&gt;With the observations above in mind, Figure 6 illustrates the new query plan:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;The non-overlapped data streams are sorted by time, descending.&lt;/li&gt;
  &lt;li&gt;A new operator, &lt;strong&gt;ProgressiveEval,&lt;/strong&gt; replaces the &lt;strong&gt;merge&lt;/strong&gt; operator&lt;strong&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The new ProgressiveEval operator pulls data from its input streams sequentially and stops when it reaches the requested limit. The big difference between &lt;strong&gt;ProgressiveEval&lt;/strong&gt; and &lt;strong&gt;merge&lt;/strong&gt; operators is that the &lt;strong&gt;merge&lt;/strong&gt; operator can only start merging data &lt;strong&gt;after all&lt;/strong&gt; its input &lt;strong&gt;sort&lt;/strong&gt; operators complete, while &lt;strong&gt;ProgressiveEval&lt;/strong&gt; can start pulling data immediately after the&lt;strong&gt;first sort&lt;/strong&gt; operator finishes.&lt;/p&gt;

&lt;p&gt;&lt;img style="padding-top: 30px;" src="//images.ctfassets.net/o7xu9whrs0u9/2HpuKw5qq5D2hmwxBpw9lS/9b247c9b8fecb148c617285f7ae72b23/Figure-6.png" alt="Figure-6" /&gt;&lt;/p&gt;
&lt;p class="is-italic has-text-centered" style="font-size: 16px;"&gt;Figure 6: Optimized Query plan reads data progressively, stopping early when the limit is reached&lt;/p&gt;

&lt;p&gt;When the query plan in Figure 6 executes, it only runs the operators shown in Figure 7. InfluxDB has a pull-based executor (&lt;a href="https://docs.rs/datafusion/latest/datafusion/index.html#execution"&gt;based on Apache Arrow DataFusion’s Execution&lt;/a&gt;), which means that when &lt;strong&gt;ProgressiveEval&lt;/strong&gt; starts, it will ask the first &lt;strong&gt;sort,&lt;/strong&gt; which in turn asks its inputs for data. The &lt;strong&gt;sort&lt;/strong&gt; then performs the sort and sends the sorted results up to ProgressiveEval.&lt;/p&gt;

&lt;p&gt;&lt;img style="padding-top: 30px;" src="//images.ctfassets.net/o7xu9whrs0u9/pkPxbYrsfCye8tK8TY8MN/ee1c6e11d732414505eab89b87f5b365/Figure-7.png" alt="Figure-7" /&gt;&lt;/p&gt;
&lt;p class="is-italic has-text-centered" style="font-size: 16px;"&gt;Figure 7: The &lt;b&gt;needed&lt;/b&gt; execution operators if the latest signal of the specified device is in the latest-time-range file&lt;/p&gt;

&lt;p&gt;Due to the &lt;code&gt;device = 10&lt;/code&gt; parameter, the query filters data while scanning, and we do not know in which file contains the latest signal of device number 10. In addition, because it takes time for each &lt;strong&gt;sort&lt;/strong&gt; operator to complete, when &lt;strong&gt;ProgressiveEval&lt;/strong&gt; pulls data from a stream, it also starts executing the &lt;em&gt;next&lt;/em&gt; stream to prefetch data that is necessary if the first stream doesn’t contain the desired rows.&lt;/p&gt;

&lt;p&gt;Figure 8 shows that when pulling data from Stream 1 of the first &lt;strong&gt;Sort&lt;/strong&gt;, the second &lt;strong&gt;Sort&lt;/strong&gt; executes simultaneously so that data from Stream 2 is ready if Stream1 does not include the requested data from device number 10. If the data from device number 10 is in Stream 1, &lt;strong&gt;ProgressiveEval&lt;/strong&gt; stops as soon as it hits the limit and cancels Stream 2. If data from &lt;strong&gt;ProgressiveEval&lt;/strong&gt; pulls data from Stream 2, it also begins pre-executing the &lt;strong&gt;Sort&lt;/strong&gt; of Stream 3, and so on.&lt;/p&gt;

&lt;p&gt;&lt;img style="padding-top: 30px;" src="//images.ctfassets.net/o7xu9whrs0u9/1kcqJqhvYHLRyizKiDF397/bac21e45feb01dc60c681dd0499e2dfa/Figure-8.png" alt="Figure-8" /&gt;&lt;/p&gt;
&lt;p class="is-italic has-text-centered" style="font-size: 16px;"&gt;Figure 8: The &lt;b&gt;actual&lt;/b&gt; execution operators if the latest signal of the specified device is in the latest-time-range file&lt;/p&gt;

&lt;h2 id="analyzing-the-benefits-of-the-improvements"&gt;Analyzing the benefits of the improvements&lt;/h2&gt;

&lt;p&gt;Let’s compare the original plan in Figure 5 and the optimized plan in Figure 8:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Returns Results Faster&lt;/strong&gt;: The original plan must scan and sort all files that may contain data, regardless of the number of rows needed, before producing results. Thus, the longer the time range, the more files there are to read, and the slower the original plan is. This explains why our results show improvements for longer time ranges.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Fewer Resources and Improved Concurrency&lt;/strong&gt;: In addition to producing data more quickly, the optimized plan requires far less memory and CPU—it typically will scan and sort only two files (the most recent one and pre-fetching the next most recent one). This means more queries can run concurrently with the same resources.&lt;/li&gt;
&lt;/ol&gt;

&lt;h1 id="type-of-queries-that-benefit-from-this-work"&gt;Type of queries that benefit from this work&lt;/h1&gt;

&lt;p&gt;At this time (March 2024), this optimization only works on one type of query, &lt;strong&gt;“What are the most/least recent values&lt;/strong&gt; …?” In other words, the SQL of the query must include &lt;code&gt;ORDER BY time DESC/ASC LIMIT n&lt;/code&gt; where ‘&lt;em&gt;n&lt;/em&gt;’ can be any number and the time can be ordered ascending or descending. All other supported SQL queries will work but may not benefit from this optimization. We continue to work on improving them.&lt;/p&gt;

&lt;h1 id="conclusion"&gt;Conclusion&lt;/h1&gt;

&lt;p&gt;The optimization not only makes the most recent value queries faster but also reduces ‌resource usage and increases the concurrency level of the system. In general, if a query plan includes &lt;strong&gt;sort merge&lt;/strong&gt; on &lt;strong&gt;potentially non-overlapped data streams&lt;/strong&gt;, this optimization is applicable. We have found many query plans in this category and are working on improving them.&lt;/p&gt;

&lt;p&gt;We would like to thank Paul Dix for suggesting this design based on the progressive scan behavior of Elastic in the ELK stack.&lt;/p&gt;
</description>
      <pubDate>Mon, 18 Mar 2024 08:00:00 +0000</pubDate>
      <link>https://www.influxdata.com/blog/making-recent-value-queries-hundreds-times-faster/</link>
      <guid isPermaLink="true">https://www.influxdata.com/blog/making-recent-value-queries-hundreds-times-faster/</guid>
      <category>Developer</category>
      <author>Nga Tran, Andrew Lamb (InfluxData)</author>
    </item>
  </channel>
</rss>
