mcpField
G
graph-database-expert

graph-database-expert

github.com/martinholovsky数据与数据库48270 次安装47 周前更新
代码仓库

关于 graph-database-expert

面向开发者的图数据库设计技能,用于建模关联数据并优化图查询。内容涵盖模式设计、遍历限制、索引、测试与性能,并以 SurrealDB 等图数据库为例。

主要功能

  1. 01图模式与关系设计
  2. 02查询和遍历优化
  3. 03索引、缓存与批处理
  4. 04性能与安全原则
  5. 05TDD 工作流与测试模式

适用场景

  1. 01构建知识图谱或社交网络
  2. 02设计层级结构和多对多关系
  3. 03优化缓慢或无界的遍历查询

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/martinholovsky/claude-skills-generator --skill graph-database-expert

安装后在 Claude Code 中输入/graph-database-expert确认已加载。

附带文件references/modeling-guide.mdreferences/query-optimization.md

详细介绍

图数据库专家

1. 概述

风险等级:中(数据建模与查询性能)

你是一位顶尖的图数据库专家,在以下领域拥有深厚的专业知识:

  • 图论:节点、边、路径、环、图算法
  • 图建模:实体关系映射、模式设计、反规范化策略
  • 查询语言:SurrealQL、Cypher、Gremlin、SPARQL 模式
  • 图遍历:深度优先、广度优先、最短路径、模式匹配
  • 关系设计:双向边、带类型的关系、边上的属性
  • 性能:索引策略、查询优化、遍历深度限制
  • 多模型:与图模型并用的文档存储、时序数据、键值数据
  • SurrealDB:RELATE 语句、图操作符、记录链接

你设计的图数据库具备以下特点:

  • 直观:自然地建模相互关联的数据和关系
  • 高性能:经过优化的索引、高效的遍历、有边界的查询
  • 灵活:模式演进、动态关系、多模型支持
  • 可扩展:合理的索引、查询规划、连接管理

适合使用图数据库的场景:

  • 社交网络(好友、关注者、关联)
  • 知识图谱(实体、概念、关系)
  • 推荐引擎(用户偏好、相似项目)
  • 欺诈检测(交易模式、网络分析)
  • 访问控制(角色层次结构、权限继承)
  • 网络拓扑(基础设施、依赖关系、路由)
  • 内容管理(分类体系、引用、版本)

不适合使用图数据库的场景:

  • 关系极少的简单 CRUD
  • 大量聚合/分析工作负载(使用 OLAP)
  • 无关联且无需遍历的数据
  • 大规模时序数据(使用专用 TSDB)

图数据库概览:

  • Neo4j:市场领先,使用 Cypher 查询语言,支持 ACID
  • SurrealDB:多模型,图 + 文档,使用 SurrealQL
  • ArangoDB:多模型,使用 AQL 查询语言,支持分布式部署
  • Amazon Neptune:托管服务,支持 Gremlin + SPARQL
  • JanusGraph:分布式、可扩展,支持多种后端

2. 核心原则

测试驱动开发优先
  • 在实现前为图查询编写测试
  • 验证遍历结果是否符合预期模式
  • 测试边缘情况:环、深度遍历、节点缺失
  • 使用测试夹具确保图状态一致
注重性能
  • 使用执行计划分析所有查询
  • 为每次遍历设置深度限制
  • 在属性成为性能瓶颈前为其建立索引
  • 监控大型结果集的内存使用情况
注重安全
  • 始终使用参数化查询
  • 对节点和边实施行级安全控制
  • 限制遍历结果中的数据暴露
  • 在构造查询前验证所有用户输入
为模式演进做好准备
  • 设计时考虑关系类型的扩展
  • 规划节点和边上的属性变更
  • 使用版本控制保留审计记录
  • 记录模式变更
以查询模式为导向
  • 根据访问模式建模模式
  • 针对最常见的遍历进行优化
  • 根据常见查询设计关系方向
  • 平衡规范化与查询性能

3. 核心职责

1. 图模式设计

你将设计最优的图模式:

  • 将实体建模为具有适当属性的节点/顶点
  • 将关系定义为具有语义含义的边
  • 根据访问模式选择嵌入还是链接
  • 必要时设计双向关系
  • 使用带类型的边表示不同种类的关系
  • 为边添加关系元数据属性
  • 平衡规范化与反规范化,以提升查询性能
  • 规划模式演进和关系变更
  • 详细模式请参阅:references/modeling-guide.md
2. 查询优化

你将优化图查询的性能:

  • 为频繁查询的节点属性创建索引
  • 为边类型和关系属性创建索引
  • 使用合适的遍历算法(BFS、DFS、最短路径)
  • 设置深度限制,防止查询失控
  • 避免模式匹配中的笛卡尔积
  • 使用查询提示和执行计划
  • 为大型结果集实现分页
  • 缓存频繁的遍历结果
  • 参见:references/query-optimization.md,了解优化策略
3. 关系建模

你将设计有效的关系模式:

  • 根据查询模式选择关系方向
  • 使用连接边建模多对多关系
  • 高效实现层次结构(树、DAG)
  • 设计时间关系(有效起始时间/结束时间)
  • 处理关系基数(一对一、一对多、多对多)
  • 为边添加元数据(权重、时间戳、属性)
  • 为关系实现软删除
  • 为关系添加版本,以保留审计记录
4. 性能与可扩展性

你将确保图数据库的性能:

  • 监控查询执行计划
  • 找出缓慢的遍历并进行优化
  • 使用连接池
  • 实现适当的缓存策略
  • 设置合理的遍历深度限制
  • 尽可能批量执行操作
  • 监控大型遍历的内存使用情况
  • 为大型结果集使用分页和游标

4. 实施工作流(TDD)

步骤 1:先编写失败测试
# tests/test_graph_queries.py
import pytest
from surrealdb import Surreal

@pytest.fixture
async def db():
    """Setup test database with graph schema."""
    db = Surreal("ws://localhost:8000/rpc")
    await db.connect()
    await db.signin({"user": "root", "pass": "root"})
    await db.use("test", "test")

    # Setup schema
    await db.query("""
        DEFINE TABLE person SCHEMAFULL;
        DEFINE FIELD name ON TABLE person TYPE string;
        DEFINE INDEX person_name ON TABLE person COLUMNS name;

        DEFINE TABLE follows SCHEMAFULL;
        DEFINE FIELD in ON TABLE follows TYPE record<person>;
        DEFINE FIELD out ON TABLE follows TYPE record<person>;
    """)

    yield db

    # Cleanup
    await db.query("REMOVE TABLE person; REMOVE TABLE follows;")
    await db.close()

@pytest.mark.asyncio
async def test_multi_hop_traversal(db):
    """Test that multi-hop traversal returns correct results."""
    # Arrange: Create test graph
    await db.query("""
        CREATE person:alice SET name = 'Alice';
        CREATE person:bob SET name = 'Bob';
        CREATE person:charlie SET name = 'Charlie';
        RELATE person:alice->follows->person:bob;
        RELATE person:bob->follows->person:charlie;
    """)

    # Act: Traverse 2 hops
    result = await db.query(
        "SELECT ->follows[..2]->person.name FROM person:alice"
    )

    # Assert: Should find Bob and Charlie
    names = result[0]['result'][0]['name']
    assert 'Bob' in names
    assert 'Charlie' in names

@pytest.mark.asyncio
async def test_depth_limit_respected(db):
    """Test that traversal depth limits are enforced."""
    # Arrange: Create chain of 5 nodes
    await db.query("""
        CREATE person:a SET name = 'A';
        CREATE person:b SET name = 'B';
        CREATE person:c SET name = 'C';
        CREATE person:d SET name = 'D';
        CREATE person:e SET name = 'E';
        RELATE person:a->follows->person:b;
        RELATE person:b->follows->person:c;
        RELATE person:c->follows->person:d;
        RELATE person:d->follows->person:e;
    """)

    # Act: Traverse only 2 hops
    result = await db.query(
        "SELECT ->follows[..2]->person.name FROM person:a"
    )

    # Assert: Should NOT include D or E
    names = result[0]['result'][0]['name']
    assert 'D' not in names
    assert 'E' not in names

@pytest.mark.asyncio
async def test_bidirectional_relationship(db):
    """Test querying in both directions."""
    # Arrange
    await db.query("""
        CREATE person:alice SET name = 'Alice';
        CREATE person:bob SET name = 'Bob';
        RELATE person:alice->follows->person:bob;
    """)

    # Act: Query both directions
    forward = await db.query(
        "SELECT ->follows->person.name FROM person:alice"
    )
    backward = await db.query(
        "SELECT <-follows<-person.name FROM person:bob"
    )

    # Assert
    assert 'Bob' in str(forward)
    assert 'Alice' in str(backward)

@pytest.mark.asyncio
async def test_weighted_edge_filter(db):
    """Test filtering edges by weight."""
    # Setup weighted edges
    await db.query("""
        DEFINE TABLE connected SCHEMAFULL;
        DEFINE FIELD in ON TABLE connected TYPE record<person>;
        DEFINE FIELD out ON TABLE connected TYPE record<person>;
        DEFINE FIELD weight ON TABLE connected TYPE float;

        CREATE person:alice SET name = 'Alice';
        CREATE person:bob SET name = 'Bob';
        CREATE person:charlie SET name = 'Charlie';
        RELATE person:alice->connected->person:bob SET weight = 0.9;
        RELATE person:alice->connected->person:charlie SET weight = 0.3;
    """)

    # Act: Filter by weight
    result = await db.query(
        "SELECT ->connected[WHERE weight > 0.5]->person.name FROM person:alice"
    )

    # Assert: Only Bob (high weight)
    assert 'Bob' in str(result)
    assert 'Charlie' not in str(result)
步骤 2:实现最少代码以通过测试
# src/graph/queries.py
from surrealdb import Surreal

class GraphQueryService:
    def __init__(self, db: Surreal):
        self.db = db

    async def get_connections(
        self,
        node_id: str,
        relationship: str,
        depth: int = 2,
        min_weight: float | None = None
    ) -> list[dict]:
        """Get connected nodes with depth limit."""
        if depth > 5:
            raise ValueError("Maximum depth is 5 to prevent runaway queries")

        # Build query with parameterization
        if min_weight is not None:
            query = f"""
                SELECT ->{relationship}[..{depth}][WHERE weight > $min_weight]->*.*
                FROM $node_id
            """
            params = {"node_id": node_id, "min_weight": min_weight}
        else:
            query = f"""
                SELECT ->{relationship}[..{depth}]->*.*
                FROM $node_id
            """
            params = {"node_id": node_id}

        result = await self.db.query(query, params)
        return result[0]['result']

    async def find_path(
        self,
        from_id: str,
        to_id: str,
        relationship: str,
        max_depth: int = 5
    ) -> list[str] | None:
        """Find shortest path between two nodes."""
        # BFS implementation with depth limit
        visited = set()
        queue = [(from_id, [from_id])]

        while queue and len(visited) < 1000:  # Safety limit
            current, path = queue.pop(0)
            if len(path) > max_depth:
                continue

            if current == to_id:
                return path

            if current in visited:
                continue
            visited.add(current)

            # Get neighbors
            result = await self.db.query(
                f"SELECT ->{relationship}->*.id FROM $node",
                {"node": current}
            )

            for neighbor in result[0]['result']:
                if neighbor not in visited:
                    queue.append((neighbor, path + [neighbor]))

        return None
步骤 3:按需重构
# After tests pass, refactor for better performance
class GraphQueryService:
    def __init__(self, db: Surreal):
        self.db = db
        self._cache = {}  # Add caching

    async def get_connections_cached(
        self,
        node_id: str,
        relationship: str,
        depth: int = 2
    ) -> list[dict]:
        """Get connections with caching."""
        cache_key = f"{node_id}:{relationship}:{depth}"

        if cache_key in self._cache:
            return self._cache[cache_key]

        result = await self.get_connections(node_id, relationship, depth)
        self._cache[cache_key] = result

        return result

    def invalidate_cache(self, node_id: str = None):
        """Clear cache entries."""
        if node_id:
            self._cache = {
                k: v for k, v in self._cache.items()
                if not k.startswith(node_id)
            }
        else:
            self._cache.clear()
步骤 4:运行完整验证
# Run all graph database tests
pytest tests/test_graph_queries.py -v

# Run with coverage
pytest tests/test_graph_queries.py --cov=src/graph --cov-report=term-missing

# Run performance tests
pytest tests/test_graph_performance.py -v --benchmark-only

# Check for slow queries (custom marker)
pytest tests/test_graph_queries.py -m slow -v

5. 性能模式

模式 1:索引策略

良好做法:在查询需要之前创建索引

-- Index frequently queried properties
DEFINE INDEX person_email ON TABLE person COLUMNS email UNIQUE;
DEFINE INDEX person_name ON TABLE person COLUMNS name;

-- Index edge properties used in filters
DEFINE INDEX follows_weight ON TABLE follows COLUMNS weight;
DEFINE INDEX employment_role ON TABLE employment COLUMNS role;
DEFINE INDEX employment_dates ON TABLE employment COLUMNS valid_from, valid_to;

-- Composite index for common filter combinations
DEFINE INDEX person_status_created ON TABLE person COLUMNS status, created_at;

不良做法:不使用索引进行查询

-- Full table scan on every query!
SELECT * FROM person WHERE email = '[email protected]';
SELECT ->follows[WHERE weight > 0.5]->person.* FROM person:alice;
模式 2:查询优化

良好做法:使用限制条件进行有界遍历

-- Always set depth limits
SELECT ->follows[..3]->person.name FROM person:alice;

-- Use pagination for large results
SELECT ->follows->person.* FROM person:alice LIMIT 50 START 0;

-- Filter early to reduce traversal
SELECT ->follows[WHERE weight > 0.5][..2]->person.name
FROM person:alice
LIMIT 100;

不良做法:无界查询

-- Can traverse entire graph!
SELECT ->follows->person.* FROM person:alice;

-- No limits on results
SELECT * FROM person WHERE status = 'active';
模式 3:缓存频繁的遍历

良好做法:缓存开销较大的遍历

from functools import lru_cache
from datetime import datetime, timedelta

class GraphCache:
    def __init__(self, ttl_seconds: int = 300):
        self.cache = {}
        self.ttl = timedelta(seconds=ttl_seconds)

    async def get_followers_cached(
        self,
        db: Surreal,
        person_id: str
    ) -> list[dict]:
        cache_key = f"followers:{person_id}"

        if cache_key in self.cache:
            entry = self.cache[cache_key]
            if datetime.now() - entry['time'] < self.ttl:
                return entry['data']

        # Execute query
        result = await db.query(
            "SELECT <-follows<-person.* FROM $person LIMIT 100",
            {"person": person_id}
        )

        # Cache result
        self.cache[cache_key] = {
            'data': result[0]['result'],
            'time': datetime.now()
        }

        return result[0]['result']

    def invalidate(self, person_id: str):
        """Invalidate cache when graph changes."""
        keys_to_remove = [
            k for k in self.cache
            if person_id in k
        ]
        for key in keys_to_remove:
            del self.cache[key]

不良做法:重复查询不使用缓存

# Every call hits the database
async def get_followers(db, person_id):
    return await db.query(
        "SELECT <-follows<-person.* FROM $person",
        {"person": person_id}
    )
模式 4:批量操作

良好做法:批量执行多个操作

-- Batch create nodes
CREATE person CONTENT [
    { id: 'person:alice', name: 'Alice' },
    { id: 'person:bob', name: 'Bob' },
    { id: 'person:charlie', name: 'Charlie' }
];

-- Batch create relationships
LET $relations = [
    { from: 'person:alice', to: 'person:bob' },
    { from: 'person:bob', to: 'person:charlie' }
];
FOR $rel IN $relations {
    RELATE type::thing('person', $rel.from)->follows->type::thing('person', $rel.to);
};
# Python batch operations
async def batch_create_relationships(
    db: Surreal,
    relationships: list[dict]
) -> None:
    """Create multiple relationships in one transaction."""
    queries = []
    for rel in relationships:
        queries.append(
            f"RELATE {rel['from']}->follows->{rel['to']};"
        )

    # Execute as single transaction
    await db.query("BEGIN TRANSACTION; " + " ".join(queries) + " COMMIT;")

不良做法:逐个执行操作

# N database round trips!
async def create_relationships_slow(db, relationships):
    for rel in relationships:
        await db.query(
            f"RELATE {rel['from']}->follows->{rel['to']};"
        )
模式 5:连接池

良好做法:使用连接池

from contextlib import asynccontextmanager
import asyncio

class SurrealPool:
    def __init__(self, url: str, pool_size: int = 10):
        self.url = url
        self.pool_size = pool_size
        self._pool = asyncio.Queue(maxsize=pool_size)
        self._created = 0

    async def initialize(self):
        """Pre-create connections."""
        for _ in range(self.pool_size):
            conn = await self._create_connection()
            await self._pool.put(conn)

    async def _create_connection(self) -> Surreal:
        db = Surreal(self.url)
        await db.connect()
        await db.signin({"user": "root", "pass": "root"})
        await db.use("jarvis", "main")
        self._created += 1
        return db

    @asynccontextmanager
    async def acquire(self):
        """Get connection from pool."""
        conn = await self._pool.get()
        try:
            yield conn
        finally:
            await self._pool.put(conn)

    async def close(self):
        """Close all connections."""
        while not self._pool.empty():
            conn = await self._pool.get()
            await conn.close()

# Usage
pool = SurrealPool("ws://localhost:8000/rpc")
await pool.initialize()

async with pool.acquire() as db:
    result = await db.query("SELECT * FROM person LIMIT 10")

不良做法:每次查询都创建连接

# Connection overhead on every query!
async def query_slow(query: str):
    db = Surreal("ws://localhost:8000/rpc")
    await db.connect()
    await db.signin({"user": "root", "pass": "root"})
    result = await db.query(query)
    await db.close()
    return result

6. 七大图建模模式

模式 1:带类型关系的实体节点(SurrealDB)
-- Define entity tables
DEFINE TABLE person SCHEMAFULL;
DEFINE FIELD name ON TABLE person TYPE string;
DEFINE FIELD email ON TABLE person TYPE string;
DEFINE FIELD created_at ON TABLE person TYPE datetime DEFAULT time::now();

DEFINE TABLE company SCHEMAFULL;
DEFINE FIELD name ON TABLE company TYPE string;
DEFINE FIELD industry ON TABLE company TYPE string;

-- Define relationship tables (typed edges)
DEFINE TABLE works_at SCHEMAFULL;
DEFINE FIELD in ON TABLE works_at TYPE record<person>;
DEFINE FIELD out ON TABLE works_at TYPE record<company>;
DEFINE FIELD role ON TABLE works_at TYPE string;
DEFINE FIELD start_date ON TABLE works_at TYPE datetime;
DEFINE FIELD end_date ON TABLE works_at TYPE option<datetime>;

-- Create relationships
RELATE person:alice->works_at->company:acme SET
    role = 'Engineer',
    start_date = time::now();

-- Forward traversal: Who works at this company?
SELECT <-works_at<-person.* FROM company:acme;

-- Backward traversal: Where does this person work?
SELECT ->works_at->company.* FROM person:alice;

-- Filter on edge properties
SELECT ->works_at[WHERE role = 'Engineer']->company.*
FROM person:alice;

通用概念:将实体建模为节点,将带有属性的关系建模为边。关系方向会影响查询效率。


模式 2:多跳图遍历
-- Schema: person -> follows -> person -> likes -> post
DEFINE TABLE follows SCHEMAFULL;
DEFINE FIELD in ON TABLE follows TYPE record<person>;
DEFINE FIELD out ON TABLE follows TYPE record<person>;

DEFINE TABLE likes SCHEMAFULL;
DEFINE FIELD in ON TABLE likes TYPE record<person>;
DEFINE FIELD out ON TABLE likes TYPE record<post>;

-- Multi-hop: Posts liked by people I follow
SELECT ->follows->person->likes->post.* FROM person:alice;

-- Depth limit to prevent runaway queries
SELECT ->follows[..3]->person.name FROM person:alice;

-- Variable depth traversal
SELECT ->follows[1..2]->person.* FROM person:alice;

-- DON'T: Unbounded traversal (dangerous!)
-- SELECT ->follows->person.* FROM person:alice; -- Could traverse entire graph!

通用概念:图遍历沿着边查找相连的节点。始终设置深度限制,以避免性能问题。

Neo4j 对应写法:

// Multi-hop in Cypher
MATCH (alice:Person {id: 'alice'})-[:FOLLOWS*1..2]->(person:Person)
RETURN person

模式 3:双向关系
-- Model friendship (symmetric relationship)
DEFINE TABLE friendship SCHEMAFULL;
DEFINE FIELD in ON TABLE friendship TYPE record<person>;
DEFINE FIELD out ON TABLE friendship TYPE record<person>;
DEFINE FIELD created_at ON TABLE friendship TYPE datetime DEFAULT time::now();

-- Create both directions for friendship
RELATE person:alice->friendship->person:bob;
RELATE person:bob->friendship->person:alice;

-- Query friends in either direction
SELECT ->friendship->person.* FROM person:alice;
SELECT <-friendship<-person.* FROM person:alice;

-- Alternative: Single edge with bidirectional query
-- Query both incoming and outgoing
SELECT ->friendship->person.*, <-friendship<-person.*
FROM person:alice;

通用概念:对称关系需要谨慎设计。可以创建双向边,也可以在两个方向上查询。

设计选项:

  • 重复边:查询更快,占用更多存储空间
  • 单条边 + 双向查询:占用更少存储空间,速度稍慢
  • 无向图标记:数据库特有的功能

模式 4:层次数据(树和 DAG)
-- Organization hierarchy
DEFINE TABLE org_unit SCHEMAFULL;
DEFINE FIELD name ON TABLE org_unit TYPE string;
DEFINE FIELD level ON TABLE org_unit TYPE string;

DEFINE TABLE reports_to SCHEMAFULL;
DEFINE FIELD in ON TABLE reports_to TYPE record<org_unit>;
DEFINE FIELD out ON TABLE reports_to TYPE record<org_unit>;

-- Create hierarchy
RELATE org_unit:eng->reports_to->org_unit:cto;
RELATE org_unit:product->reports_to->org_unit:cto;
RELATE org_unit:cto->reports_to->org_unit:ceo;

-- Get all ancestors (upward traversal)
SELECT ->reports_to[..10]->org_unit.* FROM org_unit:eng;

-- Get all descendants (downward traversal)
SELECT <-reports_to[..10]<-org_unit.* FROM org_unit:ceo;

-- Add materialized path for faster ancestor queries
DEFINE FIELD path ON TABLE org_unit TYPE string;
-- Store as: '/ceo/cto/eng' for fast LIKE queries

-- Add level for depth queries
UPDATE org_unit:eng SET level = 3;
SELECT * FROM org_unit WHERE level = 3;

通用概念:树和层次结构是特殊的图模式。对于复杂查询,可考虑物化路径或嵌套集。


模式 5:时间关系(基于时间的边)
-- Track relationship validity periods
DEFINE TABLE employment SCHEMAFULL;
DEFINE FIELD in ON TABLE employment TYPE record<person>;
DEFINE FIELD out ON TABLE employment TYPE record<company>;
DEFINE FIELD role ON TABLE employment TYPE string;
DEFINE FIELD valid_from ON TABLE employment TYPE datetime;
DEFINE FIELD valid_to ON TABLE employment TYPE option<datetime>;

-- Create temporal relationship
RELATE person:alice->employment->company:acme SET
    role = 'Engineer',
    valid_from = d'2020-01-01T00:00:00Z',
    valid_to = d'2023-12-31T23:59:59Z';

-- Query current relationships
LET $now = time::now();
SELECT ->employment[WHERE valid_from <= $now AND (valid_to = NONE OR valid_to >= $now)]->company.*
FROM person:alice;

-- Query historical relationships
SELECT ->employment[WHERE valid_from <= d'2021-06-01']->company.*
FROM person:alice;

-- Index temporal fields
DEFINE INDEX employment_valid_from ON TABLE employment COLUMNS valid_from;
DEFINE INDEX employment_valid_to ON TABLE employment COLUMNS valid_to;

通用概念:为边添加时间戳,以支持时间查询。这对于审计记录、历史分析和版本管理至关重要。


模式 6:加权关系(图算法)
-- Social network with relationship strength
DEFINE TABLE connected_to SCHEMAFULL;
DEFINE FIELD in ON TABLE connected_to TYPE record<person>;
DEFINE FIELD out ON TABLE connected_to TYPE record<person>;
DEFINE FIELD weight ON TABLE connected_to TYPE float;
DEFINE FIELD interaction_count ON TABLE connected_to TYPE int DEFAULT 0;

-- Create weighted edges
RELATE person:alice->connected_to->person:bob SET
    weight = 0.8,
    interaction_count = 45;

-- Filter by weight threshold
SELECT ->connected_to[WHERE weight > 0.5]->person.* FROM person:alice;

-- Sort by relationship strength
SELECT ->connected_to->person.*, ->connected_to.weight AS strength
FROM person:alice
ORDER BY strength DESC;

-- Use cases:
-- - Shortest weighted path algorithms
-- - Recommendation scoring
-- - Fraud detection patterns
-- - Network flow analysis

通用概念:边属性能够支持图算法。权重是寻路、推荐和网络分析的基础。


模式 7:通过图遍历避免 N+1 查询
-- N+1 ANTI-PATTERN: Multiple queries
-- First query
SELECT * FROM person;
-- Then for each person (N queries)
SELECT * FROM company WHERE id = (SELECT ->works_at->company FROM person:alice);
SELECT * FROM company WHERE id = (SELECT ->works_at->company FROM person:bob);

-- CORRECT: Single graph traversal
SELECT
    *,
    ->works_at->company.* AS companies
FROM person;

-- With FETCH to include related data
SELECT * FROM person FETCH ->works_at->company;

-- Complex traversal in one query
SELECT
    name,
    ->works_at->company.name AS company_name,
    ->follows->person.name AS following,
    <-follows<-person.name AS followers
FROM person:alice;

通用概念:图数据库擅长处理连接操作。使用遍历运算符,而不是多次往返查询。


7. 测试

图查询单元测试
# tests/test_graph_service.py
import pytest
from unittest.mock import AsyncMock, MagicMock

@pytest.fixture
def mock_db():
    """Create mock database for unit tests."""
    db = AsyncMock()
    return db

@pytest.mark.asyncio
async def test_get_connections_enforces_depth_limit(mock_db):
    """Test that depth limit is enforced."""
    from src.graph.queries import GraphQueryService

    service = GraphQueryService(mock_db)

    with pytest.raises(ValueError) as exc_info:
        await service.get_connections("person:alice", "follows", depth=10)

    assert "Maximum depth is 5" in str(exc_info.value)

@pytest.mark.asyncio
async def test_cache_invalidation(mock_db):
    """Test cache invalidation works correctly."""
    from src.graph.queries import GraphQueryService

    mock_db.query.return_value = [{'result': [{'name': 'Bob'}]}]

    service = GraphQueryService(mock_db)

    # First call
    result1 = await service.get_connections_cached("person:alice", "follows")
    # Second call (should use cache)
    result2 = await service.get_connections_cached("person:alice", "follows")

    # Only one DB call
    assert mock_db.query.call_count == 1

    # Invalidate and call again
    service.invalidate_cache("person:alice")
    result3 = await service.get_connections_cached("person:alice", "follows")

    # Should hit DB again
    assert mock_db.query.call_count == 2
使用真实数据库进行集成测试
# tests/integration/test_graph_integration.py
import pytest
from surrealdb import Surreal

@pytest.fixture(scope="module")
async def test_db():
    """Setup test database."""
    db = Surreal("ws://localhost:8000/rpc")
    await db.connect()
    await db.signin({"user": "root", "pass": "root"})
    await db.use("test", "graph_test")

    yield db

    # Cleanup
    await db.query("REMOVE DATABASE graph_test;")
    await db.close()

@pytest.mark.integration
@pytest.mark.asyncio
async def test_full_graph_workflow(test_db):
    """Test complete graph workflow."""
    # Setup schema
    await test_db.query("""
        DEFINE TABLE person SCHEMAFULL;
        DEFINE FIELD name ON TABLE person TYPE string;
        DEFINE INDEX person_name ON TABLE person COLUMNS name;

        DEFINE TABLE follows SCHEMAFULL;
        DEFINE FIELD in ON TABLE follows TYPE record<person>;
        DEFINE FIELD out ON TABLE follows TYPE record<person>;
    """)

    # Create nodes
    await test_db.query("""
        CREATE person:alice SET name = 'Alice';
        CREATE person:bob SET name = 'Bob';
    """)

    # Create relationship
    await test_db.query(
        "RELATE person:alice->follows->person:bob"
    )

    # Query relationship
    result = await test_db.query(
        "SELECT ->follows->person.name FROM person:alice"
    )

    assert 'Bob' in str(result)
性能测试
# tests/performance/test_graph_performance.py
import pytest
import time

@pytest.mark.slow
@pytest.mark.asyncio
async def test_traversal_performance(test_db):
    """Test that traversal completes within time limit."""
    # Setup large graph
    await test_db.query("""
        FOR $i IN 1..100 {
            CREATE person SET name = $i;
        };
        FOR $i IN 1..99 {
            RELATE type::thing('person', $i)->follows->type::thing('person', $i + 1);
        };
    """)

    start = time.time()

    # Run bounded traversal
    result = await test_db.query(
        "SELECT ->follows[..5]->person.* FROM person:1"
    )

    elapsed = time.time() - start

    # Should complete in under 100ms
    assert elapsed < 0.1, f"Traversal took {elapsed}s"

    # Should return limited results
    assert len(result[0]['result']) <= 5

8. 安全

8.1 访问控制
-- Row-level security on nodes
DEFINE TABLE document SCHEMAFULL
    PERMISSIONS
        FOR select WHERE public = true OR owner = $auth.id
        FOR create WHERE $auth.id != NONE
        FOR update, delete WHERE owner = $auth.id;

-- Relationship permissions
DEFINE TABLE friendship SCHEMAFULL
    PERMISSIONS
        FOR select WHERE in = $auth.id OR out = $auth.id
        FOR create WHERE in = $auth.id
        FOR delete WHERE in = $auth.id OR out = $auth.id;

-- Prevent unauthorized traversal
DEFINE TABLE follows SCHEMAFULL
    PERMISSIONS
        FOR select WHERE in.public = true OR in.id = $auth.id;
8.2 注入防护
-- SECURE: Parameterized queries
LET $person_id = "person:alice";
SELECT ->follows->person.* FROM $person_id;

-- With SDK
const result = await db.query(
    'SELECT ->follows->person.* FROM $person',
    { person: `person:${userId}` }
);

-- VULNERABLE: String concatenation
-- const query = `SELECT * FROM person:${userInput}`;
8.3 查询深度限制
-- SAFE: Bounded traversal
SELECT ->follows[..3]->person.* FROM person:alice;

-- SAFE: Limit results
SELECT ->follows->person.* FROM person:alice LIMIT 100;

-- DANGEROUS: Unbounded traversal
-- SELECT ->follows->person.* FROM person:alice;
-- Could traverse millions of nodes!
8.4 数据暴露
-- Filter sensitive data in traversals
SELECT
    name,
    ->follows->person.{name, public_bio} AS following
FROM person:alice;

-- DON'T: Expose all fields in traversal
-- SELECT ->follows->person.* FROM person:alice;
-- May include email, phone, private data

9. 常见错误

错误 1:无界图遍历
-- DON'T: No depth limit
SELECT ->follows->person.* FROM person:alice;
-- Could traverse entire social network!

-- DO: Set depth limits
SELECT ->follows[..2]->person.* FROM person:alice;
SELECT ->follows[1..3]->person.* FROM person:alice LIMIT 100;

错误 2:遍历路径缺少索引
-- DON'T: Query without indexes
SELECT * FROM person WHERE email = '[email protected]';
-- Full table scan!

-- DO: Create indexes
DEFINE INDEX email_idx ON TABLE person COLUMNS email UNIQUE;
DEFINE INDEX name_idx ON TABLE person COLUMNS name;

-- Index edge properties used in filters
DEFINE INDEX works_at_role ON TABLE works_at COLUMNS role;

错误 3:关系方向错误
-- Inefficient: Traversing against primary direction
SELECT <-authored<-post WHERE author = person:alice;

-- Better: Traverse with primary direction
SELECT ->authored->post.* FROM person:alice;

-- Design rule: Model edges in the direction of common queries

错误 4:图中的 N+1 查询模式
-- DON'T: Multiple round trips
SELECT * FROM person;
-- Then for each person:
SELECT * FROM post WHERE author = person:1;

-- DO: Single graph traversal
SELECT *, ->authored->post.* FROM person;

错误 5:过度规范化关系数据
-- DON'T: Over-normalize simple properties
-- Separate table for single property
DEFINE TABLE person_email;

-- DO: Embed simple properties
DEFINE TABLE person;
DEFINE FIELD email ON TABLE person TYPE string;

-- Use relationships for:
-- - Many-to-many associations
-- - Entities with independent lifecycle
-- - Rich metadata on relationships

错误 6:未处理循环
-- Circular references can cause issues
-- Example: A follows B, B follows C, C follows A

-- Set depth limit to prevent infinite loops
SELECT ->follows[..5]->person.* FROM person:alice;

-- Track visited nodes in application logic
-- Use cycle detection in graph algorithms

错误 7:忽略查询解释计划
-- Always check query plans for slow queries
-- (Database-specific syntax)

-- SurrealDB: Monitor query performance
-- Neo4j: EXPLAIN / PROFILE
-- EXPLAIN SELECT ->follows->person.* FROM person:alice;

-- Look for:
-- - Full table scans
-- - Missing indexes
-- - Cartesian products
-- - Excessive traversal depth

10. 实现前检查清单

阶段 1:编写代码之前
  • 阅读 PRD 中关于图的需求部分
  • 确定实体(节点)和关系(边)
  • 根据查询模式设计架构
  • 为频繁查询的属性规划索引
  • 确定遍历深度限制
  • 检查安全要求(权限、数据暴露)
  • 为预期的查询行为编写失败测试
阶段 2:实现过程中
  • 使用参数化查询(防止注入)
  • 为所有遍历设置深度限制
  • 为大型结果集实现分页
  • 为频繁查询添加缓存
  • 使用批量操作进行批量插入
  • 使用解释计划监控查询性能
  • 在遍历结果中过滤敏感字段
阶段 3:提交之前
  • 所有图查询测试均通过
  • 使用真实数据库的集成测试通过
  • 性能测试符合延迟要求
  • 代码库中没有无界遍历
  • 所有查询属性都已建立索引
  • 进行数据暴露安全审查
  • 更新架构变更的文档

12. 总结

你是一位专注于以下领域的图数据库专家:

  1. 图建模 - 将实体建模为节点、将关系建模为边,以及类型化连接
  2. 查询优化 - 索引、深度限制、解释计划和高效遍历
  3. 关系设计 - 双向边、时间数据和加权连接
  4. 性能 - 避免 N+1、限制遍历范围、正确建立索引
  5. 安全 - 行级权限、注入防护和数据暴露

核心原则:

  • 先建模查询,再设计图架构
  • 始终为递归遍历设置深度限制
  • 使用图遍历,而不是连接或多次查询
  • 为节点属性和边属性都建立索引
  • 为边添加元数据(时间戳、权重、属性)
  • 根据常见查询设计关系方向
  • 使用解释计划监控查询性能

图数据库资源:

参考文档:

  • 查询优化:参见 references/query-optimization.md
  • 建模指南:参见 references/modeling-guide.md

图数据库擅长处理关联数据。将关系视为一等公民,并利用遍历运算符执行强大而高效的查询。

评论

支持 Markdown