web-dev-qa-db-fra.com

Comment concevoir une base de données pour les champs définis par l'utilisateur?

Mes exigences sont:

  • Besoin de pouvoir ajouter dynamiquement des champs définis par l'utilisateur de tout type de données
  • Besoin de pouvoir interroger rapidement les fichiers UDF
  • Nécessité de pouvoir effectuer des calculs sur les FDU en fonction du type de données
  • Besoin de pouvoir trier les FDU en fonction du type de données

Les autres informations:

  • Je recherche la performance principalement
  • Il existe quelques millions d’enregistrements maîtres auxquels des données UDF peuvent être attachées.
  • Lors de ma dernière vérification, notre base de données actuelle contenait plus de 50 millions d'enregistrements UDF.
  • La plupart du temps, une FDU n’est attachée qu’à quelques milliers d’archives principales, mais pas toutes.
  • Les fichiers UDF ne sont pas joints ni utilisés comme clés. Ce ne sont que des données utilisées pour des requêtes ou des rapports

Options:

  1. Créez une grande table avec StringValue1, StringValue2 ... IntValue1, IntValue2, etc.

  2. Créez un tableau dynamique qui ajoute une nouvelle colonne à la demande en fonction des besoins. Je n'aime pas non plus cette idée, car j'estime que la performance serait lente à moins d'indexer chaque colonne.

  3. Créez une seule table contenant UDFName, UDFDataType et Value. Lorsqu'un nouveau fichier UDF est ajouté, générez une vue qui extrait uniquement ces données et les analyse dans le type spécifié. Les articles qui ne répondent pas aux critères d'analyse renvoient NULL.

  4. Créez plusieurs tables UDF, une par type de données. Nous aurions donc des tables pour UDFStrings, UDFDates, etc. Cela ferait probablement la même chose que # 2 et générerait automatiquement une vue à chaque fois qu'un nouveau champ serait ajouté.

  5. XML DataTypes? Je n'ai jamais travaillé avec ceux-ci auparavant, mais je les ai vus mentionnés. Je ne sais pas s'ils me donneraient les résultats que je veux, surtout en matière de performance.

  6. Autre chose?

136
Rachel

Si la performance est la préoccupation principale, je choisirais le numéro 6 ... un tableau par UDF (en réalité, il s’agit d’une variante du numéro 2). Cette réponse est spécifiquement adaptée à cette situation et à la description de la distribution des données et des modèles d'accès décrits.

Avantages:

  1. Étant donné que vous indiquez que certaines fonctions définies par l'utilisateur ont des valeurs pour une petite partie de l'ensemble de données, un tableau distinct vous donnerait les meilleures performances, car ce tableau ne sera que la taille nécessaire pour prendre en charge la fonction définie par l'utilisateur. Il en va de même pour les indices associés.

  2. Vous obtenez également un gain de vitesse en limitant la quantité de données à traiter pour les agrégations ou autres transformations. Le fractionnement des données en plusieurs tables vous permet d'effectuer une partie de l'agrégation et d'autres analyses statistiques sur les données UDF, puis de joindre ce résultat à la table principale via une clé étrangère pour obtenir les attributs non agrégés.

  3. Vous pouvez utiliser des noms de table/colonne qui reflètent la nature réelle des données.

  4. Vous avez le contrôle total pour utiliser les types de données, les contraintes de vérification, les valeurs par défaut, etc. pour définir les domaines de données. Ne sous-estimez pas l'impact sur les performances résultant de la conversion à la volée des types de données. Ces contraintes aident également les optimiseurs de requêtes SGBDR à élaborer des plans plus efficaces.

  5. Si vous avez besoin d'utiliser des clés étrangères, l'intégrité référentielle déclarative intégrée est rarement surpassée par l'application de contraintes basée sur des déclencheurs ou au niveau de l'application.

Les inconvénients:

  1. Cela pourrait créer beaucoup de tables. L'application d'une séparation de schéma et/ou d'une convention de dénomination atténuerait ce problème.

  2. Il faut plus de code d'application pour utiliser la définition et la gestion UDF. Je pense que cela nécessitera encore moins de code que pour les options d’origine 1, 3 et 4.

Autres considérations:

  1. Si la nature des données présente un intérêt pour le regroupement des FDU, cela devrait être encouragé. De cette façon, ces éléments de données peuvent être combinés dans un seul tableau. Par exemple, supposons que vous avez des fonctions définies par l'utilisateur pour la couleur, la taille et le coût. La tendance dans les données est que la plupart des instances de ces données ressemblent à

     'red', 'large', 45.03 
    

    plutôt que

     NULL, 'medium', NULL
    

    Dans un tel cas, vous ne subirez pas de pénalité liée à la vitesse en combinant les 3 colonnes dans 1 tableau car peu de valeurs seraient NULL et vous éviterez de créer 2 tableaux supplémentaires, ce qui représente 2 jointures de moins nécessaires lorsque vous devez accéder aux 3 colonnes. .

  2. Si vous heurtez un mur de performance d'un fichier UDF très peuplé et utilisé fréquemment, vous devriez envisager de l'inclure dans le tableau principal.

  3. La conception d'une table logique peut vous mener à un certain point, mais lorsque le nombre d'enregistrements devient vraiment énorme, vous devez également commencer à regarder quelles options de partitionnement de table sont fournies par votre SGBDR de choix.

47
Phil Helmer

J'ai écrit à propos de ce problème beaucoup . La solution la plus courante est l'antipatterne Entity-Attribute-Value, qui est similaire à ce que vous décrivez dans votre option n ° 3. Évitez cette conception comme la peste .

Ce que j’utilise pour cette solution lorsque j’ai besoin de champs personnalisés réellement dynamiques, c’est de les stocker dans un blob XML, afin que je puisse ajouter de nouveaux champs à tout moment. Mais pour le rendre rapide, créez également des tables supplémentaires pour chaque champ sur lequel vous devez effectuer une recherche ou un tri (vous ne devez pas créer de table par champ, mais simplement une table par consultable champ). Cela s'appelle parfois une conception d'index inversé.

Vous pouvez lire un article intéressant de 2009 sur cette solution ici: http://backchannel.org/blog/friendfeed-schemaless-mysql

Vous pouvez également utiliser une base de données orientée document, où il est prévu que vous ayez des champs personnalisés par document. Je choisirais Solr .

22
Bill Karwin

Je créerais probablement une table de la structure suivante:

  • nom varchar
  • type varchar
  • decimal NumberValue
  • varchar StringValue
  • date DateValue

Les types exacts de cours dépendent de vos besoins (et bien sûr du dbms que vous utilisez). Vous pouvez également utiliser le champ NumberValue (decimal) pour les int et booleans. Vous aurez peut-être aussi besoin d'autres types.

Vous avez besoin d’un lien vers les fiches maîtres qui possèdent la valeur. Il est probablement le plus simple et le plus rapide de créer une table de champs utilisateur pour chaque table principale et d’ajouter une simple clé étrangère. De cette façon, vous pouvez filtrer facilement et rapidement les fiches types par champs utilisateur.

Vous voudrez peut-être avoir une sorte d'informations de métadonnées. Donc, vous vous retrouvez avec ce qui suit:

Table UdfMetaData

  • int id
  • nom varchar
  • type varchar

Table MasterUdfValues

  • int Master_FK
  • int MetaData_FK
  • decimal NumberValue
  • varchar StringValue
  • date DateValue

Quoi que vous fassiez, je voudrais pas changer la structure de la table de manière dynamique. C'est un cauchemar d'entretien. Je voudrais aussi pas utiliser des structures XML, elles sont beaucoup trop lentes.

9

Cela ressemble à un problème qui pourrait être mieux résolu par une solution non relationnelle, telle que MongoDB ou CouchDB.

Ils permettent tous les deux l'expansion dynamique du schéma tout en vous permettant de maintenir l'intégrité du tuple que vous recherchez.

Je suis d'accord avec Bill Karwin, le modèle EAV n'est pas une approche performante pour vous. L'utilisation de paires nom-valeur dans un système relationnel n'est pas intrinsèquement mauvaise, mais ne fonctionne correctement que lorsque la paire nom-valeur constitue un tuple d'informations complet. Lorsque son utilisation vous oblige à reconstruire dynamiquement une table au moment de l'exécution, toutes sortes de choses commencent à devenir difficiles. Interroger devient un exercice de maintenance de pivot ou vous oblige à insérer la reconstruction du tuple dans le calque d'objet.

Vous ne pouvez pas déterminer si une valeur nulle ou manquante est une entrée valide ou une absence d'entrée sans incorporer de règles de schéma dans votre couche d'objet.

Vous perdez la capacité de gérer efficacement votre schéma. Un type varchar de 100 caractères est-il le bon type pour le champ "valeur"? 200 caractères? Devrait-il être nvarchar à la place? Cela peut être un compromis difficile et qui finit par vous imposer des limites artificielles à la nature dynamique de votre jeu. Quelque chose comme "vous ne pouvez avoir que x champs définis par l'utilisateur et chacun ne peut contenir que y caractères.

Avec une solution orientée document, telle que MongoDB ou CouchDB, vous conservez tous les attributs associés à un utilisateur dans un même Tuple. Étant donné que les jointures ne sont pas un problème, la vie est heureuse, car aucune des deux ne se débrouille bien avec les jointures, malgré le battage publicitaire. Vos utilisateurs peuvent définir autant d'attributs qu'ils le souhaitent (ou vous autoriserez) à des longueurs qui ne deviennent difficiles à gérer que lorsque vous atteignez environ 4 Mo.

Si vous avez des données qui nécessitent une intégrité de niveau ACID, vous pouvez envisager de scinder la solution, les données à haute intégrité résidant dans votre base de données relationnelle et les données dynamiques résidant dans un magasin non relationnel.

8
Data Monk

Même si vous indiquez à un utilisateur d'ajouter des colonnes personnalisées, les requêtes sur ces colonnes ne seront pas nécessairement performantes. De nombreux aspects de la conception des requêtes leur permettent de bien fonctionner, le plus important étant la spécification appropriée sur ce qui doit être stocké en premier lieu. Ainsi, fondamentalement, voulez-vous permettre aux utilisateurs de créer un schéma sans se soucier des spécifications et de pouvoir dériver rapidement des informations à partir de ce schéma? Si tel est le cas, il est alors peu probable qu'une telle solution évolue bien, surtout si vous souhaitez permettre à l'utilisateur d'effectuer une analyse numérique des données.

Option 1

Cette approche vous donne un schéma sans aucune connaissance de ce qu’il signifie, une recette pour un désastre et un cauchemar pour les concepteurs de rapports. C'est-à-dire que vous devez disposer des métadonnées pour savoir quelle colonne stocke quelles données. Si ces métadonnées sont perturbées, elles risquent de traiter vos données. De plus, il est facile de mettre les mauvaises données dans la mauvaise colonne. ("Quoi? String1 contient le nom des couvents? Je pensais que c'était la drogue préférée de Chalie Sheen.")

Option 3,4,5

OMI, les exigences 2, 3 et 4 éliminent toute variation d'un EAV. Si vous avez besoin d'interroger, de trier ou de faire des calculs sur ces données, un EAV est le rêve de Cthulhu et le cauchemar de votre équipe de développement et de votre DBA. Les systèmes EAV créeront un goulot d'étranglement en termes de performances et ne vous donneront pas l'intégrité des données dont vous avez besoin pour obtenir rapidement les informations souhaitées. Les requêtes se tourneront rapidement vers les nœuds Gordiens du tableau croisé.

Option 2,6

Cela laisse vraiment un choix: rassembler les spécifications, puis construire le schéma.

Si le client souhaite obtenir les meilleures performances possibles pour les données qu'il souhaite stocker, il doit alors suivre le processus de travail avec un développeur pour comprendre ses besoins et les stocker de manière aussi efficace que possible. Il pourrait toujours être stocké dans une table séparée du reste des tables avec un code qui construit dynamiquement un formulaire basé sur le schéma de la table. Si vous avez une base de données qui autorise les propriétés étendues sur les colonnes, vous pouvez même les utiliser pour aider le générateur de formulaire à utiliser les libellés Nice, les info-bulles, etc. Il suffit donc d’ajouter le schéma. Quoi qu'il en soit, pour créer et exécuter des rapports efficacement, les données doivent être stockées correctement. Si les données en question contiennent de nombreuses valeurs NULL, certaines bases de données peuvent stocker ce type d'informations. Par exemple, SQL Server 2008 possède une fonctionnalité appelée Colonnes éparses, spécialement conçue pour les données contenant beaucoup de valeurs NULL.

S'il ne s'agissait que d'un ensemble de données sur lesquelles aucune analyse, filtrage ou tri ne devait être effectué, je dirais qu'une variante d'un EAV pourrait faire l'affaire. Toutefois, compte tenu de vos besoins, la solution la plus efficace consiste à obtenir les spécifications appropriées même si vous stockez ces nouvelles colonnes dans des tables séparées et créez des formulaires de manière dynamique à partir de ces tables.

colonnes éparses

6
Thomas

C'est une situation problématique et aucune des solutions ne semble "correcte". Cependant, l'option 1 est probablement la meilleure en termes de simplicité et de performances.

C'est également la solution utilisée dans certaines applications d'entreprise commerciales.

[~ # ~] éditer [~ # ~]

une autre option disponible maintenant, mais qui n'existait pas (ou du moins n'était pas arrivée à maturité) lorsque la question avait été posée à l'origine, consistait à utiliser des champs JSON dans la base de données.

de nombreuses bases de données relationnelles prennent désormais en charge les champs basés sur JSON (pouvant inclure une liste dynamique de sous-champs) et autorisent les requêtes sur ces champs.

postgress

mysql

4
Ophir Yoktan
  1. Créez plusieurs tables UDF, une par type de données. Nous aurions donc des tables pour UDFStrings, UDFDates, etc. Cela ferait probablement la même chose que # 2 et générerait automatiquement une vue à chaque fois qu'un nouveau champ serait ajouté.

Selon mes recherches, plusieurs tables basées sur le type de données ne vont pas vous aider en termes de performances. Surtout si vous avez des données en vrac, comme des enregistrements 20K ou 25K avec plus de 50 UDF. La performance était la pire.

Vous devriez aller avec une seule table avec plusieurs colonnes comme:

varchar Name
varchar Type
decimal NumberValue
varchar StringValue
date DateValue
4
Amit Contractor

J'ai déjà eu l'expérience des étapes 1, 3 et 4 et elles finissent toutes en désordre, les données ne sont pas claires ou très compliquées avec une sorte de catégorisation souple pour décomposer les données en types d'enregistrements dynamiques.

Je serais tenté d'essayer XML, vous devriez pouvoir appliquer des schémas au contenu du XML pour vérifier le typage des données, etc., ce qui vous aidera à conserver des ensembles de données UDF différents. Dans les versions plus récentes de SQL Server, vous pouvez indexer des champs XML, ce qui devrait vous aider à améliorer les performances. (voir http://blogs.technet.com/b/josebda/archive/2009/03/23/sql-server-2008-xml-indexing.aspx ) par exemple

2
Jon Egerton

Si vous utilisez SQL Server, ne négligez pas le type sqlvariant. C'est assez rapide et devrait faire votre travail. D'autres bases de données peuvent avoir quelque chose de similaire.

Les types de données XML ne sont pas très bons pour des raisons de performances. Si vous effectuez des calculs sur le serveur, vous devez constamment les désérialiser.

L'option 1 semble mauvaise et a l'air crue, mais la performance peut être votre meilleur pari. J'ai créé des tables avec des colonnes nommées Field00-Field99 auparavant parce que vous ne pouvez tout simplement pas battre les performances. Vous devrez peut-être également prendre en compte vos performances INSERT, auquel cas il s’agit également de celui à privilégier. Vous pouvez toujours créer des vues sur cette table si vous voulez qu'elle soit soignée!

2
Tim Rogers

SharePoint utilise l'option 1 et présente des performances raisonnables.

1
Nathan DeWitt

J'ai réussi très bien dans le passé en n'utilisant aucune de ces options (option 6? :)).

Je crée un modèle avec lequel les utilisateurs peuvent jouer (stocker en tant que XML et l'exposer via un outil de modélisation personnalisé) et à partir des tables et des vues générées par le modèle pour joindre les tables de base aux tables de données définies par l'utilisateur. Ainsi, chaque type aurait une table de base avec des données de base et une table d’utilisateur avec des champs définis par l’utilisateur.

Prenons un document comme exemple: les champs typiques seraient nom, type, date, auteur, etc. Cela irait dans la table principale. Ensuite, les utilisateurs définiraient leurs propres types de document spéciaux avec leurs propres champs, tels que contract_end_date, renew_clause, blah blah blah. Pour ce document défini par l'utilisateur, il y aurait la table de document principale, la table xcontract, jointe à une clé primaire commune (la clé primaire de xcontracts est donc également étrangère à la clé primaire de la table principale). Ensuite, je générerais une vue pour envelopper ces deux tableaux. Les performances lors des requêtes étaient rapides. Des règles de gestion supplémentaires peuvent également être intégrées aux vues. Cela a très bien fonctionné pour moi.

1
Kell

Je recommanderais # 4 car ce type de système était utilisé dans Magento, qui est une plate-forme CMS de commerce électronique hautement accréditée. Utilisez une seule table pour définir vos champs personnalisés à l'aide des colonnes fieldId & label. Ensuite, ayez des tables séparées pour chaque type de données et dans chacune de ces tables, un index indexé par fieldId et le type de données valeur colonnes. Ensuite, dans vos requêtes, utilisez quelque chose comme:

SELECT *
FROM FieldValues_Text
WHERE fieldId IN (
    SELECT fieldId FROM Fields WHERE userId=@userId
)
AND value LIKE '%' + @search + '%'

Cela garantira, à mon avis, les meilleures performances possibles pour les types définis par l'utilisateur.

D'après mon expérience, j'ai travaillé sur plusieurs sites Web Magento, qui servent des millions d'utilisateurs par mois, hébergent des milliers de produits avec des attributs personnalisés et la base de données gère facilement la charge de travail, même pour la génération de rapports.

Pour la création de rapports, vous pouvez utiliser PIVOT pour convertir vos valeurs Champs table libellé en noms de colonnes, puis faites pivoter les résultats de votre requête de chaque table de types de données vers ceux-ci. colonnes.

0
Mark Entingh

Dans les commentaires, je vous ai vu affirmer que les champs UDF doivent vider les données importées qui ne sont pas correctement mappées par l'utilisateur.

Une autre option consiste peut-être à suivre le nombre de fonctions définies par chaque utilisateur et à les obliger à réutiliser des champs en indiquant qu'ils peuvent utiliser 6 sommets personnalisés (ou une autre limite également aléatoire).

Lorsque vous êtes confronté à un problème de structuration de base de données tel que celui-ci, il est souvent préférable de revenir à la conception de base de l'application (système d'importation dans votre cas) et d'imposer quelques restrictions supplémentaires.

Maintenant, ce que je ferais, c’est l’option 4 (EDIT) avec l’ajout d’un lien vers les utilisateurs:

general_data_table
id
...


udfs_linked_table
id
general_data_id
udf_id


udfs_table
id
name
type
owner_id --> Use this to filter for the current user and limit their UDFs
string_link_id --> link table for string fields
int_link_id
type_link_id

Maintenant, assurez-vous de créer des vues pour optimiser les performances et obtenir vos index correctement. Ce niveau de normalisation réduit l'encombrement de la base de données mais rend votre application plus complexe.

0
Wouter Simons

Notre base de données alimente une SaaS (logiciel de support technique) où les utilisateurs ont plus de 7 000 "champs personnalisés". Nous utilisons une approche combinée:

  1. (EntityID, FieldID, Value) table pour rechercher les données
  2. un champ JSON dans la table entities, qui contient toutes les valeurs d'entité, utilisé pour afficher les données. (De cette façon, vous n'avez pas besoin d'un million de jointures pour obtenir les valeurs).

Vous pourriez en outre scinder la division n ° 1 pour avoir une "table par type de données" comme cette réponse suggère, de cette façon, vous pouvez même indexer vos fichiers UDF.

P.S. Quelques mots pour défendre l'approche "Entité-Attribut-Valeur" que tout le monde continue de dénigrer. Nous avons utilisé le n ° 1 sans le n ° 2 pendant des décennies et cela a très bien fonctionné. Parfois, c'est une décision d'affaires. Avez-vous le temps de réécrire votre application et de redéfinir la base de données ou pouvez-vous gagner quelques dollars sur un serveur en nuage, qui est vraiment bon marché de nos jours? En passant, lorsque nous utilisions l'approche n ° 1, notre base de données détenait des millions d'entités, auxquelles accédaient des centaines de milliers d'utilisateurs, et un serveur de base de données à double cœur de 16 Go fonctionnait parfaitement (un serveur virtuel "r3" sur AWS). .

0
Alex