# Базовый SQL: SELECT, WHERE по id и простая фильтрация<no value>

## Что мы изучим?

**`SELECT`** = **R**ead из CRUD: чтение данных из таблицы.

- **Все строки**: `SELECT * FROM students`.
- **Выбранные столбцы**: `SELECT name, email FROM students`.
- **Одну строку по id**: `SELECT ... WHERE id = 2`.
- **Простую фильтрацию** с `WHERE` и условиями.
- **Сортировку** с `ORDER BY`.

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

Два самых важных запроса в любом API:

| Endpoint | SQL |
|---|---|
| `GET /students` | `SELECT ... FROM students` |
| `GET /students/7` | `SELECT ... FROM students WHERE id = 7` |

Каждый раз, когда вы открываете список в приложении (товары, сообщения, заказы), за кулисами выполняется запрос SELECT.

## Объяснение с основ

### SELECT ... FROM

```sql
SELECT name, age FROM students;
```

«**Выбери** столбцы `name` и `age` **из** таблицы `students`». Результат — **новая (временная) таблица** с запрошенными строками и столбцами. База **не меняется**: SELECT только читает, точно как GET.

- `*` (звёздочка) = **все столбцы**.
- Порядок в списке задаёт порядок столбцов в результате.

### WHERE — фильтрация строк

```sql
SELECT name, age FROM students WHERE age > 23;
```

`WHERE` = «только строки, для которых условие **истинно**». База проверяет условие на каждой строке и возвращает только подходящие.

| Оператор | Значение | Пример |
|---|---|---|
| `=` | Равно (**один знак!**, не `==`) | `WHERE id = 2` |
| `<>` (или `!=`) | Не равно | `WHERE name <> 'Avi'` |
| `>` `<` `>=` `<=` | Сравнение | `WHERE age >= 18` |
| `AND` | Оба условия | `WHERE age > 20 AND age < 30` |
| `OR` | Хотя бы одно | `WHERE name = 'Dana' OR name = 'Noa'` |

### WHERE id — самое важное

`id` — первичный ключ, уникальный, поэтому **`WHERE id = 2` возвращает не больше одной строки**. Это выборка для `GET /students/{student_id}`. И база находит её очень быстро, даже среди миллионов строк (потому что первичный ключ автоматически индексируется).

### ORDER BY — сортировка

```sql
SELECT name, age FROM students ORDER BY age;
SELECT name, age FROM students ORDER BY age DESC;
```

По умолчанию порядок возрастающий (`ASC`). `DESC` = убывающий. **Без `ORDER BY` порядок не гарантирован**, поэтому, если порядок важен, просите его явно.

{{< box "analogy" "Аналогия: библиотекарь" >}}
`SELECT name, age FROM students` = «принесите мне имя и возраст всех студентов». `WHERE age > 23` = «только тех, кто старше 23». `ORDER BY age` = «упорядоченных по возрасту». `WHERE id = 2` = «папку номер 2». Вы говорите, **что** нужно, а **как** это найти — решает библиотекарь (декларативный SQL).
{{< /box >}}

## Синтаксис

```sql
SELECT * FROM table_name;                                 -- all columns, all rows
SELECT col_a, col_b FROM table_name;                      -- chosen columns
SELECT col_a, col_b FROM table_name WHERE id = 3;         -- one row by primary key
SELECT col_a FROM table_name WHERE col_b > 10 AND col_c = 'x';
SELECT col_a FROM table_name ORDER BY col_b DESC;
```

`--` в начале текста в SQL = **комментарий** (как `#` в Python).

## Первый пример кода

Таблица из предыдущего урока:

```text
id | name | age | email
---+------+-----+--------------
 1 | Dana |  24 | dana@mail.com
 2 | Avi  |  31 | avi@mail.com
 3 | Noa  |  22 | noa@mail.com
```

Выполните в Database Client (в файле `.sql`: выделить → Run):

```sql
SELECT * FROM students;
```

**Результат:**

```text
id | name | age | email
 1 | Dana |  24 | dana@mail.com
 2 | Avi  |  31 | avi@mail.com
 3 | Noa  |  22 | noa@mail.com
```

```sql
SELECT name, email FROM students;
```

```text
name | email
Dana | dana@mail.com
Avi  | avi@mail.com
Noa  | noa@mail.com
```

```sql
SELECT id, name, age, email FROM students WHERE id = 2;
```

```text
id | name | age | email
 2 | Avi  |  31 | avi@mail.com
```

## Разбор кода построчно

| Запрос | Что сделала база |
|---|---|
| `SELECT * FROM students` | Все столбцы, все строки. |
| `SELECT name, email FROM students` | Все строки, но только два столбца, в запрошенном порядке. |
| `SELECT id, name, age, email ... WHERE id = 2` | Прошла по строкам (на практике — сразу к id 2 благодаря первичному ключу) и вернула **только** строку, где `id = 2`. |

**Обратите внимание:** в третьем запросе мы явно написали список столбцов, а не `*`. Эту привычку мы будем использовать в коде: если в будущем в таблицу добавится столбец (возможно, с чувствительной информацией, например паролем), он не «просочится» по ошибке в ответы API.

## Ещё один пример

**Фильтрация, составные условия, сортировка и пустой результат.**

```sql
SELECT name, age FROM students WHERE age > 23;
```

```text
name | age
Dana |  24
Avi  |  31
```

```sql
SELECT name, age FROM students WHERE age > 20 AND age < 30 ORDER BY age;
```

```text
name | age
Noa  |  22
Dana |  24
```

```sql
SELECT name FROM students ORDER BY name;
```

```text
name
Avi
Dana
Noa
```

```sql
SELECT id, name FROM students WHERE id = 99;
```

```text
(no rows)
```

**Разбор:**

- **`age > 23`** — Дана (24) и Ави (31). Ноа (22) не прошла условие.
- **`AND` + `ORDER BY`** — сначала фильтрация (только от 20 до 30), а затем сортировка оставшегося. **Порядок в запросе фиксирован:** `SELECT ... FROM ... WHERE ... ORDER BY ...`.
- **`ORDER BY name`** — сортировка текста по алфавиту.
- **`WHERE id = 99`** — такой строки нет. **Это не ошибка!** Результат просто пуст: 0 строк. В Python `fetchone()` вернёт `None`, а `fetchall()` — пустой список. **В API это превратится в 404.**

## Типичная ошибка

**Ошибка 1: `==` вместо `=`.**

```sql
SELECT * FROM students WHERE id == 2;
```

```text
1064 (42000): You have an error in your SQL syntax; ... near '== 2' at line 1
```

**В SQL сравнение — один `=`.** (И это противоположно Python, где `=` — присваивание!)

**Ошибка 2: строка без кавычек.**

```sql
SELECT * FROM students WHERE name = Avi;
```

```text
1054 (42S22): Unknown column 'Avi' in 'where clause'
```

Без кавычек SQL думает, что `Avi` — **имя столбца** (точно как `print(Hello)` в Python ищет переменную). → `WHERE name = 'Avi'`.

**Ошибка 3: неправильный порядок частей.**

```sql
SELECT name FROM students ORDER BY age WHERE age > 20;
```

```text
1064 (42000): You have an error in your SQL syntax; ... near 'WHERE age > 20' at line 1
```

Порядок: `SELECT` → `FROM` → `WHERE` → `ORDER BY`.

**Ошибка 4: считать пустой результат ошибкой — или наоборот.** `WHERE id = 99`, который ничего не нашёл, возвращает **0 строк** без исключения. Проверить («не найдено») и вернуть 404 должен наш код. Если не проверить, `None[0]` упадёт.

**Ошибка 5: считать, что сравнение текста чувствительно к регистру.** В MySQL по умолчанию `WHERE name = 'avi'` **находит** `Avi` — сравнение **не** различает заглавные и строчные буквы. Это удобно для поиска, но у этого есть цена: с `UNIQUE` на `email` база откажется сохранить `Dana@mail.com`, если `dana@mail.com` уже существует. (И это отличается от Python, где `"avi" == "Avi"` — `False`!)

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Практика

Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.

### Упражнение 1: Базовый SELECT

Во всех упражнениях этого урока сначала создайте таблицу и данные:

```py
import mysql.connector

connection = mysql.connector.connect(
    host="localhost", user="root", password="kodkod", database="school")
cursor = connection.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS students (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL, age INT NOT NULL, city VARCHAR(50))""")
cursor.execute("""
INSERT INTO students (name, age, city) VALUES
    ('Dana', 24, 'Haifa'), ('Avi', 31, 'Tel Aviv'),
    ('Noa', 22, 'Haifa'), ('Tom', 28, NULL)""")
connection.commit()
```

Напишите запрос, который возвращает имя и возраст **всех** студентов, и напечатайте каждую строку.

**Ожидаемый вывод:**

```text
('Dana', 24)
('Avi', 31)
('Noa', 22)
('Tom', 28)
```

{{< solution >}}
```python
import mysql.connector

connection = mysql.connector.connect(
    host="localhost", user="root", password="kodkod", database="school")
cursor = connection.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS students (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL, age INT NOT NULL, city VARCHAR(50))""")
cursor.execute("""
INSERT INTO students (name, age, city) VALUES
    ('Dana', 24, 'Haifa'), ('Avi', 31, 'Tel Aviv'),
    ('Noa', 22, 'Haifa'), ('Tom', 28, NULL)""")
connection.commit()

cursor.execute("SELECT name, age FROM students")
for row in cursor.fetchall():
    print(row)
connection.close()
```

```text
('Dana', 24)
('Avi', 31)
('Noa', 22)
('Tom', 28)
```

`SELECT name, age` — только нужные столбцы. `SELECT *` возвращает все.
{{< /solution >}}

### Упражнение 2: WHERE по id

С теми же данными получите студента, у которого `id` равен 2, и студента, у которого `id` равен 99.

**Ожидаемый вывод:**

```text
(2, 'Avi', 31, 'Tel Aviv')
None
```

{{< solution >}}
```python
import mysql.connector

connection = mysql.connector.connect(
    host="localhost", user="root", password="kodkod", database="school")
cursor = connection.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS students (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL, age INT NOT NULL, city VARCHAR(50))""")
cursor.execute("""
INSERT INTO students (name, age, city) VALUES
    ('Dana', 24, 'Haifa'), ('Avi', 31, 'Tel Aviv'),
    ('Noa', 22, 'Haifa'), ('Tom', 28, NULL)""")
connection.commit()

cursor.execute("SELECT * FROM students WHERE id = 2")
print(cursor.fetchone())
cursor.execute("SELECT * FROM students WHERE id = 99")
print(cursor.fetchone())
connection.close()
```

```text
(2, 'Avi', 31, 'Tel Aviv')
None
```

**Пустой результат — не ошибка**: `fetchone()` возвращает `None`, когда такой строки нет.
{{< /solution >}}

### Упражнение 3: Фильтрация и сортировка

С теми же данными: получите имена студентов из Хайфы младше 30 лет и всех студентов, отсортированных по возрасту от старшего к младшему.

**Ожидаемый вывод:**

```text
[('Dana',), ('Noa',)]
[('Avi', 31), ('Tom', 28), ('Dana', 24), ('Noa', 22)]
```

{{< solution >}}
```python
import mysql.connector

connection = mysql.connector.connect(
    host="localhost", user="root", password="kodkod", database="school")
cursor = connection.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS students (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL, age INT NOT NULL, city VARCHAR(50))""")
cursor.execute("""
INSERT INTO students (name, age, city) VALUES
    ('Dana', 24, 'Haifa'), ('Avi', 31, 'Tel Aviv'),
    ('Noa', 22, 'Haifa'), ('Tom', 28, NULL)""")
connection.commit()

cursor.execute("SELECT name FROM students WHERE city = 'Haifa' AND age < 30")
print(cursor.fetchall())
cursor.execute("SELECT name, age FROM students ORDER BY age DESC")
print(cursor.fetchall())
connection.close()
```

```text
[('Dana',), ('Noa',)]
[('Avi', 31), ('Tom', 28), ('Dana', 24), ('Noa', 22)]
```

Каждая строка — tuple, даже когда в ней один столбец: `('Dana',)`.
{{< /solution >}}

### Упражнение 4: NULL

С теми же данными: у какого студента нет города? Напишите два запроса — один с `city = NULL` и один с `city IS NULL` — и объясните разницу.

**Ожидаемый вывод:**

```text
[]
[('Tom',)]
```

{{< solution >}}
```python
import mysql.connector

connection = mysql.connector.connect(
    host="localhost", user="root", password="kodkod", database="school")
cursor = connection.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS students (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL, age INT NOT NULL, city VARCHAR(50))""")
cursor.execute("""
INSERT INTO students (name, age, city) VALUES
    ('Dana', 24, 'Haifa'), ('Avi', 31, 'Tel Aviv'),
    ('Noa', 22, 'Haifa'), ('Tom', 28, NULL)""")
connection.commit()

cursor.execute("SELECT name FROM students WHERE city = NULL")
print(cursor.fetchall())
cursor.execute("SELECT name FROM students WHERE city IS NULL")
print(cursor.fetchall())
connection.close()
```

```text
[]
[('Tom',)]
```

`NULL` означает «нет значения», поэтому `city = NULL` никогда не истинно — даже для Тома. Отсутствующее значение проверяют **только** через `IS NULL`.
{{< /solution >}}

### Упражнение 5: Запросы к товарам

Напишите запросы SQL к таблице `products` (name, price, stock) и выполните их в Database Client:

1. Названия товаров, запас которых меньше 5.
2. Все товары, отсортированные по цене от самого дешёвого к самому дорогому, — название и цена.
3. Все товары, отсортированные по цене от самого дорогого к самому дешёвому.

{{< solution >}}
```sql
SELECT name FROM products
WHERE stock < 5;

SELECT name, price FROM products
ORDER BY price;

SELECT name, price FROM products
ORDER BY price DESC;
```

- `ORDER BY` по умолчанию сортирует от меньшего к большему. `DESC` меняет порядок на обратный.
{{< /solution >}}

<!-- exercises:end -->

## Итоги

- **`SELECT cols FROM table`** — чтение. `*` = все столбцы, но в коде лучше перечислять их явно.
- **`WHERE condition`** — только подходящие строки. `=` (один знак), `<>`, `>`, `<`, `AND`, `OR`. Строки — в `'...'`.
- **`WHERE id = 2`** — не больше одной строки, по первичному ключу. Это `GET /students/{id}`.
- **`ORDER BY col [DESC]`** — сортировка. Без ORDER BY порядок не гарантирован.
- Порядок частей: **SELECT → FROM → WHERE → ORDER BY**.
- **Пустой результат — не ошибка.** Код проверяет его (`None` / `[]`) и возвращает 404. Для NULL используйте `IS NULL`.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Официальная документация

- [MySQL — SELECT](https://dev.mysql.com/doc/refman/8.4/en/select.html)
- [MySQL Tutorial — Selecting Particular Rows](https://dev.mysql.com/doc/refman/8.4/en/selecting-rows.html)

<!-- docs:end -->
