📖 图数据库概述
图数据库(Graph Database)是以图结构(节点、边、属性)存储和查询数据的数据库系统。它是知识图谱最自然的存储方式——知识图谱本身就是图。
🎯 为什么知识图谱需要图数据库
- 原生图存储:无需将图拆解为表,无需昂贵的JOIN
- 高效图遍历:邻居查询O(1),多层遍历远快于关系数据库
- 灵活Schema:添加新节点类型和关系无需改表结构
- 可视化友好:直接映射到图可视化工具
📐 图数据库 vs 关系数据库
| 维度 | 关系数据库 | 图数据库 |
| 数据模型 | 表(行/列) | 图(节点/边) |
| 关联查询 | JOIN,O(n*m) | 指针遍历,O(k) |
| 多跳查询 | 指数级变慢 | 近似常数级 |
| Schema变更 | ALTER TABLE | 即时添加 |
| 适合场景 | 结构化事务 | 关联密集型 |
💻 Python实现:简易图数据库
from collections import defaultdict
import json
class SimpleGraphDB:
"""简易图数据库 - 支持节点、边、属性和索引"""
def __init__(self):
self.nodes = {}
self.edges = []
self.out_edges = defaultdict(list)
self.in_edges = defaultdict(list)
self.label_index = defaultdict(set)
self.property_index = defaultdict(dict)
self._next_id = 0
def create_node(self, label, properties=None):
"""创建节点"""
node_id = f"n{self._next_id}"
self._next_id += 1
props = properties or {}
self.nodes[node_id] = {"label": label, "properties": props}
self.label_index[label].add(node_id)
for key, val in props.items():
idx_key = (label, key)
self.property_index[idx_key].setdefault(val, set()).add(node_id)
return node_id
def create_edge(self, source_id, target_id, edge_type, properties=None):
">>>创建边"""
edge_id = f"e{self._next_id}"
self._next_id += 1
edge = {
"id": edge_id, "source": source_id, "target": target_id,
"type": edge_type, "properties": properties or {}
}
self.edges.append(edge)
self.out_edges[source_id].append(edge)
self.in_edges[target_id].append(edge)
return edge_id
def get_node(self, node_id):
return self.nodes.get(node_id)
def find_nodes(self, label, property_filter=None):
"""按标签和属性查找节点"""
candidates = self.label_index.get(label, set())
if property_filter:
for key, val in property_filter.items():
idx_key = (label, key)
if idx_key in self.property_index:
candidates = candidates & self.property_index[idx_key].get(val, set())
else:
candidates = {n for n in candidates
if self.nodes[n]["properties"].get(key) == val}
return list(candidates)
def get_neighbors(self, node_id, edge_type=None, direction="out"):
"""获取邻居节点"""
edges = self.out_edges[node_id] if direction in ("out", "both") else []
edges += self.in_edges[node_id] if direction in ("in", "both") else []
if edge_type:
edges = [e for e in edges if e["type"] == edge_type]
neighbors = []
for e in edges:
if e["source"] == node_id:
neighbors.append((e["target"], e["type"], "out"))
else:
neighbors.append((e["source"], e["type"], "in"))
return neighbors
def traverse(self, start_id, max_depth=3, edge_types=None):
"""图遍历(BFS)"""
visited = {start_id}
queue = [(start_id, 0)]
result = []
while queue:
current, depth = queue.pop(0)
if depth >= max_depth:
continue
neighbors = self.get_neighbors(current, direction="both")
for neighbor_id, rel_type, direction in neighbors:
if edge_types and rel_type not in edge_types:
continue
if neighbor_id not in visited:
visited.add(neighbor_id)
queue.append((neighbor_id, depth + 1))
result.append((current, rel_type, neighbor_id, depth + 1))
return result
def stats(self):
return {
"节点数": len(self.nodes),
">边数": len(self.edges),
">标签类型": list(self.label_index.keys()),
">索引大小": sum(len(v) for v in self.property_index.values())
}
db = SimpleGraphDB()
luxun = db.create_node("作家", {"name": "鲁迅", "原名": "周树人", "生年": 1881})
laoshe = db.create_node("作家", {"name": "老舍", ">原名": "舒庆春", ">生年": 1899})
xuzhimo = db.create_node("作家", {"name": "徐志摩", "生年": 1897})
nahan = db.create_node("作品", {"name": "呐喊", "年份": 1923})
fanghuang = db.create_node("作品", {"name": "彷徨", "年份": 1926})
luotuo = db.create_node("作品", {"name": "骆驼祥子", "年份": 1937})
shaoxing = db.create_node("地点", {"name": "绍兴"})
beijing = db.create_node("地点", {"name": "北京"})
zhejiang = db.create_node("地点", {"name": "浙江省"})
db.create_edge(luxun, nahang, "创作")
db.create_edge(luxun, fanghuang, "创作")
db.create_edge(luxun, shaoxing, "出生地")
db.create_edge(laoshe, luotuo, "创作")
db.create_edge(laoshe, beijing, "出生地")
db.create_edge(shaoxing, zhejiang, "属于")
print("=== 图数据库统计 ===")
for k, v in db.stats().items():
print(f" {k}: {v}")
print("
=== 查找所有作家 ===")
for nid in db.find_nodes("作家"):
print(f" {db.get_node(nid)}")
print("
=== 鲁迅的邻居 ==="
for nid, rel, d in db.get_neighbors(luxun):
print(f" {db.get_node(nid)['properties']['name']} ({rel}, {d})")
print("
=== 从鲁迅出发2跳遍历 ===")
for src, rel, tgt, depth in db.traverse(luxun, max_depth=2):
src_name = db.get_node(src)["properties"].get("name", src)
tgt_name = db.get_node(tgt)["properties"].get("name", tgt)
print(f" {' '*depth}{src_name} --[{rel}]--> {tgt_name} (depth={depth})")
=== 图数据库统计 ===
节点数: 9
边数: 6
标签类型: ['作家', '作品', '地点']
索引大小: 12
=== 查找所有作家 ===
{'label': '作家', 'properties': {'name': '鲁迅', '原名': '周树人', '生年': 1881}}
{'label': '作家', 'properties': {'name': '老舍', '原名': '舒庆春', '生年': 1899}}
{'label': '作家', 'properties': {'name': '徐志摩', '生年': 1897}}
=== 鲁迅的邻居 ===
呐喊 (创作, out)
彷徨 (创作, out)
绍兴 (出生地, out)
=== 从鲁迅出发2跳遍历 ===
鲁迅 --[创作]--> 呐喊 (depth=1)
鲁迅 --[创作]--> 彷徨 (depth=1)
鲁迅 --[出生地]--> 绍兴 (depth=1)
绍兴 --[属于]--> 浙江省 (depth=2)
🌍 主流图数据库对比
| 数据库 | 类型 | 查询语言 | 特点 |
| Neo4j | 属性图 | Cypher | 最流行,生态丰富 |
| JanusGraph | 属性图 | Gremlin | 分布式,Hadoop集成 |
| Amazon Neptune | 两者 | Gremlin/SPARQL | 托管服务 |
| TigerGraph | 属性图 | GSQL | 分布式,高性能 |
| Virtuoso | RDF | SPARQL | 语义Web标准 |
| ArangoDB | 多模型 | AQL | 图+文档+KV |
📝 实战练习
练习1:添加属性索引
为"年份"属性添加范围查询索引,支持按年份范围筛选作品。
练习2:实现图删除
实现级联删除:删除一个节点时,自动删除其所有关联边。
练习3:导出为JSON
实现将整个图数据库导出为JSON格式的功能,方便备份和迁移。
💾
🏆 第11课成就解锁
图数据库入门者
💾 图存储
🔍 索引查询
🚶 图遍历
📊 邻居查询