Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 4 из 10 Средний 40 мин 120 XP

WHERE и фильтрация в SQL: условия, LIKE, BETWEEN и IN

Главный фильтр SQL: сравнения и логика с AND, OR, NOT, шаблоны LIKE с % и _, диапазоны BETWEEN, списки IN — и классическая ловушка = NULL.

Редакция Питоники

До этого урока SELECT возвращал всё подряд — пять строк, десять, сто тысяч. Настоящие запросы почти всегда с фильтром: товары дешевле тысячи, книги Чехова, заказы за март. Фильтр в SQL — предложение WHERE, и это самое часто используемое слово языка после SELECT. Разберём его по всем правилам: сравнения, логика, шаблоны, диапазоны и загадочные NULL.

Как устроена наша учебная таблица?

Чтобы результаты фильтров были осязаемыми, весь урок живёт на одной таблице книг: пять строк с названием, автором, годом и ценой. Каждый блок ниже создаёт её заново — база в песочнице с :memory: живёт от запуска до запуска блока, так что примеры можно менять без оглядки на соседние.

учебная база: пять книг
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("""
    CREATE TABLE books (
        id     INTEGER PRIMARY KEY,
        title  TEXT,
        author TEXT,
        year   INTEGER,
        price  REAL
    )
""")
cur.executemany(
    "INSERT INTO books (title, author, year, price) VALUES (?, ?, ?, ?)",
    [
        ("Мастер и Маргарита", "Булгаков", 1967, 850.0),
        ("Собачье сердце", "Булгаков", 1925, 450.0),
        ("Преступление и наказание", "Достоевский", 1866, 600.0),
        ("Идиот", "Достоевский", 1869, 620.0),
        ("Вишнёвый сад", "Чехов", 1903, 390.0),
    ],
)

cur.execute("SELECT title, author, year, price FROM books")
for row in cur.fetchall():
    print(row)
conn.close()
Вывод
('Мастер и Маргарита', 'Булгаков', 1967, 850.0)
('Собачье сердце', 'Булгаков', 1925, 450.0)
('Преступление и наказание', 'Достоевский', 1866, 600.0)
('Идиот', 'Достоевский', 1869, 620.0)
('Вишнёвый сад', 'Чехов', 1903, 390.0)

Как работает WHERE на простом сравнении?

Механика проста: движок берёт каждую строку таблицы и подставляет её значения в условие. Условие — выражение, которое даёт истину, ложь или NULL. Строка прошла проверку — летит в выдачу, не прошла — отброшена. Сравнивать можно колонку с числом, колонку со строкой и даже колонку с колонкой.

WHERE с простыми сравнениями
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, author TEXT, year INTEGER, price REAL)")
cur.executemany(
    "INSERT INTO books (title, author, year, price) VALUES (?, ?, ?, ?)",
    [
        ("Мастер и Маргарита", "Булгаков", 1967, 850.0),
        ("Собачье сердце", "Булгаков", 1925, 450.0),
        ("Преступление и наказание", "Достоевский", 1866, 600.0),
        ("Идиот", "Достоевский", 1869, 620.0),
        ("Вишнёвый сад", "Чехов", 1903, 390.0),
    ],
)

cur.execute("SELECT title FROM books WHERE year > 1900")
print("year > 1900      :", cur.fetchall())

cur.execute("SELECT title, price FROM books WHERE price <= 450.0")
print("price <= 450     :", cur.fetchall())

cur.execute("SELECT title, year FROM books WHERE author = 'Чехов'")
print("author = Чехов   :", cur.fetchall())
conn.close()
Вывод
year > 1900      : [('Мастер и Маргарита',), ('Собачье сердце',), ('Вишнёвый сад',)]
price <= 450     : [('Собачье сердце', 450.0), ('Вишнёвый сад', 390.0)]
author = Чехов   : [('Вишнёвый сад', 1903)]

Три оператора на витрине: строгое неравенство >, нестрогое <= (строка с ценой 450.0 попала — граница включается) и равенство =. Да, в SQL равенство — один знак =, а не двойной, как в Python; неравенство пишется <> или !=. Равенство строк честное: 'Чехов' и 'чехов' — разные значения, регистр важен.

Как соединять условия: AND, OR, NOT и скобки?

Сложные критерии собираются из простых связками AND (и), OR (или), NOT (не). Проблема в приоритетах: AND вычисляется раньше OR. Запрос WHERE price > 800 OR author = 'Чехов' AND price < 500 движок читает как цена > 800 ИЛИ (Чехов И цена < 500), а не как ты, возможно, хотел. Скобки ничего не стоят — ставь их всегда, когда в условии встретились оба оператора.

приоритет AND и OR: со скобками и без
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, author TEXT, year INTEGER, price REAL)")
cur.executemany(
    "INSERT INTO books (title, author, year, price) VALUES (?, ?, ?, ?)",
    [
        ("Мастер и Маргарита", "Булгаков", 1967, 850.0),
        ("Собачье сердце", "Булгаков", 1925, 450.0),
        ("Преступление и наказание", "Достоевский", 1866, 600.0),
        ("Идиот", "Достоевский", 1869, 620.0),
        ("Вишнёвый сад", "Чехов", 1903, 390.0),
    ],
)

# AND крепче OR: движок читает это как price > 800 OR (Чехов AND price < 500)
cur.execute("""
    SELECT title FROM books
    WHERE price > 800.0 OR author = 'Чехов' AND price < 500.0
""")
print("без скобок :", cur.fetchall())

# скобки меняют смысл: (price > 800 OR Чехов) AND price < 500
cur.execute("""
    SELECT title FROM books
    WHERE (price > 800.0 OR author = 'Чехов') AND price < 500.0
""")
print("со скобками:", cur.fetchall())

# NOT и составное условие:
cur.execute("""
    SELECT title FROM books
    WHERE NOT author = 'Достоевский' AND year > 1900
""")
print("NOT        :", cur.fetchall())
conn.close()
Вывод
без скобок : [('Мастер и Маргарита',), ('Вишнёвый сад',)]
со скобками: [('Вишнёвый сад',)]
NOT        : [('Мастер и Маргарита',), ('Собачье сердце',), ('Вишнёвый сад',)]

Что делают LIKE, % и подчёркивание в шаблоне?

Когда точное равенство не нужно, а нужно «похоже на», включается оператор LIKE. Он сравнивает строку с шаблоном, где % — любое количество любых символов (в том числе ноль), а _ — ровно один любой символ. Классика: LIKE 'С%' — начинается на С, LIKE '%сердце' — заканчивается на «сердце», LIKE '_ишнёвый%' — первый символ любой, дальше «ишнёвый».

LIKE: шаблоны с % и _
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, author TEXT, year INTEGER, price REAL)")
cur.executemany(
    "INSERT INTO books (title, author, year, price) VALUES (?, ?, ?, ?)",
    [
        ("Мастер и Маргарита", "Булгаков", 1967, 850.0),
        ("Собачье сердце", "Булгаков", 1925, 450.0),
        ("Преступление и наказание", "Достоевский", 1866, 600.0),
        ("Идиот", "Достоевский", 1869, 620.0),
        ("Вишнёвый сад", "Чехов", 1903, 390.0),
    ],
)

cur.execute("SELECT title FROM books WHERE title LIKE 'С%'")
print("LIKE 'С%':", cur.fetchall())

cur.execute("SELECT title FROM books WHERE title LIKE '%сердце'")
print("LIKE '%сердце':", cur.fetchall())

cur.execute("SELECT title FROM books WHERE title LIKE '_ишнёвый%'")
print("LIKE '_ишнёвый%':", cur.fetchall())
conn.close()
Вывод
LIKE 'С%': [('Собачье сердце',)]
LIKE '%сердце': [('Собачье сердце',)]
LIKE '_ишнёвый%': [('Вишнёвый сад',)]

Как фильтровать диапазон BETWEEN-ом?

Диапазон «от и до» записывается как BETWEEN нижняя AND верхняя, и обе границы включаются в результат. Это просто сахар: year BETWEEN 1860 AND 1930 значит то же, что year >= 1860 AND year <= 1930, — но читается быстрее и не оставляет шанса перепутать направление границ.

BETWEEN по годам и по ценам
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, author TEXT, year INTEGER, price REAL)")
cur.executemany(
    "INSERT INTO books (title, author, year, price) VALUES (?, ?, ?, ?)",
    [
        ("Мастер и Маргарита", "Булгаков", 1967, 850.0),
        ("Собачье сердце", "Булгаков", 1925, 450.0),
        ("Преступление и наказание", "Достоевский", 1866, 600.0),
        ("Идиот", "Достоевский", 1869, 620.0),
        ("Вишнёвый сад", "Чехов", 1903, 390.0),
    ],
)

cur.execute("SELECT title, year FROM books WHERE year BETWEEN 1860 AND 1930")
print(cur.fetchall())

cur.execute("SELECT title, price FROM books WHERE price BETWEEN 400.0 AND 700.0")
print(cur.fetchall())
conn.close()
Вывод
[('Собачье сердце', 1925), ('Преступление и наказание', 1866), ('Идиот', 1869), ('Вишнёвый сад', 1903)]
[('Собачье сердце', 450.0), ('Преступление и наказание', 600.0), ('Идиот', 620.0)]

Второй запрос собрал «средний ценовой сегмент» от 400 до 700 включительно — с обеих сторон границы вошли. Обрати внимание: BETWEEN работает не только с числами, но и со строками — title BETWEEN 'А' AND 'К' отфильтрует названия по алфавиту, что бывает удобно для алфавитных указателей.

Как проверить вхождение в список IN-ом?

Проверка «значение — одно из перечисленных» выглядит как IN (значение1, значение2, ...). Она заменяет длинную цепочку OR: author IN ('Чехов', 'Булгаков') — то же, что author = 'Чехов' OR author = 'Булгаков', только короче и без риска напортачить со скобками. Отрицание — NOT IN.

IN и NOT IN: списки значений
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, author TEXT, year INTEGER, price REAL)")
cur.executemany(
    "INSERT INTO books (title, author, year, price) VALUES (?, ?, ?, ?)",
    [
        ("Мастер и Маргарита", "Булгаков", 1967, 850.0),
        ("Собачье сердце", "Булгаков", 1925, 450.0),
        ("Преступление и наказание", "Достоевский", 1866, 600.0),
        ("Идиот", "Достоевский", 1869, 620.0),
        ("Вишнёвый сад", "Чехов", 1903, 390.0),
    ],
)

cur.execute("SELECT title FROM books WHERE author IN ('Чехов', 'Булгаков')")
print("IN     :", cur.fetchall())

cur.execute("SELECT title FROM books WHERE author NOT IN ('Достоевский', 'Булгаков')")
print("NOT IN :", cur.fetchall())
conn.close()
Вывод
IN     : [('Мастер и Маргарита',), ('Собачье сердце',), ('Вишнёвый сад',)]
NOT IN : [('Вишнёвый сад',)]

У IN есть вторая жизнь: вместо списка значений в скобках можно передать другой SELECT — подзапрос, например «авторы, чьи книги дороже восьмисот». Подзапросы — отдельная тема; пока запомни сам оператор: в реальных отчётах IN встречается постоянно, особенно в связке с GROUP BY шестого урока.

Почему year = NULL не находит строки?

Вот классика, на которой спотыкается каждый второй. NULL — не значение, а отметка об отсутствии: «данные нет». Сравнение с NULL даёт не истину и не ложь, а третье состояние — неизвестность, поэтому WHERE = NULL не находит ничего. Ни заполненных, ни пустых строк: запрос выполняется без ошибок и молча возвращает пустоту. Проверяем.

= NULL против IS NULL
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, year INTEGER)")
cur.executemany(
    "INSERT INTO books (title, year) VALUES (?, ?)",
    [
        ("Герой нашего времени", 1840),
        ("Неоконченная повесть", None),
        ("Записки охотника", 1852),
    ],
)

cur.execute("SELECT title FROM books WHERE year = NULL")   # ЛОВУШКА
print("year = NULL :", cur.fetchall())

cur.execute("SELECT title FROM books WHERE year IS NULL")  # правильно
print("IS NULL     :", cur.fetchall())

cur.execute("SELECT title FROM books WHERE year IS NOT NULL")
print("IS NOT NULL :", cur.fetchall())
conn.close()
Вывод
year = NULL : []
IS NULL     : [('Неоконченная повесть',)]
IS NOT NULL : [('Герой нашего времени',), ('Записки охотника',)]

Что вычисляется раньше: WHERE или SELECT?

Логический порядок выполнения такой: сначала FROM достаёт строки, потом WHERE их фильтрует, и только затем SELECT вычисляет выражения и собирает выдачу. Отсюда практический вывод: условие в WHERE всегда работает с исходными колонками таблицы, а не с алиасами из SELECT. Отфильтруем книги дешевле пятисот и сразу посчитаем цену со скидкой.

фильтр по исходной колонке, а не по алиасу
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, author TEXT, year INTEGER, price REAL)")
cur.executemany(
    "INSERT INTO books (title, author, year, price) VALUES (?, ?, ?, ?)",
    [
        ("Мастер и Маргарита", "Булгаков", 1967, 850.0),
        ("Собачье сердце", "Булгаков", 1925, 450.0),
        ("Преступление и наказание", "Достоевский", 1866, 600.0),
        ("Идиот", "Достоевский", 1869, 620.0),
        ("Вишнёвый сад", "Чехов", 1903, 390.0),
    ],
)

cur.execute("""
    SELECT title, price - 100 AS discounted
    FROM books
    WHERE price < 500.0
""")
for row in cur.fetchall():
    print(row)
conn.close()
Вывод
('Собачье сердце', 350.0)
('Вишнёвый сад', 290.0)

Условие написано по колонке price, хотя в выдаче она уже превращена в discounted: фильтр отработал раньше, вычисление — позже. Забавная деталь про SQLite: она, в отличие от большинства баз, разрешает писать и WHERE discounted < 500 — алиас она найдёт. Но так делать не стоит: в PostgreSQL и MySQL этот запрос упадёт с «нет такой колонки», а код, работающий только в одной базе, — это мина в проекте, который однажды переедет.

продакшн-сниппет: поиск по подстроке с параметром
# Продакшн-сниппет: поиск по шаблону от пользователя
import sqlite3

def search_books(conn, needle):
    """Возвращает книги, в названии которых есть подстрока needle."""
    # сам шаблон - тоже параметр: % приклеиваем к ЗНАЧЕНИЮ, не к SQL
    cur = conn.execute(
        "SELECT title FROM books WHERE title LIKE ?",
        ("%" + needle + "%",),
    )
    return cur.fetchall()
Функция для твоего проекта — на странице ей не с чем работать, в песочнице урока те же запросы выполнены выше. Смысл: единственный аргумент LIKE — параметр ?, а проценты приклеиваются к значению в Python.

Что дальше

WHERE — это половина выразительности SQL: теперь ты вынимаешь из таблицы ровно нужные строки — по числам, строкам, шаблонам, диапазонам и спискам, с NULL обращаешься правильно. Две ступени наверх: в уроке 5 выдачей научатся управлять — ORDER BY, LIMIT и DISTINCT наведут порядок и сделают постраничную выдачу, а в шестом уроке появятся агрегаты и GROUP BY. И не забудь, откуда растут руки: параметры ? из урока 3 нужны и внутри WHERE — пользовательский критерий никогда не вклеивается в текст запроса.

WHERE — самое часто используемое слово SQL после SELECT: настоящий запрос почти всегда с фильтром.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE t (x INTEGER)")
cur.execute("INSERT INTO t VALUES (NULL)")
cur.execute("SELECT COUNT(*) FROM t WHERE x = NULL")
print(cur.fetchone()[0])
cur.execute("SELECT COUNT(*) FROM t WHERE x IS NULL")
print(cur.fetchone()[0])
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE words (w TEXT)")
cur.execute("INSERT INTO words VALUES ('нос')")
cur.execute("INSERT INTO words VALUES ('сон')")
cur.execute("INSERT INTO words VALUES ('стон')")
cur.execute("SELECT w FROM words WHERE w LIKE 'с_н'")
print(cur.fetchall())
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE t (a INTEGER, b INTEGER)")
cur.execute("INSERT INTO t VALUES (0, 0)")
cur.execute("INSERT INTO t VALUES (1, 1)")
cur.execute("SELECT a FROM t WHERE a = 0 OR a = 1 AND b = 1")
print(cur.fetchall())
Проверь себя
0 / 5

1. Какие строки попадут в выдачу запроса с WHERE?

2. Как вычисляется WHERE price > 800 OR author = 'Чехов' AND price < 500?

3. Что найдёт шаблон LIKE '_%'? (подчёркивание и процент)

4. Чему равен результат выражения year = NULL для строки, где year — NULL?

5. Чем year BETWEEN 1860 AND 1930 отличается от year >= 1860 AND year <= 1930?

Карточки терминов
Запомнено: 0 / 6
Практика

В редакторе подготовлена таблица с четырьмя книгами. Выведите название и год книг, написанных в XIX веке: условие WHERE с BETWEEN по колонке year от 1800 до 1899. Формат вывода — как в заготовке.

practice.py
Вопросы и ответы по уроку

Как в SQL написать условие «содержит подстроку»?

Оператором LIKE с процентами по бокам: WHERE title LIKE '%слово%' — '%'* означает любое число любых символов. Значение для шаблона передавай параметром: cur.execute("... LIKE ?", ('%' + слово + '%',)) — тогда пользовательский ввод не сможет подменить сам запрос.

Почему WHERE column = NULL не работает?

Потому что NULL — не значение, а отметка об отсутствии данных, и любое сравнение с ним даёт «неизвестно», а не истину. Запрос отрабатывает без ошибок и возвращает ноль строк. Пустоту проверяют только IS NULL (найти пустые) и IS NOT NULL (найти заполненные).

В чём разница между = и LIKE?

= проверяет точное равенство значения, LIKE сравнивает строку с шаблоном, где % и _ обозначают «любые символы». LIKE 'Иванов' вернёт то же, что = 'Иванов', а LIKE 'Иванов%' — всё, что начинается с «Иванов». В SQLite LIKE не различает регистр только для латинских букв.

Какой приоритет у AND и OR в SQL?

AND вычисляется раньше OR, как умножение раньше сложения. Запрос WHERE a OR b AND c читается как a OR (b AND c). Если нужен другой смысл — ставь скобки: WHERE (a OR b) AND c. Скобки ничего не стоят, а от тихих ошибок фильтра спасают всегда.

Понравился урок? Сошлитесь на него

«Сравнение с NULL даёт не истину и не ложь, а третье состояние — неизвестность, поэтому WHERE = NULL не находит ничего.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.