<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" version="2.0">
  <channel>
    <title>Community Articles topics</title>
    <link>https://community.databricks.com/t5/community-articles/bd-p/Knowledge-Sharing-Hub</link>
    <description>Community Articles topics</description>
    <pubDate>Wed, 30 Sep 2026 18:36:11 GMT</pubDate>
    <dc:creator>Knowledge-Sharing-Hub</dc:creator>
    <dc:date>2026-09-30T18:36:11Z</dc:date>
    <item>
      <title>Row-level security for a RAG agent: what Unity Catalog enforces, and what you have to build</title>
      <link>https://community.databricks.com/t5/community-articles/row-level-security-for-a-rag-agent-what-unity-catalog-enforces/m-p/170272#M1619</link>
      <description>&lt;DIV&gt;&lt;BR /&gt;&lt;DIV&gt;&lt;SPAN&gt;Two colleagues ask the same chatbot the same question. Should they get the same answer, or&lt;/SPAN&gt;&lt;/DIV&gt;&lt;DIV&gt;&lt;SPAN&gt;different ones because they are allowed to see different things? Every organisation that puts an AI&lt;/SPAN&gt;&lt;/DIV&gt;&lt;DIV&gt;&lt;SPAN&gt;assistant on top of its own documents runs into this sooner or later.&lt;/SPAN&gt;&lt;/DIV&gt;&lt;BR /&gt;&lt;DIV&gt;&lt;SPAN&gt;Unity Catalog has a good implementation for that on calls to tables. Attach a row filter and a&lt;/SPAN&gt;&lt;/DIV&gt;&lt;DIV&gt;&lt;SPAN&gt;column mask, and every reader gets their own view of the same object, evaluated against whoever is&lt;/SPAN&gt;&lt;/DIV&gt;&lt;DIV&gt;&lt;SPAN&gt;asking. It does not have a type of grant for row filters &lt;/SPAN&gt;&lt;SPAN&gt;*inside*&lt;/SPAN&gt;&lt;SPAN&gt; a vector index. An AI Search&lt;/SPAN&gt;&lt;/DIV&gt;&lt;DIV&gt;&lt;SPAN&gt;index is a Unity Catalog object with object-level grants, so you can allow or deny querying it, but&lt;/SPAN&gt;&lt;/DIV&gt;&lt;DIV&gt;&lt;SPAN&gt;it has no row filters and no column masks. Filtering an index is a parameter you pass in the query,&lt;/SPAN&gt;&lt;/DIV&gt;&lt;DIV&gt;&lt;SPAN&gt;from your own application code.&lt;/SPAN&gt;&lt;/DIV&gt;&lt;/DIV&gt;</description>
      <pubDate>Wed, 30 Sep 2026 17:19:43 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/row-level-security-for-a-rag-agent-what-unity-catalog-enforces/m-p/170272#M1619</guid>
      <dc:creator>SvenRelijveld</dc:creator>
      <dc:date>2026-09-30T17:19:43Z</dc:date>
    </item>
    <item>
      <title>The 32nd Column: Why Your Z-Order or Clustering Key May Be Doing Nothing</title>
      <link>https://community.databricks.com/t5/community-articles/the-32nd-column-why-your-z-order-or-clustering-key-may-be-doing/m-p/170251#M1618</link>
      <description>&lt;P&gt;You cluster a table on a column, run the query, and nothing improves. The layout command completed successfully. The column is definitely in the filter. The file count in the query profile has not moved.&lt;/P&gt;&lt;P&gt;Before assuming the clustering did not work, check whether statistics exist for that column at all. In a wide table they may not, and nothing in the process will tell you.&lt;/P&gt;&lt;P&gt;How skipping actually works&lt;/P&gt;&lt;P&gt;Delta records statistics for each data file: the minimum value, the maximum value, and the null count per column, plus the record count. At query time the engine compares your predicate against those numbers and discards files that cannot possibly contain a match. A file is never opened, so the cost of that file drops to zero.&lt;/P&gt;&lt;P&gt;Clustering exists to make those ranges narrow. If a file spans the entire range of a column, the engine cannot rule it out. If the file covers a small slice, most queries can discard it immediately.&lt;/P&gt;&lt;P&gt;The whole mechanism rests on one assumption: that statistics exist for the column you are filtering on.&lt;/P&gt;&lt;P&gt;Where that assumption breaks&lt;/P&gt;&lt;P&gt;For Unity Catalog external tables, statistics are collected on the first 32 columns defined in your schema. Not the 32 most useful columns. Not the columns you filter on. The first 32, in declaration order.&lt;/P&gt;&lt;P&gt;Column 33 onward has no statistics. A filter on one of those columns cannot skip a single file. The query returns correct results, runs at full scan cost, and reports no error.&lt;/P&gt;&lt;P&gt;This is easy to walk into. Wide tables grow by accretion. Someone adds a field, then another, then a nested struct whose fields also count toward the limit. Two years later a new filter column sits at position 41 and the table quietly stops being skippable on it.&lt;/P&gt;&lt;P&gt;The documentation is explicit on the consequence for Z-order: Databricks recommends not using ZORDER BY on columns that do not have statistics collected, because it is ineffective and consumes compute for nothing. Liquid clustering reads the same per-file statistics, so the same reasoning applies to a clustering key that sits past the boundary.&lt;/P&gt;&lt;P&gt;One important exception&lt;/P&gt;&lt;P&gt;For Unity Catalog managed tables, predictive optimization does not use a fixed 32-column list. It runs ANALYZE and collects skipping statistics on the columns that actually appear most often in your query filters, so the limit does not apply.&lt;/P&gt;&lt;P&gt;This matters because it means two tables in the same workspace can behave completely differently, and the difference is invisible in the query text. Before debugging, establish which regime your table is in. If it is a managed table with predictive optimization enabled, the column position is not your problem and you should look elsewhere.&lt;/P&gt;&lt;P&gt;How to check&lt;/P&gt;&lt;P&gt;Start with what the table is configured to collect:&lt;/P&gt;&lt;P&gt;SHOW TBLPROPERTIES my_catalog.my_schema.my_table;&lt;/P&gt;&lt;P&gt;Look for delta.dataSkippingStatsColumns and delta.dataSkippingNumIndexedCols. If neither is set and the table is external, you are on the 32-column default.&lt;/P&gt;&lt;P&gt;To see what was actually written rather than what is configured, read the statistics out of the transaction log. Get the path first, then read the add actions:&lt;/P&gt;&lt;P&gt;DESCRIBE DETAIL my_catalog.my_schema.my_table;&lt;/P&gt;&lt;P&gt;Take the location value, then:&lt;/P&gt;&lt;P&gt;SELECT add.stats FROM json.&amp;lt;location&amp;gt;/_delta_log/*.json WHERE add IS NOT NULL LIMIT 5;&lt;/P&gt;&lt;P&gt;The stats field is a JSON document with minValues, maxValues and nullCount objects. The column you care about either appears in there or it does not, and that single check settles the question.&lt;/P&gt;&lt;P&gt;The most direct behavioural evidence is in the query profile. Compare files pruned against files read for a filter on the suspect column, then run the same shape of query filtered on a column you know is early in the schema. If one prunes and the other does not, you have your answer.&lt;/P&gt;&lt;P&gt;Three ways to fix it&lt;/P&gt;&lt;P&gt;Name the columns explicitly. On DBR 13.3 LTS and above this is the cleanest option, because it decouples statistics from schema position entirely:&lt;/P&gt;&lt;P&gt;ALTER TABLE my_table SET TBLPROPERTIES ('delta.dataSkippingStatsColumns' = 'region, order_date, promo_code');&lt;/P&gt;&lt;P&gt;This property supersedes the numeric limit. Be deliberate about the list, since statistics are not free to compute or store.&lt;/P&gt;&lt;P&gt;Raise the count. Works on all runtimes, but remains order dependent, so it is a blunter instrument:&lt;/P&gt;&lt;P&gt;ALTER TABLE my_table SET TBLPROPERTIES ('delta.dataSkippingNumIndexedCols' = '48');&lt;/P&gt;&lt;P&gt;Reorder the schema. Move the columns you filter on to the front. Correct, but it usually means rewriting the table, so it is rarely the pragmatic choice on an existing large table.&lt;/P&gt;&lt;P&gt;The step almost everyone misses&lt;/P&gt;&lt;P&gt;Changing either property does not recompute anything. It changes the behaviour of future writes only. Your existing files keep whatever statistics they were written with, and until they are rewritten your queries will not improve. People make the change, see no difference, and conclude the setting does not work.&lt;/P&gt;&lt;P&gt;On DBR 14.3 LTS and above you can force the recomputation:&lt;/P&gt;&lt;P&gt;ANALYZE TABLE my_table COMPUTE DELTA STATISTICS;&lt;/P&gt;&lt;P&gt;Two related traps&lt;/P&gt;&lt;P&gt;Long string columns are truncated during statistics collection. A column holding URLs, JSON blobs or free text will have min and max values that are effectively useless for skipping, while still costing you to collect. These are good candidates to exclude from the statistics column list rather than to include.&lt;/P&gt;&lt;P&gt;Nested fields count toward the limit individually. For statistics purposes each scalar field inside a struct is treated as its own column, so a struct with fifteen fields consumes fifteen of your slots. A table with twelve top-level columns can already be past the boundary if several of them are nested. Map and array columns cannot have statistics collected at all.&lt;/P&gt;&lt;P&gt;The takeaway&lt;/P&gt;&lt;P&gt;Clustering and statistics are two halves of the same mechanism, and only one of them is visible in the command you run. Layout decides whether a file can be ruled out. Statistics decide whether the engine is able to ask the question in the first place. If the second half is missing, the first half is doing nothing, and nothing in the platform will raise its hand to tell you.&lt;/P&gt;&lt;P&gt;Before choosing clustering keys on a wide table, check that statistics exist for those columns. It takes one command and it occasionally explains months of confusing results.&lt;/P&gt;&lt;P&gt;Has anyone else lost time to this one? And for teams on managed tables with predictive optimization, has letting it choose statistics columns from query history worked better than the columns you would have picked yourself?&lt;/P&gt;</description>
      <pubDate>Wed, 30 Sep 2026 13:39:18 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/the-32nd-column-why-your-z-order-or-clustering-key-may-be-doing/m-p/170251#M1618</guid>
      <dc:creator>Islam_hoti</dc:creator>
      <dc:date>2026-09-30T13:39:18Z</dc:date>
    </item>
    <item>
      <title>Solution Accelerator Series | Incident Investigation Using Graphistry</title>
      <link>https://community.databricks.com/t5/community-articles/solution-accelerator-series-incident-investigation-using/m-p/170167#M1617</link>
      <description>&lt;P&gt;&lt;SPAN&gt;Cybersecurity investigations often require sifting through large volumes of log and telemetry data to uncover the patterns and relationships behind threat activity. The &lt;/SPAN&gt;&lt;STRONG&gt;Incident Investigation Using Graphistry Solution Accelerator&lt;/STRONG&gt;&lt;SPAN&gt; shows how to query investigation data, visualize connections and anomalies with graph analytics, and perform investigative analysis using natural language.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;FONT size="4"&gt;&lt;STRONG&gt;With this Accelerator, you get&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/H3&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;STRONG&gt;Ready-to-use resources:&lt;/STRONG&gt;&lt;A href="https://notebooks.databricks.com/notebooks/SEC/incident-investigation-using-graphistry/index.html?itm_source=www&amp;amp;itm_category=solutions&amp;amp;itm_page=incident-investigation-using-graphistry&amp;amp;itm_location=body&amp;amp;itm_component=cta-image-block&amp;amp;itm_offer=index.html#incident-investigation-using-graphistry_1.html" target="_self"&gt;&lt;SPAN&gt; pre-built code, sample data and step-by-step instructions ready to go in a Databricks notebook&lt;/SPAN&gt;&lt;/A&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;STRONG&gt;Query investigative patterns:&lt;/STRONG&gt;&lt;SPAN&gt; use SQL, Python or Scala in Databricks notebooks, with support from the Databricks Assistant to write and debug queries.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;STRONG&gt;Visualize complex connections:&lt;/STRONG&gt;&lt;SPAN&gt; use Graphistry on the Lakehouse to explore intricate relationships and anomalies.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;STRONG&gt;Investigate using natural language:&lt;/STRONG&gt;&lt;SPAN&gt; perform conversational analysis with&lt;/SPAN&gt;&lt;A href="https://www.databricks.com/blog/introducing-lakehouseiq-ai-powered-engine-uniquely-understands-your-business" target="_blank"&gt; &lt;SPAN&gt;LakehouseIQ&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; and&amp;nbsp;&lt;/SPAN&gt;&lt;A href="https://www.louie.ai/" target="_self" rel="noreferrer" data-external-link="true"&gt;L.O.U.I.E&lt;/A&gt;&lt;A href="https://www.louie.ai/" target="_blank"&gt;&lt;SPAN&gt;.&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;STRONG&gt;Work with different LLM models:&lt;/STRONG&gt;&lt;SPAN&gt; use the&lt;/SPAN&gt;&lt;A href="https://www.databricks.com/blog/announcing-mlflow-ai-gateway" target="_self"&gt; &lt;SPAN&gt;AI Gateway&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt; to switch between third-party and self-hosted models.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P class="p8i6j01 paragraph"&gt;&lt;A style="background-color: #ff3621; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; font-weight: bold; display: inline-block;" href="https://www.databricks.com/solutions/accelerators/incident-investigation-using-graphistry?itm_source=www&amp;amp;itm_category=solutions&amp;amp;itm_page=accelerators&amp;amp;itm_location=body&amp;amp;itm_component=general-asset-card&amp;amp;itm_offer=incident-investigation-using-graphistry" target="_blank" rel="noopener"&gt; &lt;span class="lia-unicode-emoji" title=":link:"&gt;🔗&lt;/span&gt; Launch Solution Accelerator &lt;span class="lia-unicode-emoji" title=":backhand_index_pointing_left:"&gt;👈&lt;/span&gt;&lt;/A&gt;&lt;/P&gt;</description>
      <pubDate>Tue, 29 Sep 2026 14:53:43 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/solution-accelerator-series-incident-investigation-using/m-p/170167#M1617</guid>
      <dc:creator>Tushar_Parekar</dc:creator>
      <dc:date>2026-09-29T14:53:43Z</dc:date>
    </item>
    <item>
      <title>Architecting a Medallion Lakehouse</title>
      <link>https://community.databricks.com/t5/community-articles/architecting-a-medallion-lakehouse/m-p/169946#M1614</link>
      <description>&lt;P&gt;Hi Everyone,&lt;/P&gt;&lt;P&gt;Recently, I led an end-to-end Lakehouse project for a client that was struggling with manual, error-prone reporting and disconnected data silos. Their core business problem was simple yet critical: the business could not trust its own numbers. They needed a modern data platform, but we faced a strict constraint: We could not place any analytical load on their production transactional systems.&lt;/P&gt;&lt;P&gt;In this article, I’ll walk through the three core architectural decisions that allowed us to transform their data landscape into a robust, automated Medallion architecture.&lt;/P&gt;&lt;OL&gt;&lt;LI&gt;The Ingestion Dilemma: Prioritizing Source Stability The client required up-to-date reporting, but querying their production SQL Server databases directly would have impacted their live operations.&lt;/LI&gt;&lt;/OL&gt;&lt;UL&gt;&lt;LI&gt;The Decision: We implemented Change Data Capture (CDC) to stream data into our landing zone.&lt;/LI&gt;&lt;LI&gt;The Result: By reading transaction logs rather than querying live tables, we achieved near real-time data ingestion with zero performance impact on the source systems. We combined this with Auto Loader for file-based sources, ensuring our ingestion was incremental, schema-aware, and highly resilient.&lt;/LI&gt;&lt;/UL&gt;&lt;OL&gt;&lt;LI&gt;The "Medallion" Strategy: Building for Evolution It’s often tempting to build shortcuts from raw data directly to final reports. However, in a real-world production environment, requirements always change.&lt;/LI&gt;&lt;/OL&gt;&lt;UL&gt;&lt;LI&gt;Bronze: Our "Source of Truth" copy. Durable, replayable, and immutable.&lt;/LI&gt;&lt;LI&gt;Silver: Where the "real" work happens. We implemented logic to convert raw CDC events into a clean, historical record (SCD Type 2).&lt;/LI&gt;&lt;LI&gt;Gold: We deliberately partitioned our Gold layer into separate, business-driven data marts. By decoupling these tables, we ensure that a bug in one department’s logic—such as fulfillment tracking—doesn't impact the accuracy of another department's dashboard, like Finance.&lt;/LI&gt;&lt;/UL&gt;&lt;OL&gt;&lt;LI&gt;The Truth About Referential Integrity in Lakehouses A major point of architectural debate: How do we handle foreign keys? In a high-volume streaming environment, checking referential integrity on every write doesn't scale.&lt;/LI&gt;&lt;/OL&gt;&lt;UL&gt;&lt;LI&gt;The Compromise: We declared our foreign key constraints in Unity Catalog to provide metadata for the query optimizer, but we did not enforce them at write-time. Instead, we shifted that check to Data Quality as Code, alerting our team only if orphaned rows appear. This keeps the pipeline performant while maintaining full visibility into data health.&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;Conclusion The lesson here is that a successful Lakehouse isn't just about the tools—it’s about making the right trade-offs. By prioritizing pipeline reliability through decoupled data marts, managed CDC, and monitored constraints, we didn't just build a pipeline; we built a system that the business can finally trust.&lt;/P&gt;</description>
      <pubDate>Sun, 27 Sep 2026 17:28:50 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/architecting-a-medallion-lakehouse/m-p/169946#M1614</guid>
      <dc:creator>Khasim_1</dc:creator>
      <dc:date>2026-09-27T17:28:50Z</dc:date>
    </item>
    <item>
      <title>Declaring a Primary Key RELY turned my dbt unique Test Green on Duplicate Data</title>
      <link>https://community.databricks.com/t5/community-articles/declaring-a-primary-key-rely-turned-my-dbt-unique-test-green-on/m-p/169864#M1611</link>
      <description>&lt;P&gt;An open dbt-databricks feature request, &lt;A href="https://github.com/databricks/dbt-databricks/issues/1271" target="_blank" rel="noopener"&gt;#1271&lt;/A&gt;, asks for native &lt;FONT face="courier new, courier, monospace"&gt;RELY&lt;/FONT&gt; support on primary keys and suggests that &lt;FONT face="courier new, courier, monospace"&gt;rely: true&lt;/FONT&gt; "implicitly create the &lt;FONT face="courier new, courier, monospace"&gt;not_null&lt;/FONT&gt; and &lt;FONT face="courier new, courier, monospace"&gt;unique&lt;/FONT&gt; constraints" as "a safeguard against incorrect query results". Databricks does not enforce &lt;FONT face="courier new, courier, monospace"&gt;UNIQUE&lt;/FONT&gt; constraints, so only a uniqueness test would check the data. I tested whether dbt's &lt;FONT face="courier new, courier, monospace"&gt;unique&lt;/FONT&gt; test still works once the key says &lt;FONT face="courier new, courier, monospace"&gt;RELY&lt;/FONT&gt;.&lt;/P&gt;&lt;P&gt;It did not. I built two dbt models from the same source, which contains one duplicate order. Their contracts differ only in the primary key option. dbt's &lt;FONT face="courier new, courier, monospace"&gt;unique&lt;/FONT&gt; test failed on the &lt;FONT face="courier new, courier, monospace"&gt;NORELY&lt;/FONT&gt; model and passed on the &lt;FONT face="courier new, courier, monospace"&gt;RELY&lt;/FONT&gt; model.&lt;/P&gt;&lt;P align="center"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="dbt build output: the unique test fails on the NORELY model and passes on the RELY model" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31512iD239D18BC4C3FB57/image-size/large?v=v2&amp;amp;px=999" role="button" title="fig1-dbt-build.png" alt="fig1-dbt-build.png" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;P align="center"&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;Figure 1.&lt;/STRONG&gt;&lt;/FONT&gt; &lt;EM&gt;The same unique test on two models built from the same rows.&lt;/EM&gt;&lt;/P&gt;&lt;BLOCKQUOTE&gt;&lt;P&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;WHAT THE DOCS SAY&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;"If &lt;FONT face="courier new, courier, monospace"&gt;RELY&lt;/FONT&gt;, Databricks may exploit the constraint to rewrite and optimize queries. It is the user's responsibility to ensure the constraint is satisfied. Relying on a constraint that is not satisfied may lead to incorrect query results."&lt;/P&gt;&lt;P&gt;&lt;A href="https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-syntax-ddl-create-table-constraint" target="_blank" rel="noopener"&gt;CONSTRAINT clause, Databricks SQL reference&lt;/A&gt;&lt;/P&gt;&lt;/BLOCKQUOTE&gt;&lt;P align="center"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="orange-rule.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31509iC4066A97F786B51C/image-size/large?v=v2&amp;amp;px=999" role="button" title="orange-rule.png" alt="orange-rule.png" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#E0661A"&gt;The setup&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;The source holds six orders, and order &lt;FONT face="courier new, courier, monospace"&gt;3&lt;/FONT&gt; appears twice with different amounts, as a replayed event would. Until #1271 lands, &lt;FONT face="courier new, courier, monospace"&gt;expression&lt;/FONT&gt; does the job: dbt-databricks appends it to the primary key DDL, so this contract creates &lt;FONT face="courier new, courier, monospace"&gt;PRIMARY KEY (order_id) RELY&lt;/FONT&gt;.&lt;/P&gt;&lt;PRE&gt;models:
  - name: orders_rely
    config:
      contract: {enforced: true}
    constraints:
      - type: primary_key
        columns: [order_id]
        expression: RELY          # the control model uses NORELY, the default
    columns:
      - name: order_id
        data_type: bigint
        constraints: [{type: not_null}]
        data_tests: [unique]
      # customer and amount columns omitted&lt;/PRE&gt;&lt;P&gt;Both models keep the duplicate: filtering on &lt;FONT face="courier new, courier, monospace"&gt;order_id = 3&lt;/FONT&gt; returns two rows from each. I ran everything on a serverless SQL warehouse (Photon, DBSQL 2026.36) with dbt-core 1.12.3 and dbt-databricks 1.12.5. The docs say these rewrites need Photon; I did not test other compute.&lt;/P&gt;&lt;P align="center"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="orange-rule.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31509iC4066A97F786B51C/image-size/large?v=v2&amp;amp;px=999" role="button" title="orange-rule.png" alt="orange-rule.png" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#E0661A"&gt;Why the test passes&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;dbt's &lt;FONT face="courier new, courier, monospace"&gt;unique&lt;/FONT&gt; test groups by the column and returns the groups where &lt;FONT face="courier new, courier, monospace"&gt;count(*) &amp;gt; 1&lt;/FONT&gt;. If &lt;FONT face="courier new, courier, monospace"&gt;order_id&lt;/FONT&gt; is unique, each group holds one row and &lt;FONT face="courier new, courier, monospace"&gt;count(*) &amp;gt; 1&lt;/FONT&gt; cannot be true. With &lt;FONT face="courier new, courier, monospace"&gt;RELY&lt;/FONT&gt;, the optimizer takes the key at its word and replaces the query with an empty result. The physical plans, trimmed:&lt;/P&gt;&lt;PRE&gt;-- PRIMARY KEY NORELY
PhotonFilter (n_records &amp;gt; 1)
+- PhotonGroupingAgg(keys=[order_id], functions=[count(1)])
   +- PhotonScan parquet workspace.rely_lab_dbt.orders_norely[order_id]

-- PRIMARY KEY RELY
LocalTableScan &amp;lt;empty&amp;gt;, [unique_field, n_records]&lt;/PRE&gt;&lt;P&gt;A larger table behaved the same way. On 1,000,010 rows in 16 files with 10 duplicated keys, the &lt;FONT face="courier new, courier, monospace"&gt;NORELY&lt;/FONT&gt; run found all 10 and read every row. The &lt;FONT face="courier new, courier, monospace"&gt;RELY&lt;/FONT&gt; run found none and read no rows, and &lt;FONT face="courier new, courier, monospace"&gt;COUNT(DISTINCT order_id)&lt;/FONT&gt; on the same table returned 1,000,010 instead of 1,000,000.&lt;/P&gt;&lt;P align="center"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="system.query.history result: the NORELY run read 1,000,010 rows from 16 files and found 10 duplicate keys; the RELY run read 0 rows from 0 files and found 0" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31510i7E3905E763FF6AE1/image-size/large?v=v2&amp;amp;px=999" role="button" title="fig2-query-history.png" alt="fig2-query-history.png" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;P align="center"&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;Figure 2.&lt;/STRONG&gt;&lt;/FONT&gt; &lt;EM&gt;The same duplicate-key query in &lt;FONT face="courier new, courier, monospace"&gt;system.query.history&lt;/FONT&gt;. The RELY run opened no files.&lt;/EM&gt;&lt;/P&gt;&lt;P align="center"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="orange-rule.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31509iC4066A97F786B51C/image-size/large?v=v2&amp;amp;px=999" role="button" title="orange-rule.png" alt="orange-rule.png" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#E0661A"&gt;Which checks go blind&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;I loaded the same six rows into tables with no key, a &lt;FONT face="courier new, courier, monospace"&gt;NORELY&lt;/FONT&gt; key and a &lt;FONT face="courier new, courier, monospace"&gt;RELY&lt;/FONT&gt; key. The first two matched in every check.&lt;/P&gt;&lt;TABLE&gt;&lt;TBODY&gt;&lt;TR&gt;&lt;TD&gt;&lt;STRONG&gt;Check&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;&lt;STRONG&gt;No key or NORELY&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;&lt;STRONG&gt;RELY&lt;/STRONG&gt;&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;&lt;STRONG&gt;dbt &lt;FONT face="courier new, courier, monospace"&gt;unique&lt;/FONT&gt; (&lt;FONT face="courier new, courier, monospace"&gt;GROUP BY … HAVING COUNT(*) &amp;gt; 1&lt;/FONT&gt;)&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;1 duplicate key&lt;/TD&gt;&lt;TD&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;0&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;&lt;STRONG&gt;PySpark &lt;FONT face="courier new, courier, monospace"&gt;groupBy("order_id").count().filter("count &amp;gt; 1")&lt;/FONT&gt;, serverless job&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;1&lt;/TD&gt;&lt;TD&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;0&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;&lt;STRONG&gt;&lt;FONT face="courier new, courier, monospace"&gt;COUNT(DISTINCT order_id)&lt;/FONT&gt;&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;5&lt;/TD&gt;&lt;TD&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;6&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;&lt;STRONG&gt;&lt;FONT face="courier new, courier, monospace"&gt;LEFT JOIN&lt;/FONT&gt; to a customer table with a duplicate key, row count&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;8&lt;/TD&gt;&lt;TD&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;6&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;&lt;STRONG&gt;&lt;FONT face="courier new, courier, monospace"&gt;COUNT(*) OVER (PARTITION BY order_id) &amp;gt; 1&lt;/FONT&gt;&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;2 rows&lt;/TD&gt;&lt;TD&gt;2 rows&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;&lt;STRONG&gt;DQX 0.16.0 &lt;FONT face="courier new, courier, monospace"&gt;is_unique&lt;/FONT&gt;&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;2 rows flagged&lt;/TD&gt;&lt;TD&gt;2 rows flagged&lt;/TD&gt;&lt;/TR&gt;&lt;/TBODY&gt;&lt;/TABLE&gt;&lt;P&gt;The fan-out check missed because the optimizer dropped the join: with a unique customer key, a &lt;FONT face="courier new, courier, monospace"&gt;LEFT JOIN&lt;/FONT&gt; cannot add rows.&lt;/P&gt;&lt;P align="center"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="SQL editor result comparing total rows, distinct keys, duplicate keys found and LEFT JOIN rows for the no-key, NORELY and RELY tables" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31511i9F62EE86BCA1A025/image-size/large?v=v2&amp;amp;px=999" role="button" title="fig3-summary-grid.png" alt="fig3-summary-grid.png" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;P align="center"&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;Figure 3.&lt;/STRONG&gt;&lt;/FONT&gt; &lt;EM&gt;One query over three tables with identical rows. The RELY row reports six distinct keys, no duplicates and no join fan-out.&lt;/EM&gt;&lt;/P&gt;&lt;P align="center"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="orange-rule.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31509iC4066A97F786B51C/image-size/large?v=v2&amp;amp;px=999" role="button" title="orange-rule.png" alt="orange-rule.png" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#E0661A"&gt;What to do about it&lt;/FONT&gt;&lt;/H2&gt;&lt;UL&gt;&lt;LI&gt;&lt;STRONG&gt;Find your RELY keys.&lt;/STRONG&gt; &lt;FONT face="courier new, courier, monospace"&gt;information_schema.table_constraints&lt;/FONT&gt; returns the same row for a &lt;FONT face="courier new, courier, monospace"&gt;RELY&lt;/FONT&gt; and a &lt;FONT face="courier new, courier, monospace"&gt;NORELY&lt;/FONT&gt; key. &lt;FONT face="courier new, courier, monospace"&gt;SHOW CREATE TABLE&lt;/FONT&gt; and &lt;FONT face="courier new, courier, monospace"&gt;DESCRIBE TABLE EXTENDED&lt;/FONT&gt; show the option.&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Validate before you declare RELY.&lt;/STRONG&gt; Check uniqueness on the staging table or incoming batch, before rows reach a &lt;FONT face="courier new, courier, monospace"&gt;RELY&lt;/FONT&gt; table, and declare &lt;FONT face="courier new, courier, monospace"&gt;RELY&lt;/FONT&gt; only where the writer guarantees uniqueness. Ingestion that can replay events does not.&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Count with a window in dbt.&lt;/STRONG&gt; A &lt;FONT face="courier new, courier, monospace"&gt;databricks__test_unique&lt;/FONT&gt; macro in your project replaces the built-in test on Databricks. With this version, the &lt;FONT face="courier new, courier, monospace"&gt;RELY&lt;/FONT&gt; model's test failed with 2 rows, one per duplicated row. Measure its runtime on your largest models before you switch.&lt;/LI&gt;&lt;/UL&gt;&lt;PRE&gt;{% macro databricks__test_unique(model, column_name) %}
select unique_field, n_records
from (
    select {{ column_name }} as unique_field,
           count(*) over (partition by {{ column_name }}) as n_records
    from {{ model }}
    where {{ column_name }} is not null
) counted
where n_records &amp;gt; 1
{% endmacro %}&lt;/PRE&gt;&lt;UL&gt;&lt;LI&gt;&lt;STRONG&gt;DQX caught it.&lt;/STRONG&gt; &lt;FONT face="courier new, courier, monospace"&gt;is_unique&lt;/FONT&gt; in DQX 0.16.0 counts with a window function and flagged both rows here and all 20 in the million-row table.&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Check the check.&lt;/STRONG&gt; If &lt;FONT face="courier new, courier, monospace"&gt;EXPLAIN&lt;/FONT&gt; shows no scan of the table, or query history shows 0 rows read, the check tested nothing.&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;The window-based checks held in these runs. I found no documentation promising they will keep working, so re-test after upgrades.&lt;/P&gt;&lt;P align="center"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="orange-rule.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31509iC4066A97F786B51C/image-size/large?v=v2&amp;amp;px=999" role="button" title="orange-rule.png" alt="orange-rule.png" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#E0661A"&gt;Conclusion&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;A &lt;FONT face="courier new, courier, monospace"&gt;RELY&lt;/FONT&gt; key changes the answers of the queries meant to verify it. Here, dbt's &lt;FONT face="courier new, courier, monospace"&gt;unique&lt;/FONT&gt; test, a PySpark &lt;FONT face="courier new, courier, monospace"&gt;groupBy&lt;/FONT&gt;, &lt;FONT face="courier new, courier, monospace"&gt;COUNT(DISTINCT)&lt;/FONT&gt; and a join fan-out check all missed a duplicate key that window-based checks caught. Whatever safeguard #1271 adds should validate uniqueness before the key says &lt;FONT face="courier new, courier, monospace"&gt;RELY&lt;/FONT&gt;, or count with a window.&lt;/P&gt;</description>
      <pubDate>Fri, 25 Sep 2026 18:30:18 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/declaring-a-primary-key-rely-turned-my-dbt-unique-test-green-on/m-p/169864#M1611</guid>
      <dc:creator>ivanvyd</dc:creator>
      <dc:date>2026-09-25T18:30:18Z</dc:date>
    </item>
    <item>
      <title>Solution Accelerator Series | Entity Resolution for Public Sector</title>
      <link>https://community.databricks.com/t5/community-articles/solution-accelerator-series-entity-resolution-for-public-sector/m-p/169611#M1610</link>
      <description>&lt;P&gt;&lt;SPAN&gt;Public sector agencies need well-informed decisions to support the livelihood and security of citizens. The &lt;/SPAN&gt;&lt;STRONG&gt;Entity Resolution for Public Sector Solution Accelerator&lt;/STRONG&gt;&lt;SPAN&gt; shows how entity resolution can connect disparate data sources, reveal relationships and patterns, and create a more complete view of the entities represented in agency data.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;FONT size="4"&gt;&lt;STRONG&gt;With this Accelerator, you get&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/H3&gt;
&lt;UL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;STRONG&gt;Ready-to-use resources:&lt;/STRONG&gt; &lt;A href="https://notebooks.databricks.com/notebooks/PUB/auto-data-linkage/index.html?itm_source=www&amp;amp;itm_category=solutions&amp;amp;itm_page=entity-resolution-public-sector&amp;amp;itm_location=body&amp;amp;itm_component=large-header&amp;amp;itm_offer=index.html#auto-data-linkage_1.html" target="_blank"&gt;&lt;SPAN&gt;pre-built code, sample data and step-by-step instructions ready to go in a Databricks notebook&lt;/SPAN&gt;&lt;/A&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;STRONG&gt;Create a complete 360-degree view:&lt;/STRONG&gt;&lt;SPAN&gt; join data sets that represent the same underlying entity.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;STRONG&gt;Improve data consistency:&lt;/STRONG&gt;&lt;SPAN&gt; reduce inconsistencies to support more accurate and timely decision-making.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;STRONG&gt;Unlock insights with machine learning:&lt;/STRONG&gt;&lt;SPAN&gt; analyze disparate data sources to uncover additional value.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;STRONG&gt;Improve interoperability:&lt;/STRONG&gt;&lt;SPAN&gt; support data flow within departments, across departments and across government.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P class="p8i6j01 paragraph"&gt;&lt;A style="background-color: #ff3621; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; font-weight: bold; display: inline-block;" href="https://www.databricks.com/solutions/accelerators/entity-resolution-public-sector?itm_source=www&amp;amp;itm_category=solutions&amp;amp;itm_page=accelerators&amp;amp;itm_location=body&amp;amp;itm_component=general-asset-card&amp;amp;itm_offer=entity-resolution-public-sector" target="_blank" rel="noopener"&gt; &lt;span class="lia-unicode-emoji" title=":link:"&gt;🔗&lt;/span&gt; Launch Solution Accelerator &lt;span class="lia-unicode-emoji" title=":backhand_index_pointing_left:"&gt;👈&lt;/span&gt;&lt;/A&gt;&lt;/P&gt;</description>
      <pubDate>Wed, 23 Sep 2026 17:06:08 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/solution-accelerator-series-entity-resolution-for-public-sector/m-p/169611#M1610</guid>
      <dc:creator>Tushar_Parekar</dc:creator>
      <dc:date>2026-09-23T17:06:08Z</dc:date>
    </item>
    <item>
      <title>Learn Databricks AI Agents | Build, Deploy &amp; Evaluate on Databricks Platform</title>
      <link>https://community.databricks.com/t5/community-articles/learn-databricks-ai-agents-build-deploy-amp-evaluate-on/m-p/169855#M1609</link>
      <description>&lt;DIV style="width: 100%; margin: 0 auto; font-family: Arial,Helvetica,sans-serif; color: #1b3139;"&gt;
&lt;DIV style="background-color: #1b3139; padding: 32px 34px 30px 34px; border-radius: 14px 14px 0 0; overflow: hidden;"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-left" image-alt="DAT_Stacked_Lock_up_Full_Color_White@2x.png" style="width: 104px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/29975iB27742B0C457B7AC/image-size/medium?v=v2&amp;amp;px=400" width="104" role="button" title="DAT_Stacked_Lock_up_Full_Color_White@2x.png" alt="DAT_Stacked_Lock_up_Full_Color_White@2x.png" /&gt;&lt;/span&gt;
&lt;DIV style="color: #9fb0b6; font-size: 13px; font-weight: bold; letter-spacing: 2px; text-transform: uppercase;"&gt;Databricks Training &amp;amp; Certifications&lt;/DIV&gt;
&lt;DIV style="color: #ffffff; font-size: 26px; font-weight: 800; line-height: 1.25; margin-top: 4px;"&gt;Learn Databricks AI Agents &lt;span class="lia-unicode-emoji" title=":robot_face:"&gt;🤖&lt;/span&gt;&lt;/DIV&gt;
&lt;DIV style="color: #c8d0d3; font-size: 14px; margin-top: 6px;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;Systems that reason, call tools, and take action - built on the Databricks Platform.&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;DIV style="background-color: #f5f3ef; padding: 24px 34px 28px 34px;"&gt;
&lt;DIV style="text-align: center; margin-bottom: 22px;"&gt;&lt;A style="display: inline-block; background-color: #ffe1db; color: #c42a17; font-size: 13px; font-weight: 800; padding: 8px 16px; border-radius: 24px; text-decoration: none; margin: 0 5px 8px 0;" target="_blank"&gt;&lt;span class="lia-unicode-emoji" title=":robot_face:"&gt;🤖&lt;/span&gt; Agentic AI&lt;/A&gt; &lt;A style="display: inline-block; background-color: #ffe1db; color: #c42a17; font-size: 13px; font-weight: 800; padding: 8px 16px; border-radius: 24px; text-decoration: none; margin: 0 5px 8px 0;" target="_blank"&gt;&lt;span class="lia-unicode-emoji" title=":high_voltage:"&gt;⚡&lt;/span&gt; Mosaic AI &amp;amp; Agent Bricks&lt;/A&gt; &lt;A style="display: inline-block; background-color: #ffe1db; color: #c42a17; font-size: 13px; font-weight: 800; padding: 8px 16px; border-radius: 24px; text-decoration: none; margin: 0 5px 8px 0;" target="_blank"&gt;🧠 Persistent memory&lt;/A&gt;&lt;/DIV&gt;
&lt;DIV style="font-size: 17px; line-height: 1.6; color: #3b4a52;"&gt;&lt;STRONG&gt;AI agents&lt;/STRONG&gt; are systems that reason, plan, and take action on their own - calling tools, retrieving knowledge, and completing multi-step tasks. On Databricks you build, deploy, and evaluate them with &lt;STRONG&gt;Mosaic AI&lt;/STRONG&gt; and &lt;STRONG&gt;Agent Bricks&lt;/STRONG&gt;, all governed by Unity Catalog.&lt;/DIV&gt;
&lt;DIV style="font-size: 17px; line-height: 1.6; color: #3b4a52; margin-top: 14px;"&gt;The catalog offers &lt;STRONG&gt;three AI Agents courses&lt;/STRONG&gt; across two levels - from a free onboarding intro to hands-on Associate labs that give your agents durable memory. Follow the path below. &lt;span class="lia-unicode-emoji" title=":backhand_index_pointing_down:"&gt;👇&lt;/span&gt;&lt;/DIV&gt;
&lt;DIV style="margin: 30px 0 14px 0;"&gt;&lt;A style="display: inline-block; width: 34px; height: 34px; line-height: 34px; text-align: center; background-color: #ff3621; color: #ffffff; font-size: 16px; font-weight: 800; border-radius: 50%; text-decoration: none; vertical-align: middle; margin-right: 12px;" target="_blank"&gt;1&lt;/A&gt; &lt;SPAN&gt;Start here&lt;/SPAN&gt; &lt;SPAN&gt;· Onboarding&lt;/SPAN&gt;&lt;/DIV&gt;
&lt;DIV style="background-color: #ffffff; border-left: 5px solid #FF3621; border-radius: 0 12px 12px 0; padding: 20px 22px; margin-bottom: 6px;"&gt;&lt;A style="display: inline-block; background-color: #ffe1db; color: #c42a17; font-size: 12px; font-weight: 800; padding: 4px 12px; border-radius: 20px; text-decoration: none;" target="_blank"&gt;Onboarding · 2H&lt;/A&gt;
&lt;DIV style="font-size: 18px; font-weight: 800; color: #1b3139; line-height: 1.3; margin-top: 10px;"&gt;Get Started with AI Agents on Databricks&lt;/DIV&gt;
&lt;DIV style="font-size: 14px; line-height: 1.55; color: #5a6a72; margin-top: 8px;"&gt;Learn what agents are, how they differ from traditional AI systems, and their key components - then build, deploy, and evaluate your first agent with Mosaic AI and Agent Bricks through interactive demos and hands-on labs.&lt;/DIV&gt;
&lt;DIV style="margin-top: 14px;"&gt;&lt;A style="display: inline-block; background-color: #ff3621; color: #ffffff; font-size: 13px; font-weight: 800; text-decoration: none; padding: 10px 18px; border-radius: 8px; margin: 0 8px 8px 0;" href="https://www.databricks.com/training/catalog/get-started-with-ai-agents-on-databricks-4464?itm_source=www&amp;amp;itm_category=training&amp;amp;itm_page=catalog&amp;amp;itm_location=body&amp;amp;itm_component=general-asset-card&amp;amp;itm_offer=get-started-with-ai-agents-on-databricks-4464" target="_blank"&gt;Free · Self-paced →&lt;/A&gt; &lt;A style="display: inline-block; background-color: #ff3621; color: #ffffff; font-size: 13px; font-weight: 800; text-decoration: none; padding: 10px 18px; border-radius: 8px; margin: 0 8px 8px 0;" href="https://www.databricks.com/training/catalog/get-started-with-ai-agents-on-databricks-4459?itm_source=www&amp;amp;itm_category=training&amp;amp;itm_page=catalog&amp;amp;itm_location=body&amp;amp;itm_component=general-asset-card&amp;amp;itm_offer=get-started-with-ai-agents-on-databricks-4459" target="_blank"&gt;Free · Instructor-led →&lt;/A&gt; &lt;A style="display: inline-block; background-color: #1b3139; color: #ffffff; font-size: 13px; font-weight: 800; text-decoration: none; padding: 10px 18px; border-radius: 8px; margin: 0 8px 8px 0;" href="https://www.databricks.com/training/catalog/get-started-with-ai-agents-on-databricks-mandarin-chinese-5482?itm_source=www&amp;amp;itm_category=training&amp;amp;itm_page=catalog&amp;amp;itm_location=body&amp;amp;itm_component=general-asset-card&amp;amp;itm_offer=get-started-with-ai-agents-on-databricks-mandarin-chinese-5482" target="_blank"&gt;Free · Mandarin (中文) →&lt;/A&gt;&lt;/DIV&gt;
&lt;DIV style="font-size: 12px; line-height: 1.5; color: #8a969c; margin-top: 6px;"&gt;Self-paced also available in English, 日本語, and 한국어.&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;DIV style="margin: 30px 0 8px 0;"&gt;&lt;A style="display: inline-block; width: 34px; height: 34px; line-height: 34px; text-align: center; background-color: #ff3621; color: #ffffff; font-size: 16px; font-weight: 800; border-radius: 50%; text-decoration: none; vertical-align: middle; margin-right: 12px;" target="_blank"&gt;2&lt;/A&gt; &lt;SPAN&gt;Go deeper: Give your agents memory&lt;/SPAN&gt; &lt;SPAN&gt;· Associate&lt;/SPAN&gt;&lt;/DIV&gt;
&lt;DIV style="font-size: 15px; line-height: 1.6; color: #5a6a72; margin-bottom: 16px;"&gt;Real agents need to remember - both within a conversation and across sessions. These hands-on courses use &lt;STRONG&gt;Lakebase&lt;/STRONG&gt; as the persistence layer, with LangGraph checkpointers, MLflow tracing, and deployment as a Databricks App.&lt;/DIV&gt;
&lt;DIV style="background-color: #ffffff; border-left: 5px solid #FF3621; border-radius: 0 12px 12px 0; padding: 20px 22px; margin-bottom: 14px;"&gt;&lt;A style="display: inline-block; background-color: #ffe1db; color: #c42a17; font-size: 12px; font-weight: 800; padding: 4px 12px; border-radius: 20px; text-decoration: none;" target="_blank"&gt;Associate · 4H&lt;/A&gt;
&lt;DIV style="font-size: 18px; font-weight: 800; color: #1b3139; line-height: 1.3; margin-top: 10px;"&gt;Building Persistent Memory for AI Agents with Lakebase&lt;/DIV&gt;
&lt;DIV style="font-size: 14px; line-height: 1.55; color: #5a6a72; margin-top: 8px;"&gt;Give agents short-term and long-term memory with LangGraph's Checkpointer and Store, backed by Lakebase autoscaling. Build and test in notebooks, observe behavior through MLflow traces, and deploy a fully stateful agent as a Databricks App.&lt;/DIV&gt;
&lt;DIV style="margin-top: 14px;"&gt;&lt;A style="display: inline-block; background-color: #ff3621; color: #ffffff; font-size: 13px; font-weight: 800; text-decoration: none; padding: 10px 18px; border-radius: 8px; margin: 0 8px 8px 0;" href="https://www.databricks.com/training/catalog/building-persistent-memory-for-ai-agents-with-lakebase-on-databricks-5706?itm_source=www&amp;amp;itm_category=training&amp;amp;itm_page=catalog&amp;amp;itm_location=body&amp;amp;itm_component=general-asset-card&amp;amp;itm_offer=building-persistent-memory-for-ai-agents-with-lakebase-on-databricks-5706" target="_self"&gt;Free →&lt;/A&gt; &lt;A style="display: inline-block; background-color: #1b3139; color: #ffffff; font-size: 13px; font-weight: 800; text-decoration: none; padding: 10px 18px; border-radius: 8px; margin: 0 8px 8px 0;" href="https://www.databricks.com/training/catalog/building-persistent-memory-for-ai-agents-with-lakebase-on-databricks-5707?itm_source=www&amp;amp;itm_category=training&amp;amp;itm_page=catalog&amp;amp;itm_location=body&amp;amp;itm_component=general-asset-card&amp;amp;itm_offer=building-persistent-memory-for-ai-agents-with-lakebase-on-databricks-5707" target="_self"&gt;Paid / Subscription · Lab →&lt;/A&gt;&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;DIV style="background-color: #ffffff; border-left: 5px solid #FF3621; border-radius: 0 12px 12px 0; padding: 20px 22px; margin-bottom: 6px;"&gt;&lt;A style="display: inline-block; background-color: #ffe1db; color: #c42a17; font-size: 12px; font-weight: 800; padding: 4px 12px; border-radius: 20px; text-decoration: none;" target="_blank"&gt;Associate · 1H&lt;/A&gt;
&lt;DIV style="font-size: 18px; font-weight: 800; color: #1b3139; line-height: 1.3; margin-top: 10px;"&gt;AI Agents and Long-Term Memory on Lakebase&lt;/DIV&gt;
&lt;DIV style="font-size: 14px; line-height: 1.55; color: #5a6a72; margin-top: 8px;"&gt;A focused, hands-on lab: distinguish the three memory categories, persist a LangGraph conversation with a checkpointer, resume a thread after a restart, and recall facts across threads with a semantic store.&lt;/DIV&gt;
&lt;DIV style="margin-top: 14px;"&gt;&lt;A style="display: inline-block; background-color: #1b3139; color: #ffffff; font-size: 13px; font-weight: 800; text-decoration: none; padding: 10px 18px; border-radius: 8px; margin: 0 8px 8px 0;" href="https://www.databricks.com/training/catalog/ai-agents-and-long-term-memory-on-lakebase-6349?itm_source=www&amp;amp;itm_category=training&amp;amp;itm_page=catalog&amp;amp;itm_location=body&amp;amp;itm_component=general-asset-card&amp;amp;itm_offer=ai-agents-and-long-term-memory-on-lakebase-6349" target="_blank"&gt;Paid / Subscription · Lab →&lt;/A&gt;&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;DIV style="text-align: center; margin-top: 24px;"&gt;&lt;A style="display: inline-block; background-color: #ff3621; color: #ffffff; font-size: 15px; font-weight: 800; text-decoration: none; padding: 14px 30px; border-radius: 8px;" href="https://www.databricks.com/training/catalog?search=ai+agents" target="_blank"&gt;Browse all AI Agents courses in the catalog →&lt;/A&gt;&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;DIV style="background-color: #1b3139; padding: 22px 34px; border-radius: 0 0 14px 14px; text-align: center;"&gt;
&lt;DIV style="color: #c8d0d3; font-size: 13px; line-height: 1.6;"&gt;Reason. Act. Remember. Learn to build AI agents the right way with official Databricks training.&lt;/DIV&gt;
&lt;/DIV&gt;
&lt;/DIV&gt;</description>
      <pubDate>Fri, 25 Sep 2026 17:23:24 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/learn-databricks-ai-agents-build-deploy-amp-evaluate-on/m-p/169855#M1609</guid>
      <dc:creator>Tushar_Parekar</dc:creator>
      <dc:date>2026-09-25T17:23:24Z</dc:date>
    </item>
    <item>
      <title>Tech summit FY 2027</title>
      <link>https://community.databricks.com/t5/community-articles/tech-summit-fy-2027/m-p/169836#M1607</link>
      <description>&lt;P&gt;&lt;SPAN&gt;Just wrapped Partner Tech Summit: four days across Genie &amp;amp; Agents, the Lakehouse Platform, Lakebase and Governance. The big lesson? &lt;/SPAN&gt;&lt;STRONG&gt;AI doesn’t have an intelligence problem. It has a context problem.&lt;/STRONG&gt;&lt;SPAN&gt; Well-governed, well-organised data is what turns it into something businesses can trust.&lt;/SPAN&gt;&lt;/P&gt;</description>
      <pubDate>Fri, 25 Sep 2026 14:05:15 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/tech-summit-fy-2027/m-p/169836#M1607</guid>
      <dc:creator>DPatil</dc:creator>
      <dc:date>2026-09-25T14:05:15Z</dc:date>
    </item>
    <item>
      <title>USE CONNECTION bypasses your MCP Service's Tool Selection and Policies</title>
      <link>https://community.databricks.com/t5/community-articles/use-connection-bypasses-your-mcp-service-s-tool-selection-and/m-p/169770#M1605</link>
      <description>&lt;P&gt;I registered a mock MCP server in Unity Catalog, exposed only its read tool, attached a deny policy, and then called the delete tool anyway. What decided the outcome was access to the underlying connection, and it came from grants on the schema, not on the service.&lt;/P&gt;&lt;P align="center"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="orange-rule.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31489iAE4C6A56504062ED/image-size/large?v=v2&amp;amp;px=999" role="button" title="orange-rule.png" alt="orange-rule.png" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;BLOCKQUOTE&gt;&lt;P&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;WHAT THE EXPERIMENT SHOWED&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;Tool selection and service policies only govern calls &lt;STRONG&gt;through the MCP Service&lt;/STRONG&gt;. An identity with &lt;FONT face="courier new, courier, monospace"&gt;USE CONNECTION&lt;/FONT&gt; called the blocked delete tool through the connection proxy.&lt;/LI&gt;&lt;LI&gt;On a &lt;STRONG&gt;schema-level connection&lt;/STRONG&gt;, &lt;FONT face="courier new, courier, monospace"&gt;ALL PRIVILEGES&lt;/FONT&gt; on the schema or &lt;FONT face="courier new, courier, monospace"&gt;USE CONNECTION&lt;/FONT&gt; on the catalog also opened the proxy. A &lt;STRONG&gt;metastore-level connection&lt;/STRONG&gt; opened only for a grant on the connection itself.&lt;/LI&gt;&lt;LI&gt;Proxied calls are audited as &lt;FONT face="courier new, courier, monospace"&gt;ucHttpConnectionProxiedRequest&lt;/FONT&gt;, with the caller but &lt;STRONG&gt;no tool name&lt;/STRONG&gt;.&lt;/LI&gt;&lt;/UL&gt;&lt;/BLOCKQUOTE&gt;&lt;P&gt;An MCP Service looks like the natural security boundary for an agent tool. You pick which tools it exposes, attach service policies, grant &lt;FONT face="courier new, courier, monospace"&gt;EXECUTE&lt;/FONT&gt;, and Unity Gateway logs each call. All of that applies to calls that go through the service. The external server also sits behind a Unity Catalog HTTP connection, and the connection has its own entry point.&lt;/P&gt;&lt;P&gt;Databricks says so plainly in the registration guide:&lt;/P&gt;&lt;BLOCKQUOTE&gt;&lt;P&gt;&lt;EM&gt;"Invoking an MCP Service requires no privilege on the underlying connection—&lt;FONT face="courier new, courier, monospace"&gt;EXECUTE&lt;/FONT&gt; on the MCP Service is sufficient. Don't grant &lt;FONT face="courier new, courier, monospace"&gt;USE CONNECTION&lt;/FONT&gt; to end users: it lets them call the external server directly through the connection, or register their own MCP Service on it, bypassing the tool selection, service policies, and auditing of your MCP Service. Reserve connection access for service authors and administrators."&lt;/EM&gt;&lt;/P&gt;&lt;P&gt;— &lt;A href="https://docs.databricks.com/aws/en/ai-gateway/register-mcp-service" target="_blank" rel="noopener"&gt;Register an external MCP server&lt;/A&gt;&lt;/P&gt;&lt;/BLOCKQUOTE&gt;&lt;P&gt;I wanted to see what each part of that warning looks like in practice.&lt;/P&gt;&lt;P align="center"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="A caller reaches the same external MCP server by two paths: the MCP Service path checks EXECUTE, tool selection and the service policy; the connection proxy path checks USE CONNECTION only. Both arrive with the same bearer token." style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31487i631A2E5EEB04CF54/image-size/large?v=v2&amp;amp;px=999" role="button" title="two-paths.png" alt="two-paths.png" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;P align="center"&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;Figure 1.&lt;/STRONG&gt;&lt;/FONT&gt; &lt;EM&gt;Two paths to one server. &lt;FONT face="courier new, courier, monospace"&gt;POST /ai-gateway/mcp-services/&amp;lt;name&amp;gt;&lt;/FONT&gt; runs the service's tool selection, policies and &lt;FONT face="courier new, courier, monospace"&gt;mcpCall&lt;/FONT&gt; audit. &lt;FONT face="courier new, courier, monospace"&gt;POST /api/2.0/unity-catalog/connections/&amp;lt;name&amp;gt;/proxy&lt;/FONT&gt; checks connection privileges and forwards the request; none of the service controls run.&lt;/EM&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#E0661A"&gt;The setup&lt;/FONT&gt;&lt;/H2&gt;&lt;UL&gt;&lt;LI&gt;A small FastMCP server with two tools: &lt;FONT face="courier new, courier, monospace"&gt;get_inventory&lt;/FONT&gt; (read) and &lt;FONT face="courier new, courier, monospace"&gt;delete_inventory_record&lt;/FONT&gt;, a mock write that returns &lt;FONT face="courier new, courier, monospace"&gt;"deleted": true&lt;/FONT&gt; and deletes nothing. Bearer-token protected, published through a temporary Cloudflare tunnel.&lt;/LI&gt;&lt;LI&gt;A schema-level HTTP connection, &lt;FONT face="courier new, courier, monospace"&gt;workspace.mcp_boundary.inventory_conn&lt;/FONT&gt;, which is the placement the docs recommend.&lt;/LI&gt;&lt;LI&gt;An MCP Service, &lt;FONT face="courier new, courier, monospace"&gt;inventory_readonly&lt;/FONT&gt;, with &lt;FONT face="courier new, courier, monospace"&gt;include_tool_selectors: ["get_*"]&lt;/FONT&gt;.&lt;/LI&gt;&lt;LI&gt;A custom service policy (Beta) that returns &lt;FONT face="courier new, courier, monospace"&gt;DENY&lt;/FONT&gt; when the tool is &lt;FONT face="courier new, courier, monospace"&gt;delete_inventory_record&lt;/FONT&gt;.&lt;/LI&gt;&lt;LI&gt;Three service principals calling with OAuth machine-to-machine tokens:&lt;UL&gt;&lt;LI&gt;&lt;STRONG&gt;A&lt;/STRONG&gt; has &lt;FONT face="courier new, courier, monospace"&gt;USE CATALOG&lt;/FONT&gt;, &lt;FONT face="courier new, courier, monospace"&gt;USE SCHEMA&lt;/FONT&gt; and &lt;FONT face="courier new, courier, monospace"&gt;EXECUTE&lt;/FONT&gt; on the service.&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;B&lt;/STRONG&gt; has the same, plus &lt;FONT face="courier new, courier, monospace"&gt;USE CONNECTION&lt;/FONT&gt;.&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;C&lt;/STRONG&gt; has &lt;FONT face="courier new, courier, monospace"&gt;USE CATALOG&lt;/FONT&gt; and &lt;FONT face="courier new, courier, monospace"&gt;ALL PRIVILEGES&lt;/FONT&gt; on the schema, and nothing granted on the service or the connection.&lt;/LI&gt;&lt;/UL&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;Each identity sent &lt;FONT face="courier new, courier, monospace"&gt;tools/list&lt;/FONT&gt;, a read and a delete down both paths.&lt;/P&gt;&lt;P align="center"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="The MCP Service in Catalog Explorer with one tool listed: get_inventory." style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31482iEA9109B6A94CF76B/image-size/large?v=v2&amp;amp;px=999" role="button" title="service-tools.jpg" alt="service-tools.jpg" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;P align="center"&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;Figure 2.&lt;/STRONG&gt;&lt;/FONT&gt; &lt;EM&gt;The service as its consumers see it: one tool, &lt;FONT face="courier new, courier, monospace"&gt;get_inventory&lt;/FONT&gt;. The server behind it has two.&lt;/EM&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#E0661A"&gt;What each identity could reach&lt;/FONT&gt;&lt;/H2&gt;&lt;TABLE&gt;&lt;TBODY&gt;&lt;TR&gt;&lt;TD&gt;&lt;STRONG&gt;Identity&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;&lt;STRONG&gt;Service: tools/list&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;&lt;STRONG&gt;Service: delete&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;&lt;STRONG&gt;Proxy: tools/list&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;&lt;STRONG&gt;Proxy: delete&lt;/STRONG&gt;&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;&lt;STRONG&gt;A (EXECUTE only)&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;get_inventory&lt;/TD&gt;&lt;TD&gt;-32003 blocked&lt;/TD&gt;&lt;TD&gt;403&lt;/TD&gt;&lt;TD&gt;403&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;&lt;STRONG&gt;B (+ USE CONNECTION)&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;get_inventory&lt;/TD&gt;&lt;TD&gt;-32003 blocked&lt;/TD&gt;&lt;TD&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;both tools&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/TD&gt;&lt;TD&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;deleted: true&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;&lt;STRONG&gt;C (ALL PRIVILEGES on schema)&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;get_inventory&lt;/TD&gt;&lt;TD&gt;-32003 blocked&lt;/TD&gt;&lt;TD&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;both tools&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/TD&gt;&lt;TD&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;deleted: true&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/TD&gt;&lt;/TR&gt;&lt;/TBODY&gt;&lt;/TABLE&gt;&lt;P&gt;Through the service, all three saw one tool, and the delete came back with the documented error, &lt;FONT face="courier new, courier, monospace"&gt;-32003 "Tool not allowed by MCP service configuration."&lt;/FONT&gt; On the proxy they split. A got &lt;FONT face="courier new, courier, monospace"&gt;403 PERMISSION_DENIED&lt;/FONT&gt;. B and C listed both tools and ran the delete:&lt;/P&gt;&lt;PRE&gt;=== User B: EXECUTE + USE CONNECTION ===
MCP Service (Unity Gateway)  tools/list                    HTTP 200  tools=['get_inventory']
MCP Service (Unity Gateway)  call delete_inventory_record  HTTP 200  {"code": -32003, "message": "Tool not allowed by MCP service configuration."}
Connection proxy (direct)    tools/list                    HTTP 200  tools=['get_inventory', 'delete_inventory_record']
Connection proxy (direct)    call delete_inventory_record  HTTP 200  result={"sku":"SKU-001","deleted":true,"note":"MOCK - nothing was actually deleted"}&lt;/PRE&gt;&lt;H2&gt;&lt;FONT color="#E0661A"&gt;The policy never sees the proxy&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;For a second run I widened the tool selection to every tool, so the deny policy was the only guard left. Through the service, both A and B got the policy's reason back as a JSON-RPC error: &lt;FONT face="courier new, courier, monospace"&gt;-32003 "Deleting inventory records is not permitted through this service."&lt;/FONT&gt; B then sent the same delete to the proxy and got &lt;FONT face="courier new, courier, monospace"&gt;"deleted": true&lt;/FONT&gt;. The policy ran on the path it is attached to. Nothing consulted it on the other path. (The policy docs describe a block as an HTTP 200 response carrying a &lt;FONT face="courier new, courier, monospace"&gt;databricks_service_policy&lt;/FONT&gt; object; through this MCP Service it arrived as the JSON-RPC error above.)&lt;/P&gt;&lt;P align="center"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="The Policies tab of the MCP Service showing deny_inventory_delete applied to all account users." style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31481i710109200EE26EE5/image-size/large?v=v2&amp;amp;px=999" role="button" title="service-policy.jpg" alt="service-policy.jpg" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;P align="center"&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;Figure 3.&lt;/STRONG&gt;&lt;/FONT&gt; &lt;EM&gt;The deny policy is attached to the service and applies to all account users. It does not apply to the connection.&lt;/EM&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#E0661A"&gt;USE CONNECTION can come from the schema&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;User C mattered more than I expected. Nobody granted C &lt;FONT face="courier new, courier, monospace"&gt;USE CONNECTION&lt;/FONT&gt;. C held &lt;FONT face="courier new, courier, monospace"&gt;ALL PRIVILEGES&lt;/FONT&gt; on the schema that contains the connection, and its proxy calls went through exactly as B's did. When I revoked that grant and left C with &lt;FONT face="courier new, courier, monospace"&gt;USE SCHEMA&lt;/FONT&gt; only, the proxy returned &lt;FONT face="courier new, courier, monospace"&gt;403&lt;/FONT&gt; and the service returned &lt;FONT face="courier new, courier, monospace"&gt;-32007 "Not authorized to invoke MCP service."&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;This follows from the docs' own advice. They recommend creating the connection at the schema level "so it's governed alongside the MCP Service." In this workspace the schema-level connection picked up schema grants, so &lt;FONT face="courier new, courier, monospace"&gt;ALL PRIVILEGES&lt;/FONT&gt; or &lt;FONT face="courier new, courier, monospace"&gt;USE CONNECTION&lt;/FONT&gt; on the schema reached the connection. The schema was also the only place I could grant it: &lt;FONT face="courier new, courier, monospace"&gt;GRANT … ON CONNECTION&lt;/FONT&gt; with the three-part name did not parse, and the connection's Permissions tab returned an error. Here, a review that only read the connection's own permission list would have missed both grants.&lt;/P&gt;&lt;P align="center"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="Schema permissions: A has USE SCHEMA; C has ALL PRIVILEGES; B has CREATE SERVICE, USE CONNECTION and USE SCHEMA." style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31483i6241D92238A0EE4D/image-size/large?v=v2&amp;amp;px=999" role="button" title="schema-grants.jpg" alt="schema-grants.jpg" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;P align="center"&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;Figure 4.&lt;/STRONG&gt;&lt;/FONT&gt; &lt;EM&gt;Grants on the schema, not on the connection. Two of these rows open the proxy path.&lt;/EM&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#E0661A"&gt;Where the connection lives decides who inherits it&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;To check that placement was the cause, I created a second connection to the same server at the metastore level, with its own MCP Service limited to &lt;FONT face="courier new, courier, monospace"&gt;get_*&lt;/FONT&gt;. I also added User D, who has &lt;FONT face="courier new, courier, monospace"&gt;USE CONNECTION&lt;/FONT&gt; on the catalog and &lt;FONT face="courier new, courier, monospace"&gt;EXECUTE&lt;/FONT&gt; on both services. Then I ran every identity against both connections.&lt;/P&gt;&lt;P align="center"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="Nested view of metastore, catalog and schema: the schema-level connection opened for B, C and D; the metastore-level connection opened only for B." style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31488i746F77DD59E85D9F/image-size/large?v=v2&amp;amp;px=999" role="button" title="placement.png" alt="placement.png" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;P align="center"&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;Figure 5.&lt;/STRONG&gt;&lt;/FONT&gt; &lt;EM&gt;The same grants, two placements. Grants on the schema and catalog reached the connection inside them and stopped at the one outside.&lt;/EM&gt;&lt;/P&gt;&lt;TABLE&gt;&lt;TBODY&gt;&lt;TR&gt;&lt;TD&gt;&lt;STRONG&gt;Identity&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;&lt;STRONG&gt;Grant that could open the proxy&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;&lt;STRONG&gt;Schema-level connection&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;&lt;STRONG&gt;Metastore-level connection&lt;/STRONG&gt;&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;&lt;STRONG&gt;A&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;none&lt;/TD&gt;&lt;TD&gt;403&lt;/TD&gt;&lt;TD&gt;403&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;&lt;STRONG&gt;B&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;&lt;FONT face="courier new, courier, monospace"&gt;USE CONNECTION&lt;/FONT&gt; on the schema; on the metastore connection directly&lt;/TD&gt;&lt;TD&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;both tools, deleted: true&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/TD&gt;&lt;TD&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;both tools, deleted: true&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;&lt;STRONG&gt;C&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;&lt;FONT face="courier new, courier, monospace"&gt;ALL PRIVILEGES&lt;/FONT&gt; on the schema&lt;/TD&gt;&lt;TD&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;both tools, deleted: true&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/TD&gt;&lt;TD&gt;403&lt;/TD&gt;&lt;/TR&gt;&lt;TR&gt;&lt;TD&gt;&lt;STRONG&gt;D&lt;/STRONG&gt;&lt;/TD&gt;&lt;TD&gt;&lt;FONT face="courier new, courier, monospace"&gt;USE CONNECTION&lt;/FONT&gt; on the catalog&lt;/TD&gt;&lt;TD&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;both tools, deleted: true&lt;/STRONG&gt;&lt;/FONT&gt;&lt;/TD&gt;&lt;TD&gt;403&lt;/TD&gt;&lt;/TR&gt;&lt;/TBODY&gt;&lt;/TABLE&gt;&lt;P&gt;Through either MCP Service, all four saw only &lt;FONT face="courier new, courier, monospace"&gt;get_inventory&lt;/FONT&gt;, and the delete returned &lt;FONT face="courier new, courier, monospace"&gt;-32003&lt;/FONT&gt;. On the proxy, the schema-level connection accepted grants from its schema and its catalog. The metastore-level connection accepted only the grant made on the connection itself, and refused everyone else with &lt;FONT face="courier new, courier, monospace"&gt;User does not have any privileges on Connection 'mcp_boundary_ms_conn'&lt;/FONT&gt;. The docs' reason for recommending the schema level is that the connection is then governed alongside the MCP Service. The same placement lets connection access arrive from the schema and the catalog above it.&lt;/P&gt;&lt;P align="center"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="Permissions of the metastore-level connection: one row, mcp-exp-user-b with USE CONNECTION." style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31486i42B1CD53F182857E/image-size/large?v=v2&amp;amp;px=999" role="button" title="ms-conn-grants.jpg" alt="ms-conn-grants.jpg" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;P align="center"&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;Figure 6.&lt;/STRONG&gt;&lt;/FONT&gt; &lt;EM&gt;At the metastore level the connection has its own permission list, and it was the only grant that opened the proxy.&lt;/EM&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#E0661A"&gt;The second bypass: your own MCP Service&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;The warning also covers registering another MCP Service on the same connection. That needs &lt;FONT face="courier new, courier, monospace"&gt;CREATE SERVICE&lt;/FONT&gt; too. B's first attempt failed with &lt;FONT face="courier new, courier, monospace"&gt;User does not have CREATE SERVICE on Schema&lt;/FONT&gt;. After I added that grant, B created &lt;FONT face="courier new, courier, monospace"&gt;shadow_b&lt;/FONT&gt; with no tool selection; it listed both tools and ran the delete. As the schema owner, and not a metastore admin, my list of MCP services in the schema still returned only &lt;FONT face="courier new, courier, monospace"&gt;inventory_readonly&lt;/FONT&gt;. C also created a service, but calls through it failed with &lt;FONT face="courier new, courier, monospace"&gt;Insufficient privileges on connection … to vend credentials&lt;/FONT&gt;. I can't explain that difference yet.&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#E0661A"&gt;What the logs show&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;The mock server can't tell the callers apart. A's governed read and B's direct delete arrived with the same header set: &lt;FONT face="courier new, courier, monospace"&gt;User-Agent: Databricks-MCP-Proxy/1.0&lt;/FONT&gt;, the connection's one bearer token, and no header naming the caller or the path. Calls blocked by tool selection or the policy never reached it.&lt;/P&gt;&lt;P&gt;Databricks does record both paths in &lt;FONT face="courier new, courier, monospace"&gt;system.access.audit&lt;/FONT&gt;, at different levels of detail. A governed call is an &lt;FONT face="courier new, courier, monospace"&gt;mcpCall&lt;/FONT&gt; row with the service name and &lt;FONT face="courier new, courier, monospace"&gt;tool_name&lt;/FONT&gt;. B's blocked delete is an &lt;FONT face="courier new, courier, monospace"&gt;mcpCall&lt;/FONT&gt; row with status &lt;FONT face="courier new, courier, monospace"&gt;403&lt;/FONT&gt; and &lt;FONT face="courier new, courier, monospace"&gt;tool_name&lt;/FONT&gt; &lt;FONT face="courier new, courier, monospace"&gt;delete_inventory_record&lt;/FONT&gt;, even though the caller received HTTP 200 with a JSON-RPC error. A proxied call is a &lt;FONT face="courier new, courier, monospace"&gt;ucHttpConnectionProxiedRequest&lt;/FONT&gt; row with the caller and the connection, but no tool name. You can see that B used the connection. You can't see from that row that B ran a delete. Calls through &lt;FONT face="courier new, courier, monospace"&gt;shadow_b&lt;/FONT&gt; appear as &lt;FONT face="courier new, courier, monospace"&gt;mcpCall&lt;/FONT&gt; rows naming &lt;FONT face="courier new, courier, monospace"&gt;shadow_b&lt;/FONT&gt;. This query surfaces all three patterns. Replace the service name with your own:&lt;/P&gt;&lt;PRE&gt;SELECT event_time,
       coalesce(user_identity.email, user_identity.subject_name) AS caller,
       action_name,
       response.status_code AS status,
       request_params['connection_name'] AS connection_name,
       coalesce(request_params['fully_qualified_name'], request_params['mcp_service_id']) AS mcp_service,
       request_params['tool_name'] AS tool_name
FROM system.access.audit
WHERE event_date &amp;gt;= current_date() - INTERVAL 30 DAYS
  AND (
        action_name = 'ucHttpConnectionProxiedRequest'
     OR action_name = 'createMcpService'
     OR (action_name = 'mcpCall'
         AND coalesce(request_params['fully_qualified_name'], '')
             &amp;lt;&amp;gt; 'workspace.mcp_boundary.inventory_readonly')
  )
ORDER BY event_time DESC&lt;/PRE&gt;&lt;P&gt;In my workspace it returned 30 rows. They included every proxied request (B's and C's succeeded, A's were denied with &lt;FONT face="courier new, courier, monospace"&gt;403&lt;/FONT&gt;), the &lt;FONT face="courier new, courier, monospace"&gt;createMcpService&lt;/FONT&gt; rows for &lt;FONT face="courier new, courier, monospace"&gt;shadow_b&lt;/FONT&gt; and &lt;FONT face="courier new, courier, monospace"&gt;shadow_c&lt;/FONT&gt;, and the &lt;FONT face="courier new, courier, monospace"&gt;mcpCall&lt;/FONT&gt; rows through both. Audit rows appeared roughly 7 to 15 minutes after the calls.&lt;/P&gt;&lt;P align="center"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="system.access.audit rows for A and B: mcpCall rows name the tool; ucHttpConnectionProxiedRequest rows name only the connection." style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31485i770E783A4FC99D5E/image-size/large?v=v2&amp;amp;px=999" role="button" title="audit-rows.jpg" alt="audit-rows.jpg" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;P align="center"&gt;&lt;FONT color="#E0661A"&gt;&lt;STRONG&gt;Figure 7.&lt;/STRONG&gt;&lt;/FONT&gt; &lt;EM&gt;&lt;FONT face="courier new, courier, monospace"&gt;system.access.audit&lt;/FONT&gt; for A and B during the first run, with identities relabelled. Service calls name the tool. Proxied calls name only the connection.&lt;/EM&gt;&lt;/P&gt;&lt;P align="center"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="orange-rule.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31489iAE4C6A56504062ED/image-size/large?v=v2&amp;amp;px=999" role="button" title="orange-rule.png" alt="orange-rule.png" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#E0661A"&gt;The permission model to use&lt;/FONT&gt;&lt;/H2&gt;&lt;UL&gt;&lt;LI&gt;&lt;STRONG&gt;Consumers:&lt;/STRONG&gt; &lt;FONT face="courier new, courier, monospace"&gt;EXECUTE&lt;/FONT&gt; on the MCP Service, plus &lt;FONT face="courier new, courier, monospace"&gt;USE CATALOG&lt;/FONT&gt; and &lt;FONT face="courier new, courier, monospace"&gt;USE SCHEMA&lt;/FONT&gt;. That was all A needed to use the read tool, and it reached nothing else.&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Service authors and admins:&lt;/STRONG&gt; &lt;FONT face="courier new, courier, monospace"&gt;USE CONNECTION&lt;/FONT&gt;, and &lt;FONT face="courier new, courier, monospace"&gt;CREATE SERVICE&lt;/FONT&gt; where they register services.&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;A schema-level connection:&lt;/STRONG&gt; give consumer groups &lt;FONT face="courier new, courier, monospace"&gt;USE SCHEMA&lt;/FONT&gt; and &lt;FONT face="courier new, courier, monospace"&gt;USE CATALOG&lt;/FONT&gt; only. &lt;FONT face="courier new, courier, monospace"&gt;ALL PRIVILEGES&lt;/FONT&gt; or &lt;FONT face="courier new, courier, monospace"&gt;USE CONNECTION&lt;/FONT&gt; on that schema, or &lt;FONT face="courier new, courier, monospace"&gt;USE CONNECTION&lt;/FONT&gt; on its catalog, is connection access. Keep &lt;FONT face="courier new, courier, monospace"&gt;CREATE SERVICE&lt;/FONT&gt; for service authors.&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;A metastore-level connection:&lt;/STRONG&gt; it did not inherit the schema or catalog grants I tested, so its own permission list is the one to review.&lt;/LI&gt;&lt;LI&gt;&lt;STRONG&gt;Reviews:&lt;/STRONG&gt; for a schema-level connection, check grants on the connection, its schema and its catalog. Run the audit query above for proxied requests and for services you didn't create.&lt;/LI&gt;&lt;/UL&gt;&lt;H2&gt;&lt;FONT color="#E0661A"&gt;Conclusion&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;An MCP Service is a real control. In this experiment, tool selection hid the delete tool, the deny policy blocked it, and the audit log recorded the blocked attempt with the tool's name. None of that applied to the connection underneath. Any identity that could use the connection reached the server directly, and on a schema-level connection that included identities whose only relevant grant was on the schema or catalog above it.&lt;/P&gt;&lt;P&gt;So when you review who can reach an MCP server, start with the connection: where it lives, who holds &lt;FONT face="courier new, courier, monospace"&gt;USE CONNECTION&lt;/FONT&gt; on it, and which grants on the levels above it carry that privilege down. Give consumers &lt;FONT face="courier new, courier, monospace"&gt;EXECUTE&lt;/FONT&gt; on the service and nothing that adds up to &lt;FONT face="courier new, courier, monospace"&gt;USE CONNECTION&lt;/FONT&gt;.&lt;/P&gt;</description>
      <pubDate>Fri, 25 Sep 2026 01:06:55 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/use-connection-bypasses-your-mcp-service-s-tool-selection-and/m-p/169770#M1605</guid>
      <dc:creator>ivanvyd</dc:creator>
      <dc:date>2026-09-25T01:06:55Z</dc:date>
    </item>
    <item>
      <title>Tags no Databricks: Governança, FinOps e MLOps</title>
      <link>https://community.databricks.com/t5/community-articles/tags-no-databricks-governan%C3%A7a-finops-e-mlops/m-p/169706#M1598</link>
      <description>&lt;P&gt;&lt;SPAN&gt;Tags no&lt;/SPAN&gt; &lt;SPAN class=""&gt;&lt;A class="" href="https://www.linkedin.com/company/databricks/" target="_blank" rel="noopener" aria-label="Ver empresa: Databricks"&gt;&lt;STRONG&gt;&lt;SPAN class=""&gt;Databricks&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/A&gt;&lt;/SPAN&gt;&lt;BR /&gt;&lt;BR /&gt;&lt;SPAN&gt;Você acha que tags no Databricks são só para billing?&lt;/SPAN&gt;&lt;BR /&gt;&lt;BR /&gt;&lt;SPAN&gt;Em 2026, uma estratégia de tags fraca não é apenas ineficiente — é uma falha de arquitetura. Custos que explodem, dados sensíveis expostos e um MLOps caótico são apenas alguns dos sintomas.&lt;/SPAN&gt;&lt;BR /&gt;&lt;BR /&gt;&lt;SPAN&gt;Escrevi um artigo sobre o tema, mergulhando fundo em:&lt;/SPAN&gt;&lt;BR /&gt;&lt;SPAN&gt;&lt;span class="lia-unicode-emoji" title=":small_blue_diamond:"&gt;🔹&lt;/span&gt; A Tríade da Governança: Ungoverned, Governed e as novas System Tags&lt;/SPAN&gt;&lt;BR /&gt;&lt;SPAN&gt;&lt;span class="lia-unicode-emoji" title=":small_blue_diamond:"&gt;🔹&lt;/span&gt; FinOps de Verdade: A fórmula para calcular o TCO real (DBU + Infra) por tag&lt;/SPAN&gt;&lt;BR /&gt;&lt;SPAN&gt;&lt;span class="lia-unicode-emoji" title=":small_blue_diamond:"&gt;🔹&lt;/span&gt; ABAC sem Erros: A "Regra de Ouro" da herança de tags que 99% dos times erram&lt;/SPAN&gt;&lt;BR /&gt;&lt;SPAN&gt;&lt;span class="lia-unicode-emoji" title=":small_blue_diamond:"&gt;🔹&lt;/span&gt; Hands-On Completo: Códigos em SQL, Python (MLflow) e Terraform&lt;/SPAN&gt;&lt;BR /&gt;&lt;BR /&gt;&lt;SPAN&gt;Este não é um artigo superficial. &lt;/SPAN&gt;&lt;BR /&gt;&lt;SPAN&gt;É um deep dive técnico para Arquitetos de Dados, Engenheiros e especialistas em FinOps/MLOps que se recusam a aceitar o básico.&lt;/SPAN&gt;&lt;BR /&gt;&lt;BR /&gt;&lt;SPAN&gt;&lt;span class="lia-unicode-emoji" title=":backhand_index_pointing_down:"&gt;👇&lt;/span&gt; Leia o artigo completo.&lt;/SPAN&gt;&lt;/P&gt;&lt;P&gt;&lt;A href="https://www.linkedin.com/pulse/tags-databricks-governan%C3%A7a-finops-e-mlops-thomaz-antonio-rossito-neto-ow0nf/" target="_blank" rel="noopener"&gt;link artigo&lt;/A&gt;&lt;BR /&gt;&lt;BR /&gt;&lt;SPAN class=""&gt;&lt;A class="" href="https://www.linkedin.com/search/results/all/?keywords=%23databricks&amp;amp;origin=HASH_TAG_FROM_FEED" target="_blank" rel="noopener" aria-label="Ver hashtag: #databricks"&gt;&lt;STRONG&gt;#Databricks&lt;/STRONG&gt;&lt;/A&gt;&lt;/SPAN&gt; &lt;SPAN class=""&gt;&lt;A class="" href="https://www.linkedin.com/search/results/all/?keywords=%23datagovernance&amp;amp;origin=HASH_TAG_FROM_FEED" target="_blank" rel="noopener" aria-label="Ver hashtag: #datagovernance"&gt;&lt;STRONG&gt;#DataGovernance&lt;/STRONG&gt;&lt;/A&gt;&lt;/SPAN&gt; &lt;SPAN class=""&gt;&lt;A class="" href="https://www.linkedin.com/search/results/all/?keywords=%23finops&amp;amp;origin=HASH_TAG_FROM_FEED" target="_blank" rel="noopener" aria-label="Ver hashtag: #finops"&gt;&lt;STRONG&gt;#FinOps&lt;/STRONG&gt;&lt;/A&gt;&lt;/SPAN&gt; &lt;SPAN class=""&gt;&lt;A class="" href="https://www.linkedin.com/search/results/all/?keywords=%23mlops&amp;amp;origin=HASH_TAG_FROM_FEED" target="_blank" rel="noopener" aria-label="Ver hashtag: #mlops"&gt;&lt;STRONG&gt;#MLOps&lt;/STRONG&gt;&lt;/A&gt;&lt;/SPAN&gt; &lt;SPAN class=""&gt;&lt;A class="" href="https://www.linkedin.com/search/results/all/?keywords=%23unitycatalog&amp;amp;origin=HASH_TAG_FROM_FEED" target="_blank" rel="noopener" aria-label="Ver hashtag: #unitycatalog"&gt;&lt;STRONG&gt;#UnityCatalog&lt;/STRONG&gt;&lt;/A&gt;&lt;/SPAN&gt; &lt;SPAN class=""&gt;&lt;A class="" href="https://www.linkedin.com/search/results/all/?keywords=%23dataarchitecture&amp;amp;origin=HASH_TAG_FROM_FEED" target="_blank" rel="noopener" aria-label="Ver hashtag: #dataarchitecture"&gt;&lt;STRONG&gt;#DataArchitecture&lt;/STRONG&gt;&lt;/A&gt;&lt;/SPAN&gt; &lt;SPAN class=""&gt;&lt;A class="" href="https://www.linkedin.com/search/results/all/?keywords=%23cloud&amp;amp;origin=HASH_TAG_FROM_FEED" target="_blank" rel="noopener" aria-label="Ver hashtag: #cloud"&gt;&lt;STRONG&gt;#Cloud&lt;/STRONG&gt;&lt;/A&gt;&lt;/SPAN&gt;&lt;/P&gt;</description>
      <pubDate>Thu, 24 Sep 2026 14:19:52 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/tags-no-databricks-governan%C3%A7a-finops-e-mlops/m-p/169706#M1598</guid>
      <dc:creator>ThomazNeto</dc:creator>
      <dc:date>2026-09-24T14:19:52Z</dc:date>
    </item>
    <item>
      <title>Spark Internals on Databricks</title>
      <link>https://community.databricks.com/t5/community-articles/spark-internals-on-databricks/m-p/169683#M1596</link>
      <description>&lt;P&gt;Every Spark engineer eventually hits the same wall: the query works, but you don't know why it's slow, or why it suddenly got fast after a one-line config change. The gap between "I can write Spark SQL" and "I can reason about Spark's execution" is almost entirely a gap in understanding four subsystems — the Catalyst optimizer, Adaptive Query Execution, the shuffle mechanism, and Spark's memory model — plus, on Databricks specifically, how Photon layers on top of all of it. This post walks through each one, not as an API reference, but as a mental model you can use the next time a query profile looks wrong and you need to know where to look.&lt;/P&gt;&lt;P&gt;&lt;FONT size="5"&gt;From SQL to Execution: The Catalyst Optimizer&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;When you submit a query — SQL, DataFrame API, or a CLUSTER BY DDL statement — none of it runs directly. It first becomes a tree of logical operations, and Catalyst's job is to turn that tree into something the cluster can actually execute efficiently. This happens in four phases.&lt;/P&gt;&lt;P&gt;Analysis resolves names. Every column reference, table reference, and function call in your query is unresolved at first — Catalyst doesn't know that f.customer_level refers to a real column until it looks up the schema of fact_profitability_actuals in the catalog. This is also where type coercion happens: if you compare a string column to an integer literal, analysis is where Catalyst decides how to cast one to match the other. A query that fails with AnalysisException failed here, before a single byte of data was touched.&lt;/P&gt;&lt;P&gt;Logical optimization is where the famous rule-based transformations live: predicate pushdown, constant folding, column pruning, null propagation. This is the phase that decides whether your WHERE fiscper &amp;gt;= ... filter gets pushed all the way down to the file scan (so files get skipped) or gets applied late (so everything gets read first). It's also where Catalyst can fold CONCAT(CAST(YEAR(CURRENT_DATE())...)) into a literal at plan time, which is why constant date-range expressions in a WHERE clause don't need to be manually pre-computed — Catalyst does it for you, once, before the physical plan is even generated.&lt;/P&gt;&lt;P&gt;Physical planning is where logical operations get mapped onto actual Spark execution strategies. A logical Join node becomes one of several physical join implementations — broadcast hash join, sort-merge join, shuffle hash join — and Catalyst picks based on cost estimates: table sizes, whether statistics are available, and configured thresholds like spark.sql.autoBroadcastJoinThreshold. This is the single most consequential decision Catalyst makes for a large query, and it's also the one most likely to go wrong when statistics are stale or missing, which is why ANALYZE TABLE and Delta's file-level stats matter more than most people realize.&lt;/P&gt;&lt;P&gt;Code generation is the last step: Spark doesn't interpret the physical plan row by row. It generates actual JVM bytecode (whole-stage code generation) for chains of operators, fusing them into a single tight loop rather than paying virtual-call overhead per row per operator. This is why a filter-then-project-then-aggregate chain in Spark can be competitive with hand-written Scala — the interpretation overhead is compiled away.&lt;/P&gt;&lt;P&gt;The practical upshot: when a query is slow, EXPLAIN FORMATTED isn't just diagnostic theater. It's the only way to see whether logical optimization pushed your filters where you think it did, whether physical planning chose the join strategy you expect, and — critically on Databricks — whether Photon accepted or rejected each operator, since Photon operators are annotated distinctly in the plan.&lt;/P&gt;</description>
      <pubDate>Thu, 24 Sep 2026 11:20:08 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/spark-internals-on-databricks/m-p/169683#M1596</guid>
      <dc:creator>aayush_410</dc:creator>
      <dc:date>2026-09-24T11:20:08Z</dc:date>
    </item>
    <item>
      <title>Real-Time SAP Accounts Receivable on Databricks Lakeflow Declarative Pipelines</title>
      <link>https://community.databricks.com/t5/community-articles/real-time-sap-accounts-receivable-on-databricks-lakeflow/m-p/169616#M1594</link>
      <description>&lt;P&gt;&lt;STRONG&gt;One way to keep your accounts receivable information current in real time on Databricks, with the change semantics written into the pipeline definition instead of reconstructed by whoever reads the table. Here is how the data product behind it is structured, what it exposes to whoever queries it, and where every tile on the dashboard comes from.&lt;/STRONG&gt;&lt;/P&gt;&lt;P&gt;Imagine opening your receivables view at eleven in the morning and knowing it is current. Not current as of last night's close. Current as of the invoice that posted four minutes ago and the payment that cleared while you were reading the screen.&lt;/P&gt;&lt;P&gt;Now imagine the thing that makes it correct not being a pipeline somebody tuned, but a block of code you can read: this table's key is these three columns, order its changes by this clock, and when the change type is a delete, delete the row.&lt;/P&gt;&lt;P&gt;That is what an accounts receivable data product on Databricks can be, and it is less about speed than it sounds. What makes it work is that everything underneath is structured so a single row can be corrected in place at the moment the transaction that changes it happens. Get the structure right and freshness is something you schedule. Get it wrong and no amount of streaming saves you, because you will simply be wrong faster.&lt;/P&gt;&lt;P&gt;So this goes from the bottom up, using the Account Receivable Visibility data product as the worked example. Sources, layers, what it publishes, and where every tile on the dashboard comes from.&lt;/P&gt;&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="Josue2603_0-1790183492567.png" style="width: 400px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31444i053DD29DD4E68927/image-size/medium?v=v2&amp;amp;px=400" role="button" title="Josue2603_0-1790183492567.png" alt="Josue2603_0-1790183492567.png" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;P&gt;&lt;EM&gt;The whole shape on one page. Everything below is a closer look at one of these four columns.&lt;/EM&gt;&lt;/P&gt;&lt;H2&gt;&lt;STRONG&gt;The shape of the flow: SAP, then Kafka, then Databricks&lt;/STRONG&gt;&lt;/H2&gt;&lt;P&gt;Three hops, and it is worth stating them plainly before anything else, because everything after this depends on the shape.&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;SAP to Kafka.&lt;/STRONG&gt; This hop starts with a definition, not with code. In the One Connect SAP Data Modeler you declare what a data product is, and the declaration is the deliverable: which SAP tables belong to it, how they join, and what every field is called on the way out.&lt;/P&gt;&lt;P&gt;That definition ships as a pair of spreadsheets, which sounds unglamorous and is the point, because a spreadsheet is a thing a functional consultant can read and argue with. For this product the first one declares the sixteen entities: source table, join fields on both sides, join type, its level in the hierarchy, a business alias, and any filter. VBAK joined to VBAP on VBELN, inner, level two, filtered to order type OR. The second declares 1,203 fields across those entities, and for each one the source table and field, the business name it is published under, whether it is a key, whether it is selectable, and a description. VBRK.VBELN becomes bill_doc, marked as key.&lt;/P&gt;&lt;P&gt;So the join topology and the field semantics are declared once, in a file, before anything streams. What the business events then publish to Kafka is that declaration made live. One topic per source table, created as needed, with schema registry entries and schema evolution handled at that boundary. The precise capture mechanism is a later section; for now what matters is that Kafka is where SAP data lands, not where it is queried.&lt;/P&gt;&lt;P&gt;And you rarely start from an empty modeler. The Onibex Marketplace lists 159 pre-modeled data products across eleven SAP modules, 97 built from tables and 60 from CDS views. The sixteen this product needs come from that catalogue, which is why the interesting work in a receivables project is the last mile rather than the first.&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;Kafka to Databricks.&lt;/STRONG&gt; From those topics the data lands in bronze Delta tables in Unity Catalog, one per SAP source table, and everything above bronze is built by a declarative pipeline that reads the change data feed of the table below it. No orchestrator deciding what changed, no high water mark column somebody has to maintain, no nightly full reload standing in for incremental logic. The table's own commit history is the input.&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;Databricks to whoever asks.&lt;/STRONG&gt; A dashboard, a warehouse, a CRM, a machine learning workload. They query the top layer and never touch SAP. The dashboard that ships with this product is a single Power BI report, and the rest of that list is whatever else in the organization can already read a table.&lt;/P&gt;&lt;P&gt;The reason to separate those three hops rather than build one pipeline from SAP to a report is that the middle hop is where the meaning gets added, and it is the part you want to write once and reuse. Which brings us to how the layers are organized.&lt;/P&gt;&lt;H2&gt;&lt;STRONG&gt;The medallion architecture, and what sits in each layer&lt;/STRONG&gt;&lt;/H2&gt;&lt;P&gt;The middle hop follows the medallion architecture, bronze to silver to gold, and the useful way to read it here is as a count that shrinks: sixteen, then seven, then eight, then two.&lt;/P&gt;&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="Josue2603_1-1790183492597.png" style="width: 400px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31446i96E6B0E512D0F423/image-size/medium?v=v2&amp;amp;px=400" role="button" title="Josue2603_1-1790183492597.png" alt="Josue2603_1-1790183492597.png" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;P&gt;&lt;EM&gt;Sixteen foundational data products, seven business data products, eight gold entities, five kinds of consumer.&lt;/EM&gt;&lt;/P&gt;&lt;P&gt;Bronze holds sixteen foundational data products. Raw SAP tables grouped into the smallest units that mean something on their own. VBAK, VBAP and VBFA as the sales order. VBRK and VBRP as the billing document. ACDOCA, BSID, BSAD and BSEG as the journal entries. KNA1, KNVV and KNVP as the customer. PA0000 and PA0001 as personnel. MARA and MARM as the material. TCURR, TCURF and TCURV as currency. Plus eight small ones for organizational structure: sales organization, distribution channel, sales division, sales group, customer group, payment terms, region texts and the Nielsen indicator.&lt;/P&gt;&lt;P&gt;Those sixteen come from four different areas of SAP. Finance contributes three, order to cash four, human resources one, and organizational structure eight. That ratio is the first thing worth noticing: half the sources in a finance data product are not financial. They are the semantics that make a finance number readable by somebody who does not know SAP.&lt;/P&gt;&lt;P&gt;Silver holds seven business data products. One per business concept: journal entries, sales billing, sales order, currency, personnel, material master, customer master. Sixteen becomes seven because the eight organizational structure entities are not concepts in their own right, they are descriptions that get attached later, and the two receivables entities, open items and cleared items, collapse into a single journal entries product.&lt;/P&gt;&lt;P&gt;Gold holds eight entities. The consolidations, the two unit of measure normalizations, the two summaries, and the two that consumers actually query.&lt;/P&gt;&lt;P&gt;Those last two are the product's real surface. Everything else is scaffolding, and the discipline of the medallion pattern is that the scaffolding is visible and inspectable rather than buried inside one query nobody wants to open.&lt;/P&gt;&lt;P&gt;Notice who is on the top row of that diagram. Not only the finance team. A CRM, an operational data store, a generative AI or machine learning workload. The same two contracts serve all of them, which is only possible because they are contracts and not reports.&lt;/P&gt;&lt;H2&gt;&lt;STRONG&gt;Two streams, because exposure is two questions&lt;/STRONG&gt;&lt;/H2&gt;&lt;P&gt;The pipeline runs two streams that never merge. That is deliberate, and it is the structural decision that matters most.&lt;/P&gt;&lt;P&gt;The finance stream answers what has been invoiced and not paid. It is anchored on ACDOCA, the Universal Journal line items, joined to BSEG on document, fiscal year and line item, and to BSID, open customer items, on company code, document, fiscal year, line item and customer.&lt;/P&gt;&lt;P&gt;Then the join that is worth stopping on. BSAD, cleared items, joins on ACDOCA.BELNR equal to BSAD.AUGBL, and on the line item. AUGBL is the clearing document. An invoice does not become paid because a flag flips; it becomes paid because a second document appears and points back at the first. Payment behavior is not a state on the invoice, it is a separate document joined on a field most people never look at.&lt;/P&gt;&lt;P&gt;The sales stream answers what has been committed and not yet invoiced. It is anchored on the sales order: VBAK for the header, VBAP for the items, VBFA for the document flow, VBPA for the partners, VBKD for the business data. Those are the same five tables I wrote about when describing what an SAP data product is, and this product consumes that data product rather than rebuilding it.&lt;/P&gt;&lt;P&gt;The sales stream ends with a definition of an open order written as three exclusions, and on Databricks you can see where the three flags come from rather than only the filter that uses them. Each one is derived from the subsequent document category in VBFA: category M sets the invoice flag, category P sets the deposit flag, category O sets the credit note flag. Everything already billed, prepaid, or credited then comes out. What remains is business the company has committed to and has not yet turned into a receivable.&lt;/P&gt;&lt;P&gt;Two streams, because credit exposure is not the receivables balance. It is what the customer owes plus what they are about to owe, and the second part lives in Sales and Distribution rather than in Finance.&lt;/P&gt;&lt;H2&gt;&lt;STRONG&gt;What the product exposes: contract one&lt;/STRONG&gt;&lt;/H2&gt;&lt;P&gt;The finance stream ends in a table published at one row per journal entry line. The key is client, ledger, document number, line item, fiscal year, and company code.&lt;/P&gt;&lt;P&gt;What a consumer gets on that row, grouped by what it is for:&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;Identity and posting.&lt;/STRONG&gt; Document number, line item, fiscal year, fiscal period, posting date, account, cost center, account type.&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;The billing link.&lt;/STRONG&gt; Billing document, billing date, net value from the billing item, billing type. This is how an accounting line traces back to the invoice a customer actually received.&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;Receivable state.&lt;/STRONG&gt; Due date, payment terms, cleared amount, clearing document and clearing date from the open items side, and the same four again from the cleared items side. Both sides are on the row, which is what makes a single query able to compare outstanding against collected without a second pass.&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;Measures.&lt;/STRONG&gt; Quantity in the material's base unit, and amount in group currency.&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;Dimensions with their descriptions.&lt;/STRONG&gt; Sales organization and its text. Distribution channel and its text. Division and its text. Plant. Customer number, name, city, country, region and the region's text. Customer group and its text. Sales group and its text. Four additional customer group fields. Nielsen indicator and its text.&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;Ownership.&lt;/STRONG&gt; The personnel number of the sales representative.&lt;/P&gt;&lt;P&gt;Every code arrives with its word. That is not decoration; it is the difference between a table a finance analyst can query and a table that needs a lookup sheet.&lt;/P&gt;&lt;H2&gt;&lt;STRONG&gt;What the product exposes: contract two&lt;/STRONG&gt;&lt;/H2&gt;&lt;P&gt;The sales stream ends in a second table, published at one row per open sales order item. The key is the sales order and the item.&lt;/P&gt;&lt;P&gt;The columns are narrower, because an open order has less to say than a posted document: order date, fiscal year and month, order type, sales unit, material, net value, currency, and the three status flags described above.&lt;/P&gt;&lt;P&gt;But the dimensional half is nearly identical to contract one. The same sales organization and text, distribution channel and text, division and text, plant, customer with name, city, country, region and text, the same customer group fields, the same sales representative.&lt;/P&gt;&lt;P&gt;That repetition is the design, not an accident.&lt;/P&gt;&lt;H2&gt;&lt;STRONG&gt;Why the dashboard has one filter bar&lt;/STRONG&gt;&lt;/H2&gt;&lt;P&gt;The dashboard filters on company code, plant, sales organization, sales group, country, and region. Six filters, applied to both halves of the picture at once.&lt;/P&gt;&lt;P&gt;That only works because both contracts carry the same dimensional spine. Filter to a region and the invoiced-and-unpaid number and the committed-but-not-invoiced number both narrow, in the same way, resolving the same code to the same word.&lt;/P&gt;&lt;P&gt;If the two streams had been enriched independently, this is exactly where it would break. One would resolve region from the customer master and the other from the sales document, and a regional exposure figure would stop reconciling with the sum of its parts. The eight organizational structure sources are shared for that reason.&lt;/P&gt;&lt;H2&gt;&lt;STRONG&gt;What the dashboard visualizes, and where each tile comes from&lt;/STRONG&gt;&lt;/H2&gt;&lt;P&gt;The dashboard ships as a single Power BI report, built directly on the two gold entities. It is a starting point, and a starting point only needs to exist once, because the first thing any finance team does with a delivered dashboard is change it.&lt;/P&gt;&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="Josue2603_2-1790183492605.png" style="width: 400px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31445i9BAE321CAF644233/image-size/medium?v=v2&amp;amp;px=400" role="button" title="Josue2603_2-1790183492605.png" alt="Josue2603_2-1790183492605.png" /&gt;&lt;/span&gt;&lt;/P&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;P&gt;&lt;EM&gt;Six filters across the top, six tiles, three visuals and the ledger. Every element resolves to a column in one of the two gold entities. Figures shown are from a demonstration environment.&lt;/EM&gt;&lt;/P&gt;&lt;P&gt;What matters more than the tool is that nothing on that page needs a join. The report reads two tables and does arithmetic, which is what a data product is supposed to leave for the consumer.&lt;/P&gt;&lt;P&gt;Total receivables and overdue receivables both resolve to the open item amount. The difference between them is a comparison of the due date against today, which is why the due date and the payment terms travel on the same row as the amount.&lt;/P&gt;&lt;P&gt;Percent overdue is the ratio of those two, and it is the tile most likely to be wrong for structural reasons rather than arithmetic ones. Getting it right depends entirely on which date the product treats as the due date.&lt;/P&gt;&lt;P&gt;Days sales outstanding comes from the two dates the product ships rather than from a column named DSO: the posting date and the clearing date. The product publishes the dates. The measure is defined in the dashboard layer, which is the correct place for it, because two finance teams will define it differently and neither definition belongs baked into a pipeline.&lt;/P&gt;&lt;P&gt;Cash collected reads the cleared side: clearing date and cleared amount, from the BSAD half of the row.&lt;/P&gt;&lt;P&gt;Predicted cash is the one tile that needs both streams. Receivables coming due in the window from the finance stream, plus open orders expected to bill from the sales stream. A tile built on the finance stream alone would forecast only the money already invoiced, which understates the pipeline by exactly the amount of business in flight.&lt;/P&gt;&lt;P&gt;Aging analysis buckets the open items by how far past due they are. Worth being precise about what the product hands over here: it ships the due date and the payment term day count, not the buckets. The bucket boundaries are a business decision and they live in the reporting layer.&lt;/P&gt;&lt;P&gt;Top customers is the customer name and the amount, ranked. The name is on the row because the customer master is one of the sixteen sources.&lt;/P&gt;&lt;P&gt;The invoice ledger is the underlying grain made visible: document number, customer, name, posting date, due date. It is the same rows the tiles aggregate, which is why a number on a tile can be clicked down to the documents behind it.&lt;/P&gt;&lt;H2&gt;&lt;STRONG&gt;How the data reaches Databricks&lt;/STRONG&gt;&lt;/H2&gt;&lt;P&gt;Onibex One Connect captures changes in SAP ECC and SAP S/4HANA through application-layer business events (RAP, BOR, BTE, PPF), pushed outbound over an HTTP RFC destination of Type G to the Smart Gateway, and streamed to Apache Kafka, Confluent Cloud, Databricks, Snowflake and other targets. Nothing is read from the database layer. Nothing is pulled.&lt;/P&gt;&lt;P&gt;That is the first hop, and for receivables the relevant property of it is not raw speed. It is that the update is triggered by the transaction rather than by a schedule, because a schedule does not know when anything happened. It only knows what time it is.&lt;/P&gt;&lt;P&gt;The second hop, Kafka into the medallion layers, is where the more common misreading happens. Bronze, silver and gold are frequently built as scheduled batch jobs, one layer waiting on the one below it, which is how a three-layer architecture turns into a three-window delay. Which brings us to the part of this that is specific to Databricks.&lt;/P&gt;&lt;H2&gt;&lt;STRONG&gt;Where the change semantics are actually declared&lt;/STRONG&gt;&lt;/H2&gt;&lt;P&gt;Every silver and gold table in this pipeline is built the same way, in two moves.&lt;/P&gt;&lt;P&gt;The first move reads the change data feed of the bronze Delta table, not the table itself:&lt;/P&gt;&lt;P&gt;def cdf_reader(table):&lt;/P&gt;&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; return (&lt;/P&gt;&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; spark.readStream.format("delta")&lt;/P&gt;&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .option("readChangeFeed", "true")&lt;/P&gt;&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .option("startingVersion", "0")&lt;/P&gt;&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .table(table)&lt;/P&gt;&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; )&lt;/P&gt;&lt;P&gt;What comes back is not rows, it is changes: each one carrying a _change_type of insert, update or delete, a _commit_version and a _commit_timestamp. The Delta table's own commit history is the change feed. Nothing had to be added to the source to produce it.&lt;/P&gt;&lt;P&gt;The second move turns that stream of changes back into a current-state table, and this is the declaration that matters:&lt;/P&gt;&lt;P&gt;dlt.apply_changes(&lt;/P&gt;&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; target="acdoca_cur",&lt;/P&gt;&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; source="acdoca_cdf",&lt;/P&gt;&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; keys=["belnr", "docln", "gjahr"],&lt;/P&gt;&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; sequence_by="_commit_timestamp",&lt;/P&gt;&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; apply_as_deletes="_change_type = 'delete'",&lt;/P&gt;&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; ignore_null_updates=True,&lt;/P&gt;&lt;P&gt;)&lt;/P&gt;&lt;P&gt;Four statements about the data, in four lines, on the table they apply to.&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;keys&lt;/STRONG&gt; is the declared grain. ACDOCA is unique on document, line and fiscal year. BSID and BSAD need five columns, company code, document, fiscal year, line item and customer. VBAK needs one, VBELN. VBAP needs two. VBFA needs three. That is not configuration, it is the grain of the business object written down where the code that maintains it can be checked against it.&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;sequence_by&lt;/STRONG&gt; is the clock that decides which version of a row wins when two changes arrive for the same key. This is the argument that quietly decides whether your table is right.&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;apply_as_deletes&lt;/STRONG&gt; is the line that makes a delete a delete. Without it, a removed row in SAP leaves a row behind in silver forever, and nothing about that failure is visible, because the evidence of the problem is exactly the record that stopped arriving. I have written about the same failure happening on a different stack for the opposite reason, and the lesson generalizes: a pipeline that cannot express a delete will never tell you it has one.&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;ignore_null_updates&lt;/STRONG&gt; keeps a partial update from blanking fields it did not carry.&lt;/P&gt;&lt;P&gt;The result is what the code calls a Type 1 snapshot: current state, no history. Those _cur tables are what the silver joins read, which means the join is always operating on one committed version of each source row rather than on a stream of changes it would have to reconcile itself.&lt;/P&gt;&lt;H2&gt;&lt;STRONG&gt;The argument on clocks, and an honest inconsistency&lt;/STRONG&gt;&lt;/H2&gt;&lt;P&gt;Two things about that declaration deserve more than a passing mention, and the second one is a defect rather than a design.&lt;/P&gt;&lt;P&gt;The first is what sequence_by actually chooses. Ordering by _commit_timestamp orders changes by when the lakehouse committed them. Ordering by an event timestamp carried from the source orders them by when the business said they happened. Those are the same thing only while delivery is perfectly ordered, and the whole point of putting Kafka in the middle is that delivery is not guaranteed to be. If two updates to the same sales order are produced in one order and committed in the other, commit time confidently applies the wrong one last.&lt;/P&gt;&lt;P&gt;The second is that this pipeline does both. The finance tables sequence by _commit_timestamp. The sales order tables sequence by event_ts, the timestamp the business event carries out of SAP. Two clocks, in one pipeline, on data that ends up on the same dashboard. Each is defensible on its own; having both without a stated reason is not, and it is the kind of thing that only ever surfaces as a number nobody can reconcile. It is on the list to settle, and I am naming it here rather than writing around it.&lt;/P&gt;&lt;P&gt;There is also a naming detail worth knowing before you copy any of this. Databricks has renamed Delta Live Tables to Lakeflow declarative pipelines, and apply_changes is now create_auto_cdc_flow, with the same signature. The dlt module and the old function names still work and the code above still runs, but new pipelines should use from pyspark import pipelines as dp and the current names.&lt;/P&gt;&lt;H2&gt;&lt;STRONG&gt;Where the freshness comes from, which is not the table&lt;/STRONG&gt;&lt;/H2&gt;&lt;P&gt;This is the honest difference from the same product on Snowflake, and it is worth being direct about rather than claiming parity.&lt;/P&gt;&lt;P&gt;On Snowflake, the freshness contract is a property of the table. Every dynamic table declares TARGET_LAG = DOWNSTREAM and defers to the one consuming it, and the final entity declares an actual target, one minute, on the object that answers the question. You can read the intended freshness of the whole pipeline out of one table definition in version control.&lt;/P&gt;&lt;P&gt;On Databricks there is no such line in the table definition. What decides how current the gold tables are is how the pipeline runs: continuous mode, where the pipeline stays up and processes changes as they arrive, or triggered mode, where each run processes what has accumulated and stops. That is a property of the pipeline, not of the table, and it is set where pipelines are configured rather than next to the code a reviewer reads.&lt;/P&gt;&lt;P&gt;The trade is real in both directions. Snowflake makes the freshness target reviewable and puts the incremental mechanics inside the service, where you accept what the planner decides to maintain incrementally. Databricks makes the change mechanics reviewable, which is what the entire section above is about, and leaves freshness to the run configuration.&lt;/P&gt;&lt;P&gt;Neither of those is better. They put the reviewable part in different places, and if you are choosing between them, that is the question to ask rather than which one is faster.&lt;/P&gt;&lt;H2&gt;&lt;STRONG&gt;The same contracts on other engines&lt;/STRONG&gt;&lt;/H2&gt;&lt;P&gt;Worth saying plainly, because it is the reason any of the above is reusable: the two contracts are the product, and Databricks is one way to maintain them.&lt;/P&gt;&lt;P&gt;The same sixteen sources, the same grain and the same column names also ship as Snowflake dynamic tables on incremental refresh, as Flink SQL jobs with an upsert changelog over compacted Kafka topics, and as PostgreSQL layers maintained by stored procedures. What differs across them is where state lives and how the incremental update is expressed. A consumer querying the gold table cannot tell which one is running underneath, and that is the point of publishing a contract instead of a pipeline.&lt;/P&gt;&lt;P&gt;Databricks is the one worth writing up in this much detail because it is the one where the change semantics are written out in full, in code, next to the table they govern. The grain, the ordering clock and the delete rule are four lines somebody can review in a pull request rather than behavior you have to infer from a diagram.&lt;/P&gt;&lt;H2&gt;&lt;STRONG&gt;Where the product stops&lt;/STRONG&gt;&lt;/H2&gt;&lt;P&gt;Four things are deliberately not in it, and a pre-modeled data product that pretends otherwise is worse than one that draws the line.&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;Thresholds.&lt;/STRONG&gt; Nothing in the product decides that a customer at ninety percent of limit gets released and one at ninety-five does not.&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;Bucket and metric definitions.&lt;/STRONG&gt; Aging boundaries and the DSO formula belong to the reporting layer, for the reason given above.&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;Currency conversion parameters.&lt;/STRONG&gt; The gold sales layer converts to USD at exchange rate type M, matched on the document date, with returns negated. The target currency, the rate type and the date the rate is matched on are all choices written into the transformation. All three change the number on the dashboard, and all three need an owner.&lt;/P&gt;&lt;P&gt;&lt;STRONG&gt;History.&lt;/STRONG&gt; The current-state tables are Type 1. The change feed that fed them retains only as far back as the source table's history does, so if you need to answer what this row said last quarter, that is a Type 2 design and a different conversation.&lt;/P&gt;&lt;P&gt;What the structure does guarantee is narrower and more useful than a claim about speed. Every tile traces to a column, every column traces to a source, the invoiced half and the committed half of exposure filter the same way, and the rule that decides what happens to a row when SAP changes it is four lines somebody chose rather than behavior nobody wrote down. That is what makes the six tiles worth looking at.&lt;/P&gt;&lt;P&gt;&lt;EM&gt;Technical reference: the deployment artifacts and integration steps are documented in oneconnect-docs. The Finance data products this one consumes are listed in the Onibex Marketplace.&lt;/EM&gt;&lt;/P&gt;&lt;P&gt;&lt;EM&gt;Josue Ramirez works on data and AI at Onibex. &lt;/EM&gt;&lt;EM&gt;More at onibex.com.&lt;/EM&gt;&lt;/P&gt;</description>
      <pubDate>Wed, 23 Sep 2026 17:12:56 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/real-time-sap-accounts-receivable-on-databricks-lakeflow/m-p/169616#M1594</guid>
      <dc:creator>Josue2603</dc:creator>
      <dc:date>2026-09-23T17:12:56Z</dc:date>
    </item>
    <item>
      <title>Databricks Data Quality - Agent Proposes, DQX Disposes</title>
      <link>https://community.databricks.com/t5/community-articles/databricks-data-quality-agent-proposes-dqx-disposes/m-p/169513#M1591</link>
      <description>&lt;P&gt;&lt;FONT color="#333333"&gt;&lt;SPAN&gt;Every serious data platform grows a dead-letter table, and it always becomes a graveyard. Here is how we turned one into a self-healing queue — without letting a language model anywhere near the warehouse.&lt;BR /&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;Every data platform that takes quality seriously ends up with a dead-letter table. You run pre-commit checks, and the rows that fail don’t go into Silver — they go into quarantine. It is the responsible thing to do. It is also, in practice, where data goes to die.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;Nobody wants to open the quarantine table. The rows that land there are, by definition, the annoying ones: a code with a stray alphabetic prefix, a timestamp in the wrong format, a duplicate that got emitted twice, a foreign key that doesn’t quite line up because one side trims whitespace and the other doesn’t. Fixing them is fiddly, one-off work, and the backlog only grows.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;We built a system that closes the loop. An LLM agent triages quarantined rows and proposes fixes. Those fixes are re-validated deterministically against the exact checks that quarantined the row in the first place. Only the rows that pass are merged back into Silver. Everything else escalates to a human review queue instead of looping forever.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;The interesting part isn’t the LLM. It’s the constraints around it.&lt;BR /&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;01&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Two design principles&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;Everything in the system falls out of two rules we decided not to break.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT size="4" color="#333333"&gt;“&lt;EM&gt;The agent proposes. DQX disposes.”&lt;/EM&gt;&lt;/FONT&gt;&lt;BR /&gt;&lt;FONT color="#333333"&gt;&lt;FONT size="2"&gt;&lt;SPAN class=""&gt;&lt;FONT size="2"&gt;PRINCIPLE ONE&lt;/FONT&gt;&lt;BR /&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/FONT&gt;&lt;FONT size="3"&gt;&lt;SPAN class=""&gt;&lt;SPAN&gt;The agent never writes to Silver or Gold. Not once. Every agent-curated row is re-run through the same data-quality engine — we use&amp;nbsp;&lt;/SPAN&gt;&lt;A href="https://databrickslabs.github.io/dqx/" target="_blank" rel="noopener"&gt;DQX&lt;/A&gt;&lt;SPAN&gt;, the Databricks Labs quality framework — that quarantined it. If the “fix” doesn’t actually make the row pass the checks, it doesn’t get in. This keeps a hallucinated correction from silently corrupting the warehouse, and it makes the whole thing auditable: the gate is code, not vibes.&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT size="4" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;SPAN&gt;&lt;EM&gt;“Documented fixes beat guessed fixes.”&lt;/EM&gt;&lt;BR /&gt;&lt;FONT size="2"&gt;PRINCIPLE TWO&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;If a rule owner has already written down exactly how a violation should be remediated —&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;“primary-key collision on&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;customer_id: keep the row with the latest&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;updated_at, drop the rest”&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;— then that fix is applied deterministically and the LLM never sees the row. The agent is the fallback for rules nobody has written a playbook for yet. It is not the default path.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;That second principle matters more than it sounds. The cheapest, most reliable, most auditable model call is the one you never make.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="KeerthiItigimat_0-1790129785984.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31406i5A25641102E02696/image-size/large?v=v2&amp;amp;px=999" role="button" title="KeerthiItigimat_0-1790129785984.png" alt="KeerthiItigimat_0-1790129785984.png" /&gt;&lt;/span&gt;&lt;/FONT&gt;&lt;BR /&gt;&lt;FONT color="#808080"&gt;&lt;STRONG&gt;&lt;FONT color="#333333"&gt;Figure 1&lt;/FONT&gt;.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;The loop. A quarantined row is fixed by a documented playbook strategy or, failing that, by the agent — then re-validated against the identical DQX checks before anything touches Silver.&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;02&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;The shape of the pipeline&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;The whole thing runs as a Databricks Workflow. Because a&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;For Each&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;task can only wrap a single task, not a sub-graph, the per-table pipeline is a child job that the parent fans out to — one child run per quarantine table.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="KeerthiItigimat_1-1790129900413.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31407i4FDB3831171B1C85/image-size/large?v=v2&amp;amp;px=999" role="button" title="KeerthiItigimat_1-1790129900413.png" alt="KeerthiItigimat_1-1790129900413.png" /&gt;&lt;/span&gt;&lt;/FONT&gt;&lt;BR /&gt;&lt;FONT color="#808080"&gt;&lt;STRONG&gt;&lt;FONT color="#333333"&gt;Figure 2&lt;/FONT&gt;.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;The Workflow DAG. Parent discovers and fans out; each child run walks the same seven-stage path.&amp;nbsp;&lt;/SPAN&gt;06&lt;SPAN&gt;&amp;nbsp;runs on&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;at least one success&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;of reingest or a false gate, so every table produces an audit row either way.&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="KeerthiItigimat_2-1790129972242.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31408i2DFC6F4AEF03F463/image-size/large?v=v2&amp;amp;px=999" role="button" title="KeerthiItigimat_2-1790129972242.png" alt="KeerthiItigimat_2-1790129972242.png" /&gt;&lt;/span&gt;&lt;/FONT&gt;&lt;BR /&gt;&lt;FONT color="#808080"&gt;&lt;STRONG&gt;&lt;FONT color="#333333"&gt;Figure 3&lt;/FONT&gt;.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;A recreation of the parent job’s run graph in Databricks Workflows —&amp;nbsp;&lt;/SPAN&gt;discover&lt;SPAN&gt;, then a&amp;nbsp;&lt;/SPAN&gt;For&amp;nbsp;Each&lt;SPAN&gt;&amp;nbsp;fan-out to one child run per quarantine table. Table names and durations are illustrative.&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;&lt;SPAN&gt;Each stage writes only to a control table — a review queue with one row per quarantined row per pass. Nothing touches Silver until the final merge, and that merge only ever sees rows that have already passed re-validation.&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;03&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Triage&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;Pull the unprocessed rows from one quarantine table, dedupe to the latest quarantined version per row, and flatten the failure metadata. DQX records failures as an array of structs —&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;{name, message, …}&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;— and triage flattens those names into a plain&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;array&amp;lt;string&amp;gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;of violated rule names, then seeds the review queue with status&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;pending.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;It also captures the business key at this moment. Reconstructing a natural key later, after the row has been patched, is guesswork; capturing it at quarantine time is not.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;04&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Playbook remediation — the deterministic half&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;This is where documented fixes get applied. Each dataset has a YAML file — not in the codebase, but in a Unity Catalog Volume, so adding or changing a rule needs no deploy:&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="KeerthiItigimat_3-1790130132909.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31409iDD4E3BE790D5355D/image-size/large?v=v2&amp;amp;px=999" role="button" title="KeerthiItigimat_3-1790130132909.png" alt="KeerthiItigimat_3-1790130132909.png" /&gt;&lt;/span&gt;&lt;/FONT&gt;&lt;BR /&gt;&lt;BR /&gt;&lt;FONT color="#333333"&gt;Entries run in priority order; the first one that matches a row wins. Three things we learned to bake in:&lt;/FONT&gt;&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;FONT color="#333333"&gt;&lt;STRONG&gt;where&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;predicates let one rule route by value.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;status = 'Duplicate'&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;goes to dedupe;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;status = 'Invalid'&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;has no entry, so it falls through to the agent. Same rule, different handling, driven by the data.&lt;/FONT&gt;&lt;/LI&gt;&lt;LI&gt;&lt;FONT color="#333333"&gt;&lt;STRONG&gt;reject_rows&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;is a terminal state, not a failure.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;Some rows genuinely can’t be recovered. Dropping them is a decision the rule owner made on purpose, and it is recorded as such — not retried, not escalated, not seen by the agent.&lt;/FONT&gt;&lt;/LI&gt;&lt;LI&gt;&lt;FONT color="#333333"&gt;&lt;STRONG&gt;Dedup is configured only here.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;The agent never deduplicates. Choosing which of two near-identical rows survives is a business call with a documented tie-breaker, not something to infer.&lt;/FONT&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;The strategy&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;functions&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;—&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;dedupe_keep_latest,&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;standardize_value,&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;fill_default,&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;reject_rows&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;— are code. A new&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;kind&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;of fix needs a deploy. A new&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;instance&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;of an existing kind is just a file upload. Behavior in code, policy in config: that split is what keeps the playbook maintainable by the people who own the rules rather than the people who own the pipeline.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;05&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Agent curation — the fallback&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;Only rows with no playbook match reach the agent. And here is the decision that makes this effective:&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;&lt;EM&gt;&lt;FONT size="4"&gt;“One model call per distinct violation signature. Not per row.”&lt;/FONT&gt;&lt;BR /&gt;&lt;/EM&gt;&lt;FONT size="2"&gt;&lt;SPAN class=""&gt;THE COST MODEL&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT size="3" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;SPAN&gt;If 40,000 rows all failed the same check on the same column in the same way, that is&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;one&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;call. The model sees the rule name, the check function, the message, a sample of failing values, and up to five passing examples per flagged column. It returns a single deterministic&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;transform&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;per column, chosen from a fixed vocabulary:&lt;BR /&gt;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="KeerthiItigimat_4-1790130422163.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31410i8C37309F712406BE/image-size/large?v=v2&amp;amp;px=999" role="button" title="KeerthiItigimat_4-1790130422163.png" alt="KeerthiItigimat_4-1790130422163.png" /&gt;&lt;/span&gt;&lt;/FONT&gt;&lt;BR /&gt;&lt;BR /&gt;&lt;FONT color="#333333"&gt;Spark then applies that transform to every row in the signature. The model does not see 40,000 rows, does not write 40,000 fixes, and does not get to invent an expression. It picks&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;lower_trim&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;for a casing problem or&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;regexp_replace&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;to strip a known prefix, and that is the entire surface area. Token cost is independent of quarantine volume.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;Transforms can be&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;chained&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;— strip a prefix,&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;then&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;zero-pad — which covers most of the real formatting damage: typos, column shifts, date formats, casing, padding, regex-strippable junk. And we guard against no-ops: if the model proposes&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;left_pad&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;on a string already at the target length, that is not a fix, and it gets downgraded to an escalation.&lt;/FONT&gt;&lt;BR /&gt;&lt;BR /&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;06&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;The cross-table probe — no model at all&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;Referential-integrity checks — a row in table A must match a row in table B — are common, and the LLM is bad at them. So before the agent is involved, a deterministic probe runs: on a sample, normalize the compared column (trim,&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;lower_trim,&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;upper_trim) on both sides of the join and check whether the mismatch clears. If a normalization resolves, say, 95% of the sample, propose that transform. If it doesn’t, escalate.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;A surprising fraction of “broken foreign keys” are just one side of a join carrying trailing whitespace.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;07&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;The confidence gate&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;The agent attaches a confidence score to every&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;fix&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;decision. Between curation and application, a gate checks the&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;minimum&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;confidence across the batch against a threshold. Above it: proceed to auto-apply. Below it: skip straight to audit, and those rows wait for a human.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;One low-confidence signature holds back the whole batch for that table. We would rather under-automate than merge a shaky fix.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;08&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Apply and re-validate&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;For rows that clear the gate: apply the patch, strip every bookkeeping column, and re-run the row through&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;approved_checks_for(silver_table)&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;— the&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;same rules table&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;that quarantined it. Not a copy of the checks. Not a checks file that might have drifted. The same source. “Re-run the exact checks that quarantined it” is only true if both sides read from one place.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;Rows that pass are staged. Rows that fail get&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;retry_count += 1&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;and go back into the queue. After three failed attempts a row auto-escalates regardless of confidence. Nothing loops forever.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;09&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Reingest&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;A dynamic&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;MERGE INTO&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;Silver, keyed on the business key that triage captured, columns intersected with the target schema. Every reingested row is tagged with lineage:&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;agent_curated = true, the&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;confidence&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;score, the run id, the timestamp. Downstream consumers who don’t trust agent-curated data can filter it out. That is their call to make, and we give them the column to make it with.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="KeerthiItigimat_5-1790130549577.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31411i997C4E9C6E7DBD79/image-size/large?v=v2&amp;amp;px=999" role="button" title="KeerthiItigimat_5-1790130549577.png" alt="KeerthiItigimat_5-1790130549577.png" /&gt;&lt;/span&gt;&lt;/FONT&gt;&lt;BR /&gt;&lt;FONT color="#333333"&gt;&lt;FONT color="#808080"&gt;&lt;STRONG&gt;&lt;FONT color="#333333"&gt;Figure 4&lt;/FONT&gt;.&lt;/STRONG&gt;&lt;/FONT&gt;&lt;SPAN&gt;&lt;FONT color="#808080"&gt;&amp;nbsp;The review queue — one row per quarantined row per pass. Terminal states in green/red, human work in amber.&lt;/FONT&gt;&lt;BR /&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/FONT&gt;&lt;FONT color="#333333"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="KeerthiItigimat_6-1790130596298.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31412iD5D385EE5F5A4C54/image-size/large?v=v2&amp;amp;px=999" role="button" title="KeerthiItigimat_6-1790130596298.png" alt="KeerthiItigimat_6-1790130596298.png" /&gt;&lt;/span&gt;&lt;/FONT&gt;&lt;BR /&gt;&lt;FONT color="#333333"&gt;&lt;FONT color="#808080"&gt;&lt;STRONG&gt;&lt;FONT color="#333333"&gt;Figure 5&lt;/FONT&gt;.&lt;/STRONG&gt;&lt;/FONT&gt;&lt;SPAN&gt;&lt;SPAN&gt;&lt;FONT color="#808080"&gt;&amp;nbsp;Illustrative outcome split for a single run. The playbook clears the bulk; the agent handles a couple hundred; a couple dozen reach a human.&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;10&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Dry runs&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;Because “an LLM will now edit rows that flow into our warehouse” is a sentence that makes people nervous, there are two ways to see what&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;would&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;happen without committing anything.&lt;/FONT&gt;&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;FONT color="#333333"&gt;A&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;read-only estimate&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;that computes the deterministic half for real — match, apply strategy, re-validate — and reports the agent half as a count only. The model is never called. One row per table into a report table, nothing else.&lt;/FONT&gt;&lt;/LI&gt;&lt;LI&gt;&lt;FONT color="#333333"&gt;A&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;full dry run&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;that runs the whole pipeline, calls the agent, writes the queue and the staging table — but holds the final&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;MERGE&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;and instead reports the exact insert/update split against Silver.&lt;/FONT&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;Run the first before you turn the system on. Run the second in staging when you want to see precisely what will land.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;11&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;What this bought us&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;The quarantine table stopped being a graveyard. On the first real run, the large majority of quarantined rows resolved deterministically — dedup and structural rejects that rule owners had already documented. A few hundred went to the agent. A couple dozen genuinely needed a human, and those are the ones a person should be looking at.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT color="#333333"&gt;The lesson we keep relearning: the LLM is the least important component. The value is in the scaffolding — the deterministic-first routing, the fixed transform vocabulary, the re-validation gate, the retry cap, the confidence threshold, the dry runs. The agent is a fallback that turns “a human triages every weird row” into “a human triages the genuinely ambiguous ones.”&lt;/FONT&gt;&lt;BR /&gt;&lt;BR /&gt;&lt;FONT size="5" color="#008000"&gt;&lt;SPAN&gt;Agent proposes. DQX disposes. Silver stays clean.&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;DIV class=""&gt;&amp;nbsp;&lt;/DIV&gt;</description>
      <pubDate>Wed, 23 Sep 2026 02:40:25 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/databricks-data-quality-agent-proposes-dqx-disposes/m-p/169513#M1591</guid>
      <dc:creator>KeerthiItigimat</dc:creator>
      <dc:date>2026-09-23T02:40:25Z</dc:date>
    </item>
    <item>
      <title>Enterprise AI Has a Serious Context Problem</title>
      <link>https://community.databricks.com/t5/community-articles/enterprise-ai-has-a-serious-context-problem/m-p/169472#M1589</link>
      <description>&lt;P&gt;After coming back from the Databricks Data + AI Summit this year, one thought has stayed with me. I keep hearing Databricks co-founder and CEO Ali Ghodsi talk about something that I am also starting to see in my own Data + AI POCs.&lt;/P&gt;&lt;P&gt;For most enterprise AI use cases, the biggest problem may not be model intelligence anymore.&lt;/P&gt;&lt;P&gt;It is context.&lt;/P&gt;&lt;P&gt;Think about how much business knowledge exists inside a company.&lt;/P&gt;&lt;P&gt;Meetings where important decisions were made. Business rules that teams follow every day. Past decisions and exceptions. Workflows built over many years. And a lot of knowledge that simply lives inside employees’ heads.&lt;/P&gt;&lt;P&gt;AI does not automatically know any of this.&lt;/P&gt;&lt;P&gt;I have seen this while building POCs as well.&lt;/P&gt;&lt;P&gt;Today, connecting an LLM and building an AI agent is becoming easier. We can create an interesting demo surprisingly fast.&lt;/P&gt;&lt;P&gt;But then the real questions start.&lt;/P&gt;&lt;P&gt;Does the AI know what “revenue” actually means inside your company?&lt;/P&gt;&lt;P&gt;Does it know which customer definition is trusted?&lt;/P&gt;&lt;P&gt;Does it know which system is the source of truth?&lt;/P&gt;&lt;P&gt;Does it understand why a business process works the way it does?&lt;/P&gt;&lt;P&gt;That is where things become difficult.&lt;/P&gt;&lt;P&gt;The model may be very intelligent, but without the right business context, it can still give the wrong answer.&lt;/P&gt;&lt;P&gt;This is why I am becoming more interested in the context layer of enterprise AI.&lt;/P&gt;&lt;P&gt;Databricks is also moving in this direction with capabilities such as Genie Ontology, which is designed to bring business meaning and organizational context closer to data and AI.&lt;/P&gt;&lt;P&gt;Ali has also made another point that I think is important. Many organizations are still very early in actually using AI to automate work and create meaningful business value.&lt;/P&gt;&lt;P&gt;From what I am seeing, the next phase of enterprise AI may not be about finding an even smarter model.&lt;/P&gt;&lt;P&gt;It may be about helping AI understand how our businesses actually work.&lt;/P&gt;&lt;P&gt;AI already knows a lot about the world. Now we need to help it understand our organizations.&lt;/P&gt;&lt;P&gt;For data engineers, I think this creates an interesting opportunity.&lt;/P&gt;&lt;P&gt;We have spent years building pipelines that move data.&lt;/P&gt;&lt;P&gt;Now we may also need to help build the context that gives that data meaning.&lt;/P&gt;&lt;P&gt;What are you seeing in your Data + AI projects? Is context becoming a challenge for you too?&lt;/P&gt;</description>
      <pubDate>Tue, 22 Sep 2026 16:12:28 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/enterprise-ai-has-a-serious-context-problem/m-p/169472#M1589</guid>
      <dc:creator>Brahmareddy</dc:creator>
      <dc:date>2026-09-22T16:12:28Z</dc:date>
    </item>
    <item>
      <title>Row-level security when the filter column is not on your table</title>
      <link>https://community.databricks.com/t5/community-articles/row-level-security-when-the-filter-column-is-not-on-your-table/m-p/169339#M1587</link>
      <description>&lt;DIV style="max-width: 860px; margin: 0 auto; padding: 0 32px 80px; background: #FFFFFF; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; color: #1b3139; line-height: 1.75; font-size: 17px;"&gt;
&lt;P&gt;A common question when setting up row-level security in Unity Catalog is what to do when the column that decides who can see a row is not on the table you want to protect. The value lives in a related table, one join away. This comes up often in insurance, where a claims table is keyed by policy, but which agent services which policy is held in a separate mapping table.&lt;/P&gt;
&lt;P&gt;This post walks through that situation. It shows why the first idea does not work, the pattern that does, and a way to set it up so one small control table drives the same rule across many tables. Every table and column name here is a plain example you can rename to your own.&lt;/P&gt;
&lt;DIV style="background: #F0F4F6; border-left: 4px solid #FF3621; padding: 20px 24px; margin: 28px 0; border-radius: 6px;"&gt;
&lt;DIV style="font-size: 12px; font-weight: bold; color: #ff3621; text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 12px;"&gt;Key takeaways&lt;/DIV&gt;
&lt;UL style="margin: 0; padding-left: 20px; color: #1b3139;"&gt;
&lt;LI style="margin-bottom: 8px;"&gt;A row filter is a SQL function, so it can hold a subquery. That lets you scope a table by a value that lives in a related table.&lt;/LI&gt;
&lt;LI style="margin-bottom: 8px;"&gt;You cannot filter a table and also read that same table inside another table's filter. Unity Catalog stops it with an error.&lt;/LI&gt;
&lt;LI style="margin-bottom: 8px;"&gt;Keep the lookup in a small control table that is never filtered, and point every table's filter at it.&lt;/LI&gt;
&lt;LI style="margin-bottom: 0;"&gt;The filter reads the control table under the owner's rights, so users need no access to it. Onboarding a user is a one row change.&lt;/LI&gt;
&lt;/UL&gt;
&lt;/DIV&gt;
&lt;DIV style="background: #FAFBFC; border: 1px solid #E8ECF0; border-radius: 8px; padding: 20px 28px; margin: 24px 0 16px;"&gt;
&lt;DIV style="font-size: 13px; font-weight: bold; color: #ff3621; text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 12px;"&gt;What is in this post&lt;/DIV&gt;
&lt;OL style="margin: 0; padding-left: 20px;"&gt;
&lt;LI style="margin-bottom: 6px; font-size: 15px;"&gt;&lt;A style="color: #1b3139; text-decoration: none;" href="#problem" target="_blank"&gt;The problem&lt;/A&gt;&lt;/LI&gt;
&lt;LI style="margin-bottom: 6px; font-size: 15px;"&gt;&lt;A style="color: #1b3139; text-decoration: none;" href="#how" target="_blank"&gt;How row filters and ABAC work&lt;/A&gt;&lt;/LI&gt;
&lt;LI style="margin-bottom: 6px; font-size: 15px;"&gt;&lt;A style="color: #1b3139; text-decoration: none;" href="#why-not" target="_blank"&gt;Why filtering the mapping table does not work&lt;/A&gt;&lt;/LI&gt;
&lt;LI style="margin-bottom: 6px; font-size: 15px;"&gt;&lt;A style="color: #1b3139; text-decoration: none;" href="#pattern" target="_blank"&gt;How to scope the fact table&lt;/A&gt;&lt;/LI&gt;
&lt;LI style="margin-bottom: 6px; font-size: 15px;"&gt;&lt;A style="color: #1b3139; text-decoration: none;" href="#control" target="_blank"&gt;One control table for many tables&lt;/A&gt;&lt;/LI&gt;
&lt;LI style="margin-bottom: 6px; font-size: 15px;"&gt;&lt;A style="color: #1b3139; text-decoration: none;" href="#read-map" target="_blank"&gt;When users also need to read the mapping table&lt;/A&gt;&lt;/LI&gt;
&lt;LI style="margin-bottom: 6px; font-size: 15px;"&gt;&lt;A style="color: #1b3139; text-decoration: none;" href="#checking" target="_blank"&gt;Checking it works&lt;/A&gt;&lt;/LI&gt;
&lt;LI style="margin-bottom: 0; font-size: 15px;"&gt;&lt;A style="color: #1b3139; text-decoration: none;" href="#keep-in-mind" target="_blank"&gt;Things to keep in mind&lt;/A&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;/DIV&gt;
&lt;H2 id="problem" style="font-size: 26px; font-weight: bold; color: #1b3139; margin: 36px 0 16px; padding-bottom: 8px; border-bottom: 3px solid #FF3621; display: inline-block;"&gt;The problem&lt;/H2&gt;
&lt;P&gt;Here is the model. There are three tables.&lt;/P&gt;
&lt;UL style="padding-left: 24px; margin: 0 0 16px;"&gt;
&lt;LI style="margin-bottom: 8px;"&gt;&lt;STRONG&gt;The claims fact table&lt;/STRONG&gt;, &lt;CODE&gt;claim_fact&lt;/CODE&gt;. One row per claim, keyed by &lt;CODE&gt;policy_id&lt;/CODE&gt;. It does not carry an agent id.&lt;/LI&gt;
&lt;LI style="margin-bottom: 8px;"&gt;&lt;STRONG&gt;The mapping table&lt;/STRONG&gt;, &lt;CODE&gt;policy_agent_map&lt;/CODE&gt;. One row per policy an agent services, holding &lt;CODE&gt;policy_id&lt;/CODE&gt; and &lt;CODE&gt;agent_id&lt;/CODE&gt;.&lt;/LI&gt;
&lt;LI style="margin-bottom: 8px;"&gt;&lt;STRONG&gt;The directory&lt;/STRONG&gt;, &lt;CODE&gt;agent_directory&lt;/CODE&gt;. Maps the signed-in user, &lt;CODE&gt;user_email&lt;/CODE&gt;, to their &lt;CODE&gt;agent_id&lt;/CODE&gt;.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;The goal is that an agent sees only the claims for the policies they service, and can also read the mapping table and see only their own policies. The value that decides all of this, the agent id, is two joins away from the claims table.&lt;/P&gt;
&lt;FIGURE style="margin: 20px 0;"&gt;
&lt;P style="color: #6b8a97; font-family: monospace; font-size: 13px; text-align: center; margin: 16px 0 0;"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="diagram_1_setup.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31343i98CF1D6236355FAE/image-size/large?v=v2&amp;amp;px=999" role="button" title="diagram_1_setup.png" alt="diagram_1_setup.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;FIGCAPTION style="font-size: 13px; color: #6b8a97; margin-top: 8px; font-style: italic; text-align: center;"&gt;The claims table reaches its entitlement two joins away, through the mapping table to the directory, based on the signed-in user.&lt;/FIGCAPTION&gt;
&lt;/FIGURE&gt;
&lt;FIGURE style="margin: 20px 0; text-align: center;"&gt;
&lt;P style="color: #6b8a97; font-family: monospace; font-size: 13px; text-align: center; margin: 16px 0 0;"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="shot_schema.png" style="width: 400px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31345iDA2C6E67248921D1/image-size/medium?v=v2&amp;amp;px=400" role="button" title="shot_schema.png" alt="shot_schema.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;FIGCAPTION style="font-size: 13px; color: #6b8a97; margin-top: 8px; font-style: italic; text-align: center;"&gt;The objects in the schema. The claims fact and the mapping table, the directory and the control table, and the two filter functions.&lt;/FIGCAPTION&gt;
&lt;/FIGURE&gt;
&lt;H2 id="how" style="font-size: 26px; font-weight: bold; color: #1b3139; margin: 36px 0 16px; padding-bottom: 8px; border-bottom: 3px solid #FF3621; display: inline-block;"&gt;How row filters and ABAC work&lt;/H2&gt;
&lt;P&gt;A row filter in Unity Catalog is a SQL function bound to a column. It runs for every row and returns true or false. If it returns true, the row is shown. A simple filter on a table that already has the agent id looks like this.&lt;/P&gt;
&lt;PRE style="background: #1B3139; border-left: 4px solid #FF3621; border-radius: 6px; padding: 20px 24px; margin: 20px 0; font-family: 'SF Mono', Monaco, Consolas, 'Courier New', monospace; font-size: 13.5px; line-height: 1.6; color: #e8ecf0; overflow-x: auto;"&gt;&lt;SPAN&gt;CREATE OR REPLACE FUNCTION&lt;/SPAN&gt; scope_by_agent(agent_id &lt;SPAN&gt;STRING&lt;/SPAN&gt;)
  &lt;SPAN&gt;RETURN&lt;/SPAN&gt; is_account_group_member(&lt;SPAN&gt;'claims_supervisors'&lt;/SPAN&gt;)
      &lt;SPAN&gt;OR&lt;/SPAN&gt; agent_id = current_user();

&lt;SPAN&gt;ALTER TABLE&lt;/SPAN&gt; agent_book &lt;SPAN&gt;SET ROW FILTER&lt;/SPAN&gt; scope_by_agent &lt;SPAN&gt;ON&lt;/SPAN&gt; (agent_id);&lt;/PRE&gt;
&lt;P&gt;Attribute-based access control, ABAC, does the same job through a governed tag on a column plus a policy. You tag the column once, write one policy, and it covers every table that carries that tag, including tables added later. That is the better fit when the same rule applies across many tables. The row filter idea below works the same way whether you bind it to one table by hand or apply it through a tag and policy.&lt;/P&gt;
&lt;H2 id="why-not" style="font-size: 26px; font-weight: bold; color: #1b3139; margin: 36px 0 16px; padding-bottom: 8px; border-bottom: 3px solid #FF3621; display: inline-block;"&gt;Why filtering the mapping table does not work&lt;/H2&gt;
&lt;P&gt;The first idea most people reach for is to put a row filter on the mapping table, then have the claims filter read that mapping table to find the agent's policies. This does not work.&lt;/P&gt;
&lt;P&gt;Unity Catalog does not allow a table that already has an active row filter or column mask to be read inside another table's filter. The query fails with the error &lt;CODE&gt;UNSUPPORTED_NESTED_ROW_OR_COLUMN_ACCESS_POLICY&lt;/CODE&gt;. So the mapping table cannot be both filtered for users and used as the lookup for the claims filter simultaneously.&lt;/P&gt;
&lt;FIGURE style="margin: 20px 0;"&gt;
&lt;P style="color: #6b8a97; font-family: monospace; font-size: 13px; text-align: center; margin: 16px 0 0;"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="diagram_2_nested.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31346i2F93D2E0DC4518CA/image-size/large?v=v2&amp;amp;px=999" role="button" title="diagram_2_nested.png" alt="diagram_2_nested.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;FIGCAPTION style="font-size: 13px; color: #6b8a97; margin-top: 8px; font-style: italic; text-align: center;"&gt;A filtered mapping table cannot be read inside the claims table's filter. The two protections cannot stack this way.&lt;/FIGCAPTION&gt;
&lt;/FIGURE&gt;
&lt;P&gt;Here is the error in the workspace. The mapping table already has a row filter, and the claims table's filter tries to read it.&lt;/P&gt;
&lt;FIGURE style="margin: 16px 0;"&gt;
&lt;P style="color: #6b8a97; font-family: monospace; font-size: 13px; text-align: center; margin: 16px 0 0;"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="shot_sql_error.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31347iE1115D2DA04AAA83/image-size/large?v=v2&amp;amp;px=999" role="button" title="shot_sql_error.png" alt="shot_sql_error.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;FIGCAPTION style="font-size: 13px; color: #6b8a97; margin-top: 8px; font-style: italic; text-align: center;"&gt;The bind fails because the filter reads a table that already has its own filter. The call sequence in the error names both tables.&lt;/FIGCAPTION&gt;
&lt;/FIGURE&gt;
&lt;H2 id="pattern" style="font-size: 26px; font-weight: bold; color: #1b3139; margin: 36px 0 16px; padding-bottom: 8px; border-bottom: 3px solid #FF3621; display: inline-block;"&gt;How to scope the fact table&lt;/H2&gt;
&lt;P&gt;A row filter is a SQL function, so it can hold a subquery. Bind the filter to the join key on the claims table, and resolve the agent inside the function by joining the mapping table to the directory on the current user. As long as the mapping table has no filter of its own, there is no nesting and it works for every agent.&lt;/P&gt;
&lt;PRE style="background: #1B3139; border-left: 4px solid #FF3621; border-radius: 6px; padding: 20px 24px; margin: 20px 0; font-family: 'SF Mono', Monaco, Consolas, 'Courier New', monospace; font-size: 13.5px; line-height: 1.6; color: #e8ecf0; overflow-x: auto;"&gt;&lt;SPAN&gt;-- The filter takes the claims table's join key, the policy id.&lt;/SPAN&gt;
&lt;SPAN&gt;CREATE OR REPLACE FUNCTION&lt;/SPAN&gt; scope_claims_by_policy(policy_id &lt;SPAN&gt;STRING&lt;/SPAN&gt;)
  &lt;SPAN&gt;RETURN&lt;/SPAN&gt; is_account_group_member(&lt;SPAN&gt;'claims_supervisors'&lt;/SPAN&gt;)
      &lt;SPAN&gt;OR&lt;/SPAN&gt; policy_id &lt;SPAN&gt;IN&lt;/SPAN&gt; (
           &lt;SPAN&gt;SELECT&lt;/SPAN&gt; m.policy_id
           &lt;SPAN&gt;FROM&lt;/SPAN&gt;   policy_agent_map m
           &lt;SPAN&gt;JOIN&lt;/SPAN&gt;   agent_directory d &lt;SPAN&gt;ON&lt;/SPAN&gt; m.agent_id = d.agent_id
           &lt;SPAN&gt;WHERE&lt;/SPAN&gt;  d.user_email = current_user()
         );

&lt;SPAN&gt;ALTER TABLE&lt;/SPAN&gt; claim_fact &lt;SPAN&gt;SET ROW FILTER&lt;/SPAN&gt; scope_claims_by_policy &lt;SPAN&gt;ON&lt;/SPAN&gt; (policy_id);&lt;/PRE&gt;
&lt;FIGURE style="margin: 16px 0;"&gt;
&lt;P style="color: #6b8a97; font-family: monospace; font-size: 13px; text-align: center; margin: 16px 0 0;"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="shot_claimfact_filter.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31348iA1C9C95394ED28AA/image-size/large?v=v2&amp;amp;px=999" role="button" title="shot_claimfact_filter.png" alt="shot_claimfact_filter.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;FIGCAPTION style="font-size: 13px; color: #6b8a97; margin-top: 8px; font-style: italic; text-align: center;"&gt;The claims table in Catalog Explorer. The row filter is bound to &lt;CODE&gt;scope_by_policy&lt;/CODE&gt; on the &lt;CODE&gt;policy_id&lt;/CODE&gt; column.&lt;/FIGCAPTION&gt;
&lt;/FIGURE&gt;
&lt;FIGURE style="margin: 16px 0;"&gt;
&lt;P style="color: #6b8a97; font-family: monospace; font-size: 13px; text-align: center; margin: 16px 0 0;"&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="shot_function.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31355i99B05D5C00A784AB/image-size/large?v=v2&amp;amp;px=999" role="button" title="shot_function.png" alt="shot_function.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;FIGCAPTION style="font-size: 13px; color: #6b8a97; margin-top: 8px; font-style: italic; text-align: center;"&gt;The filter function as it sits in the catalog. Note that the security type is DEFINER, which is why the agent needs no access to the lookup.&lt;/FIGCAPTION&gt;
&lt;/FIGURE&gt;
&lt;P&gt;There is a useful detail in how this runs. Row filters run with the object owner's rights, apart from the identity checks &lt;CODE&gt;current_user()&lt;/CODE&gt; and &lt;CODE&gt;is_account_group_member()&lt;/CODE&gt;, which run as the querying user. So an agent needs no access to the mapping table or the directory. The function reads them under the owner's rights, and only the identity check runs as the agent. The agent needs access only to the claims table.&lt;/P&gt;
&lt;H2 id="control" style="font-size: 26px; font-weight: bold; color: #1b3139; margin: 36px 0 16px; padding-bottom: 8px; border-bottom: 3px solid #FF3621; display: inline-block;"&gt;One control table for many tables&lt;/H2&gt;
&lt;P&gt;In a real estate of tables, the same question comes up again and again on different tables. Rather than scatter lookups across many functions, put the mappings in one place. Create a small control table that holds the user and every key you scope on, and never apply a filter to it. Then point the filter for each table at that one control table.&lt;/P&gt;
&lt;PRE style="background: #1B3139; border-left: 4px solid #FF3621; border-radius: 6px; padding: 20px 24px; margin: 20px 0; font-family: 'SF Mono', Monaco, Consolas, 'Courier New', monospace; font-size: 13.5px; line-height: 1.6; color: #e8ecf0; overflow-x: auto;"&gt;&lt;SPAN&gt;-- One control table. Keep it locked down and never filter it.&lt;/SPAN&gt;
&lt;SPAN&gt;CREATE TABLE&lt;/SPAN&gt; agent_entitlements (
  user_email &lt;SPAN&gt;STRING&lt;/SPAN&gt;,
  agent_id   &lt;SPAN&gt;STRING&lt;/SPAN&gt;,
  policy_id  &lt;SPAN&gt;STRING&lt;/SPAN&gt;
);
&lt;SPAN&gt;REVOKE ALL PRIVILEGES&lt;/SPAN&gt; &lt;SPAN&gt;ON TABLE&lt;/SPAN&gt; agent_entitlements &lt;SPAN&gt;FROM&lt;/SPAN&gt; &lt;SPAN&gt;`account users`&lt;/SPAN&gt;;

&lt;SPAN&gt;-- Filter for any table keyed by policy id.&lt;/SPAN&gt;
&lt;SPAN&gt;CREATE OR REPLACE FUNCTION&lt;/SPAN&gt; scope_by_policy(policy_id &lt;SPAN&gt;STRING&lt;/SPAN&gt;)
  &lt;SPAN&gt;RETURN&lt;/SPAN&gt; is_account_group_member(&lt;SPAN&gt;'claims_supervisors'&lt;/SPAN&gt;)
      &lt;SPAN&gt;OR&lt;/SPAN&gt; policy_id &lt;SPAN&gt;IN&lt;/SPAN&gt; (
           &lt;SPAN&gt;SELECT&lt;/SPAN&gt; policy_id &lt;SPAN&gt;FROM&lt;/SPAN&gt; agent_entitlements
           &lt;SPAN&gt;WHERE&lt;/SPAN&gt; user_email = current_user()
         );

&lt;SPAN&gt;-- Filter for any table keyed by agent id, the mapping table included.&lt;/SPAN&gt;
&lt;SPAN&gt;CREATE OR REPLACE FUNCTION&lt;/SPAN&gt; scope_by_agent(agent_id &lt;SPAN&gt;STRING&lt;/SPAN&gt;)
  &lt;SPAN&gt;RETURN&lt;/SPAN&gt; is_account_group_member(&lt;SPAN&gt;'claims_supervisors'&lt;/SPAN&gt;)
      &lt;SPAN&gt;OR&lt;/SPAN&gt; agent_id &lt;SPAN&gt;IN&lt;/SPAN&gt; (
           &lt;SPAN&gt;SELECT&lt;/SPAN&gt; agent_id &lt;SPAN&gt;FROM&lt;/SPAN&gt; agent_entitlements
           &lt;SPAN&gt;WHERE&lt;/SPAN&gt; user_email = current_user()
         );&lt;/PRE&gt;
&lt;FIGURE style="margin: 20px 0;"&gt;
&lt;P style="color: #6b8a97; font-family: monospace; font-size: 13px; text-align: center; margin: 16px 0 0;"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="diagram_3_control.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31351i18EAC47D82F6F931/image-size/large?v=v2&amp;amp;px=999" role="button" title="diagram_3_control.png" alt="diagram_3_control.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;/FIGURE&gt;
&lt;P&gt;Because the control table is a separate object that is never filtered, you can now apply a filter to any table safely, including the mapping table. Tag each key column with a governed tag and create one policy per key type in Catalog Explorer, and the rule covers every table that carries that key. You can also bind a function to a single table by hand with &lt;CODE&gt;SET ROW FILTER&lt;/CODE&gt;. Onboarding an agent or moving a book of policies becomes a change to rows in the control table, not a change to code.&lt;/P&gt;
&lt;H2 id="read-map" style="font-size: 26px; font-weight: bold; color: #1b3139; margin: 36px 0 16px; padding-bottom: 8px; border-bottom: 3px solid #FF3621; display: inline-block;"&gt;When users also need to read the mapping table&lt;/H2&gt;
&lt;P&gt;If agents only ever read the claims table, you are done. If they also need to read the mapping table directly, there are two clean choices.&lt;/P&gt;
&lt;UL style="padding-left: 24px; margin: 0 0 16px;"&gt;
&lt;LI style="margin-bottom: 8px;"&gt;&lt;STRONG&gt;A view.&lt;/STRONG&gt; Leave the mapping table unfiltered, and create a view over it that filters to the current user. Give agents access to the view, not the base table. This is the smaller change.&lt;/LI&gt;
&lt;LI style="margin-bottom: 8px;"&gt;&lt;STRONG&gt;The control table.&lt;/STRONG&gt; With the control table in place, apply a filter to the mapping table directly with the &lt;CODE&gt;scope_by_agent&lt;/CODE&gt; function above. The control table is separate and never filtered, so there is no nesting and no view to maintain.&lt;/LI&gt;
&lt;/UL&gt;
&lt;PRE style="background: #1B3139; border-left: 4px solid #FF3621; border-radius: 6px; padding: 20px 24px; margin: 20px 0; font-family: 'SF Mono', Monaco, Consolas, 'Courier New', monospace; font-size: 13.5px; line-height: 1.6; color: #e8ecf0; overflow-x: auto;"&gt;&lt;SPAN&gt;-- The view option, if you would rather not filter the mapping table.&lt;/SPAN&gt;
&lt;SPAN&gt;CREATE OR REPLACE VIEW&lt;/SPAN&gt; policy_agent_map_user &lt;SPAN&gt;AS&lt;/SPAN&gt;
  &lt;SPAN&gt;SELECT&lt;/SPAN&gt; * &lt;SPAN&gt;FROM&lt;/SPAN&gt; policy_agent_map
  &lt;SPAN&gt;WHERE&lt;/SPAN&gt; is_account_group_member(&lt;SPAN&gt;'claims_supervisors'&lt;/SPAN&gt;)
     &lt;SPAN&gt;OR&lt;/SPAN&gt; agent_id &lt;SPAN&gt;IN&lt;/SPAN&gt; (
           &lt;SPAN&gt;SELECT&lt;/SPAN&gt; agent_id &lt;SPAN&gt;FROM&lt;/SPAN&gt; agent_entitlements
           &lt;SPAN&gt;WHERE&lt;/SPAN&gt; user_email = current_user()
         );&lt;/PRE&gt;
&lt;P&gt;Taking the control table option, the mapping table then carries its own row filter, and it resolves through the same control table as the claims table.&lt;/P&gt;
&lt;FIGURE style="margin: 16px 0;"&gt;
&lt;P style="color: #6b8a97; font-family: monospace; font-size: 13px; text-align: center; margin: 16px 0 0;"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="shot_map_filter.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31352i3760B000E60D73B9/image-size/large?v=v2&amp;amp;px=999" role="button" title="shot_map_filter.png" alt="shot_map_filter.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;FIGCAPTION style="font-size: 13px; color: #6b8a97; margin-top: 8px; font-style: italic; text-align: center;"&gt;The mapping table with its own row filter, &lt;CODE&gt;scope_by_agent&lt;/CODE&gt; on the &lt;CODE&gt;agent_id&lt;/CODE&gt; column, driven by the same control table.&lt;/FIGCAPTION&gt;
&lt;/FIGURE&gt;
&lt;H2 id="checking" style="font-size: 26px; font-weight: bold; color: #1b3139; margin: 36px 0 16px; padding-bottom: 8px; border-bottom: 3px solid #FF3621; display: inline-block;"&gt;Checking it works&lt;/H2&gt;
&lt;P&gt;These results are from a live run. The claims table holds 40 claims across 12 policies and 3 agents. Signed in as an agent who services 3 policies, a plain query with no where clause returns only their rows.&lt;/P&gt;
&lt;FIGURE style="margin: 16px 0;"&gt;
&lt;P style="color: #6b8a97; font-family: monospace; font-size: 13px; text-align: center; margin: 16px 0 0;"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="shot_sql_scoped.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31353i57823D656CFB7B80/image-size/large?v=v2&amp;amp;px=999" role="button" title="shot_sql_scoped.png" alt="shot_sql_scoped.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;FIGCAPTION style="font-size: 13px; color: #6b8a97; margin-top: 8px; font-style: italic; text-align: center;"&gt;Claims grouped by policy for the signed-in agent. Three policies returned, 11 of the 40 claims in the table.&lt;/FIGCAPTION&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;/FIGURE&gt;
&lt;P&gt;The same agent reading the mapping table sees only their own three policies, not all twelve.&lt;/P&gt;
&lt;FIGURE style="margin: 16px 0;"&gt;
&lt;P style="color: #6b8a97; font-family: monospace; font-size: 13px; text-align: center; margin: 16px 0 0;"&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="shot_sql_mapping.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31354iE9AB0CC8986803F0/image-size/large?v=v2&amp;amp;px=999" role="button" title="shot_sql_mapping.png" alt="shot_sql_mapping.png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;FIGCAPTION style="font-size: 13px; color: #6b8a97; margin-top: 8px; font-style: italic; text-align: center;"&gt;The mapping table read by the same agent. Three of its twelve rows.&lt;/FIGCAPTION&gt;
&lt;/FIGURE&gt;
&lt;TABLE style="border-collapse: collapse; width: 100%; margin: 16px 0; font-size: 15px;"&gt;
&lt;TBODY&gt;
&lt;TR&gt;
&lt;TH style="background: #1B3139; color: #fff; padding: 10px 14px; text-align: left;"&gt;Query, run with no where clause&lt;/TH&gt;
&lt;TH style="background: #1B3139; color: #fff; padding: 10px 14px; text-align: left;"&gt;All rows in the table&lt;/TH&gt;
&lt;TH style="background: #1B3139; color: #fff; padding: 10px 14px; text-align: left;"&gt;Seen as one agent&lt;/TH&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD style="padding: 10px 14px; border-bottom: 1px solid #E0E0E0;"&gt;claims&lt;/TD&gt;
&lt;TD style="padding: 10px 14px; border-bottom: 1px solid #E0E0E0;"&gt;40&lt;/TD&gt;
&lt;TD style="padding: 10px 14px; border-bottom: 1px solid #E0E0E0;"&gt;11&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR style="background: #F7F9FA;"&gt;
&lt;TD style="padding: 10px 14px; border-bottom: 1px solid #E0E0E0;"&gt;distinct policies&lt;/TD&gt;
&lt;TD style="padding: 10px 14px; border-bottom: 1px solid #E0E0E0;"&gt;12&lt;/TD&gt;
&lt;TD style="padding: 10px 14px; border-bottom: 1px solid #E0E0E0;"&gt;3&lt;/TD&gt;
&lt;/TR&gt;
&lt;TR&gt;
&lt;TD style="padding: 10px 14px; border-bottom: 1px solid #E0E0E0;"&gt;mapping rows&lt;/TD&gt;
&lt;TD style="padding: 10px 14px; border-bottom: 1px solid #E0E0E0;"&gt;12&lt;/TD&gt;
&lt;TD style="padding: 10px 14px; border-bottom: 1px solid #E0E0E0;"&gt;3&lt;/TD&gt;
&lt;/TR&gt;
&lt;/TBODY&gt;
&lt;/TABLE&gt;
&lt;P&gt;Test the all-access path too. A member of the &lt;CODE&gt;claims_supervisors&lt;/CODE&gt; group, or a service account that runs the pipelines, gets every row. To check that path without waiting on group membership to take effect, add one row to the control table for your own user and rerun. The counts move at once. Remove the row and they return to the scoped numbers.&lt;/P&gt;
&lt;H2 id="keep-in-mind" style="font-size: 26px; font-weight: bold; color: #1b3139; margin: 36px 0 16px; padding-bottom: 8px; border-bottom: 3px solid #FF3621; display: inline-block;"&gt;Things to keep in mind&lt;/H2&gt;
&lt;UL style="padding-left: 24px; margin: 0 0 16px;"&gt;
&lt;LI style="margin-bottom: 8px;"&gt;The tables a filter reads must not have their own active filter or mask. Keep the control table and the directory unfiltered.&lt;/LI&gt;
&lt;LI style="margin-bottom: 8px;"&gt;Keep the control table small so the join runs as a broadcast hash join. A few simple conditions read faster than many.&lt;/LI&gt;
&lt;LI style="margin-bottom: 8px;"&gt;Match the function parameter type to the key column type, and keep ANSI mode on, so a bad cast raises an error rather than returning null and letting rows through.&lt;/LI&gt;
&lt;LI style="margin-bottom: 8px;"&gt;A table can have one row filter in effect. A policy keyed on policy id and a policy keyed on agent id are separate and do not clash on the same table.&lt;/LI&gt;
&lt;LI style="margin-bottom: 8px;"&gt;Use the key that matches the table's grain. Use the policy id for a policy-level table, or a household or account key for those grains, and tag whichever key you join on.&lt;/LI&gt;
&lt;/UL&gt;
&lt;HR /&gt;
&lt;P&gt;The shape of this pattern is the same whenever the value that decides access is not on the table you want to protect. Put the mapping in a control table, keep it unfiltered, and read it from a row filter bound to the join key. It scopes one table or a whole estate with the same small set of pieces.&lt;/P&gt;
&lt;P style="font-size: 15px; color: #5a7a86; margin-top: 24px;"&gt;Further reading in the Databricks documentation: &lt;A style="color: #ff3621; text-decoration: none;" href="https://docs.databricks.com/aws/en/tables/row-and-column-filters" target="_blank"&gt;Filter sensitive table data using row filters and column masks&lt;/A&gt;, &lt;A style="color: #ff3621; text-decoration: none;" href="https://docs.databricks.com/aws/en/data-governance/unity-catalog/abac/" target="_blank"&gt;Attribute-based access control in Unity Catalog&lt;/A&gt;, and &lt;A style="color: #ff3621; text-decoration: none;" href="https://docs.databricks.com/aws/en/views/dynamic" target="_blank"&gt;Create a dynamic view&lt;/A&gt;.&lt;/P&gt;
&lt;P&gt;I would like to hear from others, too. Have you had cases where the value that decides access sits in an awkward place, a different table, a hierarchy, or several hops away, and how did you handle it in your own projects? Share what worked for you in the comments.&lt;/P&gt;
&lt;/DIV&gt;</description>
      <pubDate>Mon, 21 Sep 2026 13:39:31 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/row-level-security-when-the-filter-column-is-not-on-your-table/m-p/169339#M1587</guid>
      <dc:creator>Ashwin_DSA</dc:creator>
      <dc:date>2026-09-21T13:39:31Z</dc:date>
    </item>
    <item>
      <title>Check my blog on Databricks Unity Catalog</title>
      <link>https://community.databricks.com/t5/community-articles/check-my-blog-on-databricks-unity-catalog/m-p/169318#M1586</link>
      <description>&lt;P&gt;&lt;SPAN&gt;Struggling with a legacy Hive Metastore? I published a deep dive on modernizing your Data Governance on Databricks.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Read how we executed a Unity Catalog migration for a complex, multi-tenant enterprise without disrupting daily business operations.&lt;/SPAN&gt;&lt;/P&gt;&lt;P&gt;&lt;A href="https://medium.com/@kapil.dua/navigating-multi-tenant-data-governance-transitioning-to-databricks-unity-catalog-7fd90b4fe48f" target="_blank"&gt;https://medium.com/@kapil.dua/navigating-multi-tenant-data-governance-transitioning-to-databricks-unity-catalog-7fd90b4fe48f&lt;/A&gt;&lt;/P&gt;</description>
      <pubDate>Mon, 21 Sep 2026 12:28:53 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/check-my-blog-on-databricks-unity-catalog/m-p/169318#M1586</guid>
      <dc:creator>kapil_dua</dc:creator>
      <dc:date>2026-09-21T12:28:53Z</dc:date>
    </item>
    <item>
      <title>Genie Ontology na prática</title>
      <link>https://community.databricks.com/t5/community-articles/genie-ontology-na-pr%C3%A1tica/m-p/169253#M1582</link>
      <description>&lt;P&gt;&lt;A class="" href="https://www.linkedin.com/company/databricks/" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Databricks&lt;/SPAN&gt;&lt;/A&gt; &lt;SPAN&gt;- Genie Ontology 🧞‍&lt;span class="lia-unicode-emoji" title=":male_sign:"&gt;♂️&lt;/span&gt;&lt;/SPAN&gt; &lt;SPAN&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;SPAN&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;SPAN&gt;Se "receita" significa uma coisa para Vendas, outra para o Financeiro e uma terceira no ERP, o seu agente de IA vai escolher uma sozinho. E vai responder com a mesma confiança nos três casos.&lt;/SPAN&gt;&lt;SPAN&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;SPAN&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;SPAN&gt;É esse o problema que o Genie Ontology tenta resolver, e montei as quatro peças do zero num workspace Databricks.&lt;/SPAN&gt;&lt;SPAN&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;SPAN&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;SPAN&gt;No artigo eu explico, com a documentação oficial na mão e print do meu ambiente em cada etapa:&lt;/SPAN&gt;&lt;SPAN&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;SPAN&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;SPAN&gt;&lt;span class="lia-unicode-emoji" title=":heavy_check_mark:"&gt;✔️&lt;/span&gt; o que é Genie Ontology, e como ele decide em qual fonte confiar quando duas se contradizem (a lógica de authority score é parecida com PageRank)&lt;/SPAN&gt;&lt;SPAN&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;SPAN&gt;&lt;span class="lia-unicode-emoji" title=":heavy_check_mark:"&gt;✔️&lt;/span&gt; o que é um Domain, por que ele é uma governed tag por baixo, e o ganho de negócio de delimitar a descoberta por área&lt;/SPAN&gt;&lt;SPAN&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;SPAN&gt;&lt;span class="lia-unicode-emoji" title=":heavy_check_mark:"&gt;✔️&lt;/span&gt; o que é uma Page, quais são os 7 campos dela, e o detalhe que mais me interessou: definição escrita por gente vence contexto inferido no desempate&lt;/SPAN&gt;&lt;SPAN&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;SPAN&gt;&lt;span class="lia-unicode-emoji" title=":heavy_check_mark:"&gt;✔️&lt;/span&gt; o que é certificação, e por que ela influencia diretamente qual contexto prevalece&lt;/SPAN&gt;&lt;SPAN&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;SPAN&gt;&lt;span class="lia-unicode-emoji" title=":heavy_check_mark:"&gt;✔️&lt;/span&gt; o Genie One citando a minha Page como fonte, com a conta conferida na mão&lt;/SPAN&gt;&lt;SPAN&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;SPAN&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;SPAN&gt;E o caminho de começar sem virar megaprojeto: um domínio, uma métrica crítica, um fluxo onde o time já perde tempo reconciliando número na mão.&lt;BR /&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;SPAN&gt;Artigo no linkedin:&amp;nbsp;&lt;A href="https://www.linkedin.com/posts/thomaz-antonio-rossito-neto_databricks-genieontology-datagovernance-activity-7506666887947280386-i2XZ?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAApnG2sBB0_3dQhhqwrICkcUDtERVIHY0KE" target="_blank" rel="noopener"&gt;https://www.linkedin.com/posts/thomaz-antonio-rossito-neto_databricks-genieontology-datagovernance-activity-7506666887947280386-i2XZ?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAApnG2sBB0_3dQhhqwrICkcUDtERVIHY0KE&lt;/A&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;SPAN&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;A class="" href="https://www.linkedin.com/search/results/all/?keywords=%23databricks&amp;amp;origin=HASH_TAG_FROM_FEED" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;&lt;SPAN&gt;#&lt;/SPAN&gt;Databricks&lt;/SPAN&gt;&lt;/A&gt; &lt;A class="" href="https://www.linkedin.com/search/results/all/?keywords=%23genieontology&amp;amp;origin=HASH_TAG_FROM_FEED" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;&lt;SPAN&gt;#&lt;/SPAN&gt;GenieOntology&lt;/SPAN&gt;&lt;/A&gt; &lt;A class="" href="https://www.linkedin.com/search/results/all/?keywords=%23datagovernance&amp;amp;origin=HASH_TAG_FROM_FEED" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;&lt;SPAN&gt;#&lt;/SPAN&gt;DataGovernance&lt;/SPAN&gt;&lt;/A&gt; &lt;A class="" href="https://www.linkedin.com/search/results/all/?keywords=%23unitycatalog&amp;amp;origin=HASH_TAG_FROM_FEED" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;&lt;SPAN&gt;#&lt;/SPAN&gt;UnityCatalog&lt;/SPAN&gt;&lt;/A&gt; &lt;A class="" href="https://www.linkedin.com/search/results/all/?keywords=%23engenhariadedados&amp;amp;origin=HASH_TAG_FROM_FEED" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;&lt;SPAN&gt;#&lt;/SPAN&gt;EngenhariaDeDados&lt;/SPAN&gt;&lt;/A&gt; &lt;A class="" href="https://www.linkedin.com/search/results/all/?keywords=%23arquitetodedados&amp;amp;origin=HASH_TAG_FROM_FEED" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;&lt;SPAN&gt;#&lt;/SPAN&gt;ArquitetoDeDados&lt;/SPAN&gt;&lt;/A&gt; &lt;A class="" href="https://www.linkedin.com/search/results/all/?keywords=%23ai&amp;amp;origin=HASH_TAG_FROM_FEED" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;&lt;SPAN&gt;#&lt;/SPAN&gt;AI&lt;/SPAN&gt;&lt;/A&gt; &lt;A class="" href="https://www.linkedin.com/search/results/all/?keywords=%23agentic&amp;amp;origin=HASH_TAG_FROM_FEED" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;&lt;SPAN&gt;#&lt;/SPAN&gt;Agentic&lt;/SPAN&gt;&lt;/A&gt;&lt;/P&gt;</description>
      <pubDate>Sun, 20 Sep 2026 18:31:53 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/genie-ontology-na-pr%C3%A1tica/m-p/169253#M1582</guid>
      <dc:creator>ThomazNeto</dc:creator>
      <dc:date>2026-09-20T18:31:53Z</dc:date>
    </item>
    <item>
      <title>Property &amp; Casualty Insurance Lakehouse</title>
      <link>https://community.databricks.com/t5/community-articles/property-amp-casualty-insurance-lakehouse/m-p/169210#M1581</link>
      <description>&lt;P&gt;When building a Property &amp;amp; Casualty Insurance Lakehouse, one of the first things to understand is:&lt;/P&gt;&lt;P&gt;A Claim is not just a Claim Amount.&lt;/P&gt;&lt;P&gt;Consider a simple auto claim.&lt;/P&gt;&lt;P&gt;At different points in the lifecycle, the same claim might have:&lt;/P&gt;&lt;P&gt;Indemnity Reserve = $15,000&lt;BR /&gt;Medical Reserve = $8,000&lt;BR /&gt;Legal/Defense Reserve = $4,000&lt;/P&gt;&lt;P&gt;Then actual payments start:&lt;/P&gt;&lt;P&gt;Indemnity Paid = $10,000&lt;BR /&gt;Medical Paid = $3,000&lt;BR /&gt;Legal Expense Paid = $1,500&lt;/P&gt;&lt;P&gt;And later the carrier may recover:&lt;/P&gt;&lt;P&gt;Subrogation Recovery = $6,000&lt;BR /&gt;Salvage Recovery = $1,000&lt;/P&gt;&lt;P&gt;This is where the data modeling becomes interesting.&lt;/P&gt;&lt;P&gt;If our Lakehouse has only:&lt;/P&gt;&lt;P&gt;claim_id | claim_amount&lt;/P&gt;&lt;P&gt;we have already lost much of the business context.&lt;/P&gt;&lt;P&gt;Instead, the model needs to preserve the relationship between:&lt;/P&gt;&lt;P&gt;Policy&lt;BR /&gt;→ Policy Term&lt;BR /&gt;→ Coverage&lt;BR /&gt;→ Insured Risk&lt;BR /&gt;→ Claim&lt;BR /&gt;→ Claim Exposure&lt;BR /&gt;→ Claim Financials&lt;/P&gt;&lt;P&gt;And Claim Financials themselves need to distinguish:&lt;/P&gt;&lt;P&gt;Reserve&lt;BR /&gt;Payment&lt;BR /&gt;Expense&lt;BR /&gt;Recovery&lt;/P&gt;&lt;P&gt;with additional classifications such as indemnity, medical, legal, subrogation and salvage.&lt;/P&gt;&lt;P&gt;Why does this matter?&lt;/P&gt;&lt;P&gt;Because the Lakehouse should eventually be able to answer questions such as:&lt;/P&gt;&lt;P&gt;What is the outstanding reserve?&lt;/P&gt;&lt;P&gt;How much has actually been paid?&lt;/P&gt;&lt;P&gt;How much of the loss is indemnity vs expense?&lt;/P&gt;&lt;P&gt;How much has been recovered through subrogation?&lt;/P&gt;&lt;P&gt;What was the applicable coverage when the loss occurred?&lt;/P&gt;&lt;P&gt;What is the incurred loss at a given point in time?&lt;/P&gt;&lt;P&gt;What is the loss ratio by coverage, risk, geography or line of business?&lt;/P&gt;&lt;P&gt;This is why I find industry data models interesting.&lt;/P&gt;&lt;P&gt;The value isn't simply having hundreds of tables.&lt;/P&gt;&lt;P&gt;The value is preserving the business grain and relationships so that downstream analytics can reconstruct what actually happened.&lt;/P&gt;&lt;P&gt;In Part 2 of my exploration of Databricks Lakehouse Industry Data Models, I went deeper into what a P&amp;amp;C Insurance reference architecture could look like — from Party, Submission and Policy through Coverage, Risk, Premium, Claims, Claim Financials, Reinsurance and Catastrophe.&lt;/P&gt;&lt;P&gt;Full article:&lt;/P&gt;&lt;P&gt;&lt;A href="https://dataengineeringcopilot.com/blog/databricks-pc-insurance-data-model" target="_blank"&gt;https://dataengineeringcopilot.com/blog/databricks-pc-insurance-data-model&lt;/A&gt;&lt;/P&gt;&lt;P&gt;My next step is to take this from architecture to implementation.&lt;/P&gt;&lt;P&gt;I want to build a small P&amp;amp;C Lakehouse on Databricks:&lt;/P&gt;&lt;P&gt;Bronze → raw policy, claim and financial transactions&lt;/P&gt;&lt;P&gt;Silver → standardized policy, coverage, claim exposure, reserve, payment and recovery data&lt;/P&gt;&lt;P&gt;Gold → Policy 360, Claim 360 and insurance KPIs such as paid loss, outstanding reserve, incurred loss and loss ratio&lt;/P&gt;&lt;P&gt;That is where an industry data model becomes much more than an ER diagram.&lt;/P&gt;&lt;P&gt;It becomes the semantic foundation of the Lakehouse.&lt;/P&gt;&lt;P&gt;#Databricks #Lakehouse #DataEngineering #Insurance #PropertyAndCasualty #DataModeling #DeltaLake #Claims #DataArchitecture&lt;/P&gt;</description>
      <pubDate>Sun, 20 Sep 2026 03:42:47 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/property-amp-casualty-insurance-lakehouse/m-p/169210#M1581</guid>
      <dc:creator>AmitDECopilot</dc:creator>
      <dc:date>2026-09-20T03:42:47Z</dc:date>
    </item>
    <item>
      <title>DQX - Everything The Docs Don’t Say Yet</title>
      <link>https://community.databricks.com/t5/community-articles/dqx-everything-the-docs-don-t-say-yet/m-p/169166#M1580</link>
      <description>&lt;P class=""&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;DQX Studio already has its own documentation for the rule editor, the approval workflow, the scheduler. This is what running it against real pipelines, real approvers, and a growing quarantine table made us add on top of it — and around it. It'll be out of date the next time we ship something, and that's fine.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;A href="https://databrickslabs.github.io/dqx/" target="_blank" rel="noopener"&gt;DQX&lt;/A&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;gives you check functions and an engine. DQX Studio — the UI and control plane we run on top of it — gives you a place to author, approve, schedule, and review those checks without writing a notebook. Studio itself is documented elsewhere; this isn’t that document.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;This is the list of things that turned out to be missing once real people, not just a pipeline, depended on it. An analyst who wants to see the data before writing a filter. An approver who wants to know why a rule looks the way it does, six weeks later. A scheduler that has to decide, unattended, whether “the last hour” means anything for a given table. A quarantine table that needs an owner other than whoever happens to open it.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;What follows, roughly in the order we hit them.&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="KeerthiItigimat_0-1789797084706.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31314iE74E870298014204/image-size/large?v=v2&amp;amp;px=999" role="button" title="KeerthiItigimat_0-1789797084706.png" alt="KeerthiItigimat_0-1789797084706.png" /&gt;&lt;/span&gt;&lt;STRONG&gt;Figure 1.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Three layers, one API contract. Studio's own docs cover the middle layer; this post is the outer one.&lt;/SPAN&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;/DIV&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;01&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Preview the data before you write anything&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Every rule starts with someone looking at the table. The preview panel in the rule editor runs a live sample against Unity Catalog under the caller’s own identity — on-behalf-of, so row filters and column masks apply the same way they would to any other query that person runs — with two things layered on:&lt;/FONT&gt;&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;STRONG&gt;A column picker&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;to narrow a wide table down to what’s relevant before you start authoring a check against it.&lt;/FONT&gt;&lt;/LI&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;STRONG&gt;A natural-language filter box.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;Type “orders from the last week with a null customer_id” and it’s handed to a Foundation Model with the table’s real column list and a hard constraint — use only these columns, always cap the row count, default to a plain&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;SELECT *&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;if the request doesn’t resolve to anything sensible. The model returns SQL, not an answer; if the call fails for any reason, the preview falls back to an ordinary unfiltered sample rather than erroring out.&lt;/FONT&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Small feature, but it collapses the gap between “I can describe the bad rows” and “I can write a SQL predicate for them” — which is most of the distance between a domain expert and a rule.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;02&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Two ways to run a check, on purpose&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Databricks Apps don’t run Spark. So every dry run has to leave the app process one way or another — and we deliberately built two different exits for it, because “fast” and “exactly how it’ll run in production” are different needs.&lt;/FONT&gt;&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;STRONG&gt;Run Locally&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;executes against a small sample through a local, serverless Spark session — a few seconds, no job to wait on. Good for iterating on a rule while you’re still writing it.&lt;/FONT&gt;&lt;/LI&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;STRONG&gt;Run as Workflow&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;submits the same check to the actual task-runner job — the identical path a schedule will use in production, including the real sample size and Row Scope. Slower, but it’s not a simulation of the production path, it&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;is&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;the production path.&lt;/FONT&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;An author uses the first ten times while shaping a rule and the second once, right before submitting it for approval, to confirm it survives contact with the real execution environment.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;03&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Every rule remembers how it was made&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;A rule authored six weeks ago, by someone else, using a phrase like “flag orders where the region code doesn’t match our list” — an approver reviewing that later shouldn’t have to reverse-engineer intent from a YAML blob. Two things carry that context forward: a full audit trail (dq_quality_rules_history) recording every version, every status transition, and who made it, not just the current state; and, for AI-generated checks specifically, the original natural-language instruction travels with the rule as metadata rather than being discarded once the check function comes back. The prompt that produced a rule is part of the rule’s own record, not a one-time scratch input.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;04&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Global rules, for the check that isn’t about one table&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Most checks are bound to a table —&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;orders.customer_id is not null. Some aren’t really about a table at all; they’re about a&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;shape&lt;/EM&gt;: “any column named&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;email&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;should look like an email,” regardless of which of forty tables it shows up in. Global rules are authored once, table-independent, and apply wherever a matching column exists — without re-authoring the same check forty times or drifting slightly different each time someone copies it.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;05&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Cross-table checks, and what production found&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;DQX’s&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;sql_query&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;check is the right tool for anything that isn’t row-local — a duplicate key, a referential mismatch between two tables. It also has a real contract: an explicit&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;condition&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;column, and&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;merge_columns&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;to join violations back to specific input rows. An author writing “give me the rows that violate this” doesn’t know that contract, and shouldn’t have to.&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;DIV class=""&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;what an author writes vs. what DQX's engine requires&lt;/FONT&gt;&lt;/DIV&gt;&lt;DIV class=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="KeerthiItigimat_1-1789797204606.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31315i997A702D28713AD9/image-size/large?v=v2&amp;amp;px=999" role="button" title="KeerthiItigimat_1-1789797204606.png" alt="KeerthiItigimat_1-1789797204606.png" /&gt;&lt;/span&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;The editor derives that wrap on save, and only trusts a column as a merge key once it’s checked against the target table’s&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;real&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;schema — a renamed join column looks safe in the query text and fails at execution time, so schema-text alone isn’t enough. A related fix went the other way: for a query that doesn’t actually depend on its home table’s rows (its own independent join), the engine was pre-sampling that input before the check ran, which could silently drop real violations whose key wasn’t in the sampled slice. The fix routes that shape through the same fast path a true cross-table check already used, instead of teaching the row-level engine a special case.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Underneath both: Unity Catalog metadata reaching a fresh serverless job can lag well behind a SQL warehouse seeing the same object, so the read path needed a retry budget sized off actual observed timelines, not a guess.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;06&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Alerting that isn’t just “check the app”&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;A validation result nobody sees is worth nothing. Two channels out:&lt;/FONT&gt;&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;STRONG&gt;Microsoft Teams&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;webhooks, scoped per channel by trigger — all runs, scheduled-only, or manual-only — so a channel used for production monitoring doesn’t fill up with every dry run an author fires off while iterating.&lt;/FONT&gt;&lt;/LI&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;STRONG&gt;Site24x7&lt;/STRONG&gt;, or any uptime monitor, via a pair of read-only status endpoints — latest result by table, or by a specific run id — that return a plain 200 on a clean run and a 503 the moment error rows show up. External monitoring infrastructure gets to treat data quality the same way it treats an HTTP health check, without knowing anything about DQX.&lt;/FONT&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;07&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Schedules that know what they’re sampling&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;“Validate the last hour” only means something if the scheduler knows which column is time, and on a table it’s never seen before, that’s a real guess. It tries typed timestamp/date columns first, then a name-priority list, and surfaces the candidates as editable tags — a schedule owner can pin the right one, remove one that’s wrong for their table, or leave it on automatic. Row Scope pairs with tag-based scope filtering, so one schedule can target “every approved rule labeled&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;pii” across every table that has one, on either the full table or a time-windowed sample, instead of one schedule per table.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;08&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Closing the loop, not just quarantining&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Everything above eventually produces failing rows, and Studio’s side of what happens to them is deliberately small: a per-dataset YAML runbook, edited or uploaded in the app, written straight to a Unity Catalog Volume. The app checks that it’s valid YAML and writes it — it doesn’t parse rule names, strategies, or parameters, so a new remediation strategy is a change to the consuming pipeline, never to Studio.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;What reads those runbooks — an agentic re-ingestion system that tries the documented fix first and falls back to an LLM only for what nobody has written a playbook for — is its own build, written up separately:&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;A href="https://keerthimath.github.io/blog/agent-proposes-dqx-disposes/" target="_blank" rel="noopener"&gt;&lt;STRONG&gt;Agent Proposes, DQX Disposes →&lt;/STRONG&gt;&lt;/A&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;09&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;dqx-validator — the write side of the dead-letter pattern&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Everything so far assumes rows are already in a Silver table by the time DQX sees them.&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;dqx-validator&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;is the piece that runs earlier — a standalone library an ingestion pipeline calls directly, mid-flight, to apply Studio’s own approved rules to a DataFrame before it lands, and quarantine whatever fails.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;It loads&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;status = 'approved'&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;checks from the same rules tables Studio’s approval workflow writes to, resolved per-environment from a small table config. The loader is exposed as a single function,&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;approved_checks_for(table_fqns), and the agentic re-ingestion side calls the exact same function — so the checks that quarantined a row and the checks that later re-validate a proposed fix are guaranteed to be the same object, not two copies that can drift.&lt;BR /&gt;&lt;BR /&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="KeerthiItigimat_3-1789797710611.png" style="width: 708px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31317i40D76A52F8CD8492/image-dimensions/708x209?v=v2" width="708" height="209" role="button" title="KeerthiItigimat_3-1789797710611.png" alt="KeerthiItigimat_3-1789797710611.png" /&gt;&lt;/span&gt;&lt;BR /&gt;&lt;BR /&gt;&lt;SPAN&gt;Splitting the DataFrame is one call to DQX’s own&lt;/SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;apply_checks_by_metadata_and_split&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;— no custom validation logic, just the public engine. The quarantine row carries the input schema unchanged plus bookkeeping columns (&lt;/SPAN&gt;_error&lt;SPAN&gt;,&lt;/SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;_warning&lt;SPAN&gt;,&lt;/SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;_generated_at&lt;SPAN&gt;,&lt;/SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;_data_source&lt;SPAN&gt;,&lt;/SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;_rule_sources&lt;SPAN&gt;,&lt;/SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;_is_cross_table&lt;SPAN&gt;), and one more that matters more than it looks:&lt;/SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;_row_id&lt;SPAN&gt;, a hash of whichever business-key columns the caller names. That’s the field the entire review queue on the read side keys every quarantined row on. Name the wrong columns, or none, and the hash falls back to every input column — which still works, but logs a warning, because at that point two rows differing only in a bookkeeping timestamp look like two different violations instead of one.&lt;/SPAN&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;/DIV&gt;&lt;DIV class=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="KeerthiItigimat_2-1789797607909.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31316i946E6220F74AF4AF/image-size/large?v=v2&amp;amp;px=999" role="button" title="KeerthiItigimat_2-1789797607909.png" alt="KeerthiItigimat_2-1789797607909.png" /&gt;&lt;/span&gt;&lt;/DIV&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#808080"&gt;&lt;STRONG&gt;Figure 2.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;Items 08 and 09 are two ends of the same loop — dqx-validator writes to quarantine, the remediation pipeline reads it back out, and both sides resolve rules the same way.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;10&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Ask it directly, instead of building another chart&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Studio has an Insights dashboard for the questions we anticipated. It doesn’t have one for the question someone asks once, in passing — “which tables had a spike in errors last Tuesday” isn’t worth a dashboard tile, but it’s a completely reasonable thing to want to know. A Genie space scoped to Studio’s own schema — run history, quarantine records, metrics — turns that into a conversation instead of a feature request: ask in plain language, get an answer grounded in the real tables, no ticket filed to add a chart nobody will look at again.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;11&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/FONT&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;What all of this has in common&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;None of the ten items above touch the DQX library. Every one of them is a caller of its public API, the same way a notebook is — the editor, the validator library, the agentic pipeline, all of it sits beside DQX rather than inside a fork of it. That was never an accident; it’s the only way any of this stays upgradeable as the library keeps moving.&lt;/FONT&gt;&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;&lt;FONT face="georgia,palatino" size="4" color="#008080"&gt;Studio is documented. This is everything the documentation doesn’t say yet.&lt;/FONT&gt;&lt;/STRONG&gt;&lt;/P&gt;</description>
      <pubDate>Sat, 19 Sep 2026 06:04:43 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/dqx-everything-the-docs-don-t-say-yet/m-p/169166#M1580</guid>
      <dc:creator>KeerthiItigimat</dc:creator>
      <dc:date>2026-09-19T06:04:43Z</dc:date>
    </item>
    <item>
      <title>Unmasking the Connected Consumer</title>
      <link>https://community.databricks.com/t5/community-articles/unmasking-the-connected-consumer/m-p/169161#M1579</link>
      <description>&lt;P&gt;&lt;FONT face="georgia,palatino" size="3" color="#333333"&gt;&lt;SPAN&gt;Behind every clean “Golden Record” is a messy web of shared emails, phones, and legacy IDs. Here’s how we turned that web into an interactive, cluster-aware graph a data steward can actually work with — built on Streamlit, NetworkX, and a Databricks SQL Warehouse.&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;In the world of Master Data Management, the “Single View of the Customer” is the holy grail. But behind every clean Golden Record lies a messy web of raw data: five different email addresses, shared phone numbers, and legacy system IDs that may or may not belong to the same person.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Identifying these links is&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;identity resolution&lt;/STRONG&gt;. Visualizing them, however, is where the real insights happen — and where we built the&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;Entity Linkage Explorer&lt;/STRONG&gt;: a Streamlit app that lets data stewards traverse identity clusters, isolate specific data “islands,” and filter out noisy attributes in real time, all against a live Databricks SQL Warehouse.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="KeerthiItigimat_0-1789796005971.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31308i0343E10EC800A777/image-size/large?v=v2&amp;amp;px=999" role="button" title="KeerthiItigimat_0-1789796005971.png" alt="KeerthiItigimat_0-1789796005971.png" /&gt;&lt;/span&gt;&lt;FONT face="georgia,palatino" color="#808080"&gt;&lt;STRONG&gt;Figure 1.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;The Entity Linkage Explorer running as a Databricks App — the sidebar drives what the graph shows; the canvas renders only the clusters the steward has toggled on.&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;01&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;From tables to topology&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Traditional data quality tools give you a table. But tables are terrible at showing&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;transitive relationships&lt;/STRONG&gt;. If Person A shares an email with Person B, and Person B shares a phone number with Person C, a table makes it hard to see that all three might be the same household.&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;P&gt;&lt;FONT size="4" color="#008000"&gt;&lt;EM&gt;&lt;FONT face="georgia,palatino"&gt;Tables show rows. They don’t show the shape of a relationship.&lt;/FONT&gt;&lt;/EM&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;FONT face="georgia,palatino" size="4" color="#333333"&gt;&lt;SPAN class=""&gt;The core problem&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/DIV&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;We needed a tool that could:&lt;/FONT&gt;&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;STRONG&gt;Query scale.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;Pull deep lineage from a Databricks SQL Warehouse without dragging the whole table across the wire.&lt;/FONT&gt;&lt;/LI&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;STRONG&gt;Identify clusters.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;Automatically group connected individuals into “islands.”&lt;/FONT&gt;&lt;/LI&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;STRONG&gt;Handle noise.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;Dynamically exclude junk data —&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;test@test.com,&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;123 Main St&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;— that causes false links.&lt;/FONT&gt;&lt;/LI&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;STRONG&gt;Perform.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;Render hundreds of nodes without crashing the browser.&lt;/FONT&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;02&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;A four-tier, graph-first architecture&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;The app separates security, data, logic, and rendering into their own tiers — each one only doing the job it’s good at.&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="KeerthiItigimat_1-1789796189053.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31309iC01E2D30130F2C51/image-size/large?v=v2&amp;amp;px=999" role="button" title="KeerthiItigimat_1-1789796189053.png" alt="KeerthiItigimat_1-1789796189053.png" /&gt;&lt;/span&gt;&lt;/DIV&gt;&lt;DIV class=""&gt;&lt;FONT color="#FF6600"&gt;&lt;FONT color="#808080"&gt;&lt;STRONG&gt;&lt;FONT color="#808080"&gt;Figure&lt;/FONT&gt; &lt;FONT color="#808080"&gt;2.&lt;/FONT&gt;&lt;/STRONG&gt;&lt;/FONT&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;&lt;FONT color="#808080"&gt;The app captures the user’s OBO token to establish a secure session, pulls only the columns needed for linking, builds a logical graph with NetworkX, then hands it to PyVis to render&lt;/FONT&gt;.&lt;/SPAN&gt;&lt;/FONT&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;/DIV&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;To keep the data tier fast, we avoid&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;SELECT *&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;and pull only the attributes necessary for linking. Before drawing anything, we build a logical graph in&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;NetworkX&lt;/STRONG&gt;: every person and every attribute — an email, a phone number, an address — becomes a node, and a shared attribute becomes an edge between the people who share it. Running&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;Connected Components&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;over that graph instantly identifies every isolated cluster in the dataset, no matter how many hops apart the shared attribute is.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;03&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Cluster filtering tames the hairball&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;One of the biggest pain points in graph visualization is the “hairball effect” — too many nodes create an unreadable mess. So the app never renders anything until the user has chosen what to look at.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Connected components are computed once, then listed in the sidebar as named, countable clusters:&lt;BR /&gt;&lt;BR /&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="KeerthiItigimat_2-1789796376244.png" style="width: 609px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31310iF32292DDBE5D18A5/image-dimensions/609x57?v=v2" width="609" height="57" role="button" title="KeerthiItigimat_2-1789796376244.png" alt="KeerthiItigimat_2-1789796376244.png" /&gt;&lt;/span&gt;&lt;BR /&gt;&lt;BR /&gt;&lt;SPAN&gt;Users toggle clusters on or off, and only the visible nodes are ever handed to PyVis. If a specific email address is stitching together two families that shouldn’t be linked, the steward can see exactly which cluster it sits in and reach for the&lt;/SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;Unlink / Exclude&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;feature to remove it — without touching anyone else’s view.&lt;/SPAN&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;/DIV&gt;&lt;DIV class=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="KeerthiItigimat_3-1789796441733.png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31311iB192582D7C1DBD60/image-size/large?v=v2&amp;amp;px=999" role="button" title="KeerthiItigimat_3-1789796441733.png" alt="KeerthiItigimat_3-1789796441733.png" /&gt;&lt;/span&gt;&lt;/DIV&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#808080"&gt;&lt;STRONG&gt;Figure 3.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;Excluding one bad phone number splits a falsely merged household into two correct clusters — the exact “What-If” check a data steward runs before trusting a linkage.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;04&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Engineering for performance&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Thousands of identity links can turn naive Python loops into the bottleneck. Three specific optimizations keep the UI snappy.&lt;/FONT&gt;&lt;/P&gt;&lt;H3&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;itertuples() instead of iterrows()&lt;/FONT&gt;&lt;/H3&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;iterrows()&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;is notoriously slow because Pandas builds a fresh&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;Series&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;for every row. Switching to&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;itertuples()&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;improved graph-building speed by nearly&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;50x&lt;/STRONG&gt;, processing thousands of links in milliseconds.&lt;/FONT&gt;&lt;/P&gt;&lt;H3&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Two-layer caching&lt;/FONT&gt;&lt;/H3&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Streamlit’s&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;a href="https://community.databricks.com/t5/user/viewprofilepage/user-id/12028"&gt;@ST&lt;/a&gt;.cache_data&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;is split into two layers: a&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;data cache&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;for the raw SQL results, and a&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;graph cache&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;for the NetworkX object itself. Changing a purely visual setting — background color, node size — redraws the existing graph instead of re-querying Databricks or reprocessing the DataFrame.&lt;/FONT&gt;&lt;/P&gt;&lt;H3&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Physics stabilization&lt;/FONT&gt;&lt;/H3&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Graph simulations are CPU-intensive. Configuring PyVis’s&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;forceAtlas2Based&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;solver with a high stabilization threshold lets the layout settle&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;before&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;render, instead of animating a bouncing simulation that freezes the browser on large loads.&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;DIV class=""&gt;&lt;STRONG&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;pyvis network options&lt;/FONT&gt;&lt;/STRONG&gt;&lt;/DIV&gt;&lt;PRE&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;net.set_options(&lt;SPAN class=""&gt;"""&lt;/SPAN&gt;
&lt;SPAN class=""&gt;{&lt;/SPAN&gt;
&lt;SPAN class=""&gt;  "physics": {&lt;/SPAN&gt;
&lt;SPAN class=""&gt;    "solver": "forceAtlas2Based",&lt;/SPAN&gt;
&lt;SPAN class=""&gt;    "stabilization": {"iterations": 150}&lt;/SPAN&gt;
&lt;SPAN class=""&gt;  }&lt;/SPAN&gt;
&lt;SPAN class=""&gt;}&lt;/SPAN&gt;
&lt;SPAN class=""&gt;"""&lt;/SPAN&gt;)&lt;/FONT&gt;&lt;/PRE&gt;&lt;/DIV&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;05&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Handling data quality on the fly&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Data is rarely perfect. A generic placeholder like&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;NA&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;or a shared “house” phone number will link thousands of unrelated people together if you let it. The Explorer includes a&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;Dynamic Exclusion&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;engine: type any value into a “Values to Remove” box, and the app instantly:&lt;/FONT&gt;&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Drops those nodes from the NetworkX graph.&lt;/FONT&gt;&lt;/LI&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Recalculates the clusters.&lt;/FONT&gt;&lt;/LI&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Redraws the now-cleaned visualization.&lt;/FONT&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;DIV class=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="KeerthiItigimat_5-1789796578733.png" style="width: 777px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31313iAB0028D12813764D/image-dimensions/777x169?v=v2" width="777" height="169" role="button" title="KeerthiItigimat_5-1789796578733.png" alt="KeerthiItigimat_5-1789796578733.png" /&gt;&lt;/span&gt;&lt;/DIV&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;This is the point of the whole tool: entity linkage isn’t just an algorithm, it’s human-in-the-loop verification. NetworkX supplies the analytical power; Streamlit supplies the rapid UI. Together they turn a static lineage report into something relationships can be explored, cleaned, and understood at a glance.&lt;/FONT&gt;&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;&lt;FONT face="georgia,palatino" size="4" color="#008000"&gt;Better data quality. Fewer duplicate customers. A much clearer picture of who&lt;BR /&gt;your consumers actually are.&lt;/FONT&gt;&lt;/STRONG&gt;&lt;/P&gt;</description>
      <pubDate>Sat, 19 Sep 2026 05:47:08 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/unmasking-the-connected-consumer/m-p/169161#M1579</guid>
      <dc:creator>KeerthiItigimat</dc:creator>
      <dc:date>2026-09-19T05:47:08Z</dc:date>
    </item>
  </channel>
</rss>

