PostgreSQL: тест продвинутого сёрфера интернета
После того, как я успешно разобрал базовый и средний тест по PostgreSQL, стало ясно, что пора двигаться дальше. А что, если поднять ставки и взяться за что-то по-настоящему серьёзное? Именно с такой мыслью я отправился на поиски теста в интернетах. И знаете что? Я его нашёл. Этот тест обещал быть настоящим испытанием: оптимизация сложных запросов, работа с большими объёмами данных, продвинутые триггеры и CTE (common table expressions).
В этой статье я поделюсь своим опытом прохождения этого теста, если вы готовы нырнуть в дебри PostgreSQL вместе со мной, добро пожаловать!
👉🏻 Навигация по всем материалам в Telegram
Вопрос 1
Какая команда должна быть на месте [...]
в запросе, чтобы создать новую запись в таблице role
?
Варианты ответа:
Обоснование:
Описание структуры базы данных:
Требование для записи данных в массив: Чтобы корректно вставить значения в поле типа permission[]
, необходимо использовать массив. В PostgreSQL массив создается с помощью функции array[]
или фигурных скобок {}
.
{read'} as permission[]
— Неверно, так как это некорректный синтаксис. Объявление массива типаpermission[]
должно быть оформлено иначе.array['read']::permission[]
— Верно, так как это явное создание массива со спецификацией типаpermission[]
.array['read']
— Также верно, так как PostgreSQL автоматически приведет массив к соответствующему типу, если поле уже имеет типpermission[]
.{permission.read}
— Неверно, так как синтаксис{}
подходит только для строковых значений. В этом случае требуются именно элементы перечисления типаpermission
."read"
— Неверно, так как это строковое значение, а не массив типаpermission[]
.
Чтобы корректно вставить значения в поле типа permission[]
, нужно использовать массив. В PostgreSQL массив создается с помощью функции array[]
или фигурных скобок {}
. Вариант array['read']::permission[]
явным образом указывает тип массива и соответствует типу данных поля.
📌Правильный ответ:
Вопрос 2
Какое утверждение относительно генерируемых колонок является ложью?
- Можно ссылаться на другие генерируемые колонки
- В выражении вычисления нельзя ссылаться на большинство системных таблиц
- Для stored генерируемых колонок значение пересчитывается каждый раз при изменении значений в колонках, на которые ссылаются в выражении
- Невозможно установить значение вручную через команды insert и update
- Один из видов занимает место в памяти
Обоснование:
Генерируемые колонки (computed или virtual/generated columns) имеют определенные правила:
- Можно ссылаться на другие генерируемые колонки: Это не всегда возможно, так как порядок вычислений колонок может быть неопределенным.
- В выражении вычисления нельзя ссылаться на большинство системных таблиц: Это верно, так как доступ к системным таблицам ограничен.
- Для stored генерируемых колонок значение пересчитывается: Это правильно, значение пересчитывается при изменении данных в зависимых колонках.
- Невозможно установить значение вручную: Это верно для виртуальных колонок, но для stored колонок значение хранится, и его нельзя изменить вручную.
- Один из видов занимает место в памяти: Это относится к stored колонкам, которые физически хранят данные.
📌Правильный ответ:
Можно ссылаться на другие генерируемые колонки (неправда, так как это возможно только в некоторых случаях, а зачастую запрещено).
Вопрос 3
Как добавить пермиссию read
во все существующие записи таблицы role
, чтобы не было дубликатов?
update role set permissions = array_append(permissions,'demoMode') where permissions->'demoMode' is null
update role set permissions = array_append(permissions,'read') where array_position(permission,'read') is -1
update table role alter column permissions = distinct(array_append(permissions, 'read'))
update table role alter column permissions = array_append(permissions, 'read')
update role set permissions = array_append(permissions,'read') where array_position(permission,'read') is null
Обоснование:
Чтобы добавить элемент в массив PostgreSQL без дублирования:
- array_append добавляет элемент в конец массива.
- Проверка через array_position гарантирует, что элемент (
read
) не добавляется, если он уже существует. - Условие
array_position(permissions, 'read') is null
корректно проверяет отсутствие элемента в массиве перед добавлением.
- Неверно. Проверка
permissions->'demoMode'
применяется к JSON-данным, что не подходит для массива типаpermission[]
. - Верно.
array_append
добавляетread
, если его нет в массиве (через проверкуarray_position
). - Неверно.
distinct
нельзя применить к массиву напрямую. - Неверно. Нет проверки на существование
read
, поэтому дублирование возможно. - Неверно. Условие
array_position(permission,'read') is null
некорректно из-за ошибки в синтаксисе (должно бытьpermissions
вместоpermission
).
update role set permissions = array_append(permissions,'read') where array_position(permissions,'read') is null
Вопрос 4
Какая особенность при использовании <query1> UNION <query2>
правдива?
Intersect
оператор имеет больший приоритет, если они встречаются в одном запросе.- Сохраняется порядок, то есть в результате работы
UNION
все строки из выборки<query2>
будут после строк из выборки<query1>
. - Совместимые типы данных автоматически не приводятся к одному типу (например,
bigserial
иsmallint
). - Допускаются дубликаты в результате.
- Количество колонок в таблицах, к которым обращаются в запросах
<query1>
и<query2>
, должно быть одинаковым.
UNION
ключевые особенности:- Удаляет дубликаты строк из объединенного результата (если нужен вывод с дубликатами, используется
UNION ALL
). - Количество и порядок колонок в обеих выборках должны совпадать.
- Типы данных автоматически приводятся к наиболее общему типу (например,
smallint
преобразуется вbigint
при необходимости). - Сохраняется порядок строк, если явно указан
ORDER BY
. Intersect
имеет больший приоритет в случае комбинации операторов, но это не является специфической особенностьюUNION
.- Анализ вариантов:
- Неверно. Это относится к оператору
INTERSECT
, а не кUNION
. - Неверно. Порядок строк не гарантируется, если не используется
ORDER BY
. - Неверно. Типы данных автоматически приводятся к одному совместимому типу.
- Неверно.
UNION
удаляет дубликаты, поэтому дубликаты не допускаются. - Верно. Количество и порядок колонок должны совпадать, это обязательное требование для корректной работы
UNION
.
📌Правильный ответ:
5. Количество колонок в таблицах, к которым обращаются в запросах <query1>
и <query2>
, должно быть одинаковым.
Вопрос 5
Какие проблемы могут возникнуть при удалении записей из t1
при помощи оператора DELETE
?
- Чтобы обойти ограничение референциальной целостности, придется использовать оператор
TRUNCATE...CASCADE
с условиемWHERE
. - При наличии записей в таблице
t2
, которые ссылаются на записи, которые вы хотите удалить, будет выброшена ошибка, которая запретит такого рода удаление. - Операция не завершится успехом, так как транзакция, в которой выполняется операция, не позволяет обращаться к вручную созданным последовательностям.
- Если при удалении записи из
t1
в ней не будет записи сid = nextval('seq')
, удаление не завершится из-за ограничения референциальной целостности для таблицыt2
. - При удалении записи из таблицы
t1
все ссылающиеся на нее записи из таблицыt2
изменят значение наnull
, что в дальнейшем придется исправлять руками.
- Рассмотрение структуры:
- Таблица
t1
содержит первичный ключid
. - Таблица
t2
имеет внешний ключ, который ссылается наt1(id)
. При удалении записи изt1
срабатывает условиеon delete set default
, устанавливающее значение внешнего ключа вt2
на значение по умолчанию (default nextval('seq')
). - Проверка утверждений:
- Неверно. Для удаления записей из
t1
с учетом связей не требуется использоватьTRUNCATE...CASCADE
, так как в данном случае удаление разрешено с установкой значения по умолчанию. - Неверно. Ошибка не возникнет, так как срабатывает поведение
on delete set default
, которое предотвращает нарушение референциальной целостности. - Неверно. Удаление не связано с транзакциями последовательностей.
- Неверно. Референциальная целостность обеспечивается автоматически через установку значения по умолчанию в
t2
. - Верно. При удалении записи из
t1
значения вt2
, ссылающиеся на удаляемую запись, изменяются на значение по умолчанию, что может потребовать последующей ручной корректировки.
📌Правильный ответ:
5. При удалении записи из таблицы t1
все ссылающиеся на нее записи из таблицы t2
изменят значение на null
, что в дальнейшем придется исправлять руками.
Вопрос 6
В каких запросах планировщик будет использовать индекс, при условии, что в таблице 1 млн записей?
select * from some_table where name like '^...'
select * from some_table where param < 100 and id > 100
select * from some_table where lower(name) like '...'
select * from some_table where name like '%...%'
select * from some_table where id < 800000
- Использование индексов:
Индексы работают эффективно только в тех случаях, когда условие запроса может быть применено к самому индексу без необходимости построчного перебора таблицы. Это зависит от типа запроса и структуры индекса: - Для поля
id
индекс может использоваться при любых сравнениях (<
,>
,=
). - Для поля
name
индекс может использоваться только при поиске, который начинается с фиксированной строки (name like '...'
), но не при поиске с подстановкой в начале (name like '%...'
). - Анализ запросов:
select * from some_table where name like '^...'
Индекс используется, так как запрос начинается с конкретного символа.select * from some_table where param < 100 and id > 100
Полеparam
не имеет индекса, поэтому для него будет построчный перебор. Индекс может быть применен только для фильтрацииid > 100
.select * from some_table where lower(name) like '...'
Индекс не используется, так как функцияlower()
изменяет поле и делает индекс недоступным.select * from some_table where name like '%...%'
Индекс не используется, так как строка начинается с символа подстановки%
, что требует построчного перебора.select * from some_table where id < 800000
Индекс используется, так как условие применимо напрямую к индексуid
.
5. select * from some_table where id < 800000
Запрос находит записи, используя индекс на поле id
, что наиболее эффективно и точно соответствует условиям использования индекса в данной структуре таблицы.
Вопрос 7
Какой индекс лучше всего подходит для наиболее часто используемого запроса:
create index <index_name> on some_table using hash(major,minor)
create index <index_name> on some_table(major,minor)
create index <index_name> on some_table(minor,major)
create index <index_name> on some_table using btree(minor,major)
create index <index_name> on some_table(major)
create index <index_name> on some_table(minor)
Обоснование:
Для наилучшей производительности важно учитывать порядок колонок в индексе. PostgreSQL при составлении индекса использует порядок колонок слева направо, а значит:
- Индекс
(major, minor)
подходит, так как запрос фильтрует сначала поmajor
, а затем поminor
. - Индекс
(minor, major)
будет менее оптимальным, так как PostgreSQL не сможет эффективно использовать индекс, если фильтр поmajor
идет первым. - Хэш-индексы (
hash
) используются только для равенства, но они менее универсальны, чем B-деревья, и часто не поддерживают сортировку или диапазоны. - Создание отдельных индексов на
major
иminor
не так эффективно, как комбинированный индекс(major, minor)
, поскольку оптимизатор запросов не сможет объединить их для данной структуры фильтра.
📌Правильный ответ:
2. create index <index_name> on some_table(major,minor)
Вопрос 8
Существуют две транзакции (T1 и T2). Какой вид изолированности транзакции НЕ позволит выполнить commit
?
- Serializable:
- Полная изоляция. Обеспечивает сериализуемое выполнение транзакций, имитируя последовательное выполнение.
- В данном случае транзакции T1 и T2 конфликтуют, так как одна читает данные, которые другая модифицирует (и наоборот). Такой конфликт приведет к невозможности выполнения одной из транзакций.
- Repeatable Read:
- Обеспечивает неизменность данных, считанных транзакцией, но не предотвращает фантомные записи.
- В данной ситуации T1 и T2 могут завершиться, так как этот уровень изоляции не гарантирует предотвращения конфликта вставки новых данных.
- Read Uncommitted:
- Минимальный уровень изоляции. Разрешает чтение "грязных" данных.
- В этом режиме обе транзакции успешно завершатся, даже если данные будут конфликтовать.
- Read Committed:
- Гарантирует, что транзакция читает только подтвержденные данные, но изменения могут быть видны между чтениями.
- Здесь транзакции T1 и T2 также завершатся успешно, так как конфликт предотвращается только на уровне чтения.
- Snapshot:
📌Правильный ответ:
Serializable
Режим Serializable
не позволит обеим транзакциям завершиться из-за конфликта чтения и записи, обеспечивая полную изоляцию и предотвращая параллельное выполнение, нарушающее целостность данных.
Вопрос 9
Какая команда может быть выполнена над той же таблицей в другой транзакции без конфликтов, если в одной транзакции выполняется команда CREATE INDEX
?
- Команда
CREATE INDEX CONCURRENTLY
позволяет создать индекс без эксклюзивной блокировки таблицы, что позволяет другим транзакциям выполнять операции с таблицей параллельно. - Другие команды, такие как
SELECT FOR UPDATE
,CREATE TRIGGER
,TRUNCATE TABLE
, иINSERT
, требуют блокировок, которые конфликтуют сCREATE INDEX
. CREATE INDEX CONCURRENTLY
специально разработан для минимизации конфликтов.
📌Правильный ответ:
2. create index concurrently
Вопрос 10
Какие утверждения о следующем запросе правдивы?
- Для представления
some_view
можно создать индекс. - Чтение возможно сразу после создания представления
some_view
. - Возможна операция
refresh materialized view concurrently some_view
. - Так как использовалось выражение
select *
, при модификации таблицыsome_table
изменятся и колонки в представленииsome_view
. - Возможна операция вставки в представление
some_view
.
- Для представления
some_view
можно создать индекс: - Верно. Материализованное представление хранится физически как таблица, и на него можно создавать индексы, что полезно для оптимизации запросов. Это утверждение остается корректным независимо от других условий.
- Чтение возможно сразу после создания представления
some_view
: - Неверно. Ключевое слово
WITH NO DATA
указывает, что данные в представление не загружаются при создании. Оно должно быть обновлено с помощью командыREFRESH MATERIALIZED VIEW
, чтобы стать доступным для чтения. - Возможна операция
refresh materialized view concurrently some_view
: - Вроде верно. Если для материализованного представления существует уникальный индекс, возможно его обновление с использованием опции
CONCURRENTLY
, что позволяет другим запросам обращаться к нему во время обновления. - Так как использовалось выражение
select *
, при модификации таблицыsome_table
изменятся и колонки в представленииsome_view
: - Неверно. Материализованное представление не обновляется автоматически при изменении исходной таблицы. Для обновления данных необходимо выполнять команду
REFRESH MATERIALIZED VIEW
. - Возможна операция вставки в представление
some_view
:
Для представления some_view
можно создать индекс.
Вопрос 11
Какую бы команду вы использовали на копии рабочей версии базы данных для проведения замеров и уверенности в правильности измерений?
VACUUM
: Эта команда предназначена для очистки и анализа таблиц, но она не используется для замеров производительности запросов.EXPLAIN
: Показывает план выполнения запроса, но без выполнения самого запроса. Подходит для анализа структуры плана, но не дает замеров времени или использования ресурсов.EXPLAIN ANALYZE
: Выполняет запрос и показывает реальный план выполнения, включая время выполнения. Это лучший способ замерить производительность запросов.EXPLAIN (ANALYZE BUFFERS)
: Дополняет выводEXPLAIN ANALYZE
информацией о количестве считанных и записанных блоков, что полезно для глубокого анализа.- Нагрузить базу и использовать
EXPLAIN
: Не дает точных данных, так как нагрузка влияет на планировщик запросов и результаты могут быть неоднородными.
📌Правильный ответ:
4. EXPLAIN ANALYZE
Эта команда выполняет запрос на копии базы, измеряя его фактическое время выполнения и ресурсы, что наиболее важно для замеров производительности.
Вопрос 12
Какой вид сканирования таблицы будет выбран планировщиком после выполнения следующих команд?
- Создание таблицы:
- Поле
t
имеет уникальный индекс (по умолчанию создается дляserial unique
). Это позволяет использовать индекс для поиска. - Запрос с фильтром
t > 2
: - Поскольку поле
t
проиндексировано и используется в условии фильтрации, планировщик запросов PostgreSQL может использовать индекс. - Однако выбор между
seq scan
,index scan
, или другим видом сканирования зависит от количества записей, фильтруемого диапазона и статистики таблицы. - Когда выбирается
seq scan
: - Если фильтруется большая часть данных, планировщик предпочитает последовательное сканирование (
seq scan
), так как оно быстрее, чем использование индекса для большого объема данных. - Условия для
index scan
и других видов индекса: - Если фильтруется небольшая часть данных, планировщик использует индексное сканирование (
index scan
илиbitmap index scan
). - Данный случай:
Вопрос 13
Как временно предотвратить вставку данных в таблицу action_log
, где action = 'logIn'
, сохранив уже существующие записи?
- Создать
before insert trigger
, запрещающий'logIn'
. - Создать
constraint
на колонкуaction
, запрещающий'logIn'
. - Создать представление с условием:
where action != 'logIn'
. - Создать
instead of trigger
, запрещающий'logIn'
. - Создать
after insert trigger
, удаляющий вставленную строку.
before insert trigger
:- Это наиболее подходящее решение. Триггер проверяет условие до вставки, и если оно выполняется (
action = 'logIn'
), триггер может остановить операцию. constraint
:- Ограничение (
constraint
) обычно используется для постоянного контроля данных, а не для временных изменений. К тому же, его добавление может быть сложным для существующих данных. - Представление:
instead of trigger
:- Используется для замены действия, например, вставки в представления. Это не подходит для физической таблицы.
after insert trigger
:
📌Правильный ответ:
Создать before insert trigger
, запрещающий 'logIn'.
Вопрос 14
Каков результат выполнения команды:
- Если выполнивший команду не superuser, manuel не получит никаких прав.
- Если исполнивший команду superuser, manuel получит все права на таблицу kinds, а также сможет назначать права другим пользователям.
- Если у manuel есть все права на схему, в которой находится таблица kinds, он получит права на модификацию данных.
- Если выполнивший команду не owner, manuel не получит никаких прав.
- Если выполнивший команду ни superuser, ни owner таблицы kinds, manuel получит только те права на таблицу kinds, что у есть у исполнителя команды.
GRANT
команда:
Позволяет передать привилегии пользователям или ролям. Чтобы успешно выполнить командуGRANT
, исполнитель должен быть либо владельцем таблицы, либо superuser.- Анализ вариантов:
- Неверно. Пользователь, не являющийся superuser, может выполнять
GRANT
, если он является владельцем таблицы. - Неверно. Superuser может передать права, но права на назначение другим пользователям (
GRANT OPTION
) передаются отдельно. - Неверно. Права на схему не влияют напрямую на права на таблицу.
- Верно. Если пользователь, выполнивший команду, не является владельцем таблицы и не superuser, он не может передать права на эту таблицу.
- Неверно.
GRANT
не может передать больше прав, чем имеет исполнитель команды.
📌Правильный ответ:
4. Если выполнивший команду не owner, manuel не получит никаких прав.
Вопрос 15
Какими методиками можно воспользоваться для поддержания работоспособности сервиса с учетом того, что запросов по ключу большинство?
- Репликация
- Репликация вместе с горизонтальным шардингом
- Горизонтальное шардинг
- Репликация вместе с вертикальным шардингом
- Вертикальное шардинг
Обоснование:
Для системы с большим количеством запросов по ключу важны масштабируемость и высокая доступность данных. Каждая методика обладает своими особенностями:
- Репликация:
Обеспечивает дублирование данных для повышения отказоустойчивости и производительности чтения, но не решает проблему масштабируемости записи. - Горизонтальное шардинг:
Разделяет данные между разными серверами по ключу (например, по ID), что улучшает масштабируемость. Но без репликации уязвимость данных увеличивается. - Репликация + горизонтальный шардинг:
Совмещает преимущества репликации (для отказоустойчивости) и шардинга (для масштабируемости). Это оптимальный вариант для систем с большим количеством запросов по ключу. - Вертикальное шардинг:
Разделяет данные по структуре таблиц (например, одни таблицы на одном сервере, другие на другом). Этот подход не эффективен для запросов по ключу, так как данные одного ключа могут оказаться на разных серверах. - Репликация + вертикальный шардинг:
Может улучшить отказоустойчивость, но не является решением для большинства запросов по ключу.
📌Правильный ответ:
2. Репликация вместе с горизонтальным шардингом
Этот метод совмещает отказоустойчивость, высокую доступность и масштабируемость, что идеально подходит для сценария с частыми запросами по ключу.
The conguration test will pass!
Заключение
Этот тест стал настоящим испытанием для моего ума: от оптимизации сложных запросов до работы с продвинутыми конструкциями PostgreSQL. Но, как говорится, чем сложнее задача, тем больше удовольствия от её решения. И хотя я справился с этим вызовом, понимаю, что в мире PostgreSQL всегда есть что изучать.
Кстати, если вы пропустили мои предыдущие статьи, рекомендую их прочитать, чтобы проследить весь путь от базового уровня до сегодняшнего
Оставайтесь на связи, следите за обновлениями материалов в Telegram 🚀