<?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: Herbert Tzekian</title>
    <description>The latest articles on DEV Community by Herbert Tzekian (@herbze).</description>
    <link>https://dev.to/herbze</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%2F3994159%2Fbc81abd7-7e30-4076-9943-74ac34dc700a.png</url>
      <title>DEV Community: Herbert Tzekian</title>
      <link>https://dev.to/herbze</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/herbze"/>
    <language>en</language>
    <item>
      <title>When pandas runs out of RAM, I change one import line</title>
      <dc:creator>Herbert Tzekian</dc:creator>
      <pubDate>Wed, 24 Jun 2026 15:01:00 +0000</pubDate>
      <link>https://dev.to/herbze/when-pandas-runs-out-of-ram-i-change-one-import-line-35k0</link>
      <guid>https://dev.to/herbze/when-pandas-runs-out-of-ram-i-change-one-import-line-35k0</guid>
      <description>&lt;p&gt;You know the moment. You've got a Parquet file or a fat CSV, you write the obvious &lt;code&gt;pd.read_parquet(...)&lt;/code&gt;, and Python sits there. Then either it finishes in its own sweet time, or you get the message everyone who's done data work in Python has seen at least once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MemoryError: Unable to allocate ... for an array
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file is bigger than your RAM, pandas wants the whole thing in memory at once, and your laptop says no.&lt;/p&gt;

&lt;p&gt;I used to solve this by chunking, or by giving up and rewriting the whole notebook in SQL against some database just to run one &lt;code&gt;GROUP BY&lt;/code&gt;. Both are annoying, and the second one means I now maintain two versions of the same analysis. What I do now is change one import line and keep writing the exact same pandas I already had. No server, no connection string, and crucially no SQL, because I never leave the DataFrame API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line change
&lt;/h2&gt;

&lt;p&gt;The library is &lt;strong&gt;chDB&lt;/strong&gt;. It's ClickHouse compiled as an in-process engine you &lt;code&gt;pip install&lt;/code&gt;, basically the ClickHouse query engine as a Python library, with nothing to run in Docker and nothing to connect to. If you want the internals, it's &lt;a href="https://clickhouse.com/resources/engineering/what-is-chdb" rel="noopener noreferrer"&gt;ClickHouse compiled as an in-process engine&lt;/a&gt;, but the part that changed my day-to-day is its &lt;code&gt;datastore&lt;/code&gt; module.&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;chdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Change this:
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="c1"&gt;# To this:
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;chdb.datastore&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="c1"&gt;# Everything else stays the same.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Your existing pandas code keeps working, with the same method names, the same arguments, and the same DataFrames coming back. The one thing that changes is how you point at a file too big to open: instead of &lt;code&gt;pd.read_parquet&lt;/code&gt;, you hand the file to a &lt;code&gt;DataStore&lt;/code&gt;, which streams it instead of loading it whole. From there it's the pandas API you already know. The difference underneath is that operations are lazy and compile down to the ClickHouse engine, so nothing executes until you actually need a result (a &lt;code&gt;print()&lt;/code&gt;, a &lt;code&gt;len()&lt;/code&gt;, plotting). The giant file never gets slurped into RAM in one go.&lt;/p&gt;

&lt;h2&gt;
  
  
  The query I'd otherwise have crashed on
&lt;/h2&gt;

&lt;p&gt;Here's a real example. Events Parquet, I want the top user actions in the last 30 days. This is the textbook &lt;code&gt;read_parquet&lt;/code&gt; (boom) followed by a filter and a groupby. Written as plain pandas, except it doesn't fall over:&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;chdb.datastore&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;events&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="n"&gt;DataStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;events.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;recent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_date&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-05-21&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recent&lt;/span&gt;
       &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&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;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
       &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ascending&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&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;20&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;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the same code I'd have written against pandas. I didn't drop into SQL, I didn't chunk anything, I didn't think about memory. Because it's lazy and columnar underneath, reading three columns out of forty doesn't pay for the other thirty-seven, and the filter gets pushed down into the file scan so rows I don't want never get decoded. That's the difference between "loads 12 GB" and "reads the slice you asked for."&lt;/p&gt;

&lt;h2&gt;
  
  
  It's the same pandas all the way through
&lt;/h2&gt;

&lt;p&gt;You don't get a cut-down subset that handles &lt;code&gt;groupby&lt;/code&gt; and bails on everything else. The whole DataFrame API is here: filtering, column selection, computed columns, the string and datetime accessors, joins:&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;chdb.datastore&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;reqs&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="n"&gt;DataStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requests.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;reqs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;reqs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# datetime accessor, computed column
&lt;/span&gt;&lt;span class="n"&gt;hourly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reqs&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hour&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;latency_ms&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;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_index&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;hourly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# tiny result now, plots like any pandas Series
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The big file got crunched down to a few hundred rows before anything had to fit in memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Joining across files (and databases) without leaving pandas
&lt;/h2&gt;

&lt;p&gt;The bit I didn't expect to use as much as I do: the same DataFrame can be backed by a file &lt;em&gt;or&lt;/em&gt; a remote source, and you join them with a normal &lt;code&gt;.join()&lt;/code&gt;. So a Parquet file on disk and a table in Postgres or MySQL line up in one expression.&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;chdb.datastore&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;orders&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="n"&gt;DataStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders.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;customers&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="n"&gt;DataStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mysql://root:pass@db:3306/crm/customers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;by_country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;
              &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;left_on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_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;right_on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;country&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="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
              &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ascending&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&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;by_country&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also reads CSV, TSV, JSON and friends the same way, so the &lt;a href="https://clickhouse.com/resources/engineering/read-csv-file-python" rel="noopener noreferrer"&gt;same approach works for a giant CSV&lt;/a&gt; you'd otherwise be afraid to open, and for &lt;a href="https://clickhouse.com/resources/engineering/how-to-query-parquet-file" rel="noopener noreferrer"&gt;Parquet&lt;/a&gt; as shown above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this became my default
&lt;/h2&gt;

&lt;p&gt;The honest reason I keep using it isn't the speed, though it is faster. It's that I stopped maintaining two versions of my analysis.&lt;/p&gt;

&lt;p&gt;Before, a notebook experiment used pandas, and then if it needed to run for real on the full dataset it got rewritten into SQL, against some warehouse, with a different dialect and a deployment story. With &lt;code&gt;chdb.datastore&lt;/code&gt; the exploration code &lt;em&gt;is&lt;/em&gt; the production code. It's pandas while I'm poking at a file on my laptop, and it's still pandas when the file is too big to open the normal way. I scale up by changing what the DataFrame points at, not by rewriting it.&lt;/p&gt;

&lt;p&gt;So my rule of thumb now: if a file is small and friendly, sure, regular pandas is fine. The second it's big enough to make Python sweat, I swap the import line. The code doesn't change, it doesn't run out of RAM, and the throwaway version turns out not to be throwaway.&lt;/p&gt;

&lt;p&gt;Next time you hit &lt;code&gt;MemoryError&lt;/code&gt;, try changing &lt;code&gt;import pandas as pd&lt;/code&gt; to &lt;code&gt;import chdb.datastore as pd&lt;/code&gt; and rerun the cell. Usually that's the whole fix.&lt;/p&gt;

</description>
      <category>database</category>
      <category>python</category>
    </item>
    <item>
      <title>I stopped writing throwaway scripts for messy CSVs and just use SQL now</title>
      <dc:creator>Herbert Tzekian</dc:creator>
      <pubDate>Sat, 20 Jun 2026 13:41:23 +0000</pubDate>
      <link>https://dev.to/herbze/i-stopped-writing-throwaway-scripts-for-messy-csvs-and-just-use-sql-now-p6h</link>
      <guid>https://dev.to/herbze/i-stopped-writing-throwaway-scripts-for-messy-csvs-and-just-use-sql-now-p6h</guid>
      <description>&lt;p&gt;Someone sends you a CSV. Then a folder of CSVs. Then a CSV that's actually tab-separated but named &lt;code&gt;.csv&lt;/code&gt;, with a stray header row and a column that's a number on most rows and the string &lt;code&gt;N/A&lt;/code&gt; on the rest.&lt;/p&gt;

&lt;p&gt;For years my answer to "can you pull a quick number out of this?" was a throwaway Python script. Read it in, fight pandas about dtypes, &lt;code&gt;groupby&lt;/code&gt;, print, delete the script, forget everything, repeat next week. It worked. It was also slow and I never kept any of it.&lt;/p&gt;

&lt;p&gt;These days I just point SQL at the file. I want to show you the exact workflow because it's embarrassingly simple and it's saved me a lot of evenings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one binary I actually use
&lt;/h2&gt;

&lt;p&gt;The tool is &lt;a href="https://clickhouse.com/resources/engineering/what-is-clickhouse-local" rel="noopener noreferrer"&gt;&lt;code&gt;clickhouse-local&lt;/code&gt;&lt;/a&gt;. It's a single binary, the ClickHouse engine minus the server. You download it and run SQL against files on your disk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://clickhouse.com/ | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a &lt;code&gt;clickhouse&lt;/code&gt; binary in the current directory. Now you can do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./clickhouse &lt;span class="nb"&gt;local&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"SELECT count() FROM file('orders.csv')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. It read the file, sniffed the format and the column types, and counted the rows. No setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Querying the thing like it's a table
&lt;/h2&gt;

&lt;p&gt;Say I've got &lt;code&gt;orders.csv&lt;/code&gt; and I want revenue by country, top 10. Normally that's a few lines of pandas. Here it's the query you'd write anyway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./clickhouse &lt;span class="nb"&gt;local&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"
  SELECT country, round(sum(amount), 2) AS revenue
  FROM file('orders.csv')
  GROUP BY country
  ORDER BY revenue DESC
  LIMIT 10
"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;file()&lt;/code&gt; function is the whole trick. It &lt;a href="https://clickhouse.com/resources/engineering/run-sql-on-csv-file" rel="noopener noreferrer"&gt;reads the file and gives you something you can &lt;code&gt;SELECT&lt;/code&gt; from&lt;/a&gt;. It auto-detects CSV, TSV, JSON, Parquet and a pile of others from the extension and contents, and it infers column names and types from the header and the data. The example above is honestly 90% of what you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the file is "somebody else's CSV"
&lt;/h2&gt;

&lt;p&gt;Real files are messy, so here's where this stops being a toy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's actually tab-separated.&lt;/strong&gt; Override the format instead of renaming the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./clickhouse &lt;span class="nb"&gt;local&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"SELECT * FROM file('weird.csv', 'TSV') LIMIT 5"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A column has &lt;code&gt;N/A&lt;/code&gt; mixed in with numbers.&lt;/strong&gt; Read it as text and clean it inline, no preprocessing pass:&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;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toFloat64OrNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_amount&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders.csv'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;toFloat64OrNull&lt;/code&gt; turns the junk into &lt;code&gt;NULL&lt;/code&gt; instead of blowing up, and &lt;code&gt;avg&lt;/code&gt; skips nulls. I use the &lt;code&gt;*OrNull&lt;/code&gt; and &lt;code&gt;*OrZero&lt;/code&gt; functions constantly for this exact reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A whole folder of files.&lt;/strong&gt; Glob them and query all at once, still one query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./clickhouse &lt;span class="nb"&gt;local&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"
  SELECT _file, count()
  FROM file('exports/*.csv')
  GROUP BY _file
"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_file&lt;/code&gt; is a virtual column telling you which file each row came from. Great for "which of these 40 exports is missing data."&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn the slow file into a fast file
&lt;/h2&gt;

&lt;p&gt;If I'm going to keep poking at the same CSV, the first thing I do is &lt;a href="https://clickhouse.com/resources/engineering/convert-csv-to-parquet" rel="noopener noreferrer"&gt;convert it to Parquet&lt;/a&gt; once. Columnar, compressed, types baked in, so every query after that is faster and smaller on disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./clickhouse &lt;span class="nb"&gt;local&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"
  SELECT * FROM file('orders.csv')
  INTO OUTFILE 'orders.parquet'
  FORMAT Parquet
"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then query &lt;code&gt;orders.parquet&lt;/code&gt; from then on. This one habit alone made my repeated ad-hoc queries feel instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I stuck with this one
&lt;/h2&gt;

&lt;p&gt;Two reasons, and the second is the one that surprised me.&lt;/p&gt;

&lt;p&gt;First, the obvious one: it's fast and there's no ceremony. A multi-GB CSV that made my old pandas script swap is a sub-second &lt;code&gt;GROUP BY&lt;/code&gt; here, because the engine is columnar and uses all my cores without me asking.&lt;/p&gt;

&lt;p&gt;Second, and this is why I didn't just bounce to the next shiny CLI tool, it's &lt;em&gt;the same SQL and the same engine&lt;/em&gt; whether the data is a 5 MB CSV on my laptop or billions of rows in a real ClickHouse cluster. When a "quick look at a file" turns into "okay we actually need to run this every hour over a year of data," I'm not rewriting anything. Same &lt;code&gt;file()&lt;/code&gt;, same functions, same query, it just moves to a server and keeps going. I've been burned before by prototyping in one tool and then re-implementing everything for production. Not having to do that is worth a lot.&lt;/p&gt;

&lt;p&gt;So now the answer to "can you pull a number out of this?" is thirty seconds and a SQL query, and if it turns out to matter, the thirty-second version is already the production version.&lt;/p&gt;

&lt;p&gt;Give the messy-CSV thing a try next time one lands in your inbox. You'll stop writing the throwaway script too.&lt;/p&gt;

</description>
      <category>database</category>
    </item>
  </channel>
</rss>
