Один из старых черновиков, пролежавших несколько лет, был доработан и опубликован на фоне недавних дискуссий о новых языках запросов вроде Acadia.
Одна из главных причин популярности NoSQL в том, что SQL, будучи мощным языком по заложенным в него идеям, часто реализуется неуклюже и архаично. Язык, который учился бы на опыте SQL, мог бы сделать работу с реляционными данными удобнее для программистов. Дальше разбираются проблемы, знакомые по реальной практике, и то, как более продуманный язык запросов мог бы их решить.
Опыт работы с СУБД у автора идеи в основном связан с MySQL и Db2, но также приходилось иметь дело с SQLite, SQL Server, Oracle и Postgres (в порядке убывания знакомства).
Более удобный синтаксис
Вопросы эстетики синтаксиса для многих программистов принципиальны — они как малыши, которым подавай макароны с сыром, а не брокколи. Основа синтаксиса SQL на PL/I — выбор IBM 1970-х годов, который вряд ли прижился бы сегодня. По требованию рынка новый язык, скорее всего, взял бы за основу эстетику C или Python, возможно, с влиянием ML или Prolog (как это демонстрирует, например, Rust).
Вместе с лучшим синтаксисом хотелось бы получить и лучшие парсеры. Особенно раздражает парсер MySQL, который редко объясняет, где именно проблема и в чём она заключается — если это, конечно, не что-то абсурдное вроде директивы DELIMITER. При этом в существующих реализациях встречаются и хорошие парсеры SQL — Oracle на удивление внятно сообщает об ошибках, прямо указывая, что ожидалось в этом месте.
Примеры в тексте приведены исключительно для иллюстрации; синтаксис не претендует на окончательность. Явное влияние на эти примеры оказали F# (семейство ML), Erlang (в духе Prolog) и Elixir (похож на Erlang и Ruby).
Функциональный язык, не враждебный функциональному программированию
Главное оружие SQL — его свойства языка четвёртого поколения (4GL), когда данные описываются, а не перебираются вручную в цикле. Это довольно близко к парадигмам функционального программирования вроде ленивых вычислений — привет, Haskell. К сожалению, стандартная библиотека большинства диалектов SQL в этом отношении довольно бедна: она оптимизирована под процедурные программы 1980-х. Большинство диалектов SQL в итоге обзавелись хранимыми процедурами, которые по своей природе процедурны — что идёт вразрез с декларативной природой SQL. Это отражается и в пользовательском SQL-коде, который подражает стилю, навязанному языком и стандартной библиотекой: обилие изменяемого состояния (курсоры…) и процедур вместо функций. Значения по умолчанию имеют значение.
Менее непрозрачные планировщики запросов
Свойства 4GL — сильная сторона там, где мощные компиляторы и оптимизаторы хорошо справляются с оптимизацией кода, но легко допустить ошибку, которая сделает запрос значительно дороже, а планировщики запросов при этом остаются загадкой для всех, кроме экспертов по оптимизации SQL. (Отдельное недоброе слово — инструментам "explain" в MySQL, которые особенно плохи в этом отношении.) Строго говоря, это не связано с теорией языков программирования напрямую, но остаётся слабым местом текущих реализаций SQL, при том что computer science накопила немало знаний в этой области.
Более развитые пользовательские типы данных
Некоторые СУБД предлагают концепцию доменов для определения пользовательских типов данных (это опциональная часть стандарта SQL), но их возможности зачастую ограничены — обычно это просто синтаксический сахар вокруг диапазонов или проверок. Похоже, полноценно это поддерживает только Postgres; в Oracle поддержка появилась, судя по всему, совсем недавно (хотя выглядит она, возможно, даже гибче, чем в Postgres). Впрочем, практического опыта с обеими реализациями недостаточно, чтобы судить об этом уверенно. При этом домены описаны ещё в книге Кодда The Relational Model — основополагающем тексте для реляционных СУБД. Учитывая происхождение Postgres от Ingres, который базировался на QUEL, а тот, в свою очередь, был ближе к видению Кодда, чем SQL, логично, что именно Postgres в итоге пошёл по этому пути.
Суммы типов, размеченные объединения и сопоставление с образцом
Схема, которая хорошо иллюстрирует, как современные приёмы функционального программирования могли бы здесь пригодиться, — это функция, возвращающая информацию о кадрах стека. Для контекста: операционная система IBM i предоставляет множество SQL-функций для системного администрирования в рамках раздела «Services». Это очень полезно администраторам, вышедшим из DBA, в разгар отладки, но реализация неуклюжа: по сути там есть «группы» столбцов, взаимоисключающих друг друга, из-за чего много nullable-полей, а также строковые поля, фактически играющие роль перечислений.
Часть проблем — это просто неудачный дизайн схемы (возможно, усугублённый требованием вернуть всё в одной таблице — возврат нескольких таблиц тоже был бы интересным направлением); строковые псевдо-перечисления можно было бы исправить внешним ключом на таблицу, играющую роль enum. Но кое-что упирается именно в выразительность языка реализации.
Отталкиваясь от этой идеи, можно предложить более удачный пример, делающий запросы менее многословными и менее подверженными ошибкам:
// heavily omitting things for simplicity; i.e displacement or additional enum cases, as well as defining enums ad-hoc (they could be declared out of the type too)
// Each frame type, while similar, is not identical, and has different
// semantics or qualifications.
type MachineInterfaceInfo =
{
ActivationGroup: long;
ASP: long;
Library: string;
}
// For those that lack context here, IBM i supports multiple program models:
// - Java programs, which runtime provides the system some special insight
// - OPM programs, the old managed runtime program ABI
// - ILE programs, the new managed runtime program ABI
// - AIX programs, through syscall emulation
// - LIC, the IBM i kernel
// It can generate stack traces for all these kinds of programs; some programs
// may have a call stack containing a frame entry of each type.
type FrameType =
// Inherit fields from another record type.
| ILE { MachineInterfaceInfo | ServiceProgram: string; Module: string; }
| OPM { MachineInterfaceInfo | Program: string; }
| AIX { Bitness: enum(32 | 64); LibArchive: Option(string); Module: string, Syscall: bool; }
| Java { MethodType: enum(DirectExecution | Glue | Interp | JIT | MMI); ClassName: string; Signature: Option(string); }
table Frame =
{
ThreadID: long;
FrameType: FrameType;
Function: Option(string);
}
function StackInfo(JobID: string) : Frame;
// An SQL-like select with pattern matching to filter.
select Function from StackInfo("1234/JOB/5678") where AIX { Bitness: 64 } = FrameType;
// this would return FrameType of ILE and OPM
select Function from StackInfo("1234/JOB/5678") where MachineInterfaceInfo { Library: "QSYS" } = FrameType;
select Function from StackInfo("1234/JOB/5678") where AIX { LibArchive: "libc.a" } = FrameType;
select Function from StackInfo("1234/JOB/5678") where AIX { LibArchive: None } = FrameType;
// A function that prints information with a pattern match inside of it.
function FrameFullySpecifiedProgramName(frame : Frame) : string =
match frame.FrameInfo with
| OPM { Program: program } -> program
| ILE { ServiceProgram: srvpgm, Module: module } -> "#{srvpgm}/#{module}"
| AIX { LibArchive: None, Module: module } -> module
| AIX { LibArchive: lib, Module: module } -> "#{lib}(#{module})"
| Java { ClassName: class } -> class
// we must match all possible types, or discard with _
| _ -> "?"
// A function that uses pattern matching based overloads and destructuring.
function FrameJavaFunctionDef(frame : Frame { Java { Signature: None } = .FrameInfo }) : string =
"#{frame.Function}()"
function FrameJavaFunctionDef(frame : Frame { Java { Signature: signature } = .FrameInfo }) : string =
"#{frame.Function}(#{signature})"
// A call to this with a non-Java frame is an error, because no patterns could match.
Если удастся схлопнуть взаимоисключающие наборы столбцов, это также заметно упростит визуализацию данных. Гораздо меньше прокрутки влево-вправо, если такие данные можно, например, превратить в подстолбцы, показываемые внутри более крупного столбца для каждой строки, или отображать как строки, форматируемые по-разному в зависимости от типа.
Внешние ключи, ссылающиеся на несколько типов
Допустим, есть таблицы «Software», «Version» и «Download» (что-то вроде иерархии WEMI, уже разбиравшейся в другом материале этого блога), и у каждой из них могут быть изображения, хранящиеся в таблице «Picture» (поскольку у самих изображений есть метаданные, они оформлены как таблица, а не как столбец). Обычно для каждого типа связи заводится отдельная таблица «многие ко многим»: «SoftwarePicture», «VersionPicture» и так далее. Это выглядит как бессмысленное дублирование — вместо этого можно было бы иметь одну таблицу «многие ко многим» с фактически размеченным объединением по внешним ключам:
table ObjectPictures =
{
// a foreign key is assumed to have the same type as what it relates to
PictureID: key relates to (Picture.PictureID);
ObjectID: key relates to (Software.SoftwareID | Version.VersionID | Download.DownloadID);
}
insert into ObjectPictures (PictureID, ObjectID) values (0x1234, DownloadID { 0x1234 });
insert into ObjectPictures (PictureID, ObjectID) values (0x1234, SoftwareID { 0x1234 });
select SoftwareID { software_id } from ObjectPictures where PictureID = 0x1234;
select PictureID from ObjectPictures where ObjectID = DownloadID { 0x1234 };