Ένα πείραμα που δοκιμάζει την εφαρμογή του γραφήματος JanusGraph DBMS για την επίλυση του προβλήματος της εύρεσης κατάλληλων μονοπατιών

Ένα πείραμα που δοκιμάζει την εφαρμογή του γραφήματος JanusGraph DBMS για την επίλυση του προβλήματος της εύρεσης κατάλληλων μονοπατιών

Γεια σε όλους. Αναπτύσσουμε ένα προϊόν για ανάλυση κίνησης εκτός σύνδεσης. Το έργο έχει ένα έργο που σχετίζεται με τη στατιστική ανάλυση των διαδρομών επισκεπτών σε διάφορες περιοχές.

Ως μέρος αυτής της εργασίας, οι χρήστες μπορούν να υποβάλουν ερωτήματα συστήματος του ακόλουθου τύπου:

  • πόσοι επισκέπτες πέρασαν από την περιοχή "Α" στην περιοχή "Β";
  • πόσοι επισκέπτες πέρασαν από την περιοχή "Α" στην περιοχή "Β" μέσω της περιοχής "Γ" και στη συνέχεια από την περιοχή "Δ";
  • πόσος χρόνος χρειάστηκε για έναν συγκεκριμένο τύπο επισκέπτη να ταξιδέψει από την περιοχή «Α» στην περιοχή «Β».

και μια σειρά από παρόμοια αναλυτικά ερωτήματα.

Η κίνηση του επισκέπτη στις περιοχές είναι ένα κατευθυνόμενο γράφημα. Αφού διάβασα το Διαδίκτυο, ανακάλυψα ότι τα DBMS γραφημάτων χρησιμοποιούνται επίσης για αναλυτικές αναφορές. Είχα την επιθυμία να δω πώς τα DBMS γραφημάτων θα αντιμετωπίσουν τέτοια ερωτήματα (TL · DR; πτωχώς).

Επέλεξα να χρησιμοποιήσω το DBMS JanusGraph, ως εξαιρετικός εκπρόσωπος του DBMS ανοιχτού κώδικα γραφημάτων, το οποίο βασίζεται σε μια στοίβα ώριμων τεχνολογιών, οι οποίες (κατά τη γνώμη μου) θα πρέπει να του παρέχουν αξιοπρεπή λειτουργικά χαρακτηριστικά:

  • Backend αποθήκευσης BerkeleyDB, Apache Cassandra, Scylla;
  • σύνθετα ευρετήρια μπορούν να αποθηκευτούν στα Lucene, Elasticsearch, Solr.

Οι συντάκτες του JanusGraph γράφουν ότι είναι κατάλληλο τόσο για OLTP όσο και για OLAP.

Έχω δουλέψει με BerkeleyDB, Apache Cassandra, Scylla και ES και αυτά τα προϊόντα χρησιμοποιούνται συχνά στα συστήματά μας, επομένως ήμουν αισιόδοξος για τη δοκιμή αυτού του DBMS γραφήματος. Μου φάνηκε περίεργο να επιλέξω το BerkeleyDB έναντι του RocksDB, αλλά αυτό πιθανότατα οφείλεται στις απαιτήσεις συναλλαγής. Σε κάθε περίπτωση, για επεκτάσιμη χρήση προϊόντος, προτείνεται η χρήση backend στο Cassandra ή στο Scylla.

Δεν εξέτασα το Neo4j γιατί η ομαδοποίηση απαιτεί μια εμπορική έκδοση, δηλαδή το προϊόν δεν είναι ανοιχτού κώδικα.

Τα DBMS γραφημάτων λένε: "Αν μοιάζει με γράφημα, αντιμετώπισέ το σαν γράφημα!" - ομορφιά!

Πρώτα, σχεδίασα ένα γράφημα, το οποίο έγινε ακριβώς σύμφωνα με τους κανόνες των DBMS γραφημάτων:

Ένα πείραμα που δοκιμάζει την εφαρμογή του γραφήματος JanusGraph DBMS για την επίλυση του προβλήματος της εύρεσης κατάλληλων μονοπατιών

Υπάρχει μια ουσία 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, πάρτε όλες τις κορυφές από τις οποίες πηγαίνει μια άκρη σε αυτήν (ZoneStep), ποδοπατήστε χωρίς να επιστρέψετε μέχρι να βρείτε εκείνα τα ZoneSteps από τα οποία υπάρχει μια άκρη στη Ζώνη με ID=19, μετρήστε τον αριθμό αυτών των αλυσίδων.

Δεν προσποιούμαι ότι γνωρίζω όλες τις περιπλοκές της αναζήτησης σε γραφήματα, αλλά αυτό το ερώτημα δημιουργήθηκε με βάση αυτό το βιβλίο (https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html).

Φόρτωσα 50 χιλιάδες κομμάτια μήκους από 3 έως 20 σημεία σε μια βάση δεδομένων γραφημάτων JanusGraph χρησιμοποιώντας το backend BerkeleyDB, δημιούργησα ευρετήρια σύμφωνα με ηγεσία.

Σενάριο λήψης Python:


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)

Χρησιμοποιήσαμε VM με 4 πυρήνες και 16 GB RAM σε SSD. Το JanusGraph αναπτύχθηκε χρησιμοποιώντας αυτήν την εντολή:

docker run --name janusgraph -p8182:8182 janusgraph/janusgraph:latest

Σε αυτήν την περίπτωση, τα δεδομένα και τα ευρετήρια που χρησιμοποιούνται για αναζητήσεις ακριβούς αντιστοίχισης αποθηκεύονται στο BerkeleyDB. Έχοντας εκτελέσει το αίτημα που δόθηκε νωρίτερα, έλαβα χρόνο ίσο με αρκετές δεκάδες δευτερόλεπτα.

Εκτελώντας παράλληλα τα 4 παραπάνω σενάρια, κατάφερα να μετατρέψω το DBMS σε κολοκύθα με μια χαρούμενη ροή Java stacktraces (και σε όλους μας αρέσει να διαβάζουμε Java stacktraces) στα αρχεία καταγραφής του Docker.

Μετά από λίγη σκέψη, αποφάσισα να απλοποιήσω το διάγραμμα γραφήματος ως εξής:

Ένα πείραμα που δοκιμάζει την εφαρμογή του γραφήματος JanusGraph DBMS για την επίλυση του προβλήματος της εύρεσης κατάλληλων μονοπατιών

Αποφασίζοντας ότι η αναζήτηση βάσει χαρακτηριστικών οντοτήτων θα ήταν ταχύτερη από την αναζήτηση κατά ακμές. Ως αποτέλεσμα, το αίτημά μου μετατράπηκε στο εξής:

g.V().hasLabel('ZoneStep').has('id',0).repeat(__.out().simplePath()).until(__.hasLabel('ZoneStep').has('id',19)).count().next()

Αυτό που στα ρωσικά είναι κάπως έτσι: βρείτε το ZoneStep με ID=0, κάντε stomp χωρίς να επιστρέψετε μέχρι να βρείτε το ZoneStep με ID=19, μετρήστε τον αριθμό τέτοιων αλυσίδων.

Επίσης, απλοποίησα το σενάριο φόρτωσης που δόθηκε παραπάνω για να μην δημιουργήσω περιττές συνδέσεις, περιορίζοντάς τον σε χαρακτηριστικά.

Το αίτημα χρειάστηκε ακόμα αρκετά δευτερόλεπτα για να ολοκληρωθεί, κάτι που ήταν εντελώς απαράδεκτο για την εργασία μας, καθώς δεν ήταν καθόλου κατάλληλο για τους σκοπούς των αιτημάτων AdHoc οποιουδήποτε είδους.

Δοκίμασα να αναπτύξω το JanusGraph χρησιμοποιώντας το Scylla ως την ταχύτερη εφαρμογή Cassandra, αλλά και αυτό δεν οδήγησε σε σημαντικές αλλαγές στην απόδοση.

Έτσι, παρά το γεγονός ότι "μοιάζει με γράφημα", δεν μπόρεσα να αποκτήσω το DBMS γραφήματος για να το επεξεργαστεί γρήγορα. Υποθέτω πλήρως ότι υπάρχει κάτι που δεν γνωρίζω και ότι ο JanusGraph μπορεί να αναγκαστεί να εκτελέσει αυτήν την αναζήτηση σε κλάσματα δευτερολέπτου, ωστόσο, δεν κατάφερα να το κάνω.

Δεδομένου ότι το πρόβλημα έπρεπε ακόμη να λυθεί, άρχισα να σκέφτομαι τα JOIN και τα Pivots των τραπεζιών, τα οποία δεν ενέπνεαν αισιοδοξία από άποψη κομψότητας, αλλά θα μπορούσαν να είναι μια απολύτως εφαρμόσιμη επιλογή στην πράξη.

Το έργο μας χρησιμοποιεί ήδη το Apache ClickHouse, γι' αυτό αποφάσισα να δοκιμάσω την έρευνά μου σε αυτό το αναλυτικό DBMS.

Αναπτύχθηκε το ClickHouse χρησιμοποιώντας μια απλή συνταγή:

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
    )

Δεδομένου ότι τα ένθετα έρχονται σε παρτίδες, το γέμισμα ήταν πολύ πιο γρήγορο από το JanusGraph.

Δημιουργήθηκαν δύο ερωτήματα χρησιμοποιώντας JOIN. Για να μετακινηθείτε από το σημείο Α στο σημείο Β:

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 δευτερόλεπτα. Ακολουθεί ένα παράδειγμα του χρόνου εκτέλεσης του ερωτήματος για το count(*) που διέρχεται από 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 δημιούργησε ελάχιστο φορτίο στο υποσύστημα του δίσκου.

Συμπέρασμα

Αποφασίσαμε να χρησιμοποιήσουμε το ClickHouse για την εξυπηρέτηση αυτού του τύπου αιτήματος. Μπορούμε πάντα να βελτιστοποιήσουμε περαιτέρω τα ερωτήματα χρησιμοποιώντας υλοποιημένες προβολές και παραλληλοποίηση, προ-επεξεργάζοντας τη ροή συμβάντων χρησιμοποιώντας το Apache Flink πριν τα φορτώσουμε στο ClickHouse.

Η απόδοση είναι τόσο καλή που πιθανότατα δεν θα χρειαστεί καν να σκεφτούμε την περιστροφή πινάκων προγραμματικά. Προηγουμένως, έπρεπε να κάνουμε περιστροφές δεδομένων που ανακτήθηκαν από τη Vertica μέσω μεταφόρτωσης στο Apache Parquet.

Δυστυχώς, μια άλλη προσπάθεια χρήσης ενός DBMS γραφήματος ήταν ανεπιτυχής. Δεν βρήκα το JanusGraph να έχει ένα φιλικό οικοσύστημα που να διευκολύνει την ενημέρωση του προϊόντος. Ταυτόχρονα, για τη διαμόρφωση του διακομιστή, χρησιμοποιείται ο παραδοσιακός τρόπος Java, ο οποίος θα κάνει τους ανθρώπους που δεν είναι εξοικειωμένοι με την Java να κλαίνε με δάκρυα αίματος:

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}

Κατάφερα κατά λάθος να «βάλω» την έκδοση BerkeleyDB του JanusGraph.

Η τεκμηρίωση είναι αρκετά στρεβλή όσον αφορά τα ευρετήρια, καθώς η διαχείριση των ευρετηρίων απαιτεί να εκτελέσετε έναν μάλλον περίεργο σαμανισμό στο Groovy. Για παράδειγμα, η δημιουργία ενός ευρετηρίου πρέπει να γίνει γράφοντας κώδικα στην κονσόλα 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()

Επίλογος

Κατά μία έννοια, το παραπάνω πείραμα είναι μια σύγκριση μεταξύ ζεστού και μαλακού. Εάν το καλοσκεφτείτε, ένα DBMS γραφήματος εκτελεί άλλες λειτουργίες για να αποκτήσει τα ίδια αποτελέσματα. Ωστόσο, ως μέρος των δοκιμών, πραγματοποίησα επίσης ένα πείραμα με ένα αίτημα όπως:

g.V().hasLabel('ZoneStep').has('id',0)
    .repeat(__.out().simplePath()).until(__.hasLabel('ZoneStep').has('id',1)).count().next()

που αντανακλά την απόσταση με τα πόδια. Ωστόσο, ακόμη και σε τέτοια δεδομένα, το γράφημα DBMS έδειξε αποτελέσματα που ξεπερνούσαν μερικά δευτερόλεπτα... Αυτό φυσικά οφείλεται στο γεγονός ότι υπήρχαν μονοπάτια όπως 0 -> X -> Y ... -> 1, το οποίο έλεγξε και η μηχανή γραφημάτων.

Ακόμη και για ένα ερώτημα όπως:

g.V().hasLabel('ZoneStep').has('id',0).out().has('id',1)).count().next()

Δεν μπόρεσα να λάβω μια παραγωγική απάντηση με χρόνο επεξεργασίας λιγότερο από ένα δευτερόλεπτο.

Το ηθικό δίδαγμα της ιστορίας είναι ότι μια όμορφη ιδέα και παραδειγματική μοντελοποίηση δεν οδηγούν στο επιθυμητό αποτέλεσμα, το οποίο αποδεικνύεται με πολύ υψηλότερη αποτελεσματικότητα χρησιμοποιώντας το παράδειγμα του ClickHouse. Η περίπτωση χρήσης που παρουσιάζεται σε αυτό το άρθρο είναι ένα ξεκάθαρο αντί-μοτίβο για τα DBMS γραφημάτων, αν και φαίνεται κατάλληλη για μοντελοποίηση στο παράδειγμά τους.

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο