Skip to content

TwentyBelow27/Skills

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 

Repository files navigation

QUESTIONS

Вопросы по моим основным навыкам.



Theory

Image

  1. OLTP и OLAP-системы

  2. ACID и транзакции

  3. Виды изоляции в транзакциях

  4. Что такое DWH по Инмону и Кимбалу?

  5. Какие бывают слои данных?

  6. Озеро данных (Data Lake)

  7. Что такое нормализация данных? (3 нормальная форма)

Что такое и все НФ до 3-й Скрытый текст или Markdown-контент.
3+НФ, 4НФ, 5НФ, 6НФ (хуетень) Скрытый текст или Markdown-контент.
  1. Какие бывают модели данных?
Звездочка и снежинка Скрытый текст или Markdown-контент.
Data Vault Скрытый текст или Markdown-контент.
Витрина данных Скрытый текст или Markdown-контент.
  1. Факты, измерения и ключи.

  2. Потоковая и пакетная обработка данных. Kappa и Lambda-архитектура.

  3. SCD (Slowly Changing Dimentions)

Python

Image

  1. Переменные и копирование в Python.
Ответ В Python переменные - это имена, которые ссылаются на объекты в памяти. Важно понимать, что переменные не содержат значения напрямую, а являются ссылками на объекты.
a = 5  # переменная a ссылается на объект 5
b = a  # теперь b тоже ссылается на тот же объект 5
  1. Присваивание (не создает копию)

При простом присваивании создается новая ссылка на тот же объект:

list1 = [1, 2, 3]
list2 = list1  # обе переменные ссылаются на один и тот же список

list1[0] = 100
print(list2)  # [100, 2, 3] - изменение отразилось в list2
  1. Поверхностное копирование (shallow copy)

Создает новый объект, но копирует только ссылки на вложенные объекты:

import copy

list1 = [1, 2, [3, 4]]
list2 = copy.copy(list1)  # или list1.copy() или list1[:]

list1[0] = 100
print(list2)  # [1, 2, [3, 4]] - первый уровень изменился только в list1

list1[2][0] = 300
print(list2)  # [1, 2, [300, 4]] - вложенный список изменился в обоих!
  1. Глубокое копирование (deep copy)

Создает полностью независимую копию со всеми вложенными объектами:

import copy

list1 = [1, 2, [3, 4]]
list2 = copy.deepcopy(list1)

list1[2][0] = 300
print(list1)  # [1, 2, [300, 4]]
print(list2)  # [1, 2, [3, 4]] - list2 не изменился

Особенности для разных типов данных

Неизменяемые типы (int, float, str, tuple): при изменении создается новый объект

Изменяемые типы (list, dict, set): изменения происходят в том же объекте

a = 5
b = a
a = 10
print(b)  # 5 - int неизменяем, создался новый объект

lst1 = [1, 2]
lst2 = lst1
lst1.append(3)
print(lst2)  # [1, 2, 3] - список изменяем, обе ссылки указывают на измененный объект

Выбор типа копирования зависит от ваших потребностей и структуры данных, с которыми вы работаете.

  1. Списковое (List Comprehension), словарное и множественное включение
Ответ Включения (comprehensions) - это компактный способ создания коллекций (списков, словарей, множеств) на основе итерируемых объектов.
  1. Списковое включение (List Comprehension)

Синтаксис: [expression for item in iterable if condition]

Примеры:

# Создание списка квадратов чисел
squares = [x**2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# Фильтрация четных чисел
evens = [x for x in range(20) if x % 2 == 0]
# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# Вложенные циклы
pairs = [(x, y) for x in [1, 2, 3] for y in [3, 1, 4] if x != y]
# [(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]
  1. Словарное включение (Dict Comprehension)

Синтаксис: {key_expr: value_expr for item in iterable if condition}

Примеры:

# Создание словаря квадратов
square_dict = {x: x**2 for x in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# Инвертирование словаря
original = {'a': 1, 'b': 2, 'c': 3}
inverted = {v: k for k, v in original.items()}
# {1: 'a', 2: 'b', 3: 'c'}

# Фильтрация элементов
filtered = {k: v for k, v in original.items() if v > 1}
# {'b': 2, 'c': 3}
  1. Множественное включение (Set Comprehension)

Синтаксис: {expression for item in iterable if condition}

Примеры:

# Создание множества уникальных символов в строке
unique_chars = {ch for ch in 'abracadabra'}
# {'a', 'b', 'c', 'd', 'r'}

# Множество длин слов
words = ['apple', 'banana', 'cherry']
lengths = {len(word) for word in words}
# {5, 6}

# Фильтрация чисел
nums = {x for x in range(20) if x % 3 == 0}
# {0, 3, 6, 9, 12, 15, 18}

Сравнение с генераторами

Включения можно преобразовать в генераторы, заменив квадратные/фигурные скобки на круглые:

# Генератор списка (ленивое вычисление)
squares_gen = (x**2 for x in range(10))

# Генератор словаря (Python 3.8+)
dict_gen = ((x, x**2) for x in range(5))

Вложенные включения

# Создание матрицы
matrix = [[i*j for j in range(1, 4)] for i in range(1, 4)]
# [[1, 2, 3], [2, 4, 6], [3, 6, 9]]

# Плоское вложение списка
flat = [num for row in matrix for num in row]
# [1, 2, 3, 2, 4, 6, 3, 6, 9]

Преимущества включений:

Лаконичность - код становится короче и читаемее

Производительность - обычно работают быстрее эквивалентных циклов

Выразительность - ясно выражают намерение программиста.

  1. Генераторы и итераторы в Python.
Ответ

Концепция итерации в Python

Итерируемые объекты (Iterables)

Итерируемый объект - это любой объект, который может возвращать свои элементы по одному. Основные характеристики:

Реализуют метод __iter__(), который возвращает итератор

Могут использоваться в циклах for и других контекстах, ожидающих последовательности

Примеры: списки, строки, словари, множества, файлы

Итераторы (Iterators)

Итератор - это объект, который управляет итерацией по итерируемому объекту. Особенности:

Реализуют два метода: __iter__() и __next__()

__iter__() возвращает сам итератор (позволяет использовать итератор везде, где принимается итерируемый объект)

__next__() возвращает следующий элемент или вызывает StopIteration

Сохраняют состояние итерации (знают, какой элемент будет следующим)

Генераторы: особый вид итераторов

Определение генераторов:

Генераторы - это синтаксически удобный способ создания итераторов. Они бывают двух видов:

Функции-генераторы (содержат yield)

Выражения-генераторы (аналогичны списковым включениям)

Ключевые свойства генераторов:

  • Ленивые вычисления - значения вычисляются по требованию, а не все сразу

  • Экономия памяти - не хранят всю последовательность в памяти

  • Одноразовость - после полного прохода нельзя итерировать снова

  • Состояние - сохраняют точку выполнения между вызовами

Принцип работы yield

Ключевое слово yield:

  • Приостанавливает выполнение функции

  • Сохраняет локальное состояние

  • Возвращает значение вызывающему коду

  • При следующем вызове возобновляет выполнение с точки остановки

Сравнительная характеристика

Итераторы:

  • Более явная реализация протокола итерации

  • Позволяют более сложную логику управления итерацией

  • Могут реализовывать дополнительную функциональность

  • Часто используются для обертки существующих структур данных

Генераторы:

  • Более лаконичный синтаксис

  • Автоматическое управление состоянием

  • Идеальны для ленивых вычислений

  • Особенно полезны для работы с потоками данных и pipelines

Теоретические аспекты применения

Паттерны использования:

  • Ленивая загрузка - обработка данных по мере необходимости

  • Бесконечные последовательности - генерация без ограничений по размеру

  • Конвейеры обработки данных - цепочки генераторов для поэтапной обработки

Преимущества перед обычными коллекциями:

  • Эффективность памяти - не требуют хранения всей последовательности

  • Возможность работы с бесконечными потоками

  • Более чистое разделение ответственности между производством и потреблением данных

  • Поддержка конкурентного выполнения (в сочетании с asyncio)

Внутренняя механика

Протокол итератора:

  • Вызов iter() для получения итератора

  • Многократные вызовы next() для получения элементов

  • Обработка StopIteration для определения конца последовательности

Жизненный цикл генератора:

  • Создание при вызове функции-генератора

  • Запуск при первом вызове next()

  • Приостановка на каждом yield

  • Завершение при достижении return или конца функции

  • Невозможность повторного использования

Заключение

Генераторы и итераторы представляют собой фундаментальную концепцию Python для работы с последовательностями и потоками данных. Их понимание позволяет:

  1. Эффективно работать с большими объемами данных

  2. Создавать более выразительный и поддерживаемый код

  3. Реализовывать сложные паттерны обработки данных

  4. Оптимизировать использование памяти и процессорного времени

Эти механизмы лежат в основе многих аспектов Python, включая асинхронное программирование и обработку потоков данных, что делает их понимание критически важным для продвинутой работы с языком.

  1. Что такое декораторы? Как работают?
Ответ

Декораторы в Python — простыми словами

Декоратор — это специальная функция, которая "оборачивает" другую функцию, чтобы добавить ей новое поведение без изменения её исходного кода.

🔹 Как работают декораторы?

Декоратор принимает функцию, которую нужно улучшить.

Добавляет новый функционал (например, замер времени, логирование, проверку прав доступа).

Возвращает модифицированную функцию.

🔹 Простой пример

Допустим, у нас есть функция greet(), которая выводит приветствие:

def greet():
    print("Привет, мир!")

Мы хотим добавить логгирование перед её вызовом. Вместо того чтобы изменять greet(), создадим декоратор:

def log_decorator(func):  # принимает функцию
    def wrapper():        # обёртка
        print("Функция вызвана")  # новый функционал
        func()            # вызываем исходную функцию
    return wrapper        # возвращаем обёрнутую функцию

# Применяем декоратор к greet
greet = log_decorator(greet)

# Теперь при вызове greet() сначала пишется лог
greet()

🔹 Синтаксис с @ (сахар для декораторов)

Вместо greet = log_decorator(greet), можно написать так:

@log_decorator
def greet():
    print("Привет, мир!")

greet()  # Выведет: "Функция вызвана" → "Привет, мир!"

🔹 Зачем нужны декораторы?

  1. Логирование (запись действий функции).

  2. Замер времени выполнения.

  3. Проверка прав доступа (например, @login_required в Django/Flask).

  4. Кэширование (сохранение результатов функции, чтобы не считать заново).

🔹 Пример: декоратор для замера времени

import time

def timer_decorator(func):
    def wrapper():
        start_time = time.time()
        func()
        print(f"Функция выполнилась за {time.time() - start_time:.2f} сек.")
    return wrapper

@timer_decorator
def long_operation():
    time.sleep(2)

long_operation()  # Выведет время выполнения

Вывод

Декораторы — это мощный инструмент Python, позволяющий расширять функции, не меняя их код. Они делают программу чище и удобнее для масштабирования.

  1. Какие типы данных являются изменяемыми, а какие нет?
Ответ В Python все типы данных делятся на изменяемые (mutable) и неизменяемые (immutable). От этого зависит, можно ли изменить объект после создания или нет.
  1. Неизменяемые (immutable) типы

Эти объекты нельзя изменить после создания. При "изменении" создаётся новый объект.

Числа

  • int (целые)

  • float (вещественные)

  • bool (логические)

  • complex (комплексные)

x = 10
x += 5  # Создаётся новый объект 15, старый 10 остаётся в памяти

Строки (str)

s = "hello"
s[0] = "H"  # Ошибка! Нельзя изменить символ
s = "Hello" # Но можно пересоздать строку

Кортежи (tuple)

t = (1, 2, 3)
t[0] = 10  # Ошибка! Кортеж неизменяем
  1. Изменяемые (mutable) типы

Эти объекты можно изменять после создания.

Списки (list)

lst = [1, 2, 3]
lst[0] = 10  # OK, список изменён

Словари (dict)

d = {"a": 1, "b": 2}
d["a"] = 100  # OK, значение изменено

Множества (set)

s = {1, 2, 3}
s.add(4)  # OK, множество изменено

🔹 Почему это важно?

  1. Присваивание и копирование

Для изменяемых объектов = создаёт ссылку, а не копию.

Если изменить один объект, изменится и другой.

a = [1, 2, 3]
b = a  # b и a ссылаются на один список
b[0] = 10
print(a)  # [10, 2, 3] – изменился и a!
  1. Аргументы функций

Изменяемые объекты могут быть изменены внутри функции, что повлияет на оригинал.

def change_list(lst):
lst.append(4)

my_list = [1, 2, 3]
change_list(my_list)
print(my_list)  # [1, 2, 3, 4] – изменился!
  1. Ключи словарей

Только неизменяемые типы (int, str, tuple) могут быть ключами.

Изменяемые (list, dict, set) — нельзя.

d = {[1, 2]: "value"}  # TypeError: unhashable type: 'list'

🔹 Как проверить изменяемость?

Попробуйте изменить объект. Если ошибки нет — он изменяемый. Или используйте hash() (неизменяемые объекты имеют хеш):

print(hash("abc"))  # Работает (str — immutable)
print(hash([1, 2])) # Ошибка (list — mutable)

Итог

Знание этого помогает избегать ошибок, особенно при работе с функциями и копированием объектов.

  1. Можно ли в словарь в key записать изменяемый тип? Почему?
Ответ

❌ Нет, нельзя.

Почему это запрещено?

Ключи словаря должны быть хешируемыми (hashable), а изменяемые типы (list, dict, set) — нехешируемые.

Что значит «хешируемый»?

  • Объект имеет уникальный хеш (целое число), который не меняется в течение его жизни
  • Python использует хеш для быстрого поиска значений в словаре
  • Если объект изменяется, его хеш тоже изменится → это нарушит работу словаря

Пример ошибки

my_dict = {}
key = [1, 2, 3]  # list — изменяемый
my_dict[key] = "value"  # Вызовет ошибку:
# TypeError: unhashable type: 'list'  
  1. Как работает память в Python?
Ответ

Python память управляется автоматически, и вам обычно не нужно заботиться о её освобождении. Но вот как это работает:

1️. Объекты и ссылки

Всё в Python — это объекты (числа, строки, списки и т. д.).

Когда вы создаёте переменную, она ссылается на объект в памяти.

a = 10      # Создаётся объект `10`, `a` ссылается на него  
b = a       # `b` тоже ссылается на тот же объект  

2️. Cчётчик ссылок

Python следит, сколько переменных ссылаются на объект.

Когда ссылок не остаётся (a = None, переменная удаляется и т. д.), память освобождается.

a = [1, 2, 3]  # У списка 1 ссылка (`a`)  
b = a          # Теперь 2 ссылки (`a` и `b`)  
a = None       # Осталась 1 ссылка (`b`)  
b = None       # Ссылок нет → Python удалит список  

3️. Сборщик мусора (Garbage Collector, GC)

Иногда объекты ссылаются друг на друга, но уже не нужны (циклические ссылки):

x = [1, 2]  
y = [3, 4]  
x.append(y)  # x ссылается на y  
y.append(x)  # y ссылается на x → цикл  

Здесь счётчик ссылок не поможет, поэтому сборщик мусора находит и удаляет такие "зависшие" объекты. 4️. Куча (Heap) и стек (Stack)

Куча — здесь хранятся все объекты (списки, словари и т. д.).

Стек — здесь хранятся ссылки на объекты (например, переменные в функциях).

5️. Изменяемые vs неизменяемые объекты

Неизменяемые (int, str, tuple) — при изменении создаётся новый объект.

Изменяемые (list, dict, set) — можно менять без создания нового объекта.

a = "hello"  
a += " world"  # Создаётся НОВАЯ строка  

b = [1, 2]  
b.append(3)    # Тот же список, но изменённый  

💡 Вывод

Python сам управляет памятью через счётчик ссылок и сборщик мусора.

Вам обычно не нужно думать об освобождении памяти.

Но если программа работает с огромными данными, можно вручную удалять ненужные объекты (del x) или вызывать gc.collect().

Пример:

import gc  

big_data = [i for i in range(1_000_000)]  # Большой список  
# ... работаем с big_data ...  
del big_data  # Удаляем ссылку → память освободится  
gc.collect()  # Принудительная очистка (редко нужно)  

Теперь вы знаете, как Python заботится о памяти за вас!

  1. Как в Python работает сборщик мусора?
Ответ

В Python используется комбинированная система управления памятью, состоящая из:

  • Подсчёта ссылок (основной механизм)

  • Сборщика мусора (для циклических ссылок)

  1. Основной механизм: подсчёт ссылок

Каждый объект в Python имеет счётчик ссылок (refcount), который увеличивается когда:

  • Объект присваивается переменной

  • Объект добавляется в контейнер (список, словарь и т.д.)

  • Объект передаётся в функцию

Когда счётчик достигает нуля, память немедленно освобождается.

a = [1, 2, 3]  # refcount = 1
b = a          # refcount = 2
del a          # refcount = 1
b = None       # refcount = 0 → память освобождена
  1. Сборщик мусора для циклических ссылок

Проблема: когда объекты ссылаются друг на друга, но недоступны из программы.

class Node:
    def __init__(self):
        self.parent = None
        self.children = []

# Создаём циклическую ссылку
parent = Node()
child = Node()
parent.children.append(child)
child.parent = parent

# Удаляем ссылки
parent = None
child = None
# Объекты остались в памяти!

Как работает сборщик мусора:

  • Поколения объектов (Generational GC):

  • Все объекты делятся на 3 поколения (0, 1, 2)

  • Новые объекты попадают в поколение 0

  • Выжившие после сборки переходят в следующее поколение

Пороги срабатывания:

  • Для каждого поколения есть порог (threshold)

  • При превышении порога запускается сборка мусора

Алгоритм работы:

  • Находит все достижимые объекты (корни - глобальные переменные, стек вызовов)

  • Помечает все объекты, до которых можно дойти от корней

  • Удаляет всё недостижимое

  1. Управление сборщиком мусора

Модуль gc позволяет управлять процессом:

import gc

# Получить текущие настройки
print(gc.get_threshold())  # (700, 10, 10) - пороги для поколений 0, 1, 2

# Запустить сборку вручную
gc.collect()  # возвращает количество собранных объектов

# Включить/выключить сборщик
gc.disable()
gc.enable()

# Получить список всех объектов в памяти
for obj in gc.get_objects():
    print(type(obj), id(obj))
  1. Когда сборщик мусора не поможет
  • Ресурсы не-Python (файлы, сокеты) - нужно использовать контекстные менеджеры (with)

  • Кэширование - может удерживать объекты в памяти

  • Глобальные переменные - живут до конца программы

Практические советы

  • Для больших структур данных используйте del когда они больше не нужны

  • Избегайте ненужных глобальных переменных

  • Для ресурсов всегда используйте with

  • Для сложных случаев можно временно отключать GC (gc.disable())

Сборщик мусора делает Python удобным языком, но понимание его работы помогает писать более эффективный код.

  1. ООП в Python.
4 принципа ООП

Четыре столпа объектно-ориентированного программирования (ООП) в Python

ООП основано на четырёх ключевых принципах, которые делают код более структурированным, гибким и удобным для поддержки:

  1. Инкапсуляция (Encapsulation)

Смысл: Скрытие внутренней реализации объекта и защита данных от неправильного использования. Как работает в Python:

  • Атрибуты и методы объединяются в одном классе.

  • Доступ к внутренним данным контролируется через интерфейс (методы).

  • Используются "приватные" атрибуты (через _ или __), но Python не запрещает доступ жёстко — это соглашение.

class BankAccount:
    def __init__(self):
        self.__balance = 0  # Приватный атрибут (на самом деле _BankAccount__balance)

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount

    def get_balance(self):
        return self.__balance
        
account = BankAccount()
account.deposit(100)
print(account.get_balance())  # 100
# print(account.__balance)  # Ошибка! Доступ запрещён (но можно через account._BankAccount__balance)
  1. Наследование (Inheritance)

Смысл: Создание новых классов на основе существующих (родительских). Позволяет переиспользовать код и расширять функциональность. Как работает в Python:

  • Дочерний класс наследует все атрибуты и методы родителя.

  • Можно переопределять методы родителя или добавлять новые.

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "Звук животного"
        
class Dog(Animal):  # Наследование
    def speak(self):  # Переопределение метода
        return "Гав!"

dog = Dog("Бобик")
print(dog.name)     # Бобик (унаследовано от Animal)
print(dog.speak())  # Гав! (переопределённый метод)
  1. Полиморфизм (Polymorphism)

Смысл: Возможность использовать объекты разных классов через единый интерфейс. Как работает в Python:

  • Разные классы могут иметь методы с одинаковыми именами, но разной реализацией.

  • Python поддерживает "утиную типизацию" (если объект имеет нужный метод, он подходит).

class Cat:
    def speak(self):
        return "Мяу!"

class Robot:
    def speak(self):
        return "Бип-бип!"

def make_sound(obj):
    print(obj.speak())  # Не важно, какой класс, главное — метод speak()

make_sound(Cat())    # Мяу!
make_sound(Robot())  # Бип-бип!
  1. Абстракция (Abstraction)

Смысл: Сокрытие сложной реализации и предоставление простого интерфейса. Как работает в Python:

  • Абстрактные классы (ABC) определяют "шаблон" для других классов.

  • Они могут содержать абстрактные методы (без реализации), которые должны быть переопределены в дочерних классах.

from abc import ABC, abstractmethod

class Shape(ABC):  # Абстрактный класс
    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass
        
class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side ** 2

    def perimeter(self):
        return 4 * self.side

# shape = Shape()  # Ошибка! Нельзя создать экземпляр абстрактного класса
square = Square(5)
print(square.area())  # 25

Итог: Инкапсуляция - Скрытие данных и защита от неправильного использования Банкомат: вы не видите, как он считает деньги, но можете снять их через интерфейс.

Наследование - переиспользование и расширение кода Класс "Электромобиль" наследует от "Автомобиль".

Полиморфизм - единый интерфейс для разных классов Кнопка "Сохранить" работает по-разному в Word и Photoshop.

Абстракция - упрощение сложной реальности через выделение главного Вы знаете, как пользоваться ручкой, но не обязательно разбираться в её устройстве.

Эти принципы помогают писать код, который легче:

  • понимать (чёткая структура),

  • расширять (новые функции добавляются без проблем),

  • тестировать (изолированные компоненты),

  • исправлять (меньше побочных эффектов).

Python реализует эти принципы гибко, без излишней строгости (как в Java или C++), что делает его удобным для ООП.

Классовые и статические методы
  1. Обычные методы (instance methods)
  • Работают с конкретным объектом (экземпляром класса)

  • Первый аргумент всегда self (ссылка на объект)

  • Используются для работы с данными объекта

Пример:

class Dog:
    def __init__(self, name):
        self.name = name
    
    def bark(self):  # обычный метод
        print(f"{self.name} лает")
        
dog = Dog("Шарик")
dog.bark()  # Шарик лает
  1. Классовые методы (@classmethod)
  • Работают с самим классом, а не с объектами

  • Первый аргумент всегда cls (ссылка на класс)

Используются:

  • для создания альтернативных конструкторов

  • когда нужно работать с данными класса (не объекта)

Пример:

class Car:
    total_cars = 0  # данные класса
    
    def __init__(self, model):
        self.model = model
        Car.total_cars += 1
    
    @classmethod
    def count_cars(cls):  # работает с классом
        print(f"Всего машин: {cls.total_cars}")
        
Car.count_cars()  # Всего машин: 0
car1 = Car("Toyota")
Car.count_cars()  # Всего машин: 1
  1. Статические методы (@staticmethod)
  • Не получают ни self, ни cls

  • Это обычные функции, просто объявленные внутри класса

  • Используются для логической группировки кода

Пример:

class MathHelper:
    @staticmethod
    def add(a, b):  # не зависит от класса или объекта
        return a + b

print(MathHelper.add(2, 3))  # 5

Когда что использовать:

  • Обычный метод — когда нужен доступ к данным объекта

  • Классовый метод — когда нужно работать с классом или создать объект особым способом

  • Статический метод — когда функция логически относится к классу, но не требует доступа ни к классу, ни к объекту

Главное отличие:

class MyClass:
    def normal_method(self):  # нужен объект
        pass
    
    @classmethod
    def class_method(cls):  # нужен класс
        pass
    
    @staticmethod
    def static_method():  # ничего не нужно
        pass

Статические методы — это просто функции внутри класса, а классовые методы работают с самим классом.

Композиция и агрегация В чем суть?

Композиция и агрегация — это два способа создания сложных объектов из более простых. Они позволяют строить отношения между классами без наследования.

  1. Композиция (сильная связь)

Принцип: "Часть не может существовать без целого"

  • Объект-часть создается внутри объекта-целого

  • Когда основной объект удаляется, его части тоже удаляются

Пример из жизни: Двигатель не существует отдельно от конкретного автомобиля

Код:

class Engine:
    def start(self):
        print("Двигатель запущен")

class Car:
    def __init__(self):
        self.engine = Engine()  # Композиция: двигатель создается внутри автомобиля
    
    def start(self):
        self.engine.start()
        
my_car = Car()
my_car.start()  # Двигатель запущен
# При удалении my_car двигатель тоже удалится
  1. Агрегация (слабая связь)

Принцип: "Часть может существовать отдельно от целого"

  • Объект-часть передается в объект-целое извне

  • Когда основной объект удаляется, части могут продолжать существовать

Пример из жизни: Водитель может существовать без автомобиля и пересаживаться в разные машины

Код:

class Driver:
    def __init__(self, name):
        self.name = name

class Taxi:
    def __init__(self, driver):
        self.driver = driver  # Агрегация: водитель приходит извне

driver = Driver("Иван")
taxi1 = Taxi(driver)
taxi2 = Taxi(driver)  # Один водитель в двух такси

# При удалении taxi1 или taxi2 водитель останется

Когда что использовать?

Композиция:

  • Когда части не имеют смысла без основного объекта

  • Например: страницы книги, кнопки на пульте

Агрегация:

  • Когда части могут использоваться разными объектами

  • Например: студенты в университете, игроки в команде

Практическое правило: Если при удалении основного объекта части должны удалиться — используйте композицию. Если части должны остаться — агрегацию.

  1. Какие бывают магические функции в Python?
Ответ

Магические методы (dunder-методы) — это специальные методы с двойными подчеркиваниями __метод__, которые Python вызывает автоматически в определенных ситуациях.

  1. Основные магические методы

Для создания и инициализации объектов

  • __new__(cls, ...) — создает новый экземпляр (вызывается до __init__)

  • __init__(self, ...) — конструктор, инициализирует объект

  • __del__(self) — деструктор, вызывается при удалении объекта

class Example:
    def __init__(self, value):
        self.value = value  # Инициализация
    
    def __del__(self):
        print("Объект удален")

Для строкового представления объекта

  • __str__(self) — возвращает строку для print(obj) и str(obj)

__repr__(self) — возвращает "официальное" строковое представление (для отладки)

class Book:
    def __init__(self, title):
        self.title = title
    
    def __str__(self):
        return f"Книга: {self.title}"
    
    def __repr__(self):
        return f"Book('{self.title}')"
        
book = Book("Python")
print(book)       # Книга: Python (вызов __str__)
print(repr(book)) # Book('Python') (вызов __repr__)

Для сравнения объектов

  • __eq__(self, other) — ==

  • __ne__(self, other) — !=

  • __lt__(self, other) — <

  • __gt__(self, other) — >

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
        
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2)  # True (вызов __eq__)

Для математических операций

  • __add__(self, other) — +

  • __sub__(self, other) — -

  • __mul__(self, other) — *

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
        
v1 = Vector(1, 2)
v2 = Vector(3, 4)
v3 = v1 + v2  # Vector(4, 6)

Для работы с коллекциями

  • __len__(self) — вызывается len(obj)

  • __getitem__(self, key) — доступ по индексу obj[key]

  • __setitem__(self, key, value) — присваивание obj[key] = value

  • __contains__(self, item) — проверка item in obj

class MyList:
    def __init__(self, items):
        self.items = items
    
    def __len__(self):
        return len(self.items)
    
    def __getitem__(self, index):
        return self.items[index]
        
lst = MyList([1, 2, 3])
print(len(lst))  # 3
print(lst[1])    # 2

Для вызова объектов как функций

  • __call__(self, ...) — позволяет вызывать объект как функцию
class Multiplier:
    def __init__(self, factor):
        self.factor = factor
    
    def __call__(self, x):
        return x * self.factor
        
double = Multiplier(2)
print(double(5))  # 10 (вызов __call__)
  1. Другие полезные магические методы
  • __iter__(self) — для итерации (например, в циклах for)

  • __next__(self) — возвращает следующий элемент итератора

  • __enter__(self), __exit__(self, ...) — для менеджеров контекста (with)

  • __getattr__(self, name) — вызывается при обращении к несуществующему атрибуту

  • __setattr__(self, name, value) — вызывается при присваивании атрибута

Когда использовать магические методы?

Для удобного представления объекта → __str__, __repr__

Для перегрузки операторов → __add__, __eq__

Для работы с коллекциями → __len__, __getitem__

Для создания контейнеров и итераторов → __iter__, __next__

Для управления контекстом → __enter__, __exit__

Эти методы делают классы более интуитивно понятными и удобными в использовании.

  1. Try, except, else, finally.
Ответ Это блоки для обработки ошибок (исключений) в Python. Позволяют писать код, который не сломается, даже если что-то пойдет не так.
  1. Основные блоки
try:
    # Код, который может вызвать ошибку
    x = 10 / 0  # Деление на ноль → ошибка ZeroDivisionError
except ZeroDivisionError:
    # Сработает, если в try произошла конкретная ошибка
    print("Делить на ноль нельзя")
except Exception as e:
    # Ловит любые другие ошибки (необязательно)
    print(f"Произошла ошибка: {e}")
else:
    # Сработает, если в try НЕ было ошибок (необязательно)
    print("Ошибок нет, всё ок")
finally:
    # Сработает ВСЕГДА, даже если была ошибка (необязательно)
    print("Это выполнится в любом случае")
  1. Как это работает?

try — выполняет код, который может вызвать ошибку. Пример: чтение файла, деление чисел, работа с сетью.

except — ловит конкретную ошибку (например, ZeroDivisionError, FileNotFoundError).

Можно ловить несколько ошибок:

except (ZeroDivisionError, ValueError) as e:
print(f"Ошибка: {e}")

Exception ловит все ошибки (лучше уточнять тип).

else (необязательно) — выполняется только если в try не было ошибок. Пример: если файл прочитался успешно — вывести его содержимое.

finally (необязательно) — выполняется всегда, даже если:

  • была ошибка,

  • сработал except или else,

  • был return в try или except.

Пример: закрыть файл или соединение с базой данных.

  1. Примеры

Пример 1: Деление чисел

try:
    a = int(input("Введите число a: "))
    b = int(input("Введите число b: "))
    result = a / b
except ValueError:
    print("Ошибка: Введите число, а не текст")
except ZeroDivisionError:
    print("Ошибка: Делить на ноль нельзя")
else:
    print(f"Результат: {result}")
finally:
    print("Вычисление завершено")

Пример 2: Работа с файлом

try:
    file = open("data.txt", "r")
    content = file.read()
except FileNotFoundError:
    print("Файл не найден")
else:
    print(content)
finally:
    file.close()  # Закрыть файл в любом случае
  1. Когда что использовать?
  • try/except — если код может вызвать ошибку и нужно её обработать.

  • else — если нужно выполнить код только при успешном выполнении try.

  • finally — если нужно выполнить код в любом случае (очистка ресурсов: закрытие файлов, соединений и т. д.).

  1. Важные нюансы
  • Без except или finally блок try не работает.

  • else выполняется только если нет ошибок.

  • finally выполняется даже при return или break.

  • Можно вкладывать try/except друг в друга.

Итог: Эти блоки помогают писать устойчивый код, который не падает при ошибках и корректно освобождает ресурсы.

  1. Асинхронное, многопоточное и мультипроцессорное выполнение.
Ответ

Представь, что ты повар на кухне. Ты можешь работать по-разному:

  1. Асинхронность (Async) — как повар-жонглёр

Как работает:

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

Пример: Ты начинаешь варить суп → пока он кипит (ждёт), ты режешь салат → пока салат в холодильнике маринуется, ставишь чайник. Ты один, но постоянно переключаешься между задачами, пока другие "ждут".

Когда так делать: Когда много дел, где нужно ждать (заказ еды по телефону, пока сварится паста).

Пример из жизни: Ты пишешь 3 письма, отправляешь и пока ждёшь ответа — работаешь с другими.

Код:

import asyncio

async def download(url):
    print(f"Начинаю загрузку {url}")
    await asyncio.sleep(2)  # Имитация ожидания
    print(f"Загрузил {url}")

async def main():
    await asyncio.gather(
        download("url1.com"),
        download("url2.com"),
        download("url3.com"),
    )

asyncio.run(main())

Плюсы:

  • Быстро переключается между задачами, не тратит время на ожидание.

  • Эффективнее потоков для I/O операций.

Минусы:

  • Не ускоряет вычисления (если задача требует много математики, async не поможет).
  1. Потоки (Threading) — как повар с фальшивыми руками

Как работает: Запускается несколько потоков в одном процессе. Потоки выполняются "параллельно", но из-за GIL (глобальной блокировки) Python на самом деле быстро переключается между ними.

Пример: У тебя есть 4 руки, но мозг один (GIL). Ты якобы делаешь всё сразу, но на самом деле быстро переключаешься. Например, одной рукой мешаешь суп, другой режешь овощи, но если обе задачи требуют "мозга" (CPU), ты всё равно делаешь их по очереди.

Код:

from threading import Thread
import time

def task(name):
    print(f"Задача {name} началась")
    time.sleep(2)
    print(f"Задача {name} завершена")

threads = []
for i in range(3):
    t = Thread(target=task, args=(f"Поток {i}",))
    t.start()
    threads.append(t)

for t in threads:
    t.join()  # Ждём завершения всех потоков

Когда так делать: Когда задач много, но они простые (мешать, резать, мыть посуду).

Пример из жизни: Ты разговариваешь по телефону и параллельно помешиваешь кофе — но если нужно думать (считать), всё встанет.

Плюсы:

  • Проще, чем асинхронный код, если задачи независимые.
  • Подходит для операций с ожиданием (сеть, файлы).

Минусы:

  • Из-за GIL не даёт выигрыша в CPU-задачах (например, сложные вычисления).
  1. Процессы (Multiprocessing) — как команда поваров

Как работает: Запускает несколько отдельных процессов (как разные программы). Каждый процесс использует своё ядро процессора.

Пример: У тебя несколько отдельных поваров (процессов), у каждого своя кухня (память) и своя голова (CPU). Один варит суп, второй печёт пирог, третий жарит стейк — по-настоящему одновременно.

Когда так делать: Когда задачи сложные и требуют полной мощности (печь торт, считать налоги, обрабатывать фото).

Пример из жизни: Строительство дома: один копает, второй кладёт кирпичи, третий стелит крышу — каждый занят своим делом.

Код:

from multiprocessing import Process
import time

def heavy_calculation(num):
    print(f"Вычисление {num} начато")
    time.sleep(2)  # Имитация сложной задачи
    print(f"Вычисление {num} завершено")

processes = []
for i in range(3):
    p = Process(target=heavy_calculation, args=(i,))
    p.start()
    processes.append(p)

for p in processes:
    p.join()  # Ждём завершения всех процессов

Плюсы:

  • Настоящая параллельность (использует все ядра CPU).

  • Нет ограничений GIL.

Минусы:

  • Больше потребляет памяти.
  • Сложнее обмениваться данными между процессами.

Как выбрать?

Если задачи «ждут» (загрузка файлов, запросы в интернет) → Асинхронность или потоки.

Если задачи «считают» (математика, видеообработка) → Процессы.

Если хочется просто → Потоки, но без фанатизма.

  1. GIL
Ответ GIL — это глобальная блокировка (замок) в Python, которая не позволяет нескольким потокам выполнять Python-код одновременно.

Представь, что Python-интерпретатор — это один микрофон на конференции. Даже если у тебя есть несколько спикеров (потоков), говорить в микрофон может только один в каждый момент времени.

Как это работает?

В Python потоки не работают по-настоящему параллельно из-за GIL. Даже если у процессора несколько ядер, Python переключается между потоками, а не запускает их одновременно. Это сделано для безопасности, чтобы избежать проблем с памятью.

Представь, что у тебя есть:

  1. Один бухгалтер (поток) с калькулятором (CPU)
  2. Несколько задач (документов) на столе

Без GIL:

Несколько бухгалтеров могли бы работать одновременно, но могли бы случайно испортить общие данные (например, два человека записывают в одну таблицу).

С GIL:

Только один бухгалтер работает в каждый момент.

Он быстро переключается между документами (потоками), но не делает их одновременно.

Когда GIL мешает?

  1. В CPU-bound задачах (тяжёлые вычисления, математика, обработка данных).

  2. Потоки не ускоряют такие задачи, потому что GIL заставляет их работать по очереди.

Решение: мультипроцессинг (запуск отдельных процессов).

  1. В многопоточных программах, где нужна настоящая параллельность.

Например, если 4 потока должны считать числа, они не будут работать в 4 раза быстрее.

Когда GIL не мешает?

  1. В I/O-bound задачах (сеть, файлы, базы данных).

  2. Пока поток ждёт ответа от сервера, GIL отпускает его, и работает другой поток.

Поэтому для сетевых задач потоки всё ещё полезны.

  1. В асинхронном коде (async/await).

Там нет потоков, а есть переключение задач в одном потоке.

Почему GIL до сих пор в Python?

  1. Исторически так устроен CPython (основная реализация Python).

  2. Упрощает работу с памятью (не нужно думать о конкурентных гонках данных).

  3. Для C-расширений это делает их проще в разработке.

Как обойти GIL?

  1. Мультипроцессинг (multiprocessing)

Запускает отдельные процессы (каждый со своим GIL).

Подходит для CPU-задач.

Использование других языков (Cython, Rust)

Критичные части кода можно вынести в C-расширения.

  1. Асинхронность (asyncio)

Если задача связана с ожиданием (I/O), можно вообще не использовать потоки.

Вывод:

GIL — это "замок", который не даёт потокам работать одновременно в Python. Он мешает только в CPU-задачах, но не в I/O. Если нужно ускорение вычислений — используй процессы, а не потоки.

  1. Инструменты для отладки, логгирования.
Ответ
  1. Отладка (поиск и исправление ошибок)

Простые способы:

  • print() - самый простой "отладчик"
print("Дошли до этой точки")  # Увидим в консоли, что программа сюда дошла
print(f"Значение x сейчас: {x}")  # Проверим текущее значение переменной

Более продвинутые инструменты:

pdb - встроенный отладчик (как GPS для кода)

import pdb; pdb.set_trace()  # Поставим "точку остановки" - код остановится здесь

Когда программа остановится, можно:

n - перейти к следующей строке

c - продолжить выполнение

p - посмотреть значение переменной

q - выйти из отладчика

  1. Логирование (запись работы программы)

Базовое логирование:

import logging

logging.basicConfig(level=logging.INFO)  # Настроим уровень важности сообщений
logging.info("Программа запущена")  # Запишем информационное сообщение
logging.warning("Что-то подозрительное")  # Запишем предупреждение
logging.error("Произошла ошибка")  # Запишем ошибку

Полезные дополнения:

Запись в файл:

logging.basicConfig(filename='app.log')  # Логи будут сохраняться в файл

Разные уровни важности:

  • DEBUG - для отладки (самые подробные сообщения)

  • INFO - обычная информация о работе

  • WARNING - предупреждения

  • ERROR - ошибки

  • CRITICAL - критические проблемы

Когда что использовать:

print() - для быстрой проверки в маленьких скриптах

pdb - когда нужно разобраться в сложной ошибке

logging - для серьёзных проектов, где важно сохранять историю работы

Логи помогают понять:

  1. Что происходило в программе

  2. Когда случилась ошибка

  3. Какие данные были в момент ошибки

Это как "чёрный ящик" в самолёте - записывает всё важное, что происходит с программой.

  1. Сериализация и валидация данных.
Ответ

Сериализация (упаковка данных)

Что это? Преобразование данных Python в формат, который можно сохранить или передать (как упаковка вещей в чемодан перед поездкой).

Основные форматы:

  • JSON - текстовый формат, понятный и людям и машинам

  • Pickle - бинарный формат Python (только для Python)

  • YAML - удобный для чтения формат конфигураций

  • MessagePack - компактный бинарный формат

Популярные инструменты:

import json
import pickle
import yaml
import msgpack

# JSON
data = {'name': 'Alice', 'age': 30}
json_str = json.dumps(data)  # В строку
loaded = json.loads(json_str)  # Обратно

# Pickle (только для доверенных данных)
binary_data = pickle.dumps(data)
loaded = pickle.loads(binary_data)

# YAML
yaml_str = yaml.dump(data)
loaded = yaml.safe_load(yaml_str)  # Безопасная загрузка

# MessagePack
packed = msgpack.packb(data)
unpacked = msgpack.unpackb(packed)

Валидация (проверка данных)

Что это? Проверка, что данные соответствуют нужным правилам (как проверка документов на таможне).

Основные задачи:

  • Проверить тип данных (число, строка и т.д.)

  • Проверить диапазон значений (возраст от 0 до 120)

  • Проверить обязательные поля

  • Преобразовать данные в нужный формат

Pydantic - самый современный инструмент

pydantic — это библиотека Python для проверки данных и работы с конфигурациями. Представьте, что это строгий секретарь, который проверяет все документы перед тем, как их подписать.

Основные возможности:

  1. Валидация данных — проверяет, что данные соответствуют требованиям

  2. Преобразование типов — автоматически приводит данные к нужному формату

  3. Работа с настройками — удобное управление конфигурациями приложения

from pydantic import BaseModel, ValidationError, Field

class User(BaseModel):
    name: str
    age: int = Field(gt=0, lt=120)  # Возраст от 1 до 119
    
try:
    user = User(name="Alice", age=25)  # Валидация при создании
except ValidationError as e:
    print("Ошибка валидации:", e)

Где это полезно?

  1. Веб-приложения — проверка данных от пользователей

  2. Конфигурации — загрузка и проверка настроек

  3. Базы данных — работа с записями

  4. API — проверка входящих и исходящих данных

Встроенные средства (для простых случаев):

def validate_user(data):
    if not isinstance(data['name'], str):
        raise ValueError("Имя должно быть строкой")
    if not 0 < data['age'] < 120:
        raise ValueError("Недопустимый возраст")

Почему Pydantic лучше обычных проверок?

  1. Читаемость — правила данных видны сразу в модели

  2. Автоматизация — не нужно писать много проверок вручную

  3. Безопасность — защита от неправильных данных

  4. Удобство — работает со многими фреймворками (FastAPI, Django и др.)

Pydantic экономит время и делает код надежнее, автоматизируя рутинные проверки данных.

Итог.

Сериализация:

JSON - для обмена данными между системами

Pickle - для временного сохранения данных Python (не для безопасности!)

YAML - для конфигурационных файлов

MessagePack - для эффективной передачи данных

Валидация:

Pydantic - для сложных структур данных (рекомендуется)

Свои проверки - для очень простых случаев

  1. UNIT-тестирование
Ответ Что такое unit-тесты?

Unit-тесты (модульные тесты) — это проверки отдельных частей вашего кода (функций, классов) на правильность работы. Это как тест-драйв автомобиля перед покупкой — вы проверяете каждую деталь отдельно. Зачем нужны unit-тесты?

  1. Находят ошибки — ловят баги до того, как код попадёт в продакшен

  2. Экономят время — автоматическая проверка быстрее ручной

  3. Помогают изменять код — убеждаются, что изменения ничего не сломали

  4. Документируют код — тесты показывают, как должна работать функция

Как писать тесты в Python?

  1. Используем модуль unittest (встроенный в Python)
import unittest

# Функция, которую будем тестировать
def add(a, b):
    return a + b

# Создаем тестовый класс
class TestAddFunction(unittest.TestCase):
    def test_add_numbers(self):
        self.assertEqual(add(2, 3), 5)  # Проверяем, что 2+3=5
    
    def test_add_negative(self):
        self.assertEqual(add(-1, -1), -2)  # Проверяем с отрицательными числами
        
# Запускаем тесты
if __name__ == '__main__':
    unittest.main()
  1. Альтернатива: pytest (более современный вариант)

Пример теста:

# test_calc.py
def add(a, b):
    return a + b

def test_add_numbers():
    assert add(2, 3) == 5  # Проверяем сложение

def test_add_negative():
    assert add(-1, -1) == -2  # Проверяем с отрицательными числами

Запуск:

pytest test_calc.py

Какие бывают проверки (asserts)?

assertEqual(a, b) — a == b

assertTrue(x) — x истинно

assertFalse(x) — x ложно

assertRaises(Error) — проверка на ошибку

assertAlmostEqual(a, b) — для чисел с плавающей точкой

Как правильно писать тесты?

  1. Один тест — одна проверка — не смешивайте несколько проверок в одном тесте

  2. Тестируйте крайние случаи:

  • Пустые входные данные

  • Неправильные типы данных

  • Предельные значения

  1. Тесты должны быть быстрыми — не делайте долгих операций

  2. Используйте понятные имена — test_add_negative_numbers лучше чем test1

Пример хорошего теста:

import unittest

def divide(a, b):
    if b == 0:
        raise ValueError("Нельзя делить на ноль")
    return a / b

class TestDivideFunction(unittest.TestCase):
    def test_divide_normal(self):
        self.assertAlmostEqual(divide(10, 2), 5.0)
    
    def test_divide_by_zero(self):
        with self.assertRaises(ValueError):  # Проверяем что будет ошибка
            divide(10, 0)
    
    def test_divide_float_result(self):
        self.assertAlmostEqual(divide(5, 2), 2.5)

Частые ошибки новичков:

  1. Слишком большие тесты — тест должен проверять одну конкретную вещь

  2. Зависимость между тестами — тесты должны работать в любом порядке

  3. Тестирование не того, что нужно — не тестируйте встроенные функции Python

  4. Игнорирование ошибок — если тест упал, это нужно исправить, а не удалить тест

Как часто запускать тесты?

  • Перед каждым коммитом — чтобы не ломать основной код

  • После значительных изменений — проверить, что ничего не сломалось

  • В CI/CD пайплайне — автоматически при каждом пулл-реквесте

Unit-тесты — это страховка для вашего кода, которая окупается уменьшением количества багов и более уверенной разработкой.

  1. Алгоритмическая сложность функций Python.
Ответ

Алгоритмическая сложность функции в Python (или любом другом языке) — это оценка того, как быстро растёт время работы или потребление памяти в зависимости от размера входных данных. Простыми словами:

Представь, что у тебя есть список чисел, и ты хочешь найти в нём максимальное.

Если список маленький (например, 5 чисел) — программа справится мгновенно.

Если список огромный (например, 1 000 000 чисел) — программа может работать дольше.

Алгоритмическая сложность помогает понять, насколько сильно замедлится программа при увеличении данных. Основные виды сложности (от быстрой к медленной):

O(1) – Константная сложность.

Время работы не зависит от размера данных.

Пример: доступ к элементу списка по индексу (my_list[5]). Код:

def get_first_element(lst):
    return lst[0]  # Всегда выполняется за одно действие

# Пример:
print(get_first_element([1, 2, 3, 4, 5]))  # 1

O(log n) – Логарифмическая сложность.

Время растёт медленнее, чем сами данные.

Пример: бинарный поиск в отсортированном списке.

Код:

def binary_search(lst, target):
    left, right = 0, len(lst) - 1
    while left <= right:
        mid = (left + right) // 2
        if lst[mid] == target:
            return mid
        elif lst[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return -1

# Пример (список должен быть отсортирован):
sorted_list = [1, 3, 5, 7, 9, 11]
print(binary_search(sorted_list, 7))  # Индекс 3

O(n) – Линейная сложность.

Время растёт пропорционально размеру данных.

Пример: поиск максимума в неотсортированном списке (нужно проверить все элементы).

Код:

def find_max(lst):
    max_val = lst[0]
    for num in lst:  # Проходим весь список один раз
        if num > max_val:
            max_val = num
    return max_val

# Пример:
print(find_max([5, 2, 9, 1, 5]))  # 9

O(n log n) – Логарифмически-линейная.

Хуже, чем O(n), но лучше, чем O(n²).

Пример: быстрая сортировка (sorted() в Python).

Код:

def merge_sort(lst):
    if len(lst) > 1:
        mid = len(lst) // 2
        left = lst[:mid]
        right = lst[mid:]
        merge_sort(left)   # Рекурсивный вызов
        merge_sort(right)  # Рекурсивный вызов
        # Слияние (O(n) на каждом уровне рекурсии)
        i = j = k = 0
        while i < len(left) and j < len(right):
            if left[i] < right[j]:
                lst[k] = left[i]
                i += 1
            else:
                lst[k] = right[j]
                j += 1
            k += 1
        while i < len(left):
            lst[k] = left[i]
            i += 1
            k += 1
        while j < len(right):
            lst[k] = right[j]
            j += 1
            k += 1

# Пример:
lst = [5, 2, 9, 1, 5, 6]
merge_sort(lst)
print(lst)  # [1, 2, 5, 5, 6, 9]

O(n²) – Квадратичная сложность.

Время растёт как квадрат размера данных.

Пример: вложенные циклы (пузырьковая сортировка, проверка всех пар в списке).

Код:

def bubble_sort(lst):
    n = len(lst)
    for i in range(n):          # Внешний цикл O(n)
        for j in range(n - 1):  # Внутренний цикл O(n)
            if lst[j] > lst[j + 1]:
                lst[j], lst[j + 1] = lst[j + 1], lst[j]

# Пример:
lst = [5, 2, 9, 1, 5]
bubble_sort(lst)
print(lst)  # [1, 2, 5, 5, 9]

O(2ⁿ), O(n!) – Экспоненциальная и факториальная.

Очень медленно, даже для небольших данных.

Пример: перебор всех подмножеств (2ⁿ) или всех перестановок (n!).

Код O(2ⁿ):

def fibonacci_recursive(n):
    if n <= 1:
        return n
    return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2)

# Пример (очень медленно при n > 30):
print(fibonacci_recursive(10))  # 55

Код O(n!):

from itertools import permutations

def generate_permutations(lst):
    return list(permutations(lst))  # Генерирует все возможные перестановки

# Пример (очень медленно при n > 10):
print(generate_permutations([1, 2, 3]))  
# [(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]

Как определить сложность функции?

Посмотри на циклы:

Один цикл по списку из n элементов → O(n).

Два вложенных цикла → O(n²).

Рекурсия может увеличивать сложность (например, O(2ⁿ) для рекурсивного Фибоначчи без оптимизации).

Встроенные функции тоже имеют сложность:

len() → O(1)

x in list → O(n)

x in set → O(1) (из-за хеш-таблиц)

Зачем это нужно?

Чтобы выбирать оптимальные алгоритмы для больших данных. Например:

Для поиска в списке из 1 000 000 элементов:

x in list (O(n)) — медленно.

x in set (O(1)) — мгновенно.

Чем меньше сложность, тем лучше функция масштабируется на больших данных.

SQL

Image

  1. В каком порядке выполняется запрос и краткая характеристика каждого оператора:
SELECT 
    t1.col_2 , 
    COUNT(*), 
    COUNT(*) OVER() 
FROM Table_1 AS t1
    INNER JOIN Table_2 AS t2
        ON t1.id =t2.id
WHERE 1 = 1 AND 
    t1.col_1  > 2
GROUP BY 
    t1.col_1
HAVING 
    COUNT(*) > 1
QUALIFY 
    COUNT(*) OVER() > 1
ORDER BY 2
LIMIT 1
Ответ Вот порядок выполнения запроса и краткая характеристика каждого оператора:
  1. FROM и JOIN (первый этап выполнения)

FROM Table_1 AS t1 - выбирается исходная таблица Table_1 с алиасом t1

INNER JOIN Table_2 AS t2 ON t1.id = t2.id - соединяется с Table_2 по условию равенства id

  1. WHERE (фильтрация строк)

WHERE 1 = 1 AND t1.col_1 > 2 - фильтрует строки, оставляя только те, где col_1 > 2 (1=1 обычно не влияет на результат)

  1. GROUP BY (группировка)

GROUP BY t1.col_1 - группирует результат по столбцу col_1

HAVING (фильтрация групп)

HAVING COUNT(*) > 1 - оставляет только группы с количеством строк больше 1

  1. SELECT (выбор столбцов и вычисление агрегатов)

t1.col_2 - выбирается столбец col_2

COUNT(*) - подсчитывает количество строк в каждой группе

COUNT(*) OVER() - оконная функция, считает общее количество строк в результирующем наборе

  1. QUALIFY (фильтрация по оконным функциям)

QUALIFY COUNT(*) OVER() > 1 - оставляет только строки, где общее количество строк в результате > 1

  1. ORDER BY (сортировка)

ORDER BY 2 - сортирует результат по второму столбцу (COUNT(*))

  1. LIMIT (ограничение количества строк)

LIMIT 1 - оставляет только первую строку результата

Особенности:

QUALIFY - это нестандартное расширение (есть в Snowflake, Teradata), которое фильтрует результат после применения оконных функций

Порядок важен: например, WHERE выполняется до GROUP BY, а HAVING - после

Оконная функция COUNT(*) OVER() вычисляется после группировки, но до QUALIFY

  1. Какие есть виды команд в SQL? Назовите примеры команд каждого вида и их задачу.
Ответ

В SQL есть несколько видов команд, которые выполняют разные задачи. Вот основные виды с примерами:

  1. DDL (Data Definition Language) – Язык определения данных

Эти команды работают со структурой базы данных: создают, изменяют или удаляют таблицы и другие объекты.

CREATE – создаёт объекты (таблицы, базы данных и т. д.).

CREATE TABLE users (id INT, name VARCHAR(50));

ALTER – изменяет структуру таблицы (добавляет столбцы, меняет типы данных).

ALTER TABLE users ADD age INT;

DROP – удаляет объекты (таблицы, базы данных).

DROP TABLE users;

TRUNCATE – очищает таблицу (удаляет все данные, но оставляет структуру).

TRUNCATE TABLE users;
  1. DML (Data Manipulation Language) – Язык управления данными

Эти команды работают с данными внутри таблиц: добавляют, изменяют, удаляют или выбирают записи.

SELECT – выбирает данные из таблицы.

SELECT * FROM users;

INSERT – добавляет новые записи.

INSERT INTO users (id, name) VALUES (1, 'Иван');

UPDATE – изменяет существующие записи.

UPDATE users SET name = 'Пётр' WHERE id = 1;

DELETE – удаляет записи.

DELETE FROM users WHERE id = 1;
  1. DCL (Data Control Language) – Язык управления доступом

Эти команды управляют правами пользователей.

GRANT – даёт права пользователю.

GRANT SELECT ON users TO user1;

REVOKE – забирает права.

    REVOKE SELECT ON users FROM user1;
  1. TCL (Transaction Control Language) – Язык управления транзакциями

Эти команды управляют транзакциями (группами операций, которые должны выполниться вместе).

COMMIT – сохраняет изменения.

COMMIT;

ROLLBACK – отменяет изменения.

ROLLBACK;

SAVEPOINT – создаёт точку для отката внутри транзакции.

    SAVEPOINT backup;
  1. DQL (Data Query Language) – Язык запросов данных

Иногда SELECT выделяют отдельно, так как он не изменяет данные, а только выбирает их.

SELECT – извлекает данные.

SELECT name FROM users WHERE age > 18;

Это основные виды команд SQL. Они помогают управлять структурой базы данных, данными, правами и транзакциями.

  1. Отличие DROP, DELETE, TRUNCATE.
Ответ Вот основные отличия DROP, DELETE и TRUNCATE:
  1. DROP TABLE
  • Полностью удаляет таблицу – данные и структуру (саму таблицу).

  • Нельзя откатить (ROLLBACK не сработает, если не используется в транзакции).

  • Освобождает место на диске – таблица исчезает из БД.

  • Быстрый, но опасный – таблицу потом нужно создавать заново.

  • Используется, когда таблица больше не нужна.

  1. TRUNCATE TABLE
  • Удаляет все данные из таблицы, но оставляет структуру (сама таблица остаётся).

  • Нельзя откатить в большинстве СУБД (но в PostgreSQL, например, можно внутри транзакции).

  • Работает быстрее DELETE – не записывает каждую строку в журнал транзакций.

  • Сбрасывает счётчики автоинкремента (в отличие от DELETE).

  • Используется, когда нужно быстро очистить таблицу.

  1. DELETE FROM
  • Удаляет строки по условию (если нет WHERE – удаляет все данные, но медленнее TRUNCATE).

  • Можно откатить (ROLLBACK работает, если операция в транзакции).

  • Не сбрасывает автоинкремент (в некоторых СУБД, например MySQL, можно сбросить вручную).

  • Медленнее TRUNCATE, т.к. логирует каждое удаление.

  • Используется, когда нужно удалить часть данных или важна возможность отката.

Краткое сравнение

  1. DROP – удаляет таблицу полностью (данные + структура).

  2. TRUNCATE – быстро очищает таблицу, но не удаляет её.

  3. DELETE – гибкое удаление строк (можно с WHERE), но медленнее.

Если нужно полностью избавиться от таблицы → DROP. Если нужно быстро очистить таблицу → TRUNCATE. Если нужно удалить часть данных или нужен откат → DELETE

  1. Назовите основные типы данных в SQL. За основу возьмите любую БД.
Ответ Вот основные типы данных в PostgreSQL (одной из самых популярных современных СУБД):
  1. Числовые типы
  • INTEGER (или INT) — целые числа (−2,147,483,648 до 2,147,483,647)

  • BIGINT — большие целые числа (±9,223,372,036,854,775,807)

  • SMALLINT — маленькие целые числа (−32,768 до 32,767)

  • DECIMAL(p, s) / NUMERIC(p, s) — точные числа с фиксированной запятой (p — общее количество цифр, s — после запятой)

  • REAL — числа с плавающей запятой (~6 знаков после запятой)

  • DOUBLE PRECISION — двойная точность (~15 знаков после запятой)

  • SERIAL / BIGSERIAL — автоинкремент (для первичных ключей)

  1. Строковые типы
  • VARCHAR(n) — строка переменной длины (макс. n символов)

  • CHAR(n) — строка фиксированной длины (дополняется пробелами)

  • TEXT — строка неограниченной длины (аналог VARCHAR без лимита)

  1. Логический тип
  • BOOLEAN — TRUE/FALSE/NULL
  1. Дата и время
  • DATE — дата (год, месяц, день)

  • TIME — время (часы, минуты, секунды)

  • TIMESTAMP — дата + время

  1. Назовите "нестандартные" типы данных SQL, которые знаете.
Ответ 1. JSON / JSONB (Structured Data)

Что это? Формат для хранения и обработки структурированных данных (например, вложенных объектов).

JSON – текст с валидацией (медленнее поиск).

JSONB – бинарный формат (быстрый поиск, сжатие).

PostgreSQL

JSON – хранит данные в текстовом формате с валидацией.

JSONB – бинарный формат (оптимизирован для поиска, поддерживает индексы).

Операторы: ->, ->>, #>, jsonb_path_query() и др.

Пример:
CREATE TABLE users (id SERIAL, profile JSONB);
INSERT INTO users (profile) VALUES ('{"name": "Alice", "age": 25}');

-- Извлечение поля
SELECT profile->>'name' FROM users;  -- "Alice"

-- Индекс для ускорения поиска
CREATE INDEX idx_profile_name ON users ((profile->>'name'));

ClickHouse

Поддерживает JSON через тип String или специализированные функции.

JSONExtract – извлекает данные из JSON.

Пример:

CREATE TABLE events (event String) ENGINE = MergeTree ORDER BY tuple();
INSERT INTO events VALUES ('{"user": "Bob", "action": "click"}');

-- Извлечение поля
SELECT JSONExtractString(event, 'user') FROM events;  -- "Bob"

Hive

Нет нативного типа JSON, но можно хранить как STRING и парсить через UDF (например, get_json_object).

Пример:

CREATE TABLE logs (json_data STRING);
SELECT get_json_object(json_data, '$.user') FROM logs;
  1. UUID (Unique Identifiers)

Что это? Уникальный 128-битный идентификатор (например, a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11).

Используется для уникальных ключей.

Генерируется функцией gen_random_uuid().

PostgreSQL

Тип UUID (16-байтовый идентификатор).

Генерация через gen_random_uuid() или uuid-ossp расширение.

Пример:

CREATE TABLE orders (id UUID DEFAULT gen_random_uuid(), item TEXT);
INSERT INTO orders (item) VALUES ('Laptop');

ClickHouse

Поддерживает UUID как тип данных.

Генерация через generateUUIDv4().

Пример:

CREATE TABLE transactions (id UUID, amount Float64) ENGINE = MergeTree ORDER BY id;
INSERT INTO transactions VALUES (generateUUIDv4(), 100.50);

Hive

Нет встроенного UUID, но можно эмулировать через STRING и Java-функции.

Пример:

SELECT reflect('java.util.UUID', 'randomUUID') AS uuid;
  1. INTERVAL (Time Intervals)

Что это? Хранит промежутки времени (например, "2 часа 30 минут").

PostgreSQL

Тип INTERVAL для хранения временных промежутков.

Пример:

SELECT NOW() + INTERVAL '1 day';  -- Завтра
CREATE TABLE events (duration INTERVAL);
INSERT INTO events VALUES (INTERVAL '2 hours 30 minutes');

ClickHouse

Нет типа INTERVAL, но есть арифметика с датами:

SELECT now() + INTERVAL 1 DAY;  -- Аналогично

Hive

Используются функции date_add, date_sub:

SELECT date_add(CURRENT_DATE, 7);  -- +7 дней
  1. ENUM (Перечисления)

Что это? Список допустимых значений для столбца (например, статусы заказа).

PostgreSQL

Создается как отдельный тип:

CREATE TYPE status AS ENUM ('active', 'pending', 'deleted');
CREATE TABLE tasks (id SERIAL, status status);
INSERT INTO tasks (status) VALUES ('active');

ClickHouse

Нет типа ENUM, но можно эмулировать через LowCardinality(String):

CREATE TABLE orders (status LowCardinality(String)) ENGINE = MergeTree;

Hive

Нет ENUM, но можно использовать STRING с ограничениями:

CREATE TABLE users (role STRING CHECK (role IN ('admin', 'user')));

Ключевые выводы:

PostgreSQL – наиболее богатая поддержка всех типов.

ClickHouse – оптимизирован для аналитики, но некоторые типы эмулируются.

Hive – ограниченная поддержка, требуется использование UDF и строк.

  1. Различие строковых типов данных CHAR, VARCHAR, TEXT.
Ответ
  1. CHAR(n) — фиксированная длина

Хранение: Всегда занимает n символов, даже если строка короче.

Пример: CHAR(10) с данными "abc" займёт 10 символов (дополняется пробелами).

Производительность: Быстрее для поиска, если длина строки постоянна.

Использование: Подходит для данных фиксированного размера (например, коды стран 'RU', 'US').

  1. VARCHAR(n) — переменная длина с ограничением

Хранение: Занимает только фактическую длину строки + 1-2 байта (для учёта длины).

Пример: VARCHAR(10) с данными "abc" займёт ~3-4 байта.

Ограничение: Максимальная длина — n символов (если строка длиннее — ошибка или обрезка).

Использование: Для строк переменной длины, где важно ограничение (имена, emails).

  1. TEXT — неограниченная длина

Хранение: Аналог VARCHAR без лимита на длину (в PostgreSQL до 1 ГБ).

Производительность: Медленнее VARCHAR при очень больших данных, но гибче.

Использование: Для длинного текста (статьи, логи, JSON без парсинга)

Когда что использовать?

CHAR — если все значения одинаковой длины (коды, шифры).

VARCHAR — если длина варьируется, но есть разумный предел (например, 255 символов для email).

TEXT — если длина неизвестна или может быть большой (описания, контент).

  1. Назовите логические виды JOIN и для чего они нужны.
Ответ

В SQL есть несколько основных логических видов JOIN, которые используются для объединения данных из разных таблиц. Вот они с пояснениями:

  1. INNER JOIN (Внутреннее соединение)

Что делает: Возвращает только те строки, где есть совпадение в обеих таблицах. Для чего нужен: Когда нужны только общие данные (например, «показать пользователей, у которых есть заказы»). Пример:

SELECT users.name, orders.amount
FROM users
INNER JOIN orders ON users.id = orders.user_id;

→ Вернёт только пользователей, сделавших хотя бы один заказ.

  1. LEFT JOIN (LEFT OUTER JOIN)

Что делает: Возвращает все строки из левой таблицы (первой в запросе) и совпадения из правой. Если совпадений нет — в правой части NULL.

Для чего нужен: Когда нужно получить все записи из основной таблицы, даже если в связанной нет данных (например, «все пользователи, включая тех, у кого нет заказов»). Пример:

SELECT users.name, orders.amount
FROM users
LEFT JOIN orders ON users.id = orders.user_id;

→ Покажет всех пользователей, даже если у них нет заказов (в amount будет NULL).

  1. RIGHT JOIN (RIGHT OUTER JOIN)

Что делает: Возвращает все строки из правой таблицы и совпадения из левой. Если совпадений нет — в левой части NULL. Для чего нужен: Когда важно получить все записи из правой таблицы (например, «все заказы, включая те, у которых нет пользователя»). Пример:

SELECT users.name, orders.amount
FROM users
RIGHT JOIN orders ON users.id = orders.user_id;

→ Покажет все заказы, даже если пользователь удалён (в name будет NULL).

  1. FULL JOIN (FULL OUTER JOIN)

Что делает: Возвращает все строки из обеих таблиц. Если совпадений нет — в недостающей части NULL. Для чего нужен: Когда нужно объединить данные из двух таблиц без потерь (например, «все пользователи и все заказы, даже если связи между ними нет»). Пример:

SELECT users.name, orders.amount
FROM users
FULL JOIN orders ON users.id = orders.user_id;

→ Покажет и пользователей без заказов, и заказы без пользователей.

  1. CROSS JOIN (Перекрёстное соединение)

Что делает: Возвращает декартово произведение — все возможные комбинации строк из обеих таблиц. Для чего нужен: Когда нужно объединить каждый элемент первой таблицы с каждым элементом второй (например, «все варианты размеров и цветов товара»). Пример:

SELECT colors.name, sizes.name
FROM colors
CROSS JOIN sizes;

→ Выведет все сочетания цветов и размеров (например, «красный + S», «синий + XL»).

  1. SELF JOIN

Что делает: Соединение таблицы самой с собой (используется псевдонимы). Для чего нужен: Для анализа иерархических данных (например, «сотрудники и их менеджеры» в одной таблице). Пример:

SELECT a.name AS employee, b.name AS manager
FROM employees a
LEFT JOIN employees b ON a.manager_id = b.id;

→ Покажет список сотрудников и их руководителей.

LEFT JOIN используется чаще, чем RIGHT (запросы с LEFT проще читать).

INNER JOIN — самый быстрый, но «отсекает» данные без совпадений.

FULL JOIN может быть медленным на больших таблицах.

CROSS JOIN опасен на больших таблицах (может создать миллиарды строк).

  1. Назовите физические виды JOIN и их принцип работы.
Ответ

SQL физические виды JOIN — это способы, которыми СУБД выполняет соединение таблиц на уровне движка. Они определяют, как оптимизатор обрабатывает запрос для эффективного получения результата. Вот основные типы:

  1. Nested Loops Join (Вложенные циклы)

Принцип работы:

  • Для каждой строки из первой (внешней) таблицы сканируется вторая (внутренняя) таблица.

  • Условие соединения (ON) проверяется для каждой комбинации строк.

  • Если есть индексы — использует их для быстрого поиска во внутренней таблице.

Когда используется:

  • Когда одна таблица маленькая, а вторая имеет индекс по ключу соединения.

  • Эффективен для OLTP-систем (быстрые точечные запросы).

Пример плана запроса:

EXPLAIN SELECT * FROM users JOIN orders ON users.id = orders.user_id;

→ Если оптимизатор выбрал Nested Loops, в плане будет Nested Loop Join. Плюсы:

  • Быстро, если внешняя таблица маленькая.

  • Не требует сортировки данных.

Минусы:

  • Медленно при больших таблицах без индексов.
  1. Hash Join (Хеш-соединение)

Принцип работы:

  • Создаётся хеш-таблица по ключу соединения из меньшей таблицы.

  • Сканируется вторая таблица, и для каждой строки вычисляется хеш, который ищется в хеш-таблице.

  • Совпадения добавляются в результат.

Когда используется:

  • Для больших таблиц без индексов.

  • Когда нужно обработать много данных (аналитика, OLAP).

Пример:

-- Чаще используется для равенства (=) в JOIN
SELECT * FROM large_table1 
JOIN large_table2 ON large_table1.id = large_table2.id;

→ В плане: Hash Join. Плюсы:

  • Эффективен для больших несортированных данных.

  • Работает за линейное время (O(n)).

Минусы:

  • Требует много памяти для хеш-таблицы.

  • Не подходит для условий > или <.

  1. Merge Join (Сортировка-слияние)

Принцип работы:

  • Обе таблицы сортируются по ключу соединения.

  • Происходит параллельное сканирование обеих таблиц (как в алгоритме слияния).

  • Строки с совпадающими ключами объединяются.

Когда используется:

  • Когда таблицы уже отсортированы (или есть индексы).

  • Для больших данных с упорядоченными ключами.

Пример:

SELECT * FROM table1 
JOIN table2 ON table1.sorted_column = table2.sorted_column;

→ В плане: Merge Join. Плюсы:

  • Очень быстр для предварительно отсортированных данных.

  • Минимальные затраты памяти.

Минусы:

  • Требует сортировки (если данные не отсортированы — дорого).
  1. Как определить, что функция является оконной?
Ответ Функция является оконной (window function) в SQL, если она:
  1. Содержит ключевое слово OVER()

Любая оконная функция обязательно включает предложение OVER(), которое определяет "окно" (группу строк) для вычислений.

Пример:

SUM(salary) OVER()  -- Оконная функция (вычисляет сумму всех salary)
SUM(salary)         -- Агрегатная функция (не оконная!)
  1. Не сворачивает строки в одну

Оконные функции не группируют данные в одну строку (в отличие от агрегатных с GROUP BY). Они сохраняют все исходные строки, добавляя результат вычислений в отдельный столбец.

Пример:

-- Оконная функция (строки не группируются)
SELECT name, salary, SUM(salary) OVER() AS total_salary
FROM employees;

-- Агрегатная функция (сворачивает строки в одну)
SELECT SUM(salary) FROM employees;
  1. Может включать PARTITION BY и ORDER BY

Внутри OVER() могут быть:

  • PARTITION BY — разбивает данные на группы (аналог GROUP BY, но без сворачивания).

  • ORDER BY — задаёт порядок обработки строк (например, для накопительных сумм).

  • Фрейм (ROWS / RANGE BETWEEN) - определяет подмножество строк ("окно") для расчёта внутри партиции.

Варианты:

  • ROWS BETWEEN — физические строки.

  • RANGE BETWEEN — логические диапазоны значений.

Примеры фреймов:

-- Текущая строка + предыдущая
SUM(salary) OVER(ROWS BETWEEN 1 PRECEDING AND CURRENT ROW)

-- Все строки от начала партиции до текущей
SUM(salary) OVER(ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)

-- 3 строки вокруг текущей (предыдущая, текущая, следующая)
SUM(salary) OVER(ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING)

-- Диапазон значений (например, ±100 от текущего salary)
SUM(salary) OVER(RANGE BETWEEN 100 PRECEDING AND 100 FOLLOWING)
  1. Использует специфичные для оконных функций конструкции

Некоторые функции только оконные и не работают без OVER():

  • ROW_NUMBER()

  • RANK()

  • DENSE_RANK()

  • LEAD() / LAG()

  • FIRST_VALUE() / LAST_VALUE()

  1. Назовите виды оконных функций.
Ответ
  1. Агрегатные функции (как оконные)

Применяют агрегацию, но без свёртки строк (в отличие от GROUP BY).

  • SUM() — сумма по окну.

  • AVG() — среднее значение.

  • COUNT() — количество строк.

  • MIN() / MAX() — минимальное/максимальное значение.

Пример:

SELECT department, salary, 
       AVG(salary) OVER(PARTITION BY department) AS avg_salary
FROM employees;
  1. Ранжирующие функции

Помогают пронумеровать строки или определить их позицию в наборе.

  • ROW_NUMBER() — уникальный номер строки (1, 2, 3...).

  • RANK() — ранжирование с пропусками (1, 2, 2, 4...).

  • DENSE_RANK() — ранжирование без пропусков (1, 2, 2, 3...).

  • NTILE(n) — разбивает данные на n групп (например, для квантилей).

Пример:

SELECT name, salary,
       RANK() OVER(ORDER BY salary DESC) AS salary_rank
FROM employees;
  1. Функции смещения (доступа к соседним строкам)

Работают с данными из других строк относительно текущей.

  • LAG(column, n) — значение из строки на n выше текущей.

  • LEAD(column, n) — значение из строки на n ниже текущей.

  • FIRST_VALUE(column) — первое значение в окне.

  • LAST_VALUE(column) — последнее значение в окне.

Пример:

SELECT date, revenue,
       LAG(revenue, 1) OVER(ORDER BY date) AS prev_day_revenue
FROM sales;
  1. Аналитические функции

Используются для статистического анализа.

  • PERCENT_RANK() — относительный ранг (от 0 до 1).

  • CUME_DIST() — кумулятивное распределение.

  • PERCENTILE_CONT() / PERCENTILE_DISC() — вычисляют процентили.

Пример:

SELECT student, score,
       PERCENT_RANK() OVER(ORDER BY score) AS percentile
FROM exam_results;
  1. В чем разница между RANK и DENSE_RANK?
Ответ
  1. RANK()
  • При совпадении значений присваивает одинаковый ранг, пропуская следующие номера.

  • Пример: 1, 2, 2, 4, 5 (после двух 2-к ранг 3 пропущен).

  1. DENSE_RANK()
  • При совпадении значений также присваивает одинаковый ранг, но без пропусков.

  • Пример: 1, 2, 2, 3, 4 (после двух 2-к идёт ранг 3).

Когда что использовать?

RANK — если важно сохранить позицию в рейтинге (например, "4-е место после двух серебряных призёров").

DENSE_RANK — если нумерация должна быть непрерывной (например, для категорий "A, B, B, C").

  1. Как определяется окно строк в оконной функции? Объясни, как понимаешь принцип работы оконной функции.
Ответ

Окно строк — это набор записей, над которыми оконная функция выполняет вычисления для текущей строки. Оно задаётся внутри OVER() с помощью:

  1. PARTITION BY — разбивает данные на группы (аналогично GROUP BY, но без свёртки).

  2. ORDER BY — определяет порядок строк в окне (важно для накопительных функций).

  3. Фрейм (ROWS/RANGE BETWEEN) — задаёт границы окна относительно текущей строки:

  • UNBOUNDED PRECEDING — начало партиции.

  • n PRECEDING — n строк перед текущей.

  • CURRENT ROW — текущая строка.

  • n FOLLOWING — n строк после текущей.

  • UNBOUNDED FOLLOWING — конец партиции.

Принцип работы оконной функции:

Шаг 1: Разбиение на партиции Если указан PARTITION BY, данные делятся на группы. Если нет — вся таблица считается одной партицией.

Шаг 2: Сортировка (если есть ORDER BY) Строки в каждой партиции сортируются по указанному столбцу. Без ORDER BY порядок не гарантирован.

Шаг 3: Определение фрейма Для каждой строки определяется подмножество строк (фрейм), над которым вычисляется функция:

Без ORDER BY фрейм по умолчанию — вся партиция.

С ORDER BY фрейм по умолчанию — RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.

Шаг 4: Вычисление и вывод Функция применяется к фрейму текущей строки, а результат добавляется в отдельный столбец без свёртки строк.

  1. Как применял оконные функции на практике? Пример.
... Скрытый текст или Markdown-контент.
  1. Как ты понимаешь, для чего нужно оптимизировать запросы?
Ответ

Оптимизация SQL-запросов — это процесс улучшения их производительности, чтобы они выполнялись быстрее, потребляли меньше ресурсов и эффективно работали с базой данных. Основные цели оптимизации SQL-запросов:

  1. Ускорение выполнения запросов
  • Медленные запросы могут тормозить работу приложения.

  • Оптимизация уменьшает время отклика, что критично для пользовательского опыта.

  1. Снижение нагрузки на сервер БД
  • Неоптимальные запросы потребляют много CPU, памяти и дискового I/O.

  • Это может привести к замедлению всей системы, особенно при высокой нагрузке.

  1. Экономия ресурсов
  • В облачных средах неэффективные запросы увеличивают затраты (например, в BigQuery или AWS RDS).

  • Оптимизация снижает расходы на инфраструктуру.

  1. Улучшение масштабируемости
  • База данных должна справляться с ростом данных и пользователей.

  • Плохие запросы могут стать узким местом при увеличении нагрузки.

  1. Предотвращение блокировок и deadlock’ов
  • Долгие транзакции могут блокировать другие операции.

  • Оптимизация уменьшает конфликты в многопользовательской среде.

Как оптимизировать SQL-запросы?

  • Использовать индексы (но не переусердствовать).

  • Избегать SELECT * – выбирать только нужные столбцы.

  • Оптимизировать JOIN’ы – уменьшать количество объединяемых строк.

  • Анализировать EXPLAIN (план запроса) – находить узкие места.

  • Кэшировать результаты часто выполняемых запросов.

  • Избегать сложных подзапросов, где можно использовать JOIN.

  • Правильно проектировать БД (нормализация/денормализация).

Вывод

Оптимизация SQL-запросов нужна для скорости, стабильности и экономии ресурсов. Даже небольшие улучшения могут дать значительный прирост производительности на больших данных.

  1. В чем различия команд EXPLAIN и EXPLAIN ANALYZE?
Ответ Разница между EXPLAIN и EXPLAIN ANALYZE
  1. EXPLAIN

Что делает: Показывает предполагаемый план выполнения запроса, который оптимизатор СУБД собирается использовать.

Особенности:

  • Не выполняет запрос на самом деле, только анализирует его.

  • Выводит информацию о том, какие индексы, соединения (JOIN), фильтры (WHERE) и методы сканирования (Seq Scan, Index Scan) будут использованы.

  • Полезен для предварительной проверки, почему запрос может работать медленно.

Пример:

EXPLAIN SELECT * FROM users WHERE age > 30;

Результат — это теоретический план, без реальных данных о времени выполнения.

  1. EXPLAIN ANALYZE

Что делает: Выполняет запрос и показывает реальный план выполнения вместе с фактическими затратами времени и ресурсов.

Особенности:

Запускает запрос на реальных данных, поэтому может быть медленным на больших таблицах.

Показывает точное время выполнения каждого этапа (например, сколько миллисекунд заняло сканирование таблицы).

Выявляет расхождения между ожидаемым (EXPLAIN) и реальным (EXPLAIN ANALYZE) поведением запроса.

Пример:

EXPLAIN ANALYZE SELECT * FROM users WHERE age > 30;

Результат включает не только план, но и фактические метрики (время, количество строк, затраченные ресурсы).

Когда что использовать?

EXPLAIN — если нужно быстро понять, как СУБД будет обрабатывать запрос, без его реального выполнения.

EXPLAIN ANALYZE — если требуется точная информация о производительности, включая реальные задержки и узкие места.

Пример вывода EXPLAIN ANALYZE:

Seq Scan on users  (cost=0.00..15.30 rows=530 width=36) (actual time=0.012..1.230 rows=500 loops=1)
  Filter: (age > 30)
  Rows Removed by Filter: 100
Planning Time: 0.045 ms
Execution Time: 1.450 ms

Здесь видно, что:

  • Сканирование таблицы заняло 1.23 мс.

  • Отфильтровано 100 строк.

  • Общее время выполнения — 1.45 мс.

EXPLAIN же показал бы только cost=0.00..15.30 (предполагаемые затраты), без реальных временных метрик.

Вывод:

  • EXPLAIN — для быстрого анализа плана запроса.

  • EXPLAIN ANALYZE — для точного замера производительности, но с реальным выполнением запроса.

  1. Расскажи алгоритм того, как бы ты оптимизировал запрос.
Ответ
  1. Анализ текущей производительности

Запустить EXPLAIN ANALYZE для запроса, чтобы получить:

  • План выполнения.

  • Фактическое время операций.

  • Количество обработанных строк.

  • Выявить самые дорогие операции (например, Seq Scan, Sort, Nested Loop).

  1. Поиск узких мест
  • Полное сканирование таблицы (Seq Scan) → Возможно, нет подходящего индекса.

  • Медленные JOIN → Проверить, по каким полям соединяются таблицы.

  • Сортировка (Sort) или агрегация (HashAggregate) → Возможно, можно избежать с помощью индексов или переписать запрос.

  • Фильтрация (Filter) с большим Rows Removed → Условия WHERE неэффективны.

  1. Оптимизация структуры запроса
  • Избегать SELECT * → Выбирать только нужные столбцы.

  • Заменить подзапросы на JOIN → Если подзапросы выполняются для каждой строки.

  • Использовать LIMIT → Если нужны не все данные.

  • Оптимизировать условия WHERE → Переносить самые строгие условия в начало и избегать функций над полями (WHERE LOWER(name) = 'alice').

  1. Работа с индексами
  • Добавить индексы на поля, используемые в:

WHERE (условия фильтрации).

JOIN (поля соединения).

ORDER BY (сортировка).

  • Проверить использование индексов через EXPLAIN (если индекс не применяется, возможно, нужно переписать запрос).

  • Удалить неиспользуемые индексы → Они замедляют вставку/обновление.

  1. Переписать сложные операции
  • Разбить один сложный запрос на несколько → Например, сначала отфильтровать данные, затем соединять.

  • Использовать CTE (WITH) для читаемости → Но проверить, не ухудшает ли это производительность.

  • Заменить DISTINCT на GROUP BY → Если это ускоряет запрос.

  1. Проверить статистику и настройки БД
  • Обновить статистику таблиц → ANALYZE table_name; (актуально для PostgreSQL).

  • Проверить настройки сервера → Размер кэша, work_mem и т. д.

  1. Тестирование изменений
  • Сравнить время выполнения до и после оптимизации.

  • Убедиться, что запрос возвращает корректные данные.

Пример оптимизации Исходный запрос (медленный):

SELECT * 
FROM orders
WHERE customer_id IN (SELECT id FROM customers WHERE status = 'active')
ORDER BY created_at DESC;

Шаги оптимизации:

  • EXPLAIN ANALYZE показал Seq Scan на orders и Nested Loop.

Добавил индекс для ускорения JOIN:

CREATE INDEX idx_customer_status ON customers(id) WHERE status = 'active';

Переписал запрос с JOIN вместо подзапроса:

SELECT o.* 
FROM orders o
JOIN customers c ON o.customer_id = c.id AND c.status = 'active'
ORDER BY o.created_at DESC;

Добавил индекс для сортировки:

    CREATE INDEX idx_orders_created_at ON orders(created_at DESC);

Проверил через EXPLAIN ANALYZE → Время выполнения уменьшилось в 10 раз.

Итог

Оптимизация SQL — это итеративный процесс: анализ → изменение → тестирование. Главное — понимать план выполнения и влияние каждого изменения.

  1. Что делают команды VACUUM, ANALYZE и VACUUM ANALYZE?
Ответ

Команды VACUUM, ANALYZE и VACUUM ANALYZE в PostgreSQL

Эти команды используются для обслуживания базы данных (оптимизация хранения данных и обновление статистики). Рассмотрим каждую из них.

  1. VACUUM

Что делает:

  • Освобождает место, занимаемое "мертвыми" (удаленными или обновленными) строками.

  • Обновляет карту видимости (visibility map), чтобы ускорить запросы.

  • Не блокирует таблицу (работает параллельно с обычными запросами).

Когда использовать:

  • После массовых DELETE или UPDATE.

  • Если таблица занимает больше места, чем нужно.

  • Для предотвращения "раздувания" (bloat) базы данных.

Пример:

VACUUM;          -- Обработать все таблицы в БД  
VACUUM orders;   -- Обработать только таблицу `orders`  

Варианты:

VACUUM FULL – перезаписывает таблицу заново, полностью освобождая место, но блокирует таблицу.

VACUUM VERBOSE – выводит подробный отчет о выполнении.

  1. ANALYZE

Что делает:

  • Собирает статистику о распределении данных в таблицах (например, сколько уникальных значений в столбце).

  • Нужен для оптимизатора запросов, чтобы PostgreSQL выбирал лучший план выполнения.

Когда использовать:

  • После загрузки большого объема данных.

  • Если запросы стали выполняться медленнее без очевидных причин.

Пример:

ANALYZE;           -- Обновить статистику для всех таблиц  
ANALYZE customers; -- Только для таблицы `customers`  
  1. VACUUM ANALYZE

Что делает:

  • Комбинация VACUUM + ANALYZE (сначала очищает "мертвые" строки, затем обновляет статистику).

  • Удобно для регулярного обслуживания БД.

Пример:

VACUUM ANALYZE;          -- Для всей БД  
VACUUM ANALYZE products; -- Для конкретной таблицы  

Разница между VACUUM и VACUUM FULL

Команда | Освобождает место? | Блокирует таблицу? | Скорость VACUUM | Частично (возвращает место ОС не всегда) | Нет | Быстро VACUUM FULL | Полностью (перезаписывает таблицу) Да (блокировка) Медленно

VACOOM - освобождает место частично (возвращает место ОС не всегда), работает быстро, не блокирует таблицу.

VACUUM FULL - освобождает место полностью (перезаписывает таблицу), работает медленно, блокирует таблицу.

Когда использовать VACUUM FULL?

Только если таблица сильно раздута (bloat > 30-40%), и обычный VACUUM не помогает.

Вывод

VACUUM – очищает "мертвые" строки, уменьшает раздутость таблиц.

ANALYZE – обновляет статистику для оптимизатора запросов.

VACUUM ANALYZE – делает оба действия за один проход.

VACUUM FULL – радикальная очистка, но блокирует таблицу.

Для большинства случаев хватает AUTOVACUUM, но ручной вызов нужен после больших изменений данных.

  1. Что такое spill таблиц?
Ответ

Spill (переполнение) — это ситуация, когда СУБД не хватает оперативной памяти (RAM) для выполнения операции (например, сортировки или хэширования), и она вынуждена записывать промежуточные данные на диск (во временные файлы).

Как это происходит?

  • Запрос требует много памяти (например, ORDER BY, GROUP BY, JOIN с большими таблицами).

  • Оперативной памяти не хватает (из-за настроек СУБД или большого объема данных).

  • СУБД начинает использовать диск (вместо RAM), что резко замедляет выполнение запроса.

Как обнаружить spill?

В плане выполнения (EXPLAIN ANALYZE) ищутся ключевые слова:

Disk: ... used (PostgreSQL).

Spill to tempdb (SQL Server).

Temp Files (Oracle).

Пример (PostgreSQL):

EXPLAIN ANALYZE 
SELECT * FROM large_table 
ORDER BY some_column;

Вывод может содержать:

Sort Method: external merge  Disk: 10240kB

Это значит, что сортировка пошла на диск из-за нехватки памяти. Почему это плохо?

  • Диск в сотни раз медленнее RAM → запрос выполняется дольше.

  • Увеличивается нагрузка на I/O (дисковую подсистему).

  • Может приводить к блокировкам (если диск перегружен).

Как избежать spill?

  1. Увеличить память для операций (настройки СУБД):
  • PostgreSQL: work_mem (например, SET work_mem = '64MB';).
  1. Оптимизировать запрос:
  • Добавить LIMIT, если нужны не все данные.

  • Использовать индексы для сортировки/группировки.

  • Разбить сложный запрос на несколько этапов (например, через CTE).

  • Увеличить общий объем RAM сервера БД.

Вывод

Spill — это вынужденная запись данных на диск из-за нехватки оперативной памяти. Он резко замедляет запросы, поэтому его нужно отслеживать через EXPLAIN и предотвращать (настройками памяти или оптимизацией SQL).

  1. Что выведет следующая команда:
    SELECT NULL + 5,
           5 - NULL,
           10 * NULL,
           10 / NULL,
           NULL / 10,
           NULL || 'какая-то прикольная фраза'
Ответ Вернет: | NULL | NULL | NULL | NULL | NULL | NULL |

Почему все операции возвращают NULL?

  1. Арифметические операции (+, -, *, /) с NULL
  • В SQL любая арифметическая операция, где один из операндов — NULL, автоматически возвращает NULL.

  • Это происходит потому, что NULL означает "неизвестное значение", и результат операции с неизвестным тоже неизвестен.

  1. Конкатенация строк (||) с NULL
  • В большинстве СУБД (PostgreSQL, Oracle) конкатенация строки с NULL даёт NULL.

Вывод:

Все операции с NULL возвращают NULL, потому что NULL — это не ноль и не пустая строка, а отсутствие данных. Если нужно заменить NULL на конкретное значение, используйте функции:

  1. Чем отличаются COUNT(*), COUNT(1), COUNT('a'), COUNT(<имя колонки>) и COUNT(DISTINCT <имя колонки>)?
Ответ Вот детальное сравнение всех вариантов COUNT в SQL, включая их отличия, производительность и применение:
  1. COUNT(*)

Что делает: Считает все строки в таблице или группе, включая NULL-записи и дубликаты.

Особенности:

  • Не анализирует содержимое столекций — просто подсчитывает строки.

  • Оптимизирован в большинстве СУБД (PostgreSQL, MySQL, Oracle) как самый быстрый вариант.

Пример:

SELECT COUNT(*) FROM users;  -- Всего строк в таблице
  1. COUNT(1)

Что делает: Аналогичен COUNT(*) — подсчитывает все строки, включая NULL.

Особенности:

  • Внутри создаёт константный столбец со значением 1 для каждой строки и считает его.

  • На практике современные СУБД преобразуют COUNT(1) в COUNT(*), поэтому разницы в скорости нет.

Миф: Раньше считалось, что COUNT(1) быстрее, но это устарело (актуально для старых версий MySQL).

Пример:

SELECT COUNT(1) FROM users;  -- То же, что COUNT(*)
  1. COUNT('a') (или любая константа)

Что делает: Работает так же, как COUNT(1) и COUNT(*) — считает все строки.

Особенности:

  • СУБД игнорирует значение константы ('a', 42, NULL).

  • Нет разницы между COUNT(1), COUNT('abc') или COUNT(NULL).

Пример:

    SELECT COUNT('любой_текст') FROM users;  -- Результат = COUNT(*)
  1. COUNT(<имя_колонки>)

Что делает: Считает только не-NULL значения в указанном столбце.

Особенности:

  • Игнорирует строки, где столбец равен NULL.

  • Медленнее COUNT(*), если столбец не проиндексирован.

Пример:

SELECT COUNT(email) FROM users;  -- Только строки, где email NOT NULL
  1. COUNT(DISTINCT <имя_колонки>)

Что делает: Считает уникальные не-NULL значения в столбце.

Особенности:

  • Затратная операция (особенно на больших данных), так как требует сортировки или хэширования.

  • Нельзя использовать с * (только с конкретным столбцом).

Пример:

SELECT COUNT(DISTINCT country) FROM users;  -- Число уникальных стран
  1. Как посчитать среднее значение? (Вопрос с подвохом)
Ответ В SQL есть стандартная агрегатная функция AVG(), которая вычисляет среднее арифметическое значений столбца.

Пример:

SELECT AVG(price) AS average_price FROM products;

→ Вернет среднюю цену товаров.

Но где подвох?

  1. AVG() игнорирует NULL

Если в столбце есть NULL, они не учитываются в расчетах.

Например, если из 10 записей price = [10, 20, NULL, 30], то AVG() посчитает только (10 + 20 + 30) / 3 = 20, а не 60 / 4 = 15.

Если нужно учесть NULL как 0, используйте:

SELECT AVG(COALESCE(price, 0)) FROM products;
  1. Если все значения NULL → результат NULL
SELECT AVG(NULL) FROM table;  -- Вернет NULL
  1. Целочисленное деление:

В некоторых СУБД (например, PostgreSQL) при вычислении среднего целых чисел (INT) результат тоже будет целым (если не привести к FLOAT):

SELECT AVG(3 + 5) / 2;  -- Может вернуть 4 (вместо 4.0)

Решение:

SELECT AVG(price * 1.0) FROM products;  -- Явное приведение к дробному  
  1. Какими конструкциями дополняется ORDER BY, чтобы значения NULL стояли в начале и в конце таблицы?
Ответ В SQL с помощью ORDER BY можно явно указать, где должны располагаться значения NULL — в начале или в конце результата. Для этого используются дополнительные конструкции:
  1. NULLS FIRST — NULL в начале
SELECT column1, column2
FROM table
ORDER BY column1 NULLS FIRST;

Результат:

Сначала идут строки с NULL в column1.

Затем все остальные значения, отсортированные по умолчанию (ASC/DESC).

  1. NULLS LAST — NULL в конце
SELECT column1, column2
FROM table
ORDER BY column1 NULLS LAST;

Результат:

Сначала идут все не-NULL значения.

В конце — строки с NULL.

  1. Комбинация с сортировкой ASC/DESC

Можно комбинировать с обычной сортировкой:

-- NULL в начале + сортировка по убыванию (DESC)
SELECT name, salary
FROM employees
ORDER BY salary DESC NULLS FIRST;

-- NULL в конце + сортировка по возрастанию (ASC)
SELECT name, salary
FROM employees
ORDER BY salary ASC NULLS LAST;  
  1. Как избавиться от NULL-значений? Как ввести ограничение на вставку NULL-значений в таблице?
Ответ Как избавиться от NULL-значений в существующих данных
  1. Заменить NULL на конкретное значение:
UPDATE table_name 
SET column_name = 'default_value' 
WHERE column_name IS NULL;
  1. Удалить строки с NULL-значениями:
DELETE FROM table_name 
WHERE column_name IS NULL;
  1. Использовать COALESCE или ISNULL в запросах (не меняет данные, только отображение):
SELECT COALESCE(column_name, 'default_value') FROM table_name;
-- или для SQL Server:
SELECT ISNULL(column_name, 'default_value') FROM table_name;

Как запретить NULL-значения при создании таблицы

CREATE TABLE table_name (
    column1 datatype NOT NULL,
    column2 datatype NOT NULL,
    ...
);

Как добавить ограничение NOT NULL к существующему столбцу

Для большинства СУБД:

ALTER TABLE table_name 
ALTER COLUMN column_name datatype NOT NULL;

Для MySQL/MariaDB:
sql

ALTER TABLE table_name 
MODIFY column_name datatype NOT NULL;  
  1. Как разделить значение на 0, чтобы получить в ответе NULL?
Ответ В SQL при обычном делении на ноль возникает ошибка. Чтобы вместо ошибки получить NULL, можно использовать следующие подходы:
  1. Использование CASE выражения
SELECT 
    CASE 
        WHEN denominator = 0 THEN NULL
        ELSE numerator / denominator
    END AS result
FROM your_table;
  1. Использование NULLIF функции (наиболее элегантное решение)
SELECT numerator / NULLIF(denominator, 0) AS result
FROM your_table;

Функция NULLIF возвращает NULL, если два выражения равны. Таким образом, если знаменатель равен 0, NULLIF вернет NULL, и деление на NULL даст NULL.

  1. В чем отличие между группировкой и оконной функцией?
Ответ Группировка и оконные функции решают разные задачи при обработке данных: 1. Группировка (GROUP BY)
  • Сворачивает строки в группы, возвращая по одной строке на группу

  • Применяет агрегатные функции (SUM, COUNT, AVG и т.д.) ко всей группе

  • Уменьшает количество строк в результате

SELECT department_id, COUNT(*) as emp_count, AVG(salary) as avg_salary
FROM employees
GROUP BY department_id;

→ Возвращает одну строку для каждого department_id с агрегированными данными.

  1. Оконные функции (Window Functions)
  • Не сворачивает строки, а добавляет вычисления к каждой строке

  • Сохраняет все исходные строки, не уменьшая их количество

  • Позволяет видеть как агрегированные, так и детальные данные одновременно

SELECT 
    employee_id,
    department_id,
    salary,
    AVG(salary) OVER (PARTITION BY department_id) as avg_dept_salary
FROM employees;

→ Возвращает все строки, но добавляет столбец со средним значением зарплаты по отделу.

Когда что использовать?

✅ GROUP BY – когда нужны итоги (например, общая сумма продаж по категориям). ✅ Оконные функции – когда нужны агрегированные данные без потери деталей (например, сравнение зарплаты сотрудника со средней по отделу).

Пример совместного использования:

SELECT 
    employee_id,
    department_id,
    salary,
    AVG(salary) OVER (PARTITION BY department_id) as avg_dept_salary,
    salary - AVG(salary) OVER (PARTITION BY department_id) as diff_from_avg
FROM employees
WHERE department_id IN (10, 20);

→ Показывает зарплату каждого сотрудника, среднюю по отделу и разницу между ними, не теряя исходные строки.

  1. Чем отличаются результаты запросов:
SELECT col1, COUNT(*) 
    FROM t1 
    GROUP BY col1;

SELECT DISTINCT col1, COUNT(*) OVER(PARTITION BY col2)
    FROM t1
Ответ Запрос 1: Группировка с GROUP BY
SELECT col1, COUNT(*) 
FROM t1 
GROUP BY col1;

Что делает:

  1. Группирует строки таблицы t1 по уникальным значениям столбца col1

  2. Для каждой группы подсчитывает количество строк (COUNT(*))

  3. Возвращает по одной строке на каждую уникальную группу

Результат:

  • Строк в результате = количеству уникальных значений в col1

  • Вторая колонка показывает, сколько строк принадлежит каждой группе

Запрос 2: DISTINCT с оконной функцией

SELECT DISTINCT col1, COUNT(*) OVER(PARTITION BY col1)
FROM t1;

Что делает:

  1. Оконная функция COUNT(*) OVER(PARTITION BY col1) подсчитывает количество строк для каждого значения col1

  2. Вычисляет это значение для каждой строки исходной таблицы

  3. DISTINCT оставляет только уникальные комбинации col1 и результата оконной функции

Результат:

Технически возвращает тот же набор данных, что и первый запрос

Но механизм работы совершенно другой:

Оконная функция сначала вычисляет COUNT для каждой строки, затем DISTINCT удаляет дубликаты

Ключевые отличия:

Механизм выполнения:

  • GROUP BY сначала группирует, затем агрегирует

  • Оконная функция + DISTINCT сначала вычисляет для всех строк, затем удаляет дубли

Производительность:

  • GROUP BY обычно эффективнее для такой задачи

  • Оконная функция с DISTINCT может быть менее оптимальной

    Гибкость:

  • В варианте с оконной функцией можно легко добавить другие колонки без изменения группировки

  • GROUP BY требует включать все неагрегированные колонки в список GROUP BY

Порядок выполнения операций:

GROUP BY: WHERE → GROUP BY → агрегация → HAVING

Оконная функция: WHERE → оконные функции → DISTINCT

Когда результаты будут различаться?

Результаты будут одинаковыми только если:

  • col2 в вашем втором запросе совпадает с col1 (как я исправил в анализе)

  • В таблице нет NULL-значений (для некоторых СУБД группировка и оконные функции по-разному обрабатывают NULL)

Если же оставить оригинальный вариант с PARTITION BY col2, то результаты будут совершенно разными, так как группировка идёт по разным столбцам.

  1. Есть 2 таблицы: в первой 10 строк, во второй 100 строк. Назови максимальное и минимальное количество возвращаемых строк при разных видах JOIN. (Данные могут быть любые.)
Ответ
  1. INNER JOIN
  • Минимум: 0 строк (если нет совпадений).

  • Максимум: 1000 строк (если все 10 строк из A совпадают со всеми 100 строками из B, например, при неуникальных ключах).

  1. LEFT JOIN (A LEFT JOIN B)
  • Минимум: 10 строк (если ни одна строка из A не совпадает с B, все значения из B будут NULL).

  • Максимум: 1000 строк (если каждая строка из A совпадает со всеми 100 строками из B).

  1. RIGHT JOIN (A RIGHT JOIN B)
  • Минимум: 100 строк (если ни одна строка из B не совпадает с A, все значения из A будут NULL).

  • Максимум: 1000 строк (если каждая строка из B совпадает со всеми 10 строками из A).

  1. FULL JOIN (A FULL JOIN B)
  • Минимум: 110 строк (если нет ни одного совпадения, возвращаются все строки из A и B с NULL для несовпадающих полей).

  • Максимум: 1000 строк (если все строки из A совпадают со всеми строками из B).

  1. CROSS JOIN
  • Фиксированное количество строк: 1000 строк (декартово произведение: 10 × 100).
  1. Есть 2 таблицы: в первой 10 строк, во второй 100 строк. Назови максимальное и минимальное количество возвращаемых строк при разных видах JOIN. (Без NULL-значений и индексы не повторяются в рамках одной таблицы, т.е. в двух таблицах данные могут как повторяться, так и нет.)
Ответ Рассмотрим соединения (JOIN) между таблицей A (10 строк) и таблицей B (100 строк), где:
  • Нет NULL-значений в ключах соединения.

  • Внутри одной таблицы индексы уникальны (но могут совпадать между таблицами).

  1. INNER JOIN

Возвращает строки, где ключи совпадают в обеих таблицах.

  • Минимум: 0 строк (если нет совпадений).

  • Максимум: 10 строк (если все ключи из A есть в B, и каждый ключ A соответствует ровно одному ключу B).

  1. LEFT JOIN (A LEFT JOIN B)
  • Возвращает все строки из A, а из B — только совпадающие (или NULL, если нет совпадений).

  • Минимум: 10 строк (если ни один ключ из A не совпадает с B, все значения из B будут NULL).

  • Максимум: 10 × 100 = 1000 строк (если один ключ из A совпадает со всеми ключами B, но это противоречит условию уникальности в B).

Уточнение: Если в A ключи уникальны, а в B — тоже, то максимум 10 строк (каждый ключ A соответствует не более чем одному ключу B).

  1. RIGHT JOIN (A RIGHT JOIN B)

Аналогично LEFT JOIN, но возвращает все строки из B, а из A — только совпадающие.

  • Минимум: 100 строк (если ни один ключ из B не совпадает с A, все значения из A будут NULL).

  • Максимум: 100 строк (если каждый ключ B соответствует не более чем одному ключу A).

  1. FULL JOIN (A FULL JOIN B)

Возвращает все строки из A и B, с NULL при отсутствии совпадений.

  • Минимум: 10 + 100 = 110 строк (если нет ни одного совпадения).

  • Максимум: 10 + 100 - min(10, 100) = 100 строк (если все ключи A есть в B, и наоборот).

  1. CROSS JOIN

Декартово произведение: каждая строка A соединяется с каждой строкой B.

Фиксированное количество строк: 10 × 100 = 1000.

  1. В таблице 100 млн строк, необходимо удалить 90 млн. Как ты это сделаешь и почему?
Ответ
  1. Лучший вариант: CREATE TABLE AS SELECT + замена (для минимального времени простоя)
-- Шаг 1: Создаем новую таблицу только с нужными 10 млн строк
CREATE TABLE new_table AS
SELECT * FROM original_table 
WHERE условие_для_сохранения_10млн_строк;

-- Шаг 2: Создаем индексы и ограничения (если были)
CREATE INDEX idx_new_table ON new_table(column);
ALTER TABLE new_table ADD CONSTRAINT pk_primary_key PRIMARY KEY (id);

-- Шаг 3: Переименовываем таблицы (мгновенная операция)
BEGIN TRANSACTION;
    DROP TABLE original_table;  -- или RENAME TO original_table_old
    ALTER TABLE new_table RENAME TO original_table;
COMMIT;

Почему?

  • Не блокирует таблицу на долгое время (кроме финального переименования).

  • Минимальный объем журнала транзакций.

  • Не фрагментирует данные, как DELETE.

  1. Альтернатива: DELETE с пакетной обработкой (если нельзя создать новую таблицу)
-- Удаляем партиями по 10-50 тыс. строк
WHILE EXISTS (SELECT 1 FROM original_table WHERE условие_для_удаления)
BEGIN
    DELETE TOP (50000) FROM original_table 
    WHERE условие_для_удаления;
    
    CHECKPOINT;  -- Для SQL Server (сброс журнала)
    COMMIT;
END;

Почему пакетами?

  • Избегаем блокировок всей таблицы.

  • Снижаем нагрузку на журнал транзакций.

  • Можно прервать процесс без отката всей операции.

  1. Для PostgreSQL: Использование TRUNCATE + частичная вставка
-- Если можно быстро выделить 10 млн строк для сохранения
CREATE TEMP TABLE saved_rows AS 
SELECT * FROM original_table WHERE условие_сохранения;

TRUNCATE original_table;  -- Мгновенное удаление всех строк

INSERT INTO original_table 
SELECT * FROM saved_rows;

Почему?

TRUNCATE работает мгновенно (не сканирует строки).

Критические факторы выбора метода:

  • Доступное окно обслуживания – если downtime допустим, лучше CREATE TABLE AS.

  • Наличие индексов/ограничений – их пересоздание может занять время.

  • Свободное место на диске – метод с копированием требует 2x места.

Триггеры и репликация – могут усложнить DELETE пакетами.

Совет: Перед операцией сделайте бэкап и проверьте план выполнения на тестовом сервере!

  1. Является ли следующая команда транзакцией?
SELECT * FROM t1 WHERE id > 5;
Ответ Нет, команда SELECT * FROM t1 WHERE id > 5; не является транзакцией в классическом понимании.

Почему?

  • Транзакция требует явного начала (BEGIN TRANSACTION / START TRANSACTION) и завершения (COMMIT / ROLLBACK).

  • SELECT без обертки в транзакцию выполняется как одиночный автономный запрос (autocommit mode).

Он не изменяет данные и не требует отката или подтверждения.

Когда SELECT может быть частью транзакции?

Только если он явно включен в блок:

BEGIN TRANSACTION;
    SELECT * FROM t1 WHERE id > 5;  -- Теперь это часть транзакции
    UPDATE t1 SET col1 = 10 WHERE id = 6;  -- Изменение данных
COMMIT;

Исключения (зависит от СУБД)

  • В PostgreSQL даже простой SELECT создает "неявную транзакцию" на время выполнения, но она не имеет смысла без изменяющих операций.
  1. Есть запрос, который работает ночью и строит отчет. Ежедневно он работал нормально и создавал отчет за 2 часа. Сегодня утром ты пришел на работу, а отчета нет. Смотришь свой пайплайн, а он все еще крутится на чтении запроса. Что могло произойти?
Ответ Почему запрос завис?
  1. Кто-то заблокировал данные

Другой запрос или процесс заблокировал таблицу, и ваш отчет не может прочитать данные.

Что делать: Найти и завершить блокирующую операцию.

  1. Запрос стал работать медленнее

В таблице появилось больше данных, или оптимизатор выбрал плохой план выполнения.

Что делать: Проверить, использует ли запрос индексы, перестроить статистику.

  1. Не хватает ресурсов

Сервер перегружен (закончилась память, процессор на 100%, медленный диск).

Что делать: Проверить нагрузку на сервер, освободить ресурсы.

  1. Сетевые проблемы

Соединение с базой оборвалось, но запрос "висит" в ожидании.

Что делать: Перезапустить запрос или увеличить таймауты.

5.Ошибка в логике запроса

Например, случайный CROSS JOIN, из-за которого запрос обрабатывает миллиарды строк.

Что делать: Проверить план выполнения (EXPLAIN).

Как быстро починить?

  1. Отменить зависший запрос (если он уже неадекватно долго работает).

  2. Проверить блокировки и завершить мешающие процессы.

  3. Запустить упрощенную версию (например, с LIMIT 1000), чтобы понять, работает ли запрос вообще.

  4. Разбить отчет на части (если данных стало слишком много).

Если проблема повторяется — надо разбираться с оптимизацией запроса или сервера.

  1. Как быстро посчитать количество строк в таблице, имеющей нормальную структуру, например, в 3 НФ, где много колонок и пусть в ней даже JSON'овский сырец хранится? Структура таблицы пусть будет следующая:
CREATE TABLE t1 
(
    id int primary key,
    col1 text,
    FK_id int NOT NULL,
    col2 text,
    col3 text,
    ....
    col100 jsonb
)
Ответ

Для таблицы с нормальной структурой (3НФ) и большим количеством колонок, включая JSON-данные, есть несколько оптимальных подходов:

  1. Самый быстрый вариант (использует метаданные)
SELECT reltuples::bigint AS estimate 
FROM pg_class 
WHERE relname = 't1';

Плюсы: Мгновенный результат (0.1 мс)

Минусы: Приблизительная оценка (точность ~95-99%)

Когда использовать: Когда допустима небольшая погрешность

  1. Точно и достаточно быстро (использует первичный ключ)
SELECT COUNT(id) FROM t1;

Плюсы: Точно и быстрее COUNT(*)

Минусы: Медленнее метаданных, но быстрее полного сканирования

Оптимизация: Если id - первичный ключ, запрос будет использовать самый узкий индекс

  1. Альтернатива для очень больших таблиц
/* Используем ONLY для избежания сканирования дочерних таблиц (если есть наследование) */
SELECT COUNT(*) FROM ONLY t1;
  1. Параллельный подсчёт (PostgreSQL 9.6+)
SET max_parallel_workers_per_gather = 4;
SELECT COUNT(*) FROM t1;

Почему COUNT(id) лучше COUNT(*)?

В вашем случае:

  • COUNT(*) потребует полного сканирования таблицы со всеми 100+ колонками

  • COUNT(id) использует индекс первичного ключа (гораздо уже)

Если нужна максимальная производительность:

  1. Сначала получить оценку из pg_class

  2. Если нужна точность - запустить COUNT(id)

  3. Для мониторинга использовать приблизительные значения

Для таблицы на 100+ млн строк:

  • Оценка из pg_class: 0.1 мс

  • COUNT(id): 1-10 сек (в зависимости от сервера)

  • COUNT(*): 10-100 сек (из-за чтения всех колонок)

  1. Что такое и для чего нужен подзапрос? Что такое коррелируемый и не коррелируемый подзапрос? В чем отличиие СТЕ от подзапроса?
Ответ Скрытый текст или Markdown-контент.
  1. Для чего нужна временная таблица, если есть CTE?
Ответ Скрытый текст или Markdown-контент.
  1. Для чего нужна команда UNION и в чем её различие с UNION ALL? Какая команда работает быстрее и почему? Какие еще операции над множествами ты знаешь и что они делают?
Ответ Скрытый текст или Markdown-контент.
  1. Расскажи про условия в SQL. Для чего нужна конструкция CASE, как она записывается и в каких конструкциях запроса её можно использовать?
Ответ Скрытый текст или Markdown-контент.
  1. Различие между конструкциями WHERE, HAVING, QUALIFY?
Ответ Скрытый текст или Markdown-контент.
  1. Индексы в SQL
Ответ Скрытый текст или Markdown-контент.

Airflow

Image

  1. Что такое Apache Airflow и для каких задач он используется?

  2. Какие основные компоненты Airflow (Scheduler, Worker, Webserver, Executor)?

  3. Что такое DAG (Directed Acyclic Graph) и как он описывается в Airflow?

  4. Чем отличается Celery Executor от local executor?

  5. Какие операторы (Operators) вы знаете? Чем отличается BashOperator от PythonOperator?

  6. Какая база данных используется в Airflow?

  7. Что такое XCom (Cross-Communication) и как передавать данные между задачами?

  8. Как настроить зависимости между задачами (set_upstream, set_downstream, >>, <<)?

  9. Что такое Backfilling и как его выполнить в Airflow?

  10. Как работают триггеры (Trigger Rules): all_success, one_failed, none_failed?

  11. Какие метрики (Metrics) можно отслеживать в Airflow (Prometheus, StatsD)?

  12. Как дебажить упавшую задачу?

  13. Как оптимизировать производительность Airflow (параллелизм, настройка Executor)?

  14. Что такое DAG Serialization и зачем оно нужно?

  15. Как бороться с "зомби-задачами" (Zombie Tasks)?

  16. Как создать кастомный оператор (Custom Operator)?

  17. Что такое Dynamic DAG Generation и когда его применять?

  18. Как использовать Task Groups для организации сложных DAG?

  19. Как работает SubDAG и почему его не рекомендуют использовать?

  20. Как настроить High Availability (HA) для Airflow

Hadoop (HDFS/YARN/MapReduce)

Image

  1. Что такое Hadoop? Какие основные компоненты входят в его экосистему?

HDFS

  1. Cравните системы управления реляционными базами данных с HDFS (распределённой файловой системой Hadoop)?

  2. В чем разница между HDFS и обычной файловой системой?

  3. Объясните архитектуру HDFS. Почему она подходит для больших данных?

  4. Какие проблемы решает Hadoop (масштабируемость, отказоустойчивость и т. д.)?

  5. Что такое NameNode, DataNode и Secondary NameNode?

  6. В каких режимах работает Apache Hadoop?

  7. Что такое HDFS блоки и какие у них есть минусы?

  8. Как бороться с маленькими файлами в HDFS? Переполнение NameNode.

  9. Как работает репликация данных в HDFS? Можно ли изменить коэффициент репликации?

  10. Что если DataNode упадет?

  11. Что произойдёт, если два пользователя попытаются получить доступ к одному и тому же файлу в HDFS?

  12. Пожалуйста, объясните, как в HDFS достигается отказоустойчивость?

  13. Как в Hadoop пропускать или обрабатывать поврежденные или некорректные данные?

  14. Какие 2 ключевых типа метадаты храняться на NameNode?

  15. Как работает кэширование в Hadoop (HDFS, Hive, Spark)?

MapReduce

  1. Объясните принцип работы MapReduce (фазы: Map, Shuffle & Sort, Reduce).

  2. Что такое InputSplit и как он связан с Mapper?

  3. Как Hadoop обрабатывает пропущенные или битые записи (bad records)?

  4. Чем отличается Combiner от Reducer?

  5. Как работает Partitioner в MapReduce?

  6. Что такое Speculative Execution и зачем оно нужно?

  7. Как можно оптимизировать производительность MapReduce-задач?

YARN

  1. Что такое YARN и какова его роль в Hadoop?

  2. Какие компоненты входят в YARN (ResourceManager, NodeManager, ApplicationMaster)?

  3. Как YARN управляет ресурсами в кластере?

  4. В чем разница между FIFO, Fair Scheduler и Capacity Scheduler?

More

  1. Hadoop CLI

  2. Различия между Hadoop 2.x и 3.x

  3. Что такое Apache Sqoop?

Hive

Image


  1. Что такое Hive и чем он отличается от традиционных СУБД?

  2. Как Hive преобразует SQL-запросы в MapReduce/Tez/Spark?

  3. Что такое Hive Metastore и зачем он нужен?

  4. Какие типы таблиц поддерживает Hive (Managed vs External tables)?

  5. Как работают партиции (Partitions) и бакеты (Bucketing) в Hive?

  6. Чем отличается HiveQL от стандартного SQL?

  7. Как можно оптимизировать запросы в Hive?

Impala

Image

  1. Что такое Apache Impala и чем он отличается от Hive?

  2. Какие преимущества дает Impala по сравнению с традиционными SQL-движками в Hadoop?

  3. В чем разница между Impala, Hive-on-Tez/Spark и Presto?

  4. Как Impala обеспечивает интерактивные SQL-запросы на больших данных?

  5. Из каких основных компонентов состоит архитектура Impala (Impala Daemon, Catalog Service, Statestore)?

  6. Как работает распределенное выполнение запросов в Impala?

  7. Какой роль играет Statestore в Impala?

  8. Как Catalog Service синхронизирует метаданные между узлами?

  9. Почему Impala не использует MapReduce?

  10. Как Impala достигает высокой скорости выполнения запросов?

  11. Какие механизмы кэширования поддерживает Impala?

  12. Как работает Runtime Code Generation (LLVM) в Impala?

  13. Как партиционирование и бакетирование влияют на производительность Impala?

  14. Какие форматы файлов лучше всего подходят для Impala (Parquet, Avro, ORC)?

  15. Как можно оптимизировать сложные JOIN-запросы в Impala?

  16. Как Impala взаимодействует с HDFS?

  17. Как Impala работает с Hive Metastore?

  18. Поддерживает ли Impala транзакции и ACID (как Hive 3.0+)?

  19. Как проверить план выполнения запроса (EXPLAIN) в Impala?

  20. Какие инструменты мониторинга подходят для Impala (Cloudera Manager, Grafana)?

  21. Как бороться с "маленькими файлами" при работе с Impala?

  22. Как обновить статистику таблиц (COMPUTE STATS) и зачем это нужно?

  23. Какие ограничения есть в Impala (например, по размеру данных, типам запросов)?

  24. В каких случаях выбрать Impala, а в каких Hive-on-Spark?

Spark

Image

  1. Почему нельзя использовать Pandas для больших данных, а нужно использовать Spark?

  2. Объясните архитектуру Spark (Driver, Executor, Cluster Manager).

  3. Чем Spark отличается от MapReduce?

  4. Минимальное параллелизм в Spark и что это такое?

  5. Что такое RDD в Spark?

  6. Что такое линия кровности (lineage) в RDD и как она обеспечивает отказоустойчивость?

  7. Что такое Dataset и чем отличается от dataframe и RDD?

  8. Как Spark SQL оптимизирует запросы (например, через Catalyst Optimizer)?

  9. Как выполнить JOIN двух DataFrame и какие стратегии соединения поддерживает Spark?

  10. Как работает Tungsten Engine и какие преимущества он дает?

  11. Какие виды кэширования существуют в Spark и чем они отличаются?

  12. Что такое persist в Spark и какие storage levels существуют?

  13. Чем отличается repartition от coalesce в Spark?

  14. Как работает широкое преобразование (wide transformation) и узкое преобразование (narrow transformation)?

  15. Какие настройки Spark applications вы используете?

  16. Сколько гигабайт памяти выделяется на каждую задачу в Spark?

  17. Что такое spill в Spark?

  18. Что такое broadcast join в Spark и как его настроить?

  19. Что такое ленивые вычисления в Spark?

  20. Что такое Adaptive query execution?

  21. Как прочитать данные из PostgreSQL в Spark?

  22. Как записать DataFrame в партиционированную Hive-таблицу?

  23. Как реализовать оконные функции (window functions) в Spark SQL?

  24. Как настроить сериализацию Kryo и зачем она нужна?

ClickHouse

Image

  1. Как работает ClickHouse?

  2. Что такое колоночная база данных, в чем ее преимущества и недостатки?

  3. Основные движки ClickHouse

About

Вопросы по моим основным навыкам

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors