<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Max Mealing</title>
    <description>The latest articles on DEV Community by Max Mealing (@maxbonnard).</description>
    <link>https://dev.to/maxbonnard</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4032337%2F41320bce-53bd-4722-9997-b15a2d6125a0.png</url>
      <title>DEV Community: Max Mealing</title>
      <link>https://dev.to/maxbonnard</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maxbonnard"/>
    <language>en</language>
    <item>
      <title>The Semantic Layer You Already Have</title>
      <dc:creator>Max Mealing</dc:creator>
      <pubDate>Thu, 16 Jul 2026 14:26:22 +0000</pubDate>
      <link>https://dev.to/maxbonnard/the-semantic-layer-you-already-have-3632</link>
      <guid>https://dev.to/maxbonnard/the-semantic-layer-you-already-have-3632</guid>
      <description>&lt;p&gt;Semantic layers are having a revival. With agents now querying data across businesses, the thinking goes that they need a specific "language" to query it reliably.&lt;/p&gt;

&lt;p&gt;Your company is openly discussing whether adopting one makes sense. Management are still bruised from the last failed warehouse-querying agent implementation and are looking for a better solution. Someone mentions Cube.dev, another suggests building a custom semantic layer.&lt;/p&gt;

&lt;p&gt;As the lead data engineer in your company, you're thinking practically about the challenges of implementing a semantic layer: another abstraction to be maintained; a second copy of business logic that drifts as the data models and metrics evolve; another endpoint that needs to be hosted and monitored; a new framework to learn.&lt;/p&gt;

&lt;p&gt;Before committing to anything, there's another option you should consider: &lt;strong&gt;leveraging your database's native metadata and functions as a lightweight semantic layer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's a surprisingly large lever. In our own agent testing, adding authored descriptions to an opaque schema took an agent's first-try table-finding from roughly one in four to near-perfect.&lt;/p&gt;

&lt;p&gt;If you're already using dbt, you're likely most of the way there already, and maintaining a single source of truth is trivial.&lt;/p&gt;

&lt;p&gt;Here's how you can achieve this in a few days' work.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Create your agent gold layer
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Instead of revealing all your marts to agents, curate a helpful set of views (or tables) and columns that meet the specific contexts and use cases you want to provide.&lt;/strong&gt; This is arguably where the most work will go. Aim for 20-30 views to begin with, then later you can scale up to 50-100 (which agents handle well with a good set of tools). Aim to group each view by domain and hide unnecessary internal views.&lt;/p&gt;

&lt;p&gt;Since you're building views, you can also bake your metric definitions into them, for example a &lt;code&gt;net_revenue&lt;/code&gt; column that already excludes refunds, so the agent uses the correct calculation and inherits your definition instead of re-deriving it.&lt;/p&gt;

&lt;p&gt;When using BigQuery/Redshift/Snowflake, declaring primary, foreign and unique keys (even if unenforced) goes a long way later on to help agents build out complex multi-join queries, and spend less time on discovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Comments on views
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use view comments to help agents discover the right table for their task.&lt;/strong&gt; A good view comment should help the agent answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When should I use this table&lt;/li&gt;
&lt;li&gt;What does the table contain&lt;/li&gt;
&lt;li&gt;What is the granularity of the data&lt;/li&gt;
&lt;li&gt;Business synonyms that real users may actually use&lt;/li&gt;
&lt;li&gt;Relationships to other tables in the schema&lt;/li&gt;
&lt;li&gt;Any caveats around the scope/lifecycle e.g. "current vs historical, excludes cancelled, soft-deletes and deprecated"
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;finance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt;
  &lt;span class="s1"&gt;'One row per customer order (aka sales, purchases). Current orders only,
   excludes cancelled (see finance.orders_all). Joins to finance.customers
   on customer_id. Filter on status; time grain is created_at.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Comment the columns that matter, not every column. A blanket, low-value description on all 60 columns adds noise the agent has to wade through. Depth on the filter, join and date columns beats coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Column names
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Keep these legible, as if one of your business users were using them directly.&lt;/strong&gt; They should implicitly carry a lot of meaning already:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full words over abbreviations e.g. &lt;code&gt;shipping_address&lt;/code&gt; over &lt;code&gt;shp_adr&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Consistent naming conventions: use &lt;code&gt;snake_case&lt;/code&gt;, as mixed-case identifiers force quoting and will case-fold when unquoted, which leads to a lot of agent errors down the line.&lt;/li&gt;
&lt;li&gt;Suffixes give context e.g. &lt;code&gt;_id&lt;/code&gt;, &lt;code&gt;_at&lt;/code&gt;, &lt;code&gt;_date&lt;/code&gt;, &lt;code&gt;is_*&lt;/code&gt; (for bools), &lt;code&gt;_pct&lt;/code&gt;, &lt;code&gt;_ratio&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add units and scale in the name when ambiguous e.g. &lt;code&gt;amount_cents&lt;/code&gt;, &lt;code&gt;weight_kg&lt;/code&gt;, &lt;code&gt;duration_seconds&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Consistent key naming for table joins e.g. &lt;code&gt;customer_id&lt;/code&gt;, &lt;code&gt;account_id&lt;/code&gt; etc.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Hard for an agent to read&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;ord&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;oid&lt;/span&gt;   &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;cust&lt;/span&gt;  &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;amt&lt;/span&gt;   &lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;st&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;dt&lt;/span&gt;    &lt;span class="nb"&gt;timestamp&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Self-describing: the name carries the meaning&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;            &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt;   &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;-- FK -&amp;gt; customers.id&lt;/span&gt;
  &lt;span class="n"&gt;total_cents&lt;/span&gt;   &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;-- minor units&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;    &lt;span class="n"&gt;timestamptz&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Comments on columns
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Column comments help agents use the various columns in the table correctly.&lt;/strong&gt; Depending on the column type, these should typically provide the agent with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Column meanings&lt;/li&gt;
&lt;li&gt;Metric definitions&lt;/li&gt;
&lt;li&gt;Additive properties and aggregation caveats for numeric types&lt;/li&gt;
&lt;li&gt;Units and formatting&lt;/li&gt;
&lt;li&gt;Code → label mapping hints&lt;/li&gt;
&lt;li&gt;Join key → targets on other tables&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Native enums on categorical columns
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For categorical columns with a predictable set of values, expose that set so the agent writes the literal verbatim with the right casing.&lt;/strong&gt; Agents will often guess "Cancelled" vs "cancelled" and return a wrong answer to the user when they get the value wrong. Upfront information about category values also means the agent avoids extra queries to discover the possible values each time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Native enum: the allowed values become metadata the agent can read&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;order_status&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;ENUM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'shipped'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'cancelled'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;      &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;  &lt;span class="n"&gt;order_status&lt;/span&gt;   &lt;span class="c1"&gt;-- not `text`&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Native &lt;code&gt;ENUM&lt;/code&gt; types exist on Postgres, ClickHouse, DuckDB and MySQL. BigQuery, Redshift and Snowflake have no enum type, so use the fallback ladder there: a &lt;code&gt;CHECK&lt;/code&gt; constraint where supported → the allowed values written into the column comment → a lookup/dimension table joined by a foreign key. The lookup table is also the better choice anywhere the value set churns, since a native &lt;code&gt;ENUM&lt;/code&gt; is rigid (adding a value is a DDL change) and a lookup row can carry a label and description per value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;order_statuses&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;code&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- 'pending', 'paid', ...&lt;/span&gt;
  &lt;span class="n"&gt;label&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;description&lt;/span&gt;  &lt;span class="nb"&gt;text&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Use the analytical warehouse's physical keys as semantic hints
&lt;/h2&gt;

&lt;p&gt;Analytical warehouses like Redshift, ClickHouse and BigQuery are columnar engines usually serving denormalized models, built for big scans rather than row-level integrity. ClickHouse in particular has no foreign keys at all, and even on BigQuery and Redshift any keys you declare are unenforced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But these engines come with their own set of physical keys, chosen for performance, that double as the semantic hints a PK/FK would have given you in Postgres:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Distribution key&lt;/strong&gt; (Redshift) → the column data is co-located on, usually the natural join key (only when the table sets &lt;code&gt;DISTSTYLE KEY&lt;/code&gt;; the default &lt;code&gt;AUTO&lt;/code&gt; may pick none)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sort key / ORDER BY / partition / clustering key&lt;/strong&gt; → the columns that filter cheaply, and typically the primary time grain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Primary key&lt;/strong&gt; (ClickHouse) → the grain (it's a sparse-index/sort hint, not a uniqueness constraint)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ClickHouse: the ORDER BY / PARTITION BY are the grain + cheap filters&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;event_date&lt;/span&gt;  &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;     &lt;span class="n"&gt;UInt64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_type&lt;/span&gt;  &lt;span class="n"&gt;Enum8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'view'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'click'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'purchase'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;amount&lt;/span&gt;      &lt;span class="nb"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MergeTree&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;toYYYYMM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;-- time grain + cheapest filter&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;-- grain + the join/filter columns&lt;/span&gt;

&lt;span class="c1"&gt;-- Redshift: distkey = the join column, sortkey = the cheap range filter&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;event_date&lt;/span&gt;  &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;     &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;amount&lt;/span&gt;      &lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;DISTSTYLE&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="n"&gt;DISTKEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;SORTKEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read together, these let an agent infer a table's grain, the cheapest columns to filter on, and, from the distribution key specifically, a likely join column. Note the split: the distribution key is the join-shaped hint, while the sort and partition keys are grain and cheap-filter hints, not join paths.&lt;/p&gt;

&lt;p&gt;Two caveats: they're heuristics, not guarantees (a distribution key is usually the join key, but not always), so where an engine lets you declare an unenforced PK/FK, do that too; and choose these keys deliberately, since they're now doing double duty as both a performance decision and documentation an agent reads for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Let the engine's own statistics describe the data
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Your warehouse already computes a lot about your data. Surface it instead of making the agent go and find out.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At the column level, Postgres' &lt;code&gt;pg_stats&lt;/code&gt; and DuckDB's &lt;code&gt;SUMMARIZE&lt;/code&gt; expose helpful statistics e.g. &lt;code&gt;n_distinct&lt;/code&gt;, &lt;code&gt;most_common_vals&lt;/code&gt;, &lt;code&gt;histogram_bounds&lt;/code&gt;, min/max, null %. These let an agent interpret a column before it queries: the real range of a date column before it writes a &lt;code&gt;BETWEEN&lt;/code&gt;, or whether a &lt;code&gt;status&lt;/code&gt; column is skewed 95% to one value. (Availability varies: Snowflake and BigQuery expose no user-readable column-stats view, so lean on comments and enums there.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;SUMMARIZE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- column_name | min | max | approx_unique | null_percentage | ...  (per column)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the table level, row counts and byte size tell the agent what volume it is working with so it can adjust its SQL accordingly. This also serves as fact-vs-dimension routing, so an agent can tell a 50M-row fact table from a 12-row lookup and pick the right driving table in a join.&lt;/p&gt;

&lt;p&gt;A word of caution in multi-tenant setups: on a table with row-level security, Postgres hides &lt;code&gt;pg_stats&lt;/code&gt; from a non-privileged reader (you get nothing), while the row counts here (&lt;code&gt;reltuples&lt;/code&gt;, &lt;code&gt;n_live_tup&lt;/code&gt;) are RLS-blind and report the whole-table volume regardless of policy. Treat both carefully when tenants share a table.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this won't do
&lt;/h2&gt;

&lt;p&gt;This gets you discovery and legibility, not a full metrics platform. The main thing you give up versus a dedicated tool like Cube is caching and pre-aggregation: an agent that fans out into dozens of follow-up questions hits your warehouse, and your bill, each time. If controlling cost at high query volume is a hard requirement, that's where a semantic-layer product still earns its place.&lt;/p&gt;

&lt;p&gt;One last thing to keep in mind if your agents are external or per-tenant: metadata itself can leak. Table names, comments and RLS-blind row counts are readable through the catalog, so findable is not the same as safe to expose. Scope what each agent's role can read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;By applying some or all of the above tips depending on your current warehouse setup, you can create a remarkably effective foundation for agent queries without adopting a completely new tool or framework.&lt;/p&gt;

&lt;p&gt;However, this isn't all there is to it. Agents will also need the right tools to explore and query your new semantic layer effectively.&lt;/p&gt;

&lt;p&gt;Our next article will cover what a good agent toolset looks like, whether you are handrolling your own agents or creating a data MCP for your users.&lt;/p&gt;

</description>
      <category>semanticlayer</category>
      <category>database</category>
      <category>data</category>
      <category>mcp</category>
    </item>
  </channel>
</rss>
