Мета-данные записей или отдельные таблицы базы данных
При разработке плагинов, требующих хранения данных, какие есть плюсы и минусы использования того или иного метода?
Объяснение, данное в кодексе, не является подробным:
Прежде чем создавать новую таблицу, подумайте, не подойдет ли для хранения данных вашего плагина использование WordPress Post Meta (также известные как Произвольные поля). Post Meta является предпочтительным методом; используйте его, когда это возможно/практично.

Что ж, если я надену шляпу WP-скрипткидди, мой ответ будет: используй post_meta, всегда.
Однако, я кое-что знаю о базах данных, поэтому мой ответ: никогда, никогда, никогда не используй EAV (то есть таблицу post_meta) для хранения данных, которые вам может понадобиться запрашивать.
Что касается индексов, в мета-таблицах их практически нет. Так что если вы храните данные типа XYZ и надеетесь запросить все посты, у которых XYZ имеет значение 'abc'
... что ж, удачи. (Посмотрите все тикеты, связанные с пользователями/ролями/правами в WP trac, чтобы понять, насколько всё может усложниться.)
При соединении таблиц вы быстро упрётесь в ограничение, при котором оптимизатор решает использовать общий алгоритм вместо анализа запроса, когда есть несколько условий соединения.
Так что нет, нет, нет, нет. Никогда, никогда, никогда не используйте мета-поля. Если только то, что вы храните, является косметическим и никогда не будет частью критериев запроса.
Всё зависит от вашего приложения. Если вы храните, скажем, дату рождения режиссёра фильма — без проблем. Используйте мета-поля сколько угодно. Но если вы храните, например, дату выхода фильма, было бы безумием не использовать отдельную таблицу (или не добавить столбцы в таблицу постов) и не создать индекс для этого столбца.

Да, плагины, которые я разрабатываю, обрабатывают пользовательские данные, такие как события, новости, пресс-релизы, вакансии... Извне "мира WordPress", использование таблиц не совсем вариант. Но совет из WordPress Codex немного сбивает с толку. Как сериализованные фрагменты данных могут быть предпочтительнее нормализованных/структурированных/индексированных данных?

Если спросить среднего WP-разработчика, он, скорее всего, ответит "используй мета-поля" или "используй таксономии". И я согласен, до того момента, когда вам нужно выполнять запросы против этих данных. Если это так, а я считаю, что в вашем случае именно так, мой единственный ответ — добавьте поля в таблицу posts или создайте отдельную таблицу. Иначе вас ждут огромные проблемы с производительностью при выполнении запросов и, что еще важнее для списков элементов, сортировке top-n.

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

@Denis - Довольно страстная защита против postmeta, да? Ты же понимаешь, что идешь наперекор ортодоксам и выпадешь из милости высших жрецов церкви поэзии кода, если продолжишь такие речи, не так ли? :-) Но серьезно, не кажется ли тебе, что ты немного преувеличиваешь? Все зависит от того, будут ли десятки тысяч записей метаданных или нет. Во многих случаях просто недостаточно записей, чтобы беспокоиться. Один сложный сайт, который я развертываю, содержит около 10 000 записей метаданных, и с ним все в порядке (кстати, это не блог).

@Mike: 10k строк — это немного. Когда я тестирую схему, которую использую, я заполняю её 200k строк и убеждаюсь, что ни один из распространенных запросов не выполняется дольше нескольких мс. Это включает и запросы, связанные с (ориентированным графом) разрешений, замечу. Для дополнительных аргументов за и против могу сослаться на тему на SO: http://stackoverflow.com/questions/870808/entity-attribute-value-database-vs-strict-relational-model-ecommerce-question

@Mike: и, не пойми меня неправильно. Я использую EAV то тут, то там. Просто я ограничиваюсь хранением несущественных данных в нём, с частичными индексами в таблице метаданных для тех редких полей, которые действительно нуждаются в них, но явно не принадлежат к таблице узлов (одноразовый токен или булева настройка приходят на ум).

@Denis - Спасибо за комментарии. И пойми меня правильно, я скорее всего склоняюсь больше к твоей точке зрения, но сочетание 1.) часовых дебатов с Matt на WordCamp Birmingham о преимуществах полей в стиле Pods и 2.) простота meta заставляет меня сосредоточиться на других вопросах, которые я потенциально мог бы изменить. После WCB я понял, что пока Matt у руля, ничего не изменится, потому что (как мне кажется) он настолько очарован идеей уменьшения количества таблиц, что не позволяет себе признать недостатки индексирования по 768-байтовому ключу. <вздох>

Хехе. Думаю, тебе понравится CMS, которую я разрабатываю, если я в конечном итоге выпущу часть или всю её в мир. :-)

Если ваш плагин будет работать с БОЛЬШИМ объемом данных, то использование таблицы wp_postmeta
— НЕ лучшая идея, как показано ниже:
Возьмем WooCommerce в качестве примера. В магазине с ~30 000 товаров в среднем будет около ~40 метаданных на каждый товар (атрибуты и прочее), 5 изображений на товар, а значит, ~4 метаданных на каждое изображение:
30 000 товаров × 40 метаданных = 1 200 000 строк в wp_postmeta
+
30 000 товаров × 5 изображений × 4 метаданных = 600 000 строк в wp_postmeta
Таким образом, всего лишь при 30 000 товарах таблица wp_postmeta
будет содержать 1 800 000 строк.
Если добавить больше свойств к товарам или их изображениям, это число будет расти.
Проблема здесь двойная:
- Самосоединения (self joins) очень ресурсоемки в MySQL
- Таблица
wp_postmeta
не индексируется, если только вы не используете более поздние версии MySQL (например, нет FULLTEXT-индекса дляmeta_value
)
Приведу реальный пример:
SELECT meta_value FROM wp_postmeta WHERE meta_key LIKE '_shipping_city'
Этот запрос, выбирающий город доставки из всех деталей заказов, выполняется около 3 секунд даже на выделенном сервере начального уровня при наличии всего 5-10 заказов. Это происходит потому, что запрос выполняется в таблице wp_postmeta
, содержащей около 3 миллионов строк в рабочей установке.
Даже главная страница загружается медленно, потому что тема получает различные элементы из wp_postmeta
— слайдеры, несколько отзывов, другие метаданные. В целом вывод товаров очень медленный, поиск также работает медленно при отображении товаров.
Эту проблему нельзя решить стандартными методами. Можно установить Elastic Search на сервер и использовать соответствующий плагин для WordPress, можно задействовать redis/memcached, можно подключить хороший плагин кеширования страниц, но фундаментальная проблема останется — выборка данных из раздутой таблицы wp_postmeta
всегда будет медленной. На сервере, где я тестировал приведенное ниже решение, все эти инструменты были установлены, настроены и оптимизированы, и сайт работал приемлемо для неавторизованных пользователей или часто выполняемых запросов, поскольку срабатывали плагины кеширования.
Но стоило авторизованному пользователю выполнить что-то нестандартное, или если крон-задачи, плагины кеширования или другие утилиты пытались получить реальные данные из базы для кеширования или других действий, всё начинало тормозить.
Поэтому я попробовал другой подход:
Я написал небольшой плагин, который переносит все метаданные товаров (postmeta для типа записи product) в пользовательскую таблицу, созданную программно. Этот плагин берет все метаданные для каждой записи, создает таблицу, добавляя каждое метаполе как отдельный столбец, и вставляет значения в каждую строку. Я преобразовал EAV-формат в горизонтальный, плоский реляционный формат. Также плагин удаляет метаданные перенесенных товаров из таблицы wp_postmeta
.
Заодно я перенес метаданные вложений (attachment) и метаданные других типов записей в их собственные таблицы.
Затем я подключился к фильтру get_(post_type)_meta
, чтобы переопределить получение метаданных и брать их из новых пользовательских таблиц.
Теперь тот же запрос, который выполнялся ~3 секунды в wp_postmeta
, выполняется за ~0,006 секунды. Сайт теперь работает так, будто это новая установка WordPress.
....................
Естественно, следование "стандартному" подходу WordPress предпочтительнее. Это фактически норма.
Однако также очевидно, что таблица EAV крайне неэффективна при масштабировании. Она бесконечно гибкая и позволяет хранить любые данные, но платой за это является производительность. Это фундаментальный компромисс.
В этом контексте сложно рекомендовать кому-то, кто планирует работать с огромным объемом данных и — упаси боже — выполнять запросы/поиск по этим данным, использовать таблицу wp_postmeta
. Производительность пострадает значительно.
Использование пользовательских таблиц позволит вашим данным накапливаться и при этом оставаться достаточно быстрыми.
Как отметил Pippin Williams, создатель плагина Easy Digital Downloads, если бы он начинал разрабатывать свой плагин сейчас, он бы использовал пользовательские таблицы. Если вы создаете что-то, что будет использоваться долгое время или накапливать много данных, эффективнее использовать хорошо спроектированные пользовательские таблицы.
Необходимо убедиться, что другие разработчики плагинов/дополнений могут подключиться к вашему плагину для манипуляции данными до и после их получения. Если вы это обеспечите, всё будет работать отлично.

Интересный материал! Хочу уточнить, что упомянутый фильтр "get_(post_type)meta" на самом деле называется "get(meta-type)_metadata", где meta-type может быть post, comment или user. Таким образом, get_post_meta() будет проходить через фильтр get_post_metadata, независимо от типа записи. Возвращаемое значение фильтра и будет окончательным значением метаданных.

get_(meta-type)_metadata -> действительно, это работает со всеми типами записей, и финальная функция, которая вызывается, это get_post_metadata. Однако фильтр всё равно работает, когда вы его используете.

Привет @unity100, не мог бы ты поделиться своим плагином? Или фрагментами кода всех хуков, которые ты использовал для перемещения метаданных вложений в пользовательскую таблицу? Спасибо.

@Manu https://wordpress.org/plugins/meta-accelerator/ — это то, что я тестировал с WooCommerce, и оно работало достаточно хорошо. Потребовалась некоторая настройка для различных целей, но казалось, что это того стоит. Возможно, за эти годы были внесены некоторые изменения в обработку метаданных, и плагин может нуждаться в обновлении, но это было бы хорошим началом.

Это зависит от того, что вы делаете. Рекомендуемый способ в WordPress — использовать существующие таблицы, так как они разработаны с достаточной гибкостью. Однако иногда может возникнуть необходимость работать с новым типом данных, которые нельзя разместить в существующих таблицах. Например, если вам нужны метаданные для категорий, можно создать таблицу wp_termsmeta.
Однако в большинстве случаев ваши данные можно удобно разместить в существующих таблицах. Где именно хранить данные, зависит от функционала вашего плагина.
- Для общих настроек плагина используйте API-функцию get_option() — эти данные также кэшируются.
- Для настроек плагина, относящихся к отдельной записи, используйте метаданные записи с помощью get_post_meta(). Обычно этого вполне достаточно.
В WordPress также реализовано кэширование для ускорения времени отклика.

Полностью согласен с Денисом. Но есть обходной путь.
Проблема с использованием метаполей записей для значений, по которым выполняется запрос, возникает, когда значения представляют собой массивы и т.п. Например:
array(
'key1' => 'val 1',
'key2' => 'val 2'
);
Это сохраняется в базе данных как сериализованная строка, которая будет выглядеть примерно так:
{array["key1"]...{}...}
Поэтому, если нужно запросить все записи, где array['key2'] = 'val 2'
, WordPress должен извлечь каждую метазапись с именем array, распаковать её, проверить и перейти к следующей. Это точно приведёт к падению сервера, если сайт успешен и содержит множество записей, страниц, пользовательских типов записей и т.д.
Решение зависит от проекта, и вот почему. Если хранить данные в формате var = val
, WordPress сможет выполнять поиск без необходимости распаковки каждой записи средствами PHP. В приведённом выше сценарии можно использовать пространство имён и сохранить метаполя следующим образом:
_array_key1 = 'val 1';
_array_key2 = 'val 2';
Тогда WordPress сможет сразу найти key2 со значением val2. Однако это зависит от проекта. В моём текущем проекте нужно хранить около 20 различных типов данных для каждой пользовательской записи, поэтому описанный выше подход приведёт к созданию огромной таблицы для поиска, учитывая, что мы ожидаем сотни тысяч записей. В таком сценарии единственный выход — использование пользовательской таблицы.
Надеюсь, это поможет кому-нибудь.

Для моего сайта FarmVille :) Я сделал оба варианта, но так и не закончил, потому что продал его:
- Я читал XML FarmVille и сохранял данные в пользовательской таблице
- В WordPress я автоматически создавал произвольные поля для каждого поля в этой таблице (и некоторые дополнительные)
- Теперь беспокойство о том, что произойдет, если значение изменится либо в таблице, либо на другой стороне: в произвольном поле, поскольку они должны постоянно синхронизироваться
Я сделал это, потому что хотел, с одной стороны, позволить пользователям редактировать сайт WordPress, вводя новые данные FarmVille, например, "корова стоит 10 монет", НО с точки зрения интеграции: ЕСЛИ изменение в XML означало, что корова теперь стоит "20 монет" (через плагин редактирования на фронтенде), это будет предложено как вариант после этого: так что либо XML, либо пользователь был прав (что-то вроде вики-системы).
Вот пример использования обоих вариантов.
