> ## Documentation Index
> Fetch the complete documentation index at: https://honeydew.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# From Tableau to Honeydew

<Tip>
  This guide helps Tableau users understand how to model their analytics in Honeydew.

  If you are not familiar with Tableau, head over to the [introduction](/introduction) instead.
</Tip>

## Introduction

Honeydew is a standalone semantic layer. It sits between your data warehouse
and the tools that consume it: Tableau, Power BI, Excel, SQL clients, AI
agents, notebooks. Tableau is one of those tools. The job of the semantic
layer is to define the building blocks once and use them everywhere.

Semantics include the following concepts:

1. How business entities (such as "customer") are mapped to data sources
   (which tables to read from)
2. How data tables are related (which joins to use)
3. How metrics are calculated (which aggregations to perform)
4. Metadata (such as descriptions, display names, formatting) associated
   with presenting those objects to business users.

If you build dashboards in Tableau, you already work with all four: you
map fields to tables in a data source, define relationships, write
calculated fields and LODs, and add display formatting. The Honeydew
equivalents share the same intent, just centralized so multiple workbooks,
dashboards, and tools can reuse them.

When you connect Tableau to Honeydew, Tableau queries a virtual table
instead of querying raw warehouse tables directly. The logic that today
lives in your Tableau workbooks (calculated fields, LOD expressions, data
blending) moves into Honeydew where it can be reused across dashboards
and tools.

<Tip>
  The goal is not to replicate Tableau logic 1:1 in Honeydew. Instead, you
  define the building blocks (entities, relationships, metrics) once, and
  Tableau consumes them through a [live connection](/integration/bi-tools/tableau).
</Tip>

### Visual to declarative

In Tableau you think visually: drag a dimension to Rows, drop a measure on
Columns, add a filter to the shelf, write an LOD expression to change the grain.

In Honeydew you think declaratively: define entities, connect them with
relationships, write metrics that describe *what* to calculate. The *how* -
which joins, which `GROUP BY`, which filters - is resolved automatically at
query time.

| Tableau concept              | Honeydew equivalent                                                                                                              |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Data Source / Data Model     | [Workspace](/architecture) + [Entities](/modeling/entities)                                                                      |
| Relationship / Join          | [Relationship](/modeling/relations)                                                                                              |
| Dimension (green pill)       | [Attribute](/calculations/attributes)                                                                                            |
| Measure (blue pill)          | [Metric](/calculations/metrics)                                                                                                  |
| Calculated Field (dimension) | [Calculated Attribute](/calculations/attributes)                                                                                 |
| Calculated Field (measure)   | [Metric](/calculations/metrics)                                                                                                  |
| LOD Expression               | [Calculated attribute](/calculations/attributes) or metric with [GROUP BY qualifier](/calculations/metrics#controlling-grouping) |
| Table Calculation            | Derived metric or calculated attribute                                                                                           |
| Set / Group                  | Calculated attribute                                                                                                             |
| Parameter                    | [BI Parameter](/recipes/dynamic-bi-parameters) or [conditional filtering](/advanced-modeling/conditional-filtering)              |
| Context Filter               | [Semantic filter](/domains#semantic-filters)                                                                                     |
| Data Source Filter           | [Source filter](/domains#source-filters)                                                                                         |
| Published Data Source        | [Domain](/domains)                                                                                                               |

## Data Sources and Entities

A Tableau data source connects to one or more warehouse tables, defines joins
between them, and exposes dimensions and measures for worksheets.

In Honeydew, each table becomes an [Entity](/modeling/entities). Joins between
tables become [Relationships](/modeling/relations) defined once and reused
across all queries.

<Tip>
  A Tableau Published Data Source maps to a Honeydew
  [Domain](/domains) - a curated subset of the model tailored to a specific
  business area.
</Tip>

### Custom SQL in data sources

Tableau allows a data source to use Custom SQL. An entity in Honeydew can
also be backed by a custom SQL query instead of a single table - see
[source data](/modeling/source-data).

### Data blending

In Tableau, data blending links fields across separate physical data
sources using a common dimension. Honeydew operates on a single physical
source (a cloud data warehouse), so the equivalent depends on what was
being blended.

When the blended data lives in the same warehouse and just needs to be
linked through a common dimension, use a
[relationship](/modeling/relations), often backed by a
[shared dimension](/advanced-modeling/shared-dimensions).

When the data lives in a different warehouse, database, or system, there
is no Honeydew equivalent. The cross-source join has to happen either in
the warehouse itself (through federation or external tables) or in the
dashboard layer.

## Relationships and Joins

Tableau's data model supports relationships (logical layer) and joins
(physical layer).

Honeydew [relationships](/modeling/relations) combine both concepts:

* **Join fields**: Honeydew supports composite keys (multiple fields) and
  calculated attributes as join keys - no need to create concatenated key fields.

* **Join type**: `LEFT`, `INNER`, `RIGHT`, or `FULL OUTER`.

* **Cardinality**: one-to-many and many-to-one work directly. Many-to-many
  requires a [connecting table](/modeling/relations#modeling-many-many-relations).

* **Cross-filtering**: controls whether filters on one entity affect another.
  The default is bidirectional, matching how Tableau's logical layer behaves.

<Note>
  In Tableau's logical layer, tables are related but not joined until needed.
  Honeydew works similarly - joins are defined as relationships and only executed
  when a query requires fields from both entities.
</Note>

## Calculated Fields → Attributes

A Tableau calculated field that produces a dimension (string, date, boolean,
etc.) maps to a [calculated attribute](/calculations/attributes) in Honeydew.

### Basic calculated fields

Tableau:

```
IF [Region] = "West" THEN "Pacific" ELSE "Other" END
```

Honeydew calculated attribute in the `customer` entity:

```sql theme={null}
customer.region_group =
  CASE WHEN customer.region = 'West'
    THEN 'Pacific' ELSE 'Other' END
```

### Fields from related tables

In Tableau, a calculated field can reference any field in the data source
after a join.

In Honeydew, relationships are resolved automatically. Reference any related
entity directly:

```sql theme={null}
sales.discount = product.unit_price - sales.net_price
```

<Tip>
  No need to specify how to join - Honeydew resolves the path from `sales` to
  `product` through defined relationships.
</Tip>

### Groups and Sets

Tableau Groups let you manually bucket dimension values. Tableau Sets create
binary in/out classifications based on conditions.

Both map to calculated attributes in Honeydew:

Tableau Group:

```sql theme={null}
product.size_bucket =
  CASE
    WHEN product.weight < 1 THEN 'Small'
    WHEN product.weight < 10 THEN 'Medium'
    ELSE 'Large'
  END
```

Tableau Set (high-value customers above a threshold):

```sql theme={null}
customer.is_high_value = sales.total_sales > 10000
```

<Tip>
  The `is_high_value` attribute uses a metric (`sales.total_sales`) in its
  definition. Calculated attributes that reference metrics are evaluated at the
  entity's grain - here, per customer.
</Tip>

## Calculated Fields → Metrics

A Tableau calculated field that produces a measure (an aggregation) maps to a
[metric](/calculations/metrics) in Honeydew.

### Basic aggregations

Tableau:

```
SUM([Sales Amount])
```

Honeydew:

```sql theme={null}
sales.total_sales = SUM(sales.sales_amount)
```

Tableau:

```
COUNTD([Customer ID])
```

Honeydew:

```sql theme={null}
sales.unique_customers = customer.count
```

<Tip>
  `customer.count` is the entity count metric of the `customer` entity. Honeydew
  resolves the relationship between `sales` and `customer` automatically, giving
  a distinct count of customers per the query's grouping.

  `COUNT(DISTINCT sales.customer_id)` would also work, but referencing the entity
  count is the recommended approach.
</Tip>

### Common aggregation mapping

<Note>
  The SQL syntax depends on the connected data warehouse (Snowflake, Databricks,
  etc.). The examples below use Snowflake syntax.
</Note>

| Tableau                 | SQL                                                     |
| ----------------------- | ------------------------------------------------------- |
| `SUM(expr)`             | `SUM(expr)`                                             |
| `AVG(expr)`             | `AVG(expr)`                                             |
| `COUNT(expr)`           | `COUNT(expr)`                                           |
| `COUNTD(expr)`          | `COUNT(DISTINCT expr)`                                  |
| `MIN(expr)`             | `MIN(expr)`                                             |
| `MAX(expr)`             | `MAX(expr)`                                             |
| `MEDIAN(expr)`          | `MEDIAN(expr)`                                          |
| `PERCENTILE(expr, n)`   | `PERCENTILE_CONT(n) WITHIN GROUP (ORDER BY expr)`       |
| `STDEV(expr)`           | `STDDEV_SAMP(expr)`                                     |
| `VAR(expr)`             | `VAR_SAMP(expr)`                                        |
| `ZN(expr)`              | `COALESCE(expr, 0)`                                     |
| `IIF(test, then, else)` | `CASE WHEN test THEN then_val ELSE else_val END`        |
| `IF THEN ELSEIF END`    | `CASE WHEN ... THEN ... WHEN ... THEN ... ELSE ... END` |

### Derived metrics

Metrics in Honeydew can reference other metrics:

```sql theme={null}
sales.total_sales = SUM(sales.sales_amount)
```

```sql theme={null}
sales.total_cost = SUM(sales.quantity * product.unit_price)
```

```sql theme={null}
sales.profit = sales.total_sales - sales.total_cost
```

<CodeGroup>
  ```sql Snowflake theme={null}
  sales.margin = DIV0(sales.profit, sales.total_sales)
  ```

  ```sql Databricks theme={null}
  sales.margin = sales.profit / NULLIF(sales.total_sales, 0)
  ```
</CodeGroup>

<Tip>
  Define each building block once and reuse it. The profit and margin metrics do
  not repeat the aggregation logic - they compose existing metrics.
</Tip>

## Filters

Tableau has multiple layers of filters that execute in a specific order: extract
filters, data source filters, context filters, dimension filters, measure filters.

Honeydew domains have two types of filters:

* **[Source filters](/domains#source-filters)** apply directly on source tables
  before any calculation or join. Used for performance optimization (e.g.,
  partition pruning) and data deduplication.
* **[Semantic filters](/domains#semantic-filters)** apply to every query in a
  domain, after joins are resolved. Used for governance and access control (e.g.,
  tenant filtering). A semantic filter may introduce additional joins to enforce
  its condition.

In addition, user query filters are applied through SQL:

* `WHERE` filters on attributes (dimensions)
* `HAVING` filters on metrics (measures)

See [order of filtering](/advanced-modeling/order-of-computation) for the full
computation order.

| Tableau filter     | Honeydew equivalent                          | When it applies                        |
| ------------------ | -------------------------------------------- | -------------------------------------- |
| Extract filter     | [Source filter](/domains#source-filters)     | Before joins and calculations          |
| Data Source filter | [Source filter](/domains#source-filters)     | Before joins, on source tables         |
| Context filter     | [Semantic filter](/domains#semantic-filters) | After joins, before user query filters |
| Dimension filter   | `WHERE` in user query                        | After semantic filters, before metrics |
| Measure filter     | `HAVING` in user query                       | After metric aggregation               |

<Note>
  Semantic filters always apply to every query in a domain - they cannot be toggled
  by the user. For filters that should apply by default but can be overridden by
  user selections, see
  [conditional filtering](/advanced-modeling/conditional-filtering).
</Note>

### Adding filters to a metric

Tableau does not have a first-class "filtered measure" construct. To make a
measure that *always* filters to a subset of rows - regardless of what the
user selects in the view - you fold the filter into the aggregate using an
IF expression. Non-matching rows return NULL and SUM ignores them.

Tableau:

```
SUM(IF [Color] = "Red" THEN [Sales Amount] END)
```

Honeydew has [filtered metrics](/calculations/metrics#filtered-metrics) as a
first-class concept: a `FILTER (WHERE ...)` qualifier attached to a metric
expression. The filter lives next to the metric, not inside it.

```sql theme={null}
sales.red_sales = SUM(sales.sales_amount)
  FILTER (WHERE product.color = 'Red')
```

The qualifier also works on derived metrics that wrap an existing metric:

```sql theme={null}
sales.red_sales = sales.total_sales
  FILTER (WHERE product.color = 'Red')
```

<Tip>
  The `FILTER (WHERE ...)` qualifier adds a filter to the metric without
  affecting the rest of the query. All user filters still apply on top.
</Tip>

## LOD Expressions

LOD (Level of Detail) expressions are one of the most common Tableau constructs
that need to be rethought in Honeydew.

In Tableau, LODs let you control the granularity of a calculation independently
from the level of detail in the view.

In Honeydew, there are two ways to control granularity, and the choice depends
on how the calculation should interact with user filters:

* **[Calculated attributes](/calculations/attributes)** are computed *before*
  user query filters. They ignore user `WHERE` and `HAVING` filters entirely.
* **[Metrics](/calculations/metrics)** with a
  [GROUP BY qualifier](/calculations/metrics#controlling-grouping) are computed
  *after* user query filters. User filters still affect the result.

This distinction is important when translating LOD expressions.

### FIXED

A `FIXED` LOD computes an aggregation at a specific grain. In Tableau, `FIXED`
ignores dimension filters but respects context filters and data source filters.

<Tip>
  For the full reference of how each Tableau filter type interacts with LOD
  expressions, see Tableau's
  [Filters and Level of Detail Expressions](https://help.tableau.com/current/pro/desktop/en-us/calculations_calculatedfields_lod_filters.htm).
</Tip>

Tableau:

```
{ FIXED [Customer ID] : SUM([Sales Amount]) }
```

Honeydew - as a calculated attribute (ignores user filters):

```sql theme={null}
customer.lifetime_sales = SUM(sales.sales_amount)
```

A calculated attribute that uses an aggregation automatically groups to the
entity's primary key. Since calculated attributes are computed before user
filters, this closely matches the Tableau `FIXED` behavior of ignoring
dimension filters.

Honeydew - as a metric with fixed grouping (respects user filters):

```sql theme={null}
sales.sales_per_customer = SUM(sales.sales_amount)
  GROUP BY (customer.customer_id)
```

<Note>
  The calculated attribute and the metric with `GROUP BY` both fix the grain to
  customer, but they differ in how they handle user filters:

  * `customer.lifetime_sales` (attribute) always returns the same value regardless
    of user filters.
  * `sales.sales_per_customer` (metric) respects user `WHERE` filters - if a user
    filters to 2024 data, the metric returns sales per customer in 2024 only.

  Choose based on whether the calculation should be affected by user filters or not.
</Note>

#### FIXED with multiple dimensions

Tableau:

```
{ FIXED [Region], [Category] : SUM([Sales]) }
```

Honeydew:

```sql theme={null}
sales.regional_category_sales = SUM(sales.sales_amount)
  GROUP BY (customer.region, product.category)
```

### INCLUDE

An `INCLUDE` LOD adds dimensions to the view's current grain.

Tableau:

```
{ INCLUDE [Customer ID] : COUNTD([Order ID]) }
```

Honeydew:

```sql theme={null}
sales.orders_per_customer =
  COUNT(DISTINCT sales.order_id)
  GROUP BY (*, sales.customer_id)
```

`GROUP BY (*, field)` means "the user's current grouping plus this additional
field." This is the Honeydew equivalent of Tableau's `INCLUDE`.

<Tip>
  This is the foundation of multi-level aggregations. Wrap
  `sales.orders_per_customer` in an outer aggregate to get an "average orders
  per customer" metric:

  ```sql theme={null}
  sales.avg_orders_per_customer =
    AVG(sales.orders_per_customer)  -- the metric defined above
  ```

  The inner metric fixes the per-customer grain; the outer `AVG` averages
  across whatever the user breaks the result down by - region, segment,
  month, or all of them at once.
</Tip>

### EXCLUDE

An `EXCLUDE` LOD removes dimensions from the view's current grain.

Tableau:

```
{ EXCLUDE [Month] : SUM([Sales]) }
```

This removes the month dimension from the aggregation, giving a coarser total
while the view still shows monthly detail.

Honeydew:

```sql theme={null}
sales.sales_excl_month = SUM(sales.sales_amount)
  GROUP BY (NOT date.month)
```

<Tip>
  `GROUP BY (NOT field)` means "the user's current grouping minus this field."
  This is the Honeydew equivalent of Tableau's `EXCLUDE`.
</Tip>

### Percent of total

A very common Tableau pattern is percent of total using LODs:

Tableau:

```
SUM([Sales]) / { FIXED : SUM([Sales]) }
```

Or using a table calculation:

```
SUM([Sales]) / TOTAL(SUM([Sales]))
```

Honeydew:

<CodeGroup>
  ```sql Snowflake theme={null}
  sales.pct_of_total = DIV0(
    sales.total_sales,
    sales.total_sales GROUP BY ()
  )
  ```

  ```sql Databricks theme={null}
  sales.pct_of_total =
    sales.total_sales / NULLIF(
      sales.total_sales GROUP BY (), 0
    )
  ```
</CodeGroup>

`GROUP BY ()` removes all grouping, giving the grand total. The numerator uses
the query's grouping.

### Percent of parent (nested LOD pattern)

Tableau:

```
SUM([Sales])
/ { FIXED [Region] : SUM([Sales]) }
```

Honeydew:

<CodeGroup>
  ```sql Snowflake theme={null}
  sales.pct_of_region = DIV0(
    sales.total_sales,
    sales.total_sales GROUP BY (customer.region)
  )
  ```

  ```sql Databricks theme={null}
  sales.pct_of_region =
    sales.total_sales / NULLIF(
      sales.total_sales GROUP BY (customer.region), 0
    )
  ```
</CodeGroup>

### LOD summary

| Tableau LOD                      | Honeydew qualifier | Effect                                  |
| -------------------------------- | ------------------ | --------------------------------------- |
| `{ FIXED [A], [B] : AGG(expr) }` | `GROUP BY (a, b)`  | Sets an exact grain                     |
| `{ FIXED : AGG(expr) }`          | `GROUP BY ()`      | Grand total - no grouping               |
| `{ INCLUDE [A] : AGG(expr) }`    | `GROUP BY (*, a)`  | Adds a dimension to the user grain      |
| `{ EXCLUDE [A] : AGG(expr) }`    | `GROUP BY (NOT a)` | Removes a dimension from the user grain |

## Table Calculations

Tableau table calculations operate on the result set *after* aggregation -
running totals, ranks, moving averages, percent differences. They depend on
the sort order and partitioning of the view.

Honeydew handles these patterns differently depending on the calculation type.

### Running total / cumulative sum

Tableau:

```
RUNNING_SUM(SUM([Sales]))
```

Honeydew - as a calculated attribute with a window function:

```sql theme={null}
sales.cumulative_sales =
  SUM(sales.sales_amount) OVER (
    ORDER BY sales.order_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  )
```

<Note>
  Window functions in calculated attributes operate at the entity row level, not
  at the query result level. For cumulative sums over a query result, use Tableau's
  built-in table calculation on a Honeydew metric.
</Note>

### Rank

Tableau:

```
RANK(SUM([Sales]))
```

Ranking over a query result is best done in Tableau on top of a Honeydew metric.
Honeydew provides the aggregated values; Tableau applies the rank.

For ranking at the entity level (e.g., rank orders by price within a customer),
use a window function in a calculated attribute:

```sql theme={null}
orders.price_rank = RANK() OVER (
  PARTITION BY orders.customer_id ORDER BY orders.total_price DESC
)
```

### Percent difference

Tableau:

```
(ZN(SUM([Sales])) - LOOKUP(ZN(SUM([Sales])), -1))
/ ABS(LOOKUP(ZN(SUM([Sales])), -1))
```

For period-over-period comparisons, Honeydew offers
[time-based metrics](/advanced-modeling/time-metrics) that compute offsets like
previous period values without relying on result-set ordering.

<Tip>
  Table calculations that depend on the visual layout (sort order, partition) are
  best left in Tableau. Honeydew provides the well-defined aggregated metrics;
  Tableau applies layout-dependent calculations on top.
</Tip>

## Parameters

Tableau parameters let users select a value at runtime to control filters,
reference lines, or calculation logic.

Honeydew supports two approaches:

### BI parameters

Honeydew can pass through Tableau parameter values using
[BI parameters](/recipes/dynamic-bi-parameters). This lets you build metrics
whose behavior changes based on user selection.

### Conditional filtering

For parameters that switch between dimensions or control which filter to apply,
[conditional filtering](/advanced-modeling/conditional-filtering) in Honeydew
provides `GET_FIELD_SELECTION()`:

```sql theme={null}
-- Returns the value the user filtered on,
-- or a default if no filter is applied
GET_FIELD_SELECTION(product.category, 'All')
```

This is useful for building dynamic titles, conditional logic, or metrics that
behave differently based on user filter selections.

## Other Patterns

### Threshold filtering

Tableau lets you create filters based on measure values (e.g., only show
products with more than \$1,000 in sales). In Tableau, the threshold is often
controlled by a parameter.

In Honeydew, use a metric in a
[filter](/calculations/metrics#filtered-metrics) with
[conditional filtering](/advanced-modeling/conditional-filtering) to make the
threshold dynamic:

```sql theme={null}
sales.sales_from_active_products =
  SUM(sales.sales_amount)
  FILTER (WHERE
    sales.total_sales
    GROUP BY (product.product_id)
      > GET_FIELD_SELECTION(dim_threshold.min_sales, 1000)
  )
```

The threshold defaults to 1,000 but the user can change it by filtering
`dim_threshold.min_sales` to a different value - similar to how a Tableau
parameter works.

### Top N filtering

Tableau lets you filter a dimension to show only the top N items by a measure
(e.g., top 10 products by sales).

Top N filtering depends on the query's grouping and sort order, so it should be
applied in Tableau using a Tableau parameter and a Top N filter on a Honeydew
metric. Honeydew provides the aggregated metric values; Tableau handles the
ranking and filtering.

### Period-over-period comparison

Tableau:

```
SUM([Sales]) - LOOKUP(SUM([Sales]), -1)
```

Honeydew provides [time-based metrics](/advanced-modeling/time-metrics) for
period offsets. This is the year-over-year comparison:

```sql theme={null}
sales.total_sales_prev_year = TIME_METRIC(
  sales.total_sales,
  date_field => sales.order_date,
  offset => '1 year'
)
```

```sql theme={null}
sales.yoy_growth =
  sales.total_sales - sales.total_sales_prev_year
```

<Tip>
  Use time-based metrics when the comparison must be defined centrally so
  every consumer sees the same year-over-year (or other-period) definition.
  For comparisons that only matter inside one workbook, it is often simpler
  to keep them in Tableau as table calculations or part of the visual
  presentation.
</Tip>

## Next Steps

* [Metrics reference](/calculations/metrics) - full syntax for metrics,
  qualifiers, and grouping
* [Calculated attributes](/calculations/attributes) - building reusable
  dimensions
* [Domains](/domains) - curate subsets of your model for specific Tableau
  workbooks
