<?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: Mohammed Arshad Ansari</title>
    <description>The latest articles on DEV Community by Mohammed Arshad Ansari (@mohammed_arshadansari_f2).</description>
    <link>https://dev.to/mohammed_arshadansari_f2</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%2F3609964%2Fd0a642d8-0372-4976-99f8-0575aa92a93c.png</url>
      <title>DEV Community: Mohammed Arshad Ansari</title>
      <link>https://dev.to/mohammed_arshadansari_f2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mohammed_arshadansari_f2"/>
    <language>en</language>
    <item>
      <title>Querying Parquet on S3 with DuckDB: secrets, speed and the settings that matter</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Mon, 26 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/querying-parquet-on-s3-with-duckdb-secrets-speed-and-the-settings-that-matter-3gij</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/querying-parquet-on-s3-with-duckdb-secrets-speed-and-the-settings-that-matter-3gij</guid>
      <description>&lt;p&gt;DuckDB reads Parquet on S3 in one line, and that part is not the problem. The problem shows up a month later, when a query that took two seconds on your laptop takes forty against the bucket and nobody can say why.&lt;/p&gt;

&lt;p&gt;The answer is almost never bandwidth. &lt;strong&gt;It is the number of requests.&lt;/strong&gt; Object storage charges you a round trip for every file you open and every column chunk you fetch. A layout that costs nothing on local disk can mean tens of thousands of round trips on S3. This post covers credentials first, because they are what people search for, and then the layout and settings that decide speed.&lt;/p&gt;

&lt;p&gt;One honest note. My own production pipeline reads Parquet from an NFS share on a NAS, not from S3. The credential and request-level detail here comes from DuckDB's documentation and from my book. The many-files failure near the end is mine, from that NFS pipeline, and it would have hit the same way on S3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can DuckDB read from S3?
&lt;/h2&gt;

&lt;p&gt;Yes. The &lt;code&gt;httpfs&lt;/code&gt; extension handles S3, and DuckDB &lt;a href="https://duckdb.org/docs/current/core_extensions/httpfs/overview.html" rel="noopener noreferrer"&gt;autoloads it&lt;/a&gt; the first time you touch an &lt;code&gt;s3://&lt;/code&gt; path. You don't need to install or load anything by hand, though doing so in production scripts makes the dependency explicit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'s3://analytics/events/dt=2026-09-*/*.parquet'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Parquet, DuckDB does not download whole files. Its docs describe &lt;a href="https://duckdb.org/docs/current/core_extensions/httpfs/https.html" rel="noopener noreferrer"&gt;partial reading&lt;/a&gt;: it reads the file's metadata, then uses HTTP range requests to fetch only the parts the query needs. A &lt;code&gt;count(*)&lt;/code&gt; can be answered from metadata alone. That is why Parquet on S3 works at all, and also why the number of files matters so much: every file needs its own metadata read before anything else happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting S3 credentials: CREATE SECRET
&lt;/h2&gt;

&lt;p&gt;DuckDB manages credentials as &lt;strong&gt;secrets&lt;/strong&gt;. The &lt;a href="https://duckdb.org/docs/current/core_extensions/httpfs/s3api.html" rel="noopener noreferrer"&gt;S3 docs&lt;/a&gt; show two providers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;credential_chain&lt;/code&gt;&lt;/strong&gt; asks the AWS SDK for credentials, the same way the AWS CLI would. That covers environment variables, profiles, SSO, assumed roles and instance or container roles. Use it on servers, so no key ever sits in your code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="n"&gt;SECRET&lt;/span&gt; &lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;PROVIDER&lt;/span&gt; &lt;span class="n"&gt;credential_chain&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;config&lt;/code&gt;&lt;/strong&gt; takes the values directly. Use it when the keys come from somewhere else, such as a secrets manager your app already reads. Pass them in from code rather than typing them into SQL:&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;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;

&lt;span class="n"&gt;con&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CREATE OR REPLACE SECRET aws &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(TYPE s3, PROVIDER config, KEY_ID ?, SECRET ?, REGION &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;eu-west-2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&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="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS_SECRET_ACCESS_KEY&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details that save time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scope secrets to buckets.&lt;/strong&gt; Add &lt;code&gt;SCOPE 's3://raw-bucket'&lt;/code&gt; and the secret only applies there. With several matching secrets, the &lt;a href="https://duckdb.org/docs/current/configuration/secrets_manager.html" rel="noopener noreferrer"&gt;secrets manager&lt;/a&gt; picks the longest matching prefix. &lt;code&gt;FROM which_secret('s3://raw-bucket/x.parquet', 's3')&lt;/code&gt; tells you which one a path will use, and &lt;code&gt;FROM duckdb_secrets()&lt;/code&gt; lists them with sensitive values redacted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent secrets are plain files.&lt;/strong&gt; Secrets live in memory for the session by default. &lt;code&gt;CREATE PERSISTENT SECRET&lt;/code&gt; writes them to &lt;code&gt;~/.duckdb/stored_secrets&lt;/code&gt;, and the docs say they are "stored in unencrypted binary format on the disk." On a shared server, use &lt;code&gt;credential_chain&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3-compatible stores need three more fields.&lt;/strong&gt; For MinIO and similar, set &lt;code&gt;ENDPOINT&lt;/code&gt;, &lt;code&gt;URL_STYLE 'path'&lt;/code&gt; and, for a local HTTP endpoint, &lt;code&gt;USE_SSL false&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="n"&gt;SECRET&lt;/span&gt; &lt;span class="n"&gt;minio&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;PROVIDER&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;KEY_ID&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SECRET&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ENDPOINT&lt;/span&gt; &lt;span class="s1"&gt;'minio:9000'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;URL_STYLE&lt;/span&gt; &lt;span class="s1"&gt;'path'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;USE_SSL&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SCOPE&lt;/span&gt; &lt;span class="s1"&gt;'s3://analytics'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have older code that uses &lt;code&gt;SET s3_access_key_id = ...&lt;/code&gt;, it still runs. Chapter 9 of &lt;em&gt;Local-First Analytics&lt;/em&gt; uses that style for its MinIO example. DuckDB's docs now call it &lt;a href="https://duckdb.org/docs/current/core_extensions/httpfs/s3api_legacy_authentication.html" rel="noopener noreferrer"&gt;a legacy method&lt;/a&gt; and say to avoid it, because settings are easy to print by accident. Move to secrets when you next touch that code.&lt;/p&gt;




&lt;p&gt;This is the first part. The full post — including the rest of the working details — is on my site: &lt;a href="https://hikmahtechnologies.com/blog/duckdb-s3-parquet/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;Querying Parquet on S3 with DuckDB: secrets, speed and the settings that matter&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>database</category>
      <category>sql</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Dagster and dbt: do you need both?</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Fri, 23 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/dagster-and-dbt-do-you-need-both-27l7</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/dagster-and-dbt-do-you-need-both-27l7</guid>
      <description>&lt;p&gt;"Why use dbt if I have Dagster?" is the number-two Google result for this topic, and it's a Reddit thread. That tells you the question is real. People see two tools that both draw a lineage graph, both run on a schedule, and both run tests, and they wonder if they are paying twice.&lt;/p&gt;

&lt;p&gt;They aren't, quite. dbt is a &lt;strong&gt;SQL transformation framework&lt;/strong&gt;. Dagster is an &lt;strong&gt;orchestrator&lt;/strong&gt;. They overlap in the middle, and the overlap is where the confusion lives. My own production pipeline runs on Dagster with no dbt at all, so I'll also say when skipping dbt is the right call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each tool actually does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;dbt&lt;/strong&gt; takes a folder of SELECT statements with Jinja templating. It works out their order from &lt;code&gt;ref()&lt;/code&gt; calls, compiles them for your warehouse, runs them as tables or views, and runs tests (&lt;code&gt;not_null&lt;/code&gt;, &lt;code&gt;unique&lt;/code&gt;, custom SQL) against the results. It does not schedule itself. Something has to call &lt;code&gt;dbt build&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dagster&lt;/strong&gt; decides when things run, records what each run produced, retries failures, tracks partitions, and runs arbitrary Python. It has its own data-quality checks (asset checks) and its own lineage graph, built from the dependencies you declare.&lt;/p&gt;

&lt;p&gt;The overlap: both have a lineage graph and both have tests. The difference: dbt's graph covers SQL models in one warehouse. Dagster's graph covers everything, including the Python that loads data before dbt runs and the models you train after it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why use dbt if you have Dagster
&lt;/h2&gt;

&lt;p&gt;You still want dbt when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Most of your transformations are SQL in one warehouse.&lt;/strong&gt; dbt's templating, &lt;code&gt;ref()&lt;/code&gt;, incremental materializations and the docs site are a decade of work you would otherwise rebuild badly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analysts write the models.&lt;/strong&gt; dbt is the tool analysts already know. Asking them to write Dagster assets in Python is a bigger change than it sounds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want the project to be portable.&lt;/strong&gt; A dbt project runs under Dagster, Airflow, Prefect, cron or dbt Cloud. Your SQL is not locked to the orchestrator.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can skip dbt when the transformations are mostly Python, or when one engineer writes everything and is happy to keep the SQL inside Python assets. That is my situation, below.&lt;/p&gt;




&lt;p&gt;This is the first part. The full post — including the rest of the working details — is on my site: &lt;a href="https://hikmahtechnologies.com/blog/dagster-with-dbt/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;Dagster and dbt: do you need both?&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>python</category>
      <category>sql</category>
      <category>data</category>
    </item>
    <item>
      <title>AEGIS v1 is archived. Here is what replaces it</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Thu, 22 Oct 2026 04:30:51 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/aegis-v1-is-archived-here-is-what-replaces-it-580j</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/aegis-v1-is-archived-here-is-what-replaces-it-580j</guid>
      <description>&lt;p&gt;I've shut down AEGIS v1 and archived &lt;a href="https://github.com/hikmahtech/aegis" rel="noopener noreferrer"&gt;github.com/hikmahtech/aegis&lt;/a&gt;. The code stays public and MIT-licensed. You can still read it, fork it and learn from it. It just won't get fixes, releases or answers to issues any more.&lt;/p&gt;

&lt;p&gt;AEGIS itself isn't going away. It is being rebuilt as version 2, and version 2 is not open source. This post explains why, what v2 looks like, and what is already running on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What v1 was
&lt;/h2&gt;

&lt;p&gt;AEGIS v1 was one system that did everything for one person: my tasks, email, money, research, and the alerts from the machines in my house. Three services shared one Postgres database. A Temporal worker ran every workflow on one task queue. A handful of named agents each owned a slice of the work, and every time one needed me, it asked through the same approval card.&lt;/p&gt;

&lt;p&gt;I &lt;a href="https://hikmahtechnologies.com/blog/aegis-is-open-source/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;open-sourced it in July&lt;/a&gt;, and the ideas in it held up. One primitive for every interruption. Durable workflows that survive a weekend of waiting for my tap. Local models first. An audit line for everything that matters. None of that is changing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong with one system
&lt;/h2&gt;

&lt;p&gt;The shape was the problem, not the ideas.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One deploy for everything.&lt;/strong&gt; A fix to bank-statement parsing redeployed the code that restarts services in my homelab. Every change carried the risk of every other area.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One database for everything.&lt;/strong&gt; The accounting tables and the alert tables lived side by side. Nothing stopped one area from reading or writing another's rows, so over time some did.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing could leave.&lt;/strong&gt; If someone wanted just the bookkeeping part, there was no way to give it to them. It came with my task manager, my research feeds and my infrastructure agent attached.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last one matters most. The bookkeeping lane is useful to people who aren't me. To hand it to anyone, it has to stand on its own.&lt;/p&gt;




&lt;p&gt;This is the first part. The full post, including the rest of the working details, is on my site: &lt;a href="https://hikmahtechnologies.com/blog/aegis-v1-is-archived/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;AEGIS v1 is archived. Here is what replaces it&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
    <item>
      <title>DuckDB vs Spark: which workloads actually need a cluster</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Wed, 21 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/duckdb-vs-spark-which-workloads-actually-need-a-cluster-4gbe</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/duckdb-vs-spark-which-workloads-actually-need-a-cluster-4gbe</guid>
      <description>&lt;p&gt;Most "DuckDB vs Spark" questions are really a sizing question. Spark is a distributed engine: it splits a job across many machines and pays to coordinate them. DuckDB is an in-process engine: it runs the whole job on one machine and pays nothing to coordinate. Which one is faster depends almost entirely on whether one machine can hold the work.&lt;/p&gt;

&lt;p&gt;So the useful question is not which engine is better. It is: &lt;strong&gt;how many bytes does your job actually touch, and does that fit on one box?&lt;/strong&gt; Here is the arithmetic, where a cluster earns its cost, and what one node has handled for me. I have not run Spark in production; this is the single-node side of the line, measured, plus what Spark's own documentation says about the other side.&lt;/p&gt;

&lt;h2&gt;
  
  
  DuckDB vs Spark: what each one is built for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Spark&lt;/strong&gt; spreads work over a cluster. A driver plans the job, executors on many machines each process part of the data, and a shuffle moves rows between machines when a join or group-by needs them together. If a machine dies, Spark rebuilds the lost piece. Its documentation puts it plainly: if any partition of a cached dataset is lost, "it will automatically be recomputed using the transformations that originally created it." That is what you rent a cluster for: data larger than any one machine, and jobs that keep going when one machine fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DuckDB&lt;/strong&gt; is a columnar SQL engine that runs inside your Python process, a CLI or a service. No driver, no executors, no shuffle over a network. It reads Parquet and CSV in place, uses every core, and spills to disk when a join or sort outgrows memory. The &lt;a href="https://duckdb.org/docs/current/guides/performance/how_to_tune_workloads.html" rel="noopener noreferrer"&gt;tuning guide&lt;/a&gt; lists GROUP BY, JOIN, ORDER BY and window functions as operators that can work larger than memory.&lt;/p&gt;

&lt;p&gt;Both read the same Parquet. That matters more than any benchmark: the choice is reversible, because the files don't care which engine opens them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sizing math that decides it
&lt;/h2&gt;

&lt;p&gt;The number that matters is the &lt;strong&gt;working set&lt;/strong&gt;: the bytes a job has to process after partition pruning and column selection. It is not the size of the lake.&lt;/p&gt;

&lt;p&gt;Three steps get you there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Start from what the job reads, not what exists.&lt;/strong&gt; A 20 TB events table, partitioned by day, queried for the last 30 days, touching 6 of 60 columns, reads roughly 30/365 × 6/60 of the table. That is about 160 GB on disk before any filter on row values. Parquet compresses, so memory use runs higher than disk size. But most of that work streams through and never has to sit in memory at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Budget memory by operation.&lt;/strong&gt; DuckDB's &lt;a href="https://duckdb.org/docs/current/guides/performance/environment.html" rel="noopener noreferrer"&gt;environment guide&lt;/a&gt; gives the rule of thumb: about &lt;strong&gt;1–2 GB of memory per thread for aggregation-heavy work and 3–4 GB per thread for join-heavy work&lt;/strong&gt;. So a 32-thread job needs roughly 32–64 GB for aggregations, or 96–128 GB for joins. That is one ordinary server, not a cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Compare with the biggest box you can rent.&lt;/strong&gt; Single machines are large now. AWS lists an &lt;a href="https://aws.amazon.com/ec2/instance-types/x2i/" rel="noopener noreferrer"&gt;x2iedn.metal&lt;/a&gt; with 128 vCPUs and 4,096 GiB of memory. You don't need that. The point is that the ceiling of "one machine" sits far above where most teams assume it is.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Local-First Analytics&lt;/em&gt; draws the same line. Chapter 5 suggests DuckDB for up to about 1 TB of partitioned Parquet for aggregation-heavy work, and less for join-heavy work. It also warns that many-to-many joins blow up intermediate state whatever the input size. Its rule of thumb for starting local-first is a working set that fits in 32 GB of RAM and fewer than ten people running queries. Both bars are much lower than where a single node actually gives out.&lt;/p&gt;

&lt;p&gt;If your working set after this arithmetic is in the gigabytes or low hundreds of gigabytes, you are running Spark for the cluster's sake, not the data's.&lt;/p&gt;




&lt;p&gt;This is the first part. The full post — including the rest of the working details — is on my site: &lt;a href="https://hikmahtechnologies.com/blog/duckdb-vs-spark/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;DuckDB vs Spark: which workloads actually need a cluster&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>database</category>
      <category>python</category>
      <category>data</category>
    </item>
    <item>
      <title>Dagster vs Airflow: which to choose for a small data team</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Mon, 19 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/dagster-vs-airflow-which-to-choose-for-a-small-data-team-3g18</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/dagster-vs-airflow-which-to-choose-for-a-small-data-team-3g18</guid>
      <description>&lt;p&gt;"Dagster vs Airflow" sounds like a feature contest. It mostly isn't. Both schedule Python, both retry failures, both have a UI, both are free to self-host. The real difference is what each one thinks it is scheduling. Airflow schedules &lt;strong&gt;tasks&lt;/strong&gt;. Dagster schedules the &lt;strong&gt;things those tasks produce&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For a big team with an Airflow platform already running, that difference rarely justifies a migration. For a small data team starting from nothing, it decides how much you can hold in your head. I run a production Dagster pipeline alone: 112 assets, 93 asset checks, 57 schedules. This post is about why that model fit, what still hurt, and when Airflow is the better answer. I have not run Airflow in production, so the Airflow side comes from its documentation, not from my scars.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tasks or assets: the one difference that matters
&lt;/h2&gt;

&lt;p&gt;In Airflow you write a DAG: task A, then task B, then task C. Airflow knows whether each task succeeded. It does not, by default, know what the task wrote.&lt;/p&gt;

&lt;p&gt;In Dagster you write an asset: a function that produces a table, a file or a model, and names the assets it depends on. The graph comes from those dependencies. Dagster records each materialization, so it knows which partitions of which table exist, when they were built, and whether the checks on them passed.&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;dagster&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AssetIn&lt;/span&gt;

&lt;span class="nd"&gt;@asset&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;daily_prices&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ins&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;prices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AssetIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;daily_prices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sector_statistics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole pitch. If your job is "keep these 100 tables correct and current", the asset model matches the job. If your job is "call these 40 systems in this order", Airflow's task model matches it at least as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the asset model fit a 112-asset pipeline run by one person
&lt;/h2&gt;

&lt;p&gt;My pipeline ingests market data, builds features, trains models and writes to ClickHouse. The counts today: &lt;strong&gt;112 assets&lt;/strong&gt;, &lt;strong&gt;36 of them partitioned&lt;/strong&gt; (34 daily, 2 static), &lt;strong&gt;100 jobs&lt;/strong&gt;, &lt;strong&gt;57 schedules&lt;/strong&gt;, &lt;strong&gt;3 sensors&lt;/strong&gt;, &lt;strong&gt;93 asset checks&lt;/strong&gt;. It has 913 commits since November 2025 and about 3,000 tests. The &lt;a href="https://hikmahtechnologies.com/blog/building-a-data-platform-solo-ansaar-teardown/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;teardown of the whole platform&lt;/a&gt; gives the architecture; it has grown since that was written.&lt;/p&gt;

&lt;p&gt;Three things made the asset model worth it for one person:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Partitions are data, not a loop.&lt;/strong&gt; Dagster tracks the status of every daily partition. When a scrape fails for one date, I see one red square and rerun one square. I don't write backfill scripts that remember which dates they have done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checks sit next to the thing they check.&lt;/strong&gt; 93 asset checks run against the assets they guard and show up on the asset's page. When something is wrong, the check and the data are in one place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Granularity is a decision, not a default.&lt;/strong&gt; I could choose to drop partitions where they cost more than they saved. One sector-statistics asset rebuilt &lt;strong&gt;1,882 dates, 109,152 rows, in about 45 seconds&lt;/strong&gt; as a single unpartitioned asset. The per-partition version would have meant over 2,000 partitions and 5,000 to 20,000 queries. The first phase of that change materialised 9 of 10 such "full" assets in about six minutes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this is impossible in Airflow. Airflow 3 now has assets too (more below). But in Dagster the asset is the centre of the tool, and with one maintainer, the centre of the tool is what you get for free.&lt;/p&gt;




&lt;p&gt;This is the first part. The full post — including the rest of the working details — is on my site: &lt;a href="https://hikmahtechnologies.com/blog/dagster-vs-airflow/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;Dagster vs Airflow: which to choose for a small data team&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>python</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>Polars vs Pandas: the three differences that matter</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Wed, 14 Oct 2026 14:00:01 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/polars-vs-pandas-the-three-differences-that-matter-1eid</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/polars-vs-pandas-the-three-differences-that-matter-1eid</guid>
      <description>&lt;p&gt;Every Polars post opens with a benchmark where it beats pandas by a factor of ten. That number is real on some workload somewhere, and it is the least useful thing about the library.&lt;/p&gt;

&lt;p&gt;Three differences actually cause the gap. Once you know them, you can predict when Polars will help you and when it will not, which is worth more than a chart.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. It builds the plan before it does the work
&lt;/h2&gt;

&lt;p&gt;Pandas is eager. Each line runs when it is read:&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="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transactions.parquet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# whole file, all columns
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;df&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                      &lt;span class="c1"&gt;# walk it, allocate a new frame
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&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="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.029&lt;/span&gt;               &lt;span class="c1"&gt;# walk it again
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop_duplicates&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transaction_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;    &lt;span class="c1"&gt;# and again
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file is fully loaded before the first filter, including columns you never mention again. Every step allocates.&lt;/p&gt;

&lt;p&gt;Polars, given &lt;code&gt;scan_&lt;/code&gt; instead of &lt;code&gt;read_&lt;/code&gt;, does nothing at all until you ask:&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="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scan_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transactions.parquet&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;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_columns&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&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="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.029&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fee&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;unique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subset&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;transaction_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="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;# only now does anything run
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By the time &lt;code&gt;collect&lt;/code&gt; fires, Polars knows the whole pipeline, so it can do two things pandas structurally cannot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Projection pushdown.&lt;/strong&gt; Your query mentions three columns, so it reads three columns off disk. The other forty are never touched.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Predicate pushdown.&lt;/strong&gt; The filter goes down to the file reader, so non-matching rows are never materialised at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also where Parquet and Polars compound. Columnar storage makes reading three of forty-three columns cheap, and lazy execution is what tells the reader it only needs three.&lt;/p&gt;

&lt;p&gt;One trap worth knowing, because it silently undoes all of it:&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="n"&gt;lazy_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# runs the whole query, throws away almost everything
&lt;/span&gt;&lt;span class="n"&gt;lazy_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# pushes the limit into the plan, stops scanning early
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you learned Polars from older material you may reach for &lt;code&gt;.fetch(n)&lt;/code&gt;. That is deprecated in Polars 1.x; &lt;code&gt;.head(n).collect()&lt;/code&gt; is the current form.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. No hidden copies, because everything is Arrow
&lt;/h2&gt;

&lt;p&gt;Polars stores data in Arrow buffers, which is the same layout DuckDB and PyArrow use. When you pass a frame between them, they exchange pointers rather than bytes.&lt;/p&gt;

&lt;p&gt;That sounds like an implementation detail until you count the boundaries in a real pipeline. Read Parquet into pandas with the NumPy backend, transform, hand the result to DuckDB, and each crossing allocates fresh memory the size of your data, spends CPU translating layouts, and adds garbage collector pressure. On a gigabyte of decoded data that is roughly 2.3 seconds through the copying path against 0.4 through the Arrow one, and about half the peak memory.&lt;/p&gt;

&lt;p&gt;Memory is the more reliable win, and it is the one people notice on real jobs. Pandas materialises every intermediate frame, so a chain of eight transforms over a large table holds several copies at once. That is the difference between a job that fits in your container and one that gets killed.&lt;/p&gt;




&lt;p&gt;This is the first part. The full post — including the rest of the working details — is on my site: &lt;a href="https://hikmahtechnologies.com/blog/polars-vs-pandas-three-differences/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;Polars vs Pandas: the three differences that matter&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>datascience</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Parquet vs CSV: what you actually save, and why</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Mon, 12 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/parquet-vs-csv-what-you-actually-save-and-why-2h97</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/parquet-vs-csv-what-you-actually-save-and-why-2h97</guid>
      <description>&lt;p&gt;"Just use Parquet" is the most repeated advice in data engineering and the least explained. It is good advice. But if you do not know &lt;em&gt;why&lt;/em&gt; it wins, you cannot tell when it will not, and you end up either converting files that should have stayed CSV or being surprised when your ratio is 3x instead of the 20x you read about.&lt;/p&gt;

&lt;p&gt;There are three mechanical reasons, and none of them need a paper.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. It reads only the columns you asked for
&lt;/h2&gt;

&lt;p&gt;A CSV stores a row at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id,name,email,signup_date,revenue
1,Alice,alice@x.com,2026-01-15,127.50
2,Bob,bob@y.com,2026-01-16,340.00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To total the revenue, the reader walks the whole file. Every name, every email, every date, all read off disk and parsed to reach one column.&lt;/p&gt;

&lt;p&gt;Parquet stores a column at a time. &lt;code&gt;SELECT SUM(revenue)&lt;/code&gt; reads the revenue column and nothing else. Most analytical queries touch 10 to 20% of the columns, so they read 10 to 20% of the file. CSV always reads 100%.&lt;/p&gt;

&lt;p&gt;That is the entire trick behind the headline benchmarks, and it is why the win grows as your tables get wider.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Similar things sit next to each other, so they compress
&lt;/h2&gt;

&lt;p&gt;Compression works on patterns. A CSV row is a string, a date, a float and two integers jammed together, which is close to noise. A Parquet column is ten million floats, or ten million dates, or the word "USA" repeated ten thousand times.&lt;/p&gt;

&lt;p&gt;Parquet picks an encoding per column with no input from you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run-length encoding&lt;/strong&gt; turns ten thousand repeats of "USA" into the pair &lt;code&gt;["USA", 10000]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dictionary encoding&lt;/strong&gt; maps "San Francisco" to &lt;code&gt;0&lt;/code&gt; and "New York" to &lt;code&gt;1&lt;/code&gt;, then stores the small integers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delta encoding&lt;/strong&gt; turns hourly timestamps &lt;code&gt;[1704067200, 1704070800, 1704074400]&lt;/code&gt; into &lt;code&gt;[1704067200, +3600, +3600]&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then a general compressor runs over the result.&lt;/p&gt;

&lt;p&gt;This is why asking "what compression ratio will I get?" has no answer without your schema. What decides it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column type&lt;/th&gt;
&lt;th&gt;Typical ratio&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Booleans&lt;/td&gt;
&lt;td&gt;50-100x&lt;/td&gt;
&lt;td&gt;Bit-packing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timestamps&lt;/td&gt;
&lt;td&gt;10-50x&lt;/td&gt;
&lt;td&gt;Delta encoding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low-cardinality strings&lt;/td&gt;
&lt;td&gt;10-30x&lt;/td&gt;
&lt;td&gt;Dictionary encoding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integers, IDs, counts&lt;/td&gt;
&lt;td&gt;5-20x&lt;/td&gt;
&lt;td&gt;Dictionary and bit-packing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-cardinality strings&lt;/td&gt;
&lt;td&gt;2-5x&lt;/td&gt;
&lt;td&gt;Little structure to exploit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Floats, measurements&lt;/td&gt;
&lt;td&gt;2-4x&lt;/td&gt;
&lt;td&gt;Little structure to exploit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A table of flags, IDs and timestamps compresses enormously. A table of sensor readings barely compresses at all. Both are normal.&lt;/p&gt;

&lt;p&gt;Real numbers, so you have an anchor: ten million rows of synthetic user data go from about 635 MB of CSV to about 29 MB of Parquet, roughly 22x. Ten million NYC taxi rides go from 2.1 GB to 180 MB, roughly 12x. Event logs usually land at 15 to 25x, transactional data at 10 to 15x, float-heavy sensor data at 4 to 8x.&lt;/p&gt;

&lt;p&gt;Measure your own. It takes one write.&lt;/p&gt;




&lt;p&gt;This is the first part. The full post — including the rest of the working details — is on my site: &lt;a href="https://hikmahtechnologies.com/blog/parquet-vs-csv-what-you-actually-save/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;Parquet vs CSV: what you actually save, and why&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>python</category>
      <category>data</category>
    </item>
    <item>
      <title>DuckDB as an LLM cache: what to cache, what to key on, and where it breaks</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Sat, 10 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/duckdb-as-an-llm-cache-what-to-cache-what-to-key-on-and-where-it-breaks-392n</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/duckdb-as-an-llm-cache-what-to-cache-what-to-key-on-and-where-it-breaks-392n</guid>
      <description>&lt;p&gt;Someone reached this site last month asking, word for word, &lt;em&gt;what are the pros and cons of using DuckDB as an LLM cache in a production-grade data app?&lt;/em&gt; Short answer: it fits when &lt;strong&gt;one process owns the cache&lt;/strong&gt;, the hard part is the key rather than the database, and the reason to pick it over Redis is that the cache becomes a table you can ask questions of. The long answer follows, including where it's the wrong tool. Whether DuckDB belongs in production at all is &lt;a href="https://hikmahtechnologies.com/blog/duckdb-in-production-what-its-good-at/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;its own post&lt;/a&gt;; this one is only about the cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  "LLM cache" is three different caches
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Exact-match.&lt;/strong&gt; Key: a hash of the fully resolved prompt plus the versions below. Value: the response. A hit means this exact call was made before, so the answer is identical by construction. Hit rates are poor for free-text chat and excellent for classification, extraction, batch jobs and retries — the shapes in &lt;a href="https://hikmahtechnologies.com/blog/calling-an-llm-from-inside-sql/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;calling an LLM from inside SQL&lt;/a&gt;, where every re-run re-classifies the same rows unless something remembers. This is the tier that pays. (Your API vendor's &lt;em&gt;prompt caching&lt;/em&gt; is a different thing: it makes a repeated prefix cheaper but still makes the call.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic.&lt;/strong&gt; Embed the prompt, find the nearest stored prompt, serve its answer if the similarity clears a threshold. Every hit is a bet that two different prompts wanted the same answer. A false positive isn't a miss; it's a wrong answer the user sees.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool and retrieval results.&lt;/strong&gt; The search call, the database lookup, the chunks fetched to build the prompt. Deterministic given their inputs and the source's version, run more often than the model, usually the cheapest win. They want a TTL more than a version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why DuckDB is plausible here
&lt;/h2&gt;

&lt;p&gt;An exact-match hit is a function call, not a network round trip. On DuckDB 1.5.5 with 8 threads, a keyed lookup over 200,000 rows measured &lt;strong&gt;0.26ms p50, 0.39ms p99&lt;/strong&gt; with an index on the key (2ms / 5.7ms without). The rows are columnar, so the cache is also a dataset — hit rate, spend saved and which prompts dominate are one query, which is the strongest argument and gets its own section. The same table takes a &lt;code&gt;FLOAT[1536]&lt;/code&gt; column for the semantic tier. And there is nothing to run: a file inside the process you already operate. The &lt;code&gt;memory_limit&lt;/code&gt; / &lt;code&gt;temp_directory&lt;/code&gt; / &lt;code&gt;threads&lt;/code&gt; setup is in &lt;a href="https://hikmahtechnologies.com/blog/running-duckdb-on-your-own-infra/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;running DuckDB on your own infrastructure&lt;/a&gt;; the one addition here is that the HNSW index lives outside that memory limit entirely.&lt;/p&gt;




&lt;p&gt;This is the first part. The full post — including the rest of the working details — is on my site: &lt;a href="https://hikmahtechnologies.com/blog/duckdb-as-an-llm-cache/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;DuckDB as an LLM cache: what to cache, what to key on, and where it breaks&lt;/a&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>python</category>
      <category>database</category>
    </item>
    <item>
      <title>Paper Before Real Money: How My Finance Agent Earns the Right to Trade</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Fri, 09 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/paper-before-real-money-how-my-finance-agent-earns-the-right-to-trade-18cj</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/paper-before-real-money-how-my-finance-agent-earns-the-right-to-trade-18cj</guid>
      <description>&lt;p&gt;Maou is the finance agent in &lt;a href="https://hikmahtechnologies.com/aegis?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;AEGIS&lt;/a&gt;, the self-hosted agent platform I run my life on. A few weeks ago it learned to keep my books properly, and I wrote about &lt;a href="https://hikmahtechnologies.com/blog/a-ledger-is-not-a-database-table?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;why a ledger is not a database table&lt;/a&gt;. Now it has a second job: it runs a paper trading desk on real market prices.&lt;/p&gt;

&lt;p&gt;The goal is simple. I have a trading system that decides what to hold each day. I want Maou to place those trades with real money. But not yet. First it has to show, on paper and over months, that the whole chain works and that the results are worth having.&lt;/p&gt;

&lt;p&gt;This post covers both halves of what Maou now does with money, and what has to be true before the paper desk becomes a real one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is a write-up about engineering, not investment advice. The desk trades on paper, and this post deliberately does not show which stocks it holds.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The books: one view of where the money stands
&lt;/h2&gt;

&lt;p&gt;The books are a plain-text double-entry journal in git, checked by &lt;a href="https://hledger.org" rel="noopener noreferrer"&gt;hledger&lt;/a&gt;. Postgres is only an index over it. That was the last post. What changed since is that I can now see the money, not just trust it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy4brnmlu1kfw5bvtf7ge.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy4brnmlu1kfw5bvtf7ge.png" alt="Maou's Money page, with every figure blurred: net worth over time, money in and out by month, and where the money went this month" width="799" height="529"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The figures are blurred because they are my real finances. The page shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Net worth at each month end&lt;/strong&gt;: everything I have, less everything I owe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Money in and money out&lt;/strong&gt;, month by month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where it went this month&lt;/strong&gt;, by category. My business spending splits into its own categories rather than arriving as one bar called "business".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every number comes from hledger, asked at the moment the page loads. Nothing is copied into a second table that could drift.&lt;/p&gt;

&lt;p&gt;One rule mattered more than any chart. Some of my spending is in dollars and pounds, and the books have no exchange rate for those days yet. The easy move is to guess a rate and fold them in. Instead the page leaves them out of the chart and says so underneath: "left out of the charts because there is no rate for them: $, £". A wrong number that looks right is worse than a gap you can see.&lt;/p&gt;

&lt;p&gt;Behind the charts are the parts that need action: bills still to pay, spending the books could not explain, and bank statements that have not been matched line by line. Maou raises those as tasks for me, and a task closes itself when its problem goes away.&lt;/p&gt;

&lt;h2&gt;
  
  
  The desk: who decides what
&lt;/h2&gt;

&lt;p&gt;The split of responsibilities is the most important design choice, so here it is plainly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The trading system decides what to hold.&lt;/strong&gt; Every evening it publishes a list of positions and how much of the portfolio each should be. That system is &lt;a href="https://hikmahtechnologies.com/blog/building-a-data-platform-solo-ansaar-teardown?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;Ansaar&lt;/a&gt;, my market data platform, with its own models and its own risk manager.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maou decides nothing about the picks.&lt;/strong&gt; It turns that list into orders for whole shares against the paper capital, places them, and fills them at the next session's opening price, using real prices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maou scores the result&lt;/strong&gt; against a benchmark every week and every month.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent is dumb on purpose. What I am testing is not whether an LLM can pick stocks. It is whether the full chain holds together: the trading system's decisions, the data they arrive on, the sizing, the fills, the costs and the scoring. When real money is involved, every link in that chain can lose it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F20re5hd7zt0o4x68wxs1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F20re5hd7zt0o4x68wxs1.png" alt="Maou's trading desk: return since the first fill against SHARIABEES and the Nifty, drawdown, and how much of the money was invested each day" width="800" height="653"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;This is the first part. The full post — including the rest of the working details — is on my site: &lt;a href="https://hikmahtechnologies.com/blog/paper-before-real-money/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;Paper Before Real Money: How My Finance Agent Earns the Right to Trade&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>automation</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The agent writes the code. I'm paid for the rest.</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Wed, 07 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/the-agent-writes-the-code-im-paid-for-the-rest-4jd</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/the-agent-writes-the-code-im-paid-for-the-rest-4jd</guid>
      <description>&lt;p&gt;I don't type much code any more. Over the past year I rebuilt &lt;a href="https://hikmahtechnologies.com/aegis/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;AEGIS&lt;/a&gt;, my own operations layer, into its third version, kept the &lt;a href="https://hikmahtechnologies.com/products/ansaar/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;Ansaar&lt;/a&gt; data platform running every night, and shipped most of the &lt;a href="https://hikmahtechnologies.com/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;Hikmah Technologies site&lt;/a&gt;. Claude Code wrote most of the lines. Work I would once have planned in weeks now fits in days.&lt;/p&gt;

&lt;p&gt;So where do my hours go? Almost none of them go on writing code. They go on three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finding out what the person actually means.&lt;/li&gt;
&lt;li&gt;Deciding what a correct result looks like.&lt;/li&gt;
&lt;li&gt;Checking what got built against the real world.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the job now. The agent handles the part in the middle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code was the easy part all along
&lt;/h2&gt;

&lt;p&gt;For most of my career it didn't feel that way, because writing code was slow and took up the day. Whoever could write it was the expensive person in the room, and every other role was arranged around them.&lt;/p&gt;

&lt;p&gt;Now a clear description turns into working code in minutes. Agents are also getting steadily better at the work after that: reviewing, fixing bugs, deploying. I expect them to get most of the way there. What I don't expect them to get is the description itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The knowledge isn't written down
&lt;/h2&gt;

&lt;p&gt;Take a simple request: "we need a way to track our leads."&lt;/p&gt;

&lt;p&gt;Hundreds of products fit that sentence. The right one for a property brokerage is not the right one for a software company. At a brokerage, the lead may be a family, not a person. The same buyer calls in through three channels. A site visit matters more than an email open. The rules about who "owns" a lead can decide whether the partners stay friends. Usually none of that is written down. The owner carries it in their head and would struggle to list it if you asked directly.&lt;/p&gt;

&lt;p&gt;No model has seen it, because it has never been written anywhere. Someone has to sit with the business, pull it out piece by piece, and turn it into something exact enough to build. Then they have to show the result and hear "no, not like that", because the first version is always wrong in a way nobody could have predicted.&lt;/p&gt;

&lt;p&gt;This also does not carry over from one client to the next. What "correct" means for one brokerage's leads is not quite what it means for the next. That is why off-the-shelf software grows a hundred settings and still doesn't fit. Cheap code makes it possible to build the version that does fit, but only if somebody has done the finding out.&lt;/p&gt;




&lt;p&gt;This is the first part. The full post — including the rest of the working details — is on my site: &lt;a href="https://hikmahtechnologies.com/blog/the-agent-writes-the-code-im-paid-for-the-rest/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;The agent writes the code. I'm paid for the rest.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>softwareengineering</category>
      <category>programming</category>
    </item>
    <item>
      <title>DuckDB vs ClickHouse: which one, and when you need both</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Tue, 06 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/duckdb-vs-clickhouse-which-one-and-when-you-need-both-4ekf</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/duckdb-vs-clickhouse-which-one-and-when-you-need-both-4ekf</guid>
      <description>&lt;p&gt;DuckDB and ClickHouse get compared because they sit near each other in every analytical benchmark: both are columnar, both are vectorised, both are very fast. The benchmark framing hides the difference that actually decides it. &lt;strong&gt;DuckDB is a library you put inside your program. ClickHouse is a server your programs connect to.&lt;/strong&gt; Almost every practical difference follows from that.&lt;/p&gt;

&lt;p&gt;I run both. ClickHouse is the analytical store behind &lt;a href="https://hikmahtechnologies.com/systems/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;Ansaar&lt;/a&gt;, feeding a live API. DuckDB is what I reach for in batch jobs and local work, and it is the subject of my book. So this is the comparison from operating them, not from a benchmark table.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;Pick &lt;strong&gt;DuckDB&lt;/strong&gt; when the work happens inside one program — a transformation job, a service that owns its own queries, a notebook, a CI run — and the data arrives in batches. There is nothing to operate, and it is extraordinarily fast for that one program.&lt;/p&gt;

&lt;p&gt;Pick &lt;strong&gt;ClickHouse&lt;/strong&gt; when many clients query the same data at the same time, the data keeps arriving, and people expect the numbers to be seconds old. That is a server's job, and ClickHouse is one of the best servers for it.&lt;/p&gt;

&lt;p&gt;If you have both shapes, run both. That's more common than either camp admits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each one is
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;DuckDB&lt;/strong&gt; is an in-process analytical database — the usual shorthand is "SQLite for analytics". You import it into Python, Node, Go or the CLI, and it runs a columnar query engine inside that process. It reads Parquet, CSV and JSON directly, including from S3, with no load step. There is no server, no port, no cluster. Only one process may open a database file read-write, and while it does, no other process can open the file at all; any number of processes may read it if none is writing. The pattern that scales is one writer producing Parquet and many read-only readers over it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ClickHouse&lt;/strong&gt; is a client-server columnar database built for real-time analytics. You run it as a service (or buy ClickHouse Cloud), clients connect over the network, and it stores data in its MergeTree engine: rows land in sorted parts on disk, and background merges combine them. It is built to ingest continuously and answer aggregations over billions of rows in milliseconds, for many clients at once. It scales out across machines when one isn't enough.&lt;/p&gt;




&lt;p&gt;This is the first part. The full post — including the rest of the working details — is on my site: &lt;a href="https://hikmahtechnologies.com/blog/duckdb-vs-clickhouse/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;DuckDB vs ClickHouse: which one, and when you need both&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>database</category>
      <category>analytics</category>
    </item>
    <item>
      <title>A Pile of Documents Is Not Knowledge</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Fri, 02 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/a-pile-of-documents-is-not-knowledge-3lf8</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/a-pile-of-documents-is-not-knowledge-3lf8</guid>
      <description>&lt;p&gt;My research agent had read about 20,400 documents. It had concluded nothing.&lt;/p&gt;

&lt;p&gt;Not metaphorically. The knowledge store held roughly twenty thousand rows — papers, articles, PDFs, nightly log entries — and there was nowhere in the system that said &lt;em&gt;what any of it meant&lt;/em&gt;. Research answers were stored as &lt;code&gt;aegis://research/&lt;/code&gt; rows filed among ten thousand PDFs. The nightly journal wrote one dated entry into the same pile, in a format nobody could open.&lt;/p&gt;

&lt;p&gt;This is the third of three posts about rebuilding lanes of &lt;a href="https://hikmahtechnologies.com/aegis?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;AEGIS&lt;/a&gt;, my self-hosted agent platform, between 5 and 13 September. The first two were &lt;a href="https://hikmahtechnologies.com/blog/your-alerts-have-no-identity?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;alerting&lt;/a&gt; and &lt;a href="https://hikmahtechnologies.com/blog/a-ledger-is-not-a-database-table?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;money&lt;/a&gt;. All three ended at the same rule, and this lane is where it's easiest to see why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure use, not ingestion
&lt;/h2&gt;

&lt;p&gt;Every RAG dashboard I have ever seen measures the wrong end of the pipe. Documents ingested. Chunks embedded. Index size. All of it is &lt;em&gt;input&lt;/em&gt;, and input is the part that is trivially easy to grow.&lt;/p&gt;

&lt;p&gt;The measurement that mattered took one join: a log of which documents were injected into which prompt, joined back to where each document came from. Over the 30 days to 12 September:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;arXiv: 1,889 papers, 89,669 chunks — 90% of all RSS chunks.&lt;/strong&gt; Prompts used &lt;strong&gt;14 papers&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Across the whole corpus, arXiv PDFs were &lt;strong&gt;93% of all chunks&lt;/strong&gt;, and &lt;strong&gt;78 of 10,284&lt;/strong&gt; had ever reached a prompt.&lt;/li&gt;
&lt;li&gt;The intelligence scans had stored &lt;strong&gt;371 items&lt;/strong&gt;. Exactly &lt;strong&gt;one&lt;/strong&gt; was ever used in a prompt.&lt;/li&gt;
&lt;li&gt;Those same scans created &lt;strong&gt;277 task-manager items in 30 days&lt;/strong&gt;, every one auto-classified as reference material on arrival. My task list was being used as a log file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A retention preview said the same thing in bytes. One rule — keep only the first chunk of any PDF no prompt has used in 30 days — would touch 8,297 PDFs and drop 362,153 of 500,055 chunks, freeing about 530 MB of text and 1 GB of vectors from a 4.8 GB table. The PDFs themselves stay, and nothing has been deleted: the preview is a dry run, and deleting is a separate, explicit decision.&lt;/p&gt;

&lt;p&gt;This is not a story about arXiv being low quality. It is excellent. It is a story about a system that was optimising the metric it could see.&lt;/p&gt;

&lt;h2&gt;
  
  
  The filter I measured and did not ship
&lt;/h2&gt;

&lt;p&gt;The obvious fix is a topic gate: only store the full text when the item matches something you care about. I built it, measured it, and made it &lt;strong&gt;opt-in&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is why. On arXiv the gate would pass 41% of papers — about 2.3× fewer chunks, for real added complexity. And on every &lt;em&gt;other&lt;/em&gt; feed, the gate would have kept the full text of only &lt;strong&gt;2 of the 10 documents a prompt actually used&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That second number killed it as a default. A filter that removes 80% of the value you can prove, to save storage you have plenty of, is not an optimisation. The measurement changed the design, which is the only reason to take a measurement.&lt;/p&gt;

&lt;p&gt;The actual win was somewhere else entirely.&lt;/p&gt;




&lt;p&gt;This is the first part. The full post — including the rest of the working details — is on my site: &lt;a href="https://hikmahtechnologies.com/blog/a-pile-of-documents-is-not-knowledge/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;A Pile of Documents Is Not Knowledge&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rag</category>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
