LIMITED TIME 🎁: Register now to get 60 minutes of AI Mock Interviewing for FREE!

Join
    SIG (Susquehanna) Interview Insights
    S
    SIG (Susquehanna)·Data Scientist·Technical Phone Screen·Intermediate
    IntermediatePrefer not to say
    Jul 2026Remote
    5

    Summary

    SQL-heavy technical screen for a data scientist role at SIG. Six questions back to back, all analytics SQL, no behavioral fluff. The problems ranged from basic conversion funnels to some genuinely tricky window function and cross-join completeness stuff.

    Questions Asked(6)

    Product Analytics & MetricsData Modeling
    A
    Author's notesFirst line only

    Straightforward on the surface but the 'regardless of when delivered' part tripped me up briefly.

    Suggested Approach

    Start by clearly defining the 7-day window filter on the 'created_at' timestamp to scope the order cohort, then aggregate delivered orders (status = 'delivered' AND delivered_at IS NOT NULL) grouped by city and courier_type. Express the conversion rate as COUNT(delivered orders) / COUNT(total created orders) per group, handling potential division-by-zero edge cases.

    Pro tip: Explicitly call out that 'delivered' status is evaluated regardless of when delivery occurred — this means you filter the window on created_at only, not delivered_at, which is a subtle but critical distinction that separates strong candidates from average ones.
    1

    Define the Time Window

    Clarify the 7-day window by filtering orders where created_at falls within a specific date range (e.g., BETWEEN '2024-01-01' AND '2024-01-07'). Confirm whether the window is fixed historically or rolling, and use inclusive/exclusive bounds consistently.

    2

    Identify Delivered Orders

    Within the scoped cohort, flag an order as delivered only when status = 'delivered' AND delivered_at IS NOT NULL. Note that the delivery event can occur outside the 7-day window — what matters is that the order was created within it.

    3

    Aggregate by City and Courier Type

    Group the filtered dataset by city and courier_type, computing COUNT(*) for total created orders and SUM of the delivered flag for delivered orders. This gives the numerator and denominator needed for the conversion rate.

    4

    Compute Conversion Rate Safely

    Calculate conversion_rate = delivered_count / NULLIF(created_count, 0) to avoid division-by-zero errors. Optionally multiply by 100 and round to a meaningful decimal precision for readability.

    5

    Validate and Interpret Results

    Sanity-check the output by verifying that conversion rates fall between 0 and 1 (or 0–100%), and flag any city/courier_type combinations with very low order volumes where rates may be statistically unreliable. Consider adding a volume threshold or confidence note.

    Key Points to Mention

    Filter on created_at for the 7-day window — NOT delivered_at — since the question asks for orders created in that period
    Dual condition for delivery: status = 'delivered' AND delivered_at IS NOT NULL (both conditions required to avoid counting erroneous records)
    Use NULLIF or CASE WHEN to guard against division-by-zero when a city/courier_type bucket has zero created orders
    GROUP BY city, courier_type to produce the full breakdown matrix across all combinations
    Acknowledge that low-volume segments may produce noisy or misleading conversion rates and suggest a minimum volume filter
    Optionally use a CTE or subquery structure to separate the window scoping logic from the aggregation for readability and maintainability
    Product Analytics & MetricsData Modeling
    A
    Author's notesFirst line only

    The zero-fill requirement is where people mess up.

    Suggested Approach

    Start by generating a complete spine of all (month, courier_type) combinations for the last 3 months using a cross join, then left-join actual order data onto that spine so zero-activity months are preserved. Aggregate delivered counts and created counts separately, then compute GMV as delivered_count multiplied by the fixed rate per courier type.

    Pro tip: Explicitly call out the 'zero-activity month' requirement as a classic completeness trap — interviewers at quant firms like SIG use this to test whether you default to INNER JOINs (which silently drop missing periods) versus a deliberate spine-based approach with CROSS JOIN and COALESCE.
    1

    Define the Date Spine

    Generate a reference table of the last 3 calendar months (e.g., using DATE_TRUNC + GENERATE_SERIES or a CTE with explicit month values) to ensure every month appears in the output regardless of activity.

    2

    Enumerate All Courier Types

    Pull the distinct list of courier types from the reference/dimension table (or from the orders table itself) so you can cross join it with the date spine to create every (month, courier_type) combination.

    3

    Aggregate Order Metrics

    Left-join the orders table onto the spine, grouping by month and courier type to compute COUNT of orders created and COUNT of orders with a delivered status, using COALESCE to replace NULLs with zero.

    4

    Calculate GMV

    Multiply the delivered count by the fixed rate per courier type (either a constant or looked up from a rates table) to derive GMV, ensuring the rate is applied after the COALESCE so zero-activity rows yield GMV of 0.

    5

    Format and Validate Output

    Order results by month and courier type, then sanity-check that every expected (month, courier_type) row is present and that aggregate totals align with known benchmarks or a simpler rollup query.

    Key Points to Mention

    Spine/scaffold pattern using CROSS JOIN between date series and courier types to guarantee zero-activity rows appear
    LEFT JOIN from spine to orders table (not INNER JOIN) to avoid silently dropping missing periods
    COALESCE(COUNT(...), 0) or SUM with COALESCE to convert NULLs to zero for created and delivered counts
    DATE_TRUNC('month', order_date) for consistent monthly bucketing and handling of month boundaries
    GMV calculation as delivered_count * fixed_rate, and whether the rate varies by courier type (clarifying assumption)
    Filtering logic for 'last 3 months' — clarify whether it means the 3 most recent complete calendar months or rolling 90 days, and handle timezone consistency
    Algorithms & Data StructuresData Modeling
    A
    Author's notesFirst line only

    This was the one that made me sweat.

    Suggested Approach

    Filter out non-delivered orders first, then use a window function to assign row numbers partitioned by user and ordered by time, resetting the sequence whenever a non-biker delivery is encountered. Identify consecutive biker-only streaks of length ≥ 3 and extract the first such streak per user, returning the 1st and 3rd order IDs along with the timestamp difference in minutes.

    Pro tip: At SIG, precision in edge-case handling signals strong analytical thinking — explicitly address what happens when a user has multiple streaks of exactly 3 (return only the earliest one) and when non-delivered orders appear mid-streak (they are invisible, so two biker deliveries separated by a non-delivered order still count as consecutive).
    1

    Filter & Rank Delivered Orders

    Remove all non-delivered orders from the dataset entirely, as they are irrelevant to streak calculation. Assign a row number (e.g., delivered_rank) partitioned by user_id and ordered by order timestamp to establish a clean sequence of delivered orders.

    2

    Flag Biker vs. Non-Biker Deliveries

    Within the filtered delivered orders, add a boolean flag indicating whether each order was delivered by a biker. Any non-biker delivery resets the streak, so this flag is the key signal for grouping.

    3

    Assign Streak Groups

    Use the classic 'gaps and islands' technique: compute delivered_rank minus a row number partitioned by user_id and biker_flag to create a group identifier. Rows with the same group ID and biker_flag = true form a consecutive biker streak.

    4

    Identify Valid Streaks of Length ≥ 3

    Count the size of each streak group and filter to those with at least 3 orders. Within each qualifying streak, use ROW_NUMBER() to label positions 1, 2, 3... so you can pinpoint the 1st and 3rd orders.

    5

    Extract First Streak Per User & Compute Time Span

    Rank the valid streaks per user by the timestamp of their first order and keep only rank = 1 (the earliest streak). Return the order_id of position 1, the order_id of position 3, and DATEDIFF or TIMESTAMPDIFF in minutes between their timestamps.

    Key Points to Mention

    Gaps-and-islands pattern using ROW_NUMBER() minus ROW_NUMBER() trick to identify consecutive groups without self-joins
    Importance of filtering non-delivered orders before any ranking or grouping, not after, to avoid incorrect sequence numbering
    Non-biker delivered orders reset the streak counter, so they must be included in the ranking step to correctly break groups
    Using FIRST_VALUE() or self-joining on streak position to retrieve both the 1st and 3rd order IDs within the same streak
    Handling ties or duplicate timestamps — clarify assumptions with the interviewer (e.g., use a tiebreaker like order_id)
    Time span calculation using TIMESTAMPDIFF(MINUTE, ...) in MySQL or EXTRACT(EPOCH FROM ...) / 60 in PostgreSQL, and noting timezone considerations
    Product Analytics & Metrics
    A
    Author's notesFirst line only

    Pretty standard once you know percentile_cont.

    Suggested Approach

    Approach this as a SQL aggregation problem requiring window functions or percentile functions, filtering for delivered orders within the last 30 days and grouping by courier type. Use a database-appropriate percentile function (e.g., PERCENTILE_CONT in standard SQL or APPROX_PERCENTILE in distributed systems) to compute the 95th percentile of delivery duration in minutes. Clearly define delivery time as the difference between delivery timestamp and order creation timestamp.

    Pro tip: At a quant-driven firm like SIG, mention awareness of approximate vs. exact percentile functions — APPROX_PERCENTILE is faster on large datasets but trades off precision, which is a real engineering decision worth flagging. Also note that outliers (e.g., cancelled-then-redelivered orders) can skew percentile results and should be discussed.
    1

    Clarify the Schema & Definitions

    Identify the relevant table(s) and columns: order creation timestamp, delivery timestamp, order status, and courier type. Confirm that 'delivery time' means the elapsed minutes between order creation and delivery completion.

    2

    Filter the Dataset

    Apply WHERE conditions to restrict rows to orders with status = 'delivered' and where the order creation timestamp falls within the last 30 days (e.g., created_at >= CURRENT_DATE - INTERVAL '30 days').

    3

    Compute Delivery Duration

    Calculate delivery time in minutes using timestamp arithmetic, such as EXTRACT(EPOCH FROM (delivered_at - created_at)) / 60 in PostgreSQL or DATEDIFF('minute', created_at, delivered_at) in other dialects.

    4

    Apply Percentile & Count Aggregations

    Group by courier_type and apply PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY delivery_minutes) for the 95th percentile, along with COUNT(*) for the group size.

    5

    Validate & Interpret Results

    Check for NULL delivery timestamps or negative durations that could indicate data quality issues, and briefly interpret what a high 95th percentile for a specific courier type would mean operationally.

    Key Points to Mention

    Use of PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY ...) for exact percentile calculation in standard SQL
    Distinction between exact percentile functions and approximate alternatives (APPROX_PERCENTILE) for scalability on large datasets
    Proper timestamp arithmetic to convert delivery duration into minutes, accounting for SQL dialect differences
    Filtering on both order status ('delivered') and creation date within the last 30 days to ensure correct scope
    Handling of data quality issues such as NULL delivered_at timestamps, duplicate records, or negative delivery times
    Business interpretation: how the 95th percentile (tail latency) is more actionable than the mean for identifying worst-case customer experiences
    Data ModelingProduct Analytics & Metrics
    A
    Author's notesFirst line only

    Harder version of task B.

    Suggested Approach

    Start by identifying all distinct cities from both the users and couriers tables using a UNION to create a complete city dimension, then cross join it with all courier types and all months to build a complete spine of combinations. Finally, left join the actual order data onto this spine so that missing combinations naturally appear as zero rather than being omitted.

    Pro tip: Explicitly calling out that cities must be sourced from BOTH users and couriers tables (not just one) signals you understand real-world data asymmetry — cities may have couriers but no users yet, or vice versa — which is the kind of edge case that separates strong analysts from average ones.
    1

    Build the Complete City Dimension

    Use UNION (not UNION ALL) on the city column from both the users and couriers tables to get every distinct city that appears in either source, eliminating duplicates.

    2

    Generate the Full Spine via CROSS JOIN

    Cross join the city dimension with all distinct courier types and all relevant months (e.g., from a date dimension or derived from the orders table) to produce every possible combination as the reporting spine.

    3

    Aggregate Actual Order Data

    Separately aggregate the orders table by city, courier type, and month to get real volume counts, keeping this as a subquery or CTE before joining.

    4

    Left Join Actuals onto the Spine

    Left join the aggregated order data onto the full spine using city, courier type, and month as join keys, then use COALESCE to replace NULL order counts with zero.

    5

    Validate and Sanity-Check Output

    Verify the total row count equals cities × courier_types × months, and confirm that the sum of the new report's volume matches the original report's total to ensure no data was lost or duplicated.

    Key Points to Mention

    Using UNION (not UNION ALL) across users and couriers tables to deduplicate cities from both sources
    CROSS JOIN to generate a complete Cartesian product of all dimension combinations (city × courier type × month)
    LEFT JOIN from the spine to actuals — not the other way around — to preserve zero-volume rows
    COALESCE(order_count, 0) to convert NULLs from unmatched rows into meaningful zero values
    Ensuring the month dimension covers a consistent, pre-defined range rather than only months with activity
    Row count and volume sum validation to confirm the spine is complete and aggregation is correct
    Product Analytics & MetricsRoot Cause Analysis
    A
    Author's notesFirst line only

    Easy one to end on.

    Suggested Approach

    Approach this as a filtering and derived-column problem: first scope the dataset to orders at or before the cutoff timestamp, then compute the delivery duration and apply the two-hour threshold logic. Handle NULL delivered_at values explicitly using COALESCE or IS NULL checks, and surface the result as a boolean flag column for downstream use.

    Pro tip: At a quant-driven firm like SIG, interviewers appreciate when you proactively mention edge cases such as timezone normalization, clock skew between systems, or whether 'created_at' and 'delivered_at' live in the same timezone — it signals production-level thinking beyond just writing correct SQL.
    1

    Clarify the Cutoff and Schema

    Confirm the data types of created_at and delivered_at (TIMESTAMP vs. DATETIME) and how the cutoff value is supplied — as a parameter, a variable, or a hardcoded literal. This prevents silent type-mismatch errors.

    2

    Filter Orders Up to the Cutoff

    Use a WHERE clause with created_at <= :cutoff_timestamp to scope the result set before applying any delivery logic, keeping the query efficient and the intent clear.

    3

    Compute Delivery Duration

    Calculate the elapsed time between created_at and delivered_at using TIMESTAMPDIFF (MySQL), EXTRACT(EPOCH FROM ...) (PostgreSQL), or DATEDIFF with appropriate units, depending on the SQL dialect in use.

    4

    Apply the Stale-Order Condition

    Flag an order as stale when delivered_at IS NULL OR the computed duration exceeds 7200 seconds (2 hours). Use a CASE WHEN expression or a boolean cast to produce the flag column, e.g., CASE WHEN delivered_at IS NULL OR delivery_seconds > 7200 THEN TRUE ELSE FALSE END AS is_stale.

    5

    Validate and Discuss Extensions

    Sanity-check the output by checking counts of stale vs. non-stale orders and verifying a few NULL rows manually. Mention how this flag could feed into a root-cause analysis — e.g., joining with courier or region tables to identify systemic delays.

    Key Points to Mention

    Explicit NULL handling: delivered_at IS NULL must be treated as a stale order independently of the duration check, using OR logic rather than relying on NULL arithmetic returning NULL.
    SQL dialect awareness: timestamp arithmetic syntax differs across engines (TIMESTAMPDIFF in MySQL, EPOCH extraction in PostgreSQL, DATEADD/DATEDIFF in SQL Server) — state your assumption or ask.
    Boolean column representation: some SQL dialects lack a native BOOLEAN type; clarify whether to return TRUE/FALSE, 1/0, or a string flag depending on the downstream consumer.
    Cutoff parameterization: using a bind parameter or CTE variable for the cutoff timestamp makes the query reusable and avoids hardcoding, which is important for scheduled pipelines.
    Timezone consistency: confirm both timestamps are stored in the same timezone (ideally UTC) to avoid incorrect duration calculations across DST boundaries.
    Downstream analytical value: the boolean flag enables easy aggregation (SUM(is_stale), AVG(is_stale)) for KPI dashboards or root-cause slicing by region, product category, or courier.

    Discussion(5)

    Sign in to join the discussion.

    L
    Lily_P· 57d ago
    Q1For a fixed 7-day window, compute conversion rate (delivered / created) broken down by city and courier type. An order counts as delivered if status is 'delivered' and delivered_at is not null, regardless of when it was delivered.

    The 'regardless of when delivered' phrasing is doing a lot of work there and it's easy to blow past it. Your fix is right: anchor the window filter entirely on created_at, then use CASE WHEN status = 'delivered' AND delivered_at IS NOT NULL THEN 1 ELSE 0 END inside a SUM (or a COUNT with a FILTER clause if you're on Postgres) to get delivered count. The denominator is just COUNT(*) of all orders in that created_at window. I've seen people write this with a WHERE on status and then wonder why their denominator is wrong, which is a subtler version of the same mistake you almost made. Fast reading kills you on these.

    Q
    QuestionsByK· 57d ago
    Q6Find all orders created on or before a cutoff timestamp that were not delivered within 2 hours of creation. Include orders where delivered_at is null or where it exceeds the 2-hour threshold. Return a boolean column flagging these stale orders.

    Yeah, this is a relief question after Q3. The boolean column is just (delivered_at IS NULL OR delivered_at > created_at + INTERVAL '2 hours') AS is_stale, filter the whole thing on created_at <= your cutoff, and you're done. The only micro-gotcha is making sure you're not accidentally excluding undelivered orders with a status filter somewhere upstream.

    V
    VectorVector· 57d ago
    Q3For each user, find the first streak of 3 consecutive delivered biker orders. Any non-biker delivery breaks the streak; non-delivered orders are ignored entirely. Return the first and third order IDs and the time span between them in minutes.

    Gap-and-islands with a filtered subset is rough, and doing it live under a SIG phone screen sounds painful. Your approach is the right one though. Number all delivered orders per user chronologically (ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY delivered_at)), then separately number only the delivered biker orders per user the same way. Subtract the biker-specific row number from the overall delivered row number and you get a constant group key for each consecutive biker run. A non-biker delivered order increments the overall counter but not the biker counter, so the difference shifts and breaks the group. To isolate the first streak, you MIN() the group key per user and filter to that. Then within that group you grab the row where biker_rn = 1 and biker_rn = 3 for your order IDs, and DATEDIFF or EXTRACT(EPOCH ...) between those two timestamps for the minutes span. Messy to type out fast but the logic holds.

    A
    ArrayOfHope· 57d ago
    Q5Extend the monthly volume report to include city as a dimension, ensuring every combination of city (from both users and couriers), courier type, and month appears in the output even with zero orders.

    This one is legitimately harder than Q2 because the city dimension doesn't live on the order itself, which creates a join sequencing problem that's easy to get tangled in. The spine needs three things crossed together: months, courier types, and cities. For cities, you want a UNION (not UNION ALL) of the city column from users and couriers so you catch every city that appears anywhere in the data. Then CROSS JOIN that cities list with your courier types list and your calendar months. That's your full spine. To get actual order data onto it, join orders to couriers on courier_id to pull in the courier's city, then aggregate, then LEFT JOIN that aggregated result onto the spine. The key mistake you described, joining city in the wrong direction, usually happens when you try to join the spine to orders directly and end up filtering out spine rows that have no matching orders. The aggregation has to happen first, then the left join to the spine, not the other way around.

    Q
    QuestionsByK· 57d ago
    Q2Generate a monthly volume report for the last 3 months showing orders created, orders delivered, and GMV (delivered count times a fixed rate) per courier type. Months with zero activity must still appear in the output.

    Zero-fill requirements are genuinely where SQL screens separate people, and the cross join piece is the part that's easy to forget under pressure. The structure I'd reach for: generate your 3-month calendar spine with something like a recursive CTE or a VALUES list of the three month-start dates, then CROSS JOIN that with SELECT DISTINCT courier_type FROM couriers to get every combination you need to preserve. From there it's a LEFT JOIN onto your aggregated orders, with COALESCE wrapping the counts and GMV to turn NULLs into zeros. The GMV calc is almost an afterthought once the spine is solid. One thing worth double-checking: make sure your month truncation (DATE_TRUNC('month', created_at)) in the orders aggregation matches exactly how you're labeling months in your spine, otherwise the join silently drops rows and you get gaps again without realizing why.

    Interview Details

    CompanySIG (Susquehanna)
    RoleData Scientist
    RoundTechnical Phone Screen
    LevelIntermediate
    OutcomePrefer not to say
    DateJul 2026
    LocationRemote

    Questions in this post

    Share your own experience

    Help the community by sharing what you went through.