<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Nikita Volodarskiy]]></title><description><![CDATA[Data Engineer in Holidu | Munich 🇩🇪 Ex: EPAM, Glowbyte Consulting Background in analytics, BA & data products.]]></description><link>https://nikitavolodarskiy.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!PwXA!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b682f96-f251-4786-82eb-082724cfeb78_640x640.jpeg</url><title>Nikita Volodarskiy</title><link>https://nikitavolodarskiy.substack.com</link></image><generator>Substack</generator><lastBuildDate>Wed, 20 May 2026 09:43:32 GMT</lastBuildDate><atom:link href="https://nikitavolodarskiy.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Nikita Volodarskiy]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[nikitavolodarskiy@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[nikitavolodarskiy@substack.com]]></itunes:email><itunes:name><![CDATA[Nikita Volodarskiy]]></itunes:name></itunes:owner><itunes:author><![CDATA[Nikita Volodarskiy]]></itunes:author><googleplay:owner><![CDATA[nikitavolodarskiy@substack.com]]></googleplay:owner><googleplay:email><![CDATA[nikitavolodarskiy@substack.com]]></googleplay:email><googleplay:author><![CDATA[Nikita Volodarskiy]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[How I See the IT Job Market in 2026]]></title><description><![CDATA[The IT job market is flooded with applicants, which makes cold applications useless &#8211; you're 1 of 1,000 with no way to stand out. Think of it as a Signaling Game and have a network.]]></description><link>https://nikitavolodarskiy.substack.com/p/how-i-see-the-it-job-market-in-2026</link><guid isPermaLink="false">https://nikitavolodarskiy.substack.com/p/how-i-see-the-it-job-market-in-2026</guid><dc:creator><![CDATA[Nikita Volodarskiy]]></dc:creator><pubDate>Wed, 13 May 2026 11:02:19 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!hcoz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d85ade-bccd-4a93-94ab-81a188213749_1920x1080.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Before we begin, let me set up some constraints &#8211; my experience is based on:</p><ul><li><p>Germany, first half of 2026</p></li><li><p>BI/Data/Business Analyst &amp; Data Engineer positions</p></li><li><p>Personal feelings about the topic; individual experiences may vary</p></li></ul><p>With that being said, let&#8217;s proceed.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!hcoz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d85ade-bccd-4a93-94ab-81a188213749_1920x1080.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!hcoz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d85ade-bccd-4a93-94ab-81a188213749_1920x1080.png 424w, https://substackcdn.com/image/fetch/$s_!hcoz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d85ade-bccd-4a93-94ab-81a188213749_1920x1080.png 848w, https://substackcdn.com/image/fetch/$s_!hcoz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d85ade-bccd-4a93-94ab-81a188213749_1920x1080.png 1272w, https://substackcdn.com/image/fetch/$s_!hcoz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d85ade-bccd-4a93-94ab-81a188213749_1920x1080.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!hcoz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d85ade-bccd-4a93-94ab-81a188213749_1920x1080.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a9d85ade-bccd-4a93-94ab-81a188213749_1920x1080.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:37212,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://nikitavolodarskiy.substack.com/i/197486902?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d85ade-bccd-4a93-94ab-81a188213749_1920x1080.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!hcoz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d85ade-bccd-4a93-94ab-81a188213749_1920x1080.png 424w, https://substackcdn.com/image/fetch/$s_!hcoz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d85ade-bccd-4a93-94ab-81a188213749_1920x1080.png 848w, https://substackcdn.com/image/fetch/$s_!hcoz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d85ade-bccd-4a93-94ab-81a188213749_1920x1080.png 1272w, https://substackcdn.com/image/fetch/$s_!hcoz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d85ade-bccd-4a93-94ab-81a188213749_1920x1080.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>From what my friends working in IT told me, it is very hard to hire people for the positions mentioned above. One shared that they were looking for 6 months without any results, while having interviews with candidates regularly. And this looks quite like a common case.</p><p>So what&#8217;s happening? <strong>I think</strong> there are a lot of underqualified people in IT looking for a job. Thousands of applications per position. The recruiting team can only interview a handful, a drop in the ocean.</p><p>This is why <strong>I believe</strong> cold applications do not work. Because there will be 999 other people who engineered their CV to match this job position. There will be 0 difference and no reason to prioritize you even if you are qualified. The problem is that in the end the recruiting team ends up interviewing unqualified candidates instead of you. <strong>Sorry, that&#8217;s maths.</strong></p><p>This reminds me of <strong>Game Theory and the concept of Signaling Games</strong>, where you need to send some kind of signal so that the receiver sees you as a qualified candidate, while underqualified candidates cannot send this signal because it costs too much for them to produce.</p><p>Now let&#8217;s think about what those signals can be:</p><ul><li><p>University</p></li><li><p>Online course certificates</p></li><li><p>Official certifications</p></li><li><p>Previous experience</p></li><li><p>Network</p></li></ul><p><strong>University</strong> can be a somewhat difficult thing to get through, and can be a nice additional signal if it matches the position.</p><p><strong>Courses and certifications</strong> really show nothing at all. At best they show that the person studied and passed an exam. At worst it shows that the person clicked through videos, skipped through materials, did homework with AI, and memorized answers to certification questions. Of course some effort is required, but hey! They are also motivated to get a job.</p><p><strong>Previous experience</strong> &#8211; there&#8217;s a catch. Write anything you want on your LinkedIn, come up with nice stories about what you did. However, having been an interviewer for 3 years, I can say that in most cases you can identify made-up experience through questions about specific cases. Just dig a bit deeper. Ask about implementation, architecture &#8211; things that someone who was actually there would know, even if they weren&#8217;t responsible for it. The person who made it up doesn&#8217;t have that experience. So again, made-up experience increases the load on the recruiting team, because it may even require 2 interviews to decline a candidate.</p><p><strong>Network</strong> &#8211; <strong>I found this to be the best signal that helped me get interviews.</strong> And I would even say the only instrument that worked.</p><p>The key thing I found: it should be your real network, not some random person who works at a company, whom you reached out to on LinkedIn and who is okay to refer you.</p><p>When people actually know you, the recruiting team will reach out to them and they will provide valuable feedback. Don&#8217;t underestimate it! If the person doesn&#8217;t know you, they will just say &#8220;We met on LinkedIn, their profile looks relevant.&#8221; That&#8217;s it &#8211; doesn&#8217;t sound encouraging. While your ex-colleague, or someone you know well from a different company, will be able to tell a real story about you.</p><p>This is a very good signal &#8211; it weighs more than all the previous ones. I don&#8217;t have access to applicant scoring models, but this is how I see it: when you apply cold you are 1 of 1000; being referred and you can be the only one for the position. You get into a separate queue entirely. Now it only depends on you, how good you really are.</p><p>So how do you build a real network? You don&#8217;t build it when you need a job &#8211; build it before. <strong>Have good relationships with your colleagues.</strong> Go to meetups, contribute to discussions, help people with their problems, stay in touch with former colleagues. <strong>The network is the side effect</strong> of genuinely engaging with your professional community over time. You can&#8217;t fake it, which is exactly what makes it a strong signal.</p><h2>The junior problem</h2><p>This whole framework breaks down for juniors. They don&#8217;t have a network yet and lack relevant experience. They may have university degrees, certificates, pet projects &#8211; and that&#8217;s it. Which makes it extremely hard for a talented new graduate to send a strong signal to a hiring company.</p><p>The result of all this, unfortunately, is that the recruiting team still gets overloaded with thousands of irrelevant candidates. The junior hiring problem is mostly unsolved, and I don&#8217;t see it getting better soon. (Especially given that AI doesn't make it any better)</p><h2>Where does this leave us</h2><p>The job market for data roles right now rewards one thing above everything else: people who know you. Not your certificates, not your LinkedIn profile, not even your CV. People who can say something real about you.</p><p>If you are mid-level or senior invest in your network <strong>before you need it</strong>. If you are a junior &#8211; I am sorry, I really have no idea what to say here. But I am confident that cold applications are a lottery with the odds being not in your favor.</p>]]></content:encoded></item><item><title><![CDATA[Deduplication Spark vs Athena (Trino)]]></title><description><![CDATA[Deduplicating 16 Billion Rows: Why Athena Failed and How Spark Didn't.]]></description><link>https://nikitavolodarskiy.substack.com/p/deduplication-spark-vs-athena-trino</link><guid isPermaLink="false">https://nikitavolodarskiy.substack.com/p/deduplication-spark-vs-athena-trino</guid><dc:creator><![CDATA[Nikita Volodarskiy]]></dc:creator><pubDate>Wed, 29 Apr 2026 18:54:57 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!5rlR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb953a44d-4e2d-4eeb-ac04-a6de25aa6042_1920x1080.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We had a table in Athena that grew enormously to 16 billion records, where only 6 billion records were unique. This happened because a join condition was incorrectly populating duplicates over a long period of time. After we fixed the underlying condition, we also needed to deduplicate the existing data to restore query performance.</p><p>For context: it&#8217;s a Hive external table without partitions, stored as Parquet files on S3.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5rlR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb953a44d-4e2d-4eeb-ac04-a6de25aa6042_1920x1080.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5rlR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb953a44d-4e2d-4eeb-ac04-a6de25aa6042_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!5rlR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb953a44d-4e2d-4eeb-ac04-a6de25aa6042_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!5rlR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb953a44d-4e2d-4eeb-ac04-a6de25aa6042_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!5rlR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb953a44d-4e2d-4eeb-ac04-a6de25aa6042_1920x1080.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5rlR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb953a44d-4e2d-4eeb-ac04-a6de25aa6042_1920x1080.jpeg" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b953a44d-4e2d-4eeb-ac04-a6de25aa6042_1920x1080.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:106365,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://nikitavolodarskiy.substack.com/i/195902701?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb953a44d-4e2d-4eeb-ac04-a6de25aa6042_1920x1080.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!5rlR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb953a44d-4e2d-4eeb-ac04-a6de25aa6042_1920x1080.jpeg 424w, https://substackcdn.com/image/fetch/$s_!5rlR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb953a44d-4e2d-4eeb-ac04-a6de25aa6042_1920x1080.jpeg 848w, https://substackcdn.com/image/fetch/$s_!5rlR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb953a44d-4e2d-4eeb-ac04-a6de25aa6042_1920x1080.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!5rlR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb953a44d-4e2d-4eeb-ac04-a6de25aa6042_1920x1080.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Athena</h2><p>We tried a couple of approaches to delete duplicates directly in Athena.</p><h3>Row number window function</h3><p>The first approach was the standard one &#8211; use <code>ROW_NUMBER() </code>to rank rows per ID and keep only the latest:</p><pre><code><code>SELECT * FROM (
  SELECT *,
    ROW_NUMBER() OVER (PARTITION BY id ORDER BY last_modified DESC) AS rn
  FROM data_lake.customer
)
WHERE rn = 1</code></code></pre><p>It failed immediately with <code>Query exhausted resources</code>. Trino has <a href="https://trino.io/docs/current/admin/spill.html">limited ability to spill to disk for window functions</a> and in practice, large window partitions frequently exhaust per-query memory.</p><h3>Join-based approach</h3><p>A known workaround in Athena is to replace the window function with a self-join, which Trino can handle more efficiently:</p><pre><code><code>WITH grouped_max AS (
  SELECT id, MAX(last_modified) AS max_modified
  FROM data_lake.customer
  GROUP BY id
)
SELECT a.*
FROM data_lake.customer a
JOIN grouped_max b ON a.id = b.id AND a.last_modified = b.max_modified</code></code></pre><p>This approach works well when duplicates differ by <code>last_modified</code>. In our case though, we had <strong>complete duplicates</strong> &#8211; rows where every column, including <code>last_modified</code>, was identical. The join would match all of them and return duplicates unchanged.</p><p>We also tried <code>SELECT DISTINCT</code> and <code>GROUP BY</code> with <code>MAX</code>, but both hit the same <code>Query exhausted resources</code> wall. At 16 billion rows, Athena simply doesn&#8217;t have enough per-query memory to materialise the intermediate state any of these approaches require.</p><p>So all Athena approaches were dead ends.</p><h2>Spark</h2><p>We decided to run the deduplication in PySpark in local mode on a dedicated Kubernetes pod (16 vCPU, 120 GB RAM). We don&#8217;t have a Spark cluster, so local mode on a large pod was the pragmatic choice &#8211; no cluster overhead, no executor coordination, just one JVM with all available memory. </p><p>The obvious approach, read the entire table and run a window function, also fails here for the same fundamental reason: shuffling 16 billion rows requires materialising all of them on a single machine, which no reasonable pod size can support.</p><h3>Hash-partitioned batches</h3><p>The solution was to split the work into 32 independent buckets using a deterministic hash function:</p><pre><code><code>pmod(xxhash64(id), 32) == bucket  # bucket in 0..31</code></code></pre><p><code>xxhash64</code> returns a signed 64-bit integer. <code>pmod</code> (positive modulo, as opposed to the <code>%</code> remainder operator) always produces a value in [0, 31] regardless of the sign of the hash. This is important &#8211; with the regular <code>%</code> operator, negative hash values produce negative remainders that never match any bucket, silently dropping roughly half of all IDs. <code>pmod</code> eliminates that.</p><p>Because the hash is deterministic, all rows for a given <code>id</code> always land in exactly one bucket. This guarantees deduplication is complete &#8211; no ID can be split across jobs.</p><p>Each of the 32 buckets ran as an independent job on a separate Kubernetes pod:</p><pre><code><code>bucket_filter = F.pmod(F.xxhash64(F.col("id")), F.lit(32)) == bucket

window = Window.partitionBy("id").orderBy(F.col("last_modified").desc())
deduped = (
    spark.read.parquet(source_path)
    .filter(bucket_filter)
    .withColumn("rn", F.row_number().over(window))
    .filter(F.col("rn") == 1)
    .drop("rn")
)
deduped.write.mode("append").parquet(destination_path)</code></code></pre><p>Up to 8 buckets ran in parallel, each writing to the same output prefix in append mode.</p><h3>Why didn&#8217;t scanning the full table cause OOM?</h3><p>Each job reads the entire 16B-row table to find its 1/32 slice. You&#8217;d expect that to cause memory problems &#8211; but it doesn&#8217;t.</p><p>Spark reads Parquet in <a href="https://spark.apache.org/docs/latest/sql-data-sources-parquet.html#vectorized-reader">columnar batches of 4096 rows by default</a>. Within each batch, <a href="https://www.databricks.com/blog/2016/05/23/apache-spark-as-a-compiler-joining-a-billion-rows-per-second-on-a-laptop.html">whole-stage code generation</a> fuses the scan and filter into a single compiled code path: rows that fail the predicate are skipped inline and never passed to the next operator as intermediate objects.</p><p>Rows that pass the filter are buffered in memory. Periodically, Spark checks whether the task can still acquire memory from the execution pool. When it can&#8217;t, the buffer is sorted and spilled to a temporary file on local disk, freeing memory for the next batch. At the end of the scan, all spill files and any remaining buffer are merged into the final <a href="https://spark.apache.org/docs/latest/rdd-programming-guide.html#shuffle-operations">shuffle output file</a>.</p><p>This is why memory doesn&#8217;t grow with table size: the read buffer is 4096 rows (by default), and the write buffer is kept bounded by the execution memory pool through incremental spills &#8211; not by the number of rows scanned. Disk accumulates the ~500M surviving rows for this bucket throughout the scan. Only once all map-side tasks are complete does <a href="https://github.com/apache/spark/blob/master/core/src/main/scala/org/apache/spark/scheduler/DAGScheduler.scala">the stage barrier release</a> and the second phase begin.</p><p>The two phases have different resource profiles:</p><ul><li><p><strong>Scan + shuffle write:</strong> memory bounded by execution pool; disk accumulates ~500M rows</p></li><li><p><strong>Sort + window:</strong> reads the shuffle files and sorts rows by id and last_modified; the window operator then evaluates row-by-row over the sorted partitions</p></li></ul><p>Per bucket, in our case ~500M rows fit comfortably within the 175 Gi ephemeral volume, with room for sort spill.. A single job on all 16B rows would produce ~32x more shuffle data on disk, well beyond 175 Gi, before the sort phase even starts.</p><pre><code><code>Phase 1 &#8211; Scan + shuffle write:
  S3 read -&gt; 4096-row batches (vectorized reader)
      -&gt; filter: 31/32 rows dropped via WSCG inline code
      -&gt; survivors -&gt; ExternalSorter (in-memory buffer)
      -&gt; buffer spills to disk when execution pool exhausted
      -&gt; repeat for all 16B rows
      -&gt; result: ~500M rows in shuffle files on local disk (~125 GB)

Phase 2 &#8211; Sort + window:
  [stage barrier: waits for all Phase 1 tasks to complete]
  read shuffle files -&gt; SortExec (UnsafeExternalSorter, spills if needed)
      -&gt; WindowExec: row-by-row over pre-sorted partitions
      -&gt; ROW_NUMBER, keep rn=1
      -&gt; write deduplicated rows to S3</code></code></pre><h2>Could the batching approach work in Athena?</h2><p>The truth is, it may. After ruling out the straightforward approaches and learning more about how Athena handles spilling to disk, we decided Spark would be a better tool here. Too much data? No problem &#8211; just spill to disk and wait 2 hours for deduplication to happen.</p><p>But if you still want to stay in Athena, you could try applying the same bucket filter there:</p><pre><code><code>SELECT * FROM (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY id ORDER BY last_modified DESC) AS rn
  FROM data_lake.customer
  WHERE abs(from_big_endian_64(xxhash64(to_utf8(cast(id AS varchar))))) % 32 = 0
)
WHERE rn = 1</code></code></pre><p>Run it 32 times, once per bucket. or even try increasing the number of buckets.</p><p>The unknowns are whether Athena&#8217;s <a href="https://trino.io/docs/current/admin/spill.html">limited window function spill support</a> holds up under that load, whether shared worker memory leaves enough headroom for your bucket size, and whether you stay within the <a href="https://docs.aws.amazon.com/athena/latest/ug/service-limits.html">30-minute query timeout</a> (though this one can be increased up to 240 min)</p><p>It might just work. Give it a try and see.</p>]]></content:encoded></item><item><title><![CDATA[ElasticSearch Federated Query via Athena Connector]]></title><description><![CDATA[Is it good? TL;DR no, not really :(]]></description><link>https://nikitavolodarskiy.substack.com/p/elasticsearch-federated-query-via</link><guid isPermaLink="false">https://nikitavolodarskiy.substack.com/p/elasticsearch-federated-query-via</guid><dc:creator><![CDATA[Nikita Volodarskiy]]></dc:creator><pubDate>Fri, 24 Apr 2026 15:36:49 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!xIbA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff62aa64d-5fa1-424a-bc5a-fe738ad42189_3592x1882.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When you need Elasticsearch data in Athena for analytics, the Athena Federated Query connector looks like the obvious first choice &#8211; no pipelines, no exports, just SQL on top of live ES data. This post covers what we ran into when we tried it, so you don&#8217;t have to spend the time on a PoC yourself.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xIbA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff62aa64d-5fa1-424a-bc5a-fe738ad42189_3592x1882.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xIbA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff62aa64d-5fa1-424a-bc5a-fe738ad42189_3592x1882.png 424w, https://substackcdn.com/image/fetch/$s_!xIbA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff62aa64d-5fa1-424a-bc5a-fe738ad42189_3592x1882.png 848w, https://substackcdn.com/image/fetch/$s_!xIbA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff62aa64d-5fa1-424a-bc5a-fe738ad42189_3592x1882.png 1272w, https://substackcdn.com/image/fetch/$s_!xIbA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff62aa64d-5fa1-424a-bc5a-fe738ad42189_3592x1882.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xIbA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff62aa64d-5fa1-424a-bc5a-fe738ad42189_3592x1882.png" width="1456" height="763" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f62aa64d-5fa1-424a-bc5a-fe738ad42189_3592x1882.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:763,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1381836,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://nikitavolodarskiy.substack.com/i/195357552?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff62aa64d-5fa1-424a-bc5a-fe738ad42189_3592x1882.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xIbA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff62aa64d-5fa1-424a-bc5a-fe738ad42189_3592x1882.png 424w, https://substackcdn.com/image/fetch/$s_!xIbA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff62aa64d-5fa1-424a-bc5a-fe738ad42189_3592x1882.png 848w, https://substackcdn.com/image/fetch/$s_!xIbA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff62aa64d-5fa1-424a-bc5a-fe738ad42189_3592x1882.png 1272w, https://substackcdn.com/image/fetch/$s_!xIbA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff62aa64d-5fa1-424a-bc5a-fe738ad42189_3592x1882.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Context</h2><p>Our Elasticsearch cluster has a handful of indexes that the analytics team needs to query in Athena. The smallest is ~50 MB; others on the average around 13 GB each. ES is optimized for search, not analytics, and doesn&#8217;t integrate with Athena natively &#8211; so the federated connector is the most direct path to bridge that gap.</p><h2>What Is Athena Federated Query</h2><p>Athena Federated Query lets you run SQL against external data sources without moving the data first. AWS maintains a set of Lambda-backed connectors for common sources: DynamoDB, RDS, Redis, and others. You deploy the connector as a Lambda function, register it as a data catalog in Athena, and then query it alongside your S3-based tables using standard SQL.</p><p>For Elasticsearch and OpenSearch, AWS provides the <a href="https://docs.aws.amazon.com/athena/latest/ug/connectors-elasticsearch.html">Amazon Athena connector for Elasticsearch/OpenSearch</a>, available through the Serverless Application Repository (SAR).</p><h2>Setup</h2><p>The connector is deployed from SAR with a handful of parameters:</p><ul><li><p><strong>ES endpoint</strong> &#8211; the cluster URL</p></li><li><p><strong>Secret ARN</strong> &#8211; credentials stored in AWS Secrets Manager (username/password or API key)</p></li><li><p><strong>Spill bucket and prefix</strong> &#8211; S3 location used when query result sets exceed Lambda memory limits</p></li><li><p><strong>Lambda function name</strong> &#8211; becomes your data catalog identifier in Athena</p></li></ul><p>Once deployed, you register the Lambda as a data source in the Athena console under <em>Data sources &#8594; Connect data source &#8594; Lambda</em>. After that, the ES indexes appear as databases and tables within Athena &#8211; no schema definition required, since the connector infers it from ES mappings (But there is a twist, wait for it).</p><h2>Two Connector Variants</h2><p>Two SAR applications exist for this connector:</p><p><strong>AthenaElasticsearchConnector</strong> &#8211; the standard variant. Connects to ES using the endpoint and credentials you provide. Straightforward to deploy and configure.</p><p><strong>AthenaElasticsearchConnectorWithGlueConnection</strong> &#8211; intended for setups where network connectivity is managed via a Glue Connection. This one could not be made to work. The likely cause is a known limitation documented by AWS:</p><blockquote><p><em>Due to a known issue, the OpenSearch connector cannot be used with a VPC.</em></p></blockquote><p>Since we connect to ES through a VPC, this variant is a dead end. All further testing used the standard connector.</p><h2>Problems</h2><h3>Permissions</h3><p>A minor friction point, but worth calling out upfront. The IAM permissions required to deploy the connector and register the Athena catalog are spread across a few services &#8211; Lambda, Glue, Athena, S3, Secrets Manager, and IAM itself. Before starting, review the <a href="https://docs.aws.amazon.com/athena/latest/ug/athena-catalog-access.html">permissions required to create a connector and Athena catalog</a> to avoid deploy failures mid-setup.</p><h3>Schema Inference Errors</h3><p>Here is the twist!</p><p>The connector infers the table schema directly from ES mappings at query time. This breaks when a field has inconsistent types across documents. For example, a field that is a single struct in some documents and an array in others.</p><p>The critical detail: <strong>the error triggers even when the problematic field is not selected in the query</strong>. The connector deserializes the full document before applying column projection, so any type inconsistency anywhere in the index will surface as a query error.</p><p>A partial workaround: create a Glue table manually with a schema that includes only the fields you need. The connector detects the Glue table and uses it as the schema, skipping deserialization of fields not listed. This works, but it adds ongoing maintenance overhead. Any new document with an inconsistent field on a column you do care about can still break queries unexpectedly.</p><h3>Blank Values</h3><p>One of the indexes returned the correct number of rows, but all field values were empty. The data was present in ES &#8211; the connector deserialized the documents without error but produced no values. We did not dig into the root cause because the next issue...</p><h3>Performance</h3><p>This was the deciding factor.</p><p>Index sizes and query time:</p><p> ~50 MB (small) ~15 minutes </p><p>~13 GB (can we call it large?) No result after 30 minutes</p><p>Athena Federated Query works by pushing the query down to the Lambda connector, which fetches data from ES in batches, spills large intermediate results to S3, and returns them to Athena for final aggregation. For large indexes, the sheer volume of data flowing through this path is the bottleneck &#8211; ES is not built for full-scan analytics queries, and the connector does not change that.</p><p>For a 50 MB index, 15 minutes is already unusable for an analytics workload. For 13 GB indexes, the connector did not complete within 30-minute, we decided not to wait longer.</p><h2>Assessment</h2><p>The federated connector is the right idea: live data, no pipeline to maintain, minimal setup. In practice, it ran into too many failures for our use case:</p><ul><li><p>VPC incompatibility ruled out one of the two connector variants entirely</p></li><li><p>Schema inference errors required a manual Glue schema workaround, with fragile ongoing behavior</p></li><li><p>One index returned blank results with no clear path to debug</p></li><li><p>Query performance was 15 minutes on a 50 MB index and didn&#8217;t finish on 13 GB indexes</p></li></ul><p>The connector may be viable for small, schema-consistent indexes where query latency requirements are loose. For anything larger or more complex, the performance characteristics make it unsuitable for production analytics use.</p>]]></content:encoded></item></channel></rss>