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}
len(), no indexing.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)
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
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.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 вместо строковых путей.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)
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)
.get() chains and to flatten (see Problem 5). Know JSONL streams vs one giant JSON array (must fully load)..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)
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()
[^"]* over .*? where you can. Anchor when possible. Always handle the no-match case.[^"]* вместо .*?, где можно. Якори, где возможно. Всегда обрабатывай случай отсутствия совпадения.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()
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
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.
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
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
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"]))
@dataclass show intent. Be ready to say you'd run mypy/pyright and write pytest cases incl. edge cases (empty, malformed, duplicates).@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 / пустые строки против нуля.
- Несортированный ввод, когда логика полагается на порядок.
- Таймзоны и смешанные форматы меток времени.
- Очень большой ввод → влезает ли в память?
Python Coding Problems (flip to reveal)Задачи по Python (нажми для раскрытия)
▼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.
Строки с непостоянным регистром, лишними пробелами, пропущенными суммами, закавыченными запятыми. Вывести сумму и счётчик на регион, отсортированные по убыванию.
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(#регионов) памяти.
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. Если равны → оставить последнюю увиденную.
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 — упомяни параллель.
itertools.groupby.Если поток огромный и несортированный, хеш-мапа оптимальна. Если предотсортирован по (id, ts), можно дедуплицировать с O(1) доп. памятью через itertools.groupby.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). Выдать {дата: сумма}, смежные дни, заполнить нулями пробелы.
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 + дней).
Detect anomalies in a series via z-scoreОбнаружить аномалии в ряду через z-score
Flag points more than k std-devs from the mean. Discuss robustness.
Отметить точки более чем на k стандартных отклонений от среднего. Обсудить робастность.
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 и маскирует другие.
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}.
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.
Merge overlapping date rangesСлить перекрывающиеся диапазоны дат
Given [(start, end), ...], merge overlapping/adjacent intervals into the minimal set.
Даны [(start, end), ...], слить перекрывающиеся/смежные интервалы в минимальное множество.
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.
[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] меняет <= на <. Спроси перед кодингом.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 максимальных значений на символ без полной сортировки всего.
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.
heapq.nlargest(k, stream). O(n log k).heapq.nlargest(k, stream). O(n log k).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.
Ленивое вычисление скользящего среднего по потоку с константной памятью; покажи, как это композируется в чистый пайплайн.
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) памяти, полностью потоковая. Генераторный вывод композируется с другими ленивыми стадиями.
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 избегает дрифта.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
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 = физические 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;
ORDER BY for determinism (e.g. updated_at DESC, id DESC). Without it, "latest" is ambiguous on ties.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-логика — корзина ловушек
| Expression | Result | Note |
|---|---|---|
NULL = NULL | NULL (not TRUE) | Use IS NULL / IS NOT DISTINCT FROM |
x NOT IN (1, NULL) | never TRUE | Silent row loss — prefer NOT EXISTS |
COUNT(col) | ignores NULLs | COUNT(*) counts all rows |
SUM/AVG over NULLs | skips NULLs | AVG denominator excludes them |
| aggregate of 0 rows | SUM→NULL, COUNT→0 | Wrap with COALESCE |
| Выражение | Результат | Заметка |
|---|---|---|
NULL = NULL | NULL (не TRUE) | Используй IS NULL / IS NOT DISTINCT FROM |
x NOT IN (1, NULL) | никогда не TRUE | Тихая потеря строк — используй NOT EXISTS |
COUNT(col) | игнорирует NULL | COUNT(*) считает все строки |
SUM/AVG по NULL | пропускает NULL | знаменатель AVG их исключает |
| агрегация 0 строк | SUM→NULL, COUNT→0 | Оборачивай в COALESCE |
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
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;
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;
LAST_VALUE ... IGNORE NULLS. Mention interpolation as the alternative to 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;
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.
NULLor 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
EXPLAINplan; look for full scans, exploding shuffles, skew.
- Агрегируй до джойна, где возможно (уменьшает входы джойна).
- Остерегайся fan-out джойнов, раздувающих
SUM/COUNT— дедуплицируй или используй semi-joins. - Функции на колонке (
WHERE DATE(ts)=...) могут сломать partition pruning / индексы; фильтруй по сырому диапазону. - Читай план
EXPLAIN; ищи full scans, взрывающиеся shuffle, перекосы.
SQL Practice Problems (flip to reveal)Задачи по SQL (нажми для раскрытия)
▼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 наивысших зарплат в каждом департаменте; учесть равенства как точку обсуждения.
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.
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). Вывести дедуплицированный набор и общий счётчик удалённых дублей.
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.
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 % изменение.
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 по дизайну.
7-day rolling average of daily active users7-дневное скользящее среднее дневных активных пользователей
Given daily counts, compute the trailing 7-day moving average per day.
Даны дневные счётчики, вычислить скользящее 7-дневное среднее на день.
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 — отметь это или фильтруй до полных окон.
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 на пользователя.
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.
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). Найти пары просмотр-без-покупки.
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-разность / анти-джойн мышление.
(user_id, product_id) to make the anti-join efficient on large tables.Упомяни индексирование/партиционирование по (user_id, product_id), чтобы анти-джойн был эффективен на больших таблицах.Interview Closers & Craftsmanship ChecklistФиналы интервью и чеклист мастерства
▼How to run the 60 minutesКак провести 60 минут
- Clarify input format, size, ordering, duplicates, edge cases — before coding.
- State approach + big-O out loud; get a nod.
- Code cleanly: good names, small pure functions, handle empties.
- Walk a small example to verify; mention test cases you'd add.
- Discuss scale: "if this didn't fit in memory I'd stream / push to SQL".
- Уточни формат ввода, размер, упорядочивание, дубликаты, граничные случаи — до кодирования.
- Сформулируй подход + big-O вслух; получи кивок.
- Кодируй чисто: хорошие имена, маленькие чистые функции, обрабатывай пустоты.
- Пройди малый пример для проверки; упомяни тест-кейсы, которые бы добавил.
- Обсуди масштаб: «если бы это не влезало в память, я бы стримил / пушил в SQL».
Python ↔ SQL equivalences (say these)Python ↔ SQL эквиваленции (проговаривай это)
| TaskЗадача | Python | SQL |
|---|---|---|
| Dedup latestДедуп последних | dict by id | ROW_NUMBER=1 |
| Top-N/key | per-key heap | RANK ≤ N |
| FrequenciesЧастоты | Counter | GROUP BY COUNT |
| Rolling avgСкольз. среднее | deque + sum | AVG OVER ROWS |
| Anti-join | set difference | NOT EXISTS |
| Merge runsСлияние рядов | sort + sweep | gaps-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 на границах → тестируемо.
- Ты активно называешь компромиссы и ограничения.
- Ты бы добавил типы + юнит-тесты в реальном коде.