<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Databricks SQL SME on Medium]]></title>
        <description><![CDATA[Stories by Databricks SQL SME on Medium]]></description>
        <link>https://medium.com/@databricks_sql_sme?source=rss-205045f50ad7------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*oazf1fIuTZN_3m-KijzS0Q.png</url>
            <title>Stories by Databricks SQL SME on Medium</title>
            <link>https://medium.com/@databricks_sql_sme?source=rss-205045f50ad7------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 06:47:40 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@databricks_sql_sme/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Unlocking Geospatial Analysis with Databricks SQL]]></title>
            <link>https://medium.com/dbsql-sme-engineering/unlocking-geospatial-analysis-with-databricks-sql-0d0618016276?source=rss-205045f50ad7------2</link>
            <guid isPermaLink="false">https://medium.com/p/0d0618016276</guid>
            <category><![CDATA[geospatial]]></category>
            <category><![CDATA[databricks-sql]]></category>
            <category><![CDATA[geospatial-data]]></category>
            <category><![CDATA[data-warehouse]]></category>
            <category><![CDATA[databricks]]></category>
            <dc:creator><![CDATA[Databricks SQL SME]]></dc:creator>
            <pubDate>Thu, 02 Oct 2025 17:40:56 GMT</pubDate>
            <atom:updated>2025-10-02T17:40:56.797Z</atom:updated>
            <content:encoded><![CDATA[<p>Native Geography and Geometry types and expanded Spatial SQL Expressions in DBR 17.1+</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/797/1*0G7UQhnN4SdyLX12iKYnWQ.png" /></figure><p>Author: <a href="https://www.linkedin.com/in/championjared">Jared Champion</a></p><p>Geospatial analytics has become increasingly critical for modern data-driven organizations, from location intelligence and urban planning to supply chain optimization and environmental monitoring. With the release of Databricks Runtime (DBR) 17.1, Databricks has significantly enhanced its geospatial SQL capabilities, introducing native geospatial data types and optimized functions that make spatial analysis more powerful and accessible than ever before.</p><p>This post will explore the new geospatial features in DBR 17.1 that can significantly improve spatial analytics in Databricks.</p><p>Let’s get ready to unlock the full potential of geospatial analytics on the Databricks Platform!</p><h4>What’s new in DBR 17.1 — Geospatial Data Types and Functions</h4><p>DBR 17.1 introduces native support for geospatial data types and additional functions to work with these types.</p><h4><a href="https://www.google.com/url?q=https://docs.databricks.com/aws/en/sql/language-manual/data-types/geometry-type&amp;sa=D&amp;source=editors&amp;ust=1759428327987949&amp;usg=AOvVaw0dv4J0mJeE534WPKJTcpYr">GEOMETRY</a> type</h4><p>The GEOMETRY type represents a geometry object (point, linestring, or polygon, etc.) using a Euclidean coordinate reference system (X, Y) and an optional Z coordinate, as though the Earth were flat. The underlying coordinate reference is described by the SRID value of the GEOMETRY type. There are over 11K SRIDs supported for this type.</p><p>Example:</p><p>SELECT st_geomfromtext(‘POINT(1 2)’, 4326);</p><h4><a href="https://www.google.com/url?q=https://docs.databricks.com/aws/en/sql/language-manual/data-types/geography-type&amp;sa=D&amp;source=editors&amp;ust=1759428327988865&amp;usg=AOvVaw33WU9l1oRpjXP-glA6ewM2">GEOGRAPHY</a> type</h4><p>The GEOGRAPHY type represents a geographic object (point, linestring, polygon, etc.) using a geographic coordinate system on the Earth&#39;s surface. The coordinates are expected to be valid longitude and latitude values. Note: Currently, only 4326 is supported for this type.</p><p>Example:</p><p>SELECT st_geogfromtext(‘POINT(1 2)’);</p><h4><a href="https://www.google.com/url?q=https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-st-geospatial-functions&amp;sa=D&amp;source=editors&amp;ust=1759428327989717&amp;usg=AOvVaw1w2a8DG_jsfsloKEQDjS8a">80+ SPATIAL Functions</a></h4><p>A wide range of spatial functions have also been added — too many to dive into for this post! We will review a select few, but please check out the full list at the link above. These functions can assist in importing/exporting spatial data, editing and updating spatial types, understanding spatial relationships, and transforming spatial data.</p><h4>Working with Geospatial types in DBSQL</h4><p>In the following sections, we will dive into the Geospatial types in Databricks! To follow along, ensure you are using a cluster with DBR 17.1 or higher. A sample notebook with the code used in this section is <a href="https://www.google.com/url?q=https://e2-demo-field-eng.cloud.databricks.com/editor/notebooks/180155737974753?o%3D1444828305810485%23command/6446718906660814&amp;sa=D&amp;source=editors&amp;ust=1759428327990740&amp;usg=AOvVaw23Ds5wo8vMvenzlx7nFktA">available here</a>.</p><p>Let’s start by creating some sample geospatial data! For our example, let&#39;s take a retail use case. We will create tables to represent:</p><ul><li>Retail Stores (points)</li><li>Delivery Zones (polygons)</li><li>Routes (paths)</li><li>Customer locations (points)</li></ul><h4>Retail Stores</h4><pre><br>-- Create a table of retail store locations using the new POINT geometry type<br><br>CREATE OR REPLACE TABLE IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;retail_stores&#39;) AS<br>SELECT<br> store_id,<br> store_name,<br> city,<br> state,<br> ST_Point(longitude, latitude, 4326) as location,<br> annual_revenue,<br> employee_count,<br> store_type<br>FROM VALUES<br> (1, &#39;Downtown Seattle&#39;, &#39;Seattle&#39;, &#39;WA&#39;, -122.3328, 47.6061, 2500000, 25, &#39;flagship&#39;),<br> (2, &#39;Pike Place Market&#39;, &#39;Seattle&#39;, &#39;WA&#39;, -122.3417, 47.6089, 1800000, 18, &#39;standard&#39;),<br> (3, &#39;SFO Union Square&#39;, &#39;San Francisco&#39;, &#39;CA&#39;, -122.4074, 37.7873, 3200000, 32, &#39;flagship&#39;),<br> (4, &#39;Mission District&#39;, &#39;San Francisco&#39;, &#39;CA&#39;, -122.4194, 37.7599, 1600000, 16, &#39;standard&#39;),<br> (5, &#39;Beverly Hills&#39;, &#39;Los Angeles&#39;, &#39;CA&#39;, -118.4000, 34.0736, 2800000, 28, &#39;flagship&#39;),<br> (6, &#39;Santa Monica&#39;, &#39;Los Angeles&#39;, &#39;CA&#39;, -118.4912, 34.0195, 2100000, 21, &#39;standard&#39;),<br> (7, &#39;Times Square&#39;, &#39;New York&#39;, &#39;NY&#39;, -73.9857, 40.7580, 4200000, 42, &#39;flagship&#39;),<br> (8, &#39;Brooklyn Heights&#39;, &#39;New York&#39;, &#39;NY&#39;, -73.9958, 40.6955, 1900000, 19, &#39;standard&#39;),<br> (9, &#39;Michigan Avenue&#39;, &#39;Chicago&#39;, &#39;IL&#39;, -87.6244, 41.8781, 3100000, 31, &#39;flagship&#39;),<br> (10, &#39;Lincoln Park&#39;, &#39;Chicago&#39;, &#39;IL&#39;, -87.6376, 41.9217, 1700000, 17, &#39;standard&#39;)<br>AS t(store_id, store_name, city, state, longitude, latitude, annual_revenue, employee_count, store_type);</pre><p>We can see that this table has saved our location in the new geometry type!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/628/1*8R1vuPXDTOO7niSvvhXRWA.png" /></figure><pre>-- Verify the store data and examine the geometry type<br>SELECT<br> store_name,<br> city,<br> location,<br> ST_AsText(location) as location_wkt,<br> ST_X(location) as longitude,<br> ST_Y(location) as latitude<br>FROM IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;retail_stores&#39;)<br>LIMIT 5;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/553/1*phm4ybKGv4vgGUViiGvZmA.png" /></figure><h4>Delivery Zones</h4><pre><br>-- Create delivery zone polygons using ST_GeomFromText<br>CREATE OR REPLACE TABLE IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;delivery_zones&#39;) AS<br>SELECT<br> zone_id,<br> zone_name,<br> city,<br> ST_GeomFromText(zone_polygon, 4326) as zone_boundary,<br> max_delivery_time_minutes,<br> delivery_fee<br>FROM VALUES<br> (1, &#39;Seattle Central&#39;, &#39;Seattle&#39;, &#39;POLYGON((-122.3500 47.5900, -122.3200 47.5900, -122.3200 47.6200, -122.3500 47.6200, -122.3500 47.5900))&#39;, 30, 5.99),<br> (2, &#39;SF Downtown&#39;, &#39;San Francisco&#39;, &#39;POLYGON((-122.4200 37.7700, -122.3900 37.7700, -122.3900 37.8000, -122.4200 37.8000, -122.4200 37.7700))&#39;, 25, 7.99),<br> (3, &#39;LA West Side&#39;, &#39;Los Angeles&#39;, &#39;POLYGON((-118.5000 34.0000, -118.3500 34.0000, -118.3500 34.1000, -118.5000 34.1000, -118.5000 34.0000))&#39;, 45, 6.99),<br> (4, &#39;Manhattan Midtown&#39;, &#39;New York&#39;, &#39;POLYGON((-74.0000 40.7400, -73.9700 40.7400, -73.9700 40.7800, -74.0000 40.7800, -74.0000 40.7400))&#39;, 20, 8.99),<br> (5, &#39;Chicago Loop&#39;, &#39;Chicago&#39;, &#39;POLYGON((-87.6400 41.8600, -87.6100 41.8600, -87.6100 41.8900, -87.6400 41.8900, -87.6400 41.8600))&#39;, 35, 6.99)<br>AS t(zone_id, zone_name, city, zone_polygon, max_delivery_time_minutes, delivery_fee);</pre><pre><br>-- Examine the delivery zones<br>SELECT<br> zone_name,<br> city,<br> ST_AsText(zone_boundary) as polygon_wkt,<br> -- Use ST_Transform to change SRID to one that is measured in meters for easier calculation<br> ST_Area(ST_Transform(zone_boundary, 5070)) as area_sq_meters<br>FROM IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;delivery_zones&#39;);</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/524/1*fG4oDqKIbBXNBaiBmqyDBw.png" /></figure><h4>Customer Data with H3 Integration</h4><pre>-- Create customer locations with H3 indexing (combining new geometry types with H3)<br>CREATE OR REPLACE TABLE IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;customers&#39;) AS<br>SELECT<br> customer_id,<br> customer_name,<br> ST_Point(longitude, latitude, 4326) as location,<br> h3_pointash3(ST_AsText(location), 7) as h3_cell_level7,<br> city,<br> state,<br> last_order_date,<br> total_lifetime_value<br>FROM VALUES<br> (1001, &#39;Alice Johnson&#39;, -122.3350, 47.6062, &#39;Seattle&#39;, &#39;WA&#39;, &#39;2025-01-15&#39;, 1250.00),<br> (1002, &#39;Bob Smith&#39;, -122.3400, 47.6080, &#39;Seattle&#39;, &#39;WA&#39;, &#39;2025-01-20&#39;, 890.50),<br> (1003, &#39;Carol Davis&#39;, -122.4080, 37.7875, &#39;San Francisco&#39;, &#39;CA&#39;, &#39;2025-01-18&#39;, 2100.75),<br> (1004, &#39;David Wilson&#39;, -122.4200, 37.7600, &#39;San Francisco&#39;, &#39;CA&#39;, &#39;2025-01-22&#39;, 1650.25),<br> (1005, &#39;Eva Brown&#39;, -118.4010, 34.0740, &#39;Los Angeles&#39;, &#39;CA&#39;, &#39;2025-01-17&#39;, 1890.00),<br> (1006, &#39;Frank Miller&#39;, -118.4920, 34.0200, &#39;Los Angeles&#39;, &#39;CA&#39;, &#39;2025-01-25&#39;, 980.30),<br> (1007, &#39;Grace Lee&#39;, -73.9860, 40.7585, &#39;New York&#39;, &#39;NY&#39;, &#39;2025-01-19&#39;, 2850.90),<br> (1008, &#39;Henry Garcia&#39;, -73.9960, 40.6960, &#39;New York&#39;, &#39;NY&#39;, &#39;2025-01-21&#39;, 1320.60),<br> (1009, &#39;Iris Martinez&#39;, -87.6250, 41.8785, &#39;Chicago&#39;, &#39;IL&#39;, &#39;2025-01-16&#39;, 1450.80),<br> (1010, &#39;Jack Rodriguez&#39;, -87.6380, 41.9220, &#39;Chicago&#39;, &#39;IL&#39;, &#39;2025-01-23&#39;, 1750.40)<br>AS t(customer_id, customer_name, longitude, latitude, city, state, last_order_date, total_lifetime_value);</pre><pre>-- Verify customer data with H3 cells<br>SELECT<br> customer_name,<br> city,<br> ST_AsText(location) as point_wkt,<br> h3_cell_level7<br>FROM IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;customers&#39;)<br>LIMIT 5;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/555/1*53oV38MqqeEhXUzNSxHFgg.png" /></figure><h4>Examples of Geospatial Analytics on Retail Samples</h4><p>Spatial Relationships and filtering</p><p>In this section, we are finding all customers within the delivery zones. We can use ST_DistanceSpheroid on SRID 4326 points to determine the distance in meters between two points on the Earth. As a performance trade-off, ST_DistanceSphere can be used instead at the cost of some accuracy.</p><pre>-- Find all customers within delivery zones using ST_Within<br>SELECT<br> c.customer_name,<br> c.city,<br> dz.zone_name,<br> dz.delivery_fee,<br> -- ST_DistanceSperoid will give us meters to the zone centre<br> ST_DistanceSpheroid(c.location, ST_Centroid(dz.zone_boundary)) as distance_to_zone_center<br>FROM IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;customers&#39;) c<br>JOIN IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;delivery_zones&#39;) dz ON dz.city = c.city<br>WHERE ST_Within(c.location, dz.zone_boundary)<br>ORDER BY c.city, distance_to_zone_center;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/632/1*Cr-CD0ZLjb1tMlpHE1L83w.png" /></figure><p>Distance and Area calculations</p><p>Here, we are using a different method to obtain our measurement in meters. By leveraging the <a href="https://www.google.com/url?q=https://docs.databricks.com/aws/en/sql/language-manual/functions/st_transform&amp;sa=D&amp;source=editors&amp;ust=1759428328029094&amp;usg=AOvVaw005pmbElTb3gdADjAitluy">ST_Transform</a> function to project our <a href="https://www.google.com/url?q=https://epsg.io/4326&amp;sa=D&amp;source=editors&amp;ust=1759428328029263&amp;usg=AOvVaw2a86RYO3eoPOJ8ga5xto1z">SRID 4326</a> coordinates to <a href="https://www.google.com/url?q=https://epsg.io/5070&amp;sa=D&amp;source=editors&amp;ust=1759428328029357&amp;usg=AOvVaw3LRdoNj_BiUD8OcFT6mXgQ">SRID 507,0</a>, which is measured in meters.</p><p>Because we are using SRID 5070 here, we can leverage <a href="https://www.google.com/url?q=https://docs.databricks.com/aws/en/sql/language-manual/functions/st_distance&amp;sa=D&amp;source=editors&amp;ust=1759428328029698&amp;usg=AOvVaw16SKG28e4KcQwlKnG-iAm4">ST_Distance</a> to directly calculate the distance in the native units (meters) instead of calling <a href="https://www.google.com/url?q=https://docs.databricks.com/aws/en/sql/language-manual/functions/st_distancesphere&amp;sa=D&amp;source=editors&amp;ust=1759428328029974&amp;usg=AOvVaw3RJ35OocRCXA4UOTWlvxMB">ST_DistanceSphere</a> or <a href="https://www.google.com/url?q=https://docs.databricks.com/aws/en/sql/language-manual/functions/st_distancespheroid&amp;sa=D&amp;source=editors&amp;ust=1759428328030124&amp;usg=AOvVaw0Kj8bMWd2rzjk_AnkQK7zw">ST_DistanceSpheroid</a>. For more information on the differences between the distance functions, refer to the <a href="https://www.google.com/url?q=https://www.databricks.com/blog/introducing-spatial-sql-databricks-80-functions-high-performance-geospatial-analytics%23:~:text%3D4.%2520Measuring%2520distances%2520between%2520locations&amp;sa=D&amp;source=editors&amp;ust=1759428328030390&amp;usg=AOvVaw2lNMI5fLV78Hs8rlr0PQGm">official launch post for Spatial SQL.</a></p><pre><br>-- Calculate area of delivery zones and find nearest store<br>SELECT<br> dz.zone_name,<br> dz.city,<br> ROUND(ST_Area(ST_Transform(dz.zone_boundary, 5070)), 2) as area_sq_meters,<br> rs.store_name as nearest_store,<br> -- We can also project to the SRID that is measured in meters<br> ROUND(ST_Distance(ST_Centroid(ST_Transform(dz.zone_boundary, 5070)), ST_Transform(rs.location, 5070)), 2) as distance_to_store_meters<br>FROM IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;delivery_zones&#39;) dz<br>CROSS JOIN IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;retail_stores&#39;) rs<br>WHERE rs.city = dz.city<br>QUALIFY ROW_NUMBER() OVER (PARTITION BY dz.zone_id ORDER BY ST_Distance(ST_Centroid(dz.zone_boundary), rs.location)) = 1<br>ORDER BY dz.city;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/630/1*WCAGmrk_DvUYuPIBJQkG_A.png" /></figure><h4>Buffer analysis for market expansion</h4><pre>-- Create 5km buffer zones around flagship stores to identify market opportunities<br>WITH flagship_buffers AS (<br> SELECT<br>   store_id,<br>   store_name,<br>   city,<br>   location,<br>   ST_Buffer(location, 0.045) as buffer_5km -- Approximately 5km at these latitudes<br> FROM IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;retail_stores&#39;)<br> WHERE store_type = &#39;flagship&#39;<br>),<br>potential_customers AS (<br> SELECT<br>   fb.store_name,<br>   fb.city,<br>   COUNT(c.customer_id) as customers_in_buffer,<br>   ROUND(SUM(c.total_lifetime_value), 2) as total_market_value,<br>   ROUND(AVG(c.total_lifetime_value), 2) as avg_customer_value<br> FROM flagship_buffers fb<br> LEFT JOIN IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;customers&#39;) c ON ST_Within(c.location, fb.buffer_5km)<br> GROUP BY fb.store_id, fb.store_name, fb.city<br>)<br>SELECT * FROM potential_customers<br>ORDER BY total_market_value DESC;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AvjJKslBZKK1rF2wVYvFqA.png" /></figure><p>Density Analysis w/ H3 Aggregations</p><p>Let&#39;s use our sample dataset to visualise the density of our delivery zones. By finding all customers within a given hex boundary, we can see where our customers are clustered and where to focus our services.</p><pre>-- Analyze customer density using H3 hexagonal grid<br>SELECT<br> h3_cell_level7,<br> h3_boundaryaswkt(h3_cell_level7) as hex_boundary,<br> COUNT(customer_id) as customer_count,<br> ROUND(SUM(total_lifetime_value), 2) as total_value,<br> ROUND(AVG(total_lifetime_value), 2) as avg_value_per_customer<br>FROM IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;customers&#39;)<br>GROUP BY h3_cell_level7<br>HAVING customer_count &gt;= 1 <br>ORDER BY total_value DESC;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*X0fNQQ9qiFm-P338CaweSw.png" /></figure><h4>Performance with Spatial Data</h4><p>Especially in these early days of the features, passing hints into the queries can significantly speed up our geospatial processing. If, for example, we know that one side of our join is small enough to fit in memory, then leveraging the existing broadcast hints can provide big boosts in performance!</p><pre>-- Optimized: Broadcast the smaller delivery_zones table<br>SELECT /*+ BROADCAST(dz) */<br> c.customer_name,<br> dz.zone_name,<br> ROUND(ST_DistanceSpheroid(c.location, ST_Centroid(dz.zone_boundary)), 2) as distance_meters<br>FROM IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;customers&#39;) c<br>JOIN IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;delivery_zones&#39;) dz ON ST_Within(c.location, dz.zone_boundary);</pre><h4>Delta Lake optimisations</h4><p>Let&#39;s optimize the current tables and then run a complex query on the data.</p><pre>-- Optimize tables for better query performance<br>ALTER TABLE customers_indexed CLUSTER BY (h3_level8);<br><br>-- Add table properties for better performance<br>ALTER TABLE customers_indexed SET TBLPROPERTIES (<br> &#39;delta.autoOptimize.optimizeWrite&#39; = &#39;true&#39;,<br> &#39;delta.autoOptimize.autoCompact&#39; = &#39;true&#39;<br>);</pre><h4>Store Delivery Optimisation in Retail</h4><pre>-- Find optimal delivery dispatch<br>WITH customer_store_distances AS (<br> SELECT<br>   c.customer_id,<br>   c.customer_name,<br>   c.location as customer_location,<br>   rs.store_id,<br>   rs.store_name,<br>   rs.location as store_location,<br>   ST_DistanceSpheroid(c.location, rs.location) as distance,<br>   ROW_NUMBER() OVER (PARTITION BY c.customer_id ORDER BY ST_DistanceSpheroid(c.location, rs.location)) as store_rank<br> FROM IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;customers&#39;) c<br> CROSS JOIN IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;retail_stores&#39;) rs<br> WHERE rs.city = c.city  -- Only consider stores in the same city<br>),<br>optimal_routes AS (<br> SELECT<br>   customer_id,<br>   customer_name,<br>   store_id,<br>   store_name,<br>   distance as distance_meters<br> FROM customer_store_distances<br> WHERE store_rank = 1  -- Closest store<br>)<br>SELECT<br> store_name,<br> COUNT(customer_id) as assigned_customers,<br> ROUND(AVG(distance_meters), 2) as avg_distance_meters,<br> ROUND(SUM(distance_meters), 2) as total_route_distance<br>FROM optimal_routes<br>GROUP BY store_id, store_name<br>ORDER BY assigned_customers DESC;</pre><h4>Best Practices Summary</h4><ul><li>Leverage native geometry and geography types for type safety and performance</li><li>Broadcast small tables on large spatial joins (for now)</li><li>Combine ST functions and H3 approximations to efficiently operate on spatial data at scale</li><li>Leverage Databricks&#39; in-built optimisations, such as liquid clustering</li></ul><p>Finally, let&#39;s finish up with a comprehensive analysis of the Retail Supply chain coverage in our sample data:</p><pre>-- Combine all components to determine where to most effectively invest to improve our service<br>WITH delivery_metrics AS (<br> SELECT<br>   rs.store_id,<br>   rs.store_name,<br>   rs.city,<br>   COUNT(DISTINCT c.customer_id) as total_customers,<br>   ROUND(AVG(ST_DistanceSpheroid(rs.location, c.location)), 2) as avg_delivery_distance,<br>   ROUND(SUM(c.total_lifetime_value), 2) as total_market_value,<br>   -- Calculate service area using envelope around customers<br>   ST_Envelope(ST_Union_Agg(c.location)) as service_area<br> FROM IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;retail_stores&#39;) rs<br> LEFT JOIN IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;customers&#39;) c ON c.city = rs.city<br> GROUP BY rs.store_id, rs.store_name, rs.city<br>),<br>zone_coverage AS (<br> SELECT<br>   dm.*,<br>   dz.zone_name,<br>   dz.max_delivery_time_minutes,<br>   ROUND(<br>     COALESCE(<br>       ST_Area(ST_Intersection(dm.service_area, dz.zone_boundary)) /<br>       NULLIF(ST_Area(dz.zone_boundary), 0),<br>       0<br>     ), 3<br>   ) as zone_coverage_ratio<br> FROM delivery_metrics dm<br> LEFT JOIN IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;delivery_zones&#39;) dz ON dz.city = dm.city<br>),<br>efficiency_metrics AS (<br> SELECT<br>   *,<br>   ROUND(total_market_value / NULLIF(avg_delivery_distance, 0), 2) as value_per_km,<br>   CASE<br>     WHEN zone_coverage_ratio &gt; 0.8 THEN &#39;excellent&#39;<br>     WHEN zone_coverage_ratio &gt; 0.6 THEN &#39;good&#39;<br>     WHEN zone_coverage_ratio &gt; 0.3 THEN &#39;fair&#39;<br>     WHEN zone_coverage_ratio &gt; 0.15 THEN &#39;partial&#39;<br>     ELSE &#39;needs_improvement&#39;<br>   END as coverage_rating<br> FROM zone_coverage<br>)<br>SELECT<br> store_name,<br> city,<br> total_customers,<br> avg_delivery_distance as avg_delivery_distance_m,<br> total_market_value,<br> value_per_km as efficiency_ratio,<br> ROUND(zone_coverage_ratio * 100, 1) as coverage_percentage,<br> coverage_rating<br>FROM efficiency_metrics<br>ORDER BY efficiency_ratio DESC;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8lg_l9dEpFxENfHUZw_sLA.png" /></figure><h4>Conclusion</h4><p>Hopefully, this has got you excited to start leveraging Spatial SQL for your Geospatial workloads on Databricks!</p><p>For further reading, take a look at the <a href="https://www.google.com/url?q=https://www.databricks.com/blog/introducing-spatial-sql-databricks-80-functions-high-performance-geospatial-analytics&amp;sa=D&amp;source=editors&amp;ust=1759428328079281&amp;usg=AOvVaw33StbtiowIciujGJoXdQB-">launch post for Spatial SQL</a>. You can find all the code used in this post in the accompanying notebook here. Additional sample code and notebooks are also available in the Databricks Documentation.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0d0618016276" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dbsql-sme-engineering/unlocking-geospatial-analysis-with-databricks-sql-0d0618016276">Unlocking Geospatial Analysis with Databricks SQL</a> was originally published in <a href="https://medium.com/dbsql-sme-engineering">DBSQL SME Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Driving Business Insights with Recursive CTEs in DBSQL]]></title>
            <link>https://medium.com/dbsql-sme-engineering/driving-business-insights-with-recursive-ctes-in-dbsql-00ad222fa0be?source=rss-205045f50ad7------2</link>
            <guid isPermaLink="false">https://medium.com/p/00ad222fa0be</guid>
            <category><![CDATA[databricks-sql]]></category>
            <category><![CDATA[sql]]></category>
            <category><![CDATA[recursive-cte]]></category>
            <category><![CDATA[databricks]]></category>
            <category><![CDATA[analytics]]></category>
            <dc:creator><![CDATA[Databricks SQL SME]]></dc:creator>
            <pubDate>Thu, 02 Oct 2025 15:42:30 GMT</pubDate>
            <atom:updated>2025-10-02T15:42:30.940Z</atom:updated>
            <content:encoded><![CDATA[<h4>Unlocking complex decision logic, hierarchies, and planning using Recursive CTEs in Databricks SQL</h4><p>Author: <a href="https://www.linkedin.com/in/abhishek-d-4679b6b/">Abhishek Dey</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*E9E5zDiJTn4weOOG.png" /></figure><p><strong>Introduction</strong></p><p>Recursive Common Table Expressions (CTEs) are a powerful yet underutilised feature of SQL that allows queries to <strong>repeatedly reference their own results</strong>. In Databricks SQL (DBSQL), Recursive CTEs provide scalable and elegant solutions to problems involving <strong>hierarchies</strong>, <strong>paths</strong>, and <strong>dependencies,</strong> eliminating the need for procedural code or custom loops.</p><p><strong>How Recursive CTEs Work</strong></p><p>Recursive CTEs are a form of self-referencing SQL that builds output over successive evaluations. A recursive CTE has two components:</p><ol><li><strong>Anchor query</strong> — seeds the recursion with base results</li><li><strong>Recursive query</strong> — joins back on its own output to expand the result set</li><li><strong>Termination</strong> — stops when no new rows are returned</li></ol><p>In Databricks SQL:</p><ul><li>The WITH RECURSIVE keyword is mandatory</li><li>Recursive references must be at the top level of the statement</li><li>Recursion depth can be implicitly controlled using filters or guards</li><li>It is highly performant</li><li>Recursive level is configurable using MAX RECURSION LEVEL</li><li>Up to 1M rows in the resultset, which can be overruled with a LIMIT in the driving query</li></ul><p>For more information, please refer to our <a href="https://www.google.com/url?q=https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-syntax-qry-select-cte&amp;sa=D&amp;source=editors&amp;ust=1759361543515392&amp;usg=AOvVaw2YFTuktHmbiZUChUa58dMJ">documentation</a>.</p><p><strong>What did users do before Recursive CTEs?</strong></p><p>Before Recursive CTEs, teams often had to:</p><ul><li><strong>Write PySpark or Python code</strong> to simulate hierarchy or graph traversals</li><li><strong>Manage recursion manually</strong> using loops, state tracking, and temp views</li><li><strong>Break recursion into multiple flat joins</strong> or procedural SQL blocks</li><li><strong>Use third-party graph engines or orchestration tools</strong> outside SQL</li><li><strong>Accept delays or stale results</strong> because real-time graph logic wasn’t SQL-native</li></ul><p>These approaches were:</p><ul><li>Heavy for data engineers</li><li>Too complex for most SQL analysts</li><li>Slower to build, debug, and maintain</li></ul><p><strong>What do Recursive CTEs unlock?</strong></p><p>With this feature, users can now:</p><ul><li><strong>Express deeply structured problems declaratively</strong> — in pure SQL</li><li><strong>Eliminate Python boilerplate</strong> and flattening logic</li><li><strong>Traverse trees, DAGs, and dependency chains</strong> directly in the query engine</li><li><strong>Model stateful or multi-hop systems</strong> (e.g., org charts, workflows, routes)</li><li>Keep business logic in one place — fast, explainable, governed</li></ul><p>Now, let’s look at some real-life use cases and how Recursive CTEs in Databricks SQL drive business insights.</p><h3>Use Case 1: Hierarchies and Business Rollups</h3><p><strong>Scenario:<br></strong>In large enterprises, departments are often organized hierarchically (e.g., Corporate → Engineering → ML → AI Lab). Business users often need to calculate <strong>total salary spend</strong>, <strong>headcount</strong>, or <strong>budget roll-ups</strong> across this hierarchy.</p><p><strong>What Recursive CTE Solves:<br></strong>Recursive CTEs help you <strong>traverse the organizational tree</strong>, identify all descendant departments under a given root, and <strong>aggregate values like salary or cost</strong> across those levels.</p><pre>-- Drop &amp; create department hierarchy<br>DROP TABLE IF EXISTS dbx_departments;<br><br><br>CREATE TABLE IF NOT EXISTS dbx_departments (<br> dept_id STRING, parent_dept_id STRING, dept_name STRING<br>);<br><br><br>INSERT INTO dbx_departments<br> VALUES<br>   (&#39;D1&#39;, NULL, &#39;Corporate&#39;),<br>   (&#39;D2&#39;, &#39;D1&#39;, &#39;Finance&#39;),<br>   (&#39;D3&#39;, &#39;D1&#39;, &#39;Engineering&#39;),<br>   (&#39;D4&#39;, &#39;D3&#39;, &#39;Platform&#39;),<br>   (&#39;D5&#39;, &#39;D3&#39;, &#39;ML&#39;),<br>   (&#39;D6&#39;, &#39;D5&#39;, &#39;AI Lab&#39;);<br><br><br>-- Drop &amp; create employees table<br>DROP TABLE IF EXISTS dbx_employees;<br><br><br>CREATE TABLE IF NOT EXISTS dbx_employees (<br> emp_id STRING, emp_name STRING, dept_id STRING, salary INT<br>);<br><br><br>INSERT INTO dbx_employees<br> VALUES<br>   (&#39;E1&#39;, &#39;Alice&#39;, &#39;D2&#39;, 120000),<br>   (&#39;E2&#39;, &#39;Bob&#39;, &#39;D3&#39;, 150000),<br>   (&#39;E3&#39;, &#39;Charlie&#39;, &#39;D4&#39;, 140000),<br>   (&#39;E4&#39;, &#39;David&#39;, &#39;D5&#39;, 135000),<br>   (&#39;E5&#39;, &#39;Eva&#39;, &#39;D6&#39;, 180000),<br>   (&#39;E6&#39;, &#39;Frank&#39;, &#39;D6&#39;, 170000);<br><br><br>-- Recursive CTE to expand department hierarchy up to 5 levels<br>WITH RECURSIVE dept_tree (<br>  root_dept,<br>  dept_id,<br>  path,<br>  level<br>)<br>MAX RECURSION LEVEL 5 AS (<br><br><br>  -- Anchor: start with each department as a root<br>  SELECT<br>    dept_id AS root_dept,<br>    dept_id,<br>    dept_name AS path,<br>    1 AS level<br>  FROM dbx_departments<br><br><br>  UNION ALL<br><br><br>  -- Recursive: find child departments and build the path<br>  SELECT<br>    dt.root_dept,<br>    d.dept_id,<br>    CONCAT(dt.path, &#39; &gt; &#39;, d.dept_name),<br>    dt.level + 1<br>  FROM dbx_departments d<br>  JOIN dept_tree dt ON d.parent_dept_id = dt.dept_id<br>)<br><br><br>-- Aggregate total salary for each root department and show the department path<br>SELECT<br>  root_dept,<br>  MAX(path) AS dept_path,<br>  SUM(e.salary) AS total_salary<br>FROM dept_tree dt<br>LEFT JOIN dbx_employees e ON dt.dept_id = e.dept_id<br>GROUP BY root_dept<br>ORDER BY root_dept, total_salary DESC;</pre><p><strong>Output:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/490/1*jYMDTt5ZicT9_rIu_3jMtQ.png" /></figure><h3>How It Works Technically</h3><ul><li><strong>Start with Every Department as a Root<br></strong> The anchor query initializes each department as its own root with level <strong>1</strong> and a starting <strong>path</strong>.</li><li><strong>Recursively Traverse the Hierarchy<br></strong> The recursive part joins child departments to their parents, building a full <strong>path</strong> and incrementing the <strong>level</strong>.</li><li><strong>Limit Depth to 5 Levels<br></strong> The <strong>MAX RECURSION LEVEL 5 </strong>directive ensures the hierarchy expansion stops after 5 levels, preventing runaway recursion.</li><li><strong>Join Employees and Roll Up Salaries<br></strong> For each department reached in the tree, the salaries of employees are joined and grouped by the original root department.</li><li><strong>Aggregate and Present Paths and Totals<br></strong> The final output shows the root department, the full department path, and the total salary cost under each subtree.</li></ul><p><strong>Business Outcomes:</strong></p><ul><li>Enables LOB-level financial planning</li><li>Simplifies compliance rollups and cost centre analytics</li><li>Powers org-wide KPI dashboards without needing ETL restructuring</li></ul><h3>Use Case 2: Valid Flight Itineraries (Multi-Hop Path Planning)</h3><p><strong>Scenario:<br></strong>In global travel and logistics networks, customers or systems need to find <strong>all valid flight itineraries</strong> between two locations, considering multiple stops, avoiding loops, and optionally factoring in timing or layover constraints. For example, discovering paths like <strong>JFK → CDG → SIN → SYD </strong>without revisiting the same city.</p><p><strong>What Recursive CTE Solves:<br></strong> Recursive CTEs simulate <strong>multi-hop traversal across a flight graph</strong>, where each node is an airport and edges are valid flights. They allow SQL to:</p><ul><li>Build <strong>paths of arbitrary length</strong> from a given origin</li><li>Accumulate total travel time or distance</li><li>Track visited airports to <strong>avoid cycles</strong></li><li>Support constraints such as <strong>layover time windows</strong>, <strong>flight directionality</strong>, or <strong>airline preferences</strong></li></ul><p><strong>How it works technically:<br></strong> The anchor query selects direct flights from the origin. The recursive part then <strong>joins flights</strong> from the current destination, expanding the path and preventing cycles using ARRAY logic.</p><pre>-- DROP and create base flights table<br>DROP TABLE IF EXISTS flights;<br><br><br>CREATE TABLE flights (flight_id STRING, origin STRING, destination STRING, duration INT);<br><br><br>-- Sample data with multiple hops<br>INSERT INTO flights<br> VALUES<br>   (&#39;F1&#39;, &#39;JFK&#39;, &#39;CDG&#39;, 7),<br>   (&#39;F2&#39;, &#39;CDG&#39;, &#39;SIN&#39;, 13),<br>   (&#39;F3&#39;, &#39;SIN&#39;, &#39;SYD&#39;, 8),<br>   (&#39;F4&#39;, &#39;JFK&#39;, &#39;LHR&#39;, 6),<br>   (&#39;F5&#39;, &#39;LHR&#39;, &#39;SYD&#39;, 22),<br>   (&#39;F6&#39;, &#39;CDG&#39;, &#39;SYD&#39;, 18),<br>   (&#39;F7&#39;, &#39;JFK&#39;, &#39;DXB&#39;, 12),<br>   (&#39;F8&#39;, &#39;DXB&#39;, &#39;SIN&#39;, 9),<br>   (&#39;F9&#39;, &#39;SIN&#39;, &#39;JFK&#39;, 20);<br><br><br>-- cycle potential<br>-- Recursive CTE to find all valid itineraries from JFK to SYD<br>WITH RECURSIVE flight_paths (<br> origin,<br> destination,<br> path,<br> total_time,<br> hops,<br> cycle<br>) AS (<br> -- Anchor: start from JFK<br> SELECT<br>   origin,<br>   destination,<br>   ARRAY(origin, destination),<br>   duration,<br>   1,<br>   false<br> FROM<br>   flights<br> WHERE<br>   origin = &#39;JFK&#39;<br> UNION ALL<br>   -- Recursive: expand next legs<br> SELECT<br>   fp.origin,<br>   f.destination,<br>   array_union(fp.path, ARRAY(f.destination)),<br>   fp.total_time + f.duration,<br>   fp.hops + 1,<br>   array_contains(fp.path, f.destination)<br> FROM<br>   flight_paths fp<br>   JOIN flights f ON fp.destination = f.origin<br> WHERE<br>   NOT fp.cycle<br>   AND fp.hops &lt; 5<br>) -- Final result: valid acyclic paths to SYD<br>SELECT<br> path,<br> total_time,<br> hops<br>FROM<br> flight_paths<br>WHERE<br> destination = &#39;SYD&#39;<br> AND NOT cycle<br>ORDER BY<br> total_time ASC;</pre><p><strong>Output:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/635/1*ksF-MN_Z2oS5r_5Dae3F-w.png" /></figure><p><strong>Business Outcomes:</strong></p><ul><li>Empowers travel engines and OTAs to provide <strong>flexible, rule-based itineraries</strong></li><li>Supports <strong>route optimization</strong> for cargo and logistics routing systems</li><li>Enables <strong>what-if analysis</strong> in network design</li><li>Builds the foundation for pricing engines, availability queries, or travel planning UX tools</li></ul><p>This use case showcases the effectiveness of SQL recursion as a <strong>graph traversal tool</strong>, enabling the solution of use cases that were previously addressed in code or graph databases.</p><p><strong>Use Case 3: Satellite Missions Orbital Planner</strong></p><p><strong>Scenario:</strong></p><p>In space logistics, management of concurrent satellite missions, each with different objectives — communications, reconnaissance, and scientific observation is needed. These missions must navigate a congested orbital environment with increasing risk from space debris and limited propulsion capacity.</p><p>Each spacecraft:</p><ul><li>Has a specific payload type, which limits allowed orbital transitions.</li><li>Operates under a strict delta-v (Δv) budget, constraining how much it can maneuver.</li><li>Must avoid blocked orbits, especially those recently impacted by debris-generating events.</li><li>Can only initiate transfers during valid orbital windows.</li><li>Needs to find a safe, legal, and fuel-efficient path to its target orbit (e.g., from Low Earth Orbit(LEO) to Geostationary Earth Orbit(GEO) ).</li></ul><p><strong>What Recursive CTE Solves</strong></p><p>They enable SQL-based exploration of all valid orbital paths from <strong>LEO</strong> to <strong>GEO</strong>, respecting mission constraints like fuel (<strong>delta-v</strong>), debris avoidance, and payload routing. Recursive CTEs solve the challenge of pathfinding across a dynamic graph while enforcing complex physical and operational rules — all inside SQL.</p><p><strong>How it works technically:</strong></p><p>A Recursive CTE starts with a base case (starting from <strong>LEO</strong>) and expands paths one hop at a time by joining with allowed transitions. It accumulates metrics such as total delta-v, tracks visited orbits to prevent cycles, and filters out paths that violate constraints, including blocked orbits or fuel limits. This recursion continues until no new valid paths can be found.</p><pre>-- Step 1: Define transition graph<br>DROP TABLE IF EXISTS orbit_transfers;<br>CREATE TABLE orbit_transfers (<br> from_band STRING,<br> to_band STRING,<br> delta_v_kms DOUBLE<br>);<br><br><br>INSERT INTO orbit_transfers VALUES<br> (&#39;LEO&#39;, &#39;MEO&#39;, 2.5),<br> (&#39;MEO&#39;, &#39;GTO&#39;, 3.0),<br> (&#39;GTO&#39;, &#39;GEO&#39;, 1.5),<br> (&#39;LEO&#39;, &#39;GTO&#39;, 5.0),<br> (&#39;LEO&#39;, &#39;SSO&#39;, 2.2),<br> (&#39;SSO&#39;, &#39;GTO&#39;, 4.5);<br><br><br>-- Step 2: Flag debris-heavy zones<br>DROP TABLE IF EXISTS debris_zones;<br>CREATE TABLE debris_zones (<br> band STRING,<br> is_blocked BOOLEAN<br>);<br><br><br>INSERT INTO debris_zones VALUES<br> (&#39;LEO&#39;, false),<br> (&#39;MEO&#39;, true),   -- High collision risk<br> (&#39;GTO&#39;, false),<br> (&#39;GEO&#39;, false),<br> (&#39;SSO&#39;, false);<br><br><br>-- Step 3: Recursive CTE simulating valid orbital paths<br>WITH RECURSIVE orbit_path (<br> current_band,<br> path,<br> total_delta_v,<br> visited,<br> cycle,<br> blocked<br>) AS (<br> -- Anchor: start from LEO<br> SELECT<br>   from_band,<br>   ARRAY(from_band, to_band),<br>   delta_v_kms,<br>   ARRAY(from_band, to_band),<br>   false,<br>   dz.is_blocked<br> FROM orbit_transfers ot<br> JOIN debris_zones dz ON ot.to_band = dz.band<br> WHERE from_band = &#39;LEO&#39; AND dz.is_blocked = false<br><br><br> UNION ALL<br><br><br> -- Recursively expand paths<br> SELECT<br>   ot.to_band,<br>   array_union(op.path, ARRAY(ot.to_band)),<br>   op.total_delta_v + ot.delta_v_kms,<br>   array_union(op.visited, ARRAY(ot.to_band)),<br>   array_contains(op.visited, ot.to_band),<br>   dz.is_blocked<br> FROM orbit_path op<br> JOIN orbit_transfers ot ON ot.from_band = op.current_band<br> JOIN debris_zones dz ON ot.to_band = dz.band<br> WHERE NOT op.cycle<br>   AND dz.is_blocked = false<br>   AND op.total_delta_v + ot.delta_v_kms &lt;= 9.5<br>)<br><br><br>-- Step 4: Return optimal paths to GEO<br>SELECT<br> path,<br> total_delta_v<br>FROM orbit_path<br>WHERE current_band = &#39;GEO&#39; AND NOT cycle<br>ORDER BY total_delta_v ASC;</pre><p><strong>Output :</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/635/1*TFfgR5vZhhFCG8hLYxfNEQ.png" /></figure><p><strong>Business Outcomes:</strong></p><ul><li>Autonomous Routing: Recursive CTEs in DBSQL simulate all valid orbital transfer paths in SQL, eliminating the need for external graph algorithms.</li><li>Real-Time Adaptability: They dynamically avoid debris zones and respect mission timing, enabling safer and more adaptive planning.</li><li>Faster Planning Cycles: Mission plans that once took hours via scripting are now computed in seconds using SQL alone.</li><li>Cost Optimization: By minimizing fuel (delta-v) and avoiding delays, Recursive CTEs reduce operational costs per mission.</li><li>Auditability and Compliance: All logic is versioned, governed, and integrated with enterprise controls, ensuring traceable, compliant planning.</li></ul><p>Recursive CTEs empower aerospace and satellite teams to build safer, faster, and more cost-effective missions — entirely in Databricks SQL.</p><p><strong>Best Practices When Using Recursive CTEs in DBSQL</strong></p><p>- Define an Exit Condition <br> — Ensure recursion converges either by reaching a LIMIT, checking for visited states, usage of MAX RECURSION LEVEL &lt;INTEGER_VALUE&gt; or tracking a depth column <br> — Add a WHERE NOT cycle or WHERE depth &lt;= X to prevent infinite loops.</p><p>- Use Arrays or Sets to Track Paths <br> — Track visited nodes with ARRAY or MAP types to detect cycles <br> — array_contains(visited, next_node) is a common pattern to prevent infinite recursion</p><p>- Recursion Depth <br> — Use a max recursion level or depth column and cap it (depth &lt;= 10, etc.) to avoid runaway computation</p><p>- Filter Early <br> — Push constraints into the anchor and recursive parts to reduce the result set <br> — Avoid full-table joins within the recursion</p><p>- Use Liquid Clustering when possible <br> — If tables are large (e.g., hierarchies or graph edge lists), optimize with liquid clustering on join keys like parent_id, from_node, or dept_id</p><p><strong>Troubleshooting Tips</strong></p><p>- No Output <br> — If your recursive CTE returns no results, verify that your anchor query returns rows</p><p>- Infinite Recursion or Data Explosion <br> — Add a depth limit and cycle detection (via visited arrays or map_contains) to prevent runaway recursion</p><p>- Unexpected or Incomplete Paths <br> — Debug step-by-step using depth, path, and cardinality; validate that joins are expanding correctly.</p><p>- Cycle Detection Not Working <br> — Ensure correct use of array_contains() or map_contains() — especially when tracking STRUCTs or multiple keys.</p><p>- Recursion Stops Too Soon <br> — Watch for filters or logical errors (e.g., blocked paths, early cycle flags) that halt recursion prematurely.</p><ul><li>Column Reference Errors <br> — Always declare column names explicitly in recursive CTE headers</li></ul><p>Recursive CTEs bring the power of graph traversal, hierarchy modeling, and complex state simulation <strong>directly into your SQL environment</strong>. Whether you’re planning satellite missions, resolving org structures, or simulating dependencies, this feature empowers you to solve real-world challenges <strong>without leaving your data platform</strong>.</p><p>For <strong>data and platform product teams</strong>, Recursive CTEs using DBSQL are a game-changer:</p><ul><li><strong>Ship Faster</strong>: Eliminate external code (Python, orchestration logic) and build complex data logic natively in SQL. No round trips, no pipeline delays.</li><li><strong>Lower Platform Complexity</strong>: Fewer microservices, no need for custom DAGs or graph engines — everything lives in SQL, fully governed.</li><li><strong>Enable Real-Time Features</strong>: Power use cases like dependency resolution, lineage tracing, or route planning directly from live datasets.</li><li><strong>Scale Confidently</strong>: Built on Databricks SQL engine, Recursive CTEs scale to millions of rows without sacrificing performance.</li><li><strong>Keep Logic Close to Data</strong>: Business rules are expressed where the data lives, making it easier to maintain, test, and collaborate across data, product, and engineering teams.</li><li><strong>Migrate to Databricks SQL</strong>: Recursive CTEs have always been complex objects to migrate to Databricks With the support of recursive CTEs, it gets much easier to</li><li>Unify logic and compute on the lakehouse</li><li>Get fully managed recursion with explicit controls</li><li>Build real-time graph logic, dependency trees, hierarchies, and workflows — all declarative in Databricks SQL</li></ul><p>If you’re using <strong>Databricks SQL</strong>, now is the time to try out Recursive CTEs:</p><ul><li>Model your hierarchies and dependencies natively</li><li>Replace Python or graph engine workarounds with declarative SQL</li><li>Explore your data recursively — securely and at scale</li></ul><p><strong>Give it a try in your workspace</strong>, and see how much complexity you can eliminate by thinking recursively!</p><h4>Related Blogs</h4><ul><li><a href="https://www.google.com/url?q=https://www.databricks.com/product/databricks-sql&amp;sa=D&amp;source=editors&amp;ust=1759361543559091&amp;usg=AOvVaw1FTpB06jP5mGrR0CPxtj7g">https://www.databricks.com/product/databricks-sql</a></li><li><a href="https://www.google.com/url?q=https://www.databricks.com/blog/introducing-sql-scripting-support-databricks-part-1&amp;sa=D&amp;source=editors&amp;ust=1759361543559318&amp;usg=AOvVaw1elG4YAwwqVE8cUAnq7s-d">https://www.databricks.com/blog/introducing-sql-scripting-support-databricks-part-1</a></li><li><a href="https://www.google.com/url?q=https://www.databricks.com/blog/introducing-recursive-common-table-expressions-databricks&amp;sa=D&amp;source=editors&amp;ust=1759361543559509&amp;usg=AOvVaw3EMAcKX-J4ZeR4dbxg-tpA">https://www.databricks.com/blog/introducing-recursive-common-table-expressions-databricks</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=00ad222fa0be" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dbsql-sme-engineering/driving-business-insights-with-recursive-ctes-in-dbsql-00ad222fa0be">Driving Business Insights with Recursive CTEs in DBSQL</a> was originally published in <a href="https://medium.com/dbsql-sme-engineering">DBSQL SME Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Graph Analytics with DBSQL Stored Procedures and Agents]]></title>
            <link>https://medium.com/dbsql-sme-engineering/graph-analytics-with-dbsql-stored-procedures-and-agents-5e53bad68032?source=rss-205045f50ad7------2</link>
            <guid isPermaLink="false">https://medium.com/p/5e53bad68032</guid>
            <category><![CDATA[agents]]></category>
            <category><![CDATA[stored-procedure]]></category>
            <category><![CDATA[databricks]]></category>
            <category><![CDATA[databricks-sql]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Databricks SQL SME]]></dc:creator>
            <pubDate>Tue, 30 Sep 2025 17:29:05 GMT</pubDate>
            <atom:updated>2025-09-30T17:39:49.922Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6VmsLduRYUa47aiDvTwEeA.png" /></figure><p><strong>Author</strong>: <a href="https://medium.com/u/13bce6a63d8f">Cody Austin Davis</a>, Field Engineering Manager at Databricks</p><h3>Introduction</h3><p>In this article, we are going to cover a ton of ground. So much has happened in the Databricks SQL world this year, so we are going to walk through a single use case to demonstrate where analytics is heading on DBSQL in the era where AI is embedded in all future analytics projects. This use case will focus on creating and analyzing STIX Cyber Threat Intelligence data in a graph to analyze patterns and campaigns. This use case requires a lot, and we will leverage this example to showcase the following capabilities in Databricks SQL that are now available:</p><ul><li><strong>Recursive CTEs </strong>— Databricks SQL now supports recursive CTEs, which unlocks a TON of cool analytics use cases — the main one in which we will focus on today is recursive graph traversal on tables in Databricks.</li><li><strong>Stored Procedures </strong>— We can now create and use Stored Procedures in Databricks sql to run any arbitrary compound SQL workflows. This is really important for packaging up advanced SQL scripts (like our graph traversal logic) into reusable components that end users and agents can use without any heavy lifting.</li><li><strong>Agentic Analytics </strong>— Now that we can create functions and stored procedures in Unity Catalog, the next step is to use that function registry as a tool catalog for any agents that we build with them, this is critical for context and logical governance for agentic systems.</li></ul><p>Since this is a dense article, lets outline first what we will accomplish, and you can feel free to skip to any section if you want to zoom in on a particular areas:</p><ul><li><strong>Step 1</strong> — STIX data graph generation — generate some synthetic STIX data to analyze via a Graph data model.</li><li><strong>Step 2 </strong>— Write some logic to analyze the graph with <a href="https://www.databricks.com/blog/introducing-recursive-common-table-expressions-databricks">recursive CTEs</a> to do common analysis patterns like search and page rank. Create <a href="https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-syntax-ddl-create-sql-function">Functions</a> and <a href="https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-syntax-ddl-create-procedure">Stored Procedures</a> to analyze the graph with composable and reusable tools.</li><li><strong>Step 3</strong>— Tie these <a href="https://docs.databricks.com/aws/en/generative-ai/agent-framework/create-custom-tool#-wrap-the-function-using-the-ucfunctiontoolkit">functions into an agentic system</a> that can help end users analyze this graph without the need to code!</li></ul><p>Lets get started! <a href="https://github.com/CodyAustinDavis/dbsql_sme/tree/main/Demos%20and%20Tutorials/Advanced%20Notebooks/Graph%20Analytics%20Agents%20on%20DBSQL">The Git repo for this whole project from data generation, function definition, to building an agent prototype can be found here in the Git repo</a>.</p><p><strong><em>Disclaimer</em></strong>: This is a cyber focused use case to demonstrate new DBSQL features, but me (the author) is NOT a cyber security expert — so apologies if I get some domain expertise concepts wrong :).</p><h3>Step 1 — Create a STIX dataset to analyze</h3><p>We start with a python script to generate some node and edge data for a graph of STIX data of various malware, tools, infra, threat-actors, etc. The script can be re-run from anywhere, you can find the whole script <a href="https://github.com/CodyAustinDavis/dbsql_sme/blob/main/Demos%20and%20Tutorials/Advanced%20Notebooks/Graph%20Analytics%20Agents%20on%20DBSQL/Graph%20Data%20Generation%20Script.py">here in the Git repo</a>.</p><pre>import uuid<br>import random<br>import json<br>from datetime import datetime<br>from faker import Faker<br>from stix2 import (<br>    ThreatActor, Malware, Indicator, Relationship, ObservedData,<br>    Identity, Tool, Infrastructure, Report, CourseOfAction<br>)<br>from pyspark.sql import Row<br>from pyspark.sql.types import StructType, StructField, StringType, MapType<br><br>from pyspark.sql.functions import from_json, col, parse_json<br><br># === Configuration ===<br>NUM_THREAT_ACTORS = 100<br>NUM_MALWARE = 100<br>NUM_CAMPAIGNS = 10<br>NUM_OBSERVED = 1000<br>NUM_TOOLS = 10<br>NUM_INFRASTRUCTURE = 10<br>NUM_REPORTS = 10<br>NUM_COURSE_OF_ACTION = 5<br>NUM_IDENTITIES = 10000<br><br>fake = Faker()<br><br># === Reusable Core Entities ===<br>threat_actors = [<br>    ThreatActor(<br>        id=f&quot;threat-actor--{uuid.uuid4()}&quot;,<br>        name=fake.company(),<br>        description=fake.text(),<br>        roles=[&quot;intrusion-set&quot;],<br>        labels=[&quot;apt&quot;]<br>    ) for _ in range(NUM_THREAT_ACTORS)<br>]<br><br>malware_pool = [<br>    Malware(<br>        id=f&quot;malware--{uuid.uuid4()}&quot;,<br>        name=fake.word(),<br>        is_family=True,<br>        labels=[&quot;trojan&quot;, &quot;spyware&quot;]<br>    ) for _ in range(NUM_MALWARE)<br>]<br><br>tools = [<br>    Tool(<br>        id=f&quot;tool--{uuid.uuid4()}&quot;,<br>        name=fake.word(),<br>        labels=[&quot;remote-access&quot;, &quot;loader&quot;]<br>    ) for _ in range(NUM_TOOLS)<br>]<br><br>infrastructure_list = [<br>    Infrastructure(<br>        id=f&quot;infrastructure--{uuid.uuid4()}&quot;,<br>        name=fake.domain_name(),<br>        infrastructure_types=[&quot;command-and-control&quot;]<br>    ) for _ in range(NUM_INFRASTRUCTURE)<br>]<br><br>identities = [<br>    Identity(<br>        id=f&quot;identity--{uuid.uuid4()}&quot;,<br>        name=fake.company(),<br>        identity_class=&quot;organization&quot;,<br>        sectors=[&quot;technology&quot;, &quot;financial-services&quot;],<br>        contact_information=fake.email()<br>    ) for _ in range(NUM_IDENTITIES)<br>]<br><br># === Generate STIX Objects ===<br>stix_objects = []<br><br># Campaigns<br>for _ in range(NUM_CAMPAIGNS):<br>    ta = random.choice(threat_actors)<br>    malware_used = random.sample(malware_pool, k=random.randint(1, 3))<br>    indicators = [<br>        Indicator(<br>            id=f&quot;indicator--{uuid.uuid4()}&quot;,<br>            name=&quot;Malicious IP&quot;,<br>            pattern_type=&quot;stix&quot;,<br>            pattern=f&quot;[ipv4-addr:value = &#39;{fake.ipv4()}&#39;]&quot;<br>        ) for _ in range(random.randint(1, 2))<br>    ]<br>    relationships = []<br>    for mal in malware_used:<br>        relationships.append(Relationship(<br>            id=f&quot;relationship--{uuid.uuid4()}&quot;,<br>            relationship_type=&quot;uses&quot;,<br>            source_ref=ta.id,<br>            target_ref=mal.id,<br>        ))<br>        for ind in indicators:<br>            relationships.append(Relationship(<br>                id=f&quot;relationship--{uuid.uuid4()}&quot;,<br>                relationship_type=&quot;indicates&quot;,<br>                source_ref=ind.id,<br>                target_ref=mal.id,<br>            ))<br><br>    stix_objects.append(ta)<br>    stix_objects.extend(malware_used)<br>    stix_objects.extend(indicators)<br>    stix_objects.extend(relationships)<br><br># Observed Data (e.g. logs, detections)<br>for _ in range(NUM_OBSERVED):<br>    first = fake.date_time_this_year()<br>    last = fake.date_time_between_dates(datetime_start=first)<br><br>    observed = ObservedData(<br>        id=f&quot;observed-data--{uuid.uuid4()}&quot;,<br>        first_observed=first,<br>        last_observed=last,<br>        number_observed=random.randint(1, 10),<br>        objects={<br>            &quot;0&quot;: {&quot;type&quot;: &quot;ipv4-addr&quot;, &quot;value&quot;: fake.ipv4()},<br>            &quot;1&quot;: {&quot;type&quot;: &quot;domain-name&quot;, &quot;value&quot;: fake.domain_name()}<br>        }<br>    )<br>    stix_objects.append(observed)<br><br># Contextual entities<br>stix_objects.extend(tools)<br>stix_objects.extend(infrastructure_list)<br>stix_objects.extend(identities)<br><br># Reports<br>for _ in range(NUM_REPORTS):<br>    published = fake.date_time_this_year()<br>    obj_refs = random.sample([o.id for o in stix_objects if hasattr(o, &quot;id&quot;)], k=3)<br>    report = Report(<br>        id=f&quot;report--{uuid.uuid4()}&quot;,<br>        name=fake.sentence(),<br>        published=fake.date_time_this_year(),<br>        object_refs=obj_refs<br>    )<br>    stix_objects.append(report)<br><br># Courses of Action<br>for _ in range(NUM_COURSE_OF_ACTION):<br>    coa = CourseOfAction(<br>        id=f&quot;course-of-action--{uuid.uuid4()}&quot;,<br>        name=&quot;Apply security patch&quot;,<br>        description=fake.sentence()<br>    )<br>    stix_objects.append(coa)<br><br># === Convert to Spark Rows ===<br>def to_row(obj):<br>    parsed = json.loads(obj.serialize())<br>    return Row(<br>        stix_id=parsed.get(&quot;id&quot;),<br>        stix_type=parsed.get(&quot;type&quot;),<br>        name=parsed.get(&quot;name&quot;, None),<br>        description=parsed.get(&quot;description&quot;, None),<br>        raw=json.dumps(parsed)  # No extra whitespace, no escape issues<br>    )<br><br>rows = [to_row(o) for o in stix_objects]<br><br># === Define schema with raw as string ===<br>schema = StructType([<br>    StructField(&quot;stix_id&quot;, StringType(), True),<br>    StructField(&quot;stix_type&quot;, StringType(), True),<br>    StructField(&quot;name&quot;, StringType(), True),<br>    StructField(&quot;description&quot;, StringType(), True),<br>    StructField(&quot;raw&quot;, StringType(), True)<br>])<br><br># === Create Spark DataFrame and Parse JSON ===<br>df = spark.createDataFrame(rows, schema=schema)<br>df = df.withColumn(&quot;raw&quot;, parse_json(col(&quot;raw&quot;)))<br><br>(df.write.format(&quot;delta&quot;)<br> .mode(&quot;overwrite&quot;)<br> .option(&quot;overwriteSchema&quot;, &quot;true&quot;)<br> .saveAsTable(f&quot;{TARGET_CATALOG}.{TARGET_SCHEMA}.full_synthetic_stix_graph&quot;)<br>)</pre><p>Our output should look like this in Unity Catalog:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_iYp-XsybFC6xAOlGPfZkQ.png" /></figure><p>where can can review some sample data:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lAIVPcP6Bt4p6VKJZw6_Vw.png" /><figcaption>Nodes sample</figcaption></figure><p>This example node is a synthetic malware node that also contains edges that connects to other objects like threat-actors (such as threat actors → uses → malware). We now have a graph of connected actors, malware, and tools that we can analyze. But what if we have a LOT of these? We need to build more robust graph pattern search and traversal functions to get enough insight out of our threat graph. This is where all the new features of DBSQL start to shine!</p><h3>Step 2 — Graph Analytics Functions on Unity Catalog</h3><p>To analyze any basic graph, we need to be able to do a few things, such as</p><ul><li>Get information about a node or edge</li><li>Find ‘neighbors’, or connections to nodes we care about</li><li>Perform pattern search to do things like identifying nodes with a lot of influence, finding common relationships between 2 nodes, and cluster patterns of nodes/activity together.</li></ul><p>Now there are a ton of graph traversal algos out there, but lets focus on some basic ones to identify high influential nodes and find all the nearest connections to a node we care about. This is a common need in security and many other industries like adtech, healthcare, finance fraud detection, etc.</p><p>Graph algos are not as straightforward as common SQL analytics problems. They require more advanced algorithms that track state and perform recursion. Now with Stored Procedures and Recursive CTEs, we can do these kinds of things natively in SQL!</p><p>Lets write a few functions out to start to provide some reusable tools that analysts or AI agents can use:</p><ul><li><strong>Get Note / Edge Types </strong>— we need to be able to easily discover the types of nodes/edges we have in our graph first. We create out first functions to do this here:</li></ul><pre>CREATE OR REPLACE FUNCTION main.threat_graph_analytics.get_node_types()<br>RETURNS ARRAY&lt;STRING&gt;<br>COMMENT &#39;This gets all the available node types in the graph&#39;<br>RETURN<br>  (SELECT collect_set(type)<br>  FROM main.threat_graph_analytics.stix_graph_nodes<br>  LIMIT 1)<br>;<br><br>CREATE OR REPLACE FUNCTION main.threat_graph_analytics.get_relationship_types()<br>RETURNS ARRAY&lt;STRING&gt;<br>COMMENT &#39;This gets all the available edge connection types in the graph&#39;<br>RETURN<br>  SELECT collect_set(relationship_type)<br>  FROM main.threat_graph_analytics.stix_graph_edges<br>  LIMIT 1;</pre><ul><li><strong>Get Node Metadata </strong>— we cant do much if we cant look at info about a node! So let’s define a function to do that for a given node we care about:</li></ul><pre>CREATE OR REPLACE FUNCTION main.threat_graph_analytics.get_node_metadata(node_id STRING)<br>RETURNS STRUCT&lt;id: STRING, type: STRING, raw: STRING&gt;<br>COMMENT &#39;Get all metadata / information about a particular node id&#39;<br>RETURN<br>  (SELECT<br>    STRUCT(id, type, CAST(raw AS STRING) AS raw)<br>  FROM main.threat_graph_analytics.stix_graph_nodes<br>  WHERE id = node_id<br>  LIMIT 1);</pre><ul><li>T<strong>op Connected Nodes </strong>— to just get a sense for the top nodes with the most connections, we can write a simple function to find the most connected nodes:</li></ul><pre>CREATE OR REPLACE FUNCTION main.threat_graph_analytics.get_top_connected_nodes(limit_num INT DEFAULT 2, connected_node_types ARRAY&lt;STRING&gt; DEFAULT NULL)<br>RETURNS ARRAY&lt;STRUCT&lt;node_id: STRING, type: STRING, degree: INT&gt;&gt;<br>COMMENT &#39;Get the top connected nodes to a give node id to begin deeper analysis on impactful nodes. You can optionally supply a filter array&lt;string&gt; list of node types you want connections for.&#39;<br>RETURN (<br>  SELECT collect_list(<br>    STRUCT(<br>      node_id,<br>      type,<br>      degree<br>    )<br>  )<br>  FROM (<br>    SELECT<br>      n.id AS node_id,<br>      n.type,<br>      (COALESCE(e_in.degree_in, 0) + COALESCE(e_out.degree_out, 0)) AS degree,<br>      ROW_NUMBER() OVER (ORDER BY (COALESCE(e_in.degree_in, 0) + COALESCE(e_out.degree_out, 0)) DESC) AS rn<br>    FROM main.threat_graph_analytics.stix_graph_nodes n<br>    LEFT JOIN (<br>      SELECT dst AS id, COUNT(*) AS degree_in<br>      FROM main.threat_graph_analytics.stix_graph_edges<br>      GROUP BY dst<br>    ) e_in ON n.id = e_in.id<br>    LEFT JOIN (<br>      SELECT src AS id, COUNT(*) AS degree_out<br>      FROM main.threat_graph_analytics.stix_graph_edges<br>      GROUP BY src<br>    ) e_out ON n.id = e_out.id<br>  )<br>  WHERE rn &lt;= limit_num<br>  AND connected_node_types IS NULL OR array_contains(connected_node_types, type)<br>);</pre><ul><li><strong>Breadth First Search — n</strong>ow we get into analyzing patterns/relationships in the graph. This function helps us find all other nodes closely connected to a node we are analyzing within N degrees of separation away from the node.</li></ul><pre>-- Use Stored procedures for more complex compound statements with multiple steps and params<br>CREATE OR REPLACE PROCEDURE main.threat_graph_analytics.bfs_search(<br>  input_node_id STRING, <br>  max_level INT, <br>  connected_node_types ARRAY&lt;STRING&gt; DEFAULT NULL<br>)<br>LANGUAGE SQL<br>SQL SECURITY INVOKER<br>COMMENT &#39;Breadth first search from a given input node id with max_level being the number of degrees out from the node you want to search. You can also supply an array&lt;list&gt; of node types you want to track connections for&#39;<br>AS<br>BEGIN<br>    WITH RECURSIVE neighborhood(level, node_id, prev_node_id, relationship_type, path, visited) AS (<br>      -- Base case: start from the given node<br>      SELECT<br>        0 AS level,<br>        id AS node_id,<br>        CAST(NULL AS STRING) AS prev_node_id,<br>        CAST(NULL AS STRING) AS relationship_type,<br>        ARRAY(id) AS path,<br>        ARRAY(id) AS visited<br>      FROM main.threat_graph_analytics.stix_graph_nodes AS n<br>      WHERE n.id = input_node_id<br><br>      UNION ALL<br><br>      -- Breadth-first search: traverse level by level<br>      SELECT<br>        n.level + 1,<br>        CASE WHEN e.src = n.node_id THEN e.dst ELSE e.src END AS node_id,<br>        n.node_id AS prev_node_id,<br>        e.relationship_type,<br>        array_union(n.path, ARRAY(CASE WHEN e.src = n.node_id THEN e.dst ELSE e.src END)) AS path,<br>        array_union(n.visited, ARRAY(CASE WHEN e.src = n.node_id THEN e.dst ELSE e.src END)) AS visited<br>      FROM neighborhood n<br>      JOIN main.threat_graph_analytics.stix_graph_edges e<br>        ON n.node_id = e.src OR n.node_id = e.dst<br>      WHERE n.level &lt; max_level<br>        AND NOT array_contains(n.visited, CASE WHEN e.src = n.node_id THEN e.dst ELSE e.src END)<br>    )<br><br>    SELECT<br>      STRUCT(<br>        n.level AS level,<br>        n.node_id AS node_id,<br>        nodes.type AS type,<br>        n.prev_node_id AS prev_node_id,<br>        n.relationship_type AS relationship_type,<br>        array_join(n.path, &#39;, &#39;) AS path<br>      )<br>    FROM neighborhood n<br>    JOIN main.threat_graph_analytics.stix_graph_nodes nodes<br>      ON n.node_id = nodes.id<br>    WHERE n.level &gt; 0<br>      AND (connected_node_types IS NULL OR array_contains(connected_node_types, nodes.type))<br>;<br><br>END;</pre><p>This is where some of our new DBSQL functionality starts to come into play! In the above example, we created a <strong>Stored Procedure </strong>— this wraps any arbitrary SQL logic into a single callable object, making managing more complex pipelines or analytical functions much easier to manage and reuse! Additionally, we leverage <strong>recursive CTEs </strong>to run a graph traversal algorithm to find all connected nodes to an input node within N degrees of separation with breadth-first search. This is really powerful!</p><ul><li><strong>Page Rank </strong>— This is a classic algorithm to determine the amount of influence a node has relative to others. Basically a popularity contest based on the number of inbound connections to it weighted by the influence of the nodes it is connected to. <strong>We can now implement this in a stored procedure.</strong></li></ul><pre>CREATE OR REPLACE PROCEDURE main.threat_graph_analytics.pagerank_top_n(<br>  top_n INT,<br>  filter_node_types ARRAY&lt;STRING&gt; DEFAULT NULL<br>)<br>LANGUAGE SQL<br>SQL SECURITY INVOKER<br>COMMENT &#39;Run page rank on the STIX graph to get a stack rank of the most influential nodes, optionally filtered by node types&#39;<br>AS<br>BEGIN<br>  WITH RECURSIVE<br>  params AS (<br>    SELECT CAST(0.85 AS DOUBLE) AS d, CAST(30 AS INT) AS max_iter<br>  ),<br>  nodes AS (<br>    SELECT id, type<br>    FROM main.threat_graph_analytics.stix_graph_nodes<br>    WHERE filter_node_types IS NULL OR array_contains(filter_node_types, type)<br>  ),<br>  total_nodes AS (<br>    SELECT COUNT(*) AS n FROM nodes<br>  ),<br>  edges AS (<br>    SELECT e.src, e.dst<br>    FROM main.threat_graph_analytics.stix_graph_edges e<br>    JOIN nodes ns ON ns.id = e.src<br>    JOIN nodes nd ON nd.id = e.dst<br>  ),<br>  outdeg AS (<br>    SELECT src, COUNT(*) AS outdeg FROM edges GROUP BY src<br>  )<br><br>  /* Paths recursion: carry per-path weight; NO aggregation inside recursion */<br>  paths (node_id, weight, iteration) AS (<br>    -- iteration 0: start with 1/N at each node<br>    SELECT<br>      n.id         AS node_id,<br>      CAST(1.0 / t.n AS DOUBLE) AS weight,<br>      0            AS iteration<br>    FROM nodes n<br>    CROSS JOIN total_nodes t<br><br>    UNION ALL<br><br>    -- push weight along outgoing edges, scaling by d/outdeg<br>    SELECT<br>      e.dst        AS node_id,<br>      CAST(p.weight * par.d / NULLIF(od.outdeg, 0) AS DOUBLE) AS weight,<br>      p.iteration + 1 AS iteration<br>    FROM paths p<br>    JOIN params par         ON TRUE<br>    JOIN edges e            ON e.src = p.node_id<br>    LEFT JOIN outdeg od     ON od.src = e.src<br>    WHERE p.iteration &lt; par.max_iter<br>  ),<br><br>  last_iter AS (<br>    SELECT MAX(iteration) AS k FROM paths<br>  ),<br><br>  /* Sum contributions at the final iteration and add teleport mass */<br>  final_rank AS (<br>    SELECT<br>      n.id AS node_id,<br>      n.type AS node_type,<br>      -- teleport series: ((1-d)/N) * (1 - d^k) / (1 - d)<br>      CAST( ((1 - par.d) / t.n) * (1 - POWER(par.d, li.k)) / (1 - par.d) AS DOUBLE )<br>        + COALESCE(pr_k.inbound_sum, 0.0) AS pagerank<br>    FROM nodes n<br>    CROSS JOIN params par<br>    CROSS JOIN total_nodes t<br>    CROSS JOIN last_iter li<br>    LEFT JOIN (<br>      SELECT node_id, SUM(weight) AS inbound_sum<br>      FROM paths<br>      WHERE iteration = (SELECT k FROM last_iter)<br>      GROUP BY node_id<br>    ) pr_k<br>    ON pr_k.node_id = n.id<br>  )<br><br>  SELECT node_id, node_type, pagerank<br>  FROM final_rank<br>  ORDER BY pagerank DESC<br>  LIMIT top_n;<br>END;</pre><p>Now we have some really strong tools that we can reuse for Graph analysis, and all we need to do is run these functions! For functions, we call them by using them in any SQL expression like below:</p><pre>SELECT main.threat_graph_analytics.get_node_types();<br>SELECT main.threat_graph_analytics.get_top_connected_nodes(5);<br>SELECT main.threat_graph_analytics.get_node_metadata(&#39;malware--c3072957-766c-4e5a-8f0c-6d95f1f00537&#39;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wfN5ho_AIQlSJIYeIyZziA.png" /><figcaption>Results of getting the node types in our graph</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wh688uuu1dZJQW_Z4I0S1w.png" /><figcaption>Results of getting the nodes with the most connections</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*iacRDKhYt_oVpSdtWOeoZQ.png" /><figcaption>Results of getting details about a given node</figcaption></figure><p>For Stored Procedures, since these are more formal compound scripts that are run, we invoke them with a CALL statement like the following:</p><pre>-- Call your stored procedures to do advanced analytics<br>CALL main.threat_graph_analytics.pagerank_top_n(10, split(&#39;threat-actor,malware&#39;, &#39;,&#39;));<br>CALL main.threat_graph_analytics.bfs_search(&#39;threat-actor--c2a4aee6-e118-466d-93ef-72d42718455c&#39;,2)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1vmds4WWpG5fGz2UFj48uw.png" /><figcaption>Results of a breadth first search call</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*u8i5nfbb-aO_y-xO8v3BfA.png" /><figcaption>Results of top 10 pageranked nodes</figcaption></figure><p>We are starting to collect a strong repository of tools we can use for advanced analytics — and now in AI agentic systems! N<strong>otice that we add comments that describe what this function does — this is much more important to do when using these functions in AI systems, so the model can understand what it does and when it may need to be used.</strong></p><h3>Step 3 — Use functions as tools in Agents on Databricks</h3><p>Now that we have some strong reusable functions to analyze our graph, we can empower analysts to use them to perform searches for patterns in our graph — OR we could build an agentic system to help us to that better! To do that, we can leverage the Mosaic Agent Framework on Databricks to construct an agent that not only can summarize information, but can actually do analysis on our graph data for us!</p><p>First things first, we can start by using the functions we defined above to pass clean information into an AI model directly in Databricks SQL like so:</p><pre>-- You can query your custom function to do analysis / get node / campaign info, and then pass to AI models or even custom agents you host in model serving!<br>WITH results AS (<br>  SELECT main.threat_graph_analytics.get_node_metadata(&#39;malware--8ba54ee3-823b-4c8f-af94-4cfab3a6ee22&#39;) AS node<br>)<br>SELECT ai_query(<br>  &#39;databricks-meta-llama-3-3-70b-instruct&#39;,<br>  CONCAT(&#39;Summarize the following metadata: &#39;, CAST(node AS STRING))<br>) AS summary<br>FROM results;</pre><p>In this example, we run our function to get data about a node and pass it into the <a href="https://docs.databricks.com/aws/en/sql/language-manual/functions/ai_query"><strong>ai_query</strong></a><strong> </strong>function and specify any model hosted on Databricks. <strong>This is a powerful generic AI function to be able to access any native model or model serving endpoint hosted on Databricks straight from SQL.</strong></p><p>But we can do a lot more by building an Agent hosted on Databricks so that it not only summarizes information you give it, but can actually run the analysis for us (and decide how to run the analysis given the tools it has access to) to automatically build analytical reports without the end user needing to know anything about Graph analytics.</p><p>If you have yet to built an agent in Databricks, here are some links to get started building an agent and testing it in the Databricks playground via code or with AgentBricks:</p><ul><li><a href="https://docs.databricks.com/aws/en/generative-ai/agent-framework/create-agent">Create to Agents on Databricks</a> — Show the different ways to build out of the box or custom agents with Databricks</li><li><a href="https://docs.databricks.com/aws/en/generative-ai/agent-bricks/">Create Out of the Box Agents with AgentBricks</a> — no code immediate agents to do common task like document parsing, information extraction, customLLM, and an agent supervisor for evaluation.</li><li><a href="https://docs.databricks.com/aws/en/generative-ai/agent-framework/author-agent">Create an Agent with Code and Serve on Databricks </a>— Learn how to build a custom agent for production use cases end to end from agent development, evaluation, serving, monitoring, and rapid prototyping.</li></ul><p>We are going to build an Agent in code with the AI Agent Framework in Databricks to use our Graph tools. We wont go through the entire agent development process in this article, but you can easily plug in your model and tools to the existing template notebooks available <a href="https://docs.databricks.com/aws/en/notebooks/source/generative-ai/responses-agent-langgraph.html">here</a> (live notebook) and <a href="https://docs.databricks.com/aws/en/generative-ai/agent-framework/author-agent?language=LangGraph#-use-responsesagent-to-author-agents">here</a> (docs). You do not need to code all this from scratch, so use these templates to get started quickly.</p><p>These template notebooks walk through the process of defining the agent, integrating it with <a href="https://www.databricks.com/blog/introducing-enhanced-agent-evaluation">ML Flow 3.0 for tracing and agent evaluation (very important for production use cases)</a>, and deploying your agent to a <a href="https://docs.databricks.com/aws/en/generative-ai/agent-framework/deploy-agent#deploy-an-agent-using-deploy">Databricks Model Serving endpoint</a> to be used anywhere.</p><p>Since the above template notebooks walk you through the whole process, we will focus here on how to add our tools to the agent from this <a href="https://docs.databricks.com/aws/en/notebooks/source/generative-ai/responses-agent-langgraph.html">notebook starting point</a>.</p><p>First we set mlflow to autolog, define the model we want to use, and define a system prompt for our agent base model.</p><pre>from typing import Any, Generator, Optional, Sequence, Union,Iterable, List, Dict, Any<br>import mlflow<br>from databricks_langchain import (<br>    ChatDatabricks,<br>    VectorSearchRetrieverTool,<br>    DatabricksFunctionClient,<br>    UCFunctionToolkit,<br>    set_uc_function_client,<br>)<br>from unitycatalog.ai.langchain.toolkit import UnityCatalogTool<br>from langchain_core.language_models import LanguageModelLike<br>from langchain_core.runnables import RunnableConfig, RunnableLambda<br>from langchain_core.tools import BaseTool, Tool, StructuredTool<br>from langgraph.graph import END, StateGraph<br>from langgraph.graph.graph import CompiledGraph<br>from langgraph.graph.state import CompiledStateGraph<br>from langgraph.prebuilt.tool_node import ToolNode<br>from mlflow.langchain.chat_agent_langgraph import ChatAgentState, ChatAgentToolNode<br>from mlflow.pyfunc import ChatAgent<br>from mlflow.types.agent import (<br>    ChatAgentChunk,<br>    ChatAgentMessage,<br>    ChatAgentResponse,<br>    ChatContext,<br>)<br>import os<br>from langchain_community.utilities.sql_database import SQLDatabase<br>from pydantic import BaseModel, Field<br>from databricks import sql<br><br>mlflow.langchain.autolog()<br><br>client = DatabricksFunctionClient()<br>set_uc_function_client(client)<br><br>############################################<br># Define your LLM endpoint and system prompt<br>############################################<br>LLM_ENDPOINT_NAME = &quot;databricks-claude-3-7-sonnet&quot;<br>WAREHOUSE_ID = &quot;&lt;WAREHOUSE_ID&gt;&quot;<br><br>llm = ChatDatabricks(endpoint=LLM_ENDPOINT_NAME)<br><br>system_prompt = &quot;&quot;&quot;This is a Graph Analytics Agent for cyber threat detection data with STIX dataset. Your goal is to analyze that graph and product threat reports. <br>&quot;&quot;&quot;</pre><p>Next, we need to define tools and attach them to our agent. To do this, we use the UCFunctionToolkit and bind our UC functions as tools to our LangGraph agent. Lets do that:</p><pre>### Add tools out of the box from Unity Catalog Functions<br><br>tools = []<br><br># You can use UDFs in Unity Catalog as agent tools<br>uc_tool_names = [&quot;main.threat_graph_analytics.get_node_types&quot;,<br>&quot;main.threat_graph_analytics.get_relationship_types&quot;, <br>&quot;main.threat_graph_analytics.get_node_metadata&quot;,<br>&quot;main.threat_graph_analytics.get_top_connected_nodes&quot;]<br><br>uc_toolkit = UCFunctionToolkit(function_names=uc_tool_names)<br><br>tools.extend(uc_toolkit.tools)<br><br></pre><p>This is very simple for UC Functions! We can also add any custom tools we want here as well. In our example, we will add a custom query handler to push to the DBSQL Warehouse we want as well as a custom page_rank tool to call our Stored Procedure:</p><pre><br>#### Tool 1 - Custom Query Tool<br><br># Define the schema for the tool input<br>class QueryInput(BaseModel):<br>    query: str = Field(..., description=&quot;A SELECT-only SQL query to run on Databricks.&quot;)<br><br># Your execution function<br>def run_databricks_query(query: str, params: Optional[tuple] = None) -&gt; str:<br>    return run_query(query, params)<br><br># Create the tool object manually<br>databricks_sql_tool = Tool(<br>    name=&quot;databricks_sql_query&quot;,<br>    description=&quot;Run a read-only SQL query against the configured Databricks catalog.schema.&quot;,<br>    func=run_databricks_query,<br>    args_schema=QueryInput<br>)<br><br><br># Append it to your existing list of tools<br>tools.append(databricks_sql_tool)<br><br><br>#### Tool 2 - Breadth first search stored procedure<br>class NeighborhoodInput(BaseModel):<br>    node_id: str = Field(..., description=&quot;Start node ID (e.g., a STIX ID).&quot;)<br>    max_level: int = Field(2, ge=1, le=10, description=&quot;Max hop distance from the start node.&quot;)<br><br><br>def run_recursive_neighborhood_query(node_id: str, max_level: int = 2) -&gt; list[dict]:<br>    query = &quot;&quot;&quot;CALL main.threat_graph_analytics.bfs_search(?,?)&quot;&quot;&quot;<br>    # Uses your existing DB-API runner with &#39;?&#39; binding<br>    return run_query(query, params=(node_id, int(max_level)), as_dict=True)<br><br><br>recursive_neighborhood_tool = StructuredTool.from_function(<br>    func=run_recursive_neighborhood_query,<br>    name=&quot;recursive_neighborhood_query&quot;,<br>    description=(<br>        &quot;Breadth-first neighborhood from a starting node up to `max_level` hops. &quot;<br>        &quot;Returns rows with level, node_id, node_type, prev_node_id, relationship_type, and path.&quot;<br>    ),<br>    args_schema=NeighborhoodInput<br>)<br><br>tools.append(recursive_neighborhood_tool)<br><br><br>#### Tool 3 - Custom Page Rank Tool<br>class PageRankModelInput(BaseModel):<br>    top_n: int = Field(10, ge=1, le=10, description=&quot;Top N nodes in page rank popularity.&quot;)<br>    node_types: Optional[List[str]] = Field(<br>        default=None,<br>        description=&quot;Optional list of node types to filter on (e.g., [&#39;threat-actor&#39;, &#39;malware&#39;]).&quot;<br>    )<br><br>def run_page_rank_query(top_n: int = 10, node_types: Optional[List[str]] = None) -&gt; list[dict]:<br>    if node_types:<br>        # Convert list to comma-separated string for SQL<br>        node_types_str = &quot;,&quot;.join(node_types)<br>        query = &quot;&quot;&quot;CALL main.threat_graph_analytics.pagerank_top_n(?, split(?, &#39;,&#39;))&quot;&quot;&quot;<br>        params = (top_n, node_types_str)<br>    else:<br>        query = &quot;&quot;&quot;CALL main.threat_graph_analytics.pagerank_top_n(?)&quot;&quot;&quot;<br>        params = (top_n,)<br>    return run_query(query, params=params, as_dict=True)<br><br>page_rank_tool = StructuredTool.from_function(<br>    func=run_page_rank_query,<br>    name=&quot;page_rank_query&quot;,<br>    description=(<br>        &quot;Page rank algorithm to get the top N most popular nodes, optionally filtered by a list of node types, by page rank influence.&quot;<br>    ),<br>    args_schema=PageRankModelInput<br>)<br><br>tools.append(page_rank_tool)</pre><p>All a tool needs is an input Schema (via a Pydantic data class), a function, a description, and a name! With this, we can add our more custom stored procedures right alongside our initial UCfunction toolkit. Now we have a list of tool objects, and all thats left is to bind those tools to our agent and compile it!</p><pre><br>def create_tool_calling_agent(<br>    model: LanguageModelLike,<br>    tools: Union[Sequence[BaseTool], ToolNode],<br>    system_prompt: Optional[str] = None,<br>) -&gt; CompiledGraph:<br>    ## This is where be connect our tools we created to the agent<br>    model = model.bind_tools(tools)<br><br>.... more code.....</pre><p>The rest of the code in the notebook is our starting template for our agent, feel free to follow that notebook and play with it to build your own!</p><p>Now we follow the notebook to register the agent as a model in MLFlow in Unity Catalog, write a py file, evaluate the agent, and deploy it to a model serving endpoint!</p><p>Once we run agent.deploy(), we can then open the Databricks playground and test our agent by selecting the model we just deployed:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_LrHjlrR8q3zaTCmn7eM4A.png" /><figcaption>Agent with DBSQL Stored Procedures and Functions</figcaption></figure><p>We have a quick and easy playground to try out our agent, and evaluate its live performance. From here, now we iterate on the agent to refine the tools, prompts, agent logical flow, as well as the evaluation cases we need to run our ML Flow Agent Evaluation against to make sure it passes all our tests.</p><p>This is just a starting point to showcase how all the latest and greatest DBSQL features can drastically up-level your analytics, and integrate seamlessly with the latest AI development frameworks available — all on Databricks.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5e53bad68032" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dbsql-sme-engineering/graph-analytics-with-dbsql-stored-procedures-and-agents-5e53bad68032">Graph Analytics with DBSQL Stored Procedures and Agents</a> was originally published in <a href="https://medium.com/dbsql-sme-engineering">DBSQL SME Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Introducing version 2.0 of the Databricks Usage Dashboard]]></title>
            <link>https://medium.com/dbsql-sme-engineering/introducing-version-2-0-of-the-databricks-usage-dashboard-823e8d38068b?source=rss-205045f50ad7------2</link>
            <guid isPermaLink="false">https://medium.com/p/823e8d38068b</guid>
            <category><![CDATA[databricks-sql]]></category>
            <category><![CDATA[observability]]></category>
            <category><![CDATA[databricks]]></category>
            <category><![CDATA[cost-optimization]]></category>
            <category><![CDATA[finops]]></category>
            <dc:creator><![CDATA[Databricks SQL SME]]></dc:creator>
            <pubDate>Mon, 07 Jul 2025 14:47:21 GMT</pubDate>
            <atom:updated>2025-10-22T23:49:54.133Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*g5mIPWAw5uCnllG9w5Zvew.png" /><figcaption>Account Usage Dashboard v2.0 home page</figcaption></figure><p><strong>Authors</strong>: <a href="https://medium.com/u/13bce6a63d8f">Cody Austin Davis</a>, Field Engineering Manager @ Databricks; <a href="https://www.linkedin.com/in/sadhana-bala-2864b295/">Sadhana Bala</a>, Product Manager @ Databricks</p><h3><strong>Introduction</strong></h3><p>Maximizing the value of your Databricks account starts with clear visibility into cost and usage. To support this, last year, Databricks launched <a href="https://docs.databricks.com/aws/en/admin/account-settings/usage">v1 of the Usage Dashboard powered by system tables</a><strong>. </strong>Since then, thousands of customers have used the dashboard to gain essential cost observability across their Databricks accounts.</p><p>We’ve received great feedback from teams who rely on the dashboard for high level day-to-day cost monitoring. At the same time, we’ve heard a clear demand for deeper, more actionable insights to better manage usage across the Databricks platform.</p><p>This year, we’re excited to preview <a href="https://github.com/CodyAustinDavis/dbsql_sme/tree/main/Observability%20Dashboards%20and%20DBA%20Resources/Account%20Usage%20Dashboard">version 2.0 of the Usage Dashboard</a> (<a href="https://github.com/CodyAustinDavis/dbsql_sme/tree/main/Observability%20Dashboards%20and%20DBA%20Resources/Account%20Usage%20Dashboard">link</a>) that takes observability a step further and helps users not only understand spend at a high level, but also:</p><p>🔍Deep dive into team and use case specific spend</p><p>🔍 Monitor untagged, or mistagged usage</p><p>🔍 Identify specific objects and owners driving spend anomalies</p><p>In this blog, we will showcase the new sections in the dashboard and highlight some of the updated use cases that customers are using this dashboard for!</p><h3><strong>Set up</strong></h3><p>This dashboard is now available in Databricks Labs (<a href="https://github.com/CodyAustinDavis/dbsql_sme/tree/main/Observability%20Dashboards%20and%20DBA%20Resources/Account%20Usage%20Dashboard">link</a>) to be downloaded and imported by any user with access to system tables. To get started, review the README, import the dashboard, and start managing your usage!</p><h3><strong>Sections of the Dashboard</strong></h3><p>The dashboard now leverages the latest and greatest of AI/BI Dashboards (such as global filters, dynamic measures, and multi-tabbed sections!) and is comprised of 4 key sections:</p><ol><li><strong>Usage Overview: </strong>Summarize executive-friendly spend analysis and trends</li><li><strong>Usage Overview — Top N: </strong>Quickly identify top N drivers of spend and find owners of that object</li><li><strong>Usage Analysis — Tag Matching: </strong>Understand usage in the way that makes sense for your organization through tags; find usage that contains specific sets of tags, and find untagged/mistagged usage to match your organization’s cost management model</li><li><strong>Usage Analysis — Top Objects: </strong>Dive deeper by finding top objects driving spend as well as identifying ownership and location of objects to immediately take action at a granular level</li></ol><h3><strong>Section I: Usage Overview</strong></h3><p>This section provides an executive summary of spend across a Databricks account to answer broad questions on trends, forecast usage using an AI-powered function, and display high level spend by product, workspace over different periods.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5S7BTCArBmSscq5mhKF8Zg.png" /><figcaption>New Account Usage Overview with Forecasting</figcaption></figure><p><em>How it works:</em></p><ul><li>Choose the time grain you want to look at (Daily/Weekly/Monthly/etc.), and quickly see the top spending products, usage run rate, as well as optionally forecasted usage</li><li>Leverage the global filters to enable different preview functions</li><li>By default the dashboard pulls in <a href="https://docs.databricks.com/aws/en/admin/system-tables/pricing">system.billing.list_prices</a>, but use the “<strong>Preview — Price Table</strong>” filter to switch over to system.billing.account_prices (only works if you are enrolled in the PrPr for AWS and GCP),</li><li>If not enabled on account_prices, input your “<strong>Discount Override</strong>” by specifying SKU-level or billing_origin_product level discounts</li><li>Enable forecasted projections of spend with the “<strong>Preview — Forecas</strong>t” filter, leveraging the <a href="https://docs.databricks.com/gcp/en/sql/language-manual/functions/ai_forecast">AI_FORECAST</a> function (in PuPr)</li></ul><p>Many customers use these tables to export from the Dashboard and do further analysis and budgeting outside the tool!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*POZJUnAWVkEHXWsX" /><figcaption>Usage Overview — Breakdown spend by product / workspace</figcaption></figure><h3><strong>Section II: Usage Overview — Top N</strong></h3><p>In this section, users can quickly identify top N objects, ranked by spend. For example, in our dashboard above, we see that SQL is our top spend driver. In this top N section, we can quickly look at the top spending warehouses.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*B6rKbcYJxhMseM9W" /><figcaption>Quick Top N Analysis of drivers by object</figcaption></figure><p><em>How it works</em></p><ul><li>Select the object type that you want to <strong>rank by</strong> — here we are looking at warehouse_ids</li><li>Specify the number (“<strong>top N</strong>”) of objects you want to break out — here we look at top 10</li></ul><p>This now identifies the top 10 warehouses over time as well as shows us how these top objects compare to the “long tail” of remaining warehouses (enable or disable this comparison with the “<strong>Include Remaining Billable Objects</strong>” filter). This is often an important comparison because it helps users easily see how skewed usage is to the top objects (i.e. “is 80% of my SQL spend coming from these top warehouses?”). I</p><p>If we scroll down, we can identify in which workspaces theses warehouses live, as well as get a period over period analysis:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KC21MGVR8FFnJEOlqExcGQ.png" /><figcaption>Top N Usage by Workspace (name-id)— so you can see where your top drivers are</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Rr5YizndkVuDBxWwIOCPzg.png" /><figcaption>Detailed period over period analysis by object</figcaption></figure><p>In our example above, we can see the top objects and see that our top 10 warehouses make up about 90% of our total SQL spend ($55k spend in REMAINING OBJECTS vs $541k total SQL spend). We can also <strong>click on these hyperlinked object_ids </strong>to navigate directly to those warehouses, and see everything about that warehouse like owner, sizing, usage patterns, etc.</p><h3><strong>Section III: Usage Analysis — Tag Matching</strong></h3><p>This section allows you to manage and view your usage within the semantic terms that your business defines through tags. It allows you to ask questions such as: <strong><em>“What is the usage for my ads platform?”, “What are my development costs for my supply chain use case?”, and “What products make up these use cases’ spend?“.</em></strong></p><p>For example, let’s say we tag our objects in production with a <strong>team</strong> tag. We can search for that tag and see how much of our total usage is properly tagged with <strong>team </strong>like so:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LpX7d2g9HPtG8oZTA81zNQ.png" /><figcaption>Tag Matching Analysis Section Overview</figcaption></figure><p><em>How it works</em></p><ul><li>Search for multiple tag key, value pairs (or just keys) in the <strong>Tag search filter</strong></li><li>Filter for just usage matching this tag, or report on usage not matching this tag, or not tagged at all using the <strong>Usage Match Filter</strong></li><li><strong>Normalize Tags</strong>, so you can track tags regardless of casing or extra spacing in the terms</li></ul><p>You can now report on spend by product and workspace, for the team tag, to support chargebacks and showbacks. If the tag match rate is low, we can Group by product and identify if tagging is low across all the product lines. The next section will also help deep dive into which objects are improperly tagged or untagged usage!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PT7qV9KKH4RllThhwjkqng.png" /><figcaption>Tagged Usage By workspace, product, with period over period analysis</figcaption></figure><p>If you do not yet know which tags to search on, you can now leverage the “<strong>Tag Popularity</strong>” section at the bottom of this tab to find the most common tags:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hr5CPzTyGsHEgnkHzKwpWA.png" /><figcaption>Tag popularity section</figcaption></figure><h3><strong>Section IV: Usage Analysis — Top Objects</strong></h3><p>Let’s say you identify a team that has some out-of-budget spend. What do you do to actually start to find the cause of that and <strong><em>take action</em></strong>? You can then leverage the Usage Analysis — Top Objects section to drill deeper into the specific objects (jobs, warehouses, endpoints, clusters, etc.) that are driving usage for that team.</p><p><em>How it works</em></p><ul><li>Use the tag search filter to search for the tag(s) of interest — in this example we search for <strong>team=databricks_shared</strong>, as they are our top spending team</li><li>Optionally filter for what type of objects you want to filter for by leveraging the “<strong>Billing origin product” </strong>Global filter (e.g., SQL only)</li><li>Filter out old objects that are not used recently with the “<strong>Days Since Last Usage”</strong> slider</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*U9Nkkg32nUvh7JVmiJnUJA.png" /><figcaption>Search by tag key-value pair, analyze usage by workspace, product</figcaption></figure><p>As we filter for a specific team / tag set, the above visual gives us a quick heatmap of where spend is high by workspace and product. We can even click on the “SQL” part of the heatmap to quickly filter for the drivers of that high spend. We can very quickly see that this team is a heavy SQL user<a href="http://user.we/">.</a> But what warehouses are driving that, and who owns them? We can dig in further in the below card that shows identifiable details of each warehouse (id, owner, name), and when it was last used:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wD2toJh3PJm7-rkZWo4zZg.png" /><figcaption>Detailed object-level intelligence</figcaption></figure><p>If you click on the <strong>Billable Object Id</strong>, this will take you directly to the warehouse to monitor it and take action!</p><h3><strong>Conclusion</strong></h3><p>Now, in a single dashboard, you can zoom in from high level executive spend management all the way down to specific drivers of spend that you can immediately take action on. Customers have begun to use this tool as a starting point to build a rich cost observability and management system on Databricks!</p><p>This dashboard is still in preview so please feel free to reach out to <a href="/service/mailto:sadhana.bala@databricks.com">sadhana.bala@databricks.com</a> (PM, Cost Management) to provide feedback!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=823e8d38068b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dbsql-sme-engineering/introducing-version-2-0-of-the-databricks-usage-dashboard-823e8d38068b">Introducing version 2.0 of the Databricks Usage Dashboard</a> was originally published in <a href="https://medium.com/dbsql-sme-engineering">DBSQL SME Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building an Earthquake Monitor with DBSQL’s HTTP_REQUEST]]></title>
            <link>https://medium.com/dbsql-sme-engineering/building-an-earthquake-monitor-with-dbsqls-http-request-0feaff209e55?source=rss-205045f50ad7------2</link>
            <guid isPermaLink="false">https://medium.com/p/0feaff209e55</guid>
            <category><![CDATA[databricks]]></category>
            <category><![CDATA[sql]]></category>
            <category><![CDATA[data]]></category>
            <category><![CDATA[bi]]></category>
            <category><![CDATA[databricks-sql]]></category>
            <dc:creator><![CDATA[Databricks SQL SME]]></dc:creator>
            <pubDate>Tue, 20 May 2025 21:30:40 GMT</pubDate>
            <atom:updated>2025-05-20T22:17:20.310Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*zQMgJGzXCRf6xzc3" /><figcaption>Real-time earthquake monitor in SQL</figcaption></figure><p><strong>Author</strong>: <a href="https://medium.com/u/c2b03321f728">Chris Koester</a>, Specialist Solutions Architect @ Databricks</p><h3>Introduction</h3><p>The new <a href="https://docs.databricks.com/aws/en/sql/language-manual/functions/http_request">http_request</a> function enables performing HTTP requests with DBSQL, and with connections governed by Unity Catalog. http_request unlocks compelling new opportunities for SQL, and should be considered when there’s a preference to use SQL over Python, use governed HTTP connections, or use DBSQL warehouses for compute.</p><p>To demonstrate the new capabilities, this post will guide you through building a simple earthquake monitor using the http_request function, AI/BI, and workflows.</p><h3>Key Components</h3><ol><li><a href="https://earthquake.usgs.gov/earthquakes/feed/v1.0/geojson.php"><strong>USGS earthquakes data feed</strong></a><strong>:</strong> Data feed provided by the <a href="https://www.usgs.gov/">USGS</a>. This is a great data source because it doesn’t require authentication, and is updated every minute.</li><li><a href="https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-syntax-ddl-create-connection"><strong>HTTP connection in Unity Catalog</strong></a>: Governed connection to the USGS earthquakes data feeds.</li><li><a href="https://docs.databricks.com/aws/en/sql/language-manual/functions/http_request"><strong>http_request</strong></a><strong>:</strong> SQL function that allows us to make HTTP requests using the HTTP connection defined above, and is available in DBSQL and DBR 16.2+.</li></ol><h3>Step-by-Step Guide</h3><h3>1. Establish an HTTP Connection in Unity Catalog</h3><p>First, we need to create an <a href="https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-syntax-ddl-create-connection#http-options">HTTP connection</a> for the USGS feeds. This governed connection in Unity Catalog allows us to manage permissions and credentials in one place. The connection will also automatically propagate to your <a href="https://docs.databricks.com/aws/en/security/network/serverless-network-security/manage-network-policies#apply-network-policy-changes">serverless network policy</a>.</p><p>Developers can use any feed available from the base path of the connection.</p><p>The USGS feeds don’t require authentication, so a placeholder of “na” is used for the bearer_token argument.</p><pre>create connection if not exists usgs_conn<br>type HTTP<br>options (<br>  host &#39;https://earthquake.usgs.gov&#39;,<br>  port &#39;443&#39;,<br>  base_path &#39;/earthquakes/feed/v1.0/&#39;,<br>  bearer_token &#39;na&#39;<br>);</pre><h3>2. Make the HTTP Request</h3><p>Next, we’ll make a GET request to the data feed using http_request. Notice that we use the HTTP connection that we created above. <a href="https://docs.databricks.com/aws/en/sql/language-manual/functions/parse_json">parse_json</a> is used to return the data as the <a href="https://www.databricks.com/blog/introducing-open-variant-data-type-delta-lake-and-apache-spark">variant</a> type. Variant is used for storing semi-structured data, and provides an easier option than using a struct and an explicit schema.</p><pre>select<br>  parse_json(<br>    http_request(<br>      conn =&gt; &#39;usgs_conn&#39;,<br>      method =&gt; &#39;GET&#39;,<br>      path =&gt; &#39;summary/all_day.geojson&#39;<br>    ).text<br>  ) as resp_variant</pre><p>This is what the results look like as variant. The data we’re looking for is in the features array.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/819/0*Evrc_MlSTLWsAmHv" /><figcaption>Response in VARIANT</figcaption></figure><h3>3. Explode the Features Array</h3><p>We need to <a href="https://docs.databricks.com/aws/en/sql/language-manual/functions/explode">explode</a> the features array and return the array elements as rows. Explode requires an array or map, so we’ll cast features from variant to array&lt;variant&gt;.</p><pre>with resp as (<br>select<br>  parse_json(<br>    http_request(<br>      conn =&gt; &#39;usgs_conn&#39;,<br>      method =&gt; &#39;GET&#39;,<br>      path =&gt; &#39;summary/all_day.geojson&#39;<br>    ).text<br>  ) as resp_variant<br>)<br>select<br>  explode(variant_get(resp_variant, &#39;$.features&#39;)::array&lt;variant&gt;) as col<br>from resp</pre><p>We now have a row for each element in the array.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*4ppA8hTRq4jM2dF9" /><figcaption>Exploded variant data</figcaption></figure><h3>4. Extracting Earthquake Details</h3><p>The data are in a single variant column, and we’ll extract the desired fields using the intuitive dot-notation syntax. Type casting and processing will be performed here as well.</p><pre>-- Get response and return as variant<br>with resp as (<br>select<br>  parse_json(<br>    http_request(<br>      conn =&gt; &#39;usgs_conn&#39;,<br>      method =&gt; &#39;GET&#39;,<br>      path =&gt; &#39;summary/all_day.geojson&#39;<br>    ).text<br>  ) as resp_variant<br>),<br>-- Convert array of features to rows<br>response_rows as (<br>  select<br>    explode(variant_get(resp_variant, &#39;$.features&#39;)::array&lt;variant&gt;) as col<br>  from resp<br>)<br>-- Select desired columns<br>select<br>  from_unixtime(col:properties.time::bigint/1000) as utc_time,<br>  col:properties.mag::double,<br>  col:properties.place::string,<br>  col:geometry.coordinates[0]::double as longitude,<br>  col:geometry.coordinates[1]::double as latitude,<br>  col:geometry.coordinates[2]::double as depth<br>from response_rows</pre><p>The results look like the example below, and can now be written to Delta by wrapping the statement above with create or replace table usgs as. Since the feed provides data for a rolling time period (Past day), create or replace table provides an easy way of maintaining the same rolling period in Delta.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Zbx19XQc8z0b5qik" /></figure><h3>5. Schedule as a Job</h3><p>With the SQL statement complete, we can create a job for easy scheduling and orchestration. The screenshot below shows a couple dozen runs with a 2X-Small warehouse, and the longest duration is 41 seconds.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/950/0*g0fjzDCVbBPuziEL" /></figure><h3>6. Visualize with AI/BI</h3><p>With data being updated frequently using a job, we can share fresh insights using an AI/BI dashboard.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*2JhnGN5KD94GgQpq" /></figure><h3>Handle Unsuccessful Responses with SQL Scripting</h3><p>We can improve the robustness of the solution by checking the response status code and taking different actions based on the status. If our HTTP request to the feed is successful, we’ll receive a response with a status code of 200 along with the requested data.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/819/0*odGfYWVHU-okx2SG" /></figure><p>If we receive a status code other than 200, we want to log the response and exit. We don’t want to proceed without data, and the log will allow us to monitor unsuccessful requests. For example, here’s a response where the path url was incorrect.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/820/0*gxC8BIbLcPd7TfvY" /></figure><p><a href="https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-scripting">SQL scripting</a> (Available in DBSQL 2025.15 and DBR 16.3) offers a way of checking the HTTP response and taking a different action based on the status code. Let’s review the steps. It is assumed that the HTTP connection shown earlier has already been created.</p><h3>1. Create Logging Table</h3><p>We’ll use a simple table for logging unsuccessful requests. The job id and job run id are also logged.</p><pre>create table if not exists earthquakes_error_log (<br>  timestamp timestamp,<br>  job_id string,<br>  job_run_id string,<br>  status_code int,<br>  error_message string<br>);</pre><h3>2. Make the HTTP Request</h3><p>Retrieve the complete response (status and text) and store it in a variable.</p><pre>declare or replace resp struct&lt;status_code int, text string&gt;;<br><br>set var resp = (<br>  select<br>    http_request(<br>      conn =&gt; &#39;usgs_conn&#39;,<br>      method =&gt; &#39;GET&#39;,<br>      path =&gt; &#39;summary/all_day.geojson&#39;<br>    )<br>);</pre><h3>3. Load Data or Log Non-200 Responses</h3><p>An <a href="https://docs.databricks.com/aws/en/sql/language-manual/control-flow/if-stmt">IF THEN ELSE statement</a> is used to check the response status code and determine which action is taken. This prevents us from trying to parse and load unsuccessful requests. You could also choose to raise an exception and fail the job for unsuccessful requests.</p><pre>-- Log non-200 responses to Delta table<br>-- Script must be in separate notebook cell<br>begin<br>  if resp.status_code == 200 then<br>    create or replace table earthquakes as<br>    with raw_data as (<br>      select explode(variant_get(parse_json(resp.text), &#39;$.features&#39;)::array&lt;variant&gt;) as col<br>    )<br>    select<br>      from_unixtime(col:properties.time::bigint/1000) as utc_time,<br>      col:properties.mag::double,<br>      col:properties.place::string,<br>      col:geometry.coordinates[0]::double as longitude,<br>      col:geometry.coordinates[1]::double as latitude,<br>      col:geometry.coordinates[2]::double as depth<br>    from raw_data;<br>  else<br>    insert into IDENTIFIER(:catalog || &#39;.&#39; || :schema || &#39;.&#39; || &#39;earthquakes_log&#39;)<br>      values (current_timestamp(), :job_id, :job_run_id, resp.status_code, resp.text);<br>  end if;<br>end;</pre><h3>Other Patterns and Opportunities</h3><p>The example described in this post just scratches the surface of what is newly available at the intersection of HTTP and SQL. http_request also supports HTTP methods ‘POST’, ‘PUT’, ‘DELETE’, and ‘PATCH’.</p><p>Databricks documentation shows how to <a href="https://docs.databricks.com/aws/en/generative-ai/agent-framework/external-connection-tools">connect AI agent tools to external services</a>, and the companion repo for this post also contains some additional examples:</p><ul><li><a href="https://github.com/chris-koester-db/dbsql_http/tree/main/nasa_asteroids">nasa_asteroids</a> shows how to generate a collection of metadata for multiple HTTP requests, then iterate through them using a workflows for each task.</li><li><a href="https://github.com/chris-koester-db/dbsql_http/tree/main/databricks_api">databricks_api</a> shows how to work with the Databricks API using simple SQL functions.</li></ul><h3>Conclusion</h3><p>In this post, we demonstrated how to construct an end-to-end solution using a serverless DBSQL warehouse to retrieve and process data from a web-based API. By leveraging the http_request function, we can fetch data from external APIs and persist it to Delta Lake using SQL. Orchestration and sharing of insights can be accomplished with workflows and AI/BI.</p><p>Furthermore, we showed how the solution’s robustness can be improved with SQL scripting.</p><p>The solution described in this post can be deployed as a <a href="https://docs.databricks.com/aws/en/dev-tools/bundles/">Databricks Asset Bundle</a> (DAB), which can be found in the <a href="https://github.com/chris-koester-db/dbsql_http">DBSQL HTTP</a> repo.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0feaff209e55" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dbsql-sme-engineering/building-an-earthquake-monitor-with-dbsqls-http-request-0feaff209e55">Building an Earthquake Monitor with DBSQL’s HTTP_REQUEST</a> was originally published in <a href="https://medium.com/dbsql-sme-engineering">DBSQL SME Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Introducing the Power BI on Databricks Best Practices Cheat Sheet]]></title>
            <link>https://medium.com/dbsql-sme-engineering/introducing-the-power-bi-on-databricks-best-practices-cheatsheet-a55e0aed9575?source=rss-205045f50ad7------2</link>
            <guid isPermaLink="false">https://medium.com/p/a55e0aed9575</guid>
            <category><![CDATA[data-warehouse]]></category>
            <category><![CDATA[power-bi]]></category>
            <category><![CDATA[databricks-sql]]></category>
            <category><![CDATA[bi]]></category>
            <category><![CDATA[sql]]></category>
            <dc:creator><![CDATA[Databricks SQL SME]]></dc:creator>
            <pubDate>Mon, 21 Apr 2025 20:23:59 GMT</pubDate>
            <atom:updated>2025-04-21T20:39:51.279Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*6-sCZi_Z-F_6pcUw" /><figcaption>Power BI on Databricks cheet sheet</figcaption></figure><h4>Authors</h4><p><a href="https://medium.com/u/adda38e9cd3f">Andrey Mirskiy</a>, Lead Specialist SA @ Databricks<br><a href="https://www.linkedin.com/in/davideveneziano/">Davide Veneziano, </a>Sr. Solutions Architect @ Databricks</p><h3>Introduction</h3><p>The blog introduces the cheatsheet, a compilation of best practices for integrating Databricks with Power BI, to optimize performance, scalability, cost, and report efficiency.</p><p>In today’s data-driven landscape, integrating powerful tools like Databricks and Power BI is essential for businesses aiming to maximize their data’s potential. Databricks provides a robust platform for data engineering and analytics, while Power BI excels in data visualization. To fully leverage these tools, careful planning and adherence to best practices are crucial.</p><p>This blog post aims to introduce the best practices when using Power BI on Databricks. Here, we introduce a one-pager cheatsheet which provides a concise and structured overview, outlining the key best practices to ensure meeting high performance, scalability, and security requirements and manage costs efficiently.</p><h3><strong>Unlocking Business Insights with Databricks and Power BI</strong></h3><p>Databricks offers robust data warehousing capabilities through its Databricks SQL engine, which is designed for high performance and scalability. By leveraging a Lakehouse architecture, Databricks enables users to analyze virtually unlimited data volumes. For scalability, Databricks offers Serverless SQL warehouses that automatically scale in and out to meet workload demands, offering high performance and cost efficiency. This is critical for BI applications where fluctuating and spiky workloads are common.</p><p>Integrating Power BI with Databricks offers several advantages that enhance business intelligence capabilities. It enables organizations to analyze large datasets efficiently, leveraging Databricks’ distributed computing platform, coupled with Power BI’s semantic modeling capabilities and dynamic visualization, to ensure optimal performance even with massive datasets, all under a unified governance framework.</p><h3><strong>Why Do Best Practices Matter?</strong></h3><p>Is your report running slow? In most cases this is due to sub-optimal configuration or design.</p><p>Not following best practices using Power BI on Databricks can lead to multiple challenges that impact performance and overall user experience. Poorly optimized tables, inefficient semantic models, and sub-optimal configuration settings can lead to slow report loading times, as well as unnecessary costs.</p><p>Adoption of best practices is the key to ensure performance, scalability, and cost efficiency: your reports will load quicker and more efficiently, providing users with timely insights at lower costs.</p><h3><strong>Introducing the Power BI on Databricks Best Practices Cheatsheet</strong></h3><p>In our daily interactions with customers, we observed that while there are numerous resources available, there’s a significant gap: a concise summary of the most important best practices for using Power BI on Databricks is sorely needed.</p><p><strong>To address this need, we introduce </strong><a href="https://www.databricks.com/sites/default/files/2025-04/2025-04-power-bi-on-databricks-best-practices-cheat-sheet.pdf"><strong>Power BI on Databricks Best Practices Cheat Sheet</strong></a><strong>.This resource provides concise and structured guidance specifically designed for Power BI and Databricks practitioners.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*wSwlqb4rN3uGZJHs" /></figure><p>Best practices are organized into four typical phases, which may correspond to different roles within an organization. In some cases, these roles might be handled by the same individual leading to overlapping.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*A3qA6tL7140AyvqJ" /><figcaption>Cheat Sheet Summary</figcaption></figure><ol><li><strong>Data Preparation</strong><br> This phase is focused on preparing the dataset for reporting. The objective is to implement an efficient data model and optimize the storage layer for better query performance.</li><li><strong>SQL Serving</strong><br>This phase involves setting up and configuring the Databricks SQL Warehouse that Power BI connects to. The goal is to meet performance SLAs and scalability requirements while managing costs efficiently.</li><li><strong>Power BI Integration</strong><br>This phase focuses on configuring the integration between Power BI and Databricks. The objective is to optimize Power BI semantic models for performance and leverage unified governance.</li><li><strong>Power BI Report Design</strong><br>In this phase, teams design semantic models and reports in Power BI. The goal is to create fast and efficient reports and dashboards that inherit optimizations from previous phases.</li></ol><p>For each phase, an extensive list of best practices and recommendations is provided.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ZnVDbpCwHnJSLSmI" /><figcaption>Cheat Sheet Detail</figcaption></figure><p>Every best practice includes a concise description, the explanation of the impact, and the link to the relevant source, such as documentation and blogs.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/482/0*u9sad4nwLtBhjBPa" /></figure><p>In the example above we recommend specific storage mode configurations in Power BI semantic models depending on the type of tables. For fact tables, we suggest using DirectQuery storage mode where Power BI submits queries directly to the data source, namely Databricks SQL. For dimension tables, we recommend using Dual storage mode which allows Power BI to decide whether to use cached data or query the data source based on the context. These recommendations aim to leverage both Databricks’ scalability and Power BI’s caching capabilities effectively. A link to the documentation is provided for further details. The rationale behind this best practice is also explained: implementing these configurations enables Power BI to generate more efficient SQL queries, improve scalability, optimize available capacity, and effectively utilize Databricks’ SQL engine. This approach of providing both recommendations and their justifications is common for most best practices discussed.</p><p>Additionally, each section includes troubleshooting steps to help identify the root cause of slow-running reports.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*dRSHkxhLM8UZIomD" /></figure><p>Finally, the document concludes with a list of links to in-depth articles for further reading.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*XqQ8ysX4e52CEOFI" /></figure><h3>Conclusion</h3><p>Adopting best practices for integrating Databricks with Power BI is not just a recommendation; it’s a necessity for organizations seeking to maximize their data analytics capabilities. By optimizing data models, managing costs efficiently, and ensuring performance and scalability, businesses can unlock the full potential of these powerful tools.</p><p>We believe that the structure of <a href="https://www.databricks.com/sites/default/files/2025-04/2025-04-power-bi-on-databricks-best-practices-cheat-sheet.pdf">the cheat sheet</a>, which presents a simple and actionable checklist grouped into common phases, can accelerate the adoption of best practices. By following these structured guidelines, practitioners can efficiently optimize the use of Power BI on Databricks and ensure that their setup is efficient, performant, and scalable from the very beginning.</p><p>Whether you’re looking to improve report performance, manage costs, or enhance data governance, following <a href="https://www.databricks.com/sites/default/files/2025-04/2025-04-power-bi-on-databricks-best-practices-cheat-sheet.pdf">Power BI on Databricks best practices</a> will be instrumental in achieving your goals.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a55e0aed9575" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dbsql-sme-engineering/introducing-the-power-bi-on-databricks-best-practices-cheatsheet-a55e0aed9575">Introducing the Power BI on Databricks Best Practices Cheat Sheet</a> was originally published in <a href="https://medium.com/dbsql-sme-engineering">DBSQL SME Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Chat Your Way to Cost Visibility with Databricks Genie]]></title>
            <link>https://medium.com/dbsql-sme-engineering/chat-your-way-to-cost-visibility-with-databricks-genie-b2f657469fee?source=rss-205045f50ad7------2</link>
            <guid isPermaLink="false">https://medium.com/p/b2f657469fee</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[bi]]></category>
            <category><![CDATA[databricks]]></category>
            <category><![CDATA[databricks-sql]]></category>
            <category><![CDATA[sql]]></category>
            <dc:creator><![CDATA[Databricks SQL SME]]></dc:creator>
            <pubDate>Wed, 16 Apr 2025 16:27:36 GMT</pubDate>
            <atom:updated>2025-04-16T16:27:36.129Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9bC6bp3_5LYXU7CE8SWOvA.png" /><figcaption>Genie output with system table data</figcaption></figure><h3>Author</h3><p><a href="https://medium.com/u/0382a5fa0871">Numan Ali</a>, Solutions Architect @ Databricks</p><h3>Introduction</h3><p>Databricks has launched <a href="https://docs.databricks.com/aws/en/genie/">Genie</a>, a user-friendly tool that allows any user to interact with their data using everyday language. Genie uses generative AI that understands your company’s specific terms and data, and it keeps improving by learning from user feedback. Genie makes it easier for everyone to access and understand data, all while governing the data sources, contexts, and improving the trusted results over time collaboratively.</p><p>One of the most common datasets in the organizations are the audit and usage logs generated by the data platforms. In Databricks, system tables are a rich analytical store of the users’ operations data. System tables include audit logs and lineage tables, which contain operational data for all Databricks workspaces in a customer’s account and the billing system table, which contains data for all workspaces. Customers have been using the system tables to create dashboards and reports that help them understand how the platform is being used. However, sometimes, those dashboards and reports are not enough and the platform owners and sponsors want to know more about the usage in different dimensions. They typically send their questions to the platform admins who then have to write custom queries to get the answers out. Combining Genie and System tables can help those users to get the information they need simply using the natural language.</p><p>In this article, we will cover 4 example use cases for analyzing system table cost and usage data:</p><ol><li><strong>Basic usage spike investigation</strong> — Identify anomalies in usage over time, and dig into drivers of trends</li><li><strong>Contract management &amp; burndown — </strong>Add your contract discounts, terms, and other context to help Genie manage your spend relative to your business contracts.</li><li><strong>Tag-based usage analysis — </strong>Analyze drivers of usage based on your own contextual grouping of usage with tag-based analysis.</li><li><strong>Spend forecasting </strong>— Forecast spend in natural language with SQL functions</li></ol><h3>Setting up Genie Space</h3><p>Setting up a Genie space is very easy. We simply create a space and define the universe of tables from Unity Catalog that Genie can access for its space. For this particular Genie space, weneed the following tables;</p><ul><li><a href="https://docs.databricks.com/en/admin/system-tables/compute.html">System.compute.clusters</a> — Records compute configurations in your account.</li><li><a href="https://docs.databricks.com/en/admin/system-tables/billing.html">System.billing.usage</a> — Provides the information about billable usage</li><li><a href="https://docs.databricks.com/en/admin/system-tables/pricing.html">System.billing.list_prices</a> — Gives you access to a historical log of SKU pricing</li><li><a href="http://system.workflow.jobs/">system.wor</a><a href="https://docs.databricks.com/en/admin/system-tables/jobs.html#runs">kflow</a><a href="http://system.workflow.jobs/">.jobs</a> — Tracks creation, deletion, and basic information of jobs.</li><li><a href="https://docs.databricks.com/en/admin/system-tables/jobs.html#job-run-timeline-table-schema">System.workflow.job_run_timeline </a>— Records the start, end, and resulting state of job runs.</li><li><a href="https://docs.databricks.com/en/admin/system-tables/jobs.html#job-task-run-timeline-table-schema">System.workflow.job_task_run_timeline</a> — Records the start, end, and resulting state of job tasks.</li><li><a href="https://docs.databricks.com/en/admin/system-tables/jobs.html#job-task-table-schema">System.workflow.job_tasks</a> — Tracks creation, deletion, and basic information of job tasks.</li><li><a href="https://docs.databricks.com/en/admin/system-tables/compute.html#node-timeline-table-schema">System.compute.node_timeline</a> — Includes minute-by-minute records of your compute’s utilization metrics.</li></ul><h3>Investigating a usage spike</h3><p>Let’s imagine we area Platform Manager or in a position where weneed to look at the cost of running Databricks frequently. We would start with the usage trend of the current month. If we have to write it in SQL it would look something like this:</p><pre>SELECT<br> usage_date,<br> SUM(usage_quantity) AS total_dbus<br>FROM<br> system.billing.usage<br>WHERE<br> usage_date BETWEEN DATE_TRUNC(&#39;month&#39;, CURRENT_DATE()) AND CURRENT_DATE()<br>GROUP BY 1<br>ORDER BY 1</pre><p>There are many challenges with manually writing queries to get answers from the platform:</p><ol><li>Users might not have time or skills to write a SQL code to get the answer out of the system or even if they do, they need to learn the table schemas and relations to write the SQL query, which can take up unnecessary time and effort.</li><li>Without a standardized way to improve intelligence on the data, users will likely ask the same questions over and over again, and often get the answers inconsistently, wasting time and decreasing confidence in the results!</li></ol><p>In Genie, users can simply write this in natural language and let the Genie do this for them. Genie understands the relations and context of the tables that have been set up in the space. <strong>Users can ask questions, experiment, refine, and get to a cohesive answer without needing to write code from scratch. Most importantly, they can save the queries and context that Genie generates so users never have to ask the same question twice by adding to Genie’s trusted answers and SQL Queries / Functions context.</strong></p><p>Here is a snapshot of how to get this information out using Genie:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Fq_QLLMu5pZCR7sm" /><figcaption>Basic question on consumption in Genie</figcaption></figure><p>Genie also explains the logic behind the SQL query it generates as you can see in the above example.</p><p>You can also click on the “Show generate code” button and see the code that Genie creates.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*yzeiKpgv2pfyowdn" /><figcaption>See the code Genie generates, and add to its trusted instruction base</figcaption></figure><p>Not only can users see and improve the code Genie writes, but users can also add positive/negative feedback about its correctness, as well as add code as formal re-usable functions that Genie can access later.</p><p>That’s a great start but what if we would like to know what happened on a particular day e-g 7th of August as it seems to be the highest usage day? We can dig further in natural language.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Yln_N0yyA31XRErU" /><figcaption>Digging into usage in Genie</figcaption></figure><p>It came out that the most usage happened at 4am in the morning. So, lets follow up with this question:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*SKLDk_YDE11MseUu" /><figcaption>Raw SKU-level output on usage for a particular hour we are investigating</figcaption></figure><p>This is a bit too detailed for what we really need. Since Databricks has different prices for different SKUs and regions, it lists each one as a separate SKU in the system tables to track usage more accurately. But for my analysis, we want to look at usage by product. For example, weconsider both “ENTERPRISE_SERVERLESS_REAL_TIME_INFERENCE_US_WEST_OREGON” and “ENTERPRISE_SERVERLESS_REAL_TIME_INFERENCE_AP_TOKYO” are simply “<strong>Inference</strong>” or “<strong>Real Time Inference</strong>”</p><p>Therefore, we would follow up with this question to clean up our understanding of product groupings:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*QpNfaze98ciAmGHu" /><figcaption>product level usage at a particular hour</figcaption></figure><p>Billing.Usage tables keeps track of the origin product in “billing_origin_product” so we can add an <a href="https://docs.databricks.com/aws/en/genie/set-up#-provide-instructions"><strong>instruction</strong></a> to use that column while answering the “Product” related questions like so:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*xhoArqtKk-6kDa7b" /></figure><p>You have probably noticed the response to above questions about usage was more in terms of DBUs (Databricks Units) and not the actual “dollar spend”. It is because the systems.billing.usage keeps the usage only in DBUs. But if you need the dollar value (or what we call $DBUs) you will then have to ask the question in a slightly different way.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*gr-fLgGGq3bk45Yg" /><figcaption>Ask about $$ spend, and then save as trusted query!</figcaption></figure><p>When we look at the generated code now, wecan see the JOIN between system.billing.usage and system.billing.list_prices table to get the actual “list price”.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*UOXMWq0X8kSfOKnS" /><figcaption>Genie SQL output</figcaption></figure><p>Note that this is the “list price” and not the price you might get with the discounts you have either with Databricks or your Cloud provider. To make it more “obvious” we can also add instructions to get the “spend” from the system.billing.list_prices tables.</p><p>Since we have spent the effort to refine Genie’s understanding of navigating our pricing system tables, we can now save all these queries as trusted queries by adding SQL Queries to our Genie context as well as sample questions to our settings to automatically suggest the questions to users in the future! <strong>We only have to solve problems once.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*P9NZ6HeNHxndRrYpNjV7qg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Iqe78y81YEZBCjEJGtwVYw.png" /><figcaption>Adding sample trusted queries to a Genie space</figcaption></figure><h3>Adding Contract Info and Discount values to Genie space for Exec Management</h3><p>In many cases, Databricks users might have a direct contract with Databricks that provides them discounts to specific product SKUs. Those users, oftentimes, would like to know how much are they spending against their contract, how much of the contract they have left etc. The Contract term and value is unique to each customer and Databricks does not yet have this data in the System Tables. <strong>But in Genie, we can easily add contract terms and contract values as an instruction and guide Genie to use those values to answer contract related questions for executive to easily manage spend in natural language.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*brAznAWVdblPxRjF" /><figcaption>Adding our contract context for Genie to use to manage spend</figcaption></figure><p>Now, let’s ask the question about how we are doing against the example contract we gave Genie above.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*B41s-TueP1ZCvyc3" /><figcaption>Using the contract instructions</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*VzzImy4B0puIBSNd" /><figcaption>Genie generated code with contract context</figcaption></figure><p>You can see that Genie used contract and discount values that we added to calculate the spend and compare against the contract amount.</p><h3>Monitoring Usage with Tags</h3><p>It is possible to add <a href="https://learn.microsoft.com/en-us/azure/databricks/admin/account-settings/usage-detail-tags">custom tags </a>in Databricks to attribute workloads to specific workspaces, teams, projects, and users. As a platform manager, we might want to keep a track of the spend against a specific project, use case, or a team to manage my resources better.</p><p>The information about tags is stored in system.billing.usage table under customer_tags attribute which is of a STRUCT data type. There are multiple ways to retrieve the information from a STRUCT type attribute but we would like Genie to always use backticks `` because it can handle the case sensitivity. We can add that as an instruction as below:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*olEXEw6SIZK4p1W-" /></figure><p>Now lets take an example. We would like to monitor the usage for tags “demo” and “project” that we have added against specific resources.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*kx6-6YPOg5njqooM" /><figcaption>Output of spend by tag</figcaption></figure><p>Let’s review the query that was generated. We asked Genie to show the “discounted” spend for each “Product.” To do this, Genie followed three steps: (a) it used “billing_origin_product” to identify the product, (b) it applied discounts based on each product, and it used backticks `` to extract the “demo” and “project” tags.</p><pre>SELECT<br> `usage`.`billing_origin_product`,<br> `usage`.`custom_tags`.`demo` AS `demo_tag`,<br> `usage`.`custom_tags`.`project` AS `project_tag`,<br> SUM(<br>   `usage`.`usage_quantity` * `list_prices`.`pricing`.`effective_list`.`default`<br> ) AS `total_spend`<br>FROM<br> `system`.`billing`.`usage`<br>   JOIN `system`.`billing`.`list_prices`<br>     ON `usage`.`sku_name` = `list_prices`.`sku_name`<br>WHERE<br> `usage`.`billing_origin_product` IS NOT NULL<br> AND `usage`.`custom_tags`.`demo` IS NOT NULL<br> AND `usage`.`custom_tags`.`project` IS NOT NULL<br>GROUP BY<br> `usage`.`billing_origin_product`,<br> `usage`.`custom_tags`.`demo`,<br> `usage`.`custom_tags`.`project`</pre><h3>Can Genie forecast the spend?</h3><p>Yes! Databricks introduced <a href="https://docs.databricks.com/aws/en/large-language-models/ai-functions">AI Functions </a>to apply AI, like text translation or sentiment analysis, on your data that is stored on Databricks. One of them is <a href="https://docs.databricks.com/aws/en/sql/language-manual/functions/ai_forecast">ai_forecast()</a> which is designed to extrapolate time series data into the future. This time, we just need to add a SQL query as an instruction to answer such questions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*t8e2xCf6TkxqGQoZ" /><figcaption>Save SQL query to forecast spend with ai_forecast</figcaption></figure><p>Now we would like to see my forecasted spend of the year after applying the discount.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*GQcMu6oBpjVbCm0M" /></figure><h3>What’s Next?</h3><p>Now that the Genie space is set up, you can interact with it asking different questions such as:</p><ol><li>What is the total spend of the platform last week, month or year.</li><li>What is the product wise (i-e Interactive, DLT, SQL) spent last week, month or year.</li><li>What is the spend by a specific workspace.</li><li>What is the “job” wise spend?</li><li>What are the clusters that have spent more than 1K?</li><li>What is the cluster-owner wise spend.</li></ol><p><strong>Full instruction set for the above Genie Room:</strong></p><pre>*Use system.billing.usage.billing_origin_product to answer Product related questions<br>*Use list_prices.pricing.effective_list.default to calculate price or spend;<br>*Use system.billing.usage/usage_metadata.job_id for job_id and exclude &#39;null&#39; values;<br>*Use system.billing.usage.usage_metadata.cluster_id for cluster related usage, spend or pricing questions;<br>*Use system.billing.usage.usage_metadata.notebook_id for notebook related usage questions.<br><br><br>*Total_Contract_Value = 10000000<br>*Contract_Start_Date = &#39;2025-01-01&#39;<br>*Contract_End_Date = &#39;2025-12-31&#39;<br>*Use &quot;Total_Contract_Value&quot; , &quot;Contract_Start_Date&quot; and &quot;Contract_End_Date&quot;. to measure the spend against the contract<br>*When pulling usage by tag, tag key/values can be inconsistent, so always as the user they want to normalized the case and spacing on all tags to find usage matching by usage.custom_tags<br><br><br>*SQL discount = 0.15<br>*ALL_PURPOSE discount = 0.10<br>*DLT discount = 0.20<br>*JOBS discount 0.15<br><br><br>*multiply total spend of an billing_origin_product with their respective discounts to get the actual spend.<br><br><br>*Use ai_forecast() function on total spend to forecast the spend for future<br><br><br>*Use system.billing.usage.custom_tags to answer Tags related questions</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b2f657469fee" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dbsql-sme-engineering/chat-your-way-to-cost-visibility-with-databricks-genie-b2f657469fee">Chat Your Way to Cost Visibility with Databricks Genie</a> was originally published in <a href="https://medium.com/dbsql-sme-engineering">DBSQL SME Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Databricks SQL Orchestration Patterns with For Each and Dynamic Value References]]></title>
            <link>https://medium.com/dbsql-sme-engineering/databricks-sql-orchestration-patterns-with-for-each-and-dynamic-value-references-18bba1696577?source=rss-205045f50ad7------2</link>
            <guid isPermaLink="false">https://medium.com/p/18bba1696577</guid>
            <category><![CDATA[databricks]]></category>
            <category><![CDATA[databricks-sql]]></category>
            <category><![CDATA[data-engineering]]></category>
            <category><![CDATA[orchestration]]></category>
            <category><![CDATA[sql]]></category>
            <dc:creator><![CDATA[Databricks SQL SME]]></dc:creator>
            <pubDate>Wed, 16 Apr 2025 15:34:19 GMT</pubDate>
            <atom:updated>2025-04-16T15:34:19.688Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Gy8X6Uj26dTNhSVEDbuWkQ.png" /><figcaption>Dynamic SQL Orchestration on DBSQL with Workflows</figcaption></figure><h4>Author</h4><p><a href="https://medium.com/u/1e2e0a6dddab">Justin Kolpak</a>, Sr. Solutions Architect @ Databricks</p><h4><strong>Introduction</strong></h4><p>To reduce the burden of writing and maintaining repetitive, boilerplate code, it’s often a best practice to build parameterized pipelines for the more common, repetitive tasks (for example, ingesting data from the source), and pass in the relevant inputs to those pipelines. The result is a solution that requires less development time, a smaller, more maintainable code base, and ultimately allows for more time to focus on the more interesting and value-add responsibilities.</p><p>With recent enhancements to the Databricks Workflows product, you can build out this entire pattern using Databricks native functionality! This blog presents 3 orchestration patterns to build robust configuration-driven, parameterized, SQL-based ETL pipelines with Databricks SQL and Workflows using the <em>For Each</em> <em>task type</em> and the <strong>new</strong> <a href="https://docs.databricks.com/aws/en/jobs/dynamic-value-references#supported-value-references"><em>dynamic value references for SQL outputs</em></a>.</p><h4><strong>Feature Overview</strong></h4><p>Logically, this configuration-driven pattern relies on three key pieces of functionality:</p><ol><li>Write code with parameters: Of course, we’ll need the ability to write parameterized code, and luckily, this has long been supported in Databricks. In the examples to follow, we’ll show how this can be used with <a href="https://docs.databricks.com/en/sql/user/queries/query-parameters.html">Databricks SQL Query Parameters</a> using the :parameter syntax.</li><li>Retrieve and pass in parameters to each iteration of the loop: Once we have our parameterized code, we need to figure out which inputs to pass into the query. There are many ways to store these configurations and retrieve the values, but in this blog, we’ll focus on doing this in a SQL-first approach by retrieving the inputs with a Databricks SQL query and passing in the parameters via <a href="https://docs.databricks.com/aws/en/jobs/dynamic-value-references"><em>dynamic value references for SQL outputs</em></a>.</li><li>Loop/iterate over a set of inputs: Finally, you need a looping construct to iterate through the parameters. For this, we’ll use the Databricks Workflows <a href="https://docs.databricks.com/aws/en/jobs/for-each"><em>For Each Task Type</em></a><em>.</em></li></ol><h3><strong>Examples</strong></h3><p>Let’s dig into the details to show exactly how this pattern works. All of the code to follow is available in <a href="https://github.com/CodyAustinDavis/dbsql_sme/tree/main/Demos%20and%20Tutorials/L300-400%20Tutorials/Orchestration%20Patterns">this GitHub Repository</a>. The Workflows are represented as <a href="https://docs.databricks.com/en/dev-tools/bundles/index.html">Databricks Asset Bundles</a>, making it easy to recreate in your own workspace and follow along.</p><p>For context, we have a dataset stored as text-delimited files in cloud object storage that we’d like to ingest and process into bronze and silver layers. We’ll start with a simple base case and add on complexity from there.</p><h3><em>Example 1: Simple For Each</em></h3><p>Let’s start with the basics. In this example, we have a basic SQL query that creates a Delta Lake table using the COPY INTO function, which reads data from a file path and uses a few other options to properly parse a delimited file. I know this is a pattern that I’ll want to use more than once, so I write the query with <a href="https://docs.databricks.com/en/sql/user/queries/query-parameters.html">Query Parameters</a>.</p><pre>-- Declare Variables<br>DECLARE OR REPLACE VARIABLE _create_table_sql STRING;<br>DECLARE OR REPLACE VARIABLE _copy_into_sql STRING;<br><br><br>-- Create Table<br>SET VAR _create_table_sql = &#39;CREATE TABLE IF NOT EXISTS &#39; || :tgt_catalog || &#39;.&#39; || :tgt_schema || &#39;.&#39; || :tgt_table || &#39; (&#39; || :src_column_list || &#39; , _last_modified_ts timestamp&#39; || &#39;)<br>TBLPROPERTIES (delta.enableChangeDataFeed = true)&#39;;<br><br><br>SELECT _create_table_sql;<br>EXECUTE IMMEDIATE _create_table_sql;<br><br><br>-- COPY INTO Table<br>SET VAR _copy_into_sql = &quot;COPY INTO &quot; || :tgt_catalog || &#39;.&#39; || :tgt_schema || &#39;.&#39; || :tgt_table || &quot;<br>BY POSITION<br>FROM (<br> SELECT<br>     *<br>     , now() AS _last_modified_ts<br> FROM &#39;&quot; || :src_path || &quot;&#39;<br> )<br>FILEFORMAT = &quot;|| :src_format || &quot;<br>FORMAT_OPTIONS (&#39;mergeSchema&#39; = &#39;true&#39;, &#39;sep&#39; = &#39;&quot;|| :src_sep || &quot;&#39;)<br>COPY_OPTIONS(&#39;mergeSchema&#39; = &#39;true&#39;)&quot;;<br><br><br>SELECT _copy_into_sql;<br>EXECUTE IMMEDIATE _copy_into_sql;</pre><p>How can we go about applying this code on every file location and bronze table I’d like to produce? Historically, I might have reached for a Python for loop + the <a href="https://docs.databricks.com/en/notebooks/notebook-workflows.html#run-a-databricks-notebook-from-another-notebook">Databricks %run command</a> or used an external orchestrator, each which introduces their own drawbacks.</p><p>Enter: The <em>For Each task</em> in Databricks Workflows. Here’s how it works:</p><p>Start out by creating a new task, and setting the Type to “For Each” and providing an array of inputs in the “Inputs” field. In this case, the Inputs align to the parameters in the query above:</p><pre>[{&quot;bronze_catalog&quot;:&quot;justin_kolpak&quot;,&quot;<br>bronze_schema&quot;:&quot;tpch_blog&quot;,&quot;bronze_table&quot;:<br>&quot;bronze_customer&quot;,<br>&quot;primary_key&quot;:&quot;c_custkey&quot;,<br>&quot;silver_catalog&quot;:&quot;justin_kolpak&quot;,<br>&quot;silver_schema&quot;:&quot;tpch&quot;,<br>&quot;silver_table&quot;:&quot;silver_customer&quot;,<br>&quot;src_column_list&quot;:&quot;c_custkey int, c_name string, c_address string, c_nationkey int, c_phone string, c_acctbal decimal(15,2), c_mktsegment string, c_comment string, c_dummy string&quot;,<br>&quot;src_format&quot;:&quot;csv&quot;,<br>&quot;src_header&quot;:&quot;false&quot;,<br>&quot;src_path&quot;:&quot;/Volumes/justin_kolpak/tpch/raw/customer&quot;,<br>&quot;src_sep&quot;:&quot;|&quot;},<br><br>{&quot;bronze_catalog&quot;:&quot;justin_kolpak&quot;,&quot;bronze_schema&quot;:&quot;tpch_blog&quot;,&quot;bronze_table&quot;:&quot;bronze_orders&quot;,&quot;primary_key&quot;:&quot;o_orderkey&quot;,&quot;silver_catalog&quot;:&quot;justin_kolpak&quot;,&quot;silver_schema&quot;:&quot;tpch&quot;,&quot;silver_table&quot;:&quot;silver_orders&quot;,&quot;src_column_list&quot;:&quot;o_orderkey int, o_custkey int, o_orderstatus string, o_totalprice double, o_order_date date, o_orderpriority string, o_clerk string, o_shippriority int, o_comment string, o_dummy string&quot;,&quot;src_format&quot;:&quot;csv&quot;,&quot;src_header&quot;:&quot;false&quot;,&quot;src_path&quot;:&quot;/Volumes/justin_kolpak/tpch/raw/orders&quot;,&quot;src_sep&quot;:&quot;|&quot;},{&quot;bronze_catalog&quot;:&quot;justin_kolpak&quot;,&quot;bronze_schema&quot;:&quot;tpch_blog&quot;,&quot;bronze_table&quot;:&quot;bronze_lineitem&quot;,&quot;primary_key&quot;:&quot;l_orderkey&quot;,&quot;silver_catalog&quot;:&quot;justin_kolpak&quot;,&quot;silver_schema&quot;:&quot;tpch&quot;,&quot;silver_table&quot;:&quot;silver_lineitem&quot;,&quot;src_column_list&quot;:&quot;l_orderkey int, l_partkey int, l_suppkey int, l_linenumber int, l_quantity decimal(15,2), l_extendedprice decimal(15,2), l_discount decimal(15,2), l_tax decimal(15,2), l_returnflag string, l_linestatus string, l_shipdate date, l_commitdate date, l_receiptdate date, l_shipinstruct string, l_shipmode string, l_comment string, l_dummy string&quot;,&quot;src_format&quot;:&quot;csv&quot;,&quot;src_header&quot;:&quot;false&quot;,&quot;src_path&quot;:&quot;/Volumes/justin_kolpak/tpch/raw/lineitem&quot;,&quot;src_sep&quot;:&quot;|&quot;}]</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*jVPVU_QfrBVwpjja" /><figcaption><em>For Each task type can take a hard-coded list of values as an “Input”</em></figcaption></figure><p>Then from there, add in the “inner” task that the For Each operator will iterate through.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*2aifUDaI2EXov7gK" /><figcaption><em>Inner task is a SQL file that takes the values from the “Inputs” field as Parameter values</em></figcaption></figure><p>And that’s it!</p><p>When this job runs, the tasks all run concurrently and the UI shows execution details at the iteration-level, making it simple to monitor, observe, and debug issues with this pipeline. In the absence of a capability like this, we would likely only see 1 Success (if everything succeeded) or 1 Failure (if at least one of the components failed) at the job- or task-level, and any troubleshooting efforts would begin with digging through the code/logs trying to find out what specifically caused the failure. By having iteration-level details, we can immediately see what succeeded, what failed, how long it took, what parameters it used, what the code looked like, etc.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*7NCj0rejOahCukRR" /><figcaption>Example <em>Job Run Details show the iteration-level metrics, including duration, status, and input parameters used</em></figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PiQmaPQsMEGQ1eP6HpIZoA.png" /><figcaption>Succesfull run example</figcaption></figure><h3><em>Example 2: For Each + Dynamic Value Reference for SQL Outputs</em></h3><p>What if the list of inputs is constantly changing over time? In the real world, things are always in flux, being added, removed, or modified, and it’s not practical to go into the Workflow and update the “Inputs” field every time there’s a change. Instead, let’s store these inputs in a configuration file (using a JSON file here, but a table or a YAML file would work great too!) and see how we can fit them into this Workflow.</p><p>In this Workflow, we’ve added an initial task that queries the configuration table/file and returns a result set with all of the parameters to pass into the subsequent task. Then using <a href="https://docs.databricks.com/en/jobs/share-task-context.html">task values</a>, we can pass in these parameters to downstream tasks.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*fru3fukZC4GQUo1Q" /><figcaption><em>Task values from get_ingest_metadata task can be used in the load_bronze for each task</em></figcaption></figure><p><a href="https://docs.databricks.com/en/jobs/task-values.html">Task values</a> have been around for a long time with Python, which would require explicitly <a href="https://docs.databricks.com/en/jobs/task-values.html#set-task-values">setting the task values</a> at the end of the <em>get_ingest_metadata</em> task. Now, with dynamic value references for SQL outputs, we 1) can use SQL to do this and 2) don’t need to explicitly set task values — we just need to write a SQL query that returns a meaningful result set and use it through the rest of the workflow.</p><p>Here’s the <em>get_ingest_metadata</em> task — it’s a SQL query that returns a result set with all of the parameters needed downstream.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*f4zb3U_iM7UAZZaC" /><figcaption>Example dynamic SQL query that creates multiple inputs from a SQL query</figcaption></figure><p>Instead of hardcoding the values in the “Inputs” field, as in Example 1 above, this Workflow can be more dynamic by setting the “Inputs” field to <em>{{tasks.get_ingest_metadata.output.rows}}. </em>With this configuration, the Workflow will use the result set of the previous SQL task (<em>get_ingest_metadata) </em>and pass them in as an input to the For Each operator (<em>load_bronze</em>, in this case).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*FW0AYjE5Z0CK9N_g" /><figcaption>Using the dynamic SQL output values in a downstream dynamic SQL forEach task</figcaption></figure><h3><em>Example 3: For Each + Dynamic Value References + Run Job</em></h3><p>In the examples above, the <em>For Each task</em> is iterating over just a single task. What if the task we want to iterate over is more complex than just 1 task? Enter: the <a href="https://docs.databricks.com/en/jobs/create-run-jobs.html#task-type-options">Run Job task type</a>, which supports the ability to run another job from the current job. This provides the ability to build highly modular, DRY (Don’t Repeat Yourself) workflows, and when combined with <em>For Each</em> and <em>dynamic value references</em>, creates a very robust, maintainable, and observable pipeline — all with SQL!</p><p>Instead of the inner task using a SQL File or Query, we can set it to “<a href="https://docs.databricks.com/en/jobs/run-job.html">Run Job</a>”. If there are needs for more advanced logic than can reasonably be expressed with a single task (as in Examples 1 and 2), being able to build a separate job with orchestration logic provides more flexibility in your workflow definitions.</p><p>In this example, it’s a relatively straightforward job consisting of two tasks to be run in sequence, but this pattern can be extended to much more complex tasks. For example, you may want to build conditional control flow logic into your workflow, or you may find yourself with code written in different languages (e.g. a SQL query and a Python script) that you’d like to orchestrate as part of the same process.</p><p>By using this pattern, you can handle highly complex orchestration patterns, while still taking advantage of a configuration-driven approach facilitated by the dynamic value references and For Each operator.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*NHNk8wgtnSIrSKlw" /><figcaption><em>Inner Task is set to Run Job type</em></figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*tAPhBn_9pwzuTjmj" /><figcaption><em>load_bronze_silver is defined as separate job with a more complex structure than the single task</em></figcaption></figure><h3><strong>How To Get Started</strong></h3><p>All of the resources in this blog are available on <a href="https://github.com/CodyAustinDavis/dbsql_sme/tree/main/Demos%20and%20Tutorials/L300-400%20Tutorials/Orchestration%20Patterns">this GitHub Repository</a> as a Databricks Asset Bundle. Follow the instructions in the <a href="https://github.com/CodyAustinDavis/dbsql_sme/blob/main/Demos%20and%20Tutorials/L300-400%20Tutorials/Orchestration%20Patterns/README.md">Getting Started</a> section.</p><h3><strong>Conclusion</strong></h3><p>These examples are just scratching the surface on what can be accomplished with these tools. Workflows supports a variety of advanced orchestration operators that we didn’t have a chance to discuss (like <a href="https://docs.databricks.com/en/jobs/run-if.html#add-a-run-if-condition-to-a-task">conditional execution</a>, run <a href="https://docs.databricks.com/en/jobs/dbt.html">dbt projects</a> or <a href="https://docs.databricks.com/en/jobs/pipeline.html">DLT pipelines</a>, and much more). By putting all of these tools together, inspired by the patterns outlined in this blog, you can build highly sophisticated data pipelines using just Databricks Workflows + DBSQL.</p><p>If you have any questions, feedback, or ideas on how this can be extended to other use cases, feel free to reach out to me at <a href="/service/mailto:justin.kolpak@databricks.com">justin.kolpak@databricks.com</a>.</p><p>Note: All code is provided as-is and is not subject to support SLAs. The examples in this code base are specifically designed to work with the TPC-H dataset, and any production application of this will require some level of customization to work with your datasets and requirements. The intention of publishing these examples is to highlight the high-level design patterns that you can build using Databricks SQL and Workflows.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=18bba1696577" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dbsql-sme-engineering/databricks-sql-orchestration-patterns-with-for-each-and-dynamic-value-references-18bba1696577">Databricks SQL Orchestration Patterns with For Each and Dynamic Value References</a> was originally published in <a href="https://medium.com/dbsql-sme-engineering">DBSQL SME Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Data Federation vs Data Ingestion: updated guidance on an optimal strategy for the integration of…]]></title>
            <link>https://medium.com/dbsql-sme-engineering/data-federation-vs-data-ingestion-updated-guidance-on-an-optimal-strategy-for-the-integration-of-7c6056d45201?source=rss-205045f50ad7------2</link>
            <guid isPermaLink="false">https://medium.com/p/7c6056d45201</guid>
            <category><![CDATA[databricks]]></category>
            <category><![CDATA[query-optimization]]></category>
            <category><![CDATA[databricks-sql]]></category>
            <category><![CDATA[sql]]></category>
            <category><![CDATA[data-engineering]]></category>
            <dc:creator><![CDATA[Databricks SQL SME]]></dc:creator>
            <pubDate>Fri, 11 Apr 2025 16:32:15 GMT</pubDate>
            <atom:updated>2025-04-11T16:55:11.440Z</atom:updated>
            <content:encoded><![CDATA[<h3>Data Federation vs Data Ingestion: updated guidance on an optimal strategy for the integration of external data sources on Databricks</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*PeoPWShxk_oAVv9t" /><figcaption>Federation vs MV</figcaption></figure><h4>Authors</h4><p><a href="https://medium.com/u/77a73679b229">Patrick Sciortino</a>, Sr. Solutions Engineer at Databricks<br><a href="https://medium.com/u/e1328b7966dc">Heeren Sharma</a>, Sr. Solutions Architect at Databricks</p><h3>Introduction</h3><p>Having a unified data catalog that enables comprehensive data discovery and governance is essential for organizations striving to effectively manage their data estate and maximize the value they can derive from data and AI. However, integrating data from different sources is an ongoing endeavor as new operational systems emerge and organizational structures evolve. This article examines two key strategies for integrating and managing diverse data sources through Unity Catalog in the Databricks Data Intelligence Platform context: <em>data ingestion</em> and <em>data federation, </em>enabled by <em>lakehouse federation</em>. We’ll explore each approach’s advantages, drawbacks, and ideal use cases, helping you make informed decisions for your data management needs.</p><h3>Data Ingestion</h3><p>There are various ways to ingest data into the Databricks Data Intelligence Platform; a deep dive is out of scope for this article and is extensively documented <a href="https://docs.databricks.com/aws/en/ingestion/">here</a>. Having said that, the following end result is quite common regardless of the specific ingestion method:</p><ul><li>A data ingestion pipeline copies the data from the source system to the organization’s Data Lake (generally to an area called <em>landing zone</em>, <em>staging area</em> or <em>Bronze</em> layer, depending on naming preferences). These pipelines are typically built to perform incremental data ingestion, in other words, to only load new changes from the source system once historical data has been copied.</li><li>A Unity Catalog table is registered that points to that data in the Data Lake (where data is ingested directly into an optimized data format) or to a corresponding format-optimized version of it (in cases where data is ingested in its raw format first, before being converted). In itself, the Unity Catalog table (as shown in the following figure) is simply a metadata artefact that enables easy consumption and governance of that data, and storing the underlying data in an open-source and optimized format like Delta is a way to ensure optimal compaction and query performance as well as to guarantee ACID properties and other benefits as mentioned <a href="https://delta.io/blog/delta-lake-vs-parquet-comparison/">here</a> while embracing the <a href="https://www.databricks.com/glossary/data-lakehouse">benefits of an open Data Lakehouse approach</a>.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*PVGcJ96zQ_NoMcgS" /></figure><p>Several tools can help streamline the setup of such data ingestion pipelines:</p><ul><li><a href="https://docs.databricks.com/aws/en/ingestion/lakeflow-connect/">Lakeflow Connect</a> is the Databricks-native data ingestion product that leverages Change Data Capture for optimal incremental data ingestion out of the box. It also stores the data in the optimized and open-source Delta format and registers the corresponding Unity Catalog table without additional overhead. External technology partners like <a href="https://www.databricks.com/partners/fivetran">Fivetran</a>, <a href="https://docs.databricks.com/aws/en/partners/ingestion/qlik">Qlik</a>, etc. also deliver the same managed experience.</li><li>For real-time or near real-time requirements, Databricks can natively ingest data from event buses like Apache Kafka, AWS Kinesis, Confluent Cloud, Amazon MSK, or Azure Event Hubs using <a href="https://docs.databricks.com/aws/en/dlt/load#load-data-from-a-message-bus">Delta Live Tables</a> or Spark Structured Streaming and load it directly into a Unity Catalog table.</li></ul><p>Another typical pattern involves copying the data to the Data Lake in its original format using a standard data integration tool like AWS Glue, Azure Data Factory, or Google’s Cloud Data Fusion, and then loading data from the corresponding file location into a Delta table via <a href="https://docs.databricks.com/aws/en/ingestion/cloud-object-storage/copy-into/">COPY INTO</a> or <a href="https://docs.databricks.com/aws/en/ingestion/cloud-object-storage/auto-loader/unity-catalog">Auto Loader</a>.</p><p>Regardless of the chosen approach, the result is that source data is being centralized in the Delta Lake, which offers optimal query speed and scalability, leveraging Databricks’ performance optimizations. A prerequisite is that data engineering or data platform teams are required to develop and maintain data ingestion pipelines, which, depending on the case and the chosen approach, may involve more or less complex implementation effort before any business value can be delivered from the data.</p><h3>Data Federation with Lakehouse Federation</h3><p>Now, what if the organization’s data could be managed and queried via Unity Catalog without copying that data from where it lives? That would mean any query would point to the freshest operational data while saving storage costs and avoiding discrepancies between the analytical and operational worlds. Isn’t that like having your cake and eating it too? To nuance this, one needs to consider the exact implications of that approach.</p><p><a href="https://docs.databricks.com/aws/en/query-federation/">Lakehouse Federation</a> is Databricks’ native query federation solution. It enables Unity Catalog data consumers (users and systems) to leverage read-only managed JDBC connections to popular database solutions and access external catalogs without needing to migrate the underlying data physically. Another benefit is that federated tables and respective connections can be governed alongside all other data and AI assets in Unity Catalog — allowing, for example, Unity Catalog admins to set granular permissions on these federated tables, or to visualize their downstream dependencies as part of Unity Catalog’s out-of-the-box lineage graph.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*YeXqylLKkx89KpG9" /></figure><p>Enabling access and centralized governance on dozens or even hundreds of tables from external systems in one go and without data copy is undoubtedly attractive in specific scenarios (more on that later). However, an important implication of this setup is that all queries on federated tables are effectively executed directly on the source system. This pushback of query workloads to the source can be just fine in some cases (e.g. in case of small data volumes and infrequent queries) but comes with the risks of causing throttling on the source system, generating unnecessarily high egress traffic, or lowering query performances, depending on the specific source and on the query patterns (frequency, complexity, and returned data volume).</p><h3>Lakehouse Federation + Materialized Views for Data Materialization</h3><p>Because of the above mentioned risks, avoiding hitting the source system at every query is often desirable. In that case, a common (and usually recommended) approach is to complement Lakehouse Federation with Materialized Views. <a href="https://docs.databricks.com/aws/en/views/materialized-views-how-it-works">Materialized Views</a> in Databricks are a special type of database object that stores the result of an SQL query as a physical table and keeps it up-to-date over time by refreshing (or recomputing the result) on an arbitrary schedule. Unlike regular views, which resolve the underlying data at query time, materialized views’ precomputation provides a powerful way to enhance the performance of downstream data processing and analysis workloads, making it easy to deliver fresh data quickly and efficiently.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*LsYQ9XmlUjdFy0yF" /></figure><p>However, Materialized Views — when pointing to Federated Tables –have a drawback. They can’t update incrementally, so the entire dataset must be reloaded to reflect fresh data. This can be expensive and cause high data transfer if a large dataset needs frequent updates. Besides, it is critical to ensure that these full refresh workloads, being pushed to the source, don’t create throttling and thereby slow down other data processing workloads or worse, operational processes.</p><p>That being said, the Lakehouse Federation + Materialized Views pattern offers a way to quickly enable access and governance on external sources without compromising downstream query performances — making it a fantastic tool to accelerate value delivery in the first phase of a migration project, enable quick Proof of Concepts or even as a long-term solution when it has been validated that the full refreshes of Materialized Views are viable for the case at hand. <a href="https://medium.com/dbsql-sme-engineering/lakehouse-federation-single-pane-of-glass-for-all-your-data-bc4b26144c59">This article</a> covers the approach in more detail and offers helpful code snippets to get started with such an implementation.</p><h3>Comparison Matrix</h3><p>The following table summarizes the primary considerations for the data integration options we discussed and typical use cases for each approach.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*03tEzrz8IAde_mLEdSuFCw.png" /><figcaption>When to use each path</figcaption></figure><h3>The case for a Hybrid Approach</h3><p>Many organizations find that combining data integration approaches creates an optimal balance between performance, cost, and operational efficiency. To illustrate this, let’s consider BrightPath, a hypothetical global education services leader offering online learning platforms and tools to schools, universities, and corporate clients. The company uses Salesforce as its central CRM to manage customer relationships, track sales opportunities, and provide personalized marketing campaigns. BrightPath started its journey with Databricks last year to unify data and AI workflows at scale.</p><p>BrightPath realized that they could ingest data from <a href="https://docs.databricks.com/aws/en/ingestion/lakeflow-connect/salesforce/salesforce-ingestion/">Salesforce efficiently using Lakeflow Connect</a>. It required little overhead to set up with a few clicks in the UI. Powered by serverless compute, a managed, robust pipeline experience efficiently ingests any new or changed data from the desired Salesforce objects into Delta tables every 6 hours. All while governed through Unity Catalog.</p><p>Brightpath also wanted to provide an improved and automated content recommendation on their portal using advanced machine learning models. Furthermore, to enhance user engagement, it would like to leverage generative AI to generate relevant exercises for a student given their learning history, previous scores and learning goals. To achieve this, <a href="https://docs.databricks.com/aws/en/machine-learning/model-serving/foundation-model-overview">Databricks-hosted AI models</a> like state of the art <a href="https://www.databricks.com/blog/introducing-metas-llama-4-databricks-data-intelligence-platform">Llama-4 models</a>, needed to be fed by real-time data on platform user activity, which is processed from the application to the Lakehouse in near real time by having the user session data sent to BrightPath’s event bus like a Kafka topic and then read by <a href="https://docs.databricks.com/aws/en/dlt/load#load-data-from-a-message-bus">Delta Live Table pipelines</a>.</p><p>More recently, BrightPath acquired HighPotential, a mid-market enterprise focusing on AI-driven simplification of highly technical content for educational purposes. While the longer-term plan following the acquisition is to consolidate the technical landscape of the new organization, business leaders needed quick access to the analytics-ready data processed through HighPotential’s data warehouse, BigQuery. This could be easily achieved by setting up Lakehouse federation to <a href="https://docs.databricks.com/aws/en/query-federation/bigquery">run federated queries on BigQuery data</a>. Because this data is often queried and joined with BrightPath’s Salesforce data, adding a layer of materialized views allowed the conglomerate to augment existing analytical reports with the new information without altering the refresh performances of those user-facing reports.</p><p>By combining this hybrid approach, BrightPath can execute on its migration and unification of various data assets to the Data Intelligence Platform while instantly unlocking valuable insights to technical and business users.</p><h3>Conclusion</h3><p>Using the right tool for the right job:</p><ul><li>We discussed <em>data federation</em> and <em>data ingestion</em> as two alternative approaches to integrating data into the Lakehouse. Each approach has pros and cons, but <a href="https://docs.databricks.com/aws/en/ingestion/">data ingestion</a> is often more scalable and, when properly implemented, offers an effective and reliable solution. <a href="https://docs.databricks.com/aws/en/ingestion/lakeflow-connect/">Lakeflow Connect</a> or ingestion partner tools such as Fivetran can speed up the setup of end-to-end incremental data ingestion pipelines by significantly lowering the implementation barrier.</li><li>On the other hand, data federation, facilitated by <a href="https://docs.databricks.com/aws/en/query-federation/">Lakehouse Federation</a>, can speed up PoCs and migrations (covered more in detail in <a href="https://www.databricks.com/blog/navigating-your-migration-databricks-architectures-and-strategic-approaches">this blog</a>) as it allows data teams to deliver business value before data ingestion pipelines are fully developed. It can also be a valid approach for long-term solutions where the scope of the Materialized Views query or their freshness requirements are such that the benefits of avoiding data duplication balance out the lower efficiency compared to incremental ingestion methods. <a href="https://medium.com/dbsql-sme-engineering/lakehouse-federation-single-pane-of-glass-for-all-your-data-bc4b26144c59">This blog post</a> is a recommended read for more details on the approach.</li><li>As is often the case, there is no one-size-fits-all approach for data integration, and we observe that large organizations frequently adopt hybrid strategies. Hopefully, this article will give you the keys to designing your organization’s optimal data integration pathway!</li></ul><p><strong>Related articles :</strong></p><ul><li><a href="https://medium.com/dbsql-sme-engineering/lakehouse-federation-single-pane-of-glass-for-all-your-data-bc4b26144c59">Lakehouse Federation — Single Pane of Glass for all Your Data</a></li><li><a href="https://www.databricks.com/blog/navigating-your-migration-databricks-architectures-and-strategic-approaches">Navigating Your Migration to Databricks: Architectures and Strategic Approaches</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7c6056d45201" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dbsql-sme-engineering/data-federation-vs-data-ingestion-updated-guidance-on-an-optimal-strategy-for-the-integration-of-7c6056d45201">Data Federation vs Data Ingestion: updated guidance on an optimal strategy for the integration of…</a> was originally published in <a href="https://medium.com/dbsql-sme-engineering">DBSQL SME Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Real-Time Query Monitoring on DBSQL with Alerts]]></title>
            <link>https://medium.com/dbsql-sme-engineering/real-time-query-monitoring-on-dbsql-with-alerts-24e4e7e4a904?source=rss-205045f50ad7------2</link>
            <guid isPermaLink="false">https://medium.com/p/24e4e7e4a904</guid>
            <category><![CDATA[query-optimization]]></category>
            <category><![CDATA[observability]]></category>
            <category><![CDATA[databricks-sql]]></category>
            <category><![CDATA[sql]]></category>
            <category><![CDATA[databricks]]></category>
            <dc:creator><![CDATA[Databricks SQL SME]]></dc:creator>
            <pubDate>Thu, 27 Mar 2025 17:42:57 GMT</pubDate>
            <atom:updated>2025-03-27T17:42:57.885Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5cstVMNJNXjiY54WuIQwaA.png" /><figcaption>Real Time Alerting with DBSQL</figcaption></figure><h4>Author: <a href="https://medium.com/u/13bce6a63d8f">Cody Austin Davis</a></h4><h4>Introduction</h4><p>In a previous <a href="https://medium.com/dbsql-sme-engineering/monitor-dbsql-warehouse-slas-with-real-time-alerts-c4dc36c45c5c">article</a>, we showed how to set up performance SLA alerting and monitoring with DBSQL Alerts, Slack, and PagerDuty. This allows warehouse admins to monitor their warehouse SLAs with a suite of tools such as the <a href="https://medium.com/dbsql-sme-engineering/the-dbsql-warehouse-advisor-dashboard-v5-multi-warehouse-analytics-ef4f07578ac1">Warehouse Advisor Dashboard</a> and the <a href="https://medium.com/dbsql-sme-engineering/the-dbsql-warehouse-advisor-dashboard-v5-multi-warehouse-analytics-ef4f07578ac1">Alerting System defined in our previous article</a> to achieve real-time intel as well as deep analysis of your warehouses on Databricks.</p><p>Now, in addition to deep analysis with dashboards and SLA alerting with Alerts, we can now go even further and react faster to issues that arise with one of the coolest new feature in <a href="https://docs.databricks.com/aws/en/sql/language-manual/functions/http_request">Unity Catalog — HTTP API federation.</a></p><p>This feature allows users to register REST API endpoints as governed connections as well as use that API directly in SQL — all without needing to expose and hardcode credentials in the code itself. This opens up a ton of use cases, but for observability, this allows us to poll the live <a href="https://docs.databricks.com/api/workspace/queryhistory/list">query history</a> api to monitor queries as they come in real-time. This offers the advantage of much more real-time observability compared to dashboards and SQL alerts based on system tables, which can be delayed. With this example, we can poll the query history real-time to quickly identify and react to information like long running queries, queued queries for a long time, high data scanning queries, errors, and anything that we can find in the query history/profile results! Lets create a simple example that can be expanded to any need for query analysis.</p><h4>Steps:</h4><ol><li>Create a UC Connection to Query History</li><li>Write SQL query to identify long running queries stuck in progress</li><li>Create an alert to trigger and show the long running queries</li><li>Schedule that alert and monitor the job</li></ol><h4>Step 1 — Create UC Connection to Query History</h4><p>The first thing we do it create a UC connection to register our query history api to our workspace as a governed connection. To do this, <strong>go to Catalog → External Data → Connections</strong> in the workspace.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GFutMQ5Gt4_fp3fusD9_mg.png" /><figcaption>Catalog Navigation</figcaption></figure><p>Create a new HTTP connection — call it Query History API. Pick your auth type and hit “next”</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bTxpROAuQTAn79OPtgrD1w.png" /><figcaption>New Connection</figcaption></figure><p>Put in your workspace url, token / auth info, and then make your base path the query history api from <a href="https://docs.databricks.com/api/workspace/queryhistory/list">here</a>. Hit Save!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XAO4uKh6aYnDHPbjRt-ucA.png" /><figcaption>Save Connection</figcaption></figure><p>Now you can use you connection in a SQL query with the <a href="https://docs.databricks.com/aws/en/sql/language-manual/functions/http_request">http_request</a> function.</p><h4>Step 2 — Write a SQL Query to get long running queries from the API in real time</h4><p>Now we can write a SQL query to ping the query history API in real time to poll for long running queries. Here is an example:</p><pre>WITH raw_results AS (<br>  SELECT http_request(<br>    conn =&gt; &#39;QueryHistoryAPI&#39;,<br>    method =&gt; &#39;GET&#39;,<br>    path =&gt; &#39;/&#39;,<br>    json =&gt; to_json(named_struct(<br>      &#39;warehouse_ids&#39;, &quot;[&#39;&lt;warehouse_ids&gt;&#39;]&quot;,<br>      &#39;statuses&#39;, &quot;[&#39;QUEUED&#39;, &#39;RUNNING&#39;]&quot;<br>    )<br>    )) AS raw_resp,<br>    current_timestamp() AS snapshot_time<br>),<br><br>-- Put the raw STRING result into a VARIANT to parse<br>clean_results AS (<br>SELECT <br>explode(PARSE_JSON(raw_resp.text):res::array&lt;variant&gt;) AS clean_resp,<br>snapshot_time<br>FROM raw_results<br>WHERE raw_resp.status_code::integer == 200<br>),<br><br>-- Calculate the Query runtime and pull any other metrics you want to track from the variant<br>query_runtime_cals AS (<br>SELECT <br>clean_resp AS queries,<br>queries:query_start_time_ms::long AS query_start_time_ms,<br>snapshot_time,<br>timestamp_millis(query_start_time_ms) AS query_start_time,<br>(unix_timestamp(snapshot_time) - query_start_time_ms / 1000.0) AS runtime_from_snapshot_seconds<br>FROM clean_results<br>)<br><br>-- Add a filter for queries that are still running longer than 60 seconds!<br>SELECT <br>*<br>FROM query_runtime_cals<br>WHERE runtime_from_snapshot_seconds &gt;= 60 -- SLA / Alert trigger in seconds</pre><p>This SQL statement pulls from the Query History API, parses the raw response into a <a href="https://docs.databricks.com/aws/en/sql/language-manual/data-types/variant-type">VARIANT</a> column, and then gets queries that are still running or queued for longer the 60 seconds from when it started to when we polled for its status. This will get us a list of all queries that are running too long for our preference like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RBH6WaK2Of13X8qIhJpRXw.png" /><figcaption>All long running query results</figcaption></figure><p>The query is also very fast! Running each poll under a second or two now allows us to trigger alerts or submit this query from any application (try building a <a href="https://www.databricks.com/blog/introducing-databricks-apps">data app</a> with it!) and get these long running queries in real time! For our example, lets just start with an alert that runs every minute. First we save this query to our queries registry by clicking “Save” at the top right corner. This query is called “RealTime_SLA_Alert”.</p><h4>Step 3&amp;4 — Create a DBSQL Alert to show long running queries and schedule it</h4><p>Now we can take this sql and use it in an alert. To do this, we navigate to the left-hand side of the workspace under the “SQL” tab and click “Alerts”.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rVPfqGkTnsQzt_Pl2hhgYg.png" /><figcaption>Create an alert</figcaption></figure><p>We simply select our query we just created, make the trigger condition be when the count of queries that are long running is &gt; 0, and add any formatting we want to our notification email. Once we do this, we can click the “Add Schedule” button and make our alert trigger every 1 minute.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZS7nzMs97q9Y-YCJ5hbOMw.png" /><figcaption>Adding a schedule</figcaption></figure><p>Once the schedule is saved, this creates a Databricks Workflow that runs the schedule refresh process on the DBSQL warehouse we choose, and we have full observability of that schedule by clicking into the workflow from the Alert.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4PUYDcID29rgIm3sGLOQvw.png" /><figcaption>Go to scheduled workflow from Alert page</figcaption></figure><p>Clicking “View workflow details” takes us to our job that is running our alerts every minute!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hmKC-fU6Hw8yQCWm04Vz1Q.png" /><figcaption>Each alert run only takes a few seconds!</figcaption></figure><h4>Conclusion</h4><p>Having the ability to use API endpoints directly in SQL all within a safely governed catalog creates endless use cases, but especially for real time observability! Try it out, and see what you can create with http_request and alerts!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=24e4e7e4a904" width="1" height="1" alt=""><hr><p><a href="https://medium.com/dbsql-sme-engineering/real-time-query-monitoring-on-dbsql-with-alerts-24e4e7e4a904">Real-Time Query Monitoring on DBSQL with Alerts</a> was originally published in <a href="https://medium.com/dbsql-sme-engineering">DBSQL SME Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>