65.9K
CodeProject 正在变化。 阅读更多。
Home

那不是数据库,伙计!

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (15投票s)

2022年12月1日

CPOL

12分钟阅读

viewsIcon

19593

你可以用很多东西来存储信息,但并非所有这些都是数据库。而试图将它们用作数据库是一个非常非常糟糕的主意。

引言

我们都存储数据,我们都希望存储是安全、可靠且易于使用的。

背景:规模问题

让我们暂时忽略电脑,看看当事情开始扩大规模时现实世界中会发生什么。

你和一位朋友开了一家公司销售“新改进小部件”,有时你需要为办公室购买一些小东西:咖啡、糖、牛奶、纸、订书钉、一些零星的软件套餐、支付给出租车司机的费用以拜访客户——这些“零零碎碎”的开销维持着公司的正常运转,它们并非小部件核心业务的必需品,但没有它们公司就无法很好地运作。虽然涉及的钱不多,但公司需要支付(否则你或你的朋友会不公平地自掏腰包),而且你确实想把它列入“开支”项,这样你就不用为此向政府缴税了。嗯,你肯定不想给他们比他们应得的更多,对吧?

所以你做了所有小公司都会做的事:你买了一个零钱罐,并把一些现金放进去。你需要订书钉吗?拿一些现金,在回家的路上买它们,把收据和零钱放进罐子里。简单。

这很有效:每个月左右,你都会添加更多的现金,并将收据放在一边,供会计师处理。

公司发展得很好。你现在雇佣了5个人。这没问题——你很信任他们——所以你只做了一个小小的改变:你和你的朋友都有罐子的钥匙。公司需要订书钉,你给乔一些现金,提醒他要收据,然后第二天早上把零钱拿回来。这仍然是同一个系统,它仍然有效。

一年后,你有了100名员工,这个系统已经濒临崩溃:这个罐子现在放在保险箱里,因为它里面有一些重要的钱,你不想在公司关闭时被偷。你有一个本子,上面记录了你给了哪个员工多少钱,用途是什么,以及何时收到了收据和零钱——因为否则你会忘记谁欠你零钱。但你很确定调度部门的迈克已经改动了几次并把差额私吞了,你不是很信任他。虽然你无法证明,而且他工作表现不错,但是……我们还是消除偷窃的诱惑吧,好吗?这个本子也放在保险箱里,所以只有你和你的朋友可以编辑它。

十年后,“小部件”业务真的蓬勃发展!10,000名员工,八个国家的十二个基地,以及……一个巨大的零钱罐,光是处理它就占据了你大部分时间。事实上,大约有100名员工专门负责处理零钱罐里不同货币的账目,并仔细核对每笔交易,因为每月流经它的钱足以买一套漂亮的房子!

这就是规模问题:在小规模上有效的方法,在大规模上就变得无法操作,无论你如何包装它,无论你如何“棍棒伺候”。它就是不适合新的情况。

大公司不用零钱罐!

我们来说说文件吧

有些读者可能会惊讶地发现,计算机上只有一种文件类型:二进制数据。其他一切都是由处理文件的软件在二进制数据之上提供的“层”。

记住:一切都是二进制数据,包括可执行文件、文本文件、数据库、图像、JSON、XML、CSV。即使人类可以以某种方式阅读它(文本、JSON、XML、PDF、HTML、CS、PHP、VB等),最基本的一切都是二进制的,而以可读方式呈现它的行为则取决于应用程序对该二进制数据的解释。

例如,文本文件包含有限数量的二进制数据值(“可打印”字符)以及一些特殊的“控制代码”,这些代码告诉读取器行在哪里结束以及制表符从哪里开始:例如,Windows 文件使用两个特殊的字符 CR 和 LF 一起表示行尾。如果你有这样一个文件:

那么,如果你在十六进制文件编辑器中查看它,它看起来像这样:

“新顶行”只是行内十六进制的二进制字节地址,数据从“4C”开始,这是我们解释为大写“L”的十六进制值。沿着该行查看,你会看到一个“0D”和一个“0A”——这就是我所说的两个 CR 和 LF 特殊字符。右侧是“文本阅读器”版本,它将“不可打印字符”替换为“.”。

这很重要:这意味着文本文件中不存在“基于行”的格式。行是对二进制数据的解释,而不是文本文件的内在特征。这意味着行甚至不会以相同的空间量存储在文件中:更长的行意味着 CR/LF 左侧的字符更多,文件也更大。

更糟糕的是,要在“第1行”和“第2行”之间添加一行,我们需要将“第1行”末尾之前的所有数据(包括CR和LF)复制到一个新文件,将我们的新行连同新的CR/LF组合写入新文件,然后将旧文件中之后的所有数据复制到新文件,最后关闭两个文件,删除原始文件,并重命名新文件。而这正是你将“第1行”更改为“Hello World!”时也需要做的过程。如果你只是以读/写访问方式打开文件,写入一个新行并保存文件,你将用你的新数据覆盖第二行的部分内容。

你无法在文本文件中插入或删除任何数据而不重写整个文件,它没有行的概念!

这与你所说的“规模问题”无关!

不是吗?

实际上,它有关联。当您使用文本文件存储数据时:每项数据都在一个新“行”上,它运行良好——您和您的应用程序都很容易阅读,而且它非常健壮。零钱罐运行良好!但是……如果您开始改变处理数据文件的方式,那么事情就会变得非常复杂。

在文本文件中插入两行之间的新行,删除文本文件中的一行:这两者都需要仔细的工作:读取文件直到插入点(插入点不在固定位置,因此您必须搜索文件中的数据才能找到它),同时将数据复制到新文件;将新行插入新文件(用于插入)或跳过到要删除的行尾(用于删除),然后复制原始文件的其余部分。删除输入文件,重命名输出文件。零钱罐现在在保险箱里。

你可以做到——它没有那么复杂——但必须小心翼翼地完成,而且如果你正在处理多个用户访问文件的情况,你的软件必须能够应对这种情况以及文件在你读取时可能发生变化的情况……现在有一个完整的部门在照看那个零钱罐!

这和我的数据有什么关系?

好问题!

答案很简单:一切。

文本本身并不是特别有用——对于用户阅读来说没问题,但如果你想让计算机读取它,它通常需要更多的结构。所以大多数应用程序不使用“原始”文本文件,它们添加了一层抽象,应用程序用它来可靠地恢复数据。

policyID,statecode,county,eq_site_limit,hu_site_limit,fl_site_limit,
fr_site_limit,tiv_2011,tiv_2012,eq_site_deductible,hu_site_deductible,
fl_site_deductible,fr_site_deductible,point_latitude,point_longitude,
line,construction,point_granularity
119736,FL,CLAY COUNTY,498960,498960,498960,498960,498960,792148.9,0,
9979.2,0,0,30.102261,-81.711777,Residential,Masonry,1
448094,FL,CLAY COUNTY,1322376.3,1322376.3,1322376.3,1322376.3,
1322376.3,1438163.57,0,0,0,0,30.063936,-81.707664,Residential,Masonry,3
206893,FL,CLAY COUNTY,190724.4,190724.4,190724.4,190724.4,190724.4,
192476.78,0,0,0,0,30.089579,-81.700455,Residential,Wood,1 
[
 {
   "policyID": 119736,
   "statecode": "FL",
   "county": "CLAY COUNTY",
   "eq_site_limit": 498960,
   "hu_site_limit": 498960,
   "fl_site_limit": 498960,
   "fr_site_limit": 498960,
   "tiv_2011": 498960,
   "tiv_2012": 792148.9,
   "eq_site_deductible": 0,
   "hu_site_deductible": 9979.2,
   "fl_site_deductible": 0,
   "fr_site_deductible": 0,
   "point_latitude": 30.102261,
   "point_longitude": -81.711777,
   "line": "Residential",
   "construction": "Masonry",
   "point_granularity": 1
 },
 {
   "policyID": 448094,
   "statecode": "FL",
   "county": "CLAY COUNTY",
   "eq_site_limit": 1322376.3,
   "hu_site_limit": 1322376.3,
   "fl_site_limit": 1322376.3,
   "fr_site_limit": 1322376.3,
   "tiv_2011": 1322376.3,
   "tiv_2012": 1438163.57,
   "eq_site_deductible": 0,
   "hu_site_deductible": 0,
   "fl_site_deductible": 0,
   "fr_site_deductible": 0,
   "point_latitude": 30.063936,
   "point_longitude": -81.707664,
   "line": "Residential",
   "construction": "Masonry",
   "point_granularity": 3
 },
 {
   "policyID": 206893,
   "statecode": "FL",
   "county": "CLAY COUNTY",
   "eq_site_limit": 190724.4,
   "hu_site_limit": 190724.4,
   "fl_site_limit": 190724.4,
   "fr_site_limit": 190724.4,
   "tiv_2011": 190724.4,
   "tiv_2012": 192476.78,
   "eq_site_deductible": 0,
   "hu_site_deductible": 0,
   "fl_site_deductible": 0,
   "fr_site_deductible": 0,
   "point_latitude": 30.089579,
   "point_longitude": -81.700455,
   "line": "Residential",
   "construction": "Wood",
   "point_granularity": 1
 }
]
<!--?xml version="1.0" encoding="UTF-8" ?-->
<root>
  <row-0>
    <policyid>119736</policyid>
    <statecode>FL</statecode>
    <county>CLAY COUNTY</county>
    <eq_site_limit>498960</eq_site_limit>
    <hu_site_limit>498960</hu_site_limit>
    <fl_site_limit>498960</fl_site_limit>
    <fr_site_limit>498960</fr_site_limit>
    <tiv_2011>498960</tiv_2011>
    <tiv_2012>792148.9</tiv_2012>
    <eq_site_deductible>0</eq_site_deductible>
    <hu_site_deductible>9979.2</hu_site_deductible>
    <fl_site_deductible>0</fl_site_deductible>
    <fr_site_deductible>0</fr_site_deductible>
    <point_latitude>30.102261</point_latitude>
    <point_longitude>-81.711777</point_longitude>
    <line>Residential</line>
    <construction>Masonry</construction>
    <point_granularity>1</point_granularity>
  </row-0>
  <row-1>
    <policyid>448094</policyid>
    <statecode>FL</statecode>
    <county>CLAY COUNTY</county>
    <eq_site_limit>1322376.3</eq_site_limit>
    <hu_site_limit>1322376.3</hu_site_limit>
    <fl_site_limit>1322376.3</fl_site_limit>
    <fr_site_limit>1322376.3</fr_site_limit>
    <tiv_2011>1322376.3</tiv_2011>
    <tiv_2012>1438163.57</tiv_2012>
    <eq_site_deductible>0</eq_site_deductible>
    <hu_site_deductible>0</hu_site_deductible>
    <fl_site_deductible>0</fl_site_deductible>
    <fr_site_deductible>0</fr_site_deductible>
    <point_latitude>30.063936</point_latitude>
    <point_longitude>-81.707664</point_longitude>
    <line>Residential</line>
    <construction>Masonry</construction>
    <point_granularity>3</point_granularity>
  </row-1>
  <row-2>
    <policyid>206893</policyid>
    <statecode>FL</statecode>
    <county>CLAY COUNTY</county>
    <eq_site_limit>190724.4</eq_site_limit>
    <hu_site_limit>190724.4</hu_site_limit>
    <fl_site_limit>190724.4</fl_site_limit>
    <fr_site_limit>190724.4</fr_site_limit>
    <tiv_2011>190724.4</tiv_2011>
    <tiv_2012>192476.78</tiv_2012>
    <eq_site_deductible>0</eq_site_deductible>
    <hu_site_deductible>0</hu_site_deductible>
    <fl_site_deductible>0</fl_site_deductible>
    <fr_site_deductible>0</fr_site_deductible>
    <point_latitude>30.089579</point_latitude>
    <point_longitude>-81.700455</point_longitude>
    <line>Residential</line>
    <construction>Wood</construction>
    <point_granularity>1</point_granularity>
  </row-2>
</root>

你可以阅读这些——JSON 很容易,XML 稍微难一点,CSV 有点困难——对于应用程序来说,读取或写入任何一个都非常容易。

但插入?删除?更新?你有两种选择:

  • 使用理解源格式的包将整个文件读入内存,并将其扩展为您的应用程序理解的对象,然后添加您的新对象,删除或修改有问题的对象,然后再次使用该包将所有内容写回一个新文件。
  • 或者,编写代码读取格式化数据并确定您的更改应该在哪里进行(同时将数据写入新文件),根据需要进行修改并写入,复制文件的其余部分,关闭两者,然后删除/重命名,就像您处理文本文件中的一行一样,但问题更严重!您的应用程序需要非常了解 JSON(或 XML,或 CSV,或……有许多格式),以及如何在文本文件中插入字符串,并且您编写的代码也要能做到这些!

这两种选择都效率低下,都不简单,而且都充满了潜在问题。

JSON 和 XML 被设计成数据传输格式,而不是数据存储格式——这是一个非常不同的概念。它们都被格式化为人类可读和可编辑(在一定程度上),并提供与编程语言无关的数据传输。当你将 JSON 或 XML 读入你的应用程序时,它会被转换成你可以使用的类和变量,就好像数据是在你的应用程序中写入的一样——无论你的应用程序是用什么语言编写的。所以它们“知道”整数、浮点数、字符串、日期和数组——所有现代语言提供的基本元素。而且大多数现代语言都有可用的包,让你只需几行代码就能读取和写入 JSON 和 XML。

但是……那是复杂的分层数据,这也是上段中“在一定程度上”的警告语的原因。是的,你可以手动编辑它们——但如果你这样做,就很容易把整个文件搞砸!而且,通过你的应用程序以“纯文本”方式编辑 JSON 或 XML 来插入和删除项目是极其复杂的——你的应用程序需要自己理解并能够处理 JSON 或 XML,那可是一大堆工作。

这就是问题的关键:如果你想修改基于行的数据或基于表格的数据,那么你需要使用一种设计来轻松处理此类存储的格式。

太棒了!我用Excel!

是的,你可以将数据存储在电子表格中。它们在这方面相当不错。是的,它们是基于行和列的。是的,你的应用程序可以与Excel引擎进行接口。但是……当电子表格变得很大时,它们会变得很慢——非常慢——而且难以操作。这是为什么呢?

部分原因在于它们存储数据的方式:XLSX 文件是一个包含多个 XML 文件的 ZIP 存档,这些 XML 文件包含数据。因此,每次你插入一行时,相应的 XML 文件都必须用新数据重写,然后整个内容再次压缩,并写入一个带有 XLSX 扩展名的新 ZIP 文件。

这又是 JSON / XML / CSV 的那一套方法,只不过稍微隐藏起来了!

我也和大家一样犯过同样的错误:当我需要一个“快速数据源”时,我就会拿出我的电子表格并填写数据——我甚至会把它读入我的应用程序进行处理。但当它变得庞大,或者我需要更好的处理时,我就会转换到数据库,并编写一个快速的“输入应用程序”来加载/更改数据,因为我知道如果不这样做我会面临什么问题。

这正好引出了本文的标题。

那不是数据库,伙计!

或者更准确地说,“数据库正是你需要使用的东西”。

数据库不以文本形式存储:它们使用一种二进制格式,在文件中为行分配内存,允许该空间增长和收缩,当你删除一行时将内存释放回内存池,以及许多其他功能,这些功能使基于行的数据操作变得像“插入这个……”,“删除那个……”,以及“将那个改成……”一样简单。

你的代码与数据库引擎进行接口(我承认,这可能需要一点时间才能理解),并发出 SQL 命令,这些命令可以像读取所有行中的所有数据一样简单。

SELECT * FROM MyTable

或添加一行

INSERT INTO MyTable (FirstName, Surname) VALUES ('Mike', 'Smith')

直到一些真正复杂的代码,将十几个表的数据组合成一个单一的汇总表。

看起来很复杂,但基础真的非常简单:《W3Schools SQL 教程》对此解释得很好,而且大多数现代语言/框架都提供了包,让你可以通过代码以简单一致的方式直接访问数据库。

简而言之

在小规模上有效的数据处理方式,一旦尝试扩展,就会灾难性地失败,特别是如果你最初是从“这很容易做到”的角度出发。

文本文件非常适合作为应用程序的初始值和类似的“小型稳定数据”应用程序——`*.INI` 和 `*.CONFIG` 很常见。

结构化数据非常适用于数据变化不大的更复杂的数据应用,如果数据确实发生变化,也总是将数据追加到现有文件的末尾——`.CSV`、`.JSON`、`.XML` 和 `.XLSX` 都有其各自的优点。

但是,如果你知道你需要更改数据、删除部分、重新排序数据,或者进行任何比读取更复杂的处理,那么你真的,真的需要考虑使用数据库。从长远来看,它将为你节省大量时间!

历史

  • 2022年12月1日:第一个版本
  • 2022年12月3日:修正了打字错误并改进了描述,感谢 PIEBALDconsult。
© . All rights reserved.