cswork / libraryOpen practice workspace

Articles

SQL Attribution Interview Questions: First Touch, Last Touch, and Conversion Windows

Practice SQL attribution questions with first- and last-touch queries, conversion windows, tie-breaking, and checks for missed or duplicate conversions.

SQL attribution interview questions ask you to turn a business rule into a query over dated touchpoints and conversion events. Define the grain, find eligible touches for each conversion, then choose a deterministic first or last touch inside an explicit window. A user may convert more than once, so a user-level assignment can lose conversion paths.

Treat the examples here as rule-based reporting exercises: they assign credit according to a stated convention, but they do not prove that a channel caused a conversion.

Short answer: filter touches to each conversion's stated lookback window, then rank earliest and latest with a stable tie-breaker. Preserve unmatched conversions, define direct-traffic handling, and verify one output row per conversion.

Define the row grain before writing SQL

Start by saying what one output row represents. For a basic attribution table, it is one row per conversion, not one row per user, session, or touchpoint. That distinction matters when the same person buys twice, sees several ads, or converts after a later campaign interaction.

Ask which fields identify the entities. A useful simplified schema has conversions(conversion_id, user_id, converted_at, conversion_value) and touchpoints(touch_id, user_id, touched_at, channel). If identity can change across devices, the question needs a clearly defined identity key; do not assume that two devices belong to one person because their timestamps look similar.

Before coding, clarify whether the task credits impressions, clicks, sessions, or any marketing interaction; whether direct visits are eligible; how long before conversion to search; and what to do with repeated conversions. A “30-day window” can mean the 30×24 hours immediately before each conversion or 30 calendar dates in a declared timezone. Those definitions differ around midnight and daylight-saving changes.

First touch and last touch are ordering rules

Given a fixed eligible path, first-touch attribution selects its earliest event and last-touch attribution selects its latest. Snowplow's SQL tutorial demonstrates one practical implementation; it is an example, not a universal standard. In a window-function answer, ROW_NUMBER() makes the chosen row explicit. Add a unique event key after the timestamp so two events with identical times do not produce an arbitrary result.

For a conversion-specific path, the ranking key should include conversion_id. Partitioning only by user_id finds one first or last touch for the user's whole history. That is correct only if the requested output is explicitly user-level acquisition; it is usually wrong when conversions are counted individually.

Distinguish first touch inside a conversion window from first-ever acquisition. If asked where the user was originally acquired, do not limit the search to the 30 days before a later purchase. State the time scope and identity key.

These labels have product-specific meanings. Official product fact: Google Analytics Help lists data-driven attribution, paid-and-organic last click, and Google-paid-channels last click in its reports. First-click, linear, time-decay, and position-based models have been unavailable since November 2023, and last-click applies Google-specific Direct rules. A raw first/last-touch SQL exercise is an implementation convention, not a description of every platform. (Google Analytics: Get started with attribution)

RuleOrdering inside each conversion's eligible pathWhat the output means
First touchtouched_at ASC, touch_id ASCThe earliest qualifying touch receives the full conversion credit.
Last touchtouched_at DESC, touch_id DESCThe latest qualifying touch receives the full conversion credit.
Last non-directLatest eligible non-direct touch; fall back only if the prompt says howA product/reporting convention that differs from a plain last-touch query.

Worked example: one user converts twice

Suppose user u1 has these events, all times stored in UTC. A 30-day lookback includes the lower endpoint and excludes events at or after the conversion time.

EventTimeChannelValue
Touch t1Apr 1, 09:00Search—
Touch t2Apr 5, 12:00Email—
Touch t3Apr 10, 08:00Paid search—
Touch t4Apr 20, 08:00Paid social—
Conversion c1Apr 21, 08:00—$120
Touch t5Apr 22, 14:00Email—
Conversion c2May 3, 08:00—$50

For c1, the first qualifying channel is Search and the last is Paid social. t5 occurs after c1, so it cannot receive credit for that conversion. For c2, a rolling 30-day window starts on Apr 3; t1 has aged out, making Email the first qualifying touch (t2) and Email the last (t5). Those labels come from two different Email events.

Notice what happens if you rank only by user: the query might assign both conversions to Search, or return only one conversion row. Either outcome loses the conversion-specific path. A conversion ID in the partition keeps each decision independent.

Add an explicit lookback window

The SQLite example uses normalized UTC text (YYYY-MM-DD HH:MM:SS) and returns first and last channel per conversion, including unmatched conversions. It includes Direct and does not implement last-non-direct attribution. The bound :lookback_days parameter is 30 in the fixture.

WITH ranked_touches AS (
    SELECT
        c.conversion_id,
        c.user_id,
        c.converted_at,
        c.conversion_value,
        t.touch_id,
        t.channel,
        ROW_NUMBER() OVER (
            PARTITION BY c.conversion_id
            ORDER BY t.touched_at ASC, t.touch_id ASC
        ) AS first_rank,
        ROW_NUMBER() OVER (
            PARTITION BY c.conversion_id
            ORDER BY t.touched_at DESC, t.touch_id DESC
        ) AS last_rank
    FROM conversions AS c
    LEFT JOIN touchpoints AS t
      ON t.user_id = c.user_id
     AND t.touched_at >= datetime(
           c.converted_at,
           printf('-%d days', :lookback_days)
         )
     AND t.touched_at < c.converted_at
)
SELECT
    conversion_id,
    user_id,
    converted_at,
    conversion_value,
    MAX(CASE WHEN first_rank = 1 THEN
        CASE WHEN touch_id IS NULL THEN 'Unattributed'
             ELSE COALESCE(channel, 'Unknown channel') END
    END) AS first_touch_channel,
    MAX(CASE WHEN last_rank = 1 THEN
        CASE WHEN touch_id IS NULL THEN 'Unattributed'
             ELSE COALESCE(channel, 'Unknown channel') END
    END) AS last_touch_channel
FROM ranked_touches
GROUP BY conversion_id, user_id, converted_at, conversion_value
ORDER BY converted_at, conversion_id;

For that fixture, the query returns c1 as Search/Paid social and c2 as Email/Email. An execution fixture also checks an exact 30-day boundary and an unmatched conversion; the expected labels are Affiliate/Affiliate and Unattributed/Unattributed, respectively.

The lower bound is inclusive, so a touch exactly 30 days before conversion qualifies. The upper bound is exclusive, so a touch recorded at or after conversion does not. If the prompt wants calendar-day windows, define a timezone and cast to dates deliberately instead of reusing this rolling interval.

Official product fact: Google Analytics defines its key-event lookback window as how far back a touchpoint can be to receive attribution credit. Its current documentation lists a 30-day default for acquisition events and a 90-day default for other key events, with other selectable values. Those defaults apply to Google Analytics settings; they are not a universal interview answer or a recommendation for every business. (Google Analytics: Select attribution settings)

Test the boundary cases that change the answer

The query is only as useful as its edge-case behavior. State the policy before changing the SQL; an interview prompt may intentionally leave one of these choices open so you can ask a clarifying question.

CaseExpected behavior under the example ruleCheck
Touch exactly 30 days before conversionIncludedCompare against the inclusive lower bound.
Touch one second earlierExcludedConfirm the filter uses the rolling timestamp, not a date-only comparison.
Touch at conversion time or laterExcludedKeep the < converted_at predicate.
Two touches share a timestampOne selected by the declared touch_id tie-breakerAsk whether an upstream sequence number is more meaningful.
Conversion has no eligible touchConversion stays in output as UnattributedKeep the eligibility predicates in the LEFT JOIN condition.
Same user converts twiceTwo output rows, each with its own eligible pathPartition by conversion_id.

There is a subtle join bug here: put the time-window conditions in a WHERE clause after a left join, and unmatched conversions are filtered out. Keep them in the ON clause if you want the unmatched conversion row preserved. Also deduplicate repeated event deliveries before ranking when the source can resend the same touch_id; otherwise a duplicate may alter counts or tie behavior.

Validate attribution without trusting the totals blindly

Run checks at conversion grain before grouping by channel. First, compare the number of distinct source conversion_id values with the final row count. Second, verify that every conversion appears once, including those with no path. Third, sum conversion value once from the conversion table and again from the attributed output; an increase usually signals a fanout join, while a decrease often means a conversion was dropped.

Then inspect selected rows manually: earliest eligible touch, latest eligible touch, a touch on each window boundary, a touch after conversion, and a user with two conversions. A query can return plausible channel totals while still using an incorrect join key. Joining only on user_id is not enough if the event log is not deduplicated or if the attribution policy requires ad, campaign, product, or session matching as well.

In SQLite, window functions such as row_number() assign a row number according to the ordering in the OVER clause. That makes the explicit tie-breaker material: if the ORDER BY fields are tied, the query has not stated a complete business rule. The SQLite window-function documentation describes ranking functions; its date/time documentation covers the date modifiers used in this example.

Separate assigned credit from causal impact

First- or last-touch SQL answers, “Which eligible interaction gets credit under this rule?” It does not answer, “Would the person have converted without this interaction?” People who receive or notice a channel can differ from people who do not, and channel timing may be related to intent.

Inference: When a case asks whether to shift budget, present attribution as one descriptive view and state what additional evidence would support an incremental claim—often a suitable randomized holdout or a defensible quasi-experimental design. Do not turn a channel's credited revenue into proof that the channel created that revenue. Google's description of data-driven attribution also distinguishes model-based credit allocation from a simple last-touch rule; the chosen reporting model should still be understood as an attribution method, not a universal causal guarantee.

Practice with five reported SQL question types

QuestionPractice focus
Compute ad impression conversion ratesMatch a conversion to an eligible impression within seven days, then preserve ads with zero attributed conversions.
Analyze Thirty-Day Ad Performance with SQLState the reporting window, grain of intermediate tables, denominator, and attribution rule.
Determine Top Advertisers by Conversion Rate and CTR AnalysisSeparate descriptive time comparisons from evidence of causal lift.
Retrieve Ad Metrics and Rates for Last 7 DaysJoin event rows without multiplying impressions, then protect rate denominators.
Query conversion and retention with SQL windowsDeduplicate user events and define ordered conversion windows.

A concise way to explain your answer

Before presenting SQL, say: “I’ll return one row per conversion, match the same resolved user ID within the stated 30-day window, include its lower bound, exclude the conversion instant and later touches, and preserve unmatched conversions. I’ll rank first and last separately, using touch_id to break timestamp ties.”

Walk through one conversion, name a validation query, and describe what changes for last-non-direct or multi-touch credit. Clarify identity and window rules before coding when they are underspecified.

Before running a query, write down its grain, window, tie rule, and unmatched-row policy.

Sources and Further Reading

Research checked September 12, 2026. Google Analytics settings are product-specific; the SQL conventions above are stated assumptions for an interview exercise.