Metric

What is average check size (average ticket)?

Definition

Average check size — the average ticket — is net sales divided by closed checks. It looks trivial and almost never is: whether comps, discounts, tax, and tips sit inside the number changes it by double digits, and a single large party check can move a slow-daypart average more than a whole week of normal service.

Formula: Average check size = net sales for closed, non-voided checks / count of those checks

What data do you need?

  • Checks with status, void flag, close timestamp, and location
  • Gross sales plus discount, comp, tax, service charge, and tip amounts as separate fields
  • Channel — dine-in, takeout, first-party online, delivery marketplace
  • Guest count per check for per-guest views
  • Line items for mix and attachment analysis

SQL pattern

Average check by daypart and channel, with median and p90PostgreSQL
WITH closed AS (
  SELECT
    c.check_id,
    c.location_id,
    c.channel,
    -- Dayparts are wall-clock concepts, so bucket by the venue's LOCAL
    -- time. If closed_at is stored in UTC, convert it with the location's
    -- timezone first; bucketing raw UTC hours misdates every venue that
    -- isn't on UTC. (Drop the conversion if closed_at is already local.)
    (c.closed_at AT TIME ZONE 'UTC' AT TIME ZONE l.timezone)
      AS local_closed_at,
    c.guest_count,
    -- Convention, stated once and applied everywhere: net sales is
    -- post-discount and post-comp, pre-tax, pre-tip, pre-service-charge.
    c.gross_sales - c.discount_amount - c.comp_amount AS net_sales,
    c.gross_sales
  FROM pos_checks c
  JOIN pos_locations l ON l.location_id = c.location_id
  WHERE c.status = 'closed'
    AND c.voided_at IS NULL
    AND c.closed_at >= CURRENT_DATE - INTERVAL '30 days'
), parted AS (
  SELECT
    closed.*,
    -- Late night spans 22:00–04:59 so a post-midnight close isn't
    -- misfiled as breakfast — the classic bug for venues open past 12.
    CASE
      WHEN EXTRACT(hour FROM local_closed_at) BETWEEN 5 AND 10
        THEN 'breakfast'
      WHEN EXTRACT(hour FROM local_closed_at) BETWEEN 11 AND 15
        THEN 'lunch'
      WHEN EXTRACT(hour FROM local_closed_at) BETWEEN 16 AND 21
        THEN 'dinner'
      ELSE 'late night'
    END AS daypart
  FROM closed
)
SELECT
  daypart,
  channel,
  COUNT(*) AS checks,
  ROUND(AVG(net_sales), 2) AS avg_check,
  ROUND(
    PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY net_sales)::numeric, 2
  ) AS median_check,
  -- p90 next to the mean shows when large party checks are doing the
  -- work; a mean far above the median is a distribution, not a trend.
  ROUND(
    PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY net_sales)::numeric, 2
  ) AS p90_check,
  -- True per-guest figure: total sales over total guests. Averaging the
  -- per-check ratio would weight a 2-top the same as a 20-top.
  ROUND(SUM(net_sales) / NULLIF(SUM(guest_count), 0), 2) AS sales_per_guest,
  ROUND(
    100.0 * SUM(gross_sales - net_sales) / NULLIF(SUM(gross_sales), 0), 1
  ) AS discount_and_comp_pct,
  ROUND(
    100.0 * COUNT(*) FILTER (WHERE guest_count IS NULL)
    / NULLIF(COUNT(*), 0), 1
  ) AS pct_missing_guest_count
FROM parted
GROUP BY 1, 2
ORDER BY 1, 2;

Common pitfalls

Leaving voided and reopened checks in the count.→ Voids inflate check counts and deflate the average. Filter on void status, and watch the void rate itself — a spike usually means a training or POS-workflow issue, not a sales issue.
Not deciding where tax, tips, and service charges live.→ Write the convention down before building the card. Tax and a 20% tip can push the same check 30% higher; two dashboards with different conventions will never reconcile.
One average across dine-in and delivery marketplaces.→ Marketplace orders carry different basket sizes, inflated menu prices, and 15–30% commissions. Segment by channel, and read the number against the commission it carries.
Reporting the mean alone for a small-volume daypart.→ One 20-top can lift a slow lunch average by several dollars. Lead with the median, keep the mean beside it, and let the gap tell you when large parties are the story.
Computing sales per guest as the average of each check's per-guest ratio.→ That is an average of averages: it weights a 2-top and a 20-top equally. Use total net sales over total guests — <code>SUM(net_sales) / SUM(guest_count)</code> — so every guest counts once.
Bucketing dayparts from a UTC timestamp, or by close hour alone.→ Convert the timestamp to the venue's local time before extracting the hour, and let late night span past midnight (22:00–04:59) so a 12:30am close isn't filed as breakfast.

Where does this metric apply?

This metric commonly uses data from Toast, Lightspeed, Square, Clover, plus any warehouse models built at the same grain.

Dashboards

Integrations

Metrics

Analytics

FAQ

Average check or sales per guest?
Both, on the same card. Average check moves when party sizes change; sales per guest isolates whether people are actually spending more. A rising check with a flat per-guest number just means bigger tables.
How do I find what actually drives the check up?
Join pos_check_items to pos_checks and measure attachment rate — the share of checks containing an appetizer, a dessert, or a second beverage. Attachment rate moves with server behavior and menu layout, which makes it far more actionable than the headline average.
Is average check the same as average order value?
Structurally yes — average order value is the ecommerce name for it. The conventions differ though: AOV usually excludes tax and shipping, while POS reporting often includes tax by default. Reconcile the definitions before comparing an online store to a physical one.