Про Picodata часто говорят коротко: «она понимает протокол PostgreSQL». Это правда, и правда практичная — обычный psql подключается к кластеру без единого адаптера, а Go-код с pgx отличается от кода для PostgreSQL ровно одной строкой подключения. Но между «понимает протокол» и «ведёт себя как PostgreSQL» лежит расстояние, и знать его размер лучше до переезда, а не после.
Эта статья измеряет расстояние. Не по документации, а прогоном 59 SQL-конструкций через пять независимых клиентов на живом кластере.
Это вторая статья серии про Picodata, продолжение первой — про то, чем Picodata отличается от Tarantool. Все числа ниже — из стенда databases/picodata, подстенд pg-compat.
В статье
- Как проверялось
- Что работает
- Что не работает и что это ломает
- Транзакции: худший вид несовместимости
- Все клиенты равны, но драйверы — нет
- pg_dump и мигратор
- Чем платим
- Что дальше
Как проверялось
Набор проб лежит в одном файле, и его исполняют пять раннеров: psql из образа PostgreSQL 18.4, Go с pgx/v5, Python с psycopg, Java со штатным драйвером org.postgresql и Java с фирменным io.picodata:picodata-jdbc. Каждый печатает строки одного формата — идентификатор пробы, вердикт, точное сообщение об ошибке.
Пять клиентов нужны не для солидности. Они разделяют два разных случая: конструкцию не понимает сервер или её не умеет отправить драйвер. Если пробу отвергли все — дело в Picodata; если один — дело в нём.
Две вещи в методике стоят отдельного объяснения, потому что без них матрица была бы красивой и бессмысленной.
Контрольные пробы. Первые две пробы в наборе проверяют не Picodata, а сам раннер: одна заведомо валидна, вторая заведомо нет. Раннер, который не дал OK на первой и FAIL на второй, не различает исходы — например, если после первой же ошибки соединение осталось непригодным и всё дальнейшее посыпалось по инерции. Сборщик отчёта проверяет эту пару у каждого клиента и отказывается строить матрицу, если она не сошлась.
Пробы исполняются из файлов, а не из строк командной оболочки. Это не педантизм: на предварительной разведке проба LIKE 'too%', записанная через вложенные кавычки в docker exec sh -c, потеряла одинарные кавычки, превратилась в обращение к колонке с именем too% и вернула «column not found». Ещё немного — и в статье появилось бы утверждение, что Picodata не поддерживает LIKE. Поэтому раннер psql подаёт запрос на стандартный ввод, а остальные читают его из файла как строку.
Что работает
Сразу оговорю, что означает «работает», потому что разница существенная. Матрица из 59 проб отвечает только на вопрос, принимает ли сервер конструкцию: раннер выполняет запрос и смотрит на факт отказа. «Принято» здесь — это «не отвергнуто», а не «даёт тот же ответ, что PostgreSQL».
Совпадение ответов проверяется отдельным набором: одиннадцать пар запросов, сформулированных под обе схемы, результаты которых сравниваются построчно, плюс постусловия для DML — после INSERT строка читается, после UPDATE значение новое, после DELETE строки нет, после TRUNCATE таблица пуста. Совпадение засчитывается строго: оба запроса должны завершиться успешно (код возврата psql — ноль) и вернуть непустой результат, и только тогда сравниваются данные — без текста ошибок. Иначе одинаковая ошибка на обеих сторонах или два пустых ответа сошли бы за совпадение; что проверка их ловит, подтверждено отдельной пробой с заведомо несуществующей таблицей. В таком режиме все одиннадцать пар совпали, все семь постусловий выполнились — вывод прогона лежит в стенде, pg-compat/results.txt. Дальше «работает» означает именно это, и не больше: проверенное подмножество, а не вся принятая матрица.
Из 59 проб приняты 42:
- соединения таблиц —
JOIN,LEFT JOIN, подзапросы сIN, некоррелированныйEXISTS; - множества —
UNION,UNION ALL,EXCEPT,DISTINCT; - агрегация —
GROUP BY … HAVING,sum,avg,min,max,count,count(DISTINCT …),string_agg; - CTE — обычный
WITH(нерекурсивный); - выражения —
CASE,COALESCE,CAST,LIKE,ILIKE,BETWEEN,IS NULL, конкатенация,substr,upper/lower; - DDL —
CREATE TABLE,ALTER TABLE ADD COLUMN,CREATE INDEX,DROP INDEX,TRUNCATE,DROP TABLE; - DML —
INSERT(в том числе многострочный),UPDATE,DELETE; - служебное —
EXPLAIN,SET.
Набор достаточный, чтобы обычный прикладной код — прочитать, посчитать, записать — имел шанс переехать без переписывания. Именно шанс: проверенное подмножество отвечает так же, как PostgreSQL, но за пределами этих одиннадцати пар совпадение ответов стенд не гарантирует, и переезд всё равно требует прогона своих запросов на своих данных. Разрыв же начинается там, где код перестаёт быть обычным.
Что не работает и что это ломает
Семнадцать проб отвергнуты, и важно не то, что их семнадцать, а что именно за ними стоит.
Нет OFFSET. Классическая постраничная выдача LIMIT 20 OFFSET 40 не выполнится. Пагинацию придётся строить по ключу (WHERE id > последний_виденный), что для больших наборов и правильнее, но переписывать её придётся везде, где она была.
Нет системных каталогов. information_schema, pg_catalog.pg_tables, pg_class, current_database(), current_schema() — всё отвергается. Это ломает не запросы приложения, а всё, что строится на интроспекции: ORM, определяющие схему на старте, инструменты миграции, генераторы кода, административные панели.
ORDER BY только по тому, что выбрано. SELECT id FROM products ORDER BY views не пройдёт: column with name "views" not found. Сортировка работает по колонке из списка выборки, по её алиасу, по выражению над ней и по порядковому номеру — но не по колонке, которой в проекции нет. В PostgreSQL так можно, и запрос, годами работавший, здесь остановится.
Нет коррелированных подзапросов. EXISTS (SELECT 1 FROM q WHERE q.id = p.id) отвергается: внутренний запрос не видит алиас внешнего. Некоррелированный EXISTS при этом работает.
Нет RETURNING и нет ON CONFLICT. Два самых частых приёма современного кода записи: получить сгенерированный идентификатор одним запросом и сделать upsert одной операцией. Оба придётся разбирать на отдельные шаги.
Нет ALTER TABLE DROP COLUMN. Ответ прямой: unsupported DDL: ALTER TABLE _ DROP COLUMN is not yet supported. Колонку можно добавить, но не удалить — а значит, откат миграции, добавившей колонку, невыполним средствами SQL.
Нет WITH RECURSIVE, INTERSECT, SHOW, now().
Оконные функции — случай, где легко ошибиться в выводе. Проба row_number() OVER (ORDER BY id) по всей таблице падает, и напрашивается вывод «оконных функций нет». Он неверен: та же функция на подзапросе из 99 строк выполняется без замечаний. Смотреть надо на текст ошибки, а он про другое — Exceeded maximum number of rows (5000) in virtual table: 66751. Это лимит на материализацию промежуточной таблицы, а не отсутствие функции. А вот rank() действительно отсутствует, и говорится об этом прямо: window function rank does not exist. Две внешне одинаковые пробы, две разные причины — если бы мы записали только вердикт, а не сообщение, разницу бы потеряли.
Транзакции: худший вид несовместимости
Всё перечисленное выше — отказы. Приложение упадёт, разработчик увидит ошибку и починит. Но есть случай хуже, чем отказ.
Команды управления транзакциями Picodata принимает и не выполняет. Работа идёт в режиме autocommit, а BEGIN, COMMIT и ROLLBACK реализованы как заглушки. Выглядит это так:
BEGIN;
INSERT INTO tx_probe VALUES (1);
ROLLBACK;
SELECT count(*) FROM tx_probe; -- 1, строка на местеROLLBACK отчитался успешно. Строка осталась. Код, который рассчитывает откатить изменения при ошибке, здесь не упадёт — он тихо оставит систему в состоянии, которого не должно было существовать. Такие вещи не находятся тестами на отказ; они находятся расследованием инцидента через полгода.
Эта проба вынесена в автоматическую проверку стенда отдельным шагом. И проверка устроена так, что упадёт, если однажды ROLLBACK начнёт откатывать по-настоящему: тогда сигнал будет означать не поломку стенда, а то, что поведение изменилось и текст этой статьи устарел.
Все клиенты равны, но драйверы — нет
Теперь результат, которого я не ожидал. У всех пяти клиентов совпал вердикт по каждой из 59 проб — «принято» или «отвергнуто». Ни одного расхождения: что отверг psql, то отвергли и pgx, и psycopg, и оба JDBC-драйвера. Речь именно о вердикте принятия, а не о сравнении данных: совпадение самих ответов — это отдельная проверка из раздела «Что работает».
Вывод простой и полезный: на уровне SQL граница совместимости проходит по серверу, и выбор драйвера её не сдвигает. Если конструкция не работает — она не заработает от смены клиента.
Тогда зачем Picodata собственный JDBC-драйвер, который она рекомендует для DBeaver? Ответ нашёлся не в SQL, а на уровень выше — в метаданных JDBC, тех самых, по которым графические клиенты строят дерево таблиц:
Вызов DatabaseMetaData |
PostgreSQL-JDBC 42.7.4 | picodata-jdbc 5.1.0 |
|---|---|---|
getDatabaseProductName() |
PostgreSQL 15.0 |
Picodata 15.0 |
getTables() |
ошибка | 1 таблица |
getColumns() |
ошибка | 7 колонок |
getSchemas() |
ошибка | 1 |
getPrimaryKeys() |
ошибка | 1 |
getTypeInfo() |
ошибка | 16 типов |
supportsTransactions() |
true |
false |
Штатный драйвер спотыкается на всех запросах метаданных — он спрашивает pg_catalog, которого нет. Практически это значит, что DBeaver со штатным драйвером не покажет дерево объектов: подключение будет, а навигации не будет.
Но самая интересная строка — последняя. Штатный драйвер на вопрос «поддерживаются ли транзакции» отвечает true, потому что видит перед собой «PostgreSQL 15.0» и отвечает за него. Фирменный отвечает false — честно. Любой инструмент, который спрашивает драйвер о возможностях вместо того, чтобы их проверить, со штатным драйвером получит неверную картину молча.
pg_dump и мигратор
Отсутствие каталогов и транзакций позволяет предсказать судьбу прикладных инструментов, но предсказание — не факт, поэтому оба проверены.
pg_dump 18.4 не работает. Он падает на самом первом запросе, даже не добравшись до схемы:
pg_dump: error: query failed: ERROR: sbroad: rule parsing error:
SELECT pg_catalog.set_config('search_path', '', false);Код возврата 1 в обоих режимах — и --schema-only, и --data-only. Резервное копирование средствами PostgreSQL отпадает целиком; у Picodata для этого свои механизмы.
Мигратор goose не работает — и ломается раньше, чем можно подумать. Дело не в тексте миграции: он падает на создании собственной таблицы версий, где используется id integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY. То есть инструмент не может даже завести учёт применённых миграций, не говоря о том, чтобы что-то применить. Ни таблицы миграции, ни журнала после попытки не появляется.
Попутно: метакоманды psql вроде \dt тоже не работают — они разворачиваются в запросы к pg_catalog. Список таблиц берётся из системного представления самой Picodata, _pico_table.
Чем платим
- Совместимость покрывает код заметно лучше, чем инструментарий. Прикладные запросы в проверенной части переносятся почти без правок; ломается то, что смотрит на базу снаружи — дампы, миграторы, ORM с интроспекцией, GUI-навигация.
- Проверять придётся своими запросами. Матрица показывает принимаемый синтаксис, а совпадение ответов подтверждено на ограниченном наборе. Для оценки переезда этого мало: нужен прогон собственных запросов на собственных данных, и стенд опубликован в том числе для этого.
- Часть правок неизбежна и точечна: пагинация без
OFFSET, запись безRETURNINGи upsert-а, сортировка только по выбранным колонкам. - Транзакции требуют пересмотра логики, а не синтаксиса. Их отсутствие не выражается в ошибке компиляции или запроса — только в поведении.
- Выбор драйвера не влияет на SQL, но влияет на инструменты. Для Java и DBeaver фирменный драйвер практически обязателен, и он тянется из собственного репозитория Picodata, а не из Maven Central.
- Матрица привязана к версии. Всё перечисленное снято на Picodata 26.1.3; часть ограничений в сообщениях сервера прямо помечена как «not yet supported», то есть состав списка со временем меняется. Перед решением стоит перепроверить на своей версии — стенд для этого и опубликован.
Что дальше
Почему одни конструкции проходят, а другие упираются в лимит промежуточной таблицы, — вопрос устройства планировщика: статья про распределённый SQL Picodata. Как расширять кластер собственным кодом, когда SQL не хватает, — статья про плагины на Rust.
Тот же PostgreSQL-протокол в прикладном контексте — в статье про Tarantool и Picodata серии «Вычисления в оперативной памяти», где на нём построен перенос вычислений к данным.
Комментарии