<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vladislav Koval</title>
    <description>The latest articles on DEV Community by Vladislav Koval (@vladislav_koval_f4bed0e76).</description>
    <link>https://dev.to/vladislav_koval_f4bed0e76</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3581563%2Fc82a19e0-9383-4d19-b14f-73e0301b10e4.jpg</url>
      <title>DEV Community: Vladislav Koval</title>
      <link>https://dev.to/vladislav_koval_f4bed0e76</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vladislav_koval_f4bed0e76"/>
    <language>en</language>
    <item>
      <title>I Built a Data Quality Library Because Existing Tools Were Too Slow for My Pipelines</title>
      <dc:creator>Vladislav Koval</dc:creator>
      <pubDate>Fri, 11 Sep 2026 07:37:02 +0000</pubDate>
      <link>https://dev.to/vladislav_koval_f4bed0e76/i-built-a-data-quality-library-because-existing-tools-were-too-slow-for-my-pipelines-3i4l</link>
      <guid>https://dev.to/vladislav_koval_f4bed0e76/i-built-a-data-quality-library-because-existing-tools-were-too-slow-for-my-pipelines-3i4l</guid>
      <description>&lt;h1&gt;
  
  
  I got mass fed up with data quality tools, so I built my own
&lt;/h1&gt;

&lt;p&gt;I work as a data engineer. BigQuery, Athena, Polars, the usual stack. Every project I've been on had the same problem: how do you check that your data isn't garbage before it hits the dashboard?&lt;/p&gt;

&lt;p&gt;I tried everything. GX took 90 seconds on 41 million rows. Soda pulled 30+ dependencies and wanted me to pay for basic things like seeing if my null rate went up since yesterday. Pandera was decent for DataFrames but couldn't talk to a warehouse.&lt;/p&gt;

&lt;p&gt;None of them gave me what I actually wanted: run checks, show me what failed, tell me how many rows, give me samples. Fast. On any source.&lt;/p&gt;

&lt;p&gt;So I built Eliza DQ.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually does
&lt;/h2&gt;

&lt;p&gt;You point it at something (a BigQuery table, a parquet file, a DataFrame, whatever) and tell it what to check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;eliza&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data.parquet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not_null&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unique&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not_negative&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="c1"&gt;# "2 passed, 2 failed (1M rows, 12ms)"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For warehouses you write a YAML config and run from CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;eliza-dq[bigquery]
eliza check &lt;span class="nt"&gt;--config&lt;/span&gt; orders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get back fail counts, fail rates, sample failing rows, PDF report if you want one, Slack alert if you set it up. Not just "pass/fail". Not just an exception with no context.&lt;/p&gt;

&lt;p&gt;2 dependencies total. polars + pyyaml. That's the whole thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing that actually made me build this
&lt;/h2&gt;

&lt;p&gt;It was sample queries. When a check fails, you want to see which rows are broken. Makes sense. Soda does this by running &lt;code&gt;SELECT *&lt;/code&gt; for every failing check. On a 48-column BigQuery table, that means scanning all 48 columns just to show you one column that failed.&lt;/p&gt;

&lt;p&gt;Here's the thing about BigQuery pricing that a lot of people don't realize: &lt;code&gt;LIMIT&lt;/code&gt; doesn't reduce cost. The price is determined by which columns are in your &lt;code&gt;SELECT&lt;/code&gt;, not how many rows come back. So &lt;code&gt;SELECT * FROM table WHERE col IS NULL LIMIT 100&lt;/code&gt; costs the same as scanning the entire table.&lt;/p&gt;

&lt;p&gt;Eliza runs &lt;code&gt;SELECT id, failing_column&lt;/code&gt; instead. 2 columns, not 48. That's 24x less data per sample query. And it runs them in parallel instead of one by one.&lt;/p&gt;

&lt;p&gt;Real numbers on a 5-billion-row table, 25 checks, 21 of them failing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Eliza&lt;/th&gt;
&lt;th&gt;Soda Core&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;707ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;13.7s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Columns per sample&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you run 10 failing checks daily on a 1TB table, that's $3,200/year vs $25,000 in BQ costs. Just from being smarter about which columns you scan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmarks
&lt;/h2&gt;

&lt;p&gt;I spent a lot of time on these because I got burned early. First version of the README had inflated numbers from BQ query cache. A community tester called me out on it, and he was right. So now everything is measured properly: same checks, same data, warmup discarded, min of 3 runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DataFrames (NYC Yellow Taxi, all in memory):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rows&lt;/th&gt;
&lt;th&gt;Eliza&lt;/th&gt;
&lt;th&gt;Pandera&lt;/th&gt;
&lt;th&gt;GX&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3M&lt;/td&gt;
&lt;td&gt;2ms&lt;/td&gt;
&lt;td&gt;21ms&lt;/td&gt;
&lt;td&gt;1.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;41M&lt;/td&gt;
&lt;td&gt;14ms&lt;/td&gt;
&lt;td&gt;257ms&lt;/td&gt;
&lt;td&gt;92s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;109M&lt;/td&gt;
&lt;td&gt;35ms&lt;/td&gt;
&lt;td&gt;711ms&lt;/td&gt;
&lt;td&gt;60s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most of the speed comes from Polars, not from me being clever. I group checks by column so each column is scanned once, and Polars streaming does the rest. Credit where it's due.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streaming from files:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;259 million rows across 72 parquet files: 1.3 seconds, under 1GB RAM. Other tools need to load everything into memory first and OOM on an 8GB machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQL pushdown (you can reproduce this yourself):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;eliza-dq soda-core-duckdb
python benchmarks/sql_local.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rows&lt;/th&gt;
&lt;th&gt;Eliza&lt;/th&gt;
&lt;th&gt;Soda&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3M&lt;/td&gt;
&lt;td&gt;15ms&lt;/td&gt;
&lt;td&gt;31ms&lt;/td&gt;
&lt;td&gt;2.1x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;41M&lt;/td&gt;
&lt;td&gt;179ms&lt;/td&gt;
&lt;td&gt;318ms&lt;/td&gt;
&lt;td&gt;1.8x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What Soda doesn't give you for free
&lt;/h2&gt;

&lt;p&gt;Soda Core (the open source version) has zero persistence. Run finishes, results are gone. Want to see if your null rate went up? Want anomaly detection? Want any kind of history? Pay for Soda Cloud.&lt;/p&gt;

&lt;p&gt;I get why they do it. But for a small team or a solo DE, paying $10K+ a year for "your data got worse since last Tuesday" is a lot.&lt;/p&gt;

&lt;p&gt;I'm building run history into Eliza with SQLite by default (zero config) and optional write-to-your-DWH. Anomaly detection, trends, the whole thing. Free, open source, your data stays with you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;eliza-dq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: github.com/Se7enquick/eliza-dq&lt;/p&gt;

&lt;p&gt;17 checks, 8 warehouse connectors, PDF reports, Slack alerts. MIT license. I use it in production myself.&lt;/p&gt;

&lt;p&gt;If something breaks, open an issue. I read all of them.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>python</category>
      <category>dataquality</category>
      <category>bigquery</category>
    </item>
  </channel>
</rss>
