> ## 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.

# Choosing between LOD fields, window functions, CTEs, and Excel

> Compare Omni LOD fields, SQL window functions, CTEs, and Excel for multi-level aggregation and cohort analysis.

export const categoryIcons = {
  'administration': 'lock',
  'api': 'terminal',
  'connections': 'database',
  'dashboards': 'table-columns',
  'embed': 'code',
  'errors': 'exclamation',
  'migration': 'angles-right',
  '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 688 690" width="48" height="48">
                <defs>
                  <clipPath id="progressClip">
                    <rect x="0" y={0} width="688" height={progress * 6.9} />
                  </clipPath>
                </defs>

                {}
                <path d="M343.67 1.5C542.684 1.5 685.84 149.351 685.84 344.84C685.84 540.328 542.685 688.18 343.67 688.18C144.655 688.18 1.5 540.318 1.5 344.84C1.50007 149.361 144.655 1.50005 343.67 1.5Z" fill="#FCFCF7" stroke="#FF5FA2" strokeWidth="3" />

                {}
                <path d="M343.67 0C143.81 0 0 148.55 0 344.84C0 541.13 143.81 689.68 343.67 689.68C543.53 689.68 687.34 541.14 687.34 344.84C687.34 148.54 543.53 0 343.67 0Z" fill="#FF5FA2" clipPath="url(#progressClip)" />

                {}
                <path d="M337.89 319.29C337.89 336.75 322.49 350.14 302.81 349.83C286.18 349.57 273.89 337.29 274.37 321.45C274.88 304.82 290.91 290.88 309.98 290.44C325.69 290.09 337.88 302.69 337.88 319.29H337.89Z" fill="#4D122C" />
                <path d="M566.17 319.29C566.17 336.75 550.77 350.14 531.09 349.83C514.46 349.57 502.17 337.29 502.65 321.45C503.16 304.82 519.19 290.88 538.26 290.44C553.97 290.09 566.16 302.69 566.16 319.29H566.17Z" fill="#4D122C" />
                <path d="M367.74 342.07C360.22 346.32 359.4 354.9 370.62 366.4C381.85 377.9 399.76 389.56 420.81 389.18C441.88 389.1 460.67 377.72 472.47 363.53C473.83 361.93 478.84 356.88 478.51 351.07C478.32 348.35 476.17 341.19 467.83 341.38C463.46 341.44 461.21 343.68 456.69 347.36C445.2 356.14 432.7 361.21 420.56 361.27C408.43 361.43 395.68 356.17 385.39 347.22C380.32 342.81 375.25 337.82 367.74 342.07Z" fill="#4D122C" />
              </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="patterns"
  updatedDate="May 2026"
  relatedLinks={[
{ title: "Level of detail fields", href: "/analyze-explore/custom-fields/level-of-detail" },
{ title: "Build cohort analysis using level of detail", href: "/guides/patterns/build-cohort-analysis-using-level-of-detail" },
{ title: "Derived tables and query views", href: "/modeling/query-views/index" },
{ title: "level_of_detail parameter reference", href: "/modeling/dimensions/parameters/level-of-detail" }
]}
/>

<GuideTitle title="Choosing between LOD fields, window functions, CTEs, and Excel" />

Some aggregations don't fit a simple `GROUP BY`. For example, you may want to show each customer's lifetime spend on a row-level breakout by product category, or compute each region's percentage of a grand total. Omni gives you four ways to handle these: an LOD field, a SQL window function, a CTE or derived table, or a spreadsheet.

In this guide, you'll learn about each of these methods, how they compare to each other, and when to choose each one.

## Before you start

If you're new to LOD fields, read [Level of detail fields](/analyze-explore/custom-fields/level-of-detail) first — this guide assumes basic familiarity with the field type.

## Available methods

The right choice usually comes down to whether the calculation needs to be reusable, where you're comfortable writing logic, and how much data is involved. Use the table below as a starting point; the [side-by-side comparison](#comparing-methods) goes deeper on the trade-offs.

| Method               | Use this when...                                                                                                                                                                                                                                    |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Omni LOD field       | The same calculation will appear in multiple workbooks, when it needs to respond to filters dynamically, or when you'd rather not write SQL. Cohorts, lifetime customer metrics, and percent-of-total calculations are good fits for this approach. |
| SQL window function  | The calculation only matters inside one query and you're comfortable in SQL. Use for one-off analyses that don't need to be reusable.                                                                                                               |
| CTE or derived table | You're working with a large dataset and benefit from materializing the intermediate result in your warehouse, or when the calculation involves multi-step transformations that don't fit cleanly into a single query.                               |
| Excel                | Only when the data already lives in a spreadsheet or for a quick ad-hoc check. Excel isn't scalable and the logic isn't reusable outside the file.                                                                                                  |

## Comparing methods

This table compares the four methods across the traits that usually drive the choice. Use it when you want to compare a specific dimension across all four options.

| Trait              | Omni LOD field                                                                       | SQL window function                                                                              | CTE / derived table                                          | Excel                                                         |
| :----------------- | :----------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------- | :----------------------------------------------------------- | :------------------------------------------------------------ |
| **Primary use**    | Aggregating at a specific level without changing query granularity.                  | Aggregating relative to the current row in a single query pass.                                  | Creating temporary tables to join back to main data.         | Aggregating across a range or within a pivot table.           |
| **Where it lives** | UI or semantic layer; reusable across [workbooks](/analyze-explore/workbook-basics). | Inside a single SQL query.                                                                       | In SQL; can be promoted to a [shared model view](/modeling). | A specific spreadsheet file.                                  |
| **Flexibility**    | Dynamic; responds to other filters while maintaining its fixed level.                | Fixed to the query's current result-set granularity.                                             | Static; requires rewriting SQL to change the grouped level.  | Manual; requires updating ranges or refreshing pivot sources. |
| **Ease of use**    | UI-driven; no code required.                                                         | Requires knowledge of window function parameters such as `OVER`, `PARTITION BY`, and `ORDER BY`. | Requires SQL and manual join logic.                          | Familiar for one-off math; hard to scale.                     |
| **Performance**    | Optimized by Omni as part of query generation.                                       | Database-dependent.                                                                              | Best for massive datasets when pre-materialized.             | Limited by local CPU and memory.                              |
| **Reusability**    | Define once, use everywhere.                                                         | Trapped inside a single query.                                                                   | Reusable if built into a shared model view.                  | Trapped inside a specific file.                               |

<Tip>
  For the best performance on massive datasets, consider using a [derived table](/modeling/query-views/index) which can be materialized in your warehouse.
</Tip>

## Common scenarios

The three scenarios below cover the patterns that come up most often: bucketing customers by lifetime spend, building acquisition cohorts, and computing percent of total. Each is solved with all four methods side by side, so you can take a pattern you already know in one tool and find its equivalent in another.

### Customer lifetime spend

In this scenario, you want "total spend per customer" as a dimension you can use to bucket customers (`$0–100`, `$100–500`, and so on), even when viewing orders by product category.

<Tabs>
  <Tab title="Omni LOD" id="lod-lifetime-spend">
    The fields in the workbook's LOD editor would look like the following:

    * **Field to aggregate** - `Sale price`
    * **Aggregation** - `Sum`
    * **Level of Detail Grouping** - `Fixed`
    * **Fixed dimension(s)** - `User ID`
  </Tab>

  <Tab title="SQL window function" id="sql-lifetime-spend">
    ```sql theme={null}
    SUM(sale_price) OVER (PARTITION BY user_id)
    ```
  </Tab>

  <Tab title="CTE" id="cte-lifetime-spend">
    ```sql theme={null}
    WITH user_spend AS (
      SELECT user_id,
             SUM(sale_price) AS total
      FROM orders
      GROUP BY 1
    )
    SELECT o.*, us.total
    FROM orders o
    JOIN user_spend us ON o.user_id = us.user_id
    ```
  </Tab>

  <Tab title="Excel" id="excel-lifetime-spend">
    In Excel, you'd have a formula where `A` is `User ID` and `C` is `Sale Price`:

    ```text theme={null}
    =SUMIFS(C:C, A:A, A2)
    ```
  </Tab>
</Tabs>

### First-touch cohort date

In this scenario, you want each user's first order date so you can analyze performance by acquisition cohort.

<Tabs>
  <Tab title="Omni LOD" id="lod-cohort-date">
    The fields in the workbook's LOD editor would look like the following:

    * **Field to aggregate** - `Order date`
    * **Aggregation** - `Min`
    * **Level of Detail Grouping** - `Fixed`
    * **Fixed dimension(s)** - `User ID`

    For a full walkthrough using this approach, see [Build cohort analysis using level of detail](/guides/patterns/build-cohort-analysis-using-level-of-detail).
  </Tab>

  <Tab title="SQL window function" id="sql-cohort-date">
    ```sql theme={null}
    FIRST_VALUE(order_date) OVER (PARTITION BY user_id ORDER BY order_date ASC)
    ```
  </Tab>

  <Tab title="CTE" id="cte-cohort-date">
    ```sql theme={null}
    WITH first_purchase AS (
      SELECT user_id,
             MIN(order_date) AS first_order_date
      FROM orders
      GROUP BY 1
    )
    SELECT o.*, fp.first_order_date
    FROM orders o
    JOIN first_purchase fp ON o.user_id = fp.user_id
    ```
  </Tab>

  <Tab title="Excel" id="excel-cohort-date">
    In Excel, you'd have an array formula finding the minimum date (`B`) per user (`A`). In Excel versions without dynamic arrays, enter with `Ctrl+Shift+Enter`.

    ```text theme={null}
    {=MIN(IF(A:A=A2, B:B))}
    ```
  </Tab>
</Tabs>

### Percent of total

In this scenario, you're viewing sales by region and want to know what percentage of the company's total sales each region represents.

<Tabs>
  <Tab title="Omni LOD" id="lod-percent-of-total">
    The fields in the workbook's LOD editor would look like the following:

    * **Field to aggregate** - `Sales`
    * **Aggregation** - `Sum`
    * **Level of Detail Grouping** - `Exclude`
    * **Fixed dimension(s)** - `Region`

    This returns the grand total on every row, so you can divide each row's sales by it to get percent of total.
  </Tab>

  <Tab title="SQL window function" id="sql-percent-of-total">
    ```sql theme={null}
    SUM(sales) OVER ()
    ```
  </Tab>

  <Tab title="CTE" id="cte-percent-of-total">
    ```sql theme={null}
    SELECT region, 
           sales, 
           (
            SELECT SUM(sales)
              FROM orders
           ) AS grand_total
      FROM orders
    ```
  </Tab>

  <Tab title="Excel" id="excel-percent-of-total">
    In Excel, build it as a pivot:

    1. Add `Sales` to **Values** twice.
    2. Set the second to **Show Values As > % of Column Total**.
  </Tab>
</Tabs>

## Next steps

* Create your first LOD field in a [workbook](/analyze-explore/custom-fields/level-of-detail)
* Build a [cohort analysis](/guides/patterns/build-cohort-analysis-using-level-of-detail) using an LOD field
* Promote logic to a [derived table or query view](/modeling/query-views/index)
* Read Omni's [blog post on level of detail fields](https://omni.co/blog/level-of-detail-fields)
