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 — единственный способ спросить про пустоту.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Зачем в конце
create tableстоитstrictи что будет без него? - Почему
updateбезwhereопасен и какие два слова спасают от ошибки? - Почему
where cover = nullне находит строки без обложки?
Задание
Обязательное. Добавьте в таблицу колонку lang со значением по умолчанию 'kz'. Вставьте четвёртую статью на русском, а потом одним запросом выведите статьи по-казахски, отсортированные по числу слов. Затем поднимите words у самой короткой статьи до 200 — одной командой, с where, и проверьте changes().
По желанию.
- Вставьте вторую статью с тем же
slugи прочитайте, что скажет база. - Уберите
strictизcreate tableи положите вwordsстроку'много'. Посмотрите, что вернётselect typeof(words). - Напишите
updateбезwhereвнутриbegin, посмотрите наchanges()и откатите.
Куда это встанет в блоге
Таблица articles — это и есть будущее хранилище блога. Но статья редко живёт одна: к ней приходят комментарии, а позже теги. Класть их в ту же таблицу нельзя, и следующий урок — про то, как связать две таблицы ключом и собрать их обратно одним запросом.
Долги. Индексов мы не заводили — на четырёх строках это незаметно, а на десяти тысячах поиск по slug станет перебором. Время хранится текстом, потому что отдельного типа для даты в SQLite нет.
Ответы
Показать ответы
strictтребует соблюдать типы колонок. Без него SQLite согласится положить'abc'в колонкуintegerи промолчит, а вы узнаете об этом через неделю, когда программа попробует прибавить к этому единицу.- Потому что
updateбезwhereменяет всю таблицу, и предупреждения не будет:changes()покажет не одну строку, а все. Спасаютbeginв начале иrollbackв конце — изменения откатываются, будто их не было, а число перед глазами. - Потому что
null— не значение, а отметка отсутствия, и сравнение с ним даёт не «истину» и не «ложь», а «неизвестно». В ответ попадают только строки, для которых условие истинно, поэтому не попадает ни одна. Спрашивать надо черезis null.
Источники
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.