trino: an origin story (nailed it!)

I wish I created this video as it is engaging AND SUCCINCT. You might think 20 minutes isn’t succinct, but wow… an INCREDIBLE STORY is told in those 20 minutes and I highly recommend this video for anyone new to Trino to get you started as well as those that have been in this space a while to help you with your own storytelling.

I love that the creator walks through the whole reason we coupled data in systems like Hadoop. Data locality really did matter when the network became the bottleneck. I still say that the “no-network” model of moving the compute to data instead of the data to the computer will always be faster, but today’s separation of storage & compute offers many more benefits (especially cost & flexibility) that is is a the current right approach to data analytics at scale.

I also loved that the video creator talked about resiliency vs performance and even compared Trino with Spark in addition to Hive. Check out my hive, trino & spark features (their journeys to sql, performance & durability) post about how fault-tolerant execution is possible with Trino.

I can’t recommend the YouTube video above enough. Check it out!

hive to iceberg migration tool (rev1)

I’m preparing for a Hive to Iceberg migration webinar coming up on May 8, 2024, (yes, a #shamelessselfpromotion there) where I’ll talk about the in-place & shadow migration strategies you learn more about in this hands-on Starburst tutorial. It is cool to migrate one table at a time, but what if you have a whole schema full of tables to migrate. Heck, what if you have a WHOLE BUNCH of schemas, too?!?!

Well, that’s why programmers will always have a job! They can build a tool to help you out. I’m sure someone is going to build something better than what I did this evening, but I did put together an initial, “happy path”, migration tool. I put it up on GitHub at lestermartin/trino-dataframes-exploration/IcebergMigrationTool, too, so you can check it out if you want as well. What a swell guy I am. 😉

You can find instructions there of how to use this Jupyter notebook and I even recorded a quick demonstration of the features in this initial version.

This first cut only tackles the easy stuff, but I’ll work on it and share more here when it is worth mentioning again. As always, more eyes on it will only make it better so let me know what it is missing or give me a pull request with your updates.

data universe 2024 workshops (feedback appreciated)

I’m super thrilled to be leading FOUR workshops at Data Universe 2024.

All my materials are hosted on this project, https://github.com/lestermartin/du2024, so come and take a peek if you are interested.

pystarburst via a jupyter notebook (exploring the tpc-h dataset)

I just committed a new jupyter notebook full of code examples of using PyStarburst with Starburst Galaxy. Check it out at https://github.com/starburstdata/pystarburst-examples/blob/main/notebooks/tpch.ipynb.

If you don’t have jupyter set up already, the cool thing is that on the front page of the https://github.com/starburstdata/pystarburst-examples project there is a nice button labeled as “lunch binder” that will spin up a container with jupyter runing so you can test out this notebook.

I went ahead and recorded myself walking through this notebook and I’d love you to check out my video.

building scalar udf’s w/sql for trino (aka sql routines)

I’m finally getting a bit of time to try out Trino SQL routines in Starburst Galaxy and I thought I’d share my experiences. Like is often the case, the intro docs actually help quite a bit. I thought I’d show a simple example to get this post started and then add another slightly more complex one to illustrate their biggest benefit — abstraction!

A simple SQL routine

Run the following to create a SQL routine that takes a first and last name as parameters and then returns a single string in the “last_name, first_name” format.

CREATE FUNCTION combine_name(
    first_name varchar, last_name varchar)
  RETURNS varchar
  RETURN concat_ws(', ', last_name, first_name);

SELECT combine_name('Lester', 'Martin');

Starburst Galaxy has a sample table that can help test this out a bit more.

SELECT custkey, 
       combine_name(first_name, last_name) AS combined_name
  FROM sample.burstbank.customer
 LIMIT 5;

Easy-peasy, but for this simple example it would have been perfectly acceptable to just use the concat_ws function in-stream of the SELECT statement.

A slightly more complex one

For this one, let’s continue to use the sample Burst Bank customer table. Here are a few of the key columns we will work with.

SELECT custkey, paycheck_dd, 
       estimated_income, fico
  FROM sample.burstbank.customer 
 LIMIT 5;

Our business partners have created a medallion status algorithm based on creating a score by adding the following values together.

  1. If the customer is using direct deposit, start with 50,000 points (0 points if not).
  2. Their income (as-is).
  3. 100 times their credit score.

Then assign a medallion status based on the following score ranges.

bronzeup to 75,000
silverup to 150,000
goldup to 250,000
platinumeverything else

Here is the implementation.

CREATE OR REPLACE FUNCTION medallion_status(
    paycheck_dd varchar, estimated_income double, fico integer)
  RETURNS varchar
  BEGIN 
    DECLARE m_score double DEFAULT 1.00;
    SET m_score = IF(paycheck_dd = 'Y', 50000.00, 0.00) +
                  estimated_income + (fico * 100);
    IF m_score < 75000.01 THEN 
      RETURN 'bronze';
    ELSEIF m_score < 150000.01 THEN 
      RETURN 'silver';
    ELSEIF m_score < 250000.01 THEN 
      RETURN 'gold';
    ELSE 
      RETURN 'platinum';
    END IF;
    RETURN null;
  END;

Run a quick smoke-test.

SELECT paycheck_dd, estimated_income, fico,  
       medallion_status(paycheck_dd, estimated_income, fico)
  FROM sample.burstbank.customer LIMIT 5;

Let’s do a quick aggregation on the newly created medallion status to verify it makes sense across the full customer base.

WITH incl_medallion AS (
SELECT paycheck_dd, estimated_income, fico, state,
       medallion_status(paycheck_dd, estimated_income, fico) 
         AS medallion_status
  FROM sample.burstbank.customer
)
SELECT medallion_status, 
       round(avg(estimated_income),2) AS avg_income,
       round(avg(fico),0) AS avg_fico
  FROM incl_medallion
 GROUP BY medallion_status
 ORDER BY avg_income desc;

Yep, it looks good from here! Plus, we can abstract away the full user-defined function (UDF) so it doesn’t make our query so convoluted AND we can reuse it easily when needed. Cool beans!

apache iceberg table maintenance (is_current_ancestor part deux)

This post is about the maintenance activities that Apache Iceberg tables need to undergo as more and more versions are created. I will be using the details from my prior iceberg snapshot post to build upon and I do recommend you check it out first.

As before, I’m using Starburst Galaxy built on top of Trino and will be leveraging the ALTER TABLE EXECUTE docs which details the steps for the needed maintenance.

Starting point

Querying the table from the iceberg snapshot post we see there are 8 records at this point.

SELECT * FROM test_rb ORDER BY f1;

Querying the view below gives us some details about the current snapshot of the test_rb table.

-- see prior post for the CREATE VIEW statement
SELECT * FROM curr_ver_dets;

More easily visualized with the following graphic.

A peek at the folder in S3 where this table is rooted at shows…

  • ./metadata/ — has 51 files
  • ./data/ — has 14 files

Run the following to see how many data files make up the current version.

SELECT file_path, file_format, 
       record_count, file_size_in_bytes
  FROM "test_rb$files";

The output shows just 5 of the 14 files are used in this version (let’s not worry about all those “small files” — that’s a discussion for another time & just a byproduct of our simple tests).

Compact the data

The optimize command is used for rewriting the content of the specified table so that it is merged into fewer but larger files. If the table is partitioned, the data compaction acts separately on each partition selected for optimization. This operation improves read performance.

https://docs.starburst.io/latest/connector/iceberg.html#optimize
ALTER TABLE test_rb EXECUTE optimize;

After running the query on "test_rb$files" again, it doesn’t look like much has happened; down from 5 to 4 files We have to take into account these are TINY little files and Trino runs in parallel.

If you look closely, you’ll see there are only the 8 records shown earlier that are spread across the files — down from 13 previously. This is because the compaction process is also doing a merge-on-write operation to get rid of those merge-on-read “delete files”.

Note to self… Seems we need a blog post explaining how the “no in-place deletes” constraint actually allows deletes with Iceberg…

It may not sound like we changed the data or the structure (which causes the creation of a new version), but we did. Rewriting the data like this creates yet another version. Running the previous curr_ver_dets query shows this (only displaying the last two rows).

Here is the updated visualization.

And yes, because we created another version we have even more files on the data lake. The metadata subfolder has 60 files now and data has 18.

Prune old versions

The expire_snapshots command removes all snapshots and all related metadata and data files. Regularly expiring snapshots is recommended to delete data files that are no longer needed, and to keep the size of table metadata small. The procedure affects all snapshots that are older than the time period configured with the retention_threshold parameter.

https://docs.starburst.io/latest/connector/iceberg.html#expire-snapshots

Everything before this last version occurred more than a few days ago, so my command below is taking that in account to get rid of all versions except the current one. Normally, you would likely want to keep some prior versions and only be dropping those you know you can get rid of.

ALTER TABLE test_rb 
EXECUTE expire_snapshots(retention_threshold => '3d');

GOOD! The system is suggesting that we are being too aggressive. Well, let’s just be “aggressive” anyways! Run the session override below and then the expire_snapshots command again.

SET SESSION mycatalog.expire_snapshots_min_retention = '2d';

We only have a single version now!

Let’s verify we cannot rollback to that version it references as its parent_id (notice the operation value of replace above).

CALL mycatalog.system.rollback_to_snapshot(
   'myschema', 'test_rb',
    4056689309859241417);

Remove orphaned files

There are still a lot of data files (11) on the data lake for this table although our file compaction process lowered the count down to 4 needed for the current version. Thankfully, there’s another maintenance activity that can clean up these files that are no longer referenced.

The remove_orphan_files command removes all files from a table’s data directory that are not linked from metadata files and that are older than the value of retention_threshold parameter. Deleting orphan files from time to time is recommended to keep size of a table’s data directory under control.

https://docs.starburst.io/latest/connector/iceberg.html#remove-orphan-files

Like before, we need to be a bit “aggressive” and lower the minimum threshold value.

SET SESSION 
webinar2.remove_orphan_files_min_retention = '2d';

ALTER TABLE test_rb 
EXECUTE remove_orphan_files(retention_threshold => '2d');

Hooray, the data lake only has the 4 referenced file shown earlier!

Wrapping up

These maintenance activities rebuild many small files into fewer larger ones, get rid of those pesky/interesting “delete files”, and reduce the amount of data lake storage required maintaining so many historical versions. Coupling these maintenance activities along with the inherent abilities that come from leveraging the metadata on the data lake will allow your Iceberg tables to continue to perform at internet-scale!

iceberg snapshot is_current_ancestor flag (what does it tell us)

xr:d:DAF2Cx_uJfE:14,j:5412612120539734786,t:23120419

I was leading a webinar this week focused on Apache Iceberg when I was asked about the purpose of the is_current_ancestor column of the $history metadata table. I made an educated guess & I also CLEARLY said “I’m surely not sure”. Thankfully, Monica Miller, from Starburst‘s DevRel team offered up the following answer in the chat.

That got my mind turning and I promised myself right then that I’d figure this one out and be able to answer it correctly next time. Like always, a quick search ultimately gave me what I was looking for. 

The Iceberg experts over at Tabular explain it briefly in this blog post. It states, “The is_current_ancestor flag lets you know if a snapshot is part of the linear history of the current table”. If that’s enough of an answer for you, you can walk away right now.

Ok… you’re still here. Let’s try it out for ourselves.

Set up your environment

Being a huge fan of Trino AND being rather lazy, I decided to use Startburst Galaxy. Why not, it is set up for you and you can register for free! Of course, you can do these steps with any SQL engine that supports Apache Iceberg. (Another) of course, my instructions are focused on using Galaxy.

Once you get logged into your very own Galaxy tenant, you need to make sure you have a data lake catalog you can write to — see docs and/or tutorials. On the tutorials page, toggle the Category and Catalog selectors as shown below to get help with the “big 3”.

My querying … data in the cloud post shows you how I setup Galaxy and S3 previously as yet another helper to get you started with this BYOB (Bring Your Own Bucket) model.

Operation 1; create a table

Use appropriate values for mycatalog.myschema then create an Iceberg table and query it afterwards. 

CREATE mycatalog.myschema;  -- or use an existing schema
USE mycatalog.myschema;  

CREATE TABLE test_rb (
    f1 int, f2 varchar
) WITH (type='iceberg');

SELECT * FROM test_rb;

As expected, there are no rows yet.

As you can see below, I’m joining 3 different metadata tables to ultimately get the results I want. To make it easy, I’m also hiding this query behind a view for ease of use later.

CREATE VIEW curr_ver_dets AS
SELECT concat_ws(' > ', r.name, r.type) 
           AS curr_ver,
       date_format(s.committed_at, '%Y/%m/%d-%T')
           AS committed_at,
       s.snapshot_id, s.parent_id, 
       h.is_current_ancestor, s.operation
  FROM "test_rb$snapshots" AS s
  JOIN "test_rb$history" AS h
    ON (s.snapshot_id = h.snapshot_id)
  LEFT JOIN "test_rb$refs" AS r
    ON (h.snapshot_id = r.snapshot_id)
 ORDER BY s.committed_at;

Now, and in future steps, you can just read from the new curr_ver_dets view.

SELECT * FROM curr_ver_dets;

Each change to the structure or content of an Iceberg table creates a new version (aka snapshot). Notice there is only one snapshot at this point and that it is the initial version (identified by curr_ver reporting main > BRANCH) as it has no parent snapshot. And yep, is_current_ancestor reports true. That means it is part of the linear history of the active snapshot. 

Pretty obvious with just one snapshot and the image below is a bit of overkill at this point. Note that I’m only putting the last 4 numbers of the snapshot_id in the diagram and will continue that strategy with future snapshots.

Operations 2-3; two insert statements

Run two separate INSERT statements that will create a new snapshot for each operation.

INSERT INTO test_rb (f1, f2) 
VALUES 
  (1, '1st insert (2nd txn)'), 
  (2, '1st insert (2nd txn)');

INSERT INTO test_rb (f1, f2) 
VALUES 
  (3, '2nd insert (3rd txn)'), 
  (4, '2nd insert (3rd txn)'), 
  (5, '2nd insert (3rd txn)');

SELECT * FROM test_rb ORDER BY f1;

We have 5 records now. What does the list of snapshots look like?

SELECT * FROM curr_ver_dets;

Two more snapshots were created and the final one is the current version. You can trace its parent_id to the previous row and then again from that middle row to the initial snapshot. 

The diagram below shows all 3 snapshots are part of the “current ancestor tree” (i.e. their is_current_ancestor flags are set to true).

Operation 4; delete statement

Delete any record whose f1 value is an even number and verify only the odd numbered records are left.

DELETE FROM test_rb
 WHERE mod(f1, 2) = 0;

We are down to just 3 records now and the contents of the curr_ver_dets view shows we continue to build a very linear set of snapshots whose parent is the snapshot that was created before it. Notice the is_current_ancestor values are all reporting true. 

Operation 5; rollback to operation 2

Rollback to the snapshot_id that was created on the first INSERT statement in Operation 2. In my example the snapshop_id needed is 5611312366620665402. Change it to be the snapshot_id that was created in your environment. Be sure to use appropriate values for mycatalog and myschema, too.

CALL mycatalog.system.rollback_to_snapshot(
   'myschema', 'test_rb',
    5611312366620665402);

Query test_rb to verify we only have the first 2 records that were inserted in the transaction right after the table creation one.

Query curr_ver_dets to see that we now have have the current version of the table pointing to the snapshot_id we rolled backed to. 

The diagram below is showing you what the results above are declaring. No new snapshot was created, but the current version is 2 versions back and the last 2 snapshots are no longer part of the linear history of the active version (indicated by the green shapes). These snapshots still exist and can be rolled backed to at anytime as well.

Operations 6-7; two more inserts

Run 2 more INSERT statements that collectively add 3 more rows and create 2 more snapshots.

INSERT INTO test_rb (f1, f2) 
VALUES 
  (61, '3rd insert (oper #6)'), 
  (62, '3rd insert (oper #6)');

INSERT INTO test_rb (f1, f2) 
VALUES 
  (70, '4th insert (7th operation)');

The most recent 2 snapshots are part of the current ancestor tree and 2 snapshots before them still have their is_current_ancestor set to false.

Operation 8; rollback to operation 4

Rollback to the snapshot_id that was created by Operation 4’s DELETE statement. In my example the snapshop_id needed is 6149468753676344494. Change it to be the snapshot_id that was created in your environment. Be sure to use appropriate values for mycatalog and myschema, too.

CALL webinar2.system.rollback_to_snapshot(
   'myschema', 'test_rb',
    6149468753676344494);

You should only see those early odd numbered records.

As you can see above & below, no new snapshots were created, but the current version has been reset and appropriate changes have been made to is_current_ancestor values.

Operations 9-10; insert 2 records and change 1 of them

Let’s see if it is all making sense…

INSERT INTO test_rb (f1, f2) 
VALUES 
  (91, 'OPER #09'), 
  (92, 'OPER #09');

UPDATE test_rb SET f2 = 'OPER #10'
 WHERE f1 = 92;

Make sense? If not, drop me a comment.

Operations 11-14; drill it home!

This all makes sense, too? Feel free to do one operation at a time and review it every step of the way.

CALL webinar2.system.rollback_to_snapshot(
   'myschema', 'test_rb',
    8421281246956311672);

UPDATE test_rb SET f2 = 'update from Op 12';

INSERT INTO test_rb VALUES (113, 'Op 13');

INSERT INTO test_rb (f1, f2) 
VALUES 
  (114, 'OPER # 14'), 
  (214, 'OPER # 14');

If this makes your head hurt AND you can’t figure it out, I’ll see you in the comments.

HAPPY SNAPSHOTTING!

dbt cloud & starburst galaxy workshop (beta testers welcome)

I’ll be presenting a hands-on workshop for a live audience soon where participants will build out a data pipeline with dbt Cloud and Starburst Galaxy. As per my usual process, I record presentations & labs to see if all flows well, to catch errors, and to validate my timing estimates.

Usually, these are not shared, but I wanted to get some more eyeballs on the lab exercises (and hopefully hands on keyboards) as some feedback would be awesome. That said, no worries if you just want to watch me do the hands-on components (i.e. all but the first 10 mins of intro material).

For those who can’t wait any longer, here’s the YouTube playlist!

I need to call out that this is NOT a workshop I created from scratch. Far from it! 

The original Quickstart for dbt Cloud and Starburst Galaxy was was created by Amy Chen (dbt) and then Kyle Payne (Starburst) released it as Build a data transformation pipeline with dbt and Starburst Galaxy. He enhanced it to showcase Trino/Starburst abilities of connecting to multiple data sources instead of being limited to a single source by the dbt project itself.

My spin on the workshop was to explore more features of Starburst Galaxy. These include global search, schema discovery, data quality checks, tagging, and of course, data products. I also was able to adjust some of the screenshots as some of the flows look different in both of the tools since these last ones were created.

Interested? Want to try out the labs yourself? I’d suggest watching the first video in the YouTube playlist to get your bearings and then jump into the lab guide itself as it will walk you through all of the steps for this workshop.

And absolutely feel free to provide feedback here in this blog post’s comments section or the same place on the most appropriate YouTube video in the playlist.

Here are the videos themselves!