J'ai un projet qui nécessite des attributs définis par l'utilisateur pour un objet particulier au moment de l'exécution (disons un objet personne dans cet exemple). Le projet aura de nombreux utilisateurs différents (plus de 1000), chacun définissant ses propres attributs uniques pour ses propres ensembles d'objets « Personne ».

(Par exemple, l'utilisateur n°1 aura un ensemble d'attributs définis, qui s'appliqueront à tous les objets de personne « possédés » par cet utilisateur. Multipliez cela par 1000 utilisateurs, et c'est le nombre minimum d'utilisateurs avec lesquels l'application fonctionnera.) Ces attributs seront utilisés pour interroger l'objet people et renvoyer les résultats.

Je pense que ce sont les approches possibles que je peux utiliser. J'utiliserai C# (et n'importe quelle version de .NET 3.5 ou 4), et j'aurai le champ libre pour savoir quoi utiliser pour un magasin de données. (J'ai mysql et mssql disponibles, bien que j'ai la liberté d'utiliser n'importe quel logiciel, tant qu'il conviendra)

Ai-je oublié quelque chose ou fait des hypothèses incorrectes dans mon évaluation ?

Parmi ces choix, quelle solution choisiriez-vous ?

  1. Modèle objet EAV hybride. (Définissez la base de données à l'aide d'un modèle relationnel normal et ayez une table « sac de propriétés » pour la table Personne).

    Inconvénients : beaucoup de jointures par / requête. Mauvaise performance. Peut atteindre une limite du nombre de jointures/tables utilisées dans une requête.

    J'ai créé un échantillon rapide, doté d'une interface Subsonic 2.x 'esqe :

    Select().From().Where  ... etc
    

    Ce qui génère les jointures correctes, puis filtre + fait pivoter les données renvoyées en c#, pour renvoyer une table de données configurée avec l'ensemble de données correctement typé.

    Je n'ai pas encore chargé de tester cette solution. Il est basé sur les conseils d'EA contenus dans ce livre blanc de Microsoft : Documents RTM SQL Server 2008 Meilleur Pratiques de modélisation sémantique des données pour les performances et l'évolutivité

  2. Permettre à l'utilisateur de créer/modifier dynamiquement la table de l'objet au moment de l'exécution. Cette solution est ce que je pense que NHibernate fait en arrière-plan lors de l'utilisation de propriétés dynamiques, comme indiqué où

    http://bartreyserhove.blogspot.com/2008/ 02/dynamic-domain-mode-using-nhibernate.html

    Inconvénients:

    Au fur et à mesure que le système grandit, le nombre de colonnes définies devient très important et peut atteindre le nombre maximal de colonnes. S'il y a 1000 utilisateurs, chacun avec 10 attributs distincts pour leurs objets 'Person', alors nous aurions besoin d'une table contenant 10k colonnes. Non évolutif dans ce scénario.

    Je suppose que je pourrais autoriser une table attributaire de personne par utilisateur, mais s'il y a 1000 utilisateurs pour commencer, cela fait 1000 tables plus les 10 autres impaires dans l'application.

    Je ne sais pas si cela serait évolutif - mais cela ne semble pas le cas. Quelqu'un s'il vous plaît corrigez-moi si je suis incorrect!

  3. Utiliser une banque de données NoSQL, telle que CouchDb / MongoDb

    D'après ce que j'ai lu, ceux-ci n'ont pas encore fait leurs preuves dans les applications à grande échelle, basées sur des chaînes, et en sont au tout début de la phase de développement. SI je me trompe dans cette évaluation, quelqu'un peut-il me le faire savoir ?

    http://www.eflorenzano.com/blog/post/why-couchdb -suce/

  4. Utilisation de la colonne XML dans la table people pour stocker les attributs

    Inconvénients - pas d'indexation lors de l'interrogation, donc chaque colonne devrait être récupérée et interrogée pour renvoyer un ensemble de résultats, ce qui entraînerait de mauvaises performances de requête.

  5. Sérialisation d'un graphe d'objets dans la base de données.

    Inconvénients - pas d'indexation lors de l'interrogation, donc chaque colonne devrait être récupérée et interrogée pour renvoyer un ensemble de résultats, ce qui entraînerait de mauvaises performances de requête.

  6. Liaisons C# pour berkelyDB

    D'après ce que j'ai lu ici : http://www.dinosaurtech.com/2009 /berkeley-db-c-bindings/

    Berkeley Db s'est avéré utile, mais comme Robert l'a souligné, il n'y a pas d'interface simple. L'ensemble de votre wrapper wOO doit être codé à la main et tous vos index sont maintenus à la main. C'est beaucoup plus difficile que SQL/linq-to-sql, mais c'est le prix à payer pour une vitesse ridicule.

    Cela semble une surcharge importante - mais si quelqu'un peut fournir un lien vers un didacticiel sur la façon de maintenir les index en C # - cela pourrait être un passionné.

  7. Hybride SQL/RDF. Bizarre je n'y avais pas pensé avant. Similaire à l'option 1, mais au lieu d'une table « sac de propriétés », juste XREF vers un magasin RDF ? L'interrogation impliquerait 2 étapes - interroger le magasin RDF pour les personnes atteignant les attributs corrects, pour renvoyer le ou les objets personne, et utiliser les ID de ces objets personne dans la requête SQL pour renvoyer les données relationnelles. Frais généraux supplémentaires, mais pourrait être un amateur.

20
James 10 janv. 2010 à 16:46
1
+1 pour une bonne recherche avant de poser une question. Regardez MongoDB. Écoutez ce podcast et il semble être ce que vous recherchez . Je suis désolé, je ne l'ai pas utilisé personnellement.
 – 
shahkalpesh
10 janv. 2010 à 16:54
Pour info sur 4; vous pouvez combiner des colonnes XML avec des colonnes calculées + persistantes + indexées, mais vous vous retrouvez dans le scénario "trop ​​de colonnes". Avez-vous besoin de pouvoir rechercher sur les propriétés dynamiques ? Cela peut être le facteur de code qui fait la différence entre un sac de propriétés via des paires clé/valeur dans une table, et la sérialisation/xml.
 – 
Marc Gravell♦
10 janv. 2010 à 17:17
Oui - l'objectif principal de l'application est de rechercher efficacement les propriétés dynamiques. Je n'ai pas pensé à combiner des colonnes XML avec des colonnes calculées + persistantes + indexées - avez-vous des ressources expliquant cela plus en détail ?
 – 
James
10 janv. 2010 à 17:27
+1 et favo pour l'excellente question écrite. Suivra la question car vous m'avez rendu curieux.
 – 
bastijn
11 janv. 2010 à 10:26

5 réponses

Meilleure réponse

Le moteur de base de données ESENT sous Windows est largement utilisé pour ce type de données semi-structurées. Un exemple est Microsoft Exchange qui, comme votre application, compte des milliers d'utilisateurs où chaque utilisateur peut définir son propre ensemble de propriétés (propriétés nommées MAPI). Exchange utilise une version légèrement modifiée d'ESENT.

ESENT a beaucoup de fonctionnalités qui permettent aux applications avec de grandes exigences de métadonnées : chaque table ESENT peut avoir environ ~ 32K colonnes définies ; des tables, des index et des colonnes peuvent être ajoutés au moment de l'exécution ; les colonnes éparses n'occupent pas d'espace d'enregistrement lorsqu'elles ne sont pas définies ; et les modèles de tableaux peuvent réduire l'espace utilisé par les métadonnées elles-mêmes. Il est courant que les grandes applications aient des milliers de tables/index.

Dans ce cas, vous pouvez avoir une table par utilisateur et créer les colonnes par utilisateur dans la table, en créant des index sur toutes les colonnes que vous souhaitez interroger. Ce serait similaire à la façon dont certaines versions d'Exchange stockent leurs données. L'inconvénient de cette approche est qu'ESENT n'a pas de moteur de requête, vous devrez donc créer manuellement vos requêtes en tant qu'appels MakeKey/Seek/MoveNext.

Un wrapper géré pour ESENT est ici :

http://managedesent.codeplex.com/

7
Laurion Burchall 28 janv. 2010 à 03:23
Wow! Oui, j'ai certainement raté cela dans mes recherches. Cela semble trop beau pour être vrai. Je me demande s'il y a quelqu'un qui l'utilise pour exécuter une application Web (autre que l'échange)... Hmnnn...
 – 
James
29 janv. 2010 à 16:27
1
Désolé pour l'acceptation tardive - esent gagne haut la main, même avec l'API d'interrogation légèrement verbeuse !
 – 
James
1 févr. 2010 à 12:29

Dans un modèle EAV, vous n'avez pas besoin d'avoir beaucoup de jointures, car vous pouvez simplement avoir les jointures dont vous avez besoin pour le filtrage des requêtes. Pour l'ensemble de résultats, retournez les entrées de propriété sous la forme d'un ensemble de lignes distinct. C'est ce que nous faisons dans notre implémentation EAV.

Par exemple, une requête peut renvoyer des personnes avec la propriété étendue 'Age' > 18 :

Tableau des propriétés :

1        Age
2        NickName

Premier jeu de résultats :

PersonID Name
1        John
2        Mary

Deuxième jeu de résultats :

PersonID PropertyID Value
1        1         24
1        2         'Neo'
2        1         32
2        2         'Pocahontas'

Pour le premier jeu de résultats, vous avez besoin d'une jointure interne pour la propriété étendue 'age' pour interroger la partie d'entité de l'objet Person de base :

select p.ID, p.Name from Persons p
join PersonExtendedProperties pp
on p.ID = pp.PersonID
where pp.PropertyName = 'Age'
and pp.PropertyValue > 18 -- probably need to convert to integer here

Pour le deuxième jeu de résultats, nous effectuons une jointure externe du premier jeu de résultats avec la table PersonExtendedProperties pour obtenir le reste des propriétés étendues. C'est un jeu de résultats "étroit", nous ne faisons pas pivoter les propriétés dans SQL, nous n'avons donc pas besoin de plusieurs jointures ici.

En fait, nous utilisons des tables séparées pour différents types afin d'éviter la conversion de type de données, d'avoir des propriétés étendues indexées et facilement interrogeables.

2
George Polevoy 11 janv. 2010 à 11:09
Très intéressant... Quel RDMS utilisez-vous comme backend ? Je vais maintenant (si le temps le permet - c'est-à-dire le week-end) modifier mon test d'origine (option 1), pour refléter cette méthode (uniquement jointure par attribut interrogé) et faire pivoter la table avec le contenu de la jointure externe (avec un peu de filtrage etc). J'ai besoin de ce pivot final, donc je peux brancher n'importe quel résultat de requête EAV dans tout ce qui accepte une table de données... Hmmnnn... Si seulement j'avais plus de temps... Mon intuition est que cette méthode surpassera la méthode d'origine (en raison de moins jointures), tant que nous avons affaire à des collections/données d'attributs relativement petites.
 – 
James
11 janv. 2010 à 17:04
Serveur MSSQL. La requête elle-même (sans alimenter l'ensemble de résultats) surpasse en fait certaines tables larges natives dans mes tests. (4 paramètres impliqués dans le filtrage sur 20 disponibles). C'est peut-être dû à la logistique des index de table "larges" sur le disque.
 – 
George Polevoy
11 janv. 2010 à 18:41

En supposant que vous placiez une limite, N, sur le nombre d'attributs personnalisés que chaque utilisateur peut définir ; ajoutez simplement N colonnes supplémentaires à la table Person. Ensuite, ayez une table séparée où vous stockez les métadonnées par utilisateur pour décrire comment interpréter le contenu de ces colonnes pour chaque utilisateur. Semblable à #1 une fois que vous avez lu les données, mais aucune jointure n'est nécessaire pour extraire les attributs personnalisés.

0
rwhit 11 janv. 2010 à 01:50
Cela semble bien - mais cela ne limitera-t-il pas les objets à des « attributs uniques » ? Si je voulais stocker, par exemple, une collection des 10 meilleurs livres pour une personne, dans le modèle EAV hybride, je pourrais définir plusieurs attributs "favourite_book" et interroger comme : "WHERE pp1.PropertyName = 'favourite_book' and pp1.PropertyValue = 'catch22' AND pp2.PropertyName = 'favourite_book' and pp2.PropertyValue = 'bible'" Si je dois définir une limite sur le nombre d'attributs personnalisés (utiliser des colonnes de table), je ne serais pas en mesure de stocker ces données. (Je pourrais mais je serais à court de colonnes et les requêtes seraient difficiles à générer dynamiquement)
 – 
James
11 janv. 2010 à 16:50

Pour un problème similaire à votre problème, nous avons utilisé l'approche « colonne XML » (la quatrième dans votre enquête sur les méthodes). Mais vous devez noter que de nombreuses bases de données (SGBD) prennent en charge l'index pour les valeurs XML.

Je vous recommande d'utiliser une table pour Person qui contient une colonne XML avec d'autres colonnes communes. En d'autres termes, concevez la table Person avec des colonnes communes à tous les enregistrements de personne et ajoutez une seule colonne XML pour les attributs dynamiques et différents.

Nous utilisons Oracle. il prend en charge l'index pour son type xml. Deux types d'index sont pris en charge : 1- XMLIndex pour l'indexation d'éléments et d'attributs dans un XML, 2- Oracle Text Index pour permettre la recherche en texte intégral dans les champs de texte du xml.

Par exemple, dans Oracle, vous pouvez créer un index tel que :

CREATE INDEX index1 ON table_name (XMLCast(XMLQuery ('$p/PurchaseOrder/Reference' 
  PASSING XML_Column AS "p" RETURNING CONTENT) AS VARCHAR2(128)));

Et xml-query est pris en charge dans certaines requêtes :

SELECT count(*) FROM purchaseorder
  WHERE XMLCast(XMLQuery('$p/PurchaseOrder/Reference'
  PASSING OBJECT_VALUE AS "p" RETURNING CONTENT)
  AS INTEGER) = 25;

Comme je le sais, d'autres bases de données telles que PostgreSQL et MS SQL Server (mais pas mysql) prennent en charge de tels modèles d'index pour la valeur xml.

Voir également: http://docs.oracle.com/cd/E11882_01 /appdev.112/e23094/xdb_indexing.htm#CHDEADIH

0
Radim Köhler 26 juin 2013 à 10:18

Ma recommandation:

Autoriser les propriétés à être marquées comme indexables. Avoir une petite limite stricte sur le nombre de propriétés indexables et sur les colonnes par objet. Avoir une grande limite stricte sur le nombre total de types de colonnes dans tous les objets.

Implémentez les index sous forme de tables séparées (une par index) jointes à la table principale de données (la table principale a une grande clé unique pour l'objet). (Les tables d'index peuvent ensuite être créées/supprimées selon les besoins).

Sérialisez les données, y compris les colonnes d'index, et placez les propriétés d'index dans des colonnes relationnelles de première classe dans leurs tables d'index dédiées. Utilisez JSON au lieu de XML pour économiser de l'espace dans la table. Appliquez la stratégie de nom de colonne court (ou la stratégie de nom d'affichage long et de nom stocké court) pour économiser de l'espace et augmenter les performances.

Utilisez des quarks pour les identifiants de champ (mais uniquement dans le moteur principal pour économiser de la RAM et accélérer certaines opérations de lecture - ne vous fiez pas à la comparaison de pointeurs de quarks dans tous les cas).

Mon avis sur tes options :

1 est un possible. Les performances seront clairement inférieures à celles si les colonnes d'ID de champ ne sont pas stockées.

2 est un non en général, les moteurs de base de données ne sont pas tous satisfaits des changements de schéma dynamiques. Mais un oui possible si votre moteur DB est bon à cela.

3 Possible.

4 Oui, mais j'utiliserais JSON.

5 Semble comme 4 seulement moins optimisé ??

6 ça sonne bien ; irait avec s'il était heureux d'essayer quelque chose de nouveau et aussi s'il était satisfait de la fiabilité et des performances, mais voudrait généralement opter pour une technologie plus traditionnelle. J'aimerais également réduire le nombre de moteurs impliqués dans la coordination d'une transaction à un niveau inférieur à ce qui serait vrai ici.

Modifier : Bien sûr, bien que j'aie recommandé quelque chose, il ne peut y avoir de réponse générale correcte ici : profilez divers modèles de données et approches avec vos données pour voir ce qui fonctionne le mieux pour votre application.

Modifier : Modification du libellé de la dernière modification.

0
martinr 10 janv. 2010 à 17:51
Salut Martinr, merci ! Grands points, en particulier concernant : #6 et le nombre de moteurs... Je ne suis pas sûr de 2 éléments - pourriez-vous s'il vous plaît développer ? re: 1: "Les performances seront clairement inférieures à celles des colonnes d'ID de champ non stockées." Je ne comprends pas bien ce que vous voulez dire, pourriez-vous développer s'il vous plaît ? re: "Implémenter les index en tant que tables séparées". Si je comprends bien, cela signifie-t-il que pour chacun des 1000 "utilisateurs" du système, disons qu'ils auront/utiliseront 10 colonnes indexables chacun, cela signifierait qu'il y aurait 10k tables "index" dans la base de données ? Ou ai-je complètement mal compris ?
 – 
James
10 janv. 2010 à 18:18
RE : 1 Je suppose que votre sac de propriétés aurait des colonnes KEY & VALUE. Tout ce que je dis dans mon commentaire sur 1, c'est qu'il vaut probablement mieux ne pas stocker KEY. RE : "10 000 tables d'index ?" Je ne suis pas sûr de croire que vous auriez 10 000 propriétés uniques pour une personne !! (plus comme 2000 tops) mais théoriquement si vous commencez à avoir un grand nombre de tables, vous pourriez avoir N index numériques standard disponibles, M index de chaîne standard disponibles et l'utilisateur mappe ses propriétés sur celles-ci, et vous partitionnez un peu les index entre l'utilisateur population. Je concède que les changements de schéma constants sur les tables 10k sonnent un peu beaucoup.
 – 
martinr
10 janv. 2010 à 18:41
Je veux dire par "mieux vaut ne pas stocker KEY" pour utiliser une autre solution (pas un sac de propriétés) où vous n'avez pas de champ SQL KEY.
 – 
martinr
10 janv. 2010 à 18:42
Smashing - est parfaitement logique. Merci pour la clarification!
 – 
James
10 janv. 2010 à 18:48
:-) Idéalement, les changements de schéma ne se produisent pas fréquemment. Idéalement, tous les schémas sont spécifiés une fois. Mais nous parlons d'un système où le schéma de l'utilisateur peut changer s'il en a besoin. Il peut être judicieux de placer tous les index sur une seule table principale, avec un USERID. Le mappage entre les champs d'index et les champs d'utilisateur serait alors dans le code de l'application. Peut-être que la table principale est divisée en plusieurs tables en fonction de la valeur USERID. Excellente question Jacques.
 – 
martinr
10 janv. 2010 à 18:50