<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>iceberg &amp;mdash; bits on data</title>
    <link>https://bitsondata.dev/tag:iceberg</link>
    <description>\&gt;_ the imposter&#39;s guide to software, data, and life</description>
    <pubDate>Fri, 17 Apr 2026 11:40:18 +0000</pubDate>
    <image>
      <url>https://i.snap.as/vWVqkBBl.png</url>
      <title>iceberg &amp;mdash; bits on data</title>
      <link>https://bitsondata.dev/tag:iceberg</link>
    </image>
    <item>
      <title>Iceberg won the table format war</title>
      <link>https://bitsondata.dev/iceberg-won-the-table-format-war?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[the night sky is lit up over the water&#xA;&#xA;Photo by Michail Dementiev on Unsplash&#xA;&#xA;TL;DR: I believe Apache Iceberg won the table format wars, not because of a feature race, but primarily because of the open Iceberg spec. There are some features only available in Iceberg due to the breaking of compatibility with Hive, which was also a contributing factor to the adoption of the implementation.&#xA;&#xA;!--more--&#xA;&#xA;Disclaimer: I am the Head of Developer Relations at Tabular and a Developer Advocate in both Apache Iceberg and Trino communities. All of my 🌶️ takes are my biased opinion and not necessarily the opinion of Tabular, the Apache Software Foundation, the Trino Software Foundation, or the communities I work with. This also goes into a bit of my personal story for leaving my previous company but relates to my reasoning so I offer you a TL;DR if you don’t care about the details.&#xA;&#xA;!--emailsub--&#xA;&#xA;My revelation with Iceberg&#xA;&#xA;Two months ago, I made the difficult decision to leave Starburst which was hands down the best job I’ve ever had up to this point. Since I left I’ve had a lot of questions about my motivations for leaving and wanted to put some concerns to rest. This role allowed me to get deeply involved in open-source during working hours and showed me how I could aid the community to get traction on their work and drive the roadmap for many in the project. This was a new calling that overlapped with many altruist parts of how I define myself and was deeply rewarding.&#xA;&#xA;I made some incredible friends, some of which have become invaluable mentors during this process of learning the nuances and interplay between venture capital and an open-source community. So why did I leave this job that I love so much?&#xA;&#xA;Apache Iceberg Baby&#xA;&#xA;Let’s time-travel (pun intended) to the first Iceberg episode of the Trino Community Broadcast. In true ADHD form, I crammed learning about Apache Iceberg well into the night before the broadcast with the creator of Iceberg, Ryan Blue. While setting up that demo, I really started to understand what a game-changer Iceberg was. I had heard the Trino users and maintainers talk about Iceberg replacing Hive but it just didn’t sink in for the first couple of months. I mean really, what could be better than Hive? 🥲&#xA;&#xA;While researching I learned about hidden partitioning, schema evolution, and most importantly, the open specification. The whole package was just such an elegant solution to problems that had caused me and many in the Trino community failed deployments and late-night calls. Just as I had the epiphany with Trino (Presto at the time) of how big of a productivity booster SQL queries over multiple systems were, I had a similar experience with Iceberg that night. Preaching the combination of these two became somewhat of a mission of mine after that.&#xA;&#xA;ANSI SQL + Iceberg + Parquet + S3&#xA;&#xA;Immediately after that show, I wrote a four-blog series on Trino on Iceberg, did a talk, and built out the getting started repository for Iceberg. I was rather hooked on the thought of these two technologies in tandem. You start out with a system that can connect to any data source you throw at it and sees it as yet another SQL table. Take that system and add a table format that interacts with all the popular analytics engines people use today from Spark, Snowflake, and DuckDB, to Trino variants like EMR, Athena, and Starburst.&#xA;&#xA;Standards all the way down&#xA;&#xA;This approach to data virtualization is so interesting as each system offers full leverage over vendors trying to lock you in their particular query language or storage format. It pushes the incentives for vendors to support these open standards which puts them in a seemingly vulnerable position compared to locking users in. However, that’s a fallacy I hope vendors will slowly begin to understand is not true. With the open S3 storage standard, open file standards like Parquet, open table standards like Iceberg, and the ANSI SQL spec closely followed by Trino, the entire analytics warehouse has become a modular stack of truly open components. This is not just open in the sense of the freedom to use and contribute to a project, but the open standard that enables you the freedom to simply move between different projects.&#xA;&#xA;This new freedom gives the user the features of a data warehouse, with the scalability of the cloud, and a free market of a la carte services to handle your needs at whatever price point you need at any given time. All users need to do in this new ecosystem is shop around and choose any open project or vendor that implements the open standard and your migration cost will be practically non-existent. This is the ultimate definition of future-proofing your architecture.&#xA;&#xA;!--emailsub--&#xA;&#xA;Back to why I left Starburst&#xA;&#xA;Trino Community&#xA;&#xA;I’ll quickly tie up why I left Starburst before I reveal why Iceberg won the table format wars. For the last three years, I have worked on building awareness around Trino. My partner in crime, Manfred Moser, had been in the Trino community and literally wrote the book on Trino. Together we spent long days and nights growing the Trino community. I loved every minute of it and honestly didn’t see myself leaving Starburst or shifting focus from Trino until it became an analytics organization standard.&#xA;&#xA;Something became apparent though. Trino community health was thriving, and there were many organic product movements taking place in the Trino community. Cole Bowden was boosting the Trino release process getting us to cutting Trino releases every 1-2 weeks which is unprecedented in open-source. Cole, Manfred, and I did a manual scan over the pull requests and gracefully closed or revived outdated or abandoned pull requests. The Trino community is in great shape.&#xA;&#xA;Iceberg Community&#xA;&#xA;As I looked at Iceberg, the adoption and awareness were growing at an unprecedented rate with Snowflake, BigQuery, Starburst, and Athena all announcing support between 2021 and 2022. However, nothing was moving the needle forward from a developer experience perspective. There was some initial amazing work done by Sam Redai, but there was still so much to be done. I noticed the Iceberg documentation needed improvement. While many vendors were advocating for Iceberg, there was nobody putting in consistent work to the vanilla Iceberg site. PMCs like Ryan Blue, Jack Ye, Russel Spitzer, Dan Weeks, and many others are doing a great shared job of driving roadmap features for Iceberg, but no individual currently has the time to dedicate to the cat herding, improving communication in the project, or bettering the developer and contributor experience for users. Since Trino was on stable ground it felt imperative to move to Iceberg and fill in these gaps. When Ryan approached me with a Head of DevRel position at Tabular, I couldn’t pass up the opportunity. To be clear I left Starburst but not the Trino community. Being at Iceberg also helped me in my mission to continue forging these two technologies that I believe in so much.&#xA;&#xA;Tell us why Iceberg won already!&#xA;&#xA;Moving back to the meat of the subject. My first blog in the Trino community covered what I once called, the invisible Hive spec to alleviate confusion around why Trino would need a “Hive” connector if it’s a query engine itself. The reason we called the Hive connector as such is that it translated Parquet files sitting in an S3 object store into a schema that could be read and modified via a query engine that knew the Hive spec. This had nothing to do with Hive the query engine, but Hive the spec. Why was the Hive spec invisible 👻? Because nobody wrote it down. It was in the minds of the engineers who put Hive together, spread across Cloudera forums by engineers who had bashed their heads against the wall and reverse-engineered the binaries to understand this “spec”.&#xA;&#xA;Why do you even need a spec?&#xA;&#xA;Having an “invisible” spec was rather problematic, as every implementation of that spec ran on different assumptions. I spent days searching Hive solutions on Hortonworks/Cloudera forums trying to solve an error with solutions that for whatever reason, didn’t work on my instance of Hive. There were also implicit dependencies required with using the Hive spec. It’s actually incorrect to call the Hive spec as such because a spec should have independence of platform, programming language, and hardware, while including minimal implementation details not required for interoperability between implementations. SQL, for instance, is a declarative language that runs on systems written in, C++, Rust, Java, and doesn’t get into the business of telling query engines how to answer that query, just what behavior is expected.&#xA;&#xA;The open spec is why Iceberg won&#xA;&#xA;By the time there were various iterations of Hive from the Hadoop and Big Data Boom, there was not a very central source to lay a stake in the ground to standardize this spec. While you may imagine that we would have learned our lesson, until Iceberg, none of the projects officially formalized their assumptions and specifications for their project. Delta Lake and Hudi extended the Hive models to make migration from Hive simpler but kept a few of the issues that Hive introduced, like exposing the partitioning format to users running analysis.&#xA;&#xA;Open specifications and vendor politics&#xA;&#xA;You may seem skeptical that an open specification for a table format holds such weight, but it crept its way into a well-known feud between two large analytics vendors of the day, Databricks and Snowflake. Databricks has been the leading vendor in the data lakehouse market while Snowflake dominated the data warehouse market. Snowflake’s original strategy initially involved encouraging movement from the data lakehouse market back to the data warehouse market, while Databrick’s original strategy was to do the exact opposite. This conveyed the outdated trap that many vendors still fall prey to. This idea is that locking users in will help your business and stakeholders reduce churn and keep customers in the long run. Vendor lock-in was once a decades-long play, but as B2C consumerism expectations creep into B2B consumerism, we are seeing a gradual shift of practitioners demanding interoperability from vendors to give them the level of autonomy they have experienced with open-source projects.&#xA;&#xA;This surfaced with Snowflake when they announced that they would be offering support for an Iceberg external table functionality in 2022. At first, I raised my eyebrow at this as I figured this was a feeble attempt for Snowflake to market itself as an open-friendly warehouse when the external table would be nothing but an easy way to migrate data from the data lake to Snowflake. Whatever their motives, this was good visibility for Iceberg and I was thrilled that Snowflake was showcasing the need for an open specification, gimmick or not. This even pressured other competing data warehouses like BigQuery to add Iceberg support.&#xA;&#xA;!--emailsub--&#xA;&#xA;The final signal that the open Iceberg spec won&#xA;&#xA;I didn’t, however, expect to see what took place at both Snowflake Summit and Data + AI Summit this year in 2023. If you don’t know these are Snowflake and Databricks’ big events that happened on the same week as an extension of their feud. There were already some hints that Snowflake had dropped to signal that they were ramping up their Iceberg support but were doing a great job at keeping it under wraps. The final reveal came at Snowflake Summit; Snowflake now offers managed Iceberg catalog support.&#xA;&#xA;I was thrilled to see that finally, a data warehouse vendor understands there’s no way to beat open standards and they should adopt open storage as part of their core business model. What’s even more about this picture is they show both Trino and Spark engines as open compute alternatives to their own engine. This was beyond my expectations for Snowflake, and definitely showed me they were heading in the right direction for their customers.&#xA;&#xA;Meanwhile, in a California town not far away, Databricks would also have a response to Snowflake’s announcement stored up. Delta Lake 3.0 was announced and it now supports compatibility across Delta Lake, Iceberg, and Hudi (be it limited compatibility for Iceberg for now). Whatever happens, there is now opportunity for Databricks customers to trial Iceberg, and this should excite everyone. We’re one step closer to having this spec become the common denominator. With both of these moves from a company that initially only wanted their proprietary format to win and a company that built on a competing format, I have the opinion that Iceberg has won the format wars.&#xA;&#xA;Now I don’t want to act like these vendors simply have users’ best interests in mind. All they want is for you to make the decision to choose them. I work for a vendor, we also want your money. What I am seeing though is that now the industry is trending towards incentivizing openness as more customers demand it. In order for companies to stay ahead, they must embrace this fact rather than fight it. What is rather historical about this moment in time is that since the dawn of analytics and the data warehouse, there has been vendor lock-in on many fronts of the analytics market. This to me, signals the nail in the coffin of any capability for vendors to do this on the level playing field of open standards. It’s now up to the vendors to keep you happy and continuously stop you from churning. This in the long run is good for users and vendors and will ultimately drive better products.&#xA;&#xA;One quick aside, some may mention that Hudi recently added a “spec” so why does Iceberg having a spec give it the winning vote? I recommend you go back to reading the purpose of an open spec section earlier in this blog, then look at both the Iceberg spec and the Hudi “spec” and determine which one satisfies the criteria. Hudi’s “spec” exposes Java dependencies making it unusable for any system not running on the JDK, doesn’t clarify schema, and has a lot of implementation details rather than leaving that to the systems that implement it. This “spec” is something closer to an architecture document than an open spec.&#xA;&#xA;Will the Iceberg project drive Delta Lake and Hudi to nonexistence?&#xA;&#xA;Maybe, or maybe not. That really depends on the feature set that these three table formats offer and what the actual value they bring to the users is. In other words, you decide what’s important, we decide what we’re going to support, and vendors and the larger data community decide who stays and who goes. This simply boils down to what users want, and if there is enough deviation for it to make sense for multiple formats to exist. Having competition in open-source is every bit as healthy as having competition in the vendor space - with some exceptions:&#xA;&#xA;Exception 1: The projects don’t collectively build on an open specification. This enables vendor lock-in under the guise of being “open” simply because the source is available.&#xA;&#xA;Exception 2: The projects are not just doubled efforts of each other. This is a sad waste of time and adds analysis paralysis to users. If two projects have clear value added in different domains with some overlap, then the choice becomes clearer based on the use case.&#xA;&#xA;Exception 3: Any of the competing OSS projects primarily serve the needs of one or a few companies over the larger community. This exception is an anti-pattern of innovation brought up in the Innovator’s Dilemma. Focusing on the needs of the few will eventually force the majority to move to the next thing. Truly open projects will continue to evolve at the pace of the industry.&#xA;&#xA;Just because I’m touting that Apache Iceberg has “won the table format wars” with an open spec, does not mean I am discrediting the hard work done by the competing table formats or advocating not to use them. Delta Lake has made sizeable efforts to stay competitive on a feature-by-feature basis. I’m also still good friends with Mr. DeltaLake himself, Denny Lee. I hold no grudge against these amazing people trying to do better for their users and customers. I am also excited at the fact that all formats now have some level of interoperability for users outside of the Iceberg ecosystem!&#xA;&#xA;And…Hudi?&#xA;&#xA;I toiled with my take on Hudi. I really enjoyed working with the folks from Hudi and while I usually follow the “don’t say anything unless it’s nice” rule, it would be disingenuous not to give my real opinion on this matter. I even quoted Reddit user u/JudgingYouThisSecond to initially avoid saying my own words, but even his take still doesn’t quite capture my thoughts concisely:&#xA;&#xA;  Ultimately, there is room enough for there to be several winners in the table format space:&#xA;    Hudi, for folks who care about or need near-real-time ingestion and streaming. This project will likely be #3 in the overall pecking order as it&#39;s become less relevant and seems to be losing steam realative to the other two in this list (at least IMO)&#xA;  Delta Lake, for people who play a lot in the Databricks ecosystem and don&#39;t mind that Delta Lake (while OSS) may end up being a bit of a &#34;gateway drug&#34; into a pay-to-play world&#xA;  * Iceberg, for folks looking to avoid vendor lock-in and are looking for a table format with a growing community behind it.&#xA;    While my opinion may change as these projects evolve, I consider myself an &#34;Iceberg Guy&#34; at the moment.&#xA;    Comment by u/JudgingYouThisSecond in dataengineering&#xA;&#xA;I feel like the open-source Hudi project is just not in a state where I would recommend it to a friend. I once thought perhaps Hudi’s upserts made their complexity worth it, but both Delta and Iceberg have improved on this front. My opinion from supporting all three formats in the Trino community is that Hudi is an overly complex implementation with many leaky abstractions such as exposing Hadoop and other legacy dependencies (e.g. the KyroSerializer) and doesn’t actually provide proper transaction semantics.&#xA;&#xA;What I personally hope for is that we get to a point where we converge down to the minimum set of table format projects needed to scratch the different itches that shouldn’t be overlapping, and all adhering to the Iceberg specification. In essence, really only the Iceberg spec has won and Iceberg is currently the only implementation to support it. I would be thrilled to see Delta Lake and Hudi projects support the Iceberg spec and make this a fair race and make the new question, which Iceberg implementation won the most adoption? That game will be fun to play. Imagine Tabular, Databricks, Cloudera, and Onehouse all building on the same spec!&#xA;&#xA;Note: Please don’t respond with silly performance benchmarks with TPC (or any standardized dataset in a vendor-controlled benchmark) to suggest the performance of these benchmarks is relevant to this conversation. It’s not.&#xA;&#xA;Thanks for reading, and if you disagree and want to debate or love this and want to discuss it, reach out to me on LinkedIn, Twitter, or the Apache Iceberg Slack.&#xA;&#xA;Stay classy Data Folks!&#xA;&#xA;#iceberg #opensource #openstandard&#xA;&#xA;bits&#xA;&#xA;!--emailsub--]]&gt;</description>
      <content:encoded><![CDATA[<p><a href="https://images.unsplash.com/photo-1650434908449-9e7db28bbf5a?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwzMDAzMzh8MHwxfHNlYXJjaHwzfHxzdGFycyUyMGljZWJlcmd8ZW58MHx8fHwxNjgxNzg1OTE4&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080"><img src="https://images.unsplash.com/photo-1650434908449-9e7db28bbf5a?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwzMDAzMzh8MHwxfHNlYXJjaHwzfHxzdGFycyUyMGljZWJlcmd8ZW58MHx8fHwxNjgxNzg1OTE4&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=1080" alt="the night sky is lit up over the water" title="the night sky is lit up over the water"/></a></p>

<p>Photo by <a href="https://unsplash.com/@bot_va">Michail Dementiev</a> on <a href="https://unsplash.com/">Unsplash</a></p>

<p><strong>TL;DR:</strong> I believe Apache Iceberg won the table format wars, not because of a feature race, but primarily because of <a href="https://iceberg.apache.org/spec/">the open Iceberg spec</a>. There are <a href="https://iceberg.apache.org/docs/latest/partitioning/#icebergs-hidden-partitioning">some</a> <a href="https://iceberg.apache.org/spec/#snapshot-reference">features</a> only available in Iceberg due to the breaking of compatibility with Hive, which was also a contributing factor to the adoption of the implementation.</p>



<p><strong>Disclaimer:</strong> I am the Head of Developer Relations at Tabular and a Developer Advocate in both Apache Iceberg and Trino communities. All of my 🌶️ takes are my biased opinion and not necessarily the opinion of Tabular, the Apache Software Foundation, the Trino Software Foundation, or the communities I work with. This also goes into a bit of my personal story for leaving my previous company but relates to my reasoning so I offer you a TL;DR if you don’t care about the details.</p>



<h2 id="my-revelation-with-iceberg" id="my-revelation-with-iceberg">My revelation with Iceberg</h2>

<p>Two months ago, I made the difficult decision to leave <a href="https://www.starburst.io/">Starburst</a> which was hands down the best job I’ve ever had up to this point. Since I left I’ve had a lot of questions about my motivations for leaving and wanted to put some concerns to rest. This role allowed me to get deeply involved in open-source during working hours and showed me how I could aid the community to get traction on their work and drive the roadmap for many in the project. This was a new calling that overlapped with many altruist parts of how I define myself and was deeply rewarding.</p>

<p><a href="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38c6c7f7-dc35-4960-8808-02a4a3a723b7_2000x1015.png"><img src="https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38c6c7f7-dc35-4960-8808-02a4a3a723b7_2000x1015.png" alt=""/></a></p>

<p>I made some incredible friends, some of which have become invaluable mentors during this process of learning the nuances and interplay between venture capital and an open-source community. So why did I leave this job that I love so much?</p>

<h2 id="apache-iceberg-baby" id="apache-iceberg-baby">Apache Iceberg Baby</h2>

<p>Let’s time-travel (<a href="https://iceberg.apache.org/docs/latest/branching/">pun intended</a>) to the <a href="https://trino.io/episodes/15.html">first Iceberg episode of the Trino Community Broadcast</a>. In true ADHD form, I crammed learning about <a href="https://iceberg.apache.org/">Apache Iceberg</a> well into the night before the broadcast with the creator of Iceberg, <a href="https://www.linkedin.com/in/rdblue/">Ryan Blue</a>. While setting up that demo, I really started to understand what a game-changer Iceberg was. I had heard the Trino users and maintainers talk about Iceberg replacing Hive but it just didn’t sink in for the first couple of months. I mean really, what could be better than Hive? 🥲</p>

<p>While researching I learned about <a href="https://iceberg.apache.org/docs/latest/partitioning/#icebergs-hidden-partitioning">hidden partitioning</a>, <a href="https://iceberg.apache.org/docs/latest/evolution/#schema-evolution">schema evolution</a>, and most importantly, <a href="https://iceberg.apache.org/spec/">the open specification</a>. The whole package was just such an elegant solution to problems that had caused me and many in the Trino community failed deployments and late-night calls. Just as I had the epiphany with Trino (<a href="https://trino.io/blog/2022/08/02/leaving-facebook-meta-best-for-trino.html">Presto at the time</a>) of how big of a productivity booster SQL queries over multiple systems were, I had a similar experience with Iceberg that night. Preaching the combination of these two became somewhat of a mission of mine after that.</p>

<p><a href="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea787b8f-f9fa-4cc0-8e26-c18868bdf0ff_1151x647.png"><img src="https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea787b8f-f9fa-4cc0-8e26-c18868bdf0ff_1151x647.png" alt=""/></a></p>

<h2 id="ansi-sql-iceberg-parquet-s3" id="ansi-sql-iceberg-parquet-s3">ANSI SQL + Iceberg + Parquet + S3</h2>

<p>Immediately after that show, <a href="https://trino.io/blog/2021/05/03/a-gentle-introduction-to-iceberg.html">I wrote a four-blog series</a> on Trino on Iceberg, <a href="https://www.youtube.com/watch?v=5-Q74rCX2Z8">did a talk</a>, and built out <a href="https://github.com/bitsondatadev/trino-getting-started/tree/main/iceberg/trino-iceberg-minio">the getting started repository for Iceberg</a>. I was rather hooked on the thought of these two technologies in tandem. You start out with a system that can connect to <a href="https://trino.io/docs/current/connector.html">any data source</a> you <a href="https://trino.io/docs/current/develop/spi-overview.html">throw at it</a> and sees it as yet another SQL table. Take that system and add a table format that interacts with all the popular analytics engines people use today from <a href="https://iceberg.apache.org/docs/latest/getting-started/">Spark</a>, <a href="https://www.snowflake.com/blog/single-platform-improves-performance-analytics-data-types/">Snowflake</a>, and <a href="https://py.iceberg.apache.org/api/#duckdb">DuckDB</a>, to Trino variants like <a href="https://docs.aws.amazon.com/emr/latest/ReleaseGuide/emr-iceberg.html">EMR</a>, <a href="https://docs.aws.amazon.com/athena/latest/ug/querying-iceberg.html">Athena</a>, and <a href="https://docs.starburst.io/latest/connector/iceberg.html">Starburst</a>.</p>

<h3 id="standards-all-the-way-down" id="standards-all-the-way-down">Standards all the way down</h3>

<p>This <a href="https://www.youtube.com/watch?v=2bkPMrjqxgs&amp;list=PLIlqnK97FLdtQEad5pi22BefNuaSeBt1e">approach to data virtualization</a> is so interesting as each system offers full leverage over vendors trying to lock you in their particular query language or storage format. It pushes the incentives for vendors to support these open standards which puts them in a seemingly vulnerable position compared to locking users in. However, that’s a fallacy I hope vendors will slowly begin to understand is not true. With the open S3 storage standard, open file standards like Parquet, open table standards like Iceberg, and the ANSI SQL spec closely followed by Trino, the entire analytics warehouse has become a modular stack of truly open components. This is not just open in the sense of the freedom to use and contribute to a project, but the open standard that enables you the freedom to simply move between different projects.</p>

<p>This new freedom gives the user the features of a data warehouse, with the scalability of the cloud, and a free market of a la carte services to handle your needs at whatever price point you need at any given time. All users need to do in this new ecosystem is shop around and choose any open project or vendor that implements the open standard and your migration cost will be practically non-existent. This is the ultimate definition of future-proofing your architecture.</p>

<p><a href="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03492f03-0475-4682-addd-90623a616c64_500x500.jpeg"><img src="https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03492f03-0475-4682-addd-90623a616c64_500x500.jpeg" alt=""/></a></p>



<h2 id="back-to-why-i-left-starburst" id="back-to-why-i-left-starburst">Back to why I left Starburst</h2>

<h3 id="trino-community" id="trino-community">Trino Community</h3>

<p>I’ll quickly tie up why I left Starburst before I reveal why Iceberg won the table format wars. For the last three years, I have worked on building awareness around Trino. My partner in crime, <a href="https://www.linkedin.com/in/manfredmoser/">Manfred Moser</a>, had been in the Trino community and <a href="https://simpligility.ca/2022/10/trino-the-definitive-guide-2nd-edition/">literally wrote the book on Trino</a>. Together we spent long days and nights growing the Trino community. I loved every minute of it and honestly didn’t see myself leaving Starburst or shifting focus from Trino until it became an analytics organization standard.</p>

<p>Something became apparent though. Trino <a href="https://trino.io/blog/2023/01/10/trino-2022-the-rabbit-reflects.html">community health was thriving</a>, and there were many <a href="https://github.com/trinodb/trino/pull/17940">organic</a> <a href="https://www.youtube.com/watch?v=JMUtPl-cMRc">product</a> <a href="https://github.com/trinodb/trino/pull/17909">movements</a> taking place in the Trino community. <a href="https://www.linkedin.com/in/cole-m-bowden/">Cole Bowden</a> was boosting the Trino release process getting us to cutting Trino releases every 1-2 weeks which is unprecedented in open-source. Cole, Manfred, and I did a manual scan over the pull requests and gracefully closed or revived outdated or abandoned pull requests. The Trino community is in great shape.</p>

<h3 id="iceberg-community" id="iceberg-community">Iceberg Community</h3>

<p>As I looked at Iceberg, the adoption and awareness were growing at an unprecedented rate with <a href="https://www.snowflake.com/blog/expanding-the-data-cloud-with-apache-iceberg/">Snowflake</a>, <a href="https://cloud.google.com/bigquery/docs/iceberg-tables#create-using-biglake-metastore">BigQuery</a>, <a href="https://www.techtarget.com/searchdatamanagement/news/252509796/Starburst-Enterprise-brings-Apache-Iceberg-to-data-lakehouse">Starburst</a>, and <a href="https://docs.aws.amazon.com/athena/latest/ug/querying-iceberg.html">Athena</a> all announcing support between 2021 and 2022. However, nothing was moving the needle forward from a developer experience perspective. There was some initial amazing work done by <a href="https://www.linkedin.com/in/sredai/">Sam Redai</a>, but there was still so much to be done. I noticed the <a href="https://github.com/apache/iceberg/issues?q=is%3Aopen+is%3Aissue+label%3Adocs">Iceberg documentation needed improvement</a>. While many vendors were advocating for Iceberg, there was nobody putting in consistent work to the vanilla Iceberg site. PMCs like Ryan Blue, Jack Ye, Russel Spitzer, Dan Weeks, and many others are doing a great shared job of driving roadmap features for Iceberg, but no individual currently has the time to dedicate to the cat herding, improving communication in the project, or bettering the developer and contributor experience for users. Since Trino was on stable ground it felt imperative to move to Iceberg and fill in these gaps. When Ryan approached me with a Head of DevRel position at Tabular, I couldn’t pass up the opportunity. To be clear I left Starburst but not the Trino community. Being at Iceberg also helped me in my mission to continue forging these two technologies that I believe in so much.</p>

<h2 id="tell-us-why-iceberg-won-already" id="tell-us-why-iceberg-won-already">Tell us why Iceberg won already!</h2>

<p>Moving back to the meat of the subject. My first blog in the Trino community covered what I once called, <a href="https://trino.io/blog/2020/10/20/intro-to-hive-connector.html">the invisible Hive spec</a> to alleviate confusion around why Trino would need a “Hive” connector if it’s a query engine itself. The reason we called the Hive connector as such is that it translated Parquet files sitting in an S3 object store into a schema that could be read and modified via a query engine that knew the Hive spec. This had nothing to do with Hive the query engine, but Hive the spec. Why was the Hive spec <em>invisible</em> 👻? Because nobody wrote it down. It was in the minds of the engineers who put Hive together, spread across Cloudera forums by engineers who had bashed their heads against the wall and reverse-engineered the binaries to understand this “spec”.</p>

<h3 id="why-do-you-even-need-a-spec" id="why-do-you-even-need-a-spec">Why do you even need a spec?</h3>

<p>Having an “invisible” spec was rather problematic, as every implementation of that spec ran on different assumptions. I spent days <a href="https://community.cloudera.com/t5/forums/filteredbylabelpage/board-id/Questions/label-name/Apache%20Hive?type=everything">searching Hive solutions on Hortonworks/Cloudera forums</a> trying to solve an error with solutions that for whatever reason, didn’t work on my instance of Hive. There were also implicit dependencies required with using the Hive spec. It’s actually incorrect to call the Hive spec as such because a spec should have independence of platform, programming language, and hardware, while including minimal implementation details not required for interoperability between implementations. SQL, for instance, is a declarative language that runs on systems written in, C++, Rust, Java, and doesn’t get into the business of telling query engines how to answer that query, just what behavior is expected.</p>

<h3 id="the-open-spec-is-why-iceberg-won" id="the-open-spec-is-why-iceberg-won">The open spec is why Iceberg won</h3>

<p>By the time there were various iterations of Hive from the Hadoop and Big Data Boom, there was not a very central source to lay a stake in the ground to standardize this spec. While you may imagine that we would have learned our lesson, until Iceberg, none of the projects officially formalized their assumptions and specifications for their project. Delta Lake and Hudi extended the Hive models to make migration from Hive simpler but kept a few of the issues that Hive introduced, like exposing the partitioning format to users running analysis.</p>

<h3 id="open-specifications-and-vendor-politics" id="open-specifications-and-vendor-politics">Open specifications and vendor politics</h3>

<p>You may seem skeptical that an open specification for a table format holds such weight, but it crept its way into <a href="https://www.reddit.com/r/dataengineering/comments/13wqhby/databricks_and_snowflake_stop_fighting_on_social/">a well-known feud</a> between two large analytics vendors of the day, Databricks and Snowflake. Databricks has been the leading vendor in the data lakehouse market while Snowflake dominated the data warehouse market. Snowflake’s <a href="https://web.archive.org/web/20230201001308/https://www.snowflake.com/guides/what-data-lakehouse">original strategy</a> initially involved encouraging movement from the data lakehouse market back to the data warehouse market, while Databrick’s <a href="https://web.archive.org/web/20230626115324/https://www.databricks.com/glossary/data-warehouse">original strategy</a> was to do the exact opposite. This conveyed the outdated trap that many vendors still fall prey to. This idea is that locking users in will help your business and stakeholders reduce churn and keep customers in the long run. Vendor lock-in was once a decades-long play, but as B2C consumerism expectations creep into B2B consumerism, we are seeing a gradual shift of practitioners demanding interoperability from vendors to give them the level of autonomy they have experienced with open-source projects.</p>

<p>This surfaced with Snowflake when they announced that they would be offering <a href="https://www.snowflake.com/blog/expanding-the-data-cloud-with-apache-iceberg/">support for an Iceberg external table functionality</a> in 2022. At first, I raised my eyebrow at this as I figured this was a feeble attempt for Snowflake to market itself as an open-friendly warehouse when the external table would be nothing but an easy way to migrate data from the data lake to Snowflake. Whatever their motives, this was good visibility for Iceberg and I was thrilled that Snowflake was showcasing the need for an open specification, gimmick or not. This even pressured other competing data warehouses like BigQuery to add Iceberg support.</p>



<h3 id="the-final-signal-that-the-open-iceberg-spec-won" id="the-final-signal-that-the-open-iceberg-spec-won">The final signal that the open Iceberg spec won</h3>

<p>I didn’t, however, expect to see what took place at both Snowflake Summit and Data + AI Summit this year in 2023. If you don’t know these are Snowflake and Databricks’ big events that happened on the same week as an extension of their feud. There were <a href="https://www.reddit.com/r/dataengineering/comments/13wqhby/comment/jmdo259/">already some hints that Snowflake had dropped</a> to signal that they were ramping up their Iceberg support but were doing a great job at keeping it under wraps. The final reveal came at Snowflake Summit; Snowflake now offers <a href="https://www.linkedin.com/feed/update/urn:li:activity:7079822990607089665">managed Iceberg catalog support</a>.</p>

<p><a href="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9168cb20-ff36-4f58-873f-1a7a568fc75f_4096x3072.png"><img src="https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9168cb20-ff36-4f58-873f-1a7a568fc75f_4096x3072.png" alt=""/></a></p>

<p>I was thrilled to see that finally, a data warehouse vendor understands there’s no way to beat open standards and they should adopt open storage as part of their core business model. What’s even more about this picture is they show both Trino and Spark engines as open compute alternatives to their own engine. This was beyond my expectations for Snowflake, and definitely showed me they were heading in the right direction for their customers.</p>

<p>Meanwhile, in a California town not far away, Databricks would also have a response to Snowflake’s announcement stored up. <a href="https://www.databricks.com/company/newsroom/press-releases/announcing-delta-lake-30-new-universal-format-offers-automatic">Delta Lake 3.0 was announced</a> and it now supports compatibility across Delta Lake, Iceberg, and Hudi (be it <a href="https://github.com/delta-io/delta/commit/54492016f0eb9c12d25cbbdcc85804fb0a5d9a3a">limited compatibility for Iceberg for now</a>). Whatever happens, there is now opportunity for Databricks customers to trial Iceberg, and this should excite everyone. We’re one step closer to having this spec become the common denominator. With both of these moves from a company that initially only wanted their proprietary format to win and a company that built on a competing format, I have the opinion that Iceberg has won the format wars.</p>

<p>Now I don’t want to act like these vendors simply have users’ best interests in mind. All they want is for you to make the decision to choose them. I work for a vendor, we also want your money. What I am seeing though is that now the industry is trending towards incentivizing openness as more customers demand it. In order for companies to stay ahead, they must embrace this fact rather than fight it. What is rather historical about this moment in time is that since the dawn of analytics and the data warehouse, there has been vendor lock-in on many fronts of the analytics market. This to me, signals the nail in the coffin of any capability for vendors to do this on the level playing field of open standards. It’s now up to the vendors to keep you happy and continuously stop you from churning. This in the long run is good for users and vendors and will ultimately drive better products.</p>

<p>One quick aside, some may mention that Hudi recently added a “spec” so why does Iceberg having a spec give it the winning vote? I recommend you go back to reading the purpose of an open spec section earlier in this blog, then look at both the Iceberg <a href="https://web.archive.org/web/20230521215437/https://iceberg.apache.org/spec/">spec</a> and the Hudi <a href="https://web.archive.org/web/20230609034725/https://hudi.apache.org/tech-specs/">“spec”</a> and determine which one satisfies the criteria. Hudi’s “spec” <a href="https://web.archive.org/web/20230609034725/https://hudi.apache.org/tech-specs/#log-file-format">exposes Java dependencies</a> making it unusable for any system not running on the JDK, doesn’t clarify schema, and has <a href="https://web.archive.org/web/20230609034725/https://hudi.apache.org/tech-specs/#reader-expectations">a lot of implementation details</a> rather than leaving that to the systems that implement it. This “spec” is something closer to an architecture document than an open spec.</p>

<h3 id="will-the-iceberg-project-drive-delta-lake-and-hudi-to-nonexistence" id="will-the-iceberg-project-drive-delta-lake-and-hudi-to-nonexistence">Will the Iceberg project drive Delta Lake and Hudi to nonexistence?</h3>

<p>Maybe, or maybe not. That really depends on the feature set that these three table formats offer and what the <em><strong>actual value</strong></em> they bring to the users is. In other words, you decide what’s important, we decide what we’re going to support, and vendors and the larger data community decide who stays and who goes. This simply boils down to what users want, and if there is enough deviation for it to make sense for multiple formats to exist. Having competition in open-source is every bit as healthy as having competition in the vendor space – with some exceptions:</p>

<p>Exception 1: The projects don’t collectively build on an open specification. This enables vendor lock-in under the guise of being “open” simply because the source is available.</p>

<p>Exception 2: The projects are not just doubled efforts of each other. This is a sad waste of time and adds analysis paralysis to users. If two projects have clear value added in different domains with some overlap, then the choice becomes clearer based on the use case.</p>

<p>Exception 3: Any of the competing OSS projects primarily serve the needs of one or a few companies over the larger community. This exception is an anti-pattern of innovation brought up in <a href="https://en.wikipedia.org/wiki/The_Innovator%27s_Dilemma">the Innovator’s Dilemma</a>. Focusing on the needs of the few will eventually force the majority to move to the next thing. Truly open projects will continue to evolve at the pace of the industry.</p>

<p>Just because I’m touting that Apache Iceberg has “won the table format wars” with an open spec, does not mean I am discrediting the hard work done by the competing table formats or advocating not to use them. Delta Lake has made <a href="https://www.databricks.com/blog/2022/06/30/open-sourcing-all-of-delta-lake.html">sizeable efforts to stay competitive</a> on a feature-by-feature basis. I’m also <a href="https://www.linkedin.com/feed/update/urn:li:ugcPost:7055051825321832448?commentUrn=urn%3Ali%3Acomment%3A%28ugcPost%3A7055051825321832448%2C7055173804460818432%29&amp;replyUrn=urn%3Ali%3Acomment%3A%28ugcPost%3A7055051825321832448%2C7055245121428094976%29&amp;dashCommentUrn=urn%3Ali%3Afsd_comment%3A%287055173804460818432%2Curn%3Ali%3AugcPost%3A7055051825321832448%29&amp;dashReplyUrn=urn%3Ali%3Afsd_comment%3A%287055245121428094976%2Curn%3Ali%3AugcPost%3A7055051825321832448%29">still good friends</a> with Mr. DeltaLake himself, <a href="https://www.linkedin.com/in/dennyglee/">Denny Lee</a>. I hold no grudge against these amazing people trying to do better for their users and customers. I am also excited at the fact that all formats now have some level of interoperability for users outside of the Iceberg ecosystem!</p>

<p><a href="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43f2c01a-a8ec-4368-b106-a351c1b4f3fb_1280x960.jpeg"><img src="https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F43f2c01a-a8ec-4368-b106-a351c1b4f3fb_1280x960.jpeg" alt=""/></a></p>

<h3 id="and-hudi" id="and-hudi">And…Hudi?</h3>

<p>I toiled with my take on Hudi. I <a href="https://www.youtube.com/watch?v=aL3PfMjvFM4">really enjoyed working with the folks from Hudi</a> and while I usually follow the “don’t say anything unless it’s nice” rule, it would be disingenuous not to give my real opinion on this matter. I even quoted Reddit user u/JudgingYouThisSecond to initially avoid saying my own words, but even his take still doesn’t quite capture my thoughts concisely:</p>

<blockquote><p>Ultimately, there is room enough for there to be several winners in the table format space:</p>
<ul><li>Hudi, for folks who care about or need near-real-time ingestion and streaming. This project will likely be #3 in the overall pecking order as it&#39;s become less relevant and seems to be losing steam realative to the other two in this list (at least IMO)</li>
<li>Delta Lake, for people who play a lot in the Databricks ecosystem and don&#39;t mind that Delta Lake (while OSS) may end up being a bit of a “gateway drug” into a pay-to-play world</li>
<li>Iceberg, for folks looking to avoid vendor lock-in and are looking for a table format with a growing community behind it.</li></ul>

<p>While my opinion may change as these projects evolve, I consider myself an “Iceberg Guy” at the moment.</p>

<p><a href="https://www.reddit.com/r/dataengineering/comments/13gevpu/whats_the_view_on_apache_iceberg/jjzntu1/">Comment</a> by <a href="https://www.reddit.com/user/JudgingYouThisSecond">u/JudgingYouThisSecond</a> in <a href="https://www.reddit.com/r/dataengineering/">dataengineering</a></p></blockquote>

<p>I feel like the open-source Hudi project is just not in a state where I would recommend it to a friend. I once thought perhaps Hudi’s upserts made their complexity worth it, but both <a href="https://docs.delta.io/latest/delta-streaming.html#table-streaming-reads-and-writes">Delta</a> and <a href="https://lists.apache.org/thread/1pjn2010r45lq6tk7jn2g4klg0xv68zn">Iceberg</a> have improved on this front. My opinion from supporting all three formats in the Trino community is that Hudi is <a href="https://www.reddit.com/r/dataengineering/comments/11qvpnl/is_hudi_a_hardcomplex_tool_as_it_seems/">an overly complex implementation</a> with many leaky abstractions such as exposing Hadoop and other legacy dependencies (e.g. the KyroSerializer) and doesn’t actually provide proper transaction semantics.</p>

<p>What I personally hope for is that we get to a point where we converge down to the minimum set of table format projects needed to scratch the different itches that shouldn’t be overlapping, and all adhering to the Iceberg specification. In essence, really only the Iceberg spec has won and Iceberg is currently the only implementation to support it. I would be thrilled to see Delta Lake and Hudi projects support the Iceberg spec and make this a fair race and make the new question, which Iceberg implementation won the most adoption? That game will be fun to play. Imagine Tabular, Databricks, Cloudera, and Onehouse all building on the same spec!</p>

<p>Note: Please don’t respond with <a href="https://write.as/bitsondatadev/what-is-benchmarketing-and-why-is-it-bad">silly performance benchmarks with TPC</a> (or any standardized dataset in a vendor-controlled benchmark) to suggest the performance of these benchmarks is relevant to this conversation. It’s not.</p>

<p>Thanks for reading, and if you disagree and want to debate or love this and want to discuss it, reach out to me on <a href="https://www.linkedin.com/in/bitsondatadev/">LinkedIn</a>, <a href="https://twitter.com/bitsondatadev">Twitter</a>, or the <a href="https://join.slack.com/t/apache-iceberg/shared_invite/zt-1uva9gyp1-TrLQl7o~nZ5PsTVgl6uoEQ">Apache Iceberg Slack</a>.</p>

<p>Stay classy Data Folks!</p>

<p><a href="https://bitsondata.dev/tag:iceberg" class="hashtag"><span>#</span><span class="p-category">iceberg</span></a> <a href="https://bitsondata.dev/tag:opensource" class="hashtag"><span>#</span><span class="p-category">opensource</span></a> <a href="https://bitsondata.dev/tag:openstandard" class="hashtag"><span>#</span><span class="p-category">openstandard</span></a></p>

<p><em>bits</em></p>


]]></content:encoded>
      <guid>https://bitsondata.dev/iceberg-won-the-table-format-war</guid>
      <pubDate>Wed, 05 Jul 2023 17:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Trino on ice IV: Deep dive into Iceberg internals</title>
      <link>https://bitsondata.dev/trino-iceberg-iv-deep-dive?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[&#xA;&#xA;So far, this series has covered some very interesting user level concepts of the Iceberg model, and how you can take advantage of them using the Trino query engine. This blog post dives into some implementation details of Iceberg by dissecting some files that result from various operations carried out using Trino. To dissect you must use some surgical instrumentation, namely Trino, Avro tools, the MinIO client tool and Iceberg’s core library. It’s useful to dissect how these files work, not only to help understand how Iceberg works, but also to aid in troubleshooting issues, should you have any issues during ingestion or querying of your Iceberg table. I like to think of this type of debugging much like a fun game of operation, and you’re looking to see what causes the red errors to fly by on your screen.&#xA;&#xA;!--more--&#xA;&#xA;---&#xA;&#xA;Trino on ice is a series, covering the details around how the Iceberg table format works with the Trino query engine. It’s recommended to read the posts sequentially as the examples build on previous posts in this series:&#xA;&#xA;Trino on ice I: A gentle introduction to Iceberg&#xA;Trino on ice II: In-place table evolution and cloud compatibility with Iceberg&#xA;Trino on ice III: Iceberg concurrency model, snapshots, and the Iceberg spec&#xA;Trino on ice IV: Deep dive into Iceberg internals&#xA;&#xA;---&#xA;&#xA;Understanding Iceberg metadata&#xA;&#xA;Iceberg can use any compatible metastore, but for Trino, it only supports the  Hive metastore and AWS Glue similar to the Hive connector. This is because there is already a vast amount of testing and support for using the Hive metastore in Trino. Likewise, many Trino use cases that currently use data lakes already use the Hive connector and therefore the Hive metastore. This makes it convenient to have as the leading supported use case as existing users can easily migrate between Hive to Iceberg tables. Since there is no indication of which connector is actually executed in the diagram of the Hive connector architecture, it serves as a diagram that can be used for both Hive and Iceberg. The only difference is the connector used, but if you create a table in Hive, you can  view the same table in Iceberg.&#xA;&#xA;To recap the steps taken from the first three blogs; the first blog created an events table, while the first two blogs ran two insert statements. The first insert contained three records, while the second insert contained a single record.&#xA;&#xA;Up until this point, the state of the files in MinIO haven’t really been shown except some of the manifest list pointers from the snapshot in the third blog post. Using the MinIO client tool, you can list files that Iceberg generated through all these operations and then try to understand what purpose they are serving.&#xA;&#xA;% mc tree -f local/&#xA;local/&#xA;└─ iceberg&#xA;   └─ logging.db&#xA;      └─ events&#xA;         ├─ data&#xA;         │  ├─ eventtimeday=2021-04-01&#xA;         │  │  ├─ 51eb1ea6-266b-490f-8bca-c63391f02d10.orc&#xA;         │  │  └─ cbcf052d-240d-4881-8a68-2bbc0f7e5233.orc&#xA;         │  └─ eventtimeday=2021-04-02&#xA;         │     └─ b012ec20-bbdd-47f5-89d3-57b9e32ea9eb.orc&#xA;         └─ metadata&#xA;            ├─ 00000-c5cfaab4-f82f-4351-b2a5-bd0e241f84bc.metadata.json&#xA;            ├─ 00001-27c8c2d1-fdbb-429d-9263-3654d818250e.metadata.json&#xA;            ├─ 00002-33d69acc-94cb-44bc-b2a1-71120e749d9a.metadata.json&#xA;            ├─ 23cc980c-9570-42ed-85cf-8658fda2727d-m0.avro&#xA;            ├─ 92382234-a4a6-4a1b-bc9b-24839472c2f6-m0.avro&#xA;            ├─ snap-2720489016575682283-1-92382234-a4a6-4a1b-bc9b-24839472c2f6.avro&#xA;            ├─ snap-4564366177504223943-1-23cc980c-9570-42ed-85cf-8658fda2727d.avro&#xA;            └─ snap-6967685587675910019-1-bcbe9133-c51c-42a9-9c73-f5b745702cb0.avro&#xA;&#xA;There are a lot of files here, but here are a couple of patterns that you can observe with these files.&#xA;&#xA;First, the top two directories are named data and metadata.&#xA;&#xA;/bucket/database/table/data//bucket/database/table/metadata/&#xA;&#xA;As you might expect, data contains the actual ORC files split by partition. This is akin to what you would see in a Hive table data directory. What is really of interest here is the metadata directory. There are specifically three patterns of files you’ll find here.&#xA;&#xA;/bucket/database/table/metadata/file-id.avro&#xA;&#xA;/bucket/database/table/metadata/snap-snapshot-id-version-file-id.avro&#xA;&#xA;/bucket/database/table/metadata/version-commit-UUID.metadata.json&#xA;&#xA;Iceberg has a persistent tree structure that manages various snapshots of the data that are created for every mutation of the data. This enables not only a concurrency model that supports serializable isolation, but also cool features like time travel across a linear progression of snapshots.&#xA;&#xA;This tree structure contains two types of Avro files, manifest lists and manifest files. Manifest list files contain pointers to various manifest files and the manifest files themselves point to various data files. This post starts out by covering these manifest files, and later covers the table metadata files that are suffixed by .metadata.json.&#xA;&#xA;The last blog covered the command in Trino that shows the snapshot information that is stored in the metastore. Here is that command and its output again for your review.&#xA;&#xA;SELECT manifestlist &#xA;FROM iceberg.logging.&#34;events$snapshots&#34;;&#xA;&#xA;Result:&#xA;&#xA;table&#xA;trthsnapshots/th/tr&#xA;trtds3a://iceberg/logging.db/events/metadata/snap-6967685587675910019-1-bcbe9133-c51c-42a9-9c73-f5b745702cb0.avro/td/tr&#xA;trtds3a://iceberg/logging.db/events/metadata/snap-2720489016575682283-1-92382234-a4a6-4a1b-bc9b-24839472c2f6.avro/td/tr&#xA;trtds3a://iceberg/logging.db/events/metadata/snap-4564366177504223943-1-23cc980c-9570-42ed-85cf-8658fda2727d.avro/td/tr&#xA;/table&#xA;&#xA;You’ll notice that the manifest list returns the Avro files prefixed with&#xA;snap- are returned. These files are directly correlated with the snapshot record stored in the metastore. According to the diagram above, snapshots are records in the metastore that contain the url of the manifest list in the Avro file. Avro files are binary files and not something you can just open up in a text editor to read. Using the avro-tools.jar tool distributed by the Apache Avro project, you can actually inspect the contents of this file to get a better understanding of how it is used by Iceberg.&#xA;&#xA;The first snapshot is generated on the creation of the events table. Upon inspecting this file, you notice that the file is empty. The output is an empty line that the jq JSON command line utility removes on pretty printing the JSON that is returned, which is just a newline. This snapshot represents an empty state of the table upon creation. To investigate the snapshots you need to download the files to your local filesystem. Let&#39;s move them to the home  directory:&#xA;&#xA;% java -jar  ~/Desktop/avrofiles/avro-tools-1.10.0.jar tojson ~/snap-6967685587675910019-1-bcbe9133-c51c-42a9-9c73-f5b745702cb0.avro | jq .&#xA;&#xA;Result: (is empty)&#xA;&#xA;The second snapshot is a little more interesting and actually shows us the contents of a manifest list.&#xA;&#xA;% java -jar  ~/Desktop/avrofiles/avro-tools-1.10.0.jar tojson ~/snap-2720489016575682283-1-92382234-a4a6-4a1b-bc9b-24839472c2f6.avro | jq .&#xA;&#xA;Result:&#xA;&#xA;{&#xA;   &#34;manifestpath&#34;:&#34;s3a://iceberg/logging.db/events/metadata/92382234-a4a6-4a1b-bc9b-24839472c2f6-m0.avro&#34;,&#xA;   &#34;manifestlength&#34;:6114,&#xA;   &#34;partitionspecid&#34;:0,&#xA;   &#34;addedsnapshotid&#34;:{&#xA;      &#34;long&#34;:2720489016575682000&#xA;   },&#xA;   &#34;addeddatafilescount&#34;:{&#xA;      &#34;int&#34;:2&#xA;   },&#xA;   &#34;existingdatafilescount&#34;:{&#xA;      &#34;int&#34;:0&#xA;   },&#xA;   &#34;deleteddatafilescount&#34;:{&#xA;      &#34;int&#34;:0&#xA;   },&#xA;   &#34;partitions&#34;:{&#xA;      &#34;array&#34;:[&#xA;         {&#xA;            &#34;containsnull&#34;:false,&#xA;            &#34;lowerbound&#34;:{&#xA;               &#34;bytes&#34;:&#34;\u001eI\u0000\u0000&#34;&#xA;            },&#xA;            &#34;upperbound&#34;:{&#xA;               &#34;bytes&#34;:&#34;\u001fI\u0000\u0000&#34;&#xA;            }&#xA;         }&#xA;      ]&#xA;   },&#xA;   &#34;addedrowscount&#34;:{&#xA;      &#34;long&#34;:3&#xA;   },&#xA;   &#34;existingrowscount&#34;:{&#xA;      &#34;long&#34;:0&#xA;   },&#xA;   &#34;deletedrowscount&#34;:{&#xA;      &#34;long&#34;:0&#xA;   }&#xA;}&#xA;&#xA;To understand each of the values in each of these rows, you can refer to the  Iceberg &#xA;specification in the manifest list file section. Instead of covering these exhaustively, let&#39;s focus on a few key fields. Below are the fields, and their definition according to the specification.&#xA;&#xA;manifestpath - Location of the manifest file.&#xA;partitionspecid - ID of a partition spec used to write the manifest; must be listed in table metadata partition-specs.&#xA;addedsnapshotid - ID of the snapshot where the manifest file was added.&#xA;partitions - A list of field summaries for each partition field in the spec. Each field in the list corresponds to a field in the manifest file’s partition spec.&#xA;addedrowscount - Number of rows in all files in the manifest that have status ADDED, when null this is assumed to be non-zero.&#xA;&#xA;As mentioned above, manifest lists hold references to various manifest files. These manifest paths are the pointers in the persistent tree that tells any client using Iceberg where to find all of the manifest files associated with a particular snapshot. To traverse this tree, you can look over the different manifest paths to find all the manifest files associated with the particular snapshot you want to traverse. Partition spec ids are helpful to know the current partition specification which are stored in the table metadata in the metastore. This references where to find the spec in the metastore. Added snapshot ids tells you which snapshot is associated with the manifest list. Partitions hold some high level partition bound information to make for faster querying. If a query is looking for a particular value, it only traverses the manifest files where the query values fall within the range of the file values. Finally, you get a few metrics like the number of changed rows and data files, one of which is the count of added rows. The first operation consisted of three rows inserts and the second operation was the insertion of one row. Using the row counts you can easily determine which manifest file belongs to which operation.&#xA;&#xA;The following command shows the final snapshot after both operations executed and filters out only the fields pointed out above.&#xA;&#xA;% java -jar  ~/Desktop/avrofiles/avro-tools-1.10.0.jar tojson ~/snap-4564366177504223943-1-23cc980c-9570-42ed-85cf-8658fda2727d.avro | jq &#39;. | {manifestpath: .manifestpath, partitionspecid: .partitionspecid, addedsnapshotid: .addedsnapshotid, partitions: .partitions, addedrowscount: .addedrowscount }&#39;&#xA;&#xA;Result: &#xA;&#xA;{&#xA;   &#34;manifestpath&#34;:&#34;s3a://iceberg/logging.db/events/metadata/23cc980c-9570-42ed-85cf-8658fda2727d-m0.avro&#34;,&#xA;   &#34;partitionspecid&#34;:0,&#xA;   &#34;addedsnapshotid&#34;:{&#xA;      &#34;long&#34;:4564366177504223700&#xA;   },&#xA;   &#34;partitions&#34;:{&#xA;      &#34;array&#34;:[&#xA;         {&#xA;            &#34;containsnull&#34;:false,&#xA;            &#34;lowerbound&#34;:{&#xA;               &#34;bytes&#34;:&#34;\u001eI\u0000\u0000&#34;&#xA;            },&#xA;            &#34;upperbound&#34;:{&#xA;               &#34;bytes&#34;:&#34;\u001eI\u0000\u0000&#34;&#xA;            }&#xA;         }&#xA;      ]&#xA;   },&#xA;   &#34;addedrowscount&#34;:{&#xA;      &#34;long&#34;:1&#xA;   }&#xA;}&#xA;{&#xA;   &#34;manifestpath&#34;:&#34;s3a://iceberg/logging.db/events/metadata/92382234-a4a6-4a1b-bc9b-24839472c2f6-m0.avro&#34;,&#xA;   &#34;partitionspecid&#34;:0,&#xA;   &#34;addedsnapshotid&#34;:{&#xA;      &#34;long&#34;:2720489016575682000&#xA;   },&#xA;   &#34;partitions&#34;:{&#xA;      &#34;array&#34;:[&#xA;         {&#xA;            &#34;containsnull&#34;:false,&#xA;            &#34;lowerbound&#34;:{&#xA;               &#34;bytes&#34;:&#34;\u001eI\u0000\u0000&#34;&#xA;            },&#xA;            &#34;upperbound&#34;:{&#xA;               &#34;bytes&#34;:&#34;\u001fI\u0000\u0000&#34;&#xA;            }&#xA;         }&#xA;      ]&#xA;   },&#xA;   &#34;addedrowscount&#34;:{&#xA;      &#34;long&#34;:3&#xA;   }&#xA;}&#xA;&#xA;In the listing of the manifest file related to the last snapshot, you notice the first operation where three rows were inserted is contained in the manifest file in the second JSON object. You can determine this from the snapshot id, as well as, the number of rows that were added in the operation. The first JSON object contains the last operation that inserted a single row. So the most recent operations are listed in reverse commit order.&#xA;&#xA;The next command does the same listing of the file that you ran with the manifest list, except you run this on the manifest files themselves to expose their contents and discuss them. To begin with, you run the command to show the contents of the manifest file associated with the insertion of three rows.&#xA;&#xA;% java -jar  ~/avro-tools-1.10.0.jar tojson ~/Desktop/avrofiles/92382234-a4a6-4a1b-bc9b-24839472c2f6-m0.avro | jq .&#xA;&#xA;Result: &#xA;&#xA;{&#xA;   &#34;status&#34;:1,&#xA;   &#34;snapshotid&#34;:{&#xA;      &#34;long&#34;:2720489016575682000&#xA;   },&#xA;   &#34;datafile&#34;:{&#xA;      &#34;filepath&#34;:&#34;s3a://iceberg/logging.db/events/data/eventtimeday=2021-04-01/51eb1ea6-266b-490f-8bca-c63391f02d10.orc&#34;,&#xA;      &#34;fileformat&#34;:&#34;ORC&#34;,&#xA;      &#34;partition&#34;:{&#xA;         &#34;eventtimeday&#34;:{&#xA;            &#34;int&#34;:18718&#xA;         }&#xA;      },&#xA;      &#34;recordcount&#34;:1,&#xA;      &#34;filesizeinbytes&#34;:870,&#xA;      &#34;blocksizeinbytes&#34;:67108864,&#xA;      &#34;columnsizes&#34;:null,&#xA;      &#34;valuecounts&#34;:{&#xA;         &#34;array&#34;:[&#xA;            {&#xA;               &#34;key&#34;:1,&#xA;               &#34;value&#34;:1&#xA;            },&#xA;            {&#xA;               &#34;key&#34;:2,&#xA;               &#34;value&#34;:1&#xA;            },&#xA;            {&#xA;               &#34;key&#34;:3,&#xA;               &#34;value&#34;:1&#xA;            },&#xA;            {&#xA;               &#34;key&#34;:4,&#xA;               &#34;value&#34;:1&#xA;            }&#xA;         ]&#xA;      },&#xA;      &#34;nullvaluecounts&#34;:{&#xA;         &#34;array&#34;:[&#xA;            {&#xA;               &#34;key&#34;:1,&#xA;               &#34;value&#34;:0&#xA;            },&#xA;            {&#xA;               &#34;key&#34;:2,&#xA;               &#34;value&#34;:0&#xA;            },&#xA;            {&#xA;               &#34;key&#34;:3,&#xA;               &#34;value&#34;:0&#xA;            },&#xA;            {&#xA;               &#34;key&#34;:4,&#xA;               &#34;value&#34;:0&#xA;            }&#xA;         ]&#xA;      },&#xA;      &#34;nanvaluecounts&#34;:null,&#xA;      &#34;lowerbounds&#34;:{&#xA;         &#34;array&#34;:[&#xA;            {&#xA;               &#34;key&#34;:1,&#xA;               &#34;value&#34;:&#34;ERROR&#34;&#xA;            },&#xA;            {&#xA;               &#34;key&#34;:3,&#xA;               &#34;value&#34;:&#34;Oh noes&#34;&#xA;            }&#xA;         ]&#xA;      },&#xA;      &#34;upperbounds&#34;:{&#xA;         &#34;array&#34;:[&#xA;            {&#xA;               &#34;key&#34;:1,&#xA;               &#34;value&#34;:&#34;ERROR&#34;&#xA;            },&#xA;            {&#xA;               &#34;key&#34;:3,&#xA;               &#34;value&#34;:&#34;Oh noes&#34;&#xA;            }&#xA;         ]&#xA;      },&#xA;      &#34;keymetadata&#34;:null,&#xA;      &#34;splitoffsets&#34;:null&#xA;   }&#xA;}&#xA;{&#xA;   &#34;status&#34;:1,&#xA;   &#34;snapshotid&#34;:{&#xA;      &#34;long&#34;:2720489016575682000&#xA;   },&#xA;   &#34;datafile&#34;:{&#xA;      &#34;filepath&#34;:&#34;s3a://iceberg/logging.db/events/data/eventtimeday=2021-04-02/b012ec20-bbdd-47f5-89d3-57b9e32ea9eb.orc&#34;,&#xA;      &#34;fileformat&#34;:&#34;ORC&#34;,&#xA;      &#34;partition&#34;:{&#xA;         &#34;eventtimeday&#34;:{&#xA;            &#34;int&#34;:18719&#xA;         }&#xA;      },&#xA;      &#34;recordcount&#34;:2,&#xA;      &#34;filesizeinbytes&#34;:1084,&#xA;      &#34;blocksizeinbytes&#34;:67108864,&#xA;      &#34;columnsizes&#34;:null,&#xA;      &#34;valuecounts&#34;:{&#xA;         &#34;array&#34;:[&#xA;            {&#xA;               &#34;key&#34;:1,&#xA;               &#34;value&#34;:2&#xA;            },&#xA;            {&#xA;               &#34;key&#34;:2,&#xA;               &#34;value&#34;:2&#xA;            },&#xA;            {&#xA;               &#34;key&#34;:3,&#xA;               &#34;value&#34;:2&#xA;            },&#xA;            {&#xA;               &#34;key&#34;:4,&#xA;               &#34;value&#34;:2&#xA;            }&#xA;         ]&#xA;      },&#xA;      &#34;nullvaluecounts&#34;:{&#xA;         &#34;array&#34;:[&#xA;            {&#xA;               &#34;key&#34;:1,&#xA;               &#34;value&#34;:0&#xA;            },&#xA;            {&#xA;               &#34;key&#34;:2,&#xA;               &#34;value&#34;:0&#xA;            },&#xA;            {&#xA;               &#34;key&#34;:3,&#xA;               &#34;value&#34;:0&#xA;            },&#xA;            {&#xA;               &#34;key&#34;:4,&#xA;               &#34;value&#34;:0&#xA;            }&#xA;         ]&#xA;      },&#xA;      &#34;nanvaluecounts&#34;:null,&#xA;      &#34;lowerbounds&#34;:{&#xA;         &#34;array&#34;:[&#xA;            {&#xA;               &#34;key&#34;:1,&#xA;               &#34;value&#34;:&#34;ERROR&#34;&#xA;            },&#xA;            {&#xA;               &#34;key&#34;:3,&#xA;               &#34;value&#34;:&#34;Double oh noes&#34;&#xA;            }&#xA;         ]&#xA;      },&#xA;      &#34;upperbounds&#34;:{&#xA;         &#34;array&#34;:[&#xA;            {&#xA;               &#34;key&#34;:1,&#xA;               &#34;value&#34;:&#34;WARN&#34;&#xA;            },&#xA;            {&#xA;               &#34;key&#34;:3,&#xA;               &#34;value&#34;:&#34;Maybeh oh noes?&#34;&#xA;            }&#xA;         ]&#xA;      },&#xA;      &#34;keymetadata&#34;:null,&#xA;      &#34;splitoffsets&#34;:null&#xA;   }&#xA;}&#xA;&#xA;Now this is a very big output, but in summary, there’s really not too much to these files. As before, there is a Manifest section in the Iceberg spec that details what each of these fields means. Here are the important fields:&#xA;&#xA;snapshotid - Snapshot id where the file was added, or deleted if status is two. Inherited when null.&#xA;datafile - Field containing metadata about the data files pertaining to the manifest file, such as file path, partition tuple, metrics, etc…&#xA;datafile.filepath - Full URI for the file with FS scheme.&#xA;datafile.partition - Partition data tuple, schema based on the partition spec.&#xA;datafile.recordcount - Number of records in the data file.&#xA;datafile.count - Multiple fields that contain a map from column id to  number of values, null, nan counts in the file. These can be used to quickly  filter out unnecessary get operations.&#xA;datafile.bounds - Multiple fields that contain a map from column id to lower or upper bound in the column serialized as binary. Each value must be less than or equal to all non-null, non-NaN values in the column for the file.&#xA;&#xA;Each data file struct contains a partition and data file that it maps to. These files only be scanned and returned if the criteria for the query is met when  checking all of the count, bounds, and other statistics that are recorded in the file. Ideally only files that contain data relevant to the query should be scanned at all. Having information like the record count may also help in the query planning process to determine splits and other information. This particular optimization hasn’t been completed yet as planning typically happens before traversal of the files. It is still in ongoing discussion and is discussed a bit by Iceberg creator Ryan Blue in a recent meetup. If this is something you are interested in, keep posted on the Slack channel and releases as the Trino Iceberg connector progresses in this area.&#xA;&#xA;As mentioned above, the last set of files that you find in the metadata directory which are suffixed with .metadata.json. These files at baseline are a bit strange as they aren’t stored in the Avro format, but instead the JSON format. This is because they are not part of the persistent tree structure. These files are essentially a copy of the table metadata that is stored in the metastore. You can find the fields for the table metadata listed in the Iceberg specification. These tables are typically stored persistently in a metasture much like the Hive metastore but could easily be replaced by any datastore that can support an atomic swap (check-and-put) operation required for Iceberg to support the optimistic concurrency operation.&#xA;&#xA;The naming of the table metadata includes a table version and UUID: &#xA;table-version-UUID.metadata.json. To commit a new metadata version, which just adds 1 to the current version number, the writer performs these steps:&#xA;&#xA;It creates a new table metadata file using the current metadata.&#xA;It writes the new table metadata to a file following the naming with the next version number.&#xA;It requests the metastore swap the table’s metadata pointer from the old location to the new location.&#xA;&#xA;    If the swap succeeds, the commit succeeded. The new file is now the &#xA;    current metadata.&#xA;    If the swap fails, another writer has already created their own. The&#xA;    current writer goes back to step 1.&#xA;&#xA;If you want to see where this is stored in the Hive metastore, you can reference the TABLEPARAMS table. At the time of writing, this is the only method of using the metastore that is supported by the Trino Iceberg connector.&#xA;&#xA;SELECT PARAMKEY, PARAMVALUE FROM metastore.TABLEPARAMS;&#xA;&#xA;Result:&#xA;&#xA;table&#xA;trthPARAMKEY/ththPARAMVALUE/th/tr&#xA;trtdEXTERNAL/tdtdTRUE/td/tr&#xA;trtdmetadatalocation/tdtds3a://iceberg/logging.db/events/metadata/00002-33d69acc-94cb-44bc-b2a1-71120e749d9a.metadata.json/td/tr&#xA;trtdnumFiles/tdtd2/td/tr&#xA;trtdpreviousmetadatalocation/tdtds3a://iceberg/logging.db/events/metadata/00001-27c8c2d1-fdbb-429d-9263-3654d818250e.metadata.json/td/tr&#xA;trtdtabletype/tdtdiceberg/td/tr&#xA;trtdtotalSize/tdtd5323/td/tr&#xA;trtdtransientlastDdlTime/tdtd1622865672/td/tr&#xA;/table&#xA;&#xA;So as you can see, the metastore is saying the current metadata location is the&#xA;00002-33d69acc-94cb-44bc-b2a1-71120e749d9a.metadata.json file. Now you can dive in to see the table metadata that is being used by the Iceberg connector.&#xA;&#xA;% cat ~/Desktop/avrofiles/00002-33d69acc-94cb-44bc-b2a1-71120e749d9a.metadata.json&#xA;&#xA;Result: &#xA;&#xA;{&#xA;   &#34;format-version&#34;:1,&#xA;   &#34;table-uuid&#34;:&#34;32e3c271-84a9-4be5-9342-2148c878227a&#34;,&#xA;   &#34;location&#34;:&#34;s3a://iceberg/logging.db/events&#34;,&#xA;   &#34;last-updated-ms&#34;:1622865686323,&#xA;   &#34;last-column-id&#34;:5,&#xA;   &#34;schema&#34;:{&#xA;      &#34;type&#34;:&#34;struct&#34;,&#xA;      &#34;fields&#34;:[&#xA;         {&#xA;            &#34;id&#34;:1,&#xA;            &#34;name&#34;:&#34;level&#34;,&#xA;            &#34;required&#34;:false,&#xA;            &#34;type&#34;:&#34;string&#34;&#xA;         },&#xA;         {&#xA;            &#34;id&#34;:2,&#xA;            &#34;name&#34;:&#34;eventtime&#34;,&#xA;            &#34;required&#34;:false,&#xA;            &#34;type&#34;:&#34;timestamp&#34;&#xA;         },&#xA;         {&#xA;            &#34;id&#34;:3,&#xA;            &#34;name&#34;:&#34;message&#34;,&#xA;            &#34;required&#34;:false,&#xA;            &#34;type&#34;:&#34;string&#34;&#xA;         },&#xA;         {&#xA;            &#34;id&#34;:4,&#xA;            &#34;name&#34;:&#34;callstack&#34;,&#xA;            &#34;required&#34;:false,&#xA;            &#34;type&#34;:{&#xA;               &#34;type&#34;:&#34;list&#34;,&#xA;               &#34;element-id&#34;:5,&#xA;               &#34;element&#34;:&#34;string&#34;,&#xA;               &#34;element-required&#34;:false&#xA;            }&#xA;         }&#xA;      ]&#xA;   },&#xA;   &#34;partition-spec&#34;:[&#xA;      {&#xA;         &#34;name&#34;:&#34;eventtimeday&#34;,&#xA;         &#34;transform&#34;:&#34;day&#34;,&#xA;         &#34;source-id&#34;:2,&#xA;         &#34;field-id&#34;:1000&#xA;      }&#xA;   ],&#xA;   &#34;default-spec-id&#34;:0,&#xA;   &#34;partition-specs&#34;:[&#xA;      {&#xA;         &#34;spec-id&#34;:0,&#xA;         &#34;fields&#34;:[&#xA;            {&#xA;               &#34;name&#34;:&#34;eventtime_day&#34;,&#xA;               &#34;transform&#34;:&#34;day&#34;,&#xA;               &#34;source-id&#34;:2,&#xA;               &#34;field-id&#34;:1000&#xA;            }&#xA;         ]&#xA;      }&#xA;   ],&#xA;   &#34;default-sort-order-id&#34;:0,&#xA;   &#34;sort-orders&#34;:[&#xA;      {&#xA;         &#34;order-id&#34;:0,&#xA;         &#34;fields&#34;:[&#xA;            &#xA;         ]&#xA;      }&#xA;   ],&#xA;   &#34;properties&#34;:{&#xA;      &#34;write.format.default&#34;:&#34;ORC&#34;&#xA;   },&#xA;   &#34;current-snapshot-id&#34;:4564366177504223943,&#xA;   &#34;snapshots&#34;:[&#xA;      {&#xA;         &#34;snapshot-id&#34;:6967685587675910019,&#xA;         &#34;timestamp-ms&#34;:1622865672882,&#xA;         &#34;summary&#34;:{&#xA;            &#34;operation&#34;:&#34;append&#34;,&#xA;            &#34;changed-partition-count&#34;:&#34;0&#34;,&#xA;            &#34;total-records&#34;:&#34;0&#34;,&#xA;            &#34;total-data-files&#34;:&#34;0&#34;,&#xA;            &#34;total-delete-files&#34;:&#34;0&#34;,&#xA;            &#34;total-position-deletes&#34;:&#34;0&#34;,&#xA;            &#34;total-equality-deletes&#34;:&#34;0&#34;&#xA;         },&#xA;         &#34;manifest-list&#34;:&#34;s3a://iceberg/logging.db/events/metadata/snap-6967685587675910019-1-bcbe9133-c51c-42a9-9c73-f5b745702cb0.avro&#34;&#xA;      },&#xA;      {&#xA;         &#34;snapshot-id&#34;:2720489016575682283,&#xA;         &#34;parent-snapshot-id&#34;:6967685587675910019,&#xA;         &#34;timestamp-ms&#34;:1622865680419,&#xA;         &#34;summary&#34;:{&#xA;            &#34;operation&#34;:&#34;append&#34;,&#xA;            &#34;added-data-files&#34;:&#34;2&#34;,&#xA;            &#34;added-records&#34;:&#34;3&#34;,&#xA;            &#34;added-files-size&#34;:&#34;1954&#34;,&#xA;            &#34;changed-partition-count&#34;:&#34;2&#34;,&#xA;            &#34;total-records&#34;:&#34;3&#34;,&#xA;            &#34;total-data-files&#34;:&#34;2&#34;,&#xA;            &#34;total-delete-files&#34;:&#34;0&#34;,&#xA;            &#34;total-position-deletes&#34;:&#34;0&#34;,&#xA;            &#34;total-equality-deletes&#34;:&#34;0&#34;&#xA;         },&#xA;         &#34;manifest-list&#34;:&#34;s3a://iceberg/logging.db/events/metadata/snap-2720489016575682283-1-92382234-a4a6-4a1b-bc9b-24839472c2f6.avro&#34;&#xA;      },&#xA;      {&#xA;         &#34;snapshot-id&#34;:4564366177504223943,&#xA;         &#34;parent-snapshot-id&#34;:2720489016575682283,&#xA;         &#34;timestamp-ms&#34;:1622865686278,&#xA;         &#34;summary&#34;:{&#xA;            &#34;operation&#34;:&#34;append&#34;,&#xA;            &#34;added-data-files&#34;:&#34;1&#34;,&#xA;            &#34;added-records&#34;:&#34;1&#34;,&#xA;            &#34;added-files-size&#34;:&#34;746&#34;,&#xA;            &#34;changed-partition-count&#34;:&#34;1&#34;,&#xA;            &#34;total-records&#34;:&#34;4&#34;,&#xA;            &#34;total-data-files&#34;:&#34;3&#34;,&#xA;            &#34;total-delete-files&#34;:&#34;0&#34;,&#xA;            &#34;total-position-deletes&#34;:&#34;0&#34;,&#xA;            &#34;total-equality-deletes&#34;:&#34;0&#34;&#xA;         },&#xA;         &#34;manifest-list&#34;:&#34;s3a://iceberg/logging.db/events/metadata/snap-4564366177504223943-1-23cc980c-9570-42ed-85cf-8658fda2727d.avro&#34;&#xA;      }&#xA;   ],&#xA;   &#34;snapshot-log&#34;:[&#xA;      {&#xA;         &#34;timestamp-ms&#34;:1622865672882,&#xA;         &#34;snapshot-id&#34;:6967685587675910019&#xA;      },&#xA;      {&#xA;         &#34;timestamp-ms&#34;:1622865680419,&#xA;         &#34;snapshot-id&#34;:2720489016575682283&#xA;      },&#xA;      {&#xA;         &#34;timestamp-ms&#34;:1622865686278,&#xA;         &#34;snapshot-id&#34;:4564366177504223943&#xA;      }&#xA;   ],&#xA;   &#34;metadata-log&#34;:[&#xA;      {&#xA;         &#34;timestamp-ms&#34;:1622865672894,&#xA;         &#34;metadata-file&#34;:&#34;s3a://iceberg/logging.db/events/metadata/00000-c5cfaab4-f82f-4351-b2a5-bd0e241f84bc.metadata.json&#34;&#xA;      },&#xA;      {&#xA;         &#34;timestamp-ms&#34;:1622865680524,&#xA;         &#34;metadata-file&#34;:&#34;s3a://iceberg/logging.db/events/metadata/00001-27c8c2d1-fdbb-429d-9263-3654d818250e.metadata.json&#34;&#xA;      }&#xA;   ]&#xA;}&#xA;&#xA;As you can see, these JSON files can quickly grow as you perform different updates on your table. This file contains a pointer to all of the snapshots and manifest list files, much like the output you found from looking at the snapshots in the table. A really important piece to note is the schema is stored here. This is what Trino uses for validation on inserts and reads. As you may expect, there is the root location of the table itself, as well as a unique table identifier. The final part I’d like to note about this file is the partition-spec and partition-specs fields. The partition-spec field holds the current partition spec, while the partition-specs is an array that can hold a list of all partition specs that have existed for this table. As pointed out earlier, you can have many different manifest files that use different partition specs. That wraps up all of the metadata file types you can expect to see in Iceberg!&#xA;&#xA;This post wraps up the Trino on ice series. Hopefully these blog posts serve as a helpful initial dialogue about what is expected to grow as a vital portion of an open data lakehouse stack. What are you waiting for? Come join the fun and help us implement some of the missing features or instead go ahead and try Trino on Ice(berg) yourself!&#xA;&#xA;#trino #iceberg]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://trino.io/assets/blog/trino-on-ice/trino-iceberg.png" alt=""/></p>

<p>So far, this series has covered some very interesting user level concepts of the Iceberg model, and how you can take advantage of them using the Trino query engine. This blog post dives into some implementation details of Iceberg by dissecting some files that result from various operations carried out using Trino. To dissect you must use some surgical instrumentation, namely Trino, Avro tools, the MinIO client tool and Iceberg’s core library. It’s useful to dissect how these files work, not only to help understand how Iceberg works, but also to aid in troubleshooting issues, should you have any issues during ingestion or querying of your Iceberg table. I like to think of this type of debugging much like a fun game of operation, and you’re looking to see what causes the red errors to fly by on your screen.</p>



<hr/>

<p>Trino on ice is a series, covering the details around how the Iceberg table format works with the Trino query engine. It’s recommended to read the posts sequentially as the examples build on previous posts in this series:</p>
<ul><li><a href="https://bitsondata.dev/trino-iceberg-i-gentle-intro">Trino on ice I: A gentle introduction to Iceberg</a></li>
<li><a href="https://bitsondata.dev/trino-iceberg-ii-table-evolution-cloud">Trino on ice II: In-place table evolution and cloud compatibility with Iceberg</a></li>
<li><a href="https://write.as/bitsondatadev/trino-iceberg-iii-concurrency-snapshots-spec">Trino on ice III: Iceberg concurrency model, snapshots, and the Iceberg spec</a></li>
<li><a href="https://bitsondata.dev/trino-iceberg-iv-deep-dive">Trino on ice IV: Deep dive into Iceberg internals</a></li></ul>

<hr/>

<p><img src="https://trino.io/assets/blog/trino-on-ice/operation.gif" alt=""/></p>

<h2 id="understanding-iceberg-metadata" id="understanding-iceberg-metadata">Understanding Iceberg metadata</h2>

<p>Iceberg can use any compatible metastore, but for Trino, it only supports the  Hive metastore and AWS Glue similar to the Hive connector. This is because there is already a vast amount of testing and support for using the Hive metastore in Trino. Likewise, many Trino use cases that currently use data lakes already use the Hive connector and therefore the Hive metastore. This makes it convenient to have as the leading supported use case as existing users can easily migrate between Hive to Iceberg tables. Since there is no indication of which connector is actually executed in the diagram of the Hive connector architecture, it serves as a diagram that can be used for both Hive and Iceberg. The only difference is the connector used, but if you create a table in Hive, you can  view the same table in Iceberg.</p>

<p><img src="https://trino.io/assets/blog/trino-on-ice/iceberg-metadata.png" alt=""/></p>

<p>To recap the steps taken from the first three blogs; the first blog created an events table, while the first two blogs ran two insert statements. The first insert contained three records, while the second insert contained a single record.</p>

<p><img src="https://trino.io/assets/blog/trino-on-ice/iceberg-snapshot-files.png" alt=""/></p>

<p>Up until this point, the state of the files in MinIO haven’t really been shown except some of the manifest list pointers from the snapshot in the third blog post. Using the <a href="https://docs.min.io/minio/baremetal/reference/minio-cli/minio-mc.html">MinIO client tool</a>, you can list files that Iceberg generated through all these operations and then try to understand what purpose they are serving.</p>

<pre><code>% mc tree -f local/
local/
└─ iceberg
   └─ logging.db
      └─ events
         ├─ data
         │  ├─ event_time_day=2021-04-01
         │  │  ├─ 51eb1ea6-266b-490f-8bca-c63391f02d10.orc
         │  │  └─ cbcf052d-240d-4881-8a68-2bbc0f7e5233.orc
         │  └─ event_time_day=2021-04-02
         │     └─ b012ec20-bbdd-47f5-89d3-57b9e32ea9eb.orc
         └─ metadata
            ├─ 00000-c5cfaab4-f82f-4351-b2a5-bd0e241f84bc.metadata.json
            ├─ 00001-27c8c2d1-fdbb-429d-9263-3654d818250e.metadata.json
            ├─ 00002-33d69acc-94cb-44bc-b2a1-71120e749d9a.metadata.json
            ├─ 23cc980c-9570-42ed-85cf-8658fda2727d-m0.avro
            ├─ 92382234-a4a6-4a1b-bc9b-24839472c2f6-m0.avro
            ├─ snap-2720489016575682283-1-92382234-a4a6-4a1b-bc9b-24839472c2f6.avro
            ├─ snap-4564366177504223943-1-23cc980c-9570-42ed-85cf-8658fda2727d.avro
            └─ snap-6967685587675910019-1-bcbe9133-c51c-42a9-9c73-f5b745702cb0.avro
</code></pre>

<p>There are a lot of files here, but here are a couple of patterns that you can observe with these files.</p>

<p>First, the top two directories are named <code>data</code> and <code>metadata</code>.</p>

<pre><code>/&lt;bucket&gt;/&lt;database&gt;/&lt;table&gt;/data//&lt;bucket&gt;/&lt;database&gt;/&lt;table&gt;/metadata/
</code></pre>

<p>As you might expect, <code>data</code> contains the actual ORC files split by partition. This is akin to what you would see in a Hive table <code>data</code> directory. What is really of interest here is the <code>metadata</code> directory. There are specifically three patterns of files you’ll find here.</p>

<pre><code>/&lt;bucket&gt;/&lt;database&gt;/&lt;table&gt;/metadata/&lt;file-id&gt;.avro

/&lt;bucket&gt;/&lt;database&gt;/&lt;table&gt;/metadata/snap-&lt;snapshot-id&gt;-&lt;version&gt;-&lt;file-id&gt;.avro

/&lt;bucket&gt;/&lt;database&gt;/&lt;table&gt;/metadata/&lt;version&gt;-&lt;commit-UUID&gt;.metadata.json
</code></pre>

<p>Iceberg has a persistent tree structure that manages various snapshots of the data that are created for every mutation of the data. This enables not only a concurrency model that supports serializable isolation, but also cool features like time travel across a linear progression of snapshots.</p>

<p><img src="https://trino.io/assets/blog/trino-on-ice/iceberg-metastore-files.png" alt=""/></p>

<p>This tree structure contains two types of Avro files, manifest lists and manifest files. Manifest list files contain pointers to various manifest files and the manifest files themselves point to various data files. This post starts out by covering these manifest files, and later covers the table metadata files that are suffixed by <code>.metadata.json</code>.</p>

<p><a href="https://bitsondata.dev/trino-on-ice-iii-iceberg-concurrency-snapshots-spec">The last blog covered</a> the command in Trino that shows the snapshot information that is stored in the metastore. Here is that command and its output again for your review.</p>

<pre><code>SELECT manifest_list 
FROM iceberg.logging.&#34;events$snapshots&#34;;
</code></pre>

<p>Result:</p>

<table>
<tr><th>snapshots</th></tr>
<tr><td>s3a://iceberg/logging.db/events/metadata/snap-6967685587675910019-1-bcbe9133-c51c-42a9-9c73-f5b745702cb0.avro</td></tr>
<tr><td>s3a://iceberg/logging.db/events/metadata/snap-2720489016575682283-1-92382234-a4a6-4a1b-bc9b-24839472c2f6.avro</td></tr>
<tr><td>s3a://iceberg/logging.db/events/metadata/snap-4564366177504223943-1-23cc980c-9570-42ed-85cf-8658fda2727d.avro</td></tr>
</table>

<p>You’ll notice that the manifest list returns the Avro files prefixed with
<code>snap-</code> are returned. These files are directly correlated with the snapshot record stored in the metastore. According to the diagram above, snapshots are records in the metastore that contain the url of the manifest list in the Avro file. Avro files are binary files and not something you can just open up in a text editor to read. Using the <a href="https://downloads.apache.org/avro/avro-1.10.2/java/avro-tools-1.10.2.jar">avro-tools.jar tool</a> distributed by the <a href="https://avro.apache.org/docs/current/index.html">Apache Avro project</a>, you can actually inspect the contents of this file to get a better understanding of how it is used by Iceberg.</p>

<p>The first snapshot is generated on the creation of the events table. Upon inspecting this file, you notice that the file is empty. The output is an empty line that the <code>jq</code> JSON command line utility removes on pretty printing the JSON that is returned, which is just a newline. This snapshot represents an empty state of the table upon creation. To investigate the snapshots you need to download the files to your local filesystem. Let&#39;s move them to the home  directory:</p>

<pre><code>% java -jar  ~/Desktop/avro_files/avro-tools-1.10.0.jar tojson ~/snap-6967685587675910019-1-bcbe9133-c51c-42a9-9c73-f5b745702cb0.avro | jq .
</code></pre>

<p>Result: (is empty)</p>

<pre><code>
</code></pre>

<p>The second snapshot is a little more interesting and actually shows us the contents of a manifest list.</p>

<pre><code>% java -jar  ~/Desktop/avro_files/avro-tools-1.10.0.jar tojson ~/snap-2720489016575682283-1-92382234-a4a6-4a1b-bc9b-24839472c2f6.avro | jq .
</code></pre>

<p>Result:</p>

<pre><code>{
   &#34;manifest_path&#34;:&#34;s3a://iceberg/logging.db/events/metadata/92382234-a4a6-4a1b-bc9b-24839472c2f6-m0.avro&#34;,
   &#34;manifest_length&#34;:6114,
   &#34;partition_spec_id&#34;:0,
   &#34;added_snapshot_id&#34;:{
      &#34;long&#34;:2720489016575682000
   },
   &#34;added_data_files_count&#34;:{
      &#34;int&#34;:2
   },
   &#34;existing_data_files_count&#34;:{
      &#34;int&#34;:0
   },
   &#34;deleted_data_files_count&#34;:{
      &#34;int&#34;:0
   },
   &#34;partitions&#34;:{
      &#34;array&#34;:[
         {
            &#34;contains_null&#34;:false,
            &#34;lower_bound&#34;:{
               &#34;bytes&#34;:&#34;\u001eI\u0000\u0000&#34;
            },
            &#34;upper_bound&#34;:{
               &#34;bytes&#34;:&#34;\u001fI\u0000\u0000&#34;
            }
         }
      ]
   },
   &#34;added_rows_count&#34;:{
      &#34;long&#34;:3
   },
   &#34;existing_rows_count&#34;:{
      &#34;long&#34;:0
   },
   &#34;deleted_rows_count&#34;:{
      &#34;long&#34;:0
   }
}
</code></pre>

<p>To understand each of the values in each of these rows, you can refer to the  Iceberg
<a href="https://iceberg.apache.org/spec/#manifest-lists">specification in the manifest list file section</a>. Instead of covering these exhaustively, let&#39;s focus on a few key fields. Below are the fields, and their definition according to the specification.</p>
<ul><li><code>manifest_path</code> – Location of the manifest file.</li>
<li><code>partition_spec_id</code> – ID of a partition spec used to write the manifest; must be listed in table metadata partition-specs.</li>
<li><code>added_snapshot_id</code> – ID of the snapshot where the manifest file was added.</li>
<li><code>partitions</code> – A list of field summaries for each partition field in the spec. Each field in the list corresponds to a field in the manifest file’s partition spec.</li>
<li><code>added_rows_count</code> – Number of rows in all files in the manifest that have status ADDED, when null this is assumed to be non-zero.</li></ul>

<p>As mentioned above, manifest lists hold references to various manifest files. These manifest paths are the pointers in the persistent tree that tells any client using Iceberg where to find all of the manifest files associated with a particular snapshot. To traverse this tree, you can look over the different manifest paths to find all the manifest files associated with the particular snapshot you want to traverse. Partition spec ids are helpful to know the current partition specification which are stored in the table metadata in the metastore. This references where to find the spec in the metastore. Added snapshot ids tells you which snapshot is associated with the manifest list. Partitions hold some high level partition bound information to make for faster querying. If a query is looking for a particular value, it only traverses the manifest files where the query values fall within the range of the file values. Finally, you get a few metrics like the number of changed rows and data files, one of which is the count of added rows. The first operation consisted of three rows inserts and the second operation was the insertion of one row. Using the row counts you can easily determine which manifest file belongs to which operation.</p>

<p>The following command shows the final snapshot after both operations executed and filters out only the fields pointed out above.</p>

<pre><code>% java -jar  ~/Desktop/avro_files/avro-tools-1.10.0.jar tojson ~/snap-4564366177504223943-1-23cc980c-9570-42ed-85cf-8658fda2727d.avro | jq &#39;. | {manifest_path: .manifest_path, partition_spec_id: .partition_spec_id, added_snapshot_id: .added_snapshot_id, partitions: .partitions, added_rows_count: .added_rows_count }&#39;
</code></pre>

<p>Result:</p>

<pre><code>{
   &#34;manifest_path&#34;:&#34;s3a://iceberg/logging.db/events/metadata/23cc980c-9570-42ed-85cf-8658fda2727d-m0.avro&#34;,
   &#34;partition_spec_id&#34;:0,
   &#34;added_snapshot_id&#34;:{
      &#34;long&#34;:4564366177504223700
   },
   &#34;partitions&#34;:{
      &#34;array&#34;:[
         {
            &#34;contains_null&#34;:false,
            &#34;lower_bound&#34;:{
               &#34;bytes&#34;:&#34;\u001eI\u0000\u0000&#34;
            },
            &#34;upper_bound&#34;:{
               &#34;bytes&#34;:&#34;\u001eI\u0000\u0000&#34;
            }
         }
      ]
   },
   &#34;added_rows_count&#34;:{
      &#34;long&#34;:1
   }
}
{
   &#34;manifest_path&#34;:&#34;s3a://iceberg/logging.db/events/metadata/92382234-a4a6-4a1b-bc9b-24839472c2f6-m0.avro&#34;,
   &#34;partition_spec_id&#34;:0,
   &#34;added_snapshot_id&#34;:{
      &#34;long&#34;:2720489016575682000
   },
   &#34;partitions&#34;:{
      &#34;array&#34;:[
         {
            &#34;contains_null&#34;:false,
            &#34;lower_bound&#34;:{
               &#34;bytes&#34;:&#34;\u001eI\u0000\u0000&#34;
            },
            &#34;upper_bound&#34;:{
               &#34;bytes&#34;:&#34;\u001fI\u0000\u0000&#34;
            }
         }
      ]
   },
   &#34;added_rows_count&#34;:{
      &#34;long&#34;:3
   }
}
</code></pre>

<p>In the listing of the manifest file related to the last snapshot, you notice the first operation where three rows were inserted is contained in the manifest file in the second JSON object. You can determine this from the snapshot id, as well as, the number of rows that were added in the operation. The first JSON object contains the last operation that inserted a single row. So the most recent operations are listed in reverse commit order.</p>

<p>The next command does the same listing of the file that you ran with the manifest list, except you run this on the manifest files themselves to expose their contents and discuss them. To begin with, you run the command to show the contents of the manifest file associated with the insertion of three rows.</p>

<pre><code>% java -jar  ~/avro-tools-1.10.0.jar tojson ~/Desktop/avro_files/92382234-a4a6-4a1b-bc9b-24839472c2f6-m0.avro | jq .
</code></pre>

<p>Result:</p>

<pre><code>{
   &#34;status&#34;:1,
   &#34;snapshot_id&#34;:{
      &#34;long&#34;:2720489016575682000
   },
   &#34;data_file&#34;:{
      &#34;file_path&#34;:&#34;s3a://iceberg/logging.db/events/data/event_time_day=2021-04-01/51eb1ea6-266b-490f-8bca-c63391f02d10.orc&#34;,
      &#34;file_format&#34;:&#34;ORC&#34;,
      &#34;partition&#34;:{
         &#34;event_time_day&#34;:{
            &#34;int&#34;:18718
         }
      },
      &#34;record_count&#34;:1,
      &#34;file_size_in_bytes&#34;:870,
      &#34;block_size_in_bytes&#34;:67108864,
      &#34;column_sizes&#34;:null,
      &#34;value_counts&#34;:{
         &#34;array&#34;:[
            {
               &#34;key&#34;:1,
               &#34;value&#34;:1
            },
            {
               &#34;key&#34;:2,
               &#34;value&#34;:1
            },
            {
               &#34;key&#34;:3,
               &#34;value&#34;:1
            },
            {
               &#34;key&#34;:4,
               &#34;value&#34;:1
            }
         ]
      },
      &#34;null_value_counts&#34;:{
         &#34;array&#34;:[
            {
               &#34;key&#34;:1,
               &#34;value&#34;:0
            },
            {
               &#34;key&#34;:2,
               &#34;value&#34;:0
            },
            {
               &#34;key&#34;:3,
               &#34;value&#34;:0
            },
            {
               &#34;key&#34;:4,
               &#34;value&#34;:0
            }
         ]
      },
      &#34;nan_value_counts&#34;:null,
      &#34;lower_bounds&#34;:{
         &#34;array&#34;:[
            {
               &#34;key&#34;:1,
               &#34;value&#34;:&#34;ERROR&#34;
            },
            {
               &#34;key&#34;:3,
               &#34;value&#34;:&#34;Oh noes&#34;
            }
         ]
      },
      &#34;upper_bounds&#34;:{
         &#34;array&#34;:[
            {
               &#34;key&#34;:1,
               &#34;value&#34;:&#34;ERROR&#34;
            },
            {
               &#34;key&#34;:3,
               &#34;value&#34;:&#34;Oh noes&#34;
            }
         ]
      },
      &#34;key_metadata&#34;:null,
      &#34;split_offsets&#34;:null
   }
}
{
   &#34;status&#34;:1,
   &#34;snapshot_id&#34;:{
      &#34;long&#34;:2720489016575682000
   },
   &#34;data_file&#34;:{
      &#34;file_path&#34;:&#34;s3a://iceberg/logging.db/events/data/event_time_day=2021-04-02/b012ec20-bbdd-47f5-89d3-57b9e32ea9eb.orc&#34;,
      &#34;file_format&#34;:&#34;ORC&#34;,
      &#34;partition&#34;:{
         &#34;event_time_day&#34;:{
            &#34;int&#34;:18719
         }
      },
      &#34;record_count&#34;:2,
      &#34;file_size_in_bytes&#34;:1084,
      &#34;block_size_in_bytes&#34;:67108864,
      &#34;column_sizes&#34;:null,
      &#34;value_counts&#34;:{
         &#34;array&#34;:[
            {
               &#34;key&#34;:1,
               &#34;value&#34;:2
            },
            {
               &#34;key&#34;:2,
               &#34;value&#34;:2
            },
            {
               &#34;key&#34;:3,
               &#34;value&#34;:2
            },
            {
               &#34;key&#34;:4,
               &#34;value&#34;:2
            }
         ]
      },
      &#34;null_value_counts&#34;:{
         &#34;array&#34;:[
            {
               &#34;key&#34;:1,
               &#34;value&#34;:0
            },
            {
               &#34;key&#34;:2,
               &#34;value&#34;:0
            },
            {
               &#34;key&#34;:3,
               &#34;value&#34;:0
            },
            {
               &#34;key&#34;:4,
               &#34;value&#34;:0
            }
         ]
      },
      &#34;nan_value_counts&#34;:null,
      &#34;lower_bounds&#34;:{
         &#34;array&#34;:[
            {
               &#34;key&#34;:1,
               &#34;value&#34;:&#34;ERROR&#34;
            },
            {
               &#34;key&#34;:3,
               &#34;value&#34;:&#34;Double oh noes&#34;
            }
         ]
      },
      &#34;upper_bounds&#34;:{
         &#34;array&#34;:[
            {
               &#34;key&#34;:1,
               &#34;value&#34;:&#34;WARN&#34;
            },
            {
               &#34;key&#34;:3,
               &#34;value&#34;:&#34;Maybeh oh noes?&#34;
            }
         ]
      },
      &#34;key_metadata&#34;:null,
      &#34;split_offsets&#34;:null
   }
}
</code></pre>

<p>Now this is a very big output, but in summary, there’s really not too much to these files. As before, there is a <a href="https://iceberg.apache.org/spec/#manifests">Manifest section in the Iceberg spec</a> that details what each of these fields means. Here are the important fields:</p>
<ul><li><code>snapshot_id</code> – Snapshot id where the file was added, or deleted if status is two. Inherited when null.</li>
<li><code>data_file</code> – Field containing metadata about the data files pertaining to the manifest file, such as file path, partition tuple, metrics, etc…</li>
<li><code>data_file.file_path</code> – Full URI for the file with FS scheme.</li>
<li><code>data_file.partition</code> – Partition data tuple, schema based on the partition spec.</li>
<li><code>data_file.record_count</code> – Number of records in the data file.</li>
<li><code>data_file.*_count</code> – Multiple fields that contain a map from column id to  number of values, null, nan counts in the file. These can be used to quickly  filter out unnecessary get operations.</li>
<li><code>data_file.*_bounds</code> – Multiple fields that contain a map from column id to lower or upper bound in the column serialized as binary. Each value must be less than or equal to all non-null, non-NaN values in the column for the file.</li></ul>

<p>Each data file struct contains a partition and data file that it maps to. These files only be scanned and returned if the criteria for the query is met when  checking all of the count, bounds, and other statistics that are recorded in the file. Ideally only files that contain data relevant to the query should be scanned at all. Having information like the record count may also help in the query planning process to determine splits and other information. This particular optimization hasn’t been completed yet as planning typically happens before traversal of the files. It is still in ongoing discussion and <a href="https://youtu.be/ifXpOn0NJWk?t=2132">is discussed a bit by Iceberg creator Ryan Blue in a recent meetup</a>. If this is something you are interested in, keep posted on the Slack channel and releases as the Trino Iceberg connector progresses in this area.</p>

<p>As mentioned above, the last set of files that you find in the metadata directory which are suffixed with <code>.metadata.json</code>. These files at baseline are a bit strange as they aren’t stored in the Avro format, but instead the JSON format. This is because they are not part of the persistent tree structure. These files are essentially a copy of the table metadata that is stored in the metastore. You can find the fields for the table metadata listed <a href="https://iceberg.apache.org/spec/#table-metadata-fields">in the Iceberg specification</a>. These tables are typically stored persistently in a metasture much like the Hive metastore but could easily be replaced by any datastore that can support <a href="https://iceberg.apache.org/spec/#metastore-tables">an atomic swap (check-and-put) operation</a> required for Iceberg to support the optimistic concurrency operation.</p>

<p>The naming of the table metadata includes a table version and UUID:
<code>&lt;table-version&gt;-&lt;UUID&gt;.metadata.json</code>. To commit a new metadata version, which just adds 1 to the current version number, the writer performs these steps:</p>
<ol><li>It creates a new table metadata file using the current metadata.</li>
<li>It writes the new table metadata to a file following the naming with the next version number.</li>

<li><p>It requests the metastore swap the table’s metadata pointer from the old location to the new location.</p>
<ol><li>If the swap succeeds, the commit succeeded. The new file is now the
current metadata.</li>
<li>If the swap fails, another writer has already created their own. The
current writer goes back to step 1.</li></ol></li></ol>

<p>If you want to see where this is stored in the Hive metastore, you can reference the <code>TABLE_PARAMS</code> table. At the time of writing, this is the only method of using the metastore that is supported by the Trino Iceberg connector.</p>

<pre><code>SELECT PARAM_KEY, PARAM_VALUE FROM metastore.TABLE_PARAMS;
</code></pre>

<p>Result:</p>

<table>
<tr><th>PARAM_KEY</th><th>PARAM_VALUE</th></tr>
<tr><td>EXTERNAL</td><td>TRUE</td></tr>
<tr><td>metadata_location</td><td>s3a://iceberg/logging.db/events/metadata/00002-33d69acc-94cb-44bc-b2a1-71120e749d9a.metadata.json</td></tr>
<tr><td>numFiles</td><td>2</td></tr>
<tr><td>previous_metadata_location</td><td>s3a://iceberg/logging.db/events/metadata/00001-27c8c2d1-fdbb-429d-9263-3654d818250e.metadata.json</td></tr>
<tr><td>table_type</td><td>iceberg</td></tr>
<tr><td>totalSize</td><td>5323</td></tr>
<tr><td>transient_lastDdlTime</td><td>1622865672</td></tr>
</table>

<p>So as you can see, the metastore is saying the current metadata location is the
<code>00002-33d69acc-94cb-44bc-b2a1-71120e749d9a.metadata.json</code> file. Now you can dive in to see the table metadata that is being used by the Iceberg connector.</p>

<pre><code>% cat ~/Desktop/avro_files/00002-33d69acc-94cb-44bc-b2a1-71120e749d9a.metadata.json
</code></pre>

<p>Result:</p>

<pre><code>{
   &#34;format-version&#34;:1,
   &#34;table-uuid&#34;:&#34;32e3c271-84a9-4be5-9342-2148c878227a&#34;,
   &#34;location&#34;:&#34;s3a://iceberg/logging.db/events&#34;,
   &#34;last-updated-ms&#34;:1622865686323,
   &#34;last-column-id&#34;:5,
   &#34;schema&#34;:{
      &#34;type&#34;:&#34;struct&#34;,
      &#34;fields&#34;:[
         {
            &#34;id&#34;:1,
            &#34;name&#34;:&#34;level&#34;,
            &#34;required&#34;:false,
            &#34;type&#34;:&#34;string&#34;
         },
         {
            &#34;id&#34;:2,
            &#34;name&#34;:&#34;event_time&#34;,
            &#34;required&#34;:false,
            &#34;type&#34;:&#34;timestamp&#34;
         },
         {
            &#34;id&#34;:3,
            &#34;name&#34;:&#34;message&#34;,
            &#34;required&#34;:false,
            &#34;type&#34;:&#34;string&#34;
         },
         {
            &#34;id&#34;:4,
            &#34;name&#34;:&#34;call_stack&#34;,
            &#34;required&#34;:false,
            &#34;type&#34;:{
               &#34;type&#34;:&#34;list&#34;,
               &#34;element-id&#34;:5,
               &#34;element&#34;:&#34;string&#34;,
               &#34;element-required&#34;:false
            }
         }
      ]
   },
   &#34;partition-spec&#34;:[
      {
         &#34;name&#34;:&#34;event_time_day&#34;,
         &#34;transform&#34;:&#34;day&#34;,
         &#34;source-id&#34;:2,
         &#34;field-id&#34;:1000
      }
   ],
   &#34;default-spec-id&#34;:0,
   &#34;partition-specs&#34;:[
      {
         &#34;spec-id&#34;:0,
         &#34;fields&#34;:[
            {
               &#34;name&#34;:&#34;event_time_day&#34;,
               &#34;transform&#34;:&#34;day&#34;,
               &#34;source-id&#34;:2,
               &#34;field-id&#34;:1000
            }
         ]
      }
   ],
   &#34;default-sort-order-id&#34;:0,
   &#34;sort-orders&#34;:[
      {
         &#34;order-id&#34;:0,
         &#34;fields&#34;:[
            
         ]
      }
   ],
   &#34;properties&#34;:{
      &#34;write.format.default&#34;:&#34;ORC&#34;
   },
   &#34;current-snapshot-id&#34;:4564366177504223943,
   &#34;snapshots&#34;:[
      {
         &#34;snapshot-id&#34;:6967685587675910019,
         &#34;timestamp-ms&#34;:1622865672882,
         &#34;summary&#34;:{
            &#34;operation&#34;:&#34;append&#34;,
            &#34;changed-partition-count&#34;:&#34;0&#34;,
            &#34;total-records&#34;:&#34;0&#34;,
            &#34;total-data-files&#34;:&#34;0&#34;,
            &#34;total-delete-files&#34;:&#34;0&#34;,
            &#34;total-position-deletes&#34;:&#34;0&#34;,
            &#34;total-equality-deletes&#34;:&#34;0&#34;
         },
         &#34;manifest-list&#34;:&#34;s3a://iceberg/logging.db/events/metadata/snap-6967685587675910019-1-bcbe9133-c51c-42a9-9c73-f5b745702cb0.avro&#34;
      },
      {
         &#34;snapshot-id&#34;:2720489016575682283,
         &#34;parent-snapshot-id&#34;:6967685587675910019,
         &#34;timestamp-ms&#34;:1622865680419,
         &#34;summary&#34;:{
            &#34;operation&#34;:&#34;append&#34;,
            &#34;added-data-files&#34;:&#34;2&#34;,
            &#34;added-records&#34;:&#34;3&#34;,
            &#34;added-files-size&#34;:&#34;1954&#34;,
            &#34;changed-partition-count&#34;:&#34;2&#34;,
            &#34;total-records&#34;:&#34;3&#34;,
            &#34;total-data-files&#34;:&#34;2&#34;,
            &#34;total-delete-files&#34;:&#34;0&#34;,
            &#34;total-position-deletes&#34;:&#34;0&#34;,
            &#34;total-equality-deletes&#34;:&#34;0&#34;
         },
         &#34;manifest-list&#34;:&#34;s3a://iceberg/logging.db/events/metadata/snap-2720489016575682283-1-92382234-a4a6-4a1b-bc9b-24839472c2f6.avro&#34;
      },
      {
         &#34;snapshot-id&#34;:4564366177504223943,
         &#34;parent-snapshot-id&#34;:2720489016575682283,
         &#34;timestamp-ms&#34;:1622865686278,
         &#34;summary&#34;:{
            &#34;operation&#34;:&#34;append&#34;,
            &#34;added-data-files&#34;:&#34;1&#34;,
            &#34;added-records&#34;:&#34;1&#34;,
            &#34;added-files-size&#34;:&#34;746&#34;,
            &#34;changed-partition-count&#34;:&#34;1&#34;,
            &#34;total-records&#34;:&#34;4&#34;,
            &#34;total-data-files&#34;:&#34;3&#34;,
            &#34;total-delete-files&#34;:&#34;0&#34;,
            &#34;total-position-deletes&#34;:&#34;0&#34;,
            &#34;total-equality-deletes&#34;:&#34;0&#34;
         },
         &#34;manifest-list&#34;:&#34;s3a://iceberg/logging.db/events/metadata/snap-4564366177504223943-1-23cc980c-9570-42ed-85cf-8658fda2727d.avro&#34;
      }
   ],
   &#34;snapshot-log&#34;:[
      {
         &#34;timestamp-ms&#34;:1622865672882,
         &#34;snapshot-id&#34;:6967685587675910019
      },
      {
         &#34;timestamp-ms&#34;:1622865680419,
         &#34;snapshot-id&#34;:2720489016575682283
      },
      {
         &#34;timestamp-ms&#34;:1622865686278,
         &#34;snapshot-id&#34;:4564366177504223943
      }
   ],
   &#34;metadata-log&#34;:[
      {
         &#34;timestamp-ms&#34;:1622865672894,
         &#34;metadata-file&#34;:&#34;s3a://iceberg/logging.db/events/metadata/00000-c5cfaab4-f82f-4351-b2a5-bd0e241f84bc.metadata.json&#34;
      },
      {
         &#34;timestamp-ms&#34;:1622865680524,
         &#34;metadata-file&#34;:&#34;s3a://iceberg/logging.db/events/metadata/00001-27c8c2d1-fdbb-429d-9263-3654d818250e.metadata.json&#34;
      }
   ]
}
</code></pre>

<p>As you can see, these JSON files can quickly grow as you perform different updates on your table. This file contains a pointer to all of the snapshots and manifest list files, much like the output you found from looking at the snapshots in the table. A really important piece to note is the schema is stored here. This is what Trino uses for validation on inserts and reads. As you may expect, there is the root location of the table itself, as well as a unique table identifier. The final part I’d like to note about this file is the partition-spec and partition-specs fields. The partition-spec field holds the current partition spec, while the partition-specs is an array that can hold a list of all partition specs that have existed for this table. As pointed out earlier, you can have many different manifest files that use different partition specs. That wraps up all of the metadata file types you can expect to see in Iceberg!</p>

<p>This post wraps up the Trino on ice series. Hopefully these blog posts serve as a helpful initial dialogue about what is expected to grow as a vital portion of an open data lakehouse stack. What are you waiting for? Come join the fun and help us implement some of the missing features or instead go ahead and try <a href="https://github.com/bitsondatadev/trino-getting-started/tree/main/iceberg/trino-iceberg-minio">Trino on Ice(berg)</a> yourself!</p>

<p><a href="https://bitsondata.dev/tag:trino" class="hashtag"><span>#</span><span class="p-category">trino</span></a> <a href="https://bitsondata.dev/tag:iceberg" class="hashtag"><span>#</span><span class="p-category">iceberg</span></a></p>
]]></content:encoded>
      <guid>https://bitsondata.dev/trino-iceberg-iv-deep-dive</guid>
      <pubDate>Thu, 12 Aug 2021 05:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Trino on ice III: Iceberg concurrency model, snapshots, and the Iceberg spec</title>
      <link>https://bitsondata.dev/trino-iceberg-iii-concurrency-snapshots-spec?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[&#xA;&#xA;In the last two blog posts, we’ve covered a lot of cool feature improvements of Iceberg over the Hive model. I recommend you take a look at those if you haven’t yet. We introduced concepts and issues that table formats address. This blog closes up the overview of Iceberg features by discussing the concurrency model Iceberg uses to ensure data integrity, how to use snapshots via Trino, and the Iceberg Specification.&#xA;&#xA;!--more--&#xA;&#xA;---&#xA;&#xA;Trino on ice is a series, covering the details around how the Iceberg table format works with the Trino query engine. It’s recommended to read the posts sequentially as the examples build on previous posts in this series:&#xA;&#xA;Trino on ice I: A gentle introduction to Iceberg&#xA;Trino on ice II: In-place table evolution and cloud compatibility with Iceberg&#xA;Trino on ice III: Iceberg concurrency model, snapshots, and the Iceberg spec&#xA;Trino on ice IV: Deep dive into Iceberg internals&#xA;&#xA;---&#xA;&#xA;Concurrency Model&#xA; Some issues with the Hive model are the distinct locations where the metadata is stored and where the data files are stored. Having your data and metadata split up like this is a recipe for disaster when trying to apply updates to both services atomically.&#xA;&#xA; Iceberg metadata diagram of runtime, and file storage&#xA; A very common problem with Hive is that if a writing process failed during insertion, many times you would find the data written to file storage, but the metastore writes failed to occur. Or conversely, the metastore writes were successful, but the data failed to finish writing to file storage due to a  network or file IO failure. There’s a good  Trino Community Broadcast episode that talks about a function in Trino that exists to resolve these issues by syncing the metastore and file storage. You can watch  a simulation of this error on that episode.&#xA;&#xA; Aside from having issues due to the split state in the system, there are many  other issues that stem from the file system itself. In the case of HDFS,  depending on the specific filesystem implementation you are using, you may have different atomicity guarantees for various file systems and their operations, such as creating, deleting, and renaming files and directories. HDFS isn’t the only troublemaker here. Other than Amazon S3’s  recent announcement of strong consistency in their S3 service, most object storage systems only offer eventual consistency that may not show the latest files immediately after writes. Despite storage systems showing more progress towards offering better performance and guarantees, these systems still offer no reliable locking mechanism.&#xA;&#xA; Iceberg addresses all of these issues in a multitude of ways. One of the primary ways Iceberg introduces transactional guarantees is by storing the metadata in the same datastore as the data itself. This simplifies handling commit failures down to rolling back on one system rather than trying to coordinate a rollback across two systems like in Hive. Writers independently write their metadata and attempt to perform their operations, needing no coordination with other writers. The only time the writers coordinate is when they attempt to perform a commit of their operations. In order to do a commit, they perform a lock of the current snapshot record in a database. This concurrency model where writers eagerly do the work upfront is called optimistic concurrency control.&#xA; Currently, in Trino, this method still uses the Hive metastore to perform the lock-and-swap operation necessary to coordinate the final commits. Iceberg  creator, Ryan Blue, covers this lock-and-swap mechanism and how the metastore can be replaced with alternate locking methods. In the event that two writers attempt to commit at the same time, the writer that first acquires the lock successfully commits by swapping its snapshot as the current snapshot, while the second writer will retry to apply its changes again. The second writer should have no problem with this, assuming there are no conflicting changes between the two snapshots.&#xA;&#xA; &#xA;&#xA; This works similarly to a git workflow where the main branch is the locked resource, and two developers try to commit their changes at the same time. The first developer’s changes may conflict with the second developer’s changes. The second developer is then forced to rebase or merge the first developer’s code with their changes before commiting to the main branch again. The same logic applies to merging data files. Currently, Iceberg clients use a copy-on-write mechanism that makes a new file out of the merged data in the next snapshot. This enables accurate time traveling and preserves previous split versions of the files. At the time of writing, upserts via MERGE INTO syntax are not supported in Trino, but  this is in active development. UPDATE: Since the original writing of this post, the  MERGE syntax exists as of version 393.&#xA;&#xA; One of the great benefits of tracking each individual change that gets written to Iceberg is that you are given a view of the data at every point in time. This enables a really cool feature that I mentioned earlier called time travel.&#xA;&#xA; ## Snapshots and Time Travel&#xA;&#xA; To showcase snapshots, it’s best to go over a few examples drawing from the event table we  created in the previous blog posts. This time we’ll only be working with the Iceberg table, as this capability is not available in Hive. Snapshots allow you to have an immutable set of your data at a given time. They are automatically created on every append or removal of data. One thing to note is that for now, they do not store the state of your metadata.&#xA; Say that you have c&#xA; reated your events table and inserted the three initial rows as we did previously. Let’s look at the data we get back and see how to check the existing snapshots in Trino:&#xA;&#xA; &#xA;SELECT level, message&#xA;FROM iceberg.logging.events;&#xA;&#xA;Result:&#xA;&#xA;| level | message |&#xA;| --- | --- |&#xA;| ERROR | Double oh noes |&#xA;| WARN | Maybeh oh noes? |&#xA;| ERROR | Oh noes |&#xA;&#xA;To query the snapshots, all you need is to use the $ operator appended to the&#xA;end of the table name, and add the hidden table, snapshots:&#xA;&#xA;SELECT snapshotid, parentid, operation&#xA;FROM iceberg.logging.“events$snapshots”;&#xA;&#xA;Result:&#xA;&#xA;| snapshotid | parentid | operation |&#xA;| --- | --- | --- |&#xA;| 7620328658793169607 | | append |&#xA;| 2115743741823353537 | 7620328658793169607 | append |&#xA;&#xA;Let’s take a look at the manifest list files that are associated with each &#xA;snapshot ID. You can tell which file belongs to which snapshot based on the &#xA;snapshot ID embedded in the filename:&#xA;&#xA;SELECT manifestlist&#xA;FROM iceberg.logging.“events$snapshots”;&#xA;&#xA;Result:&#xA;&#xA;| shapshots |&#xA;| --- |&#xA;| s3a://iceberg/logging.db/events/metadata/snap-7620328658793169607-1-cc857d89-1c07-4087-bdbc-2144a814dae2.avro | &#xA;| s3a://iceberg/logging.db/events/metadata/snap-2115743741823353537-1-4cb458be-7152-4e99-8db7-b2dda52c556c.avro | &#xA;&#xA;Now, let’s insert another row to the table:&#xA;&#xA;INSERT INTO iceberg.logging.events&#xA;VALUES&#xA;(&#xA;‘INFO’,&#xA;timestamp ‘2021-04-02 00:00:11.1122222’,&#xA;‘It is all good’,&#xA;ARRAY [‘Just updating you!’]&#xA;);&#xA;&#xA;Let’s check the snapshot table again:&#xA;&#xA;SELECT snapshotid, parentid, operation&#xA;FROM iceberg.logging.“events$snapshots”;&#xA;&#xA;Result:&#xA;&#xA;| snapshotid | parentid | operation |&#xA;| --- | --- | --- |&#xA;| 7620328658793169607 | | append |&#xA;| 2115743741823353537 | 7620328658793169607 | append |&#xA;| 7030511368881343137 | 2115743741823353537 | append |&#xA;&#xA;Let’s also verify that our row was added:&#xA;&#xA;SELECT level, message&#xA;FROM iceberg.logging.events;&#xA;&#xA;Result:&#xA;&#xA;| level | message |&#xA;| --- | --- |&#xA;|ERROR|Oh noes |&#xA;|INFO |It is all good |&#xA;|ERROR|Double oh noes |&#xA;|WARN |Maybeh oh noes?|&#xA;&#xA; Since Iceberg is already tracking the list of files added and removed at each snapshot, it would make sense that you can travel back and forth between these different views into the system, right? This concept is called time traveling. You need to specify which snapshot you would like to read from and you will see the view of the data at that timestamp. In Trino, you need to use the @ operator, followed by the snapshot you wish to read from:&#xA; &#xA;&#xA;SELECT level, message&#xA;FROM iceberg.logging.“events@2115743741823353537”;&#xA;&#xA;Result:&#xA;&#xA;| level | message |&#xA;| --- | --- |&#xA;|ERROR|Double oh noes |&#xA;|WARN |Maybeh oh noes?|&#xA;|ERROR|Oh noes |&#xA;&#xA; If you determine there is some issue with your data, you can always roll back to the previous state permanently as well. In Trino we have a function called rollbacktosnapshot to move the table state to another snapshot:&#xA; &#xA;CALL system.rollbacktosnapshot(‘logging’, ‘events’, 2115743741823353537);&#xA;&#xA;Now that we have rolled back, observe what happens when we query the events&#xA;table with:&#xA;&#xA;SELECT level, message&#xA;FROM iceberg.logging.events;&#xA;&#xA;Result:&#xA;&#xA;| level | message |&#xA;| --- | --- |&#xA;|ERROR|Double oh noes |&#xA;|WARN |Maybeh oh noes?|&#xA;|ERROR|Oh noes |&#xA; &#xA; Notice the INFO row is still missing even though we query the table without specifying a snapshot id. Now just because we rolled back, doesn’t mean we’ve lost the snapshot we just rolled back from. In fact, we can roll forward, or as I like to call it,  back to the future! In Trino, you use the same function call but with a predecessor of the existing snapshot:&#xA; &#xA;CALL system.rollbacktosnapshot(‘logging’, ‘events’, 7030511368881343137)&#xA;&#xA;And now we should be able to query the table again and see the INFO row &#xA;return:&#xA;&#xA;SELECT level, message&#xA;FROM iceberg.logging.events;&#xA;&#xA;Result:&#xA;&#xA;| level | message |&#xA;| --- | --- |&#xA;|ERROR|Oh noes |&#xA;|INFO |It is all good |&#xA;|ERROR|Double oh noes |&#xA;|WARN |Maybeh oh noes?|&#xA; &#xA; As expected, the INFO row returns when you roll back to the future.&#xA; &#xA; Having snapshots not only provides you with a level of immutability that is key to the eventual consistency model, but gives you a rich set of features to version and move between different versions of your data like a git repository.&#xA; &#xA; ## Iceberg Specification&#xA; &#xA; Perhaps saving the best for last, the benefit of using Iceberg is the community that surrounds it, and the support you receive. It can be daunting to have to choose a project that replaces something so core to your architecture. While Hive has so many drawbacks, one of the things keeping many companies locked in is the fear of the unknown. How do you know which table format to choose? Are there unknown data corruption issues that I’m about to take on? What if this doesn’t scale like it promises on the label? It is worth noting that  alternative table formats are also emerging in this space  and we encourage you to investigate these for your own use cases. When sitting down with Iceberg creator, Ryan Blue,  comparing Iceberg to other table formats,  he claims the community’s greatest strength is their ability to look forward. They intentionally broke compatibility with Hive to enable them to provide a richer level of features. Unlike Hive, the Iceberg project explained their thinking in a spec.&#xA;&#xA; The strongest argument I can see for Iceberg is that it has a specification. This is something that has largely been missing from Hive and shows a real maturity in how the Iceberg community has approached the issue. On the Trino project, we think standards are important. We adhere to many of them ourselves, such as the ANSI SQL syntax, and exposing the client through a JDBC connection. By creating a standard around this, you’re no longer tied to any particular technology, not even Iceberg itself. You are adhering to a standard that will hopefully become the de facto standard over a decade or two, much like Hive did. Having the standard in clear writing invites multiple communities to the table and brings even more use  cases. Doing so improves the standards and therefore the technologies that implement them.&#xA; &#xA; The previous three blog posts of this series covered the features and massive benefits from using this novel table format. The following post will dive deeper and discuss more about how Iceberg achieves some of this functionality, with an overview into some of the internals and metadata layouts. In the meantime, feel free to try  Trino on Ice(berg).&#xA;&#xA;#trino #iceberg&#xA;&#xA;bits_&#xA;&#xA;!--emailsub--]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://trino.io/assets/blog/trino-on-ice/trino-iceberg.png" alt=""/></p>

<p>In the last two blog posts, we’ve covered a lot of cool feature improvements of Iceberg over the Hive model. I recommend you take a look at those if you haven’t yet. We introduced concepts and issues that table formats address. This blog closes up the overview of Iceberg features by discussing the concurrency model Iceberg uses to ensure data integrity, how to use snapshots via Trino, and the <a href="https://iceberg.apache.org/spec/">Iceberg Specification</a>.</p>



<hr/>

<p>Trino on ice is a series, covering the details around how the Iceberg table format works with the Trino query engine. It’s recommended to read the posts sequentially as the examples build on previous posts in this series:</p>
<ul><li><a href="https://bitsondata.dev/trino-iceberg-i-gentle-intro">Trino on ice I: A gentle introduction to Iceberg</a></li>
<li><a href="https://bitsondata.dev/trino-iceberg-ii-table-evolution-cloud">Trino on ice II: In-place table evolution and cloud compatibility with Iceberg</a></li>
<li><a href="https://write.as/bitsondatadev/trino-iceberg-iii-concurrency-snapshots-spec">Trino on ice III: Iceberg concurrency model, snapshots, and the Iceberg spec</a></li>
<li><a href="https://bitsondata.dev/trino-iceberg-iv-deep-dive">Trino on ice IV: Deep dive into Iceberg internals</a></li></ul>

<hr/>

<h2 id="concurrency-model" id="concurrency-model">Concurrency Model</h2>

<p> Some issues with the Hive model are the distinct locations where the metadata is stored and where the data files are stored. Having your data and metadata split up like this is a recipe for disaster when trying to apply updates to both services atomically.</p>

<p> <img src="https://trino.io/assets/blog/trino-on-ice/iceberg-metadata.png" alt="Iceberg metadata diagram of runtime, and file storage"/>
 A very common problem with Hive is that if a writing process failed during insertion, many times you would find the data written to file storage, but the metastore writes failed to occur. Or conversely, the metastore writes were successful, but the data failed to finish writing to file storage due to a  network or file IO failure. There’s a good  <a href="https://trino.io/episodes/5.html">Trino Community Broadcast episode</a> that talks about a function in Trino that exists to resolve these issues by syncing the metastore and file storage. You can watch  <a href="https://www.youtube.com/watch?v=OXyJFZSsX5w&amp;t=2097s">a simulation of this error</a> on that episode.</p>

<p> Aside from having issues due to the split state in the system, there are many  other issues that stem from the file system itself. In the case of HDFS,  depending on the specific filesystem implementation you are using, you may have <a href="https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-common/filesystem/introduction.html#Core_Expectations_of_a_Hadoop_Compatible_FileSystem">different atomicity guarantees for various file systems and their operations</a>, such as creating, deleting, and renaming files and directories. HDFS isn’t the only troublemaker here. Other than Amazon S3’s  <a href="https://aws.amazon.com/about-aws/whats-new/2020/12/amazon-s3-now-delivers-strong-read-after-write-consistency-automatically-for-all-applications/">recent announcement of strong consistency in their S3 service,</a> most object storage systems only offer <em>eventual</em> consistency that may not show the latest files immediately after writes. Despite storage systems showing more progress towards offering better performance and guarantees, these systems still offer no reliable locking mechanism.</p>

<p> Iceberg addresses all of these issues in a multitude of ways. One of the primary ways Iceberg introduces transactional guarantees is by storing the metadata in the same datastore as the data itself. This simplifies handling commit failures down to rolling back on one system rather than trying to coordinate a rollback across two systems like in Hive. Writers independently write their metadata and attempt to perform their operations, needing no coordination with other writers. The only time the writers coordinate is when they attempt to perform a commit of their operations. In order to do a commit, they perform a lock of the current snapshot record in a database. This concurrency model where writers eagerly do the work upfront is called <strong><em>optimistic concurrency control</em></strong>.
 Currently, in Trino, this method still uses the Hive metastore to perform the lock-and-swap operation necessary to coordinate the final commits. Iceberg  creator, <a href="https://www.linkedin.com/in/rdblue/">Ryan Blue</a>, <a href="https://youtu.be/-iIY2sOFBRc?t=1351">covers this lock-and-swap mechanism</a> and how the metastore can be replaced with alternate locking methods. In the event that <a href="https://iceberg.apache.org/reliability/#concurrent-write-operations">two writers attempt to commit at the same time</a>, the writer that first acquires the lock successfully commits by swapping its snapshot as the current snapshot, while the second writer will retry to apply its changes again. The second writer should have no problem with this, assuming there are no conflicting changes between the two snapshots.</p>

<p> <img src="https://trino.io/assets/blog/trino-on-ice/iceberg-files.png" alt=""/></p>

<p> This works similarly to a git workflow where the main branch is the locked resource, and two developers try to commit their changes at the same time. The first developer’s changes may conflict with the second developer’s changes. The second developer is then forced to rebase or merge the first developer’s code with their changes before commiting to the main branch again. The same logic applies to merging data files. Currently, Iceberg clients use a <a href="https://iceberg.apache.org/reliability/#concurrent-write-operations">copy-on-write mechanism</a> that makes a new file out of the merged data in the next snapshot. This enables accurate time traveling and preserves previous split versions of the files. At the time of writing, upserts via <code>MERGE INTO</code> syntax are not supported in Trino, but  <a href="https://github.com/trinodb/trino/issues/7708">this is in active development</a>. <strong><em>UPDATE:</em></strong> Since the original writing of this post, the  <a href="https://github.com/trinodb/trino/pull/7933"><code>MERGE</code> syntax exists as of version 393</a>.</p>

<p> One of the great benefits of tracking each individual change that gets written to Iceberg is that you are given a view of the data at every point in time. This enables a really cool feature that I mentioned earlier called <strong><em>time travel</em></strong>.</p>

<p> ## Snapshots and Time Travel</p>

<p> To showcase snapshots, it’s best to go over a few examples drawing from the event table we  created in the previous blog posts. This time we’ll only be working with the Iceberg table, as this capability is not available in Hive. Snapshots allow you to have an immutable set of your data at a given time. They are automatically created on every append or removal of data. One thing to note is that for now, they do not store the state of your metadata.
 Say that you have c
 reated your events table and inserted the three initial rows as we did previously. Let’s look at the data we get back and see how to check the existing snapshots in Trino:</p>

<pre><code>SELECT level, message
FROM iceberg.logging.events;
</code></pre>

<p>Result:</p>

<table>
<thead>
<tr>
<th>level</th>
<th>message</th>
</tr>
</thead>

<tbody>
<tr>
<td>ERROR</td>
<td>Double oh noes</td>
</tr>

<tr>
<td>WARN</td>
<td>Maybeh oh noes?</td>
</tr>

<tr>
<td>ERROR</td>
<td>Oh noes</td>
</tr>
</tbody>
</table>

<p>To query the snapshots, all you need is to use the $ operator appended to the
end of the table name, and add the hidden table, <code>snapshots</code>:</p>

<pre><code>SELECT snapshot_id, parent_id, operation
FROM iceberg.logging.“events$snapshots”;
</code></pre>

<p>Result:</p>

<table>
<thead>
<tr>
<th>snapshot_id</th>
<th>parent_id</th>
<th>operation</th>
</tr>
</thead>

<tbody>
<tr>
<td>7620328658793169607</td>
<td></td>
<td>append</td>
</tr>

<tr>
<td>2115743741823353537</td>
<td>7620328658793169607</td>
<td>append</td>
</tr>
</tbody>
</table>

<p>Let’s take a look at the manifest list files that are associated with each
snapshot ID. You can tell which file belongs to which snapshot based on the
snapshot ID embedded in the filename:</p>

<pre><code>SELECT manifest_list
FROM iceberg.logging.“events$snapshots”;
</code></pre>

<p>Result:</p>

<table>
<thead>
<tr>
<th>shapshots</th>
</tr>
</thead>

<tbody>
<tr>
<td>s3a://iceberg/logging.db/events/metadata/snap-7620328658793169607-1-cc857d89-1c07-4087-bdbc-2144a814dae2.avro</td>
</tr>

<tr>
<td>s3a://iceberg/logging.db/events/metadata/snap-2115743741823353537-1-4cb458be-7152-4e99-8db7-b2dda52c556c.avro</td>
</tr>
</tbody>
</table>

<p>Now, let’s insert another row to the table:</p>

<pre><code>INSERT INTO iceberg.logging.events
VALUES
(
‘INFO’,
timestamp ‘2021-04-02 00:00:11.1122222’,
‘It is all good’,
ARRAY [‘Just updating you!’]
);
</code></pre>

<p>Let’s check the snapshot table again:</p>

<pre><code>SELECT snapshot_id, parent_id, operation
FROM iceberg.logging.“events$snapshots”;
</code></pre>

<p>Result:</p>

<table>
<thead>
<tr>
<th>snapshot_id</th>
<th>parent_id</th>
<th>operation</th>
</tr>
</thead>

<tbody>
<tr>
<td>7620328658793169607</td>
<td></td>
<td>append</td>
</tr>

<tr>
<td>2115743741823353537</td>
<td>7620328658793169607</td>
<td>append</td>
</tr>

<tr>
<td>7030511368881343137</td>
<td>2115743741823353537</td>
<td>append</td>
</tr>
</tbody>
</table>

<p>Let’s also verify that our row was added:</p>

<pre><code>SELECT level, message
FROM iceberg.logging.events;
</code></pre>

<p>Result:</p>

<table>
<thead>
<tr>
<th>level</th>
<th>message</th>
</tr>
</thead>

<tbody>
<tr>
<td>ERROR</td>
<td>Oh noes</td>
</tr>

<tr>
<td>INFO</td>
<td>It is all good</td>
</tr>

<tr>
<td>ERROR</td>
<td>Double oh noes</td>
</tr>

<tr>
<td>WARN</td>
<td>Maybeh oh noes?</td>
</tr>
</tbody>
</table>

<p> Since Iceberg is already tracking the list of files added and removed at each snapshot, it would make sense that you can travel back and forth between these different views into the system, right? This concept is called time traveling. You need to specify which snapshot you would like to read from and you will see the view of the data at that timestamp. In Trino, you need to use the <code>@</code> operator, followed by the snapshot you wish to read from:</p>

<pre><code>SELECT level, message
FROM iceberg.logging.“events@2115743741823353537”;
</code></pre>

<p>Result:</p>

<table>
<thead>
<tr>
<th>level</th>
<th>message</th>
</tr>
</thead>

<tbody>
<tr>
<td>ERROR</td>
<td>Double oh noes</td>
</tr>

<tr>
<td>WARN</td>
<td>Maybeh oh noes?</td>
</tr>

<tr>
<td>ERROR</td>
<td>Oh noes</td>
</tr>
</tbody>
</table>

<p> If you determine there is some issue with your data, you can always roll back to the previous state permanently as well. In Trino we have a function called <code>rollback_to_snapshot</code> to move the table state to another snapshot:</p>

<pre><code>CALL system.rollback_to_snapshot(‘logging’, ‘events’, 2115743741823353537);
</code></pre>

<p>Now that we have rolled back, observe what happens when we query the events
table with:</p>

<pre><code>SELECT level, message
FROM iceberg.logging.events;
</code></pre>

<p>Result:</p>

<table>
<thead>
<tr>
<th>level</th>
<th>message</th>
</tr>
</thead>

<tbody>
<tr>
<td>ERROR</td>
<td>Double oh noes</td>
</tr>

<tr>
<td>WARN</td>
<td>Maybeh oh noes?</td>
</tr>

<tr>
<td>ERROR</td>
<td>Oh noes</td>
</tr>
</tbody>
</table>

<p> Notice the <code>INFO</code> row is still missing even though we query the table without specifying a snapshot id. Now just because we rolled back, doesn’t mean we’ve lost the snapshot we just rolled back from. In fact, we can roll forward, or as I like to call it,  <a href="https://en.wikipedia.org/wiki/Back_to_the_Future">back to the future</a>! In Trino, you use the same function call but with a predecessor of the existing snapshot:</p>

<pre><code>CALL system.rollback_to_snapshot(‘logging’, ‘events’, 7030511368881343137)
</code></pre>

<p>And now we should be able to query the table again and see the <code>INFO</code> row
return:</p>

<pre><code>SELECT level, message
FROM iceberg.logging.events;
</code></pre>

<p>Result:</p>

<table>
<thead>
<tr>
<th>level</th>
<th>message</th>
</tr>
</thead>

<tbody>
<tr>
<td>ERROR</td>
<td>Oh noes</td>
</tr>

<tr>
<td>INFO</td>
<td>It is all good</td>
</tr>

<tr>
<td>ERROR</td>
<td>Double oh noes</td>
</tr>

<tr>
<td>WARN</td>
<td>Maybeh oh noes?</td>
</tr>
</tbody>
</table>

<p> As expected, the INFO row returns when you roll back to the future.</p>

<p> Having snapshots not only provides you with a level of immutability that is key to the eventual consistency model, but gives you a rich set of features to version and move between different versions of your data like a git repository.</p>

<p> ## Iceberg Specification</p>

<p> Perhaps saving the best for last, the benefit of using Iceberg is the community that surrounds it, and the support you receive. It can be daunting to have to choose a project that replaces something so core to your architecture. While Hive has so many drawbacks, one of the things keeping many companies locked in is the fear of the unknown. How do you know which table format to choose? Are there unknown data corruption issues that I’m about to take on? What if this doesn’t scale like it promises on the label? It is worth noting that  <a href="https://lakefs.io/hudi-iceberg-and-delta-lake-data-lake-table-formats-compared/">alternative table formats are also emerging in this space</a>  and we encourage you to investigate these for your own use cases. When sitting down with Iceberg creator, Ryan Blue,  <a href="https://www.twitch.tv/videos/989098630">comparing Iceberg to other table formats</a>,  he claims the community’s greatest strength is their ability to look forward. They intentionally broke compatibility with Hive to enable them to provide a richer level of features. Unlike Hive, the Iceberg project explained their thinking in a spec.</p>

<p> The strongest argument I can see for Iceberg is that it has a <a href="https://iceberg.apache.org/spec/">specification</a>. This is something that has largely been missing from Hive and shows a real maturity in how the Iceberg community has approached the issue. On the Trino project, we think standards are important. We adhere to many of them ourselves, such as the ANSI SQL syntax, and exposing the client through a JDBC connection. By creating a standard around this, you’re no longer tied to any particular technology, not even Iceberg itself. You are adhering to a standard that will hopefully become the de facto standard over a decade or two, much like Hive did. Having the standard in clear writing invites multiple communities to the table and brings even more use  cases. Doing so improves the standards and therefore the technologies that implement them.</p>

<p> The previous three blog posts of this series covered the features and massive benefits from using this novel table format. The following post will dive deeper and discuss more about how Iceberg achieves some of this functionality, with an overview into some of the internals and metadata layouts. In the meantime, feel free to try  <a href="https://github.com/bitsondatadev/trino-getting-started/tree/main/iceberg/trino-iceberg-minio">Trino on Ice(berg)</a>.</p>

<p><a href="https://bitsondata.dev/tag:trino" class="hashtag"><span>#</span><span class="p-category">trino</span></a> <a href="https://bitsondata.dev/tag:iceberg" class="hashtag"><span>#</span><span class="p-category">iceberg</span></a></p>

<p><em>bits</em></p>


]]></content:encoded>
      <guid>https://bitsondata.dev/trino-iceberg-iii-concurrency-snapshots-spec</guid>
      <pubDate>Fri, 30 Jul 2021 05:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Trino on ice II: In-place table evolution and cloud compatibility with Iceberg</title>
      <link>https://bitsondata.dev/trino-iceberg-ii-table-evolution-cloud?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[&#xA;&#xA;The first post covered how Iceberg is a table format and not a file format It demonstrated the benefits of hidden partitioning in Iceberg in contrast to exposed partitioning in Hive. There really is no such thing as “exposed partitioning.” I just thought that sounded better than not-hidden partitioning. If any of that wasn’t clear, I recommend either that you stop reading now, or go back to the first post before starting this one. This post discusses evolution. No, the post isn’t covering Darwinian nor Pokémon evolution, but in-place table evolution! &#xA;&#xA;!--more--&#xA;&#xA;---&#xA;&#xA;Trino on ice is a series, covering the details around how the Iceberg table format works with the Trino query engine. It’s recommended to read the posts sequentially as the examples build on previous posts in this series:&#xA;&#xA;Trino on ice I: A gentle introduction to Iceberg&#xA;Trino on ice II: In-place table evolution and cloud compatibility with Iceberg&#xA;Trino on ice III: Iceberg concurrency model, snapshots, and the Iceberg spec&#xA;Trino on ice IV: Deep dive into Iceberg internals&#xA;&#xA;---&#xA;&#xA;You may find it a little odd that I am getting excited over tables evolving &#xA;in-place, but as mentioned in the last post, if you have experience performing table evolution in Hive, you’d be as happy as Ash Ketchum when Charmander evolved into Charmeleon discovering that Iceberg supports Partition evolution and schema evolution. That is, until Charmeleon started treating Ash like a jerk after the evolution from Charmander. Hopefully, you won’t face the same issue when your tables evolve. &#xA;&#xA;Another important aspect that is covered, is how Iceberg is developed with cloud storage in mind. Hive and other data lake technologies were developed with file systems as their primary storage layer. This is still a very common layer today, but as more companies move to include object storage, table formats did not adapt to the needs of object stores. Let’s dive in!&#xA;&#xA;Partition Specification evolution&#xA;&#xA;In Iceberg, you are able to update the partition specification, shortened to partition spec in Iceberg, on a live table. You do not need to perform a table migration as you do in Hive. In Hive, partition specs don’t explicitly exist because they are tightly coupled with the creation of the Hive table. Meaning, if you ever need to change the granularity of your data partitions at any point, you need to create an entirely new table, and move all the data to the new partition granularity you desire. No pressure on choosing the right granularity or anything!&#xA;&#xA;In Iceberg, you’re not required to choose the perfect partition specification upfront, and you can have multiple partition specs in the same table, and query across the different sized partition specs. How great is that! This means, if you’re initially partitioning your data by month, and later you decide to move to a daily partitioning spec due to a growing ingest from all your new customers, you can do so with no migration, and query over the table with no issue. &#xA;&#xA;This is conveyed pretty succinctly in this graphic from the Iceberg &#xA;documentation. At the end of the year 2008, partitioning occurs at a monthly granularity and after 2009, it moves to a daily granularity. When the query to pull data from December 14th, 2008 and January 13th, 2009, the entire month of December gets scanned due to the monthly partition, but for the dates in January, only the first 13 days are scanned to answer the query.&#xA;&#xA;At the time of writing, Trino is able to perform reads from tables that have multiple partition spec changes but partition evolution write support does not yet exist. There are efforts to add this support in the near future. Edit: this has since been merged!&#xA;&#xA;Schema evolution&#xA;&#xA;Iceberg also handles schema evolution much more elegantly than Hive. In Hive, adding columns worked well enough, as data inserted before the schema change just reports null for that column. For formats that use column names, like ORC and Parquet, deletes are also straightforward for Hive, as it simply ignores fields that are no longer part of the table. For unstructured files like CSV that use the position of the column, deletes would still cause issues, as deleting one column shifts the rest of the columns. Renames for schemas pose an issue for all formats in Hive as data written prior to the rename is not modified to the new field. This effectively works the same as if you deleted the old field and added a new column with the new name. This lack of support for schema. evolution across various file types in Hive requires a lot of memorizing&#xA;the formats underneath various tables. This is very susceptible to causing user errors if someone executes one of the unsupported operations on the wrong table.&#xA;&#xA;table&#xA;thead&#xA;  tr&#xA;    th colspan=&#34;4&#34;Hive 2.2.0 schema evolution based on file type and operation./th&#xA;  /tr&#xA;/thead&#xA;tbody&#xA;  tr&#xA;    td/td&#xA;    tdAdd/td&#xA;    tdDelete/td&#xA;    tdRename/td&#xA;  /tr&#xA;  tr&#xA;    tdCSV/TSV/td&#xA;    td✅/td&#xA;    td❌/td&#xA;    td❌/td&#xA;  /tr&#xA;  tr&#xA;    tdJSON/td&#xA;    td✅/td&#xA;    td✅/td&#xA;    td❌/td&#xA;  /tr&#xA;  tr&#xA;    tdORC/Parquet/Avro/td&#xA;    td✅/td&#xA;    td✅/td&#xA;    td❌/td&#xA;  /tr&#xA;/tbody&#xA;/table&#xA;&#xA;Currently in Iceberg, schemaless position-based data formats such as CSV and TSVare not supported, though there are some discussions on adding limited support for them. This would be good from a reading standpoint, to load data from the CSV, into an Iceberg format with all the guarantees that Iceberg offers. &#xA;&#xA;While JSON doesn’t rely on positional data, it does have an explicit dependency on names. This means, that if I remove a text column from a JSON table named severity, then later I want to add a new int column called severity, I encounter an error when I try to read in the data with the string type from before when I try to deserialize the JSON files. Even worse would be if the new severity column you add has the same type as the original but a semantically different meaning. This results in old rows containing values that are unknowingly from a different domain, which can lead to wrong analytics. After all, someone who adds the new severity column might not even be aware of the old severity column, if it was quite some time ago when it was dropped.&#xA;&#xA;ORC, Parquet, and Avro do not suffer from these issues as they are columnar formats that keep a schema internal to the file itself, and each format tracks changes to the columns through IDs rather than name values or position. Iceberg uses these unique column IDs to also keep track of the columns as changes are applied.&#xA;&#xA;In general, Iceberg can only allow this small set of file formats due to the correctness guarantees it provides. In Trino, you can add, delete, or rename columns using the ALTER TABLE command. Here’s an example that continues from the table created  in the last post  that inserted three rows. The DDL statement looked like this.&#xA;&#xA;CREATE TABLE iceberg.logging.events (&#xA;  level VARCHAR,&#xA;  eventtime TIMESTAMP(6), &#xA;  message VARCHAR,&#xA;  callstack ARRAY(VARCHAR)&#xA;) WITH (&#xA;  format = &#39;ORC&#39;,&#xA;  partitioning = ARRAY[&#39;day(eventtime)&#39;]&#xA;);&#xA;&#xA;Here is an ALTER TABLE sequence that adds a new column named severity, inserts data including into the new column, renames the column, and prints the data.&#xA;&#xA;ALTER TABLE iceberg.logging.events ADD COLUMN severity INTEGER; &#xA;&#xA;INSERT INTO iceberg.logging.events VALUES &#xA;(&#xA;  &#39;INFO&#39;, &#xA;  timestamp &#xA;  &#39;2021-04-01 19:59:59.999999&#39; AT TIME ZONE &#39;America/LosAngeles&#39;, &#xA;  &#39;es muy bueno&#39;, &#xA;  ARRAY [&#39;It is all normal&#39;], &#xA;  1&#xA;);&#xA;&#xA;ALTER TABLE iceberg.logging.events RENAME COLUMN severity TO priority;&#xA;&#xA;SELECT level, message, priority&#xA;FROM iceberg.logging.events;&#xA;&#xA;Result:&#xA;&#xA;| level |  message | priority |&#xA;| --- | --- | --- |&#xA;| ERROR | Double oh noes | NULL |&#xA;| WARN | Maybeh oh noes? | NULL |&#xA;| ERROR | Oh noes | NULL |&#xA;| INFO | es muy bueno | 1 |&#xA;&#xA;ALTER TABLE iceberg.logging.events &#xA;DROP COLUMN priority;&#xA;&#xA;SHOW CREATE TABLE iceberg.logging.events;&#xA;&#xA;Result&#xA;&#xA;CREATE TABLE iceberg.logging.events (&#xA;   level varchar,&#xA;   eventtime timestamp(6),&#xA;   message varchar,&#xA;   callstack array(varchar)&#xA;)&#xA;WITH (&#xA;   format = &#39;ORC&#39;,&#xA;   partitioning = ARRAY[&#39;day(eventtime)&#39;]&#xA;)&#xA;&#xA;Notice how the priority and severity columns are both not present in the schema. As noted in the table above, Hive renames cause issues for all file formats. Yet in Iceberg, performing all these operations causes no issues with the table and underlying data.&#xA;&#xA;Cloud storage compatibility&#xA;&#xA;Not all developers consider or are aware of the performance implications of using Hive over a cloud object storage solution like S3 or Azure Blob storage. One thing to remember is that Hive was developed with the Hadoop Distributed File System (HDFS) in mind. HDFS is a filesystem and is particularly well suited to handle listing files on the filesystem, because they were stored in a contiguous manner. When Hive stores data associated with a table, it assumes there is a contiguous layout underneath it and performs list operations that are expensive on cloud storage systems.&#xA;&#xA;The common cloud storage systems are typically object stores that do not lay out the files in a contiguous manner based on paths. Therefore, it becomes very expensive to list out all the files in a particular path. Yet, these list operations are executed for every partition that could be included in a query, regardless of only a single row, in a single file out of thousands of files needing to be retrieved to answer the query. Even ignoring the performance costs for a minute, object stores may also pose issues for Hive due to eventual  consistency. Inserting and deleting can cause inconsistent results for readers, if the files you end up reading are out of date. &#xA;&#xA;Iceberg avoids all of these issues by tracking the data at the file level, &#xA;rather than the partition level. By tracking the files, Iceberg only accesses the files containing data relevant to the query, as opposed to accessing files in the same partition looking for the few files that are relevant to the query. Further, this allows Iceberg to control for the inconsistency issue in cloud-based file systems by using a locking mechanism at the file level. See the file layout below that Hive layout versus the Iceberg layout. As you can see in the next image, Iceberg makes no assumptions about the data being contiguous or not. It simply builds a persistent tree using the snapshot (S) location stored in the metadata, that points to the manifest list (ML), which points to &#xA;manifests containing partitions (P). Finally, these manifest files contain the file (F) locations and stats that can quickly be used to prune data versus &#xA;needing to do a list operation and scanning all the files.&#xA;&#xA;Referencing the picture above, if you were to run a query where the result set only contains rows from file F1, Hive would require a list operation and scanning the files, F2 and F3. In Iceberg, file metadata exists in the manifest file, P1, that would have a range on the predicate field that prunes out files F2 and F3, and only scans file F1. This example only shows a couple of files, but imagine storage that scales up to thousands of files! Listing becomes expensive on files that are not contiguously stored in memory. Having this flexibility in the logical layout is essential to increase query performance. This is especially true on cloud object stores.&#xA;&#xA;If you want to play around with Iceberg using Trino, check out the &#xA;Trino Iceberg docs. To avoid issues like the eventual consistency issue, as well as other problems of trying to sync operations across systems, Iceberg provides optimistic concurrency support, which is covered in more detail in&#xA;the next post. &#xA;&#xA;#trino #iceberg&#xA;&#xA;bits_&#xA;&#xA;!--emailsub--]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://trino.io/assets/blog/trino-on-ice/trino-iceberg.png" alt=""/></p>

<p><a href="https://bitsondata.dev/trino-on-ice-i-a-gentle-introduction-to-iceberg">The first post</a> covered how Iceberg is a table format and not a file format It demonstrated the benefits of hidden partitioning in Iceberg in contrast to exposed partitioning in Hive. There really is no such thing as “exposed partitioning.” I just thought that sounded better than not-hidden partitioning. If any of that wasn’t clear, I recommend either that you stop reading now, or go back to the first post before starting this one. This post discusses evolution. No, the post isn’t covering Darwinian nor Pokémon evolution, but in-place table evolution!</p>



<hr/>

<p>Trino on ice is a series, covering the details around how the Iceberg table format works with the Trino query engine. It’s recommended to read the posts sequentially as the examples build on previous posts in this series:</p>
<ul><li><a href="https://bitsondata.dev/trino-iceberg-i-gentle-intro">Trino on ice I: A gentle introduction to Iceberg</a></li>
<li><a href="https://bitsondata.dev/trino-iceberg-ii-table-evolution-cloud">Trino on ice II: In-place table evolution and cloud compatibility with Iceberg</a></li>
<li><a href="https://write.as/bitsondatadev/trino-iceberg-iii-concurrency-snapshots-spec">Trino on ice III: Iceberg concurrency model, snapshots, and the Iceberg spec</a></li>
<li><a href="https://bitsondata.dev/trino-iceberg-iv-deep-dive">Trino on ice IV: Deep dive into Iceberg internals</a></li></ul>

<hr/>

<p><img src="https://trino.io/assets/blog/trino-on-ice/evolution.gif" alt=""/></p>

<p>You may find it a little odd that I am getting excited over tables evolving
in-place, but as mentioned in the last post, if you have experience performing table evolution in Hive, you’d be as happy as Ash Ketchum when Charmander evolved into Charmeleon discovering that Iceberg supports Partition evolution and schema evolution. That is, until Charmeleon started treating Ash like a jerk after the evolution from Charmander. Hopefully, you won’t face the same issue when your tables evolve.</p>

<p>Another important aspect that is covered, is how Iceberg is developed with cloud storage in mind. Hive and other data lake technologies were developed with file systems as their primary storage layer. This is still a very common layer today, but as more companies move to include object storage, table formats did not adapt to the needs of object stores. Let’s dive in!</p>

<h2 id="partition-specification-evolution" id="partition-specification-evolution">Partition Specification evolution</h2>

<p>In Iceberg, you are able to update the partition specification, shortened to partition spec in Iceberg, on a live table. You do not need to perform a table migration as you do in Hive. In Hive, partition specs don’t explicitly exist because they are tightly coupled with the creation of the Hive table. Meaning, if you ever need to change the granularity of your data partitions at any point, you need to create an entirely new table, and move all the data to the new partition granularity you desire. No pressure on choosing the right granularity or anything!</p>

<p>In Iceberg, you’re not required to choose the perfect partition specification upfront, and you can have multiple partition specs in the same table, and query across the different sized partition specs. How great is that! This means, if you’re initially partitioning your data by month, and later you decide to move to a daily partitioning spec due to a growing ingest from all your new customers, you can do so with no migration, and query over the table with no issue.</p>

<p>This is conveyed pretty succinctly in this graphic from the Iceberg
documentation. At the end of the year 2008, partitioning occurs at a monthly granularity and after 2009, it moves to a daily granularity. When the query to pull data from December 14th, 2008 and January 13th, 2009, the entire month of December gets scanned due to the monthly partition, but for the dates in January, only the first 13 days are scanned to answer the query.</p>

<p><img src="https://trino.io/assets/blog/trino-on-ice/partition-spec-evolution.png" alt=""/></p>

<p>At the time of writing, Trino is able to perform reads from tables that have multiple partition spec changes but partition evolution write support does not yet exist. <a href="https://github.com/trinodb/trino/issues/7580">There are efforts to add this support in the near future</a>. Edit: this has since been merged!</p>

<h2 id="schema-evolution" id="schema-evolution">Schema evolution</h2>

<p>Iceberg also handles schema evolution much more elegantly than Hive. In Hive, adding columns worked well enough, as data inserted before the schema change just reports null for that column. For formats that use column names, like ORC and Parquet, deletes are also straightforward for Hive, as it simply ignores fields that are no longer part of the table. For unstructured files like CSV that use the position of the column, deletes would still cause issues, as deleting one column shifts the rest of the columns. Renames for schemas pose an issue for all formats in Hive as data written prior to the rename is not modified to the new field. This effectively works the same as if you deleted the old field and added a new column with the new name. This lack of support for schema. evolution across various file types in Hive requires a lot of memorizing
the formats underneath various tables. This is very susceptible to causing user errors if someone executes one of the unsupported operations on the wrong table.</p>

<table>
<thead>
  <tr>
    <th colspan="4">Hive 2.2.0 schema evolution based on file type and operation.</th>
  </tr>
</thead>
<tbody>
  <tr>
    <td></td>
    <td>Add</td>
    <td>Delete</td>
    <td>Rename</td>
  </tr>
  <tr>
    <td>CSV/TSV</td>
    <td>✅</td>
    <td>❌</td>
    <td>❌</td>
  </tr>
  <tr>
    <td>JSON</td>
    <td>✅</td>
    <td>✅</td>
    <td>❌</td>
  </tr>
  <tr>
    <td>ORC/Parquet/Avro</td>
    <td>✅</td>
    <td>✅</td>
    <td>❌</td>
  </tr>
</tbody>
</table>

<p>Currently in Iceberg, schemaless position-based data formats such as CSV and TSVare not supported, though there are <a href="https://github.com/apache/iceberg/issues/118">some discussions on adding limited support for them</a>. This would be good from a reading standpoint, to load data from the CSV, into an Iceberg format with all the guarantees that Iceberg offers.</p>

<p>While JSON doesn’t rely on positional data, it does have an explicit dependency on names. This means, that if I remove a text column from a JSON table named <code>severity</code>, then later I want to add a new int column called <code>severity</code>, I encounter an error when I try to read in the data with the string type from before when I try to deserialize the JSON files. Even worse would be if the new <code>severity</code> column you add has the same type as the original but a semantically different meaning. This results in old rows containing values that are unknowingly from a different domain, which can lead to wrong analytics. After all, someone who adds the new <code>severity</code> column might not even be aware of the old <code>severity</code> column, if it was quite some time ago when it was dropped.</p>

<p>ORC, Parquet, and Avro do not suffer from these issues as they are columnar formats that keep a schema internal to the file itself, and each format tracks changes to the columns through IDs rather than name values or position. Iceberg uses these unique column IDs to also keep track of the columns as changes are applied.</p>

<p>In general, Iceberg can only allow this small set of file formats due to the <a href="https://iceberg.apache.org/evolution/#correctness">correctness guarantees</a> it provides. In Trino, you can add, delete, or rename columns using the <code>ALTER TABLE</code> command. Here’s an example that continues from the table created  in the last post  that inserted three rows. The DDL statement looked like this.</p>

<pre><code>CREATE TABLE iceberg.logging.events (
  level VARCHAR,
  event_time TIMESTAMP(6), 
  message VARCHAR,
  call_stack ARRAY(VARCHAR)
) WITH (
  format = &#39;ORC&#39;,
  partitioning = ARRAY[&#39;day(event_time)&#39;]
);
</code></pre>

<p>Here is an <code>ALTER TABLE</code> sequence that adds a new column named <code>severity</code>, inserts data including into the new column, renames the column, and prints the data.</p>

<pre><code>ALTER TABLE iceberg.logging.events ADD COLUMN severity INTEGER; 

INSERT INTO iceberg.logging.events VALUES 
(
  &#39;INFO&#39;, 
  timestamp 
  &#39;2021-04-01 19:59:59.999999&#39; AT TIME ZONE &#39;America/Los_Angeles&#39;, 
  &#39;es muy bueno&#39;, 
  ARRAY [&#39;It is all normal&#39;], 
  1
);

ALTER TABLE iceberg.logging.events RENAME COLUMN severity TO priority;

SELECT level, message, priority
FROM iceberg.logging.events;
</code></pre>

<p>Result:</p>

<table>
<thead>
<tr>
<th>level</th>
<th>message</th>
<th>priority</th>
</tr>
</thead>

<tbody>
<tr>
<td>ERROR</td>
<td>Double oh noes</td>
<td>NULL</td>
</tr>

<tr>
<td>WARN</td>
<td>Maybeh oh noes?</td>
<td>NULL</td>
</tr>

<tr>
<td>ERROR</td>
<td>Oh noes</td>
<td>NULL</td>
</tr>

<tr>
<td>INFO</td>
<td>es muy bueno</td>
<td>1</td>
</tr>
</tbody>
</table>

<pre><code>ALTER TABLE iceberg.logging.events 
DROP COLUMN priority;

SHOW CREATE TABLE iceberg.logging.events;
</code></pre>

<p>Result</p>

<pre><code>CREATE TABLE iceberg.logging.events (
   level varchar,
   event_time timestamp(6),
   message varchar,
   call_stack array(varchar)
)
WITH (
   format = &#39;ORC&#39;,
   partitioning = ARRAY[&#39;day(event_time)&#39;]
)
</code></pre>

<p>Notice how the priority and severity columns are both not present in the schema. As noted in the table above, Hive renames cause issues for all file formats. Yet in Iceberg, performing all these operations causes no issues with the table and underlying data.</p>

<h2 id="cloud-storage-compatibility" id="cloud-storage-compatibility">Cloud storage compatibility</h2>

<p>Not all developers consider or are aware of the performance implications of using Hive over a cloud object storage solution like S3 or Azure Blob storage. One thing to remember is that Hive was developed with the Hadoop Distributed File System (HDFS) in mind. HDFS is a filesystem and is particularly well suited to handle listing files on the filesystem, because they were stored in a contiguous manner. When Hive stores data associated with a table, it assumes there is a contiguous layout underneath it and performs list operations that are expensive on cloud storage systems.</p>

<p>The common cloud storage systems are typically object stores that do not lay out the files in a contiguous manner based on paths. Therefore, it becomes very expensive to list out all the files in a particular path. Yet, these list operations are executed for every partition that could be included in a query, regardless of only a single row, in a single file out of thousands of files needing to be retrieved to answer the query. Even ignoring the performance costs for a minute, object stores may also pose issues for Hive due to eventual  consistency. Inserting and deleting can cause inconsistent results for readers, if the files you end up reading are out of date.</p>

<p>Iceberg avoids all of these issues by tracking the data at the file level,
rather than the partition level. By tracking the files, Iceberg only accesses the files containing data relevant to the query, as opposed to accessing files in the same partition looking for the few files that are relevant to the query. Further, this allows Iceberg to control for the inconsistency issue in cloud-based file systems by using a locking mechanism at the file level. See the file layout below that Hive layout versus the Iceberg layout. As you can see in the next image, Iceberg makes no assumptions about the data being contiguous or not. It simply builds a persistent tree using the snapshot (S) location stored in the metadata, that points to the manifest list (ML), which points to
manifests containing partitions (P). Finally, these manifest files contain the file (F) locations and stats that can quickly be used to prune data versus
needing to do a list operation and scanning all the files.</p>

<p><img src="https://trino.io/assets/blog/trino-on-ice/cloud-file-layout.png" alt=""/></p>

<p>Referencing the picture above, if you were to run a query where the result set only contains rows from file F1, Hive would require a list operation and scanning the files, F2 and F3. In Iceberg, file metadata exists in the manifest file, P1, that would have a range on the predicate field that prunes out files F2 and F3, and only scans file F1. This example only shows a couple of files, but imagine storage that scales up to thousands of files! Listing becomes expensive on files that are not contiguously stored in memory. Having this flexibility in the logical layout is essential to increase query performance. This is especially true on cloud object stores.</p>

<p>If you want to play around with Iceberg using Trino, check out the
<a href="https://trino.io/docs/current/connector/iceberg.html">Trino Iceberg docs</a>. To avoid issues like the eventual consistency issue, as well as other problems of trying to sync operations across systems, Iceberg provides optimistic concurrency support, which is covered in more detail in
<a href="https://bitsondata.dev/iceberg-concurrency-snapshots-spec">the next post</a>.</p>

<p><a href="https://bitsondata.dev/tag:trino" class="hashtag"><span>#</span><span class="p-category">trino</span></a> <a href="https://bitsondata.dev/tag:iceberg" class="hashtag"><span>#</span><span class="p-category">iceberg</span></a></p>

<p><em>bits</em></p>


]]></content:encoded>
      <guid>https://bitsondata.dev/trino-iceberg-ii-table-evolution-cloud</guid>
      <pubDate>Mon, 12 Jul 2021 05:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Trino on ice I: A gentle introduction To Iceberg</title>
      <link>https://bitsondata.dev/trino-iceberg-i-gentle-intro?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[&#xA;&#xA;Back in the Gentle introduction to the Hive connector blog post, I discussed a commonly misunderstood architecture and uses of the Trino Hive connector. In short, while some may think the name indicates Trino makes a call to a running Hive instance, the Hive connector does not use the Hive runtime to answer queries. Instead, the connector is named Hive connector because it relies on Hive conventions and implementation details from the Hadoop ecosystem - the invisible Hive specification.&#xA;&#xA;!--more--&#xA;&#xA;---&#xA;&#xA;Trino on ice is a series, covering the details around how the Iceberg table format works with the Trino query engine. It’s recommended to read the posts sequentially as the examples build on previous posts in this series:&#xA;&#xA;Trino on ice I: A gentle introduction to Iceberg&#xA;Trino on ice II: In-place table evolution and cloud compatibility with Iceberg&#xA;Trino on ice III: Iceberg concurrency model, snapshots, and the Iceberg spec&#xA;Trino on ice IV: Deep dive into Iceberg internals&#xA;&#xA;---&#xA;&#xA;I call this specification invisible because it doesn’t exist. It lives in the Hive code and the minds of those who developed it. This is makes it very difficult for anybody else who has to integrate with any distributed object storage that uses Hive, since they had to rely on reverse engineering and keeping up with the changes. The way you interact with Hive changes based on which version of Hive or Hadoop you are running. It also varies if you are in the cloud or over an object store. Spark has even modified the Hive spec in some ways to fit the Hive model to their use cases. It’s a big mess that data engineers have put up with for years. Yet despite the confusion and lack of organization due to Hive’s number of unwritten assumptions, the Hive connector is the most popular connector in use for Trino. Virtually every big data query engine uses the Hive model today in some form. As a result it is used by numerous companies to store and access data in their data lakes.&#xA;&#xA;So how did something with no specification become so ubiquitous in data lakes? Hive was first in the large object storage and big data world as part of Hadoop. Hadoop became popular from good marketing for Hadoop to solve the problems of dealing with the increase in data with the Web 2.0 boom . Of course, Hive didn’t get everything wrong. In fact, without Hive, and the fact that it is open source, there may not have been a unified specification at all. Despite the many hours data engineers have spent bashing their heads against the wall with all the unintended consequences of Hive, it still served a very useful purpose.&#xA;&#xA;So why did I just rant about Hive for so long if I’m here to tell you about Apache Iceberg? It’s impossible for a teenager growing up today to truly appreciate music streaming services without knowing what it was like to have an iPod with limited storage, or listening to a scratched burnt CD that skips, or flipping your tape or record to side-B. The same way anyone born before the turn of the millennium really appreciates streaming services, so you too will appreciate Iceberg once you’ve learned the intricacies of managing a data lake built on Hive and Hadoop.&#xA;&#xA;If you haven’t used Hive before, this blog post outlines just a few pain points that come from this data warehousing software to give you proper context. If you have already lived through these headaches, this post acts as a guide to Iceberg from Hive. This post is the first in a series of blog posts discussing Apache Iceberg in great detail, through the lens of the Trino query engine user. If you’re not aware of Trino (formerly PrestoSQL) yet, it is the project that houses the founding Presto community after the founders of Presto left Facebook. This and the next couple of posts discuss the Iceberg specification and all the features Iceberg has to offer, many times in comparison with Hive.&#xA;&#xA;Before jumping into the comparisons, what is Iceberg exactly? The first thing to understand is that Iceberg is not a file format, but a table format. It may not be clear what this means by just stating that, but the function of a table format becomes clearer as the improvements Iceberg brings from the Hive table standard materialize. Iceberg doesn’t replace file formats like ORC and Parquet, but is the layer between the query engine and the data. Iceberg maps and indexes the files in order to provide a higher level abstraction that handles the relational table format for data lakes. You will understand more about table formats through examples in this series.&#xA;&#xA;Hidden Partitions&#xA;&#xA;Hive Partitions&#xA;&#xA;Since most developers and users interact with the table format via the query language, a noticeable difference is the flexibility you have while creating a partitioned table. Assume you are trying to create a table for tracking events occurring in our system. You run both sets of SQL commands from Trino, just using the Hive and Iceberg connectors which are designated by the catalog name (i.e. the catalog name starting with hive. uses the Hive connector, while the iceberg. table uses the Iceberg connector). To begin with, the first DDL statement attempts to create an events table in the logging schema in the hive catalog, which is configured to use the Hive connector. Trino also creates a partition on the events table using the eventtime field which is a TIMESTAMP field.&#xA;&#xA;CREATE TABLE hive.logging.events (&#xA;  level VARCHAR,&#xA;  eventtime TIMESTAMP,&#xA;  message VARCHAR,&#xA;  callstack ARRAY(VARCHAR)&#xA;) WITH (&#xA;  format = &#39;ORC&#39;,&#xA;  partitionedby = ARRAY[&#39;eventtime&#39;]&#xA;);&#xA;&#xA;Running this in Trino using the Hive connector produces the following error message.&#xA;&#xA;Partition keys must be the last columns in the table and in the same order as the table properties: [eventtime]&#xA;&#xA;The Hive DDL is very dependent on ordering for columns and specifically partition columns. Partition fields must be located in the final column positions and in the order of partitioning in the DDL statement. The next statement attempts to create the same table, but now with the eventtime field moved to the last column position.&#xA;&#xA;CREATE TABLE hive.logging.events (&#xA;  level VARCHAR,&#xA;  message VARCHAR,&#xA;  callstack ARRAY(VARCHAR),&#xA;  eventtime TIMESTAMP&#xA;) WITH (&#xA;  format = &#39;ORC&#39;,&#xA;  partitionedby = ARRAY[&#39;eventtime&#39;]&#xA;);&#xA;&#xA;This time, the DDL command works successfully, but you likely don’t want to partition your data on the plain timestamp. This results in a separate file for each distinct timestamp value in your table (likely almost a file for each event). In Hive, there’s no way to indicate the time granularity at which you want to partition natively. The method to support this scenario with Hive is to create a new VARCHAR column, eventtimeday that is dependent on the eventtime column to create the date partition value.&#xA;&#xA;CREATE TABLE hive.logging.events (&#xA;  level VARCHAR,&#xA;  eventtime TIMESTAMP,&#xA;  message VARCHAR,&#xA;  callstack ARRAY(VARCHAR),&#xA;  eventtimeday VARCHAR&#xA;) WITH (&#xA;  format = &#39;ORC&#39;,&#xA;  partitionedby = ARRAY[&#39;eventtimeday&#39;]&#xA;);&#xA;&#xA;This method wastes space by adding a new column to your table. Even worse, it puts the burden of knowledge on the user to include this new column for writing data. It is then necessary to use that separate column for any read access to take advantage of the performance gains from the partitioning.&#xA;&#xA;INSERT INTO hive.logging.events&#xA;VALUES&#xA;(&#xA;  &#39;ERROR&#39;,&#xA;  timestamp &#39;2021-04-01 12:00:00.000001&#39;,&#xA;  &#39;Oh noes&#39;, &#xA;  ARRAY [&#39;Exception in thread &#34;main&#34; java.lang.NullPointerException&#39;], &#xA;  &#39;2021-04-01&#39;&#xA;),&#xA;(&#xA;  &#39;ERROR&#39;,&#xA;  timestamp &#39;2021-04-02 15:55:55.555555&#39;,&#xA;  &#39;Double oh noes&#39;,&#xA;  ARRAY [&#39;Exception in thread &#34;main&#34; java.lang.NullPointerException&#39;],&#xA;  &#39;2021-04-02&#39;&#xA;),&#xA;(&#xA;  &#39;WARN&#39;, &#xA;  timestamp &#39;2021-04-02 00:00:11.1122222&#39;,&#xA;  &#39;Maybeh oh noes?&#39;,&#xA;  ARRAY [&#39;Bad things could be happening??&#39;], &#xA;  &#39;2021-04-02&#39;&#xA;);&#xA;&#xA;Notice that the last partition value &#39;2021-04-01&#39; has to match the TIMESTAMP date during insertion. There is no validation in Hive to make sure this is happening because it only requires a VARCHAR and knows to partition based on different values.&#xA;&#xA;On the other hand, If a user runs the following query:&#xA;&#xA;SELECT &#xA;FROM hive.logging.events&#xA;WHERE eventtime &lt; timestamp &#39;2021-04-02&#39;;&#xA;&#xA;they get the correct results back, but have to scan all the data in the table:&#xA;&#xA;table&#xA;trthlevel/ththeventtime/ththmessage/ththcallstack/th/tr&#xA;trtdERROR/tdtd2021-04-01 12:00:00/tdtdOh noes/tdtdException in thread &#34;main&#34; java.lang.NullPointerException/td/tr&#xA;/table&#xA;&#xA;This happens because the user forgot to include the eventtimeday &lt; &#39;2021-04-02&#39; predicate in the WHERE clause. This eliminates all the benefits that led us to create the partition in the first place and yet frequently this is missed by the users of these tables.&#xA;&#xA;SELECT &#xA;FROM hive.logging.events&#xA;WHERE eventtime &lt; timestamp &#39;2021-04-02&#39; &#xA;AND eventtimeday &lt; &#39;2021-04-02&#39;;&#xA;&#xA;Result:&#xA;&#xA;table&#xA;trthlevel/ththeventtime/ththmessage/ththcallstack/th/tr&#xA;trtdERROR/tdtd2021-04-01 12:00:00/tdtdOh noes/tdtdException in thread &#34;main&#34; java.lang.NullPointerException/td/tr&#xA;/table&#xA;&#xA;Iceberg Partitions&#xA;&#xA;The following DDL statement illustrates how these issues are handled in Iceberg via the Trino Iceberg connector.&#xA;&#xA;CREATE TABLE iceberg.logging.events (&#xA;  level VARCHAR,&#xA;  eventtime TIMESTAMP(6),&#xA;  message VARCHAR,&#xA;  callstack ARRAY(VARCHAR)&#xA;) WITH (&#xA;  partitioning = ARRAY[&#39;day(eventtime)&#39;]&#xA;);&#xA;&#xA;Taking note of a few things. First, notice the partition on the eventtime column that is defined without having to move it to the last position. There is also no need to create a separate field to handle the daily partition on the eventtime field. The partition specification is maintained internally by Iceberg, and neither the user nor the reader of this table needs to know anything about the partition specification to take advantage of it. This concept is called hidden partitioning , where only the table creator/maintainer has to know the partitioning specification. Here is what the insert statements look like now:&#xA;&#xA;INSERT INTO iceberg.logging.events&#xA;VALUES&#xA;(&#xA;  &#39;ERROR&#39;,&#xA;  timestamp &#39;2021-04-01 12:00:00.000001&#39;,&#xA;  &#39;Oh noes&#39;, &#xA;  ARRAY [&#39;Exception in thread &#34;main&#34; java.lang.NullPointerException&#39;]&#xA;),&#xA;(&#xA;  &#39;ERROR&#39;,&#xA;  timestamp &#39;2021-04-02 15:55:55.555555&#39;,&#xA;  &#39;Double oh noes&#39;,&#xA;  ARRAY [&#39;Exception in thread &#34;main&#34; java.lang.NullPointerException&#39;]),&#xA;(&#xA;  &#39;WARN&#39;, &#xA;  timestamp &#39;2021-04-02 00:00:11.1122222&#39;,&#xA;  &#39;Maybeh oh noes?&#39;,&#xA;  ARRAY [&#39;Bad things could be happening??&#39;]&#xA;);&#xA;&#xA;The VARCHAR dates are no longer needed. The eventtime field is internally converted to the proper partition value to partition each row. Also, notice that the same query that ran in Hive returns the same results. The big difference is that it doesn’t require any extra clause to indicate to filter partition as well as filter the results.&#xA;&#xA;SELECT *&#xA;FROM iceberg.logging.events&#xA;WHERE eventtime &lt; timestamp &#39;2021-04-02&#39;;&#xA;&#xA;Result:&#xA;&#xA;table&#xA;trthlevel/ththeventtime/ththmessage/ththcallstack/th/tr&#xA;trtdERROR/tdtd2021-04-01 12:00:00/tdtdOh noes/tdtdException in thread &#34;main&#34; java.lang.NullPointerException/td/tr&#xA;/table&#xA;&#xA;So hopefully that gives you a glimpse into what a table format and specification are, and why Iceberg is such a wonderful improvement over the existing and outdated method of storing your data in your data lake. While this post covers a lot of aspects of Iceberg’s capabilities, this is just the tip of the Iceberg…&#xA;&#xA;If you want to play around with Iceberg using Trino, check out the Trino Iceberg docs. The next post covers how table evolution works in Iceberg, as well as, how Iceberg is an improved storage format for cloud storage.&#xA;&#xA;#trino #iceberg&#xA;&#xA;bits&#xA;&#xA;!--emailsub--]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://trino.io/assets/blog/trino-on-ice/trino-iceberg.png" alt=""/></p>

<p>Back in the <a href="https://trino.io/blog/2020/10/20/intro-to-hive-connector">Gentle introduction to the Hive connector</a> blog post, I discussed a commonly misunderstood architecture and uses of the Trino Hive connector. In short, while some may think the name indicates Trino makes a call to a running Hive instance, the Hive connector does not use the Hive runtime to answer queries. Instead, the connector is named Hive connector because it relies on Hive conventions and implementation details from the Hadoop ecosystem – the invisible Hive specification.</p>



<hr/>

<p>Trino on ice is a series, covering the details around how the Iceberg table format works with the Trino query engine. It’s recommended to read the posts sequentially as the examples build on previous posts in this series:</p>
<ul><li><a href="https://bitsondata.dev/trino-iceberg-i-gentle-intro">Trino on ice I: A gentle introduction to Iceberg</a></li>
<li><a href="https://bitsondata.dev/trino-iceberg-ii-table-evolution-cloud">Trino on ice II: In-place table evolution and cloud compatibility with Iceberg</a></li>
<li><a href="https://write.as/bitsondatadev/trino-iceberg-iii-concurrency-snapshots-spec">Trino on ice III: Iceberg concurrency model, snapshots, and the Iceberg spec</a></li>
<li><a href="https://bitsondata.dev/trino-iceberg-iv-deep-dive">Trino on ice IV: Deep dive into Iceberg internals</a></li></ul>

<hr/>

<p>I call this specification invisible because it doesn’t exist. It lives in the Hive code and the minds of those who developed it. This is makes it very difficult for anybody else who has to integrate with any distributed object storage that uses Hive, since they had to rely on reverse engineering and keeping up with the changes. The way you interact with Hive changes based on <a href="https://medium.com/hashmapinc/four-steps-for-migrating-from-hive-2-x-to-3-x-e85a8363a18">which version of Hive or Hadoop</a> you are running. It also varies if you are in the cloud or over an object store. Spark has even <a href="https://spark.apache.org/docs/2.4.4/sql-migration-guide-hive-compatibility.html">modified the Hive spec</a> in some ways to fit the Hive model to their use cases. It’s a big mess that data engineers have put up with for years. Yet despite the confusion and lack of organization due to Hive’s number of unwritten assumptions, the Hive connector is the most popular connector in use for Trino. Virtually every big data query engine uses the Hive model today in some form. As a result it is used by numerous companies to store and access data in their data lakes.</p>

<p>So how did something with no specification become so ubiquitous in data lakes? Hive was first in the large object storage and big data world as part of Hadoop. Hadoop became popular from good marketing for Hadoop to solve the problems of dealing with the increase in data with the Web 2.0 boom . Of course, Hive didn’t get everything wrong. In fact, without Hive, and the fact that it is open source, there may not have been a unified specification at all. Despite the many hours data engineers have spent bashing their heads against the wall with all the unintended consequences of Hive, it still served a very useful purpose.</p>

<p>So why did I just rant about Hive for so long if I’m here to tell you about <a href="https://iceberg.apache.org/">Apache Iceberg</a>? It’s impossible for a teenager growing up today to truly appreciate music streaming services without knowing what it was like to have an iPod with limited storage, or listening to a scratched burnt CD that skips, or flipping your tape or record to side-B. The same way anyone born before the turn of the millennium really appreciates streaming services, so you too will appreciate Iceberg once you’ve learned the intricacies of managing a data lake built on Hive and Hadoop.</p>

<p>If you haven’t used Hive before, this blog post outlines just a few pain points that come from this data warehousing software to give you proper context. If you have already lived through these headaches, this post acts as a guide to Iceberg from Hive. This post is the first in a series of blog posts discussing Apache Iceberg in great detail, through the lens of the Trino query engine user. If you’re not aware of Trino (formerly PrestoSQL) yet, it is the project that houses the founding Presto community after the <a href="https://trino.io/blog/2020/12/27/announcing-trino">founders of Presto left Facebook</a>. This and the next couple of posts discuss the Iceberg specification and all the features Iceberg has to offer, many times in comparison with Hive.</p>

<p>Before jumping into the comparisons, what is Iceberg exactly? The first thing to understand is that Iceberg is not a file format, but a table format. It may not be clear what this means by just stating that, but the function of a table format becomes clearer as the improvements Iceberg brings from the Hive table standard materialize. Iceberg doesn’t replace file formats like ORC and Parquet, but is the layer between the query engine and the data. Iceberg maps and indexes the files in order to provide a higher level abstraction that handles the relational table format for data lakes. You will understand more about table formats through examples in this series.</p>

<h2 id="hidden-partitions" id="hidden-partitions">Hidden Partitions</h2>

<h3 id="hive-partitions" id="hive-partitions">Hive Partitions</h3>

<p>Since most developers and users interact with the table format via the query language, a noticeable difference is the flexibility you have while creating a partitioned table. Assume you are trying to create a table for tracking events occurring in our system. You run both sets of SQL commands from Trino, just using the Hive and Iceberg connectors which are designated by the catalog name (i.e. the catalog name starting with <code>hive.</code> uses the Hive connector, while the <code>iceberg.</code> table uses the Iceberg connector). To begin with, the first DDL statement attempts to create an <code>events</code> table in the <code>logging</code> schema in the <code>hive</code> catalog, which is configured to use the Hive connector. Trino also creates a partition on the <code>events</code> table using the <code>event_time</code> field which is a <code>TIMESTAMP</code> field.</p>

<pre><code>CREATE TABLE hive.logging.events (
  level VARCHAR,
  event_time TIMESTAMP,
  message VARCHAR,
  call_stack ARRAY(VARCHAR)
) WITH (
  format = &#39;ORC&#39;,
  partitioned_by = ARRAY[&#39;event_time&#39;]
);
</code></pre>

<p>Running this in Trino using the Hive connector produces the following error message.</p>

<pre><code>Partition keys must be the last columns in the table and in the same order as the table properties: [event_time]
</code></pre>

<p>The Hive DDL is very dependent on ordering for columns and specifically partition columns. Partition fields must be located in the final column positions and in the order of partitioning in the DDL statement. The next statement attempts to create the same table, but now with the <code>event_time</code> field moved to the last column position.</p>

<pre><code>CREATE TABLE hive.logging.events (
  level VARCHAR,
  message VARCHAR,
  call_stack ARRAY(VARCHAR),
  event_time TIMESTAMP
) WITH (
  format = &#39;ORC&#39;,
  partitioned_by = ARRAY[&#39;event_time&#39;]
);
</code></pre>

<p>This time, the DDL command works successfully, but you likely don’t want to partition your data on the plain timestamp. This results in a separate file for each distinct timestamp value in your table (likely almost a file for each event). In Hive, there’s no way to indicate the time granularity at which you want to partition natively. The method to support this scenario with Hive is to create a new <code>VARCHAR</code> column, <code>event_time_day</code> that is dependent on the <code>event_time</code> column to create the date partition value.</p>

<pre><code>CREATE TABLE hive.logging.events (
  level VARCHAR,
  event_time TIMESTAMP,
  message VARCHAR,
  call_stack ARRAY(VARCHAR),
  event_time_day VARCHAR
) WITH (
  format = &#39;ORC&#39;,
  partitioned_by = ARRAY[&#39;event_time_day&#39;]
);
</code></pre>

<p>This method wastes space by adding a new column to your table. Even worse, it puts the burden of knowledge on the user to include this new column for writing data. It is then necessary to use that separate column for any read access to take advantage of the performance gains from the partitioning.</p>

<pre><code>INSERT INTO hive.logging.events
VALUES
(
  &#39;ERROR&#39;,
  timestamp &#39;2021-04-01 12:00:00.000001&#39;,
  &#39;Oh noes&#39;, 
  ARRAY [&#39;Exception in thread &#34;main&#34; java.lang.NullPointerException&#39;], 
  &#39;2021-04-01&#39;
),
(
  &#39;ERROR&#39;,
  timestamp &#39;2021-04-02 15:55:55.555555&#39;,
  &#39;Double oh noes&#39;,
  ARRAY [&#39;Exception in thread &#34;main&#34; java.lang.NullPointerException&#39;],
  &#39;2021-04-02&#39;
),
(
  &#39;WARN&#39;, 
  timestamp &#39;2021-04-02 00:00:11.1122222&#39;,
  &#39;Maybeh oh noes?&#39;,
  ARRAY [&#39;Bad things could be happening??&#39;], 
  &#39;2021-04-02&#39;
);
</code></pre>

<p>Notice that the last partition value <code>&#39;2021-04-01&#39;</code> has to match the <code>TIMESTAMP</code> date during insertion. There is no validation in Hive to make sure this is happening because it only requires a <code>VARCHAR</code> and knows to partition based on different values.</p>

<p>On the other hand, If a user runs the following query:</p>

<pre><code>SELECT *
FROM hive.logging.events
WHERE event_time &lt; timestamp &#39;2021-04-02&#39;;
</code></pre>

<p>they get the correct results back, but have to scan all the data in the table:</p>

<table>
<tr><th>level</th><th>event_time</th><th>message</th><th>call_stack</th></tr>
<tr><td>ERROR</td><td>2021-04-01 12:00:00</td><td>Oh noes</td><td>Exception in thread &#34;main&#34; java.lang.NullPointerException</td></tr>
</table>

<p>This happens because the user forgot to include the <code>event_time_day &lt; &#39;2021-04-02&#39;</code> predicate in the <code>WHERE</code> clause. This eliminates all the benefits that led us to create the partition in the first place and yet frequently this is missed by the users of these tables.</p>

<pre><code>SELECT *
FROM hive.logging.events
WHERE event_time &lt; timestamp &#39;2021-04-02&#39; 
AND event_time_day &lt; &#39;2021-04-02&#39;;
</code></pre>

<p>Result:</p>

<table>
<tr><th>level</th><th>event_time</th><th>message</th><th>call_stack</th></tr>
<tr><td>ERROR</td><td>2021-04-01 12:00:00</td><td>Oh noes</td><td>Exception in thread &#34;main&#34; java.lang.NullPointerException</td></tr>
</table>

<h3 id="iceberg-partitions" id="iceberg-partitions">Iceberg Partitions</h3>

<p>The following DDL statement illustrates how these issues are handled in Iceberg via the Trino Iceberg connector.</p>

<pre><code>CREATE TABLE iceberg.logging.events (
  level VARCHAR,
  event_time TIMESTAMP(6),
  message VARCHAR,
  call_stack ARRAY(VARCHAR)
) WITH (
  partitioning = ARRAY[&#39;day(event_time)&#39;]
);
</code></pre>

<p>Taking note of a few things. First, notice the partition on the <code>event_time</code> column that is defined without having to move it to the last position. There is also no need to create a separate field to handle the daily partition on the <code>event_time</code> field. The <em><strong>partition specification</strong></em> is maintained internally by Iceberg, and neither the user nor the reader of this table needs to know anything about the partition specification to take advantage of it. This concept is called <em><strong>hidden partitioning</strong></em> , where only the table creator/maintainer has to know the <em><strong>partitioning specification</strong></em>. Here is what the insert statements look like now:</p>

<pre><code>INSERT INTO iceberg.logging.events
VALUES
(
  &#39;ERROR&#39;,
  timestamp &#39;2021-04-01 12:00:00.000001&#39;,
  &#39;Oh noes&#39;, 
  ARRAY [&#39;Exception in thread &#34;main&#34; java.lang.NullPointerException&#39;]
),
(
  &#39;ERROR&#39;,
  timestamp &#39;2021-04-02 15:55:55.555555&#39;,
  &#39;Double oh noes&#39;,
  ARRAY [&#39;Exception in thread &#34;main&#34; java.lang.NullPointerException&#39;]),
(
  &#39;WARN&#39;, 
  timestamp &#39;2021-04-02 00:00:11.1122222&#39;,
  &#39;Maybeh oh noes?&#39;,
  ARRAY [&#39;Bad things could be happening??&#39;]
);
</code></pre>

<p>The <code>VARCHAR</code> dates are no longer needed. The <code>event_time</code> field is internally converted to the proper partition value to partition each row. Also, notice that the same query that ran in Hive returns the same results. The big difference is that it doesn’t require any extra clause to indicate to filter partition as well as filter the results.</p>

<pre><code>SELECT *
FROM iceberg.logging.events
WHERE event_time &lt; timestamp &#39;2021-04-02&#39;;
</code></pre>

<p>Result:</p>

<table>
<tr><th>level</th><th>event_time</th><th>message</th><th>call_stack</th></tr>
<tr><td>ERROR</td><td>2021-04-01 12:00:00</td><td>Oh noes</td><td>Exception in thread &#34;main&#34; java.lang.NullPointerException</td></tr>
</table>

<p>So hopefully that gives you a glimpse into what a table format and specification are, and why Iceberg is such a wonderful improvement over the existing and outdated method of storing your data in your data lake. While this post covers a lot of aspects of Iceberg’s capabilities, this is just the tip of the Iceberg…</p>

<p><img src="https://trino.io/assets/blog/trino-on-ice/see_myself_out.gif" alt=""/></p>

<p>If you want to play around with Iceberg using Trino, check out the <a href="https://trino.io/docs/current/connector/iceberg.html">Trino Iceberg docs</a>. The <a href="https://bitsondata.dev/in-place-table-evolution-and-cloud-compatibility-with-iceberg">next post</a> covers how table evolution works in Iceberg, as well as, how Iceberg is an improved storage format for cloud storage.</p>

<p><a href="https://bitsondata.dev/tag:trino" class="hashtag"><span>#</span><span class="p-category">trino</span></a> <a href="https://bitsondata.dev/tag:iceberg" class="hashtag"><span>#</span><span class="p-category">iceberg</span></a></p>

<p><em>bits</em></p>


]]></content:encoded>
      <guid>https://bitsondata.dev/trino-iceberg-i-gentle-intro</guid>
      <pubDate>Mon, 03 May 2021 05:00:00 +0000</pubDate>
    </item>
  </channel>
</rss>