<?xml version="1.0" encoding="UTF-8"?>
<rss  xmlns:atom="http://www.w3.org/2005/Atom" 
      xmlns:media="http://search.yahoo.com/mrss/" 
      xmlns:content="http://purl.org/rss/1.0/modules/content/" 
      xmlns:dc="http://purl.org/dc/elements/1.1/" 
      version="2.0">
<channel>
<title>Xiangpeng&#39;s blog</title>
<link>https://blog.xiangpeng.systems/</link>
<atom:link href="https://blog.xiangpeng.systems/index.xml" rel="self" type="application/rss+xml"/>
<description></description>
<generator>quarto-1.9.38</generator>
<lastBuildDate>Mon, 08 Jun 2026 00:00:00 GMT</lastBuildDate>
<item>
  <title>A system programmer’s guide to LLM inference</title>
  <link>https://blog.xiangpeng.systems/posts/how-to-llm-inference/</link>
  <description><![CDATA[ 





<div class="callout callout-style-simple callout-warning no-icon callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon no-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Warning</span>Acknowledgments
</div>
</div>
<div class="callout-body-container callout-body">
<p>Let’s take a moment to thank my PhD sponsors: <a href="https://influxdata.com">InfluxData</a>, <a href="https://www.bauplanlabs.com">Bauplan</a>, <a href="https://spiraldb.com">SpiralDB</a>, and the taxpayers of the State of Wisconsin and the federal government.</p>
</div>
</div>
<p>LLMs have become so important that I (probably you as well) want to understand them better, and the best way to learn is to build one.</p>
<p>In this blog post, I’ll share what I’ve learned about LLM inference, from the perspective of a systems programmer.</p>
<p>I pick the model <a href="https://huggingface.co/unsloth/Qwen3.6-35B-A3B-MTP-GGUF"><code>Qwen3.6-35B-A3B-UD-Q4_K_M.gguf</code></a> so that it runs on most machines but is also complex enough to count as a “modern LLM” <sup>1</sup>. This is the only model to support here. By the end, we’ll be able to run prefill at 100 tokens/s and decode at 15 tokens/s, not so bad on a CPU-only machine.</p>
<div class="no-row-height column-margin column-container"><div id="fn1"><p><sup>1</sup>&nbsp;The model was released <a href="https://www.marktechpost.com/2026/04/16/qwen-team-open-sources-qwen3-6-35b-a3b-a-sparse-moe-vision-language-model-with-3b-active-parameters-and-agentic-coding-capabilities/">in April 2026</a>.</p></div></div><p>This blog covers most of the important parts of a local LLM inference engine:</p>
<ol type="1">
<li>The LLM architecture</li>
<li>Quantization</li>
<li>Fast matrix multiplication</li>
<li>KV cache</li>
</ol>
<p>But does not cover:</p>
<ol type="1">
<li>GPU acceleration, this is a pure CPU inference engine. I’ll probably do a GPU follow up later.</li>
<li>No MTP (speculative decoding), because we are not GPU yet.</li>
<li>Things that are {vendor-specific | closed-source}, e.g., CUDA, are not covered</li>
</ol>
<section id="how-to-read-the-name-qwen3.6-35b-a3b-ud-q4_k_m.gguf" class="level2">
<h2 class="anchored" data-anchor-id="how-to-read-the-name-qwen3.6-35b-a3b-ud-q4_k_m.gguf">How to read the name? Qwen3.6-35B-A3B-UD-Q4_K_M.gguf</h2>
<p><strong>Qwen</strong> (<a href="https://en.wikipedia.org/wiki/Qwen">pronounced /kwɛn/</a>, like “when” with a “kw”) is the model family name; it comes from Alibaba, a not so popular Chinese company (in tech world).</p>
<p><strong>3.6</strong> is the version number; previous versions were 3.5 and 3, so the family has been around for a while.</p>
<p><strong>35B</strong> is the model size. 35 <strong>B</strong>illion is the number of parameters; if each parameter were 8 bits (fp8 or int8), the model would be about 35 GB.</p>
<p><strong>A3B</strong> means the model <strong>a</strong>ctivates <strong>3</strong> <strong>B</strong>illion parameters to generate a token. It also implies the model is an MoE (Mixture of Experts) model — unlike dense models where all parameters are activated for every token.</p>
<p><strong>UD-Q4_K_M</strong> means the model is <strong>q</strong>uantized to <strong>4</strong> bits (Q4), and <code>K_M</code> is the quantization scheme (there are <a href="https://github.com/ggml-org/llama.cpp/wiki/Tensor-Encoding-Schemes">many ways to quantize</a> a model). <code>UD</code> stands for <a href="https://unsloth.ai/docs/basics/unsloth-dynamic-2.0-ggufs">Unsloth Dynamic quantization</a>; Unsloth is a company that produces these quantized variants.</p>
<p><strong>gguf</strong> is the model file format. Unlike <a href="https://huggingface.co/docs/safetensors/index"><code>safetensors</code></a>, <a href="https://github.com/ggml-org/ggml/blob/master/docs/gguf.md"><code>gguf</code></a> is a self-contained format: a single file holds everything you need to run the model. In practice, it’s just metadata plus the model weights.</p>
</section>
<section id="whats-inside-the-file" class="level2">
<h2 class="anchored" data-anchor-id="whats-inside-the-file">What’s inside the file?</h2>
<p>The file format is pretty boring (as intended): some metadata describing the model, followed by pairs of tensor info and tensor data, as shown in Figure&nbsp;1.</p>
<div id="fig-gguf-layout" class="quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-gguf-layout-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://blog.xiangpeng.systems/posts/how-to-llm-inference/gguf-layout.excalidraw.png" class="img-fluid figure-img" style="width:50.0%">
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-gguf-layout-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure&nbsp;1: The GGUF file layout
</figcaption>
</figure>
</div>
<p>The tensor info is stored at the beginning of the file, and points to the actual tensor data through the <code>offset</code> and <code>len</code> fields:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb1-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">struct</span> TensorInfo <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb1-2">    name<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">String</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">// e.g., `blk.3.attn_norm_weight`</span></span>
<span id="cb1-3">    shape<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Vec</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">u64</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span></span>
<span id="cb1-4">    dtype<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">u32</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb1-5">    offset<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">u64</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb1-6">    len<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">u64</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb1-7"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
<p>Note that <code>dtype</code> is a per-tensor field, which means two tensors in the same file may use different data types (i.e., different quantization schemes). This lets us store important tensors (e.g., the embedding weights) with more bits for higher precision, while quantizing the rest more aggressively. As you might guess, per-tensor quantization is a fine art.</p>
<p>A closer look at the quantization schemes shows that the model has 6 different types of tensors, more than half of the bytes are quantized to Q4_K:</p>
<pre><code>  BF16      2 tensors         1.00 MB
  F32     368 tensors        99.78 MB
  Q4_K     82 tensors     11808.00 MB
  Q5_K     38 tensors      6688.00 MB
  Q6_K      4 tensors      1027.85 MB
  Q8_0    259 tensors      1978.38 MB
  ------ ---- ---------    ----------
  total   753 tensors     21603.01 MB</code></pre>
<p>The metadata also encodes the model architecture: the number of layers, the model family, and so on.</p>
</section>
<section id="the-model-architecture" class="level2 page-columns page-full">
<h2 class="anchored" data-anchor-id="the-model-architecture">The model architecture</h2>
<p>The model belongs to the <a href="https://huggingface.co/Qwen/Qwen3-Next-80B-A3B-Instruct">Qwen3-Next family</a>; for background on the architecture lineage, see <a href="https://magazine.sebastianraschka.com/p/the-big-llm-architecture-comparison">Sebastian Raschka’s “Big LLM Architecture Comparison”</a>.</p>
<p>Figure&nbsp;2 is a simplified view of the architecture and its weight distribution. For every input token, the model first converts it into an embedding vector (with dimension 2048 for this model), then runs it through several layers of computation to produce the final output.</p>
<p>Qwen3.6 is not a typical transformer: it mixes the so-called <code>DeltaNet</code> layers with conventional <code>Attention</code> layers at a <a href="https://vllm.ai/blog/2025-09-11-qwen3-next">3:1 ratio</a>. We will get to the details later; the high-level intuition is that attention’s per-token state (the KV cache) grows linearly with sequence length and its compute is quadratic, while DeltaNet’s state is fixed-size — this helps the model scale to longer context.</p>
<p>All Qwen3.6 family models share this architecture; larger members of the family scale by adding more layers and using a wider hidden dimension (e.g., the Qwen3-Next-80B variant has 48 layers and a 2048 hidden dim).<sup>2</sup></p>
<div class="no-row-height column-margin column-container"><div id="fn2"><p><sup>2</sup>&nbsp;Qwen3.6-35B-A3B has 40 layers organized as 10 repetitions of (3 DeltaNet + 1 Attention) blocks, so 30 DeltaNet layers and 10 Attention layers. See <a href="https://apxml.com/models/qwen36-35b-a3b">the apxml spec page</a>.</p></div></div><div id="fig-model-arch" class="quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-model-arch-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://blog.xiangpeng.systems/posts/how-to-llm-inference/model-arch.png" class="img-fluid figure-img" style="width:100.0%">
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-model-arch-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure&nbsp;2: The Qwen3.6-35B-A3B model architecture and its weights distribution
</figcaption>
</figure>
</div>
<p>Table below shows the more detailed weights distribution:</p>
<pre><code>  Group                            Params         Stored    Share
-----------------------------------------------------------------
  Token embedding                508.56 M     515.31 MiB    2.44%
  DeltaNet layers                  1.01 B       1.01 GiB    4.92%
  Attention layers               272.66 M     276.35 MiB    1.31%
  MoE blocks                      32.36 B      18.43 GiB   89.44%
  Final output                   508.56 M     397.86 MiB    1.89%
-----------------------------------------------------------------
  Reported total                  34.66 B      20.60 GiB  100.00%</code></pre>
<p>MoE blocks dominate: they account for 89.4% of model size, and each MoE block is roughly 469 MB. However, each MoE block only activates 1/32 of its parameters (8 of 256 experts per token), while the other components fire for every token. So each token activates approximately 0.508B + 1.01B + 273M + <strong>32.36B / 32</strong> + 0.508B ≈ 3.3B parameters, which rounds to the “3B” in “A3B”.<sup>3</sup></p>
<div class="no-row-height column-margin column-container"><div id="fn3"><p><sup>3</sup>&nbsp;The official “3B active” figure is computed slightly differently — see the <a href="https://huggingface.co/Qwen/Qwen3.6-35B-A3B">Qwen3.6 model card</a>. The ballpark is the same.</p></div></div><p>There are two stages of LLM inference: prefill and decode. Prefill processes the input prompt (potentially thousands of tokens) in a single forward pass and populates the per-layer state (KV cache, DeltaNet state). It is usually compute-bound. Decode generates tokens one at a time, and on each step the full weight matrix must be streamed through the FPUs again, so it is usually memory-bound. We’ll come back to this distinction later.</p>
</section>
<section id="token-string" class="level2 page-columns page-full">
<h2 class="anchored" data-anchor-id="token-string">Token &lt;–&gt; String</h2>
<p>A token is an LLM-specific representation of a string. It isn’t fundamentally different from other representations like ASCII or Unicode, where <code>42</code> means <code>*</code> (in ASCII) and <code>U+1F3D4</code> means <code>🏔</code> (in Unicode).</p>
<p>The first thing to look at is the vocabulary table, stored in the <code>tokenizer.ggml.tokens</code> metadata field as a list of strings. The first 8 tokens of this model are: [<code>!</code>, <code>\</code>, <code>#</code>, <code>$</code>, <code>%</code>, <code>&amp;</code>, <code>'</code>, <code>(</code>]. So token id 0 refers to <code>!</code>, id 7 refers to <code>(</code>, and so on.</p>
<p>Note that different models may have different vocabulary tables. A smaller vocabulary means more tokens are needed to represent the same string. Modern LLMs tend to have larger vocabularies — this model has 151,936 vocabulary tokens, which exceeds <code>u16::MAX</code> (65,535), so the token index must be <code>u32</code>.<sup>4</sup></p>
<div class="no-row-height column-margin column-container"><div id="fn4"><p><sup>4</sup>&nbsp;Anthropic changed their tokenizer in Opus 4.7, which produces <a href="https://simonwillison.net/2026/Apr/20/claude-token-counts/">1.46× more tokens</a> on the same text input — a hidden ~46% price hike at the same per-token rate.</p></div></div><section id="from-token-to-string" class="level4">
<h4 class="anchored" data-anchor-id="from-token-to-string">From token to string</h4>
<p>Token-to-string is easy: look up each token in the vocabulary and concatenate the strings.</p>
<p>The one caveat is streaming output: a single token may represent only a partial UTF-8 sequence, so the decoder needs to buffer bytes until a complete codepoint can be emitted. This is especially common with non-ASCII scripts and emoji.</p>
</section>
<section id="from-string-to-token" class="level4">
<h4 class="anchored" data-anchor-id="from-string-to-token">From string to token</h4>
<p>String-to-token is more involved, but conceptually similar to how dictionary-based compression works. We start with the property that every byte in the input string has a single-token representation — by design — which guarantees that any input is representable.</p>
<p>Then we apply <a href="https://en.wikipedia.org/wiki/Byte_pair_encoding">byte-pair encoding (BPE)</a> merge rules; the goal is to greedily merge adjacent tokens into the longest sequences the vocabulary supports, which reduces the total token count.</p>
<details>
<summary>
Actually there are more details to consider
</summary>
<ol type="1">
<li><strong>Pre-tokenization.</strong> A pre-tokenizer first splits the input into smaller chunks (along whitespace, punctuation, digits, etc.) so that downstream BPE merges don’t span boundaries we’d prefer to keep separate — e.g., a single token that straddles two Chinese characters.</li>
<li><strong>BPE merge.</strong> We then run a BPE merge over adjacent tokens. It is comparatively expensive but reduces the total token count substantially.</li>
<li><strong>Chat templates.</strong> Special tokens like <code>&lt;|im_start|&gt;</code>, <code>&lt;|im_end|&gt;</code>, etc., delimit roles in a conversation; the model uses these to recognize turn structure.</li>
</ol>
</details>
<p>Roughly speaking, an LLM is a function:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb4-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> inference(input<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Vec</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">u32</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">u32</span></span></code></pre></div></div>
</section>
</section>
<section id="the-life-of-a-tensor" class="level2">
<h2 class="anchored" data-anchor-id="the-life-of-a-tensor">The life of a tensor</h2>
<p>Once we have the input tokens, the next step is to predict the next token.</p>
<p>There are lots of algorithms and math involved, but honestly, you don’t need to know most of it — you can think of inference as a chain of matrix multiplications. Knowing what RoPE, QKV, softmax, RMSNorm, etc. <em>do</em> is cool to talk about, but from an system programmer’s perspective it doesn’t really matter.</p>
<p>What matters is the <strong>shape of the tensors</strong>, because that determines the compute and memory requirements.</p>
<div id="fig-tensor-life" class="quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-tensor-life-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://blog.xiangpeng.systems/posts/how-to-llm-inference/tensor-life.png" class="img-fluid figure-img" style="width:90.0%">
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-tensor-life-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure&nbsp;3: The life of a tensor — shapes only. Blue arrows are matrix multiplications, grey arrows are everything else.
</figcaption>
</figure>
</div>
<p>Figure&nbsp;3 shows the life of a tensor; blue arrows are matrix multiplications, grey arrows are auxiliary math like normalization and softmax.</p>
<p>The first thing to care about is the embedding table, a 2D tensor of shape <code>[vocabulary_size, embedding_dim]</code>, in this case <code>[151936, 2048]</code>. Converting a token to an embedding is a simple lookup: slice out one row to get a <code>[1, 2048]</code> tensor. This <code>[1, 2048]</code>, usually called the activation tensor, is both the input and the output of every subsequent layer.</p>
<p>The final output projection converts the <code>[1, 2048]</code> activation into a <code>[1, 151936]</code> tensor, assigning a logit to each token in the vocabulary. We can pick the token with the highest logit as the next token, or use a sampling algorithm like top-k / top-p / temperature sampling to get variation.</p>
<p>The rest of the blocks — attention, DeltaNet, and the MoE blocks — all take <code>[1, 2048]</code> as input and produce <code>[1, 2048]</code> as output; internally each is a chain of matrix multiplications with some supplementary math like normalization and softmax.</p>
<p>The above describes a single token (the decode case); prefill is the same except the activation is <code>[n, 2048]</code> rather than <code>[1, 2048]</code>. The math is unchanged.</p>
</section>
<section id="reading-the-first-tensor" class="level2 page-columns page-full">
<h2 class="anchored" data-anchor-id="reading-the-first-tensor">Reading the first tensor</h2>
<p>In an ideal world, every tensor would be a simple f32 tensor, and matrix multiplication would be simple and well-optimized.</p>
<p>But f32 is too expensive: for our 35B model, going all-f32 would require 35B × 4 = 140 GB of memory. On top of that, f32 is overkill for practical LLM inference — we can drop to 8 bits with almost no loss of accuracy, and even 4 bits retains most of the information.<sup>5</sup></p>
<div class="no-row-height column-margin column-container"><div id="fn5"><p><sup>5</sup>&nbsp;Q4_K_M typically <a href="https://vucense.com/dev-corner/gguf-quantization-explained-q4-k-m-vs-q8-0-vs-f16-2026/">retains ~92% of the original FP16 model’s quality</a> at roughly 25% of the bytes; Q8_0 is “essentially lossless” with perplexity drift around 0.01.</p></div></div><p>Using fewer bits per weight not only reduces the memory footprint but often makes compute <em>faster</em>, especially for GEMV, where memory bandwidth is the bottleneck — less data to load means a shorter critical path.</p>
<p>The flip side is that the matmul itself gets more complicated, especially since the file contains a mix of quantization schemes. We’ll come back to that.</p>
<p>As listed earlier, the model has six tensor types; here we only discuss two representative ones: <code>Q8_0</code> and <code>Q4_K</code>.</p>
<section id="q8_0-block" class="level3">
<h3 class="anchored" data-anchor-id="q8_0-block">Q8_0 block</h3>
<p>Figure&nbsp;4 shows a <code>Q8_0</code> block. It’s the simplest quantization scheme: take an fp32 value, divide by a scale, and round to the nearest integer. Dequantization is the reverse — multiply the quantized value by the scale. The scale is chosen as <code>max(|w|) / 127</code>, so the quantized values land in <code>[-127, 127]</code>. Each block holds 32 weights plus a single fp16 scale, packed into 2 + 32 = 34 bytes (<a href="https://github.com/ggml-org/llama.cpp/wiki/Tensor-Encoding-Schemes">source: llama.cpp <code>block_q8_0</code> definition</a>).</p>
<div id="fig-q8-0" class="quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-q8-0-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://blog.xiangpeng.systems/posts/how-to-llm-inference/q8_0.png" class="img-fluid figure-img" style="width:80.0%">
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-q8-0-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure&nbsp;4: A <code>Q8_0</code> block: 32 weights stored in 34 bytes (8.5 bits / weight).
</figcaption>
</figure>
</div>
</section>
<section id="q4_k-block" class="level3">
<h3 class="anchored" data-anchor-id="q4_k-block">Q4_K block</h3>
<p><code>Q4_0</code> is similar to <code>Q8_0</code>, except it uses 4 bits per weight; in fact <code>Q4_0</code> is also widely used. But it has two problems.</p>
<p>The first problem is that it assumes weights are symmetrically distributed around 0, which is not always the case. If, for example, all weights in a block are positive and we still use a signed <code>int4</code> representation, we waste half of the representable range. To fix this, we shift the weights by their minimum, so that the shifted weights are all non-negative, and use an unsigned integer to represent them.</p>
<p>But now we need to store <em>two</em> fp16 values per block — the scale and the min. If only 32 weights share these two parameters, the overhead is large; if many more weights share them, a single outlier can dominate the scale and produce unbalanced quantization.</p>
<p>Figure&nbsp;5 shows the key idea behind <code>Q4_K</code>: each group of 32 weights (a sub-block) has its own scale and min, and a 256-weight super-block re-quantizes those sub-block scales and mins to 6 bits each (packed into 12 bytes for 8 pairs), with the super-block storing a pair of fp16 values (<code>d</code> and <code>dmin</code>) to decode them. See the <a href="https://github.com/ggml-org/llama.cpp/wiki/Tensor-Encoding-Schemes">llama.cpp tensor encoding wiki</a> for the exact layout.</p>
<div id="fig-q4-k" class="quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-q4-k-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://blog.xiangpeng.systems/posts/how-to-llm-inference/q4_k.png" class="img-fluid figure-img" style="width:90.0%">
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-q4-k-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure&nbsp;5: A <code>Q4_K</code> super-block: 256 weights stored in 144 bytes (4.5 bits / weight).
</figcaption>
</figure>
</div>
</section>
<section id="fp32-vs-fp16-vs-bf16-vs-fp8" class="level3 page-columns page-full">
<h3 class="anchored" data-anchor-id="fp32-vs-fp16-vs-bf16-vs-fp8">fp32 vs fp16 vs bf16 vs fp8?</h3>
<p>Now is a good time to talk about fp16<sup>6</sup> and bf16.</p>
<div class="no-row-height column-margin column-container"><div id="fn6"><p><sup>6</sup>&nbsp;fp16 is sometimes called f16.</p></div></div><p>As Figure&nbsp;6 shows, they’re really simple: every floating-point number has three parts — sign, exponent, and mantissa. The exponent controls the range of representable values; the mantissa controls the precision.</p>
<p><code>bf16</code> and <code>fp16</code> are two different ways to cut <code>fp32</code> in half: <code>bf16</code> keeps the 8-bit exponent (so the same range as fp32, but loses mantissa precision), while <code>fp16</code> shrinks the exponent to 5 bits (smaller range, but more mantissa bits). Similarly, <code>fp8 E5M2</code> keeps a 5-bit exponent, while <code>fp8 E4M3</code> trades one exponent bit for more precision.</p>
<div id="fig-float-formats" class="quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-float-formats-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://blog.xiangpeng.systems/posts/how-to-llm-inference/float-formats.png" class="img-fluid figure-img" style="width:80.0%">
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-float-formats-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure&nbsp;6: Float formats: just different recipes for combining sign, exponent, and mantissa.
</figcaption>
</figure>
</div>
<p>Use <code>fp16</code> when you need more precision, as in the per-super-block scale of our <code>Q4_K</code>; use <code>bf16</code> when you want to match fp32’s range and not worry about overflow — that’s why it’s popular for LLM training.</p>
<p>There’s something special about <code>bf16</code>: <a href="https://en.wikipedia.org/wiki/Bfloat16_floating-point_format">converting between <code>bf16</code> and <code>fp32</code> is almost trivial</a>. bf16 → fp32 is a 16-bit left shift (zero-pad the low mantissa bits); fp32 → bf16 is a 16-bit right shift (truncate or round-to-nearest the low 16 bits).</p>
</section>
<section id="how-to-structure-the-code" class="level3">
<h3 class="anchored" data-anchor-id="how-to-structure-the-code">How to structure the code?</h3>
<p>As we’ve discussed, we don’t care about the exact math here, but we do care about the shape of the matrices—and it’s better to encode what we care about directly into the type system.</p>
<p>So instead of writing code like this, which is very much like Python and makes me nervous: it’s unclear about the shape of the tensors, and whether their quantization scheme and shape matches.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb5-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> matmul(x<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> Tensor<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> w<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> Tensor) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> Tensor</span></code></pre></div></div>
<p>This is the interface I liked:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb6-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> matmul<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">const</span> IN<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">usize</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">const</span> OUT<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">usize</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> N<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>(</span>
<span id="cb6-2">      x<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>Act<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> N<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> IN<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span></span>
<span id="cb6-3">      w<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>QWeight<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> IN<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> OUT<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span></span>
<span id="cb6-4">) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> Act<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> N<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> OUT<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span></span></code></pre></div></div>
<p>Each tensor is strongly typed with its shape and quantization scheme. If the shapes of two tensors don’t match, the code won’t even compile!</p>
<p>We can similarly encode shapes into our layer and model definitions:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb7-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">pub</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">struct</span> GatedAttentionBlock<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>B<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> Backend<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb7-2">    k_weight<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> QWeight<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>B<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> D_MODEL<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> KV_DIM<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span></span>
<span id="cb7-3">    v_weight<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> QWeight<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>B<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> D_MODEL<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> KV_DIM<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span></span>
<span id="cb7-4">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">...</span></span>
<span id="cb7-5">    q_norm_gamma<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> Dense<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>B<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> HEAD_DIM<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span></span>
<span id="cb7-6">    k_norm_gamma<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> Dense<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>B<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> HEAD_DIM<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span></span>
<span id="cb7-7"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
<p>The rest of the code follows the same philosophy: encode the constraints we care about directly into the type system!</p>
</section>
</section>
<section id="matrix-multiplication" class="level2">
<h2 class="anchored" data-anchor-id="matrix-multiplication">Matrix multiplication</h2>
<p>Now comes the elephant in the room: matrix multiplication.</p>
<p>Before judging any approach, let’s first build a mental model of what performance we should expect. For two matrices <code>[a, b]</code> and <code>[b, c]</code>, it’s not too hard to see that:</p>
<pre class="text"><code>Optimal compute: 2 * a * b * c = 2abc FLOPs
Optimal memory: ab + bc + ac</code></pre>
<p>We have to read both inputs once, write the output once, and do <code>b</code> multiply-adds for each of the <code>a*c</code> output entries. Anything more is overhead.</p>
<p>Note: A matrix multiply with <code>[1, 2048]</code> (the decode case) is usually called “GEM<strong>V</strong>” (General Matrix-<strong>V</strong>ector Multiplication), while a multiply with <code>[n, 2048]</code> (the prefill case) is “GEM<strong>M</strong>” (General Matrix-<strong>M</strong>atrix Multiplication). The math is identical but the implementation is completely different, because shape matters. GEMV is memory-bound: each weight is loaded from RAM, multiplied with one activation, and never reused — the weight matrix is streamed row by row. GEMM is often compute-bound: each weight is now reused across <code>n</code> activation columns. Once <code>n</code> is large enough that we can keep a weight tile in registers / L1 and sweep it across multiple output columns, arithmetic intensity rises above the machine’s compute-to-bandwidth ratio, and the FPU becomes the bottleneck.</p>
<section id="naive-dequant-then-multiply-is-bad" class="level3">
<h3 class="anchored" data-anchor-id="naive-dequant-then-multiply-is-bad">Naive dequant-then-multiply is bad</h3>
<p>The simplest approach is to dequantize both sides to fp32, run the good old GEMM, and quantize the result back. This works, but it throws away most of what we got from quantization. Every 0.5-byte <code>Q4_K</code> weight expands to 4 bytes of fp32 traffic before we even start multiplying — an 8× blowup of the memory we worked so hard to compress. On top of that, we pay <code>~2 * (ab + bc)</code> extra FLOPs for the dequantization itself.</p>
<p>For GEMV this is catastrophic, because GEMV is memory-bound. If we expand weights to fp32, we’re back to where we started before quantizing anything. So we want to multiply directly on the quantized weights, without ever materializing fp32 in between.</p>
</section>
<section id="picking-the-activation-format" class="level3">
<h3 class="anchored" data-anchor-id="picking-the-activation-format">Picking the activation format</h3>
<p>The weights are pre-quantized in the file, but the activation tensor (<code>[1, 2048]</code> for decode, <code>[n, 2048]</code> for prefill) is computed at runtime, so we get to choose its format.</p>
<p><code>bf16</code> is the natural carrier between layers: same range as fp32, half the size, and softmax/norm are happy with it. But it’s not the right format <em>inside</em> the matmul itself, because no CPU has a native <code>bf16 × q4</code> multiplier.</p>
<p>The instruction we <em>do</em> have, on both <a href="https://en.wikichip.org/wiki/x86/avx512_vnni">AVX-VNNI (<code>VPDPBUSD</code>)</a> and ARM (<a href="https://developer.arm.com/architectures/instruction-sets/intrinsics/vusdotq_s32"><code>USDOT</code>, available via <code>i8mm</code></a>), is the integer dot product: <code>u8 × i8 → i32</code>, accumulating 4 byte-products into each 32-bit lane. On AVX-512 VNNI a single <code>VPDPBUSD</code> produces 16 <code>i32</code> lanes per 512-bit register; on AVX-VNNI (256-bit) it produces 8 lanes; on Neon with <code>USDOT</code> it produces 4 lanes per 128-bit register. So inside the matmul we want both operands as 8-bit integers:</p>
<ul>
<li><code>Q4_K</code> weights are 4-bit unsigned; unpacking each nibble into a <code>u8</code> is essentially free.</li>
<li>Activations need to be <code>i8</code>, so we quantize bf16 to <code>Q8_K</code>. Its 256-weight super-block lines up exactly with <code>Q4_K</code>, and <code>Q8_K</code> also caches per-sub-block sums of the activation, which — as we’ll see in a moment — is exactly what we need.</li>
</ul>
</section>
<section id="the-smart" class="level3">
<h3 class="anchored" data-anchor-id="the-smart">The smart</h3>
<p>We can now narrow the problem down to this: <strong>we have a 32-weight sub-block of <code>Q4_K</code> and the matching sub-block of <code>Q8_K</code>. How do we efficiently dot-product them?</strong></p>
<p>For <code>Q4_K</code>, each dequantized weight is <img src="https://latex.codecogs.com/png.latex?w_l%20=%20q_l%20%5Ccdot%20s_l%20-%20m_l">, where <img src="https://latex.codecogs.com/png.latex?q_l"> is <code>u8</code> (range 0–15) and <img src="https://latex.codecogs.com/png.latex?s_l,%20m_l"> are fp32 sub-block scale and min reconstructed from the 6-bit packed values and the super-block’s fp16 <code>d</code> and <code>dmin</code>. For <code>Q8_K</code> each activation is <img src="https://latex.codecogs.com/png.latex?w_r%20=%20q_r%20%5Ccdot%20s_r"> — Q8_K is symmetric, so there’s no min term.</p>
<p>The dot product over the sub-block expands as:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Csum_%7Bi=0%7D%5E%7B31%7D%20w_l%5Bi%5D%20%5Ccdot%20w_r%5Bi%5D%0A%5C;=%5C;%20s_l%20%5Ccdot%20s_r%20%5Ccdot%20%5Cunderbrace%7B%5Csum_i%20q_l%5Bi%5D%20%5Ccdot%20q_r%5Bi%5D%7D_%7B%5Ctext%7Bu8%20%C3%97%20i8%20integer%20dot%7D%7D%0A%5C;-%5C;%20m_l%20%5Ccdot%20s_r%20%5Ccdot%20%5Cunderbrace%7B%5Csum_i%20q_r%5Bi%5D%7D_%7B%5Ctext%7Bi8%20sum,%20precomputed%20by%20Q8%5C_K%7D%7D%0A"></p>
<p>Two integer reductions, then two multiply-adds in fp32. The expensive part — the 32-term dot product — never leaves integer land, which is exactly what the SIMD instruction is good at. The second term, <img src="https://latex.codecogs.com/png.latex?%5Csum%20q_r">, is already cached inside the <code>Q8_K</code> block (the <a href="https://deepwiki.com/ggml-org/llama.cpp/6.3-quantization-techniques"><code>bsums</code> field</a>) when we quantized the activation, so it’s just a load, not a runtime reduction. This is the whole reason <code>Q8_K</code> exists.</p>
</section>
<section id="putting-it-together" class="level3">
<h3 class="anchored" data-anchor-id="putting-it-together">Putting it together</h3>
<p>You don’t need to understand every detail here. The key idea is that there’s a storage reality — weights are quantized to save memory — and there’s a compute reality — hardware can only do certain operations on certain shapes.</p>
<p>The goal of everything above is a delicate balance: nudge both sides a bit, pick a good instruction and move the formats a little bit.</p>
<div id="fig-matrix-mat" class="quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-matrix-mat-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://blog.xiangpeng.systems/posts/how-to-llm-inference/matrix-mat.png" class="img-fluid figure-img" style="width:80.0%">
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-matrix-mat-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure&nbsp;7: The key to efficient matrix multiplication: pick a fast instruction, then nudge the operand formats to match its requirements.
</figcaption>
</figure>
</div>
</section>
</section>
<section id="the-state-kv-cache-and-deltanet-state" class="level2">
<h2 class="anchored" data-anchor-id="the-state-kv-cache-and-deltanet-state">The state: KV cache and DeltaNet state</h2>
<p>As a systems programmer, I don’t love the name “KV cache” — it sounds like a key-value store (e.g., Redis), but it’s neither a key nor a value in that sense. Here, <code>K</code> and <code>V</code> are the names of two matrices in the attention layer.</p>
<p>Recall that LLM inference is a function:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb9-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> inference(input<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Vec</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">u32</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">u32</span></span></code></pre></div></div>
<p>which is stateless — so where does the state come from? Well, because we’re not generating a single token but a sequence of them, the real loop looks more like this:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb10-1"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">loop</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb10-2">  <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> token <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> inference(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>input)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb10-3">  input<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>push(token)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb10-4">  <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> token <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> EOS <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb10-5">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">break</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb10-6">  <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb10-7"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
<p>Each inference appends one token to the input, so the next call processes a longer input. The observation is that most of the per-token state from prior inferences can be reused — recomputing it on every step is wasted work. KV cache is the most well-known such state; DeltaNet state is specific to the Qwen3-Next family.</p>
<p>Figure&nbsp;8 shows the state of the Qwen model: KV cache and DeltaNet state. Note that both states are per-layer, not per model. For each attention layer, the KV cache stores a <code>K</code> matrix of shape <code>[n, 512]</code> and a <code>V</code> matrix of shape <code>[n, 512]</code>, where <code>n</code> is the number of tokens seen so far (Qwen3.6-35B-A3B uses 2 KV heads × 256 head dim = 512). The KV cache grows linearly with the number of tokens, so at bf16 each new token adds 2 × 512 × 2 bytes = 2 KB per attention layer. With 10 attention layers (out of 40 total layers), each token costs 20 KB of KV-cache memory.</p>
<p>The DeltaNet state is a fixed-shape matrix <code>[4096, 128]</code> of fp32 values, so 4096 × 128 × 4 = 2 MB per layer regardless of context length. That’s the whole point of DeltaNet: its state has a fixed size, so memory and compute stay bounded even on long-context workloads.</p>
<div id="fig-kv-cache" class="quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-kv-cache-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://blog.xiangpeng.systems/posts/how-to-llm-inference/kv-cache.png" class="img-fluid figure-img" style="width:50.0%">
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-kv-cache-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure&nbsp;8: The state of the LLM model: KV cache and DeltaNet state. Both states are per-layer, not per-model.
</figcaption>
</figure>
</div>
<p>Note that each token adds exactly one row to the KV cache, and that row depends only on the preceding tokens. This means “java” and “javascript” share the same first four KV-cache rows (assuming each character is a token), which is very convenient: we can save the KV cache for “javascript” and slice off the first four rows to get the KV cache for “java”. This is what powers prefix caching in production serving systems.</p>
</section>
<section id="performance" class="level2">
<h2 class="anchored" data-anchor-id="performance">Performance</h2>
<section id="fuse-kernels" class="level3">
<h3 class="anchored" data-anchor-id="fuse-kernels">Fuse kernels</h3>
<p>Kernel fusion combines two operations into one, either to reduce memory traffic or to use a better instruction. For example, multiply-then-add can be fused into a single FMA (fused multiply-add) instruction, which both halves the instruction count and avoids rounding the intermediate product.</p>
</section>
<section id="simd-instructions" class="level3">
<h3 class="anchored" data-anchor-id="simd-instructions">SIMD instructions</h3>
<p>In the matrix multiplication section, we already used SIMD instructions to vectorize the inner loop.</p>
<p>Broadly, there are two ways to leverage SIMD:</p>
<ol type="1">
<li>Auto-vectorization by the compiler. LLVM does a pretty good job here.</li>
<li>Explicit SIMD intrinsics. Auto-vectorization is a black box; it can fail to fire for two reasons: (1) the compiler is conservative and may not pick the instruction we want, or (2) we wrote the loop in a pattern the compiler doesn’t recognize. For performance-critical code I tend to prefer explicit intrinsics, because you want to know exactly what is being emitted.</li>
</ol>
<p>An explicit SIMD kernel looks like the following. It looks intimidating at first, but it’s really just a sequence of load / multiply / add / reduce instructions, and you don’t need to hand-write it — ask Codex or Claude to draft it for you and then verify.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb11-1"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> n <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">..</span>n_rows <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb11-2">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> x <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>act[base <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> n]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb11-3">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">mut</span> av <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> _mm512_setzero_si512()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb11-4">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> p <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">..</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb11-5">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> xv <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> _mm512_loadu_si512(x<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>qs<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>as_ptr()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>add(p <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">64</span>) <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">as</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">const</span> __m512i)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb11-6">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> dp <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> _mm512_dpbusd_epi32(_mm512_setzero_si512()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> w[p]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> xv)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb11-7">        av <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> _mm512_add_epi32(av<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> _mm512_mullo_epi32(dp<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> s16[p]))<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb11-8">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb11-9">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> acc_scaled <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> _mm512_reduce_add_epi32(av)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb11-10">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> pairs <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span></span>
<span id="cb11-11">        _mm256_madd_epi16(_mm256_loadu_si256(x<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>bsums<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>as_ptr() <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">as</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">const</span> __m256i)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> ones)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb11-12">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> acc_min <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> hsum_i32_256(_mm256_mullo_epi32(pairs<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> mnv))<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb11-13">    acc[n] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> x<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>d <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> (d <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> acc_scaled <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">as</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">f32</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> dmin <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> acc_min <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">as</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">f32</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb11-14"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
</section>
<section id="multi-threading" class="level3">
<h3 class="anchored" data-anchor-id="multi-threading">Multi-threading</h3>
<p>Multi-threading here is not very different from other systems; the first question is how to partition the work. The simplest scheme is to partition by user: each request gets a thread that runs the entire inference end-to-end.</p>
<p>A more interesting question is what the lowest single-user latency (time-to-first-token) we can achieve looks like. To optimize that, we need to parallelize <em>within</em> a single inference.</p>
<p>The cost model: thread creation is expensive (microseconds, sometimes more), so we use a thread pool to avoid creating and destroying threads on each call. Contention on shared memory hurts, so we want to partition work so each thread owns its own working set.</p>
</section>
<section id="how-did-we-do" class="level3">
<h3 class="anchored" data-anchor-id="how-did-we-do">How did we do?</h3>
<p>My machine has roughly 55 GB/s of usable memory bandwidth, which is the ceiling we should aim for in memory-bound kernels.</p>
<p>The table below shows end-to-end performance and per-kernel performance. The two biggest kernels are <code>qmatmul</code> and <code>moe_routed_experts</code>, and their effective bandwidth is already close to the hardware limit — we’ve done a good job.</p>
<pre class="text"><code>bench: model/Qwen3.6-MTP-35B-A3B-UD-Q4_K_M.gguf
cpu:
  load:    3.46s
  prefill: 256 tok in 2.176s — 117.7 tok/s
  decode:  64 tok in 4.234s — 15.1 tok/s (66.1 ms/tok)

------------------------------------------------------------------------------
  kernel                    count        total          bytes       eff bw
------------------------------------------------------------------------------
  qmatmul                    9024    2282.582ms   113837932544      49.87 GB/s
  moe_routed_experts         2560     815.111ms    39158022144      48.04 GB/s
  moe_shared_expert          2560     309.055ms     8577351680      27.75 GB/s
  attention                   640     272.661ms     3040870400      11.15 GB/s
  delta_rule_scan            1920     172.020ms    24238080000     140.90 GB/s
  moe_router                 5120     168.570ms     5413283840      32.11 GB/s
  causal_conv1d_silu         1920      82.260ms      660602880       8.03 GB/s
  gdn_gates                  1920      65.658ms      755466240      11.51 GB/s
  add_rmsnorm                5120      21.565ms      104857600       4.86 GB/s
  qk_norm_rope               1280       5.234ms       17694720       3.38 GB/s
  embed_rmsnorm                64       0.297ms         663552       2.23 GB/s
  append_kv                   640       0.216ms        2621440      12.11 GB/s
  slice_last_row               64       0.021ms         524288      25.26 GB/s</code></pre>
</section>
</section>
<section id="source-code" class="level2">
<h2 class="anchored" data-anchor-id="source-code">Source code</h2>
<p>Honestly, no one is going to read and reuse this source code. Just send Codex (or Claude) this blog post and ask it to write an inference engine for you.</p>
<p>The point of this post is to help <em>you</em> — as a human — understand the layer beneath the LLM inference engine, so you’re a little less afraid of the code Codex generates for you.</p>


</section>


 ]]></description>
  <guid>https://blog.xiangpeng.systems/posts/how-to-llm-inference/</guid>
  <pubDate>Mon, 08 Jun 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Four mistakes during my PhD</title>
  <link>https://blog.xiangpeng.systems/posts/phd-mistakes/</link>
  <description><![CDATA[ 





<p>I’m currently having an end-of-PhD crisis, where I keep thinking about what to do next. It’s a good time to slow down and reflect, to avoid repeating the same mistakes. I have made many mistakes during my PhD. Here, I list four major ones I’m currently aware of.</p>
<section id="mistake-1-giving-up-too-late" class="level2">
<h2 class="anchored" data-anchor-id="mistake-1-giving-up-too-late">Mistake 1: Giving up too late</h2>
<p>Since childhood, I was told to never give up, stick to it, and I would eventually win. Maybe that’s true, but I definitely took it too far.</p>
<p>It took me a while to learn that never giving up is easy; giving up at the right time is hard.</p>
<p>I have two examples.</p>
<p>My first advisor was definitely not a fit for me, and I knew it after three months of working with him. But it took me almost three years to finally make the decision to switch. It has been one of the most important decisions I have made in my life, and I wish I had made it much earlier.</p>
<p>Why did it take so long? Because I wasn’t sure if it was my problem or my advisor’s problem. I wasn’t sure if a new advisor would actually be better, and I wasn’t sure if it was worth the disruption. I was told that a PhD is about suffering, so maybe it was just a normal part of the process.</p>
<p>My first paper was submitted six times and rejected six times over three years until it was finally abandoned. It was clearly a failed project, and I knew early on that it wasn’t worth publishing.</p>
<p>Why did it take so long to abandon it? I had invested so much time and energy into the project. Every resubmission required more physical and mental effort. The more I resubmitted, the harder it was to give up.</p>
<p>It was also an ego issue: I felt that if I gave up, I would be admitting that I wasted so much time on a failed project. Is this a skill issue? What if this happens again with the next project?</p>
<p>In both examples, I always knew the right thing to do. But I was held back by the momentum of keeping the same direction. It was very hard to make a change.</p>
<p>Lessons learned: practice <strong>low ego</strong>; build the <strong>courage</strong> to give up.</p>
</section>
<section id="mistake-2-getting-real-too-late" class="level2">
<h2 class="anchored" data-anchor-id="mistake-2-getting-real-too-late">Mistake 2: Getting real too late</h2>
<p>As a junior PhD student, it’s very tempting to work on simple topics like key-value stores, indexes, graph processing, and concurrency control. Here, “simple” means well-defined, classic, and narrowly scoped problems. It’s not that these topics are unimportant, but they are often the lazy way to do research: 1. Someone has already defined the problem exceptionally well, and the research community easily understands and appreciates it. 2. They have well-defined interfaces, meaning you don’t have to work on a real codebase; you can just benchmark an isolated component. 3. There are always ways to improve them by combining them with new workloads, new hardware, or AI.</p>
<p>My biased observation is that almost nobody outside academia cares about these topics, and the resulting papers are largely products of an academic echo chamber. Ironically, the more detached a project is from real-world use, the more it creates the illusion that you’re working on something profound that “only academia can do.”</p>
<p>Unfortunately, I wasted way too much time on these feel-good projects. I fooled myself by thinking, “neural networks were ignored for decades before changing the world, so maybe my project will be useful someday too.” I could find an infinite number of examples where “forward-looking” research eventually pays off, and that my project is like one of them. <strong>It’s beyond logical to argue whether a research project is worth doing</strong> (see mistake 4), the key is not to find more examples but to <strong>be honest</strong> with yourself.</p>
<p>Getting real is painful. It involves very complex systems, diverse use cases, and energy-consuming interactions with real people. It also doesn’t seem immediately rewarding: you invest a massive amount of time without a guaranteed return. But not getting real is an existential threat to the entire academic system. Tax payers cut the funding, industry ignores research work, and fewer and fewer students want to be in the field.</p>
<p>Getting real means to optimize from keyboard to screen, rather than to optimize the layer you like. There’s a stupid culture in academia that people categorize themselves as a “database” person, a “filesystem” person, or a “networking” person; they align themselves with topics, not problems. This self-constraint makes paper publishing easy but great work hard (see mistake 3).</p>
<p>Lessons learned: I created a rule for all my future projects. Before starting, I must find three practitioners who are willing to use my system if it succeeds, write down their names, and talk to them regularly.</p>
</section>
<section id="mistake-3-care-too-much-about-paperresearch-community" class="level2 page-columns page-full">
<h2 class="anchored" data-anchor-id="mistake-3-care-too-much-about-paperresearch-community">Mistake 3: Care too much about paper/research community</h2>
<p>Every PhD student needs to graduate with a few papers, usually three. That’s the single most important metric. Once you have three papers, everything else can be worked out. Similarly, the achievements of a professor are often measured by the number of publications in top-tier conferences; everything else is just a footnote on their CV.</p>
<p>This is how we benchmark the community, nobody should be blamed for optimizing for this metric.</p>
<p>But it’s dangerous for a community to optimize for a goal that is only indirectly connected to the rest of the world. I’m sure many publications will eventually translate to real-world impact, but in my humble opinion, the conversion loss is surprisingly high.</p>
<p>I was “smart enough” to adopt a paper-centric approach. Before I had anything, I first needed to have a recipe for top-tier conference papers. First, I needed a novel idea. That goes into the abstract, and it must be novel enough to justify a paper. Then, I would frame a convincing story about why it is important, and that goes into the introduction. Finally, I would show that my design is better than existing systems, and that goes into the evaluation.</p>
<p>This paper-centric research approach works surprisingly well for publishing papers. Because no effort is wasted, every minute of research goes directly toward the final manuscript.</p>
<p>But this is a dangerous paradigm because it distorts your judgment and misaligns with how novel ideas actually emerge. What if your novel idea doesn’t actually work, and you only realize this when the paper is already written up to the evaluation section? What should you do then? Find a new workload? Find a new hardware setup? Change your storyline a little bit? The absolute last thing you want to do is change the idea itself. Your idea is so beautiful, clean, and elegant; it fits perfectly into a paper and provides an amazing storyline. Except that it doesn’t work.</p>
<p>Naturally, you are incentivized to “make it work.” This doesn’t necessarily mean cheating, but rather finding logically correct, yet practically nonsensical, approaches to cover up the flaws (see mistake 4). And it feels perfectly fine to do this because it’s completely logical and everyone else does it.</p>
<p>I believe the true nature of research is to <strong>“go and see.”</strong> You start with a general direction, keep an open mind, explore, break things, learn enough to get into the deep details, and see if you find anything valuable, and then generalize.</p>
<p>This is difficult because it takes a tremendous amount of time to “learn enough to get into the details.” <sup>1</sup> When you aren’t close enough to the details, it’s very tempting to superficially conclude that “A is essentially B,” leading you to miss interesting problems to work on.</p>
<div class="no-row-height column-margin column-container"><div id="fn1"><p><sup>1</sup>&nbsp;<a href="https://paulgraham.com/greatwork.html">How to Do Great Work</a>.</p></div></div><p>Lessons learned: do the right thing from first principles; don’t worry about what others are doing, competition is for losers.</p>
<p>I’d like to conclude this mistake with a quote from Steve Jobs:</p>
<blockquote class="blockquote">
<p>It’s the disease of thinking that a really great idea is 90% of the work. And if you just tell all these other people, ‘Here’s this great idea,’ then of course they can go off and make it happen… As you evolve that great idea, it changes and grows. It never comes out like it starts because you learn a lot more as you get into the subtleties of it.</p>
</blockquote>
<!-- 
## Mistake 4: Not ambitious enough

Everyone has their favorite topics to work on: machine learning, systems, security, etc. This interest is a major source of motivation; you can achieve nothing without it.

But sometimes that level of interest goes too far. I would sometimes think: "I like transactional databases, not analytical databases; I like filesystems, not database storage; I prefer B-Trees over LSM trees." Usually, these preferences aren't based on true interest; they are ego issues. We tend to like the things we already know, and we avoid the things that might make us look foolish or inexperienced.

There is an infinite amount of interesting things, but only a few of them are truly important. -->
</section>
<section id="mistake-4-lost-in-the-church-of-reason" class="level2 page-columns page-full">
<h2 class="anchored" data-anchor-id="mistake-4-lost-in-the-church-of-reason">Mistake 4: Lost in the Church of Reason</h2>
<p>This might be the most controversial, yet perhaps the biggest mistake I made.</p>
<p>One of the primary goals of PhD training is developing the ability of critical thinking and logical reasoning. I liked this so much. I enjoyed wielding the analytical knife to make my thoughts crystal clear. I enjoyed understanding everything I wanted to understand, and I enjoyed learning from great minds who interpret the world through simple, effective rules.</p>
<p>But after the initial excitement, it soon became boring. The sharp analytical knife quickly turned into muscle memory that we consciously or unconsciously apply to everything. It became a template that regulated my thinking, shielding me from the fear of truly free thinking. Using this framework always felt like the “correct” way to do things, and was a satisfying indicator of being well-educated.</p>
<p>Over time, I learned enough intellectual tools to be able to argue for anything. The power of logical reasoning is that you can always find an angle to frame your work as useful, novel, and important. The more deeply I understood a topic, the easier it was to invent arguments supporting my perspective. Academic research devolved into a purely intellectual exercise of crafting arguments, and I became lost in the church of reason <sup>2</sup>.</p>
<div class="no-row-height column-margin column-container"><div id="fn2"><p><sup>2</sup>&nbsp;The Church of Reason is a metaphor from the book <a href="https://en.wikipedia.org/wiki/Zen_and_the_Art_of_Motorcycle_Maintenance">Zen and the Art of Motorcycle Maintenance</a></p></div><div id="fn3"><p><sup>3</sup>&nbsp;<a href="https://www.youtube.com/watch?v=HeQX2HjkcNo">Math’s Fundamental Flaw</a></p></div></div><p>I soon fell into logical and moral nihilism, losing the ability to judge what is truly right and wrong. Furthermore, I learned that pure logic itself is fundamentally unsound, incomplete, and undecidable<sup>3</sup>. This mindset had profound consequences on my life. I lost interest in doing anything, because, from a strictly logical standpoint, doing “great” research was entirely indistinguishable from doing “bad” research.</p>
<p>The cure was finding my leap of faith<sup>4</sup>—the boundary of logical reasoning where you must unconditionally accept something as true. That final leap of faith boils down to a single question: what is good, and what is not good?</p>
<div class="no-row-height column-margin column-container"><div id="fn4"><p><sup>4</sup>&nbsp;<a href="https://en.wikipedia.org/wiki/Leap_of_faith">Leap of Faith</a></p></div></div><p>My leap of faith, after many revisions, is this: to add something unique to humanity. I don’t necessarily know exactly what that thing is yet, but I know what it <em>isn’t</em>. <strong>If I don’t do a project, but someone else will, then it’s not a true addition to humanity</strong>. In those cases, I will just let other people do it, and I will focus on creating value that only I can do.</p>
</section>
<section id="conclusion" class="level2">
<h2 class="anchored" data-anchor-id="conclusion">Conclusion</h2>
<p>I’d like to conclude with my fifth mistake: <strong>not being ambitious enough</strong>. I wasted too much time stuck in local minima—trying to satisfy my ego, maintain a sense of security, and avoid being seen as a failure.</p>
<p>I haven’t yet found the wisdom and courage to navigate my way out, but I hope to eventually leave something valuable to humanity.</p>


</section>


 ]]></description>
  <guid>https://blog.xiangpeng.systems/posts/phd-mistakes/</guid>
  <pubDate>Thu, 23 Apr 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>parquet-linter: A better Parquet is Parquet itself</title>
  <link>https://blog.xiangpeng.systems/posts/parquet-linter/</link>
  <description><![CDATA[ 





<div class="callout callout-style-simple callout-warning no-icon callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon no-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Warning</span>Acknowledgments
</div>
</div>
<div class="callout-body-container callout-body">
<p>My work is supported by <a href="https://xiangpeng.systems/fund/">funding</a> from <a href="https://influxdata.com">InfluxData</a>, <a href="https://www.bauplanlabs.com">Bauplan</a>, <a href="https://spiraldb.com">SpiralDB</a>, and the taxpayers of the State of Wisconsin and the federal government. Much appreciation!</p>
</div>
</div>
<p>Try it out here: <a href="https://github.com/XiangpengHao/parquet-linter">https://github.com/XiangpengHao/parquet-linter</a></p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> cargo <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">install</span> parquet-linter-cli</span>
<span id="cb1-2"></span>
<span id="cb1-3"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> parquet-linter <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">data.parquet</span></span>
<span id="cb1-4"></span>
<span id="cb1-5"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">warning</span> low-compression-ratio</span>
<span id="cb1-6">  <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">--</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> column<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">[</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">2</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">]</span><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"pid"</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span></span>
<span id="cb1-7">  <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">aggregated</span> compression ratio is 1.09 <span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">(</span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">SNAPPY</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">across</span> 1/1 row groups<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">data</span> is nearly incompressible</span>
<span id="cb1-8">  <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">fix:</span> set column pid compression uncompressed</span>
<span id="cb1-9"></span>
<span id="cb1-10"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">warning</span> dictionary-encoding-cardinality</span>
<span id="cb1-11">  <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">--</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> column<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">[</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">1</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">]</span><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">(</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ts_ns"</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span></span>
<span id="cb1-12">  <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">dictionary</span> data pages fell back to PLAIN in 1/1 row groups<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">estimated</span> cardinality is moderate <span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">(</span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">~132048</span> distinct / 380089 non-null = 35%<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">,</span> dictionary page size may be too small</span>
<span id="cb1-13">  <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">fix:</span> set column ts_ns dictionary_page_size_limit 2097152</span></code></pre></div></div>
<section id="parquet-is-a-spec-not-an-implementation" class="level2">
<h2 class="anchored" data-anchor-id="parquet-is-a-spec-not-an-implementation">Parquet is a spec, not an implementation</h2>
<p>When we talk about Parquet, we often refer to one of its readers or writers, likely the <a href="https://github.com/apache/parquet-java">Java</a>, <a href="https://github.com/apache/arrow/tree/main/cpp/src/parquet">C++</a> (used in PyArrow), <a href="https://github.com/apache/arrow-rs/tree/master/parquet">Rust</a>, or <a href="https://github.com/duckdb/duckdb/tree/main/extension/parquet">DuckDB</a> implementation.</p>
<p>When we complain that Parquet is not great for our use case, we’re actually complaining that the Parquet file generated by a specific implementation with a specific set of parameters is not good enough.</p>
<p>As a spec, Parquet is flexible; it doesn’t really restrict much on how the data is stored, and it allows very fine-grained configuration on almost every aspect of the file format. There’s no single best “Parquet” for all use cases.</p>
<p><strong>Configuring the right parameters for the right use case is not easy.</strong> It often requires a deep understanding of the cascading effects of encodings, compression, layouts, data types, and page/row group sizes. It’s likely that no single person on the planet can get it right on the first try.</p>
</section>
<section id="we-need-different-kinds-of-parquet" class="level2">
<h2 class="anchored" data-anchor-id="we-need-different-kinds-of-parquet">We need different kinds of Parquet</h2>
<p>You might think, why not just use the best practices? Because it’s not easy to define what is “best”.</p>
<section id="use-case-1-long-term-archival" class="level4">
<h4 class="anchored" data-anchor-id="use-case-1-long-term-archival">Use case 1: long term archival</h4>
<p>Probably the most common use case for Parquet is to archive data for long-term storage. In this use case, we don’t care about decoding speed; we want the data to be as compact as possible. Smaller Parquet files means lower S3 bill.</p>
</section>
<section id="use-case-2-fast-query-performance" class="level4">
<h4 class="anchored" data-anchor-id="use-case-2-fast-query-performance">Use case 2: fast query performance</h4>
<p>Parquet is the backbone of lakehouses like <a href="https://iceberg.apache.org/">Iceberg</a>, <a href="https://delta.io/">Delta Lake</a>, and <a href="https://ducklake.select/">DuckLake</a>. In such use cases, we want to maximize query performance. As a recent study shows (<a href="https://github.com/XiangpengHao/liquid-cache">Liquid Cache</a>), decoding Parquet takes a significant amount of query time. To minimize decoding time, we want to use lightweight compression, or even no compression at all (as in <a href="https://docs.databricks.com/en/optimizations/disk-cache.html">Databricks’ local cache</a>).</p>
</section>
<section id="use-case-3-ecosystem-compatibility" class="level4">
<h4 class="anchored" data-anchor-id="use-case-3-ecosystem-compatibility">Use case 3: ecosystem compatibility</h4>
<p>Parquet is an open-direct-access format; <a href="https://github.com/apache/spark/tree/master/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet">Spark</a>, <a href="https://github.com/trinodb/trino/tree/master/lib/trino-parquet">Trino</a>, <a href="https://github.com/duckdb/duckdb/tree/main/extension/parquet">DuckDB</a>, <a href="https://github.com/apache/datafusion">DataFusion</a>, etc., all support it. But not all readers support the same set of Parquet features. <!-- TODO: show parquet compatibility matrix here --> In this use case, we’d like to use the most conservative set of Parquet features that are supported by all readers.</p>
</section>
</section>
<section id="vision-parquet-linter" class="level2">
<h2 class="anchored" data-anchor-id="vision-parquet-linter">Vision: Parquet Linter</h2>
<p>Introducing Parquet Linter: <a href="https://github.com/XiangpengHao/parquet-linter">https://github.com/XiangpengHao/parquet-linter</a>, a tool to check compression, decompression, and compatibility issues in your Parquet files.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/parquet-linter/parquet-linter.png" class="img-fluid figure-img" style="width:80.0%"></p>
<figcaption>Parquet Linter transforms your Parquet file into a better Parquet file</figcaption>
</figure>
</div>
<p>I envision three levels of Parquet Linter.</p>
<section id="level-1-pure-gain" class="level4">
<h4 class="anchored" data-anchor-id="level-1-pure-gain">Level 1: pure gain</h4>
<p>In this level, Parquet Linter checks for issues that make Parquet files worse for all use cases and suggests better configurations.</p>
<p>For example, if a column’s compression ratio is 1.0 using a compression algorithm (this happens surprisingly often!), then we should just remove the compression because it is pure overhead.</p>
</section>
<section id="level-2-trade-offs-guided" class="level4">
<h4 class="anchored" data-anchor-id="level-2-trade-offs-guided">Level 2: trade-offs guided</h4>
<p>In this level, we allow the user to specify the trade-offs they want to prioritize, e.g., choosing compression ratio over decoding speed. Then Parquet Linter checks the configuration and suggests a better set of configurations that aligns with the user’s trade-offs.</p>
</section>
<section id="level-3-towards-intelligence" class="level4">
<h4 class="anchored" data-anchor-id="level-3-towards-intelligence">Level 3: towards intelligence</h4>
<p>The Parquet Linter is essentially a model; it takes workload features as input and outputs a set of configurations to store the data.</p>
<p>Currently, this model is a set of empirical rules, biased by the experience of a poor database PhD student. Just like what’s happening everywhere else in the world, we can replace the set of rules with a model, maybe even a large language model. I believe this can be a very interesting research direction going forward.</p>
</section>
</section>
<section id="does-it-work" class="level2">
<h2 class="anchored" data-anchor-id="does-it-work">Does it work?</h2>
<p>I picked 7 files (somewhat randomly) from <a href="https://huggingface.co/docs/datasets/index">HuggingFace’s datasets</a> and ran the Parquet Linter (Level 1) on them.</p>
<p>We track two metrics separately:</p>
<ul>
<li><code>decode time (ms)</code>: time to convert a Parquet file into Arrow <code>RecordBatch</code>es</li>
<li><code>file size (MB)</code>: size of the Parquet file on disk</li>
</ul>
<p>Even with just simple heuristics from Level 1, Parquet Linter can achieve significant reduction in both file size (up to -19.2%) and decode time (up to -29.8%). Level 2 and 3 are currently on the roadmap. With the collective wisdom of the community, I’m sure we can do much better.</p>
<section id="file-size-leaderboard-mb-lower-is-better" class="level4">
<h4 class="anchored" data-anchor-id="file-size-leaderboard-mb-lower-is-better">File Size Leaderboard (MB, lower is better)</h4>
<table class="caption-top table">
<colgroup>
<col style="width: 8%">
<col style="width: 11%">
<col style="width: 11%">
<col style="width: 11%">
<col style="width: 11%">
<col style="width: 11%">
<col style="width: 11%">
<col style="width: 11%">
<col style="width: 11%">
</colgroup>
<thead>
<tr class="header">
<th></th>
<th style="text-align: right;">File 0</th>
<th style="text-align: right;">File 1</th>
<th style="text-align: right;">File 2</th>
<th style="text-align: right;">File 3</th>
<th style="text-align: right;">File 4</th>
<th style="text-align: right;">File 5</th>
<th style="text-align: right;">File 6</th>
<th style="text-align: right;"><strong>Total</strong></th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>HuggingFace default</td>
<td style="text-align: right;">171.38</td>
<td style="text-align: right;">137.88</td>
<td style="text-align: right;">105.55</td>
<td style="text-align: right;">88.78</td>
<td style="text-align: right;">145.97</td>
<td style="text-align: right;">62.59</td>
<td style="text-align: right;">247.95</td>
<td style="text-align: right;"><strong>960.10</strong></td>
</tr>
<tr class="even">
<td><strong>parquet-linter</strong></td>
<td style="text-align: right;">171.35 (-0.02%)</td>
<td style="text-align: right;">128.31 (-6.94%)</td>
<td style="text-align: right;">90.10 (-14.64%)</td>
<td style="text-align: right;">71.70 (-19.24%)</td>
<td style="text-align: right;">141.41 (-3.12%)</td>
<td style="text-align: right;">58.08 (-7.21%)</td>
<td style="text-align: right;">244.73 (-1.30%)</td>
<td style="text-align: right;"><strong>905.68 (-5.67%)</strong></td>
</tr>
</tbody>
</table>
</section>
<section id="decode-time-leaderboard-ms-lower-is-better" class="level4">
<h4 class="anchored" data-anchor-id="decode-time-leaderboard-ms-lower-is-better">Decode Time Leaderboard (ms, lower is better)</h4>
<table class="caption-top table">
<colgroup>
<col style="width: 8%">
<col style="width: 11%">
<col style="width: 11%">
<col style="width: 11%">
<col style="width: 11%">
<col style="width: 11%">
<col style="width: 11%">
<col style="width: 11%">
<col style="width: 11%">
</colgroup>
<thead>
<tr class="header">
<th></th>
<th style="text-align: right;">File 0</th>
<th style="text-align: right;">File 1</th>
<th style="text-align: right;">File 2</th>
<th style="text-align: right;">File 3</th>
<th style="text-align: right;">File 4</th>
<th style="text-align: right;">File 5</th>
<th style="text-align: right;">File 6</th>
<th style="text-align: right;"><strong>Total</strong></th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>HuggingFace default</td>
<td style="text-align: right;">243.59</td>
<td style="text-align: right;">302.44</td>
<td style="text-align: right;">272.20</td>
<td style="text-align: right;">27.34</td>
<td style="text-align: right;">285.08</td>
<td style="text-align: right;">84.62</td>
<td style="text-align: right;">377.25</td>
<td style="text-align: right;"><strong>1592.53</strong></td>
</tr>
<tr class="even">
<td><strong>parquet-linter</strong></td>
<td style="text-align: right;">237.92 (-2.33%)</td>
<td style="text-align: right;">212.03 (-29.89%)</td>
<td style="text-align: right;">213.47 (-21.58%)</td>
<td style="text-align: right;">38.43 (+40.56%)</td>
<td style="text-align: right;">205.84 (-27.80%)</td>
<td style="text-align: right;">51.36 (-39.31%)</td>
<td style="text-align: right;">322.11 (-14.61%)</td>
<td style="text-align: right;"><strong>1281.15 (-19.55%)</strong></td>
</tr>
</tbody>
</table>
</section>
<section id="benchmark-parquet-linter" class="level4">
<h4 class="anchored" data-anchor-id="benchmark-parquet-linter">Benchmark <code>parquet-linter</code></h4>
<p>You can verify the results by running the benchmark yourself:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb2-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">cargo</span> build <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--release</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--package</span> parquet-linter-leaderboard</span>
<span id="cb2-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">./target/release/parquet-leaderboard</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--from-linter</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--iterations</span> 3</span></code></pre></div></div>


</section>
</section>

 ]]></description>
  <guid>https://blog.xiangpeng.systems/posts/parquet-linter/</guid>
  <pubDate>Sun, 22 Feb 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Vector search using only Parquet and DataFusion</title>
  <link>https://blog.xiangpeng.systems/posts/vector-search-with-parquet-datafusion/</link>
  <description><![CDATA[ 





<div class="callout callout-style-simple callout-warning no-icon callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon no-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Warning</span>Acknowledgments
</div>
</div>
<div class="callout-body-container callout-body">
<p>My work is supported by <a href="https://xiangpeng.systems/fund/">funding</a> from <a href="https://influxdata.com">InfluxData</a>, <a href="https://www.bauplanlabs.com">Bauplan</a>, <a href="https://spiraldb.com">SpiralDB</a>, and the taxpayers of the State of Wisconsin and the federal government. Much appreciation!</p>
</div>
</div>
<p>To search vector embeddings, the standard advice is almost always “spin up a vector database.”</p>
<p>Tools like <a href="https://www.pinecone.io/">Pinecone</a>, <a href="https://milvus.io/">Milvus</a>, or specialized formats like <a href="https://lancedb.com/">Lance</a> are fantastic pieces of engineering.</p>
<p>But they come with a hidden cost: complexity. New infrastructure to manage, new file formats to learn, and new data to coordinate.</p>
<p>But what if you didn’t need any of that? What if you could just keep your data in Parquet, the format you’re already using?</p>
<p>This post explores a little experiment: implementing efficient, native vector search directly inside Parquet files. Introducing <code>pq-vector</code>: <a href="https://github.com/XiangpengHao/pq-vector">https://github.com/XiangpengHao/pq-vector</a></p>
<section id="wait-isnt-parquet-terrible-for-random-access" class="level2">
<h2 class="anchored" data-anchor-id="wait-isnt-parquet-terrible-for-random-access">Wait, Isn’t Parquet Terrible for Random Access?</h2>
<p>“But Parquet is a columnar format!” I can hear you screaming. “It’s designed for heavy scans, not point lookups!”</p>
<p>You’re not wrong. The common wisdom is that Parquet is ill-suited for random access because it stores column values in compressed pages. To retrieve a single value—like a specific vector embedding—you typically have to decompress the entire page it resides in. This is wasteful if you only need that one item.</p>
<p>But let’s look at the actual numbers.</p>
<p>A typical vector embedding—say, from OpenAI’s <code>text-embedding-3-small</code> model—has 1,536 dimensions. That’s about 6KB of data. Now, guess how big a standard Parquet page is? Usually a few KB as well.</p>
<p>If we simply configure the <a href="https://docs.rs/parquet/latest/parquet/file/properties/struct.WriterPropertiesBuilder.html">Parquet writer</a> to match the page size with the embedding size, we can effectively force each embedding into its own page. With this, “decompressing a page” just means “reading the one vector we want.”</p>
<p>We don’t change the file format; we just tune it.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb1-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">// Configure parquet to store one embedding per page</span></span>
<span id="cb1-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> props <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">WriterProperties::</span>builder()</span>
<span id="cb1-3">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>set_data_page_size_limit(vector_size)     <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">// one vector per page</span></span>
<span id="cb1-4">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>set_column_compression(embedding_col<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Compression::</span><span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">None</span>)</span>
<span id="cb1-5">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>build()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span></code></pre></div></div>
<p>With this simple configuration change, we’ve effectively turned Parquet into a random-access friendly format for our vectors.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/vector-search-with-parquet-datafusion/random-access.png" class="img-fluid figure-img" style="width:50.0%"></p>
<figcaption>By simply configuring the page size to match the embedding size, we can effectively turn Parquet into a random-access friendly format for our vectors.</figcaption>
</figure>
</div>
</section>
<section id="zero-copy-vector-indexing" class="level2 page-columns page-full">
<h2 class="anchored" data-anchor-id="zero-copy-vector-indexing">Zero-copy Vector Indexing</h2>
<p>Of course, fast random access isn’t enough. If you have to scan every single row to calculate distances (O(N)), it doesn’t matter how fast you can read an individual page — it’ll still be slow. We need an index.</p>
<p>But here’s the challenge: <strong>how do we add an index without breaking compatibility?</strong> We don’t want to create a “custom Parquet” that DuckDB or Spark can’t read.</p>
<p>The solution is surprisingly elegant. Parquet allows you to embed arbitrary metadata in the file footer <sup>1</sup> . Standard readers will happily ignore it, but our specialized reader can look for it.</p>
<div class="no-row-height column-margin column-container"><div id="fn1"><p><sup>1</sup>&nbsp;This is not a hack, it a elegant Parquet feature that allows you to extend the file format, more details <a href="https://datafusion.apache.org/blog/2025/07/14/user-defined-parquet-indexes/">here</a></p></div></div><p>We chose the <a href="https://www.pinecone.io/learn/series/faiss/vector-indexes/">Inverted File (IVF) index</a> for this prototype. It works by partitioning the vector space into clusters (centroids). When we want to search, we figure out which clusters are close to our query, and then we only look at the vectors in those clusters.</p>
<p>The best part? It’s <strong>zero-copy</strong>.</p>
<p>Some vector stores (HNSW-based) force you to duplicate your data into their internal structures. Our index is just a lightweight list of pointers (row IDs) and cluster centroids. The actual heavy vector data stays right where it is — in the Parquet data pages.</p>
<p>In our experiments with ~5,000 academic papers each with 4096-dimensional embeddings (we used <code>qwen/qwen3-embedding-8b</code>), the index added a negligible <strong>0.21 MB</strong> to a <strong>68 MB</strong> file. That’s an overhead of just <strong>0.3%</strong>.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/vector-search-with-parquet-datafusion/zero-copy-index.png" class="img-fluid figure-img" style="width:50.0%"></p>
<figcaption>The vector index references to the embedding vectors in Parquet files, without duplicating the data.</figcaption>
</figure>
</div>
</section>
<section id="in-place-index-creation" class="level2 page-columns page-full">
<h2 class="anchored" data-anchor-id="in-place-index-creation">In-Place Index Creation</h2>
<p>Now we understand vector search in Parquet is not only feasible, but also fast.</p>
<p>You might ask: doesn’t this mean I need to rewrite my entire Parquet file just to create that index? But here’s the kicker: creating a vector index on Parquet doesn’t require a rewrite.</p>
<p>Wait, isn’t Parquet immutable? How can you add an index to a Parquet file without rewriting it?</p>
<p>Enter another beautiful part of Parquet files: <strong>The Footer.</strong> At the end of each Parquet file is its metadata. We can simply append our vector index after the end of the metadata, and then copy that old metadata to the end of the vector index.</p>
<p>This way we create a perfectly legit Parquet file without rewriting any data pages<sup>2</sup>.</p>
<div class="no-row-height column-margin column-container"><div id="fn2"><p><sup>2</sup>&nbsp;This assumes the embedding column is already optimized for random access. There are likely clever ways to rewrite only the embedding column, though.</p></div></div><div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/vector-search-with-parquet-datafusion/index-footer.png" class="img-fluid figure-img" style="width:50.0%"></p>
<figcaption>The index is appended after the end of the old metadata</figcaption>
</figure>
</div>
</section>
<section id="datafusion-integration" class="level2">
<h2 class="anchored" data-anchor-id="datafusion-integration">DataFusion Integration</h2>
<p>An index is useless if you can’t use it. DataFusion lacks “official” support for custom indexes. But its modularity is its superpower: anyone can extend it.</p>
<p>We implemented a DataFusion query optimizer which will automatically rewrite the query plan to use the index, if the Parquet file has one.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/vector-search-with-parquet-datafusion/query-plan.png" class="img-fluid figure-img" style="width:80.0%"></p>
<figcaption>pq-vector automatically rewrites the query plan to use the index</figcaption>
</figure>
</div>
</section>
<section id="how-it-looks-in-code" class="level2">
<h2 class="anchored" data-anchor-id="how-it-looks-in-code">How It Looks in Code</h2>
<p>We implemented a proof-of-concept using Rust, using <code>parquet</code> and <code>datafusion</code>.</p>
<section id="building-the-index" class="level3">
<h3 class="anchored" data-anchor-id="building-the-index">1. Building the Index</h3>
<p>The builder reads your existing Parquet file, trains K-means centroids, and writes the index to the file footer.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb2-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">use</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">pq_vector::</span>IndexBuilder<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb2-2"></span>
<span id="cb2-3"><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">IndexBuilder::</span>new(</span>
<span id="cb2-4">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"data/combined.parquet"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb2-5">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"embedding"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb2-6">)</span>
<span id="cb2-7"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>n_clusters(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span>)</span>
<span id="cb2-8"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>build_inplace()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">?;</span></span></code></pre></div></div>
</section>
<section id="searching-with-sql-datafusion" class="level3">
<h3 class="anchored" data-anchor-id="searching-with-sql-datafusion">2. Searching with SQL (DataFusion)</h3>
<p>To make this actually useful, we hooked it into DataFusion’s optimizer rules. This lets you run vector searches using <strong>standard SQL syntax</strong>.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb3-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">use</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">datafusion::execution::</span>SessionStateBuilder<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb3-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">use</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">datafusion::prelude::</span>SessionContext<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb3-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">use</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">pq_vector::df_vector::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>PqVectorSessionBuilderExt<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> VectorTopKOptions<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">};</span></span>
<span id="cb3-4"></span>
<span id="cb3-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">// Enable the pq-vector extension in DataFusion</span></span>
<span id="cb3-6"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> state <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">SessionStateBuilder::</span>new()</span>
<span id="cb3-7">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>with_default_features()</span>
<span id="cb3-8">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>with_pq_vector(VectorTopKOptions <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb3-9">        nprobe<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb3-10">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">..</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">Default</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">::</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">default</span>()</span>
<span id="cb3-11">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span>)</span>
<span id="cb3-12">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>build()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb3-13"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> ctx <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">SessionContext::</span>new_with_state(state)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb3-14"></span>
<span id="cb3-15">ctx<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>register_parquet(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"t"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"data/combined.parquet"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">Default</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">::</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">default</span>())</span>
<span id="cb3-16">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">?;</span></span>
<span id="cb3-17"></span>
<span id="cb3-18"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">// Standard SQL vector search!</span></span>
<span id="cb3-19"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> df <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> ctx</span>
<span id="cb3-20">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>sql(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"SELECT * FROM t ORDER BY array_distance(embedding, [0.1, 0.2, 0.3]) LIMIT 10"</span>)</span>
<span id="cb3-21">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">?;</span></span></code></pre></div></div>
<p>Notice that we can mix the vector search with normal SQL filters, one of the key features of Vector Databases. This is the power of keeping everything in one engine. <strong>You don’t need a vector search engine.</strong></p>
</section>
</section>
<section id="does-it-actually-work" class="level2">
<h2 class="anchored" data-anchor-id="does-it-actually-work">Does It Actually Work?</h2>
<p>We benchmarked this on our dataset of 4,886 vectors (4096 dimensions).</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Operation</th>
<th>Time</th>
<th>Speedup</th>
<th>Recall@10</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Brute force</td>
<td>100ms</td>
<td>1x</td>
<td>1.00</td>
</tr>
<tr class="even">
<td>IVF search (nprobe=1)</td>
<td>3.4ms</td>
<td><strong>29x</strong></td>
<td>0.83</td>
</tr>
<tr class="odd">
<td>IVF search (nprobe=5)</td>
<td>17.7ms</td>
<td><strong>5.7x</strong></td>
<td>0.96</td>
</tr>
<tr class="even">
<td>IVF search (nprobe=10)</td>
<td>32.4ms</td>
<td><strong>3.1x</strong></td>
<td>1.00</td>
</tr>
</tbody>
</table>
<p>The results are pretty clear. Even with a simple IVF index, we see massive speedups compared to a full scan. With <code>nprobe=5</code>, we’re getting <strong>96% recall</strong> at <strong>5.7x the speed</strong> of a brute force scan.</p>
<p>The latency is almost entirely dominated by random I/O, which validates our theory: if you tune the page size correctly, Parquet handles random access just fine.</p>
</section>
<section id="need-your-help" class="level2">
<h2 class="anchored" data-anchor-id="need-your-help">Need your help!</h2>
<p><code>pq-vector</code> is very early; it’s definitely not as polished as commercial vector databases now, but we have just shown that there’s nothing we can’t do with standard open source tools like Parquet and DataFusion. Here are a few things we’re planning to do next:</p>
<ul>
<li><strong>No HNSW</strong>: We used IVF because it’s simple and compact. Graph-based indexes like <a href="https://www.pinecone.io/learn/series/faiss/hnsw/">HNSW</a> are probably more accurate, but they have much higher space overhead.</li>
<li><strong>Multi-Parquet index</strong>: We often query many Parquet files, not just one file. We plan to support querying multiple Parquet files seamlessly.</li>
<li><strong>Better filter support</strong>: Like DuckDB’s VSS, our implementation currently has limited support for filters. We plan to allow users to write SQL like: <code>SELECT * FROM t WHERE col_1=5 ORDER BY array_distance(embedding, [0.1, 0.2, 0.3]) LIMIT 10</code>.</li>
</ul>
<p><a href="https://github.com/XiangpengHao/pq-vector">github.com/XiangpengHao/pq-vector</a></p>


</section>


 ]]></description>
  <guid>https://blog.xiangpeng.systems/posts/vector-search-with-parquet-datafusion/</guid>
  <pubDate>Mon, 09 Feb 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Stop building systems for agents</title>
  <link>https://blog.xiangpeng.systems/posts/stop-building-agent-systems/</link>
  <description><![CDATA[ 





<div class="callout callout-style-simple callout-warning no-icon callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon no-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Warning</span>Acknowledgments
</div>
</div>
<div class="callout-body-container callout-body">
<p>My work is supported by <a href="https://xiangpeng.systems/fund/">funding</a> from <a href="https://influxdata.com">InfluxData</a>, <a href="https://www.bauplanlabs.com">Bauplan</a>, <a href="https://spiraldb.com">SpiralDB</a>, and the taxpayers of the State of Wisconsin and the federal government. Much appreciation!</p>
</div>
</div>
<section id="the-wrong-direction" class="level3">
<h3 class="anchored" data-anchor-id="the-wrong-direction">The Wrong Direction</h3>
<p>Everyone knows today’s systems are sub-optimal for agents. They waste context, confuse models with unstructured output, and prioritize vision-based human interaction over text-based LLMs.</p>
<p>So we desperately need to build agent systems that treat LLMs as first-class citizens. I’m sure you can generate a hundred papers or spawn a hundred startups in this direction.</p>
<p>This approach surely works, but it is not fundamental: if humans can live with it, LLMs <strong>can eventually</strong> catch up. Maybe next month, maybe next year, but eventually LLMs can easily do what humans can do, and then your mission-critical agent-native system becomes a nice-to-have.</p>
<p>In the LLM era, we need to ask not what humans can do for LLMs, but what LLMs can do for humans.</p>
<p>So what is the fundamental challenge here? <strong>The velocity at which a human can take responsibility for an agent’s actions.</strong></p>
</section>
<section id="the-unaccountable-machine" class="level3">
<h3 class="anchored" data-anchor-id="the-unaccountable-machine">The Unaccountable Machine</h3>
<p><img src="https://blog.xiangpeng.systems/posts/stop-building-agent-systems/scale.png" class="img-fluid" style="width:80.0%"></p>
<p>In human society, every functional system relies on an accountability chain. When things break, we need a person to blame, and to fix it.</p>
<p>For the last few decades, we have maintained a subtle balance where <strong>the velocity of building systems roughly equals the velocity of accountability</strong>.</p>
<p>LLMs changed this by making it 1000x faster to write code, but our ability to take accountability for a system barely changed.</p>
<p>This creates a perfect <a href="https://en.wikipedia.org/wiki/The_Unaccountability_Machine">accountability sink</a>. When a vibe-system fails, we can only hope LLMs will fix it by themselves; they are unaccountable. But unaccountable systems are useless, and will eventually collapse.</p>
<p>Most LLM-vibed systems look fancy at first, but without a human owning their behavior, they are useless at best, and burdensome to society at worst.</p>
<p><strong>Axiom:</strong> AI systems, no matter how capable, must be held accountable by a human.</p>
<p>Therefore, the real bottleneck of agent adoption is the <strong>Time to Accountability</strong>: how quickly a human operator can understand, diagnose, and own the system’s behavior?</p>
</section>
<section id="the-happen-to-work-foundation" class="level3">
<h3 class="anchored" data-anchor-id="the-happen-to-work-foundation">The “Happen-to-Work” Foundation</h3>
<p>(Un)Surprisingly, this bottleneck has almost nothing to do with AI.</p>
<p>You join a new team. How long does it take for you to be held accountable for a production system? It happens when you can observe the system interactions, reproduce regressions and failures, and explain what’s going on.</p>
<p>You may think, isn’t this what we have been preparing for? Hasn’t computer science been working on this for decades? Isn’t this a solved problem?</p>
<p>Unfortunately, the answer is no. We lack the infrastructure to understand systems from a third-party perspective; instead, the wisdom is hidden in the brain of the engineer who built it. <strong>From a user’s perspective, everything just happens-to-work.</strong></p>
<p>When we compile software, it just happens-to-compile. We hope we are using the right compiler version, compiling the right dependencies, executing in the correct parallel order, and persisting files without corrupting data (in fact, we don’t even know if the data is corrupted or persisted).</p>
<p>When we issue a database transaction, it just happens-to-work. We hope the data transferred from disk returns in a deterministic order, that transactions respect isolation levels, and that our access controls actually work.</p>
</section>
<section id="llm-agents-shake-our-fragile-foundation" class="level3">
<h3 class="anchored" data-anchor-id="llm-agents-shake-our-fragile-foundation">LLM Agents Shake Our Fragile Foundation</h3>
<p>How could it be that our modern society is based on something so fragile that almost nobody understands? You might ask.</p>
<p>The answer is simple: we have only spent minimal effort to make it work most of the time. When rare edge cases happen, we just retry and hope they don’t appear. No one is able to reproduce a rare production bug, no one is able to fully explain why performance jitters, and no one is able to fully understand how sub-systems interact.</p>
<p>It has been fine, you may argue. But no, it’s not fine. As LLMs elevate humanity to the next level, what used to be ok is no longer ok.</p>
<p>We’re now capable of operating tremendously more systems; tremendous amounts of work will be cheaply digitized, and a tremendous number of tasks will be cheaply performed by LLMs. When the scale grows 1000 times, our fragile foundation becomes the problem.</p>
</section>
<section id="the-real-agent-native-systems" class="level3">
<h3 class="anchored" data-anchor-id="the-real-agent-native-systems">The Real Agent-Native Systems</h3>
<p>So what defines agent-native systems? They are human-centric:</p>
<ol type="1">
<li>Radically observable.</li>
<li>Radically deterministic.</li>
</ol>
<p><strong>Radically observable</strong> means we must see exactly how the system performs: who invokes a system call, how threads interleave, which function writes to a memory address, etc. The system should act like a glass box—observing system behavior from all levels of the stack. If performance regresses, we should see the exact reason, whether the CPU core is overheating or the SSD firmware is garbage collecting.</p>
<p><strong>Radically deterministic</strong> means that given the same input, the program produces <em>exactly</em> the same output (randomness is controlled via seeds). This applies not only to a single function, but to the entire multi-threaded, distributed system. Threads must interleave in exactly the same way, hash tables must iterate in exactly the same order, and environmental noise (like noisy neighbors) must never alter the software’s execution.</p>
<p>Stop building systems tailored for agents, build systems that are easier for humans to own, make them observable and deterministic.</p>
<hr>
    <bluesky-comments post="at://did:plc:zuu6vyw33exwnkcoe75k4vje/app.bsky.feed.post/3mdveyk6nks27" filter-config="{&quot;filterEmptyReplies&quot;:true,&quot;mutePatterns&quot;:[],&quot;muteUsers&quot;:[]}"></bluesky-comments>
  


</section>

 ]]></description>
  <guid>https://blog.xiangpeng.systems/posts/stop-building-agent-systems/</guid>
  <pubDate>Mon, 02 Feb 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>What is LiquidCache?</title>
  <link>https://blog.xiangpeng.systems/posts/what-is-liquid-cache/</link>
  <description><![CDATA[ 





<div class="callout callout-style-simple callout-warning no-icon callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon no-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Warning</span>Acknowledgments
</div>
</div>
<div class="callout-body-container callout-body">
<p>This work is supported by <a href="https://xiangpeng.systems/fund/">funding</a> from:<br> 1.&nbsp;&nbsp;<a href="https://influxdata.com">InfluxData</a>, <a href="https://www.bauplanlabs.com">Bauplan</a>, and <a href="https://spiraldb.com">SpiralDB</a>.<br> 2.&nbsp;&nbsp;The taxpayers of the State of Wisconsin and the federal government.</p>
<p>Your support for science is greatly appreciated!</p>
</div>
</div>
<p><img src="https://blog.xiangpeng.systems/posts/what-is-liquid-cache/liquid-cache.jpg" class="img-fluid"></p>
<p><a href="https://github.com/XiangpengHao/liquid-cache">LiquidCache</a> is a caching layer that unifies the design goals of compute and storage<sup>1</sup>.</p>
<div class="no-row-height column-margin column-container"><div id="fn1"><p><sup>1</sup>&nbsp;Check out our <a href="https://github.com/XiangpengHao/liquid-cache/blob/main/dev/doc/liquid-cache-vldb.pdf">research paper</a> (VLDB 2025) for more technical details.</p></div></div><p>It accelerates query performance without needing to leave Parquet.</p>
<p>It addresses this fundamental tension:</p>
<ul>
<li><p><strong>Storage systems</strong> want to optimize for ecosystem compatibility, and long-term, stable, open governance; industry thus gravitated towards Parquet as the de facto columnar format.</p></li>
<li><p><strong>Query engines</strong> want the data to be optimized for flexible layouts, rapid evolution, and performance-first optimizations.</p></li>
</ul>
<p>Instead of squeezing the last bits of performance from Parquet<sup>2</sup>, or trying to create future-proof file formats<sup>3</sup>, LiquidCache addresses this problem through a new abstraction: the caching layer.</p>
<div class="no-row-height column-margin column-container"><div id="fn2"><p><sup>2</sup>&nbsp;Great paper on Parquet selection pushdown: <a href="https://dl.acm.org/doi/10.1145/3589323">Selection Pushdown in Column Stores using Bit Manipulation Instructions</a>.</p></div><div id="fn3"><p><sup>3</sup>&nbsp;<a href="https://www.vldb.org/pvldb/vol18/p4017-gienieczko.pdf">AnyBlox: A Framework for Self-Decoding Datasets</a>, and <a href="https://db.cs.cmu.edu/papers/2025/zeng-sigmod2025.pdf">F3: The Open-Source Data File Format for the Future</a></p></div></div><ul>
<li>At a glance, LiquidCache is a distributed caching service: it supports all object storage backends (S3, GCS, Azure Blob Storage, etc.), and serves all kinds of applications (knowledge bases, dashboards, etc.) deployed on all kinds of compute (Kubernetes, Lambda, etc.).</li>
<li>Under the hood, LiquidCache <em>caches Parquet as liquid data</em>, which is ultra-optimized for compute pushdown, compressed execution, modern storage, and network‑efficient data transfer.</li>
</ul>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/what-is-liquid-cache/liquid-cache-overview.png" class="img-fluid figure-img" style="width:80.0%"></p>
<figcaption>LiquidCache overview. It caches different object store sources and serves different analytical applications.</figcaption>
</figure>
</div>
<p>It is built on open standards: <a href="https://parquet.apache.org/">Parquet</a> for data storage, <a href="https://github.com/apache/datafusion">DataFusion</a> as the query engine, and <a href="https://arrow.apache.org/docs/format/Flight.html">Arrow Flight</a> for data transfer. This makes LiquidCache highly composable – you can easily integrate it into your existing analytics stack.</p>
<section id="why-liquidcache" class="level2 page-columns page-full">
<h2 class="anchored" data-anchor-id="why-liquidcache">Why LiquidCache?</h2>
<section id="we-like-s3" class="level4">
<h4 class="anchored" data-anchor-id="we-like-s3">We like S3</h4>
<ol type="1">
<li>Simple durability: 11 nines of durability—you never have to worry about data loss.</li>
<li>Simple scalability: virtually unlimited space and throughput.</li>
</ol>
</section>
<section id="but-s3-is-slow-and-expensive" class="level4 page-columns page-full">
<h4 class="anchored" data-anchor-id="but-s3-is-slow-and-expensive">But S3 is slow and expensive</h4>
<ol type="1">
<li>≈100 ms first‑byte latency plus transfer latency; this quickly adds up when multiple round‑trips are needed to fetch data.<sup>4</sup></li>
<li>Storage, request, and data‑transfer/egress costs; prices have remained largely unchanged for a decade even as underlying hardware has become ~20× cheaper.</li>
</ol>
<div class="no-row-height column-margin column-container"><div id="fn4"><p><sup>4</sup>&nbsp;<a href="https://www.vldb.org/pvldb/vol16/p2769-durner.pdf">Exploiting Cloud Object Storage for High-Performance Analytics</a></p></div></div><div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/what-is-liquid-cache/s3-price.jpg" class="img-fluid figure-img" style="width:80.0%"></p>
<figcaption>S3 prices have barely changed for a decade, despite ~20× reductions in underlying hardware costs, <a href="https://x.com/andrewlamb1111/status/1909681433452724444">credit to Andrew Lamb</a></figcaption>
</figure>
</div>
</section>
<section id="liquidcache-foundation-of-diskless-architectures" class="level4 page-columns page-full">
<h4 class="anchored" data-anchor-id="liquidcache-foundation-of-diskless-architectures">LiquidCache: foundation of diskless architectures</h4>
<ol type="1">
<li>Caches are everywhere<sup>5</sup>: compute‑local caches (e.g., Snowflake/Databricks local NVMe, Spark host caches)<sup>6</sup>, shared‑nothing caches, and cache services<sup>7</sup>.</li>
<li>DLC trilemma: among durability, low latency, and low cost, you can only choose two<sup>8</sup>. <img src="https://blog.xiangpeng.systems/posts/what-is-liquid-cache/DLC.png" class="img-fluid" style="width:50.0%"></li>
</ol>
<!-- 
### State Management for Applications 

#### We like serverless
1. Simple scalability: automatic horizontal scaling and fine‑grained billing.
2. Lower cost via higher utilization and elasticity; zero servers to manage.
3. Available on all major cloud providers (AWS Lambda, GCP Cloud Run, Azure Functions, etc.).

#### But serverless analytics are slow and expensive 
1. S3 is both too slow and too expensive. 
2. You can't build a sticky compute‑local cache: serverless compute is one‑shot, ephemeral, and stateless.

#### LiquidCache: the missing piece for serverless analytics
In practice, most production analytics pin workloads to EC2‑like servers with a compute‑local cache for predictable performance.

With LiquidCache, all compute nodes can be serverless because they can connect to the shared LiquidCache service, which is deployed on conventional EC2 instances.

![Compare the OLTP architecture (left) with the new diskless+serverless OLAP architecture (right). LiquidCache is at the center of state management.](./state-management.png){width=100%} -->
<div class="no-row-height column-margin column-container"><div id="fn5"><p><sup>5</sup>&nbsp;<a href="https://andrew.nerdnetworks.org/other/CIDR_2025_Cloud_5_Minute_Rule.pdf">The Five-Minute Rule for the Cloud: Caching in Analytics Systems</a></p></div><div id="fn6"><p><sup>6</sup>&nbsp;<a href="https://github.com/duckdb/duckdb/pull/16463">DuckDB’s external file cache</a></p></div><div id="fn7"><p><sup>7</sup>&nbsp;<a href="https://clickhouse.com/blog/building-a-distributed-cache-for-s3">ClickHouse’s distributed cache for S3</a></p></div><div id="fn8"><p><sup>8</sup>&nbsp;<a href="https://materializedview.io/p/cloud-storage-triad-latency-cost-durability">The Cloud Storage Triad: Latency, Cost, Durability</a></p></div></div></section>
</section>
<section id="how-liquidcache-works" class="level2 page-columns page-full">
<h2 class="anchored" data-anchor-id="how-liquidcache-works">How LiquidCache Works</h2>
<section id="we-like-parquet" class="level4">
<h4 class="anchored" data-anchor-id="we-like-parquet">We like Parquet</h4>
<ol type="1">
<li>All major query engines support it (DataFusion, Spark, Trino, DuckDB, Snowflake, BigQuery, and more).</li>
<li>It is battle‑tested and keeps evolving (e.g., page indexes, new encodings).</li>
<li>It is under open, stable governance (Apache Software Foundation), so your data is in good hands.</li>
</ol>
</section>
<section id="but-sometimes-we-want-more-aggressive-performance" class="level4">
<h4 class="anchored" data-anchor-id="but-sometimes-we-want-more-aggressive-performance">But sometimes we want more aggressive performance</h4>
<ol type="1">
<li>There are better encodings and compression schemes out there.</li>
<li>Parquet is critical data infrastructure: it evolves cautiously to keep your data safe and stable—it can’t try new research today and abandon your data tomorrow.</li>
</ol>
</section>
<section id="liquidcache-cache-only-pushdown-optimized-data-representation" class="level4 page-columns page-full">
<h4 class="anchored" data-anchor-id="liquidcache-cache-only-pushdown-optimized-data-representation">LiquidCache: cache-only, pushdown-optimized data representation</h4>
<ol type="1">
<li>LiquidCache uses state‑of‑the‑art encodings and compression chosen by the workload.<sup>9</sup></li>
<li>Liquid data is invisible to the rest of the ecosystem: it is cache‑only. This means it can freely change its layout, adding or removing encodings without breaking any user code.</li>
<li>LiquidCache transparently, progressively, and selectively transcodes Parquet data to the liquid format.</li>
<li>Liquid data is designed for efficient pushdown to save both compute and network resources.</li>
</ol>
<div class="no-row-height column-margin column-container"><div id="fn9"><p><sup>9</sup>&nbsp;The liquid format is heavily inspired by <a href="https://github.com/vortex-data/vortex">Vortex</a>. We plan to support a Vortex backend in the future.</p></div></div><p>Without any changes to Parquet, LiquidCache takes care of the performance optimizations.</p>
<p><img src="https://blog.xiangpeng.systems/posts/what-is-liquid-cache/liquid-data.jpg" class="img-fluid" style="width:80.0%"></p>
</section>
</section>
<section id="conclusions" class="level2 page-columns page-full">
<h2 class="anchored" data-anchor-id="conclusions">Conclusions</h2>
<p>LiquidCache is the one‑stop shop for diskless, serverless, and pushdown‑native analytics.</p>
<p>It is built on open standards (Parquet, Arrow Flight, DataFusion) for easy integration and stable governance.</p>
<p>LiquidCache caches Parquet as liquid data, which is ultra-optimized for compute pushdown, compressed execution, modern storage, and network‑efficient data transfer.</p>
<section id="who-are-we" class="level4 page-columns page-full">
<h4 class="anchored" data-anchor-id="who-are-we">Who are we?</h4>
<ul>
<li>LiquidCache started as a research project led by <a href="https://xiangpeng.systems">Xiangpeng Hao</a> at UW‑Madison <a href="https://research.cs.wisc.edu/adsl/">ADSL</a>.</li>
<li>It was made possible by a research gift from <a href="https://influxdata.com">InfluxData</a>. One year later, <a href="https://spiraldb.com">SpiralDB</a> and <a href="https://www.bauplanlabs.com">Bauplan</a> also joined the journey.<sup>10</sup></li>
<li>LiquidCache will remain a public‑benefit project in appreciation of the support from taxpayers, research gifts, and the open‑source community.</li>
</ul>


<div class="no-row-height column-margin column-container"><div id="fn10"><p><sup>10</sup>&nbsp;Support our research <a href="https://xiangpeng.systems/fund/">here</a>!</p></div></div></section>
</section>


 ]]></description>
  <guid>https://blog.xiangpeng.systems/posts/what-is-liquid-cache/</guid>
  <pubDate>Mon, 24 Nov 2025 00:00:00 GMT</pubDate>
</item>
<item>
  <title>On the Nature of Strings</title>
  <link>https://blog.xiangpeng.systems/posts/nature-of-string/</link>
  <description><![CDATA[ 





<div class="callout callout-style-simple callout-warning no-icon callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon no-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Warning</span>Acknowledgments
</div>
</div>
<div class="callout-body-container callout-body">
<p>This work was supported by <a href="https://xiangpeng.systems/fund/">funding</a> from:</p>
<ol type="1">
<li><p><a href="https://influxdata.com">InfluxData</a>, <a href="https://www.bauplanlabs.com">Bauplan</a>, and <a href="https://spiraldb.com">SpiralDB</a>.</p></li>
<li><p>The taxpayers of the State of Wisconsin and the federal government.</p></li>
</ol>
<p>Your support for science is greatly appreciated!</p>
</div>
</div>
<p>Strings are the most common data type we encounter in analytics. When we built <a href="https://github.com/XiangpengHao/liquid-cache">LiquidCache</a> – a distributed pushdown cache for <a href="https://github.com/apache/datafusion">DataFusion</a> – we discovered that the way strings are stored and processed has an outsized impact on performance and memory. This post distills key points from my “On the Nature of Strings” talk at <a href="https://dcsystems.wtf">DC Systems</a> (slides <a href="./on-the-nature-of-strings.pdf">here</a>; recording coming soon) and explains how we rethink string storage to make LiquidCache fast and lean.</p>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">TL;DR</h2>
<ul>
<li><p>We analyzed the <a href="https://github.com/cwida/public_bi_benchmark">PublicBI dataset</a> (46 tables, 710 queries, 386 GB of data) and found that strings are the most important data type in the dataset: <strong>61 % of bytes are strings</strong>, and around <strong>40 % of projected columns are string columns</strong>.</p></li>
<li><p>There are four types of strings, categorized by distinct ratio and average length: <img src="https://blog.xiangpeng.systems/posts/nature-of-string/string-type.jpg" class="img-fluid" style="width:70.0%"></p></li>
<li><p>Dictionary encoding is crucial for storing string data: <img src="https://blog.xiangpeng.systems/posts/nature-of-string/dictionary-encoding.jpg" class="img-fluid" style="width:70.0%"></p></li>
<li><p>Different trade-offs for representing variable-length strings: <img src="https://blog.xiangpeng.systems/posts/nature-of-string/string-storage.jpg" class="img-fluid"></p></li>
<li><p>Inlined prefixes are critical for efficient comparisons: <img src="https://blog.xiangpeng.systems/posts/nature-of-string/string-prefix.jpg" class="img-fluid" style="width:70.0%"></p></li>
<li><p>Null handling is essential: <img src="https://blog.xiangpeng.systems/posts/nature-of-string/null-handling.jpg" class="img-fluid" style="width:80.0%"></p></li>
<li><p>Compression is important: <img src="https://blog.xiangpeng.systems/posts/nature-of-string/compression.jpg" class="img-fluid"></p></li>
</ul>
</section>
<section id="conclusion" class="level2">
<h2 class="anchored" data-anchor-id="conclusion">Conclusion</h2>
<ul>
<li>Strings are the most important data type in analytics, and we need to take them seriously.</li>
<li>Strings have many subtypes and should be treated differently.</li>
<li>If you’re not sure, try <a href="https://github.com/XiangpengHao/liquid-cache">LiquidCache</a> – it has a state-of-the-art string-handling pipeline and delivers best-in-class string performance with easy integration.</li>
</ul>


</section>

 ]]></description>
  <guid>https://blog.xiangpeng.systems/posts/nature-of-string/</guid>
  <pubDate>Wed, 13 Aug 2025 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Build your own S3-Select in 400 lines of Rust</title>
  <link>https://blog.xiangpeng.systems/posts/build-s3-select/</link>
  <description><![CDATA[ 





<div class="callout callout-style-simple callout-warning no-icon callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon no-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Warning</span>Acknowledgement
</div>
</div>
<div class="callout-body-container callout-body">
<p>This blog post was made possible by <a href="https://xiangpeng.systems/fund/">funding</a> support from:</p>
<ol type="1">
<li><p><a href="https://influxdata.com">InfluxData</a></p></li>
<li><p>Taxpayers of the state of Wisconsin and the federal government.</p></li>
</ol>
<p>Your support for science is greatly appreciated!</p>
</div>
</div>
<section id="tldr" class="level2 page-columns page-full">
<h2 class="anchored" data-anchor-id="tldr">TL;DR</h2>
<p><a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/selecting-content-from-objects.html">S3-Select</a> can filter S3 data before sending it back to you, significantly saving network bandwidth and time. Unfortunately, AWS killed this feature in 2024.</p>
<p>Good news: you can build your own S3-Select with <strong>all open-source tools and open standards</strong>! This blog post shows you how to do it in <strong>400 lines of Rust</strong><sup>1</sup> using the <a href="https://www.influxdata.com/glossary/fdap-stack/">FDAP stack</a><sup>2</sup>.</p>
<div class="no-row-height column-margin column-container"><div id="fn1"><p><sup>1</sup>&nbsp;Without blank lines and comments.</p></div><div id="fn2"><p><sup>2</sup>&nbsp;FDAP: Apache Arrow <strong>F</strong>light, Apache <strong>D</strong>ataFusion, Apache <strong>A</strong>rrow, Apache <strong>P</strong>arquet</p></div></div><p>Complete code is available in <a href="https://github.com/XiangpengHao/build-your-own-s3-select">this repository</a>.</p>
<p>Looking for a more complete solution? Check out <a href="https://github.com/XiangpengHao/liquid-cache"><strong>LiquidCache</strong></a>, a open-source/open-standard, push-down enabled storage system built on the same principles.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/build-s3-select/s3-select.jpg" class="img-fluid figure-img"></p>
<figcaption>Comparing S3 (left) with S3-Select (right). S3-Select evaluates the filter before returning the data to you, saving network bandwidth.</figcaption>
</figure>
</div>
</section>
<section id="architecture" class="level2 page-columns page-full">
<h2 class="anchored" data-anchor-id="architecture">Architecture</h2>
<p>As we can’t change S3, we assume that the data is stored in our own storage system (often as a cache to S3).</p>
<p>Our architecture consists of three main components: a storage server, a compute client, and the Arrow Flight protocol connecting them.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/build-s3-select/arch.jpg" class="img-fluid figure-img"></p>
<figcaption>The compute server is a full-fledged DataFusion server that evaluates user queries. The storage server contains Parquet files and also embeds a DataFusion instance for filter evaluation. The compute and storage communicate with each other via Arrow Flight.</figcaption>
</figure>
</div>
<section id="compute" class="level3">
<h3 class="anchored" data-anchor-id="compute">Compute</h3>
<p>The compute (client) is a full-fledged DataFusion node that <strong>automatically</strong> pushes down filter expressions to the storage server. Unlike S3-Select which requires manual SQL filter crafting, our system automatically decides what can be evaluated at the storage layer, while handling complex operations (joins, aggregations) in the compute node.</p>
</section>
<section id="storage" class="level3 page-columns page-full">
<h3 class="anchored" data-anchor-id="storage">Storage</h3>
<p>The storage (server) stores Parquet<sup>3</sup> files and evaluates the filters pushed down from the compute node. We leverage DataFusion’s <a href="https://datafusion.apache.org/blog/2024/11/18/datafusion-fastest-single-node-parquet-clickbench/">highly optimized</a> query engine for filter evaluation, which includes <a href="../../posts/parquet-pushdown/index.html">efficient Parquet reading with filter pushdown</a>, parallel filter evaluation across multiple threads, and streaming filtered results back to the compute node.</p>
<div class="no-row-height column-margin column-container"><div id="fn3"><p><sup>3</sup>&nbsp;Parquet is the industry standard columnar storage format. CSV/JSON/etc. are also easily supported, but you should really consider using a real format like Parquet.</p></div></div></section>
<section id="communication" class="level3">
<h3 class="anchored" data-anchor-id="communication">Communication</h3>
<p>Communication between compute and storage happens via the Arrow Flight protocol – a high-performance, open-standard protocol for efficient data exchange. Unlike S3-Select which transmits data in CSV/JSON format, we keep data in columnar format throughout the entire pipeline. This means the compute node can directly operate on the data without heavy deserialization, and any system that speaks Arrow Flight can read from our storage.</p>
</section>
</section>
<section id="life-of-a-query" class="level2">
<h2 class="anchored" data-anchor-id="life-of-a-query">Life of a query</h2>
<p>Let’s walk through how a query flows through our system:</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/build-s3-select/life-of-a-query.jpg" class="img-fluid figure-img" style="width:60.0%"></p>
<figcaption>The life of a query. The compute node pushes down the filter expression to the storage node. The storage node evaluates the filter and sends the data back to the compute node. The compute node evaluates the rest of the query and sends the result back to the user.</figcaption>
</figure>
</div>
<ol type="1">
<li><p><strong>Schema Resolution</strong>: When a user submits a query, the compute node first resolves the table schema from the storage node via Arrow Flight.</p></li>
<li><p><strong>Query Planning</strong>: The compute node generates a query plan and decides which parts should be evaluated locally versus pushed down to the storage node.</p></li>
<li><p><strong>Filter Pushdown</strong>: The compute node sends filter predicates to the storage node for evaluation close to the data.</p></li>
<li><p><strong>Filter Evaluation</strong>: The storage node evaluates these filters and streams only the matching data back to the compute node.</p></li>
<li><p><strong>Final Processing</strong>: The compute node handles the computation-heavy parts (aggregations, joins, etc.) and returns the final result to the user.</p></li>
</ol>
<p>Note that only filters are pushed down to the storage node, computation-heavy operations happen at the compute layer. This prevents overloading the storage node’s processing capabilities while reducing network traffic.</p>
</section>
<section id="implementing-the-storage-server" class="level2 page-columns page-full">
<h2 class="anchored" data-anchor-id="implementing-the-storage-server">Implementing the Storage Server</h2>
<p>Now that we understand the architecture, let’s build the storage server. Surprisingly, we can implement it in less than 100 lines of code thanks to DataFusion!</p>
<p>Our storage server is defined as:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb1-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">struct</span> StorageServer <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb1-2">    execution_plans<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> Mutex<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>HashMap<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">u64</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> Arc<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">dyn</span> ExecutionPlan<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;&gt;,</span></span>
<span id="cb1-3">    next_id<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">atomic::</span>AtomicU64<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb1-4">    ctx<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> SessionContext<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb1-5"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
<p>Let’s break down what each field does:</p>
<ul>
<li><code>execution_plans</code>: Maps query IDs to their execution plans</li>
<li><code>next_id</code>: Generates unique query IDs<sup>4</sup></li>
<li><code>ctx</code>: The DataFusion session context (we use a single context for all queries for simplicity)</li>
</ul>
<div class="no-row-height column-margin column-container"><div id="fn4"><p><sup>4</sup>&nbsp;In production, you should use a proper unique ID generator like <a href="https://docs.rs/uuid/latest/uuid/index.html">uuid</a>.</p></div></div><p>The <code>StorageServer</code> implements the <code>FlightSqlService</code> trait with three key methods:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="annotated-cell-2" style="background: #f1f3f5;"><pre class="sourceCode rust code-annotation-code code-with-copy"><code class="sourceCode rust"><span id="annotated-cell-2-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">#[</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">tonic::</span>async_trait<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">]</span></span>
<span id="annotated-cell-2-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">impl</span> FlightSqlService <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> StorageServer <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="annotated-cell-2-3">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">type</span> FlightService <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> StorageServer<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="annotated-cell-2-4"></span>
<span id="annotated-cell-2-5">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">async</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> get_flight_info_schemas(</span>
<span id="annotated-cell-2-6">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="annotated-cell-2-7">        query<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> CommandGetDbSchemas<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="annotated-cell-2-8">        _request<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> Request<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>FlightDescriptor<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span></span>
<span id="annotated-cell-2-9">    ) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Result</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>Response<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>FlightInfo<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span> Status<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="annotated-cell-2-10">        <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">todo!</span>()</span>
<span id="annotated-cell-2-11">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="annotated-cell-2-12"></span>
<span id="annotated-cell-2-13"></span>
<span id="annotated-cell-2-14">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">async</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> get_flight_info_statement(</span>
<span id="annotated-cell-2-15">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="annotated-cell-2-16">        cmd<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> CommandStatementQuery<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="annotated-cell-2-17">        _request<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> Request<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>FlightDescriptor<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span></span>
<span id="annotated-cell-2-18">    ) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Result</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>Response<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>FlightInfo<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span> Status<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="annotated-cell-2-19">        <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">todo!</span>()</span>
<span id="annotated-cell-2-20">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="annotated-cell-2-21"></span>
<span id="annotated-cell-2-22">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">async</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> do_get_fallback(</span>
<span id="annotated-cell-2-23">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="annotated-cell-2-24">        _request<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> Request<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>Ticket<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span></span>
<span id="annotated-cell-2-25">        message<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">Any</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="annotated-cell-2-26">    ) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Result</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>Response<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Self</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">as</span> FlightService<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">::</span>DoGetStream<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span> Status<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="annotated-cell-2-27">        <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">todo!</span>()</span>
<span id="annotated-cell-2-28">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="annotated-cell-2-29"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
<p>Let’s implement each method one by one.</p>
<section id="schema-resolution" class="level3">
<h3 class="anchored" data-anchor-id="schema-resolution">1. Schema Resolution</h3>
<p>First, we need to implement <code>get_flight_info_schemas</code> to handle schema retrieval:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb2-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">async</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> get_flight_info_schemas(</span>
<span id="cb2-2">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb2-3">    query<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> CommandGetDbSchemas<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb2-4">    _request<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> Request<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>FlightDescriptor<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span></span>
<span id="cb2-5">) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Result</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>Response<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>FlightInfo<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span> Status<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb2-6">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> table_url <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> query<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>catalog<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unwrap()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb2-7">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> table_name <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> query<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>db_schema_filter_pattern<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unwrap()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb2-8">    _ <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span></span>
<span id="cb2-9">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>ctx</span>
<span id="cb2-10">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>register_parquet(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>table_name<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> table_url<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">Default</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">::</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">default</span>())</span>
<span id="cb2-11">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb2-12">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> schema <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>ctx<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>table_provider(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>table_name)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unwrap()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>schema()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb2-13">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> info <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">FlightInfo::</span>new()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>try_with_schema(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>schema)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unwrap()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb2-14">    <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">Ok</span>(<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Response::</span>new(info))</span>
<span id="cb2-15"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
<p>This method:</p>
<ol type="1">
<li>Registers the Parquet file with DataFusion</li>
<li>Gets the schema from the registered table</li>
<li>Returns the schema as a FlightInfo response</li>
</ol>
</section>
<section id="query-planning" class="level3">
<h3 class="anchored" data-anchor-id="query-planning">2. Query Planning</h3>
<p>Next, we implement <code>get_flight_info_statement</code> to handle query planning:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb3-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">async</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> get_flight_info_statement(</span>
<span id="cb3-2">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb3-3">    cmd<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> CommandStatementQuery<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb3-4">    _request<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> Request<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>FlightDescriptor<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span></span>
<span id="cb3-5">) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Result</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>Response<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>FlightInfo<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span> Status<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb3-6">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> query <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> cmd<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>query<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>as_str()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb3-7">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> (state<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> logical_plan) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>ctx<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>sql(query)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unwrap()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>into_parts()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb3-8">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> plan <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> state<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>optimize(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>logical_plan)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unwrap()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb3-9">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> physical_plan <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> state<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>create_physical_plan(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>plan)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unwrap()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb3-10">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> partition_count <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> physical_plan<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>output_partitioning()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>partition_count()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb3-11">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> schema <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> physical_plan<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>schema()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb3-12">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> id <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>next_id<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>fetch_add(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">atomic::Ordering::</span>Relaxed)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb3-13">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>execution_plans</span>
<span id="cb3-14">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>lock()</span>
<span id="cb3-15">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unwrap()</span>
<span id="cb3-16">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>insert(id<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> physical_plan)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb3-17">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">mut</span> info <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">FlightInfo::</span>new()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>try_with_schema(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>schema)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unwrap()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb3-18">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> partition <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">..</span>partition_count <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb3-19">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> fetch <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> FetchResults <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb3-20">            handle<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> id<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb3-21">            partition<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> partition <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">as</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">u32</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb3-22">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">};</span></span>
<span id="cb3-23">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> buf <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> fetch<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>as_any()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>encode_to_vec()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>into()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb3-24">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> ticket <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Ticket <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span> ticket<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> buf <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">};</span></span>
<span id="cb3-25">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> endpoint <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">FlightEndpoint::</span>new()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>with_ticket(ticket<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>clone())<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb3-26">        info <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> info<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>with_endpoint(endpoint)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb3-27">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb3-28">    <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">Ok</span>(<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Response::</span>new(info))</span>
<span id="cb3-29"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
<p>This method:</p>
<ol type="1">
<li>Parses the SQL into a logical plan</li>
<li>Optimizes the logical plan and converts it to a physical plan</li>
<li>Returns partition info and query ID to the compute node</li>
</ol>
</section>
<section id="data-streaming" class="level3 page-columns page-full">
<h3 class="anchored" data-anchor-id="data-streaming">3. Data Streaming</h3>
<p>Finally, we implement <code>do_get_fallback</code> to handle data streaming:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb4-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">async</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> do_get_fallback(</span>
<span id="cb4-2">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb4-3">    _request<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> Request<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>Ticket<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span></span>
<span id="cb4-4">    message<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">Any</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb4-5">) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Result</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>Response<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Self</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">as</span> FlightService<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">::</span>DoGetStream<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span> Status<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb4-6">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> fetch_results<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> FetchResults <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> message<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unpack()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unwrap()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unwrap()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb4-7">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> plan_lock <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>execution_plans<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>lock()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unwrap()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb4-8">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> physical_plan <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> plan_lock<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>get(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>fetch_results<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>handle)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unwrap()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>clone()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb4-9">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> stream <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> physical_plan</span>
<span id="cb4-10">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>execute(fetch_results<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>partition <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">as</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">usize</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>ctx<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>task_ctx())</span>
<span id="cb4-11">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unwrap()</span>
<span id="cb4-12">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>map_err(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span>e<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">arrow_flight::error::FlightError::</span>ExternalError(<span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Box</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">::</span>new(e)))<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb4-13">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> encoder <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">FlightDataEncoderBuilder::</span>new()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>build(stream)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb4-14">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> response_stream <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span></span>
<span id="cb4-15">        encoder<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>map(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span>result<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span> result<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>map_err(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span>e<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Status::</span>internal(e<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>to_string())))<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb4-16">    <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">Ok</span>(<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Response::</span>new(<span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Box</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">::</span>pin(response_stream)))</span>
<span id="cb4-17"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
<p>This method:</p>
<ol type="1">
<li>Retrieves the physical plan for that query</li>
<li>Executes the plan on the specified partition</li>
<li>Returns the stream to the compute node</li>
</ol>
<p>That’s it for the storage server! Just these three methods are enough to implement the core functionality<sup>5</sup>.</p>
<div class="no-row-height column-margin column-container"><div id="fn5"><p><sup>5</sup>&nbsp;Of course, a production server needs way more: error handling, logging, authentication, etc. But these concerns are orthogonal to our core implementation.</p></div></div></section>
</section>
<section id="implementing-the-compute-node" class="level2 page-columns page-full">
<h2 class="anchored" data-anchor-id="implementing-the-compute-node">Implementing the Compute Node</h2>
<p>The compute node has more work to do, as it needs to:</p>
<ol type="1">
<li>Communicate with the storage server to get data</li>
<li>Decide which parts of the query to send to storage</li>
<li>Process the filtered data to produce the final result</li>
</ol>
<p>We’ll implement this through two main components:</p>
<ol type="1">
<li><code>FlightTable</code>: Decides what gets pushed down to storage</li>
<li><code>FlightExec</code>: Handles the data streaming from storage</li>
</ol>
<p>Here’s how they fit together in the query plan:</p>
<div class="quarto-figure quarto-figure-center page-columns page-full">
<figure class="figure page-columns page-full">
<p><img src="https://blog.xiangpeng.systems/posts/build-s3-select/query-plan.jpg" class="img-fluid figure-img"></p>
<figcaption>An example query plan showing how FlightTable and FlightExec connect. The ParquetExec and FilterExec<sup>6</sup> are executed on the storage server, while the rest of the plan runs on the compute node. The two plans are connected via the FlightExec node.</figcaption>
<div class="no-row-height column-margin column-container"><div id="fn6"><p><sup>6</sup>&nbsp;Strictly speaking, when <a href="../../posts/parquet-pushdown/index.html">filter pushdown</a> is enabled, the FilterExec is merged into ParquetExec in the storage server.</p></div></div></figure>
</div>
<section id="implementing-flighttable" class="level3 page-columns page-full">
<h3 class="anchored" data-anchor-id="implementing-flighttable">Implementing FlightTable</h3>
<p>Let’s start with <code>FlightTable</code>, which implements DataFusion’s <code>TableProvider</code> trait:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb5-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">pub</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">struct</span> FlightTable <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb5-2">    channel<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> Channel<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb5-3">    server<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">String</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb5-4">    table_name<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> TableReference<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb5-5">    output_schema<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> SchemaRef<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb5-6"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb5-7"></span>
<span id="cb5-8"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">#[</span>async_trait<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">]</span></span>
<span id="cb5-9"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">impl</span> TableProvider <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> FlightTable <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb5-10">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">async</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> scan(</span>
<span id="cb5-11">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb5-12">        _state<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">dyn</span> Session<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb5-13">        projection<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Option</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;&amp;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Vec</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">usize</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;,</span></span>
<span id="cb5-14">        filters<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>[Expr]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb5-15">        limit<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Option</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">usize</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span></span>
<span id="cb5-16">    ) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Result</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>Arc<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">dyn</span> ExecutionPlan<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb5-17">        <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">todo!</span>()</span>
<span id="cb5-18">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb5-19"></span>
<span id="cb5-20">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> supports_filters_pushdown(</span>
<span id="cb5-21">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb5-22">        filters<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>[<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>Expr]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb5-23">    ) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Result</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Vec</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>TableProviderFilterPushDown<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb5-24">       <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">todo!</span>() </span>
<span id="cb5-25">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb5-26"></span>
<span id="cb5-27">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">// ... other trait methods</span></span>
<span id="cb5-28"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
<p>The <code>FlightTable</code> has two important methods:</p>
<ol type="1">
<li><code>scan</code>: Creates a <code>FlightExec</code> node that pulls data from storage</li>
<li><code>supports_filters_pushdown</code>: Tells DataFusion which filters can be pushed down</li>
</ol>
<p>Let’s implement the <code>scan</code> method first:</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/build-s3-select/unparse.jpg" class="img-fluid figure-img" style="width:60.0%"></p>
<figcaption>FlightTable unparses filters, projections, and limits back to SQL and sends them to the storage server.</figcaption>
</figure>
</div>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb6-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">async</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> scan(</span>
<span id="cb6-2">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-3">    _state<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">dyn</span> Session<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-4">    projection<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Option</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;&amp;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Vec</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">usize</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;,</span></span>
<span id="cb6-5">    filters<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>[Expr]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-6">    limit<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Option</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">usize</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span></span>
<span id="cb6-7">) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Result</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>Arc<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">dyn</span> ExecutionPlan<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb6-8">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> unparsed_sql <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb6-9">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">// we don't care about actual source for the purpose of unparsing the sql.</span></span>
<span id="cb6-10">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> empty_table_provider <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">empty::EmptyTable::</span>new(<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>schema()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>clone())<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb6-11">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> table_source <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Arc::</span>new(<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">DefaultTableSource::</span>new(<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Arc::</span>new(empty_table_provider)))<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb6-12"></span>
<span id="cb6-13">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> logical_plan <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> TableScan <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb6-14">            table_name<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>table_name<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>clone()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-15">            source<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> table_source<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-16">            projection<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> projection<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>map(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span>p<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span> p<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>to_vec())<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-17">            filters<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> filters<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>to_vec()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-18">            fetch<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> limit<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-19">            projected_schema<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Arc::</span>new(<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>schema()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>as_ref()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>clone()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>to_dfschema()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unwrap())<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-20">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">};</span></span>
<span id="cb6-21">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> unparser <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Unparser::</span>new(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>PostgreSqlDialect <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{}</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb6-22">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> unparsed_sql <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> unparser</span>
<span id="cb6-23">            <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>plan_to_sql(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">LogicalPlan::</span>TableScan(logical_plan))</span>
<span id="cb6-24">            <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unwrap()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb6-25">        unparsed_sql<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>to_string()</span>
<span id="cb6-26">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">};</span></span>
<span id="cb6-27"></span>
<span id="cb6-28">    <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">println!</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"SQL send to cache: </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">\n</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">{}"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> unparsed_sql)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb6-29"></span>
<span id="cb6-30">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">mut</span> client <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">FlightSqlServiceClient::</span>new(<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>channel<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>clone())<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb6-31">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> info <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>execute(unparsed_sql<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">None</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unwrap()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb6-32"></span>
<span id="cb6-33">    <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">Ok</span>(<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Arc::</span>new(<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">FlightExec::</span>try_new(</span>
<span id="cb6-34">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>schema<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>clone()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-35">        info<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-36">        projection<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-37">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>server<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-38">    )<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">?</span>))</span>
<span id="cb6-39"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
<p>The magic happens in the unparsing step<sup>7</sup>. We leverage DataFusion’s query planner to:</p>
<div class="no-row-height column-margin column-container"><div id="fn7"><p><sup>7</sup>&nbsp;We can also send DataFusion’s physical plan (like <a href="https://github.com/apache/datafusion-ballista">Ballista</a>), or even <a href="https://substrait.io/">Substrait</a> to the storage server.</p></div></div><ol type="1">
<li>Create a logical plan containing the filters, projections, and limits</li>
<li>Unparse this plan back to SQL using DataFusion’s unparser</li>
<li>Send this SQL to the storage server for evaluation</li>
<li>Create a FlightExec node that will stream data from storage</li>
</ol>
<p>Next, let’s implement the filter pushdown support:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb7-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> supports_filters_pushdown(</span>
<span id="cb7-2">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb7-3">    filters<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>[<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>Expr]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb7-4">) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Result</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Vec</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>TableProviderFilterPushDown<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb7-5">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> filter_push_down<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Vec</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>TableProviderFilterPushDown<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> filters</span>
<span id="cb7-6">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>iter()</span>
<span id="cb7-7">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>map(</span>
<span id="cb7-8">            <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span>f<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">match</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Unparser::</span>new(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>PostgreSqlDialect <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{}</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>expr_to_sql(f) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb7-9">                <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">Ok</span>(_) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">TableProviderFilterPushDown::</span>Exact<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb7-10">                <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">Err</span>(_) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">TableProviderFilterPushDown::</span>Unsupported<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb7-11">            <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb7-12">        )</span>
<span id="cb7-13">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>collect()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb7-14">    <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">Ok</span>(filter_push_down)</span>
<span id="cb7-15"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
<p>Our rule is simple but effective: if a filter can be unparsed to SQL, we push it down to storage.</p>
</section>
<section id="implementing-flightexec" class="level3">
<h3 class="anchored" data-anchor-id="implementing-flightexec">Implementing FlightExec</h3>
<p>Now let’s implement <code>FlightExec</code>, which is responsible for streaming data from storage:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb8-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">pub</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">struct</span> FlightExec <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb8-2">    server<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">String</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-3">    partitions<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> Arc<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>[FlightPartition]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span></span>
<span id="cb8-4">    plan_properties<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> PlanProperties<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-5"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb8-6"></span>
<span id="cb8-7"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">impl</span> ExecutionPlan <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> FlightExec <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb8-8">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> execute(</span>
<span id="cb8-9">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-10">        partition<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">usize</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-11">        _context<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> Arc<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>TaskContext<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span></span>
<span id="cb8-12">    ) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Result</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>SendableRecordBatchStream<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb8-13">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> future_stream <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> flight_stream(<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>partitions[partition]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>clone()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>schema())<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb8-14">        <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">Ok</span>(<span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Box</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">::</span>pin(FlightStream <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb8-15">            state<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">FlightStreamState::</span>Init<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-16">            future_stream<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">Some</span>(<span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Box</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">::</span>pin(future_stream))<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-17">            schema<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>schema()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-18">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span>))</span>
<span id="cb8-19">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb8-20"></span>
<span id="cb8-21">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">// ... other trait methods</span></span>
<span id="cb8-22"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
<p><code>FlightExec</code> is mainly a wrapper around <code>FlightStream</code>, which handles the async streaming of data from storage:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb9-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">struct</span> FlightStream <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb9-2">    state<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> FlightStreamState<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb9-3">    future_stream<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Option</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>BoxFuture<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">'static</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Result</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>SendableRecordBatchStream<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;&gt;,</span></span>
<span id="cb9-4">    schema<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> SchemaRef<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb9-5"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb9-6"></span>
<span id="cb9-7"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">impl</span> Stream <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> FlightStream <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb9-8">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">type</span> Item <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Result</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>RecordBatch<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;;</span></span>
<span id="cb9-9"></span>
<span id="cb9-10">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> poll_next(<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">mut</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> Pin<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;&amp;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">mut</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span> cx<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">mut</span> Context<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">'_</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> Poll<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Option</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Self</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">::</span>Item<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb9-11">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> result<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> Poll<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Option</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Result</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>RecordBatch<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">loop</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb9-12">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">match</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">mut</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>state <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb9-13">                <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">FlightStreamState::</span>Init <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb9-14">                    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>state <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">FlightStreamState::</span>GetStream(<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>future_stream<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>take()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unwrap())<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb9-15">                    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">continue</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb9-16">                <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb9-17">                <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">FlightStreamState::</span>GetStream(fut) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb9-18">                    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> stream <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">ready!</span>(fut<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>as_mut()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>poll(cx))<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unwrap()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb9-19">                    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>state <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">FlightStreamState::</span>Processing(stream)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb9-20">                    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">continue</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb9-21">                <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb9-22">                <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">FlightStreamState::</span>Processing(stream) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb9-23">                    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> result <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> stream<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>as_mut()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>poll_next(cx)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb9-24">                    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">break</span> result<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb9-25">                <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb9-26">            <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb9-27">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">};</span></span>
<span id="cb9-28">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">match</span> result <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb9-29">            <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Poll::</span>Ready(<span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">Some</span>(<span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">Ok</span>(batch))) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Poll::</span>Ready(<span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">Some</span>(<span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">Ok</span>(batch)))<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb9-30">            <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Poll::</span>Ready(<span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">None</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Poll::</span>Ready(<span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">None</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb9-31">            <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Poll::</span>Ready(<span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">Some</span>(<span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">Err</span>(e))) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb9-32">                <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">panic!</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Error reading flight stream: {}"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> e)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb9-33">            <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb9-34">            _ <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Poll::</span>Pending<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb9-35">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb9-36">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb9-37"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
<p>The implementation is a bit complex due to Rust’s lack of native async iterators, requiring us to implement a state machine manually. However, the core concept is straightforward – it pulls data from storage and returns it as a stream.</p>
</section>
</section>
<section id="putting-it-all-together" class="level2">
<h2 class="anchored" data-anchor-id="putting-it-all-together">Putting It All Together</h2>
<p>Now let’s assemble our components into a working system!</p>
<section id="server-binary" class="level3">
<h3 class="anchored" data-anchor-id="server-binary">Server Binary</h3>
<p>The server binary is simple – it just starts a Flight service with our StorageServer:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb10-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">#[</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">tokio::</span>main<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">]</span></span>
<span id="cb10-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">async</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> main() <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Result</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Box</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">dyn</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">std::error::</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">Error</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb10-3">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> addr <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"127.0.0.1:50051"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>parse()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">?;</span></span>
<span id="cb10-4">    <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Server::</span>builder()</span>
<span id="cb10-5">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>add_service(<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">FlightServiceServer::</span>new(<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">StorageServer::</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">default</span>()))</span>
<span id="cb10-6">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>serve(addr)</span>
<span id="cb10-7">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">?;</span></span>
<span id="cb10-8">    <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">Ok</span>(())</span>
<span id="cb10-9"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
</section>
<section id="client-binary" class="level3">
<h3 class="anchored" data-anchor-id="client-binary">Client Binary</h3>
<p>The client binary configures DataFusion, registers our FlightTable, and runs the query:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb11-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">#[</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">tokio::</span>main<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">]</span></span>
<span id="cb11-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">async</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> main() <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Result</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Box</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">dyn</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">std::error::</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">Error</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb11-3">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">mut</span> session_config <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">SessionConfig::</span>from_env()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">?;</span></span>
<span id="cb11-4">    session_config</span>
<span id="cb11-5">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>options_mut()</span>
<span id="cb11-6">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>execution</span>
<span id="cb11-7">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>parquet</span>
<span id="cb11-8">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>pushdown_filters <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">true</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb11-9">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> ctx <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Arc::</span>new(<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">SessionContext::</span>new_with_config(session_config))<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb11-10"></span>
<span id="cb11-11">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> cache_server <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"http://localhost:50051"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb11-12">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> table_name <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"aws-edge-locations"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb11-13">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> table_url <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"./aws-edge-locations.parquet"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb11-14">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> sql <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">format!</span>(</span>
<span id="cb11-15">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"SELECT DISTINCT </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">\"</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">city</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">\"</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> FROM </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">\"</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">{table_name}</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">\"</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> WHERE </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">\"</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">country</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">\"</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> = 'United States'"</span></span>
<span id="cb11-16">    )<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb11-17"></span>
<span id="cb11-18">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> table <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">FlightTable::</span>create(cache_server<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> table_name<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> table_url)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb11-19">    ctx<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>register_table(table_name<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">Arc::</span>new(table))<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">?;</span></span>
<span id="cb11-20">    ctx<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>sql(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>sql)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">?.</span>show()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">?;</span></span>
<span id="cb11-21">    <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">Ok</span>(())</span>
<span id="cb11-22"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
</section>
<section id="running-the-system" class="level3">
<h3 class="anchored" data-anchor-id="running-the-system">Running the System</h3>
<p>To run our S3-Select alternative:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb12-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">cargo</span> run <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--bin</span> server</span>
<span id="cb12-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">cargo</span> run <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--bin</span> client</span></code></pre></div></div>
<p>You should see output like:</p>
<pre><code>```bash
SQL to run: 
-------
SELECT DISTINCT "city" FROM "aws-edge-locations" WHERE "country" = 'United States'
-------

SQL to pushdown: 
-------
SELECT "aws-edge-locations"."city" FROM "aws-edge-locations" WHERE ("aws-edge-locations"."country" = 'United States')
-------

+----------------+
| city           |
+----------------+
| Boston         |
| Chicago        |
| Portland       |
| New York       |
| Newark         |
| Detroit        |
...</code></pre>
<p>Notice that only the filter (<code>WHERE "country" = 'United States'</code>) was pushed down to storage, while the aggregation (<code>DISTINCT</code>) was evaluated by the compute node. This is exactly what we wanted!</p>
</section>
</section>
<section id="whats-next-liquidcache" class="level2">
<h2 class="anchored" data-anchor-id="whats-next-liquidcache">What’s Next: LiquidCache</h2>
<p>Congratulations! You’ve built a working S3-Select alternative in just 400 lines of Rust. However, this blog post only scratches the surface of what’s possible with this architecture.</p>
<p>To take this concept further, I’m excited to announce <a href="https://github.com/XiangpengHao/liquid-cache">LiquidCache</a> – a modern, open-source, push-down enabled storage system built on the same principles. LiquidCache extends what we’ve built here (to 15k loc) with advanced features like:</p>
<ul>
<li>Advanced caching strategies</li>
<li>Advanced filter evaluation techniques</li>
<li>Enhanced reliability and error handling</li>
<li>Performance optimizations</li>
</ul>
<p>Check out our <a href="https://github.com/XiangpengHao/liquid-cache/blob/main/dev/doc/liquid-cache-vldb.pdf">research paper</a> for technical details and consider contributing to the project!</p>
</section>
<section id="conclusion" class="level2 page-columns page-full">
<h2 class="anchored" data-anchor-id="conclusion">Conclusion</h2>
<p>Building a functional S3-Select alternative is surprisingly simple once you leverage the right building blocks. The FDAP stack (Flight, DataFusion, Arrow, Parquet) provides powerful primitives that handle most of the heavy lifting for us.</p>
<p>The real challenge – and fun – lies in understanding what these components do and how to thread them together effectively. As demonstrated by the imports alone, we’re standing on the shoulders of giants:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb14-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">use</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">arrow::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb14-2">    <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">array::</span>RecordBatch<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb14-3">    <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">datatypes::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>SchemaRef<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> ToByteSlice<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb14-4"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">};</span></span>
<span id="cb14-5"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">use</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">arrow_flight::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb14-6">    FlightClient<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> FlightEndpoint<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> FlightInfo<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> Ticket<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb14-7">    <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">flight_service_client::</span>FlightServiceClient<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb14-8">    <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">sql::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>CommandGetDbSchemas<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">client::</span>FlightSqlServiceClient<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb14-9"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">};</span></span>
<span id="cb14-10"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">use</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">datafusion::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb14-11">    <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">catalog::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>Session<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> TableProvider<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb14-12">    <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">common::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>ToDFSchema<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> project_schema<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb14-13">    <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">datasource::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>DefaultTableSource<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> TableType<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> empty<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb14-14">    <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">error::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>DataFusionError<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Result</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb14-15">    <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">execution::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>RecordBatchStream<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> SendableRecordBatchStream<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> TaskContext<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb14-16">    <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">logical_expr::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>LogicalPlan<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> TableProviderFilterPushDown<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> TableScan<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb14-17">    <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">physical_expr::</span>EquivalenceProperties<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb14-18">    <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">physical_plan::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb14-19">        DisplayAs<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> DisplayFormatType<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> ExecutionPlan<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> PlanProperties<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb14-20">        <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">execution_plan::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>Boundedness<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> EmissionType<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb14-21">        <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">stream::</span>RecordBatchStreamAdapter<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb14-22">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb14-23">    <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">prelude::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*,</span></span>
<span id="cb14-24">    <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">sql::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb14-25">        TableReference<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb14-26">        <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">unparser::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>Unparser<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">dialect::</span>PostgreSqlDialect<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb14-27">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb14-28"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">};</span></span>
<span id="cb14-29"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">use</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">futures::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>Stream<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> TryStreamExt<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">future::</span>BoxFuture<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">};</span></span>
<span id="cb14-30"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">use</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">std::task::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>Context<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> Poll<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> ready<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">};</span></span>
<span id="cb14-31"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">use</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">std::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">any::</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">Any</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">pin::</span>Pin<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">sync::</span>Arc<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">};</span></span>
<span id="cb14-32"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">use</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">tonic::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>async_trait<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">transport::</span>Channel<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">};</span></span></code></pre></div></div>
<p>The power of open-source tools and open standards isn’t just a fallback — it’s a superior approach that liberates us from vendor lock-in and service discontinuations. We’ve demonstrated that not only can we rebuild essential services like S3-Select after their commercial versions are discontinued, but we can create more powerful<sup>8</sup>, customizable, and cost-effective alternatives.</p>


<div class="no-row-height column-margin column-container"><div id="fn8"><p><sup>8</sup>&nbsp;Azure also has filter pushdown, but they <a href="https://learn.microsoft.com/en-us/azure/storage/blobs/data-lake-storage-query-acceleration-how-to?tabs=azure-powershell">don’t support Parquet files</a>.</p></div></div></section>


 ]]></description>
  <guid>https://blog.xiangpeng.systems/posts/build-s3-select/</guid>
  <pubDate>Sun, 23 Mar 2025 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Efficient Filter Pushdown in Parquet</title>
  <link>https://blog.xiangpeng.systems/posts/parquet-pushdown/</link>
  <description><![CDATA[ 





<div class="callout callout-style-simple callout-warning no-icon callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon no-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Warning</span>Acknowledgement
</div>
</div>
<div class="callout-body-container callout-body">
<p>This work (the PR, this blog post, <a href="https://parquet-viewer.xiangpeng.systems">parquet-viewer</a>, and <a href="https://github.com/XiangpengHao/liquid-cache">LiquidCache</a>) was made possible by <a href="https://xiangpeng.systems/fund/">funding</a> support from:</p>
<ol type="1">
<li><p><a href="https://influxdata.com">InfluxData</a></p></li>
<li><p>Taxpayers of the state of Wisconsin and the federal government.</p></li>
</ol>
<p>Your support for science is greatly appreciated!</p>
</div>
</div>
<p>In the <a href="../../posts/parquet-to-arrow/">previous post</a>, we discussed how DataFusion prunes Parquet files to skip irrelevant <strong>files/row_groups</strong> (sometimes also <a href="https://parquet.apache.org/docs/file-format/pageindex/">pages</a>).</p>
<p>This post discusses how Parquet readers skip irrelevant <strong>rows</strong> while scanning data.</p>
<section id="why-filter-pushdown-in-parquet" class="level2">
<h2 class="anchored" data-anchor-id="why-filter-pushdown-in-parquet">Why filter pushdown in Parquet?</h2>
<p>Below is a query that reads sensor data with filters on <code>date_time</code> and <code>location</code>:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode sql code-with-copy"><code class="sourceCode sql"><span id="cb1-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">SELECT</span> val, location </span>
<span id="cb1-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">FROM</span> sensor_data </span>
<span id="cb1-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">WHERE</span> date_time <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'2025-03-12'</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">AND</span> location <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'office'</span>;</span></code></pre></div></div>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/parquet-pushdown/pushdown-vs-no-pushdown.jpg" class="img-fluid figure-img"></p>
<figcaption>Parquet pruning skips irrelevant files/row_groups, while filter pushdown skips irrelevant rows. Without filter pushdown, all rows from location, val, and date_time columns are decoded before <code>location='office'</code> is evaluated. Filter pushdown is especially useful when the filter is selective, i.e., removes many rows.</figcaption>
</figure>
</div>
<p>In our setup, sensor data is aggregated by date — each day has its own Parquet file. DataFusion prunes the unneeded Parquet files, i.e., 2025-03-10/11.parquet.</p>
<p>Once the files to read are located, the <a href="https://github.com/apache/datafusion/issues/3463"><em>current default implementation</em></a> reads all the projected columns (<code>sensor_id</code>, <code>val</code>, and <code>location</code>) into Arrow RecordBatches, then applies the filters over <code>location</code> to get the final set of rows.</p>
<p>A better approach is <strong>filter pushdown</strong>, which evaluates filter conditions first and only decodes data that passes these conditions. In practice, this works by first processing only the filter columns (like <code>location</code>), building a boolean mask of rows that satisfy our conditions, then using this mask to selectively decode only the relevant rows from other columns (<code>sensor_id</code>, <code>val</code>). This eliminates the waste of decoding rows that will be filtered out.</p>
<p>While simple in theory, practical implementations often make performance worse.</p>
</section>
<section id="why-slower" class="level2">
<h2 class="anchored" data-anchor-id="why-slower">Why slower?</h2>
<p>At a high level, the Parquet reader first builds a filter mask – essentially a boolean array indicating which rows meet the filter criteria – and then uses this mask to selectively decode only the needed rows from the remaining columns in the projection.</p>
<p>Let’s dig into details of <a href="https://github.com/apache/arrow-rs/blob/d5339f31a60a4bd8a4256e7120fe32603249d88e/parquet/src/arrow/async_reader/mod.rs#L618-L712">how filter pushdown is implemented</a> in the current Rust implementation of Parquet readers.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/parquet-pushdown/baseline-impl.jpg" class="img-fluid figure-img"></p>
<figcaption>Implementation of filter pushdown in Rust Parquet readers – the first phase builds the filter mask, the second phase applies the filter mask to the other columns</figcaption>
</figure>
</div>
<p>The filter pushdown has two phases:</p>
<ol type="1">
<li><p>Build the filter mask (steps 1-3)</p></li>
<li><p>Apply the filter mask to the other columns (steps 4-7)</p></li>
</ol>
<p>Within each phase, it takes three steps from Parquet to Arrow:</p>
<ol type="1">
<li><p>Decompress the Parquet pages using generic decompression algorithms like LZ4, Zstd, etc. (steps 1, 4, 6)</p></li>
<li><p>Decode the page content into Arrow format (steps 2, 5, 7)</p></li>
<li><p>Evaluate the filter over Arrow data (step 3)</p></li>
</ol>
<p>In the figure above, we can see that <code>location</code> is <strong>decompressed and decoded twice</strong>, first when building the filter mask (steps 1, 2), and second when building the output (steps 4, 5). This happens for all columns that appear both in the filter and output.</p>
<p>The table below shows the corresponding CPU time on the <a href="https://github.com/apache/datafusion/blob/main/benchmarks/queries/clickbench/queries.sql#L23">ClickBench query 22</a>:</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Decompress</th>
<th>Decode</th>
<th>Apply filter</th>
<th>Others</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>206 ms</td>
<td>117 ms</td>
<td>22 ms</td>
<td>48 ms</td>
</tr>
</tbody>
</table>
<p>Clearly, decompress/decode operations dominate the time spent. With filter pushdown, we need to decompress/decode three times; but without filter pushdown, we only need to do this twice. This explains why filter pushdown is slower.</p>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>Highly selective filters may skip the entire page; but as long as we read one row from the page, we need to decompress/decode the entire page.</p>
</div>
</div>
</section>
<section id="attempt-cache-filter-columns" class="level2">
<h2 class="anchored" data-anchor-id="attempt-cache-filter-columns">Attempt: cache filter columns</h2>
<p>Intuitively, caching the filter columns and reusing them later could help.</p>
<p>But caching consumes prohibitively high memory:</p>
<ol type="1">
<li><p>We need to cache Arrow arrays, which are on average <a href="https://github.com/XiangpengHao/liquid-cache/blob/main/dev/doc/liquid-cache-vldb.pdf">4x larger than Parquet data</a>.</p></li>
<li><p>We need to cache the <strong>entire column in memory</strong>, because in Phase 1 we build filters over the entire column, and only use it in Phase 2.</p></li>
<li><p>The memory usage is proportional to the number of filter columns, which can be unboundedly high.</p></li>
</ol>
<p>Worse, caching filter columns means we need to read partially from Parquet and partially from cache, which is complex to implement and requires a radical change to the current implementation.</p>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>Feel the complexity: consider building a cache that properly handles nested columns, multiple filters, and filters with multiple columns.</p>
</div>
</div>
</section>
<section id="real-solution" class="level2">
<h2 class="anchored" data-anchor-id="real-solution">Real solution</h2>
<p>We need a solution that:</p>
<ol type="1">
<li><p>Is simple to implement, i.e., doesn’t require thousands of lines of code.</p></li>
<li><p>Incurs minimal memory overhead.</p></li>
</ol>
<p>This section describes my <a href="https://github.com/apache/arrow-rs/pull/6921#issuecomment-2718792433">&lt;700 LOC PR (with lots of comments and tests)</a> that <strong>reduces total ClickBench time by 15%, with up to 2x lower latency for some queries, no obvious regression on other queries, and caches at most 2 pages (~2MB) per column in memory</strong>.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/parquet-pushdown/new-pipeline.jpg" class="img-fluid figure-img"></p>
<figcaption>New decoding pipeline, building filter mask and output columns are interleaved in a single pass, allowing us to cache minimal pages for minimal amount of time</figcaption>
</figure>
</div>
<p>The new pipeline interleaves the previous two phases into a single pass, so that:</p>
<ol type="1">
<li><p>The page being decompressed is immediately used to build filter masks and output columns.</p></li>
<li><p>We cache the decompressed page for minimal time; after one pass (steps 1-6), the cache memory is released for the next pass.</p></li>
</ol>
<p>This allows the cache to only hold 1 page at a time, and to immediately discard the previous page after it’s used, significantly reducing the memory requirement for caching.</p>
<section id="what-pages-are-cached" class="level3">
<h3 class="anchored" data-anchor-id="what-pages-are-cached">What pages are cached?</h3>
<p>You may have noticed that only <code>location</code> is cached, not <code>val</code>, because <code>val</code> is only used for output. More generally, only columns that appear both in the filter and output are cached, and at most 1 page is cached for each such column.</p>
<p>More examples:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode sql code-with-copy"><code class="sourceCode sql"><span id="cb2-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">SELECT</span> val </span>
<span id="cb2-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">FROM</span> sensor_data </span>
<span id="cb2-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">WHERE</span> date_time <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'2025-03-12'</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">AND</span> location <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'office'</span>;</span></code></pre></div></div>
<p>In this case, we don’t cache any columns, because <code>val</code> is not used for filtering.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode sql code-with-copy"><code class="sourceCode sql"><span id="cb3-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">SELECT</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">COUNT</span>(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span>) </span>
<span id="cb3-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">FROM</span> sensor_data </span>
<span id="cb3-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">WHERE</span> date_time <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'2025-03-12'</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">AND</span> location <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'office'</span>;</span></code></pre></div></div>
<p>In this case, we also don’t cache any columns, because the output projection is empty after query plan optimization.</p>
</section>
<section id="then-why-cache-2-pagescolumn-instead-of-1" class="level3">
<h3 class="anchored" data-anchor-id="then-why-cache-2-pagescolumn-instead-of-1">Then why cache 2 pages/column instead of 1?</h3>
<p>This is another real-world nuance regarding how Parquet layouts the pages.</p>
<p>Parquet by default encodes data using <a href="https://parquet.apache.org/docs/file-format/data-pages/encodings/">dictionary encoding</a>, which writes a dictionary page as the first page of a column chunk, followed by the keys referencing the dictionary.</p>
<p>You can see this in action using <a href="https://parquet-viewer.xiangpeng.systems">parquet-viewer</a>:</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/parquet-pushdown/parquet-viewer.jpg" class="img-fluid figure-img"></p>
<figcaption><a href="https://parquet-viewer.xiangpeng.systems">Parquet viewer</a> shows the page layout of a column chunk</figcaption>
</figure>
</div>
<p>This means that to decode a page of data, we actually need to reference two pages: the dictionary page and the data page.</p>
<p>This is why we cache 2 pages per column: one dictionary page and one data page. The data page slot will move forward as we read the data; but the dictionary page slot always references the first page.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/parquet-pushdown/cached-pages.jpg" class="img-fluid figure-img"></p>
<figcaption>Cached two pages, one for dictionary (pinned), one for data (moves as we read the data)</figcaption>
</figure>
</div>
</section>
</section>
<section id="how-does-it-perform" class="level2">
<h2 class="anchored" data-anchor-id="how-does-it-perform">How does it perform?</h2>
<p>Here are my results on <a href="https://github.com/apache/datafusion/tree/main/benchmarks#clickbench">ClickBench</a> on my AMD 9900X machine. The total time is reduced by 15%, with Q23 being 2.24x faster, and queries that get slower are likely due to noise.</p>
<pre><code>┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Query        ┃ no-pushdown ┃ new-pushdown ┃        Change ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ QQuery 0     │      0.47ms │       0.43ms │ +1.10x faster │
│ QQuery 1     │     51.10ms │      50.10ms │     no change │
│ QQuery 2     │     68.23ms │      64.49ms │ +1.06x faster │
│ QQuery 3     │     90.68ms │      86.73ms │     no change │
│ QQuery 4     │    458.93ms │     458.59ms │     no change │
│ QQuery 5     │    522.06ms │     478.50ms │ +1.09x faster │
│ QQuery 6     │     49.84ms │      49.94ms │     no change │
│ QQuery 7     │     55.09ms │      55.77ms │     no change │
│ QQuery 8     │    565.26ms │     556.95ms │     no change │
│ QQuery 9     │    575.83ms │     575.05ms │     no change │
│ QQuery 10    │    164.56ms │     178.23ms │  1.08x slower │
│ QQuery 11    │    177.20ms │     191.32ms │  1.08x slower │
│ QQuery 12    │    591.05ms │     569.92ms │     no change │
│ QQuery 13    │    861.06ms │     848.59ms │     no change │
│ QQuery 14    │    596.20ms │     580.73ms │     no change │
│ QQuery 15    │    554.96ms │     548.77ms │     no change │
│ QQuery 16    │   1175.08ms │    1146.07ms │     no change │
│ QQuery 17    │   1150.45ms │    1121.49ms │     no change │
│ QQuery 18    │   2634.75ms │    2494.07ms │ +1.06x faster │
│ QQuery 19    │     90.15ms │      89.24ms │     no change │
│ QQuery 20    │    620.15ms │     591.67ms │     no change │
│ QQuery 21    │    782.38ms │     703.15ms │ +1.11x faster │
│ QQuery 22    │   1927.94ms │    1404.35ms │ +1.37x faster │
│ QQuery 23    │   8104.11ms │    3610.76ms │ +2.24x faster │
│ QQuery 24    │    360.79ms │     330.55ms │ +1.09x faster │
│ QQuery 25    │    290.61ms │     252.54ms │ +1.15x faster │
│ QQuery 26    │    395.18ms │     362.72ms │ +1.09x faster │
│ QQuery 27    │    891.76ms │     959.39ms │  1.08x slower │
│ QQuery 28    │   4059.54ms │    4137.37ms │     no change │
│ QQuery 29    │    235.88ms │     228.99ms │     no change │
│ QQuery 30    │    564.22ms │     584.65ms │     no change │
│ QQuery 31    │    741.20ms │     757.87ms │     no change │
│ QQuery 32    │   2652.48ms │    2574.19ms │     no change │
│ QQuery 33    │   2373.71ms │    2327.10ms │     no change │
│ QQuery 34    │   2391.00ms │    2342.15ms │     no change │
│ QQuery 35    │    700.79ms │     694.51ms │     no change │
│ QQuery 36    │    151.51ms │     152.93ms │     no change │
│ QQuery 37    │    108.18ms │      86.03ms │ +1.26x faster │
│ QQuery 38    │    114.64ms │     106.22ms │ +1.08x faster │
│ QQuery 39    │    260.80ms │     239.13ms │ +1.09x faster │
│ QQuery 40    │     60.74ms │      73.29ms │  1.21x slower │
│ QQuery 41    │     58.75ms │      67.85ms │  1.15x slower │
│ QQuery 42    │     65.49ms │      68.11ms │     no change │
└──────────────┴─────────────┴──────────────┴───────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Benchmark Summary           ┃            ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ Total Time (no-pushdown)    │ 38344.79ms │
│ Total Time (new-pushdown)   │ 32800.50ms │
│ Average Time (no-pushdown)  │   891.74ms │
│ Average Time (new-pushdown) │   762.80ms │
│ Queries Faster              │         13 │
│ Queries Slower              │          5 │
│ Queries with No Change      │         25 │
└─────────────────────────────┴────────────┘</code></pre>
</section>
<section id="conclusion" class="level2">
<h2 class="anchored" data-anchor-id="conclusion">Conclusion</h2>
<p>Despite being simple in theory, filter pushdown in Parquet is non-trivial to implement. It requires understanding both the Parquet format and reader implementation details. The challenge lies in efficiently navigating through the dynamics of decoding, filter evaluation, and memory management.</p>


</section>

 ]]></description>
  <guid>https://blog.xiangpeng.systems/posts/parquet-pushdown/</guid>
  <pubDate>Wed, 12 Mar 2025 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Where are we now, system researchers?</title>
  <link>https://blog.xiangpeng.systems/posts/system-researchers/</link>
  <description><![CDATA[ 





<p>We, as system researchers, face an existential crisis. We find ourselves questioning our unique strengths and fundamental purpose where <strong>industry increasingly outpaces academic innovation</strong>.</p>
<section id="research-is-a-privilege" class="level2">
<h2 class="anchored" data-anchor-id="research-is-a-privilege">Research is a privilege</h2>
<p>Research is paid for by taxpayers, but we often forget this is a gift. We think we deserve this money because we’re smart or have fancy degrees, without asking if we really earned it.</p>
<p>I think spending money on research is good for everyone, but that doesn’t mean we can just take the money without being responsible to the public.</p>
<p>When someone pull the plug on the research funding, we could easily blame the person, and name a few research achievements that would be impossible without those funding. But from the bottom of my heart, I often have the same question that whether some of the research funding would be better spent on something else.</p>
<p>I’m not saying we should only fund research that promises clear results – that would go against the whole point of exploring new ideas. What worries me is research funding decisions are unaccountable to the public. The people who decide where money goes are usually academics themselves, who are often detached and highly alienated with what regular people actually need or care about. We often talk only to each other, praising work that looks good on paper but doesn’t help real people. Over time, with no one making us prove our worth, our research drifts further from what actually matters.</p>
<p>But the public has one last ultimate weapon — cutting off the money entirely. This tough move hurts, but sometimes it’s the only way to make researchers listen: we need to do work that actually matters to people.</p>
<p><strong>Research is a privilege, not an entitlement. We must question whether our work truly justifies the public investment we receive and delivers meaningful value to society.</strong></p>
</section>
<section id="system-research-is-irrelevant" class="level2">
<h2 class="anchored" data-anchor-id="system-research-is-irrelevant">System research is irrelevant</h2>
<p>System research is irrelevant. Industry has become the better place for meaningful systems work. Most impactful and innovative systems today come from companies, not universities.</p>
<p>Industry has the money and patience to build complete systems. But most importantly, industry systems are accountable – systems that don’t deliver value get shut down quickly. This accountability creates a natural selection process. Industry systems must stay relevant or die. They evolve to meet real needs or disappear.</p>
<p>Due to the unaccountable funding agencies mentioned above, <strong>research systems are often one-shot projects —- they are immediately abandoned right after publication.</strong> We are frequently impressed by the great systems coming from industry (the most recent example being the infrastructure at DeepSeek), and we’ve realized that we are far from competing with them. As a result, we seem to constrain ourselves to a few narrow research topics, solving problems that are difficult to connect with even a single real user (essentially imaginary problems). Or we simply adjust our goal from research to education, preparing students for their industry jobs, so they can continue research there. But if that is the case, why would we need research funding at all?</p>
</section>
<section id="we-are-unqualified" class="level2">
<h2 class="anchored" data-anchor-id="we-are-unqualified">We are unqualified</h2>
<p>PhD students are typically only a few years older than undergraduates. How can they possibly compete with senior industry practitioners who have been working on the same problems for decades?</p>
<p>Yet we are not even trying to compete. We don’t like “engineering problems”, because we researchers are supposed to work on “research problems”.</p>
<p>We like research problems for two simple reasons:</p>
<ol type="1">
<li><p>Feeling “researchy” makes us feel good about ourselves; simply thinking about fancy terminology makes us feel fundamental and important.</p></li>
<li><p><strong>We don’t know how to code.</strong></p></li>
</ol>
<p>We waste too much time babbling about knowledge we learn from papers – how to schedule a million machines, how to train a billion parameters, how to design infinitely scalable systems. Just thinking about these problems makes us feel important as researchers, although most of us have never deployed a service in the cloud, never used the techniques we proposed, and never worked with the filesystems, kernels, compilers, networks, or databases we studied. We waste time on these theoretical discussions because we don’t know how to code and are unwilling to practice. As Feynman said, “What I cannot create, I do not understand.” Simply knowing how a system works from 1000 feet doesn’t mean we can build it. The nuances of real systems often explain why they’re built in particular ways. Without diving into these details, we’re merely scratching the surface.</p>
<p>Here are my bold claims:</p>
<ul>
<li><p><strong>Your opinion doesn’t matter until you write &gt;50k lines of system code.</strong></p></li>
<li><p>Your novel idea is 💩 if it’s not implemented in a system with real users.</p></li>
</ul>
<p>(Writing code does not make you a good researcher, but not writing code makes you a bad one.)</p>
<p>The system research community does not need more novel solutions – novel solutions are essentially combinations of existing techniques. When we need to solve a problem, most of us would figure out a similar solution, and what matters is the execution of the ideas.</p>
<p>Instead, we need more people willing to sit down and code, build real systems, and talk to real users. <strong>Be a solid practitioner, don’t be a feel-good researcher.</strong></p>
<p><img src="https://blog.xiangpeng.systems/posts/system-researchers/meme.jpg" class="img-fluid" style="width:50.0%"></p>
</section>
<section id="we-are-pushing-young-researchers-away" class="level2">
<h2 class="anchored" data-anchor-id="we-are-pushing-young-researchers-away">We are pushing young researchers away</h2>
<p>WE ARE TOO SLOW because we waste too much time on procedural overhead rather than actual scientific exploration.</p>
<p>Paper publishing takes too much time. We spend too much effort arguing what’s new and what’s hard, instead of focusing on doing the actual research. Writing a paper already takes too much time, and then we need to anonymize artifacts, register abstracts, wait for reviews, write rebuttals, revise the paper, and can still be rejected for arbitrary reasons. The turnaround time for a single submission can be up to 6 months.</p>
<p>There is also a dark side to paper publishing. The entire publication process can be exploited by determined actors, and we are unwilling to address this issue, probably because many big names are involved. Young researchers see low-quality papers being accepted, yet their own carefully crafted systems are rejected for arbitrary reasons.</p>
<p>All of these burn our time and energy, pushing young researchers away from the community. After all, <strong>research doesn’t have to happen in academia</strong>.</p>
</section>
<section id="our-measurement-is-wrong" class="level2">
<h2 class="anchored" data-anchor-id="our-measurement-is-wrong">Our measurement is wrong</h2>
<p>All of our research projects start with the two questions of what’s new and what’s hard, but unfortunately, these two questions have guided us toward irrelevancy. Many one-shot papers claim novelty and then disappear, preventing future research projects from making progress. They take credit for being the first to introduce an idea, even when the implementation doesn’t work or is completely wrong. Yet, all future researchers are required to compare their work against these papers.</p>
<p>Most of the time, the code is terribly implemented or overly simplified, making fair comparisons impossible. But reviewers don’t care – they see the two papers as informationally equivalent, viewing the same idea from a 1000-foot perspective, and lazily question authors about what’s new and what’s hard.</p>
<p>The real difference between papers often lies in numerous small details that sound trivial but are actually essential for relevance. In most cases, <strong>figuring out these details takes much more time and demonstrates more novelty than coming up with the initial idea itself.</strong></p>
<p>The reviewers – often just a few years older than PhD students, don’t know how to code, unable to delve into implementation details, and incapable of appreciating real system nuances – will likely reject the paper for lack of novelty.</p>
<!-- ## But why we still need system research?
Despite all previous arguments, I still believe there is value in systems research, and I still believe we can be relevant.


### Tooling
Industry is driven by **profit**, while research serves the **public good**.

Industry in general spends way more resources on directly solving problems, rather than building good tooling to help solve problems.
For example, industry builds concurrent systems for better performance and more profitable business.
Without good tooling, they can still build such systems, but it requires a tremendous amount of resources, and sometimes this becomes a moat against competitors.

### Systems that takes a step back
Industry is extremely good at **solving** the immediate problems that are in front of them, but is systematically bad at **understanding** the problem.
 -->
</section>
<section id="system-research-is-knowing" class="level2">
<h2 class="anchored" data-anchor-id="system-research-is-knowing">System research is knowing</h2>
<!-- ![We must know, and we will know](9mtoh3.jpg)[^1] -->
<p>Despite all previous arguments, I still believe there is value in systems research, and I still believe we can be relevant, but that’s for another post.</p>
<p>System research is about knowing – understanding systems deeply and completely, contrasting with AI research that embraces uncertainty and probabilistic approaches.</p>
<p>We study the complex interactions of systems, and practice to know them better. We fight against the seemingly infinite complexity of systems, and uncover the underlying principles.</p>
<p><strong>Knowing requires a lot of practicing – hard work, patience, and a lot of coding – and we need a culture that cultivates this.</strong></p>


</section>

 ]]></description>
  <guid>https://blog.xiangpeng.systems/posts/system-researchers/</guid>
  <pubDate>Sun, 09 Mar 2025 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Online Parquet Viewer with Rust</title>
  <dc:creator>Xiangpeng Hao</dc:creator>
  <link>https://blog.xiangpeng.systems/posts/parquet-viewer/</link>
  <description><![CDATA[ 





<section id="quick-look" class="level2">
<h2 class="anchored" data-anchor-id="quick-look">Quick look</h2>
<p>Online here: <a href="https://parquet-viewer.xiangpeng.systems">https://parquet-viewer.xiangpeng.systems</a></p>
<p>Source code: <a href="https://github.com/XiangpengHao/parquet-viewer">parquet-viewer</a></p>
<p><a href="screenshot4.jpg" class="lightbox" data-gallery="quarto-lightbox-gallery-1"><img src="https://blog.xiangpeng.systems/posts/parquet-viewer/screenshot4.jpg" class="img-fluid" style="width:40.0%"></a></p>
<p><strong>Run SQL queries</strong> <img src="https://blog.xiangpeng.systems/posts/parquet-viewer/screenshot1.jpg" class="img-fluid" data-group="my-gallery"></p>
<p><strong>Visualize query plans</strong> <img src="https://blog.xiangpeng.systems/posts/parquet-viewer/screenshot2.jpg" class="img-fluid" data-group="my-gallery"></p>
<p><strong>Examine metadata</strong> <img src="https://blog.xiangpeng.systems/posts/parquet-viewer/screenshot3.jpg" class="img-fluid" data-group="my-gallery"></p>
</section>
<section id="how-it-works" class="level2">
<h2 class="anchored" data-anchor-id="how-it-works">How it works</h2>
<p>It compiles Parquet/Arrow/DataFusion to webassembly which runs in the browser.</p>
<p>Specifically, it uses <a href="https://trunkrs.dev"><code>trunk</code></a> to pack wasm files, <a href="https://leptos.dev"><code>leptos</code></a> to build reactive UI components, and <a href="https://tailwindcss.com"><code>Tailwind CSS</code></a> for styling.</p>
<p>And most importantly, LLM wrote most of the code.</p>
</section>
<section id="why-do-i-need-this" class="level2">
<h2 class="anchored" data-anchor-id="why-do-i-need-this">Why do I need this?</h2>
<p>Parquet files are not human-readable, you can’t just open them like CSV or JSON files.</p>
<p>Of course, there are many CLI tools that allow you to inspect file content – if you enjoy deciphering cryptic CLI args.</p>
<p>There are also Java/C#/Windows applications that provide GUI options – if you’re nostalgia for the 90s.</p>
<p>But with this tool, everything happens inside your favorite browser, written in your favorite language, using your favorite tech stack.</p>
<p>Simply drag and drop the file, and all the important information is in front of you.</p>
</section>
<section id="rust-for-frontend" class="level2">
<h2 class="anchored" data-anchor-id="rust-for-frontend">Rust for frontend?</h2>
<section id="the-good" class="level4">
<h4 class="anchored" data-anchor-id="the-good">The good</h4>
<ul>
<li><p>Leptos is surprisingly intuitive and easy-to-use.</p></li>
<li><p>DataFusion/Arrow/Parquet compiles to wasm with <em>almost</em> no modification.</p></li>
<li><p>There’s surprisingly small amount of code needed to build an interactive UI.</p></li>
</ul>
</section>
<section id="the-bad" class="level4">
<h4 class="anchored" data-anchor-id="the-bad">The bad</h4>
<ul>
<li><p>Build size is large, debug build is 100MB, release build is 40MB. Poor dev experience due to laggy edit-build-run cycle. (User experience is fine, though)</p></li>
<li><p>Not every crate is wasm-compatible, and it’s very difficult to triangle down the root cause.</p></li>
<li><p>I haven’t figure out how to nicely use JavaScript yet – sometimes you do need JS.</p></li>
</ul>


</section>
</section>

 ]]></description>
  <guid>https://blog.xiangpeng.systems/posts/parquet-viewer/</guid>
  <pubDate>Tue, 12 Nov 2024 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Caching in DataFusion</title>
  <dc:creator>Xiangpeng Hao</dc:creator>
  <link>https://blog.xiangpeng.systems/posts/caching-datafusion/</link>
  <description><![CDATA[ 





<p><strong>📢📢📢 Checkout <a href="https://github.com/XiangpengHao/liquid-cache">LiquidCache</a>, an open-source caching solution that reduces latency by 10x for cloud-native DataFusion.</strong></p>
<p>Note: my research is <a href="https://haoxp.xyz/fund/">funded</a> by <a href="https://www.influxdata.com">InfluxData</a>, which made this blog post possible.</p>
<hr>
<p>In the <a href="../../posts/parquet-to-arrow/">last post</a>, we discussed how DataFusion prunes Parquet files to read only the necessary data. This post explores DataFusion’s caching mechanisms, which help avoid repeating reads to object storage.</p>
<section id="current-state" class="level2">
<h2 class="anchored" data-anchor-id="current-state">Current state</h2>
<p>DataFusion provides a flexible and layered caching architecture that enables developers to optimize data access at multiple levels. The caching system is designed to be extensible, allowing custom implementations while providing sensible defaults. Let’s examine the built-in caching mechanisms that make DataFusion efficient.</p>
<section id="list-files-cache" class="level4">
<h4 class="anchored" data-anchor-id="list-files-cache">1. List files cache</h4>
<p>The <a href="https://github.com/apache/datafusion/blob/5db274004bc4a7d493aba6764a8521694a67cd11/datafusion/execution/src/cache/cache_unit.rs#L103">list files cache</a> optimizes directory scanning operations in DataFusion. When a <a href="https://github.com/apache/datafusion/blob/5db274004bc4a7d493aba6764a8521694a67cd11/datafusion/core/src/datasource/listing/table.rs#L683">ListingTable</a> needs to access files in a directory, it first checks this cache before performing expensive <a href="https://github.com/apache/datafusion/blob/5db274004bc4a7d493aba6764a8521694a67cd11/datafusion/core/src/datasource/listing/url.rs#L207">filesystem listing operations</a>.</p>
<p>This cache is implemented as a simple but effective <code>HashMap</code> that stores directory paths as keys and lists of file metadata as values. The metadata includes important file information like size, last modified time, and other attributes that would otherwise require filesystem/network calls to retrieve:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb1-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">struct</span> ListFilesCache <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb1-2">    cached<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> HashMap<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Path</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Vec</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>ObjectMeta<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;,</span></span>
<span id="cb1-3"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
</section>
<section id="file-statistics-cache" class="level4">
<h4 class="anchored" data-anchor-id="file-statistics-cache">2. File statistics cache</h4>
<p>The <a href="https://github.com/apache/datafusion/blob/5db274004bc4a7d493aba6764a8521694a67cd11/datafusion/execution/src/cache/cache_unit.rs#L31">file statistics cache</a> stores important statistics about each file, such as row count and column statistics (min/max values). These statistics are used by DataFusion’s query optimizer to make better execution decisions, like pruning files that cannot contain matching data.</p>
<p>The cache avoids having to <a href="https://github.com/apache/datafusion/blob/5db274004bc4a7d493aba6764a8521694a67cd11/datafusion/core/src/datasource/listing/table.rs#L1091">recompute/re-decode these statistics</a> for each query by storing them in a <code>HashMap</code> that maps file paths to both file metadata and statistics:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb2-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">struct</span> FileStatisticsCache <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb2-2">    cached<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> HashMap<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Path</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> (ObjectMeta<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> Statistics)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;,</span></span>
<span id="cb2-3"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
</section>
<section id="parquet-metadata-cache" class="level4">
<h4 class="anchored" data-anchor-id="parquet-metadata-cache">3. Parquet metadata cache</h4>
<p>Getting Parquet metadata can be costly for two main reasons:</p>
<ol type="1">
<li>Network overhead: It requires up to 2 separate network requests to retrieve the raw metadata bytes - one for the footer offset and another for the actual metadata</li>
<li>Processing overhead: Decoding the metadata can be computationally expensive, especially for <a href="https://www.influxdata.com/blog/how-good-parquet-wide-tables/">tables with many columns</a></li>
</ol>
<p>To address these challenges, DataFusion provides the <a href="https://github.com/apache/datafusion/blob/5db274004bc4a7d493aba6764a8521694a67cd11/datafusion/core/src/datasource/physical_plan/parquet/reader.rs#L39">ParquetFileReaderFactory</a> trait. This trait allows developers to implement custom metadata handling strategies. The factory returns an <code>AsyncFileReader</code> that separates metadata access from data page access, enabling different caching and optimization approaches for each:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb3-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">pub</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">trait</span> AsyncFileReader<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">Send</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb3-2">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> get_metadata(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">mut</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> BoxFuture<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">'_</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Result</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>Arc<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>ParquetMetaData<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;&gt;;</span></span>
<span id="cb3-3"></span>
<span id="cb3-4">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> get_bytes(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">mut</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> range<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> Range<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">usize</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> BoxFuture<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">'_</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Result</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>Bytes<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;;</span></span>
<span id="cb3-5"></span>
<span id="cb3-6">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">...</span> <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">// other methods</span></span>
<span id="cb3-7"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
</section>
<section id="parquet-file-range-cache" class="level4">
<h4 class="anchored" data-anchor-id="parquet-file-range-cache">4. Parquet file range cache</h4>
<p>Lastly and most importantly, DataFusion caches the Parquet file ranges that have been read.</p>
<p>Under the hood, DataFusion uses <a href="https://docs.rs/object_store/latest/object_store/">object_store</a> to interact with the S3, GCS, or local filesystem. While object_store provides a unified interface for different storage backends, but it does not implement caching by default.</p>
<p>However, DataFusion’s flexible architecture allows developers to implement custom caching strategies by implementing the <code>AsyncFileReader</code> trait. This enables optimizations like:</p>
<ul>
<li>Caching frequently accessed data ranges in memory</li>
<li>Implementing tiered caching (e.g., memory + local disk)</li>
<li>Adding compression to reduce memory usage</li>
<li>Implementing cache eviction policies based on access patterns</li>
</ul>
<p>Notably, the <code>AsyncFileReader</code> trait has a <code>get_bytes_ranges</code> method:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb4-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">pub</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">trait</span> AsyncFileReader<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">Send</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb4-2">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">...</span> <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">// previously mentioned methods </span></span>
<span id="cb4-3"></span>
<span id="cb4-4">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> get_byte_ranges(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">mut</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> ranges<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Vec</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>Range<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">usize</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> BoxFuture<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">'_</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Result</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Vec</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>Bytes<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;&gt;;</span></span>
<span id="cb4-5"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
<p>The default implementation of <code>get_byte_ranges</code> simply calls <code>get_bytes</code> sequentially for each range, which can be inefficient when reading many small ranges. This is because each range requires a separate network request, leading to higher latency and costs.</p>
<p>However, developers can implement their own <a href="https://xiangpeng-hao.notion.site/Cool-Projects-with-Modern-Data-Analytics-11ed7d7dd30380a888eed5614fb79aa0#11ed7d7dd3038017b7b1e0c589aa6765">IO coalescing logic</a> to optimize performance. For example, they could:</p>
<ul>
<li>Merge adjacent or overlapping ranges to reduce the number of requests</li>
<li>Batch multiple small ranges into a single larger request</li>
<li>Implement prefetching for ranges likely to be needed soon</li>
</ul>
<p>These optimizations can significantly improve read performance and reduce storage costs, especially when working with remote storage like S3 or GCS.</p>
<p>So far we have discussed the core caching mechanisms built into DataFusion. These caches work together to optimize different aspects of query execution, from file discovery to data access.</p>
<p>The following sections will explore more advanced topics and future directions for DataFusion’s caching capabilities.</p>
</section>
</section>
<section id="caching-arrow" class="level2">
<h2 class="anchored" data-anchor-id="caching-arrow">Caching Arrow</h2>
<p>Arrow is the in-memory columnar format that DataFusion uses to process data efficiently. Before DataFusion can execute any query logic, Parquet data must be <a href="../../posts/parquet-to-arrow/">decoded into Arrow format</a>. This decoding process involves decompressing the data, converting between data types, and validating constraints - operations that recent research<sup>1</sup> has shown can be a performance bottleneck for many analytical workloads.</p>
<p>One promising optimization approach is to cache the decoded Arrow data rather than the raw Parquet bytes. This allows us to skip the expensive decoding step on subsequent queries, potentially improving query latency significantly.</p>
<p>The figure below compares query latencies between two caching strategies using the <a href="https://github.com/apache/datafusion/blob/main/benchmarks/queries/clickbench/queries.sql">ClickBench</a> benchmark suite. The x-axis shows the query ID (0-42) and the y-axis shows query latency in milliseconds (lower is better). For each query, we measure the latency when caching the raw Parquet bytes versus caching the decoded Arrow arrays.</p>
<p><img src="https://blog.xiangpeng.systems/posts/caching-datafusion/cache-arrow.png" class="img-fluid"></p>
<section id="takeaways" class="level4">
<h4 class="anchored" data-anchor-id="takeaways">Takeaways</h4>
<ul>
<li>Caching Arrow consistently outperforms or matches caching Parquet across all queries.</li>
<li>The performance gains vary significantly:
<ul>
<li>Scan-intensive queries (Q20-Q23) show the largest improvements, with up to 3x speedup, since they benefit directly from avoiding Parquet decoding</li>
<li>Aggregation-heavy queries (Q8-Q18) see more modest gains, as their execution time is dominated by computation rather than data access</li>
</ul></li>
<li>Memory usage can be a concern - Q23 triggered an out-of-memory error when caching Arrow data, highlighting its excessive memory usage.</li>
</ul>
</section>
<section id="but-at-what-cost" class="level3">
<h3 class="anchored" data-anchor-id="but-at-what-cost">But at what cost?</h3>
<p>While caching Arrow data can significantly improve query performance, it comes with substantial memory overhead, as demonstrated by Q23’s out-of-memory error. The figure below compares memory usage between caching Parquet versus Arrow data across the benchmark queries. Since each query was run independently, the measurements reflect the memory requirements for executing a single query in isolation.</p>
<p><img src="https://blog.xiangpeng.systems/posts/caching-datafusion/cache-arrow-memory.png" class="img-fluid"></p>
<section id="takeaways-1" class="level4">
<h4 class="anchored" data-anchor-id="takeaways-1">Takeaways</h4>
<ul>
<li>Parquet achieves roughly 4x-5x compression ratio compared to Arrow’s in-memory format</li>
<li>The benefit of caching Arrow data varies, but its cost is consistently 4x-5x higher memory usage.</li>
</ul>
</section>
</section>
<section id="zoom-in-a-bit" class="level3">
<h3 class="anchored" data-anchor-id="zoom-in-a-bit">Zoom-in a bit</h3>
<p>Now we take a closer look at Q21 – one of the queries that benefit a lot from caching Arrow:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode sql code-with-copy"><code class="sourceCode sql"><span id="cb5-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">SELECT</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">"SearchPhrase"</span>, <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">MIN</span>(<span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">"URL"</span>), <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">COUNT</span>(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span>) <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">AS</span> c </span>
<span id="cb5-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">FROM</span> hits </span>
<span id="cb5-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">WHERE</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">"URL"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">LIKE</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'%google%'</span> </span>
<span id="cb5-4">      <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">AND</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">"SearchPhrase"</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;&gt;</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">''</span> </span>
<span id="cb5-5"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">GROUP</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">BY</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">"SearchPhrase"</span> </span>
<span id="cb5-6"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">ORDER</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">BY</span> c </span>
<span id="cb5-7"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">DESC</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">LIMIT</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span>;</span></code></pre></div></div>
<p>The query scans two string columns (“URL” and “SearchPhrase”) and applies a filter on them.</p>
<p>Let’s now compare their query time and memory usage – we got roughly 3x speedup by using 4x more memory, not bad! <img src="https://blog.xiangpeng.systems/posts/caching-datafusion/cache-arrow-q21.png" class="img-fluid"></p>
</section>
<section id="how-to-cache-arrow" class="level3">
<h3 class="anchored" data-anchor-id="how-to-cache-arrow">How to cache Arrow?</h3>
<p>The figure below builds on the architecture from our <a href="../../posts/parquet-to-arrow/">previous post</a>, adding the new Arrow cache component. The Arrow cache is positioned between the <a href="https://github.com/apache/datafusion/blob/5db274004bc4a7d493aba6764a8521694a67cd11/datafusion/core/src/datasource/physical_plan/parquet/access_plan.rs#L86">ParquetAccessPlan</a> and the decoded Arrow RecordBatch. When a query requests data:</p>
<ol type="1">
<li>First, we check if the requested RecordBatch exists in the cache</li>
<li>If found, we can skip both fetching and decoding the Parquet data by pruning the AccessPlan</li>
<li>If not found, we fetch and decode the Parquet data as normal, then insert the resulting Arrow RecordBatch into the cache for future use</li>
</ol>
<p><img src="https://blog.xiangpeng.systems/posts/caching-datafusion/cache-arrow-arch.jpg" class="img-fluid"></p>
<p>Although the architecture is simple, implementing it faces several challenges:</p>
<ol type="1">
<li>How to map Parquet byte ranges to the corresponding Arrow RecordBatches</li>
<li>Granularity/shape of caching – column-level vs batch-level caching.</li>
<li>How to efficiently test if the cached ranges contains the requested range? E.g., request range (1024, 2048), but cached ranges has [(0, 2049), (4096, 3072)].</li>
<li>Memory management – implementing efficient spill-to-disk strategies</li>
</ol>
<p>We are actively working on solutions to these challenges as part of our research into high-performance Arrow caching systems. Our goal is to develop practical implementations that can be integrated into production environments. We plan to publish our findings and release the code as open source in the near future. Consider <a href="https://haoxp.xyz/fund/">funding my research</a> to support this work.</p>
</section>
</section>
<section id="standalone-caching-service" class="level2">
<h2 class="anchored" data-anchor-id="standalone-caching-service">Standalone caching service</h2>
<p>So far we have discussed caching within individual DataFusion instances, as shown in the Figure below (upper). While this approach works well for single-instance deployments, it is wasteful when multiple DataFusion instances access the same data.</p>
<p>A more scalable approach is to implement a standalone shared caching service (lower) that can be accessed by multiple DataFusion instances, with the following advantages:</p>
<ol type="1">
<li>Reduced resource usage - By eliminating redundant caching and decoding across instances, it optimizes both memory and CPU utilization</li>
<li>Improved manageability - Decoupling the caching logic from compute nodes simplifies scaling and operational management</li>
<li>Consistent performance - No latency spike after compute node restarts. With stateless compute nodes and a persistent centralized cache, the system avoids cache warmup delays after node restarts</li>
</ol>
<p><img src="https://blog.xiangpeng.systems/posts/caching-datafusion/shared-cache.jpg" class="img-fluid"></p>
<section id="caching-interface" class="level3">
<h3 class="anchored" data-anchor-id="caching-interface">Caching interface</h3>
<p>The simplest caching interface treats the caching service as a transparent proxy for object storage. The service implements DataFusion’s <code>AsyncFileReader</code> trait to intercept Parquet file reads, caching byte ranges as they are accessed.</p>
<p>The bytes are transferred between DataFusion and the cache service using HTTP and can be implemented in any programming languages. This simple interface requires minimal changes to DataFusion.</p>
<p>A more sophisticated approach is to implement caching through the Arrow Flight protocol, with the caching service running as a full DataFusion instance capable of executing query plans. Rather than just serving cached bytes, this design allows the caching service to process queries directly.</p>
<p>When a querier needs data, it sends the complete <code>ParquetExec</code> physical plan to the caching service. The service can then:</p>
<ol type="1">
<li>Execute the plan against its cached data</li>
<li>Apply filters and projections directly on the cached data (filter pushdown)</li>
<li>Return only the necessary Arrow RecordBatches to the querier</li>
</ol>
<p>This architecture provides several advantages: - Reduced network transfer by filtering data at the cache layer - Lower client-side CPU usage since filtering happens at the cache</p>
<p>The tradeoff is increased complexity in both the client and cache service implementations compared to the simple byte-range caching approach.</p>
</section>
<section id="caching-medium" class="level3">
<h3 class="anchored" data-anchor-id="caching-medium">Caching medium</h3>
<p>The standalone caching service allows a diverse set of storage mediums to optimize the performance and cost.</p>
<p>The most obvious choice is to cache data in memory, which is fast but expensive. We can do better by spilling the data to SSD when memory is full, and to HDD when SSD is full.</p>
</section>
</section>
<section id="conclusion" class="level2">
<h2 class="anchored" data-anchor-id="conclusion">Conclusion</h2>
<p>In this post, we discussed DataFusion’s caching mechanisms and explored the potential of caching Arrow data in standalone caching services. We believe that caching is a key component for every cloud-native analytics systems, and my research project is actively building systems that bridges the gap between what academia known as the state-of-the-art and what industry can actually use.</p>


</section>


<div id="quarto-appendix" class="default"><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>BtrBlocks: Efficient Columnar Compression for Data Lakes (<a href="https://www.cs.cit.tum.de/fileadmin/w00cfj/dis/papers/btrblocks.pdf">SIGMOD 2023 Paper</a>)↩︎</p></li>
</ol>
</section></div> ]]></description>
  <guid>https://blog.xiangpeng.systems/posts/caching-datafusion/</guid>
  <pubDate>Sun, 27 Oct 2024 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Parquet pruning in DataFusion</title>
  <dc:creator>Xiangpeng Hao</dc:creator>
  <link>https://blog.xiangpeng.systems/posts/parquet-to-arrow/</link>
  <description><![CDATA[ 





<p>Note: special thanks to <a href="https://www.influxdata.com">InfluxData</a> for <a href="https://xiangpeng.systems/fund/">funding</a> this blog post.</p>
<hr>
<p>Apache Parquet has become the industry standard for storing columnar data, and reading Parquet efficiently – especially from remote storage – is crucial for query performance.</p>
<p>Apache DataFusion implements advanced Parquet pruning techniques to effectively read only the data that matters for a given query.</p>
<p>Achieving high performance adds complexity. This post provides an overview of the techniques used in DataFusion to selectively read Parquet files.</p>
<section id="the-pipeline" class="level3">
<h3 class="anchored" data-anchor-id="the-pipeline">The pipeline</h3>
<p>The diagram below illustrates the Parquet reading pipeline in DataFusion, highlighting how data flows through various pruning stages before being converted to Arrow format:</p>
<p><img src="https://blog.xiangpeng.systems/posts/parquet-to-arrow/read-parquet.jpg" class="img-fluid"></p>
<section id="background-parquet-file-structure" class="level4">
<h4 class="anchored" data-anchor-id="background-parquet-file-structure">Background: Parquet file structure</h4>
<p>As shown in the figure above, each Parquet file has multiple row groups. Each row group contains a set of columns, and each column contains a set of pages.</p>
<p>Pages are the smallest units of data in Parquet files and typically contain compressed and encoded values for a specific column. This hierarchical structure enables efficient columnar access and forms the foundation for the pruning techniques we’ll discuss.</p>
<p>Check out <a href="https://www.influxdata.com/blog/querying-parquet-millisecond-latency/">Querying Parquet with Millisecond Latency</a> for more details on the Parquet file structure.</p>
</section>
<section id="read-metadata" class="level4">
<h4 class="anchored" data-anchor-id="read-metadata">1. Read metadata</h4>
<p>DataFusion first reads the Parquet metadata to understand the data in the file. Metadata often includes data schema, the exact location of each row group and column chunk, and their corresponding statistics (e.g., min/max values). It also optionally includes <a href="https://parquet.apache.org/docs/file-format/pageindex/">page-level stats</a> and <a href="https://www.influxdata.com/blog/using-parquets-bloom-filters/">Bloom filters</a>. This information is used to prune the file before reading the actual data.</p>
<p><a href="https://github.com/apache/datafusion/blob/31701b8dc9c6486856c06a29a32107d9f4549cec/datafusion/core/src/datasource/physical_plan/parquet/reader.rs#L118">Fetching metadata</a> requires up to two network requests: one to read the footer size from the end of the file, and another to read the footer itself.</p>
<p><a href="https://www.influxdata.com/blog/how-good-parquet-wide-tables/">Decoding metadata</a> is generally fast since it only requires parsing a small amount of data. However, for tables with hundreds or thousands of columns, the metadata can become quite large and decoding it can become a bottleneck. This is particularly noticeable when scanning many small files.</p>
<p>Reading metadata is latency-critical, so DataFusion allows users to cache metadata through the <a href="https://github.com/apache/datafusion/blob/31701b8dc9c6486856c06a29a32107d9f4549cec/datafusion/core/src/datasource/physical_plan/parquet/reader.rs#L39">ParquetFileReaderFactory</a> trait.</p>
</section>
<section id="prune-by-projection" class="level4">
<h4 class="anchored" data-anchor-id="prune-by-projection">2. Prune by projection</h4>
<p>The simplest yet perhaps most effective pruning is to read only the columns that are needed. This is because queries usually don’t select all columns, e.g., <code>SELECT a FROM table</code> only reads column <code>a</code>. As a <strong>columnar</strong> format, Parquet allows DataFusion to <a href="https://github.com/apache/datafusion/blob/31701b8dc9c6486856c06a29a32107d9f4549cec/datafusion/core/src/datasource/physical_plan/parquet/mod.rs#L778">only read</a> the <strong>columns</strong> that are needed.</p>
<p>This projection pruning happens at the column level and can dramatically reduce I/O when working with wide tables where queries typically access only a small subset of columns.</p>
</section>
<section id="prune-by-row-group-stats-and-bloom-filters" class="level4">
<h4 class="anchored" data-anchor-id="prune-by-row-group-stats-and-bloom-filters">3. Prune by row group stats and Bloom filters</h4>
<p>Each row group has <a href="https://github.com/apache/datafusion/blob/31701b8dc9c6486856c06a29a32107d9f4549cec/datafusion/core/src/physical_optimizer/pruning.rs#L81">basic stats</a> like min/max values for each column. DataFusion applies the query predicates to these stats to prune row groups, e.g., <code>SELECT * FROM table WHERE a &gt; 10</code> will only read row groups where <code>a</code> has a max value greater than 10.</p>
<p>Sometimes min/max stats are too simple to prune effectively, so Parquet also supports <a href="https://www.influxdata.com/blog/using-parquets-bloom-filters/">Bloom filters</a>. DataFusion <a href="https://github.com/apache/datafusion/blob/31701b8dc9c6486856c06a29a32107d9f4549cec/datafusion/core/src/datasource/physical_plan/parquet/opener.rs#L202">uses Bloom filters when available</a>.</p>
<p>Bloom filters are particularly effective for equality predicates (<code>WHERE a = 10</code>) and can significantly reduce the number of row groups that need to be read for point queries or queries with highly selective predicates.</p>
</section>
<section id="prune-by-page-stats" class="level4">
<h4 class="anchored" data-anchor-id="prune-by-page-stats">4. Prune by page stats</h4>
<p>Parquet optionally supports <a href="https://github.com/apache/parquet-format/blob/master/PageIndex.md">page-level stats</a> – similar to row group stats but more fine-grained. DataFusion implements <a href="https://github.com/apache/datafusion/blob/31701b8dc9c6486856c06a29a32107d9f4549cec/datafusion/core/src/datasource/physical_plan/parquet/opener.rs#L219">page pruning</a> when the stats are present.</p>
<p>Page-level pruning provides an additional layer of filtering after row group pruning. It allows DataFusion to skip individual pages within a row group, further reducing the amount of data that needs to be read and decoded.</p>
</section>
<section id="read-from-storage" class="level4">
<h4 class="anchored" data-anchor-id="read-from-storage">5. Read from storage</h4>
<p>Now we (hopefully) have pruned the Parquet file into small ranges of bytes, i.e., the <a href="https://github.com/apache/datafusion/blob/76a7789ace33ced54c973fa0d5fc9d1866e1bf19/datafusion/datasource-parquet/src/access_plan.rs#L86">Access Plan</a>. The last step is to <a href="https://github.com/apache/datafusion/blob/31701b8dc9c6486856c06a29a32107d9f4549cec/datafusion/core/src/datasource/physical_plan/parquet/reader.rs#L103">make requests</a> to fetch those bytes and decode them into Arrow RecordBatch.</p>
</section>
</section>
<section id="bonus-filter-pushdown" class="level3">
<h3 class="anchored" data-anchor-id="bonus-filter-pushdown">Bonus: filter pushdown</h3>
<p>So far we have discussed techniques that prune the Parquet file using only the metadata, i.e., before reading the actual data.</p>
<p>Filter pushdown, also known as predicate pushdown, is a technique that prunes data during scanning, with filters being generated and applied in the Parquet reader.</p>
<p><img src="https://blog.xiangpeng.systems/posts/parquet-to-arrow/filter-pushdown.jpg" class="img-fluid"></p>
<p>Unlike metadata-based pruning which works at the row group or page level, filter pushdown operates at the row level, allowing DataFusion to filter out individual rows that don’t match the query predicates during the decoding process.</p>
<p>DataFusion <a href="https://github.com/apache/datafusion/blob/31701b8dc9c6486856c06a29a32107d9f4549cec/datafusion/core/src/datasource/physical_plan/parquet/row_filter.rs#L154">implements filter pushdown</a> but has <a href="https://github.com/apache/datafusion/blob/31701b8dc9c6486856c06a29a32107d9f4549cec/datafusion/common/src/config.rs#L382">not enabled it by default</a> due to <a href="https://github.com/apache/datafusion/issues/3463">some performance regressions</a>.</p>
<p>We are working to remove the <a href="https://github.com/apache/arrow-rs/issues/5523#issuecomment-2429470872">remaining performance issues</a> and enable it by default, which we will discuss in the next blog post.</p>
</section>
<section id="conclusion" class="level3">
<h3 class="anchored" data-anchor-id="conclusion">Conclusion</h3>
<p>DataFusion employs a multi-step approach to Parquet pruning, from column projection to row group stats, page stats, and potentially row-level filtering. Each step may reduce the amount of data to be read and processed, significantly improving query performance.</p>


</section>

 ]]></description>
  <guid>https://blog.xiangpeng.systems/posts/parquet-to-arrow/</guid>
  <pubDate>Thu, 24 Oct 2024 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Use StringView to make DataFusion faster</title>
  <dc:creator>Xiangpeng Hao, Andrew Lamb</dc:creator>
  <link>https://blog.xiangpeng.systems/posts/string-view-datafusion/</link>
  <description><![CDATA[ 





<p>Editor note: This blog post is adapted from the InfluxData blog post series, make sure to also check out the original posts <a href="https://www.influxdata.com/blog/faster-queries-with-stringview-part-one-influxdb/">1</a> <a href="https://www.influxdata.com/blog/faster-queries-with-stringview-part-two-influxdb/">2</a>.</p>
<p>Note: Thanks to InfluxData for sponsoring this work as a summer intern project</p>
<hr>
<p>This blog describes our experience implementing <a href="https://arrow.apache.org/docs/format/Columnar.html#variable-size-binary-view-layout">StringView</a> in the <a href="https://github.com/apache/arrow-rs">Rust implementation</a> of <a href="https://arrow.apache.org/">Apache Arrow</a>, and integrating it into <a href="https://datafusion.apache.org/">Apache DataFusion</a>, significantly accelerating string-intensive queries in the <a href="https://benchmark.clickhouse.com/">ClickBench</a> benchmark by 20%- 200% (Figure 1<sup>1</sup>).</p>
<p>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 <a href="https://www.influxdata.com/blog/flight-datafusion-arrow-parquet-fdap-architecture-influxdb/">FDAP ecosystem</a>’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.&nbsp;</p>
<p>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 <a href="https://db.in.tum.de/~freitag/papers/p29-neumann-cidr20.pdf">someone describes it clearly</a>. Although simple, straightforward implementation actually <em>slows down performance for almost every query</em>. We must, therefore, apply astute observations and diligent engineering to realize the actual benefits from StringView.&nbsp;</p>
<p>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 will lead to practical system improvements.</p>
<p>StringView support was released as part of arrow-rs <a href="https://crates.io/crates/arrow/52.2.0">v52.2.0</a> and DataFusion v41.0.0. You can try it by setting the <code>schema_force_string_view</code> <a href="https://datafusion.apache.org/user-guide/configs.html">DataFusion configuration option</a>, and we are <a href="https://github.com/apache/datafusion/issues/11682">hard at work with the community to</a> 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.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/string-view-datafusion/click-perf.png" class="img-fluid figure-img"></p>
<figcaption>Figure 1: StringView improves string-intensive ClickBench query performance by 20% - 200%</figcaption>
</figure>
</div>
<section id="section-1-what-is-stringview" class="level2">
<h2 class="anchored" data-anchor-id="section-1-what-is-stringview">Section 1: What is StringView?</h2>
<p>The concept of inlined strings with prefixes (called “German Strings” <a href="https://x.com/andy_pavlo/status/1813258735965643203">by Andy Pavlo</a>, in homage to <a href="https://www.tum.de/">TUM</a>, where the <a href="https://db.in.tum.de/~freitag/papers/p29-neumann-cidr20.pdf">Umbra paper that describes</a> them originated) has been used in many recent database systems (<a href="https://engineering.fb.com/2024/02/20/developer-tools/velox-apache-arrow-15-composable-data-management/">Velox</a>, <a href="https://pola.rs/posts/polars-string-type/">Polars</a>, <a href="https://duckdb.org/2021/12/03/duck-arrow.html">DuckDB</a>, <a href="https://cedardb.com/blog/german_strings/">CedarDB</a>, etc.) and was introduced to Arrow as a new <a href="https://arrow.apache.org/docs/format/Columnar.html#variable-size-binary-view-layout">StringViewArray</a><sup>2</sup> type. Arrow’s original <a href="https://arrow.apache.org/docs/format/Columnar.html#variable-size-binary-layout">StringArray</a> 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.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/string-view-datafusion/string-view.png" class="img-fluid figure-img"></p>
<figcaption>Figure 2: Use StringArray and StringViewArray to represent the same string content.</figcaption>
</figure>
</div>
<p>A StringViewArray consists of three components:</p>
<ol type="1">
<li>The <em><code>view</code></em> array<br>
</li>
<li>The buffers<br>
</li>
<li>The buffer pointers (IDs) that map buffer offsets to their physical locations</li>
</ol>
<p>Each <code>view</code> is 16 bytes long, and its contents differ based on the string’s length:</p>
<ul>
<li>string length &lt; 12 bytes: the first four bytes store the string length, and the remaining 12 bytes store the inlined string.<br>
</li>
<li>string length &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.</li>
</ul>
<p>Figure 2 shows an example of the same logical content (left) using StringArray (middle) and StringViewArray (right):</p>
<ul>
<li>The first string – <code>“Apache DataFusion”</code> – 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 – <code>“Apac”</code> – in the view.<br>
</li>
<li>The second string, <code>“InfluxDB”</code> is only 8 bytes long, so StringViewArray completely inlines the string content in the <code>view</code> struct while StringArray stores the string in the buffer as well.<br>
</li>
<li>The third string <code>“Arrow Rust Impl”</code> is 15 bytes long and cannot be fully inlined. StringViewArray stores this in the same form as the first string.<br>
</li>
<li>The last string <code>“Apache DataFusion”</code> 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.</li>
</ul>
<p>StringViewArray provides three opportunities for outperforming StringArray:</p>
<ol type="1">
<li>Less copying via the offset + buffer format<br>
</li>
<li>Faster comparisons using the inlined string prefix<br>
</li>
<li>Reusing repeated string values with the flexible <code>view</code> layout</li>
</ol>
<p>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.</p>
</section>
<section id="section-2-faster-parquet-loading" class="level2">
<h2 class="anchored" data-anchor-id="section-2-faster-parquet-loading">Section 2: Faster Parquet Loading</h2>
<p><a href="https://parquet.apache.org/">Apache Parquet</a> is the de facto format for storing large-scale analytical data commonly stored LakeHouse-style, such as <a href="https://iceberg.apache.org">Apache Iceberg</a> and <a href="https://delta.io">Delta Lake</a>. Efficiently loading data from Parquet is thus critical to query performance in many important real-world workloads.</p>
<p>Parquet encodes strings (i.e., <a href="https://docs.rs/parquet/latest/parquet/data_type/struct.ByteArray.html">byte array</a>) 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.</p>
<p>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 <code>“Arrow Rust Impl”</code> is represented by a <code>view</code> with offset 37 and length 15 into that buffer.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/string-view-datafusion/parquet.png" class="img-fluid figure-img" style="width:70.0%"></p>
<figcaption>Figure 4: StringViewArray avoids copying by reusing decoded Parquet pages.</figcaption>
</figure>
</div>
<p><strong>Mini benchmark</strong><br>
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:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">cargo</span> bench <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--bench</span> arrow_reader <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--features</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"arrow test_common experimental"</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"arrow_array_reader/Binary.*Array/plain encoded"</span></span></code></pre></div></div>
<p>Our benchmarking machine shows that loading <em>BinaryViewArray</em> is almost 2x faster than loading BinaryArray (see next section about why this isn’t <em>String</em>ViewArray).</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode txt code-with-copy"><code class="sourceCode default"><span id="cb2-1">arrow_array_reader/BinaryArray/plain encoded                        time:   [315.86 µs 317.47 µs 319.00 µs]</span>
<span id="cb2-2"></span>
<span id="cb2-3">arrow_array_reader/BinaryViewArray/plain encoded</span>
<span id="cb2-4">time:   [162.08 µs 162.20 µs 162.32 µs]</span></code></pre></div></div>
<p>You can read more on this arrow-rs issue: <a href="https://github.com/apache/arrow-rs/issues/5904">https://github.com/apache/arrow-rs/issues/5904</a></p>
<section id="section-2.1-from-binary-to-strings" class="level3">
<h3 class="anchored" data-anchor-id="section-2.1-from-binary-to-strings">Section 2.1: From Binary to Strings</h3>
<p>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 <em>slower</em> than StringArray. Why? TLDR: Although reading StringViewArray copied less data, the initial implementation also spent much more time validating <a href="https://en.wikipedia.org/wiki/UTF-8#:~:text=UTF%2D8%20is%20a%20variable,Unicode%20Standard">UTF-8</a> (as shown in Figure 5).</p>
<p>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 <a href="https://doc.rust-lang.org/std/str/fn.from_utf8.html">optimized routines</a> for doing so.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/string-view-datafusion/utf8-perf.png" class="img-fluid figure-img" style="width:70.0%"></p>
<figcaption>Figure 5: Time to load strings from Parquet. The UTF-8 validation advantage initially eliminates the advantage of reduced copying for StringViewArray.</figcaption>
</figure>
</div>
<p>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<sup>3</sup>, all strings in the array must be UTF-8. The Rust parquet reader makes a single function call to validate the entire buffer.</p>
<p>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).</p>
<p>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.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/string-view-datafusion/simd-utf8.png" class="img-fluid figure-img" style="width:80.0%"></p>
<figcaption>Figure 6: UTF-8 validation throughput vs string length—StringArray’s contiguous buffer can be validated much faster than StringViewArray’s buffer.</figcaption>
</figure>
</div>
<p>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, <a href="https://www.vldb.org/pvldb/vol17/p148-zeng.pdf">99% of strings are shorter than 128 bytes</a>, meaning the encoded length values are smaller than 128, <strong>in which case the length itself is also valid UTF-8</strong> (in fact, it is <a href="https://en.wikipedia.org/wiki/ASCII">ASCII</a>).</p>
<p>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 <em>value</em> 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.</p>
<p>The <a href="https://github.com/apache/arrow-rs/pull/6009/files">actual implementation</a> is only nine lines of Rust (with 30 lines of comments). You can find more details in the related arrow-rs issue: <a href="https://github.com/apache/arrow-rs/issues/5995">https://github.com/apache/arrow-rs/issues/5995</a>. As expected, with this optimization, loading StringViewArray is almost 2x faster than loading StringArray.</p>
</section>
<section id="section-2.2-be-careful-about-implicit-copies" class="level3">
<h3 class="anchored" data-anchor-id="section-2.2-be-careful-about-implicit-copies">Section 2.2: Be Careful About Implicit Copies</h3>
<p>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 <a href="https://github.com/apache/arrow-rs/issues/6033">this issue</a>.</p>
<p>The copies we eventually identified come from the following innocent-looking line of Rust code, where <code>self.buf</code> is a <a href="https://en.wikipedia.org/wiki/Reference_counting">reference counted</a> pointer that should transform without copying into a buffer for use in StringViewArray.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb3-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> block_id <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> output<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>append_block(<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">self</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>buf<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>clone()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>into())<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span></code></pre></div></div>
<p>However, Rust-type coercion rules favored a blanket implementation that <em>did</em> copy data. This implementation is shown in the following code block where the <code>impl&lt;T: AsRef&lt;[u8]&gt;&gt;</code> will accept any type that implements <code>AsRef&lt;[u8]&gt;</code> and copies the data to create a new buffer. To avoid copying, users need to explicitly call <code>from_vec</code>, which consumes the <code>Vec</code> and transforms it into a buffer.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb4-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">impl</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>T<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">AsRef</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>[<span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">u8</span>]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">From</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>T<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> Buffer <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb4-2">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> from(p<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> T) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Self</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb4-3">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">// copies data here</span></span>
<span id="cb4-4">     <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">...</span></span>
<span id="cb4-5">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb4-6"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb4-7"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">impl</span> Buffer <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span> </span>
<span id="cb4-8">  <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">pub</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> from_vec<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>T<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>(data<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Vec</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>T<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">Self</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb4-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">// zero-copy transformation</span></span>
<span id="cb4-10"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">...</span></span>
<span id="cb4-11">  <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb4-12"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
<p>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 <a href="https://github.com/apache/arrow-rs/pull/6043">removed</a> the implicit API from arrow-rs in favor of an explicit API. Using this approach, we found and fixed several <a href="https://github.com/apache/arrow-rs/pull/6039">other unintentional copies</a> in the code base—hopefully, the change will help other <a href="https://github.com/spiraldb/vortex/pull/504">downstream users</a> avoid unnecessary copies.</p>
</section>
<section id="section-2.3-help-the-compiler-by-giving-it-more-information" class="level3">
<h3 class="anchored" data-anchor-id="section-2.3-help-the-compiler-by-giving-it-more-information">Section 2.3: Help the Compiler by Giving it More Information</h3>
<p>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 <code>view</code> construction, we found, counterintuitively, that constructing <strong>long</strong> strings was 10x faster than constructing <strong>short</strong> strings, which made short strings slower on StringViewArray than on StringArray!</p>
<p>As described in Section 1, StringViewArray treats long and short strings differently. Short strings (&lt;12 bytes) directly inline to the <code>view</code> struct, while long strings only inline the first 4 bytes. The code to construct a <code>view</code> looks something like this:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb5-1"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> len <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">12</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb5-2">   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">// Construct 16 byte view for short string</span></span>
<span id="cb5-3">   <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">mut</span> view_buffer <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">16</span>]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb5-4">   view_buffer[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">..</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>copy_from_slice(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>len<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>to_le_bytes())<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb5-5">   view_buffer[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">..</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> data<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>len()]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>copy_from_slice(data)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb5-6">   <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">...</span></span>
<span id="cb5-7"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>      </span>
<span id="cb5-8">   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">// Construct 16 byte view for long string</span></span>
<span id="cb5-9">   ByteView <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb5-10">       length<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> len<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb5-11">       prefix<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">u32</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">::</span>from_le_bytes(data[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">..</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>try_into()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>unwrap())<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb5-12">       buffer_index<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> block_id<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb5-13">       offset<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb5-14">   <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb5-15"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
<p>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?</p>
<p>Looking at the assembly code using <a href="https://godbolt.org/">godbolt</a>, we (with help from <a href="https://github.com/aoli-al">Ao Li</a>) found the compiler used CPU <strong>load instructions</strong> to copy the fixed-sized 4 bytes to the <code>view</code> for long strings, but it calls a function, <a href="https://doc.rust-lang.org/std/ptr/fn.copy_nonoverlapping.html"><code>ptr::copy_non_overlapping</code></a>, to copy the inlined bytes to the <code>view</code> 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 <code>ptr::copy_non_coverlapping</code>. Making a function call is significant unnecessary overhead compared to a CPU copy instruction.</p>
<p>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 <a href="https://godbolt.org/z/685YPsd5G">checking the assembly code</a>, we confirmed there are no calls to <code>ptr::copy_non_overlapping</code>, and only native CPU instructions are used. For more details, see <a href="https://github.com/apache/arrow-rs/issues/6034">the ticket</a>.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode rust code-with-copy"><code class="sourceCode rust"><span id="cb6-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> make_inlined_view<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">const</span> LEN<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">usize</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>(data<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>[<span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">u8</span>]) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">u128</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb6-2">     <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">mut</span> view_buffer <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">16</span>]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb6-3">     view_buffer[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">..</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>copy_from_slice(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>(LEN <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">as</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">u32</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>to_le_bytes())<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb6-4">     view_buffer[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">..</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> LEN]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>copy_from_slice(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>data[<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">..</span>LEN])<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb6-5">     <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">u128</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">::</span>from_le_bytes(view_buffer)</span>
<span id="cb6-6"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb6-7"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">pub</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">fn</span> make_view(data<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span>[<span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">u8</span>]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> block_id<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">u32</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> offset<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">u32</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">u128</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb6-8">     <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">let</span> len <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> data<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>len()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb6-9">     <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">// generate special code for each of the 13 possible lengths</span></span>
<span id="cb6-10">     <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">match</span> len <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb6-11">         <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">make_inlined_view::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>(data)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-12">         <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">make_inlined_view::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>(data)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-13">         <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">make_inlined_view::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>(data)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-14">         <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">make_inlined_view::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>(data)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-15">         <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">make_inlined_view::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>(data)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-16">         <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">make_inlined_view::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>(data)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-17">         <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">make_inlined_view::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>(data)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-18">         <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">7</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">make_inlined_view::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">7</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>(data)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-19">         <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">8</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">make_inlined_view::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">8</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>(data)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-20">         <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">9</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">make_inlined_view::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">9</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>(data)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-21">         <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">make_inlined_view::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>(data)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-22">         <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">11</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">make_inlined_view::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">11</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>(data)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-23">         <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">12</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">make_inlined_view::</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">12</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>(data)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-24">         _ <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=&gt;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb6-25">           <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">// handle long string</span></span>
<span id="cb6-26"><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}}}</span></span></code></pre></div></div>
</section>
<section id="section-2.4-end-to-end-query-performance" class="level3">
<h3 class="anchored" data-anchor-id="section-2.4-end-to-end-query-performance">Section 2.4: End-to-End Query Performance</h3>
<p>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 <code>"google"</code>:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode sql code-with-copy"><code class="sourceCode sql"><span id="cb7-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">SELECT</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">COUNT</span>(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span>) <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">FROM</span> hits <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">WHERE</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">"URL"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">LIKE</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'%google%'</span>;</span></code></pre></div></div>
<p>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:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode txt code-with-copy"><code class="sourceCode default"><span id="cb8-1">Projection: COUNT(*) [COUNT(*):Int64;N]</span>
<span id="cb8-2">  Aggregate: groupBy=[[]], aggr=[[COUNT(*)]] [COUNT(*):Int64;N]</span>
<span id="cb8-3">    Filter: hits.URL LIKE Utf8("%google%")</span>
<span id="cb8-4">      TableScan: hits </span></code></pre></div></div>
<p>We ran the benchmark in the DataFusion repo like this:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb9-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">cargo</span> run <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--profile</span> release-nonlto <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--bin</span> dfbench <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> clickbench <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--queries-path</span> benchmarks/queries/clickbench/queries.sql <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--iterations</span> 3 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--query</span> 20 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--path</span> benchmarks/data/hits.parquet <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--string-view</span></span></code></pre></div></div>
<p>With StringViewArray we saw a 24% end-to-end performance improvement, as shown in Figure 7. With the <code>--string-view</code> argument, the end-to-end query time is <code>944.3 ms, 869.6 ms, 861.9 ms</code> (three iterations). Without <code>--string-view</code>, the end-to-end query time is <code>1186.1 ms, 1126.1 ms, 1138.3 ms</code>.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/string-view-datafusion/quer_time.png" class="img-fluid figure-img" style="width:70.0%"></p>
<figcaption>Figure 7: StringView reduces end-to-end query time by 24% on ClickBench Q20.</figcaption>
</figure>
</div>
<p>We also double-checked with detailed profiling and verified that the time reduction is indeed due to faster Parquet loading.</p>
</section>
</section>
<section id="section-3-faster-string-operations" class="level2">
<h2 class="anchored" data-anchor-id="section-3-faster-string-operations">Section 3: Faster String Operations</h2>
<p>We have 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.</p>
<section id="section-3.1-faster-comparison" class="level3">
<h3 class="anchored" data-anchor-id="section-3.1-faster-comparison">Section 3.1: Faster comparison</h3>
<p>String comparison is ubiquitous; it is the core of <a href="https://docs.rs/arrow/latest/arrow/compute/kernels/cmp/index.html"><code>cmp</code></a>, <a href="https://docs.rs/arrow/latest/arrow/compute/fn.min.html"><code>min</code></a><code>/max</code>, and <a href="https://docs.rs/arrow/latest/arrow/compute/kernels/comparison/fn.like.html"><code>like</code></a><code>/ilike</code> 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.</p>
<p>For example, to compare the strings <code>InfluxDB</code> with <code>Apache DataFusion</code>, we only need to look at the first byte to determine the string ordering or equality. In this case, since <code>A</code> is earlier in the alphabet than <code>I,</code> <code>Apache DataFusion</code> 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 <code>cmp</code> 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.</p>
<p>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 <a href="https://www.vldb.org/pvldb/vol17/p148-zeng.pdf">60% of real-world strings which are shorter than 12 bytes</a>, as they are stored completely in the prefix.</p>
<p>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 <a href="https://github.com/apache/arrow-rs/issues/5374">a wide range</a> 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 <a href="https://github.com/apache/datafusion/issues/11752">https://github.com/apache/datafusion/issues/11752</a> if you want to help out).</p>
</section>
<section id="section-3.2-faster-take-and-filter" class="level3">
<h3 class="anchored" data-anchor-id="section-3.2-faster-take-and-filter">Section 3.2: Faster <code>take</code> and <code>filter</code></h3>
<p>After a filter operation such as <code>WHERE url &lt;&gt; ‘’</code> to avoid processing empty urls, DataFusion will often <em>coalesce</em> results to form a new array with only the passing elements. This coalescing ensures the batches are sufficiently sized to benefit from <a href="https://www.vldb.org/pvldb/vol11/p2209-kersten.pdf">vectorized processing</a> in subsequent steps.</p>
<p>The coalescing operation is implemented using the <a href="https://docs.rs/arrow/latest/arrow/compute/fn.take.html">take</a> and <a href="https://arrow.apache.org/rust/arrow/compute/kernels/filter/fn.filter.html">filter</a> 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.</p>
<p>However, <code>take</code> and <code>filter</code> for StringViewArray can avoid the copy by reusing buffers from the old array. The kernels only need to create a new list of <code>view</code>s that point at the same strings within the old buffers. Figure 8 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.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/string-view-datafusion/take.jpg" class="img-fluid figure-img" style="width:70.0%"></p>
<figcaption>Figure 8: Zero-copy take/filter for StringViewArray</figcaption>
</figure>
</div>
</section>
<section id="section-3.3-when-to-gc" class="level3">
<h3 class="anchored" data-anchor-id="section-3.3-when-to-gc">Section 3.3: When to GC?</h3>
<p>Zero-copy <code>take/filter</code> 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 <code>view</code>. This leads to excessive memory usage, especially in a <a href="https://github.com/apache/datafusion/issues/11628">filter-then-coalesce scenario</a>. 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.</p>
<p>To release unused memory, we implemented a <a href="https://docs.rs/arrow/latest/arrow/array/struct.GenericByteViewArray.html#method.gc">garbage collection (GC)</a> 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 <a href="https://pola.rs/posts/polars-string-type/">Polars blog</a> on StringView also refers to the challenge presented by garbage collection timing.</p>
<p><code>arrow-rs</code> 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 <a href="https://docs.rs/datafusion/latest/datafusion/physical_plan/coalesce_batches/struct.CoalesceBatchesExec.html"><code>CoalseceBatchesExec</code></a> 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, <a href="https://github.com/apache/datafusion/pull/11587">implemented the GC procedure</a> inside <code>CoalseceBatchesExec[^5],</code>with a heuristic that estimates when the buffers are too sparse.</p>
</section>
<section id="section" class="level3">
<h3 class="anchored" data-anchor-id="section"></h3>
</section>
<section id="section-3.4-the-art-of-function-inlining-not-too-much-not-too-little" class="level3">
<h3 class="anchored" data-anchor-id="section-3.4-the-art-of-function-inlining-not-too-much-not-too-little">Section 3.4: The art of function inlining: not too much, not too little</h3>
<p>Like string inlining, <em>function</em> 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 <a href="https://doc.rust-lang.org/reference/attributes/codegen.html#the-inline-attribute"><code>#[inline(always)]</code> directive</a>. In performance-critical code, inlined code allows us to organize large functions into smaller ones without paying the runtime cost of function invocation.</p>
<p>However, function inlining is <strong><em>not</em></strong> always better, as it leads to larger function bodies that are harder for LLVM to optimize (for example, suboptimal <a href="https://en.wikipedia.org/wiki/Register_allocation">register spilling</a>) and risk overflowing the CPU’s instruction cache. We observed several performance regressions where function inlining caused <em>slower</em> 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: <a href="https://github.com/apache/arrow-rs/pull/5900">https://github.com/apache/arrow-rs/pull/5900</a>.</p>
</section>
<section id="section-3.5-buffer-size-tuning" class="level3">
<h3 class="anchored" data-anchor-id="section-3.5-buffer-size-tuning">Section 3.5: Buffer size tuning</h3>
<p>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, <a href="https://docs.rs/arrow/latest/arrow/array/trait.Array.html#tymethod.get_array_memory_size"><code>get_array_memory_size</code></a><code>()</code> 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 <a href="https://docs.rs/arrow/latest/arrow/compute/fn.concat_batches.html"><code>concat_batches</code></a> lead to arrays with millions of buffers, which was prohibitively expensive.</p>
<p>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.</p>
<p>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).</p>
<p>To assist situations where the semantics are unknown, we also <a href="https://github.com/apache/arrow-rs/pull/6136">implemented</a> 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: <a href="https://github.com/apache/arrow-rs/issues/6094">https://github.com/apache/arrow-rs/issues/6094</a>.</p>
</section>
<section id="section-3.6-end-to-end-query-performance" class="level3">
<h3 class="anchored" data-anchor-id="section-3.6-end-to-end-query-performance">Section 3.6: End-to-end query performance</h3>
<p>We have made significant progress in optimizing StringViewArray filtering operations. Now, let’s test it in the real world to see how it works!</p>
<p>Let’s consider ClickBench query 22, which selects multiple string fields (<code>URL</code>, <code>Title</code>, and <code>SearchPhase</code>) and applies several filters.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode sql code-with-copy"><code class="sourceCode sql"><span id="cb10-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">SELECT</span> </span>
<span id="cb10-2">  <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">"SearchPhrase"</span>, </span>
<span id="cb10-3">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">MIN</span>(<span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">"URL"</span>), <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">MIN</span>(<span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">"Title"</span>), <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">COUNT</span>(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span>) <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">AS</span> c, <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">COUNT</span>(<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">DISTINCT</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">"UserID"</span>) </span>
<span id="cb10-4"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">FROM</span> hits </span>
<span id="cb10-5"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">WHERE</span> </span>
<span id="cb10-6">  <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">"Title"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">LIKE</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'%Google%'</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">AND</span> </span>
<span id="cb10-7">  <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">"URL"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">NOT</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">LIKE</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'%.google.%'</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">AND</span> </span>
<span id="cb10-8">  <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">"SearchPhrase"</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;&gt;</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">''</span> </span>
<span id="cb10-9"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">GROUP</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">BY</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">"SearchPhrase"</span> </span>
<span id="cb10-10"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">ORDER</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">BY</span> c <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">DESC</span> </span>
<span id="cb10-11"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">LIMIT</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span>;</span></code></pre></div></div>
<p>We ran the benchmark using the following command in the DataFusion repo. Again, the <code>--string-view</code> option means we use StringViewArray instead of StringArray.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb11-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">cargo</span> run <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--profile</span> release-nonlto <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--bin</span> dfbench <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> clickbench <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--queries-path</span> benchmarks/queries/clickbench/queries.sql <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--iterations</span> 3 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--query</span> 22 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--path</span> benchmarks/data/hits.parquet <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--string-view</span></span></code></pre></div></div>
<p>To eliminate the impact of the faster Parquet reading using StringViewArray (see the first part of this blog), Figure 9 plots only the time spent in <code>FilterExec</code>. 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.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://blog.xiangpeng.systems/posts/string-view-datafusion/filter-time.png" class="img-fluid figure-img" style="width:70.0%"></p>
<figcaption>Figure 9: StringViewArray reduces the filter time by 32% on ClickBench query 22.</figcaption>
</figure>
</div>
</section>
</section>
<section id="section-4-faster-string-aggregation" class="level2">
<h2 class="anchored" data-anchor-id="section-4-faster-string-aggregation">Section 4: Faster String Aggregation</h2>
<p>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.</p>
<p>As described in part one of this blog, if two strings have identical values, StringViewArray can use two different <code>view</code>s pointing at the same buffer range, thus avoiding repeating the string bytes in the buffer. This makes StringViewArray similar to an Arrow <a href="https://docs.rs/arrow/latest/arrow/array/struct.DictionaryArray.html">DictionaryArray</a> that stores Strings—both array types work well for strings with only a few distinct values.</p>
<p>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 <a href="https://docs.rs/arrow/latest/arrow/array/builder/struct.GenericByteViewBuilder.html#method.with_deduplicate_strings">opt-in string deduplication mode</a> 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.</p>
<p>Once again, we leverage DataFusion query semantics to identify StringViewArray with duplicate values, such as aggregation queries with multiple group keys. For example, some <a href="https://github.com/apache/datafusion/blob/main/benchmarks/queries/clickbench/queries.sql">ClickBench queries</a> group by two columns:</p>
<ul>
<li><code>UserID</code> (an integer with close to 1 M distinct values)<br>
</li>
<li><code>MobilePhoneModel</code> (a string with less than a hundred distinct values)</li>
</ul>
<p>In this case, the output row count is <code>count(distinct UserID) * count(distinct MobilePhoneModel)</code>, which is 100M. Each string value of <code>MobilePhoneModel</code> is repeated 1M times. With StringViewArray, we can save space by pointing the repeating values to the same underlying buffer.</p>
<p>Faster string aggregation with StringView is part of a larger project to <a href="https://github.com/apache/datafusion/issues/7000">improve DataFusion aggregation performance</a>. We have a <a href="https://github.com/apache/datafusion/pull/11794">proof of concept implementation</a> with StringView that can improve the multi-column string aggregation by 20%. We would love your help to get it production ready!</p>
</section>
<section id="section-5-stringview-pitfalls" class="level2">
<h2 class="anchored" data-anchor-id="section-5-stringview-pitfalls">Section 5: StringView Pitfalls</h2>
<p>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.</p>
<p>However, there are several cases where StringViewArray is slower than StringArray. For completeness, we have listed those instances here:</p>
<ol type="1">
<li><strong>Tiny strings (when strings are shorter than 8 bytes)</strong>: every element of the StringViewArray consumes at least 16 bytes of memory—the size of the <code>view</code> 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.<br>
</li>
<li><strong>Many repeated short strings</strong>: 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.<br>
</li>
<li><strong>Filtering:</strong> 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.</li>
</ol>
</section>
<section id="section-6-conclusion-and-takeaways" class="level2">
<h2 class="anchored" data-anchor-id="section-6-conclusion-and-takeaways">Section 6: Conclusion and Takeaways</h2>
<p>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.</p>
<p>Given that DataFusion already <a href="https://benchmark.clickhouse.com/#eyJzeXN0ZW0iOnsiQWxsb3lEQiI6ZmFsc2UsIkF0aGVuYSAocGFydGl0aW9uZWQpIjpmYWxzZSwiQXRoZW5hIChzaW5nbGUpIjpmYWxzZSwiQXVyb3JhIGZvciBNeVNRTCI6ZmFsc2UsIkF1cm9yYSBmb3IgUG9zdGdyZVNRTCI6ZmFsc2UsIkJ5Q29uaXR5IjpmYWxzZSwiQnl0ZUhvdXNlIjpmYWxzZSwiY2hEQiAoUGFycXVldCwgcGFydGl0aW9uZWQpIjpmYWxzZSwiY2hEQiI6ZmFsc2UsIkNpdHVzIjpmYWxzZSwiQ2xpY2tIb3VzZSBDbG91ZCAoYXdzKSI6ZmFsc2UsIkNsaWNrSG91c2UgQ2xvdWQgKGF3cykgUGFyYWxsZWwgUmVwbGljYXMgT04iOmZhbHNlLCJDbGlja0hvdXNlIENsb3VkIChBenVyZSkiOmZhbHNlLCJDbGlja0hvdXNlIENsb3VkIChBenVyZSkgUGFyYWxsZWwgUmVwbGljYSBPTiI6ZmFsc2UsIkNsaWNrSG91c2UgQ2xvdWQgKEF6dXJlKSBQYXJhbGxlbCBSZXBsaWNhcyBPTiI6ZmFsc2UsIkNsaWNrSG91c2UgQ2xvdWQgKGdjcCkiOmZhbHNlLCJDbGlja0hvdXNlIENsb3VkIChnY3ApIFBhcmFsbGVsIFJlcGxpY2FzIE9OIjpmYWxzZSwiQ2xpY2tIb3VzZSAoZGF0YSBsYWtlLCBwYXJ0aXRpb25lZCkiOmZhbHNlLCJDbGlja0hvdXNlIChkYXRhIGxha2UsIHNpbmdsZSkiOmZhbHNlLCJDbGlja0hvdXNlIChQYXJxdWV0LCBwYXJ0aXRpb25lZCkiOmZhbHNlLCJDbGlja0hvdXNlIChQYXJxdWV0LCBzaW5nbGUpIjpmYWxzZSwiQ2xpY2tIb3VzZSAod2ViKSI6ZmFsc2UsIkNsaWNrSG91c2UiOmZhbHNlLCJDbGlja0hvdXNlICh0dW5lZCkiOmZhbHNlLCJDbGlja0hvdXNlICh0dW5lZCwgbWVtb3J5KSI6ZmFsc2UsIkNsb3VkYmVycnkiOmZhbHNlLCJDcmF0ZURCIjpmYWxzZSwiQ3J1bmNoeSBCcmlkZ2UgZm9yIEFuYWx5dGljcyAoUGFycXVldCkiOmZhbHNlLCJEYXRhYmVuZCI6ZmFsc2UsIkRhdGFGdXNpb24gKFBhcnF1ZXQsIHBhcnRpdGlvbmVkKSI6dHJ1ZSwiRGF0YUZ1c2lvbiAoUGFycXVldCwgc2luZ2xlKSI6ZmFsc2UsIkFwYWNoZSBEb3JpcyI6ZmFsc2UsIkRydWlkIjpmYWxzZSwiRHVja0RCIChQYXJxdWV0LCBwYXJ0aXRpb25lZCkiOnRydWUsIkR1Y2tEQiI6ZmFsc2UsIkVsYXN0aWNzZWFyY2giOmZhbHNlLCJFbGFzdGljc2VhcmNoICh0dW5lZCkiOmZhbHNlLCJHbGFyZURCIjpmYWxzZSwiR3JlZW5wbHVtIjpmYWxzZSwiSGVhdnlBSSI6ZmFsc2UsIkh5ZHJhIjpmYWxzZSwiSW5mb2JyaWdodCI6ZmFsc2UsIktpbmV0aWNhIjpmYWxzZSwiTWFyaWFEQiBDb2x1bW5TdG9yZSI6ZmFsc2UsIk1hcmlhREIiOmZhbHNlLCJNb25ldERCIjpmYWxzZSwiTW9uZ29EQiI6ZmFsc2UsIk1vdGhlcmR1Y2siOmZhbHNlLCJNeVNRTCAoTXlJU0FNKSI6ZmFsc2UsIk15U1FMIjpmYWxzZSwiT3hsYSI6ZmFsc2UsIlBhcmFkZURCIChQYXJxdWV0LCBwYXJ0aXRpb25lZCkiOmZhbHNlLCJQYXJhZGVEQiAoUGFycXVldCwgc2luZ2xlKSI6ZmFsc2UsIlBpbm90IjpmYWxzZSwiUG9zdGdyZVNRTCAodHVuZWQpIjpmYWxzZSwiUG9zdGdyZVNRTCI6ZmFsc2UsIlF1ZXN0REIgKHBhcnRpdGlvbmVkKSI6ZmFsc2UsIlF1ZXN0REIiOmZhbHNlLCJSZWRzaGlmdCI6ZmFsc2UsIlNlbGVjdERCIjpmYWxzZSwiU2luZ2xlU3RvcmUiOmZhbHNlLCJTbm93Zmxha2UiOmZhbHNlLCJTUUxpdGUiOmZhbHNlLCJTdGFyUm9ja3MiOmZhbHNlLCJUYWJsZXNwYWNlIjpmYWxzZSwiVGVtYm8gT0xBUCAoY29sdW1uYXIpIjpmYWxzZSwiVGltZXNjYWxlREIgKGNvbXByZXNzaW9uKSI6ZmFsc2UsIlRpbWVzY2FsZURCIjpmYWxzZSwiVW1icmEiOmZhbHNlfSwidHlwZSI6eyJDIjp0cnVlLCJjb2x1bW4tb3JpZW50ZWQiOnRydWUsIlBvc3RncmVTUUwgY29tcGF0aWJsZSI6dHJ1ZSwibWFuYWdlZCI6dHJ1ZSwiZ2NwIjp0cnVlLCJzdGF0ZWxlc3MiOnRydWUsIkphdmEiOnRydWUsIkMrKyI6dHJ1ZSwiTXlTUUwgY29tcGF0aWJsZSI6dHJ1ZSwicm93LW9yaWVudGVkIjp0cnVlLCJDbGlja0hvdXNlIGRlcml2YXRpdmUiOnRydWUsImVtYmVkZGVkIjp0cnVlLCJzZXJ2ZXJsZXNzIjp0cnVlLCJhd3MiOnRydWUsInBhcmFsbGVsIHJlcGxpY2FzIjp0cnVlLCJBenVyZSI6dHJ1ZSwiYW5hbHl0aWNhbCI6dHJ1ZSwiUnVzdCI6dHJ1ZSwic2VhcmNoIjp0cnVlLCJkb2N1bWVudCI6dHJ1ZSwic29tZXdoYXQgUG9zdGdyZVNRTCBjb21wYXRpYmxlIjp0cnVlLCJ0aW1lLXNlcmllcyI6dHJ1ZX0sIm1hY2hpbmUiOnsiMTYgdkNQVSAxMjhHQiI6dHJ1ZSwiOCB2Q1BVIDY0R0IiOnRydWUsInNlcnZlcmxlc3MiOnRydWUsIjE2YWN1Ijp0cnVlLCJjNmEuNHhsYXJnZSwgNTAwZ2IgZ3AyIjp0cnVlLCJMIjp0cnVlLCJNIjp0cnVlLCJTIjp0cnVlLCJYUyI6dHJ1ZSwiYzZhLm1ldGFsLCA1MDBnYiBncDIiOnRydWUsIjE5MkdCIjp0cnVlLCIyNEdCIjp0cnVlLCIzNjBHQiI6dHJ1ZSwiNDhHQiI6dHJ1ZSwiNzIwR0IiOnRydWUsIjk2R0IiOnRydWUsIjE0MzBHQiI6dHJ1ZSwiZGV2Ijp0cnVlLCI3MDhHQiI6dHJ1ZSwiYzVuLjR4bGFyZ2UsIDUwMGdiIGdwMiI6dHJ1ZSwiQW5hbHl0aWNzLTI1NkdCICg2NCB2Q29yZXMsIDI1NiBHQikiOnRydWUsImM1LjR4bGFyZ2UsIDUwMGdiIGdwMiI6dHJ1ZSwiYzZhLjR4bGFyZ2UsIDE1MDBnYiBncDIiOnRydWUsImNsb3VkIjp0cnVlLCJkYzIuOHhsYXJnZSI6dHJ1ZSwicmEzLjE2eGxhcmdlIjp0cnVlLCJyYTMuNHhsYXJnZSI6dHJ1ZSwicmEzLnhscGx1cyI6dHJ1ZSwiUzIiOnRydWUsIlMyNCI6dHJ1ZSwiMlhMIjp0cnVlLCIzWEwiOnRydWUsIjRYTCI6dHJ1ZSwiWEwiOnRydWUsIkwxIC0gMTZDUFUgMzJHQiI6dHJ1ZSwiYzZhLjR4bGFyZ2UsIDUwMGdiIGdwMyI6dHJ1ZX0sImNsdXN0ZXJfc2l6ZSI6eyIxIjp0cnVlLCIyIjp0cnVlLCI0Ijp0cnVlLCI4Ijp0cnVlLCIxNiI6dHJ1ZSwiMzIiOnRydWUsIjY0Ijp0cnVlLCIxMjgiOnRydWUsInNlcnZlcmxlc3MiOnRydWUsImRlZGljYXRlZCI6dHJ1ZX0sIm1ldHJpYyI6ImhvdCIsInF1ZXJpZXMiOlt0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLHRydWUsdHJ1ZSx0cnVlLH">performs very well on ClickBench</a>, 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.</p>
<p>StringView is a big project that has received tremendous community support. Specifically, we would like to thank <a href="https://github.com/tustvold"><span class="citation" data-cites="tustvold">@tustvold</span></a>, <a href="https://github.com/ariesdevil"><span class="citation" data-cites="ariesdevil">@ariesdevil</span></a>, <a href="https://github.com/RinChanNOWWW"><span class="citation" data-cites="RinChanNOWWW">@RinChanNOWWW</span></a>, <a href="https://github.com/ClSlaid"><span class="citation" data-cites="ClSlaid">@ClSlaid</span></a>, <a href="https://github.com/2010YOUY01"><span class="citation" data-cites="2010YOUY01">@2010YOUY01</span></a>, <a href="https://github.com/chloro-pn"><span class="citation" data-cites="chloro-pn">@chloro-pn</span></a>, <a href="https://github.com/a10y"><span class="citation" data-cites="a10y">@a10y</span></a>, <a href="https://github.com/Kev1n8"><span class="citation" data-cites="Kev1n8">@Kev1n8</span></a>, <a href="https://github.com/Weijun-H"><span class="citation" data-cites="Weijun-H">@Weijun-H</span></a>, <a href="https://github.com/PsiACE"><span class="citation" data-cites="PsiACE">@PsiACE</span></a>, <a href="https://github.com/tshauck"><span class="citation" data-cites="tshauck">@tshauck</span></a>, and <a href="https://github.com/xinlifoobar"><span class="citation" data-cites="xinlifoobar">@xinlifoobar</span></a> for their valuable contributions!</p>
<p>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.</p>


</section>


<div id="quarto-appendix" class="default"><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>Benchmarked with AMD Ryzen 7600x (12 core, 24 threads, 32 MiB L3), WD Black SN770 NVMe SSD (5150MB/4950MB seq RW bandwidth)↩︎</p></li>
<li id="fn2"><p>There is also a corresponding <em>BinaryViewArray</em> which is similar except that the data is not constrained to be UTF-8 encoded strings.↩︎</p></li>
<li id="fn3"><p>We also make sure that offsets do not break a UTF-8 code point, which is <a href="https://github.com/apache/arrow-rs/blob/master/parquet/src/arrow/buffer/offset_buffer.rs#L62-L71">cheaply validated</a>.↩︎</p></li>
</ol>
</section></div> ]]></description>
  <guid>https://blog.xiangpeng.systems/posts/string-view-datafusion/</guid>
  <pubDate>Fri, 13 Sep 2024 00:00:00 GMT</pubDate>
</item>
<item>
  <title>My research statement</title>
  <link>https://blog.xiangpeng.systems/posts/research-statement/</link>
  <description><![CDATA[ 





<section id="north-star" class="level3">
<h3 class="anchored" data-anchor-id="north-star">North star</h3>
<p><img src="https://blog.xiangpeng.systems/posts/research-statement/20240808145348.png" class="img-fluid"></p>
<p>Research should connect to real people.</p>
</section>
<section id="impact-definition" class="level3">
<h3 class="anchored" data-anchor-id="impact-definition">Impact definition</h3>
<p>Research impact is defined by the number of connections between the paper and the users.</p>
<p>Note that the research paper itself does not produce an impact, as nobody’s life is changed because of reading a paper—we are not philosophers. This means that the research impact is always indirect; we have to apply the research to a system with real users.</p>
<p>Due to the indirect nature of the research’s impact, some effort must be made to connect the research to users.</p>
<p>My take: Researchers should take a holistic approach, seamlessly threading their work from the research paper to individual users.</p>
</section>
<section id="no-goals" class="level3">
<h3 class="anchored" data-anchor-id="no-goals">No-goals</h3>
<p>No goals help define goals. I’ve made all three types of mistakes, and this is my self-reflection to avoid repeating them.</p>
<p><img src="https://blog.xiangpeng.systems/posts/research-statement/20240808113448.png" class="img-fluid"> We are tempted to work on projects proven to be publishable, e.g., improving an existing published method. However, it is not uncommon to see chains of research papers working on a topic irrelevant to the rest of the world. In other words, the impact of a research paper is not defined by how many papers cite it.</p>
<p><img src="https://blog.xiangpeng.systems/posts/research-statement/20240808145432.png" class="img-fluid"> Researchers have the privilege to go wild and think big. But this privilege can be misused. Often, it is used to fool ourselves. Some research (e.g., physics and math) are/can be far from users, but not computer science – one of <em>applied science</em>. As applied scientists, we must ensure our “science” applies to the real world instead of just intellectual exercise.<br>
Some research (e.g., theory) in applied science still enjoys greater freedom than others. However, my research topics are outside them.</p>
<p><img src="https://blog.xiangpeng.systems/posts/research-statement/20240808153415.png" class="img-fluid"> This is the most common research mistake. We often believe that researchers should focus on making good ideas, and someone else will go off to make it happen. This is not true. There are way too many good ideas, yet few are impactful.</p>
<p>Most ideas are just combinations of existing techniques, and it is hard to believe anything fundamental will be discovered in system research.</p>
<p>Most importantly, a good idea evolves with the system it builds upon. It is a feedback loop that tries, fails, and repeats. And the great idea that comes out of the system always differs from its initial version. It is never the case that the research flow is a single-directional graph with no feedback from downstream users.</p>
</section>
<section id="todays-problem-vs-tomorrows-problem" class="level3">
<h3 class="anchored" data-anchor-id="todays-problem-vs-tomorrows-problem">Today’s problem vs tomorrow’s problem</h3>
<p>Both types of problems are important and meaningful. I don’t work on tomorrow’s problems (but I agree that someone must work on future problems).</p>
<p>Why not work on future problems:</p>
<ul>
<li><p>We have enough of today’s problems to work on.</p></li>
<li><p>It’s easier to fool ourselves while working on future problems.</p></li>
</ul>
<p>Don’t fool ourselves:</p>
<ul>
<li><p>We like tomorrow’s problem because it’s easier to claim novelty – future problems naturally come with novelty.</p></li>
<li><p>We like tomorrow’s problem because we don’t have to establish user connections. Instead, we use it as an excuse to avoid working on the most important and challenging parts. We are free to propose anything we want, work around any challenges, and eventually disconnect from the rest of the world.</p></li>
<li><p>How do we avoid imaginary research? How can we ensure today’s solution is not funny when tomorrow’s problem arrives? Why work on it when future researchers will beat today’s solution with much more practical considerations?</p></li>
</ul>
</section>
<section id="engineering-problem-vs-research-problem" class="level3">
<h3 class="anchored" data-anchor-id="engineering-problem-vs-research-problem">Engineering problem vs research problem</h3>
<p><img src="https://blog.xiangpeng.systems/posts/research-statement/20240821153927.png" class="img-fluid"> My take on research vs engineering problems:</p>
<ul>
<li><p><em>All</em> engineering problems are meaningful as they come from users and practitioners—no one invented them out of thin air.</p></li>
<li><p><em>All</em> research problems are publishable by definition.</p></li>
<li><p><em>Most</em> engineering problems are also research problems, evidenced by the fact that they haven’t been solved — meaning they are challenging, resource-consuming, or require a deep understanding of practical nuances, which are the definitions of <em>research problems</em>.</p></li>
<li><p>Research problems, without being grounded by engineering problems, are unlikely to be meaningful.</p></li>
</ul>
</section>
<section id="why-this-statement" class="level3">
<h3 class="anchored" data-anchor-id="why-this-statement">Why this statement?</h3>
<p>I probably only need to publish one more paper to graduate, as I have already published two papers in VLDB/SIGMOD. Moreover, my chance to stay in academia is quite low, meaning that this one-more-paper is likely the last paper I drive to publish in my career.</p>
<p>I have lots of cool ideas, but I have to choose only one. This statement helps me to crystalize my thoughts when deciding what to work on. It would be great if I knew all of these early in my Ph.D., but it is still not too late to write them down.</p>
<p>I can get lost when struggling to publish a paper or being determined to secure a faculty job. Everyone starts with the intention of doing meaningful research, but practical considerations forced them to do otherwise. If I encounter those situations (again), this statement would remind me of my north star.</p>
<!-- 
### Practical considerations
I have declared to do and not to do. But it is easier to say than execute. 
Everyone wanted impactful research, yet few had the power to stay unchanged. 
Below is my attempt.



Some of these are more of "ultimate life goals" rather than "research goals." Those are things that I believe a reasonable person in this society should pursue. One should definitely optimize for global goals rather than local goals.

With that said, local-goals often conflict with global-goals. For example, if the local-goal is to "publish enough to get tenure", then that goal might conflict with the goal of "do good research", or "be a reasonable person".

This process is alienation. Where people in the small community are trained to work towards a goal that is not properly aligned with the social goal or their global goal. When this happens, we either leave the community, or do whatever possible to make sure the goals are aligned.

That's why we need to write down our thoughts and make sure we do not fool ourselves and have enough resource to fight against alienation.

The value of axioms is that it provides a north star to where we want to go, we might need to deviate the route a bit to get somewhere, but we want to make sure we are on the right path in the long run.


Axioms are only meaningful when goals are conflicting with each other.

Example 1:
We designed a fancy new data structure that performs super well in our benchmark. And of course we published a paper in top tier conferences. And we just tell others "here's the great idea" and of course they can go off and reimplement it and use it in production.




#### How about failed projects

### Not attacking junior researchers
everyone make mistakes. we improve ourselves by making mistakes. And the research communities train junior researchers by allowing them to make mistakes.
This is my research statement, and it should not be used to attack anyone else. By reading this statement you agree that our planet is large enough to allow people with different opinions live.
### Counter arguments
Research teams explores different path, and provide intelligence when we need to choose one.
In this case, researcher should still be connected to users on every path they explores. If a path is not connected to any user, why would it waste time exploring it? -->


</section>

 ]]></description>
  <guid>https://blog.xiangpeng.systems/posts/research-statement/</guid>
  <pubDate>Wed, 21 Aug 2024 00:00:00 GMT</pubDate>
</item>
<item>
  <title>InfluxData &gt; Google and Microsoft</title>
  <link>https://blog.xiangpeng.systems/posts/working-at-influxdata/</link>
  <description><![CDATA[ 





<p>I have interned at Google, Microsoft (Research), and InfluxData. The experience at InfluxData has been the most enjoyable, and this blog post explains why.</p>
<p>Smaller companies are more <strong>connected</strong> to the real business, more <strong>connected</strong> within the company, and more willing to <strong>connect</strong> you to the rest of the world.</p>
<section id="problem-definition" class="level2">
<h2 class="anchored" data-anchor-id="problem-definition">Problem definition</h2>
<p><strong>Non-problems</strong></p>
<p>I have worked on great projects: (1) auto-tuned BigTable, (2) designed a new B-Tree system, and (3) <a href="https://www.youtube.com/watch?v=RVLshX6fbds">implemented StringView</a> in DataFusion.</p>
<p>I have met great people: <a href="https://www.cs.cmu.edu/~yixinluo/">Yixin Luo</a>, <a href="https://www.microsoft.com/en-us/research/people/badrishc/">Badrish Chandramouli</a>, and <a href="http://andrew.nerdnetworks.org">Andrew Lamb</a>. They are professional and helpful, and I learned a lot from them.</p>
<p><strong>The problem</strong></p>
<p>The problem is <em>connectivity</em>: Is the project connected to the real product/customer? Am I connected to the company? Am I connected to the rest of the world?</p>
<p>Big tech has systematic cultural issues that prevent each individual from being connected, and there’s no easy way to fix it.</p>
</section>
<section id="connect-to-business" class="level2">
<h2 class="anchored" data-anchor-id="connect-to-business">Connect to business</h2>
<p>I have constantly been reminded to connect my intern project to the InfluxData business – how it improves InfluxDB3.0, what Influx queries it can accelerate, how it compares with the current approaches, etc.</p>
<p>Andrew expects my project to deploy in production rather than behind a feature gate that no one uses, and he worked very hard to make it happen. Andrew also reminded me to connect my presentation to InfluxDB3.0 and discuss how it benefits the product.</p>
<p>This is an important feeling of real – I’m not being paid to work on a toy exploration, not a project that requires five extra teams to reach customers. Instead, it is a project grounded by real systems and real needs, and it will be running as part of a real product.</p>
<p>Being real means my time and efforts are respected and valued. Most importantly, it means the investment is sustainable because some kind of return is expected.</p>
</section>
<section id="connect-to-peers" class="level2">
<h2 class="anchored" data-anchor-id="connect-to-peers">Connect to peers</h2>
<p>Despite being a remote-only company, InfluxData is highly connected: cross-team collaborations, company-wide acknowledgments for great work, and short turn-around for approval.</p>
<p>I only learned to appreciate the above after seeing teams fighting for the same project, achievements downplayed due to politics, and bureaucratic processes blocking individual growth.</p>
<p>Connecting to peers means working with real people, rather than a faceless bureaucratic system. You work towards a shared goal, not a surprise order from high above. When unexpected happens, you know who to talk to, and you can reason the consequences by common sense, not with a rule book or guessing what the boss wants.</p>
<p>Being treated as a respected individual, rather than some resource/tool/refillable, is sometimes more important than being paid well.</p>
</section>
<section id="connect-to-the-world" class="level2">
<h2 class="anchored" data-anchor-id="connect-to-the-world">Connect to the world</h2>
<p>Big techs have trust and ego issues. They develop/control their own programming languages, frameworks, platforms, etc. They do not sincerely share with the rest of the world, and they design procedures to prevent individuals from doing so. This means your work is only visible to a small group of people, even if it could have a much broader impact.</p>
<p>At InfluxData, I have seen the opposite. Not only is all of my work open source, but the company is actively helping me share it on the company blog, X, and LinkedIn. Andrew even took me to a DataFusion meetup and introduced me to the larger FDAP community. (Meanwhile, Microsoft asks students to pay the travel fee to present company work at conferences)</p>
<p>Open source at InfluxData means not simply moving the repo to the public domain but sincerely engaging with the community, cultivating an environment where everyone is welcome to contribute, and making sure the goals and intentions are clear and well-communicated. As an example, Andrew and I have spent an extensive amount of time writing the blog post and trying our best to share what we have learned with the community, while we could have just merged the PR and moved forward.</p>
</section>
<section id="what-prevents-a-connected-culture" class="level2">
<h2 class="anchored" data-anchor-id="what-prevents-a-connected-culture">What prevents a connected culture?</h2>
<p>Small companies don’t automatically gain a connected culture, but I can tell that InfluxData and Andrew have made deliberate efforts to make it happen.</p>
<p>Large companies, on the other hand, have systematic issues that prevent this connected culture. Even though every individual I met at Google and Microsoft was nice and kind, there’s little they can do to fix the problem from the <a href="https://en.wikipedia.org/wiki/The_Trial">faceless bureaucratic system</a> – you can’t even name a person to blame; it is the procedure.</p>
<p>One of my dreams is to create an employee-first company that prioritizes respect for individuals, explores new ways to organize and connect people, and focuses on maximizing their happiness and creativity rather than just pursuing profit.</p>
<p>(I’d like to thank <a href="https://xuanwo.io">Xuanwo</a> for encouraging me to finish this blog post!)</p>


</section>

 ]]></description>
  <guid>https://blog.xiangpeng.systems/posts/working-at-influxdata/</guid>
  <pubDate>Sun, 18 Aug 2024 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Three types of good academic advisors</title>
  <link>https://blog.xiangpeng.systems/posts/good-advisor/</link>
  <description><![CDATA[ 





<section id="good-advisors" class="level2">
<h2 class="anchored" data-anchor-id="good-advisors">Good advisors</h2>
<p>Innovators, educators, and entrepreneurs.</p>
<p><strong>Innovator</strong>: good at finding the right thing to do. They have a clear vision of what important problems are. They are often smart people who deeply understand the problems they work on. Students learn a lot about their methodologies, visions, and ways of thinking/reasoning. Example: <a href="https://en.wikipedia.org/wiki/Michael_Stonebraker">Mike Stonebraker</a>.</p>
<p><strong>Educator</strong>: good at teaching and empowering the students. They empower their student to reach their potential and to find what they are good at. They motivate their students and ignite their passions. Example: <a href="https://pages.cs.wisc.edu/~remzi/">Remzi H. Arpaci-Dusseau</a>(likely also <a href="https://pages.cs.wisc.edu/~dusseau/">Andrea C. Arpaci-Dusseau</a>, but I haven’t worked with her).</p>
<p><strong>Entrepreneur</strong>: good at resource acquisition and allocation. Students have enough resources to reach their potential. They connect people and match the right students with the right resources. Example: <a href="https://www.cs.utexas.edu/~akella/">Aditya Akella</a>.</p>
<p>A good advisor doesn’t necessarily have to be in only one category (e.g., advisors above span multiple categories). Still, a good advisor often has to be in at least one of the categories.</p>
<p>I’m privileged enough to have worked with all three types of good advisors; however, finding a good advisor requires a lot of luck and effort. Reality check: many faculty members (especially in prestigious universities) are not in any of the categories, and among them, most don’t even try to be a good advisor.</p>
</section>
<section id="signs-of-bad-advisors" class="level2">
<h2 class="anchored" data-anchor-id="signs-of-bad-advisors">Signs of bad advisors</h2>
<blockquote class="blockquote">
<p>All happy families are alike; each unhappy family is unhappy in its own way. – Leo Tolstoy</p>
</blockquote>
<p>Like unhappy families, bad advisors are pretty novel in their ways of being bad; we don’t summarize them here. Instead, we list some key symptoms of working with a bad advisor.</p>
<ol type="1">
<li><strong>Long time-to-recovery</strong>, i.e., too long to recover from a meeting with your advisor.</li>
</ol>
<p>Regular meetings with your advisor can be stressful; often the time you need to prepare many slides, teach your advisor about your work, and try to get some feedback. Often, especially in the early stages of your Ph.D., you don’t have good presentation/communication skills to explain your work to your advisor. Your advisor can get impatient and think you are not progressing or working hard enough.</p>
<p>This can be frustrating and demotivating, and this pattern repeats every week. The question is, how long does it take to recover from this meeting? I have experienced that it can take a few hours, or even an entire day, to recover from the frustration and continue to work on my research. However, it does not have to be this way; a good advisor can motivate students and give concrete feedback to help them improve.</p>
<ol start="2" type="1">
<li><strong>Not being treated like a human</strong>, but a tool/machine to generate papers.</li>
</ol>
<p>Graduate students are first humans, then students, and then researchers, but never tools/machines to generate papers.</p>
<p>Most bad advisors treat their students as leverage to fulfill their own goals. They often have very concrete expectations from their students: publish X papers in Y top-tier conferences/journals and work on Z topics with W skill sets. They evaluate their student by whether they can meet those expectations; even worse, those expectations are often inexplicit and change over time.</p>
<p>If students lag behind expectations – which happens all the time – they are often blamed for not working hard enough or not being smart enough. Worse, they will be threatened to delay graduation, withdraw funding, or even be kicked out of the program. Even if students are on track, they are often not appreciated for their hard work and dedication. They are given more work to do, and the cycle repeats.</p>
<p>Graduate study is a long journey; without being treated like a real human, you <strong>feel</strong> suffering, lonely, and lost. But you deserve a supportive, fruitful, and enjoyable PhD study.</p>
<ol start="3" type="1">
<li><strong>Not aware of power dynamics</strong>, but you are my boss.</li>
</ol>
<p>Many people dreamed an advisor-advisee relationship would be like a marriage partnership: they respect, trust, and work together to achieve common goals.</p>
<p>But that is never the case in reality; advisors pay their students’ salaries, and they can decide when they can graduate. The conversation between an advisor and a student is never equal, and the power dynamics are always there.</p>
<p>The real challenge is whether the advisor is aware of these power dynamics and how they use them. When in disagreement, does the student have the real freedom to say no? Does the advisor sincerely feel comfortable to be challenged? Navigating these power dynamics is art for both students and advisors, but it’s on the advisors to practice and ensure students feel safe and respected.</p>
</section>
<section id="self-reflections" class="level2">
<h2 class="anchored" data-anchor-id="self-reflections">Self-reflections</h2>
<p>It’s easy to blame bad advisors, but it is hard not to become one.</p>
<p>I suffered a lot from bad advisors, but I often wonder if I can be better than them. Toxic advisors result from systematic academia problems, and changing the system is hard. That’s why good advisors are rare and worth being recognized and appreciated.</p>
<p>When pressured with tenure, funding, and publication, will I be as nice/patient/motivating/caring as I imagined myself to be?</p>


</section>

 ]]></description>
  <guid>https://blog.xiangpeng.systems/posts/good-advisor/</guid>
  <pubDate>Tue, 07 May 2024 00:00:00 GMT</pubDate>
</item>
<item>
  <title>What happens when you type a SQL in the database</title>
  <link>https://blog.xiangpeng.systems/posts/sql-to-results/</link>
  <description><![CDATA[ 





<section id="preface" class="level2">
<h2 class="anchored" data-anchor-id="preface">Preface</h2>
<p>A database can be complex; it involves almost all aspects (research communities) of computer science: PL (programming language), SE (software engineering), OS (operating system), networking, storage, theory; more recently, NLP (natural language processing), and ML (machine learning). The database community is centered around the people interested in making the database (the product) better instead of pure intellectual/research interests; it is, therefore, a practical and multi-disciplinary field. This makes databases awesome but also hard to learn.</p>
<p>As complex as it is, the boundaries of the building blocks within a database are clear after decades of research and real-world operations. The recent (and state-of-the-art) <a href="https://github.com/apache/datafusion">Apache DataFusion</a> project is a good example of building a database using well-defined industry standards like <a href="https://arrow.apache.org">Apache Arrow</a>, and <a href="https://parquet.apache.org">Apache Parquet</a>. Without home-grown solutions for storage and in-memory representation, DataFusion can be <a href="https://github.com/apache/datafusion/files/15149988/DataFusion_Query_Engine___SIGMOD_2024-FINAL-mk4.pdf">comparable or even better</a> than alternatives like <a href="https://github.com/duckdb/duckdb">DuckDB</a>.</p>
<p>This document aims to explain these well-defined boundaries, namely, how query engines (i.e., <a href="https://aws.amazon.com/compare/the-difference-between-olap-and-oltp">OLAP</a>) transform a plain SQL query into the results we want, how every step works, and how they are connected.</p>
<div class="cell" data-layout-align="default">
<div class="cell-output-display">
<div>
<p></p><figure class="figure"><p></p>
<div>
<pre class="mermaid mermaid-js">flowchart LR
   id1[SQL text] --&gt; |SQL parser| id2[SQL statement] 
   id2 --&gt; |Query planner| id3[Logical plan] --&gt; |Query optimizer| id4[Optimized logical plan] --&gt; |Physical planner| id5
   id5[Physical plan] --&gt; |Execution| id7[Output]

</pre>
</div>
<p></p></figure><p></p>
</div>
</div>
</div>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>This is a blog post I hoped I knew when I was younger.</p>
<p>I aim to make multi-year efforts to edit and improve it as I learn more about databases. I sometimes dreamed that this post could evolve to be the database equivalent of the <a href="https://pages.cs.wisc.edu/~remzi/OSTEP/">OSTEP</a> book (it might be too ambitious, though).</p>
</div>
</div>
</section>
<section id="section-1-end-to-end-view" class="level2">
<h2 class="anchored" data-anchor-id="section-1-end-to-end-view">Section 1: End-To-End View</h2>
<section id="input" class="level3">
<h3 class="anchored" data-anchor-id="input">Input</h3>
<section id="table-definition" class="level4">
<h4 class="anchored" data-anchor-id="table-definition">Table definition</h4>
<p>We have the following two tables (adapted from <a href="https://www.tpc.org/tpc_documents_current_versions/pdf/tpc-h_v2.17.1.pdf">TPC-H spec</a>): <code>lineitem</code> and <code>orders</code>. The <code>lineitem</code> defines the the shipment dates, while the <code>order</code> defines order details.</p>
<div class="cell" data-layout-align="default">
<div class="cell-output-display">
<div>
<p></p><figure class="figure"><p></p>
<div>
<pre class="mermaid mermaid-js">erDiagram
  lineitem {
      int l_orderkey
      int l_linenumber
      date l_shipdate
      date l_commitdate
      date l_receiptdate
      string l_shipmode
      string l_comment
  }
  orders {
      int o_orderkey
      date o_orderdate
      string o_orderpriority
      string o_clerk
      string o_comment
  }
</pre>
</div>
<p></p></figure><p></p>
</div>
</div>
</div>
</section>
<section id="sql-query" class="level4">
<h4 class="anchored" data-anchor-id="sql-query">SQL query</h4>
<p>Let’s say we have this simple query (adapted from <a href="https://github.com/apache/datafusion/blob/main/benchmarks/queries/q5.sql">TPC-H query 5</a>), which finds the <code>l_orderkey</code>, <code>l_shipdate</code>, and <code>o_orderdate</code> of orders that were placed in <code>1994</code>.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode sql code-with-copy"><code class="sourceCode sql"><span id="cb1-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">SELECT</span></span>
<span id="cb1-2">    l_orderkey, l_shipdate, o_orderdate</span>
<span id="cb1-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">FROM</span></span>
<span id="cb1-4">    orders</span>
<span id="cb1-5"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">JOIN</span></span>
<span id="cb1-6">    lineitem <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">ON</span> l_orderkey <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> o_orderkey</span>
<span id="cb1-7"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">WHERE</span></span>
<span id="cb1-8">    o_orderdate <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;=</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">DATE</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'1994-01-01'</span></span>
<span id="cb1-9">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">AND</span> o_orderdate <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">DATE</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'1995-01-01'</span>;</span></code></pre></div></div>
</section>
</section>
<section id="output" class="level3">
<h3 class="anchored" data-anchor-id="output">Output</h3>
<p>The query is pretty simple; it joins two tables on the order key and then filters the results based on the order date. If everything goes well, we should get results similar to this:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode txt code-with-copy"><code class="sourceCode default"><span id="cb2-1">+------------+------------+-------------+</span>
<span id="cb2-2">| l_orderkey | l_shipdate | o_orderdate |</span>
<span id="cb2-3">+------------+------------+-------------+</span>
<span id="cb2-4">| 1          | 1994-06-01 | 1994-05-01  |</span>
<span id="cb2-5">+------------+------------+-------------+</span></code></pre></div></div>
</section>
</section>
<section id="section-2-parsing" class="level2">
<h2 class="anchored" data-anchor-id="section-2-parsing">Section 2: Parsing</h2>
<p>I skipped it for now as it is mostly orthogonal to the data system pipelines.</p>
<section id="input-1" class="level4">
<h4 class="anchored" data-anchor-id="input-1">Input</h4>
<p>The SQL query text.</p>
</section>
<section id="output-1" class="level4">
<h4 class="anchored" data-anchor-id="output-1">Output</h4>
<p>Structured <a href="https://docs.rs/datafusion/37.1.0/datafusion/sql/parser/enum.Statement.html"><code>statement</code></a> from the SQL (significantly simplified for brevity):</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode txt code-with-copy"><code class="sourceCode default"><span id="cb3-1">from: [</span>
<span id="cb3-2">  TableWithJoins {</span>
<span id="cb3-3">    relation: Table {</span>
<span id="cb3-4">      name: ObjectName([</span>
<span id="cb3-5">        Ident {</span>
<span id="cb3-6">          value: "orders",</span>
<span id="cb3-7">          quote_style: None,</span>
<span id="cb3-8">        },</span>
<span id="cb3-9">      ]),</span>
<span id="cb3-10">    },</span>
<span id="cb3-11">    joins: [</span>
<span id="cb3-12">      Join {</span>
<span id="cb3-13">        relation: Table {</span>
<span id="cb3-14">          name: ObjectName([</span>
<span id="cb3-15">            Ident {</span>
<span id="cb3-16">              value: "lineitem",</span>
<span id="cb3-17">              quote_style: None,</span>
<span id="cb3-18">            },</span>
<span id="cb3-19">          ]),</span>
<span id="cb3-20">        },</span>
<span id="cb3-21">        join_operator: Inner(</span>
<span id="cb3-22">          On(</span>
<span id="cb3-23">            BinaryOp {</span>
<span id="cb3-24">              left: Identifier(</span>
<span id="cb3-25">                Ident {</span>
<span id="cb3-26">                  value: "l_orderkey",</span>
<span id="cb3-27">                  quote_style: None,</span>
<span id="cb3-28">                },</span>
<span id="cb3-29">              ),</span>
<span id="cb3-30">              op: Eq,</span>
<span id="cb3-31">              right: Identifier(</span>
<span id="cb3-32">                Ident {</span>
<span id="cb3-33">                  value: "o_orderkey",</span>
<span id="cb3-34">                  quote_style: None,</span>
<span id="cb3-35">                },</span>
<span id="cb3-36">              ),</span>
<span id="cb3-37">            },</span>
<span id="cb3-38">          ),</span>
<span id="cb3-39">        ),</span>
<span id="cb3-40">      },</span>
<span id="cb3-41">    ],</span>
<span id="cb3-42">  },</span>
<span id="cb3-43">],</span>
<span id="cb3-44">selection: Some(</span>
<span id="cb3-45">  BinaryOp {</span>
<span id="cb3-46">    left: BinaryOp {</span>
<span id="cb3-47">      left: Identifier(</span>
<span id="cb3-48">        Ident {</span>
<span id="cb3-49">          value: "o_orderdate",</span>
<span id="cb3-50">          quote_style: None,</span>
<span id="cb3-51">        },</span>
<span id="cb3-52">      ),</span>
<span id="cb3-53">      op: GtEq,</span>
<span id="cb3-54">      right: TypedString {</span>
<span id="cb3-55">        data_type: Date,</span>
<span id="cb3-56">        value: "1994-01-01",</span>
<span id="cb3-57">      },</span>
<span id="cb3-58">    },</span>
<span id="cb3-59">    op: And,</span>
<span id="cb3-60">    right: BinaryOp {</span>
<span id="cb3-61">      left: Identifier(</span>
<span id="cb3-62">        Ident {</span>
<span id="cb3-63">          value: "o_orderdate",</span>
<span id="cb3-64">          quote_style: None,</span>
<span id="cb3-65">        },</span>
<span id="cb3-66">      ),</span>
<span id="cb3-67">      op: Lt,</span>
<span id="cb3-68">      right: TypedString {</span>
<span id="cb3-69">        data_type: Date,</span>
<span id="cb3-70">        value: "1995-01-01",</span>
<span id="cb3-71">      },</span>
<span id="cb3-72">    },</span>
<span id="cb3-73">  },</span>
<span id="cb3-74">),</span></code></pre></div></div>
</section>
</section>
<section id="section-3-query-planning" class="level2">
<h2 class="anchored" data-anchor-id="section-3-query-planning">Section 3: Query Planning</h2>
<section id="input-2" class="level4">
<h4 class="anchored" data-anchor-id="input-2">Input</h4>
<p>The query statement from the last step.</p>
</section>
<section id="output-2" class="level4">
<h4 class="anchored" data-anchor-id="output-2">Output</h4>
<p>The logical query plan is something like this:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode txt code-with-copy"><code class="sourceCode default"><span id="cb4-1">Projection: lineitem.l_orderkey, lineitem.l_shipdate, orders.o_orderdate</span>
<span id="cb4-2">  Filter: orders.o_orderdate &gt;= CAST(Utf8("1994-01-01") AS Date32) AND orders.o_orderdate &lt; CAST(Utf8("1995-01-01") AS Date32)</span>
<span id="cb4-3">    Inner Join:  Filter: lineitem.l_orderkey = orders.o_orderkey</span>
<span id="cb4-4">      TableScan: orders</span>
<span id="cb4-5">      TableScan: lineitem</span></code></pre></div></div>
<p>Plot it as a tree.</p>
<div class="cell" data-fig-height="2" data-layout-align="default">
<div class="cell-output-display">
<div>
<p></p><figure class="figure"><p></p>
<div>
<svg width="672" height="192" viewbox="0.00 0.00 796.91 260.00" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" style="; max-width: none; max-height: none">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 256)">
<polygon fill="white" stroke="transparent" points="-4,4 -4,-256 792.91,-256 792.91,4 -4,4"></polygon>
<!-- 2 -->
<g id="node1" class="node">
<title>2</title>
<polygon fill="none" stroke="black" points="603.17,-252 185.74,-252 185.74,-216 603.17,-216 603.17,-252"></polygon>
<text text-anchor="middle" x="394.46" y="-229.8" font-family="Times,serif" font-size="14.00">Projection: lineitem.l_orderkey, lineitem.l_shipdate, orders.o_orderdate</text>
</g>
<!-- 3 -->
<g id="node2" class="node">
<title>3</title>
<polygon fill="none" stroke="black" points="788.87,-180 0.04,-180 0.04,-144 788.87,-144 788.87,-180"></polygon>
<text text-anchor="middle" x="394.46" y="-157.8" font-family="Times,serif" font-size="14.00">Filter: orders.o_orderdate &gt;= CAST(Utf8(_1994-01-01_) AS Date32) AND orders.o_orderdate &lt; CAST(Utf8(_1995-01-01_) AS Date32)</text>
</g>
<!-- 2&#45;&gt;3 -->
<g id="edge1" class="edge">
<title>2-&gt;3</title>
<path fill="none" stroke="black" d="M394.46,-205.67C394.46,-197.05 394.46,-187.79 394.46,-180.1"></path>
<polygon fill="black" stroke="black" points="390.96,-205.7 394.46,-215.7 397.96,-205.7 390.96,-205.7"></polygon>
</g>
<!-- 4 -->
<g id="node3" class="node">
<title>4</title>
<polygon fill="none" stroke="black" points="568.59,-108 220.32,-108 220.32,-72 568.59,-72 568.59,-108"></polygon>
<text text-anchor="middle" x="394.46" y="-85.8" font-family="Times,serif" font-size="14.00">Inner Join: &nbsp;Filter: lineitem.l_orderkey = orders.o_orderkey</text>
</g>
<!-- 3&#45;&gt;4 -->
<g id="edge2" class="edge">
<title>3-&gt;4</title>
<path fill="none" stroke="black" d="M394.46,-133.67C394.46,-125.05 394.46,-115.79 394.46,-108.1"></path>
<polygon fill="black" stroke="black" points="390.96,-133.7 394.46,-143.7 397.96,-133.7 390.96,-133.7"></polygon>
</g>
<!-- 5 -->
<g id="node4" class="node">
<title>5</title>
<polygon fill="none" stroke="black" points="383.42,-36 265.5,-36 265.5,0 383.42,0 383.42,-36"></polygon>
<text text-anchor="middle" x="324.46" y="-13.8" font-family="Times,serif" font-size="14.00">TableScan: orders</text>
</g>
<!-- 4&#45;&gt;5 -->
<g id="edge3" class="edge">
<title>4-&gt;5</title>
<path fill="none" stroke="black" d="M369.93,-64.48C360.56,-55.11 350.09,-44.64 341.56,-36.1"></path>
<polygon fill="black" stroke="black" points="367.61,-67.1 377.15,-71.7 372.56,-62.15 367.61,-67.1"></polygon>
</g>
<!-- 6 -->
<g id="node5" class="node">
<title>6</title>
<polygon fill="none" stroke="black" points="529.81,-36 401.11,-36 401.11,0 529.81,0 529.81,-36"></polygon>
<text text-anchor="middle" x="465.46" y="-13.8" font-family="Times,serif" font-size="14.00">TableScan: lineitem</text>
</g>
<!-- 4&#45;&gt;6 -->
<g id="edge4" class="edge">
<title>4-&gt;6</title>
<path fill="none" stroke="black" d="M419.33,-64.48C428.83,-55.11 439.45,-44.64 448.11,-36.1"></path>
<polygon fill="black" stroke="black" points="416.67,-62.18 412.01,-71.7 421.58,-67.17 416.67,-62.18"></polygon>
</g>
</g>
</svg>
</div>
<p></p></figure><p></p>
</div>
</div>
</div>
<p>Logical vs physical.</p>
<p>Todo: describe why we must distinguish between physical and logical plans.</p>
</section>
</section>
<section id="section-4-query-optimizing" class="level2">
<h2 class="anchored" data-anchor-id="section-4-query-optimizing">Section 4: Query Optimizing</h2>
<section id="input-3" class="level4">
<h4 class="anchored" data-anchor-id="input-3">Input</h4>
<p>The (unoptimized) logical plan from the last step.</p>
</section>
<section id="output-3" class="level4">
<h4 class="anchored" data-anchor-id="output-3">Output</h4>
<p>An optimized logical plan.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode txt code-with-copy"><code class="sourceCode default"><span id="cb5-1">Projection: lineitem.l_orderkey, lineitem.l_shipdate, orders.o_orderdate</span>
<span id="cb5-2">  Inner Join: orders.o_orderkey = lineitem.l_orderkey</span>
<span id="cb5-3">    Filter: orders.o_orderdate &gt;= Date32("8766") AND orders.o_orderdate &lt; Date32("9131")</span>
<span id="cb5-4">      TableScan: orders projection=[o_orderkey, o_orderdate], partial_filters=[orders.o_orderdate &gt;= Date32("8766"), orders.o_orderdate &lt; Date32("9131")]</span>
<span id="cb5-5">    TableScan: lineitem projection=[l_orderkey, l_shipdate]</span></code></pre></div></div>
<div class="cell" data-fig-height="2" data-layout-align="default">
<div class="cell-output-display">
<div>
<p></p><figure class="figure"><p></p>
<div>
<svg width="672" height="192" viewbox="0.00 0.00 1054.03 260.00" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" style="; max-width: none; max-height: none">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 256)">
<polygon fill="white" stroke="transparent" points="-4,4 -4,-256 1050.03,-256 1050.03,4 -4,4"></polygon>
<!-- 2 -->
<g id="node1" class="node">
<title>2</title>
<polygon fill="none" stroke="black" points="866.24,-252 448.81,-252 448.81,-216 866.24,-216 866.24,-252"></polygon>
<text text-anchor="middle" x="657.53" y="-229.8" font-family="Times,serif" font-size="14.00">Projection: lineitem.l_orderkey, lineitem.l_shipdate, orders.o_orderdate</text>
</g>
<!-- 3 -->
<g id="node2" class="node">
<title>3</title>
<polygon fill="none" stroke="black" points="810.94,-180 504.11,-180 504.11,-144 810.94,-144 810.94,-180"></polygon>
<text text-anchor="middle" x="657.53" y="-157.8" font-family="Times,serif" font-size="14.00">Inner Join: orders.o_orderkey = lineitem.l_orderkey</text>
</g>
<!-- 2&#45;&gt;3 -->
<g id="edge1" class="edge">
<title>2-&gt;3</title>
<path fill="none" stroke="black" d="M657.53,-205.67C657.53,-197.05 657.53,-187.79 657.53,-180.1"></path>
<polygon fill="black" stroke="black" points="654.03,-205.7 657.53,-215.7 661.03,-205.7 654.03,-205.7"></polygon>
</g>
<!-- 4 -->
<g id="node3" class="node">
<title>4</title>
<polygon fill="none" stroke="black" points="696.95,-108 174.1,-108 174.1,-72 696.95,-72 696.95,-108"></polygon>
<text text-anchor="middle" x="435.53" y="-85.8" font-family="Times,serif" font-size="14.00">Filter: orders.o_orderdate &gt;= Date32(_8766_) AND orders.o_orderdate &lt; Date32(_9131_)</text>
</g>
<!-- 3&#45;&gt;4 -->
<g id="edge2" class="edge">
<title>3-&gt;4</title>
<path fill="none" stroke="black" d="M593.54,-140.83C560.7,-130.47 521.12,-117.99 489.79,-108.11"></path>
<polygon fill="black" stroke="black" points="592.91,-144.3 603.5,-143.97 595.02,-137.62 592.91,-144.3"></polygon>
</g>
<!-- 6 -->
<g id="node5" class="node">
<title>6</title>
<polygon fill="none" stroke="black" points="1046.04,-108 715.01,-108 715.01,-72 1046.04,-72 1046.04,-108"></polygon>
<text text-anchor="middle" x="880.53" y="-85.8" font-family="Times,serif" font-size="14.00">TableScan: lineitem projection=[l_orderkey, l_shipdate]</text>
</g>
<!-- 3&#45;&gt;6 -->
<g id="edge4" class="edge">
<title>3-&gt;6</title>
<path fill="none" stroke="black" d="M721.37,-140.96C754.44,-130.58 794.41,-118.03 826.02,-108.11"></path>
<polygon fill="black" stroke="black" points="720.29,-137.63 711.79,-143.97 722.38,-144.31 720.29,-137.63"></polygon>
</g>
<!-- 5 -->
<g id="node4" class="node">
<title>5</title>
<polygon fill="none" stroke="black" points="871.08,-36 -0.03,-36 -0.03,0 871.08,0 871.08,-36"></polygon>
<text text-anchor="middle" x="435.53" y="-13.8" font-family="Times,serif" font-size="14.00">TableScan: orders projection=[o_orderkey, o_orderdate], partial_filters=[orders.o_orderdate &gt;= Date32(_8766_), orders.o_orderdate &lt; Date32(_9131_)]</text>
</g>
<!-- 4&#45;&gt;5 -->
<g id="edge3" class="edge">
<title>4-&gt;5</title>
<path fill="none" stroke="black" d="M435.53,-61.67C435.53,-53.05 435.53,-43.79 435.53,-36.1"></path>
<polygon fill="black" stroke="black" points="432.03,-61.7 435.53,-71.7 439.03,-61.7 432.03,-61.7"></polygon>
</g>
</g>
</svg>
</div>
<p></p></figure><p></p>
</div>
</div>
</div>
<p>Note the difference between an unoptimized and an optimized plan! The <code>Filter</code> has been pushed down to lower-level nodes. Part of the projection has been embedded in the <code>TableScan</code>.</p>
</section>
</section>
<section id="section-5-physical-planning" class="level2">
<h2 class="anchored" data-anchor-id="section-5-physical-planning">Section 5: Physical Planning</h2>
<section id="input-4" class="level4">
<h4 class="anchored" data-anchor-id="input-4">Input</h4>
<p>A logical plan.</p>
</section>
<section id="output-4" class="level4">
<h4 class="anchored" data-anchor-id="output-4">Output</h4>
<p>A physical plan. Unlike logical plans, physical plans are more concrete about what to do; here’s an example:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode txt code-with-copy"><code class="sourceCode default"><span id="cb6-1">Physical plan:</span>
<span id="cb6-2">ProjectionExec: expr=[l_orderkey@1 as l_orderkey, l_shipdate@2 as l_shipdate, o_orderdate@0 as o_orderdate]</span>
<span id="cb6-3">  CoalesceBatchesExec: target_batch_size=8192</span>
<span id="cb6-4">    HashJoinExec: mode=Partitioned, join_type=Inner, on=[(o_orderkey@0, l_orderkey@0)], projection=[o_orderdate@1, l_orderkey@2, l_shipdate@3]</span>
<span id="cb6-5">      CoalesceBatchesExec: target_batch_size=8192</span>
<span id="cb6-6">        RepartitionExec: partitioning=Hash([o_orderkey@0], 8), input_partitions=8</span>
<span id="cb6-7">          CoalesceBatchesExec: target_batch_size=8192</span>
<span id="cb6-8">            FilterExec: o_orderdate@1 &gt;= 8766 AND o_orderdate@1 &lt; 9131</span>
<span id="cb6-9">              RepartitionExec: partitioning=RoundRobinBatch(8), input_partitions=1</span>
<span id="cb6-10">                CsvExec: file_groups={1 group: [[Users/xiangpeng/work/coding/db-ml/bin/example-data/orders.csv]]}, projection=[o_orderkey, o_orderdate], has_header=true</span>
<span id="cb6-11">      CoalesceBatchesExec: target_batch_size=8192</span>
<span id="cb6-12">        RepartitionExec: partitioning=Hash([l_orderkey@0], 8), input_partitions=8</span>
<span id="cb6-13">          RepartitionExec: partitioning=RoundRobinBatch(8), input_partitions=1</span>
<span id="cb6-14">            CsvExec: file_groups={1 group: [[Users/xiangpeng/work/coding/db-ml/bin/example-data/lineitem.csv]]}, projection=[l_orderkey, l_shipdate], has_header=true</span></code></pre></div></div>
<p>We can also plot a physical plan to a tree graph:</p>
<div class="cell" data-fig-height="4" data-fig-width="8" data-layout-align="default">
<div class="cell-output-display">
<div>
<p></p><figure class="figure"><p></p>
<div>
<svg width="768" height="384" viewbox="0.00 0.00 1122.05 620.00" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" style="; max-width: none; max-height: none">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 616)">
<polygon fill="white" stroke="transparent" points="-4,4 -4,-616 1118.05,-616 1118.05,4 -4,4"></polygon>
<!-- 1 -->
<g id="node1" class="node">
<title>1</title>
<polygon fill="none" stroke="black" points="895.67,-612 245.15,-612 245.15,-576 895.67,-576 895.67,-612"></polygon>
<text text-anchor="middle" x="570.41" y="-589.8" font-family="Times,serif" font-size="14.00">ProjectionExec: expr=[l_orderkey@1 as l_orderkey, l_shipdate@2 as l_shipdate, o_orderdate@0 as o_orderdate]</text>
</g>
<!-- 2 -->
<g id="node2" class="node">
<title>2</title>
<polygon fill="none" stroke="black" points="710.47,-540 430.34,-540 430.34,-504 710.47,-504 710.47,-540"></polygon>
<text text-anchor="middle" x="570.41" y="-517.8" font-family="Times,serif" font-size="14.00">CoalesceBatchesExec: target_batch_size=8192</text>
</g>
<!-- 1&#45;&gt;2 -->
<g id="edge1" class="edge">
<title>1-&gt;2</title>
<path fill="none" stroke="black" d="M570.41,-565.67C570.41,-557.05 570.41,-547.79 570.41,-540.1"></path>
<polygon fill="black" stroke="black" points="566.91,-565.7 570.41,-575.7 573.91,-565.7 566.91,-565.7"></polygon>
</g>
<!-- 3 -->
<g id="node3" class="node">
<title>3</title>
<polygon fill="none" stroke="black" points="1000.06,-468 140.76,-468 140.76,-432 1000.06,-432 1000.06,-468"></polygon>
<text text-anchor="middle" x="570.41" y="-445.8" font-family="Times,serif" font-size="14.00">HashJoinExec: mode=Partitioned, join_type=Inner, on=[(o_orderkey@0, l_orderkey@0)], projection=[o_orderdate@1, l_orderkey@2, l_shipdate@3]</text>
</g>
<!-- 2&#45;&gt;3 -->
<g id="edge2" class="edge">
<title>2-&gt;3</title>
<path fill="none" stroke="black" d="M570.41,-493.67C570.41,-485.05 570.41,-475.79 570.41,-468.1"></path>
<polygon fill="black" stroke="black" points="566.91,-493.7 570.41,-503.7 573.91,-493.7 566.91,-493.7"></polygon>
</g>
<!-- 4 -->
<g id="node4" class="node">
<title>4</title>
<polygon fill="none" stroke="black" points="561.47,-396 281.34,-396 281.34,-360 561.47,-360 561.47,-396"></polygon>
<text text-anchor="middle" x="421.41" y="-373.8" font-family="Times,serif" font-size="14.00">CoalesceBatchesExec: target_batch_size=8192</text>
</g>
<!-- 3&#45;&gt;4 -->
<g id="edge3" class="edge">
<title>3-&gt;4</title>
<path fill="none" stroke="black" d="M524.69,-427.52C503.19,-417.42 477.91,-405.54 457.74,-396.07"></path>
<polygon fill="black" stroke="black" points="523.42,-430.79 533.96,-431.88 526.4,-424.46 523.42,-430.79"></polygon>
</g>
<!-- 10 -->
<g id="node10" class="node">
<title>10</title>
<polygon fill="none" stroke="black" points="899.47,-396 619.34,-396 619.34,-360 899.47,-360 899.47,-396"></polygon>
<text text-anchor="middle" x="759.41" y="-373.8" font-family="Times,serif" font-size="14.00">CoalesceBatchesExec: target_batch_size=8192</text>
</g>
<!-- 3&#45;&gt;10 -->
<g id="edge9" class="edge">
<title>3-&gt;10</title>
<path fill="none" stroke="black" d="M626.21,-428.33C653.94,-418.06 687.05,-405.8 713.32,-396.07"></path>
<polygon fill="black" stroke="black" points="624.81,-425.12 616.64,-431.88 627.24,-431.68 624.81,-425.12"></polygon>
</g>
<!-- 5 -->
<g id="node5" class="node">
<title>5</title>
<polygon fill="none" stroke="black" points="562.11,-324 118.71,-324 118.71,-288 562.11,-288 562.11,-324"></polygon>
<text text-anchor="middle" x="340.41" y="-301.8" font-family="Times,serif" font-size="14.00">RepartitionExec: partitioning=Hash([o_orderkey@0], 8), input_partitions=8</text>
</g>
<!-- 4&#45;&gt;5 -->
<g id="edge4" class="edge">
<title>4-&gt;5</title>
<path fill="none" stroke="black" d="M393.68,-353.03C382.67,-343.52 370.27,-332.8 360.2,-324.1"></path>
<polygon fill="black" stroke="black" points="391.53,-355.81 401.39,-359.7 396.11,-350.51 391.53,-355.81"></polygon>
</g>
<!-- 6 -->
<g id="node6" class="node">
<title>6</title>
<polygon fill="none" stroke="black" points="467.47,-252 187.34,-252 187.34,-216 467.47,-216 467.47,-252"></polygon>
<text text-anchor="middle" x="327.41" y="-229.8" font-family="Times,serif" font-size="14.00">CoalesceBatchesExec: target_batch_size=8192</text>
</g>
<!-- 5&#45;&gt;6 -->
<g id="edge5" class="edge">
<title>5-&gt;6</title>
<path fill="none" stroke="black" d="M335.33,-277.67C333.73,-269.05 332.01,-259.79 330.59,-252.1"></path>
<polygon fill="black" stroke="black" points="331.93,-278.5 337.2,-287.7 338.81,-277.23 331.93,-278.5"></polygon>
</g>
<!-- 7 -->
<g id="node7" class="node">
<title>7</title>
<polygon fill="none" stroke="black" points="494.62,-180 108.19,-180 108.19,-144 494.62,-144 494.62,-180"></polygon>
<text text-anchor="middle" x="301.41" y="-157.8" font-family="Times,serif" font-size="14.00">FilterExec: o_orderdate@1 &gt;= 8766 AND o_orderdate@1 &lt; 9131</text>
</g>
<!-- 6&#45;&gt;7 -->
<g id="edge6" class="edge">
<title>6-&gt;7</title>
<path fill="none" stroke="black" d="M317.47,-206.23C314.21,-197.46 310.68,-187.96 307.76,-180.1"></path>
<polygon fill="black" stroke="black" points="314.22,-207.54 320.98,-215.7 320.78,-205.1 314.22,-207.54"></polygon>
</g>
<!-- 8 -->
<g id="node8" class="node">
<title>8</title>
<polygon fill="none" stroke="black" points="510.59,-108 92.23,-108 92.23,-72 510.59,-72 510.59,-108"></polygon>
<text text-anchor="middle" x="301.41" y="-85.8" font-family="Times,serif" font-size="14.00">RepartitionExec: partitioning=RoundRobinBatch(8), input_partitions=1</text>
</g>
<!-- 7&#45;&gt;8 -->
<g id="edge7" class="edge">
<title>7-&gt;8</title>
<path fill="none" stroke="black" d="M301.41,-133.67C301.41,-125.05 301.41,-115.79 301.41,-108.1"></path>
<polygon fill="black" stroke="black" points="297.91,-133.7 301.41,-143.7 304.91,-133.7 297.91,-133.7"></polygon>
</g>
<!-- 9 -->
<g id="node9" class="node">
<title>9</title>
<polygon fill="none" stroke="black" points="602.73,-36 0.09,-36 0.09,0 602.73,0 602.73,-36"></polygon>
<text text-anchor="middle" x="301.41" y="-13.8" font-family="Times,serif" font-size="14.00">CsvExec: file_groups={1 group: [[orders.csv]]}, projection=[o_orderkey, o_orderdate], has_header=true</text>
</g>
<!-- 8&#45;&gt;9 -->
<g id="edge8" class="edge">
<title>8-&gt;9</title>
<path fill="none" stroke="black" d="M301.41,-61.67C301.41,-53.05 301.41,-43.79 301.41,-36.1"></path>
<polygon fill="black" stroke="black" points="297.91,-61.7 301.41,-71.7 304.91,-61.7 297.91,-61.7"></polygon>
</g>
<!-- 11 -->
<g id="node11" class="node">
<title>11</title>
<polygon fill="none" stroke="black" points="1026.5,-324 586.32,-324 586.32,-288 1026.5,-288 1026.5,-324"></polygon>
<text text-anchor="middle" x="806.41" y="-301.8" font-family="Times,serif" font-size="14.00">RepartitionExec: partitioning=Hash([l_orderkey@0], 8), input_partitions=8</text>
</g>
<!-- 10&#45;&gt;11 -->
<g id="edge10" class="edge">
<title>10-&gt;11</title>
<path fill="none" stroke="black" d="M776.62,-351.36C782.72,-342.28 789.42,-332.3 794.92,-324.1"></path>
<polygon fill="black" stroke="black" points="773.7,-349.44 771.03,-359.7 779.51,-353.35 773.7,-349.44"></polygon>
</g>
<!-- 12 -->
<g id="node12" class="node">
<title>12</title>
<polygon fill="none" stroke="black" points="1018.59,-252 600.23,-252 600.23,-216 1018.59,-216 1018.59,-252"></polygon>
<text text-anchor="middle" x="809.41" y="-229.8" font-family="Times,serif" font-size="14.00">RepartitionExec: partitioning=RoundRobinBatch(8), input_partitions=1</text>
</g>
<!-- 11&#45;&gt;12 -->
<g id="edge11" class="edge">
<title>11-&gt;12</title>
<path fill="none" stroke="black" d="M807.58,-277.67C807.95,-269.05 808.35,-259.79 808.68,-252.1"></path>
<polygon fill="black" stroke="black" points="804.08,-277.56 807.15,-287.7 811.08,-277.86 804.08,-277.56"></polygon>
</g>
<!-- 13 -->
<g id="node13" class="node">
<title>13</title>
<polygon fill="none" stroke="black" points="1114.19,-180 512.62,-180 512.62,-144 1114.19,-144 1114.19,-180"></polygon>
<text text-anchor="middle" x="813.41" y="-157.8" font-family="Times,serif" font-size="14.00">CsvExec: file_groups={1 group: [[lineitem.csv]]}, projection=[l_orderkey, l_shipdate], has_header=true</text>
</g>
<!-- 12&#45;&gt;13 -->
<g id="edge12" class="edge">
<title>12-&gt;13</title>
<path fill="none" stroke="black" d="M810.97,-205.67C811.46,-197.05 811.99,-187.79 812.43,-180.1"></path>
<polygon fill="black" stroke="black" points="807.47,-205.51 810.4,-215.7 814.46,-205.91 807.47,-205.51"></polygon>
</g>
</g>
</svg>
</div>
<p></p></figure><p></p>
</div>
</div>
</div>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>Note that a physical plan has much more details than a logical plan; it contains everything needed to execute the query!</p>
</div>
</div>
<p>(Optional: we often have physical optimizers that optimize on a physical plan. Omitted here for simplicity)</p>
</section>
</section>
<section id="section-6-query-execution" class="level2">
<h2 class="anchored" data-anchor-id="section-6-query-execution">Section 6: Query Execution</h2>
<section id="input-5" class="level4">
<h4 class="anchored" data-anchor-id="input-5">Input</h4>
<p>A physical plan</p>
</section>
<section id="output-5" class="level4">
<h4 class="anchored" data-anchor-id="output-5">Output</h4>
<p>The final output is like this:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode txt code-with-copy"><code class="sourceCode default"><span id="cb7-1">+------------+------------+-------------+</span>
<span id="cb7-2">| l_orderkey | l_shipdate | o_orderdate |</span>
<span id="cb7-3">+------------+------------+-------------+</span>
<span id="cb7-4">| 1          | 1994-06-01 | 1994-05-01  |</span>
<span id="cb7-5">+------------+------------+-------------+</span></code></pre></div></div>
</section>
<section id="execution-order" class="level3">
<h3 class="anchored" data-anchor-id="execution-order">Execution order</h3>
<p>The simplest execution model is <a href="https://justinjaffray.com/query-engines-push-vs.-pull/">pull-based execution</a>, which implements a <a href="https://www.freecodecamp.org/news/binary-search-tree-traversal-inorder-preorder-post-order-for-bst/">post-order traversal</a> of the physical plan. For a tree (like blow), we get a traversal order of <code>D -&gt; E -&gt; B -&gt; F -&gt; G -&gt; C -&gt; A</code>: <img src="https://blog.xiangpeng.systems/posts/sql-to-results/f4.png" class="img-fluid"></p>
<p>Applying our physical graph above, we get an execution order of:</p>
<ol type="1">
<li><p><a href="https://docs.rs/datafusion/37.1.0/datafusion/datasource/physical_plan/struct.CsvExec.html"><code>CsvExec (orders.csv)</code></a></p></li>
<li><p><a href="https://docs.rs/datafusion/37.1.0/datafusion/physical_plan/repartition/struct.RepartitionExec.html"><code>RepartitionExec</code></a></p></li>
<li><p><a href="https://docs.rs/datafusion/37.1.0/datafusion/physical_plan/filter/struct.FilterExec.html"><code>FilterExec</code></a></p></li>
<li><p><a href="https://docs.rs/datafusion/37.1.0/datafusion/physical_plan/coalesce_batches/struct.CoalesceBatchesExec.html"><code>CoalesceBatchesExec</code></a></p></li>
<li><p><a href="https://docs.rs/datafusion/37.1.0/datafusion/physical_plan/repartition/struct.RepartitionExec.html"><code>RepartitionExec</code></a></p></li>
<li><p><a href="https://docs.rs/datafusion/37.1.0/datafusion/physical_plan/coalesce_batches/struct.CoalesceBatchesExec.html"><code>CoalesceBatchesExec</code></a></p></li>
<li><p><a href="https://docs.rs/datafusion/37.1.0/datafusion/datasource/physical_plan/struct.CsvExec.html"><code>CsvExec (lineitem.csv)</code></a></p></li>
<li><p><a href="https://docs.rs/datafusion/37.1.0/datafusion/physical_plan/repartition/struct.RepartitionExec.html"><code>RepartitionExec</code></a></p></li>
<li><p><a href="https://docs.rs/datafusion/37.1.0/datafusion/physical_plan/repartition/struct.RepartitionExec.html"><code>RepartitionExec</code></a></p></li>
<li><p><a href="https://docs.rs/datafusion/37.1.0/datafusion/physical_plan/coalesce_batches/struct.CoalesceBatchesExec.html"><code>CoalesceBatchesExec</code></a></p></li>
<li><p><a href="https://docs.rs/datafusion/37.1.0/datafusion/physical_plan/joins/struct.HashJoinExec.html"><code>HashJoinExec</code></a></p></li>
<li><p><a href="https://docs.rs/datafusion/37.1.0/datafusion/physical_plan/coalesce_batches/struct.CoalesceBatchesExec.html"><code>CoalesceBatchesExec</code></a></p></li>
<li><p><a href="https://docs.rs/datafusion/37.1.0/datafusion/physical_plan/projection/struct.ProjectionExec.html"><code>ProjectionExec</code></a></p></li>
</ol>
<p>The <code>RepartitionExec</code> and <code>CoalesceBatchesExec</code> are executors that partition the data for multi-thread processing (based on the <a href="https://w6113.github.io/files/papers/volcanoparallelism-89.pdf">Volcano execution</a> style).</p>
<p>A simplified, single-threaded, no-partitioned execution order would be:</p>
<div class="cell" data-layout-align="default">
<div class="cell-output-display">
<div>
<p></p><figure class="figure"><p></p>
<div>
<pre class="mermaid mermaid-js">graph LR;
    e1["CsvExec (orders.csv)"] --&gt; FilterExec
    FilterExec --&gt; e2 
    e2["CsvExec (lineitem.csv)"] --&gt; HashJoinExec
    HashJoinExec --&gt; ProjectionExec
</pre>
</div>
<p></p></figure><p></p>
</div>
</div>
</div>
</section>
<section id="reading-from-disk" class="level3">
<h3 class="anchored" data-anchor-id="reading-from-disk">Reading from disk</h3>
<p>CSV files are row-based, and we read them row by row, it is efficient when we frequently need to read the whole row. However, modern data analytic workloads do not always need to read the whole row; they often only need to read a subset of columns. In our example above, we only need to read <code>l_orderkey</code>, <code>l_shipdate</code>, <code>o_orderdate</code>, <code>o_orderkey</code> from the tables. If using a row-based file format (like CSV), we need to load all columns into memory, which is inefficient. Column-based file formats (like <a href="https://parquet.apache.org/">Apache Parquet</a>) can be more efficient in this case.</p>
<p>See the <a href="../../posts/parquet-to-arrow/">Parquet pruning in DataFusion</a> for more details.</p>


</section>
</section>

 ]]></description>
  <guid>https://blog.xiangpeng.systems/posts/sql-to-results/</guid>
  <pubDate>Fri, 26 Apr 2024 00:00:00 GMT</pubDate>
</item>
</channel>
</rss>
