Меня, если говорить о накопителях данных, удивляет то, что понятие «файловая система», в том виде, в котором мы его знаем, за годы его существования изменилось не слишком сильно. Конечно, если сравнить то, что есть сейчас, с тем, что было, скажем, в 1960-е годы, то можно сказать, что в наши дни файловые системы дают нам гораздо более широкий функционал, чем прежде. В наши дни всё гораздо лучше в плане скорости, способов кодирования, шифрования, сжатия данных и так далее. Однако фундаментальная природа того, как мы храним файлы, и того, как с ними работаем в компьютерных программах, практически не изменилась. А всё должно быть не так. Нам известны более эффективные способы организации данных, но по каким-то причинам большинство из нас не пользуется этими возможностями в своих программах. Оказывается, правда, что пользоваться ими достаточно просто, и я собираюсь это продемонстрировать на экспериментальном приложении, которое вполне может стать отправной точкой разработки базы данных электронных компонентов для моей лаборатории.
Кунг-фу стиля Linux: мониторинг дисковой подсистемы
Кунг-фу стиля Linux: глобальный поиск и замена строк с помощью ripgrep
Кунг-фу стиля Linux: упрощение работы с awk
Кунг-фу стиля Linux: наблюдение за файловой системой
Кунг-фу стиля Linux: наблюдение за файлами
Кунг-фу стиля Linux: удобный доступ к справке при работе с bash
Кунг-фу стиля Linux: великая сила make
Кунг-фу стиля Linux: устранение неполадок в работе incron
Кунг-фу стиля Linux: расшаривание терминала в браузере
Кунг-фу стиля Linux: синхронизация настроек
Кунг-фу стиля Linux: бесплатный VPN по SSH
Кунг-фу стиля Linux: превращение веб-приложений в полноценные программы
Кунг-фу стиля Linux: утилита marker и меню для командной строки
Кунг-фу стиля Linux: sudo и поворот двух ключей
Кунг-фу стиля Linux: программное управление окнами
Кунг-фу стиля Linux: организация работы программ после выхода из системы
Кунг-фу стиля Linux: регулярные выражения
Кунг-фу стиля Linux: запуск команд
Кунг-фу стиля Linux: разбираемся с последовательными портами
Кунг-фу стиля Linux: базы данных — это файловые системы нового уровня
Кунг-фу стиля Linux: о повторении кое-каких событий сетевой истории
Кунг-фу стиля Linux: PDF для пингвинов
Основой для базы данных, подобной той, о которой я хочу рассказать, может быть файл, где в роли разделителя данных применяются запятые (CSV-файл). Тут можно воспользоваться и чем-то вроде формата JSON. Но я собираюсь задействовать полномасштабную базу данных SQLite для того чтобы избежать необходимости в «тяжёлом» сервере баз данных и сложностей, сопряжённых с его поддержкой. Можно ли, используя мой подход, сделать что-то, что способно заменить базу данных, на которой основана какая-нибудь система резервирования авиабилетов? Нет. А подойдёт ли он для решения большинства задач, которые всем нам так или иначе приходится решать? Готов поспорить, что подойдёт.
Абстракция
Если подумать о файловых системах, то окажется, что это — не более чем абстракция над физическим устройством для хранения данных. Обычно мы не знаем или попросту не беспокоимся о том, где именно хранится нечто вроде файла
hello.c
. Нас даже не интересует то, сжат ли этот файл, то, зашифрован ли он. Он, возможно, был загружен по сети, а, может, его части хаотично разбросаны по всему жёсткому диску. Обычно нас это не волнует. А что если абстрагировать саму файловую систему?Это, в общем-то, идея, лежащая в основе баз данных. Если имеется, например, список электронных компонентов, я могу сохранить его в CSV-файле и прочитать этот файл с помощью программы для работы с электронными таблицами. Или могу использовать полноценную базу данных. Проблема баз данных заключается в том, что для работы с ними обычно требуется некое серверное ПО, вроде MySQL, SQL Server или Oracle. Можно абстрагировать интерфейс базы данных, но это — весьма тяжеловесное решение, если сравнить его с простой операцией открытия файла и с обычной работой с этим файлом.
Правда, существует, например, популярная библиотека, называемая SQLite, которая даёт весьма надёжный механизм работы с базой данных, размещённой в единственном файле. При этом для работы с такой базой данных не нужен специальный сервер, её не нужно как-то по-особенному поддерживать. В работе с подобной БД, конечно, есть и ограничения. Правда, при её применении во многих простых программах можно пользоваться полезными возможностями баз данных, но при этом не тратить системные и финансовые ресурсы на поддержку инфраструктуры, обеспечивающей работу обычной СУБД.
Правильный инструмент для правильной работы
У SQLite, конечно, есть ограничения. Но если, например, некто занимается созданием собственного файлового формата для хранения неких данных, ему, возможно, стоит рассмотреть возможность перехода на SQLite и обработки этих данных средствами СУБД. В соответствии с данными, приведёнными на сайте SQLite, такой ход вполне может привести к экономии дискового пространства и к увеличению скорости доступа к данным. И ещё — после того, как программист освоится с этой технологией, окажется, что она упрощает работу с данными. И небольшое приложение, основанное на SQLite, если решено будет перевести его на что-то более серьёзное, лучше поддастся такому переходу.
Если вам надо хранить огромные объёмы информации, скажем — терабайты, или если надо, чтобы с данными одновременно могло бы работать много пользователей, особенно — если всем им надо писать данные в базу, то вам, возможно, это не подойдёт. На сайте SQLite имеется хорошая страница, на которой говорится об удачных и неудачных вариантах использования этой системы.
На стороне пользователей SQLite — возможность применения инструментов командной строки или их вариантов с графическим интерфейсом, вроде того, браузерного, который показан на следующем рисунке. Все эти инструменты позволяют работать с SQLite-базами данных без необходимости писать какой-то код.
В результате, например, можно решать задачи, вроде ввода данных в БД или исследования этих данных, и при этом совершенно не нуждаться в написании SQL-кода. Если же говорить о применении для похожих задач некоего собственного файлового формата, то, вероятно, решать эти задачи придётся вручную, или надо будет работать с данными, используя универсальные инструменты, которым ничего не известно об особенностях обрабатываемых с их помощью данных.
О моей задаче
Мне не хотелось бы тут рассказывать обо всём, что связано с разработкой приложения, я не собираюсь делать из этой статьи учебный курс по SQL — языку структурированных запросов, используемому в большинстве СУБД, включая SQLite. Мне, вместо всего этого, хочется показать то, как легко приступить к работе над простым приложением, применяющим возможности базы данных для хранения сведений об электронных компонентах, используя язык C. При этом написание кода на C будет представлять собой самую простую из наших задач. У нас есть две основных задачи, с которыми разобраться уже гораздо сложнее. Это — задача структурирования базы данных, то есть — проектирование схемы БД, и задача первоначального ввода данных в систему. Даже если БД планируется заполнять данными в процессе работы с программой, неплохо будет, если в самом начале в ней уже что-то будет, чтобы соответствующая программа с самого начала была бы работоспособной.
Основы работы с базами данных
В современных реляционных базах данных обычно имеется как минимум одна таблица. Их может быть и несколько. В каждой таблице имеются строки, в которых хранятся данные. Строки состоят из столбцов, которым назначены типы данных. Например, в таблице может присутствовать текстовый столбец, предназначенный для серийного номера электронного компонента. Ещё один столбец, предназначенный для хранения вещественных чисел, содержит сведения о напряжении в испытательной точке. А ещё один, хранящий данные логического типа, содержит значение, указывающее на то, пройдено испытание или нет.
Строки каждой таблицы имеют уникальные идентификаторы (ID). Если программист не предусмотрел механизм формирования этого ID, СУБД обычно может сама его сформировать. В частности, речь идёт об автоматическом инкрементировании подобных идентификаторов и об обеспечении того, что каждой строке таблицы будет назначен уникальный идентификатор.
Если бы возможности СУБД ограничивались тем, о чём я только что рассказал, то у них не было бы особых преимуществ перед обычными CSV-файлами. Но после того, как данные организованы вышеописанным образом, многие задачи можно решать лучше, чем при отсутствии такой структуры. Например, легко попросить систему выполнить сортировку элементов, или, например, выбрать из таблицы строки, содержащие три самых больших значения напряжения.
Надо отметить, что одной из главных сильных сторон баз данных является возможность создавать соединения данных. Предположим, у меня есть список компонентов (
Components
): печатная плата (PCB
), резистор (Resistor
), держатель аккумуляторной батареи (Battery Holder
) и светодиод (LED
). Имеется таблица, в которой для каждого из этих компонентов предусмотрена отдельная строка. Теперь представим, что у меня есть таблица с описанием сборных изделий (Assembly
), сделанных из этих компонентов. Тут можно применить простой подход:Таблица Component
ID Name
===========
1 PCB
2 Resistor
3 LED
4 Battery Holder
Таблица Assembly
ID Name Components
============================
1 Blink1 PCB, Resistor, LED, Battery Holder
2 Blink2 PCB, Resistor, LED, Resistor, LED, Battery Holder
Выглядит это некрасиво, такому подходу свойственно нерациональное использование системных ресурсов. Куда лучше будет использовать три таблицы:
Таблица Component
ID Name
===========
1 PCB
2 Resistor
3 LED
4 Battery Holder
Таблица Assembly
ID Name
=========
1 Blink1
2 Blink2
Таблица Assembly_Parts
ID Component Quan
=======================
1 1 1
1 2 1
1 3 1
1 4 1
2 1 1
2 2 2
2 3 2
2 4 1
Используя операцию соединения данных можно скомпоновать эти таблицы и создать новую таблицу, хранящую сведения о том, сколько и каких компонентов нужно для некоего сборного изделия, не дублируя при этом слишком большого количества данных.
В результате я, в своей базе данных, планирую создать три таблицы. В таблице
parts
будет храниться список имеющихся у меня компонентов. В таблице partnums
— сведения о типах компонентов (например — 7805, 2N2222 или CDP1802). И наконец — в таблице locations
будут сведения о том, где именно хранится тот или иной компонент. Мои данные можно структурировать и по-другому. Скажем, может иметься таблица, в которой хранятся сведения о способах монтажа компонента. Например, компоненту 2N2222 может быть назначено значение TO92, или может быть указано, что он предназначен для поверхностного монтажа. Кроме того, я собираюсь создать механизм для просмотра данных, представление (view), в котором все данные будут показаны в развёрнутом виде — как в первом примере. Представление — это нечто такое, что в базе данных не хранится, но, для удобства, выглядит как таблица. А на самом деле это — всего лишь результат выполнения запроса к базе данных, с которым можно что-то делать.Конечно, работа с БД этим не ограничивается. Существуют, например, внутренние и внешние соединения, имеется множество других деталей и нюансов работы с данными. Но, к нашему счастью, есть много хороших материалов о базах данных. Например — документация по SQLite.
Ровно столько SQL, сколько нужно для дела
Нам, для решения наших задач, понадобится сравнительно немного SQL-инструкций:
create
, insert
и select
. Имеется программа, sqlite3
, которая позволяет выполнять команды в применении к базе данных. Этой программе, средствами командной строки, можно передать имя базы данных, а потом можно работать с этой базой данных, что совсем несложно. Для выхода из программы используется команда .exit
.Вот команды создания таблиц базы данных. Полагаю, устроены они достаточно просто и понятно.
create table part ( id integer not null primary key, name text, partnum integer, value text,
units text, quantity integer, photo blob, data text, location integer, footprint text);
create table partnums (id integer not null primary key, partnum text, desc text);
create table locations (id integer not null primary key, location text, desc text);
create view full as select part.id, name, partnums.partnum as part_number, value, units,
quantity, data, locations.location as location, footprint from part
inner join partnums on part.partnum = partnums.id inner join locations on locations.id=part.location
Я выполнил эти команды с помощью
sqlite3
, в командной строке, но их можно было бы выполнить и из программы с графическим интерфейсом. А если бы мне это было нужно, я мог бы сделать так, чтобы они были бы выполнены из моей C-программы. Средства командной строки я использовал и для того, чтобы добавить в базу данных несколько тестовых записей. Например:insert into locations (location,desc) values ("Shop - storage II","Storage over computer desk in shop");
insert into partnums(partnum,desc) values("R.25W","Quarter Watt Resistor");
insert into part(partnum,quantity,location,value,units) values (2,111,1,"10K","ohms");
Для получения данных из базы используется команда
select
:select * from part;
select partnum, quantity from part where quantity<5;
Если вы хотите глубже разобраться в этих и других командах — существует множество учебных руководств по SQL.
Программирование!
До сих пор мы, готовя основу программы, не нуждались, собственно, в программировании. При этом, исходя из предположения о том, что в нашем распоряжении имеется пакет наподобие
libsqlite3-dev
, для того чтобы оснастить C-программу функциями, направленными на работу с базой данных, нам не придётся прилагать особенно больших усилий. В частности, в коде нужно будет подключить sqlite3.h
. Если такого заголовочного файла найти не удаётся — это может означать то, что в системе не установлено то, что нужно для SQLite-разработки. Ещё понадобится связь с libsqlite3
. Если говорить о простом однофайловом проекте, то, чтобы приступить к работе над ним, скорее всего, достаточно будет такого makefile
:CC=gcc
CFLAGS+=-std=c99 -g
LDFLAGS=-g
LDLIBS+=-lsqlite3
edatabase : main
main : main.c
Сам код очень прост. Сначала нужно открыть файл базы данных (с использованием функции
sqlite3_open
). Вместо имени файла можно передать функции :memory
. Тогда в нашем распоряжении окажется база данных, расположенная в памяти, которая будет существовать столько же времени, сколько будет работать программа. Этот вызов возвращает дескриптор базы данных. Далее — нужно подготовить SQL-запрос, который планируется выполнить. Это может быть запрос, подобный тем, которые мы уже выполняли, или — любой другой запрос. В моём случае мне нужно взять все данные из представления full
и вывести их. Поэтому я собираюсь выполнить такой запрос:select * from full;
И наконец — мы пользуемся функцией
sqlite3_step
. До тех пор, пока она возвращает SQLITE_ROW
, мы можем обрабатывать строки, пользуясь функциями вроде sqlite3_column_text
. В конце выполняется финализация и закрытие базы данных. Вот готовый код, из которого удалены механизмы обработки ошибок:#include <sqlite3.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
sqlite3 *db;
sqlite3_stmt *sql;
int rv;
rv=sqlite3_open("parts.db",&db);
rv=sqlite3_prepare_v2(db, "SELECT * from full", -1, &sql, NULL);
do
{
rv=sqlite3_step(sql);
if (rv==SQLITE_ROW)
{
printf("%s,",sqlite3_column_text(sql,0));
printf("%s\n",sqlite3_column_text(sql,2));
}
} while (rv==SQLITE_ROW);
sqlite3_finalize(sql);
sqlite3_close(db);
return 0;
}
Если хотите — вот полный код. А в ситуации, когда перебор строк нас не интересует, можно воспользоваться функцией
sqlite3_exec
. Даже в документации говорится, что это — лишь обёртка вокруг функций prepare
, step
и finalize
. Поэтому данной функции можно просто передать соответствующие входные данные и всё должно заработать.Конечно, существует и множество других функций. Например, можно воспользоваться функцией
sqlite_column_int
, или, для получения других типов, можно применить другие вызовы. Можно прикреплять к SQL-вызовам параметры для установки значений, а не пользоваться строками. Здесь я лишь продемонстрировал то, насколько простым делом может быть создание программы, в которой используется SQLite.Итоги
Когда вы в следующий раз поймёте, что занимаетесь выдумыванием нового файлового формата — поразмыслите о том, чтобы, вместо этого, воспользоваться SQLite. К вашим услугам будут свободные инструменты, а после того, как вы освоите SQL, вы поймёте, что можете сделать очень многое, не написав никакого программного кода, кроме, разве что, кода различных SQL-команд. Можно даже использовать систему для хранения разных версий базы данных, подобную той, что применяется в Git. И, кстати, некоторые используют в роли базы данных Git, но мы этого делать не рекомендуем.
Пользуетесь ли вы СУБД в своих программах?
Кунг-фу стиля Linux: мониторинг дисковой подсистемы
Кунг-фу стиля Linux: глобальный поиск и замена строк с помощью ripgrep
Кунг-фу стиля Linux: упрощение работы с awk
Кунг-фу стиля Linux: наблюдение за файловой системой
Кунг-фу стиля Linux: наблюдение за файлами
Кунг-фу стиля Linux: удобный доступ к справке при работе с bash
Кунг-фу стиля Linux: великая сила make
Кунг-фу стиля Linux: устранение неполадок в работе incron
Кунг-фу стиля Linux: расшаривание терминала в браузере
Кунг-фу стиля Linux: синхронизация настроек
Кунг-фу стиля Linux: бесплатный VPN по SSH
Кунг-фу стиля Linux: превращение веб-приложений в полноценные программы
Кунг-фу стиля Linux: утилита marker и меню для командной строки
Кунг-фу стиля Linux: sudo и поворот двух ключей
Кунг-фу стиля Linux: программное управление окнами
Кунг-фу стиля Linux: организация работы программ после выхода из системы
Кунг-фу стиля Linux: регулярные выражения
Кунг-фу стиля Linux: запуск команд
Кунг-фу стиля Linux: разбираемся с последовательными портами
Кунг-фу стиля Linux: базы данных — это файловые системы нового уровня
Кунг-фу стиля Linux: о повторении кое-каких событий сетевой истории
Кунг-фу стиля Linux: PDF для пингвинов
Комментарии (23)
Batu
24.07.2021 16:53В тему совершенствования файловой системы, хочу обратить внимание что имена столбцов с типами это есть описание атрибутов класса. А каждая строка это объект класса. Некоторые методы визуализируются как вычисляемые. Таким образом, описание таблицы - это и есть описание класса. Содержание базы данных это массивы (один из видов группирования в моей терминологии) элементов класса. Получаем универсальное определение файла. Не везде содержимое файла именно массивы объектов. Случается и по одному элементу. Но, суть такая. Структура файлов это описание классов (отличных от системных) содержащихся в файле и далее сами объекты. (в группах либо по одному) Никаких расширений. JSON вещь хорошая, но есть и лучше предложения. Такой файл не нуждается в расширениях, можно загружать универсально и, если хорошо подумать, так же универсально визуализировать в различных видах, и, о чудо, универсально редактировать! А если еще лучше подумать то, эта же засада что загружает и редактирует становится и браузером и транслятором. Но, это еще надо добавить мыслей не в тему структуры файла, а в другой теме.
Alexey2005
24.07.2021 17:23+1
olku
24.07.2021 18:46Все новое это хорошо забытое старое. Лет так 60 IBM абстрагирует хранилища данных в Z/OS в виде последовательного, индексируемого или partitioned набора данных (Data Set), и маппинг записи в структуру, например, в COBOL, прост и элегантен и без ORM и SQL - https://github.com/OlegKunitsyn/entcobol-examples/blob/master/sales/src/sales.cpy.
nad_oby
24.07.2021 18:58Так можно и Berkeley DB для тех же целей использовать, она старая проверенная.
Биндинги есть для большинства популярных языков.
MTyrz
25.07.2021 13:38Э-э…
В сухом остатке: автор предлагает использовать базу данных для хранения данных? Действительно, неожиданная и свежая идея.
Хотя с тезисом «не надо выдумывать свой формат» я согласен чуть более, чем полностью, и не только в случае хранения данных.
klirichek
25.07.2021 19:43Сдаётся мне, что лет этак 20 назад что-то подобное уже было в oracle.
Данные в таблицах? Отлично! Теперь ещё положим в таблицы процедуры работы с данными, и обзовём их апплетами. Или сервлетами. Или ещё, как фантазия подскажет!
Приземлить это на sqlite можно, но мне кажется, что тут скорее количественная или терминологическая, чем качественная разница.
ZaitsXL
25.07.2021 21:04Может я что то неправильно понял, но кажется так можно было лет 20 назад используя драйвер ODBC, он даже в Винду встроен был, и кроме того были файловые БД (названия не помню, расширение файла было *.dbf)
la0
А ещё можно смонтировать БД SQLlite как файловую систему. Естественно, через fuse
Если в папке более чем десять тысяч мелких файлов, то эта идея выглядит не настолько дурной насколько кажется таковой при первом прочтении.
dsoastro
Команду не подскажите (для монтирования)?
unsignedchar
google://sqlite fuse mount
Несколько тысяч мелких файлов — это проблема только для fat32.
Teplo_Kota
Ещё для ext3 и btrfs. От мелких файлов всё тормозит. В Gentoo база данных репозитория была представлена как куча файлов. Народ как только не извращался, чтобы её ускорить. Монтировали её в память, переносили на виртуальный жёсткий диск с reiserfs, выносили в сеть на RaspberrnyPI и уже там держали в памяти...
la0
поддерживаю этот комментарий. time ls | wc -l для папки с большим кол-вом файлов будет вызывать некоторые проблемы даже на ext4.
Самый примитивный пример -- попробуйте забыть к примеру крон чистки php-шных файловых сессий даже на не очень посещаемом уютном бложике. Через 30-40к уников это уже будет заметно.
Oxyd
Попробуйте
time find /path/to/directory -maxdepth 1|wc -l
Self_Perfection
ls сортирует записи при выводе, а если он ещё полезет на файлы stat'ы делать, чтобы вывод раскрасить, то оверхед будет вообще гигантский. Для корректного тестирования нужно вызывать ls с флагом -f
unsignedchar
Хм.
time for ((i=300000; i<400000; i++)); do n=$i; echo $i > $n; done
real 0m3,062s
user 0m1,134s
sys 0m1,602s
Еще 100 000
time for ((i=400000; i<500000; i++)); do n=$i; echo $i > $n; done
real 0m6,278s
user 0m1,351s
sys 0m1,796s
Еще 200 000
time for ((i=500000; i<600000; i++)); do n=$i; echo $i > $n; done
real 0m2,826s
user 0m1,200s
sys 0m1,553s
time for ((i=600000; i<700000; i++)); do n=$i; echo $i > $n; done
real 0m3,352s
user 0m1,142s
sys 0m1,702s
Принципиальной разницы во времени записи в пустую директорию и в заполненную нет.
Попробую чтение.
time find | wc -l
400001
real 0m0,565s
user 0m0,250s
sys 0m0,230s
time ls | wc -l
400000
real 0m1,109s
user 0m0,929s
sys 0m0,194s
time ls -f | wc -l
400002
real 0m0,244s
user 0m0,089s
sys 0m0,181s
time find -name 5\*| wc -l
100000
real 0m0,438s
user 0m0,287s
sys 0m0,161s
Сама файловая система тормозить не должна. А вот программы, что с ней работают — могут, да.
Teplo_Kota
А вы между тестами кеши сбрасывали?
unsignedchar
Я правильно понимаю, время работы файловой системы внутри sys?
time (for ((i=300000; i<400000; i++)); do n=$i; echo $i > $n; done; sync)
real 0m13,027s
user 0m1,244s
sys 0m1,151s
time (for ((i=400000; i<500000; i++)); do n=$i; echo $i > $n; done; sync)
real 0m10,109s
user 0m1,224s
sys 0m1,164s
time (for ((i=500000; i<600000; i++)); do n=$i; echo $i > $n; done; sync)
real 0m9,468s
user 0m1,104s
sys 0m1,355s
Self_Perfection
Ну вот например ext4 при опциях форматирования по-умолчанию включает фичу dir_index. Для ускорения каждому фалу в директории сопоставляется 32битный хеш. При этом два разных файла с одинаковым хешом в одной директории существовать не могут, а на десятках тысячах файлов в одной директории вероятность получить коллизию очень высока.
Очень весело отлаживать ошибку "no space left on device" при попытке создания файла, хеш имени которого уже использован, при том что места свободного ещё полно. Сталкивался с этим.
https://blog.merovius.de/2013/10/20/ext4-mysterious-no-space-left-on.html
Sly_tom_cat
Ну гораздо чаще ext[234] выдает "no space left on device" совсем по другой причине - тупая нехватка i-node которые там создаются при форматировании и их число можно только уменьшить.
И в этом нет никакой мистики. Просто формат дискового представления (тянущийся от ext2 без значительных изменений) не позволяет выделять inode динамически, как это делают практически все современные ФС.
nullc0de
Не благодарите, db.sqlite надо заменить на путь к вашему sqlite или .sqlite3 файлу, /mnt на вашу точку монтирования
la0
Не буду вдаваться в подробности, но код выше так скажем... э... точно не для прода)
(я предупредил)