Python Coding & SQL — DE Interview PrepPython Coding и SQL — Подготовка к DE-интервью

Senior Data Engineer · Fast-revision cheatsheet for Data Engineering interviews. First round (~60 min) may include live Python coding and/or SQL portions: parse/clean/aggregate structured, semi-structured (nested JSON) and unstructured (text) data.

Senior Data Engineer · Шпаргалка для быстрого повторения перед Data Engineering интервью. Первый раунд (~60 мин) может включать живой Python coding и/или SQL: парсинг/очистка/агрегация структурированных, полуструктурированных (вложенный JSON) и неструктурированных (текст) данных.

Interviewers reward correctness, clean readable code, clear reasoning out loud, complexity awareness, and handling of edge cases. Talk through trade-offs.

Интервьюеры ценят корректность, чистый читаемый код, чёткую вербализацию рассуждений, понимание сложности и обработку граничных случаев. Проговаривай компромиссы.

comprehensions · generatorscomprehensions · generators window functions · CTEswindow functions · CTE gaps-and-islands · sessionizationпробелы-и-острова · сессии time-series resamplingресамплинг временных рядов flip cards = solutionsкарточки-перевёртыши = решения
01

Idiomatic Python & Lazy EvaluationИдиоматический Python и ленивые вычисления

Comprehensions vs generatorsComprehensions vs генераторы

# list comp: materializes everything (memory)
squares = [x*x for x in rng if x % 2]
# generator expr: lazy, O(1) memory, single-pass
gen = (x*x for x in rng if x % 2)
total = sum(x*x for x in rng)  # no temp list

# dict / set comps
idx = {row.id: row for row in rows}
seen = {r.key for r in rows}
Generators give O(1) memory for streaming. Prefer them for files/logs that don't fit in RAM. Downside: single-pass, no len(), no indexing.
Генераторы дают O(1) памяти для потоковой обработки. Используй их для файлов/логов, не влезающих в RAM. Минусы: один проход, нет len(), нет индексирования.

Generators & pipelines (lazy)Генераторы и пайплайны (ленивые)

def read_lines(path):
    with open(path) as f:
        for line in f:          # file iterates line-by-line
            yield line.rstrip("\n")

# chain lazy steps; nothing runs until consumed
rows   = read_lines("big.log")
parsed = (parse(r) for r in rows)
valid  = (p for p in parsed if p)
for rec in valid:           # streams through pipeline
    handle(rec)
Each stage is a pure transform. Easy to test in isolation, composable, constant memory. Mention this when asked to process a "huge file".
Каждая стадия — чистая трансформация. Легко тестировать изолированно, композируется, константная память. Упоминай это, когда просят обработать «огромный файл».

collections toolkitИнструментарий collections

from collections import defaultdict, Counter, deque

groups = defaultdict(list)
for r in rows:
    groups[r.key].append(r)        # no KeyError

freq = Counter(words)              # O(n)
freq.most_common(10)              # top-K
freq.update(more_words)

q = deque(maxlen=5)              # O(1) ends; sliding window
"How would you count frequencies?" → Counter, then most_common(k). If only top-K matters on a stream, use a heap of size k (O(n log k)) instead of full sort.
«Как бы ты посчитал частоты?» → Counter, затем most_common(k). Если нужен только top-K на потоке, используй heap размера k (O(n log k)) вместо полной сортировки.

itertools (lazy combinators)itertools (ленивые комбинаторы)

from itertools import (groupby, islice, chain,
    accumulate, tee, count, takewhile)

# groupby needs SORTED input by the key!
for k, grp in groupby(sorted(rows, key=kf), key=kf):
    process(k, list(grp))

islice(stream, 0, 100)         # first 100, lazily
chain(a, b, c)                  # flatten iterables
accumulate(nums)                # running totals
itertools.groupby only groups consecutive equal keys. You must sort first, unlike SQL GROUP BY. Forgetting this is the #1 bug.
itertools.groupby группирует только подряд идущие одинаковые ключи. Нужна sort сначала, в отличие от SQL GROUP BY. Забыть это — ошибка №1.
Pythonic style signals: unpacking (a, *rest = xs), enumerate/zip over index loops, with for resources, dict.get(k, default), EAFP (try/except) over deep if checks, f-strings, pathlib over string paths.
Признаки питонического стиля: распаковка (a, *rest = xs), enumerate/zip вместо циклов по индексам, with для ресурсов, dict.get(k, default), EAFP (try/except) вместо глубоких if, f-strings, pathlib вместо строковых путей.
02

Files · JSON · CSV · Parquet · RegexФайлы · JSON · CSV · Parquet · Regex

CSV — use the module, not splitCSV — используй модуль, не split

import csv
with open("data.csv", newline="") as f:
    for row in csv.DictReader(f):   # dict per row
        print(row["ts"], row["value"])

with open("out.csv", "w", newline="") as f:
    w = csv.DictWriter(f, fieldnames=cols)
    w.writeheader(); w.writerows(records)
Never line.split(",") for real CSV — it breaks on quoted commas, embedded newlines, and escapes. Mention this; it shows you've handled messy real data.
Никогда не line.split(",") для настоящих CSV — ломается на закавыченных запятых, встроенных переносах строк и экранировании. Упоминай это — показывает, что ты работал с грязными реальными данными.

JSON & JSON-LinesJSON и JSON-Lines

import json
obj = json.loads(s)                 # str -> dict
s   = json.dumps(obj, default=str)  # dates etc.

# JSONL: one JSON object per line (streamable)
with open("events.jsonl") as f:
    for line in f:
        rec = json.loads(line)      # O(1) memory
        yield_event(rec)
"Semi-structured data" usually means nested JSON. Be ready to safely traverse with .get() chains and to flatten (see Problem 5). Know JSONL streams vs one giant JSON array (must fully load).
«Полуструктурированные данные» обычно значит вложенный JSON. Будь готов безопасно обходить через цепочки .get() и уплощать (см. Задачу 5). Знай разницу между JSONL-потоками и одним гигантским JSON-массивом (нужно грузить полностью).

Parquet (columnar)Parquet (колоночный)

import pyarrow.parquet as pq
table = pq.read_table("f.parquet",
    columns=["ts", "val"])      # projection pushdown

# row-group streaming for big files
pf = pq.ParquetFile("big.parquet")
for batch in pf.iter_batches(100_000):
    df = batch.to_pandas()
    process(df)
Parquet = columnar + compressed + typed + predicate/projection pushdown. Reading 2 of 50 columns reads ~2/50 of the bytes. Huge win for analytics vs CSV.
Parquet = колоночный + сжатый + типизированный + predicate/projection pushdown. Чтение 2 из 50 колонок читает ~2/50 байтов. Огромный выигрыш для аналитики против CSV.

Regex for unstructured textRegex для неструктурированного текста

import re
# Apache-ish log: ip - - [ts] "GET /x" 200 1234
pat = re.compile(
  r'(?P<ip>\S+).*\[(?P<ts>[^\]]+)\]'
  r'\s+"(?P<method>\S+)\s+(?P<path>\S+)'
  r'[^"]*"\s+(?P<code>\d+)\s+(?P<bytes>\d+)')
m = pat.search(line)
if m: rec = m.groupdict()
Compile once outside the loop. Use named groups for readability. Prefer non-greedy [^"]* over .*? where you can. Anchor when possible. Always handle the no-match case.
Компилируй один раз вне цикла. Используй именованные группы для читаемости. Предпочитай non-greedy [^"]* вместо .*?, где можно. Якори, где возможно. Всегда обрабатывай случай отсутствия совпадения.
03

pandas Essentials & Performancepandas: основы и производительность

groupby / merge / vectorizegroupby / merge / векторизация

import pandas as pd
g = (df.groupby("key")
       .agg(n=("val","size"),
            avg=("val","mean")))
m = a.merge(b, on="id", how="left",
            validate="1:1")        # catches dup keys
# vectorized, no python loop
df["z"] = (df.v - df.v.mean()) / df.v.std()
Avoid df.apply(..., axis=1) and iterrows() on big frames — they're row-by-row Python (~100x slower). Reach for vectorized column math, np.where, .map, or groupby.transform.
Избегай df.apply(..., axis=1) и iterrows() на больших датафреймах — это построчный Python (~в 100 раз медленнее). Используй векторизованную математику по колонкам, np.where, .map или groupby.transform.

Memory disciplineДисциплина по памяти

df = pd.read_csv("f.csv",
    usecols=["ts","sym","px"],
    dtype={"sym":"category","px":"float32"},
    parse_dates=["ts"])
# chunked aggregation of a file too big for RAM
acc = Counter()
for chunk in pd.read_csv("huge.csv",
        chunksize=500_000):
    acc.update(chunk.groupby("k").size()
                    .to_dict())
category dtype collapses repeated strings to ints. float32 halves numeric memory. usecols = column pruning at read time. Quote these for the "large dataset" question.
category dtype сжимает повторяющиеся строки в инты. float32 вдвое уменьшает память для чисел. usecols = отсечение колонок при чтении. Упоминай это для вопроса про «большие датасеты».

Time-series in pandasВременные ряды в pandas

df = df.set_index("ts").sort_index()
daily = df["val"].resample("1D").mean()  # bucket
filled = daily.asfreq("1D").ffill()        # gap-fill
roll = df["val"].rolling("7D").mean()      # running avg
ret  = df["px"].pct_change()              # returns
Time-series data: know resampling (resample), gap-filling (asfreq + ffill/interpolate), rolling windows, and alignment/joining of series on a date index.
Данные временных рядов: знай ресамплинг (resample), заполнение пробелов (asfreq + ffill/interpolate), скользящие окна и выравнивание/джойны серий по индексу дат.

pandas vs pure Python — when?pandas vs чистый Python — когда?

  • pandas: tabular analytics, joins, group aggregations, time-series math; fits in memory.
  • Pure Python / generators: streaming a file bigger than RAM, irregular parsing, single-pass aggregation, no heavy dep wanted.
  • DB / SQL: data already in a warehouse, or joins/aggregations that should be pushed down.
  • pandas: табличная аналитика, джойны, групповые агрегации, математика временных рядов; помещается в памяти.
  • Чистый Python / генераторы: поток файла больше RAM, нерегулярный парсинг, агрегация за один проход, не нужна тяжёлая зависимость.
  • БД / SQL: данные уже в хранилище или джойны/агрегации, которые должны быть pushed down.
In an interview, state your assumption about data size first ("if it fits in memory I'd use pandas; if not, I'd stream"). That single sentence reads as senior.
На интервью сначала формулируй предположение о размере данных («если влезает в память, использую pandas; если нет — стримлю»). Одно это предложение звучит по-сеньорски.
04

Robust, Testable, Typed CodeНадёжный, тестируемый, типизированный код

Error handling & retriesОбработка ошибок и ретраи

import time
def with_retry(fn, tries=3, base=0.5):
    for i in range(tries):
        try:
            return fn()
        except (TimeoutError, ConnectionError) as e:
            if i == tries - 1:
                raise
            time.sleep(base * 2**i)   # expo backoff
Catch specific exceptions, not bare except:. Only retry transient errors (network/timeout), never a ValueError from bad data. Add jitter in production. Always re-raise after exhausting tries.
Ловить конкретные исключения, а не голый except:. Ретраить только временные ошибки (сеть/таймауты), никогда не ValueError от плохих данных. Добавлять jitter в проде. Всегда re-raise после исчерпания попыток.

Pure functions + dependency injectionЧистые функции + dependency injection

# BAD: hidden I/O, hard to test
def total():
    return sum(r.v for r in db.query())

# GOOD: pure core, I/O at the edges
def total(rows):           # deterministic, testable
    return sum(r.v for r in rows)
total(db.query())             # inject dependency
Push side effects (DB, files, clock, network) to the boundary; keep the logic pure. Pure functions are trivially unit-testable and what DE interviewers mean by "craftsmanship".
Отталкивай побочные эффекты (БД, файлы, часы, сеть) на границу; держи логику чистой. Чистые функции тривиально юнит-тестируются — это то, что DE-интервьюеры понимают под «мастерством».

Typing & clean signaturesТипизация и чистые сигнатуры

from typing import Iterable, Iterator
from dataclasses import dataclass

@dataclass(frozen=True)
class Trade:
    ts: int; sym: str; px: float

def clean(rows: Iterable[dict]) -> Iterator[Trade]:
    for r in rows:
        yield Trade(int(r["ts"]), r["sym"],
                    float(r["px"]))
Type hints + a @dataclass show intent. Be ready to say you'd run mypy/pyright and write pytest cases incl. edge cases (empty, malformed, duplicates).
Type hints + @dataclass показывают намерение. Будь готов сказать, что прогонишь mypy/pyright и напишешь кейсы pytest, включая граничные случаи (пустой ввод, кривые данные, дубли).

Edge cases checklist (say these aloud)Чеклист граничных случаев (проговаривай вслух)

  • Empty input / single element.
  • Duplicates & ties (define tie-break explicitly).
  • Malformed / missing fields → skip vs raise (decide & state).
  • Nulls / NaN / empty strings vs zero.
  • Unsorted input when logic assumes order.
  • Timezones & mixed timestamp formats.
  • Very large input → does it fit in memory?
  • Пустой ввод / один элемент.
  • Дубликаты и равенства (определи tie-break явно).
  • Кривые / отсутствующие поля → пропустить или выбросить ошибку (реши и скажи).
  • Nulls / NaN / пустые строки против нуля.
  • Несортированный ввод, когда логика полагается на порядок.
  • Таймзоны и смешанные форматы меток времени.
  • Очень большой ввод → влезает ли в память?
05

Python Coding Problems (flip to reveal)Задачи по Python (нажми для раскрытия)

PROBLEM 1 · PARSE & AGGREGATE

Parse a messy CSV and aggregate revenue per regionРаспарсить грязный CSV и агрегировать выручку по регионам

Rows have inconsistent casing, stray whitespace, some missing amounts, quoted commas. Output total & count per region, sorted descending.

Строки с непостоянным регистром, лишними пробелами, пропущенными суммами, закавыченными запятыми. Вывести сумму и счётчик на регион, отсортированные по убыванию.

APPROACH · O(n) one pass
import csv
from collections import defaultdict

def revenue_by_region(path):
    agg = defaultdict(lambda: [0.0, 0])  # [sum, cnt]
    with open(path, newline="") as f:
        for row in csv.DictReader(f):
            region = (row.get("region") or "").strip().lower()
            raw = (row.get("amount") or "").strip()
            if not region or not raw:
                continue                  # skip bad rows
            try:
                amt = float(raw.replace(",", ""))
            except ValueError:
                continue
            agg[region][0] += amt
            agg[region][1] += 1
    return sorted(((r, s, c) for r,(s,c) in agg.items()),
                  key=lambda t: -t[1])

Normalize keys (strip+lower), use DictReader (handles quoting), decide skip-vs-raise on bad rows and say it. Single pass, O(n) time, O(#regions) memory.

Нормализуй ключи (strip+lower), используй DictReader (обрабатывает кавычки), реши «пропустить или выбросить ошибку» для плохих строк и скажи это. Один проход, O(n) время, O(#регионов) памяти.

PROBLEM 2 · DEDUP LATEST

Dedup records, keeping the latest by timestamp per idДедуплицировать записи, оставив последнюю по временной метке на id

Stream of {id, ts, payload}; for each id keep only the row with the max ts. Tie → keep last seen.

Поток {id, ts, payload}; для каждого id оставить только строку с макс ts. Если равны → оставить последнюю увиденную.

APPROACH · O(n) hash map
def dedup_latest(records):
    latest = {}                       # id -> record
    for r in records:
        cur = latest.get(r["id"])
        if cur is None or r["ts"] >= cur["ts"]:
            latest[r["id"]] = r   # >= => last wins on tie
    return list(latest.values())

Single pass, O(n) time, O(#ids) memory. State the tie-break explicitly (>= = keep later occurrence). This is exactly SQL's ROW_NUMBER() OVER (PARTITION BY id ORDER BY ts DESC)=1 — mention the parallel.

Один проход, O(n) время, O(#ids) памяти. Сформулируй tie-break явно (>= = оставить более позднюю). Это ровно SQL ROW_NUMBER() OVER (PARTITION BY id ORDER BY ts DESC)=1 — упомяни параллель.

If the stream is huge and unsorted, the hash map is optimal. If pre-sorted by (id, ts), you can dedup in O(1) extra memory with itertools.groupby.Если поток огромный и несортированный, хеш-мапа оптимальна. Если предотсортирован по (id, ts), можно дедуплицировать с O(1) доп. памятью через itertools.groupby.
PROBLEM 3 · TIME BUCKETS

Group time-series events into daily buckets (sum per day)Сгруппировать события временного ряда в дневные бакеты (сумма на день)

Events (epoch_seconds, value). Produce {date: total}, contiguous days, zero-filling gaps.

События (epoch_seconds, value). Выдать {дата: сумма}, смежные дни, заполнить нулями пробелы.

APPROACH · bucket + gap-fill
from datetime import datetime, timezone, timedelta
from collections import defaultdict

def daily_totals(events):
    buckets = defaultdict(float)
    for ts, v in events:
        d = datetime.fromtimestamp(ts, timezone.utc).date()
        buckets[d] += v
    if not buckets:
        return {}
    lo, hi = min(buckets), max(buckets)
    out, d = {}, lo
    while d <= hi:                 # fill missing days w/ 0
        out[d] = buckets.get(d, 0.0)
        d += timedelta(days=1)
    return out

Bucket via .date() (decide UTC vs local). Gap-fill so downstream sees a continuous series. O(n + days).

Бакетирование через .date() (реши UTC vs local). Заполни пробелы, чтобы downstream видел непрерывный ряд. O(n + дней).

"What about timezones / DST?" Answer: bucket in a fixed tz (UTC), convert at the edge; daily buckets avoid DST hour issues but month/quarter buckets need calendar-aware logic.
«А таймзоны / DST?» Ответ: бакетируй в фиксированной зоне (UTC), конвертируй на границе; дневные бакеты избегают проблем с часами DST, но месячные/квартальные бакеты требуют календарно-осведомлённой логики.
PROBLEM 4 · ANOMALY (Z-SCORE)

Detect anomalies in a series via z-scoreОбнаружить аномалии в ряду через z-score

Flag points more than k std-devs from the mean. Discuss robustness.

Отметить точки более чем на k стандартных отклонений от среднего. Обсудить робастность.

APPROACH · two pass, O(n)
import statistics as st

def anomalies(xs, k=3.0):
    if len(xs) < 2:
        return []
    mu = st.fmean(xs)
    sd = st.pstdev(xs)
    if sd == 0:
        return []                 # no spread, no outliers
    return [(i, x) for i, x in enumerate(xs)
            if abs((x - mu) / sd) > k]

Guard sd == 0 (divide-by-zero) and tiny n. Mean/std are not robust — a single huge outlier inflates std and masks others.

Защита от sd == 0 (деление на ноль) и крошечного n. Mean/std не робастны — один огромный выброс раздувает std и маскирует другие.

Better for real data: robust z-score using median & MAD (median absolute deviation), or a rolling window so the baseline tracks trend/seasonality. Mention this trade-off — it shows data maturity.
Лучше для реальных данных: робастный z-score через медиану и MAD (median absolute deviation) или скользящее окно, чтобы базовая линия отслеживала тренд/сезонность. Упомяни этот компромисс — показывает зрелость работы с данными.
PROBLEM 5 · FLATTEN NESTED JSON

Flatten arbitrarily nested JSON to dotted keysУплостить произвольно вложенный JSON в точечные ключи

Semi-structured input. {"a":{"b":1},"c":[10,20]}{"a.b":1,"c.0":10,"c.1":20}.

Полуструктурированный ввод. {"a":{"b":1},"c":[10,20]}{"a.b":1,"c.0":10,"c.1":20}.

APPROACH · recursion
def flatten(obj, prefix="", out=None):
    if out is None:
        out = {}
    if isinstance(obj, dict):
        for k, v in obj.items():
            flatten(v, f"{prefix}{k}.", out)
    elif isinstance(obj, list):
        for i, v in enumerate(obj):
            flatten(v, f"{prefix}{i}.", out)
    else:
        out[prefix[:-1]] = obj   # strip trailing dot
    return out

Handle dict, list, and scalar (leaf) cases. O(total nodes). Mutable-default trap avoided via out=None.

Обработка dict, list и скалярных (листьев) случаев. O(всего узлов). Ловушка mutable default избегается через out=None.

Follow-ups: deeply nested data risks recursion-depth limits → convert to an explicit stack. Decide how to handle empty dicts/lists and key collisions. Note the inverse (unflatten) is sometimes asked.
Продолжение: глубоко вложенные данные рискуют лимитом глубины рекурсии → переписать через явный стек. Реши, как обрабатывать пустые dict/list и коллизии ключей. Отметь, что инверсия (unflatten) иногда спрашивают.
PROBLEM 6 · MERGE INTERVALS

Merge overlapping date rangesСлить перекрывающиеся диапазоны дат

Given [(start, end), ...], merge overlapping/adjacent intervals into the minimal set.

Даны [(start, end), ...], слить перекрывающиеся/смежные интервалы в минимальное множество.

APPROACH · sort + sweep, O(n log n)
def merge_intervals(intervals):
    if not intervals:
        return []
    intervals = sorted(intervals)        # by start
    merged = [intervals[0]]
    for s, e in intervals[1:]:
        ls, le = merged[-1]
        if s <= le:                  # overlap/adjacent
            merged[-1] = (ls, max(le, e))
        else:
            merged.append((s, e))
    return merged

Sort by start, sweep once keeping the running end. O(n log n) dominated by sort, O(n) extra. This is the Python analogue of SQL gaps-and-islands.

Сортировка по началу, один проход с хранением текущего конца. O(n log n) доминирует сортировка, O(n) доп. Это питоновский аналог SQL gaps-and-islands.

Clarify whether touching endpoints ([1,5],[5,9]) count as overlapping — half-open [s,e) vs closed [s,e] changes the <= vs <. Ask before coding.
Уточни, считаются ли соприкасающиеся концы ([1,5],[5,9]) перекрывающимися — полуоткрытый [s,e) vs замкнутый [s,e] меняет <= на <. Спроси перед кодингом.
PROBLEM 7 · TOP-N PER KEY

Top-N items per group (e.g. top 3 trades per symbol)Top-N элементов на группу (напр. top 3 трейдов на символ)

Large stream of (symbol, value). Return the N largest values per symbol without sorting everything.

Большой поток (символ, значение). Вернуть N максимальных значений на символ без полной сортировки всего.

APPROACH · per-key min-heap, O(n log N)
import heapq
from collections import defaultdict

def top_n_per_key(rows, n=3):
    heaps = defaultdict(list)        # key -> min-heap
    for key, val in rows:
        h = heaps[key]
        if len(h) < n:
            heapq.heappush(h, val)
        elif val > h[0]:          # beats current min
            heapq.heapreplace(h, val)
    return {k: sorted(h, reverse=True)
            for k, h in heaps.items()}

A size-N min-heap per key keeps the top N in O(log N) per row. O(n log N) total, O(#keys × N) memory — far better than sorting each group fully (O(n log n)). Maps to SQL ROW_NUMBER() ... <= N.

Мин-хип размера N на ключ хранит топ N за O(log N) на строку. O(n log N) итого, O(#ключей × N) памяти — гораздо лучше полной сортировки каждой группы (O(n log n)). Соответствует SQL ROW_NUMBER() ... <= N.

Single global top-K instead? Use one heap of size K: heapq.nlargest(k, stream). O(n log k).
Один глобальный top-K вместо этого? Используй один хип размера K: heapq.nlargest(k, stream). O(n log k).
PROBLEM 8 · RUNNING AVERAGE / PIPELINE

Streaming rolling average (window of size w) + tiny pipelineПотоковое скользящее среднее (окно размера w) + мини-пайплайн

Lazily compute the moving average over a stream with constant memory; show how it composes into a clean pipeline.

Ленивое вычисление скользящего среднего по потоку с константной памятью; покажи, как это композируется в чистый пайплайн.

APPROACH · deque, O(1) per element
from collections import deque

def rolling_avg(stream, w):
    win = deque(maxlen=w)
    running = 0.0
    for x in stream:
        if len(win) == w:
            running -= win[0]     # evicted on append
        win.append(x)
        running += x
        yield running / len(win)

# compose: read -> clean -> smooth, all lazy
prices = (float(l) for l in read_lines("px.txt") if l)
for ma in rolling_avg(prices, 20):
    emit(ma)

Maintain a running sum + bounded deque → O(1) amortized per element, O(w) memory, fully streaming. Generator output composes with other lazy stages.

Поддержание running sum + ограниченной deque → O(1) амортизированно на элемент, O(w) памяти, полностью потоковая. Генераторный вывод композируется с другими ленивыми стадиями.

Don't recompute sum(win) each step (that's O(w) per element = O(nw)). Maintaining a running total is the difference between a correct-but-slow and a clean-and-fast answer. With floats over very long streams, periodic re-sum avoids drift.
Не пересчитывай sum(win) каждый шаг (это O(w) на элемент = O(nw)). Поддержание running total — разница между правильным-но-медленным и чистым-и-быстрым ответом. С float на очень длинных потоках периодический re-sum избегает дрифта.
06

SQL Core: Windows · CTEs · Joins · DedupSQL Основы: окна · CTE · джойны · дедуп

Window functionsОконные функции

SELECT
  user_id,
  event_ts,
  ROW_NUMBER() OVER (PARTITION BY user_id
                     ORDER BY event_ts DESC) AS rn,
  RANK()       OVER (ORDER BY amount DESC) AS rnk,
  LAG(amount)  OVER (PARTITION BY user_id
                     ORDER BY event_ts) AS prev_amt,
  SUM(amount)  OVER (PARTITION BY user_id
                     ORDER BY event_ts
                     ROWS BETWEEN UNBOUNDED PRECEDING
                          AND CURRENT ROW) AS running_total
FROM events;
ROW_NUMBER = no ties (1,2,3). RANK = ties share, gaps after (1,1,3). DENSE_RANK = ties share, no gaps (1,1,2). Pick deliberately — interviewers ask the difference constantly.
ROW_NUMBER = нет равенств (1,2,3). RANK = равные делят, с пробелами (1,1,3). DENSE_RANK = равные делят, без пробелов (1,1,2). Выбирай осознанно — интервьюеры постоянно спрашивают разницу.

Window frames (the subtle part)Оконные фреймы (тонкая часть)

-- default frame WITH ORDER BY is RANGE
-- UNBOUNDED PRECEDING .. CURRENT ROW
-- which makes SUM a running total (good)
-- but ties at same ORDER BY value collapse!
AVG(px) OVER (ORDER BY ts
              ROWS BETWEEN 6 PRECEDING
                   AND CURRENT ROW)  -- 7-row MA
Know ROWS vs RANGE. ROWS = physical N rows; RANGE = logical value window (peers with equal ORDER BY value treated together). Mismatched frames are a classic running-total bug.
Знай ROWS vs RANGE. ROWS = физические N строк; RANGE = логическое окно по значению (peer-ы с одинаковым ORDER BY обрабатываются вместе). Несовпадающие фреймы — классический баг running total.

Dedup with ROW_NUMBER (keep latest)Дедуп через ROW_NUMBER (оставить последние)

WITH ranked AS (
  SELECT *,
    ROW_NUMBER() OVER (
      PARTITION BY id
      ORDER BY updated_at DESC) AS rn
  FROM records)
SELECT * EXCEPT(rn)
FROM ranked
WHERE rn = 1;
The canonical dedup pattern (== Python Problem 2). Add a tiebreak column to ORDER BY for determinism (e.g. updated_at DESC, id DESC). Without it, "latest" is ambiguous on ties.
Канонический паттерн дедупликации (== Задача 2 по Python). Добавь tiebreak-колонку в ORDER BY для детерминизма (напр. updated_at DESC, id DESC). Без этого «последняя» неоднозначна при равенствах.

Joins: anti & semiДжойны: анти и полу

-- ANTI JOIN: rows in A with NO match in B
SELECT a.*
FROM a LEFT JOIN b ON a.id = b.id
WHERE b.id IS NULL;

-- SEMI JOIN: rows in A that DO match (no dup blowup)
SELECT a.*
FROM a
WHERE EXISTS (SELECT 1 FROM b WHERE b.id = a.id);
EXISTS/semi-join won't duplicate A rows when B has many matches, unlike a plain INNER JOIN. Also NOT IN is dangerous: if the subquery returns any NULL, it yields no rows. Prefer NOT EXISTS / anti-join.
EXISTS/semi-join не дублирует строки A, когда B имеет много совпадений, в отличие от обычного INNER JOIN. Также NOT IN опасен: если подзапрос вернёт хотя бы один NULL, не вернётся ни одной строки. Предпочитай NOT EXISTS / anti-join.

NULL logic — the trap basketNULL-логика — корзина ловушек

ExpressionResultNote
NULL = NULLNULL (not TRUE)Use IS NULL / IS NOT DISTINCT FROM
x NOT IN (1, NULL)never TRUESilent row loss — prefer NOT EXISTS
COUNT(col)ignores NULLsCOUNT(*) counts all rows
SUM/AVG over NULLsskips NULLsAVG denominator excludes them
aggregate of 0 rowsSUM→NULL, COUNT→0Wrap with COALESCE
ВыражениеРезультатЗаметка
NULL = NULLNULL (не TRUE)Используй IS NULL / IS NOT DISTINCT FROM
x NOT IN (1, NULL)никогда не TRUEТихая потеря строк — используй NOT EXISTS
COUNT(col)игнорирует NULLCOUNT(*) считает все строки
SUM/AVG по NULLпропускает NULLзнаменатель AVG их исключает
агрегация 0 строкSUM→NULL, COUNT→0Оборачивай в COALESCE
Expect a "why are rows missing / why is the total wrong" question rooted in NULL semantics or a fan-out join. Talk through 3-valued logic (TRUE/FALSE/UNKNOWN).
Жди вопроса «почему пропали строки / почему сумма неправильная», коренящегося в NULL-семантике или fan-out джойне. Проговаривай 3-значную логику (TRUE/FALSE/UNKNOWN).
07

SQL Time-Series & Advanced PatternsSQL временные ряды и продвинутые паттерны

Gaps-and-islandsПробелы-и-острова

-- group consecutive "active" days into islands
WITH flagged AS (
  SELECT user_id, d,
    ROW_NUMBER() OVER (PARTITION BY user_id
                       ORDER BY d) AS rn
  FROM active_days)
SELECT user_id,
       MIN(d) AS start_d,
       MAX(d) AS end_d,
       COUNT(*) AS streak
FROM flagged
GROUP BY user_id,
         d - CAST(rn AS integer)   -- date minus rownum is constant per island
Trick: for a consecutive run, date - ROW_NUMBER() is constant. Grouping by that difference isolates each "island". O(n log n) for the window sort.
Трюк: для последовательного ряда date - ROW_NUMBER() константа. Группировка по этой разнице изолирует каждый «остров». O(n log n) для оконной сортировки.

Sessionization (30-min gap = new session)Сессионизация (30-минутный пробел = новая сессия)

WITH gaps AS (
  SELECT *,
    CASE WHEN event_ts - LAG(event_ts) OVER (
           PARTITION BY user_id ORDER BY event_ts)
         > INTERVAL '30' MINUTE
         THEN 1 ELSE 0 END AS is_new
  FROM events),
sess AS (
  SELECT *,
    SUM(is_new) OVER (PARTITION BY user_id
        ORDER BY event_ts) AS session_id
  FROM gaps)
SELECT user_id, session_id,
       MIN(event_ts), MAX(event_ts), COUNT(*)
FROM sess GROUP BY user_id, session_id;
Pattern: LAG to find gaps → flag boundaries → SUM() OVER the flag = a cumulative session id. Same shape as Python rolling logic.
Паттерн: LAG для поиска пробелов → флаг границ → SUM() OVER по флагу = накопительный session id. Та же форма, что у питоновой скользящей логики.

Resampling / gap-filling a seriesРесамплинг / заполнение пробелов ряда

-- dense calendar LEFT JOINed to sparse data,
-- then carry last value forward (LOCF)
WITH cal AS (
  SELECT d FROM UNNEST(SEQUENCE(
     DATE '2024-01-01', DATE '2024-12-31',
     INTERVAL '1' DAY)) AS t(d)),
joined AS (
  SELECT cal.d, series.val
  FROM cal LEFT JOIN series ON cal.d = series.d)
SELECT d,
  COALESCE(val, LAST_VALUE(val) IGNORE NULLS OVER (
     ORDER BY d ROWS UNBOUNDED PRECEDING)) AS val
FROM joined;
Time-series: be ready to fill missing trading days/months. Two moves: (1) generate a dense calendar & LEFT JOIN; (2) forward-fill (LOCF) via LAST_VALUE ... IGNORE NULLS. Mention interpolation as the alternative to LOCF.
Временные ряды: будь готов заполнять пропущенные торговые дни/месяцы. Два хода: (1) сгенерировать плотный календарь и LEFT JOIN; (2) forward-fill (LOCF) через LAST_VALUE ... IGNORE NULLS. Упомяни интерполяцию как альтернативу LOCF.

Pivot & recursive CTEPivot и рекурсивный CTE

-- conditional aggregation = portable pivot
SELECT region,
  SUM(CASE WHEN yr=2023 THEN rev END) AS y2023,
  SUM(CASE WHEN yr=2024 THEN rev END) AS y2024
FROM sales GROUP BY region;

-- recursive CTE: generate a date spine / walk a tree
WITH RECURSIVE dates(d) AS (
  SELECT DATE '2024-01-01'
  UNION ALL
  SELECT d + INTERVAL '1' DAY FROM dates
  WHERE d < DATE '2024-12-31')
SELECT * FROM dates;
Conditional aggregation is the portable pivot (works everywhere). Recursive CTEs: date spines, org hierarchies, graph walks. Always include a terminating predicate.
Условная агрегация — портируемый pivot (работает везде). Рекурсивные CTE: календарные спайны, орг-иерархии, обход графов. Всегда включай предикат завершения.
08

SQL Query OptimizationОптимизация SQL-запросов

Predicate & projection pushdownPredicate и projection pushdown

  • Filter (WHERE) and select columns early so the engine reads less data.
  • On partitioned/Parquet tables, filter on the partition column to prune whole files (partition pruning).
  • Avoid SELECT * — projection pushdown only reads needed columns in columnar stores.
  • Фильтруй (WHERE) и выбирай колонки рано, чтобы движок читал меньше данных.
  • На партиционированных/Parquet таблицах фильтруй по колонке партиционирования, чтобы отсечь целые файлы (partition pruning).
  • Избегай SELECT * — projection pushdown читает только нужные колонки в колоночных хранилищах.

Join strategiesСтратегии джойнов

  • Broadcast (map-side): small table copied to every node. Great when one side is tiny.
  • Shuffle/hash: both sides repartitioned by key. Default for two large tables.
  • Sort-merge: good when inputs already sorted on the join key.
  • Broadcast (map-side): маленькая таблица скопирована на все ноды. Отлично, когда одна сторона крошечная.
  • Shuffle/hash: обе стороны перепартиционированы по ключу. Дефолт для двух больших таблиц.
  • Sort-merge: хорош, когда входы уже отсортированы по ключу джойна.

Data skewПерекос данных

  • One hot key (e.g. NULL or a default id) sends most rows to one reducer → straggler.
  • Fixes: filter/handle the hot key separately, salt the key (append a random bucket), pre-aggregate before the join.
  • Один горячий ключ (напр. NULL или дефолтный id) отправляет большинство строк одному reducer → straggler.
  • Лечение: фильтруй/обрабатывай горячий ключ отдельно, соли ключ (добавь случайный бакет), предагрегируй перед джойном.

General craftsmanshipОбщее мастерство

  • Aggregate before joining when possible (shrinks the join inputs).
  • Beware fan-out joins inflating SUM/COUNT — dedup or use semi-joins.
  • Functions on a column (WHERE DATE(ts)=...) can defeat partition pruning / indexes; filter on raw range instead.
  • Read the EXPLAIN plan; look for full scans, exploding shuffles, skew.
  • Агрегируй до джойна, где возможно (уменьшает входы джойна).
  • Остерегайся fan-out джойнов, раздувающих SUM/COUNT — дедуплицируй или используй semi-joins.
  • Функции на колонке (WHERE DATE(ts)=...) могут сломать partition pruning / индексы; фильтруй по сырому диапазону.
  • Читай план EXPLAIN; ищи full scans, взрывающиеся shuffle, перекосы.
"How would you make this query faster?" Lead with: reduce data scanned (pushdown + partition pruning), choose the right join (broadcast small side), and handle skew. Mention reading the query plan. That sequence covers 90% of follow-ups.
«Как бы ты ускорил этот запрос?» Веди с: уменьшить просканированные данные (pushdown + partition pruning), выбрать правильный джойн (broadcast малой стороны), обработать перекос. Упомяни чтение плана запроса. Эта последовательность покрывает 90% продолжений.
09

SQL Practice Problems (flip to reveal)Задачи по SQL (нажми для раскрытия)

SQL 1 · TOP-N PER GROUP

Top 3 highest-paid employees per departmentTop 3 самых высокооплачиваемых сотрудников на департамент

Return dept, employee, salary for the 3 highest salaries in each department; include ties as a discussion point.

Вернуть dept, сотрудник, зарплата для 3 наивысших зарплат в каждом департаменте; учесть равенства как точку обсуждения.

SOLUTION
WITH ranked AS (
  SELECT dept_id, emp_id, salary,
    DENSE_RANK() OVER (
      PARTITION BY dept_id
      ORDER BY salary DESC) AS dr
  FROM employees)
SELECT dept_id, emp_id, salary
FROM ranked
WHERE dr <= 3
ORDER BY dept_id, salary DESC;

DENSE_RANK includes salary ties at the cutoff (may return >3 rows). Use ROW_NUMBER for exactly 3 (arbitrary tie-break) — clarify which the interviewer wants. == Python Problem 7.

DENSE_RANK включает равные зарплаты на срезе (может вернуть >3 строк). Используй ROW_NUMBER для ровно 3 (произвольный tie-break) — уточни, что хочет интервьюер. == Задача 7 по Python.

SQL 2 · DEDUP + AUDIT

Keep latest row per id and count how many dupes were droppedОставить последнюю строку на id и посчитать сколько дублей выброшено

Source has multiple versions per id (by updated_at). Output the deduped set and a total duplicates-removed count.

Источник имеет несколько версий на id (по updated_at). Вывести дедуплицированный набор и общий счётчик удалённых дублей.

SOLUTION
WITH ranked AS (
  SELECT *,
    ROW_NUMBER() OVER (
      PARTITION BY id
      ORDER BY updated_at DESC, id DESC) AS rn
  FROM records)
SELECT
  (SELECT COUNT(*) FROM ranked WHERE rn > 1) AS dupes_removed;
-- and the deduped data:
SELECT * FROM ranked WHERE rn = 1;

Deterministic tie-break in ORDER BY. rn > 1 are the dupes. Mirrors Python Problem 2.

Детерминированный tie-break в ORDER BY. rn > 1 — дубли. Зеркало Задачи 2 по Python.

SQL 3 · TIME-SERIES · MoM

Month-over-month % growth of revenue, with gap-filled months% рост выручки месяц-к-месяцу с заполнением пропущенных месяцев

Some months have no sales. Produce every month in range, its revenue (0 if none), and MoM % change.

Некоторые месяцы без продаж. Вывести каждый месяц в диапазоне, его выручку (0, если нет), и MoM % изменение.

SOLUTION · calendar spine + LAG
WITH months AS (
  SELECT m FROM UNNEST(SEQUENCE(
    DATE '2024-01-01', DATE '2024-12-01',
    INTERVAL '1' MONTH)) AS t(m)),
rev AS (
  SELECT DATE_TRUNC('month', sale_dt) AS m,
         SUM(amount) AS revenue
  FROM sales GROUP BY 1),
joined AS (
  SELECT months.m,
         COALESCE(rev.revenue, 0) AS revenue
  FROM months LEFT JOIN rev ON months.m = rev.m)
SELECT m, revenue,
  LAG(revenue) OVER (ORDER BY m) AS prev,
  ROUND(100.0 * (revenue
        - LAG(revenue) OVER (ORDER BY m))
        / NULLIF(LAG(revenue) OVER (ORDER BY m), 0),
        2) AS mom_pct
FROM joined ORDER BY m;

Spine guarantees all months exist; COALESCE zero-fills; LAG gives the prior month; NULLIF(...,0) avoids divide-by-zero. First month's mom_pct is NULL by design.

Спайн гарантирует существование всех месяцев; COALESCE заполняет нулями; LAG даёт предыдущий месяц; NULLIF(...,0) избегает деления на ноль. mom_pct первого месяца NULL по дизайну.

SQL 4 · TIME-SERIES · ROLLING

7-day rolling average of daily active users7-дневное скользящее среднее дневных активных пользователей

Given daily counts, compute the trailing 7-day moving average per day.

Даны дневные счётчики, вычислить скользящее 7-дневное среднее на день.

SOLUTION · windowed frame
SELECT
  d,
  dau,
  AVG(dau) OVER (
    ORDER BY d
    ROWS BETWEEN 6 PRECEDING
         AND CURRENT ROW) AS ma7
FROM daily_active
ORDER BY d;

Use ROWS (physical 7 rows), not RANGE. If days can be missing, first build a date spine (SQL 3) so "7 rows" = "7 calendar days". Early rows average fewer than 7 — note that, or filter to full windows.

Используй ROWS (физические 7 строк), не RANGE. Если дни могут отсутствовать, сначала построй календарный спайн (SQL 3), чтобы «7 строк» = «7 календарных дней». Ранние строки усредняют меньше 7 — отметь это или фильтруй до полных окон.

== Python Problem 8 (deque rolling average). Window functions are the SQL way to do streaming aggregates.== Задача 8 по Python (deque скользящее среднее). Оконные функции — SQL-способ делать потоковые агрегации.
SQL 5 · GAPS-AND-ISLANDS

Longest streak of consecutive login days per userМаксимальный streak последовательных дней логина на пользователя

From a table of (user_id, login_date) (one row per active day), find each user's longest consecutive-day streak.

Из таблицы (user_id, login_date) (одна строка на активный день) найти максимальный последовательный streak на пользователя.

SOLUTION · date − rownum trick
WITH numbered AS (
  SELECT user_id, login_date,
    ROW_NUMBER() OVER (PARTITION BY user_id
                       ORDER BY login_date) AS rn
  FROM (SELECT DISTINCT user_id, login_date FROM logins)),
islands AS (
  SELECT user_id,
    COUNT(*) AS streak
  FROM numbered
  GROUP BY user_id,
           login_date - CAST(rn AS integer))
SELECT user_id, MAX(streak) AS longest_streak
FROM islands
GROUP BY user_id;

DISTINCT first (dupes break the trick). For consecutive dates, login_date − rn is constant within a run, so grouping on it forms islands; COUNT(*) = streak length.

DISTINCT сначала (дубли ломают трюк). Для последовательных дат login_date − rn константа внутри ряда, поэтому группировка по ней формирует острова; COUNT(*) = длина streak.

SQL 6 · ANTI-JOIN / FUNNEL

Users who viewed a product but never purchased itПользователи, просмотревшие продукт, но никогда не купившие его

Two event tables: views(user_id, product_id), purchases(user_id, product_id). Find view-without-purchase pairs.

Две таблицы событий: views(user_id, product_id), purchases(user_id, product_id). Найти пары просмотр-без-покупки.

SOLUTION · NOT EXISTS (safe anti-join)
SELECT DISTINCT views.user_id, views.product_id
FROM views
WHERE NOT EXISTS (
  SELECT 1
  FROM purchases
  WHERE purchases.user_id = views.user_id
    AND purchases.product_id = views.product_id);

Use NOT EXISTS, not NOT IN (NULL-safe). Equivalent: LEFT JOIN ... WHERE purchases.user_id IS NULL. DISTINCT because a user may view a product many times. == Python set-difference / anti-join thinking.

Используй NOT EXISTS, не NOT IN (NULL-безопасно). Эквивалент: LEFT JOIN ... WHERE purchases.user_id IS NULL. DISTINCT, потому что пользователь может просматривать продукт много раз. == питоновое set-разность / анти-джойн мышление.

Mention indexing/partitioning on (user_id, product_id) to make the anti-join efficient on large tables.Упомяни индексирование/партиционирование по (user_id, product_id), чтобы анти-джойн был эффективен на больших таблицах.
10

Interview Closers & Craftsmanship ChecklistФиналы интервью и чеклист мастерства

How to run the 60 minutesКак провести 60 минут

  1. Clarify input format, size, ordering, duplicates, edge cases — before coding.
  2. State approach + big-O out loud; get a nod.
  3. Code cleanly: good names, small pure functions, handle empties.
  4. Walk a small example to verify; mention test cases you'd add.
  5. Discuss scale: "if this didn't fit in memory I'd stream / push to SQL".
  1. Уточни формат ввода, размер, упорядочивание, дубликаты, граничные случаи — до кодирования.
  2. Сформулируй подход + big-O вслух; получи кивок.
  3. Кодируй чисто: хорошие имена, маленькие чистые функции, обрабатывай пустоты.
  4. Пройди малый пример для проверки; упомяни тест-кейсы, которые бы добавил.
  5. Обсуди масштаб: «если бы это не влезало в память, я бы стримил / пушил в SQL».

Python ↔ SQL equivalences (say these)Python ↔ SQL эквиваленции (проговаривай это)

TaskЗадачаPythonSQL
Dedup latestДедуп последнихdict by idROW_NUMBER=1
Top-N/keyper-key heapRANK ≤ N
FrequenciesЧастотыCounterGROUP BY COUNT
Rolling avgСкольз. среднееdeque + sumAVG OVER ROWS
Anti-joinset differenceNOT EXISTS
Merge runsСлияние рядовsort + sweepgaps-and-islands

Craftsmanship signals interviewers look forСигналы мастерства, которые ищут интервьюеры

  • Correctness first; edge cases handled, not hand-waved.
  • Readable: meaningful names, no clever one-liners that obscure.
  • Right data structure for the complexity you claim.
  • Pure functions / I/O at edges → testable.
  • You proactively name trade-offs and limitations.
  • You'd add types + unit tests in real code.
  • Корректность прежде всего; граничные случаи обработаны, не отмахнуты.
  • Читаемо: значимые имена, нет умных однострочников, затемняющих суть.
  • Правильная структура данных под заявленную сложность.
  • Чистые функции / I/O на границах → тестируемо.
  • Ты активно называешь компромиссы и ограничения.
  • Ты бы добавил типы + юнит-тесты в реальном коде.
Saying "here's the simple correct version, and here's how I'd optimize / what breaks at scale" is the senior signal that separates a 10-yr DE from a junior.
Сказать «вот простая корректная версия, а вот как бы я оптимизировал / что ломается на масштабе» — это сеньорский сигнал, отделяющий 10-летнего DE от джуна.