Программисты любят драки. Задайте группе программистов вопрос «EMACS или VI», выйдите из комнаты на кофе, вернитесь через полчаса, и на полу будет кровь. Для любителей хранения данных тот же вопрос, по крайней мере в последнее время, сводится к SQL и NoSQL. У каждого есть свои защитники, своя священная книга, свои ориентиры и истории успеха ... и у каждого есть свои темные секреты и потрясающие неудачи.

Однако вопрос о том, является ли содержимое данных SQL или NoSQL, упускает из виду более общую картину, которую легче всего резюмировать как «Насколько структурированы ваши данные?». Другими словами, насколько ваши данные поддаются нормализации.

Не так давно различие между данными было довольно четким - у вас были «данные» - строки, столбцы и связанные ссылки - затем у вас были документы, в которых информация содержалась в едином потоковом описании. Вы знали, где находитесь в те дни (около тридцати лет назад). Вы просто не могли поместить содержание документа в базу данных. В этом не было смысла.

В конце 1960-х, когда компьютеры стали использоваться все более и более интересными способами, в документе использовалась разметка, чтобы передавать наборщику сигналов (будь то человек или электронный) о том, как должна быть отображена данная последовательность текста. Появилось несколько различных систем, в основном проприетарных, но одна или две были признаны открытыми и стандартизованными. Наиболее широко используемым из них был язык, первоначально называвшийся Generalized Markup Language (GML), но впоследствии ставший стандартизированным общим языком разметки (SGML), поскольку различные предложения проходили через процессы стандартизации как в США, так и в более поздних стандартах ISO.

Великий гений Тима Бернерса-Ли заключался в создании диалекта SGML, называемого языком гипертекстовой разметки (HTML), и его привязки к протоколу связывания (HTTP). SGML был набором правил для создания языков, и HTML был одним из таких языков. Когда он был впервые изложен, он не был особенно богат, поскольку его основная цель заключалась в создании цитатных рефератов, но со временем стало очевидно, что вы можете расширить этот язык любым количеством различных способов.

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

Кардинальность всегда была проблемой для данных - на самом деле, это, возможно, ЦЕНТРАЛЬНАЯ проблема. Большинство данных в конечном итоге сводятся к четырем типам отношений:

  • Необязательные данные (minOccurs = 0, maxOccurs = 1), в которых контент для данного свойства может отсутствовать или присутствовать. Необязательные отношения сложны, потому что вам все равно нужно включить поле для данных в реляционную таблицу, но также необходимо включить понятие NULL - свойство не имеет связанного значения в конкретном случае.
  • Обязательные данные (minOccurs = 1, maxOccurs = 1), где для данного свойства таблицы существует одно и только одно значение (кстати, это называется функциональной связью).
  • Неограниченный (minOccurs = 0, maxOccurs = бесконечность), где свойство может присутствовать, а может и не присутствовать, и если оно присутствует, то есть любое количество ссылок.
  • Одно или несколько (minOccurs = 1, maxOccurs = бесконечность), где может быть сколь угодно много соединений, но хотя бы одно существует.

Заказ также играет огромную роль. В тот момент, когда вы переходите от maxOccurs = 1 к maxOccurs = infinity, вы вносите смещение в сторону определенной упорядоченной последовательности элементов, которая в значительной степени зависит от машины. Кроме того, вы должны иметь дело с проблемой, присущей функциональным таблицам - вы не можете поместить более одного значения в заданное поле для строки в таблице без какого-либо процесса кодирования, такого как преобразование всех записей в строки с последующим использованием разделителя. например, запятая, табуляция или вертикальная черта для обозначения разделения.

Вместо этого вы должны нормализовать отношения, создав идентификатор первичного ключа для каждой строки в «тематической» таблице, а затем создав ссылку внешнего ключа на этот первичный ключ. Таким образом вы сохраняете функциональную (1 к 1) взаимосвязь между строками каждой таблицы. Вы можете думать об этом как о направленной ссылке из подчиненной строки на ее родительскую, где подчиненная строка «владеет» этой ссылкой (отметьте эту концепцию владения закладкой - это ОЧЕНЬ важно). Кто владеет ссылками - это основа моделирования.

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

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

Одно из основных открытий, сделанных в начале 1990-х годов, заключалось в том, что на самом деле вы можете описать структуру данных с помощью разметки. Для этого было сочтено необходимым упростить структуры SGML до более ограниченного диалекта, который в конечном итоге получил название XML. Это имело два основных эффекта. Во-первых, это означало, что вы могли фактически начать разлагать документ на древовидную структуру (или лес древовидных структур), используя объектную модель документа (DOM). Во-вторых, в эту модель можно было включить метаданные, что позволило схематично описать эту структуру.

Первоначально XML был разработан на основе предположения, что порядок не является частью структуры (что не было никакой гарантии, что узлы должны возвращаться в порядке документа), но на самом деле последовательный порядок неявно предполагался вскоре после того, как язык был принят. Язык XML также предполагал существование невидимых пар ключ / ключ ссылок, которые удерживали структуру вместе. Это означало, что модель DOM сжимала определенную информацию, которая была явной в модели SQL, что позволяло создавать сложные структурированные ресурсы.

Действительно, во многих отношениях XML или JSON не столько «частично структурирован», сколько «сверхструктурирован», поскольку он имеет большую структуру (меньшую энтропию), чем таблицы SQL - структуру XML всегда можно свести к набор строк таблицы, но при этом необходимо было сгенерировать больше информации (например, приоритеты упорядочения и пары ключ / ссылка) для этого.

Это фактически вызывает одну из самых больших загадок перевода между XML и JSON. XML имеет концепцию, называемую последовательностью, которая может содержать узлы элементов, текст или связанные узлы, такие как комментарии или инструкции по обработке, и атомарные значения, такие как строки, даты или числа различных типов. Если вы вставляете последовательность в последовательность, вставленная последовательность «растворяется», и элементы добавляются непосредственно в точке вставки. В JSON такой структуры нет, но есть массив. Когда вы вставляете массив, сам вставленный массив становится отдельным объектом.

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

Это еще одна область, в которой семантика (и RDF в целом) может служить важным мостом. Например, последовательности и списки могут быть удивительно сложными структурами. RDF может описывать как массивы, так и связанные списки. Связанный список известен в RDF как коллекция и состоит из «основы» пустых узлов:

book:StormCrow  book:chapters _:list.
_:list rdf:first     chapter:StormCrow_Intro.
_:list rdf:rest      _:b1.
_:b1   rdf:first     chapter:StormCrow_Ch1.
_:b1   rdf:rest      _:b2.
_:b2   rdf:first     chapter:StormCrow_Ch2.
_:b2   rdf:rest      _:b3.
_:b3   rdf:first     chapter:StormCrow_Ch2.
_:b3   rdf:rest      _:b4.
...
_:b21  rdf:first     chapter:StormCrow_Ch20.
_:b21  rdf:rest      _:b22.
_:b22  rdf:first     chapter:StormCrow_Epilog.
_:b22  rdf:rest      rdf:nil

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

select ?chapter ?baseNode ?nextNode where {
     ?book book:chapters ?chapterList.
     ?chapterList rdf:rest*/rdf:first ?chapter.
     ?baseNode rdf:first ?chapter.
     ?baseNode rdf:rest ?nextNode.
}, {book:iri(book:StormCrow)}
=>
     chapter           baseNode     nextNode
chapter:StormCrow_Ch2    _:b2         _:b3
chapter:StormCrow_Ch7    _:b7         _:b8
chapter:StormCrow_Ch13   _:b13        _:b14
chapter:StormCrow_Ch10   _:b10        _:b11

Это не означает, что вы не можете упорядочить список вне SPARQL (это довольно простой сценарий для Java или Javascript), но это означает, что связанные списки явно не эквивалентны массивам.

Альтернативный подход - использовать индексы, которые определяют порядок как вес:

book:StormCrow  book:chapters _:list.
_:list :hasMembers
     [:resource chapter:StormCrow_Intro;
      :index "1"^^xsd:long],
     [:resource chapter:StormCrow_Ch1;
      :index "2"^^xsd:long],
     [:resource chapter:StormCrow_Ch2;
      :index "3"^^xsd:long],
     
     ...,
     [:resource chapter:StormCrow_Ch20;
      :index "21"^^xsd:long],
     [:resource chapter:StormCrow_Epilog;
      :index "22"^^xsd:long]
.

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

select ?chapter where {
    ?book book:chapters ?chapterList.
    ?chapterList :hasMember ?member.
    ?member :resource ?chapter.
    ?member :index ?index.
    } order by ?index,
{book:iri(book:StormCrow)}

Кроме того, получение заданной индексированной записи становится тривиальным - он использует тот же запрос, но фиксирует индекс до определенного значения (здесь пятая глава книги).

select ?chapter where {
    ?${book} book:chapters ?chapterList.
    ?chapterList :hasMember ?member.
    ?member :resource ?chapter.
    ?member :index ?index.
    } order by ?index,
{book:iri(book:StormCrow),index:5}

(Здесь я использую? $ {Book}, чтобы указать передачу ключа идентификатора для книги, что-то вроде того, как шаблонные литералы работают в Javascript.)

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

select ?book where {
    ?book book:chapters ?chapterList.
    ?chapterList :hasMember ?member.
    ?member :resource ?${chapter}.
    ?member :index ?index.
    } order by ?index,
{chapter:iri(chapter:StormCrow_Intro)}
=>
    ?book
book:StormCrow   # As the first 'chapter' in the book
book:Magi        # As a teaser chapter at the end of the book for the sequel

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

select ?${selections} where {
    ?book     book:chapters ?list.
    ?list     list:hasMember ?member.
    ?member   list:resource ?chapter.
    ?member   list:index ?index.
    ?book     book:publicationDate ?pubDate.
    ?book     book:title ?title.
    ?chapter  chapter:title ?chapterTitle.
    } order by ?${sort},
{selections:"?book ?pubDate",sort:"asc(?pubDate)"}
=>
    book            pubDate
book:Mage            2017
book:StormCrow       2019

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

Следовательно, с точки зрения данных, RDF можно рассматривать как нечто, разделяющее различие между документом и данными различных других форматов. Вы можете создать структуру упорядочения для элементов декоратора в абзаце (например, ‹b›, ‹u›, ‹a› и т. Д.) Почти таким же образом, хотя, по общему признанию, это становится громоздким:

[] a html:P;
    il:hasIndexedList _:list.
_:list :hasMembers
     [il:resource [html:literal "This"; a html:B];
      il:index "1"^^xsd:long],
     [il:resource [html:literal "is"; a html:U];
      il:index "2"^^xsd:long],
     [il:resource [html:literal "a"; a html:I];
      il:index "3"^^xsd:long],
     [il:resource [html:literal "test"; 
         html:href "http://www,mytest.com"; 
         a html:A];
      il:index "4"^^xsd:long],
     [il:resource [html:literal:"."; a html:text];
      il:index "5"^^xsd:long]
.

Я поместил этот конкретный проиндексированный список в пространство имен «il» (пока само пространство имен не имеет значения) в первую очередь, чтобы указать, что находится в каком домене. Индексы здесь являются частью общих сущностей-членов, а не конкретных ресурсов. В этом отношении пустой узел

[] il:resource owl:Thing;
   il:index "1"^^xsd:long;
   # a class:indexedListMember
.

является индексированным членом списка, как показано в закомментированном заявлении выше.

Это использование промежуточного «вспомогательного объекта», такого как индексированный список или связанный список, важно, потому что они переносят управление структурами от семантики первичной онтологии (здесь html) в сторону отдельного пространства структур данных, которое можно использовать в большое количество контекстов.

Учитывая преимущества индексированного списка, странно, что в спецификации RDF нет явной структуры для индексированных списков (она вроде бы есть, если вы посмотрите на определение rdf: Seq, но решение слабое). Причины во многом исторические:

  1. Связанные списки быстрее работают в OWL
  2. Вставить элемент в середину последовательности проще при использовании связанных списков.
  3. Большинство операций со стеком выполняются быстрее в связанных списках.

В самом деле, это подчеркивает различие между этими двумя способами, которое теряется в таких языках, как Javascript (и полностью скрыто в XML). Индексированный список - это структура, отличная от стека, который представляет собой связанный список. Если все мои операции включают в себя вставку и вставку вещей в стек, связанный список - отличная вещь: новые элементы помещаются в начало стека, а указатель на голову затем изменяется в соответствии с новым элементом, что является классическим Действие Тьюринга. Выталкивание выполняет обратное действие, устанавливая заголовок на следующий элемент в стеке и удаляя предыдущий заголовок. Это простые действия, которые можно выполнить в SPARQL.

С другой стороны, с индексированным списком итерация по стеку немного медленнее (сначала нужно вычислить следующий индекс в стеке, а затем искать элемент, имеющий соответствующий индекс), но произвольный доступ обычно быстрее, и вы можете получить гарантированный упорядоченный список из запросов SPARQL, чего нельзя сделать с транзитивным замыканием. Что также происходит здесь (что я считаю важным отличием), так это то, что каждый член напрямую связан с объектом списка, а не косвенно, как в случае с неиндексированным списком. Это принцип владения в действии - в связанном списке любой данный узел в магистрали принадлежит предыдущему узлу, а не конкретному объекту списка. Транзитивное замыкание - полезное свойство, ход по цепочке - полезное свойство, но оно затрудняет моделирование.

Общий вывод из этого состоит в том, что моделирование часто основывается на идентификации (и использовании) тех вспомогательных структур, которые являются семантически нейтральными, но удерживают на месте конструкции семантически конфиденциальных данных. Повествовательные структуры подразумевают явное упорядочивание и индексированные списки. Состояние сохранения обычно включает стек или связанный список. Отношения «один ко многим» всегда должны быть входящими. Если ресурс «указывает» на несколько объектов одного типа, использующих одно и то же свойство, вероятно, вы делаете что-то неправильно. Эти правила могут показаться простыми, но, как правило, они помогают наиболее эффективно распутать сложные модели данных.

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

Курт Кейгл - основатель компании Semantical LLC и редактор The Cagle Report. Он регулярно пишет по вопросам семантики, моделирования данных и науки о данных. #TheDataReport