# 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` הוא Primary Key, ייחודי, ולכן **`WHERE id = 2` מחזיר לכל היותר שורה אחת**. זו השליפה של `GET /students/{student_id}`. והמסד מוצא אותה מהר מאוד, גם מתוך מיליוני שורות (כי Primary Key מאונדקס אוטומטית).

### 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 בזכות ה-Primary Key), והחזיר **רק** את השורה שבה `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` (נושא מסדי נתונים ו-MySQL!), ו-`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)` בנושא מחשב, מערכת הפעלה ורשת). ‏←‏ `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 שורות**, בלי Exception. הקוד שלנו הוא שצריך לבדוק ("לא נמצא") ולהחזיר 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`** — שורה אחת לכל היותר, לפי Primary Key. זה `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) — WHERE

<!-- docs:end -->
