Этим постом я запускаю публикацию расширенных транскриптов лекционного курса "PostgreSQL для начинающих", подготовленного мной в рамках "Школы backend-разработчика" в "Тензоре".

В программе: рассказ об основах SQL, возможностях простых и сложных SELECT, анализ производительности запросов, разбор [не]эффективного применения индексов и особенностей работы транзакций и блокировок в этой СУБД.

Курс не претендует на лавры "войти в айти", поэтому подразумевает наличие у слушателя опыта программирования или работы с другими СУБД, и, главное, желания самостоятельно изучать тему работы с PostgreSQL глубже.

Для тех, кому комфортнее смотреть и слушать, а не читать - доступна видеозапись:

Лекция 1: Основы SQL (видео)

Что такое SQL

Сегодня у нас первая лекция из серии "PostgreSQL для начинающих", и говорить на ней мы будем о самых основах SQL – вот этого "хвостика" в названии PostgreSQL, поэтому кому-то часть информации из сегодняшней лекции может показаться очевидной и общеизвестной. Увы, это не совсем так.

Меня зовут Боровиков Кирилл, и в "Тензоре" я занимаюсь всем, что касается баз данных - как SQL, так и NoSQL - разных. Но сегодня мы будем говорить именно об SQL.

Во-первых, SQL – это Structured Query Language – "язык структурированных запросов", вот определение из wiki:

SQL (МФА: [ˈɛsˈkjuˈɛl]; аббр. от англ. Structured Query Language — «язык структурированных запросов») — декларативный язык программирования, применяемый для создания, модификации и управления данными в реляционной базе данных, управляемой соответствующей системой управления базами данных.

В нем отметим три ключевых момента: SQL – это декларативный язык, который используется для управления данными в реляционных базах. Давайте чуть подробнее посмотрим, что это значит…

Декларативный язык

Что вообще такое "декларативный" язык программирования и в противовес ему "императивный"?

Императивные и декларативные языки программирования
Императивные и декларативные языки программирования

Императивные ЯП, к которым относится большая часть тех, которые вы знаете, с которыми вам приходилось сталкиваться, заставляют вас четко описать "как" достичь нужного результата: булочку разрезать пополам, положить на нее котлету, дальше соус, сыр, … и на выходе у вас, наверное, получится гамбургер, если вы нигде в процессе не ошиблись.

В отличие от них декларативные ЯП (которых вам, наверное, встречалось всего два, зато они общеизвестны: HTML и SQL), позволяют просто описать "что" мы хотим получить. Например, просто подъехать к окну заказа и сказать "Хочу гамбургер!" И в этот момент нас не интересует ни при какой температуре, ни сколько времени он будет готовиться, ни кто конкретно его будет готовить - вся эта информация нам неинтересна, мы просто заказываем конкретный результат.

Безусловно, у обоих подходов есть плюсы и минусы – например, императивные языки позволяют лучше оперировать машинными ресурсами на "низовом" уровне: мы можем "отрезать" себе кусочек памяти, загрузить процессор конкретной задачей, зато приходится сильно постараться, чтобы где-нибудь не проехать по "чужой" памяти или не "споткнуться" о Null Pointer Exception.

Декларативные языки, как правило, позволяют вам существенно короче описать то, что вы хотите получить. Но, к сожалению, только в той области, под которую они "заточены". Например, упоминавшийся HTML хорошо приспособлен под отображение данных (в основном, текстовых), а это означает, что все остальное на нем делать будет либо вовсе невозможно, либо очень сложно и некомфортно.

А вот SQL "заточен" под...

Управление данными

Управление операциями или данными
Управление операциями или данными

Большинство императивных языков программирования взаимодействуют с какими-то атомарными, "штучными" вещами: состояниями системы или событиями, которые в ней происходят.

В отличие от них, SQL работает с большими наборами данных - записей или "строк". Строки группируются в таблицы, которые, будучи связаны некоторыми отношениями между собой, образуют базу данных.

Реляционные базы

Нереляционные и реляционные СУБД
Нереляционные и реляционные СУБД

Слово "отношения" здесь ключевое, поскольку именно оно определяет, что ваша СУБД является реляционной - то есть в такой базе будут находиться связанные таблицы.

Потому что существуют достаточно много видов нереляционных СУБД, преимущество которых заключается в возможности, хоть и жертвуя универсальностью, под каждую конкретную прикладную задачу подобрать наиболее подходящий из них: Key-Value, документарные, графовые, поисковые ("заточенные" под полнотекстовый или фразовый поиск) или даже мультипарадигмальные, приближающиеся по возможностям к традиционным SQL-базам.

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

В отличие от них, в реляционных базах, структура жестко задается на моменте разработки, и ее нельзя быстро "перетряхнуть" в динамике - это достаточно сложный процесс. Безусловным стандартом работы с ними сейчас является именно SQL, универсальный по своим возможностям, его поддерживают все ведущие enterprise-СУБД.

Хотя, конечно, не обошлось и без "ложки дегтя". Как и любой язык, SQL выработал со временем определенные "диалекты", и каждая СУБД старается "отрастить" свой, чтобы сделать использование именно своих особенностей еще удобнее.

Поэтому, если у вас стоит задача писать максимально переносимый между СУБД софт, который будет все запросы формировать одинаково понятными для всех СУБД, то либо это будет очень сложным процессом, либо вы получите крайне неэффективные запросы, не использующие хоть какую-то специфику возможностей конкретной базы. То есть любой универсальный запрос на SQL будет одинаково выполняться на всех таких базах, но на всех - не настолько эффективно, насколько можно было бы сделать с учетом специфики.

Хранение данных в реляционных базах

Хранение данных в реляционных базах
Хранение данных в реляционных базах

Все базы нужны для того, чтобы хранить какие-то прикладные данные, и в SQL-ориентированных некоторый класс объектов - например, "документы" (бумажка с подписью), с атрибутами "номер" и "дата" - будет представлен отдельной таблицей.

Атрибуты объекта будут являться ее столбцами, экземпляры объектов - строками, а на пересечении - в поле конкретной строки - будет храниться значение данного атрибута для конкретного экземпляра (Номер = 123, Дата = 01.01.2000).

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

Варианты связей и ключей
Варианты связей и ключей

Между собой таблицы связываются какими-то отношениями, которые определяются ключами.

Как правило, у любой таблицы есть первичный ключ (Primary Key, PK), и он необходим, чтобы уникально идентифицировать любую из строк этой таблицы.

Конечно, бывают ситуации, когда у вас в таблице по вполне определенным прикладными причинам может не быть первичного ключа. Например, вы пишете какую-то систему хранения логов, и запись данных в нее происходит настолько часто, что даже 1 секунда не является уникальным идентификатором - когда у вас таких событий происходит за секунду 10, вы их все пишете, а время отличить между ними никак не можете. И тогда вам или придется добавить в таблицу в качестве первичного суррогатный ключ, или вообще стоит его не делать - если отдельные записи не требуется уникально идентифицировать, а на нее никто не ссылается.

Потому что первичные ключи, классически, используются именно для того, чтобы иметь возможность сослаться на конкретную запись или провзаимодействовать с ней. А как раз чтобы "сослаться" со стороны подчиненной таблицы используются внешние ключи (Foreign Keys, FK) - они определяют по соответствию значений каких полей в дочерней и родительской таблице устанавливается связь.

В принципе, внешний ключ может ссылаться не обязательно на первичный, но и на любой уникальный ключ (Unique Key), которых на таблице, в отличие от первичного, у вас может быть несколько. Например, в таблице, куда вы записываете всех своих клиентов, первичным ключом может выступать суррогатный идентификатор, а дополнительным уникальным ключом - его ИНН.

Развитие стандарта SQL

Вехи развития стандарта SQL
Вехи развития стандарта SQL

Мы говорим об SQL, подразумевая, что это общепринятый стандарт, утвержденный ANSI и ISO, по которому даже разные сертификаты выдаются, ему уже 40 лет... Но, несмотря на столь почтенный, по меркам IT-технологий, возраст, это не какая-то замшелая скрижаль, на которой все давно высечено рунами.

Нет, это вполне живой организм, который активно развивается, и раз в 3-5 лет появляется новое расширение стандарта. И "бег" за поддержкой этого стандарта как раз и определяет путь развития современных СУБД.

Например, если взглянуть на стандарт 2016 года, то... работу с JSON пытались зарелизить в PostgreSQL 16, которая вышла в этом октябре, Row Level Security сделали еще в версии 15, если не раньше, в вот pattern matching только сейчас пытаются доработать для будущей версии 17, ровно как и JSON, поскольку финальный вариант патчей в v16 не вошел.

То есть на данный момент стандарт SQL по своей проработке опережает возможности реальных СУБД. Это ровно та самая разница между декларативным описанием в стандарте "как должно быть" и фактической реализацией на императивных языках "внутри" движка базы "как это должно работать".

Особенности PostgreSQL

Пока мы все говорили про SQL в целом, давайте теперь коснемся особенностей непосредственно PostgreSQL.

Клиент-серверная архитектура PostgreSQL
Клиент-серверная архитектура PostgreSQL

Во-первых, в отличие от некоторых других СУБД, PostgreSQL исповедует клиент-серверную архитектуру. Это означает, что у нас всегда есть некоторый клиент, который формирует запрос и по собственному протоколу "поверх" TCP/IP отправляет его серверу. Как правило, этот запрос текстовый и содержит какие-то SQL-команды. А в ответ мы получаем некоторый код результата и, возможно, выборку.

Для того, чтобы иметь возможность послать это все серверу и получить обратно, необходимо либо клиентское приложение, либо какая-то библиотека, необходимая для вашего приложения.

Общение с PostgreSQL-базой

Клиентские приложения для PostgreSQL
Клиентские приложения для PostgreSQL

Если вы будете развиваться в сфере работы с PostgreSQL, то рано или поздно станете хардкорным разработчиком или админом, или DBA, вы точно будете пользоваться утилитой psql - это нативный консольный клиент, который входит в состав стандартного дистрибутива самого PostgreSQL-сервера, поэтому "есть везде".

Но пока вы не достигли таких вершин или просто не любите консоль, можете использовать любую из пары десятков GUI-утилит.

Библиотеки для PostgreSQL
Библиотеки для PostgreSQL

Если же вам необходимо интегрировать работу с PostgreSQL в свое приложение, то вам необходимо найти подходящую к вашему языку программирования клиентскую библиотеку - их более 30 вариантов, включая экзотические в наших широтах Haskel, Erlang или Rust.

Даже офисные продукты вроде Access, Excel или даже 1C можно заставить работать как PostgreSQL-клиента, если использовать ODBC-драйвер. Так что возможности как-то повзаимодействовать с PostgreSQL ограничены исключительно вашей фантазией.

Базовые SQL-команды

Так из чего же будет состоять ваша жизнь, когда вы будете активно писать на SQL? На 99% - это будет команда SELECT:

SELECT - 99% всей работы с базой данных
SELECT - 99% всей работы с базой данных

А вместе все операторы, которые приведены на этом слайде, покрывают 99.9% всех типовых задач. То есть 99% - SELECT, а 0.9% - всякие CREATE, ALTER, INSERT, ... Потому что самая основная задача любой базы данных - это не столько хранение данных или их прием, сколько отдача.

Правильно попросить ее "отдать", что мы хотим, чтобы она сделала это эффективно, не перегрузилась при этом, и вернула именно те данные, которые были нам нужны - это мы и постараемся научиться делать в рамках данного курса.

Создаем демо-базу

Но, прежде чем говорить, как мы из базы что-нибудь возьмем, надо сначала эти данные в базе как-то получить.

«Чтобы продать прочитать что-нибудь ненужное, нужно сначала купить записать что-нибудь ненужное, а у нас денег базы нет.»

почти кот Матроскин, "Трое из Простоквашино"

Поэтому давайте начнем с создания простейшей базы. А для того, чтобы что-то создать в SQL, нам необходима команда CREATE - она отвечает за создание в SQL любого типа объектов:

Создаем что угодно с помощью CREATE
Создаем что угодно с помощью CREATE

Но нас пока будут интересовать только две: CREATE DATABASE, чтобы создать базу, и CREATE TABLE, чтобы создать таблицу в ней:

CREATE DATABASE tst;

CREATE TABLE tbl(
  k         -- имя поля
    integer -- тип поля
, v
    text
);

Полную спецификацию этих команд я приводить не буду, она доступна в документации по ссылкам выше и даже уже на русском языке, благодаря коллегам из PostgresPro.

Замечу только, что при создании таблицы нам надо заранее определить имена и типы полей – то есть формат записей, которые там будут храниться – прямо в соответствии с определением реляционной базы данных.

Базовый синтаксис

Если вдруг кто-то не догадался, то два минуса в предыдущем примере означают однострочный комментарий, прямо как "две косых" в привычных языках программирования вроде C или JavaScript, а многострочный так и вовсе выглядит точно так же.

-- это однострочный комментарий
/* а это -
         - многострочный */

fld      -- это поле/столбец
Fld      -- это то же самое поле
FLD      -- ... и это – все оно же (приводится к lower case)

"Fld"    -- а вот это – тоже поле, но совсем другое (кавычки дают регистрозависимость)

'str'    -- это строка

'st''r'  -- это строка с одинарным апострофом
E'st\'r' -- ... и это – она же
$$st'r$$ -- ... и даже вот это
$abcd$st'r$abcd$

И раз уж мы затронули тему синтаксиса, то тем, кто работал с другими СУБД некоторые вещи могут быть непривычны.

Во-первых, все поля регистронезависимы – как бы и где бы вы его не написали, оно будет приведено к нижнему регистру. Если же вам хочется указать для поля «странное» имя – например, с пробелом, по-русски, по-китайски, или просто сохранить его регистр, то указывать его надо везде в обычных двойных кавычках – никаких странностей вроде квадратных скобок или обратных апострофов.

Зато, во-вторых, обычные строковые литералы бывают как в апострофах, так и в виде эскейп-последовательностей или даже «$-quoting-string».

Базовые типы данных

При создании таблицы выше мы указывали имена полей и их типы. Какими могут быть имена, мы уже обсудили, давайте посмотрим теперь на то, какими могут быть их типы.

Все типы в PostgreSQL можно разделить на базовые (числовые, символьные, даты/времени и логический тип) и расширенные.

Числовые типы

Числовые типы в PostgreSQL определяются своей разрядностью: 2-, 4- и 8-байтные целочисленные, 4- и 8-байтовые с переменной точностью (с плавающей точкой) и numeric/decimal с указанной точностью (хранится посимвольно).

Числовые типы данных
Числовые типы данных

Выбор между целочисленными типами достаточно прост: если все ожидаемые значения в пределах сотни, то не надо резервировать под них 8-байтовый bigint. Как правило, стандартного 4-байтового integer достаточно для большинства задач.

numeric стоит использовать для различных "денежных" вещей, где недопустимо "потерять копейку на округлениях":

SELECT 3.1415926::real;
--     3.1415925 - чуток потеряли
SELECT 3.1415926::numeric;
--     3.1415926 - а тут все четко

Еще пара вещей может вызвать недоумение у людей с опытом программирования:

  • serial-псевдотипы (аналог AUTO_INCREMENT / IDENTITY из других СУБД), которые позволяют определить поля с автоматически формируемым возрастающим значением "по умолчанию": 1, 2, 3, ...

  • нет unsigned - все числовые типы знаковые, поэтому "честно" положить диапазон [0x00000000..0xFFFFFFFF] в integer не получится, только со смещением "наполовину"

Символьные типы

Символьные/строковые типы представлены парой описанных в стандарте char/varchar и парой PostgreSQL-специфичных bpchar/text.

Символьные типы
Символьные типы

Если вы не предполагаете перенос вашего приложения на другую СУБД, то можете спокойно использовать тип text везде, поскольку указание ограничения длины не дает никаких бонусов. Конечно, за исключением случаев, когда вам действительно требуется ограничить длину записываемого в поле - например, для 2-буквенного кода страны.

Типы даты/времени

Дата и время в PostgreSQL, технически, хранятся как целочисленные, со значением от POSTGRES_EPOCH (01.01.2000) в соответствующих единицах (микросекундах или сутках):

Типы даты/времени
Типы даты/времени

В этом их отличие от некоторых других СУБД, где timestamp может храниться как текстовая строка.

А раз это просто числа, то арифметические операции над ними тоже допустимы, в том числе преобразование к Unix time (время от 01.01.1970) :

SELECT '2024-01-01'::date - 1;
-- 2023-12-31 - за день до
SELECT '2024-01-01'::date - 8 * '1 hour'::interval;
-- 2023-12-31 16:00:00 - за 8 часов до
SELECT extract(epoch from '2024-01-01'::timestamp);
-- 1704067200 - превратили timestamp в double precision
SELECT '1970-01-01 00:00:00'::timestamp + 1704067200 * '1 second'::interval;
-- 2024-01-01 00:00:00 - ... и обратно

Опционально, во временном значении можно использовать часовой пояс (with time zone) или указывать сохраняемую точность (timestamp(0) означает хранение "до секунд").

Логический тип

Логические значения представлены типом boolean:

Логический тип
Логический тип

Он может принимать значения TRUE/FALSE и, с учетом SQL-специфики, значение NULL, равно как и любой другой тип.

Специальные типы данных

Помимо базовых типов, "из коробки" PostgreSQL предоставляет массу других, более специализированных, типов:

двоичные данные, перечисления, геометрические, сетевые адреса, битовые строки, вектора текстового поиска, UUID, XML, JSON, массивы, диапазоны

Например, всякие картографические сервисы любят использовать геометрические типы данных с расширением PostGIS, а слабоструктурированные данные можно хранить в JSON, причем ничуть не хуже какой-нибудь MongoDB, а идентификаторы в распределенных системах - в UUID.

Если вдруг и этих типов вам окажется мало – можно создать свой и работать с ним как с любым другим полем. Главное, правильно его описать, задать соответствующие функции ввода-вывода, хранения и обработки.

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

Базовые SQL-команды (#2)

Давайте снова вернемся к нашей демо-базе и наконец добавим туда хоть какие-то данные.

INSERT

За добавление данных, за их вставку в таблицу, в SQL отвечает команда INSERT:

INSERT INTO tbl(      -- куда будем вставлять данные
  k                   -- имена полей
, v
)
VALUES                -- перечисляем вставляемые строки
  (1, '1st string')
, (102, 'another string')
, (3, NULL);          -- вовсе не ''

Мы указываем, в какую таблицу и в какие поля должны быть добавлены данные, и, в простейшей форме INSERT … VALUES, прямо перечисляем те строки, которые хотим вставить. Значения в них позиционно соответствуют указанным полям таблицы.

Да вот беда – рука у нас дрогнула, и вместо "2" у нас вставилось "102", а третья строка у нас вставилась вообще без данных в текстовое поле… Замечу, что пустая строка и NULL – совсем разные значения.

UPDATE

Давайте мы эти данные поправим. А за изменение каких-то данных в SQL отвечает команда UPDATE:

UPDATE
  tbl
SET
  k = k - 100      -- правила изменения значений полей
, v = '2nd string'
WHERE
  k = 102;         -- условие отбора строк

В ней мы указываем, в какой таблице (а таблица - это ключевой момент работы с SQL, и практически все, что придется делать, мы будем делать именно с таблицей) и как мы хотим изменить поля.

При этом полей можно менять сразу несколько. То есть если в некоторых императивных языках программирования деструкция объектов только-только занимает свое место в стандартах, то в SQL это было всегда - "у такого-то набора полей задай новые значения вот так-то".

И, дополнительно, описываем условие, которое должно отобрать только те строки, для которых наша операция должна отработать. В данном случае мы для строки с k = 102 (обратите внимание на одинарное равенство при сравнении) хотим изменить строковое значение v на новое, а из значения k вычесть 100 (присвоение точно так же описывается одинарным символом равенства).

DELETE

А последняя строка, в которую у нас "просочился" NULL вместо текстовой строки, нам вообще не нужна. Давайте ее просто удалим - для этого есть команда DELETE:

DELETE FROM
  tbl
WHERE
  v IS NULL;

Обратите внимание, что с NULL-значениями нельзя пользоваться обычными операторами типа "равно"/"не равно", для них есть свои операторы IS. В данном случае мы используем IS NULL, чтобы проверить на совпадение с NULL-значением.

NULL-логика

Потому что базовые операторы (=, <>, NOT) выдают значение NULL, если его имеет хотя бы один из аргументов. А при приведении типов в условии оно превращается в "ложь", и ни одну запись вы не отберете.

Поэтому для сравнений NULL и с ним (в конкретном поле или для всей строки сразу) стоит использовать операторы IS/IS NOT или IS DISTINCT FROM/IS NOT DISTINCT FROM. Некоторые вещи не всегда очевидны, поэтому вот шпаргалка, которой можно пользоваться:

NULL-логика
NULL-логика
IS-операторы
IS-операторы

... RETURNING

А что, если мы хотели не просто удалить записи, но и узнать, какие именно были удалены?..

Чтобы СУБД ответила нам не просто "я вставила/обновила/удалила две строки", а "я обработала две вот такие строки", необходимо воспользоваться ключевым словом RETURNING и перечислить те поля, которые мы хотим увидеть:

DELETE FROM
  tbl
WHERE
  v IS NULL
RETURNING *;

В данном случае мы используем "*", которая в SQL означает "все поля". В данном случае мы увидим, что удаляется одна строка со значением 3 в поле k:

   k    |  v
integer | text
      3 |

SELECT

И, наконец-то, мы можем сделать то, ради чего мы базу-то и создавали - что-то из нее взять, выбрав из нашей таблицы какие-то записи.

Для этого воспользуемся командой SELECT, про которую говорили ранее, опять же, указав "*" вместо списка полей:

SELECT
  *
FROM
  tbl;

И мы увидим то, что осталось в таблице после всех наших манипуляций:

   k    |  v
integer | text
      1 | 1st string
      2 | 2nd string

Мы видим две строки, которые соответствуют тому условию, которое мы задали для выборки. Только что-то никакого условия в запросе мы при этом не видим... и это мы обсудим на следующей лекции.

А пока что вы познакомились с абсолютным минимумом, который вам будет необходим для работы с SQL в PostgreSQL - или в других SQL-ориентированных СУБД.

"Теперь, Нео, ты знаешь кунг-фу!"
"Теперь, Нео, ты знаешь кунг-фу!"

А на сегодня - все!

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


  1. Akina
    12.12.2023 07:17

    Как правило, у любой таблицы есть первичный ключ (Primary Key, PK), и он необходим, чтобы уникально идентифицировать любую из строк этой таблицы.

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

    Первичный ключ позволяет однозначно идентифицировать запись в течение всего срока жизни таблицы.

    Да, для правильного понимания это дополнение - крайне важно! Запись создана - и в её составе (либо для неё в случае автогенерации) присутствует уникальное выражение первичного ключа. Запись удалили, но тем не менее это уникальное выражение - присутствует! Например, запрос по нему позволяет убедиться, что запись отсутствует/удалена.

    Непонимание этой в общем простой вещи приводит к появлению странных для опытного пользователя вопросов "А как убрать образовавшиеся вследствие удаления дырки в поле автоинкремента?". И правильный ответ "Никак, более того, это крайне вредное мероприятие!" порой с трудом достукивается до сознания. Только после того, как проговоришь вышенаписанное, да ещё добавишь, что каждый должен заниматься своим делом, вот ПК-автоинкремент уникально идентифицирует записи, а если нужна непрерывная нумерация, то это другая задача, и её должно решать другое, дополнительно создаваемое под эту задачу поле, человек начинает более-менее понимать суть своей ошибки. Да и то не всегда.

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

    Да, из этого правила, как и из любого другого, бывают исключения. Но они весьма редки. И для того, чтобы отойти от вышеописанного, нужно иметь достаточно серьёзное обоснование.

    чтобы что-то создать в SQL, нам необходима команда CREATE - она отвечает за создание в SQL любого типа объектов

    Ну это не совсем так. Есть и иные команды, которые создают объекты БД. Например, таблица может быть создана запросом SELECT INTO.


    1. Kilor Автор
      12.12.2023 07:17

      а если нужна непрерывная нумерация, то это другая задача, и её должно решать другое, дополнительно создаваемое под эту задачу поле

      Разделяю всю боль от разработки бухгалтерского софта . ))

      таблица может быть создана запросом SELECT INTO

      Думаю, начинающему разработчику SELECT INTO использовать придется очень нескоро, поэтому множество вещей в курсе несколько упрощено.


      1. ptr128
        12.12.2023 07:17

        Если бы PostgreSQL умел использовать параллельные планы запросов для INSERT INTO, тогда другое дело. А пока, если хочешь вставку с параллельным планом - используй или CREATE TABLE ... AS, SELECT ... INTO.


        1. Kilor Автор
          12.12.2023 07:17

          В этом смысле CREATE TABLE AS выглядит куда логичнее, позволяя определить дополнительные опции для таблицы (филфактор, таблспейс, COMMIT-поведение). А SELECT INTO - это для использования в функциях больше подходит, для помещения результата в переменную.


          1. ptr128
            12.12.2023 07:17

            Суть не в этом, а в утверждении, что

            начинающему разработчику SELECT INTO использовать придется очень нескоро

            Как только этому разработчику захочется параллельного плана - уже потребуется. А CREATE TABLE AS и SELECT INTO часто взаимозаменяемы.


            1. Kilor Автор
              12.12.2023 07:17

              Документация говорит, что у SELECT INTO нет преимуществ, так зачем использовать вместо CREATE TABLE AS?

              Команда SELECT INTO действует подобно CREATE TABLE AS, но рекомендуется использовать CREATE TABLE AS, так как SELECT INTO не поддерживается в ECPG и PL/pgSQL вследствие того, что они воспринимают предложение INTO по-своему. К тому же, CREATE TABLE AS предоставляет больший набор возможностей, чем SELECT INTO.

              В отличие от CREATE TABLE AS, команда SELECT INTO не позволяет задать свойства таблицы, например выбрать метод доступа с помощью указания USING метод или табличное пространство с помощью TABLESPACE табл_пространство. Если это требуется, используйте команду CREATE TABLE AS. Таким образом, для новой таблицы выбирается метод доступа к таблицам по умолчанию.


              1. ptr128
                12.12.2023 07:17

                Например, затем, что SELECT INTO поддерживается MS SQL, а CREATE TABLE AS - нет. И если приложение должно работать и там и там, CREATE TABLE AS лучше избегать.


                1. Kilor Автор
                  12.12.2023 07:17

                  Поэтому, если у вас стоит задача писать максимально переносимый между СУБД софт, который будет все запросы формировать одинаково понятными для всех СУБД, то либо это будет очень сложным процессом, либо вы получите крайне неэффективные запросы, не использующие хоть какую-то специфику возможностей конкретной базы. То есть любой универсальный запрос на SQL будет одинаково выполняться на всех таких базах, но на всех - не настолько эффективно, насколько можно было бы сделать с учетом специфики.

                  Предпочтительно избегать необходимости писать кросс-СУБД-код. Лучше уж несколько версий специфичных запросов.


                  1. ptr128
                    12.12.2023 07:17

                    Увы, это решает бизнес. У меня сейчас идет ряд проектов, где требование переносимости между PostgreSQL и MS SQL было явно заявлено заказчиком. Нравится мне это, или нет.

                    Естественно, некоторые алгоритмы для достижения высокой производительности приходится выносить в хранимые процедуры/функции, что требует раздельной их поддержки для T-SQL и plpgsql, но стараемся минимизировать их количество. А вот в коде на C# поддержка нескольких версий специфичных запросов намного более трудоемка, чем на уровне хранимых процедур/функций, вызываемых из C# одинаково.


    1. Kilor Автор
      12.12.2023 07:17

      Запись удалили, но тем не менее это уникальное выражение - присутствует! Например, запрос по нему позволяет убедиться, что запись отсутствует/удалена.

      Вот это утверждение не вполне корректно, потому что я могу удалить запись с каким-то значением PK, а затем вставить абсолютно другую, но с тем же значением:

      CREATE TABLE _tmp(
        x integer
      , y integer
      , PRIMARY KEY(x)
      );
      INSERT INTO _tmp(x, y) VALUES(1, 1);
      DELETE FROM _tmp WHERE x = 1;
      INSERT INTO _tmp(x, y) VALUES(1, 2);

      PK гарантирует уникальную идентификацию только для одновременно существующих в таблице записей.


      1. Akina
        12.12.2023 07:17

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


        1. Kilor Автор
          12.12.2023 07:17

          Но ведь никто и не обещал, что PK гарантирует уникальность "во времени", а не "в моменте". А вопросы проектирования БД - тема для отдельного полноценного курса.


          1. Akina
            12.12.2023 07:17

            PK гарантирует уникальную идентификацию записи. О времени не говорится вообще ничего. Формально это верно - да, удалённое, но ранее существовавшее значение первичного ключа МОЖЕТ быть использовано в новой записи. Но исходя из его смысла - оно НЕ ДОЛЖНО использоваться.

            Да, есть исключения. Более того - порой они имеют под собой достаточно серьёзное, а иногда даже и железобетонное, обоснование.. но, как правило, существование этого обоснования диктуется именно особенностью предметной области. И соответственно это всегда натуральные первичные ключи. В случаях же синтетических ключей такое обоснование и придумать-то сложно, и уж тем более для синтетических ключей такого обоснования не существует естественным образом. Кроме странного желания "сэкономить на спичках".


  1. ptr128
    12.12.2023 07:17

    Как правило, у любой таблицы есть первичный ключ (Primary Key, PK), и он необходим, чтобы уникально идентифицировать любую из строк этой таблицы.

    По стандарту это действительно так. Но в PostgreSQL первичный ключ несёт декоративные функции, выделяя один из уникальных индексов. Например, как индекс по умолчанию для внешних ключей. При наличии хотя бы одного уникального индекса в таблице, я, естественно, рекомендую один из них установить первичным в декоративных целях. Но, к сожалению, однозначно строку таблицы в PostgreSQL первичный ключ идентифицирует только при отсутствии в нем NULL полей или явном указании NULLS NOT DISTINCT.

    Иными словами, ограничение (constraint) первичного ключа (primary key) в PostgreSQL не всегда гарантирует уникальную идентификацию записи в таблице. И об следует помнить.


    1. Kilor Автор
      12.12.2023 07:17

      выделяя один из уникальных индексов

      Точнее, один из покрывающих уникальных индексов.

      к сожалению, однозначно строку таблицы в PostgreSQL первичный ключ идентифицирует только при отсутствии в нем NULL полей

      Включение столбца в PK как раз и накладывает на него NOT NULL:

      CREATE TABLE _tmp(
        x integer
      , PRIMARY KEY(x)
      );
      INSERT INTO _tmp(x) VALUES(NULL);
      -- ERROR:  null value in column "x" of relation "_tmp" violates not-null constraint
      -- DETAIL:  Failing row contains (null).


      1. ptr128
        12.12.2023 07:17

        Действительно. Был не прав. Явно перепутал с уникальным индексом, где уникальность с NULL полями гарантируется только при явном указании NULLS NOT DISTINCT


  1. DennisIvanoff
    12.12.2023 07:17

    Слово "отношения" здесь ключевое, поскольку именно оно определяет, что ваша СУБД является реляционной - то есть в такой базе будут находиться связанные таблицы.

    База реляционная не из-за связей (relationship) между таблицами, а из-за реляционной модели хранения в таблицах (https://en.wikipedia.org/wiki/Relational_model). Между таблицами может не быть никаких связей, но база будет реляционной. Сколько можно эту чушь про отношения из курса в курс таскать?


    1. Kilor Автор
      12.12.2023 07:17

      Читаем по этой же ссылке:

      Реляционная модель данных включает следующие компоненты:

      • Структурный аспект (составляющая) — данные в базе данных представляют собой набор отношений.


      1. DennisIvanoff
        12.12.2023 07:17

        А Вы правда не видите, что по ссылке приведенной Вами, в качестве отношения рассматривается одиночная таблица?


    1. Akina
      12.12.2023 07:17

      Наличие связей - это потенция, а вовсе даже не обязанность. СУБД реляционна не потому, что меж таблиц установлены связи, а потому, что они могут быть установлены - для этого в составе СУБД есть весь необходимый инструментарий, позволяющий такие связи установить, на их основе контролировать целостность и непротиворечивость, и блокировать действия, которые её нарушают.

      В конце концов БД из одной таблицы - там и модели-то реляционной толком нет. И тем не менее...


  1. ptr128
    12.12.2023 07:17

    Дата и время в PostgreSQL, технически, хранятся как целочисленные, со значением от Unix Epoch (01.01.1970) в соответствующих единицах (микросекундах или сутках):

    В исходниках указано от 01.01.2000:

    #define POSTGRES_EPOCH_JDATE	2451545 /* == date2j(2000, 1, 1) */


    1. Kilor Автор
      12.12.2023 07:17

      А строкой выше:

      #define UNIX_EPOCH_JDATE		2440588 /* == date2j(1970, 1, 1) */

      И пример с преобразованием timestamp туда-обратно это подтверждает.


      1. ptr128
        12.12.2023 07:17

        Не подтверждает:

        CREATE TABLE tmp_tmp AS 
        SELECT v.id, v.t
        FROM ( VALUES 
          (1, '2000-01-01'::timestamp),
          (2, '1970-01-01'::timestamp) ) v(id, t);
        SELECT encode(t_data,'hex')
        FROM heap_page_items(get_raw_page('tmp_tmp', 0));
        
        01000000000000000000000000000000
        02000000000000000020c8c4fea2fcff

        Явно видим, что в странице дата 01.01.2000 лежит в виде нулевого значения, а 01.01.1970 - нет.


        1. Kilor Автор
          12.12.2023 07:17

          Убедили, да, слово "хранится" я использовал тут зря (сам ведь писал для нашего коллектора оптимизацию бинарного COPY-формата) - подправил.


          1. ptr128
            12.12.2023 07:17

            Ну зато курс будет более выверен, что пойдет ему только на пользу )


  1. elephanthathi
    12.12.2023 07:17

    вот за что мне нравятся статьи на Хабре, так это за комментарии!
    Автору благодарность, что сам написал и расшевелил других!


    1. leotrubach
      12.12.2023 07:17

      Да и статьи автора прекрасны. Всегда с удовольствием их читаю и перечитываю


  1. jimaltair
    12.12.2023 07:17

    Спасибо автору за понятную и нужную статью! Сам я нахватался знаний по реляционным БД на всяких курсах, остальному научился на практике и у более опытных товарищей. Но мои знания достаточно поверхностные и не структурированные, как раз искал что-то подобное чтобы разложить всё по полочкам и углубиться в теорию. Надеюсь, что автор продолжит в том же духе, буду с нетерпением ждать новых статей


    1. Kilor Автор
      12.12.2023 07:17

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


      1. jimaltair
        12.12.2023 07:17

        Благодарю за ссылку, каюсь сразу не обратил внимание) думаю что будет полезно и для остальных читателей