Shanraq.org Shanraq.org
SQL: первая таблица, INSERT и SELECT
IT

Go: с нуля до своего блога Урок 27 из 50

SQL: первая таблица, INSERT и SELECT

Двадцать седьмой урок курса по Go. Статьи наконец переживут перезапуск. Почему в курсе SQLite, а не Postgres, как создать таблицу и положить в неё строку: insert с returning, select с where и order by, update, у которого забыли where, и разница между = null и is null.

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

Блог держит статьи в срезе. Перезапустили программу — статей нет. Всё, что читатель написал через форму, живёт до первого Ctrl+C.

База данных — это место, где данные лежат между запусками, и SQL — язык, на котором с ней разговаривают. Сегодня только SQL, без единой строки на Go: Go подключится к базе в следующем уроке, и лучше прийти туда, уже понимая, что именно вы туда отправляете.

Сразу оговорка. Это не курс по SQL — это тот минимум, на котором работает блог. Настоящий SQL шире на порядок, и учить его стоит по первоисточникам: учебник PostgreSQL и документация SQLite. Здесь мы берём ровно шесть команд и две ловушки.

Почему SQLite, а не Postgres

Разница между ними не в том, что одна «учебная», а другая «настоящая». Разница в устройстве.

Postgres — это программа-сервер. Она работает всё время, отдельно от вашей, слушает порт, знает своих пользователей и их пароли. Ваша программа к ней подключается по сети — пусть даже сеть эта не выходит за пределы одного компьютера. Прямо сейчас на моей машине к постгресу открыто одиннадцать соединений, и он их обслуживает, пока я пишу этот текст.

SQLite — это библиотека внутри вашей программы. Никакого сервера нет. Нет порта, нет пользователей, нет службы, которую надо запустить. Есть файл на диске и код, который умеет его читать и писать. «Подключиться к базе» здесь означает «открыть файл».

Отсюда всё остальное. Установка: sqlite3 уже стоит на вашем маке, а на Linux ставится одной командой; постгресу нужны служба, роль, пароль и первое свидание с сообщением role "..." does not exist. Резервная копия: скопировать файл. Перенести блог на другой компьютер: скопировать файл. Удалить всё и начать заново: удалить файл.

Это не игрушечная база

Про SQLite легко подумать, что это что-то маленькое и несерьёзное. На самом деле это, по всей вероятности, самая распространённая база данных в мире — и разработчики так и пишут: «каждое устройство на Android», «каждый iPhone», и по их оценке в мире больше триллиона живых файлов SQLite.

Она лежит в вашем телефоне прямо сейчас, и не в одном экземпляре: контакты, сообщения, история браузера, настройки приложений — всё это обычно файлы SQLite. Ею пользуются Apple, Google, Microsoft, Mozilla; она стоит в самолётах Airbus и в автомобильной электронике Bosch — это их собственный список.

Так что вы учитесь не на макете. Вы учитесь на движке, который крутится у вас в кармане.

Что Postgres умеет, а SQLite — нет

Честный список, чтобы вы понимали границу.

Несколько пишущих одновременно. В SQLite писать в базу может один за раз. Проверяется это просто: открываем два соединения, начинаем запись в обоих — второе получает database is locked. Для блога, где пишете вы один, это не имеет значения; для магазина с сотней кассиров — имеет.

Доступ по сети с правами. К постгресу подключаются пять программ с разных машин, и у каждой свой пароль и свои разрешения. К SQLite «подключается» тот, у кого есть доступ к файлу.

Больше типов и возможностей. Даты, массивы, JSON с индексами, полнотекстовый поиск — у постгреса всё это богаче. У SQLite даже булева типа нет, в чём вы сейчас убедитесь.

Курс обещает блог, который работает у вас на компьютере, и для этого обещания SQLite подходит лучше. Учите вы при этом SQL, а не SQLite: схема, ключи, join, транзакции переносятся на Postgres без переучивания. Сам shanraq.org работает на Postgres — в конце модуля разберём, что именно меняется при переезде.

Сразу целиком

sqlite3 уже стоит на macOS. На Ubuntu — sudo apt install sqlite3, на Windows — один файл с sqlite.org.

Заведите папку, откройте в ней базу — файл создастся сам:

sqlite3 blog.db

Две строки настройки, чтобы таблицы читались, а не слипались, и третья — про которую будет отдельный разговор в конце урока:

.mode box
.headers on
pragma foreign_keys = on;

Третью строку придётся повторять при каждом входе в sqlite3. Почему — увидите сами, когда дойдём до удаления.

И схема — пока одна таблица:

create table articles (
    id        integer primary key,
    slug      text    not null unique,
    title     text    not null,
    words     integer not null default 0,
    published integer not null default 0,
    created   text    not null default (datetime('now'))
) strict;

Выйти — .quit. Всё, что вы записали, осталось в файле blog.db рядом.

Разбор

Таблица — это тип, записанный один раз

create table описывает форму данных примерно так же, как struct в Go описывает форму значения. Разница в том, что за соблюдением этой формы следит база, а не вы.

not null — колонка не может быть пустой. unique у slug — двух статей с одним адресом не будет, и проверять это в коде не нужно: попытка вставить второй такой же завершится ошибкой. default 0 — если значение не прислали, будет ноль.

primary key — то, чем строка отличается от всех остальных. В SQLite integer primary key вдобавок означает «нумеруй сама»: первой строке достанется 1, следующей 2.

Два места, где SQLite отличается от того, что вы увидите в чужих примерах.

strict в конце — это требование соблюдать типы. Без него SQLite согласится положить 'abc' в колонку integer, и вы узнаете об этом нескоро. Пишите strict всегда.

Булева типа в SQLite нет. published integer хранит 0 и 1, а слова true и false в запросе — просто удобная запись для той же единицы и того же нуля.

insert и returning

insert into articles (slug, title, words, published)
values ('dala', 'О степи', 400, true)
returning id, slug, created;
┌────┬──────┬─────────────────────┐
│ id │ slug │       created       │
├────┼──────┼─────────────────────┤
│ 1  │ dala │ 2026-09-06 07:15:52 │
└────┴──────┴─────────────────────┘

Мы не задавали id и created — их проставила база. returning возвращает то, что получилось, той же командой: без него пришлось бы вставлять, а потом отдельным запросом спрашивать «а какой id ты присвоила».

Несколько строк вставляют одной командой:

insert into articles (slug, title, words, published) values
  ('shanyraq', 'Что такое шанырак', 1000, true),
  ('salem',    'Привет, мир',        150, true),
  ('draft',    'Черновик',            20, false);

select: что взять, откуда и в каком порядке

select id, slug, title, words, published from articles order by id;
┌────┬──────────┬───────────────────┬───────┬───────────┐
│ id │   slug   │       title       │ words │ published │
├────┼──────────┼───────────────────┼───────┼───────────┤
│ 1  │ dala     │ О степи           │ 400   │ 1         │
│ 2  │ shanyraq │ Что такое шанырак │ 1000  │ 1         │
│ 3  │ salem    │ Привет, мир       │ 150   │ 1         │
│ 4  │ draft    │ Черновик          │ 20    │ 0         │
└────┴──────────┴───────────────────┴───────┴───────────┘

Порядок строк без order by не определён. Он может совпасть с порядком вставки, а после первого же удаления перестать совпадать. Нужен порядок — просите его явно.

Дальше три слова, из которых состоит большинство запросов блога:

select slug, title, words
from articles
where published = 1 and words > 100
order by words desc
limit 2;
┌──────────┬───────────────────┬───────┐
│   slug   │       title       │ words │
├──────────┼───────────────────┼───────┤
│ shanyraq │ Что такое шанырак │ 1000  │
│ dala     │ О степи           │ 400   │
└──────────┴───────────────────┴───────┘

where отбирает строки, order by ... desc сортирует по убыванию, limit обрезает. Ровно из этого собирается главная страница: последние десять опубликованных статей.

update: where — не украшение

update articles set words = 300, published = 1 where slug = 'draft';
select changes() as изменено;
┌──────────┐
│ изменено │
├──────────┤
│ 1        │
└──────────┘

changes() говорит, сколько строк изменилось. Одна — как и хотели.

А теперь то, ради чего этот раздел. Уберите where:

begin;
update articles set published = 0;
select changes() as изменено;
rollback;
┌──────────┐
│ изменено │
├──────────┤
│ 4        │
└──────────┘

Четыре. update без where меняет всю таблицу, и никакого предупреждения не будет.

Спасло нас здесь слово begin в начале и rollback в конце: изменения откатились, будто их не было.

select count(*) as опубликовано from articles where published = 1;
┌──────────────┐
│ опубликовано │
├──────────────┤
│ 4            │
└──────────────┘

Транзакции — тема отдельного урока, а пока запомните эти два слова как ремень безопасности: сомневаетесь в update или delete — оберните в begin и посмотрите на число, прежде чем писать commit.

Образ. Хирург, который перед разрезом ещё раз читает, какую ногу оперируют. Не потому что не помнит, а потому что цена ошибки несимметрична.

null — не значение, а его отсутствие

Добавим колонку, которую заполнили не у всех:

alter table articles add column cover text;
update articles set cover = '/static/dala.svg' where slug = 'dala';

Теперь спросим у базы статьи без обложки — так, как подсказывает привычка. И следом второй вопрос, чтобы пустой ответ было видно:

select slug from articles where cover = null;
select count(*) as всего from articles;
┌───────┐
│ всего │
├───────┤
│ 4     │
└───────┘

Первый запрос не напечатал ничего: ни строки, ни ошибки, ни предупреждения. Второй тут же ответил, что статей четыре, и обложки нет у трёх из них.

Причина в том, что null — это не значение, а отметка «здесь ничего нет». Сравнивать с ним нельзя: cover = null спрашивает «равно ли неизвестное неизвестному», и ответ на это — не «да» и не «нет», а «неизвестно». Строка попадает в ответ только на «да».

Правильно так:

select slug from articles where cover is null order by id;
┌──────────┐
│   slug   │
├──────────┤
│ shanyraq │
│ salem    │
│ draft    │
└──────────┘

is null и is not null — единственный способ спросить про пустоту.

Карта урока

Карта урока: строка уходит в таблицу и возвращается запросом

Скажите своими словами

Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.

  1. Зачем в конце create table стоит strict и что будет без него?
  2. Почему update без where опасен и какие два слова спасают от ошибки?
  3. Почему where cover = null не находит строки без обложки?

Задание

Обязательное. Добавьте в таблицу колонку lang со значением по умолчанию 'kz'. Вставьте четвёртую статью на русском, а потом одним запросом выведите статьи по-казахски, отсортированные по числу слов. Затем поднимите words у самой короткой статьи до 200 — одной командой, с where, и проверьте changes().

По желанию.

  • Вставьте вторую статью с тем же slug и прочитайте, что скажет база.
  • Уберите strict из create table и положите в words строку 'много'. Посмотрите, что вернёт select typeof(words).
  • Напишите update без where внутри begin, посмотрите на changes() и откатите.

Куда это встанет в блоге

Таблица articles — это и есть будущее хранилище блога. Но статья редко живёт одна: к ней приходят комментарии, а позже теги. Класть их в ту же таблицу нельзя, и следующий урок — про то, как связать две таблицы ключом и собрать их обратно одним запросом.

Долги. Индексов мы не заводили — на четырёх строках это незаметно, а на десяти тысячах поиск по slug станет перебором. Время хранится текстом, потому что отдельного типа для даты в SQLite нет.

Ответы

Показать ответы
  1. strict требует соблюдать типы колонок. Без него SQLite согласится положить 'abc' в колонку integer и промолчит, а вы узнаете об этом через неделю, когда программа попробует прибавить к этому единицу.
  2. Потому что update без where меняет всю таблицу, и предупреждения не будет: changes() покажет не одну строку, а все. Спасают begin в начале и rollback в конце — изменения откатываются, будто их не было, а число перед глазами.
  3. Потому что null — не значение, а отметка отсутствия, и сравнение с ним даёт не «истину» и не «ложь», а «неизвестно». В ответ попадают только строки, для которых условие истинно, поэтому не попадает ни одна. Спрашивать надо через is null.

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

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

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.