Date post: | 28-Mar-2018 |
Category: |
Documents |
Upload: | truongliem |
View: | 212 times |
Download: | 0 times |
Institut for Datalogi
Aarhus Universitet
Åbogade 34
8200 Aarhus N
Tlf .: 87154112
Fax: 87154115
E-mail: [email protected]
http://www.cs.au.dk/da
INSTITUT FOR DATALOGI SCIENCE AND TECHNOLOGY
AARHUS UNIVERSITET
Hovedopgave Master i Informationsteknologi
linien i Softwarekonstruktion
Design af en dynamisk datamodel
af Thomas Boel Sigurdsson
14. juni 2012
Thomas Boel Sigurdsson, studerende
Ira Assent, vejleder
1
Indhold
1 Motivation for projektet .................................................................. 2
2 Problemformulering ......................................................................... 3
2.1 Funktionelle krav ......................................................................... 3
2.2 Arkitektoniske krav ...................................................................... 5
3 Metode og relateret arbejde ............................................................ 8
4 Resultater ...................................................................................... 11
4.1 Design af ER model .................................................................... 11
4.2 Design af databasemodel .......................................................... 12
4.3 Selvbeskrivende datamodel ....................................................... 17
4.4 Bootstrap data ........................................................................... 18
4.5 API til datavedligehold og forespørgsler ................................... 21
4.5.1 Oprettelse af objekter ........................................................... 22
4.5.2 Forespørgsler ........................................................................ 25
4.6 Måling og tuning af forespørgsler ............................................. 28
4.6.1 Import forberedelser ............................................................. 28
4.6.2 Import af data ....................................................................... 30
4.6.3 Forespørgsler ........................................................................ 31
4.6.4 Analyse af første forespørgsel ............................................... 36
4.6.5 Måling og tuning af øvrige forespørgsler .............................. 42
4.6.6 Måling af oprettelse, opdatering og sletning ........................ 46
4.6.7 Diskussion efter indledende analyse ..................................... 50
4.7 Alternativt XML baseret design af databasemodel ................... 52
4.8 Måling og tuning af ændrede forespørgsler .............................. 57
4.8.1 Ændrede SQL forespørgsler................................................... 57
4.8.2 Forsøg med ikke-indekserede attributter ............................. 62
4.8.3 Nye målinger af oprettelse, opdatering og sletning .............. 65
4.8.4 Diskussion efter opfølgende analyse .................................... 68
4.9 Performancemåling ................................................................... 69
5 Konklussion .................................................................................... 71
6 Referencer ..................................................................................... 73
7 Bilag ............................................................................................... 74
8 Appendiks ...................................................................................... 75
8.1 Appendiks 1 – Database create script ....................................... 75
8.2 Appendiks 2 – Forespørgsels API ............................................... 78
2
1 Motivation for projektet Mange moderne forretningsapplikationer anvender en dynamisk
datamodel, frem for en konventionel datamodel, til repræsentation
af objekter i en relationel database. En dynamisk datamodel har,
under visse forhold, en række fordele sammenlignet med en
konventionel. Blandt disse er særligt, at den effektivt håndterer
store mængder attributter med spredt anvendelse. Desuden
muliggør den, at brugere kan oprette skræddersyede objekttyper og
attributter, uden at databasens relationelle skema ændres. En
dynamisk datamodel har imidlertid også typisk en række svagheder,
herunder potentielt ringere performance og mere komplekse SQL
forespørgsler i forhold til en konventionel datamodel.
Dynamiske datamodeller optræder i litteraturen under forskellige
navne. [EAV2007] benævner dem således Entity-Attribute-Value
modeller, mens de andre steder går under betegnelsen semantiske
datamodeller.
Rapporten relaterer sig til forfatterens virke hos Omada A/S i
København, som i en årrække har udviklet standardprodukter inden
for Identity Management, herunder Omada Identity Suite [OIS2012].
Omada blev stiftet i år 2000 og beskæftiger i dag knap 100
mennesker på kontorer i Danmark, Tyskland, England og USA.
Kundeporteføljen omfatter en række globale selskaber, bl.a. A.P.
Møller Mærsk, Ecco, Bayer og BMW.
Flere af Omadas produkter er baseret på dynamiske datamodeller
og er konstruerede til at kunne håndtere store datamængder.
Nærværende rapport er inspireret af et planlagt projekt hos Omada
med det formål, at fremstille en ny, genbrugelig dynamisk
datamodel, der kan anvendes som komponent i fremtidige
softwareprodukter.
Rapporten omhandler således designet af en dynamisk datamodel,
baseret på en relationel database.
3
2 Problemformulering Rapporten har til formål at designe og evaluere en dynamisk
datamodel, som opfylder kravene beskrevet i det følgende.
2.1 Funktionelle krav Den dynamiske datamodel skal opfylde følgende funktionelle krav:
1. Den dynamiske datamodel skal understøtte
oprettelsen af brugerdefinerede objekttyper og
attributtyper
2. En attributtype skal kunne tildeles én af følgende
datatyper: String, Integer, DateTime, Boolean
eller Reference
3. En attributtype skal endvidere enten kunne
tildeles en enkelt værdi eller multiple værdier
4. Der skal være et API, som understøtter
oprettelse, læsning, opdatering og sletning af
objekter
5. API’et skal desuden understøtte et
forespørgselssprog
6. Objekter skal kunne have referencer til hinanden
og dermed indgå i en objektgraf
7. Objekter skal opbevares i en relationel database
Ad 1)
Det skal være muligt at oprette brugerdefinerede objekttyper og
attributtyper, som kan bindes sammen.
Eksempelvis skal man kunne oprette objekttypen Employee med de
tilhørende attributter: EmployeeID, FirstName, LastName,
DayOfBirth og Manager.
Ad 2)
En attributtype skal kunne antage én af følgende datatyper: String,
Integer, DateTime, Boolean eller Reference.
Datatypen Reference anvendes til at angive et andet objekt fra
modellen som værdi. Et eksempel kunne være førnævnte Manager
attribut, med hvilken en Employee kan referere en anden Employee,
som er førstnævntes daglige leder.
Datatypen skal kun kunne angives ved oprettelsen af en attributtype
– den skal ikke efterfølgende kunne ændres.
4
Ad 3)
En attributtype skal enten kunne understøtte, at der angives én
enkelt værdi for den, eller alternativt at der kan angives et vilkårligt
antal værdier for den.
Denne angivelse skal ligeledes kun kunne angives ved oprettelsen af
en attributtype – den skal ikke efterfølgende kunne ændres.
Ad 4)
Anvendelsen af den dynamiske datamodel skal ske via et API med
følgende operationer:
CreateObject(attributeValues)
UpdateObject(objectId, attributeValues)
GetObject(objectId)
DeleteObject(objectId)
QueryObjects(query)
Ud over disse operationer, skal der være operationer til
vedligeholdelsen af objekttyper og attributtyper.
Ad 5)
Den dynamiske datamodel skal understøtte forespørgsler i et
simpelt sprog, der benævnes BQuery (BasicQuery). Sproget har
følgende syntaks:
<query> ::= ”/” <object type> ”[” <expressionlist> ”]”
<expressionlist> ::= <expression> |
<expression> <outeroperator> <expressionlist>
<outeroperator> ::= ”and” | ”or”
<expression> ::= <value> <inneroperator> <value>
<value> ::= <attribute> | <stringvalue> |
<integervalue> | <datetimevalue> |
<booleanvalue> | <referencevalue>
<inneroperator> ::= ”=” | ”<>” | ”<” | ”>” |
”<=” | ”>=” | ”contains”
<attribute> :== <text>
<stringvalue> :== ”’”<text>”’”
<integervalue> :== <integer>
<booleanvalue> :== ”true” | ”false”
Eksempel:
/Employee[FirstName = ’Thomas’ and LastName=’Sigurdsson’]
5
Denne forespørgsel vil finde alle Employee objekter med fornavnet
”Thomas” og efternavnet ”Sigurdsson”.
De to values, der indgår i en expression, skal have samme datatype.
En attribut af datatypen String kan således kun indgå i en
ekspression med en anden attribut af datatypen String eller en
konstant strengværdi.
De listede inneroperators er kun gyldige for visse datatyper, som vist
i Figur 1.
Inner operator Gyldig for datatype
= String Integer DateTime Boolean Reference
<> String Integer DateTime Boolean Reference
< Integer DateTime
> Integer DateTime
<= Integer DateTime
>= Integer DateTime
contains String
Figur 1 – Operatorers gyldighed for datatyper
Ad 6)
Et objekt skal kunne referere andre objekter via attributter af
datatype Reference. Eksempelvis skal et Employee objekt kunne
referere sin nærmeste leder via en Manager attribut af datatype
Reference.
Ad 7)
Den dynamiske datamodels data skal opbevares i en relationel
database.
2.2 Arkitektoniske krav Designet af den dynamiske datamodel vil blive vurderet i forhold til
følgende arkitektoniske kvaliteter:
1. Performance og skalérbarhed
2. Designets kompleksitet
3. Pladsforbrug
Til vurderingen af designet vil der blive udarbejdet et antal
arkitektoniske prototyper.
6
Ad 1)
Den dynamiske datamodel har følgende succeskriterier:
- Den skal kunne rumme 1,5 mio. objekter med i gennemsnit
5 attributværdier.
- Datamodellen skal opfylde følgende:
o Oprettelse af et objekt skal udføres på under 0,3
sekunder.
o Opdatering af et objekt skal udføres på under 0,2
sekunder.
o Læsning af et enkelt skal udføres på under 0,5
sekunder.
o En gennemsnitlig forespørgsel (se nedenfor) skal
udføres på under 2 sekunder.
o Sletning af et objekt skal udføres på under 0,5
sekunder.
Ovenstående forudsætter en almindelig serverkonfiguration efter
dagens standarder.
Det forventes, at forholdet mellem antal brugerudførte
forespørgsler og opdateringer af data vil være ca. 5:1. Dette bunder
i, at datamodellen forventes anvendt i et system med web
brugergrænseflade, hvor brugerne oftere søger efter og læser data
end de opdaterer dem.
Følgende eksempler på BQuery forespørgsler vurderes at være
repræsentative og gennemsnitlige:
”Find en medarbejder med et specifikt Id”
/Employee[ObjectId=’240031F2-5CD7-471D-BF73-F55AE02C4F82’]
”Find de medarbejdere der hedder Jens Hansen”
/Employee[FirstName=’Jens’ and LastName=’Hansen’]
”Find alle medarbejdere der hedder enten Olsen eller Hansen til
efternavn”
/Employee[LastName=’Olsen’ or LastName=’Hansen’]
”Find alle medarbejdere oprettet efter 15. marts”
/Employee[CreatedTime > 2012-03-15]
”Find de medarbejdere der er afdelingsledere”
7
/Employee[IsManager = true]
”Find de medarbejdere der er ansat efter 2008”
/Employee[EmploymentYear > 2008]
”Find alle medarbejdere hvor ’assistant’ indgår i jobtitlen”
/Employee[JobTitle contains 'assistant']
Ad 2)
Det er en målsætning, at det udarbejde design ikke har en
unødvendig høj kompleksitet. Særligt er det ønskeligt, at de SQL
forespørgsler, der skal foretages som et resultat af en
objektforespørgsel, ikke bliver unødigt komplicerede.
Selvom opfyldelsen af målsætningen er vanskelig at verificere
objektivt, medtages den, da den vurderes at være væsentlig.
Ad 3)
Det udarbejdede design vil blive målt op mod alternative designs i
forhold til hvor meget plads de optager i databasen.
8
3 Metode og relateret arbejde På baggrund af de opstillede krav til den dynamiske datamodel,
designes og implementeres et system, der kan indfri dem.
Designet læner sig op ad de retningslinier, der gives i [EAV2007];
Artiklen undersøger egnetheden af Entity-Attribute-Value (EAV)
database modellering i systemer til håndtering af kliniske data i
biomedicinske systemer. Forfatterne konkluderer, at EAV
modellering er velegnet, når mindst ét af følgende kriterier er
opfyldt:
Antallet af attributter i et system er højt og der er stor
spredning i anvendelsen af dem. Med dette menes, at der
ofte er stor forskel på antallet af potentielle attributter, som
et givet objekt kan anvende, versus det antal, som det rent
faktisk anvender.
De anvendte attributter og objekttyper er volative, dvs. at
nye attributter og objekttyper jævnligt oprettes og andre
nedlægges af brugerne.
Konventionel database modellering er mindre velegnet til spredte
attributter, da disse medfører et stort antal kolonner i tabellerne, af
hvilke kun få er udfyldte på en enkelt række. At de anvendte
attributter er volatile medfører desuden, at database skemaet og
systemets brugergrænseflade ofte skal ændres.
En EAV model siges at være række-orienteret, da hver række i en
EAV tabel repræsenterer en attributværdi for et objekt. Dette skal
ses i modsætning til en konventionel kolonne-orienteret model,
hvor en attribut repræsenteres af en kolonne.
I en EAV model repræsenteres et objekt af en række i en ”Objects”
tabel. Attributværdier repræsenteres af enten:
Flere kolonner (én per datatype) i én ”Attribute Value”
tabel.
Flere ”Attribute Value” tabeller (én per datatype) med én
enkelt værdikolonne.
Et centralt element i en EAV model er håndteringen af metadata om
de enkelte attributter. Disse anvendes til beskrivelse af
valideringsregler, præsentation og lovlige værdimængder.
Behandlingen af metadata er ofte kompleks, hvilket er prisen for at
have enkelhed i den relationelle datamodel.
Artiklen klassificerer data som enten konventionelle-, EAV- eller
hybride data. Hybride data betyder, at de ikke-spredte attributter på
9
en objekttype repræsenteres konventionelt, mens de spredte
repræsenteres som EAV data.
En ulempe ved en EAV model, er at at række klassiske database
koncepter fungerer dårligt eller ikke kan anvendes:
Muligheden for at definere attribut-specifikke database
constraints på kolonner med attributværdier er dårlig, da en
sådan kolonne rummer værdier for flere attributter.
Af samme årsag er anvendelse af triggers heller ikke
velegnet.
Til tider er der behov for at konvertere data i en EAV model til
konventionelle data, dvs. til kolonneorienterede data. Dette
benævnes pivotering af data. Pivotering kan være relevant, hvis data
i EAV modellen skal overføres til et data warehouse med henblik på
rapportering.
En EAV model kan understøtte ad-hoc attribut centriske
forespørgsler, dvs. forespørgsler der kombinerer et antal ”og”
filtreringer på specifikke attributter. Det anbefales at implementere
funktionaliteten ved at udføre flere små SQL forespørgsler (én for
hver attribut filtrering) og gemme resultatet af hver i en temporær
tabel. Rækkerne i de temporære tabeller joines derefter sammen til
det endelige resultat.
***
I forbindelse med rapporten vil der blive udviklet et API, som kan
oprette, læse, opdatere og slette objekter. Som en del af
implementeringen vil der blive foretaget query tuning og analyse af
udførelsesplaner som beskrevet i [Silberschatz2011].
Forskellige alternativer til designet vil blive diskuteret i forhold til de
opstillede kvalitetsattributter. Der vil blive udviklet arkitektoniske
prototyper til måling af, om der er væsentlige forskelle mellem
alternativerne. Prototyperne vil særligt fokusere på performance,
skalérbarhed og pladsforbrug.
10
Den udviklede løsning, samt de arkitektoniske prototyper, vil blive
implementeret baseret på MS SQL Server 2008 R2, C# og Microsoft
.Net Framework 4.0.
Alle målinger vil blive foretaget på et virtuelt HyperV image med
følgende konfiguration:
- Windows Server 2008
- SQL Server 2008 R2
- 4 GB RAM
- CPU med 4 kerner
11
4 Resultater I det følgende gennemgås projektets resultater, opdelt i et antal
gennemførte design iterationer.
4.1 Design af ER model Vi udarbejder ER modellen vist i Figur 2, med henblik på at opfylde
de opstillede krav.
Figur 2 – ER model
Et Object udgør en instans af en ObjectType, som har bindinger til et
antal AttributeTypes. På en binding kan det angives, om et objekt af
typen kræver en værdi i den pågældende attribut. En AttributeType
rummer angivelse af, om et objekt kan have en eller flere værdier
for attributten. En AttributeType er knyttet til en DataType. Et
Object identificeres på et Id og kan have værdier for de
AttributeTypes, som er bundet til objektets type.
12
4.2 Design af databasemodel På baggrund af ER modellen udarbejdes en konkret databasemodel,
som er vist i Figur 3.
Figur 3 – Databasemodel
Appendiks 1 indeholder DDL scriptet til oprettelse af databasens
tabeller, fremmednøgler og indeks.
Hovedparten af transformationen af ER modellen til database
modellen er triviel. Der dannes således tabeller for entiteterne
ObjectType, AttributeType, Object og DataType. 1-N relationen
AttributeDataType bliver til en kolonne i AttributeType tabellen. På
samme vis bliver 1-N relationen InstanceOf til en kolonne i Object
tabellen. Relationen AttributeBinding bliver til en selvstændig tabel,
dels fordi det er en N-N relation og dels fordi den har en attribut
(Requiresvalue). På samme vis (og af samme årsag) bliver
AttributeValue relationen til en selvstændig tabel. De dannede
tabeller har samme navne som entiteterne og relationerne i ER
modellen.
AttributeBinding
ObjectType
AttributeType
RequiresValue
AttributeType
Name
DataType
MultiValued
DataType
DataType
ObjectType
Name
Object
Id
ObjectType
AttributeValue
ObjectId
AttributeType
String
Integer
DateTime
Boolean
Reference
13
Denne nye AttributeValue tabel er af særlig interesse; Vi har behov
for at kunne repræsentere attributværdier af forskellige datatyper.
Der er umiddelbart fire forskellige alternativer, der kan komme på
tale:
1. Der anvendes én kolonne til repræsentation af alle
attributværdier – uanset datatype.
2. Der anvendes flere kolonner til repræsentation af
attributværdier afhængig af datatype.
3. Der anvendes én kolonne af datatype xml til repræsentation
af alle attributværdier – uanset datatype.
4. AttributeValue tabellen splittes op i flere tabeller. Én per
datatype.
Ad 1)
I denne fremgangsmåde anvendes én kolonne til repræsentation af
alle attributværdier – uanset datatype. Denne kolonne kan være af
datatype nvarchar, da alle de datatyper vi ønsker at understøtte, kan
repræsenteres som strenge.
AttributeValue tabellen får følgende udseende:
CREATE TABLE AttributeValue(
ObjectId uniqueidentifier NOT NULL,
AttributeType varchar(50) NOT NULL,
Value nvarchar(500) NOT NULL,
CONSTRAINT PK_AttributeVaue PRIMARY KEY CLUSTERED
(
ObjectId ASC,
AttributeType ASC,
Value ASC
))
Formatet af Value kolonnen afhænger af datatypen, eksempel:
1. ’Some value’ (String)
2. ’23531’ (Integer)
3. ’2012-03-11T13:35:00.0046276Z’ (DateTime)
4. ’True’ (Boolean)
5. ’C983FADF-237B-46AF-8DEA-285D49372B73’ (Reference)
Designet muliggør, at der kan defineres en primærnøgle for tabellen
bestående af ObjectId, AttributeType og Value. Ulempen ved
designet er, at vi ikke bruger databasens type-system og at der ikke
kan oprettes et meningsfuldt indeks på kolonnen, da den rummer
forskellige datatyper.
14
Et alternativ til nvarchar, er at anvende datatypen sql_variant, som
er dynamisk af natur. Datatypen frarådes imidlertid eksplicit i
[EAV2007], da den er beregningsmæssigt ineffiktiv. Vi forfølger
derfor ikke denne mulighed yderligere.
Ad 2)
I denne fremgangsmåde anvendes flere kolonner til repræsentation
af attributværdier afhængig af datatype. AttributeValue tabellen får
følgende udseende:
CREATE TABLE AttributeValue(
ObjectId uniqueidentifier NOT NULL,
AttributeType varchar(50) NOT NULL,
String nvarchar(500) NULL,
Integer int NULL,
DateTime datetime NULL,
Boolean bit NULL,
Reference uniqueidentifier NULL
)
De fem værdi-kolonner er null’able, da kun én af dem kan have en
værdi i en række. Som konsekvens af dette, kan der ikke defineres
en primærnøgle for tabellen, med mindre der oprettes en
surrogatnøgle.
SQL forespørgsler, der selekterer objekter med bestemte værdier i
bestemte attributter, bliver med fremgangsmåden mere komplekse
end i alternativ 1. Dette skyldes, at der, afhængig af en attributs’
datatype, skal ledes i forskellige kolonner.
Fremgangsmåden medfører et bredere tabel-skema end alternativ 1
og dermed et (lidt) højere pladsforbrug.
15
Ad 3)
I denne fremgangsmåde anvendes en kolonne af datatype xml til
repræsentation af alle attributværdier. Vi definerer følgende XML
Schema i databasen:
CREATE XML SCHEMA COLLECTION AttributeValueSchema AS
'<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="value">
<xs:complexType>
<xs:attribute name="b_value" type="xs:boolean" use="optional"/>
<xs:attribute name="dt_value" type="xs:dateTime" use="optional"/>
<xs:attribute name="i_value" type="xs:integer" use="optional"/>
<xs:attribute name="s_value" type="xs:string" use="optional"/>
<xs:attribute name="r_value" type="xs:string" use="optional"/>
</xs:complexType>
</xs:element>
</xs:schema>'
Dernæst får AttributeValue tabellen følgende udseende:
CREATE TABLE AttributeValue(
ObjectId uniqueidentifier NOT NULL,
AttributeType varchar(50) NOT NULL,
Value xml (CONTENT AttributeValueSchema) NOT NULL)
Med dette design er det ikke muligt at definere en primærnøgle for
tabellen, da xml kolonner ikke kan indekseres, med mindre der
indføres en surrogatnøgle.
Konceptet er, at én række i AttributeValue tabellen rummer én
værdi for én attribut. Alternativt kan man imidlertid tillade Value
kolonnen at rumme flere værdier for samme attribut (med et let
modificeret XML Schema). Fordelen ved denne fremgangsmåde er,
at primærnøglen så kan bestå af ObjectId og AttributeType. En
anden fordel er, at der så kommer færre rækker i AttributeValue
tabellen.
16
Ad 4)
Et alternativ til AttributeValue tabellen, er at definere fem
forskellige tabeller, der hver skal rumme alle værdier for attributter
af én bestemt datatype:
CREATE TABLE AttributeValue_String(
ObjectId uniqueidentifier NOT NULL,
AttributeType varchar(50) NOT NULL,
Value nvarchar(300) NOT NULL,
CONSTRAINT PK_AttributeVaue_String PRIMARY KEY CLUSTERED
(ObjectId ASC, AttributeType ASC, Value ASC)
)
CREATE TABLE AttributeValue_Integer(
ObjectId uniqueidentifier NOT NULL,
AttributeType varchar(50) NOT NULL,
Value int NOT NULL,
CONSTRAINT PK_AttributeVaue_Integer PRIMARY KEY CLUSTERED
(ObjectId ASC, AttributeType ASC, Value ASC)
)
CREATE TABLE AttributeValue_DateTime(
ObjectId uniqueidentifier NOT NULL,
AttributeType varchar(50) NOT NULL,
Value DateTime NOT NULL,
CONSTRAINT PK_AttributeVaue_DateTime PRIMARY KEY CLUSTERED
(ObjectId ASC, AttributeType ASC, Value ASC)
)
CREATE TABLE AttributeValue_Boolean(
ObjectId uniqueidentifier NOT NULL,
AttributeType varchar(50) NOT NULL,
Value bit NOT NULL,
CONSTRAINT PK_AttributeVaue_Boolean PRIMARY KEY CLUSTERED
(ObjectId ASC, AttributeType ASC, Value ASC)
)
CREATE TABLE AttributeValue_Reference(
ObjectId uniqueidentifier NOT NULL,
AttributeType varchar(50) NOT NULL,
Value uniqueidentifier NOT NULL,
CONSTRAINT PK_AttributeVaue_Reference PRIMARY KEY CLUSTERED
(ObjectId ASC, AttributeType ASC, Value ASC)
)
Fremgangsmåden bevirker, at vi får fordelt data i flere tabeller,
hvilket er godt for performance. Til gengæld skal der foretages fem
joins i hver forespørgsel i stedet for én, hvilket trækker i den
modsatte retning.
17
En fordel ved fremgangsmåden, sammenlignet med alternativ 2, er
imidlertid at de fem tabeller kan indekseres mere effektivt.
AttributeValue tabellen i alternativ 2 vil typisk have store mængder
null værdier i de fem værdi-kolonner, hvilket bevirker at indeks
bliver større end man kunne ønske sig.
SQL forespørgslerne baseret på denne fremgangsmåde bliver
(ligesom alternativ 2) mere komplekse end i alternativ 1. Dette
skyldes, at der, afhængig af en attributs’ datatype, skal joines med
forskellige tabeller.
***
Vi fravælger umiddelbart alternativ 1, da der ikke kan oprettes et
fornuftigt indeks og fordi metoden forhindrer os i at anvende
databasens type-system. Søgninger og sorteringer bliver som
konsekvens deraf for dyre.
Vi vælger derfor i første omgang at gå videre med alternativ 2 med
planen om senere at udforske alternativ 3.
Alternativ 4 vil vi ikke undersøge nærmere, da vi vurderer, at de
mange joins, som fremgangsmåden medfører, gør den mindre
velegnet.
4.3 Selvbeskrivende datamodel Vi ønsker, at alle data i datamodellen skal kunne opfattes som
objekter. Dette gælder både objekter af skræddersyede objekttyper
(så som ”Employee”), såvel som de indbyggede objekttyper
ObjectType, AttributeType og AttributeBinding. Målet er således, at
datamodellen bliver selvbeskrivende.
En selvbeskrivende datamodel er attraktiv, da den potentielt
forsimpler den funktionalitet, der senere hen skal baseres på
datamodellen. Dette kunne eksempelvis være en sikkerhedsmodel
(ikke omfattet af dette projekt), der muliggør definition af, hvem der
må oprette, læse, opdatere og slette objekter. Såfremt alle data
opfattes som objekter, kan en sådan sikkerhedsmodel anvendes til
skræddersyede objekttyper, såvel som de indbyggede objekttyper
(ObjectType, AttributeType og AttributeBinding).
Et andet eksempel kunne være et formularkoncept (ej heller
omfattet af dette projekt), der beskriver hvorledes et objekt af en
given type skal repræsenteres i et inddateringsskærmbillede. Når
alle data er objekter, kan et sådant formularkoncept anvendes til
skærmbilleder, der bruges til at konfigurere systemets objekttyper
18
og attributter, såvel som til skræddersyede objekttyper.
I begge tilfælde medvirker den selvbeskrivelse datamodel til et pænt
og ensartet design.
Med henblik på at gøre datamodellen selvbeskrivende, besluttes
det, at en række i ObjectType tabellen også skal repræsenteres af
rækker i Object og AttributeValue tabellerne. Det samme gælder for
AttributeType og AttributeBinding tabellerne.
Designet medfører dataredundans, hvilket som udgangspunkt er
uhensigtsmæssigt. En mulig løsning på problemet er at udfase
ObjectType, AttributeType og AttributeBinding tabellerne. Dette vil
imidlertid efterlade os med en database med kun to tabeller (Object
og AttributeValue). Vi vurderer, at et sådant design har en række
væsentlige svagheder, herunder at det er intuitivt svært at forstå og
at vi i så fald ikke kan drage nytte af database indeks, constraints og
fremmednøgler. Vi beslutter derfor at acceptere dataredundansen,
med de ulemper den giver, herunder at data skal vedligeholdes to
steder.
4.4 Bootstrap data Foranlediget af, at datamodellen skal være selvbeskrivende,
udstyres den med en mængde bootstrap data (indbyggede data).
Disse data udgør et antal objekttyper, attributtyper og bindinger,
som skal være til stede i databasen, for at systemet kan fungere.
Data er specielle, da API’erne, der konstrueres senere hen, vil basere
sig på, at data er til stede i databasen. Data kan af denne årsag ikke
oprettes vha. API’erne, hvorfor det sker med et database script, som
er medtaget i bilag 4.
Object types
I datamodellen indbygges en række objekttyper, som er vist i Figur
4. Objekttyperne skal både optræde i ObjectType tabellen samt i
Object+AttributeValue tabellerne.
Objekttype Beskrivelse
ObjectType Et ObjectType objekt beskriver en objekttype.
AttributeType Et AttributeType objekt beskriver en attributtype, der kan bindes til en objekttype.
AttributeBinding Et AttributeBinding objekt beskriver bindingen af en AttributeType til en ObjectType.
Figur 4 – Indbyggede objekttyper
19
Attribute types
I datamodellen indbygges en række attributtyper, som er vist i Figur
5. Attributtyperne skal både optræde i AttributeType tabellen samt i
Object+AttributeValue tabellerne.
Fire attributtyper er særlige:
ObjectId (angiver et unikt id for et objekt)
ObjectType (angivet hvilken type et objekt er af)
CreatedTime (angiver hvornår et objekt blev oprettet)
ChangedTime (angiver hvornår et objekt senest blev rettet)
Attributterne skal være bundet til alle objekttyper i systemet, hvilket
API’et får til opgave at sikre. Attributterne er desuden vedligeholdte
af systemet selv – det er således ikke muligt at ændre værdien af
dem.
Attributtype Datatype Multiple værdier
Beskrivelse
ObjectId Reference False Angiver et unikt Id for et objekt. Alle objekttyper skal have en binding til denne attribut. ObjectId attributten er speciel i og med den er af data type Reference. Den refererer imidlertid per definition altid sig selv. ObjectId tildeles af systemet i forb. med oprettelse af et objekt. ObjectId kan ikke opdateres.
ObjectType String False Angiver objekttypen for et objekt. Alle objekttyper skal have en binding til denne attribut. ObjectType tildeles af systemet i forb. med oprettelse af et objekt. ObjectType kan ikke opdateres.
CreatedTime DateTime False Angiver oprettelsestidspunktet for et objekt. Alle objekttyper skal have en binding til denne attribut. CreatedTime tildeles af systemet i forb. med oprettelse af et objekt. CreatedTime kan ikke opdateres.
ChangedTime DateTime False Angiver seneste ændringstidspunkt for et objekt. Alle objekttyper skal have en binding til denne attribut. ChangedTime tildeles af systemet i forb. med oprettelse af et objekt samt i forbindelse med alle efterfølgende opdateringer af objektet.
Name String False
20
DisplayName String False Visningsnavn for et objekt.
DataType String False Angiver datatypen for en attributtype. Lovlige værdier er { String, Integer, DateTime, Boolean, Reference }
MultiValued Boolean False Angiver om en attributtyper tillader
at der angives mere end én værdi.
BoundAttribute Reference False Angiver den attributtype, som en binding er for.
BoundObjectType Reference False Angiver den objekttype, som en binding er for.
RequiresValue Boolean False Angiver om en binding kræver
angivelse af en værdi.
Figur 5 – Indbyggede attributtyper
Attribute bindings
I datamodellen indbygges følgende attributbindinger, som fremgår
af Figur 6. Attributbindingerne skal både optræde i AttributeBinding
tabellen samt i Object+AttributeValue tabellerne.
Objekttype Attributtype Kræver værdi
Beskrivelse
ObjectType ObjectId True
ObjectType ObjectType True
ObjectType CreatedTime True
ObjectType ChangedTime True
ObjectType DisplayName
ObjectType Name True Unikt navn for en objekttype.
AttributeType ObjectId True
AttributeType ObjectType True
AttributeType CreatedTime True
AttributeType ChangedTime True
AttributeType DisplayName
AttributeType Name True Unikt navn for en attributtype.
AttributeType DataType True Datatype for en attribut. Skal være én af følgende værdier: { String, Integer, DateTime, Boolean, Reference }
AttributeType MultiValued True Angiver om en attribut tillader, at et objekt har flere værdier for den.
AttributeBinding ObjectId True
AttributeBinding ObjectType True
AttributeBinding CreatedTime True
AttributeBinding ChangedTime True
AttributeBinding DisplayName
AttributeBinding BoundObjectType True Objekttype, som binding er for.
AttributeBinding BoundAttribute True Attributtype, som binding er for.
AttributeBinding RequiresValue True Angiver om et dataobjekt af objekttypen skal have en værdi for attributten.
Figur 6 – Indbyggede attributbindinger
21
4.5 API til datavedligehold og forespørgsler Med henblik på at kunne vedligeholde og forespørge på data i den
dynamiske model, udvikles et API, der kan oprette, rette, slette og
forespørge på data ved hjælp af det definerede forespørgselssprog
(se afsnit 2.1).
API’et udvikles i C#, frem for i en stored procedure, da de sproglige
faciliteter er rigere i C#. Vi har bl.a. behov for at kunne definere
klasser og validere regulære udtryk, hvilket ikke umiddelbart er
muligt i TSQL. Ulempen ved valget er, at databasen dermed ikke kan
sikre sin egen integritet, men må stole på den eksterne logik i C#
API’et. Såfremt en stored procedure var anvendt, kunne de
relevante tabeller, via sikkerhedsmodellen i databasen, være
beskyttet mod direkte adgang. Adgang til tabellerne ville i stedet
udelukkende ske via stored procedures. En måde at komme omkring
dette problem, er kun at give en særlig databasebruger adgang til
tabellerne. API’et skal så udføre sine handlinger i databasen via
denne bruger.
De centrale klasser i API’et er vist i Figur 7.
-ObjectType : string
-Expressions
ObjectQuery
-OuterOperator
-Left : object
-InnerOperator
-Right : object
QueryExpression
1
*
+QueryObjects()
+CreateObject()
+UpdateObject()
+DeleteObject()
-DbConnection
-DbTransaction
ObjectController
-Id
-ObjectType : string
-Attributes : AttributeValueDictionary
DataObject
+Add()
+Remove()
+Clear()
-Keys
-Values
-Count
-AllowedAttributes
AttributeValueDictionary
1 1
Figur 7 – Klassediagram visende de centrale klasser i API’et
ObjectController klassen rummer metoder til at oprette, rette,
slette og forespørge på data. DataObject klassen repræsenterer et
enkelt objekt og rummer primært et Attributes dictionary med
attributværdier. Værdien/værdierne for en attribut i Attributes
repræsenteres med én af følgende datatyper: bool, DateTime, int, string, Guid, IEnumerable<DateTime>, IEnumerable<int>,
22
IEnumerable<string> eller IEnumerable<Guid>. En attributs værdier
er således repræsenteret af IEnumerable<> hvis objektet har mere
end én attributværdi.
Ved oprettelse af en ObjectController, skal angives en åben
databaseforbindelse i form af et SqlConnection og SqlTransaction
objekt. Transaktionsobjektet er ikke påkrævet, såfremt der kun skal
læses data.
4.5.1 Oprettelse af objekter
API’ets ObjectController.CreateObject metode opretter et enkelt
objekt i databasen.
public Guid CreateObject(DataObject obj)
Metoden tager et DataObject objekt som argument (bemærk at
klassen hedder DataObject og ikke Object, da sidstnævnte er et
reserveret ord i C#).
Et eksempel på anvendelse af API’et, til oprettelse af et ”Person”
objekt, er vist nedenfor.
using (SqlConnection dbConnection =
new SqlConnection("User ID=sa;Password=<removed>;Initial
Catalog=DynamicDB_MProj;Data Source=VM-TBS3-DB"))
{
dbConnection.Open();
SqlTransaction dbTransaction =
dbConnection.BeginTransaction();
try
{
DataObject person = new DataObject("Person");
person["FirstName"] = "Jens";
person["LastName"] = "Hansen";
person["DisplayName"] = "Jens P. Hansen";
person["BirthDate"] = new DateTime(1982, 3, 25);
ObjectController objectController =
new ObjectController(dbConnection, dbTransaction);
objectController.CreateObject(person);
dbTransaction.Commit();
}
catch
{
dbTransaction.Rollback();
throw;
}
}
Figur 8 – Eksempel på oprettelse af objekt via API
23
CreateObject metoden foretager en række valideringer, for at sikre
at integriteten bevares:
De anvendte attributter på et objekt skal findes i databasen
Der findes bindinger i databasen mellem objekttypen og de
anvendte attributtyper.
Værdierne for de anvendte attributter svarer til
attributtypens datatype.
Der må ikke være angivet flere værdier for en attribut, med
mindre den er markeret som ”MultiValued”.
Der skal være angivet en værdi for en attribut, hvis den på
sin binding har angivet, at der kræves en værdi.
I eksemplet i Figur 8 betyder dette, at angivelsen af
person["FirstName"] = "Jens";
medfører, at følgende valideringer udføres:
FirstName attributten findes
Person objekttypen har en binding til FirstName attributten
Værdien for FirstName er en string.
Der er kun angivet en enkelt værdi for FirstName.
Alle typer af objekter oprettes vha. ObjectController.CreateObject
metoden, inklusiv objekttyper, attributtyper og attributbindinger.
Dette giver en pæn symmetri i API’et og gør det nemt at anvende,
da man kun skal anvende ét API til det hele. Imidlertid kan
modellens selvdefinerende egenskaber potentielt være svært
tilgængelige. Det følgende eksempel (Figur 9) illustrerer begge dele.
Der oprettes først en objekttype ved navn ”Person”, dernæst to
attributtyper ”FirstName” og ”LastName” og efterfølgende to
bindinger mellem objekttypen og de nye attributtyper. Til sidst
oprettes et nyt objekt af den nye type ”Person”. Forbindelses- og
transaktionshåndtering er udeladt fra eksemplet.
24
ObjectController objectController = new ObjectController(…);
// Create new ObjectType object
DataObject personType = new DataObject("ObjectType");
personType["Name"] = "Person";
Guid personTypeId = objectController.CreateObject(personType);
// Create new AttributeType objects
DataObject firstNameAttribute = new
DataObject("AttributeType");
firstNameAttribute["Name"] = "FirstName";
firstNameAttribute["DataType"] = "String";
firstNameAttribute["MultiValued"] = false;
Guid firstNameId =
objectController.CreateObject(firstNameAttribute);
DataObject lastNameAttribute = new DataObject("AttributeType");
lastNameAttribute["Name"] = "LastName";
lastNameAttribute["DataType"] = "String";
lastNameAttribute["MultiValued"] = false;
Guid lastNameId =
objectController.CreateObject(lastNameAttribute);
// Create new AttributeBinding objects
DataObject firstNameBinding = new
DataObject("AttributeBinding");
firstNameBinding["BoundObjectType"] = personTypeId;
firstNameBinding["BoundAttribute"] = firstNameId;
firstNameBinding["RequiresValue"] = false;
objectController.CreateObject(firstNameBinding);
DataObject lastNameBinding = new
DataObject("AttributeBinding");
lastNameBinding["BoundObjectType"] = personTypeId;
lastNameBinding["BoundAttribute"] = lastNameId;
lastNameBinding["RequiresValue"] = false;
objectController.CreateObject(lastNameBinding);
// Create an object of the new type
DataObject person = new DataObject("Person");
person["FirstName"] = "Thomas";
person["LastName"] = "Sigurdsson";
objectController.CreateObject(person);
Figur 9 – Eksempel på oprettelse af objekttype, attributter m.v.
25
ObjectController.CreateObject metoden sørger for, at de
indbyggede objekttyper ”ObjectType”, ”AttributeType” og
”AttributeBinding” håndteres specielt. Dette inkluderer vedligehold
af støttetabellerne i databasen (ObjectType, AttributeType og
AttributeBinding). Hvis der oprettes et objekt af typen ”ObjectType”
sørger metoden således for, at der indsættes en række i ObjectType
tabellen. Ved oprettelse af et nyt ”ObjektType” objekt, oprettes der
automatisk ”AttributeBinding” objekter for de indbyggede
attributtyper, som skal være til stede på alle objekter i systemet:
”ObjectId”, ”ObjectType”, ”CreatedTime” og ”ChangedTime”.
4.5.2 Forespørgsler
ObjectController.QueryObject metoden muliggør udførsel af BQuery
(se afsnit 2.1) forespørgsler på data.
public List<DataObject> QueryObjects(string query)
Eksempel:
ObjectController controller = new ObjectController(cs);
List<DataObject> objs =
controller.QueryObjects("/Person[FirstName=’Thomas’]");
…
Figur 10 – Eksempel på udførsel af BQuery forespørgsel
Metoden validerer først BQuery forespørgslen vha. følgende
regulære udtryk:
^/(?<objtype>[A-Za-z0-9]+)
(\[((?<expression>\s*
(?<outerop>(and|or)?)\s*
(?<leftside>(\'[A-Za-z0-9\s]+\'|[0-9]+|((19|20)\d\d[-](0[1-
9]|1[012])[-](0[1-9]|[12][0-9]|3[01]))|true|false|[A-Za-z0-
9_]+|\'(\d|\w){8}-(\d|\w){4}-(\d|\w){4}-(\d|\w){4}-
(\d|\w){12}\'))\s*
(?<operator>(<|>|=|<=|>=|<>|contains))\s*
(?<rightside>(\'[A-Za-z0-9\s]+\'|[0-9]+|((19|20)\d\d[-](0[1-
9]|1[012])[-](0[1-9]|[12][0-9]|3[01]))|true|false|[A-Za-z0-
9_]+|\'(\d|\w){8}-(\d|\w){4}-(\d|\w){4}-(\d|\w){4}-
(\d|\w){12}\')))+)\])?$
Figur 11 – Regulært udtryk til validering af en BQuery forespørgsel
Dernæst oversættes forespørgslen til et ObjectQuery objekt, der
rummer et antal QueryExpression objekter, som hver repræsenterer
ét af forespørgslens udtryk (se Figur 7).
Efterfølgende danner metoden en SQL forespørgsel på baggrund af
ObjectQuery objektet vha. skabelonen vist i Figur 12.
26
select o.Id, av.AttributeType, av.String, av.Reference,
av.Integer, av.Boolean, av.DateTime
from Object o
join AttributeValue av on o.Id = av.ObjectId
where o.ObjectType = @ObjectType
and av.AttributeType not in ('ObjectId', 'ObjectType')
and ( <expressions sql> )
Figur 12 – SQL skabelon som anvendes af QueryObjects
Der dannes og indsættes SQL udtryk i skabelonen (ved
<expressions_sql>) for de anvendte BQuery udtryk. Betragt
eksempelvis følgende BQuery forespørgsel, der finder et ”Person”
objekt på dets Id:
/Person[ObjectId='53E7D656-F449-469A-857C-5FCFC719BF56’]
Denne resulterer i SQL forespørgslen vist i Figur 13. SQL
forespørgslen udføres via kald af sp_executesql, som er en indbygget
stored procedure i MS SQL Server. sp_executesql muliggør brug af
parametre, hvilket sparer databasen for at kompilere SQL
forespørgslen ved gentagne udførsler af den samme forespørgsel. I
vores tilfælde er dette en fordel, hvis brugeren udfører den samme
BQuery flere gange for forskellige parametre.
exec sp_executesql N'select o.Id, av.AttributeType, av.String,
av.Reference, av.Integer, av.Boolean, av.DateTime
from Object o
join AttributeValue av on o.Id = av.ObjectId
where o.ObjectType = @ObjectType
and av.AttributeType not in (''ObjectId'', ''ObjectType'')
and (exists (select 1 from AttributeValue av2
where o.Id = av2.ObjectId
and av2.AttributeType = @AttribName0
and av2.Reference = @AttribValue0))
order by o.Id',
N'@ObjectType nvarchar(6),@AttribName0 nvarchar(8),@AttribValue0
uniqueidentifier',
@ObjectType=N'Person',@AttribName0=N'ObjectId',@AttribValue0='53
E7D656-F449-469A-857C-5FCFC719BF56'
Figur 13 – BQuery omsat til SQL
Resultatet af den viste SQL forespørgsel er vist i Figur 14.
27
Figur 14 – Resultat af BQuery SQL forespørgsel
Til afprøvning af forespørgsels API’et udarbejdes en Windows
baseret applikation, som er vist i Figur 15. Applikationen muliggør,
at der kan indtastes en BQuery forespørgsel for oven, hvor efter
resultatet vises for neden i skærmbilledet. For hvert objekt, der
indgår i resultatet, listes de tilhørende attributværdier.
Figur 15 – BQuery Tool
De centrale dele af forespørgsels API’et er medtaget i appendiks 2.
28
4.6 Måling og tuning af forespørgsler Med API’et på plads ønsker vi nu at måle og tune udførslen af
BQuery forespørgsler. Med henblik på at kunne dette, oprettes
indledningsvis en række testdata i systemet.
4.6.1 Import forberedelser
Med henblik på masseoprettelse af objekter i den dynamiske
datamodel, anvender vi en (anden) til rådighed værende database
med en stor mængde person-testdata. I databasen findes tabellen
tblPerson_Generated med skemaet vist i Figur 16. Hver række i
tabellen, der indeholder nok data til, at vi kan afprøve vores
arkitektoniske målsætninger, repræsenterer oplysninger om en
person. Tabellen har kolonnen ImportDuration, som vi anvender til
at registrere, hvor lang tid det tog at oprette en person i den
dynamiske datamodel. Indledningsvis er kolonnen null i alle rækker.
Et udsnit af tabellens data er vist i Figur 17.
CREATE TABLE [dbo].[tblPerson_Generated](
[ID] [nvarchar](50) NOT NULL,
[FName] [nvarchar](50) NOT NULL,
[LName] [nvarchar](50) NOT NULL,
[Initials] [nvarchar](50) NOT NULL,
[JobTitle] [nvarchar](50) NOT NULL,
[ManagerID] [nvarchar](50) NULL,
[Department] [nvarchar](50) NOT NULL,
[ValidFromOffset] [int] NOT NULL,
[ValidToOffset] [int] NOT NULL,
[GenID] [int] IDENTITY(1,1) NOT NULL,
[ImportTime] [datetime] NULL,
[ImportDuration] [int] NULL,
[YearEmployed] [int] NULL,
[IsSpecial] [smallint] NULL,
CONSTRAINT [PK_tblPerson_Generated] PRIMARY KEY CLUSTERED
(
[ID] ASC
))
Figur 16 – Skema for tabel med person testdata
29
Figur 17 – Udsnit af person testdata
30
Med henblik på at importere persondataene til den dynamiske
datamodel, oprettes objekttypen ”Person” og tilhørende attributter
vha. koden vist i Figur 18. SchemaController klassen tilbyder en
bekvem måde at oprette disse objekter, frem for at bruge
ObjectController klassen direkte. Dette skyldes, at SchemaController
muliggør oprettelsen af de indbyggede objekter med et enkelt
metodekald, i modsætning til hvad vi gjorde i Figur 9.
SchemaController schemaController = new SchemaController(…);
schemaController.CreateObjectType("Person");
schemaController.CreateAttributeType("PersonID",
AttributeDataType.String, true);
schemaController.CreateAttributeType("FirstName",
AttributeDataType.String, true);
schemaController.CreateAttributeType("LastName",
AttributeDataType.String, true);
schemaController.CreateAttributeType("Initials",
AttributeDataType.String);
schemaController.CreateAttributeType("JobTitle",
AttributeDataType.String);
schemaController.CreateAttributeType("Department",
AttributeDataType.String);
schemaController.CreateAttributeType("YearEmployed",
AttributeDataType.Integer);
schemaController.CreateAttributeType("IsManager",
AttributeDataType.Boolean);
schemaController.CreateAttributeType("Manager",
AttributeDataType.Reference);
schemaController.CreateAttributeBinding("Person", "DisplayName");
schemaController.CreateAttributeBinding("Person", "PersonID");
schemaController.CreateAttributeBinding("Person", "FirstName");
schemaController.CreateAttributeBinding("Person", "LastName");
schemaController.CreateAttributeBinding("Person", "Initials");
schemaController.CreateAttributeBinding("Person", "JobTitle");
schemaController.CreateAttributeBinding("Person", "Department");
schemaController.CreateAttributeBinding("Person", "YearEmployed");
schemaController.CreateAttributeBinding("Person", "IsManager");
schemaController.CreateAttributeBinding("Person", "Manager");
Figur 18 – Oprettelse af objekttype og attributter til testdata
Således forberedt er vi nu klar til at importere data.
4.6.2 Import af data
Vi importerer først 100.000 objekter, før vi påbegynder analysen af
forespørgslerne. Det er bekvemt, at der er en vis mængde data i
databasen, når forespørgslerne analyseres, da man dermed helt
enkelt kan fornemme, når en forespørgsel ikke kører optimalt. Det
31
giver sig simpelthen udtryk ved, at en forespørgsel nemt tager flere
sekunder at udføre, hvilket den helst ikke skal, når vi er færdige med
optimeringerne.
Mængden af data i databasen kan også have en effekt på, hvilken
udførselsplan optimizeren vælger til en SQL forespørgsel. Ved små
datamængder kan optimizeren eksempelvis vælge at udføre en
Table Scan på en tabel, selvom der findes et passende indeks,
simpelthen fordi det er mere effektivt ved de pågældende
datamængder.
Efter importen trækkes en rapport over databasens pladsforbrug (se
Figur 19Error! Reference source not found.). Af denne fremgår det
at data indledningsvis fylder 118 MB.
Figur 19 – Rapport over databasens pladsforbrug
Efter importen opdaterer vi databasens statistik mht.
datadistribution, for at sikre, at query optimizeren har de rigtige tal
at arbejde med:
exec sp_updatestats
I det følgende eksperimenterer vi med de forskellige forespørgsler,
med henblik på at bygge indeks. Indledningsvis er der ingen indeks
for AttributeValue tabellen.
4.6.3 Forespørgsler
På baggrund af referenceforespørgslerne beskrevet i afsnit 2.2
udarbejder vi en serie konkrete forespørgsler, som vi ønsker at
afprøve i databasen med de nu 100.000 importerede objekter.
32
Planen er senere at udføre de samme forespørgsler ved endnu
større datamængder. For at opnå sammenlignelige resultater, er
forespørgslerne (og de anvendte testdata) derfor konstrueret
således, at de returnerer de samme rækker uanset hvor mange
testdata, der er oprettet i systemet. Forespørgslerne, som er vist i
Figur 20, er udstyret med oplysning om, hvor mange objekter de
returnerer. Det angivne antal vil altså være det samme, uanset hvor
mange data der er importeret.
Nr. Forespørgsel og tilsvarende BQuery Antal
returnered
e objekter
1 ”Find en person med et specifikt Id” /Person[ObjectId='08D48E54-1511-4CC3-B4CA-
195B01F1C180']
1
2 ”Find de personer der hedder Song Lan”
/Person[FirstName='Song' and LastName='Lan']
1
3 ”Find alle personer der hedder enten Tian eller Wing til efternavn” /Person[LastName='Tian' or LastName='Wing']
3
4 ”Find alle personer oprettet d. 17 april 2012”
/Person[CreatedTime >= 2012-04-17 and CreatedTime
< 2012-04-18]
10
5 ”Find de personer der er specialoprettede”
/Person[IsSpecial = true]
4
6 ”Find de personer der er ansat før 2009”
/Person[YearEmployed < 2009]
4
7 ”Find alle medarbejdere hvor ’Dept’ indgår i jobtitlen”
/Person[JobTitle contains 'Dept']
4
Figur 20 – BQuery forespørgsler og de antal objekter de returnerer
De syv BQuery forespørgsler oversættes til SQL forespørgsler af C#
API’et. De respektive SQL forespørgsler er vist i Figur 21. Alle
forespørgsler udføres via kald af sp_executesql, som anvendes af
ADO.NET når der udføres en SQL forespørgsel med parametre.
Ad forespørgsel 4)
BQuery sproget understøtter kun hele datoer, som API’et fortolker
som værende ved midnat den pågældende dato. BQuery’en skal
33
derfor skrives på den angivne måde, for at fange de personer, der er
oprettede den pågældende dato.
Nr. SQL forespørgsler dannet på baggrund af BQuery
forespørgsler
Antal
returnerede
rækker
F1 exec sp_executesql N'select o.Id,
av.AttributeType, av.String, av.Reference,
av.Integer, av.Boolean, av.DateTime
from Object o
join AttributeValue av on o.Id =
av.ObjectId where o.ObjectType =
@ObjectType
and av.AttributeType not in (''ObjectId'',
''ObjectType'')
and (exists (select 1 from AttributeValue
av2 where o.Id = av2.ObjectId and
av2.AttributeType = @AttribName0 and
av2.Reference = @AttribValue0))
order by o.Id',
N'@ObjectType varchar(6),@AttribName0
varchar(8),@AttribValue0
uniqueidentifier',@ObjectType='Person',@At
tribName0='ObjectId',@AttribValue0='08D48E
54-1511-4CC3-B4CA-195B01F1C180'
11
F2 exec sp_executesql N'select o.Id,
av.AttributeType, av.String, av.Reference,
av.Integer, av.Boolean, av.DateTime
from Object o
join AttributeValue av on o.Id =
av.ObjectId
where o.ObjectType = @ObjectType
and av.AttributeType not in (''ObjectId'',
''ObjectType'')
and (exists (select 1 from AttributeValue
av2 where o.Id = av2.ObjectId and
av2.AttributeType = @AttribName0 and
av2.String = @AttribValue0)
and exists (select 1 from AttributeValue
av2 where o.Id = av2.ObjectId and
av2.AttributeType = @AttribName1 and
av2.String = @AttribValue1))
order by o.Id',
N'@ObjectType varchar(6),@AttribName0
varchar(9),@AttribValue0
nvarchar(4),@AttribName1
varchar(8),@AttribValue1
nvarchar(3)',@ObjectType='Person',@AttribN
ame0='FirstName',@AttribValue0=N'Song',@At
tribName1='LastName',@AttribValue1=N'Lan'
12
F3 exec sp_executesql N'select o.Id,
av.AttributeType, av.String, av.Reference,
av.Integer, av.Boolean, av.DateTime
from Object o
join AttributeValue av on o.Id =
av.ObjectId
where o.ObjectType = @ObjectType
36
34
and av.AttributeType not in (''ObjectId'',
''ObjectType'')
and (exists (select 1 from AttributeValue
av2 where o.Id = av2.ObjectId and
av2.AttributeType = @AttribName0 and
av2.String = @AttribValue0)
or exists (select 1 from AttributeValue
av2 where o.Id = av2.ObjectId and
av2.AttributeType = @AttribName1 and
av2.String = @AttribValue1))
order by o.Id',
N'@ObjectType varchar(6),@AttribName0
varchar(8),@AttribValue0
nvarchar(4),@AttribName1
varchar(8),@AttribValue1
nvarchar(4)',@ObjectType='Person',@AttribN
ame0='LastName',@AttribValue0=N'Tian',@Att
ribName1='LastName',@AttribValue1=N'Wing'
F4 exec sp_executesql N'select o.Id,
av.AttributeType, av.String, av.Reference,
av.Integer, av.Boolean, av.DateTime
from Object o
join AttributeValue av on o.Id =
av.ObjectId
where o.ObjectType = @ObjectType
and av.AttributeType not in (''ObjectId'',
''ObjectType'')
and (exists (select 1 from AttributeValue
av2 where o.Id = av2.ObjectId and
av2.AttributeType = @AttribName0 and
av2.DateTime >= @AttribValue0)
and exists (select 1 from AttributeValue
av2 where o.Id = av2.ObjectId and
av2.AttributeType = @AttribName1 and
av2.DateTime < @AttribValue1))
order by o.Id',
N'@ObjectType varchar(6),@AttribName0
varchar(11),@AttribValue0
datetime,@AttribName1
varchar(11),@AttribValue1
datetime',@ObjectType='Person',@AttribName
0='CreatedTime',@AttribValue0='2012-04-17
00:00:00',@AttribName1='CreatedTime',@Attr
ibValue1='2012-04-18 00:00:00'
120
F5 exec sp_executesql N'select o.Id,
av.AttributeType, av.String, av.Reference,
av.Integer, av.Boolean, av.DateTime
from Object o
join AttributeValue av on o.Id =
av.ObjectId
where o.ObjectType = @ObjectType
and av.AttributeType not in (''ObjectId'',
''ObjectType'')
and (exists (select 1 from AttributeValue
av2 where o.Id = av2.ObjectId and
av2.AttributeType = @AttribName0 and
av2.Boolean = @AttribValue0))
order by o.Id',
N'@ObjectType varchar(6),@AttribName0
varchar(9),@AttribValue0
bit',@ObjectType='Person',@AttribName0='Is
Special',@AttribValue0=1
48
35
F6 exec sp_executesql N'select o.Id,
av.AttributeType, av.String, av.Reference,
av.Integer, av.Boolean, av.DateTime
from Object o
join AttributeValue av on o.Id =
av.ObjectId
where o.ObjectType = @ObjectType
and av.AttributeType not in (''ObjectId'',
''ObjectType'')
and (exists (select 1 from AttributeValue
av2 where o.Id = av2.ObjectId and
av2.AttributeType = @AttribName0 and
av2.Integer < @AttribValue0))
order by o.Id',
N'@ObjectType varchar(6),@AttribName0
varchar(12),@AttribValue0
int',@ObjectType='Person',@AttribName0='Ye
arEmployed',@AttribValue0=2009
48
F7 exec sp_executesql N'select o.Id,
av.AttributeType, av.String, av.Reference,
av.Integer, av.Boolean, av.DateTime
from Object o
join AttributeValue av on o.Id =
av.ObjectId
where o.ObjectType = @ObjectType and
av.AttributeType not in (''ObjectId'',
''ObjectType'')
and (exists (select 1 from AttributeValue
av2 where o.Id = av2.ObjectId and
av2.AttributeType = @AttribName0 and
av2.String like @AttribValue0))
order by o.Id',N'@ObjectType
varchar(6),@AttribName0
varchar(8),@AttribValue0
nvarchar(6)',@ObjectType='Person',@AttribN
ame0='JobTitle',@AttribValue0=N'%Dept%'
48
Figur 21 – SQL forespørgsler dannet på baggrund af BQuery forespørgsler, samt det antal rækker de returnerer
36
4.6.4 Analyse af første forespørgsel
Vi udfører SQL forespørgsel 1 (se Figur 22), der er dannet på
baggrund af følgende BQuery forespørgsel:
/Person[ObjectId='08D48E54-1511-4CC3-B4CA-195B01F1C180']
SQL forespørgslen er dannet af API’et, som beskrevet i afsnit 4.5.2.
exec sp_executesql N'select o.Id, av.AttributeType, av.String,
av.Reference, av.Integer, av.Boolean, av.DateTime
from Object o
join AttributeValue av on o.Id = av.ObjectId
where o.ObjectType = @ObjectType
and av.AttributeType not in (''ObjectId'', ''ObjectType'')
and (exists (select 1 from AttributeValue av2
where o.Id = av2.ObjectId
and av2.AttributeType = @AttribName0
and av2.Reference = @AttribValue0))
order by o.Id',
N'@ObjectType nvarchar(6),@AttribName0 nvarchar(8),@AttribValue0
uniqueidentifier',@ObjectType=N'Person',@AttribName0=N'ObjectId'
,@AttribValue0='08D48E54-1511-4CC3-B4CA-195B01F1C180'
Figur 22 – Forespørgsel F1
Vi bemærker indledningsvis, at der i SQL forespørgslen sorteres på
Object.Id, hvilket reelt er overflødigt i det konkrete tilfælde, da
forespørgslen maksimalt kan returnere ét objekt (eftersom
forespørgslen leder efter et objekt med et bestemt Id). Sorteringen
er (normalt) til for at de rækker, der indgår i et objekt, ligger
sammen i forespørgselsresultatet, så API’ets indlæsning af objekter
fra resultatet er nem. API’et forventer således, at objekternes
attributter optræder i rækkefølge i de data det modtager. Vi noterer
os at API’et skal forbedres således, at det kan udelade sorteringen,
hvis det kan afgøres, at en forespørgsel maksimalt kan returnere ét
objekt.
SQL forespørgslen ekskluderer attributterne ”ObjectId” og
”ObjectType”. Dette sker i alle forespørgsler, der genereres af
API’et. Attributten ”ObjectId” ekskluderes, da forespørgslen
selekterer værdien fra Object.Id kolonnen (oplysningen er
opbevaret redundant i databasen). ”ObjectType” ekskluderes, da en
BQuery altid er for én angivet objekttype, hvorfor denne oplysning
er kendt og derfor kan udelades af resultatet.
SQL forespørgslen resulterer i udførelsesplanen vist i Figur 23. Vha.
SQL Server Profiler (se Figur 24) kan vi konstatere, at CPU tiden er
1125 millisekunder og at der udføres 28299 læsninger.
37
Udførelsesplanen kan findes i rapportens bilag 1 i filen q1_1.sqlplan.
Figur 23 – Indledende udførelsesplan (forespørgsel F1) – ingen indeks oprettet
Figur 24 – Forespørgsel F1 – SQL Server Profiler
De målte tal er høje og indikerer, at der formentlig mangler et
indeks. Udførelsesplanen viser da også, at der udføres to Table Scan
operationer, der sammenlagt udgør 82% af omkostningen. Den
første Table Scan skyldes et manglende indeks på
AttributeValue.Reference og AttributeValue.AttributeType
kolonnerne (se Figur 25). SQL Server foreslår efter udførslen
følgende indeks, som vi opretter:
CREATE NONCLUSTERED INDEX IX_AttributeValue_Reference
ON AttributeValue (Reference,AttributeType)
38
Figur 25 – Table scan #1 (forespørgsel F1)
En ny generering af rapporten over databasens pladsforbrug
fortæller, at pladsforbruget nu er på 173 MB (mod tidligere 118
MB). Indekset (IX_AttributeValue_Reference) optager således 55
MB.
Den anden Table Scan skyldes, at der ledes (probes) efter rækker i
AttributeValue tabellen på deres ObjectId (se Figur 26). SQL Server
foreslår selv følgende indeks, som vi også opretter:
CREATE NONCLUSTERED INDEX IX_AttributeValue_ObjectId
ON AttributeValue (ObjectId)
INCLUDE(AttributeType,String,Integer,DateTime,Boolean,Reference)
Figur 26 – Table scan #2 (forespørgsel F1)
39
Indeks oprettes generelt med henblik på at opnå hurtigere
forespørgsler. Prisen er imidlertid et større pladsforbrug samt
langsommere indsættelser, opdateringer og sletninger. Jo større
indeks, jo højere er prisen. En væsentlig overvejelse, i forbindelse
med oprettelse af indeks, er derfor, hvad det forventede forhold er
mellem læsninger og opdateringer. I vores tilfælde er det beskrevet i
kravene (se afsnit 2.2), forholdet vil være ca. er 5:1. Vi vurderer
derfor, at det er fornuftigt at oprette førnævnte indeks.
Vi bemærker, at udførelsesplanen indeholder parallelitet, hvilket
betyder at den udføres på flere samtidige tråde. Databasen vælger
kun en parallel plan ved forespørgsler der har en vis (IO)
omkostning. Paralleliteten bevirker i visse tilfælde bedre udnyttelse
af databasens ressourcer, men medfører også øgede omkostninger i
form af trådoprettelse og efterfølgende synkronisering med
hovedtråden. De øgede omkostninger resulterer potentielt i en
længere samlet udførselstid for forespørgslen.
Et nærmere studie af den første Table Scan (se Figur 25) afslører, at
der udføres en CONVERT_IMPLICIT. Dette skyldes, at værdien for
AttributeValue.AttributeType i forespørgslen er angivet som en
nvarchar. I tabellen er den imidlertid defineret som en varchar,
hvilket medfører den implicitte konvertering. Den forkerte datatype
angivelse viser sig at være forårsaget af en mindre fejl i API’et, som
fejlagtigt angav den forkerte datatype ved udførelsen af
forespørgslen.
Vi er nu klar til at udføre forespørgslen igen og nulstiller derfor
databasens cache, så vi er sikre på at sammenligne på et ens
grundlag:
DBCC FREEPROCCACHE
Dernæst udfører vi forespørgsel 1 igen. CPU tiden er nu 31
millisekunder og antallet af læsninger er 32. Altså med andre ord en
markant forbedring. Antallet af læsninger er nu så lavt, at det
umiddelbart ikke kan betale sig at forsøge at forbedre den
yderligere. Udførelsesplanen ser nu ud som i Figur 27. Vi bemærker
at de to Table Scan operationer er erstattet af Index Seek. Ligeledes
bemærker vi, at der ikke længere er parallelitet, hvilket skyldes at
forespørgslens omkostning nu er langt lavere end tidligere.
Udførelsesplanen kan findes i rapportens bilag 1 i filen q1_2.sqlplan.
40
Figur 27 – Udførelsesplan (forespørgsel F1) efter oprettelse to indeks og ændring af datatyper
Indekset (IX_AttributeValue_ObjectId) inkluderer hovedparten af
tabellens kolonner, hvilket betyder at det er temmelig
pladskrævende. Pladsforbruget er således nu på 283 MB (mod
tidligere 173 MB). Indekset optager således 110 MB – altså ca. det
samme som selve dataene og præcis det dobbelte af det første
indeks.
Indekset IX_AttributeValue_ObjectId betegnes som et
såkaldt dækkende indeks, da forespørgslen kan nøjes med at slå op i
indekset og dermed ikke behøver at lave opslag i selve tabellen
[ExecPlans2008], hvilket bevirker øget hastighed.
Tabellen har på nuværende tidspunkt ikke noget klyngeindeks (af
hvilke en tabel kun kan have ét). Et klyngeindeks ligger fysisk
sammen med tabellens data og er derfor per definition altid
dækkende. Som følge deraf, er klyngeindeks normalt lidt
langsommere end almindelige indeks. Dette dog kun såfremt det
almindelige indekset ikke er dækkende og databasen som
konsekvens må slå op i selve tabellen, efter at lave lokaliseret de
relevante rækker i indekset.
Vi erstatter forsøgsvis indekset (IX_AttributeValue_ObjectId)
med et klyngeindeks, for at undersøge pladsforbrug og ydelse:
DROP INDEX IX_AttributeValue_ObjectId ON AttributeValue
CREATE CLUSTERED INDEX IX_AttributeValue_ObjectId_Clustered
ON AttributeValue (ObjectId)
Dernæst udfører vi forespørgsel 1 igen. Udførselstiden er nu 31
millisekunder og antallet af læsninger er 31, altså stort set identisk
som med det tidligere indeks (IX_AttributeValue_ObjectId).
Pladsforbruget er imidlertid nu kun 191 MB, altså 92 MB mindre end
da vi anvendte IX_AttributeValue_ObjectId. Det mindre
41
pladsforbrug skyldes, at et klyngeindeks fysisk ligger sammen med
tabellens data.
Forespørgslens udførelsesplan kan ses i Figur 28. Til forskel fra
tidligere, udføres der nu et Clustered Index Seek (i
IX_AttributeValue_ObjectId_Clustered). Vi bemærker endvidere, at
der ikke længere udføres en RID Lookup i AttributeValue tabellen.
Dette skyldes klyngeindekset, som medfører, at databasen har
direkte adgang til rækkernes data fra indekset.
Udførelsesplanen kan findes i rapportens bilag 1 i filen q1_3.sqlplan.
Figur 28 – Udførelsesplan (forespørgsel F1) efter oprettelse af klyngeindeks
42
4.6.5 Måling og tuning af øvrige forespørgsler
Vi gentager nu fremgangsmåden for de øvrige seks BQuery
forespørgsler. For hver BQuery forespørgsel måler vi performance
på udførsel af de af API’et genererede SQL forespørgsler. Dernæst
oprettes eventuelle indeks, som foreslås af databasen. Efterfølgende
måles performance igen.
Det bemærkes, at fremgangsmåden med at basere os på databasens
forslag til indeks er ikke optimal. Databasen fremkommer med sine
forslag alene på baggrund af den specifikke SQL forespørgsel vi
udfører. Der er således en risiko for, at vores syv reference
forespørgsler ikke er reelt repræsentative for de faktiske
brugsmønstre og at vi derfor overser et eller flere indeks, som burde
blive oprettet. Et andet potentielt problem er, at databasen ikke
forholder sig til hvor ofte forespørgsler udføres. Vi risikerer derfor,
at den foreslår et omkostningsfuldt indeks, som forbedrer en
forespørgsel, der eksempelvis reelt kun udføres en gang om
måneden. I definitionen af vores referenceforespørgsler (se afsnit
2.2) har vi imidlertid ikke forholdt os til hvor ofte de udføres, så vi
kan med rimelighed antage, at de alle udføres lige ofte.
BQuery forespørgslerne er listet i Figur 20. I Figur 21 er en oversigt
over alle BQuery forespørgslernes tilsvarende SQL forespørgsler.
SQL forespørgslerne er udført og målt i rækkefølge. De enkelte SQL forespørgsler drager derfor i visse tilfælde fordel af indeks oprettet på baggrund af analyse af tidligere forespørgsler.
Alle forespørgslernes CPU forbrug og antal læsninger, hhv. før og
efter oprettelse af indeks, er listet i Figur 29.
De oprettede indeks er listet i Figur 30.
Forespørgsel CPU (før) CPU (efter) Læsninger (før) Læsninger (efter)
F1 1125 31 28299 31
F2 594 125 14232 42
F3 79 79 54 54
F4 343 172 14309 121
F5 47 47 92 49
F6 406 47 14603 49
F7 671 32 (*) 6461 60 (*)
Figur 29 – Forespørgslernes CPU forbrug (i millisekunder) og antal læsninger hhv. før og efter oprettelse af indeks
(*) Forbedring skyldes ikke oprettelse af indeks – men ændring af
forespørgsel.
Indeks Foranlediget af Indeks DDL
43
forespørgsel
I1 F1 CREATE NONCLUSTERED INDEX
IX_AttributeValue_Reference
ON AttributeValue (Reference,
AttributeType)
I2 F1 CREATE CLUSTERED INDEX
IX_AttributeValue_ObjectId_Clustered
ON AttributeValue (ObjectId)
I3 F2 CREATE NONCLUSTERED INDEX
IX_AttributeValue_String
ON AttributeValue (String, AttributeType)
INCLUDE (ObjectId)
I4 F4 CREATE NONCLUSTERED INDEX
IX_AttributeValue_AttributeType_DateTime
ON AttributeValue (AttributeType, DateTime)
INCLUDE (ObjectId)
I5 F5 CREATE NONCLUSTERED INDEX
IX_AttributeValue_Boolean_AttributeType
ON AttributeValue (Boolean, AttributeType)
INCLUDE (ObjectId)
I6 F6 CREATE NONCLUSTERED INDEX
IX_AttributeValue_AttributeType_Integer
ON AttributeValue (AttributeType, Integer)
INCLUDE (ObjectId)
Figur 30 – Oversigt over oprettede indeks
Ad forespørgsel F4)
Forespørgsels API’et danner generelt en ”where exists” for hvert
udtryk, der indgår i en BQuery forespørgsel. Forespørgsel F4
indeholder imidlertid to udtryk, som begge er for den samme
attribut. Det er derfor overflødigt at danne to ”where exists” da
følgende nuværende SQL:
…
and (exists (select 1 from AttributeValue av2 where o.Id =
av2.ObjectId and av2.AttributeType = @AttribName0 and
av2.DateTime >= @AttribValue0)
and exists (select 1 from AttributeValue av2 where o.Id =
av2.ObjectId and av2.AttributeType = @AttribName1 and
av2.DateTime < @AttribValue1))
.. i stedet kan udtrykkes mere effektivt med:
… and (exists (select 1 from AttributeValue av2 where o.Id =
av2.ObjectId and av2.AttributeType = @AttribName0 and
av2.DateTime >= @AttribValue0 and av2.DateTime < @AttribValue1)
På grund af dette planlægger vi at forbedre API’et.
Ad forespørgsel F7)
44
Forespørgsel F7 er indledningsvis dyr (CPU 671 og 6461 læsninger),
hvilket ikke er overraskende, da der søges med både højre- og
venstre-wildcard (… and AttriuteValue.String like ‘%Dept%’).
Venstre-wildcard’et burde resultere i et indeks scan, hvilket
forespørgselsplanen imidlertid ikke afslører (se Figur 31). Denne
melder at der foretages en indekssøgning, men detaljerne for den
(se Figur 32) afslører at der er tale om en ”range scan”, hvilket giver
god mening. Imidlertid er det overraskende, at omkostningen
overvejende ligger i sortering af data (43%) og ikke i ”range scan”
operationen (13%).
Forsøgsvis udfører vi forespørgslen uden venstre-wildcard’et (… and
AttriuteValue.String like ‘Dept%’). Dette resulterer i en væsentlig
billigere forespørgsel (CPU 32 og 60 læsninger). Til gengæld ser
udførelsesplanen ud som før (se Figur 33) hvilket kan undre.
På baggrund af den væsentligt forbedrede performance, beslutter
vi, at kravene til systemet skal ændres, således at contains
operatoren i BQuery sproget i praksis fortolkes som ”starter med” i
stedet for ”indeholder”.
Vi overvejer om vi i stedet for contains kan anvende et fuldtekst
indeks. Vi er imidlertid ikke interesserede i den forsinkelse, der er på
vedligeholdelsen af et sådant indeks. Desuden ønsker vi eksakte
sammenligner, der tager hensyn til store og små bogstaver m.v. På
grund af dette fravælges anvendelsen af et fuldtekst indeks.
Figur 31 - Udførelsesplan (forespørgsel F7)
45
Figur 32 - Detaljer for indekssøgning (forespørgsel F7)
Figur 33 - Udførelsesplan (forespørgsel F7) når venstre-wildcard udelades
46
4.6.6 Måling af oprettelse, opdatering og sletning
Vi måler nu oprettelse, opdatering og sletning af objekter. For hver
operation måler vi performance på udførsel af de af API’et
genererede SQL operationer.
I Figur 34 er en oversigt over DML operationer for oprettelse,
opdatering og sletning af objekter. Operationerne er forholdsvis
trivielle.
Nr. DML operation
Oprettelse exec sp_executesql N'insert into Object (Id, ObjectType)
values (@newId, @objectType)',N'@newId
uniqueidentifier,@objectType
nvarchar(6)',@newId='E8FCFA6D-0FD5-4132-8E3B-
C12FDC7E9297',@objectType=N'Person'
Tilsvarende udføres én gang for hver attributværdi:
exec sp_executesql N'insert into AttributeValue (ObjectId,
AttributeType, String, Integer, DateTime, Boolean,
Reference)
values (@ObjectId, @AttributeType, @String, @Integer,
@DateTime, @Boolean, @Reference)'
,N'@ObjectId uniqueidentifier,@AttributeType
nvarchar(8),@String nvarchar(4000),@Integer
nvarchar(4000),@DateTime nvarchar(4000),@Boolean
nvarchar(4000)
,@Reference uniqueidentifier',@ObjectId='E8FCFA6D-0FD5-
4132-8E3B-
C12FDC7E9297',@AttributeType=N'ObjectId',@String=NULL,@Int
eger=NULL,@DateTime=NULL,@Boolean=NULL,@Reference='E8FCFA6
D-0FD5-4132-8E3B-C12FDC7E9297'
Opdatering exec sp_executesql N'select ObjectType from Object where
Id = @ObjectId',N'@ObjectId
uniqueidentifier',@ObjectId='1B7F3503-819A-4D13-9298-
0004C392FF10'
exec sp_executesql N'update AttributeValue set String =
@AttributeValue where ObjectId = @ObjectId and
AttributeType = @AttributeType',N'@AttributeValue
nvarchar(32),@ObjectId uniqueidentifier,@AttributeType
varchar(11)',@AttributeValue=N'Assigned on: 28-05-2012
13:19:04',@ObjectId='1B7F3503-819A-4D13-9298-
0004C392FF10',@AttributeType='DisplayName'
Sletning exec sp_executesql N'select ObjectType from Object where
Id = @ObjectId',N'@ObjectId
uniqueidentifier',@ObjectId='B45A3606-04F3-4789-AC03-
00038AFED44A'
exec sp_executesql N'select System from Object where Id =
@ObjectId',N'@ObjectId
uniqueidentifier',@ObjectId='B45A3606-04F3-4789-AC03-
00038AFED44A'
exec sp_executesql N'delete from Object where Id =
@Id',N'@Id uniqueidentifier',@Id='B45A3606-04F3-4789-AC03-
00038AFED44A'
Figur 34 – DML operationer dannet på baggrund af API’et
47
DML operation
CPU (MS) Læsninger Skrivninger Varighed (MS)
Oprettelse 0 404 71 670
Opdatering 0 35 1 96
Sletning 15 419 21 865
Figur 35 – DML operationernes CPU forbrug (i millisekunder), antal læsninger, skrivninger og varighed (i millisekunder)
Oprettelse af objekter
Oprettelsen af et objekt medfører én indsættelse i Object tabellen
samt én indsættelse i AttributeValue tabellen for hver attributværdi
som objektet har (se Figur 36). For dataene anvendt i vores måling
er der ca. 15 attributværdier for hvert objekt. Oprettelse af et objekt
resulterer således i 16 indsættelser. Dette, sammenholdt med, at
der er oprettet i alt seks indeks for AttributeValue tabellen, er
årsagen til, at den arkitektoniske målsætning (se afsnit 2.2) om
maks. 0,3 sekunder per oprettelse ikke er opfyldt.
Vi har tidligere identificeret, at det er unødigt at indsætte rækker i
AttributeValue tabellen for ObjectId og ObjectType attributterne, da
disse oplysninger også er repræsenteret på anden vis i databasen.
Dette vil bringe antallet af indsættelser i AttributeValue tabellen lidt
ned, men ikke tilstrækkeligt til at nå det arkitektoniske mål. Vi
overvejer derfor en alternativ implementering, som gennemgås
senere i afsnit 0.
Figur 36 – Operationer udført i forbindelse med oprettelse af et objekt
48
Opdatering af objekter
API’ets metode til opdatering af et objekt muliggør at én attribut på
ét objekt kan gives en ny værdi. Udførslen af en opdatering
resulterer i to SQL operationer, som vist i Figur 34.
Den første operation er et opslag på Object tabellens primærnøgle
og resulterer i en Clustered Index Seek. Det er muligt at forbedre
dette en smule med et dedikeret indeks på ObjectId og ObjectType,
men vi vurderer at fordelen er for lille i forhold til den plads- og
tidsmæssige omkostning ved indekset.
Den anden operation er selve opdateringen i AttributeValue
tabellen. Hovedparten af tiden (86%) der medgår til denne er
opdateringen af klyngeindekset. Det er svært at forbedre dette
synderligt, med mindre det fravælges at anvende et klyngeindeks.
Sammenlagt udføres de to operationer på 96 millisekunder, hvilket
er under det opstillede arkitektoniske krav op 0,2 sekunder.
Udførelsesplanen kan findes i rapportens bilag 1 i filen
object_update.sqlplan.
Sletning af objekter
Udførslen af en sletning resulterer i tre SQL operationer, som vist i
Figur 34.
Den to første operationer er opslag på Object tabellens primærnøgle
og resulterer i en Clustered Index Seek. Som nævnt tidligere, er det
muligt at forbedre dette en smule med et dedikeret indeks på
ObjectId og ObjectType, men vi vurderer stadig at fordelen er for lille
i forhold til den plads- og tidsmæssige omkostning ved indekset.
De to opslag kan dog med fordel kombineres i ét, med en lille
besparelse til følge, hvilket vi planlægger at gøre.
For selve sletningens vedkommende (i Object tabellen) medgår
hovedparten af tiden (75%) med en Clustered Index Delete.
I Figur 37 er operationerne vist i SQL Server Profiler, hvor af det
fremgår, at den tredje operation (sletningen) er klart den dyreste.
Udførelsesplanen kan findes i rapportens bilag 1 i filen
object_delete.sqlplan.
49
Figur 37 - Operationer udført i forbindelse med sletning af et objekt
I alt tager sletningen i Object tabellen 838 millisekunder. Sletningen
medfører kaskade sletninger i AttributeValue tabellen, hvilket vi
antager udgør en stor del af omkostningen.
Forsøgsvis udfører vi en sletning igen. Denne gang sletter vi først
rækker i AttributeValue tabellen, før vi sletter i Object tabellen:
…
exec sp_executesql N'delete from AttributeValue where ObjectId
= @Id',N'@Id uniqueidentifier',@Id='6B04823B-F0E8-47A3-869F-
000F9CF9D586'
exec sp_executesql N'delete from Object where Id = @Id',N'@Id
uniqueidentifier',@Id='6B04823B-F0E8-47A3-869F-000F9CF9D586'
Et nyt kig i SQL Server Profiler (Figur 38) viser at selve sletningen fra
Object nu udføres væsentlig hurtigere (29 millisekunder). Til
gengæld tager det 604 millisekunder at udføre sletningen i
AttributeValue tabellen. Sammenlagt viser forsøget, at det er noget
mere effektivt at udføre sletningerne i AttributeValue tabellen
manuelt (varighed på 633 MS mod 865 MS). Dette skyldes
formentlig, at kaskade-sletningen bevirker at AttributeValue
rækkerne slettes én for én, i modsætning til den nye
fremgangsmåde hvor de slettes i én omgang.
50
Figur 38 - Operationer udført i forbindelse med sletning af et objekt. Nu med individuel sletning fra AttributeValue tabellen.
4.6.7 Diskussion efter indledende analyse
Vi bemærker, at hovedparten af forespørgslerne (efter
forbedringerne) har i omegnen af 50 læsninger (se Figur 29). Dette
vurderes som værende tilfredsstillende, datamængderne taget i
betragtning, til at opfylde de arkitektoniske krav opstillet i afsnit 2.2.
Analysen viste, at anvendelsen af venstre-wildcard, i forb. med
BQuery contains operatoren, medfører for dyre forespørgsler. Vi
beslutter os derfor til, at ændre de arkitektoniske krav, således at
contains operatoren i BQuery sproget fremadrettet skal fortolkes
som ”begynder med” i stedet for ”indeholder”. Man vil dermed kun
kunne fremsøge et objekt med contains operatoren, hvis man
kender den første del af en attributværdi. Dette vil medføre en
forringelse af brugernes udfoldelsesmuligheder, men vi betragter
det som et acceptabelt trade-off i forhold til den forbedrede
performance.
Vi har ved inspektion af de genererede SQL forespørgsler
konstateret, at det i visse tilfælde er unødigt at sortere resultatet.
Dette gælder når vi på forhånd kan udlede, at en BQuery maksimalt
kan resultere i ét objekt. Effekten af ændringen er givet vis ikke
specielt stor, da antallet af rækker der sorteres er ret lille. Imidlertid
planlægger vi at indføre det i API’et.
Vi har konstateret, at det store antal indsættelser, der foretages i
AttributeValue tabellen, i forbindelse med oprettelse af et objekt,
gør det svært at indfri de arkitektoniske krav. Vi planlægger derfor
at ændre designet, hvilket gennemgås i afsnit 0.
51
I relation til dette, har vi bemærket, at filtreringen af attributterne
”ObjectId” og ”ObjectType”, som foretages i alle forespørgslerne, er
forholds dyr. Filtreringen foretages med ”… and av.AttributeType
not in ('ObjectId', 'ObjectType')”. Vi vil derfor i samme
ombæring undersøge, om vi helt kan undgå at foretage filtreringen.
Vi har i relation til forespørgsel F4 erfaret, at forespørgsels API’et i
visse sammenhænge med fordel kan kombinere to eller flere
BQuery udtryk i den samme ”where exists”.
I forbindelse med sletning har vi konstateret, at det bedre kan
betale sig, at slette rækkerne i AttributeValue tabellen manuelt,
frem for at lade databasen gøre det som led i en kaskade sletning.
Generelt vil de faktiske udførselstider for SQL forespørgslerne være
bedre end de beskrevne. Dette skyldes at, at alle forsøg indledes
med at nulstille databasens cache. I praksis vil cachen imidlertid tage
effekt med nedsatte udførselstider til følge.
52
4.7 Alternativt XML baseret design af
databasemodel Vi har tidligere konstateret, at det store antal rækker i
AttributeValue tabellen gør det svært at indfri de arkitektoniske
krav vedr. performance.
Som konsekvens af dette forsøger vi os nu med et alternativt design,
der involverer brugen af XML data i databasen. Anvendelsen af XML
data er ikke behandlet i [EAV2007]. Vi finder imidlertid
fremgangsmåden interessant, da den muliggør, at vi kan udlæse alle
data om et objekt fra én enkelt tabel. Samtid bibeholder vi, takket
være SQL Schema, et stærkt type begreb.
Det alternative design indebærer en ny kolonne af datatypen xml i
Object tabellen. Designet er lidt anderledes end det tidligere
introducerede (se afsnit 0), der placerede xml kolonnen i
AttributeValue tabellen. Vi vurderer, at vi, ved at placere kolonnen i
Object tabellen, kan opnå en bedre performance, da vi derved
potentielt helt kan undgå at joine AttributeValue tabellen.
XML format og database ændringer
Vi definerer et XML format til repræsentation af objektdata. Med
dette ser et objekt ud som vist i Figur 39.
<object>
<attributes>
<attribute name="ObjectType">
<value s_value="Person" />
</attribute>
<attribute name="ObjectId">
<value r_value="8B604CA5-2E48-458F-9ED5-4D9085A33251" />
</attribute>
<attribute name="CreatedTime">
<value dt_value="2012-06-10T18:56:12.4430158Z" />
</attribute>
<attribute name="DisplayName">
<value s_value="Ah lam Cai" />
</attribute>
<attribute name="PersonID">
<value s_value="ALCN_2378" />
</attribute>
<attribute name="FirstName">
<value s_value="Ah lam" />
</attribute>
<attribute name="LastName">
<value s_value="Cai" />
53
</attribute>
<attribute name="Initials">
<value s_value="ALCN_2378" />
</attribute>
<attribute name="JobTitle">
<value s_value="HR Clerk" />
</attribute>
<attribute name="Department">
<value s_value="CN_Shanghai_8_Administration" />
</attribute>
<attribute name="YearEmployed">
<value i_value="2010" />
</attribute>
<attribute name="IsManager">
<value b_value="false" />
</attribute>
<attribute name="ChangedTime">
<value dt_value="2012-06-10T18:56:12.4430158Z" />
</attribute>
</attributes>
</object>
Figur 39 – Et objekts data repræsenteret som XML
Vi definerer nu et XML Schema og opretter det i databasen, som vist
i Figur 40.
CREATE XML SCHEMA COLLECTION ObjectDataSchema AS
'<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="value">
<xs:complexType>
<xs:attribute name="b_value" type="xs:boolean" use="optional"/>
<xs:attribute name="dt_value" type="xs:dateTime" use="optional"/>
<xs:attribute name="i_value" type="xs:integer" use="optional"/>
<xs:attribute name="s_value" type="xs:string" use="optional"/>
<xs:attribute name="r_value" type="xs:string" use="optional"/>
</xs:complexType>
</xs:element>
<xs:element name="object">
<xs:complexType>
<xs:sequence>
<xs:element ref="attributes"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="attributes">
<xs:complexType>
54
<xs:sequence>
<xs:element ref="attribute" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="attribute">
<xs:complexType>
<xs:sequence>
<xs:element ref="value"/>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="dataType" type="dataType" use="optional"/>
</xs:complexType>
</xs:element>
<xs:simpleType name="dataType">
<xs:restriction base="xs:string">
<xs:enumeration value="Boolean"/>
<xs:enumeration value="DateTime"/>
<xs:enumeration value="Integer"/>
<xs:enumeration value="String"/>
<xs:enumeration value="Reference"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>'
Figur 40 – Oprettelse af XML Schema i databasen
Vi tilføjer nu en ny kolonne til Object tabellen med datatype xml,
som anvender det nyoprettede XML Schema:
ALTER TABLE dbo.Object ADD Data xml (CONTENT
dbo.ObjectDataSchema) NULL
I Data kolonnen gemmes XML repræsentation af et objekts
attributter. Kolonnen skal således udfyldes når et objekt oprettes og
opdateres når dets attributter opdateres.
Ved hjælp et særligt opdateringsprogram (indeholdt i bilag 2),
opdaterer vi nu samtlige 100.000 objekter i databasen, så de får en
XML repræsentation i Object tabellen. Dernæst ændres den nye Data
kolonne til ikke længere at acceptere NULL værdier.
Hvad med AttriuteValue tabellen?
Den nye Object.Data kolonne bevirker, at vi i princippet ikke længere
behøver AttributeValue tabellen, da dens oplysninger nu er
redundante. Vi vælger dog at bevare den, da vi ikke kan søge lige så
hurtigt i XML data, som vi kan i konventionelle kolonner, hvilket
55
skyldes, at XML data ikke kan indekseres lige så effektivt. Bevarelsen
af tabellen betyder imidlertid, at vi accepterer dataredundans,
hvilket bevirker et større pladsforbrug samt en risiko for
inkonsistens.
”Indekserede” attributter
I praksis er det imidlertid ikke lige interessant for os at søge i alle
attributter. Vi udstyrer derfor AttributeType objekter med en ny
attribut kaldet ”Indexed”, der indikerer, om værdierne for en
attribut indekseres i databasen (dvs. om der oprettes rækker i
AttributeValue tabellen for den). Såfremt det er angivet på en
attribut, at den skal indekseres, opretter vi rækker i AttributeValue
tabellen for den. Vi angiver nu på en række attributter, at de skal
indekseres. Dernæst sletter vi alle rækker i AttributeValue tabellen
for de attributter der ikke er indekserede, med det resultat, at
antallet af rækker i tabellen omtrentligt halveres. Vi forventer på
baggrund af dette, at opnå en bedre ”create” performance
(simpelthen fordi der nu skal udføres færre indsættelser i
AttributeValue tabellen), hvilket de følgende forsøg vil afdække om
er rigtigt.
Vi ændrer nu API’et til at anvende de nye xml data. API’et ændres til
at understøtte både forespørgsler mod indekserede og ikke-
indekserede attributter. For sidstnævntes vedkommende, ledes der i
XML dataene i Object tabellen.
Fordele ved nyt design
Det nye design har andre fordele for os end hurtigere oprettelse af
objekter:
1. I SQL forespørgslerne kan vi nu undlade at joine
AttributeValue tabellen.
2. Ligeledes kan vi i forespørgslerne undgå at sortere på Object.Id
3. Designet muliggør desuden nem og billig pivotering
Ad 1) Ved forespørgsler kan vi undlade at joine AttributeValue
tabellen, hvilket burde bevirke hurtigere performance. Endvidere
muliggør det, at der kan laves en ”select top” på Object tabellen,
hvilket kan være interessant, hvis man ønsker de første x objekter
der opfylder en given forespørgsel. Årsagen til at vi ikke kan lave
”select top” for nuværende er, at de forskellige objekter har et
varierende antal attributværdier. Vi risikerer derfor at ”klippe”
relevante rækker af ved anvendelsen af ”select top”.
Ad 2) For nuværende sorteres der på Object.Id i SQL
forespørgslerne. Dette skyldes, at vi ønsker at gruppere
56
AttributeValue rækkerne på det objekt de hører til, for at gøre
indlæsningen til objekter nemmere. Med anvendelsen af xml data
på Object tabellen behøver vi ikke længere at joine AttributeValue
tabellen. Dermed er et objekt altid repræsenteret af netop én række
og dermed er sortering unødvendig. Besparelsen ved ikke at sortere
på Object.Id er imidlertid begrænset, da tabellen har et
klyngeindeks på kolonnen.
Ad 3)
Det nye design bevirker, at det nu er nemt at pivotere data. Vi
opretter et view i databasen, som pivoterer
”CreatedTime”,”ChangedTime” og ”DisplayName” attributterne:
CREATE VIEW viewObject
AS
SELECT Id, ObjectType,
Data.value('(/object/attributes/attribute[@name=''CreatedTime'']
/value/@dt_value)[1]', 'datetime') AS CreatedTime,
Data.value('(/object/attributes/attribute[@name=''ChangedTime'']
/value/@dt_value)[1]', 'datetime') AS ChangedTime,
Data.value('(/object/attributes/attribute[@name=''DisplayName'']
/value/@s_value)[1]', 'nvarchar(100)') AS DisplayName, Data
FROM Object
Figur 41 – Oprettelse af pivoterings-view
Med det nye view kan vi selektere attributterne som konventionelle
kolonner:
select top 10 Id, ObjectType, CreatedTime, ChangedTime,
DisplayName
from viewObject
Resultatet er:
Figur 42 – Selektion fra viewObject
Ud over anvendelsen af XML data, foretages desuden en række
generelle forbedringer af design og implementering, svarende til
punkterne beskrevet i 4.6.7.
Pladsforbrug
57
På nuværende tidspunkt optager databasen samlet set godt 500
MB. Udviklingen i pladsforbrug kan ses nedenfor.
Hvornår? Pladsforbrug (MB)
Initielt 0
Efter import af 100.000 objekter 118
Efter oprettelse af indeks på AttriuteValue tabellen 435
Efter oprettelse af Object.Data XML kolonne 507
4.8 Måling og tuning af ændrede forespørgsler Da vi nu har foretaget en række større ændringer i design og API,
herunder at vi nu anvender XML data i databasen, foretager vi en
række nye målinger.
4.8.1 Ændrede SQL forespørgsler
Vi udfører de syv BQuery forespørgsler igen. Som følge af
introduktionen af XML data, samt generelle forbedringer af API’et,
er de resulterende SQL forespørgsler anderledes i forhold til
tidligere.
De nye SQL forespørgsler kan ses i Figur 43.
Nr. SQL forespørgsler dannet på baggrund af BQuery forespørgsler Antal returnerede rækker
F1 exec sp_executesql N'select o.Id, o.Data
from Object o
where o.ObjectType = @ObjectType
and (o.Id = @AttribValue0)',
N'@ObjectType varchar(6),@AttribValue0
uniqueidentifier',@ObjectType='Person',@Attrib
Value0='A8EAE616-C7CB-4404-837F-000559632B11'
1
F2 exec sp_executesql N'select o.Id, o.Data
from Object o
where o.ObjectType = @ObjectType
and (exists (select 1 from AttributeValue av2
where o.Id = av2.ObjectId and
av2.AttributeType = @AttribName0 and
av2.String = @AttribValue0)
and exists (select 1 from AttributeValue av2
where o.Id = av2.ObjectId and
av2.AttributeType = @AttribName1 and
av2.String = @AttribValue1))',
N'@ObjectType varchar(6),@AttribName0
varchar(9),@AttribValue0
nvarchar(4),@AttribName1
varchar(8),@AttribValue1
nvarchar(3)',@ObjectType='Person',@AttribName0
='FirstName',@AttribValue0=N'Song',@AttribName
1='LastName',@AttribValue1=N'Lan'
1
F3 exec sp_executesql N'select o.Id, o.Data
from Object o
where o.ObjectType = @ObjectType
3
58
and (exists (select 1 from AttributeValue av2
where o.Id = av2.ObjectId and
av2.AttributeType = @AttribName0 and
av2.String = @AttribValue0)
or exists (select 1 from AttributeValue av2
where o.Id = av2.ObjectId and
av2.AttributeType = @AttribName1 and
av2.String = @AttribValue1))',
N'@ObjectType varchar(6),@AttribName0
varchar(8),@AttribValue0
nvarchar(4),@AttribName1
varchar(8),@AttribValue1
nvarchar(4)',@ObjectType='Person',@AttribName0
='LastName',@AttribValue0=N'Tian',@AttribName1
='LastName',@AttribValue1=N'Wing'
F4 exec sp_executesql N'select o.Id, o.Data
from Object o
where o.ObjectType = @ObjectType
and (exists (select 1 from AttributeValue av2
where o.Id = av2.ObjectId and
av2.AttributeType = @AttribName0
and av2.DateTime >= @AttribValue0 and
av2.DateTime < @AttribValue1))'
,N'@ObjectType varchar(6),@AttribName0
varchar(11),@AttribValue0
datetime,@AttribValue1
datetime',@ObjectType='Person',@AttribName0='C
reatedTime',@AttribValue0='2012-04-17
00:00:00',@AttribValue1='2012-04-18 00:00:00'
10
F5 exec sp_executesql N'select o.Id, o.Data
from Object o
where o.ObjectType = @ObjectType
and (exists (select 1 from AttributeValue av2
where o.Id = av2.ObjectId and
av2.AttributeType = @AttribName0 and
av2.Boolean = @AttribValue0))',
N'@ObjectType varchar(6),@AttribName0
varchar(9),@AttribValue0
bit',@ObjectType='Person',@AttribName0='IsSpec
ial',@AttribValue0=1
4
F6 exec sp_executesql N'select o.Id, o.Data
from Object o
where o.ObjectType = @ObjectType
and (exists (select 1 from AttributeValue av2
where o.Id = av2.ObjectId and
av2.AttributeType = @AttribName0 and
av2.Integer < @AttribValue0))',
N'@ObjectType varchar(6),@AttribName0
varchar(12),@AttribValue0
int',@ObjectType='Person',@AttribName0='YearEm
ployed',@AttribValue0=2009
4
F7 exec sp_executesql N'select o.Id, o.Data
from Object o
where o.ObjectType = @ObjectType
and (exists (select 1 from AttributeValue av2
where o.Id = av2.ObjectId and
av2.AttributeType = @AttribName0 and
av2.String like @AttribValue0))',
N'@ObjectType varchar(6),@AttribName0
varchar(8),@AttribValue0
4
59
nvarchar(5)',@ObjectType='Person',@AttribName0
='JobTitle',@AttribValue0=N'Dept%'
Figur 43 – Nye SQL forespørgsler dannet på baggrund af BQuery forespørgsler
Alle de nye forespørgslers CPU forbrug og antal læsninger er listet i
Figur 44 sammen med de tilsvarende tal for det oprindelige design.
CPU forbruget er generelt forbedret med mere end 50%. Antallet af
læsninger er også forbedret (reduceret) men med mindre
overbevisende tal (ca. 25% forbedring). Forespørgsel F1 og F4 er
forbedret mest markant, men det skyldes i højere grad at selve SQL
forespørgslerne er ændrede, end det skyldes det nye XML koncept.
Forespørgsel CPU (opr. design)
CPU (XML design)
Læsninger (opr. design)
Læsninger (XML design)
F1 31 0 (100%) 31 7 (77%)
F2 125 46 (63%) 42 41 (2%)
F3 79 31 (61%) 54 47 (13%)
F4 172 14 (92%) 121 60 (50%)
F5 47 0 (100%) 49 37 (24%)
F6 47 16 (66%) 49 35 (29%)
F7 32 15 (53%) 60 46 (23%)
Figur 44 – Oprindelige og nye forespørgslers CPU forbrug (i millisekunder) og antal læsninger. Procentsatserne i parentes er forbedringen i forhold i det oprindelige design.
Analyse af indeks
Vi undersøger nu anvendelsen af de tidligere oprettede indeks på
AttributeValue tabellen (se Figur 30). De fleste af indeksene (I3, I4,
I5 og I6) anvendes stadig i udførelsesplanerne. Dette er ikke så
overraskende, da selektionskriterierne i forespørgslerne overordnet
set er de samme.
Indeks I1 anvendes ikke mere, men det skyldes at forespørgsel F1,
som tidligere anvendte det og som leder efter et objekt med et
specifikt ID, ikke længere resulterer i en ”where exists” på
AttributeValue tabellen. I stedet slås der nu direkte op på
primærnøglen Object.Id. Udførelsesplanen kan ses i Figur 45.
Figur 45 - Udførelsesplan (forespørgsel F1) – XML udgave
60
Vi vælger dog at beholde indeks I1 af hensyn til andre BQuery
forespørgsler, der slår op på andre attributter med datatype
”Reference” end ObjectId, selv om en sådan ikke for nuværende er
blandt vores eksempel forespørgsler.
Indeks I2, der er et klyngeindeks (se Figur 30), har vi imidlertid ikke
længere behov for til forespørgslerne. Indekset fandt anvendelse
før, hvor alle kolonnerne i AttributeValue tabellen var medtaget i
SQL forespørgslerne. De er de imidlertid ikke længere pga.
anvendelsen af XML data. Vi har dog stadig brug for et indeks på
ObjectId og AttributeType til opdateringer og sletninger af objekter,
så vi nedlægger klyngeindekset og opretter i stedet et nyt mere
velegnet indeks:
DROP INDEX IX_AttributeValue_ObjectId_Clustered ON
AttributeValue
CREATE NONCLUSTERED INDEX
IX_AttributeValue_ObjectIdAndAttributeType
ON AttributeValue (ObjectId, AttributeType)
De nye forespørgsler medfører ikke umiddelbart et behov for nye
indeks og databasen foreslår ikke selv nogen ved udførsel af
forespørgslerne.
Sammenligning af udførselsplaner
Den primære forskel på udførelsesplanerne (samlet betragtet)
sammenlignet med tidligere er, at der ikke længere udføres en
Clustered Index Seek i indeks I2
(IX_AttributeValue_ObjectId_Clustered). Dette skyldes at vi ikke
længere selekterer kolonner fra AttributeValue tabellen og at
forespørgslerne ikke længere indeholder ”and av.AttributeType not
in ('ObjectId', 'ObjectType')”. Dette kan ses i Figur 46 og Figur 47
som viser hhv. den nye og oprindelige udførelsesplan for
forespørgsel F2.
Figur 46 – Ny udførelsesplan for forespørgsel F2
61
Figur 47 – Oprindelig udførelsesplan for forespørgsel F2. Den indrammede operation indgår ikke i den nye udførelsesplan.
62
4.8.2 Forsøg med ikke-indekserede attributter
Ind til nu har vi kun udført forespørgsler, der anvender indekserede
attributter. Vi vil nu forsøgsvis prøve med en forespørgsel, der
anvender en ikke indekseret attribut, hvilket bevirker at API’et
genererer en SQL forespørgsel der slår op i Object tabellens XML
data.
Oprettelse af XML indeks
Før vi udfører forespørgslen, opretter vi et primært XML indeks på
den nye kolonne:
CREATE PRIMARY XML INDEX XML_IX_Object ON dbo.Object(Data)
Dernæst opretter vi tre sekundære XML indeks af typerne Property,
Value og Path [XmlIndexes2012]:
CREATE XML INDEX XML_IX_Object_Property ON dbo.Object(Data)
USING XML INDEX XML_IX_Object FOR PROPERTY
CREATE XML INDEX XML_IX_Object_Value ON dbo.Object(Data)
USING XML INDEX XML_IX_Object FOR VALUE
CREATE XML INDEX XML_IX_Object_Path ON dbo.Object(Data)
USING XML INDEX XML_IX_Object FOR PATH
Vi overvåger databasens pladsforbrug efter oprettelsen af hvert
indeks og bemærker at de nye indeks er temmelig pladskrævende,
som det fremgår af Figur 48.
Indeks Pladsforbrug (MB)
XML_IX_Object 400
XML_IX_Object_Property 700
XML_IX_Object_Value 300
XML_IX_Object_Path 300
Total 1700
Figur 48 – XML indeksenes pladsforbrug
Forespørgsel på ikke-indekseret attribut
Vi udfører nu følgende BQuery forespørgsel:
/Person[Initials='ALCN_235']
63
Attributten ”Initials”, som der filtreres på, er ikke indekseret. API’et
genererer derfor SQL forespørgslen vist i Figur 49, som anvender
exist operationen på XML kolonnen.
exec sp_executesql N'select o.Id, o.Data
from Object o
where o.ObjectType = @ObjectType
and (o.Data.exist(''/object/attributes/attribute[@name =
sql:variable("@AttribName0")]/value[@s_value =
sql:variable("@AttribValue0")]'') <> 0)'
,N'@ObjectType varchar(6),@AttribName0 varchar(8),@AttribValue0
nvarchar(8)',@ObjectType='Person',@AttribName0='Initials',@Attri
bValue0=N'ALCN_235'
Figur 49 – SQL forespørgsel for BQuery med ikke-indekseret attribut
SQL forespørgslen resulterer i udførelsesplanen vist i Figur 50. Vha.
SQL Server Profiler (se Figur 51) kan vi konstatere, at CPU forbruget
er 218 og at der udføres 185 læsninger. Disse tal er ringere end for
de øvrige forespørgsler, hvilket er i overensstemmelse med
forventningerne.
Udførelsesplanen kan findes i rapportens bilag 1 i filen xml_query on
unindexed attribute.sqlplan.
Figur 50 – XML forespørgsel – SQL Server Profiler
64
Figur 51 - Udførelsesplan for XML forespørgsel
Vi kan af udførelsesplanen se, at Path og Property indeksene
anvendes, mens Value indekset ikke anvendes. Det sidste giver god
mening, da vi angiver en fuld XPath sti til de elementer vi er
interesserede i [XmlIndexes2012].
Alternativt XML format
På baggrund af udførelsesplanen får vi den indskydelse, at vi
muligvis kunne opnå en bedre performance med et alternativt XML
format; I det nuværende XML format repræsenteres alle objekter af
<object> elementer og alle attributter af <attribute> elementer.
Dette bevirker at XPath udtrykket ser sådan ud:
… o.Data.exist(''/object/attributes/attribute[@name =
sql:variable("@AttribName0")]/value[@s_value =
sql:variable("@AttribValue0")]'') <> 0
Hvis vi i stedet anvendte et andet XML format, hvor alle objekttyper
og attributter repræsenteres af individuelle XML elementer, kunne
XPath ydtrykket i stedet udtrykkes noget i stil med:
… o.Data.exist(''/person/attributes/initials/value[@s_value =
sql:variable("@AttribValue0")]'') <> 0
Denne fremgangsmåde vil give hurtigere søgninger, da XML
indekseringen vil være mere effektiv. Dette skyldes, at databasen
allerede på den første node-test i udtrykket fra frasortere alle
objekter, der ikke er af typen ”Person”. Fremgangsmåden vil
imidlertid forhindre os i, at anvende et XML Schema i Object.Data
kolonnen. Dette skyldes, at præmissen for systemet er, at
objekttyperne og attributterne ikke er kendte på forhånd, hvorfor
de ikke kan opremses i et XML Schema. På grund af denne
begrænsning, fravælger vi at forfølge fremgangsmåden yderligere.
65
4.8.3 Nye målinger af oprettelse, opdatering og
sletning
Vi måler nu igen oprettelse, opdatering og sletning af objekter. For
hver operation måler vi performance på udførsel af de SQL
forespørgsler, som API’et genererer.
I Figur 52 er en oversigt over DML operationer for oprettelse,
opdatering og sletning af objekter.
Nr. DML operation
Oprettelse exec sp_executesql N'insert into Object (Id, ObjectType,
Data) values (@newId, @objectType, @data)',N'@newId
uniqueidentifier,@objectType nvarchar(6),@data
nvarchar(737)',@newId='D8AAED8C-B216-466B-9EE2-
4CC532E96AE8',@objectType=N'Person',@data=N'<object><attri
butes><attribute name="ObjectType"><value s_value="Person"
/></attribute><attribute name="DisplayName"><value
s_value="Ah lam Cai" /></attribute><attribute
name="PersonID"><value s_value="ALCN_23568"
/></attribute><attribute name="FirstName"><value
s_value="Ah lam " /></attribute><attribute
name="LastName"><value s_value="Cai"
/></attribute><attribute name="Initials"><value
s_value="ALCN_23568" /></attribute><attribute
name="JobTitle"><value s_value="HR Clerk"
/></attribute><attribute name="Department"><value
s_value="CN_Shanghai_8_Administration"
/></attribute><attribute name="YearEmployed"><value
i_value="2010" /></attribute><attribute
name="IsManager"><value b_value="false"
/></attribute></attributes></object>'
Tilsvarende udføres én gang for hver indekseret attributværdi:
exec sp_executesql N'insert into AttributeValue (ObjectId,
AttributeType, String, Integer, DateTime, Boolean,
Reference) values (@ObjectId, @AttributeType, @String,
@Integer, @DateTime, @Boolean, @Reference)'
,N'@ObjectId uniqueidentifier,@AttributeType
nvarchar(11),@String nvarchar(4000),@Integer
nvarchar(4000),@DateTime datetime,@Boolean
nvarchar(4000),@Reference
nvarchar(4000)',@ObjectId='D8AAED8C-B216-466B-9EE2-
4CC532E96AE8',@AttributeType=N'CreatedTime',@String=NULL,@
Integer=NULL,@DateTime='2012-06-10
17:26:03.083',@Boolean=NULL,@Reference=NULL
Opdatering exec sp_executesql N'select ObjectType from Object where
Id = @ObjectId',N'@ObjectId
uniqueidentifier',@ObjectId='0960BB40-A00E-45E5-A701-
5F5303996DD5'
exec sp_executesql N'select
Data.exist(''/object/attributes/attribute[@name =
sql:variable("@AttributeName")]/value'') from Object where
Id = @ObjectId',N'@AttributeName nvarchar(11),@ObjectId
uniqueidentifier',@AttributeName=N'DisplayName',@ObjectId=
'0960BB40-A00E-45E5-A701-5F5303996DD5'
exec sp_executesql N'update Object set
Data.modify(''replace value of
(/object/attributes/attribute[@name=sql:variable("@Attribu
teType")]/value/@s_value)[1] with
sql:variable("@AttributeValue")'') where Id =
@ObjectId',N'@AttributeType nvarchar(11),@AttributeValue
nvarchar(32),@ObjectId
uniqueidentifier',@AttributeType=N'DisplayName',@Attribute
Value=N'Assigned on: 10-06-2012
18:15:37',@ObjectId='0960BB40-A00E-45E5-A701-5F5303996DD5'
exec sp_executesql N'update AttributeValue set String =
66
@AttributeValue where ObjectId = @ObjectId and
AttributeType = @AttributeType',N'@AttributeValue
nvarchar(32),@ObjectId uniqueidentifier,@AttributeType
varchar(11)',@AttributeValue=N'Assigned on: 10-06-2012
18:15:37',@ObjectId='0960BB40-A00E-45E5-A701-
5F5303996DD5',@AttributeType='DisplayName'
Sletning exec sp_executesql N'select ObjectType, System from Object
where Id = @ObjectId',N'@ObjectId
uniqueidentifier',@ObjectId='6E03AB13-A11A-41C6-BC07-
5085CC503990'
exec sp_executesql N'delete from AttributeValue where
ObjectId = @Id',N'@Id uniqueidentifier',@Id='6E03AB13-
A11A-41C6-BC07-5085CC503990'
exec sp_executesql N'delete from Object where Id =
@Id',N'@Id uniqueidentifier',@Id='6E03AB13-A11A-41C6-BC07-
5085CC503990'
Figur 52 – DML operationer dannet på baggrund af API’et
I Figur 53 er en oversigt over de nye målinger. De oprindelige
målinger er anført i parentes.
DML operation
CPU (MS) Læsninger Skrivninger Varighed (MS)
Oprettelse 0 (0) 297 (404) 55 (71) 70 (670)
Opdatering 79 (0) 294 (35) 3 (1) 154 (96)
Sletning 0 (15) 285 (419) 45 (21) 560 (865)
Figur 53 – DML operationernes CPU forbrug (i millisekunder), antal læsninger, skrivninger og varighed (i millisekunder). De oprindelige målinger er anført i parentes.
Oprettelse af objekter
Til forskel fra tidligere, indsættes der nu XML data i Object tabellen,
når et objekt oprettes. Desuden indsættes der nu kun 8 rækker i
AttributeValue tabellen, mod tidligere 15. Dette skyldes dels, at der
nu aldrig indsættes rækker for ”ObjectType” og ”ObjectId”
attributterne. Derudover skyldes det introduktionen af
attributindeks-konceptet; Vi har angivet, at en række attributter ikke
skal være indekserede, da de ikke anvendes i nogen af vores BQuery
referenceforespørgsler.
Tidligere voldte oprettelses-operationen størst kvaler, da
udførelsestiden (670 MS) oversteg den arkitektoniske målsætning
på 500 MS. Efter at have udført de beskrevne ændringer, måler vi på
ny (se Figur 54Error! Reference source not found.) og udførselsiden
er nu på 70 MS, hvilket er både tilfredsstillende og overraskende
lavt.
67
Figur 54 - Operationer udført i forbindelse med oprettelse af et objekt
Opdatering af objekter
Ved ændring af et objekt, skal de nye XML data i Object tabellen
opdateres ligesom AttributeValue tabellen (som tidligere) skal
opdateres. Sidstnævnte skal dog nu kun opdateres såfremt der er
tale om en indekseret attribut.
Opdateringen af XML dataene sker med to SQL kald (se Figur 52);
først undersøges om der allerede er en værdi for attributten i XML
dataene. I så fald opdateres den og hvis ikke indsættes der en værdi.
(Indsættelsen er ikke vist i Figur 52 da der i vores tilfælde var en
værdi for attributten på det pågældende objekt).
De to XML relaterede SQL kald er en ekstra omkostning i forhold til
tidligere. Samlet set er opdateringsoperationen da også blevet
dyrere (nu 154 MS mod tidligere 96 MS). Vi ligger dog stadig under
det arkitektoniske krav på 200 MS, så vi vurderer det som værende
tilfredsstillende.
Sletning af objekter
Sletning af et objekt er ændret på en række områder i forhold til
tidligere:
- Opslag af ObjectType og System er nu samlet i ét SQL kald
(tidligere var der to)
- AttributeValue rækker slettes nu ”manuelt” før der slettes i
Object tabellen. Tidligere blev AttributeValue rækkerne
slettet vha. en kaskadesletning.
68
- Der er nu generelt færre rækker i AttributeValue tabellen og
derfor færre at slette.
Alle de nævnte ting medvirker til en bedre performance ved
sletning. De ændrede operationer kan ses i Figur 52.
Vi måler nu udførelsestiden for en sletning til 560 MS (mod tidligere
865 MS). Vi er stadig over det arkitektoniske mål på 500 MS, men vi
er så tæt på, at vi vurderer det til at være godt nok.
4.8.4 Diskussion efter opfølgende analyse
Vi har indført en ny kolonne på Object tabellen af datatypen xml,
som rummer en repræsentation af et objekternes attributværdier.
Ændringen bevirker, at vi nu kan udføre ca. 25% billigere
forespørgsler, primært fordi vi ikke længere joiner med
AttributeValue tabellen. Til gengæld optages der mere plads i
databasen, da attributværdierne nu opbevares redundant. Dette gør
imidlertid ikke så meget, da stigningen i pladsforbruget ikke er
voldsom. Desuden er diskplads billig i vore dage og vi vurderer
derfor, at performance vægter højere end pladsforbrug.
Omkostningen ved oprettelse og sletning af objekter er samlet set
faldet, mens omkostningen ved opdateringer samlet set er steget.
Vi har forsøgsvis udført en BQuery forespørgsel, der filtrerer på en
ikke-indekseret attribut. Dette bevirker en SQL forespørgsel, der slår
op i Object tabellens xml kolonne. På baggrund af forsøget kan vi
konstatere, at antallet af læsninger for forespørgslen er mere end (i
gennemsnit) 4 gange så højt som referenceforespørgslerne (se Figur
44). Den udførte BQuery forespørgsel har imidlertid et lavere
kompleksistetsniveau end hovedparten af vores
referenceforespørgsler.
Incitamentet til at slå op i xml kolonnen var indledningsvis, at
reducere antallet af indsættelser i AttributeValue tabellen. De
oprettede XML indeks optager imidlertid samlet ca. 1700 MB (se
Figur 48), hvilket skal ses i forhold til, at databasen før oprettelse af
indeks optog samlet set ca. 500 MB. Pladsforbruget synes meget
voldsomt og vi vurderer derfor, at det ikke står mål med gevinsten
ved de færre indsættelser. Vi vælger derfor at fjerne de oprettede
XML indeks igen og gå bort fra konceptet med ikke-indekserede
attributter og deraf følgende SQL forespørgsler med XML søgninger.
69
4.9 Performancemåling Efter at API og database nu er blevet ændret, ønsker vi at måle den
dynamiske datamodels performance ved forskellige datamængder.
Specifikt ønsker vi at måle performance på de syv
referenceforespørgsler ved følgende datamængder:
- 100.000 objekter (nuværende datamængde)
- 500.000 objekter
- 1.000.000 objekter
- 1.500.000 objekter
Vi indlæser de respektive datamængder efter samme
fremgangsmåde som de første 100.000 objekter (beskrevet i afsnit
4.6.2). Dernæst udfører vi de forskellige forespørgsler og måler
ydelsen vha. SQL Server Profiler.
Antallet af læsninger, der foretages af de syv forespørgsler, ved
forskellige datamængder, kan ses i Figur 55.
Udførselstiden for de samme syv forespørgsler, ved forskellige
datamængder, kan ses i Figur 56.
Udviklingen i antal læsninger er fornuftig. Der er maksimalt tale om
godt en fordobling i springet fra 100.000 til 1.500.000 objekter,
hvilket indikerer en lineær skalering.
Udviklingen i udførelsestiden forekommer lidt pudsig, da flere af
målingerne falder(!) mod de 1.500.000 objekter. Dette kan
eventuelt skyldes, at database serveren har været belastet af andre
ting ved de tidligere målinger.
Alt i alt konkluderer vi, at systemet skalerer tilfredsstillende.
Figur 55 – Antal læsninger for de syv reference forespørgsler ved forskellige datamængder
0
20
40
60
80
100
120
140
160
180
100.000 500.000 1.000.000 1.500.000
An
tal l
æsn
inge
r
Antal objekter i databasen
F1
F2
F3
F4
F5
F6
F7
70
Figur 56 – Udførelsestid (millisekunder) for de syv reference forespørgsler ved forskellige datamængder
0
100
200
300
400
500
600
100.000 500.000 1.000.000 1.500.000
Ud
føre
lse
stid
(M
S)
Antal objekter i databasen
F1
F2
F3
F4
F5
F6
F7
71
5 Konklussion Rapporten beskæftiger sig med design og evaluering af en dynamisk
datamodel, med det formål at opfylde en række opstillede krav til
funktion og arkitektonisk kvalitet.
De opstillede funktionelle krav er som følger:
1. Den dynamiske datamodel skal understøtte
oprettelsen af brugerdefinerede objekttyper og
attributtyper
2. En attributtype skal kunne tildeles én af følgende
datatyper: String, Integer, DateTime, Boolean
eller Reference
3. En attributtype skal endvidere enten kunne
tildeles en enkelt værdi eller multiple værdier
4. Der skal være et API, som understøtter
oprettelse, læsning, opdatering og sletning af
objekter
5. API’et skal desuden understøtte et
forespørgselssprog
6. Objekter skal kunne have referencer til hinanden
og dermed indgå i en objektgraf
7. Objekter skal opbevares i en relationel database
De opstillede arkitektoniske kvaliteter er:
1. Performance og skalérbarhed
2. Designets kompleksitet
3. Pladsforbrug
Der er blevet udarbejdet et design og en arkitektonisk prototype,
som er anvendt til at afprøve opfyldelsen af de funktionelle såvel
som arkitektoniske krav.
Designet læner sig op ad de retningslinier, der gives i [EAV2007]. Der
er desuden afprøvet en design variant, som baserer sig på XML data
i databasen. Erfaringerne med sidstnævnte har været positive, da
det har medvirket til en forbedret performance samt enkelhed i de
udarbejdede SQL forespørgsler.
Den udarbejdede prototype vurderes at opfylde de opstillede
funktionelle krav, samt de arkitektoniske krav til performance.
Det arkitektoniske krav om, at designet ikke har en unødig høj
kompleksitet, betragtes ligeledes som opfyldt. Vi lægger til grund, at
det udarbejdede databasedesign har få tabeller. Ligeledes at de SQL
72
forespørgsler, der genereres af det udarbejdede API, er forholdsvis
simple.
Ved hjælp af prototypen, har vi kunnet konstatere, at en database
med 100.000 objekter, der hver har 15 attributværdier tilknyttet,
optager ca. 500 MB diskplads. Vi betragter dette som acceptabelt,
særligt når man tager nutidens priser på harddiske I betragtning.
Den overordnede konklussion er således, at det har været muligt at
opfylde de opstillede krav.
73
6 Referencer [EAV2007] Dinu, Nadkarni, Guidelines for the Effective Use of Entity-
Attribute-Value Modeling for Biomedical Databases, 2007
[OIS2012] Omada, Omada Identity Suite, 2012
http://www.omada.net/Solutions-145.aspx
[Silberschatz2011] Silberschatz et al., Database System Concepts,
2011
[ExecPlans2008] Fritchey, SQL Server Execution Plans, 2008. ISBN:
978-1-906434-04-5
[XmlIndexes2012] Microsoft, Secondary XML Indexes, 2012
http://msdn.microsoft.com/en-us/library/bb522562(v=sql.105).aspx
74
7 Bilag
Bilag 1 – Forespørgselsplaner
De i rapporten analyserede forespørgselsplaner er medtaget i bilag
1. Filerne kan åbnes i MS SQL Server 2008 Management Studio eller
læses med en XML editor.
Bilag 2 – Kildekode til API og testprogram
Det beskrevne API samt testprogram til udførsel af BQuery
forespørgsler m.v. er medtaget i bilag 2 i form af et Visual Studio
2010 projekt.
Bilag 3 – Database scripts
Bilaget indeholder SQL DDL script til oprettelse af databasens
tabeller, indexes og stored procedures.
Bilag 4 – Bootstrap data
Bilaget indeholder SQL scriptet til oprettelse af de såkaldte
bootstrap data. Disse data udgøres af et antal objekttyper,
attributtyper og bindinger, som altid skal være til stede i databasen.
75
8 Appendiks Afsnittet rummer appendiks til rapporten.
Appendiks 1 – Database create script
Appendikset indeholder SQL DDL script til oprettelse af databasens
tabeller, fremmednøgler og indeks.
Appendiks 2 – QueryController
Appendikset indeholder kildekode til QueryController klassen.
8.1 Appendiks 1 – Database create script Appendikset indeholder SQL DDL script til oprettelse af databasens
tabeller, fremmednøgler og indeks. Scriptet svarer til den
indledende udgave af databasen, før XML data tages i anvendelse.
Scripts til oprettelse af den endelige udgave af databasen kan findes
i bilag 3.
Oprettelse af tabeller
CREATE TABLE [dbo].[DataType](
[DataType] [varchar](10) NOT NULL,
CONSTRAINT [PK_DataType] PRIMARY KEY CLUSTERED
(
[DataType] ASC
))
CREATE TABLE [dbo].[ObjectType](
[Name] [varchar](50) NOT NULL,
CONSTRAINT [PK_ObjectType] PRIMARY KEY CLUSTERED
(
[Name] ASC
))
CREATE TABLE [dbo].[Object](
[Id] [uniqueidentifier] NOT NULL,
[ObjectType] [varchar](50) NOT NULL,
CONSTRAINT [PK_Object] PRIMARY KEY CLUSTERED
(
[Id] ASC
))
CREATE TABLE [dbo].[AttributeType](
[Name] [varchar](50) NOT NULL,
[DataType] [varchar](10) NOT NULL,
[MultiValued] [bit] NOT NULL,
[Indexed] [bit] NOT NULL,
76
[AlwaysBind] [bit] NOT NULL,
CONSTRAINT [PK_AttributeType] PRIMARY KEY CLUSTERED
(
[Name] ASC
))
CREATE TABLE [dbo].[AttributeValue](
[ObjectId] [uniqueidentifier] NOT NULL,
[AttributeType] [varchar](50) NOT NULL,
[String] [nvarchar](400) NULL,
[Integer] [int] NULL,
[DateTime] [datetime] NULL,
[Boolean] [bit] NULL,
[Reference] [uniqueidentifier] NULL
)
CREATE TABLE [dbo].[AttributeBinding](
[ObjectType] [varchar](50) NOT NULL,
[AttributeType] [varchar](50) NOT NULL,
[RequiresValue] [bit] NOT NULL,
CONSTRAINT [PK_AttributeBinding] PRIMARY KEY CLUSTERED
(
[ObjectType] ASC,
[AttributeType] ASC
))
Oprettelse af fremmednøgler
ALTER TABLE [dbo].[AttributeBinding] WITH CHECK ADD CONSTRAINT
[FK_AttributeBinding_AttributeType]
FOREIGN KEY([AttributeType])
REFERENCES [dbo].[AttributeType] ([Name])
ON UPDATE CASCADE
ON DELETE CASCADE
ALTER TABLE [dbo].[AttributeBinding] WITH CHECK ADD CONSTRAINT
[FK_AttributeBinding_ObjectType]
FOREIGN KEY([ObjectType])
REFERENCES [dbo].[ObjectType] ([Name])
ON UPDATE CASCADE
ON DELETE CASCADE
ALTER TABLE [dbo].[AttributeType] WITH CHECK ADD CONSTRAINT
[FK_AttributeType_DataType]
FOREIGN KEY([DataType])
REFERENCES [dbo].[DataType] ([DataType])
ON UPDATE CASCADE
77
ALTER TABLE [dbo].[AttributeValue] WITH CHECK ADD CONSTRAINT
[FK_AttributeValue_AttributeType]
FOREIGN KEY([AttributeType])
REFERENCES [dbo].[AttributeType] ([Name])
ON UPDATE CASCADE
ALTER TABLE [dbo].[AttributeValue] WITH CHECK ADD CONSTRAINT
[FK_AttributeValue_Object]
FOREIGN KEY([ObjectId])
REFERENCES [dbo].[Object] ([Id])
ON UPDATE CASCADE
ON DELETE CASCADE
ALTER TABLE [dbo].[AttributeValue] WITH CHECK ADD CONSTRAINT
[FK_AttributeValue_Reference]
FOREIGN KEY([Reference])
REFERENCES [dbo].[Object] ([Id])
ALTER TABLE [dbo].[Object] WITH CHECK ADD CONSTRAINT
[FK_Object_ObjectType]
FOREIGN KEY([ObjectType])
REFERENCES [dbo].[ObjectType] ([Name])
Oprettelse af indeks
CREATE NONCLUSTERED INDEX [IX_AttributeValue_Boolean2] ON
[dbo].[AttributeValue]
(
[Boolean] ASC,
[AttributeType] ASC
)
INCLUDE ( [ObjectId])
CREATE NONCLUSTERED INDEX [IX_AttributeValue_String] ON
[dbo].[AttributeValue]
( [String] ASC)
CREATE NONCLUSTERED INDEX [IX_AttributeValue_Integer] ON
[dbo].[AttributeValue]
( [Integer] ASC)
CREATE NONCLUSTERED INDEX [IX_AttributeValue_DateTime] ON
[dbo].[AttributeValue]
( [DateTime] ASC )
CREATE NONCLUSTERED INDEX [IX_AttributeValue_Reference] ON
[dbo].[AttributeValue]
( [Reference] ASC )
78
8.2 Appendiks 2 – Forespørgsels API Appendikset indeholder kildekoden til en central del af
forespørgsels API’et. QueryController klassen muliggør udførsel af
BQuery forespørgsler, som den omdanner til SQL forespørgsler og
udfører mod databasen. Den viste kildekode svarer til den endelige
version af API’et (hvor der anvendes XML data). Den fulde kildekode
til API’et er medtaget i bilag 2.
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Xml;
using DynamicDB.Model;
using DynamicDB.Model.Exceptions;
namespace DynamicDB.AppLogic
{
/// <summary>
/// QueryController allows for execution of BQuery queries against the dynamic data store.
/// The primary methods of QueryController are also (for convenience) surfaced in ObjectController.
/// </summary>
public class QueryController : ControllerBase
{
public QueryController(string connectionString)
: base(connectionString)
{
}
public QueryController(
SqlConnection dbConnection,
SqlTransaction dbTransaction,
int dbCommandTimeout = 0)
: base(dbConnection, dbTransaction, dbCommandTimeout)
{
}
public QueryController(ControllerBase assignFrom)
: base(assignFrom)
{
}
/// <summary>
/// Returns the (Attribute) DataType of either the left- or the right-side value of an expression.
/// </summary>
/// <param name="expressionValue"></param>
/// <param name="objectType"></param>
/// <returns></returns>
private AttributeDataType GetExpressionSideDataType(
object expressionValue,
ObjectType objectType)
{
if (QueryExpression.ExpressionValueIsAttributeName(expressionValue))
{
AttributeType attributeType =
objectType.GetBoundAttribute((string)expressionValue, true);
return attributeType.DataType;
}
else
{
return QueryExpression.GetExpressionValueDataType(expressionValue);
}
}
/// <summary>
/// Converts an expression inner operator to a sql operator.
/// </summary>
/// <param name="op"></param>
/// <returns></returns>
private string InnerOperatorToSqlOperator(InnerOperator op)
{
79
switch (op)
{
case InnerOperator.Equals:
return "=";
case InnerOperator.NotEquals:
return "<>";
case InnerOperator.LessThan:
return "<";
case InnerOperator.GreaterThan:
return ">";
case InnerOperator.LessThanEquals:
return "<=";
case InnerOperator.GreaterThanEquals:
return ">=";
case InnerOperator.Contains:
return "like";
default:
throw new ArgumentException("Unknown operator: " + op, "op");
}
}
private SqlCommand BuildCommand(ObjectQuery objectQuery, ObjectType objectType)
{
if (objectQuery == null)
throw new ArgumentNullException("objectQuery");
if (objectType == null)
throw new ArgumentNullException("objectType");
this.ValidateOuterOperators(objectQuery);
string top = objectQuery.ResultSize > 0 ? " top " + objectQuery.ResultSize : String.Empty;
string cmdText = "select" + top + " o.Id, o.Data"
+ " from Object o"
+ " where o.ObjectType = @ObjectType";
Dictionary<string, object> queryParameters = new Dictionary<string, object>();
queryParameters.Add("@ObjectType", objectQuery.ObjectType);
string expressionSql = String.Empty;
List<QueryExpression> treatedExpressions = new List<QueryExpression>();
for (int idx = 0; idx < objectQuery.Expressions.Count; idx++)
{
QueryExpression expr = objectQuery.Expressions[idx];
if (!treatedExpressions.Contains(expr))
{
if (idx > 0)
{
expressionSql += (expr.OuterOperator == OuterOperator.And ? " and " : " or ");
}
// Check if we can merge the expression with the following expression(s) with the aim of
producing a cheaper sql query
List<QueryExpression> collectiveEvalExpressions =
this.GetCollectiveEvaluationExpressions(expr, idx, objectQuery, objectType);
string whereClause = this.GetExpressionWhereClause(expr, collectiveEvalExpressions,
objectQuery, objectType, queryParameters);
expressionSql += whereClause;
treatedExpressions.AddRange(collectiveEvalExpressions);
}
}
if (!String.IsNullOrEmpty(expressionSql))
cmdText += " and (" + expressionSql + ")";
// Note: we utilize SQL servers handling of and/or precedence.
SqlCommand cmd = this.CreateCommand(cmdText);
foreach (string param in queryParameters.Keys)
{
if (param == "@ObjectType" || param.StartsWith("@AttribName"))
{
// Important: we add ObjectType and AttribName like this (and not using AddWithValue)
// since AddWithValue will treat them as NVarChar,
80
// which causes a CONVERT_IMPLICIT in the query plan.
cmd.Parameters.Add(param, SqlDbType.VarChar);
cmd.Parameters[param].Value = queryParameters[param];
}
else
{
cmd.Parameters.AddWithValue(param, queryParameters[param]);
}
}
return cmd;
}
/// <summary>
/// Get expressions to be handled collectively in the evaluation.
/// </summary>
/// <param name="expr">
/// The expression that we want to find similar/mergable expressions for.
/// </param>
/// <param name="idx">
/// The index of the expression we want to find similar/mergable expressions for.
/// </param>
/// <param name="objectQuery"></param>
/// <param name="objectType"></param>
/// <returns>
/// At least the expression itself is returned.
/// </returns>
private List<QueryExpression> GetCollectiveEvaluationExpressions(QueryExpression expr, int idx,
ObjectQuery objectQuery, ObjectType objectType)
{
List<QueryExpression> result = new List<QueryExpression>();
// We always add the expression itself
result.Add(expr);
// Note: expressions for "ObjectId" are skipped because they are is handled specially in
GetExpressionWhereClause
if (expr.InvolvesSingleAttribute && expr.SingleInvolvedAttribute != "ObjectId")
{
AttributeType attributeType = objectType.GetBoundAttribute(expr.SingleInvolvedAttribute, true);
if (attributeType.Indexed)
{
for (int remainIdx = idx + 1; remainIdx < objectQuery.Expressions.Count; remainIdx++)
{
QueryExpression nextExpr = objectQuery.Expressions[remainIdx];
if (expr.SingleInvolvedAttribute == nextExpr.SingleInvolvedAttribute &&
nextExpr.OuterOperator == OuterOperator.And)
{
result.Add(nextExpr);
}
else
{
break;
}
}
}
}
return result;
}
private string GetExpressionWhereClause(
QueryExpression expr,
List<QueryExpression> collectiveEvalExpressions,
ObjectQuery objectQuery,
ObjectType objectType,
Dictionary<string, object> queryParameters)
{
if (!collectiveEvalExpressions.Contains(expr))
throw new ArgumentException("Must at least contain the expression itself",
"collectiveEvalExpressions");
AttributeDataType leftDataType = this.GetExpressionSideDataType(expr.Left, objectType);
AttributeDataType rightDataType = this.GetExpressionSideDataType(expr.Right, objectType);
if (leftDataType != rightDataType)
throw new ArgumentException(
"Left- and right-side must have same data type: '" + expr.ToString() + "'",
81
"objectQuery");
if (!QueryExpression.OperatorValidForDataType(expr.InnerOperator, leftDataType))
throw new ArgumentException(
String.Format(
"Expression's inner operator ({0}) is not valid for the datatype: {1}",
expr.InnerOperator,
leftDataType),
"objectQuery");
if (expr.LeftIsAttribute && expr.RightIsAttribute)
{
// Validate against a query such as: /Person[ObjectId = CreatedBy]
// The reason is that ObjectId is not stored in the AttributeValue table.
if ((string)expr.Left == "ObjectId" || (string)expr.Right == "ObjectId")
throw new ArgumentException(
String.Format(
"It is currently not allowed to use an expression with attributes on both sides where one
of them is ObjectId: {0}",
objectQuery.ToString()),
"objectQuery");
AttributeType leftAttributeType = objectType.GetBoundAttribute((string)expr.Left, true);
AttributeType rightAttributeType = objectType.GetBoundAttribute((string)expr.Right, true);
if (!leftAttributeType.Indexed || !rightAttributeType.Indexed)
throw new ArgumentException(
String.Format(
"Currently an expression with attributes on both sides requires that both attributes are
indexed: {0}",
objectQuery.ToString()),
"objectQuery");
string colName = EnumHelper.GetAttributeValueColumnName(leftDataType);
string result = "exists (select 1 from AttributeValue av2"
+ " join AttributeValue av3 on o.Id = av3.ObjectId"
+ " and av3.AttributeType = @RightAttribName" + expr.Identifier
+ " where o.Id = av2.ObjectId and av2.AttributeType = @LeftAttribName" + expr.Identifier
+ " and av2." + colName + " "
+ this.InnerOperatorToSqlOperator(expr.InnerOperator) + " av3." + colName
+ ")";
queryParameters.Add("@LeftAttribName" + expr.Identifier, (string)expr.Left);
queryParameters.Add("@RightAttribName" + expr.Identifier, (string)expr.Right);
return result;
}
else if (!expr.LeftIsAttribute && !expr.RightIsAttribute)
{
throw new ArgumentException("Query currently doesn't support constant expressions!");
}
else
{
// One attribute and one value
string colName = EnumHelper.GetAttributeValueColumnName(leftDataType);
string expressionAttribute = expr.LeftIsAttribute ? (string)expr.Left : (string)expr.Right;
if (expressionAttribute == "ObjectId")
{
// We deal with ObjectId in a special way.
// Partly because this is more efficient,
// partly because it isn't present in the AttributeValue table.
string result = " o.Id " + this.InnerOperatorToSqlOperator(expr.InnerOperator) + " " +
this.GetAttributeValueParameterName(expr);
this.AddExpressionAttributeValueParameter(queryParameters, expr, expr.LeftIsAttribute);
return result;
}
else
{
AttributeType attributeType = objectType.GetBoundAttribute(expressionAttribute, true);
if (attributeType.Indexed)
{
string result = "exists (select 1 from AttributeValue av2"
+ " where o.Id = av2.ObjectId and av2.AttributeType = " +
this.GetAttributeNameParameterName(expr);
82
this.AddExpressionAttributeNameParameter(queryParameters, expr, expr.LeftIsAttribute);
foreach (QueryExpression collectiveEvalExpression in collectiveEvalExpressions)
{
if (collectiveEvalExpression.LeftIsAttribute)
{
result += " and av2." + colName + " "
+ this.InnerOperatorToSqlOperator(collectiveEvalExpression.InnerOperator) + " " +
this.GetAttributeValueParameterName(collectiveEvalExpression);
}
else
{
result += " and " + this.GetAttributeValueParameterName(collectiveEvalExpression) + " "
+ this.InnerOperatorToSqlOperator(collectiveEvalExpression.InnerOperator) + " av2." +
colName;
}
this.AddExpressionAttributeValueParameter(queryParameters, collectiveEvalExpression,
collectiveEvalExpression.LeftIsAttribute);
}
result += ")";
return result;
}
else // attribute is not "indexed" so we look in the xml
{
string result = String.Format(
@"o.Data.exist('/object/attributes/attribute[@name = sql:variable(""{0}"")]/value[@{1} =
sql:variable(""{2}"")]') <> 0",
this.GetAttributeNameParameterName(expr),
EnumHelper.GetAttributeValueXmlAttributeName(attributeType.DataType),
this.GetAttributeValueParameterName(expr));
this.AddExpressionAttributeAndValueParameters(queryParameters, expr, expr.LeftIsAttribute);
return result;
}
}
}
}
private string GetAttributeNameParameterName(QueryExpression expr)
{
return "@AttribName" + expr.Identifier;
}
private string GetAttributeValueParameterName(QueryExpression expr)
{
return "@AttribValue" + expr.Identifier;
}
private void ValidateOuterOperators(ObjectQuery objectQuery)
{
int idx = 0;
foreach (QueryExpression expr in objectQuery.Expressions)
{
if (idx > 0)
{
if (expr.OuterOperator == OuterOperator.Undefined)
throw new ArgumentException(
"Non-first expression must have an outer operator: '" + expr.ToString() + "'",
"objectQuery");
}
else
{
if (expr.OuterOperator != OuterOperator.Undefined)
throw new ArgumentException(
"First expression can't have an outer operator: '" + expr.ToString() + "'",
"objectQuery");
}
idx++;
}
}
// Helper method for BuildCommand()
83
private void AddExpressionAttributeAndValueParameters(
Dictionary<string, object> queryParameters,
QueryExpression expr,
bool leftRight,
bool skipAttributeNameParam = false)
{
if (!skipAttributeNameParam)
this.AddExpressionAttributeNameParameter(queryParameters, expr, leftRight);
this.AddExpressionAttributeValueParameter(queryParameters, expr, leftRight);
}
// Helper method for BuildCommand()
private void AddExpressionAttributeNameParameter(
Dictionary<string, object> queryParameters,
QueryExpression expr,
bool leftRight)
{
string attribute = leftRight ? (string)expr.Left : (string)expr.Right;
queryParameters.Add(this.GetAttributeNameParameterName(expr), attribute);
}
// Helper method for BuildCommand()
private void AddExpressionAttributeValueParameter(
Dictionary<string, object> queryParameters,
QueryExpression expr,
bool leftRight)
{
string attribute = leftRight ? (string)expr.Left : (string)expr.Right;
object exprValue = leftRight ? expr.RightUnquoted : expr.LeftUnquoted;
if (expr.InnerOperator == InnerOperator.Contains)
exprValue = /* "%" + */ (string)exprValue + "%";
queryParameters.Add(this.GetAttributeValueParameterName(expr), exprValue);
}
/// <summary>
/// QueryObjects allows for executing a BQuery against the object store.
/// </summary>
/// <param name="query">
/// A BQuery query.
/// Example: /Person[FirstName='Thomas']
/// </param>
/// <param name="resultSize">
/// Maximum number of objects to be returned by the query.
/// If resultSize is zero all objects that meet the query are returned.
/// </param>
/// <param name="orderAttribute">
/// Name of an AttributeType.
/// The query result will be ordered (ascending) by this attribute.
/// orderAttribute is ignored if null.
/// </param>
/// <returns></returns>
public List<DataObject> QueryObjects(string query, int resultSize = 0,
string orderAttribute = null)
{
if (String.IsNullOrEmpty(query))
throw new ArgumentNullOrEmptyException("query");
if (resultSize < 0)
throw new ArgumentException("Can't be negative", "resultSize");
// TODO: implement orderAttribute
if (orderAttribute != null)
throw new ArgumentException("This options is not yet supported", "orderAttribute");
return this.DoInConnection<List<DataObject>>(
delegate
{
ObjectQuery objectQuery = new ObjectQuery(query);
objectQuery.ResultSize = resultSize;
objectQuery.OrderAttribute = orderAttribute;
// Load the ObjectType which the query is for
SchemaController schemaController = new SchemaController(this);
ObjectType objectType = schemaController.GetObjectType(objectQuery.ObjectType);
// Check that we're ordering on an attribute that exists on the object type
if (!String.IsNullOrEmpty(orderAttribute))
84
if (!objectType.HasBindingForAttribute(orderAttribute))
throw new ArgumentException("Can't order on '" + orderAttribute + "' as the ObjectType
has no binding for it", "orderAttribute");
List<DataObject> result = new List<DataObject>();
using (SqlCommand cmd = this.BuildCommand(objectQuery, objectType))
{
DataObject dataObject = null;
using (SqlDataReader dataReader = cmd.ExecuteReader())
while (dataReader.Read())
{
Guid objectId = (Guid)dataReader["Id"];
if (dataObject == null || dataObject.Id != objectId)
{
dataObject = new DataObject(objectId, objectQuery.ObjectType);
result.Add(dataObject);
}
string xml = (string)dataReader["Data"];
this.ParseXml(xml, dataObject, objectType);
}
}
return result;
});
}
private void ParseXml(string xml, DataObject dataObject, ObjectType objectType)
{
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(xml);
foreach (XmlNode node in xmlDoc.SelectNodes("/object/attributes/attribute[@name != 'ObjectType'
and @name != 'ObjectId']"))
{
string attributeName = node.Attributes["name"].Value;
AttributeType attributeType = objectType.GetBoundAttribute(attributeName, false);
if (attributeType != null)
{
if (attributeType.MultiValued)
{
List<object> values = new List<object>(); // !!! kan man det?
foreach (XmlNode valueNode in node.ChildNodes)
{
object value = this.GetValue(valueNode, attributeType);
values.Add(value);
}
dataObject.Attributes.Add(attributeType.Name, values);
}
else
{
object value = this.GetValue(node.ChildNodes[0], attributeType);
dataObject.Attributes.Add(attributeType.Name, value);
}
}
}
}
private object GetValue(XmlNode valueNode, AttributeType attributeType)
{
switch (attributeType.DataType)
{
case AttributeDataType.Boolean:
return XmlConvert.ToBoolean(valueNode.Attributes["b_value"].Value);
case AttributeDataType.Integer:
return XmlConvert.ToInt32(valueNode.Attributes["i_value"].Value);
case AttributeDataType.DateTime:
return XmlConvert.ToDateTime(valueNode.Attributes["dt_value"].Value,
XmlDateTimeSerializationMode.Utc);
case AttributeDataType.String:
return valueNode.Attributes["s_value"].Value;
case AttributeDataType.Reference:
return new Guid(valueNode.Attributes["r_value"].Value);
default:
throw new Exception("Unknown: " + attributeType.DataType);
}
}
85
}
}