<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by saravanan aryan on Medium]]></title>
        <description><![CDATA[Stories by saravanan aryan on Medium]]></description>
        <link>https://medium.com/@saravanan.aryan?source=rss-4290c711ee7e------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*YtDW6EHRlIxXVaan.</url>
            <title>Stories by saravanan aryan on Medium</title>
            <link>https://medium.com/@saravanan.aryan?source=rss-4290c711ee7e------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 03:19:44 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@saravanan.aryan/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Development in dbt: A POC Using SQLMesh’s Virtual Environments]]></title>
            <link>https://medium.com/@saravanan.aryan/development-in-dbt-a-poc-using-sqlmeshs-virtual-environments-8ce7f26b0023?source=rss-4290c711ee7e------2</link>
            <guid isPermaLink="false">https://medium.com/p/8ce7f26b0023</guid>
            <category><![CDATA[dbt]]></category>
            <category><![CDATA[snowflake]]></category>
            <category><![CDATA[data-engineering]]></category>
            <category><![CDATA[data]]></category>
            <category><![CDATA[sqlmesh]]></category>
            <dc:creator><![CDATA[saravanan aryan]]></dc:creator>
            <pubDate>Fri, 02 Jan 2026 07:34:39 GMT</pubDate>
            <atom:updated>2026-01-06T10:49:33.624Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*v1AvW28nJvTXrZYx5AZ8Jg.png" /><figcaption>AI generated Image</figcaption></figure><p>Managing parallel development in extensive dbt projects is sometimes difficult. The complexity quickly escalates when multiple developers introduce simultaneous changes, especially when it which affects each others model.</p><p>The crucial question is: How can we reliably test individual changes without conflicts or the danger of compromising production data?</p><p>The effective approach I investigated involves <strong>SQLMesh</strong>, a data transformation tool that offers a powerful, native feature: <strong>Virtual Data Environments</strong>.</p><p>What are Virtual Data Environments?</p><p>The foundational principle is a sophisticated, yet simple, separation of concerns, brilliantly implemented using database views:</p><ol><li><strong>The Physical Layer:</strong> This layer holds the <em>materialized</em> objects, the actual logic.</li><li><strong>The Virtual Layer:</strong> This layer is made up <em>exclusively</em> of database views positioned atop the physical layer objects.</li></ol><p>To know more on Sqlmesh virtual environments please read the below article, its very detail</p><p><a href="https://www.tobikodata.com/blog/virtual-data-environments">Virtual Data Environments</a></p><p>This design is transformative, enabling every developer to branch off the main production environment and instantly spin up a dedicated, isolated development/testing environment. Importantly, this happens without hindering other developers work or affecting live production data.</p><h4>Testing SQLMesh with dbt: A POC</h4><p>Given that SQLMesh is fully capable of running dbt projects, I was able to smoothly incorporate these virtual environments into a standard dbt workflow.</p><p>For this POC, I utilised the standard <a href="https://github.com/dbt-labs/jaffle-shop-cloud-template">jaffle-sl-template</a> project, which includes:</p><ul><li>Sample e-commerce data seeding.</li><li>Raw data staging.</li><li>Building core customer and order data marts.</li></ul><p><strong>Initial Project Setup Steps</strong></p><p>Clone the repository and ensure our Snowflake connection credentials set correctly.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/948/1*JL5AnivNMuzN4CUQENdLqw.png" /><figcaption>profiles.yaml</figcaption></figure><p>set up your Python environment:</p><pre>python -m venv .venv<br>source .venv/bin/activate<br>pip install dbt-snowflake</pre><p>load the raw data tables:</p><pre>dbt seed</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/798/1*NUVHlmCSIM-5dBPJzpcTUw.png" /><figcaption>raw tables created in snowflake</figcaption></figure><p>Finally, materialize the initial staging and mart models:</p><pre>dbt run</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/704/1*hmkiCbZQdqRSVt0LNMX28A.png" /><figcaption>All objects created using dbt</figcaption></figure><p><strong>Implementing Development Isolation with SQLMesh</strong></p><p>With the dbt project operational, the goal is to utilize SQLMesh to establish isolated virtual environments for two distinct development tasks.</p><ol><li>Installing SQLMesh and nesessary libraries</li></ol><pre>pip install &quot;sqlmesh[snowflake]&quot;<br>pip install &quot;sqlmesh[dbt]&quot;</pre><p>2. Initializing the dbt Project for SQLMesh</p><p>Execute the sqlmesh init command <em>from the dbt project root</em>, selecting the dbt template:</p><pre>sqlmesh init -t dbt</pre><p>This command generates the sqlmesh.yaml configuration file. A key configuration to be observed in this file is:</p><pre>virtual_environment_mode: dev_only</pre><p>This setting ensures virtual environments are enabled only for development work, preserving the production environment’s integrity.So the production code still continues to work in dbt workflow. The other option is ‘full’ .</p><p>3. Creating the First Environment: <strong>feature-1234</strong></p><p>Say a developer starts a new feature, branches their Git repository, and establishes their isolated environment using sqlmesh’s command</p><blockquote>sqlmesh plan &lt;environment name&gt;</blockquote><pre>sqlmesh plan feature-1234</pre><p>Upon successful execution, new schemas for phsysical and virtual layers will be visible in the database.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1020/1*uBIisMX2xX3IS5nddXpxHA.png" /><figcaption>Physical layer</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/894/1*WY5G4_pQz5eXnSsTIkaV8Q.png" /><figcaption>Virtual layer</figcaption></figure><p><strong>The Architectural Breakdown (for feature-1234):</strong></p><ul><li><strong>Physical Layer:</strong> SQLMesh creates physical tables, typically prefixed with sqlmesh__, which contain the actual data.</li><li><strong>Virtual Layer:</strong> This is the view layer that points to it respective physical layer object . New schemas will appear, such as:</li><li>LIVE_STAGING__FEATURE_1234</li><li>LIVE_MARTS__FEATURE_1234</li></ul><p><strong>Importantly, every object within this virtual layer is a database view.</strong> Inspecting the view’s definition will show it is simply a SELECT * from the corresponding physical object.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RpPFh_fK1bMODDv2tp7C_w.png" /><figcaption>Virtual layer view pointing to physical layer object</figcaption></figure><p>4. Creating the Second Environment: <strong>feature-5678</strong></p><p>Now say a second developer now begins work on a completely separate feature:</p><pre>sqlmesh plan feature-5678</pre><p>This command creates another set of schemas for feature-5678,.</p><p>The feature-5678 environment introduces only new virtual schemas whose views still point to shared physical tables until code changes require new snapshots<strong>.</strong></p><p>If one examines a view in this new environment (e.g., in LIVE_STAGING__FEATURE_5678), its code will reference the <strong>exact same physical object</strong> that the feature-1234 environment references. This shared reference to unchanged data is the basis for the efficiency of virtual environments.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*z2dg0n4Hnlh9D0mZuyut0A.png" /></figure><p><strong>The Real Test: Isolated Model Changes</strong></p><p>Let’s check how this mechanism guarantees protection for parallel work streams.</p><p><strong>Developer A</strong> (in feature-1234) needs to modify stg_products.sql to add a new column: product_category (classifying products as ‘menu_item’ or ‘other’).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*q_LUTTHQaVdzrOwO5HWcCg.png" /><figcaption>a new column added</figcaption></figure><p>The developer updates the model code and re-runs the plan command:</p><pre>sqlmesh plan feature-1234</pre><p><strong>The Process in Action:</strong></p><p>When SQLMesh detects the code change, it <strong>generates a <em>new</em> physical model</strong> for stg_products with a unique fingerprint hash (e.g., a table ending with 374258167. The one ending with 417388839 is the old model without the new column.).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UqG_ygw3Tw44LVtX7Jydlg.png" /></figure><p>This new physical table incorporates the updated logic and the added column.</p><ul><li>The stg_products view in the <strong>feature-1234</strong> schema is automatically updated to point to this newly created, modified physical model.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tlZyJfHavo4nRYxTXOh0Hg.png" /></figure><ul><li><strong>Crucially,</strong> the stg_products view in the <strong>feature-5678</strong> schema <strong>continues to point to the <em>original</em> physical model</strong>, remaining completely unaffected by the change in feature-1234.</li></ul><p>Thus <strong>feature-1234</strong> and <strong>feature-5678 </strong>can be tested independently without affecting each other.</p><p><strong>Conclusion</strong>:</p><p>For scenarios involving multiple concurrent contributors, SQLMesh’s virtual environment feature can be an really helpful asset in effectively eliminating the risk of accidental dependencies and data corruption.</p><p>Considering the recent merger between Fivetran and dbt Labs, and Fivetran’s prior acquisition of SQLMesh, it’s an interesting possibility that this isolated, virtual data environments feature soon become a native or more tightly integrated feature within the dbt ecosystem itself.</p><p><strong>Disclaimer</strong>: I used AI tools like ChatGPT just to help with grammar and phrasing. The content, ideas, and all the work in this article are 100% my own.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8ce7f26b0023" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Unlocking Snowflake Performance: A Deep Dive into Micro-Partitions, Clustering, and Search…]]></title>
            <link>https://medium.com/@saravanan.aryan/unlocking-snowflake-performance-a-deep-dive-into-micro-partitions-clustering-and-search-e54f3186cfdc?source=rss-4290c711ee7e------2</link>
            <guid isPermaLink="false">https://medium.com/p/e54f3186cfdc</guid>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[snowflake]]></category>
            <category><![CDATA[data-analysis]]></category>
            <category><![CDATA[big-data]]></category>
            <category><![CDATA[data-engineering]]></category>
            <dc:creator><![CDATA[saravanan aryan]]></dc:creator>
            <pubDate>Sat, 26 Jul 2025 07:54:28 GMT</pubDate>
            <atom:updated>2025-07-26T09:29:12.073Z</atom:updated>
            <content:encoded><![CDATA[<h3>Unlocking Snowflake Performance: A Deep Dive into Micro-Partitions, Clustering, and Search Optimisation</h3><h4>Unlock faster analytics by mastering micro-partition pruning, clustering strategies, and search optimisation in Snowflake.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*6sMWjQyiqp3p5ppy.png" /><figcaption>AI generated Image</figcaption></figure><p><strong>Understanding micro-partitions and pruning</strong></p><p>Snowflake automatically divides all table data into micro-partitions. Micro-partitions are contiguous storage units that typically hold between 50 MB and 500 MB of uncompressed data (though Snowflake always stores this data in a compressed format). Each micro-partition organises groups of rows in a columnar structure, enabling highly granular pruning.</p><p>What makes this powerful is the metadata Snowflake stores for every micro-partition. This includes the range of values for each column, the number of distinct values, and other properties that help optimize and speed up query processing.</p><p>Let’s Look at an example Table</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fdatawrapper.dwcdn.net%2FeL994%2F1%2F&amp;display_name=Datawrapper&amp;url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FeL994%2F1%2F&amp;image=https%3A%2F%2Fdatawrapper.dwcdn.net%2FeL994%2Fplain-s.png%3Fv%3D1&amp;type=text%2Fhtml&amp;schema=dwcdn" width="600" height="404" frameborder="0" scrolling="no"><a href="https://medium.com/media/e5cf54a1ebe0955f7187c70de27bf225/href">https://medium.com/media/e5cf54a1ebe0955f7187c70de27bf225/href</a></iframe><p>Below is the conceptual representation of how the above data might look when stored in micro-partitions in snowflake.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fdatawrapper.dwcdn.net%2FrpdMO%2F2%2F&amp;display_name=Datawrapper&amp;url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FrpdMO%2F2%2F&amp;image=https%3A%2F%2Fdatawrapper.dwcdn.net%2FrpdMO%2Fplain-s.png%3Fv%3D2&amp;type=text%2Fhtml&amp;schema=dwcdn" width="600" height="230" frameborder="0" scrolling="no"><a href="https://medium.com/media/cd0f7e8f25d12750077ad70821bb9c3b/href">https://medium.com/media/cd0f7e8f25d12750077ad70821bb9c3b/href</a></iframe><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fdatawrapper.dwcdn.net%2FqXPHs%2F1%2F&amp;display_name=Datawrapper&amp;url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FqXPHs%2F1%2F&amp;image=https%3A%2F%2Fdatawrapper.dwcdn.net%2FqXPHs%2Fplain-s.png%3Fv%3D1&amp;type=text%2Fhtml&amp;schema=dwcdn" width="600" height="230" frameborder="0" scrolling="no"><a href="https://medium.com/media/328358958542f785e217016f0c514c7a/href">https://medium.com/media/328358958542f785e217016f0c514c7a/href</a></iframe><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fdatawrapper.dwcdn.net%2Fx55QH%2F2%2F&amp;display_name=Datawrapper&amp;url=https%3A%2F%2Fdatawrapper.dwcdn.net%2Fx55QH%2F2%2F&amp;image=https%3A%2F%2Fdatawrapper.dwcdn.net%2Fx55QH%2Fplain-s.png%3Fv%3D2&amp;type=text%2Fhtml&amp;schema=dwcdn" width="600" height="230" frameborder="0" scrolling="no"><a href="https://medium.com/media/62f3015076471527e3f1f4f576334a96/href">https://medium.com/media/62f3015076471527e3f1f4f576334a96/href</a></iframe><p>This data is stored across three micro-partitions, with the rows divided equally between each. The first three rows are stored in the first micro-partition, the following three rows in the second micro-partition, and the last three in the third micro-partition, as shown above.</p><p><strong>Pruning in Snowflake</strong></p><p>Because Snowflake tracks metadata for each micro-partition (like the range of values for every column), it can optimize queries by scanning only the micro-partitions that might contain the data we need. This process of excluding unnecessary partitions is called <strong>pruning</strong>.</p><p>For example, if we query for ID = 100 in the above table,Snowflake only reads micro-partitions 1 and 2, skipping the third entirely.</p><p>But if we filter by Date = &#39;Jan-10&#39;, since that date appears in all micro-partitions, the query must scan all of them. However, if micro-partitions were sorted by date instead of ID, the query could potentially scan fewer partitions, making it more efficient.</p><p><strong>Seeing Pruning in Action</strong></p><p>Let’s try this with a large dataset from Snowflake’s sample data:</p><pre>CREATE TABLE LINEITEM AS <br>SELECT * FROM SNOWFLAKE_SAMPLE_DATA.TPCH_SF1000.LINEITEM;</pre><p>This table has over 6 billion rows. Running a query like below can take a long time if the table isn’t well-clustered, because Snowflake may need to scan all partitions.</p><pre>SELECT * FROM LINEITEM WHERE L_SHIPDATE = &#39;1992-10-30&#39;;</pre><p>The above query ran for 1minute and 23 seconds in a X-Small warehouse.</p><p>If we click on the query id and see the statistics , we see the query has to scan all the 9,231 partitions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/364/1*AVB2l756dOGhhYv4WNDDrw.png" /></figure><p><strong>Optimising with Clustering</strong></p><p>Run the below query to check how well the table is clustered based on L_SHIPDATE.</p><pre>SELECT SYSTEM$CLUSTERING_INFORMATION(&#39;LINEITEM&#39;, &#39;(L_SHIPDATE)&#39;);</pre><pre>{<br>  &quot;cluster_by_keys&quot; : &quot;LINEAR(L_SHIPDATE)&quot;,<br>  &quot;total_partition_count&quot; : 9234,<br>  &quot;total_constant_partition_count&quot; : 0,<br>  &quot;average_overlaps&quot; : 9233.0,<br>  &quot;average_depth&quot; : 9234.0,<br>  &quot;partition_depth_histogram&quot; : {<br>    &quot;00000&quot; : 0,<br>    &quot;00001&quot; : 0,<br>    &quot;00002&quot; : 0,<br>    &quot;00003&quot; : 0,<br>    &quot;00004&quot; : 0,<br>    &quot;00005&quot; : 0,<br>    &quot;00006&quot; : 0,<br>    &quot;00007&quot; : 0,<br>    &quot;00008&quot; : 0,<br>    &quot;00009&quot; : 0,<br>    &quot;00010&quot; : 0,<br>    &quot;00011&quot; : 0,<br>    &quot;00012&quot; : 0,<br>    &quot;00013&quot; : 0,<br>    &quot;00014&quot; : 0,<br>    &quot;00015&quot; : 0,<br>    &quot;00016&quot; : 0,<br>    &quot;16384&quot; : 9234<br>  },<br>  &quot;clustering_errors&quot; : [ ]<br>}</pre><p>The <em>total_constant_partition_count</em> is 0, meaning the table is not well clustered (a larger value means the clustering is better).</p><p>The <em>average_overlaps</em> value is almost the same as the total_partition_count value, which means that nearly all micro-partitions overlap, and consequently, the table is not well clustered (a lower value means that clustering is better).</p><p>The <em>average_depth</em> value is the same as the <em>total_partition_count</em> value, meaning the table is not well clustered. A smaller value means the clustering is better.</p><p>Adding a clustering key can significantly improve query performance.</p><p>Once we define a clustering key, Snowflake will periodically reorganise data in the background to optimize micro-partitioning.</p><p>Lets cluster the LINEITEM table by by L_SHIPDATE:</p><pre>ALTER TABLE LINEITEM CLUSTER BY (L_SHIPDATE);</pre><p>The clustering should happen in the backend. We can monitor the status by running the below table function.</p><pre>select *<br>  from table(information_schema.automatic_clustering_history(<br>  date_range_start=&gt;dateadd(D, -1, current_date),<br>  table_name=&gt;&#39;TPCH_COPY.PRUNING.LINEITEM&#39;));</pre><p>We should see something like below once the clustering is completed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/962/1*oygLSf0k6-FNkmpEjifQxg.png" /></figure><p>Lets now run the same query again.</p><pre>select * from LINEITEM where L_SHIPDATE =&#39;1992-10-30&#39;;</pre><p>now the completed in just 4.2 seconds. The query benefits from partition pruning — out of 9,327 partitions, only 10 are scanned.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/400/1*GvxAcMNKEshIAJCfHbls4g.png" /></figure><h3>When to Use Clustering</h3><p>Keep these criteria in mind when deciding on clustering:</p><ul><li>The table contains a large number of micro-partitions, so pruning will be significant.</li><li>Queries that select a small portion of rows, retrieving a small number of micro-partitions.</li><li>If most of the queries filter by the same column (often a date or time period) ,adding that column to the cluster key can greatly benefit from partition pruning.</li><li>The number of distinct values in the clustering column is large enough for pruning, but small enough for Snowflake to colocate rows with the same key in the same micro-partition.</li></ul><p>Adding clustering keys and allowing Snowflake to maintain them automatically in the background incurs cost. So, understand the types of queries most often executed on the table.</p><p><strong>Search Optimisation in Snowflake</strong></p><p>Let’s say we want to filter the LINEITEM table based on the L_ORDERKEY column.</p><p>To improve the performance of queries that filter on L_ORDERKEY, we can’t cluster the LINEITEM table again, since it’s already clustered by L_SHIPDATE. Instead, we can take advantage of Snowflake’s <strong>search optimisation</strong>.</p><p>The search optimisation service boosts query performance when filtering data. When search optimisation is added to a table, Snowflake creates a search access path in the background. This access path keeps track of which values of the table’s columns reside in each micro-partition. As a result, micro-partition pruning becomes much more effective when querying the table with filters. Snowflake also maintains the search access path automatically as data is inserted, updated, or deleted.</p><p>We can add search optimisation to a table without specifying columns, in which case Snowflake builds the access path for all eligible columns. However, this can be costly. It’s usually better to add search optimisation only to the columns that are frequently used in filters.</p><p>lets run the below query to add search optimisation to the L_ORDERKEY column of the LINEITEM table</p><pre>ALTER TABLE LINEITEM ADD SEARCH OPTIMIZATION ON EQUALITY(L_ORDERKEY);</pre><p>Run the below query to monitor the progress</p><pre>SHOW TABLES LIKE &#39;LINEITEM&#39;;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/718/1*OMfxygDEmROLHe6XHesGLQ.png" /></figure><p>Now, let’s look at the difference in query performance before and after enabling search optimisation</p><pre>SELECT L_ORDERKEY FROM LINEITEM WHERE L_ORDERKEY = 5461081319;</pre><p>The query ran for 31 secs before search optimisation. It has to scan almost all micro-partitions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/380/1*W5PTIRWJsiKqrr-qpIhHvQ.png" /><figcaption>before</figcaption></figure><p>Same query ran for only 991 ms after search optimisation</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/496/1*1pDPJxJ0ha_nYuTah_Eefg.png" /><figcaption>after</figcaption></figure><p>As we can see in the statistics pane, the number of scanned partitions is 3, while the total number of partitions is 9399. This indicates that search optimisation improved query performance by using micro-partition pruning.</p><p><strong>Tips for adding search optimization:</strong></p><ul><li>The table has a large number of micro-partitions.</li><li>Queries use equality filters.</li><li>The filter in the query returns only a small subset of rows.</li><li>The table is already clustered, but the columns used in filters are not the clustering key.</li></ul><p><strong>Conclusion</strong></p><p>By understanding how micro-partitions work, applying clustering where it helps, and using search optimisation for specific needs, we can make your queries run much faster. Just remember, both clustering and search optimisation can add extra costs, so use them where they make the most impact.</p><h3>References</h3><ul><li><a href="https://learning.oreilly.com/library/view/snowflake-data-engineering/9781633436855/Text/chapter-8.html#sigil_toc_id_106">Snowflake Data Engineering (O’Reilly) — Chapter 8: Clustering and Pruning</a></li><li><a href="https://docs.snowflake.com/en/user-guide/tables-clustering-micropartitions">Snowflake Documentation: Clustering &amp; Micro-Partitions</a></li><li><a href="https://docs.snowflake.com/en/user-guide/tables-clustering-keys">Snowflake Documentation: Clustering Keys</a></li><li><a href="https://docs.snowflake.com/en/user-guide/search-optimization-service">Snowflake Documentation: Search Optimization Service</a></li></ul><p><strong>Disclaimer</strong>: I used AI tools like ChatGPT just to help with grammar and phrasing. The content, ideas, and all the work in this article are 100% my own.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e54f3186cfdc" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Achieving True Compute Decoupling in Lakehouse Architecture with SQLmesh]]></title>
            <link>https://medium.com/@saravanan.aryan/achieving-true-compute-decoupling-in-lakehouse-architecture-with-sqlmesh-abd9902cceac?source=rss-4290c711ee7e------2</link>
            <guid isPermaLink="false">https://medium.com/p/abd9902cceac</guid>
            <category><![CDATA[data-engineering]]></category>
            <category><![CDATA[sqlmesh]]></category>
            <category><![CDATA[data-lakehouse]]></category>
            <category><![CDATA[icebergs]]></category>
            <category><![CDATA[sql]]></category>
            <dc:creator><![CDATA[saravanan aryan]]></dc:creator>
            <pubDate>Sun, 20 Apr 2025 15:06:57 GMT</pubDate>
            <atom:updated>2025-04-20T15:13:24.979Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*RbJxLA2dJKUUxudz" /><figcaption>Image generated by AI</figcaption></figure><p>In recent years, the lakehouse architecture has gained significant attention in the data engineering world. A lakehouse combines the best features of data lakes and data warehouses. It brings the ACID principles of data warehousing while supporting unstructured and semi-structured data, all stored in cost-effective storage solutions like AWS S3.</p><p>The technology around lakehouse is evolving rapidly, with numerous tools emerging to query data from lakehouses efficiently, reducing latency. At its core, the lakehouse architecture is known for its decoupled nature of storage and compute. This means you can use inexpensive storage solutions and pair them with any query engine to process the data. This flexibility allows organizations to switch query engines in the future based on their needs, avoiding vendor lock-in.</p><p><strong>The Promise of Decoupling: Are We There Yet?</strong></p><p>One of the most significant benefits of the lakehouse architecture is the ability to switch query engines without being locked into a single tool. However, are we truly realising this benefit? <strong>The answer is: not entirely</strong>.</p><p>Modern data engineering is not just about running ad-hoc queries. It involves building and maintaining pipelines that run on a schedule, whether in batch or streaming mode. These pipelines could be ETL (Extract, Transform, Load) or ELT (Extract, Load, Transform).</p><p>For example, in an ELT pipeline, raw data is first loaded into an open table format like Apache Iceberg using engine like apache spark. From there, layers of transformations are applied to prepare the data for use in BI tools. Typically, these transformations are written as SQL queries.</p><p>Consider the following example of a transformation pipeline:</p><p>Bronze to Silver Layer (Spark SQL):</p><pre>INSERT INTO silver.cleaned_transactions<br>SELECT <br>    transaction_id,<br>    order_date,<br>    customer_id,<br>    product_id,<br>    quantity,<br>    unit_price,<br>    quantity * unit_price as total_amount,<br>    UPPER(payment_method) as payment_method,<br>    source_system,<br>    CASE <br>        WHEN quantity &gt; 0 AND unit_price &gt; 0 THEN TRUE <br>        ELSE FALSE <br>    END as is_valid<br>FROM bronze.raw_transactions;</pre><p>Silver to Gold Layer (Spark SQL):</p><pre>INSERT INTO gold.daily_metrics<br>SELECT <br>    order_date,<br>    COUNT(DISTINCT transaction_id) as total_orders,<br>    COUNT(DISTINCT customer_id) as total_customers,<br>    SUM(total_amount) as total_revenue,<br>    AVG(total_amount) as avg_order_value<br>FROM silver.cleaned_transactions<br>WHERE is_valid = TRUE<br>AND date_add(order_date, 7) BETWEEN @start_date AND @end_date<br>GROUP BY order_date;</pre><p>In practice, tools like <a href="https://docs.getdbt.com/"><strong>DBT(Data Build Tool)</strong></a><strong> </strong>are often used to manage these SQL transformations and streamline workflows. However, with time hundreds of similar pipelines are deployed in production using a specific query engine (e.g., Spark) dialect, a challenge arises: <strong>what happens if the organisation decides to switch to a different query engine, such as DuckDB or Snowflake or Trino?</strong></p><p>This would require rewriting all the SQL queries to match the syntax of the new engine. The effort involved in such a migration can be enormous, negating the flexibility promised by the lakehouse architecture.Can we decouple the sql dialect from query engines?</p><h3><strong>SQLMesh: A Solution for True Decoupling</strong></h3><p>SQLMesh is an open-source transformation tool that we can use to address this problem. Similar to <a href="https://docs.getdbt.com/">DBT</a> but with added intelligence, SQLMesh leverages the <a href="https://github.com/tobymao/sqlglot">sqlglot</a> library, which can parse, interpret, and transpile SQL code across different dialects. This makes it possible to decouple your pipelines from a specific query engine, allowing seamless transitions between engines without breaking anything.</p><p>SQLMesh is not just a transformation tool — it’s a robust DataOps platform built to handle the complexities of modern data engineering. While DBT has been a pioneer in the transformation space, it struggles to scale with increasing data and organizational complexity. SQLMesh, on the other hand, was designed from the ground up to address these challenges.</p><p><em>Note : Sqlmesh has lots of cool features but in this article we are mainly focusing on the dialect transpilation . Please refer documentation for more details.</em></p><p><strong>Using SQLMesh to Decouple Compute</strong></p><p>To use SQLMesh, we define your transformations in its specific format. Let’s revisit the earlier example and convert it into SQLMesh models.</p><p><strong>Bronze Layer: </strong><strong>bronze.raw_transactions</strong></p><p>This is a seed model to load raw data from a CSV file.</p><pre>MODEL (<br>  name bronze.raw_transactions,<br>  kind SEED (<br>    path &#39;../seeds/seed_data.csv&#39;<br>  ),<br>  columns (<br>    transaction_id VARCHAR,<br>    order_date DATE,<br>    customer_id VARCHAR,<br>    product_id VARCHAR,<br>    quantity INTEGER,<br>    unit_price FLOAT,<br>    payment_method VARCHAR,<br>    source_system VARCHAR<br>  ),<br>  grain (transaction_id)<br>);</pre><p><strong>Silver Layer: </strong><strong>silver.cleaned_transactions</strong></p><pre>MODEL (<br>  name silver.cleaned_transactions,<br>  kind INCREMENTAL_BY_TIME_RANGE (<br>    time_column order_date<br>  ),<br>  start &#39;2024-01-01&#39;,<br>  cron &#39;@daily&#39;,<br>  grain (transaction_id)<br>);<br><br>SELECT <br>    transaction_id,<br>    order_date,<br>    customer_id,<br>    product_id,<br>    quantity,<br>    unit_price,<br>    quantity * unit_price as total_amount,<br>    UPPER(payment_method) as payment_method,<br>    source_system,<br>    CASE <br>        WHEN quantity &gt; 0 AND unit_price &gt; 0 THEN TRUE <br>        ELSE FALSE <br>    END as is_valid<br>FROM bronze.raw_transactions<br>WHERE order_date BETWEEN @start_date AND @end_date;</pre><p><strong>Gold Layer: </strong><strong>gold.daily_sales_summary</strong></p><pre>MODEL (<br>  name gold.daily_sales_summary,<br>  kind INCREMENTAL_BY_TIME_RANGE (<br>    time_column order_date<br>  ),<br>  start &#39;2024-01-01&#39;,<br>  cron &#39;@daily&#39;,<br>  grain (transaction_id)<br>);<br><br>SELECT <br>    order_date,<br>    COUNT(DISTINCT transaction_id) as total_orders,<br>    COUNT(DISTINCT customer_id) as total_customers,<br>    SUM(total_amount) as total_revenue,<br>    AVG(total_amount) as avg_order_value<br>FROM silver.cleaned_transactions<br>WHERE is_valid = TRUE<br>AND date_add(order_date, 7) BETWEEN @start_date AND @end_date<br>GROUP BY order_date;</pre><p><strong>Transpiling SQL Across Engines</strong></p><p>Below is the SQLmesh configuration file, where we define the connection details along with other details:</p><pre>gateways:<br>  local_spark_connection:<br>    connection:<br>      type: spark<br>    state_connection:<br>      type: duckdb<br>      database: spark_state.db<br>  my_snowflake_connection:<br>    connection:<br>      type: snowflake<br>      account: idapznw-wq29399<br>      user: DEMO_USER<br>      password: &lt;&lt; password here &gt;&gt;<br>      database: DEMO_DB<br>      warehouse: COMPUTE_WH<br>    state_connection:<br>      type: duckdb<br>      database: snowflake_state.db<br>  duckdb_connection:<br>    connection:<br>      type: duckdb<br>      catalogs:<br>        persistent: &#39;local.duckdb&#39;<br>        ephemeral: &#39;:memory:&#39;<br>    state_connection:<br>      type: duckdb<br>      database: duckdb_state.db<br><br>default_gateway: local_spark_connection</pre><p>For example, in the provided configuration, three connections are defined:</p><ol><li><strong>local_spark_connection</strong>: A connection to a Spark environment.</li><li><strong>my_snowflake_connection</strong>: A connection to a Snowflake environment.</li><li><strong>duckdb_connection</strong>: A connection to a DuckDB environment.</li></ol><p>The default_gateway in the configuration is set to local_spark_connection, meaning that by default, SQLMesh will deploy the models to the Spark environment. When you run the <a href="https://sqlmesh.readthedocs.io/en/latest/concepts/plans/">sqlmesh plan</a> command, SQLMesh processes the models and generates SQL queries tailored to the default gateway&#39;s dialect (in this case, Spark SQL). For example, if the gold.daily_sales_summary model is deployed to Spark, the query might look like this:</p><pre>SELECT<br>  `cleaned_transactions`.`order_date` AS `order_date`,<br>  COUNT(DISTINCT `cleaned_transactions`.`transaction_id`) AS `total_orders`,<br>  COUNT(DISTINCT `cleaned_transactions`.`customer_id`) AS `total_customers`,<br>  SUM(`cleaned_transactions`.`total_amount`) AS `total_revenue`,<br>  AVG(`cleaned_transactions`.`total_amount`) AS `avg_order_value`<br>FROM `catalog`.`silver`.`cleaned_transactions` AS `cleaned_transactions`<br>WHERE<br>  `cleaned_transactions`.`is_valid` = TRUE<br>  AND DATE_ADD(`cleaned_transactions`.`order_date`, 7) &lt;= CAST(&#39;1970-01-01&#39; AS DATE)<br>  AND DATE_ADD(`cleaned_transactions`.`order_date`, 7) &gt;= CAST(&#39;1970-01-01&#39; AS DATE)<br>GROUP BY<br>  `cleaned_transactions`.`order_date`;</pre><p>Here, the query uses Spark-specific syntax, such as DATE_ADD.</p><p>Now, if you decide to switch the default_gateway to my_snowflake_connection in the yaml file and run the sqlmesh plan command again, SQLMesh recognizes that the target environment is Snowflake. <strong>It automatically transpiles the Spark SQL query into Snowflake SQL, ensuring compatibility with Snowflake&#39;s dialect</strong>. For example, the same query for the gold.daily_sales_summary model would be rendered as:</p><pre>SELECT<br>  &quot;cleaned_transactions&quot;.&quot;order_date&quot; AS &quot;order_date&quot;,<br>  COUNT(DISTINCT &quot;cleaned_transactions&quot;.&quot;transaction_id&quot;) AS &quot;total_orders&quot;,<br>  COUNT(DISTINCT &quot;cleaned_transactions&quot;.&quot;customer_id&quot;) AS &quot;total_customers&quot;,<br>  SUM(&quot;cleaned_transactions&quot;.&quot;total_amount&quot;) AS &quot;total_revenue&quot;,<br>  AVG(&quot;cleaned_transactions&quot;.&quot;total_amount&quot;) AS &quot;avg_order_value&quot;<br>FROM &quot;catalog&quot;.&quot;silver&quot;.&quot;cleaned_transactions&quot; AS &quot;cleaned_transactions&quot;<br>WHERE<br>  &quot;cleaned_transactions&quot;.&quot;is_valid&quot; = TRUE<br>  AND DATEADD(DAY, 7, CAST(CAST(&quot;cleaned_transactions&quot;.&quot;order_date&quot; AS TIMESTAMP) AS DATE)) &lt;= TRY_CAST(&#39;1970-01-01&#39; AS DATE)<br>  AND DATEADD(DAY, 7, CAST(CAST(&quot;cleaned_transactions&quot;.&quot;order_date&quot; AS TIMESTAMP) AS DATE)) &gt;= TRY_CAST(&#39;1970-01-01&#39; AS DATE)<br>GROUP BY<br>  &quot;cleaned_transactions&quot;.&quot;order_date&quot;;</pre><blockquote>Notice how the DATE_ADD function from Spark is automatically converted to Snowflake&#39;s DATEADD function, which uses a different syntax. This transpilation happens seamlessly, saving you from manually rewriting the query.</blockquote><p>Similarly, if you switch the default_gateway to duckdb_connection and run the sqlmesh plan command, SQLMesh will transpile the query into DuckDB&#39;s dialect. For example:</p><pre>SELECT<br>  &quot;cleaned_transactions&quot;.&quot;order_date&quot; AS &quot;order_date&quot;,<br>  COUNT(DISTINCT &quot;cleaned_transactions&quot;.&quot;transaction_id&quot;) AS &quot;total_orders&quot;,<br>  COUNT(DISTINCT &quot;cleaned_transactions&quot;.&quot;customer_id&quot;) AS &quot;total_customers&quot;,<br>  SUM(&quot;cleaned_transactions&quot;.&quot;total_amount&quot;) AS &quot;total_revenue&quot;,<br>  AVG(&quot;cleaned_transactions&quot;.&quot;total_amount&quot;) AS &quot;avg_order_value&quot;<br>FROM &quot;catalog&quot;.&quot;silver&quot;.&quot;cleaned_transactions&quot; AS &quot;cleaned_transactions&quot;<br>WHERE<br>  &quot;cleaned_transactions&quot;.&quot;is_valid&quot; = TRUE<br>  AND CAST(&quot;cleaned_transactions&quot;.&quot;order_date&quot; AS DATE) + INTERVAL 7 DAY &lt;= TRY_CAST(&#39;1970-01-01&#39; AS DATE)<br>  AND CAST(&quot;cleaned_transactions&quot;.&quot;order_date&quot; AS DATE) + INTERVAL 7 DAY &gt;= TRY_CAST(&#39;1970-01-01&#39; AS DATE)<br>GROUP BY<br>  &quot;cleaned_transactions&quot;.&quot;order_date&quot;;</pre><p>In this case, the DATE_ADD function is replaced with + INTERVAL 7 DAY, which is the syntax used by DuckDB.</p><h3>Why This Matters</h3><p>This automatic transpilation ensures that your pipelines remain functional regardless of the query engine you use. You can switch between Spark, Snowflake, DuckDB, or any other supported engine without rewriting your SQL queries. SQLMesh handles the heavy lifting of adapting the SQL syntax to match the target engine’s requirements, allowing you to fully leverage the decoupling benefits of the lakehouse architecture.</p><p>By abstracting away the complexities of SQL dialect differences, SQLMesh makes it easier to build and maintain flexible, future-proof data pipelines. Whether you’re migrating to a new query engine or running a multi-engine setup, SQLMesh ensures that your transformations remain consistent and reliable.</p><h3>Disclaimer</h3><p>I used AI tools like ChatGPT just to help with grammar and phrasing. The content, ideas, and all the work in this article are 100% my own.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=abd9902cceac" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Transforming Apache Iceberg Metadata into Actionable Insights]]></title>
            <link>https://medium.com/@saravanan.aryan/transforming-apache-iceberg-metadata-into-actionable-insights-f5598f642ef9?source=rss-4290c711ee7e------2</link>
            <guid isPermaLink="false">https://medium.com/p/f5598f642ef9</guid>
            <category><![CDATA[snowflake]]></category>
            <category><![CDATA[data-engineering]]></category>
            <category><![CDATA[streamlit]]></category>
            <category><![CDATA[apache-iceberg]]></category>
            <category><![CDATA[data-lakehouse]]></category>
            <dc:creator><![CDATA[saravanan aryan]]></dc:creator>
            <pubDate>Sun, 17 Nov 2024 16:10:05 GMT</pubDate>
            <atom:updated>2024-11-20T02:43:09.442Z</atom:updated>
            <content:encoded><![CDATA[<p>Deep Dive into Iceberg Table Metrics with Streamlit</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zyHGtgBSDMPKD-nWqmAL-Q.jpeg" /><figcaption>AI generated image</figcaption></figure><p><strong>Introduction</strong></p><p><strong>Apache Iceberg tables</strong> include a layer of metadata that helps data engineers to <strong>optimize the table for better performance</strong>. This metadata is collected and stored whenever an operation occurs in the table, such as append or overwrite. As a result, <strong>accessing this metadata is less costly than scanning the entire table.</strong></p><p>This metadata provides excellent insights into the data in the table, such as:</p><ul><li>The <strong>number and size of data files.</strong></li><li><strong>Partitioning details</strong> of the table.</li><li><strong>Frequency of operations</strong> performed on the table.</li><li><strong>Evolution of table and partition columns</strong> over time.</li><li><strong>Snapshot history</strong> of the table, and much more.</li></ul><p>However, understanding these metrics can be difficult at times. With so many tables and so much metadata information, it can be <strong>overwhelming to make sense of it all.</strong> In this blog, I will explain how I used <strong>Streamlit</strong> to create an insightful dashboard that visualizes these metrics and serves as a <strong>useful tool</strong>. This dashboard can be used in production to monitor and maintain Apache Iceberg tables. It also helps to <strong>proactively optimize tables for faster performance</strong> and <strong>resolve existing issues.</strong></p><p>The <strong>Streamlit dashboard</strong> I created ,the <strong>main page</strong> of the dashboard provides an overall summary of metrics for all tables.</p><p><strong>Total Counts:</strong> Displays total counts of tables, active files, and cumulative sizes.</p><p><strong>Real-Time Monitoring:</strong> Includes a <strong>last updated timestamp.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*upBWtnGDOmQoz2Hh59B98Q.png" /><figcaption>image showing the list of tables and the main metrics panel here</figcaption></figure><p>Left side of the page lists of all tables in the namespace .Users can search or click on each table to view its metrics individually.</p><h3>Table Summary Metrics</h3><p>The first section of the dashboard provides a <strong>summary of the table’s key metrics</strong>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*El7dXBmgSr8QnJT699gmOQ.png" /><figcaption>Screenshot of User table summary section</figcaption></figure><h4>Active/Total Files &amp; Active/Total Storage</h4><ul><li>These cells provide insight into the <strong>number of active and inactive files</strong> in the table.</li><li><strong>Files are marked Inactive </strong>whenever the table is updated, deleted, or undergoes a similar operation. Since Iceberg data lakes are <strong>immutable</strong>, operations like updates or deletes don’t modify the existing files. Instead, they create <strong>new files</strong> with the updated records and update the metadata to reflect this change.</li><li><strong>Inactive files</strong> are kept rather than deleted immediately to support <strong>time travel functionality.</strong> However, maintaining a balance in managing inactive files is essential.</li></ul><h4>Total Records</h4><ul><li>Displays the <strong>total number of records</strong> in the table.</li></ul><h4>First Snapshot</h4><ul><li>Shows the <strong>first accessible snapshot</strong> for the table.</li></ul><h4>Last Snapshot</h4><ul><li>Indicates the <strong>last modification date</strong> of the table.</li></ul><h4>Total Snapshots</h4><ul><li>Shows the <strong>total number of snapshots</strong> available for the table. Running a <strong>vacuum job</strong> to remove inactive files may reduce this count.</li></ul><h4>Average File Size</h4><ul><li>Displays the <strong>average size of data files</strong> in the table.</li></ul><h3>Daily Growth Metrics</h3><p>The next section provides <strong>daily growth metrics</strong> to track how the table has grown over time.Beyond static metrics, the daily growth section gives a detailed view of how our Iceberg tables evolve over time.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*irIJmELLp98oPELSHWM8jg.png" /><figcaption>Screenshot of growth metrics Analysis</figcaption></figure><h4>Key Features:</h4><p><strong>Daily Growth Statistics</strong>:</p><p>Tracks the <strong>daily growth of records, files, and table size.</strong></p><p><strong>Timeframe Selector</strong>:</p><ul><li>Users can analyze data over specific timeframes such as <strong>1 day, 5 days, 1 month, 6 months, 1 year, or all time.</strong></li><li>Highlights the difference from the <strong>previous day</strong> using green (increase) or red (decrease).</li></ul><p><strong>Growth Visualization Charts</strong>:</p><ul><li><strong>Records Growth (Line Chart):</strong> Displays the total records and new records added each day. The <strong>solid line </strong>represents cumulative totals, while the <strong>dashed line</strong> shows daily additions.</li><li><strong>Files Growth (Line Chart):</strong> Displays the total number of files and files added daily, helping to identify <strong>file creation patterns.</strong></li><li><strong>Size Growth (Line Chart):</strong> Tracks the <strong>total table size</strong> and daily size increases. This is crucial for understanding <strong>storage trends</strong> and optimising efficiency.</li></ul><h4>Purpose and Benefits:</h4><ul><li><strong>Daily Monitoring:</strong> Enables teams to monitor the <strong>daily growth</strong> of records, files, and table size, offering a detailed view of the table’s evolution.</li><li><strong>Trend Analysis:</strong> Helps users easily identify <strong>growth patterns</strong> or anomalies, such as record count spikes or unusual file additions.</li></ul><h3>Snapshot Analysis</h3><p>This section focuses on <strong>snapshot history analysis</strong>, providing detailed insights into changes and operations performed over time. It also allows users to evaluate snapshots before <strong>time travel</strong> to a specific snapshot.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*O0gpERQp91HrAKJ3eggwcA.png" /><figcaption>Screenshot of snapsht analysis</figcaption></figure><h4>Key Features:</h4><p><strong>Snapshot History Analysis:</strong></p><ul><li><strong>Start and End Date Selector:</strong> Filters snapshot activities within a specific time range.</li><li><strong>Filter by Operation Type:</strong> Users can filter data by operations such as append and overwrite.</li><li><strong>Records Added and Deleted (Line Chart):</strong> Shows the number of records added and deleted for each snapshot. Added and deleted records are plotted separately for a clear view of <strong>data growth or reduction patterns.</strong></li><li><strong>Operation Timeline (Scatter Plot):</strong> Visualizes operations (e.g., append, overwrite, delete) with distinct markers for each type, making it easy to analyse their order and frequency.</li></ul><p><strong>Choose Snapshot for Time Travel:</strong></p><ul><li><strong>Snapshot Dropdown:</strong> Allows users to select a specific snapshot.</li><li><strong>Operational Details:</strong> Displays the <strong>operation type</strong> and the <strong>number of records added or deleted</strong> for the selected snapshot.</li><li><strong>Time Travel Button:</strong> Enables users to revert to a specific snapshot directly from the dashboard.</li></ul><p><strong>Operation Counts Over Time (Bar Chart):</strong></p><ul><li>Displays the <strong>daily count</strong> of each operation type, helping users understand the <strong>frequency and distribution</strong>of operations over a given period.</li></ul><h3>Partition Analysis</h3><p>The <strong>Partition Analysis</strong> section provides insights into how data is distributed across partitions in an Iceberg table.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*C-5TbXlq2g0fFbPhihKIcw.png" /><figcaption>Screenshot of partiton summary</figcaption></figure><h4>Key Features:</h4><p><strong>Partition Scheme Summary:</strong></p><ul><li>Offers an overview of the <strong>partitioning scheme</strong> and its evolution over time.</li><li>Displays the <strong>number of records</strong> for each partition scheme.</li></ul><p><strong>Partition Size and Record Count (Bar and Line Chart):</strong></p><ul><li><strong>Bar Chart (Partition Size):</strong> Blue bars show the <strong>total size of each partition.</strong></li><li><strong>Record Count (Line Chart):</strong> A red line overlaid on the bar chart represents the <strong>record count</strong> for each partition.</li><li>Helps identify <strong>skewed partitions</strong> for optimization.</li></ul><p><strong>File Count Distribution (Pie Chart):</strong></p><p>Displays the <strong>distribution of files</strong> by partition, with each segment representing a specific partition</p><h3>File Size Analysis</h3><p>Managing file sizes is critical to maintaining <strong>query performance</strong> and minimising I/O overhead. This section focuses on analysing file sizes in detail.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7j0xX-daGyqqSnPDHIL2TQ.png" /><figcaption>Screen shot of file size analysis</figcaption></figure><h4>Key Features:</h4><h4><strong>Overall File Size Deviation (Left Scatter Plot):</strong></h4><ul><li>Each dot represents an individual file.</li><li>A red dashed line shows the <strong>average file size,</strong> helping to identify <strong>outliers</strong> that are too small or too large.</li></ul><p><strong>Partition-Specific File Size Distribution (Right Scatter Plot):</strong></p><ul><li>Users can select a specific partition from a dropdown to drill down into its file size distribution.</li><li>Similar to the overall scatter plot, this chart shows file sizes within the selected partition.</li></ul><p><strong>File Compaction Job Trigger:</strong></p><ul><li><strong>Run Compaction Job Button:</strong> Allows users to consolidate small files into larger ones, improving <strong>query performance</strong> and <strong>storage efficiency.</strong></li></ul><p>This dashboard transforms Apache Iceberg’s rich metadata into a practical, easy-to-use tool for monitoring, optimising, maintaining and take action on tables. By visualising key metrics, it simplifies complex operations and empowers data engineers to take timely action for better performance and efficiency.There are still more metrics that can be derived from the metadata, which can be transformed into useful visualisations to provide even deeper insights into an iceberg table.</p><p>We’ll see the code to get the metrics and this dashboard in the next blog.</p><h3>References</h3><ul><li><a href="https://learning.oreilly.com/library/view/apache-iceberg-the/9781098148614/">Apache Iceberg: The Definitive Guide</a></li><li><a href="https://aws.amazon.com/blogs/big-data/monitoring-apache-iceberg-metadata-layer-using-aws-lambda-aws-glue-and-aws-cloudwatch/">Monitoring Apache Iceberg with AWS Lambda, Glue, and CloudWatch</a></li></ul><h3>Disclaimer</h3><p>I used AI tools like ChatGPT just to help with grammar and phrasing. The content, ideas, and all the work in this article are 100% my own.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f5598f642ef9" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Snowflake vs. Iceberg: A Performance Showdown on a Billion-Row Dataset]]></title>
            <link>https://medium.com/@saravanan.aryan/snowflake-vs-iceberg-a-performance-showdown-on-a-billion-row-dataset-ae3571f0d02b?source=rss-4290c711ee7e------2</link>
            <guid isPermaLink="false">https://medium.com/p/ae3571f0d02b</guid>
            <category><![CDATA[apache-iceberg]]></category>
            <category><![CDATA[apache-spark]]></category>
            <category><![CDATA[data-engineering]]></category>
            <category><![CDATA[snowflake]]></category>
            <category><![CDATA[lakehouse-architecture]]></category>
            <dc:creator><![CDATA[saravanan aryan]]></dc:creator>
            <pubDate>Tue, 17 Sep 2024 04:36:23 GMT</pubDate>
            <atom:updated>2024-09-20T11:05:56.316Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*d7fWXFRF9o6z4QBs" /><figcaption>image from google</figcaption></figure><h3>Table of Contents</h3><ol><li><strong>Introduction</strong></li><li><strong>Dataset Overview</strong></li><li><strong>Comparison Results</strong></li><li><strong>Key Observations</strong></li><li><strong>Data and Setup Process</strong></li><li><strong>Break Down of Queries</strong></li><li><strong>Final Verdict</strong></li></ol><p><strong>Introduction</strong></p><p>When working with large datasets, query performance is critical for optimizing time and resources. In this article, we compare the <strong>performance differences</strong> between querying a <strong>native Snowflake table</strong> and querying an <strong>Iceberg table</strong>, both using Snowflake.</p><p>Snowflake’s proprietary storage format is designed for <strong>fast querying</strong> and <strong>efficient data management</strong>, while <strong>Iceberg </strong>offers flexibility with open-source table formats, often stored on external systems like S3. By running similar queries on both types of tables from snowflake, we’ll highlight the differences in:</p><ul><li><strong>Query Execution Time</strong>: How quickly queries return results.</li><li><strong>Data Scanned</strong>: The amount of data read from storage.</li><li><strong>Partitioning Efficiency</strong>: How efficiently data is retrieved.</li></ul><p><strong>Note</strong>: To ensure accurate testing, <strong>I turned off the cache</strong> in Snowflake by running:</p><pre>ALTER SESSION SET USE_CACHED_RESULT = FALSE;</pre><h4>Dataset Overview</h4><p>For this performance comparison, I used a dataset containing <strong>1 billion rows</strong> of temperature measurements across various weather stations. You can access the dataset from the <a href="https://github.com/gunnarmorling/1brc?tab=readme-ov-file">1 Billion Row Challenge GitHub repository</a>.</p><p>Each row has a station_name and temperature, formatted as &lt;station_name&gt;;&lt;temperature&gt;, with the temperature having one decimal place.</p><pre>Hamburg;12.0<br>Bulawayo;8.9<br>Palembang;38.8<br>St. John&#39;s;15.2</pre><p>The dataset is around <strong>15 GB</strong>, but after loading it into Snowflake and Iceberg, the sizes were compressed to <strong>2.6 GB</strong> for the native Snowflake table and <strong>3.3 GB</strong> for the Iceberg table on S3.</p><p><strong><em>Before we unravel the implementation behind this comparison, let’s jump right into the results and see how Snowflake native tables stack up against Iceberg tables queried in Snowflake.</em></strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cP0KTYdWkJDEZXEjC59oTg.png" /><figcaption>Query Runtime Comparison: This bar chart compares the performance of querying native Snowflake tables versus Iceberg tables (queried in Snowflake) across various types of queries</figcaption></figure><p><strong>Performance Overview: Snowflake vs. Iceberg</strong></p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fdatawrapper.dwcdn.net%2FZkxRj%2F2%2F&amp;display_name=Datawrapper&amp;url=https%3A%2F%2Fdatawrapper.dwcdn.net%2FZkxRj%2F2%2F&amp;image=https%3A%2F%2Fdatawrapper.dwcdn.net%2FZkxRj%2Fplain-s.png%3Fv%3D2&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=dwcdn" width="600" height="1453" frameborder="0" scrolling="no"><a href="https://medium.com/media/e2d4c59da04671cbe3ed66387ab414c7/href">https://medium.com/media/e2d4c59da04671cbe3ed66387ab414c7/href</a></iframe><h3>Key Observations</h3><p><strong>Query Execution Time</strong>: Across a variety of queries — including simple SELECTs, aggregations, and range filters — <strong>Snowflake’s native tables</strong> consistently delivered faster performance, often executing <strong>2–5 times faster</strong> than Iceberg tables. For instance, the <strong>Simple SELECT query</strong> returned results in just <strong>0.483 seconds</strong> on Snowflake, compared to <strong>0.622 seconds</strong> on Iceberg. This performance gap widened further with more complex queries like aggregations and range filters, where Snowflake’s optimized architecture clearly demonstrated its efficiency.</p><p>Let’s Now Dig Deeper: How We Performed This Comparison</p><p><strong>Step 1: Loading Data into Snowflake Native Storage</strong></p><p>To load the dataset into Snowflake’s native storage, we begin by logging in to Snowflake using <strong>SnowSQL</strong> and then uploading the file to an internal stage before loading it into the table.</p><ol><li><strong>Login to Snowflake Account from Local Machine Using SnowSQL</strong>: Open your terminal and log in to your Snowflake account using <strong>SnowSQL</strong>.</li></ol><pre>snowsql -a &lt;account&gt; -u &lt;username&gt; -r &lt;role&gt; -w &lt;warehouse&gt;</pre><p><strong>2. Upload the File to Snowflake’s Internal Stage</strong>: Use the <strong>PUT command</strong> to upload the dataset file to the internal stage for loading.</p><pre>PUT file:///1brc/data/measurements.txt @my_int_stage AUTO_COMPRESS=TRUE;</pre><p><strong>3.Create the Table and Load Data</strong>: Once the file is uploaded, create the necessary table and load the data from the stage into the table.</p><pre>CREATE OR REPLACE TABLE measurements (<br>  location VARCHAR(50),<br>  temperature NUMBER<br>);<br><br>CREATE OR REPLACE FILE FORMAT measurement_format<br>  TYPE = &#39;CSV&#39;<br>  FIELD_DELIMITER = &#39;;&#39;;<br><br>COPY INTO measurements<br>FROM @my_int_stage<br>FILE_FORMAT = (FORMAT_NAME = measurement_format)<br>ON_ERROR = &#39;skip_file&#39;;</pre><p>The structure of the table is shown below</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*v9lBCy_w4z0ZRvZlWWAeWQ.png" /><figcaption>snowflake native table</figcaption></figure><p><strong>Step 2: Creating and Querying an Iceberg Table in S3</strong></p><p>Next, I created an <strong>Iceberg table</strong> for the same dataset, but this time stored externally on S3. I used <strong>PySpark</strong> to write the data into Iceberg format and linked it to Snowflake via the <strong>Polaris catalog</strong>.</p><ol><li><strong>Upload Data to S3 and Write as Iceberg Table</strong>:</li></ol><pre>import  pyspark.sql.functions as f<br>from pyspark.sql import SparkSession<br>from pyspark.sql.types import StructType,StructField,StringType,IntegerType,DecimalType<br>import os<br><br>spark = SparkSession.builder.appName(&#39;iceberg&#39;) \<br>.config(&#39;spark.jars.packages&#39;, &#39;org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.4.1,software.amazon.awssdk:bundle:2.20.160,software.amazon.awssdk:url-connection-client:2.20.160&#39;) \<br>.config(&#39;spark.sql.extensions&#39;, &#39;org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions&#39;) \<br>.config(&#39;spark.sql.defaultCatalog&#39;, &#39;polaris&#39;) \<br>.config(&#39;spark.sql.catalog.polaris&#39;, &#39;org.apache.iceberg.spark.SparkCatalog&#39;) \<br>.config(&#39;spark.sql.catalog.polaris.type&#39;, &#39;rest&#39;) \<br>.config(&#39;spark.sql.catalog.polaris.header.X-Iceberg-Access-Delegation&#39;,&#39;vended-credentials&#39;) \<br>.config(&#39;spark.sql.catalog.polaris.uri&#39;,&#39;https://xxxx.ap-southeast-1.snowflakecomputing.com/polaris/api/catalog&#39;) \<br>.config(&#39;spark.sql.catalog.polaris.credential&#39;,&#39;xxxxx/xx=:xxxx+xxx=&#39;) \<br>.config(&#39;spark.sql.catalog.polaris.warehouse&#39;,&#39;my_polaris_catalog&#39;) \<br>.config(&#39;spark.sql.catalog.polaris.scope&#39;,&#39;PRINCIPAL_ROLE:data_engineer_role&#39;)\<br>.config(&#39;spark.sql.catalog.polaris.client.region&#39;,&#39;ap-southeast-1&#39;)\<br>.getOrCreate()<br><br>df = spark.read.text(&#39;/1brc/data/measurements.txt&#39;)<br><br>df = df.select(f.split(f.col(&quot;value&quot;),&quot;;&quot;).alias(&#39;c1&#39;))<br><br>df = df.select(f.col(&#39;c1&#39;)[0].alias(&#39;location&#39;),f.col(&#39;c1&#39;)[1].cast(DecimalType(38,0)).alias(&#39;temperature&#39;))<br><br>database_name = &quot;onebrc&quot;<br>table_name = &quot;tbl_measurements&quot;<br><br>df.write.format(&quot;iceberg&quot;) \<br>    .mode(&quot;overwrite&quot;) \<br>    .saveAsTable(f&quot;{database_name}.{table_name}&quot;)</pre><p><strong>2. Integrate with Snowflake Polaris and create an iceberg table in Snowflake</strong>:</p><pre>CREATE OR REPLACE EXTERNAL VOLUME iceberg_external_volume<br>   STORAGE_LOCATIONS = (<br>         (<br>            NAME = &#39;my-s3-ap-southeast-1&#39;,<br>            STORAGE_PROVIDER = &#39;S3&#39;,<br>            STORAGE_BASE_URL = &#39;s3://lakehouse-8765374/&#39;,<br>            STORAGE_AWS_ROLE_ARN = &#39;arn:aws:iam::xxxxxx:role/sf_external_volume_role&#39;,<br>            STORAGE_AWS_EXTERNAL_ID = &#39;iceberg_table_external_id&#39;<br>         )<br>      );<br><br><br>CREATE OR REPLACE CATALOG INTEGRATION my_polaris_int<br>  CATALOG_SOURCE = POLARIS<br>  TABLE_FORMAT = ICEBERG<br>  CATALOG_NAMESPACE = &#39;onebrc&#39;<br>  REST_CONFIG = (<br>    CATALOG_URI = &#39;https://xxxx.ap-southeast-1.snowflakecomputing.com/polaris/api/catalog&#39;<br>    WAREHOUSE = &#39;my_polaris_catalog&#39;<br>  )<br>  REST_AUTHENTICATION = (<br>    TYPE = OAUTH<br>    OAUTH_CLIENT_ID = &#39;xxxxxx&#39;<br>    OAUTH_CLIENT_SECRET = &#39;xxxxxxx&#39;<br>    OAUTH_ALLOWED_SCOPES = (&#39;PRINCIPAL_ROLE:data_engineer_role&#39;)<br>  )<br>  ENABLED = TRUE;<br><br><br>  CREATE or replace ICEBERG TABLE measurements_iceberg<br>  EXTERNAL_VOLUME=&#39;iceberg_external_volume&#39;,<br>  CATALOG=&#39;my_polaris_int&#39;,<br>  CATALOG_TABLE_NAME=&#39;tbl_measurements&#39;;</pre><p>The structure of the table is shown below</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/451/1*r-dlCVxPbmBTFUtWIDdbsA.png" /><figcaption>iceberg table</figcaption></figure><p><strong>Detailed Breakdown of Queries:</strong></p><p>Below are the 10 queries that were run across both the <strong>Snowflake native table</strong> (measurements) and the <strong>Iceberg table</strong>(measurements_iceberg) to compare their performance in terms of runtime, data scanned, and partitions scanned.</p><ol><li><strong>Simple SELECT Query</strong>:</li></ol><pre>-- Snowflake Native Storage<br>SELECT * FROM measurements LIMIT 1000;<br><br>-- Iceberg Table in S3<br>SELECT * FROM measurements_iceberg LIMIT 1000;</pre><p>2. <strong>Filter by Temperature Range</strong>:</p><pre>-- Snowflake Native Storage<br>SELECT * <br>FROM measurements<br>WHERE temperature &gt;= 20.0 AND temperature &lt;= 30.0;<br><br>-- Iceberg Table in S3<br>SELECT * <br>FROM measurements_iceberg<br>WHERE temperature &gt;= 20.0 AND temperature &lt;= 30.0;</pre><p><strong>3. Aggregation by Location (AVG)</strong></p><pre>-- Snowflake Native Storage<br>SELECT location, AVG(temperature) AS avg_temperature <br>FROM measurements <br>GROUP BY location;<br><br>-- Iceberg Table in S3<br>SELECT location, AVG(temperature) AS avg_temperature <br>FROM measurements_iceberg <br>GROUP BY location;</pre><p><strong>4. Count Total Measurements</strong></p><pre>-- Snowflake Native Storage<br>SELECT COUNT(*) AS total_measurements <br>FROM measurements;<br><br>-- Iceberg Table in S3<br>SELECT COUNT(*) AS total_measurements <br>FROM measurements_iceberg;</pre><p><strong>5. Max Temperature by Location</strong></p><pre>-- Snowflake Native Storage<br>SELECT location, MAX(temperature) AS max_temperature <br>FROM measurements <br>GROUP BY location;<br><br>-- Iceberg Table in S3<br>SELECT location, MAX(temperature) AS max_temperature <br>FROM measurements_iceberg <br>GROUP BY location;</pre><p>6. <strong>Min Temperature by Location</strong></p><pre>-- Snowflake Native Storage<br>SELECT location, MIN(temperature) AS min_temperature <br>FROM measurements <br>GROUP BY location;<br><br>-- Iceberg Table in S3<br>SELECT location, MIN(temperature) AS min_temperature <br>FROM measurements_iceberg <br>GROUP BY location;</pre><p><strong>7. Average Temperature Across All Locations</strong></p><pre>-- Snowflake Native Storage<br>SELECT AVG(temperature) AS avg_temperature <br>FROM measurements;<br><br>-- Iceberg Table in S3<br>SELECT AVG(temperature) AS avg_temperature <br>FROM measurements_iceberg;</pre><p>8. <strong>Top 10 Hottest Locations (AVG Temperature)</strong></p><pre>-- Snowflake Native Storage<br>SELECT location, AVG(temperature) AS avg_temperature <br>FROM measurements <br>GROUP BY location <br>ORDER BY avg_temperature DESC <br>LIMIT 10;<br><br>-- Iceberg Table in S3<br>SELECT location, AVG(temperature) AS avg_temperature <br>FROM measurements_iceberg <br>GROUP BY location <br>ORDER BY avg_temperature DESC <br>LIMIT 10;</pre><p>9. <strong>Temperature Variance by Location</strong></p><pre>-- Snowflake Native Storage<br>SELECT location, VAR_SAMP(temperature) AS temperature_variance <br>FROM measurements <br>GROUP BY location;<br><br>-- Iceberg Table in S3<br>SELECT location, VAR_SAMP(temperature) AS temperature_variance <br>FROM measurements_iceberg <br>GROUP BY location;</pre><p>10. <strong>Min, Max, and Avg Temperature by Location</strong></p><pre>-- Snowflake Native Storage<br>SELECT <br>    location,<br>    MIN(temperature) AS Min,<br>    MAX(temperature) AS Max,<br>    AVG(temperature) AS Avg<br>FROM measurements <br>GROUP BY location <br>ORDER BY location;<br><br>-- Iceberg Table in S3<br>SELECT <br>    location,<br>    MIN(temperature) AS Min,<br>    MAX(temperature) AS Max,<br>    AVG(temperature) AS Avg<br>FROM measurements_iceberg <br>GROUP BY location <br>ORDER BY location;</pre><p><strong>My Final Verdict:</strong></p><p>This comparison clearly shows that <strong>Snowflake’s native storage</strong> outperforms Iceberg in terms of <strong>query execution time </strong>and <strong>data retrieval efficiency</strong>. Snowflake’s native tables consistently returned results faster across a variety of query types, particularly for aggregation and filtering queries, where the difference was most pronounced.</p><p>However, <strong>Iceberg</strong> offers flexibility with its open-source format, allowing integration with external storage systems like S3. While Iceberg’s performance was generally slower, it may still be an attractive option for users who prioritize flexibility and open standards over raw query speed.</p><p>Moving forward, I plan to continue testing by applying several <strong>optimization techniques</strong> to Iceberg, including:</p><ul><li><strong>Trying different compression settings</strong> to store the data optimally and see if query performance can be improved.</li><li><strong>Running OPTIMIZE queries</strong> to organize data more efficiently.</li><li><strong>Implementing Z-ordering</strong> to reduce the amount of data scanned and further optimize performance.</li></ul><p>Additionally, I’ll also look into <strong>Snowflake-specific optimizations.</strong></p><p>Ultimately, the choice between <strong>native Snowflake tables</strong> and <strong>Iceberg on S3</strong> depends on your specific use case. For workloads requiring <strong>speed and efficiency</strong>, Snowflake’s native storage is the better choice. For workloads prioritizing <strong>flexibility and interoperability</strong>, Iceberg may be worth considering, especially with further optimizations.</p><p>Stay tuned for updates on these optimizations and further testing to close the performance gap.</p><p><strong>Disclaimer</strong>: I used AI tools like ChatGPT just to help with grammar and phrasing. The content, ideas, and all the work in this article are 100% my own.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ae3571f0d02b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A Step-by-Step Guide to Lakehouse Architecture Using S3, Glue Catalog, and Snowflake]]></title>
            <link>https://medium.com/@saravanan.aryan/a-step-by-step-guide-to-lakehouse-architecture-using-s3-glue-catalog-and-snowflake-0053d9c105c6?source=rss-4290c711ee7e------2</link>
            <guid isPermaLink="false">https://medium.com/p/0053d9c105c6</guid>
            <category><![CDATA[aws-glue]]></category>
            <category><![CDATA[apache-iceberg]]></category>
            <category><![CDATA[data-lakehouse]]></category>
            <category><![CDATA[snowflake]]></category>
            <category><![CDATA[data-engineering]]></category>
            <dc:creator><![CDATA[saravanan aryan]]></dc:creator>
            <pubDate>Fri, 30 Aug 2024 04:16:55 GMT</pubDate>
            <atom:updated>2024-09-20T11:06:47.242Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>Introduction</strong></p><p>Lakehouse architecture is a modern approach that combines the best features of data warehouses and data lakes. It merges the flexibility and cost-effectiveness of a data lake with the reliability and performance of a data warehouse. In this article, I will guide you through implementing a lakehouse architecture using AWS S3, Glue Catalog, PySpark, Apache Iceberg, and Snowflake.</p><p><strong>Architecture of Our Implementation</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/917/1*kZDj3NlTRHUUBkbVMzcbIA.png" /></figure><p>In this implementation, we will be reading a dataset and loading it as Iceberg tables into S3. We will use the AWS Glue Catalog to capture metadata, including the location of the Iceberg metadata.json file and other column-level metadata. After setting up the Iceberg tables in S3 and capturing the metadata in the Glue Catalog, we will integrate the AWS Glue Catalog with Snowflake. This integration will allow us to use Snowflake to query the data stored in S3 directly, taking advantage of Snowflake&#39;s powerful query engine to run analytics and generate insights on the data, without needing to move or duplicate the data.</p><p>This architecture allows us to store data in AWS S3 with an additional transactional layer provided by Apache Iceberg, ensuring ACID properties. The compute and storage components are decoupled, allowing users to choose their own storage and compute resources independently. This setup supports various data formats, including structured, semi-structured, and unstructured data, and can handle tasks such as data processing, real-time analytics, business intelligence, and machine learning.</p><h3>Dataset</h3><p>We use the New York taxi dataset, which can be downloaded from <a href="https://d37ci6vzurychx.cloudfront.net/trip-data/green_tripdata_2024-01.parquet">this link</a>. This dataset includes fields capturing pick-up and drop-off dates/times, locations, trip distances, itemized fares, rate types, payment types, and passenger counts</p><h3>Processing Using PySpark and Storing as Iceberg Table in S3</h3><p>PySpark is a powerful distributed compute engine that, when used with Apache Iceberg, supports a wide range of features. We will read the New York taxi dataset with PySpark and write it as an Iceberg table in S3.</p><h4>Step 1: Install PySpark</h4><p>You can install PySpark locally using the following command:</p><pre>pip install pyspark</pre><h4>Step 2: Read and Write Data as an Iceberg Table and Update the Glue Catalog</h4><p>Below is the PySpark code to read the dataset and write it as an Iceberg table in S3, using AWS Glue Catalog for management:</p><pre>from pyspark.sql import SparkSession<br>import os<br><br># Set AWS environment variables<br>os.environ[&#39;AWS_REGION&#39;] = &#39;ap-southeast-1&#39;<br>os.environ[&#39;AWS_DEFAULT_REGION&#39;] = &#39;ap-southeast-1&#39;<br>os.environ[&#39;AWS_ACCESS_KEY_ID&#39;] = &#39;XXXXXXXXX&#39;<br>os.environ[&#39;AWS_SECRET_ACCESS_KEY&#39;] = &#39;XXXXXXXXXX&#39;<br><br># Create a Spark session<br>spark = SparkSession.builder \<br>    .appName(&quot;nyc taxi&quot;) \<br>    .config(&quot;spark.jars.packages&quot;, &#39;org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.6.0,software.amazon.awssdk:bundle:2.27.7,software.amazon.awssdk:url-connection-client:2.27.7&#39;) \<br>    .config(&quot;spark.sql.extensions&quot;, &quot;org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions&quot;) \<br>    .config(&quot;spark.sql.catalog.glue_catalog&quot;, &quot;org.apache.iceberg.spark.SparkCatalog&quot;) \<br>    .config(&quot;spark.sql.catalog.glue_catalog.warehouse&quot;, &quot;s3a://lakehouse-8765374/&quot;) \<br>    .config(&quot;spark.sql.catalog.glue_catalog.catalog-impl&quot;, &quot;org.apache.iceberg.aws.glue.GlueCatalog&quot;) \<br>    .config(&quot;spark.sql.catalog.glue_catalog.io-impl&quot;, &quot;org.apache.iceberg.aws.s3.S3FileIO&quot;) \<br>    .getOrCreate()<br><br># Read the dataset<br>df = spark.read.parquet(&quot;green_tripdata_2024-01.parquet&quot;)<br><br># Write data as Iceberg table using AWS Glue Catalog<br>database_name = &quot;nyc_taxi&quot;<br>table_name = &quot;green_trip_data&quot;<br><br>df.write.format(&quot;iceberg&quot;) \<br>    .mode(&quot;append&quot;) \<br>    .saveAsTable(f&quot;glue_catalog.{database_name}.{table_name}&quot;)<br><br>print(&quot;Data successfully written to Iceberg table in S3 using AWS Glue Catalog.&quot;)</pre><p><strong>Configuration Details and Explanations</strong></p><p>Let’s break down the key configurations and their importance:</p><ul><li><strong>Iceberg Runtime and AWS SDK</strong>: The configuration .config(&quot;spark.jars.packages&quot;, &#39;org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.6.0,software.amazon.awssdk:bundle:2.27.7,software.amazon.awssdk:url-connection-client:2.27.7&#39;) is used to download the necessary Iceberg and AWS SDK packages from maven.</li><li><strong>Glue Catalog Configuration</strong>: The configuration .config(&quot;spark.sql.catalog.glue_catalog&quot;, &quot;org.apache.iceberg.spark.SparkCatalog&quot;)specifies that a catalog named glue_catalog will be created and managed using Iceberg’s implementation of the SparkCatalog class, rather than Spark’s default implementation.</li><li>The configuration .config(&quot;spark.sql.catalog.glue_catalog.catalog-impl&quot;, &quot;org.apache.iceberg.aws.glue.GlueCatalog&quot;) points to the custom Iceberg catalog implementation class, org.apache.iceberg.aws.glue.GlueCatalog (as detailed in the <a href="https://iceberg.apache.org/javadoc/1.4.1/org/apache/iceberg/aws/glue/GlueCatalog.html">Iceberg Javadoc</a>), which in turn implements the Catalog interface (<a href="https://github.com/apache/iceberg/blob/main/api/src/main/java/org/apache/iceberg/catalog/Catalog.java">link to Catalog class</a>).</li><li>In this example, the glue_catalog leverages Iceberg’s SparkCatalog, but with a custom implementation provided by the org.apache.iceberg.aws.glue.GlueCatalog class.</li><li><strong>Warehouse Location</strong>: The configuration .config(&quot;spark.sql.catalog.glue_catalog.warehouse&quot;, &quot;s3a://lakehouse-8765374/&quot;) sets the S3 location where the data will be stored. This tells Spark and Iceberg exactly where to store the data files in AWS S3.</li><li><strong>File IO Implementation</strong>: The configuration .config(&quot;spark.sql.catalog.glue_catalog.io-impl&quot;, &quot;org.apache.iceberg.aws.s3.S3FileIO&quot;) indicates Iceberg&#39;s implementation for reading and writing data to S3.</li></ul><p>After running the above code, the Iceberg table will be created in S3, and all the metadata details will be updated in the Glue Catalog.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1001/1*5qyCycc58eZmdnfO1s70LA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*f60y7FCCT-wdKY1OjaG6PQ.png" /></figure><p>So far, we have successfully created our first Iceberg table, nyc_taxi.green_trip_data. The next step is to integrate this Glue Catalog with Snowflake, allowing us to start reading the Iceberg table directly from Snowflake</p><p><strong>Integrating Glue Catalog with Snowflake</strong></p><p>Step 1: Create a Database in Snowflake</p><pre>CREATE DATABASE lakehouse_8765374;</pre><p>Step 2: Create a Schema</p><pre>CREATE SCHEMA nyc_taxi;<br>USE lakehouse_8765374.nyc_taxi;</pre><p>Step 3: Create an External Volume Object for Iceberg Table</p><pre>CREATE OR REPLACE EXTERNAL VOLUME iceberg_external_volume<br>   STORAGE_LOCATIONS = (<br>         (<br>            NAME = &#39;my-s3-ap-southeast-1&#39;,<br>            STORAGE_PROVIDER = &#39;S3&#39;,<br>            STORAGE_BASE_URL = &#39;s3://lakehouse-8765374/&#39;,<br>            STORAGE_AWS_ROLE_ARN = &#39;arn:aws:iam::xxxxxxx:role/sf_external_volume_role&#39;,<br>            STORAGE_AWS_EXTERNAL_ID = &#39;iceberg_table_external_id&#39;<br>         )<br>      );</pre><ul><li>The STORAGE_AWS_ROLE_ARN role should have the necessary permissions to access the data in S3. For more information, see <a href="https://docs.snowflake.com/en/user-guide/tables-iceberg-configure-external-volume-s3">Snowflake&#39;s documentation</a>.</li><li>Ensure that you update the trust policy of the IAM role to Snowflake’s IAM user and the external ID. These details can be obtained by running the following command:</li></ul><pre>DESC EXTERNAL VOLUME iceberg_external_volume;</pre><p>Step 4: Create a Catalog Integration Object</p><pre>CREATE OR REPLACE CATALOG INTEGRATION glueCatalogInt<br>  CATALOG_SOURCE=GLUE,<br>  CATALOG_NAMESPACE=&#39;nyc_taxi&#39;,<br>  TABLE_FORMAT=ICEBERG,<br>  GLUE_AWS_ROLE_ARN=&#39;arn:aws:iam::xxxxxxxx:role/sf_catalog_access_role&#39;,<br>  GLUE_CATALOG_ID=&#39;xxxxxxxx&#39;,<br>  GLUE_REGION=&#39;ap-southeast-1&#39;,<br>  ENABLED=TRUE;</pre><p>Make sure the role arn:aws:iam::xxxxxxxx:role/sf_catalog_access_role has the necessary Glue permissions for Snowflake to access.</p><p>Refer to <a href="https://docs.snowflake.com/en/user-guide/tables-iceberg-configure-catalog-integration-glue">Snowflake’s documentation</a> for more details.</p><h4>Final Step: Create the Iceberg Table in Snowflake</h4><pre>CREATE ICEBERG TABLE green_trip_data<br>  EXTERNAL_VOLUME=&#39;iceberg_external_volume&#39;,<br>  CATALOG=&#39;glueCatalogInt&#39;,<br>  CATALOG_TABLE_NAME=&#39;green_trip_data&#39;;</pre><p>If everything is configured correctly, you should be able to see and access the Iceberg table in S3 through Snowflake.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/322/1*NPRNcDCKYUxtcLViaN9lSw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rUmXeYxpH28Ltf5wsHqA-w.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/848/1*J_Yorpm-o0BnlRkCHxu48Q.png" /></figure><h3>Conclusion</h3><p>In this article, we implemented a lakehouse architecture using AWS S3, Glue Catalog, PySpark, Apache Iceberg, and Snowflake.</p><p>One of the key benefits of the lakehouse architecture we achieved is the decoupling of storage and compute. This allows for independent scaling, ensuring that our architecture is both cost-effective and adaptable to changing needs. Additionally, by leveraging open-source technologies like Apache Iceberg, this setup remains future-proof, with no vendor lock-in, giving us the freedom to choose any compatible compute engine. This flexibility ensures that as our data and compute needs evolve, our architecture can seamlessly adapt to new tools and technologies.</p><h3>References</h3><ol><li><strong>Practical Lakehouse Architecture</strong><br><a href="https://learning.oreilly.com/library/view/practical-lakehouse-architecture/9781098153007/">O’Reilly Media</a></li><li><strong>Apache Iceberg: The Definitive Guide</strong><br><a href="https://learning.oreilly.com/library/view/apache-iceberg-the/9781098148614/">O’Reilly Media</a></li><li><strong>Apache Iceberg Spark Configuration Documentation</strong><br><a href="https://iceberg.apache.org/docs/1.5.0/spark-configuration/">Apache Iceberg Documentation</a></li><li><strong>AWS Glue Catalog Integration with Apache Iceberg</strong><br><a href="https://iceberg.apache.org/javadoc/1.4.1/org/apache/iceberg/aws/glue/GlueCatalog.html">Apache Iceberg Javadoc</a></li><li><strong>Apache Iceberg Spark Quickstart Guide</strong><br><a href="https://iceberg.apache.org/spark-quickstart/">Apache Iceberg</a></li><li><strong>Introduction to Apache Iceberg</strong><br><a href="https://www.dremio.com/resources/guides/apache-iceberg/">Dremio Resource Guide</a></li><li><strong>What Is a Data Lakehouse?</strong><br><a href="https://www.databricks.com/blog/2020/01/30/what-is-a-data-lakehouse.html">Databricks Blog</a></li></ol><p><strong>Disclaimer</strong>: I used AI tools like ChatGPT just to help with grammar and phrasing. The content, ideas, and all the work in this article are 100% my own.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0053d9c105c6" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Implementing a SQLMesh Project]]></title>
            <link>https://medium.com/@saravanan.aryan/implementing-a-sqlmesh-project-24824a5c42ab?source=rss-4290c711ee7e------2</link>
            <guid isPermaLink="false">https://medium.com/p/24824a5c42ab</guid>
            <category><![CDATA[data-transformation]]></category>
            <category><![CDATA[sqlmesh]]></category>
            <category><![CDATA[dbt]]></category>
            <category><![CDATA[data-engineering]]></category>
            <category><![CDATA[snowflake]]></category>
            <dc:creator><![CDATA[saravanan aryan]]></dc:creator>
            <pubDate>Sun, 04 Aug 2024 16:55:44 GMT</pubDate>
            <atom:updated>2024-09-20T11:07:22.432Z</atom:updated>
            <content:encoded><![CDATA[<p>Implementing a SQLMesh Project</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*AvDlq841rs0Xhk_2HtYKhg.png" /></figure><p><strong>Introduction</strong> In this article, we will explore how to implement a simple SQLMesh project by leveraging the codebase of a dbt (Data Build Tool) project. The foundation of this article is based on a hands-on repository created by a Udemy instructor for a dbt course, available at <a href="https://github.com/nordquant/complete-dbt-bootcamp-zero-to-hero">this GitHub repo</a>. This repository contains a comprehensive example of a dbt project, which we will use as a template to demonstrate the capabilities of SQLMesh, a next-generation data transformation tool.</p><p><strong>Project Overview</strong> The dbt project we are using focuses on analyzing an end-to-end data engineering workflow using the Airbnb dataset in Snowflake. This project involves loading raw data into Snowflake, creating various models, and performing transformations to derive meaningful insights from the data.</p><h3>Brief Explanation of the Airbnb Dataset</h3><p>The Airbnb dataset used in this project consists of three primary tables: raw_listings, raw_reviews, and raw_hosts. These tables contain information about Airbnb listings, the reviews given for those listings, and details about the hosts. Understanding the relationships and significance of these tables is crucial for processing and analyzing the data.</p><ol><li><strong>raw_listings</strong>:</li></ol><ul><li><strong>Description</strong>: This table contains details about Airbnb listings, including the listing ID, URL, name, room type, minimum nights required, host ID, price, and timestamps for creation and updates.</li><li><strong>Significance</strong>: The listings data provides a comprehensive view of the properties available on Airbnb. It’s the central table that connects with both reviews and hosts data.</li></ul><p>2. <strong>raw_reviews</strong>:</p><ul><li><strong>Description</strong>: This table includes reviews for the Airbnb listings, containing information such as listing ID, review date, reviewer name, comments, and sentiment.</li><li><strong>Significance</strong>: Reviews data offers insights into the customer experience and satisfaction. It is linked to the listings data through the listing_id.</li></ul><p><strong>3 . raw_hosts</strong>:</p><ul><li><strong>Description</strong>: This table contains information about the hosts, including host ID, name, superhost status, and timestamps for creation and updates.</li><li><strong>Significance</strong>: Host data provides details about the individuals or entities offering the listings. It is connected to the listings data through the host_id.</li></ul><h3>End Goal of the Project</h3><p>The primary objective of this project is to analyze how full moon dates affect Airbnb ratings and reviews. By leveraging the relationships between listings, reviews, and hosts, we aim to derive meaningful insights into the impact of lunar cycles on customer feedback and satisfaction.</p><h3>Steps to Implement SQLMesh Project</h3><h4>Setting Up SQLMesh for Snowflake</h4><ol><li><strong>Initialize SQLMesh Project</strong></li></ol><ul><li>Create a folder named airbnb_sqlmesh and navigate into that folder. Run the following commands to initialize the SQLMesh project:</li></ul><pre>python -m venv .venv<br>source .venv/bin/activate<br>pip install &quot;sqlmesh[snowflake]&quot;<br>sqlmesh init snowflake</pre><p><strong>2. Project Structure</strong> After initialization, the following directories and files will be created, providing a structure to organize our SQLMesh project:</p><ul><li>config.yaml: Database configuration file for your project.</li><li>models: Directory for SQL and Python models.</li><li>audits: Directory for shared audits.</li><li>tests: Directory for unit tests.</li><li>macros: Directory for macros.</li></ul><p><strong>3. Project Configuration : </strong>Define SQLMesh project configurations in the config.yaml file. This file contains database connection details and model defaults:</p><pre>gateways:<br>  snowflake_conn:<br>    connection:<br>      type: snowflake<br>      user: &lt;username&gt;<br>      password: &lt;password&gt;<br>      account: &lt;account&gt;<br>      warehouse: &lt;warehouse&gt;<br>      database: AIRBNB<br>      role: &lt;role&gt;<br>default_gateway: snowflake_conn<br><br>model_defaults:<br>  dialect: snowflake<br>  start: 2009-01-01</pre><p><strong>4. Loading Raw Data</strong> Use the following SQL scripts to load the raw data into Snowflake tables:</p><pre>CREATE DATABASE IF NOT EXISTS AIRBNB;<br>CREATE SCHEMA IF NOT EXISTS AIRBNB.RAW;<br><br>USE DATABASE airbnb;<br>USE SCHEMA RAW;<br><br>-- Create and load raw_listings table<br>CREATE OR REPLACE TABLE raw_listings (<br>  id integer,<br>  listing_url string,<br>  name string,<br>  room_type string,<br>  minimum_nights integer,<br>  host_id integer,<br>  price string,<br>  created_at datetime,<br>  updated_at datetime<br>);<br><br>COPY INTO raw_listings FROM &#39;s3://dbtlearn/listings.csv&#39;<br>  FILE_FORMAT = (type = &#39;CSV&#39; FIELD_OPTIONALLY_ENCLOSED_BY = &#39;&quot;&#39;);<br><br>-- Create and load raw_reviews table<br>CREATE OR REPLACE TABLE raw_reviews (<br>  listing_id integer,<br>  date datetime,<br>  reviewer_name string,<br>  comments string,<br>  sentiment string<br>);<br><br>COPY INTO raw_reviews FROM &#39;s3://dbtlearn/reviews.csv&#39;<br>  FILE_FORMAT = (type = &#39;CSV&#39; FIELD_OPTIONALLY ENCL_ENCLOSED_BY = &#39;&quot;&#39;);<br><br>-- Create and load raw_hosts table<br>CREATE OR REPLACE TABLE raw_hosts (<br>  id integer,<br>  name string,<br>  is_superhost string,<br>  created_at datetime,<br>  updated_at datetime<br>);<br><br>COPY INTO raw_hosts FROM &#39;s3://dbtlearn/hosts.csv&#39;<br>  FILE_FORMAT = (type = &#39;CSV&#39; FIELD_OPTIONALLY_ENCLOSED_BY = &#39;&quot;&#39;);</pre><p>Creating Models</p><ol><li><strong>Source Models</strong> We create three models src_hosts.sql, src_listings.sql, and src_reviews.sql on top of the raw data tables inside the models/source folder:</li></ol><p>The source models in this project are designed to standardize and prepare raw data from the Airbnb dataset for further processing and analysis. They act as an intermediary layer, transforming raw data into a more usable format by renaming columns, selecting relevant fields, and standardizing the data. All source models are materialized as <strong>views</strong>, ensuring that the data is clean, structured, and ready for subsequent transformations and analysis in the dimension and fact models.</p><pre>MODEL (<br>  name SOURCE.SRC_HOSTS,<br>  kind view<br>);<br><br>WITH raw_hosts AS (<br>  SELECT * FROM RAW.RAW_HOSTS<br>)<br>SELECT<br>  id AS host_id,<br>  name AS host_name,<br>  is_superhost,<br>  created_at,<br>  updated_at<br>FROM raw_hosts;</pre><pre>MODEL (<br>  name SOURCE.SRC_LISTINGS,<br>  kind view<br>);<br><br>WITH raw_listings AS (<br>  SELECT * FROM RAW.RAW_LISTINGS<br>)<br>SELECT<br>  id AS listing_id,<br>  name AS listing_name,<br>  listing_url,<br>  room_type,<br>  minimum_nights,<br>  host_id,<br>  price AS price_str,<br>  created_at,<br>  updated_at<br>FROM raw_listings;</pre><pre>MODEL (<br>  name SOURCE.SRC_REVIEWS,<br>  kind view<br>);<br><br>WITH raw_reviews AS (<br>  SELECT * FROM raw.raw_reviews<br>)<br>SELECT<br>  listing_id,<br>  date AS review_date,<br>  reviewer_name,<br>  comments AS review_text,<br>  sentiment AS review_sentiment<br>FROM raw_reviews;</pre><p><strong>2. Dimension Models</strong> Create dimension models as views inside the models/dim folder:</p><p>The dimension models clean, enrich, and combine the source data for detailed analysis. They handle null values, format fields, and merge related datasets. Models dim.dim_hosts_cleansed and dim.dim_listings_cleansed are materialized as views, while dim.dim_listings_w_hosts is materialized as a full table, reloaded entirely each time.</p><pre>MODEL (<br>  name dim.dim_hosts_cleansed,<br>  kind view<br>);<br><br>WITH src_hosts AS (<br>  SELECT * FROM SOURCE.SRC_HOSTS<br>)<br>SELECT<br>  host_id,<br>  NVL(host_name, &#39;Anonymous&#39;) AS host_name,<br>  is_superhost,<br>  created_at,<br>  updated_at<br>FROM src_hosts;</pre><pre>MODEL (<br>  name dim.dim_listings_cleansed,<br>  kind view<br>);<br><br>WITH src_listings AS (<br>  SELECT * FROM SOURCE.SRC_LISTINGS<br>)<br>SELECT<br>  listing_id,<br>  listing_name,<br>  room_type,<br>  CASE WHEN minimum_nights = 0 THEN 1 ELSE minimum_nights END AS minimum_nights,<br>  host_id,<br>  REPLACE(price_str, &#39;$&#39;)::NUMBER(10, 2) AS price,<br>  created_at,<br>  updated_at<br>FROM src_listings;</pre><pre>MODEL (<br>  name dim.dim_listings_w_hosts,<br>  kind full<br>);<br><br>WITH l AS (<br>  SELECT * FROM dim.dim_listings_cleansed<br>),<br>h AS (<br>  SELECT * FROM dim.dim_hosts_cleansed<br>)<br>SELECT<br>  l.listing_id,<br>  l.listing_name,<br>  l.room_type,<br>  l.minimum_nights,<br>  l.price,<br>  l.host_id,<br>  h.host_name,<br>  h.is_superhost AS host_is_superhost,<br>  l.created_at,<br>  GREATEST(l.updated_at, h.updated_at) AS updated_at<br>FROM l<br>LEFT JOIN h ON (h.host_id = l.host_id);</pre><p><strong>3 .Fact Models</strong> Create incremental fact models inside the models/fct folder:</p><p>The fact model, fct.reviews, processes and aggregates review data from the source models. It is an incremental model, meaning it only loads new data, which drastically decreases computational resources required for each model run. In the model, the @start and @end_date are SQLMesh macros that are rendered during the sqlmesh plan or run with the appropriate start and end date for the run. Additionally, the @GENERATE_SURROGATE_KEY macro is used to generate an MD5 hash for the given input columns, creating a surrogate key for the data.</p><pre>MODEL (<br>  name fct.reviews,<br>  kind INCREMENTAL_BY_TIME_RANGE (<br>    time_column review_date<br>  )<br>);<br><br>WITH src_reviews AS (<br>  SELECT * FROM SOURCE.SRC_REVIEWS<br>)<br>SELECT<br>  @GENERATE_SURROGATE_KEY(listing_id, review_date, reviewer_name, review_text) AS review_id,<br>  *<br>FROM src_reviews<br>WHERE review_text IS NOT NULL<br>  AND review_date BETWEEN @start_date AND @end_date;</pre><p>4. <strong>Business Layer Models</strong>: Moving on to the business layer, we need to analyze how full moon dates affect review comments. To do this, we require a table containing the dates of the full moons. This can be achieved using a seed file in SQLMesh. Download the seed file from the provided <a href="https://github.com/nordquant/complete-dbt-bootcamp-zero-to-hero/tree/main/dbtlearn/seeds"><em>link</em></a> and place it in the seeds folder of the SQLMesh project. Then, create a seed model named full_moon_dates_seed.sql inside the models folder.</p><pre>MODEL (<br>  name seed.full_moon_dates,<br>  kind SEED (<br>    path &#39;../seeds/seed_data.csv&#39;<br>  )<br>);</pre><p>For the final model, create a folder named models/mart and add the following model inside it</p><pre>MODEL (<br>  name mart.mart_fullmoon_reviews,<br>  kind full<br>);<br><br>WITH fct_reviews AS (<br>    SELECT * FROM fct.reviews<br>),<br>full_moon_dates AS (<br>    SELECT * FROM seed.full_moon_dates<br>)<br><br>SELECT<br>  r.*,<br>  CASE<br>    WHEN fm.full_moon_date IS NULL THEN &#39;not full moon&#39;<br>    ELSE &#39;full moon&#39;<br>  END AS is_full_moon<br>FROM<br>  fct_reviews<br>  r<br>  LEFT JOIN full_moon_dates<br>  fm<br>  ON (TO_DATE(r.review_date) = DATEADD(DAY, 1, fm.full_moon_date))</pre><p>Once all the models are created, the project structure should look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*5V7vfcNw_AoMENyZMZr26w.png" /></figure><p>Running SQLMesh Plan</p><p>Execute the SQLMesh plan command to check and deploy the models:</p><pre>sqlmesh plan dev</pre><p>Specify the backfill start and end dates when prompted. After confirming the backfills, the models will be deployed to the development environment.</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*1jWl6Oun9HFm4uJocWVC3w.png" /></figure><p>Once the model is executed successfully , run the sqlmesh plan against the product</p><pre>sqlmesh plan prod</pre><p>SQLMesh will not reprocess all the data as it did for the development environment. Instead, it reuses the models created in the development environment. For more information on SQLMesh virtual environments, refer to this <a href="https://tobikodata.com/virtual-data-environments.html">link</a>.</p><p><strong>Disclaimer</strong>: I used AI tools like ChatGPT just to help with grammar and phrasing. The content, ideas, and all the work in this article are 100% my own.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=24824a5c42ab" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>