← Back to Stack Overflow Dataset

Neo4j Implementation

Deployment and Query logic for Neo4j Community 5.26.0.

Schema Diagram and Graph Statistics

StackOverflow Neo4j Schema

An Interesting Discovery

When we import data to Neo4j, we discovered that there are some edge count discrepancies with other database platforms. Notably, the number of ACCEPTS edges on Neo4j is 11,755,050, which on other platforms are 11,755,280. The number of CREATED_BY edges on Neo4j is 71,196, but 109,349 on other platforms. The number of LAST_EDITED_BY edges on Neo4j is 22,832,774, but 22,880,943 on other platforms. All three discrepancies share the same underlying cause: Neo4j's MATCH clause requires both the source and destination nodes to already exist before an edge can be created. When either node is absent due to deleted users, deleted posts, or records filtered out during CSV cleaning Neo4j silently skips that edge. The other three engines are more permissive and either auto-create missing nodes or build edges independently of strict node existence checks.

Our solution is to keep Neo4j at its naturally loaded counts and document these as known discrepancies. Creating stub nodes to artificially match the other engines would inflate node counts and introduce property-less nodes that skew benchmarking results. The total difference of 86,552 edges out of 782,719,894 represents a 0.011% variance, which is negligible for benchmarking purposes. This is a meaningful finding; it highlights a fundamental behavioral difference in how graph databases handle incomplete datasets during bulk loading, with Neo4j enforcing strict referential integrity while the other engines are more permissive.

Node Statistics

Node Label Count
Comment 86,754,111
Vote 236,452,885
User 18,712,212
Post_question 23,020,127
Post_tag_wiki_excerpt 55,115
Tag 63,653
Badge 46,135,386
Post_answer 34,024,119
Post_tag_wiki 55,113

Edge Statistics

Relationship Count
(:Tag)-[:HAS_WIKI]->(:Post_tag_wiki) 44,959
(:Tag)-[:TAG_TO]->(:Post_question) 68,338,336
(:Post_question)-[:LINK_TO]->(:Post_question) 6,128,365
(:Comment)-[:ON_ANSWER]->(:Post_answer) 41,046,017
(:Tag)-[:HAS_WIKI_EXCERPT]->(:Post_tag_wiki_excerpt) 44,961
(:Comment)-[:ON_QUESTION]->(:Post_question) 45,700,093
(:Post_tag_wiki_excerpt)-[:LAST_EDITED_BY]->(:User) 54,582
(:Post_tag_wiki)-[:LAST_EDITED_BY]->(:User) 54,633
(:User)-[:EARNED]->(:Badge) 46,135,068
(:Comment)-[:COMMENTED_BY]->(:User) 85,362,385
(:Post_tag_wiki)-[:CREATED_BY]->(:User) 54,721
(:Post_tag_wiki_excerpt)-[:CREATED_BY]->(:User) 54,628
(:Post_answer)-[:ANSWERS_TO]->(:Post_question) 34,024,119
(:Post_answer)-[:ANSWERS_BY]->(:User) 33,661,702
(:Post_question)-[:OWNED_BY]->(:User) 22,562,260
(:Post_answer)-[:LAST_EDITED_BY]->(:User) 10,410,156
(:Post_question)-[:ACCEPTS]->(:Post_answer) 11,755,050
(:Vote)-[:VOTE_ON]->(:Post_answer) 125,908,676
(:Post_question)-[:LAST_EDITED_BY]->(:User) 12,361,577
(:Vote)-[:VOTE_ON]->(:Post_question) 79,414,870

Disk space: 201.08 GiB

Total node import time: 14,675.9s

Total edge import time: 49,918.1s

Data Loading

Create Indices

cypher
CREATE CONSTRAINT user_id_unique IF NOT EXISTS FOR (u:User) REQUIRE u.user_id IS UNIQUE;
CREATE CONSTRAINT post_question_id_unique IF NOT EXISTS FOR (pq:Post_question) REQUIRE pq.post_id IS UNIQUE;
CREATE CONSTRAINT post_answer_id_unique IF NOT EXISTS FOR (pa:Post_answer) REQUIRE pa.post_id IS UNIQUE;
CREATE CONSTRAINT comment_id_unique IF NOT EXISTS FOR (c:Comment) REQUIRE c.comment_id IS UNIQUE;
CREATE CONSTRAINT tag_id_unique IF NOT EXISTS FOR (t:Tag) REQUIRE t.tag_id IS UNIQUE;
CREATE CONSTRAINT badge_id_unique IF NOT EXISTS FOR (b:Badge) REQUIRE b.badge_id IS UNIQUE;
CREATE CONSTRAINT vote_id_unique IF NOT EXISTS FOR (v:Vote) REQUIRE v.vote_id IS UNIQUE;
CREATE CONSTRAINT post_history_id_unique IF NOT EXISTS FOR (ph:Post_history) REQUIRE ph.post_history_id IS UNIQUE;
CREATE CONSTRAINT post_tag_wiki_id_unique IF NOT EXISTS FOR (ptw:Post_tag_wiki) REQUIRE ptw.post_id IS UNIQUE;
CREATE CONSTRAINT post_tag_wiki_excerpt_id_unique IF NOT EXISTS FOR (ptwe:Post_tag_wiki_excerpt) REQUIRE ptwe.post_id IS UNIQUE;

Create User Nodes (785s)

First we will start with User nodes. The original users.csv file downloaded have some syntax problems that cause Neo4j to fail on loading. So we first used a python script to clean the data and save it as users_clean.csv. You can find this Python script file named clean_users_csv.py in the Download tab.

Then, run the following command to use the cleaner script.

bash
python clean_users_csv.py users.csv users_clean.csv --strip-backslashes

Now you can run the following cypher query to import User nodes.

cypher
CALL apoc.periodic.iterate(
    "LOAD CSV WITH HEADERS FROM 'file:///stackoverflow/users_clean.csv' AS row RETURN row",
    "
    MERGE (u:User {user_id: toInteger(row.id)})
    SET u.display_name      = row.display_name,
        u.about_me          = row.about_me,
        u.age               = toInteger(row.age),
        u.creation_date     = row.creation_date,
        u.last_access_date  = row.last_access_date,
        u.location          = row.location,
        u.reputation        = toInteger(row.reputation),
        u.up_votes          = toInteger(row.up_votes),
        u.down_votes        = toInteger(row.down_votes),
        u.views             = toInteger(row.views),
        u.profile_image_url = row.profile_image_url,
        u.website_url       = row.website_url
    ",
    {batchSize: 20000, parallel: false, retries: 2}
);

Create Badge Nodes (1090s)

cypher
CALL apoc.periodic.iterate(
    "LOAD CSV WITH HEADERS FROM 'file:///stackoverflow/badges.csv' AS row RETURN row",
    "
    MERGE (b:Badge {badge_id: toInteger(row.id)})
    SET b.name      = row.name,
        b.date      = row.date,
        b.user_id   = toInteger(row.user_id),
        b.class     = toInteger(row.class),
        b.tag_based = CASE WHEN row.tag_based = 'true' THEN true ELSE false END
    ",
    {batchSize: 20000, parallel: false, retries: 2}
);

Create Comment Nodes (2406.8s)

There is the same issue with comments.csv downloaded from the original dataset as users.csv. Therefore, we would need to clean up this file a little bit to prepare it for Neo4j import. Use the python script clean_comments_csv.py in the Download tab.

Then, use the following command to start cleaning the file.

bash
python clean_comments_csv.py comments.csv comments_clean.csv
cypher
CALL apoc.periodic.iterate(
    "LOAD CSV WITH HEADERS FROM 'file:///stackoverflow/comments.csv' AS row RETURN row",
    "
    MERGE (c:Comment {comment_id: toInteger(row.id)})
    SET c.text              = row.text,
        c.creation_date      = row.creation_date,
        c.post_id            = toInteger(row.post_id),
        c.user_id            = toInteger(row.user_id),
        c.user_display_name  = row.user_display_name,
        c.score             = toInteger(row.score)
    ",
    {batchSize: 20000, parallel: false, retries: 2}
);

Create Vote Nodes (7216.1s)

cypher
CALL apoc.periodic.iterate(
    "LOAD CSV WITH HEADERS FROM 'file:///stackoverflow/votes.csv' AS row RETURN row",
    "
        MERGE (v:Vote {vote_id: toInteger(row.id)})
        SET v.creation_date  = row.creation_date,
        v.post_id        = toInteger(row.post_id),
        v.vote_type_id   = toInteger(row.vote_type_id)
    ",
    {batchSize: 20000, parallel: false, retries: 2}
);

Create Post Answer Nodes (1124.1s)

There is the same issue with posts_answers.csv downloaded from the original dataset. Therefore, we would need to clean up this file a little bit to prepare it for Neo4j import. Use the python script clean_posts_csv.py in the Download tab.

Then, use the following command to start cleaning the file.

bash
python clean_posts_csv.py posts_answers.csv posts_answers_clean.csv
cypher
CALL apoc.periodic.iterate(
    "LOAD CSV WITH HEADERS FROM 'file:///stackoverflow/posts_answers_clean.csv' AS row RETURN row",
    "
        MERGE (pa:Post_answer {post_id: toInteger(row.id)})
        SET pa.title                    = row.title,
        pa.body                     = row.body,
        pa.accepted_answer_id       = toInteger(row.accepted_answer_id),
        pa.answer_count             = toInteger(row.answer_count),
        pa.comment_count            = toInteger(row.comment_count),
        pa.community_owned_date     = row.community_owned_date,
        pa.creation_date            = row.creation_date,
        pa.favorite_count           = toInteger(row.favorite_count),
        pa.last_activity_date       = row.last_activity_date,
        pa.last_edit_date           = row.last_edit_date,
        pa.last_editor_display_name = row.last_editor_display_name,
        pa.last_editor_user_id      = toInteger(row.last_editor_user_id),
        pa.owner_display_name       = row.owner_display_name,
        pa.owner_user_id            = toInteger(row.owner_user_id),
        pa.parent_id                = toInteger(row.parent_id),
        pa.post_type_id             = toInteger(row.post_type_id),
        pa.score                    = toInteger(row.score),
        pa.tags                     = row.tags,
        pa.view_count               = toInteger(row.view_count)
    ",
    {batchSize: 2000, parallel: true, concurrency: 2}
);

Create Post Question Nodes (2049s)

There is the same issue with posts_questions.csv downloaded from the original dataset. Therefore, we would need to clean up this file a little bit to prepare it for Neo4j import. Use the python script clean_posts_csv.py in the Download tab.

Then, use the following command to start cleaning the file.

bash
python clean_posts_csv.py posts_questions.csv posts_questions_clean.csv
cypher
CALL apoc.periodic.iterate(
    "LOAD CSV WITH HEADERS FROM 'file:///stackoverflow/posts_questions_clean.csv' AS row RETURN row",
    "
        MERGE (pq:Post_question {post_id: toInteger(row.id)})
        SET pq.title                    = row.title,
        pq.body                     = row.body,
        pq.accepted_answer_id       = toInteger(row.accepted_answer_id),
        pq.answer_count             = toInteger(row.answer_count),
        pq.comment_count            = toInteger(row.comment_count),
        pq.community_owned_date     = row.community_owned_date,
        pq.creation_date            = row.creation_date,
        pq.favorite_count           = toInteger(row.favorite_count),
        pq.last_activity_date       = row.last_activity_date,
        pq.last_edit_date           = row.last_edit_date,
        pq.last_editor_display_name = row.last_editor_display_name,
        pq.last_editor_user_id      = toInteger(row.last_editor_user_id),
        pq.owner_display_name       = row.owner_display_name,
        pq.owner_user_id            = toInteger(row.owner_user_id),
        pq.parent_id                = toInteger(row.parent_id),
        pq.post_type_id             = toInteger(row.post_type_id),
        pq.score                    = toInteger(row.score),
        pq.tags                     = row.tags,
        pq.view_count               = toInteger(row.view_count)
    ",
    {batchSize: 2000, parallel: true, concurrency: 2}
    );

Create Tag Nodes (1s)

cypher
CALL apoc.periodic.iterate(
    "LOAD CSV WITH HEADERS FROM 'file:///stackoverflow/tags.csv' AS row RETURN row",
    "
        MERGE (t:Tag {tag_id: toInteger(row.id)})
        SET t.tag_name        = row.tag_name,
        t.count           = toInteger(row.count),
        t.excerpt_post_id = toInteger(row.excerpt_post_id),
        t.wiki_post_id    = toInteger(row.wiki_post_id)
    ",
    {batchSize: 2000, parallel: true, concurrency: 2}
);

Create Post Tag Wiki Nodes (1.9s)

There is the same issue with posts_tag_wiki.csv downloaded from the original dataset. Therefore, we would need to clean up this file a little bit to prepare it for Neo4j import. Use the python script clean_posts_csv.py in the Download tab.

Then, use the following command to start cleaning the file.

bash
python clean_posts_csv.py posts_tag_wiki.csv posts_tag_wiki_clean.csv
cypher
CALL apoc.periodic.iterate(
    "LOAD CSV WITH HEADERS FROM 'file:///stackoverflow/posts_tag_wiki_clean.csv' AS row RETURN row",
    "
        MERGE (ptw:Post_tag_wiki {post_id: toInteger(row.id)})
        SET ptw.title                    = row.title,
        ptw.body                     = row.body,
        ptw.comment_count            = toInteger(row.comment_count),
        ptw.creation_date            = row.creation_date,
        ptw.last_activity_date       = row.last_activity_date,
        ptw.last_edit_date           = row.last_edit_date,
        ptw.last_editor_display_name = row.last_editor_display_name,
        ptw.last_editor_user_id      = toInteger(row.last_editor_user_id),
        ptw.owner_display_name       = row.owner_display_name,
        ptw.owner_user_id            = toInteger(row.owner_user_id),
        ptw.post_type_id             = toInteger(row.post_type_id),
        ptw.score                    = toInteger(row.score)
    ",
    {batchSize: 2000, parallel: true, concurrency: 2}
);

Create Post Tag Wiki Excerpt Nodes (2s)

cypher
CALL apoc.periodic.iterate(
    "LOAD CSV WITH HEADERS FROM 'file:///stackoverflow/posts_tag_wiki_excerpt.csv' AS row RETURN row",
    "
        MERGE (ptwe:Post_tag_wiki_excerpt {post_id: toInteger(row.id)})
        SET ptwe.title                    = row.title,
        ptwe.body                     = row.body,
        ptwe.comment_count            = toInteger(row.comment_count),
        ptwe.creation_date            = row.creation_date,
        ptwe.last_activity_date       = row.last_activity_date,
        ptwe.last_edit_date           = row.last_edit_date,
        ptwe.last_editor_display_name = row.last_editor_display_name,
        ptwe.last_editor_user_id      = toInteger(row.last_editor_user_id),
        ptwe.owner_display_name       = row.owner_display_name,
        ptwe.owner_user_id            = toInteger(row.owner_user_id),
        ptwe.post_type_id             = toInteger(row.post_type_id),
        ptwe.score                    = toInteger(row.score)
    ",
    {batchSize: 2000, parallel: true, concurrency: 2}
);

OWNED_BY Edges (1557.2s)

First we will create the OWNED_BY edges from Post_questions to Users.

cypher
CALL apoc.periodic.iterate(
    "
    MATCH (pq:Post_question)
    WHERE pq.owner_user_id IS NOT NULL
      AND pq.owner_user_id <> 0
    RETURN
        pq.post_id AS question_id,
        pq.owner_user_id AS owner_user_id
    ",
    "
    MATCH (pq:Post_question {post_id: question_id})
    MATCH (u:User {user_id: owner_user_id})
    MERGE (pq)-[:OWNED_BY]->(u)
    ",
    {
        batchSize: 20000,
        parallel: false,
        retries: 2
    }
);

ANSWERS_BY Edges (1986.5s)

Next, we will create ANSWERS_BY edges from Post_answer to Users.

cypher
CALL apoc.periodic.iterate(
    "
    MATCH (pa:Post_answer)
    WHERE pa.owner_user_id IS NOT NULL
      AND pa.owner_user_id <> 0
    RETURN
        pa.post_id AS answer_id,
        pa.owner_user_id AS owner_user_id
    ",
    "
    MATCH (pa:Post_answer {post_id: answer_id})
    MATCH (u:User {user_id: owner_user_id})
    MERGE (pa)-[:ANSWERS_BY]->(u)
    ",
    {
        batchSize: 20000,
        parallel: false,
        retries: 2
    }
);

CREATED_BY Edges (6.4s in total for two scripts)

Next, we will create the CREATED_BY edges from Post_tag_wiki to Users. This import took 3.2 seconds.

cypher
CALL apoc.periodic.iterate(
    "
    MATCH (ptw:Post_tag_wiki)
    WHERE ptw.owner_user_id IS NOT NULL
      AND ptw.owner_user_id <> 0
    RETURN
        ptw.post_id AS wiki_post_id,
        ptw.owner_user_id AS owner_user_id
    ",
    "
    MATCH (ptw:Post_tag_wiki {post_id: wiki_post_id})
    MATCH (u:User {user_id: owner_user_id})
    MERGE (ptw)-[:CREATED_BY]->(u)
    ",
    {
        batchSize: 10000,
        parallel: false,
        retries: 2
    }
);

Next, we will create the CREATED_BY edges from Post_tag_wiki_excerpt to Users. This import took 3.2 seconds.

cypher
CALL apoc.periodic.iterate(
    "
    MATCH (ptwe:Post_tag_wiki_excerpt)
    WHERE ptwe.owner_user_id IS NOT NULL
      AND ptwe.owner_user_id <> 0
    RETURN
        ptwe.post_id AS excerpt_post_id,
        ptwe.owner_user_id AS owner_user_id
    ",
    "
    MATCH (ptwe:Post_tag_wiki_excerpt {post_id: excerpt_post_id})
    MATCH (u:User {user_id: owner_user_id})
    MERGE (ptwe)-[:CREATED_BY]->(u)
    ",
    {
        batchSize: 10000,
        parallel: false,
        retries: 2
    }
);

LAST_EDITED_BY Edges (1475.8s in total for four scripts)

Next, we will create the LAST_EDITED_BY edges from Post_question to Users. This import took 800.2 seconds.

cypher
CALL apoc.periodic.iterate(
    "
    MATCH (pq:Post_question)
    WHERE pq.last_editor_user_id IS NOT NULL
      AND pq.last_editor_user_id <> 0
    RETURN
        pq.post_id AS question_id,
        pq.last_editor_user_id AS editor_user_id
    ",
    "
    MATCH (pq:Post_question {post_id: question_id})
    MATCH (u:User {user_id: editor_user_id})
    MERGE (pq)-[:LAST_EDITED_BY]->(u)
    ",
    {
        batchSize: 20000,
        parallel: false,
        retries: 2
    }
);

Next, we will create the LAST_EDITED_BY edges from Post_answer to Users. This import took 668.6 seconds.

cypher
CALL apoc.periodic.iterate(
    "
    MATCH (pa:Post_answer)
    WHERE pa.last_editor_user_id IS NOT NULL
      AND pa.last_editor_user_id <> 0
    RETURN
        pa.post_id AS answer_id,
        pa.last_editor_user_id AS editor_user_id
    ",
    "
    MATCH (pa:Post_answer {post_id: answer_id})
    MATCH (u:User {user_id: editor_user_id})
    MERGE (pa)-[:LAST_EDITED_BY]->(u)
    ",
    {
        batchSize: 20000,
        parallel: false,
        retries: 2
    }
);

Next, we will create the LAST_EDITED_BY edges from Post_tag_wiki to Users. This import took 3.8 seconds.

cypher
CALL apoc.periodic.iterate(
    "
    MATCH (ptw:Post_tag_wiki)
    WHERE ptw.last_editor_user_id IS NOT NULL
      AND ptw.last_editor_user_id <> 0
    RETURN
        ptw.post_id AS wiki_post_id,
        ptw.last_editor_user_id AS editor_user_id
    ",
    "
    MATCH (ptw:Post_tag_wiki {post_id: wiki_post_id})
    MATCH (u:User {user_id: editor_user_id})
    MERGE (ptw)-[:LAST_EDITED_BY]->(u)
    ",
    {
        batchSize: 10000,
        parallel: false,
        retries: 2
    }
);

Next, we will create the LAST_EDITED_BY edges from Post_tag_wiki_excerpt to Users. This import took 3.2 seconds.

cypher
CALL apoc.periodic.iterate(
    "
    MATCH (ptwe:Post_tag_wiki_excerpt)
    WHERE ptwe.last_editor_user_id IS NOT NULL
      AND ptwe.last_editor_user_id <> 0
    RETURN
        ptwe.post_id AS excerpt_post_id,
        ptwe.last_editor_user_id AS editor_user_id
    ",
    "
    MATCH (ptwe:Post_tag_wiki_excerpt {post_id: excerpt_post_id})
    MATCH (u:User {user_id: editor_user_id})
    MERGE (ptwe)-[:LAST_EDITED_BY]->(u)
    ",
    {
        batchSize: 10000,
        parallel: false,
        retries: 2
    }
);

EARNED Edges (3598.2s)

Next, we will create the EARNED edges from Users to Badges.

cypher
CALL apoc.periodic.iterate(
    "
    MATCH (b:Badge)
    WHERE b.user_id IS NOT NULL
      AND b.user_id <> 0
    RETURN
        b.badge_id AS badge_id,
        b.user_id AS user_id
    ",
    "
    MATCH (b:Badge {badge_id: badge_id})
    MATCH (u:User {user_id: user_id})
    MERGE (u)-[:EARNED]->(b)
    ",
    {
        batchSize: 20000,
        parallel: false,
        retries: 2
    }
);

ANSWERS_TO Edges (2204.4s)

Next, we will create the ANSWERS_TO edges from Post_answer to Post_question.

cypher
CALL apoc.periodic.iterate(
    "
    MATCH (pa:Post_answer)
    WHERE pa.parent_id IS NOT NULL
      AND pa.parent_id <> 0
    RETURN
        pa.post_id AS answer_id,
        pa.parent_id AS question_id
    ",
    "
    MATCH (pa:Post_answer {post_id: answer_id})
    MATCH (pq:Post_question {post_id: question_id})
    MERGE (pa)-[:ANSWERS_TO]->(pq)
    ",
    {
        batchSize: 20000,
        parallel: false,
        retries: 2
    }
);

ACCEPTS Edges (675.1s)

Next, we will create the ACCEPTS edges from Post_question to Post_answer.

cypher
CALL apoc.periodic.iterate(
    "
    MATCH (pq:Post_question)
    WHERE pq.accepted_answer_id IS NOT NULL
      AND pq.accepted_answer_id <> 0
    RETURN
        pq.post_id AS question_id,
        pq.accepted_answer_id AS answer_id
    ",
    "
    MATCH (pq:Post_question {post_id: question_id})
    MATCH (pa:Post_answer {post_id: answer_id})
    MERGE (pq)-[:ACCEPTS]->(pa)
    ",
    {
        batchSize: 20000,
        parallel: false,
        retries: 2
    }
);

COMMENTED_BY Edges (5530.3s)

Next, we will create the COMMENTED_BY edges from Comments to Users.

cypher
CALL apoc.periodic.iterate(
    "
    MATCH (c:Comment)
    WHERE c.user_id IS NOT NULL
      AND c.user_id <> 0
    RETURN
        c.comment_id AS comment_id,
        c.user_id AS user_id
    ",
    "
    MATCH (c:Comment {comment_id: comment_id})
    MATCH (u:User {user_id: user_id})
    MERGE (c)-[:COMMENTED_BY]->(u)
    ",
    {
        batchSize: 10000,
        parallel: false,
        retries: 2
    }
);

ON_QUESTION Edges (4188.8s)

Next, we will create the ON_QUESTION edges from Comments to Post_question.

cypher
CALL apoc.periodic.iterate(
    "
    MATCH (c:Comment)
    WHERE c.post_id IS NOT NULL
      AND c.post_id <> 0
    RETURN
        c.comment_id AS comment_id,
        c.post_id AS question_id
    ",
    "
    MATCH (c:Comment {comment_id: comment_id})
    MATCH (pq:Post_question {post_id: question_id})
    MERGE (c)-[:ON_QUESTION]->(pq)
    ",
    {
        batchSize: 20000,
        parallel: false,
        retries: 2
    }
);

ON_ANSWER Edges (3998.1s)

Next, we will create the ON_ANSWER edges from Comments to Post_answer.

cypher
CALL apoc.periodic.iterate(
    "
    MATCH (c:Comment)
    WHERE c.post_id IS NOT NULL
      AND c.post_id <> 0
    RETURN
        c.comment_id AS comment_id,
        c.post_id AS answer_id
    ",
    "
    MATCH (c:Comment {comment_id: comment_id})
    MATCH (pa:Post_answer {post_id: answer_id})
    MERGE (c)-[:ON_ANSWER]->(pa)
    ",
    {
        batchSize: 20000,
        parallel: false,
        retries: 2
    }
);

VOTE_ON Edges (20921.2s)

Next, we will create the VOTE_ON edges from Votes to Post_question and Post_answer.

cypher
CALL apoc.periodic.iterate(
    "
    MATCH (v:Vote)
    WHERE v.post_id IS NOT NULL
      AND v.post_id <> 0
    RETURN
        v.vote_id AS vote_id,
        v.post_id AS post_id
    ",
    "
    MATCH (v:Vote {vote_id: vote_id})

    OPTIONAL MATCH (pq:Post_question {post_id: post_id})
    OPTIONAL MATCH (pa:Post_answer {post_id: post_id})

    FOREACH (_ IN CASE WHEN pq IS NULL THEN [] ELSE [1] END |
        MERGE (v)-[:VOTE_ON]->(pq)
    )

    FOREACH (_ IN CASE WHEN pa IS NULL THEN [] ELSE [1] END |
        MERGE (v)-[:VOTE_ON]->(pa)
    )
    ",
    {
        batchSize: 20000,
        parallel: false,
        retries: 2
    }
);

TAG_TO Edges (3307s)

Next, we will create the TAG_TO edges from Tag to Post_question.

cypher
CALL apoc.periodic.iterate(
    "
    LOAD CSV WITH HEADERS
    FROM 'file:///stackoverflow/edge_tag_to_question.csv' AS row

    WITH
        toIntegerOrNull(row.src) AS tag_id,
        toIntegerOrNull(row.dst) AS question_id

    WHERE tag_id IS NOT NULL
      AND question_id IS NOT NULL

    RETURN tag_id, question_id
    ",
    "
    MATCH (t:Tag {tag_id: tag_id})
    MATCH (pq:Post_question {post_id: question_id})
    MERGE (t)-[:TAG_TO]->(pq)
    ",
    {
        batchSize: 20000,
        parallel: false,
        retries: 2
    }
);

HAS_WIKI Edges (3.3s)

Next, we will create the HAS_WIKI edges from Tag to Post_tag_wiki.

cypher
CALL apoc.periodic.iterate(
    "
    MATCH (t:Tag)
    WHERE t.wiki_post_id IS NOT NULL
      AND t.wiki_post_id <> 0
    RETURN
        t.tag_id AS tag_id,
        t.wiki_post_id AS wiki_post_id
    ",
    "
    MATCH (t:Tag {tag_id: tag_id})
    MATCH (ptw:Post_tag_wiki {post_id: wiki_post_id})
    MERGE (t)-[:HAS_WIKI]->(ptw)
    ",
    {
        batchSize: 10000,
        parallel: false,
        retries: 2
    }
);

HAS_WIKI_EXCERPT Edges (2.9s)

Next, we will create the HAS_WIKI_EXCERPT edges from Tag to Post_tag_wiki_excerpt.

cypher
CALL apoc.periodic.iterate(
    "
    MATCH (t:Tag)
    WHERE t.excerpt_post_id IS NOT NULL
      AND t.excerpt_post_id <> 0
    RETURN
        t.tag_id AS tag_id,
        t.excerpt_post_id AS excerpt_post_id
    ",
    "
    MATCH (t:Tag {tag_id: tag_id})
    MATCH (ptwe:Post_tag_wiki_excerpt {
        post_id: excerpt_post_id
    })
    MERGE (t)-[:HAS_WIKI_EXCERPT]->(ptwe)
    ",
    {
        batchSize: 10000,
        parallel: false,
        retries: 2
    }
);

LINK_TO Edges (462.8s)

Next, we will create the LINK_TO edges from Post_question to Post_question.

cypher
CALL apoc.periodic.iterate(
    "
    LOAD CSV WITH HEADERS
    FROM 'file:///stackoverflow/posts_links.csv' AS row

    WITH
        toIntegerOrNull(row.id) AS post_link_id,
        toIntegerOrNull(row.post_id) AS source_post_id,
        toIntegerOrNull(row.related_post_id) AS target_post_id,
        toIntegerOrNull(row.link_type_id) AS link_type_id,
        row.creation_date AS creation_date

    WHERE post_link_id IS NOT NULL
      AND source_post_id IS NOT NULL
      AND target_post_id IS NOT NULL
      AND source_post_id <> 0
      AND target_post_id <> 0

    RETURN post_link_id, source_post_id, target_post_id,
           link_type_id, creation_date
    ",
    "
    MATCH (pq1:Post_question {post_id: source_post_id})
    MATCH (pq2:Post_question {post_id: target_post_id})

    MERGE (pq1)-[r:LINK_TO {post_link_id: post_link_id}]->(pq2)
    SET r.link_type_id = link_type_id,
        r.creation_date = creation_date
    ",
    {
        batchSize: 20000,
        parallel: false,
        retries: 2
    }
);

Queries

Select a query below to view its logic and description.

1. Neighborhood
2. Path
3. Pattern
4. Aggregation
5. Analytics
6. Hybrid
7. Others

Q1. Comments Written by a User

Subcategory: T1.1 One-Hop Neighbors

This query lists the comments directly connected to a selected user through incoming Commented_by relationships.

cypher
MATCH (u:User {user_id: $userId})<-[:Commented_by]-(c:Comment)
RETURN c.comment_id AS commentId, c.text AS text, c.score AS score, c.creation_date AS creationDate
ORDER BY score DESC, commentId
LIMIT $limit;

Equivalent query in GQL standard:

gql
MATCH (u:User {user_id: $userId})<-[:Commented_by]-(c:Comment)
RETURN c.comment_id AS commentId, c.text AS text, c.score AS score, c.creation_date AS creationDate
ORDER BY score DESC, commentId
LIMIT $limit;

Q2. Tags within Two Hops of a User

Subcategory: T1.2 k-Hop Neighborhood

This query follows a user to their questions and then to the tags attached to those questions, returning distinct tags within two hops.

cypher
MATCH (u:User {user_id: $userId})<-[:Owned_by]-(q:Post_question)<-[:Tag_to]-(t:Tag)
RETURN DISTINCT t.tag_id AS tagId, t.tag_name AS tagName, t.count AS globalQuestionCount
ORDER BY globalQuestionCount DESC
LIMIT $limit;

Equivalent query in GQL standard:

gql
MATCH (u:User {user_id: $userId})<-[:Owned_by]-(q:Post_question)<-[:Tag_to]-(t:Tag)
RETURN DISTINCT t.tag_id AS tagId, t.tag_name AS tagName, t.count AS globalQuestionCount
ORDER BY globalQuestionCount DESC
LIMIT $limit;

Q3. Answerers Exactly Three Hops from a User

Subcategory: T1.3 Exact kth-Hop Neighbors

This query finds users exactly three relationships away from a selected question owner through question-answer interactions.

cypher
MATCH (owner:User {user_id: $userId})<-[:Owned_by]-(q:Post_question)<-[:Answers_to]-(a:Post_answer)-[:Answers_by]->(answerer:User)
WHERE answerer.user_id <> owner.user_id
RETURN DISTINCT answerer.user_id AS answererId, answerer.display_name AS answererName
ORDER BY answererId
LIMIT $limit;

Equivalent query in GQL standard:

gql
MATCH (owner:User {user_id: $userId})<-[:Owned_by]-(q:Post_question)<-[:Answers_to]-(a:Post_answer)-[:Answers_by]->(answerer:User)
WHERE answerer.user_id <> owner.user_id
RETURN DISTINCT answerer.user_id AS answererId, answerer.display_name AS answererName
ORDER BY answererId
LIMIT $limit;

Q4. High-Score Answers by a User

Subcategory: T1.4 Neighborhood with Property Filter

This query filters a user's one-hop answer neighborhood to answers whose score is at or above a supplied threshold.

cypher
MATCH (u:User {user_id: $userId})<-[:Answers_by]-(a:Post_answer)
WHERE a.score >= $minScore
RETURN a.post_id AS answerId, a.score AS score, a.creation_date AS creationDate
ORDER BY score DESC, answerId
LIMIT $limit;

Equivalent query in GQL standard:

gql
MATCH (u:User {user_id: $userId})<-[:Answers_by]-(a:Post_answer)
WHERE a.score >= $minScore
RETURN a.post_id AS answerId, a.score AS score, a.creation_date AS creationDate
ORDER BY score DESC, answerId
LIMIT $limit;

Q5. Questions with Most Views But No Accepted Answer

Subcategory: T2.1 Node Filtering (Single Node Paths)

This query finds highly viewed questions that do not have an accepted answer recorded, returning each question's ID, view count, and answer count ordered by views.

cypher
MATCH (q:Post_question)
WHERE (q.accepted_answer_id IS NULL
    OR q.accepted_answer_id = ''
    OR q.accepted_answer_id = '0'
    OR q.accepted_answer_id = 'None')
RETURN q.id                                  AS question_id,
       toInteger(q.view_count)                AS view_count,
       toInteger(q.answer_count)              AS answer_count
ORDER BY toInteger(q.view_count) DESC
LIMIT 200;

Equivalent query in GQL standard:

gql
MATCH (q:Post_question)
WHERE (q.accepted_answer_id IS NULL
    OR q.accepted_answer_id = ''
    OR q.accepted_answer_id = '0'
    OR q.accepted_answer_id = 'None')
RETURN q.id AS question_id,
    toInteger(q.view_count) AS view_count,
    toInteger(q.answer_count) AS answer_count
ORDER BY toInteger(q.view_count) DESC
LIMIT 200;

Q6. Vote Records by Type on Posts

Subcategory: T2.2 Edge Filtering

The StackOverflow Neo4j import represents votes as Vote nodes connected by Vote_on relationships, so this query filters those edge records by vote type and returns their post endpoints.

cypher
MATCH (v:Vote)-[:Vote_on]->(p)
WHERE v.vote_type_id = $voteTypeId
RETURN v.vote_id AS voteId, labels(p) AS postLabels,
       coalesce(p.post_id, p.comment_id) AS targetId,
       v.creation_date AS voteDate
ORDER BY voteDate DESC
LIMIT $limit;

Equivalent query in GQL standard:

gql
MATCH (v:Vote)-[:Vote_on]->(p)
WHERE v.vote_type_id = $voteTypeId
RETURN v.vote_id AS voteId, labels(p) AS postLabels,
       coalesce(p.post_id, p.comment_id) AS targetId,
       v.creation_date AS voteDate
ORDER BY voteDate DESC
LIMIT $limit;

Q7. Reachability between Linked Questions

Subcategory: T2.3 Reachability between Two Nodes

This query tests whether one question can reach another through up to four Link_to hops.

cypher
MATCH p = (:Post_question {post_id: $sourceQuestionId})-[:Link_to*1..4]->(:Post_question {post_id: $targetQuestionId})
RETURN count(p) > 0 AS reachable
LIMIT 1;

Equivalent query in GQL standard:

gql
MATCH p = ANY (:Post_question {post_id: $sourceQuestionId})-[:Link_to]->{1,4}(:Post_question {post_id: $targetQuestionId})
RETURN true AS reachable;

Q8. Shortest Linked-Question Path

Subcategory: T2.4 Shortest Path

This query finds the shortest directed path between two questions over the question-link graph.

cypher
MATCH p = shortestPath((src:Post_question {post_id: $sourceQuestionId})-[:Link_to*1..6]->(dst:Post_question {post_id: $targetQuestionId}))
RETURN [n IN nodes(p) | n.post_id] AS questionPath, length(p) AS hopCount;

Equivalent query in GQL standard:

gql
MATCH p = SHORTEST (:Post_question {post_id: $sourceQuestionId})-[:Link_to]->{1,6}(:Post_question {post_id: $targetQuestionId})
RETURN p;

Q9. Simple Linked-Question Paths up to Length Three

Subcategory: T2.5 All Simple Paths up to Length k

This query enumerates simple directed paths between two questions using up to three Link_to hops.

cypher
MATCH p = (src:Post_question {post_id: $sourceQuestionId})-[:Link_to*1..3]->(dst:Post_question {post_id: $targetQuestionId})
WHERE all(n IN nodes(p) WHERE single(m IN nodes(p) WHERE m = n))
RETURN [n IN nodes(p) | n.post_id] AS questionPath, length(p) AS hopCount
ORDER BY hopCount
LIMIT $limit;

Equivalent query in GQL standard:

gql
MATCH p = ACYCLIC (:Post_question {post_id: $sourceQuestionId})-[:Link_to]->{1,3}(:Post_question {post_id: $targetQuestionId})
RETURN p
LIMIT $limit;

Q10. Top Weighted Linked-Question Paths

Subcategory: T2.6 top-K Weighted Paths

This query ranks bounded linked-question paths by the cumulative score of the questions along each path.

cypher
MATCH p = (src:Post_question {post_id: $sourceQuestionId})-[:Link_to*1..3]->(dst:Post_question {post_id: $targetQuestionId})
WHERE all(n IN nodes(p) WHERE single(m IN nodes(p) WHERE m = n))
WITH p, reduce(pathScore = 0, n IN nodes(p) | pathScore + coalesce(n.score, 0)) AS totalScore
RETURN [n IN nodes(p) | n.post_id] AS questionPath, length(p) AS hopCount, totalScore
ORDER BY totalScore DESC, hopCount ASC
LIMIT $limit;

Equivalent query in GQL standard:

gql
MATCH p = ACYCLIC (:Post_question {post_id: $sourceQuestionId})-[:Link_to]->{1,3}(:Post_question {post_id: $targetQuestionId})
RETURN p
ORDER BY length(p) ASC
LIMIT $limit;

Q11. Linked-Question Triangles

Subcategory: T3.1 Triangle Detection

This query finds directed triangles in the question-link graph.

cypher
MATCH (q1:Post_question)-[:Link_to]->(q2:Post_question)-[:Link_to]->(q3:Post_question)-[:Link_to]->(q1)
WHERE q1.post_id < q2.post_id AND q2.post_id < q3.post_id
RETURN q1.post_id AS question1, q2.post_id AS question2, q3.post_id AS question3
LIMIT $limit;

Equivalent query in GQL standard:

gql
MATCH (q1:Post_question)-[:Link_to]->(q2:Post_question)-[:Link_to]->(q3:Post_question)-[:Link_to]->(q1)
WHERE q1.post_id < q2.post_id AND q2.post_id < q3.post_id
RETURN q1.post_id AS question1, q2.post_id AS question2, q3.post_id AS question3
LIMIT $limit;

Q12. Four-Question Link Cycles

Subcategory: T3.2 Square Detection

This query finds four-question cycles formed by directed Link_to relationships.

cypher
MATCH (q1:Post_question)-[:Link_to]->(q2:Post_question)-[:Link_to]->(q3:Post_question)-[:Link_to]->(q4:Post_question)-[:Link_to]->(q1)
WHERE q1.post_id < q2.post_id AND q1.post_id < q3.post_id AND q1.post_id < q4.post_id
  AND q2 <> q4
RETURN q1.post_id AS question1, q2.post_id AS question2, q3.post_id AS question3, q4.post_id AS question4
LIMIT $limit;

Equivalent query in GQL standard:

gql
MATCH (q1:Post_question)-[:Link_to]->(q2:Post_question)-[:Link_to]->(q3:Post_question)-[:Link_to]->(q4:Post_question)-[:Link_to]->(q1)
WHERE q1.post_id < q2.post_id AND q1.post_id < q3.post_id AND q1.post_id < q4.post_id
  AND q2 <> q4
RETURN q1.post_id AS question1, q2.post_id AS question2, q3.post_id AS question3, q4.post_id AS question4
LIMIT $limit;

Q13. Four-Question Link Cliques

Subcategory: T3.4 4-Clique

This query finds groups of four questions where every pair is connected by at least one Link_to relationship, treating link direction as undirected for clique membership.

cypher
MATCH (q1:Post_question)-[:Link_to]-(q2:Post_question),
      (q1)-[:Link_to]-(q3:Post_question),
      (q1)-[:Link_to]-(q4:Post_question),
      (q2)-[:Link_to]-(q3),
      (q2)-[:Link_to]-(q4),
      (q3)-[:Link_to]-(q4)
WHERE q1.post_id < q2.post_id AND q2.post_id < q3.post_id AND q3.post_id < q4.post_id
RETURN q1.post_id AS question1, q2.post_id AS question2, q3.post_id AS question3, q4.post_id AS question4
LIMIT $limit;

Equivalent query in GQL standard:

gql
MATCH (q1:Post_question)-[:Link_to]-(q2:Post_question),
      (q1)-[:Link_to]-(q3:Post_question),
      (q1)-[:Link_to]-(q4:Post_question),
      (q2)-[:Link_to]-(q3),
      (q2)-[:Link_to]-(q4),
      (q3)-[:Link_to]-(q4)
WHERE q1.post_id < q2.post_id AND q2.post_id < q3.post_id AND q3.post_id < q4.post_id
RETURN q1.post_id AS question1, q2.post_id AS question2, q3.post_id AS question3, q4.post_id AS question4
LIMIT $limit;

Q14. Question-Answer-Comment Line Pattern

Subcategory: T3.6 Line Pattern

This query follows a longer line from a question owner to a question, an answer, a comment on that answer, and the comment author.

cypher
MATCH (questioner:User)<-[:Owned_by]-(q:Post_question)<-[:Answers_to]-(a:Post_answer)<-[:On_answer]-(c:Comment)-[:Commented_by]->(commenter:User)
WHERE questioner.user_id <> commenter.user_id
RETURN questioner.user_id AS questionerId, q.post_id AS questionId, a.post_id AS answerId,
       c.comment_id AS commentId, commenter.user_id AS commenterId
LIMIT $limit;

Equivalent query in GQL standard:

gql
MATCH (questioner:User)<-[:Owned_by]-(q:Post_question)<-[:Answers_to]-(a:Post_answer)<-[:On_answer]-(c:Comment)-[:Commented_by]->(commenter:User)
WHERE questioner.user_id <> commenter.user_id
RETURN questioner.user_id AS questionerId, q.post_id AS questionId, a.post_id AS answerId,
       c.comment_id AS commentId, commenter.user_id AS commenterId
LIMIT $limit;

Q15. Question Answer-Degree Summary

Subcategory: T4.1 Ungrouped Node Degree Statistics by Label

This query computes answer-degree statistics across all question nodes by counting how many Post_answer nodes connect to each Post_question through Answers_to.

cypher
MATCH (q:Post_question)
OPTIONAL MATCH (q)<-[:Answers_to]-(a:Post_answer)
WITH q, count(DISTINCT a) AS answerDegree
RETURN count(q) AS questionCount,
       avg(answerDegree) AS avgAnswerDegree,
       min(answerDegree) AS minAnswerDegree,
       max(answerDegree) AS maxAnswerDegree;

Equivalent query in GQL standard:

gql
MATCH (q:Post_question)
OPTIONAL MATCH (q)<-[:Answers_to]-(a:Post_answer)
WITH q, count(DISTINCT a) AS answerDegree
RETURN count(q) AS questionCount,
       avg(answerDegree) AS avgAnswerDegree,
       min(answerDegree) AS minAnswerDegree,
       max(answerDegree) AS maxAnswerDegree;

Q16. Most Linked Questions

Subcategory: T4.2 Grouped Node Degree Statistics by Label

This query counts incoming Link_to relationships for each question and returns the questions that are referenced most often by other questions.

cypher
MATCH (src:Post_question)-[:Link_to]->(target:Post_question)
WITH target.id   AS question_id,
     COUNT(src)  AS link_count
WHERE link_count > 0
RETURN question_id, link_count
ORDER BY link_count DESC
LIMIT 20;

Equivalent query in GQL standard:

gql
MATCH (src:Post_question)-[:Link_to]->(target:Post_question)
WITH target.id AS question_id,
     count(src) AS link_count
WHERE link_count > 0
RETURN question_id, link_count
ORDER BY link_count DESC
LIMIT 20;

Q17. Users Who Comment Most

Subcategory: T4.2 Grouped Node Degree Statistics by Label

This query counts comments authored by each user and returns the most active commenters by total outgoing comment activity.

cypher
MATCH (c:Comment)-[:Commented_by]->(u:User)
WITH u.id AS user_id,
     u.display_name AS display_name,
     COUNT(c) AS total_comments
WHERE total_comments > 0
RETURN user_id, display_name, total_comments
ORDER BY total_comments DESC
LIMIT 20;

Equivalent query in GQL standard:

gql
MATCH (c:Comment)-[:Commented_by]->(u:User)
WITH u.id AS user_id,
     u.display_name AS display_name,
     count(c) AS total_comments
WHERE total_comments > 0
RETURN user_id, display_name, total_comments
ORDER BY total_comments DESC
LIMIT 20;

Q18. Tag-Level Question Degree Summary

Subcategory: T4.2 Grouped Node Degree Statistics by Label

This query groups questions by tag and summarizes how many questions and answers are associated with each tag.

cypher
MATCH (t:Tag)-[:Tag_to]->(q:Post_question)
OPTIONAL MATCH (q)<-[:Answers_to]-(a:Post_answer)
RETURN t.tag_name AS tagName,
       count(DISTINCT q) AS questionCount,
       count(a) AS answerCount,
       avg(coalesce(q.score, 0)) AS avgQuestionScore
ORDER BY questionCount DESC
LIMIT $limit;

Equivalent query in GQL standard:

gql
MATCH (t:Tag)-[:Tag_to]->(q:Post_question)
OPTIONAL MATCH (q)<-[:Answers_to]-(a:Post_answer)
RETURN t.tag_name AS tagName,
       count(DISTINCT q) AS questionCount,
       count(a) AS answerCount,
       avg(coalesce(q.score, 0)) AS avgQuestionScore
ORDER BY questionCount DESC
LIMIT $limit;

Q19. Vote Counts by Type and Target Label

Subcategory: T4.4 Grouped Edge Count and Property Statistics

The import stores vote facts as Vote nodes, so this query groups vote-edge records by vote type and target node label.

cypher
MATCH (v:Vote)-[:Vote_on]->(target)
UNWIND labels(target) AS targetLabel
RETURN v.vote_type_id AS voteTypeId,
       targetLabel,
       count(v) AS voteCount,
       min(v.creation_date) AS firstVoteDate,
       max(v.creation_date) AS lastVoteDate
ORDER BY voteCount DESC
LIMIT $limit;

Equivalent query in GQL standard:

gql
MATCH (v:Vote)-[:Vote_on]->(target)
UNWIND labels(target) AS targetLabel
RETURN v.vote_type_id AS voteTypeId,
       targetLabel,
       count(v) AS voteCount,
       min(v.creation_date) AS firstVoteDate,
       max(v.creation_date) AS lastVoteDate
ORDER BY voteCount DESC
LIMIT $limit;

Q20. Linked-Question Path Counts by Length

Subcategory: T4.5 Path Counts by Length

This query counts bounded question-link paths from a selected question and groups them by path length.

cypher
MATCH p = (:Post_question {post_id: $questionId})-[:Link_to*1..3]->(target:Post_question)
RETURN length(p) AS pathLength,
       count(p) AS pathCount,
       count(DISTINCT target) AS distinctTargetCount
ORDER BY pathLength;

Equivalent query in GQL standard:

gql
MATCH p = (:Post_question {post_id: $questionId})-[:Link_to]->{1,3}(target:Post_question)
RETURN length(p) AS pathLength,
       count(p) AS pathCount,
       count(DISTINCT target) AS distinctTargetCount
ORDER BY pathLength;

Q21. Linked-Question Path Counts by Length and Answer Bucket

Subcategory: T4.6 Grouped Path Counts by Length and Endpoint Type

This query groups linked-question paths by path length and by whether the endpoint question has no answers, one answer, or multiple answers.

cypher
MATCH p = (:Post_question {post_id: $questionId})-[:Link_to*1..3]->(target:Post_question)
WITH length(p) AS pathLength,
     CASE
       WHEN coalesce(target.answer_count, 0) = 0 THEN 'unanswered'
       WHEN coalesce(target.answer_count, 0) = 1 THEN 'single-answer'
       ELSE 'multi-answer'
     END AS answerBucket,
     p,
     target
RETURN pathLength, answerBucket, count(p) AS pathCount, count(DISTINCT target) AS endpointCount
ORDER BY pathLength, pathCount DESC;

Equivalent query in GQL standard:

gql
MATCH p = (:Post_question {post_id: $questionId})-[:Link_to]->{1,3}(target:Post_question)
WITH length(p) AS pathLength,
     CASE
       WHEN coalesce(target.answer_count, 0) = 0 THEN 'unanswered'
       WHEN coalesce(target.answer_count, 0) = 1 THEN 'single-answer'
       ELSE 'multi-answer'
     END AS answerBucket,
     p,
     target
RETURN pathLength, answerBucket, count(p) AS pathCount, count(DISTINCT target) AS endpointCount
ORDER BY pathLength, pathCount DESC;

Q22. Top Questions by Link Degree Centrality

Subcategory: T5.1 Degree Centrality

This query computes degree centrality directly in GQL/Cypher by counting incoming and outgoing question-link neighbors.

cypher
MATCH (q:Post_question)
OPTIONAL MATCH (q)-[:Link_to]-(neighbor:Post_question)
RETURN q.post_id AS questionId,
       q.title AS title,
       count(DISTINCT neighbor) AS linkDegree
ORDER BY linkDegree DESC
LIMIT $limit;

Equivalent query in GQL standard:

gql
MATCH (q:Post_question)
OPTIONAL MATCH (q)-[:Link_to]-(neighbor:Post_question)
RETURN q.post_id AS questionId,
       q.title AS title,
       count(DISTINCT neighbor) AS linkDegree
ORDER BY linkDegree DESC
LIMIT $limit;

Q23. PageRank on the Question Link Graph

Subcategory: T5.2 PageRank

This query projects the question-link graph and runs PageRank to find questions that are important in the directed citation-style link network.

cypher
CALL gds.graph.project(
  'StackOverflowQuestionLinksPageRank',
  'Post_question',
  {Link_to: {type: 'Link_to', orientation: 'NATURAL'}}
);

CALL gds.pageRank.stream('StackOverflowQuestionLinksPageRank')
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).post_id AS questionId,
       gds.util.asNode(nodeId).title AS title,
       score
ORDER BY score DESC
LIMIT $limit;
Note: Cannot be expressed in standard GQL because Graph Data Science (GDS) procedures and PageRank are not supported by the current GQL standard.

Q24. Weakly Connected Components of Question Links

Subcategory: T5.3 Connected Components / Weakly Connected Components

This query treats question links as undirected and reports the largest weakly connected components.

cypher
CALL gds.graph.project(
  'StackOverflowQuestionLinksWcc',
  'Post_question',
  {Link_to: {type: 'Link_to', orientation: 'UNDIRECTED'}}
);

CALL gds.wcc.stream('StackOverflowQuestionLinksWcc')
YIELD nodeId, componentId
RETURN componentId, count(*) AS componentSize
ORDER BY componentSize DESC
LIMIT $limit;
Note: Cannot be expressed in standard GQL because Graph Data Science (GDS) procedures and connected-components algorithms are not supported by the current GQL standard.

Q25. Strongly Connected Components of Question Links

Subcategory: T5.4 Strongly Connected Components

This query runs strongly connected components on the directed question-link graph.

cypher
CALL gds.graph.project(
  'StackOverflowQuestionLinksScc',
  'Post_question',
  {Link_to: {type: 'Link_to', orientation: 'NATURAL'}}
);

CALL gds.scc.stream('StackOverflowQuestionLinksScc')
YIELD nodeId, componentId
RETURN componentId, count(*) AS componentSize
ORDER BY componentSize DESC
LIMIT $limit;
Note: Cannot be expressed in standard GQL because Graph Data Science (GDS) procedures and strongly connected component algorithms are not supported by the current GQL standard.

Q26. Louvain Communities in the Question Link Graph

Subcategory: T5.5 Community Detection

This query detects communities of related questions in the undirected question-link graph using Louvain.

cypher
CALL gds.graph.project(
  'StackOverflowQuestionLinksLouvain',
  'Post_question',
  {Link_to: {type: 'Link_to', orientation: 'UNDIRECTED'}}
);

CALL gds.louvain.stream('StackOverflowQuestionLinksLouvain')
YIELD nodeId, communityId
RETURN communityId, count(*) AS communitySize
ORDER BY communitySize DESC
LIMIT $limit;
Note: Cannot be expressed in standard GQL because Graph Data Science (GDS) procedures and community detection algorithms are not supported by the current GQL standard.

Q27. Vote Distribution

Subcategory: T3.3 Star Pattern Query; T4.7 Union and Intersection

This query ranks users by the total number of votes received across their questions and answers, combining both incoming vote relationships into one activity score.

cypher
MATCH (u:User)
OPTIONAL MATCH (q:Post_question)-[:Owned_by]->(u)
OPTIONAL MATCH (vq:Vote)-[:Vote_on_question]->(q)
WITH u, COUNT(vq) AS q_votes
OPTIONAL MATCH (a:Post_answer)-[:Answers_by]->(u)
OPTIONAL MATCH (va:Vote)-[:Vote_on_answer]->(a)
WITH u.id AS user_id,
     u.display_name AS display_name,
     q_votes + COUNT(va) AS total_votes
WHERE total_votes > 0
RETURN user_id, display_name, total_votes
ORDER BY total_votes DESC
LIMIT 20;

Equivalent query in GQL standard:

gql
MATCH (u:User)
OPTIONAL MATCH (q:Post_question)-[:Owned_by]->(u)
OPTIONAL MATCH (vq:Vote)-[:Vote_on_question]->(q)
WITH u, count(vq) AS q_votes
OPTIONAL MATCH (a:Post_answer)-[:Answers_by]->(u)
OPTIONAL MATCH (va:Vote)-[:Vote_on_answer]->(a)
WITH u.id AS user_id,
     u.display_name AS display_name,
     q_votes + count(va) AS total_votes
WHERE total_votes > 0
RETURN user_id, display_name, total_votes
ORDER BY total_votes DESC
LIMIT 20;

Q28. Users with Most Accepted Answers

Subcategory: T3.5 Two-hop Join Pattern; T4.1 Ungrouped Node Degree Statistics by Label

This query counts accepted-answer relationships per user and returns the users whose answers were selected as accepted solutions most often.

cypher
MATCH (q:Post_question)-[:Accepts]->(a:Post_answer)-[:Answers_by]->(u:User)
WITH u.id AS user_id,
     u.display_name AS display_name,
     COUNT(a) AS accepted_count
WHERE accepted_count > 0
RETURN user_id, display_name, accepted_count
ORDER BY accepted_count DESC
LIMIT 20;

Equivalent query in GQL standard:

gql
MATCH (q:Post_question)-[:Accepts]->(a:Post_answer)-[:Answers_by]->(u:User)
WITH u.id AS user_id,
     u.display_name AS display_name,
     count(a) AS accepted_count
WHERE accepted_count > 0
RETURN user_id, display_name, accepted_count
ORDER BY accepted_count DESC
LIMIT 20;

Q29. User Pairs with Most Question-Answer Interactions

Subcategory: T3.5 Two-hop Join Pattern; T4.3 Ungrouped Edge Count and Property Statistics

This query finds answerer-questioner pairs connected through answer posts and question posts, counts how many times one user answered another user's questions, and returns pairs with at least five such interactions.

cypher
MATCH (a:Post_answer)-[:Answers_to]->(q:Post_question)
MATCH (a)-[:Answers_by]->(answerer:User)
MATCH (q)-[:Owned_by]->(questioner:User)
WHERE answerer.id <> questioner.id
WITH (answerer.id + ':' + questioner.id) AS pair_key,
    COUNT(*) AS interaction_count
WHERE interaction_count >= 5
RETURN pair_key, interaction_count
ORDER BY interaction_count DESC
LIMIT 100;

Equivalent query in GQL standard:

gql
MATCH (a:Post_answer)-[:Answers_to]->(q:Post_question)
MATCH (a)-[:Answers_by]->(answerer:User)
MATCH (q)-[:Owned_by]->(questioner:User)
WHERE answerer.id <> questioner.id
WITH (answerer.id + ':' + questioner.id) AS pair_key,
    count(*) AS interaction_count
WHERE interaction_count >= 5
RETURN pair_key, interaction_count
ORDER BY interaction_count DESC
LIMIT 100;

Q30. Tags with Highest Question-to-Answer Ratio

Subcategory: Other

This query groups questions by tag, counts both tagged questions and answers attached to those questions, then ranks tags by the highest question-to-answer ratio.

cypher
MATCH (t:Tag)-[:Tag_to]->(q:Post_question)
WITH t, COUNT(q) AS question_count
MATCH (t)-[:Tag_to]->(q2:Post_question)<-[:Answers_to]-(a:Post_answer)
WITH t.tag_name AS tag_name,
    question_count,
    COUNT(a) AS answer_count
WHERE answer_count > 0
RETURN tag_name,
    question_count,
    answer_count,
    toFloat(question_count) / toFloat(answer_count) AS ratio
ORDER BY ratio DESC
LIMIT 200;

Equivalent query in GQL standard:

gql
MATCH (t:Tag)-[:Tag_to]->(q:Post_question)
WITH t, count(q) AS question_count
MATCH (t)-[:Tag_to]->(q2:Post_question)<-[:Answers_to]-(a:Post_answer)
WITH t.tag_name AS tag_name,
    question_count,
    count(a) AS answer_count
WHERE answer_count > 0
RETURN tag_name,
    question_count,
    answer_count,
    toFloat(question_count) / toFloat(answer_count) AS ratio
ORDER BY ratio DESC
LIMIT 200;

Q31. Users Who Earn Badges Fastest After Account Creation

Subcategory: Other

This query groups badge-earning events by user, estimates how many days elapsed between each user's account creation and first badge date, and returns the fastest badge earners.

cypher
MATCH (u:User)-[:Earns]->(b:Badge)
WHERE u.creation_date IS NOT NULL
    AND b.date IS NOT NULL
    AND u.creation_date <> ''
    AND b.date <> ''
WITH u.id AS user_id,
     u.display_name  AS display_name,
     u.creation_date AS creation_date,
     MIN(
        (toInteger(substring(b.date, 0, 4)) - toInteger(substring(u.creation_date, 0, 4))) * 365 +
        (toInteger(substring(b.date, 5, 2)) - toInteger(substring(u.creation_date, 5, 2))) * 30  +
        (toInteger(substring(b.date, 8, 2)) - toInteger(substring(u.creation_date, 8, 2)))
     ) AS min_days
WHERE min_days >= 0
RETURN user_id, display_name, min_days
ORDER BY min_days ASC, user_id ASC
LIMIT 20;

Equivalent query in GQL standard:

gql
MATCH (u:User)-[:Earns]->(b:Badge)
WHERE u.creation_date IS NOT NULL
    AND b.date IS NOT NULL
    AND u.creation_date <> ''
    AND b.date <> ''
WITH u.id AS user_id,
     u.display_name AS display_name,
     u.creation_date AS creation_date,
     min(
        (toInteger(substring(b.date, 0, 4)) - toInteger(substring(u.creation_date, 0, 4))) * 365 +
        (toInteger(substring(b.date, 5, 2)) - toInteger(substring(u.creation_date, 5, 2))) * 30 +
        (toInteger(substring(b.date, 8, 2)) - toInteger(substring(u.creation_date, 8, 2)))
     ) AS min_days
WHERE min_days >= 0
RETURN user_id, display_name, min_days
ORDER BY min_days ASC, user_id ASC
LIMIT 20;

Q32. Percentage of Questions Answered Over the Years

Subcategory: Other

This query groups questions by creation year, counts all questions and questions with at least one answer, then returns the yearly answer rate as a percentage.

cypher
MATCH (q:Post_question)
WHERE q.creation_date IS NOT NULL
AND q.creation_date <> ''
WITH substring(q.creation_date, 0, 4) AS year_val,
     COUNT(q) AS total_questions,
     SUM(CASE WHEN toInteger(q.answer_count) > 0 THEN 1 ELSE 0 END) AS answered_questions
RETURN year_val,
       total_questions,
       answered_questions,
round(toFloat(answered_questions) * 100.0 / toFloat(total_questions), 5) AS answer_rate
ORDER BY year_val ASC;

Equivalent query in GQL standard:

gql
MATCH (q:Post_question)
WHERE q.creation_date IS NOT NULL
    AND q.creation_date <> ''
WITH substring(q.creation_date, 0, 4) AS year_val,
     count(q) AS total_questions,
     sum(CASE WHEN toInteger(q.answer_count) > 0 THEN 1 ELSE 0 END) AS answered_questions
RETURN year_val,
    total_questions,
    answered_questions,
    round(toFloat(answered_questions) * 100.0 / toFloat(total_questions), 5) AS answer_rate
ORDER BY year_val ASC;

Q33. Top 10 "Easiest" Gold Badges to Earn

Subcategory: Other

This query focuses on gold badges, counts distinct earners for each badge, estimates the fastest time from account creation to earning that badge, and returns the most commonly earned gold badges.

cypher
MATCH (u:User)-[:Earns]->(b:Badge)
WHERE b.class = '1'
AND u.creation_date IS NOT NULL
AND b.date IS NOT NULL
AND u.creation_date <> ''
AND b.date <> ''
WITH b.name AS badge_name,
     COUNT(DISTINCT u.id) AS total_earners,
     MIN(
        (toInteger(substring(b.date, 0, 4)) - toInteger(substring(u.creation_date, 0, 4))) * 365 +
        (toInteger(substring(b.date, 5, 2)) - toInteger(substring(u.creation_date, 5, 2))) * 30  +
        (toInteger(substring(b.date, 8, 2)) - toInteger(substring(u.creation_date, 8, 2)))
     ) AS min_days
WHERE min_days >= 0
RETURN badge_name, total_earners, min_days
ORDER BY total_earners DESC
LIMIT 10;

Equivalent query in GQL standard:

gql
MATCH (u:User)-[:Earns]->(b:Badge)
WHERE b.class = '1'
    AND u.creation_date IS NOT NULL
    AND b.date IS NOT NULL
    AND u.creation_date <> ''
    AND b.date <> ''
WITH b.name AS badge_name,
     count(DISTINCT u.id) AS total_earners,
     min(
        (toInteger(substring(b.date, 0, 4)) - toInteger(substring(u.creation_date, 0, 4))) * 365 +
        (toInteger(substring(b.date, 5, 2)) - toInteger(substring(u.creation_date, 5, 2))) * 30 +
        (toInteger(substring(b.date, 8, 2)) - toInteger(substring(u.creation_date, 8, 2)))
     ) AS min_days
WHERE min_days >= 0
RETURN badge_name, total_earners, min_days
ORDER BY total_earners DESC
LIMIT 10;