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

<AgentInstructions>

## Submitting Feedback

If you encounter incorrect, outdated, or confusing documentation on this page, submit feedback:

POST https://docs.omni.co/feedback

```json
{
  "path": "/guides/errors/fan-out-without-pk",
  "feedback": "Description of the issue"
}
```

Only submit feedback when you have something specific and actionable to report.

</AgentInstructions>

# Resolve 'Join fans out data without primary key' errors

> Learn to account for fan out in table joins.

export const categoryIcons = {
  'administration': 'lock',
  'api': 'terminal',
  'connections': 'database',
  'dashboards': 'table-columns',
  'embed': 'code',
  'errors': 'exclamation',
  'modeling': 'wrench',
  'patterns': 'plus',
  'schedules & alerts': 'envelope',
  'visualizations': 'chart-column',
  'workbooks': 'book'
};

export const GuideSidebar = ({category, relatedLinks, updatedDate}) => {
  const [progress, setProgress] = React.useState(0);
  React.useEffect(() => {
    const sidebar = document.querySelector('.guide-sidebar');
    if (!sidebar) return;
    let container = sidebar.parentElement;
    while (container && !container.querySelector('.guide-header')) {
      container = container.parentElement;
    }
    if (container && !container.classList.contains('guide-page-layout')) {
      container.classList.add('guide-page-layout');
    }
  }, []);
  React.useEffect(() => {
    const handleScroll = () => {
      const scrollTop = window.scrollY;
      const docHeight = document.documentElement.scrollHeight - window.innerHeight;
      const scrollPercent = docHeight > 0 ? scrollTop / docHeight * 100 : 0;
      setProgress(Math.min(100, Math.max(0, scrollPercent)));
    };
    window.addEventListener('scroll', handleScroll, {
      passive: true
    });
    handleScroll();
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);
  const icon = category ? categoryIcons[category.toLowerCase()] || 'book' : 'book';
  return <aside className="guide-sidebar">
      <div className="guide-sidebar-content">
        <a href="/guides" className="guide-sidebar-back">
          <Icon icon="arrow-left" iconType="solid" size={14} />
          <span>All guides</span>
        </a>

        <div className="guide-sidebar-section">
          <div className="guide-sidebar-label">Progress</div>
          <div className="guide-sidebar-progress">
            <div className="guide-mascot">
              <svg viewBox="0 0 450 450" width="48" height="48">
                <defs>
                  <clipPath id="progressClip">
                    <rect x="0" y={450 - progress * 4.5} width="450" height={progress * 4.5} />
                  </clipPath>
                  <linearGradient id="blobbyGradient" x1="55.9753" y1="0" x2="492.197" y2="169.724" gradientUnits="userSpaceOnUse">
                    <stop stopColor="#BCA2F3" />
                    <stop offset="0.572917" stopColor="#FF7AA2" />
                    <stop offset="1" stopColor="#F3D4A2" />
                  </linearGradient>
                </defs>

                {}
                <circle cx="223.901" cy="223.901" r="213.901" transform="matrix(-0.999988 -0.0049013 0.00491945 -0.999988 447.797 449.992)" fill="#FAFAFA" stroke="#480B38" strokeWidth="20" />

                {}
                <circle cx="223.901" cy="223.901" r="213.901" transform="matrix(-0.999988 -0.0049013 0.00491945 -0.999988 447.797 449.992)" fill="url(#blobbyGradient)" stroke="#480B38" strokeWidth="20" clipPath="url(#progressClip)" />

                {}
                <path d="M310.41 195.084C310.41 200.052 301.362 212.472 284.328 212.472C266.585 212.472 258.246 201.294 258.246 195.912" stroke="#480B38" strokeWidth="17.3883" strokeMiterlimit="1.33344" strokeLinecap="round" />
                <circle cx="21.168" cy="21.168" r="21.168" transform="matrix(-1 0 0 1 388.658 169.001)" fill="#480B38" />
                <circle cx="21.168" cy="21.168" r="21.168" transform="matrix(-1 0 0 1 223.467 169.001)" fill="#480B38" />
              </svg>
            </div>
            <span className="guide-sidebar-progress-text">{Math.round(progress)}%</span>
          </div>
        </div>

        {category && <div className="guide-sidebar-section">
            <div className="guide-sidebar-label">Category</div>
            <div className="guide-sidebar-category">
              <Icon icon={icon} iconType="solid" size={14} />
              <span>{category}</span>
            </div>
          </div>}

        {updatedDate && <div className="guide-sidebar-section">
            <div className="guide-sidebar-label">Last updated</div>
            <div className="guide-sidebar-date">{updatedDate}</div>
          </div>}

        {relatedLinks && relatedLinks.length > 0 && <div className="guide-sidebar-section">
            <div className="guide-sidebar-label">Related</div>
            <ul className="guide-sidebar-links">
              {relatedLinks.map((link, index) => <li key={index}>
                  <a href={link.href}>{link.title}</a>
                </li>)}
            </ul>
          </div>}
      </div>
    </aside>;
};

export const GuideTitle = ({title}) => {
  return <div className="guide-header">
      <h1 className="guide-title">{title}</h1>
    </div>;
};

<GuideSidebar
  categoryIcons={categoryIcons}
  category="errors"
  updatedDate="December 2025"
  relatedLinks={[
{ title: "Primary keys", href: "/modeling/dimensions/parameters/primary-key" },
{ title: "Compound primary keys", href: "/modeling/views/parameters/custom-compound-primary-key-sql" }
]}
/>

<GuideTitle title="Resolve Join fans out data without primary key errors" />

If you receive the `Join fans out data without primary key` error while you're building a workbook query, you may be trying to calculate an aggregation (sum, count, average, etc.) on a table that's being "fanned out" by a join.

## Why this happens

This happens when you're joining two tables and one row in the first table is joined onto multiple rows in the other table. For example, you have an `orders` table and an `order_items` table. Joining these tables on `order_id` will fan out the `orders` table since multiple items in `order_items` will match the same order in `orders`:

```mermaid theme={null}
flowchart LR
    subgraph orders["orders"]
        o1["order_id: 101<br/>total: $50"]
    end

    subgraph order_items["order_items"]
        i1["order_id: 101<br/>item: Shirt"]
        i2["order_id: 101<br/>item: Pants"]
        i3["order_id: 101<br/>item: Socks"]
    end

    subgraph joined["Joined result (fan out)"]
        j1["order_id: 101, total: $50, item: Shirt"]
        j2["order_id: 101, total: $50, item: Pants"]
        j3["order_id: 101, total: $50, item: Socks"]
    end

    o1 --> i1
    o1 --> i2
    o1 --> i3

    i1 --> j1
    i2 --> j2
    i3 --> j3
```

In this example, a single row in `orders` with `total: $50` is duplicated three times in the joined result. If you summed `total` without accounting for the fan out, you'd incorrectly get `$150` instead of `$50`.

Omni uses a method called [*symmetric aggregates*](/analyze-explore/sql/symmetric-aggregates) to prevent inadvertently miscalculating aggregations - such as sums, counts, and averages - when a table is fanned out. Before Omni calculates an aggregation on a fanned out table, it'll look at that table's primary key — its unique ID per row — to ensure values aren't double-counted in the aggregation.

Omni surfaces this error when a fanned out table doesn't have a defined primary key. Without a primary key, Omni won't know how to correctly calculate the aggregation.

## How to fix it

Defining a primary key for a view will resolve this error. A primary key is a column that contains a unique identifier per row. This is usually an ID column.

Primary keys can be defined in a workbook or the model IDE.

<Note>
  Remember to promote your changes to the shared model if you want to make them generally available.
</Note>

### Workbook

1. In the workbook field browser, right click on the field you want to define as the primary key.
2. Click **Modeling > Primary Key**.

### Model IDE

1. Open the model IDE.
2. Locate the view and click to open its YAML definition.
3. Add the [`primary_key`](/modeling/dimensions/parameters/primary-key) parameter to the appropriate dimension:
   ```yaml theme={null}
   id:
     primary_key: true
   ```

**If your table has a compound (composite) primary key**, add the [`custom_compound_primary_key_sql`](/modeling/views/parameters/custom-compound-primary-key-sql) parameter to the view file:

```yaml theme={null}
custom_compound_primary_key_sql: [ id, date ]
```
