Schema Diagram and Graph Statistics
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
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.
python clean_users_csv.py users.csv users_clean.csv --strip-backslashes
Now you can run the following cypher query to import User nodes.
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)
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.
python clean_comments_csv.py comments.csv comments_clean.csv
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)
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.
python clean_posts_csv.py posts_answers.csv posts_answers_clean.csv
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.
python clean_posts_csv.py posts_questions.csv posts_questions_clean.csv
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)
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.
python clean_posts_csv.py posts_tag_wiki.csv posts_tag_wiki_clean.csv
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)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Q1. Comments Written by a User
This query lists the comments directly connected to a selected user through incoming
Commented_by relationships.
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:
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
This query follows a user to their questions and then to the tags attached to those questions, returning distinct tags within two hops.
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:
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
This query finds users exactly three relationships away from a selected question owner through question-answer interactions.
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:
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
This query filters a user's one-hop answer neighborhood to answers whose score is at or above a supplied threshold.
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:
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
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.
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:
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
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.
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:
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
This query tests whether one question can reach another through up to four Link_to
hops.
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:
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
This query finds the shortest directed path between two questions over the question-link graph.
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:
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
This query enumerates simple directed paths between two questions using up to three
Link_to hops.
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:
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
This query ranks bounded linked-question paths by the cumulative score of the questions along each path.
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:
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
This query finds directed triangles in the question-link graph.
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:
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
This query finds four-question cycles formed by directed Link_to relationships.
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:
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
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.
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:
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
This query follows a longer line from a question owner to a question, an answer, a comment on that answer, and the comment author.
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:
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
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.
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:
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
This query counts incoming Link_to relationships for each question and returns the
questions that are referenced most often by other questions.
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:
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
This query counts comments authored by each user and returns the most active commenters by total outgoing comment activity.
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:
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
This query groups questions by tag and summarizes how many questions and answers are associated with each tag.
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:
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
The import stores vote facts as Vote nodes, so this query groups vote-edge records
by vote type and target node label.
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:
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
This query counts bounded question-link paths from a selected question and groups them by path length.
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:
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
This query groups linked-question paths by path length and by whether the endpoint question has no answers, one answer, or multiple answers.
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:
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
This query computes degree centrality directly in GQL/Cypher by counting incoming and outgoing question-link neighbors.
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:
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
This query projects the question-link graph and runs PageRank to find questions that are important in the directed citation-style link network.
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;
Q24. Weakly Connected Components of Question Links
This query treats question links as undirected and reports the largest weakly connected components.
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;
Q25. Strongly Connected Components of Question Links
This query runs strongly connected components on the directed question-link graph.
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;
Q26. Louvain Communities in the Question Link Graph
This query detects communities of related questions in the undirected question-link graph using Louvain.
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;
Q27. Vote Distribution
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.
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:
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
This query counts accepted-answer relationships per user and returns the users whose answers were selected as accepted solutions most often.
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:
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
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.
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:
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
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.
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:
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
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.
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:
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
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.
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:
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
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.
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:
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;