<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Opteryx Engineering - Medium]]></title>
        <description><![CDATA[Learn about Optery’x product developments. - Medium]]></description>
        <link>https://medium.com/opteryx?source=rss----89f5b4c8c0ad---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Opteryx Engineering - Medium</title>
            <link>https://medium.com/opteryx?source=rss----89f5b4c8c0ad---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 31 May 2026 16:37:22 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/opteryx" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[When we Stopped Using Regex for REGEXP_REPLACE]]></title>
            <link>https://medium.com/opteryx/when-we-stopped-using-regex-for-regexp-replace-cce38a358893?source=rss----89f5b4c8c0ad---4</link>
            <guid isPermaLink="false">https://medium.com/p/cce38a358893</guid>
            <category><![CDATA[big-data]]></category>
            <category><![CDATA[sql]]></category>
            <category><![CDATA[string-processing]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[optimization]]></category>
            <dc:creator><![CDATA[Justin Joyce]]></dc:creator>
            <pubDate>Fri, 24 Apr 2026 13:20:10 GMT</pubDate>
            <atom:updated>2026-04-24T13:20:42.435Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*siNT81mYGFRkQ1d0Kg9BwA.png" /></figure><p>This post is also published on the <a href="https://docs.opteryx.app/blog/2026-04-24-regex-replace">Opteryx Engineering Blog</a></p><h3>TL;DR</h3><p>Our REGEXP_REPLACE performance on <a href="https://benchmark.clickhouse.com/">ClickBench</a> was 10x slower than peers. Swapping regex engines barely moved the needle. The fix wasn’t finding a faster regex engine, it was avoiding regex entirely. We built a specialised DFA and reduced query times to near parity.</p><h3>The Problem</h3><p>REGEXP_REPLACE was eating our lunch.</p><p>I’ll be honest, when we first started publishing to <a href="https://benchmark.clickhouse.com/">ClickBench</a>, we were so far from the pack that the performance of one specialized query wasn’t going to close that gap.</p><p>After many iterations of the engine, performance of queries like Query 28 being ~10x slower than engines like <a href="https://duckdb.org/">DuckDB</a> stands out like a sore thumb.</p><p>When a single operator dominates runtime, that naively feels like an easy win.</p><h3>We Tried the Obvious Thing First</h3><p>First step: swap the regex engine.</p><p>DuckDB uses <a href="https://github.com/google/re2">RE2</a>. It’s well known, fast, and predictable. Google wrote and use <a href="https://docs.cloud.google.com/bigquery/docs/reference/standard-sql/string_functions#regexp_contains">RE2 in BigQuery</a>, so it’s fast and well-suited to our problem, so we integrated it into <a href="https://opteryx.app/">Opteryx</a>.</p><p>Result? Barely any change.</p><p>Maybe 5% in some cases. Hard to separate from noise. That’s not a breakthrough — that’s measurement variance.</p><p>Conclusion: the regex engine wasn’t the bottleneck.</p><h3>The Pivot</h3><p>If a faster regex engine wasn’t the answer, what was?</p><p>Most REGEXP_REPLACE calls we see don’t need full regex — they’re mostly extract or replace calls.</p><p>We had brought a regex to a slice fight.</p><p>Turns out, you don’t need backtracking to do patterned string slicing. You could use a <a href="https://en.wikipedia.org/wiki/Deterministic_finite_automaton">deterministic finite automaton</a> (DFA).</p><h3>What We Built</h3><p>We built a specialised engine, based on a DFA, for the patterns we actually see.</p><p>Not a full regex engine. A constrained one. Simple, even.</p><p>Design:</p><ul><li>compile pattern → deterministic automaton (once, at initialization)</li><li>operate directly on buffers (no Python strings)</li><li>track the capture points through the pattern</li><li>slice the buffer at the capture points to extract the value</li></ul><p>The key idea is embarrassingly simple: compile a rule you can progress through the string monotonically to execute.</p><h3>Results</h3><p>From our lab ClickBench runs (non-published):</p><p>We went from 60 seconds down to 10. Same workload, same query shape, just a different approach.</p><p>This isn’t a small optimisation. It changes the cost profile of the query.</p><h3>Engineering Lessons</h3><p>One of the strengths of SQL is the engine is free to choose how it processes the user’s request. Here, we were given a regex so we naively processed it as a regex, rather than using regex as the language to describe the intent and choosing the fastest way for us to meet that ask.</p><h3>Final Thought</h3><p>The lesson isn’t “build a faster regex engine”.</p><p>It’s “recognise when you don’t need one”.</p><p>— Justin</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cce38a358893" width="1" height="1" alt=""><hr><p><a href="https://medium.com/opteryx/when-we-stopped-using-regex-for-regexp-replace-cce38a358893">When we Stopped Using Regex for REGEXP_REPLACE</a> was originally published in <a href="https://medium.com/opteryx">Opteryx Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Rewriting the Memory Model: Moving Beyond Arrow]]></title>
            <link>https://medium.com/opteryx/rewriting-the-memory-model-moving-beyond-arrow-7368cef7d147?source=rss----89f5b4c8c0ad---4</link>
            <guid isPermaLink="false">https://medium.com/p/7368cef7d147</guid>
            <category><![CDATA[apache-arrow]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[performance]]></category>
            <category><![CDATA[big-data]]></category>
            <category><![CDATA[optimization]]></category>
            <dc:creator><![CDATA[Justin Joyce]]></dc:creator>
            <pubDate>Fri, 17 Apr 2026 11:30:08 GMT</pubDate>
            <atom:updated>2026-04-17T11:31:30.907Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wcurTGJ2Jq4F7zyRGY8VeA.png" /></figure><h3>TL;DR</h3><p><a href="https://arrow.apache.org/">Arrow</a> helped us get started, but it became a performance barrier in the execution path.</p><p>On <a href="https://benchmark.clickhouse.com/#system=-ndd&amp;type=-&amp;machine=+ca4e&amp;cluster_size=-&amp;opensource=-&amp;hardware=+c&amp;tuned=+n&amp;metric=combined&amp;queries=-">ClickBench</a> we were near the front of a group of medium-performance engines, but there was a clear gap to the fastest ones. That usually points to something structural rather than something you can tune away.</p><p>So we stopped trying to optimise around it and started replacing Arrow with something designed for how <a href="https://opteryx.app/">Opteryx</a> actually runs queries.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_-Ua_H4pmzeWL6kDl0LMUw.png" /></figure><p>⸻</p><h3>The starting point</h3><p>Arrow solved real problems for us early on.</p><p>It gave us:</p><ul><li>a lot of functionality without having to build everything ourselves</li><li>compatibility with <a href="https://parquet.apache.org/">Parquet</a> and the wider ecosystem</li></ul><p>That mattered. When you’re building a query engine, there’s a lot of value in starting from something stable.</p><p>And to be clear, Arrow is a good fit for a lot of systems. If your problem is interoperability or general-purpose data processing, you won’t go far wrong with it.</p><p>This isn’t a post about Arrow being bad.</p><p>It’s about hitting a limit.</p><p>⸻</p><h3>The problem we kept seeing</h3><p>As we worked through performance issues in Opteryx, we kept seeing the same pattern.</p><p>We could make things faster locally:</p><ul><li>tighten loops</li><li>reduce allocations</li><li>optimise operators</li></ul><p>But the gains stopped stacking.</p><p>That usually means the problem isn’t local anymore. It means the architecture is doing something expensive over and over again.</p><p>In our case, it was moving data between representations.</p><p>A typical path looked like:</p><ul><li>Arrow arrays as the source</li><li><a href="https://numpy.org/">NumPy</a> views or copies for some operations</li><li>Python objects where neither worked cleanly</li></ul><p>Every step between those had a cost.</p><p>⸻</p><h3>A concrete example</h3><p>This showed up clearly when we reworked our <a href="https://docs.opteryx.app/blog/2026-04-10-like-performance">LIKE</a> operator.</p><p>The original path moved data from Arrow → NumPy → Python.</p><p>When we processed the Arrow buffers directly, we cut about 7 seconds off a run over 100 million strings.</p><p>That wasn’t a clever optimisation. It was just removing transitions.</p><p>⸻</p><h3>The conversion tax</h3><p>The easiest way to think about it is a conversion tax.</p><p>The engine wasn’t operating on one representation end-to-end. It kept crossing boundaries, and every boundary had overhead.</p><p>Two things dominated.</p><p><strong>CPU overhead</strong></p><p>Arrow’s null handling is fine on its own. The problem is what happens when you mix it with Python and multiple execution paths:</p><ul><li>extra checks in tight loops</li><li>more branching</li><li>Python object access creeping into hot paths</li><li>vectorised paths dropping back to interpreted ones</li></ul><p>None of that is catastrophic individually, but together it adds up.</p><p><strong>Memory overhead</strong></p><p>We were also holding the same data more than once.</p><p>Combinations of:</p><ul><li>Arrow buffers</li><li>NumPy arrays</li><li>Python structures</li></ul><p>In theory Arrow supports zero-copy. In practice, null handling and layout differences often meant we couldn’t take that path cleanly.</p><p>So we ended up duplicating and adapting data instead.</p><p>⸻</p><h3>We tried to push Arrow further</h3><p>Before replacing it, we tried to make it work.</p><p>We accessed buffers directly. We avoided higher-level APIs. We pushed more work into compiled code.</p><p>That helped, but only up to a point.</p><p>We kept running into the same issue: even when the data was “zero-copy”, the execution wasn’t. The loops were still in Python, or the control flow still depended on it.</p><p>At the same time, the engine was getting more complicated. Each workaround made one path faster and something else harder to reason about.</p><p>At some point it became clear we were optimising around the mismatch instead of removing it.</p><p>⸻</p><h3>The decision</h3><p>Once we framed it properly, the direction was obvious.</p><p>If we wanted to move the performance ceiling, we needed to own the memory model used in execution.</p><p>That’s where Draken came from.</p><p>⸻</p><h3>What Draken is designed to do</h3><p>Draken isn’t trying to replace Arrow everywhere. It’s about removing it from the hot path.</p><p>A few things mattered:</p><p><strong>Keep Python out of tight loops</strong></p><p>If something is performance-critical, it shouldn’t be running through Python object machinery.</p><p>Iteration, null handling, operator execution — all of that needs to stay in native code.</p><p><strong>Stop translating data</strong></p><p>The engine shouldn’t need to convert data just to move between stages.</p><p>A query engine does enough work already. Converting representations shouldn’t be part of it.</p><p><strong>Control the layout</strong></p><p>We want the memory layout to match how the engine actually executes, not how a general-purpose format needs to behave.</p><p>We still align with Parquet where it makes sense — things like dictionary encoding still matter — but we’re not bound to Arrow’s internal structure.</p><p>⸻</p><h3>Why this changes the ceiling</h3><p>The gains here don’t come from a single optimisation.</p><p>They come from removing whole categories of work:</p><ul><li>no Python in tight loops</li><li>no repeated conversions</li><li>null handling designed for our execution model</li><li>memory layout chosen for operators, not interchange</li></ul><p>That makes things faster, but more importantly it makes further improvements easier.</p><p>⸻</p><h3>The engineering lesson</h3><p>Arrow was a good starting point, but it wasn’t the right execution format for Opteryx long term.</p><p>The mismatch between memory model and execution model kept showing up as overhead.</p><p>Owning the format let us remove that friction.</p><p>That’s where the gain comes from — not a faster component, but a simpler path.</p><p>⸻</p><h3>One unexpected win</h3><p>One thing we didn’t expect: Arrow often materialises dictionary-encoded columns into dense representations.</p><p>By keeping dictionary encoding in our internal format, we got a nice side-effect:</p><ul><li>fewer comparisons</li><li>smaller working sets</li><li>less memory traffic</li></ul><p>That wasn’t the goal, but it turned out to be a useful win.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7368cef7d147" width="1" height="1" alt=""><hr><p><a href="https://medium.com/opteryx/rewriting-the-memory-model-moving-beyond-arrow-7368cef7d147">Rewriting the Memory Model: Moving Beyond Arrow</a> was originally published in <a href="https://medium.com/opteryx">Opteryx Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[10x Faster Memory Management: Optimising Opteryx’s Core Memory Pool]]></title>
            <link>https://medium.com/opteryx/10x-faster-memory-management-optimising-opteryxs-core-memory-pool-74e9bde199b4?source=rss----89f5b4c8c0ad---4</link>
            <guid isPermaLink="false">https://medium.com/p/74e9bde199b4</guid>
            <category><![CDATA[data-processing]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[database-internals]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[performance-optimization]]></category>
            <dc:creator><![CDATA[Justin Joyce]]></dc:creator>
            <pubDate>Mon, 13 Apr 2026 19:15:01 GMT</pubDate>
            <atom:updated>2026-04-13T19:16:03.585Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WZpFr92BLIt3kws1IQ1TdA.png" /></figure><p>This post is also published on the <a href="https://docs.opteryx.app/blog/2026-04-03-memory-pool">Opteryx Engineering Blog</a></p><h3>TL;DR</h3><p>A small, surgical change to the memory pool produced a 10x improvement in allocation/commit throughput. We moved metadata tracking out of Python and into a compact C++ structure, preserved the public API, and avoided a large rewrite. The result: much higher throughput, lower variance, and no behavioural changes for users.</p><h3>The problem</h3><p>The MemoryPool is central to query execution: it allocates buffers, manages lifetime, supports zero‑copy reads, and compacts segments. For years the pool tracked segment metadata using Python dicts. That was simple and readable — but slow.</p><p>In a tight allocate→read→release loop (the exact pattern used across query plans and streaming workloads) Python hash-table lookups and object overhead dominated the hot path. The metadata lookups were the bottleneck.</p><h3>The change</h3><p>We did three things, incrementally and carefully:</p><ul><li>Replaced the Python dict used for metadata with a C++ unordered_map&lt;int64_t, SegmentMetadata&gt;.</li><li>Moved metadata into a compact C struct (SegmentMetadata) with no Python object overhead.</li><li>Kept the public Python API identical; used_segments remains a lazily-evaluated Python dict for compatibility.</li></ul><p>The key principle was minimalism: replace just the slow part and keep everything else stable.</p><h3>Why this works</h3><ul><li>Metadata access is performance‑critical but implementation‑local. Users call the same APIs; they do not rely on Python dict semantics for internal bookkeeping.</li><li>Moving metadata to C++ removes Python interpreter and object costs from the hot path.</li><li>Keeping the public API stable means tests, consumers, and integrations continue to work without change.</li></ul><p>We also retained Python RLock for synchronization because C++ template types cannot be embedded in Cython classes in our current layout — a pragmatic compromise that keeps thread-safety intact.</p><h3>Results</h3><p>Benchmarks (small allocations: 50k commits of 100 bytes):</p><p>Old implementation: 12,839 ops/sec<br>New implementation: 134,104 ops/sec</p><p>Improvement: 10.4x faster</p><p>This is a meaningful change, not a micro‑tweak — it shifts the envelope for memory‑bound workloads and reduces variance introduced by the Python runtime.</p><h3>Where it matters</h3><ul><li>Read cache: we’re planning to use the MemoryPool as a read-caching layer as part of continual IO-stack improvements — enabling hot-block reuse, reducing physical IO, and improving tail latency for common queries.</li><li>Morsel exchange: during the execution-engine rewrite the pool will act as the morsel exchange between operators, enabling efficient, zero-copy morsel handoffs and clearer ownership boundaries for execution stages.</li><li>Zero‑copy flows: lower latency between producers and consumers when memory handoffs are fast and predictable.</li><li>Classic <a href="https://opteryx.app/">Opteryx</a>: historically the MemoryPool served as the buffer pool; these planned uses extend that role into caching and operator exchange while preserving the same minimal, native hot path and public API.</li></ul><h3>How we approached it</h3><p>This was not a rewrite. The steps were:</p><ol><li>Profile to confirm the real bottleneck (dict lookups and object churn).</li><li>Design a minimal C++ metadata representation and chosen container (unordered_map&lt;int64_t, SegmentMetadata&gt;).</li><li>Implement the C++ layer behind the existing Cython/Python bindings.</li><li>Preserve the Python-facing API and lazy compatibility layers.</li><li>Run the full test-suite and benchmark under representative loads.</li></ol><p>The result was surgical: small, reviewable changes with a large impact.</p><h3>The broader lesson</h3><p>Optimising a mature codebase usually works best as a targeted, incremental effort. Identify the true hot path, replace the implementation with a low‑overhead equivalent, and keep the surrounding behaviour stable. You get the performance gains without the risk and cost of a full rewrite.</p><p>If you’re struggling with latency or throughput in a Python project, look for implementation details that are purely internal state — those are often the best places to move into faster languages without changing your public contract.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=74e9bde199b4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/opteryx/10x-faster-memory-management-optimising-opteryxs-core-memory-pool-74e9bde199b4">10x Faster Memory Management: Optimising Opteryx’s Core Memory Pool</a> was originally published in <a href="https://medium.com/opteryx">Opteryx Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Making LIKE Faster: From 93 Seconds to Single Digits]]></title>
            <link>https://medium.com/opteryx/making-like-faster-from-93-seconds-to-single-digits-220e01bf66ba?source=rss----89f5b4c8c0ad---4</link>
            <guid isPermaLink="false">https://medium.com/p/220e01bf66ba</guid>
            <category><![CDATA[data-processing]]></category>
            <category><![CDATA[database]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[boyer-moore-algorithm]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[Justin Joyce]]></dc:creator>
            <pubDate>Sat, 11 Apr 2026 21:31:26 GMT</pubDate>
            <atom:updated>2026-04-11T21:35:26.400Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XXaC5HtmsFbbeFmUX79tfQ.png" /></figure><p>This post is also published on the <a href="https://docs.opteryx.app/blog/2026-04-10-like-performance">Opteryx Engineering Blog</a></p><h3>TL;DR</h3><p>The LIKE operator, particularly the &#39;%needle%&#39; pattern (substring search), went from 93 seconds in <a href="https://opteryx.app/">Opteryx</a> 0.19.0 to 7.46 seconds in 0.26.2. The improvement came in stages: fixing the IO stack, recognising that &#39;%needle%&#39; is really a CONTAINS check (not a regex), direct buffer access, and adopting the Volnitsky algorithm with a sieve prefilter. We&#39;re now at ~2x overhead compared to single-threaded C++ engines, and we&#39;re pushing further by removing Arrow and eliminating GIL overhead in the search path.</p><h3>The Problem</h3><p>Substring search in SQL is ubiquitous: filtering user IDs, matching partial text, finding patterns in logs. The LIKE operator handles this, but implementation details matter enormously.</p><p>Our reference point is <a href="https://benchmark.clickhouse.com/"><strong>ClickBench</strong></a><strong> query 20</strong>, an example workload scanning ~100 million rows on Parquet data.</p><p>January 2025, Opteryx 0.19.0:</p><pre>93.27 seconds</pre><p>That’s unacceptable. For comparison, C++/Rust engines like DuckDB and Clickhouse doing the same work were completing in under 4 seconds.</p><p>Profiling showed a large part of the problem wasn’t algorithmic — it was fundamental. The IO stack was stalling execution. Improving string search was pointless if the data wasn’t arriving.</p><h3>Stage 1: IO Stack Fix (0.19.0 → 0.20.0)</h3><p>The first 30 seconds of that 93-second runtime was the IO bottleneck. Rewriting the IO stack to use fine-grained byte-range reads cut this down dramatically.</p><p>By 0.20.0, roughly one month later:</p><pre>56.82 seconds</pre><p>Better, but still nowhere near 4 seconds. The string search itself was still slow.</p><h3>Stage 2: Recognising LIKE ‘%needle%’ is CONTAINS (0.20.0 → 0.22.0)</h3><p>Here’s where the insight mattered.</p><p>SQL has a LIKE operator that’s technically a regex with wildcards:</p><ul><li>LIKE &#39;needle&#39; = exact match</li><li>LIKE &#39;needle%&#39; = prefix match</li><li>LIKE &#39;%needle&#39; = suffix match</li><li>LIKE &#39;%needle%&#39; = substring match (CONTAINS)</li></ul><p>Our original implementation was treating all LIKE patterns as full regular expressions, delegating to Arrow’s regex engine. This works, but it’s expensive.</p><p>The key observation: <strong>most LIKE queries in practice use the </strong><strong>&#39;%needle%&#39; form</strong>, which is really just a substring search, overkill for a regex.</p><p>We added a new operator, INSTR, and new strategy in the optimizer:</p><pre>if pattern matches &#39;%NEEDLE%&#39;:<br>  return INSTR(column, needle) != -1</pre><p>Instead of a full regex evaluation, we now do a direct substring check. This was a dramatic win:</p><p>0.22.0:</p><pre>17.57 seconds</pre><p>A 3.2x improvement over the previous version. Still not 4 seconds, but progress.</p><h3>Stage 3: Direct Buffer Access &amp; Boyer-Moore-Horspool (0.22.0 → 0.26.2)</h3><p>The next optimisation recognised that we were still going through Arrow’s abstraction layers for INSTR checks.</p><p>We moved to direct buffer access:</p><ul><li>Read the raw Arrow buffer pointers</li><li>Skip Arrow’s helper functions</li><li>Implement string search in compiled code</li></ul><p>This allowed us to use a more sophisticated algorithm: <a href="https://en.wikipedia.org/wiki/Boyer%E2%80%93Moore%E2%80%93Horspool_algorithm"><strong>Boyer-Moore-Horspool</strong></a><strong> (BMH)</strong>, a linear-time substring search that skips characters when mismatches occur.</p><p>We also added a <strong>sieve prefilter</strong>: before running the full search, we check if the needle’s characters appear at all in the haystack. If the needle contains a rare character (say &#39;z&#39;), a quick scan can reject rows cheaply.</p><p>Results, 0.26.2:</p><pre>7.46 seconds</pre><p>Now we’re only 1.8x slower than the C++ engines. We were pleased with this given at it’s heart, Opteryx is mostly written in Python.</p><h3>Stage 4: Volnitsky &amp; Removing Arrow (Current Work)</h3><p>We’re currently deep in an engine refactor:</p><ol><li><strong>Removing Arrow as the internal representation</strong> unlocks faster processing of dictionary-encoded arrays (common in real datasets). Columns like “country” or “status” are often dictionary-encoded; Arrow’s generic abstraction didn’t let us specialise on this.</li><li><strong>Pushing code off the Python interpreter</strong> to reduce GIL contention. String search loops are candidates for this — they’re tight loops over uniform data.</li></ol><p>We’ve adopted the <a href="https://github.com/ox/Volnitsky-ruby"><strong>Volnitsky algorithm</strong></a> which builds on BMH with multi-byte searches:</p><ul><li>Faster in practice on real strings</li><li>Better prefilter (sieve) integration</li><li>Clearer separation between “find first candidate” and “verify match”</li></ul><p>Early results suggest we can hit the 4 second bar set by other engines, and as all things go in circles, 2.8s of our current 3.8s lab benchmark is IO again.</p><h3>The Lesson</h3><p>Optimising a substring search operator looks simple in isolation, but the real wins come from:</p><ol><li><strong>Fixing fundamentals first</strong> (IO stack). A fast algorithm on slow data is still slow.</li><li><strong>Recognising pattern specialisation</strong> (&#39;%needle%&#39; is CONTAINS, not regex).</li><li><strong>Direct buffer access</strong> avoiding abstraction overhead.</li><li><strong>The right algorithm</strong> (BMH and Volnitsky beat naive search by orders of magnitude).</li><li><strong>Preparing for parallelism</strong> (GIL-free code paths).</li></ol><p>This progression from 93 seconds to 4 seconds shows that cumulative, targeted optimisations compound. We’re not done yet.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=220e01bf66ba" width="1" height="1" alt=""><hr><p><a href="https://medium.com/opteryx/making-like-faster-from-93-seconds-to-single-digits-220e01bf66ba">Making LIKE Faster: From 93 Seconds to Single Digits</a> was originally published in <a href="https://medium.com/opteryx">Opteryx Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[What If the Docs Wrote Themselves?]]></title>
            <link>https://medium.com/opteryx/what-if-the-docs-wrote-themselves-55d196c7ae79?source=rss----89f5b4c8c0ad---4</link>
            <guid isPermaLink="false">https://medium.com/p/55d196c7ae79</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[documentation]]></category>
            <dc:creator><![CDATA[Justin Joyce]]></dc:creator>
            <pubDate>Fri, 03 Apr 2026 20:36:13 GMT</pubDate>
            <atom:updated>2026-04-11T21:34:26.015Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*b6pq8fNxMnG9j_XwQSpt2w.png" /></figure><p>This post is also published on the <a href="https://docs.opteryx.app/blog/2026-03-19-auto-docs">Opteryx Engineering Blog</a></p><h3>TL;DR</h3><p>Updating documentation is hard to get right. Code changes quickly, documentation lives somewhere else, and keeping the two aligned usually depends on somebody remembering to update both systems.</p><p>Imagine changing code and having the user documentation update itself from the same source of truth. That means less drift, more consistent coverage, and less time spent maintaining duplicate descriptions.</p><p>We are moving toward that model in <a href="https://opteryx.app/">Opteryx</a> by extracting API metadata from the source repository into JSON, then using the documentation repository to turn that structured data into user-facing documentation. It is still a work in progress, but it is already improving the quality of the metadata in the codebase.</p><p>If you are maintaining technical docs by hand, this is the prompt to start moving them closer to the code.</p><p>This post is also published on the <a href="https://docs.opteryx.app/blog/2026-03-19-auto-docs">Opteryx Engineering Blog</a></p><h3>The Problem</h3><p>Documentation drifts.</p><p>That is especially true for product areas that change frequently, and it is particularly visible in systems like Opteryx, where the user-facing surface includes:</p><ul><li>data types</li><li>functions</li><li>operators</li><li>APIs</li></ul><p>These areas already exist as structured concepts in the engine, but the documentation for them often lives elsewhere, maintained separately, and updated later.</p><p>That creates a familiar set of problems:</p><ul><li>docs and implementation fall out of sync</li><li>small API changes are easy to miss</li><li>coverage becomes uneven</li><li>quality depends on whether someone remembered to update a second system</li></ul><p>None of this is unusual. But it is a signal that the documentation process is too detached from the thing being documented.</p><h3>The Shift in Approach</h3><p>The direction we are moving toward is to document the thing at the thing.</p><p>Instead of treating the documentation site as the primary source of truth, we are treating the source code and its metadata as the starting point.</p><p>The current shape of the pipeline is:</p><pre>source repo<br>  -&gt; extract API metadata<br>  -&gt; generate JSON manifest<br>  -&gt; hand off to docs repo<br>  -&gt; render user documentation</pre><p>The point is not just automation for its own sake.</p><p>The point is that the engine already knows a great deal about its user surface. If we can extract that information in a structured form, we can generate documentation more consistently and with less manual duplication.</p><h3>Why This Matters</h3><p>There are a few immediate benefits to this approach.</p><p>First, it improves consistency.</p><p>If data types, functions, and operators are described through a common metadata structure, the resulting documentation becomes more uniform. The same kinds of information can appear in the same places, in the same format, across the site.</p><p>Second, it improves coverage.</p><p>Manual documentation tends to accumulate around the most visible or most recently changed features. Generated documentation makes it easier to see what is missing because gaps in metadata become explicit.</p><p>Third, it improves hygiene in the codebase.</p><p>Once annotations and inline descriptions are surfaced directly to users, poor metadata becomes much more obvious. That creates healthy pressure to improve the implementation details that describe the API surface.</p><p>This is one of the most useful side effects of the work. The tooling is not just producing docs, it is encouraging higher-quality API definitions.</p><h3>What We Are Generating</h3><p>Today, the focus is on structured user-facing elements such as:</p><ul><li><a href="https://docs.opteryx.app/docs/reference/sql/data-types">types</a></li><li><a href="https://docs.opteryx.app/docs/reference/sql/operators">operators</a></li><li><a href="https://docs.opteryx.app/docs/reference/sql/functions">functions</a></li><li><a href="https://docs.opteryx.app/docs/reference/api/authentication-api">APIs</a></li></ul><p>Many of these are already created programmatically inside Opteryx.</p><p>That is useful because it means we are not starting from an unstructured codebase. There is already a model of these concepts in the system. The work now is to improve the metadata around them so that the model is rich enough to support good documentation.</p><p>In practice, that means making sure each documented element can expose things like:</p><ul><li>name</li><li>signature</li><li>description</li><li>supported argument patterns</li><li>return behavior</li><li>examples</li><li>notes on semantics or edge cases</li></ul><p>Not every part of that is complete yet, and some of the existing definitions have issues. But the important point is that these problems become easier to identify and more straightforward to fix once the documentation pipeline depends on structured metadata.</p><h3>The Trade-Offs</h3><p>There is no real shortcut here.</p><p>Generated documentation only works well if the underlying metadata is good. If the source descriptions are weak, incomplete, or inconsistent, then the generated output will be weak, incomplete, or inconsistent too.</p><p>So the trade-off is clear:</p><ul><li>less manual duplication later</li><li>more discipline required in the source now</li></ul><p>That is a trade worth making.</p><p>It shifts effort away from maintaining duplicate descriptions in separate systems and toward improving the definitions closest to the implementation. Over time that should make both the code and the documentation better.</p><h3>What This Changes Culturally</h3><p>This is not just a tooling change.</p><p>It changes where documentation work happens.</p><p>Instead of thinking of docs as a separate writing task that happens after engineering is done, this pushes us toward treating API metadata as part of the engineering work itself.</p><p>That is a better fit for technical surfaces that evolve quickly. It means changes can be described where they are introduced, reviewed where they are implemented, and surfaced to users through a repeatable pipeline.</p><p>For a project like Opteryx, that is a more scalable model than relying on manual synchronization.</p><h3>What’s Next</h3><p>The immediate next step is to keep improving the extraction pipeline and the metadata behind it.</p><p>The generated JSON is only useful if it captures enough meaning for the docs repository to produce documentation that is genuinely helpful to users.</p><p>That means we still need to refine:</p><ul><li>the completeness of API annotations</li><li>the consistency of field definitions</li><li>the shape of the JSON manifest</li><li>how examples and semantic notes are represented</li></ul><p>This is still work in progress, but the direction already looks right.</p><p>If documenting an API programmatically exposes weak metadata, that is not a failure of the approach.</p><p>That is the approach doing its job.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=55d196c7ae79" width="1" height="1" alt=""><hr><p><a href="https://medium.com/opteryx/what-if-the-docs-wrote-themselves-55d196c7ae79">What If the Docs Wrote Themselves?</a> was originally published in <a href="https://medium.com/opteryx">Opteryx Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Rewriting the Opteryx I/O Stack]]></title>
            <link>https://medium.com/opteryx/rewriting-the-opteryx-i-o-stack-d12f44ad3f04?source=rss----89f5b4c8c0ad---4</link>
            <guid isPermaLink="false">https://medium.com/p/d12f44ad3f04</guid>
            <category><![CDATA[big-data]]></category>
            <category><![CDATA[parquet]]></category>
            <category><![CDATA[sql]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Justin Joyce]]></dc:creator>
            <pubDate>Sat, 28 Mar 2026 11:57:16 GMT</pubDate>
            <atom:updated>2026-04-11T21:33:27.346Z</atom:updated>
            <content:encoded><![CDATA[<p>This post is also published on the <a href="https://docs.opteryx.app/blog/2026-03-12-rewriting-io-stack">Opteryx Engineering Blog</a></p><h3>TL;DR</h3><p>Cold queries over 100 million rows stored as Parquet on object storage originally took <strong>5 minutes</strong> in Opteryx. Profiling showed the execution engine repeatedly stalling while waiting for the IO buffer to refill.</p><p>We rewrote the IO stack to schedule <strong>fine-grained byte-range reads based on Parquet metadata (column chunks within row groups)</strong> and pipelined reads, decompression, and decoding.</p><p>The same cold query now completes in <strong>~10 seconds</strong>.</p><p>The key lesson: <strong>when reading from object storage, throughput alone isn’t enough — the granularity of work matters.</strong></p><h3>The Problem</h3><p>While profiling <a href="https://opteryx.app/">Opteryx</a> we noticed queries stalling even though the execution engine itself appeared efficient.</p><p>The workload was straightforward:</p><ul><li>~100 million rows</li><li>stored as Parquet</li><li>object storage backend</li><li>cold query execution</li></ul><p>A simple query was sufficient to surface the issue:</p><pre>SELECT DISTINCT column<br>FROM dataset;</pre><p>Runtime was approximately five minutes.</p><p>Profiling showed that the execution engine was frequently idle while waiting for the IO buffer to refill. CPU utilization remained low even though the network and reader threads were active.</p><p>The engine wasn’t compute-bound. It was waiting for data.</p><h3>Initial Attempts</h3><p>The first assumption was insufficient read parallelism.</p><p>The number of IO workers was increased:</p><pre>8 → 16 → 32</pre><p>This produced almost no change in query time.</p><p>The next hypothesis was runtime contention. To test this, the entire IO subsystem was moved into a dedicated process, communicating with the execution engine via a shared-memory ring buffer. This completely separated network activity from execution.</p><p>The stalls remained.</p><p>At this point it became clear the system was already close to the available network bandwidth per container. The issue wasn’t CPU scheduling or decode overhead.</p><p>The issue was how data was being delivered to the engine.</p><h3>The Real Issue: Coarse Units of Work</h3><p>The Parquet files in the dataset were roughly:</p><pre>128MB uncompressed<br>~30MB compressed in object storage</pre><p>The IO subsystem was issuing large contiguous reads. Even with many workers the pattern looked roughly like this:</p><pre>issue read<br>wait for blob<br>large chunk arrives<br>engine consumes<br>wait for next read</pre><p>The network was busy, but usable data arrived in bursts.</p><p>The time between issuing a request and receiving data was long enough to starve execution. Increasing worker count did not solve this; it simply queued more large reads.</p><p>The bottleneck was not bandwidth, it was granularity.</p><h3>Rewriting the IO Stack</h3><p>The solution was to redesign the IO subsystem around smaller units of work.</p><p>Parquet files contain detailed structural metadata in the footer:</p><ul><li>row group offsets</li><li>column chunk offsets</li><li>compressed sizes</li><li>exact byte ranges</li></ul><p>Using this information, the new IO stack schedules targeted range reads only for the column chunks required by the query.</p><p>Reads, decompression, and decoding are pipelined so the execution engine begins receiving usable data earlier.</p><p>The unit of work changed from:</p><pre>file</pre><p>to:</p><pre>column chunk within a row group</pre><p>This allows the system to deliver smaller fragments of data continuously rather than waiting for large reads to complete.</p><h3>Results</h3><p>The initial redesign reduced query time significantly:</p><pre>~5 minutes → ~1 minute</pre><p>After further improvements to buffering, file sizes and execution scheduling, the same cold query now completes in approximately:</p><pre>~10 seconds</pre><p>The improvement did not come from increasing available bandwidth. In fact, single-threaded decode performance is slightly slower with the new reader.</p><p>Instead of receiving large bursts of data separated by latency gaps, the execution engine now receives smaller fragments continuously. CPU utilization increases because the pipeline is almost never idle.</p><h3>Reproducing the Pattern</h3><p>This issue commonly appears when:</p><ul><li>data is stored in object storage</li><li>reads operate on large blobs</li><li>execution pipelines are faster than IO latency</li></ul><p>Typical symptoms include:</p><ul><li>low CPU utilization</li><li>active network traffic</li><li>periodic stalls in execution</li></ul><p>In these cases, adding threads or processes rarely helps if the unit of work remains large.</p><h3>Conclusion</h3><p>Object storage behaves very differently from local disks.</p><p>Large sequential reads introduce latency gaps that parallelism alone cannot eliminate. Even with many workers, execution pipelines can stall if each unit of work is too large.</p><p>The practical takeaway is:</p><p>Optimize how finely work can be scheduled, not just how fast it runs.</p><p>For Opteryx this meant redesigning the IO stack around fine-grained byte-range reads derived from Parquet metadata.</p><p>The system was never limited by Python. It was never limited by Parquet.</p><p>It was limited by treating object storage like a disk.</p><h3>What’s Next</h3><p>This IO redesign changes assumptions elsewhere in the engine.</p><p>Several components were originally optimized around file-sized units of work. Moving to fine-grained reads means revisiting parts of the execution pipeline so they can fully benefit from the new architecture.</p><p>We expect additional improvements as more of the engine adapts to this model.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d12f44ad3f04" width="1" height="1" alt=""><hr><p><a href="https://medium.com/opteryx/rewriting-the-opteryx-i-o-stack-d12f44ad3f04">Rewriting the Opteryx I/O Stack</a> was originally published in <a href="https://medium.com/opteryx">Opteryx Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Opteryx Engineering: Fixing Engine Stalls by Rethinking Parquet]]></title>
            <link>https://medium.com/opteryx/opteryx-engineering-fixing-engine-stalls-by-rethinking-parquet-0410b3b291d1?source=rss----89f5b4c8c0ad---4</link>
            <guid isPermaLink="false">https://medium.com/p/0410b3b291d1</guid>
            <category><![CDATA[big-data]]></category>
            <category><![CDATA[sql]]></category>
            <category><![CDATA[optimization]]></category>
            <category><![CDATA[cloud]]></category>
            <dc:creator><![CDATA[Justin Joyce]]></dc:creator>
            <pubDate>Sun, 01 Mar 2026 13:03:50 GMT</pubDate>
            <atom:updated>2026-03-01T13:03:51.919Z</atom:updated>
            <content:encoded><![CDATA[<p>We had hit a problem that didn’t make sense.</p><p><a href="https://github.com/mabel-dev/opteryx-core">Opteryx</a> was stalling.</p><p>Not under extreme load. Not in edge cases. Under perfectly reasonable query conditions. CPU wasn’t saturated. Memory wasn’t constrained. Reader threads were active. And yet the engine kept waiting for data.</p><p>If you build data systems, you’ve probably seen this pattern: nothing appears to be the bottleneck, but something is holding the performance back.</p><p>That’s where we started.</p><h3>The Symptom: An Idle Engine</h3><p>We were running cold queries over just under 100 million rows stored in Parquet on object storage. A simple SELECT DISTINCT over a single column was enough to surface the issue.</p><p>The query took around five minutes.</p><p>Profiling showed something uncomfortable: execution wasn’t the bottleneck. The engine spent significant time waiting for the IO buffer to refill. Workers were issuing requests. The network was active. But the data wasn’t arriving in a way that kept the engine fed.</p><p>The first instinct was obvious: increase read parallelism.</p><p>We moved from 8 IO workers to 16, then 32.</p><p>The stalls remained.</p><h3>The First Attempt: Move IO Out of Process</h3><p>The next hypothesis was runtime contention. Perhaps Python’s threading model was interfering with throughput. If IO and execution were competing for scheduling, then separating them might smooth things out.</p><p>We moved the entire IO subsystem into a dedicated IO process and moved IPC to a shared memory ring buffer. Execution was completely isolated from network handling; if CPU scheduling was the issue, this would fix it.</p><p>It didn’t.</p><p>What we saw was that we were already pushing close to the available network bandwidth per container. We weren’t CPU-bound, we weren’t decode-bound, we were network-bound.</p><p>That forced us to look more carefully at how we were using the network.</p><h3>The Real Problem: Our Unit of Work Was Too Large</h3><p>Each Parquet file was around 128MB uncompressed, roughly 30MB compressed in object storage. Our readers were pulling large contiguous ranges at a time.</p><p>Even with 32 workers, the pattern looked like this:</p><ul><li>Issue blob read</li><li>Wait for data to arrive</li><li>Large chunk arrives</li><li>Engine consumes</li><li>Wait for the next blob to arrive</li></ul><p>The network was busy. But the flow was bursty.</p><p>The time between “request issued” and “usable data available” was long enough to starve execution. More threads didn’t smooth it out; they just queued more large reads and slightly increased thrashing. The engine was still many times faster than the IO could provide it with data.</p><p>We weren’t short on bandwidth; we were short on granularity.</p><h3>The Fortuitous Timing</h3><p>At the same time, we were already working on something else.</p><p>We had started building a lean Parquet decoder, Rugo, originally to move away from some of the constraints of using PyArrow as our runtime dependency. PyArrow is excellent, but it optimises for generality and safety. As consumers, we’re not meant to reach into its internals or control byte-level scheduling.</p><p>That work turned out to be well-timed because once we realised that the problem was granularity, we needed exactly the kind of control that a general-purpose library doesn’t expose.</p><p>With PyArrow, decomposing Parquet into fine-grained, independently schedulable byte ranges would have meant relying on internals we weren’t meant to touch. With Rugo, we could make that a first-class feature of the design.</p><h3>The Shift: Decomposing the Parquet</h3><p>The Parquet footer contains everything needed to reason about the file structurally:</p><ul><li>Row group offsets</li><li>Column chunk offsets</li><li>Compressed sizes</li><li>Exact byte ranges</li></ul><p>Instead of thinking in terms of “read the file” or even “read the row group,” we started thinking in terms of byte ranges.</p><p>We decomposed the file into the smallest functional units, column chunks within row groups, and issued targeted range requests only for the data required by the query. Reads, decompression, and decoding were pipelined so that smaller fragments, which meant the latency between request and process became shorter.</p><p>The unit of work stopped being a file.</p><h3>The Outcome</h3><p>The impact wasn’t marginal.</p><p>That same cold query over just under 100 million rows dropped from roughly five minutes to around one minute.</p><p>We didn’t magically gain more bandwidth, and moving to Rugo actually slightly decreased single-threaded decode speed.</p><p>What changed was continuity. The execution buffer wasn’t empty for large portions of the query time. CPU stayed fed.</p><p>We were still bandwidth-bound, but we were being more targeted and less wasteful about how we were using bandwidth.</p><h3>The Lesson</h3><p>If you’re building on object storage, especially with a push-based or streaming execution model, raw throughput isn’t enough.</p><p>Granularity matters.</p><p>Large, coarse units of work introduce latency gaps that parallelism alone can’t eliminate. You can add threads. You can move subsystems into separate processes. You can increase CPU. But if your work units are too large to schedule effectively, your pipeline will stall.</p><p>The practical takeaway is simple:</p><p>Don’t just optimise how fast you process data — Optimise how finely you can divide it.</p><p>For us, that meant decomposing Parquet blobs into smaller, schedulable units — something that became possible because we had already started building a reader that gave us the necessary control.</p><p>The stall wasn’t caused by Python.</p><p>It wasn’t caused by Parquet.</p><p>It was caused by treating object storage like a disk.</p><p>Once we stopped doing that, the system behaved like it was designed to.</p><h3>What happens next?</h3><p>This is a fundamental change to the IO subsystem in the engine. Whilst we’ve made good progress and shown early gains from the revised architecture, to achieve more benefits from this approach, other parts of the engine, which were designed and optimised for handling file-at-a-time, need revisiting. This change will challenge design assumptions throughout the engine, so we’re anticipating more changes to the engine to improve the flow of data and further reduce query times over the next few weeks.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0410b3b291d1" width="1" height="1" alt=""><hr><p><a href="https://medium.com/opteryx/opteryx-engineering-fixing-engine-stalls-by-rethinking-parquet-0410b3b291d1">Opteryx Engineering: Fixing Engine Stalls by Rethinking Parquet</a> was originally published in <a href="https://medium.com/opteryx">Opteryx Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How We’ve Supercharged Join Performance in Opteryx]]></title>
            <link>https://medium.com/opteryx/how-weve-supercharged-join-performance-in-opteryx-156de92e4678?source=rss----89f5b4c8c0ad---4</link>
            <guid isPermaLink="false">https://medium.com/p/156de92e4678</guid>
            <dc:creator><![CDATA[Justin Joyce]]></dc:creator>
            <pubDate>Tue, 01 Jul 2025 21:39:50 GMT</pubDate>
            <atom:updated>2025-07-01T21:39:02.492Z</atom:updated>
            <content:encoded><![CDATA[<p>Join performance is something databases live and die on — it needs to be fast. Join performance is not a nice-to-have; it’s a must-have.</p><p>In the early days of <a href="https://opteryx.dev">Opteryx</a>, we were getting feedback about not having joins implemented. We came across <a href="https://github.com/TomScheffers/pyarrow_ops">https://github.com/TomScheffers/pyarrow_ops</a>, which at the time had features that weren’t implemented into PyArrow yet, and we incorporated this into <a href="https://opteryx.dev">Opteryx</a>. The joins worked, but like all brute force solutions, it didn’t scale gracefully. When PyArrow introduced native join support, we quickly adopted it. For a time, it gave us a speed boost and allowed us to move faster elsewhere. But like many general-purpose tools, it came with limitations — it has some limitations which our users weren’t happy with.</p><p>So, once we reached speed parity with PyArrow in very specific scenarios, we dropped it — we stopped using PyArrow’s join implementation, first for INNER JOIN, and quickly after the OUTER JOIN and eventually FILTER JOINs too. Most of the time, we were a little slower than PyArrow, but not by orders of magnitude, and it gave us full control to innovate.</p><h3>Introducing Bloom Filters (Proof of Concept)</h3><p>A few releases ago, we experimented with adding a bloom filter to the join pipeline — initially, a targeted proof of concept:</p><ul><li>It only activated on single-column VARCHAR joins</li><li>It only supported up to 1 million rows on the build side</li></ul><p>Despite its limitations, the experiment let us explore key questions:</p><ul><li>Should we speculatively build a filter for every compatible join?</li><li>What filter ratio threshold makes bloom filtering not worth the effort?</li><li>Could we build a non-general-purpose bloom filter that was <em>really</em> fast?</li></ul><p>This initial foray was promising. But the real gains came when we linked improvements across related parts of the engine.</p><h3>The Unexpected Speed Bottleneck in DISTINCT</h3><p>While optimising DISTINCT, we stumbled upon something eye-opening.</p><p>Our Cython implementation of DISTINCT (which was already faster than our original pure-Python version that we also derived from <em>pyarrow_ops</em>) converted PyArrow columns to NumPy arrays, hashed them in Python, and combined those hashes. Reasonable, right?</p><p>Except we found that hashing a 100 million row dataset spent <strong>7 seconds</strong> just converting Arrow to NumPy — <em>before</em> doing anything meaningful.</p><p>That was unacceptable.</p><p>So, we rethought the internals. We updated DISTINCT to access PyArrow’s memory buffers directly. In most cases, we didn’t even need to interpret the values — we just treated them as raw bytes and passed them straight into our hash function. No decoding. No conversions. Minimal allocations.</p><p>The result? DISTINCT ran up to <strong>15x faster</strong> in lab conditions.</p><h3>A Shared Foundation: JOIN and DISTINCT</h3><p>JOIN and DISTINCT may serve different purposes, but at their core, they both rely on building and using collections of hashes. So we applied the same philosophy to our joins.</p><p>We:</p><ul><li>Rewrote the Bloom filter to use the same method to hash values</li><li>Rebuilt the hash map generation on the build side</li><li>Reimplemented the probe logic, which had now become the bottleneck</li></ul><p>The improvements were immediate and substantial.</p><p>In contrived benchmark cases (e.g. 10 million vs 10 million integer INNER JOIN with no matches), we saw a <strong>10x speedup</strong>. In more representative real-world workloads, the improvements averaged around <strong>40% faster</strong> join performance.</p><h3>What’s Next?</h3><p>This isn’t the end — it’s the start of a new performance frontier. We’re exploring:</p><ul><li>SIMD-acceleration</li><li>More Join type–specific tuning (e.g. LEFT OUTER vs INNER)</li></ul><p>Opteryx is designed for speed. With this new join pipeline, we’re doubling down on that promise.</p><p>Thank you for your continued support and feedback. As always, we encourage you to reach out with any insights or issues you encounter — we’re here to listen and improve together.</p><p>You can check out <a href="https://opteryx.dev/latest/">Opteryx</a> for yourself, drop into <a href="https://github.com/mabel-dev/opteryx">GitHub and give us a star</a>!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=156de92e4678" width="1" height="1" alt=""><hr><p><a href="https://medium.com/opteryx/how-weve-supercharged-join-performance-in-opteryx-156de92e4678">How We’ve Supercharged Join Performance in Opteryx</a> was originally published in <a href="https://medium.com/opteryx">Opteryx Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Strengthening Opteryx: A Deeper Look at Our Evolving Testing Practices]]></title>
            <link>https://medium.com/opteryx/strengthening-opteryx-a-deeper-look-at-our-evolving-testing-practices-8a1e6ad1bbe2?source=rss----89f5b4c8c0ad---4</link>
            <guid isPermaLink="false">https://medium.com/p/8a1e6ad1bbe2</guid>
            <category><![CDATA[testing]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[quality-assurance]]></category>
            <category><![CDATA[data-analysis]]></category>
            <category><![CDATA[sql]]></category>
            <dc:creator><![CDATA[Justin Joyce]]></dc:creator>
            <pubDate>Sat, 31 Aug 2024 13:23:44 GMT</pubDate>
            <atom:updated>2024-08-31T13:23:04.661Z</atom:updated>
            <content:encoded><![CDATA[<p>At <a href="https://opteryx.dev/latest/">Opteryx</a>, quality and reliability have always been at the forefront of our development process. Since our last major release, we’ve be taking significant steps to reinforce our testing suite. The number of CI tests have risen from 2987 to 3931 at last count (about a 33% increase), however, increase in test cases has brought with it some interesting challenges and insights, particularly regarding our coverage statistics and the quality of our regression testing.</p><p>A quick summary of our <a href="https://opteryx.dev/latest/contributing/topics/testing/">testing approach can be found here</a>. As a generalization, we’ve preferred end-to-end testing (do SQL statements create the correct result) over unit tests.</p><h4>The Testing Paradox: More Tests, Same Coverage</h4><p>One of the most intriguing aspects of our recent testing efforts is that despite adding nearly 1000 new tests, our code coverage statistics have increased 1%, and from an initial analysis this change is more likely to be from removal of code than an increase in the tests.</p><p>The answer lies in the nature of the new tests we’ve added. Most of these tests are focused on variation and combination testing rather than new functionality. What we were finding were edge cases, where a subtle variation in the query resulted in an error.</p><h4>Addressing New Bug Reports</h4><p>One of the key drivers for reviewing our test approach was due to users reporting issues we thought should have been caught during regression testing.</p><p>To address this, we’ve taken a two-pronged approach:</p><p>1.<strong> Improving Test Variety</strong>: We’re focusing on expanding our tests to cover more variations of scenarios. This includes adding more tests that challenge the assumption that if if one variation works, all similar ones will work. For example, just because 12.0*12 and 12+12 work, doesn’t mean 12.0+12 will work.<br> <br>2. <strong>Introducing the Join Fuzzer</strong>: Recognizing the importance of joins in SQL operations, we’ve developed a new tool, the Join Fuzzer. This tool systematically generates and tests a wide range of join scenarios, helping us identify and fix bugs that might slip through more conventional tests.</p><p>What we found was that users were uncovering niche bugs in features which were tested, but usually only in one or two test cases rather than sweating the test suite with 10s of variations to root out edge case bugs.</p><h4>The Path Forward: Increasing Test Depth and Breadth</h4><p>While our existing tests provide a solid foundation, we know there’s more work to be done. Our focus moving forward is to broaden our test coverage in ways that truly matter for our users.</p><h4>Conclusion</h4><p>Our recent efforts have reinforced our commitment to delivering a reliable and robust SQL Query Engine. While the increase in test cases has highlighted some interesting challenges, it has also provided us with invaluable insights that will guide our future development.</p><p>The introduction of the Join Fuzzer and our focus on expanding test coverage in more complex areas are just the beginning. We are dedicated to ensuring that Opteryx not only meets but exceeds the expectations of our growing user base.</p><p>Thank you for your continued support and feedback. As always, we encourage you to reach out with any insights or issues you encounter — we’re here to listen and improve together.</p><p>You can check out <a href="https://opteryx.dev/latest/">Opteryx</a> for yourself, drop into <a href="https://github.com/mabel-dev/opteryx">GitHub and give us a star</a>!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8a1e6ad1bbe2" width="1" height="1" alt=""><hr><p><a href="https://medium.com/opteryx/strengthening-opteryx-a-deeper-look-at-our-evolving-testing-practices-8a1e6ad1bbe2">Strengthening Opteryx: A Deeper Look at Our Evolving Testing Practices</a> was originally published in <a href="https://medium.com/opteryx">Opteryx Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Opteryx 0.15 Release]]></title>
            <link>https://medium.com/opteryx/opteryx-0-15-release-29b1ca5e18b9?source=rss----89f5b4c8c0ad---4</link>
            <guid isPermaLink="false">https://medium.com/p/29b1ca5e18b9</guid>
            <category><![CDATA[sql]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[data-analysis]]></category>
            <category><![CDATA[data-engineering]]></category>
            <category><![CDATA[big-data]]></category>
            <dc:creator><![CDATA[Justin Joyce]]></dc:creator>
            <pubDate>Mon, 27 May 2024 16:19:53 GMT</pubDate>
            <atom:updated>2024-05-27T16:18:42.946Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*NjWOCc8gxCaj35Q6" /></figure><p>We are pleased to announce the release of <a href="https://opteryx.dev/latest/">Opteryx</a> 0.15, which focuses on significant performance improvements, new experimental features, and important reliability and usability enhancements.</p><h3>Performance Improvements</h3><p>In typical query execution, I/O operations, particularly those involving cloud storage, can account for up to 90% of the total execution time. Opteryx 0.15 addresses this by introducing parallelized reading from Google Cloud Storage, which significantly reduces data retrieval times. Combined with caching improvements, this optimization can result in up to a 500% improvement in performance for specific workloads, enhancing overall query execution speed.</p><p>Query execution improvements through additional optimization techniques and algorithmic improvements. Filters are pushed down into sub-queries and the UNNEST operation, reducing data volumes, and a new Heap Sort fused operator combines LIMIT and ORDER BY into a single efficient process.</p><h3>Experimental Features</h3><p>Full-Text Search Enhancements: Opteryx 0.15 introduces experimental support for the MATCH AGAINST feature, enabling advanced full-text search capabilities. This capability is likely to change in the future so should not be used in production systems.</p><p>Initial SQL VIEW Support: This version includes initial support for SQL VIEWs, allowing users to create virtual tables based on query results. This feature is experimental and will be further developed in future releases.</p><h3>Reliability and Usability Improvements</h3><p>Insights from the ClickBench benchmark have driven several improvements in this release.</p><h3>New Permissions Capabilities</h3><p>New capabilities have been introduced to help users build more intelligent permissions overlays, allowing for more granular and secure data access controls. Users can now implement custom permissions models that ensure data security and compliance with access policies.</p><h3>Conclusion</h3><p>Opteryx 0.15 delivers substantial performance improvements, new experimental features, and enhanced reliability and usability. We encourage users to upgrade to this latest version to benefit from these advancements.</p><p>We value your feedback and look forward to your continued support and contributions as we work to further improve Opteryx. Join us on <a href="https://github.com/mabel-dev/opteryx">GitHub</a> or drop in to give us a star.</p><p>Thank you for being a part of the Opteryx community.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=29b1ca5e18b9" width="1" height="1" alt=""><hr><p><a href="https://medium.com/opteryx/opteryx-0-15-release-29b1ca5e18b9">Opteryx 0.15 Release</a> was originally published in <a href="https://medium.com/opteryx">Opteryx Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>