هيلو سڀ. اسان آف لائن ٽرئفڪ جي تجزيو لاءِ پراڊڪٽ ٺاهي رهيا آهيون. پروجيڪٽ ۾ علائقن جي گهمڻ وارن رستن جي شمارياتي تجزيي سان لاڳاپيل ڪم آهي.
ھن ڪم جي حصي جي طور تي، صارفين ھيٺ ڏنل قسم جا سسٽم سوال پڇي سگھن ٿا:
- ڪيترا سياحن "A" کان علائقي "B" تائين گذري ويا؛
- ڪيترا سياح "اي" کان ايريا "بي" تائين ايريا "سي" ذريعي ۽ پوء ايريا "ڊي" ذريعي گذريا آهن؛
- هڪ خاص قسم جي سياحن کي علائقي ”A“ کان علائقي ”B“ تائين سفر ڪرڻ ۾ ڪيترو وقت لڳي ويو.
۽ اهڙا ڪيترائي تجزياتي سوال.
علائقن ۾ دورو ڪندڙ جي حرڪت هڪ هدايت ٿيل گراف آهي. انٽرنيٽ پڙهڻ کان پوء، مون دريافت ڪيو ته گراف ڊي بي ايم ايس پڻ تجزياتي رپورٽن لاء استعمال ڪيا ويا آهن. مون کي ڏسڻ جي خواهش هئي ته گراف ڊي بي ايم ايس اهڙين سوالن کي ڪيئن منهن ڏيندو (TL؛ DR؛ خراب).
مون DBMS استعمال ڪرڻ جو انتخاب ڪيو
- BerkeleyDB اسٽوريج پس منظر، Apache Cassandra، Scylla؛
- پيچيده انڊيڪس لوسن، ايلسٽسٽڪ سرچ، سولر ۾ محفوظ ڪري سگھجن ٿا.
JanusGraph جي ليکڪن لکي ٿو ته اهو OLTP ۽ OLAP ٻنهي لاءِ موزون آهي.
مون BerkeleyDB، Apache Cassandra، Scylla ۽ ES سان ڪم ڪيو آهي، ۽ اهي پروڊڪٽس اڪثر ڪري اسان جي سسٽم ۾ استعمال ٿينديون آهن، تنهن ڪري مان هن گراف ڊي بي ايم ايس کي جانچڻ بابت پراميد هوس. مون کي اهو عجيب لڳو ته BerkeleyDB کي RocksDB تي چونڊيو، پر اهو شايد ٽرانزيڪشن جي گهرج جي ڪري. ڪنهن به صورت ۾، اسپيبلبل، پراڊڪٽ جي استعمال لاء، اهو تجويز ڪيو ويو آهي ته هڪ پس منظر استعمال ڪيو وڃي Cassandra يا Scylla تي.
مون Neo4j تي غور نه ڪيو ڇو ته ڪلسترنگ کي تجارتي ورزن جي ضرورت آهي، اهو آهي، پراڊڪٽ اوپن سورس ناهي.
گراف ڊي بي ايم ايس چوي ٿو: "جيڪڏهن اهو گراف وانگر نظر اچي ٿو، ان کي گراف وانگر علاج ڪريو!" - خوبصورتي!
پهرين، مون هڪ گراف ٺاهيو، جيڪو بلڪل گراف ڊي بي ايم ايس جي ڪينن جي مطابق ٺاهيو ويو هو:
هڪ جوهر آهي Zone
، علائقي جو ذميوار. جيڪڏهن ZoneStep
هن سان تعلق رکي ٿو Zone
، پوءِ هن ڏانهن اشارو ڪيو. جوهر تي Area
, ZoneTrack
, Person
ڌيان نه ڏيو، اهي ڊومين سان تعلق رکن ٿا ۽ امتحان جو حصو نه سمجهيا وڃن. مجموعي طور تي، اهڙي گراف جي جوڙجڪ لاء هڪ زنجير ڳولا سوال هن طرح نظر ايندو:
g.V().hasLabel('Zone').has('id',0).in_()
.repeat(__.out()).until(__.out().hasLabel('Zone').has('id',19)).count().next()
روسي ۾ ڇا ڪجهه هن طرح آهي: ID = 0 سان هڪ زون ڳولهيو، اهي سڀئي چوڪيون وٺو جتان هڪ ڪنڊ ان ڏانهن وڃي ٿو (زون اسٽيپ)، پوئتي وڃڻ کان سواءِ اسٽمپ ڪريو جيستائين توهان انهن زون اسٽيپس کي نه ڳوليو جتان زون ڏانهن هڪ ڪنڊ آهي. ID = 19، انگ ڳڻيو جيئن زنجيرن.
مان گرافس تي ڳولڻ جي سڀني پيچيدگين کي ڄاڻڻ جو ارادو نٿو ڪريان، پر هي سوال هن ڪتاب جي بنياد تي پيدا ڪيو ويو آهي (
مون 50 هزار ٽريڪ لوڊ ڪيا جن جي ڊيگهه 3 کان 20 پوائنٽس تائين آهي هڪ JanusGraph ڊيٽابيس ۾ BerkeleyDB backend استعمال ڪندي، انڊيڪس ٺاهيا.
پٿون ڊائون لوڊ اسڪرپٽ:
from random import random
from time import time
from init import g, graph
if __name__ == '__main__':
points = []
max_zones = 19
zcache = dict()
for i in range(0, max_zones + 1):
zcache[i] = g.addV('Zone').property('id', i).next()
startZ = zcache[0]
endZ = zcache[max_zones]
for i in range(0, 10000):
if not i % 100:
print(i)
start = g.addV('ZoneStep').property('time', int(time())).next()
g.V(start).addE('belongs').to(startZ).iterate()
while True:
pt = g.addV('ZoneStep').property('time', int(time())).next()
end_chain = random()
if end_chain < 0.3:
g.V(pt).addE('belongs').to(endZ).iterate()
g.V(start).addE('goes').to(pt).iterate()
break
else:
zone_id = int(random() * max_zones)
g.V(pt).addE('belongs').to(zcache[zone_id]).iterate()
g.V(start).addE('goes').to(pt).iterate()
start = pt
count = g.V().count().next()
print(count)
اسان هڪ ايس ايس ڊي تي 4 ڪور ۽ 16 GB ريم سان VM استعمال ڪيو. JanusGraph هن حڪم کي استعمال ڪندي لڳايو ويو:
docker run --name janusgraph -p8182:8182 janusgraph/janusgraph:latest
انهي صورت ۾، ڊيٽا ۽ انڊيڪس جيڪي استعمال ڪيا ويا آهن درست ميچ ڳولها لاء برڪلي ڊي بي ۾ ذخيرو ٿيل آهن. اڳ ۾ ڏنل درخواست تي عمل ڪرڻ کان پوء، مون کي ڪيترن ئي ڏهن سيڪنڊن جي برابر وقت مليو.
متوازي ۾ 4 مٿين اسڪرپٽ کي هلائڻ سان، مون ڊي بي ايم ايس کي ڪدو ۾ تبديل ڪرڻ جو انتظام ڪيو، جاوا اسٽيڪ ٽريسز جي خوشگوار وهڪري سان (۽ اسان سڀني کي جاوا اسٽيڪ ٽريسس پڙهڻ پسند آهي) ڊڪر لاگز ۾.
ڪجهه سوچڻ کان پوء، مون فيصلو ڪيو ته هيٺ ڏنل گراف ڊاگرام کي آسان ڪرڻ:
اهو فيصلو ڪيو ته ڳولا جي خاصيتن جي ڳولا ڪنارن جي ڳولا کان تيز ٿي ويندي. نتيجي طور، منهنجي درخواست ھيٺين ۾ تبديل ٿي وئي:
g.V().hasLabel('ZoneStep').has('id',0).repeat(__.out().simplePath()).until(__.hasLabel('ZoneStep').has('id',19)).count().next()
روسي ۾ ڇا ڪجهه هن طرح آهي: ID = 0 سان ZoneStep ڳولھيو، بغير واپس وڃڻ کان سواء اسٽمپ ڪريو جيستائين توھان ZoneStep سان ID = 19 ڳوليو، اھڙين زنجيرن جو تعداد ڳڻيو.
مون مٿي ڏنل لوڊنگ اسڪرپٽ کي پڻ آسان ڪيو آهي ته جيئن غير ضروري ڪنيڪشن نه ٺاهي، پاڻ کي خاصيتن تائين محدود ڪري.
درخواست اڃا تائين مڪمل ٿيڻ ۾ ڪيترائي سيڪنڊ ورتي، جيڪا اسان جي ڪم لاءِ مڪمل طور تي ناقابل قبول هئي، ڇاڪاڻ ته اها ڪنهن به قسم جي AdHoc درخواستن جي مقصدن لاءِ بلڪل مناسب نه هئي.
مون ڪوشش ڪئي JanusGraph کي استعمال ڪندي Scylla کي تيز ترين Cassandra تي عمل ڪرڻ جي طور تي، پر ان سان پڻ ڪارڪردگي ۾ ڪا خاص تبديلي نه آئي.
تنهن ڪري ان حقيقت جي باوجود ته "اهو هڪ گراف وانگر ڏسڻ ۾ اچي ٿو"، مان گراف حاصل نه ڪري سگهيو آهيان DBMS ان کي جلدي پروسيس ڪرڻ لاءِ. مان مڪمل طور تي فرض ڪريان ٿو ته اتي ڪجھھ آھي جيڪو مون کي خبر نه آھي ۽ اھو JanusGraph ھڪڙي سيڪنڊ جي ھڪڙي حصي ۾ ھن ڳولا کي انجام ڏيڻ لاءِ ٺاھيو وڃي ٿو، جيتوڻيڪ، مان اھو ڪرڻ جي قابل نه ھوس.
جيئن ته مسئلو اڃا حل ٿيڻ جي ضرورت آهي، مون جدولن جي JOINs ۽ Pivots بابت سوچڻ شروع ڪيو، جيڪي خوبصورتي جي لحاظ کان اميد پسندي کي متاثر نه ڪن، پر عملي طور تي هڪ مڪمل طور تي قابل عمل اختيار ٿي سگهي ٿو.
اسان جو پروجيڪٽ اڳ ۾ ئي استعمال ڪري ٿو Apache ClickHouse، تنهن ڪري مون فيصلو ڪيو ته منهنجي تحقيق کي جانچڻ لاءِ هن تجزياتي DBMS تي.
هڪ سادي ترڪيب استعمال ڪندي ڪلڪ هاؤس کي ترتيب ڏنو:
sudo docker run -d --name clickhouse_1
--ulimit nofile=262144:262144
-v /opt/clickhouse/log:/var/log/clickhouse-server
-v /opt/clickhouse/data:/var/lib/clickhouse
yandex/clickhouse-server
مون هڪ ڊيٽابيس ۽ ٽيبل ٺاهيو ان ۾ هن طرح:
CREATE TABLE
db.steps (`area` Int64, `when` DateTime64(1, 'Europe/Moscow') DEFAULT now64(), `zone` Int64, `person` Int64)
ENGINE = MergeTree() ORDER BY (area, zone, person) SETTINGS index_granularity = 8192
مون ان کي هيٺين اسڪرپٽ استعمال ڪندي ڊيٽا سان ڀريو:
from time import time
from clickhouse_driver import Client
from random import random
client = Client('vm-12c2c34c-df68-4a98-b1e5-a4d1cef1acff.domain',
database='db',
password='secret')
max = 20
for r in range(0, 100000):
if r % 1000 == 0:
print("CNT: {}, TS: {}".format(r, time()))
data = [{
'area': 0,
'zone': 0,
'person': r
}]
while True:
if random() < 0.3:
break
data.append({
'area': 0,
'zone': int(random() * (max - 2)) + 1,
'person': r
})
data.append({
'area': 0,
'zone': max - 1,
'person': r
})
client.execute(
'INSERT INTO steps (area, zone, person) VALUES',
data
)
جيئن ته داخلائون بيچ ۾ اچن ٿيون، جانوس گراف جي ڀيٽ ۾ ڀرڻ تمام تيز هو.
JOIN استعمال ڪندي ٻه سوال ٺاھيو. پوائنٽ A کان پوائنٽ B ڏانهن منتقل ڪرڻ لاء:
SELECT s1.person AS person,
s1.zone,
s1.when,
s2.zone,
s2.when
FROM
(SELECT *
FROM steps
WHERE (area = 0)
AND (zone = 0)) AS s1 ANY INNER JOIN
(SELECT *
FROM steps AS s2
WHERE (area = 0)
AND (zone = 19)) AS s2 USING person
WHERE s1.when <= s2.when
3 پوائنٽ ذريعي وڃڻ لاء:
SELECT s3.person,
s1z,
s1w,
s2z,
s2w,
s3.zone,
s3.when
FROM
(SELECT s1.person AS person,
s1.zone AS s1z,
s1.when AS s1w,
s2.zone AS s2z,
s2.when AS s2w
FROM
(SELECT *
FROM steps
WHERE (area = 0)
AND (zone = 0)) AS s1 ANY INNER JOIN
(SELECT *
FROM steps AS s2
WHERE (area = 0)
AND (zone = 3)) AS s2 USING person
WHERE s1.when <= s2.when) p ANY INNER JOIN
(SELECT *
FROM steps
WHERE (area = 0)
AND (zone = 19)) AS s3 USING person
WHERE p.s2w <= s3.when
درخواستون، يقينا، ڪافي خوفناڪ نظر اچن ٿا؛ حقيقي استعمال لاء، توهان کي سافٽ ويئر جنريٽر هارنس ٺاهڻ جي ضرورت آهي. بهرحال، اهي ڪم ڪن ٿا ۽ اهي جلدي ڪم ڪن ٿا. ٻئي ۽ ٻئي درخواستون 0.1 سيڪنڊن کان گھٽ ۾ مڪمل ڪيون ويون آھن. هتي ڳڻپ (*) 3 پوائنٽن مان گذرڻ لاءِ سوال جي عمل جي وقت جو هڪ مثال آهي:
SELECT count(*)
FROM
(
SELECT
s1.person AS person,
s1.zone AS s1z,
s1.when AS s1w,
s2.zone AS s2z,
s2.when AS s2w
FROM
(
SELECT *
FROM steps
WHERE (area = 0) AND (zone = 0)
) AS s1
ANY INNER JOIN
(
SELECT *
FROM steps AS s2
WHERE (area = 0) AND (zone = 3)
) AS s2 USING (person)
WHERE s1.when <= s2.when
) AS p
ANY INNER JOIN
(
SELECT *
FROM steps
WHERE (area = 0) AND (zone = 19)
) AS s3 USING (person)
WHERE p.s2w <= s3.when
┌─count()─┐
│ 11592 │
└─────────┘
1 rows in set. Elapsed: 0.068 sec. Processed 250.03 thousand rows, 8.00 MB (3.69 million rows/s., 117.98 MB/s.)
IOPS بابت هڪ نوٽ. جڏهن ڊيٽا کي آباد ڪندي، JanusGraph IOPS جو هڪ تمام وڏو تعداد پيدا ڪيو (1000-1300 چار ڊيٽا جي آبادي جي سلسلي لاءِ) ۽ IOWAIT تمام گهڻو هو. ساڳئي وقت، ڪلڪ هائوس ڊسڪ سب سسٽم تي گهٽ ۾ گهٽ لوڊ پيدا ڪيو.
ٿڪل
اسان هن قسم جي درخواست جي خدمت ڪرڻ لاءِ ClickHouse استعمال ڪرڻ جو فيصلو ڪيو. اسان هميشه سوالن کي وڌيڪ بهتر ڪري سگھون ٿا مواد جي نظارن ۽ برابري کي استعمال ڪندي ايونٽ اسٽريم کي پري پروسيس ڪندي Apache Flink استعمال ڪرڻ کان پهريان انهن کي ClickHouse ۾ لوڊ ڪرڻ کان.
ڪارڪردگي ايتري سٺي آهي ته اسان کي شايد پروگرام جي طور تي پائيوٽنگ ٽيبل بابت سوچڻ جي ضرورت ناهي. اڳي، اسان کي اپلوڊ ذريعي ورٽيڪا مان حاصل ڪيل ڊيٽا جا محور ڪرڻا هئا Apache Parquet تي.
بدقسمتي سان، گراف ڊي بي ايم ايس استعمال ڪرڻ جي هڪ ٻي ڪوشش ناڪام ٿي. مون کي نه مليو JanusGraph هڪ دوستانه ماحولياتي نظام آهي جنهن کي پيداوار سان تيز رفتار حاصل ڪرڻ آسان بڻائي ٿي. ساڳئي وقت، سرور کي ترتيب ڏيڻ لاء، روايتي جاوا طريقو استعمال ڪيو ويندو آهي، جيڪو انهن ماڻهن کي ٺاهيندو جيڪي جاوا کان واقف نه آهن رت جا ڳوڙها روئي ٿو:
host: 0.0.0.0
port: 8182
threadPoolWorker: 1
gremlinPool: 8
scriptEvaluationTimeout: 30000
channelizer: org.janusgraph.channelizers.JanusGraphWsAndHttpChannelizer
graphManager: org.janusgraph.graphdb.management.JanusGraphManager
graphs: {
ConfigurationManagementGraph: conf/janusgraph-cql-configurationgraph.properties,
airlines: conf/airlines.properties
}
scriptEngines: {
gremlin-groovy: {
plugins: { org.janusgraph.graphdb.tinkerpop.plugin.JanusGraphGremlinPlugin: {},
org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {},
org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {},
org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]},
org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/airline-sample.groovy]}}}}
serializers:
# GraphBinary is here to replace Gryo and Graphson
- { className: org.apache.tinkerpop.gremlin.driver.ser.GraphBinaryMessageSerializerV1, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }}
- { className: org.apache.tinkerpop.gremlin.driver.ser.GraphBinaryMessageSerializerV1, config: { serializeResultToString: true }}
# Gryo and Graphson, latest versions
- { className: org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV3d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }}
- { className: org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV3d0, config: { serializeResultToString: true }}
- { className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV3d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }}
# Older serialization versions for backwards compatibility:
- { className: org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV1d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }}
- { className: org.apache.tinkerpop.gremlin.driver.ser.GryoMessageSerializerV1d0, config: { serializeResultToString: true }}
- { className: org.apache.tinkerpop.gremlin.driver.ser.GryoLiteMessageSerializerV1d0, config: {ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }}
- { className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerGremlinV2d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistry] }}
- { className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerGremlinV1d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistryV1d0] }}
- { className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0, config: { ioRegistries: [org.janusgraph.graphdb.tinkerpop.JanusGraphIoRegistryV1d0] }}
processors:
- { className: org.apache.tinkerpop.gremlin.server.op.session.SessionOpProcessor, config: { sessionTimeout: 28800000 }}
- { className: org.apache.tinkerpop.gremlin.server.op.traversal.TraversalOpProcessor, config: { cacheExpirationTime: 600000, cacheMaxSize: 1000 }}
metrics: {
consoleReporter: {enabled: false, interval: 180000},
csvReporter: {enabled: false, interval: 180000, fileName: /tmp/gremlin-server-metrics.csv},
jmxReporter: {enabled: false},
slf4jReporter: {enabled: true, interval: 180000},
gangliaReporter: {enabled: false, interval: 180000, addressingMode: MULTICAST},
graphiteReporter: {enabled: false, interval: 180000}}
threadPoolBoss: 1
maxInitialLineLength: 4096
maxHeaderSize: 8192
maxChunkSize: 8192
maxContentLength: 65536
maxAccumulationBufferComponents: 1024
resultIterationBatchSize: 64
writeBufferHighWaterMark: 32768
writeBufferHighWaterMark: 65536
ssl: {
enabled: false}
مان جانس گراف جي برڪلي ڊي بي ورزن کي حادثاتي طور تي ”پڙهڻ“ ۾ منظم ڪيو.
دستاويز انڊيڪسز جي لحاظ کان ڪافي ڪڙي آهي، ڇاڪاڻ ته انڊيڪسس کي منظم ڪرڻ لاءِ توهان کي گرووي ۾ ڪجهه عجيب شرمناڪ ڪم ڪرڻ جي ضرورت آهي. مثال طور، هڪ انڊيڪس ٺاهڻ لازمي آهي ڪوڊ لکڻ سان Gremlin ڪنسول ۾ (جيڪو، طريقي سان، دٻي کان ٻاهر ڪم نٿو ڪري). سرڪاري JanusGraph دستاويزن مان:
graph.tx().rollback() //Never create new indexes while a transaction is active
mgmt = graph.openManagement()
name = mgmt.getPropertyKey('name')
age = mgmt.getPropertyKey('age')
mgmt.buildIndex('byNameComposite', Vertex.class).addKey(name).buildCompositeIndex()
mgmt.buildIndex('byNameAndAgeComposite', Vertex.class).addKey(name).addKey(age).buildCompositeIndex()
mgmt.commit()
//Wait for the index to become available
ManagementSystem.awaitGraphIndexStatus(graph, 'byNameComposite').call()
ManagementSystem.awaitGraphIndexStatus(graph, 'byNameAndAgeComposite').call()
//Reindex the existing data
mgmt = graph.openManagement()
mgmt.updateIndex(mgmt.getGraphIndex("byNameComposite"), SchemaAction.REINDEX).get()
mgmt.updateIndex(mgmt.getGraphIndex("byNameAndAgeComposite"), SchemaAction.REINDEX).get()
mgmt.commit()
پوء
هڪ لحاظ کان، مٿي ڄاڻايل تجربو گرم ۽ نرم جي وچ ۾ مقابلو آهي. جيڪڏهن توهان ان جي باري ۾ سوچيو ٿا، هڪ گراف ڊي بي ايم ايس ساڳئي نتيجن کي حاصل ڪرڻ لاء ٻين عملن کي انجام ڏئي ٿو. بهرحال، تجربن جي حصي جي طور تي، مون پڻ هڪ درخواست سان گڏ هڪ تجربو ڪيو جيئن:
g.V().hasLabel('ZoneStep').has('id',0)
.repeat(__.out().simplePath()).until(__.hasLabel('ZoneStep').has('id',1)).count().next()
جيڪو پنڌ جي مفاصلي کي ظاهر ڪري ٿو. بهرحال، اهڙي ڊيٽا تي به، گراف ڊي بي ايم ايس نتيجا ڏيکاريا جيڪي ڪجهه سيڪنڊن کان اڳتي نڪري ويا... اهو، يقيناً، ان حقيقت جي ڪري آهي ته اهڙا رستا هئا. 0 -> X -> Y ... -> 1
جنهن کي گراف انجڻ به چيڪ ڪيو.
جيتوڻيڪ هڪ سوال لاءِ جهڙوڪ:
g.V().hasLabel('ZoneStep').has('id',0).out().has('id',1)).count().next()
مان هڪ سيڪنڊ کان به گهٽ وقت جي پروسيسنگ وقت سان پيداواري جواب حاصل ڪرڻ کان قاصر هو.
ڪهاڻيءَ جي اخلاقي خوبي اها آهي ته هڪ خوبصورت خيال ۽ تمثيل واري ماڊلنگ گهربل نتيجو نه پهچندي آهي، جنهن کي ڪلڪ هائوس جو مثال استعمال ڪندي تمام اعليٰ ڪارڪردگيءَ سان ڏيکاريو ويو آهي. هن آرٽيڪل ۾ پيش ڪيل استعمال ڪيس گراف ڊي بي ايم ايسز لاءِ واضح مخالف نمونو آهي، جيتوڻيڪ اهو لڳي ٿو ته انهن جي پيراڊم ۾ ماڊلنگ لاءِ مناسب.
جو ذريعو: www.habr.com