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

# Joining Tables Overview

> How to join RevenueBase Person, Organization, Insights, and Historical Experience tables using RBID keys — entity relationships and SQL join patterns for B2B data.

RevenueBase data is organized into separate tables that link together through shared keys. This page explains how the tables relate and how to join them correctly.

## Entity relationship

**Key relationships:**

* **Person** joins to **Organization** via `RBID_ORG = RBID` (many-to-one)
* **Insights** joins to **Organization** via `RBID_ORG = RBID` (one-to-one, org-level only)
* **Person** can join to **Insight** via `RBID_ORG = RBID_ORG` (many-to-one, gets org-level insights)
* **Historical Experience** joins to **Person** via `linkedin_url` (one-to-one)

```mermaid theme={null}
erDiagram
    ORGANIZATION ||--o{ PERSON : "RBID = RBID_ORG"
    ORGANIZATION ||--|| INSIGHT : "RBID = RBID_ORG"
    PERSON ||--|| HISTORICAL_EXPERIENCE : "linkedin_url"

    PERSON {
        string RBID_ORG
        string linkedin_url
    }

    ORGANIZATION {
        string RBID
    }

    INSIGHT {
        string RBID_ORG
    }

    HISTORICAL_EXPERIENCE {
        string linkedin_url
    }
```

## Join keys reference

| From table   | To table                       | Join key                             | Join type | Notes                                 |
| ------------ | ------------------------------ | ------------------------------------ | --------- | ------------------------------------- |
| `per_latest` | `org_latest`                   | `per.RBID_ORG = org.RBID`            | LEFT JOIN | Not all contacts have an organization |
| `org_latest` | `insights_latest`              | `org.RBID = insights.RBID_ORG`       | LEFT JOIN | All insights are org-level            |
| `per_latest` | `insights_latest`              | `per.RBID_ORG = insights.RBID_ORG`   | LEFT JOIN | Get org-level insights for contacts   |
| `per_latest` | `historical_experience_latest` | `per.LINKEDIN_URL = he.linkedin_url` | LEFT JOIN | One profile per LinkedIn URL          |

## Common join patterns

### Contacts with organization info

The most common join — enriching contacts with their organization's firmographic data:

```sql theme={null}
SELECT
    c.FIRST_NAME,
    c.LAST_NAME,
    c.EMAIL_ADDRESS,
    c.JOB_TITLE,
    co.COMPANY_NAME,
    co.INDUSTRY_LINKEDIN,
    co.EMPLOYEE_COUNT_MAX,
    co.LOCATION_CITY,
    co.LOCATION_STATE_NAME
FROM RELEASE.PER_LATEST c
LEFT JOIN RELEASE.ORG_LATEST co ON c.RBID_ORG = co.RBID
WHERE c.EMAIL_ADDRESS IS NOT NULL;
```

<Note>
  Use `LEFT JOIN` (not `INNER JOIN`) if you want to include contacts that don't have an organization record. Use `INNER JOIN` if you only want contacts with a known organization.
</Note>

### Contacts with org-level insights

```sql theme={null}
SELECT
    c.FIRST_NAME,
    c.LAST_NAME,
    c.EMAIL_ADDRESS,
    c.JOB_TITLE,
    co.COMPANY_NAME,
    i.CRM_TECH_ORG,
    i.SALES_ROLE_COUNT_ORG
FROM RELEASE.PER_LATEST c
LEFT JOIN RELEASE.ORG_LATEST co ON c.RBID_ORG = co.RBID
LEFT JOIN RELEASE.INSIGHTS_LATEST i ON c.RBID_ORG = i.RBID_ORG
WHERE c.EMAIL_ADDRESS IS NOT NULL;
```

### Full join: contacts + companies + insights

```sql theme={null}
SELECT
    c.FIRST_NAME,
    c.LAST_NAME,
    c.JOB_TITLE,
    co.COMPANY_NAME,
    co.INDUSTRY_LINKEDIN,
    co.EMPLOYEE_COUNT_MAX,
    c.EMAIL_ADDRESS,
    c.EMAIL_LAST_VERIFIED_AT,
    i.CRM_TECH_ORG,
    i.LAST_FUNDING_AMOUNT_ORG
FROM RELEASE.PER_LATEST c
LEFT JOIN RELEASE.ORG_LATEST co ON c.RBID_ORG = co.RBID
LEFT JOIN RELEASE.INSIGHTS_LATEST i ON c.RBID_ORG = i.RBID_ORG
WHERE c.EMAIL_ADDRESS IS NOT NULL
ORDER BY c.EMAIL_LAST_VERIFIED_AT DESC;
```

### Contacts with historical experience

```sql theme={null}
SELECT
    c.FIRST_NAME,
    c.LAST_NAME,
    c.EMAIL_ADDRESS,
    he.job_count,
    he.jobs
FROM RELEASE.PER_LATEST c
LEFT JOIN RELEASE.HISTORICAL_EXPERIENCE_LATEST he ON c.LINKEDIN_URL = he.linkedin_url
WHERE c.EMAIL_ADDRESS IS NOT NULL;
```

<Note>
  Historical experience uses an array structure. Use `LATERAL FLATTEN` to work with individual positions. See [Historical Experience](/docs/data-features/data-tables/historical-experience) for details.
</Note>

## Gotchas and tips

<Accordion title="NULL RBID_ORG">
  Not every contact has a linked organization. If you use `INNER JOIN` on `org_latest`, you'll silently drop those contacts. Use `LEFT JOIN` unless you specifically want to exclude unmatched contacts. Check for `RBID_ORG IS NULL` to see how many contacts lack organization associations.
</Accordion>

<Accordion title="All emails are valid">
  All emails in the dataset are verified as valid. You don't need to filter on `EMAIL_STATUS = 'VALID'` — if an email exists in the dataset, it's valid. Use `EMAIL_LAST_VERIFIED_AT` to prioritize recently verified addresses.
</Accordion>

<Accordion title="Insight is org-level only">
  All insights are org-level only. When joining insights to contacts, you get the org-level insights associated with the contact's organization (`RBID_ORG`). There are no contact-level insights.
</Accordion>

<Accordion title="Historical experience array structure">
  The historical experience table stores one row per LinkedIn profile with a `jobs` array. Use `LATERAL FLATTEN` to work with individual positions. See [Historical Experience](/docs/data-features/data-tables/historical-experience) for examples.
</Accordion>

<Accordion title="Performance on large joins">
  When joining across large tables, always include filter conditions (`WHERE` clauses) to reduce the working set before the join. Filtering on `EMAIL_LAST_VERIFIED_AT`, `INDUSTRY_LINKEDIN`, or `EMPLOYEE_COUNT_MAX` early will significantly speed up queries.
</Accordion>

<Accordion title="Pre-joined tables">
  For common combinations (people + companies, or people + companies + insights), consider using pre-joined tables like `velocity_base_unlimited_latest` or `velocity_enhanced_unlimited_latest` instead of writing joins yourself. See [Pre-Joined Tables](/docs/data-features/pre-joined-tables/pre-joined-overview) for details.
</Accordion>

## Joining with your own data

You can also join RevenueBase tables with your own internal data in Snowflake. The most common approach is matching on `domain`:

```sql theme={null}
-- Match your account list against RevenueBase companies by domain
SELECT
    your_accounts.account_name,
    co.COMPANY_NAME,
    co.EMPLOYEE_COUNT_MAX,
    co.INDUSTRY_LINKEDIN
FROM your_database.your_schema.accounts your_accounts
JOIN RELEASE.ORG_LATEST co
  ON LOWER(your_accounts.website_domain) = LOWER(co.DOMAIN);
```

<Tip>
  Always normalize domains when joining — use `LOWER()` and strip `www.` prefixes to avoid mismatches. Consider using `TRIM()` to remove whitespace as well.
</Tip>
