关系数据库与文档数据库使用比较分析


面向软件体系结构的关系数据库与面向文档数据库

我在这里经历的是:

  1. 超级快速复习这两个是什么
  2. 关键差异
  3. 优势和劣势
  4. 系统设计实例(+ Spring Java代码)
  5. 简史
在这些示例中,我在第一个示例中选择了关系型DB,在另一个示例中选择了面向文档的DB。重点是我为什么做出这样的选择。我还为这两种情况提供了一些示例代码。
在优点和缺点部分,我讨论了过去的优点/缺点以及现在的情况。


两种最常见的DB类型是:

  • 关系数据库(RDB):PostgreSQL,MySQL,MSSQL,Oracle DB,.
  • 面向文档的数据库(文档存储):MongoDB、DynamoDB、CouchDB.

RDB
关键思想是:将数据放入表中。列是属性,行是值。通过这样做,我们以一种非常结构化的方式获得数据。所以我们有很大的权力来查询数据(使用SQL)。也就是说,我们可以做各种各样的过滤器,关节等,我们安排数据到表中的方式称为数据库模式。

示例表

+----+---------+---------------------+-----+
| ID | Name    | Email               | Age |
+----+---------+---------------------+-----+
| 1  | Alice   | alice@example.com   | 30  |
| 2  | Bob     | bob@example.com     | 25  |
| 3  | Charlie | charlie@example.com | 28  |
+----+---------+---------------------+-----+

一个数据库可以有多个表。


文档存储

关键思想是:按原样存储数据。假设我们有一个对象。我们只是将其转换为JSON并按原样存储。我们把这些数据称为文档。它不限于JSON,也可以是BSON(二进制JSON)或XML。
示例文档

{ "user_id": 123, "name": "Alice", "email": "alice@example.com", "orders": [ {"id": 1, "item": "Book", "price": 12.99}, {"id": 2, "item": "Pen", "price": 1.50} ] }
每个文档都保存在一个唯一的ID下。此ID可以是路径,例如在Google Cloud Firestore中,但不必是路径。
许多文档“在同一个桶中”被称为集合。我们可以有很多收藏。
差异

模式

  • RDB有一个固定的模式。每一行都有相同的模式。
  • 文档存储区没有架构。每个文档都可以“有不同的模式”。
数据结构
  • RDB将数据分解为规范化的表,表中的关系通过外键实现
  • Document将嵌套相关数据直接存储在文档中作为嵌入对象或数组
查询语言
  • 关系数据库使用SQL,一种标准化的声明性语言
  • 文档存储通常有自己的查询API
    • 如今,常见的文档存储也支持类似SQL的查询
的评分办法
  • RDB传统上是垂直扩展的(更大/更好的机器)
    • 如今,最常见的RDB也提供了水平扩展(例如。PostgeSQL)
  • 文档存储非常适合水平扩展(更多机器)
事务支持

ACID =可用性、一致性、隔离性、持久性

  • RDB具有成熟的ACID事务支持
  • 文档存储传统上牺牲ACID保证来支持性能和可用性
    • 现在最常见的文档存储支持ACID(例如,MongoDB)

优缺点
关系数据库
我想在这里再次重复一些已经改变的事情。如前所述,现在大多数文档存储都支持SQL和ACID。同样,现在大多数RDB都支持水平扩展。

让我们以ACID为例。虽然文档存储支持它,但它在RDB中更加成熟。因此,如果你的应用程序在ACID上设置了超高的相关性,那么RDB可能会更好。但如果你的应用只需要基本的ACID,两者都很好用,这不应该是决定性因素。
因此,我把这两点放在括号里,这两点都得到了支持。

优势:

  • 数据完整性:强大的模式执行确保数据一致性
  • (复杂查询:非常适合跨多个表的复杂连接和聚合)
  • (酸)
缺点:
  • 模式:虽然模式被列为优势,但它也是一个弱点。更改模式需要进行迁移,这可能会很痛苦
  • 对象-关系阻抗不匹配:在应用程序对象和关系表之间进行转换增加了复杂性。Hibernate和其他对象关系映射(ORM)框架可以提供帮助。
  • (水平缩放:支持,但与文档存储相比,分片更复杂)
  • 初始开发速度:设置模式等需要一些时间

面向文档的数据库
优势:

  • 架构灵活性:更适合异构数据结构
  • 吞吐量:支持高吞吐量,尤其是写入吞吐量
  • (水平缩放:水平缩放更容易,您可以对文档进行分片(计算机A上的文档1-1000,计算机B上的文档1000-2000))
  • 基于文档的访问的性能:检索或更新整个文档非常有效
  • 一对多关系:在这方面是上级的。不需要连接或其他操作。
  • 地点:见下文
  • 初始开发速度:由于灵活性,启动速度更快
缺点:
  • 复杂的关系:多对一和多对多的关系很困难,通常需要反规范化或应用程序级的连接
  • 数据一致性:维护数据完整性的福尔斯责任更多地落在应用程序代码上
  • 查询优化:与关系系统相比,
  • 存储效率:潜在的数据重复会增加存储需求
  • 地点:见下文

局部性
我已经把局部性列为文件存储的优点和缺点。这就是我的意思。

在文档存储中,共时项通常存储为单个连续字符串,以JSON、XML或二进制变体(如MongoDB的BSON)等格式进行编码。当应用程序需要访问整个文档时,这种结构提供了局部性优势。

将相关数据存储在一起可以最大限度地减少磁盘寻道,这与关系数据库(RDB)不同,关系数据库(RDB)中的数据分散在多个表中-这需要多个索引查找,增加了检索时间。

但是,只有当我们需要(几乎)一次查看整个文档时,它才有好处。文档存储通常加载整个文档,即使只有一小部分被访问。这对于大型文档来说是低效的。同样,更新通常需要重写整个文档。因此,为了保持这些缺点小,请确保您的文档很小。

最后一点:局部性并不是文档存储所独有的。例如,Google Spanner或Oracle在关系模型中实现了类似的局部性。
系统设计示例

请注意,我将示例限制到最小,因此文章不会完全臃肿。代码是故意不完整的。您可以在repo的examples文件夹中找到完整的代码。

examples文件夹包含两个完整的应用程序:

  1. financial-transaction-system -使用关系数据库的Spring靴子和React应用程序(H2)
  2. content-management-system -使用面向文档的数据库(MongoDB)的Spring靴子和React应用程序
每个示例都有自己的README文件,其中包含运行应用程序的说明。

示例1:金融交易系统
要求

功能要求

  • 处理付款和转账
  • 保持准确的账户余额
  • 存储所有操作的审计跟踪
非功能性需求
  • 可靠性(!!)
  • 数据一致性(!!)

为什么这里的关系更好
我们需要可靠性和数据一致性。虽然文档存储也支持这一点(例如ACID),但它们在这方面还不太成熟。我们对文档存储的好处不感兴趣,所以我们使用RDB。

注意事项:如果我们扩展这个示例并添加卖家的配置文件,评级等内容,我们可能需要添加一个单独的数据库,其中我们有不同的优先级,例如可用性和高吞吐量。通过两个独立的数据库,我们可以支持不同的需求并独立扩展它们。

数据模型

Accounts:
- account_id (PK = Primary Key)
- customer_id (FK = Foreign Key)
- account_type
- balance
- created_at
- status

Transactions:
- transaction_id (PK)
- from_account_id (FK)
- to_account_id (FK)
- amount
- type
- status
- created_at
- reference_number


SpringBoot实现

// Entity classes
@Entity
@Table(name =
"accounts")
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long accountId;

    @Column(nullable = false)
    private Long customerId;

    @Column(nullable = false)
    private String accountType;

    @Column(nullable = false)
    private BigDecimal balance;

    @Column(nullable = false)
    private LocalDateTime createdAt;

    @Column(nullable = false)
    private String status;

   
// Getters and setters
}

@Entity
@Table(name =
"transactions")
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long transactionId;

    @ManyToOne
    @JoinColumn(name =
"from_account_id")
    private Account fromAccount;

    @ManyToOne
    @JoinColumn(name =
"to_account_id")
    private Account toAccount;

    @Column(nullable = false)
    private BigDecimal amount;

    @Column(nullable = false)
    private String type;

    @Column(nullable = false)
    private String status;

    @Column(nullable = false)
    private LocalDateTime createdAt;

    @Column(nullable = false)
    private String referenceNumber;

   
// Getters and setters
}

// Repository
public interface TransactionRepository extends JpaRepository<Transaction, Long> {
    List<Transaction> findByFromAccountAccountIdOrToAccountAccountId(Long accountId, Long sameAccountId);
    List<Transaction> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
}

// Service with transaction support
@Service
public class TransferService {
    private final AccountRepository accountRepository;
    private final TransactionRepository transactionRepository;

    @Autowired
    public TransferService(AccountRepository accountRepository, TransactionRepository transactionRepository) {
        this.accountRepository = accountRepository;
        this.transactionRepository = transactionRepository;
    }

    @Transactional
    public Transaction transferFunds(Long fromAccountId, Long toAccountId, BigDecimal amount) {
        Account fromAccount = accountRepository.findById(fromAccountId)
                .orElseThrow(() -> new AccountNotFoundException(
"Source account not found"));

        Account toAccount = accountRepository.findById(toAccountId)
                .orElseThrow(() -> new AccountNotFoundException(
"Destination account not found"));

        if (fromAccount.getBalance().compareTo(amount) < 0) {
            throw new InsufficientFundsException(
"Insufficient funds in source account");
        }

       
// Update balances
        fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
        toAccount.setBalance(toAccount.getBalance().add(amount));

        accountRepository.save(fromAccount);
        accountRepository.save(toAccount);

       
// Create transaction record
        Transaction transaction = new Transaction();
        transaction.setFromAccount(fromAccount);
        transaction.setToAccount(toAccount);
        transaction.setAmount(amount);
        transaction.setType(
"TRANSFER");
        transaction.setStatus(
"COMPLETED");
        transaction.setCreatedAt(LocalDateTime.now());
        transaction.setReferenceNumber(generateReferenceNumber());

        return transactionRepository.save(transaction);
    }

    private String generateReferenceNumber() {
        return
"TXN" + System.currentTimeMillis();
    }
}


系统设计示例2:内容管理系统

内容管理系统。
要求

  • 存储各种内容类型,包括文章和产品
  • 允许添加新内容类型
  • 支持评论
非功能性需求
  • 性能
  • 可用性
  • 弹性

为什么文档存储在这里更好
由于我们没有像上一个示例中那样的关键事务,而是只对性能、可用性和弹性感兴趣,因此文档存储是一个很好的选择。考虑到各种内容类型是一个需求,我们的生活更容易与文档存储,因为他们是模式少。

数据模型

// Article document
{
 
"id": "article123",
 
"type": "article",
 
"title": "Understanding NoSQL",
 
"author": {
   
"id": "user456",
   
"name": "Jane Smith",
   
"email": "jane@example.com"
  },
 
"content": "Lorem ipsum dolor sit amet...",
 
"tags": ["database", "nosql", "tutorial"],
 
"published": true,
 
"publishedDate": "2025-05-01T10:30:00Z",
 
"comments": [
    {
     
"id": "comment789",
     
"userId": "user101",
     
"userName": "Bob Johnson",
     
"text": "Great article!",
     
"timestamp": "2025-05-02T14:20:00Z",
     
"replies": [
        {
         
"id": "reply456",
         
"userId": "user456",
         
"userName": "Jane Smith",
         
"text": "Thanks Bob!",
         
"timestamp": "2025-05-02T15:45:00Z"
        }
      ]
    }
  ],
 
"metadata": {
   
"viewCount": 1250,
   
"likeCount": 42,
   
"featuredImage": "/images/nosql-header.jpg",
   
"estimatedReadTime": 8
  }
}

// Product document (completely different structure)
{
 
"id": "product789",
 
"type": "product",
 
"name": "Premium Ergonomic Chair",
 
"price": 299.99,
 
"categories": ["furniture", "office", "ergonomic"],
 
"variants": [
    {
     
"color": "black",
     
"sku": "EC-BLK-001",
     
"inStock": 23
    },
    {
     
"color": "gray",
     
"sku": "EC-GRY-001",
     
"inStock": 14
    }
  ],
 
"specifications": {
   
"weight": "15kg",
   
"dimensions": "65x70x120cm",
   
"material": "Mesh and aluminum"
  }
}

使用MongoDB实现SpringBoot

@Document(collection = "content")
public class ContentItem {
    @Id
    private String id;
    private String type;
    private Map<String, Object> data;

   
// Common fields can be explicit
    private boolean published;
    private Date createdAt;
    private Date updatedAt;

   
// The rest can be dynamic
    @DBRef(lazy = true)
    private User author;

    private List<Comment> comments;

   
// Basic getters and setters
}

// MongoDB Repository
public interface ContentRepository extends MongoRepository<ContentItem, String> {
    List<ContentItem> findByType(String type);
    List<ContentItem> findByTypeAndPublishedTrue(String type);
    List<ContentItem> findByData_TagsContaining(String tag);
}

// Service for content management
@Service
public class ContentService {
    private final ContentRepository contentRepository;

    @Autowired
    public ContentService(ContentRepository contentRepository) {
        this.contentRepository = contentRepository;
    }

    public ContentItem createContent(String type, Map<String, Object> data, User author) {
        ContentItem content = new ContentItem();
        content.setType(type);
        content.setData(data);
        content.setAuthor(author);
        content.setCreatedAt(new Date());
        content.setUpdatedAt(new Date());
        content.setPublished(false);

        return contentRepository.save(content);
    }

    public ContentItem addComment(String contentId, Comment comment) {
        ContentItem content = contentRepository.findById(contentId)
                .orElseThrow(() -> new ContentNotFoundException(
"Content not found"));

        if (content.getComments() == null) {
            content.setComments(new ArrayList<>());
        }

        content.getComments().add(comment);
        content.setUpdatedAt(new Date());

        return contentRepository.save(content);
    }

   
// Easily add new fields without migrations
    public ContentItem addMetadata(String contentId, String key, Object value) {
        ContentItem content = contentRepository.findById(contentId)
                .orElseThrow(() -> new ContentNotFoundException(
"Content not found"));

        Map<String, Object> data = content.getData();
        if (data == null) {
            data = new HashMap<>();
        }

       
// Just update the field, no schema changes needed
        data.put(key, value);
        content.setData(data);

        return contentRepository.save(content);
    }
}


RDBs vs NoSQL的简史

  • 埃德加·科德在1970年发表了一篇论文,提出了RDB
  • 关系数据库成为数据库的领导者,主要是由于其可靠性
  • NoSQL出现在2009年左右,Facebook和Google等公司开发了定制解决方案来处理其前所未有的规模。他们发表了关于内部数据库系统的论文,激发了MongoDB,Cassandra和Couchbase等开源替代品。
    • 这个词本身实际上来自Twitter的标签


“NoSQL愿望”的主要原因是:

  • 需要横向可扩展性
  • 更灵活的数据模型
  • 性能优化
  • 降低运营成本
然而,正如已经提到的,现在RDB也支持这些东西,所以RDB和文档存储之间的明显区别变得越来越模糊。大多数现代数据库都包含了这两种功能。