9

I am evaluating the performance of Neo4j graph database with a simple benchmark for insert, update, delete and query. Using Neo4j OGM I see significantly slower execution times (about 2-4 times) compared to the direct access via Neo4j driver. For example, delete operation (see code below) is done in 500ms vs 1200ms for 10K nodes and 11K relations on my machine. I wonder why this happens, especially because the below code for deletion doesn't even use any node entity. I can imagine that OGM has some overhead but this seems to be too much. Anyone has an idea why it's slower?

Example node:

public abstract class AbstractBaseNode {

    @GraphId
    @Index(unique = true)
    private Long id;

    public Long getId() {
        return id;
    }
}

@NodeEntity
public class Company extends AbstractBaseNode {

    private String name;

    public Company(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Example code for delete via driver:

driver = GraphDatabase.driver( "bolt://localhost:7687", AuthTokens.basic( "neo4j", "secret" ) );
session = driver.session();

long start = System.nanoTime();

session.run("MATCH (n) DETACH DELETE n").list();

System.out.println("Deleted all nodes " + ((System.nanoTime() - start) / 1000) + "μs");

Example code for delete via OGM:

public org.neo4j.ogm.config.Configuration neo4jConfiguration() {
    org.neo4j.ogm.config.Configuration config =  new org.neo4j.ogm.config.Configuration();
    config.autoIndexConfiguration().setAutoIndex(AutoIndexMode.DUMP.getName());
    config.driverConfiguration()
            .setDriverClassName("org.neo4j.ogm.drivers.bolt.driver.BoltDriver")
            .setURI("bolt://neo4j:secret@localhost")
            .setConnectionPoolSize(10);

    return config;
}

sessionFactory = new SessionFactory(neo4jConfiguration(), "net.mypackage");
session = sessionFactory.openSession();

long start = System.nanoTime();

session.query("MATCH (n) DETACH DELETE n", Collections.emptyMap()).forEach(x -> {});

System.out.println("Deleted all nodes " + ((System.nanoTime() - start) / 1000) + "μs");
Steffen Harbich
  • 2,639
  • 2
  • 37
  • 71
  • For this particular query it should really be doing the same thing. I tried to reproduce this, but I don't see much difference between the two. What versions of ogm and neo4j-java-driver have you used? Do you have a proper benchmark that would replicate this that you could share? – František Hartman May 17 '17 at 07:26
  • Thanks so far. I will try to minimize the example and upload it. – Steffen Harbich May 17 '17 at 18:12
  • Under https://www.dropbox.com/s/uf6oqrn9to0ax1j/neo4j%20min.zip?dl=0 I uploaded a gradle project. There are two test, one for driver and one for OGM access to neo4j. You can execute both test classes several times to get average measurements. As a requirement Neo4j community needs to run under default settings. I couldn't reproduce the huge difference for delete operation but for the creation of the nodes. – Steffen Harbich May 18 '17 at 08:33
  • And I found out that, with my initial benchmark, neo4j java driver version 1.1.0 was a lot of faster than 1.3.0 but I cannot reproduce it with my uploaded example. – Steffen Harbich May 18 '17 at 08:36
  • @SteffenHarbich, I don't think your example might be counted as a [MCVE](https://stackoverflow.com/help/mcve). I don't see a major difference between delete using different drivers, and as for create the difference is obvious: in one case you directly create queries and in other case you make OGM driver to analyze objects graph (yes, of one object but the OGM driver can't know it beforehand) and create queries for you. If you change OGM test to use `session.query` to create records as well, results seems to be almost indistinguishable. – SergGr May 18 '17 at 21:37
  • So you mean 2-3 sec versus 8-9 sec node creation is OK for OGM overhead? I will try to reproduce the delete difference with more entities/relations. – Steffen Harbich May 19 '17 at 07:50

1 Answers1

2

I will start by pointing out your test samples are poor. When taking time sample, you want to stress the system so that it takes a fair amount of time. The tests should also test what your interested in (are you testing how fast you can create and drop connections? Max Cypher through put? Speed of single large transaction?) With tests that are barley a second, it is impossible to tell if difference in performance is the query call, or just startup overhead (despite the name, the session doesn't actually connect until you call query(...)).

As far as I can tell, both version perform about the same in a normal setup. The only thing I can think of that would affect this is if the OSGM was doing something to starve other processes of system resources.

UPDATE

UNWIND {rows} as row 
CREATE (n:Company) 
SET n=row.props 
RETURN row.nodeRef as ref, ID(n) as id, row.type as type with params {rows=[{nodeRef=-1206180304, type=node, props={name=company_1029}}]}

VS

CREATE (a:Company {name: {name}}) // X10,000

The key difference between the driver and the OGM is that the driver does exactly what you tell it to do, which is the most efficient way of doing things; and the OGM tries to manage the query logic for you (What to return, how to save things, what to try to save). And the OGM version is more reliable because it will automatically try to consolidate nodes to the database (if possible), and will only save things that have actually changed. Since your node class doesn't have a primary key to consolidate on, it will have to create everything. The OGM Cypher is more versatile, but it also requires more memory use/access. SET n.name="rawr" is 1 db hit per property. SET n={name:"rawr"} is 3 db hits though (about 1+2*#_of_props. {name:"rawr", id:2} is 5 db hits). That is why the OGM Cypher is slower. The OGM however has smart management though, so if you than edit one node and try to save it, the driver would have to either save all, or you would have to implement your own manager. The OGM will only save the updated one.

So in short, the OGM Cyphers are less efficient than what you would write using the driver, but the OGM has smart management built in that can make it faster than a blind driver implementation in real business logic situations (loading/editing large numbers of nodes). Of course, you can implement your own management with the driver to be faster, so it's a trade off of speed and development effort. The more speed you want, the more time you have to put into managing every tiny aspect (and the point of OGM is to plug it in and it just works).

Tezra
  • 8,463
  • 3
  • 31
  • 68
  • 1
    I added a warm up phase by looping the same test 3 times in a row, regarding only the last run, and increased the number of nodes. For delete their is now difference anymore as @SergGr pointed out in the comments of the question. But for creating nodes, the difference is still their and with 30 vs 80 seconds (driver vs OGM) which I think is too much for a simple node entity like the one in my example. The initial purpose of the benchmark was a simple performance comparison between a RDBMS and Neo4j where I observed significant differences between driver and OGM access to neo4j. – Steffen Harbich May 20 '17 at 08:22
  • @SteffenHarbich I would need to see your test procedures for the CREATE to know for sure what the extra time is being spent on. But if I had to guess (based on how you are creating in the DELETE tests), I'd say your driver and OGM code are not equivalent. My understanding is that \@GraphId \@Index(unique = true) together mean that you are creating a new property named id that is indexed, and set to the same value as the internal . This means that on commit, that additional index now has to be recompiled. (Indexes trade away write speed for more read speed) – Tezra May 22 '17 at 20:50
  • I would do a MATCH (n) RETURN n on the results of both write tests to double check the nodes created in both are equivalent. – Tezra May 22 '17 at 20:52
  • Thank you for your hints. I removed the annotation @Index on the id property but it didn't change anything. Written nodes are equal on both tests. If you want to see the minized tests you can follow the dropbox link in the question's comment. – Steffen Harbich May 23 '17 at 06:24
  • @SteffenHarbich I'm not sure exactly what the extra time is being spent on. One other thing you can do is while testing, run "CALL dbms.listQueries() YIELD queryId, query, parameters, elapsedTimeMillis, cpuTimeMillis, status" (http://neo4j.com/docs/operations-manual/3.2-alpha/monitoring/query-management/procedures/#query-management-term-administrator) in the web console. If you can capture the Cypher the OGM is using, you should be able to see what it is doing extra that requires more time. (Also do a test with an extra 0 if you can). – Tezra May 23 '17 at 13:15
  • I'd say the OGM either isn't using parameters (meaning a new query plan is needed on each save), or is trying to do more work per query. – Tezra May 23 '17 at 13:18
  • Unfortunately, I don't have enterprise edition, so listQueries isn't available. But the OGM logging shows this cypher: UNWIND {rows} as row CREATE (n:``Company``) SET n=row.props RETURN row.nodeRef as ref, ID(n) as id, row.type as type with params {rows=[{nodeRef=-1206180304, type=node, props={name=company_1029}}]} – Steffen Harbich May 24 '17 at 08:36
  • @SteffenHarbich I see where the time is being spent. The OGM Cypher needs about 2-3 times the db accesses as the driver version. I updated answer with more detailed explanation. – Tezra May 24 '17 at 12:27