繁体   English   中英

从表创建嵌套数组的最佳方法:多查询/循环 VS 单查询/循环样式

[英]Best way to create nested array from tables: multiple queries/loops VS single query/loop style

假设我有 2 个表,我可以“合并”并表示在单个嵌套数组中。

我在徘徊什么是最好的方法,考虑到:

  • 效率
  • 最佳实践
  • 数据库/服务器端使用权衡
  • 你在现实生活中应该做什么
  • 可以以这种方式“合并”的 3、4 或更多表的相同情况

问题是关于任何服务器端/关系数据库。

我正在考虑的 2 种简单方法(如果你有其他方法,请提出建议!注意我要求一个简单的 SERVER-SIDE 和 RELATIONAL-DB ,所以请不要浪费时间解释为什么我不应该使用这种DB,使用MVC设计等等等等...):

  1. 2 个循环,5 个简单的“SELECT”查询
  2. 1 个循环,1 个“JOIN”查询

我试图给出一个简单而详细的例子,以解释我自己并更好地理解你的答案(尽管如何编写代码和/或发现可能的错误不是这里的问题,所以尽量不要关注那个.. .)

用于创建和插入数据到表的 SQL 脚本

CREATE TABLE persons
(
    id int NOT NULL AUTO_INCREMENT,
    fullName varchar(255),
    PRIMARY KEY (id)
);

INSERT INTO persons (fullName) VALUES ('Alice'), ('Bob'), ('Carl'), ('Dan');

CREATE TABLE phoneNumbers
(
    id int NOT NULL AUTO_INCREMENT,
    personId int,
    phoneNumber varchar(255),
    PRIMARY KEY (id)
);

INSERT INTO phoneNumbers (personId, phoneNumber) VALUES ( 1, '123-456'), ( 1, '234-567'), (1, '345-678'), (2, '456-789'), (2, '567-890'), (3, '678-901'), (4, '789-012');  

在我“合并”它们之后表的 JSON 表示:

[
  {
    "id": 1,
    "fullName": "Alice",
    "phoneNumbers": [
      "123-456",
      "234-567",
      "345-678"
    ]
  },
  {
    "id": 2,
    "fullName": "Bob",
    "phoneNumbers": [
      "456-789",
      "567-890"
    ]
  },
  {
    "id": 3,
    "fullName": "Carl",
    "phoneNumbers": [
      "678-901"
    ]
  },
  {
    "id": 4,
    "fullName": "Dan",
    "phoneNumbers": [
      "789-012"
    ]
  }
]

2 种方式的伪代码:

1.

query: "SELECT id, fullName FROM persons"
personList = new List<Person>()
foreach row x in query result:
    current = new Person(x.fullName)
    "SELECT phoneNumber FROM phoneNumbers WHERE personId = x.id"
    foreach row y in query result:
        current.phoneNumbers.Push(y.phoneNumber)
    personList.Push(current)        
print personList         

2.

query: "SELECT persons.id, fullName, phoneNumber FROM persons
            LEFT JOIN phoneNumbers ON persons.id = phoneNumbers.personId"
personList = new List<Person>()
current = null
previouseId = null
foreach row x in query result:
    if ( x.id !=  previouseId )
        if ( current != null )
            personList.Push(current)
            current = null
        current = new Person(x.fullName)
    current.phoneNumbers.Push(x.phoneNumber)
print personList            

PHP/MYSQL中的代码实现:

1.

/* get all persons */
$result = mysql_query("SELECT id, fullName FROM persons"); 
$personsArray = array(); //Create an array
//loop all persons
while ($row = mysql_fetch_assoc($result))
{
    //add new person
    $current = array();
    $current['id'] = $row['id'];
    $current['fullName'] = $row['fullName'];

    /* add all person phone-numbers */
    $id = $current['id'];
    $sub_result = mysql_query("SELECT phoneNumber FROM phoneNumbers WHERE personId = {$id}");
    $phoneNumbers = array();
    while ($sub_row = mysql_fetch_assoc($sub_result))
    {
        $phoneNumbers[] = $sub_row['phoneNumber']);
    }
    //add phoneNumbers array to person
    $current['phoneNumbers'] = $phoneNumbers;

    //add person to final result array
    $personsArray[] = $current;
}

echo json_encode($personsArray);

2.

/* get all persons and their phone-numbers in a single query */
$sql = "SELECT persons.id, fullName, phoneNumber FROM persons
            LEFT JOIN phoneNumbers ON persons.id = phoneNumbers.personId";
$result = mysql_query($sql); 

$personsArray = array();
/* init temp vars to save current person's data */
$current = null;
$previouseId = null;
$phoneNumbers = array();
while ($row = mysql_fetch_assoc($result))
{
    /*
       if the current id is different from the previous id:
       you've got to a new person.
       save the previous person (if such exists),
       and create a new one
    */
    if ($row['id'] != $previouseId )
    {
        // in the first iteration,
        // current (previous person) is null,
        // don't add it
        if ( !is_null($current) )
        {
            $current['phoneNumbers'] = $phoneNumbers;
            $personsArray[] = $current;
            $current = null;
            $previouseId = null;
            $phoneNumbers = array();
        }

        // create a new person
        $current = array();
        $current['id'] = $row['id'];
        $current['fullName'] = $row['fullName'];
        // set current as previous id
        $previouseId = $current['id'];
    }

    // you always add the phone-number 
    // to the current phone-number list
    $phoneNumbers[] = $row['phoneNumber'];
    }
}

// don't forget to add the last person (saved in "current")
if (!is_null($current))
    $personsArray[] = $current);

echo json_encode($personsArray);

PS 这个链接是一个不同问题的例子,我试图建议第二种方式: 表格到单个 json

初步的

首先,感谢您在解释问题和格式化方面付出的努力。 很高兴看到有人清楚他们在做什么,他们在问什么。

但必须注意的是,这本身就形成了一个限制:您坚信这是正确的解决方案,并且通过一些小的修正或指导,这将起作用。 那是不正确的。 所以我必须请你放弃那个观念,退后一大步,看看 (a) 整个问题和 (b) 我在没有这个观念的情况下的答案。

这个答案的上下文是:

  • 您给出的所有明确考虑都非常重要,我不会重复

  • 其中最重要的两个是,最佳实践我在现实生活中会做什么

这个答案植根于标准,即最佳实践的更高阶或参考框架。 这就是商业客户端/服务器世界所做的,或者应该做的。

这个问题,整个问题空间,正在成为一个普遍的问题。 我将在这里充分考虑,从而回答另一个 SO 问题。 因此,它可能包含您需要的更多细节。 如果是这样,请原谅。

考虑

  1. 数据库是基于服务器的资源,由许多用户共享。 在在线系统中,数据库是不断变化的。 它包含每个事实的一个版本的真相(不同于一个地方的一个事实,这是一个单独的规范化问题)。

    • 某些数据库系统没有服务器架构,因此此类软件中的服务器概念是错误的和具有误导性的,这一事实是单独但值得注意的要点。
  2. 据我了解,出于“性能原因”,需要 JSON 和类似 JSON 的结构,正是因为“服务器”不会也不能作为服务器执行。 这个概念是在每个(每个)客户端上缓存数据,这样您就不会一直从“服务器”获取它。

    • 这会打开一罐蠕虫。 如果您没有正确地设计和实现这一点,蠕虫就会使应用程序泛滥。

    • 这样的实现是对客户端/服务器架构的严重违反,它允许双方简单的代码,适当部署软件和数据组件,使得实现时间少,效率高。

    • 此外,这样的实现需要大量的实现工作,并且很复杂,由很多部分组成。 这些部分中的每一个都必须进行适当的设计。

    • 网络以及在该主题领域撰写的许多书籍提供了令人困惑的方法组合,以假定的简单性为基础进行营销; 舒适; 任何人都可以做任何事情; 免费软件可以做任何事情; 等等。任何这些建议都没有科学依据。

非建筑和次标准

正如所证明的那样,您已经了解到某些数据库设计方法是不正确的。 你曾经遇到过一个问题,一个实例该意见是错误的。 一旦你解决了这个问题,下一个你现在不明显的问题就会暴露出来。 这些概念是一系列永无止境的问题。

我不会列举有时提倡的所有错误观念。 我相信,随着您逐步完成我的回答,您会注意到一个又一个营销概念是错误的。

两条底线是:

  1. 这些概念违反了架构和设计标准,即客户端/服务器架构; 开放式架构 工程原理; 以及在这个特定问题中较小的数据库设计原则。

  2. 这会导致像您这样努力诚实工作的人被欺骗实施简单的概念,而这些概念变成了大规模的实施。 永远不会完全工作的实现,因此它们需要大量的持续维护,并且最终将被批发替换。

建筑学

被违反的中心原则是,永远不要复制任何东西。 一旦您有一个数据被复制的位置(由于缓存或复制或两个单独的单体应用程序等),您就会创建一个副本,该副本在在线情况下不同步。 所以原则是避免这样做。

  • 当然,对于严肃的第三方软件,例如 gruntly 报告工具,按照设计,它们很可能会在客户端缓存基于服务器的数据。 但请注意,他们已经投入了数百人年的时间来正确实施它,并适当考虑了上述情况。 你的不是这样的软件。

这个答案的其余部分没有提供必须理解的原则或每个错误的弊端和代价的讲座,而是提供了您在现实生活中会使用正确的架构方法(高于最佳实践的一步)所要求的内容.

架构 1

不要混淆

  • 必须归一化的数据

  • 结果集,根据定义,它是数据的扁平化(“非规范化”不太正确)视图。

数据,假设它是标准化的,将包含重复值; 重复组。 结果集包含重复值; 重复组。 那是行人。

  • 请注意,嵌套集(或嵌套关系)的概念在我看来并不是一个好的建议,它正是基于这种混淆。

  • RM出现以来的四十五年里,他们一直无法区分基础关系(规范化确实适用)和派生关系(规范化不适用)。

  • 其中两个支持者目前正在质疑第一范式的定义。 1NF 是其他 NF 的基础,如果新定义被接受,所有 NF 将变得毫无价值。 结果将是归一化本身(在数学术语中很少定义,但被专业人士清楚地理解为一门科学)即使没有被破坏也将受到严重破坏。

架构 2

有一个具有数百年历史的科学或工程原则,即内容(数据)必须与控制(程序元素)分开。 这是因为两者的分析、设计和实现是完全不同的。 这个原则在软件科学中同样重要,因为它有特定的表达方式。

为了保持这个简短(哈哈),而不是一个论述,我假设你理解:

  • 数据和程序元素之间存在科学要求的边界。 将它们混合在一起会导致容易出错且难以维护的复杂对象。

    • 这一原则的混乱在 OO/ORM 世界中已经达到了流行的程度,其后果影响深远。

    • 只有专业人士才能避免这种情况。 对于其余的人,绝大多数人,他们接受新定义为“正常”,他们一生都在解决我们根本没有的问题。

  • 根据 EF Codd 博士的关系模型,表格形式存储和呈现数据的架构优势和巨大价值。 数据规范化有特定的规则。

  • 重要的是,您可以确定撰写和销售书籍的人何时建议非关系或反关系方法。

架构 3

如果在客户端缓存数据:

  1. 缓存绝对最小值。

    这意味着只缓存在在线环境中不会改变的数据。 这意味着仅参考表和查找表、填充更高级别分类器的表、下拉列表等。

  2. 货币

    对于您缓存的每个表,您必须有一种方法来 (a) 确定缓存的数据与服务器上存在的单一版本的真相相比已经过时,以及 (b) 从服务器刷新它, (c) 逐表计算。

    通常,这涉及每 (e) 五分钟执行一次的后台进程,查询客户端上每个缓存表的 MAX 更新日期时间与服务器上的日期时间,如果更改,则刷新表及其所有子表,那些依赖于更改表的。

    当然,这要求您在每个表上都有一个UpdatedDateTime列。 这不是负担,因为无论如何您都需要 OLTP ACID 事务(如果您有一个真正的数据库,而不是一堆不合标准的文件)。

这真的意味着,永远不要复制,编码负担是令人望而却步的。

架构 4

在次商业、非服务器世界中,我理解有些人建议对“一切”进行反向缓存。

  • 这是像 PostgreSQL 这样的程序可以在多用户系统中使用的唯一方法。

  • 一分钱一分货:一分钱一分货,一分钱一分货; 你付零,你得到零。

架构 3 的推论是,如果您在客户端缓存数据,请不要缓存频繁更改的表。 这些是交易和历史表。 在客户端缓存此类表或所有表的想法完全破产了。

在真正的客户端/服务器部署中,由于使用适用标准,对于每个数据窗口,应用程序应仅查询所需的行,以满足特定需求,在特定时间,基于上下文或过滤器值等。该应用程序不应加载整个表。

如果同一用户使用同一窗口检查其内容,则在第一次检查后 15 分钟,数据将过期 15 分钟。

  • 对于免费软件/共享软件/蒸汽软件平台,它们通过没有服务器架构来定义自己,因此结果,该性能不存在,当然,您必须缓存比客户端上的最小表更多的表。

  • 如果你这样做,你必须考虑到以上所有因素,并正确实施它,否则你的应用程序将被破坏,并且后果将促使用户寻求你的终止。 如果有多个用户,他们就会有相同的原因,很快就会组成一支军队。

架构 5

现在我们开始了解如何在客户端缓存这些精心选择的表。

请注意,数据库会增长,它们会被扩展。

  • 如果系统被破坏、失败,它会以小增量增长,并且需要付出很多努力。

  • 如果这个系统即使是一个小小的成功,它也会成倍增长。

  • 如果系统(每个数据库和应用程序,分别)设计和实施得好,更改将很容易,错误将很少。

因此,应用程序中的所有组件都必须正确设计,以符合适用的标准,并且数据库必须完全规范化。 这反过来又最大限度地减少了数据库更改对应用程序的影响,反之亦然。

  • 该应用程序将由简单而非复杂的对象组成,这些对象易于维护和更改。

  • 对于您在客户端缓存的数据,您将使用某种形式的数组:OO 平台中一个类的多个实例; DataWindows (TM, google for it) 或 4GL 中的类似工具; PHP 中的简单数组。

旁白。请注意,像您这样的情况下的人在一年内生产的产品,使用商业 SQL 平台的专业提供商,商业 4GL,并遵守架构和标准。

架构 6

因此,让我们假设您了解上述所有内容,并欣赏其价值,尤其是架构 1 和 2。

  • 如果你不这样做,请在这里停下来提问,不要继续往下看。

现在我们已经建立了完整的上下文,我们可以解决您的问题的症结所在。

  • 在应用程序中的那些数组中,您究竟为什么要存储扁平化的数据视图?

    • 从而使问题变得混乱和痛苦
  • 而不是存储规范化表的副本?

回答

  1. 永远不要复制任何可以派生的东西。 这是一个架构原则,不限于数据库中的规范化。

  2. 永远不要合并任何东西。

    如果你这样做,你将创建

    • 客户端上的数据重复和大量数据。 客户端不仅会胖而且慢,还会被重复数据的压舱物固定在地板上。

    • 额外的代码,这是完全没有必要的

    • 该代码的复杂性

    • 脆弱的代码,必须不断更改。

    那就是你正在遭受的确切问题,是你直觉上知道是错误的方法的结果,必须有更好的方法。 您知道这是一个通用且常见的问题。

    还要注意那个方法,那个代码,构成了你的精神支柱。 看看您格式化它并如此精美地呈现它的方式:它对您很重要。 我不愿意告诉你这一切。

    • 哪一种不情愿很容易克服,因为你的态度认真、直率,而且知道这个方法不是你发明的
  3. 在每个代码段中,在演示时,根据需要:

    一种。 在商业客户端/服务器环境中
    执行连接简单、规范化、非重复表的查询,并仅检索符合条件的行。 从而获得当前数据值。 用户永远不会看到陈旧的数据。 在这里,经常使用视图(归一化数据的扁平视图)。

    在次商业非服务器环境中
    创建一个临时结果集数组,并连接简单的、不重复的数组(缓存的表的副本),并仅使用源数组中的合格行填充它。 其货币由后台进程维护。

    • 使用键来形成数组之间的连接,与使用键在数据库中的关系表中形成连接的方式完全相同。

    • 当用户关闭窗口时销毁这些组件。

    • 一个聪明的版本会消除结果集数组,并通过键连接源数组,并将结果限制为符合条件的行。

除了在架构上不正确之外,嵌套数组或嵌套集或 JSON 或类似 JSON 的结构不是必需的。 这是混淆架构 1 原则的结果。

  • 如果您确实选择使用此类结构,则将它们用于临时结果集数组。

最后,我相信这个论述表明n 个表不是问题。 更重要的是,数据层次结构中的m级深度,即“嵌套”,不是问题。

答案 2

现在我已经给出了完整的上下文(而不是之前),这消除了您问题中的含义,并使其成为通用的内核问题。

问题是关于任何服务器端/关系数据库。 [哪个更好]:

2 个循环,5 个简单的“SELECT”查询

1 个循环,1 个“JOIN”查询

你给出的详细例子上面没有准确描述。 准确的描述是:

  • 您的选项 1 2 个循环,每个循环用于加载每个数组 1 个单表 SELECT 查询每个循环(执行 nxm 次......仅最外层循环是一次执行)

  • 您的选项 2 1 Joined SELECT 查询执行一次,然后执行 2 个循环,每个循环用于加载每个数组

对于商业 SQL 平台,两者都不是,因为它不适用。

  • 商业 SQL 服务器是一个集合处理引擎。 使用一个带有任何需要的连接的查询,返回一个结果集。 永远不要使用循环遍历行,这会将集合处理引擎简化为 1970 年前的 ISAM 系统。 在服务器中使用视图,因为它提供了最高的性能并且代码在一个地方。

但是,对于非商业、非服务器平台,其中:

  • 您的“服务器”不是设置处理引擎,即。 它返回单行,因此您必须手动手动获取每一行并填充数组

  • 您的“服务器”提供客户机/服务器的结合,即。 它没有在客户端提供工具来将传入的结果集绑定到接收数组,因此您必须逐行逐行遍历返回的结果集,并手动填充数组,

根据您的示例,答案在很大程度上是您的选项 2。

请慎重考虑,评论或提问。

回复评论

假设我需要将此 json(或其他 html 页面)打印到某些 STOUT(例如:对:GET /allUsersPhoneNumbers 的 http 响应。这只是一个说明我期望得到的内容的示例),应该返回此 json。 我有一个 php 函数,它得到了这 2 个结果集 (1)。 现在它应该打印这个 json - 我应该怎么做? 该报告可以是员工一整年的月薪,等等。 一种或另一种方式,我需要收集这些信息并将其表示为“加入”表示

可能是我说的不够清楚。

  1. 基本上,除非绝对必要,否则不要使用 JSON。 这意味着发送到需要它的某个系统,这意味着接收系统,而这种需求是愚蠢的。

  2. 确保您的系统不会对其他人提出此类要求。

  3. 保持数据标准化。 无论是在数据库中,还是在您编写的任何程序元素中。 这意味着(在本例中)每个表或数组使用一个 SELECT。 这是用于加载目的,以便您可以在程序中的任何位置引用和检查它们。

  4. 当您需要加入时,请了解它是:

    • 结果集; 派生关系; 一个看法
    • 因此是临时的,它存在于该元素的执行期间,仅

    一种。 对于表,通过键以通常的方式加入它们。 一个查询,连接两个(或多个)表。

    对于数组,在程序中连接数组,就像通过键连接数据库中的表一样。

  5. 对于你给出的例子,它是对某个请求的响应,首先理解它是类别[4],然后实现它。

为什么还要考虑 JSON? JSON 与此有什么关系?

JSON 被误解了,人们对令人惊叹的因素感兴趣。 这是一个寻找问题的解决方案。 除非你有那个问题,否则它没有价值。 检查这两个链接:
直升机 - 什么是 JSON
StackOverflow - 什么是 JSON

现在,如果您明白这一点,它主要用于传入的提要。 从不为外向。 此外,在使用之前,它需要解析、解构等。

记起:

我需要收集这些信息并用“JOIN”ed 表示

是的。 那是行人。 加盟并不意味着JSONed。

在您的示例中,接收者期待一个扁平化视图(例如电子表格),所有单元格都已填充,是的,对于具有多个电话号码的用户,他们的用户详细信息将在第二个 nad 后续结果集行中重复。 对于任何类型的print,例如。 为了调试,我想要一个扁平化的视图。 它只是一个:

    SELECT ... FROM Person JOIN PhoneNumber

并返回。 或者,如果您满足来自数组的请求,请加入 Person 和 PhoneNumber 数组,这可能需要一个临时结果集数组,然后返回该数组。

请不要告诉我您一次只能获得 1 个用户,等等。

正确的。 如果有人告诉您回归到程序处理(即逐行,在 WHILE 循环中),其中引擎或您的程序已设置处理(即在一个命令中处理整个集合),这将他们标记为应该不被倾听。

我已经说过,你的选项 2 是正确的,选项 1 是错误的。 这是就 GET 或 SELECT 而言的。

另一方面,对于没有设置处理能力(即不能在单个命令中打印/设置/检查数组)的编程语言,或不提供客户端数组绑定的“服务器”,您确实有编写循环,数据层次结构的每个深度一个循环(在您的示例中,两个循环,一个用于个人,一个用于每个用户的电话号码)。

  • 您必须这样做才能解析传入的 JSON 对象。
  • 您必须这样做才能从选项 2 中返回的结果集中加载每个数组。
  • 您必须这样做才能从选项 2 中返回的结果集中打印每个数组。

对评论 2 的回应

我已经提到我必须返回一个以嵌套版本表示的结果(假设我正在将报告打印到页面),json 只是这种表示的一个例子。

我认为你没有理解我在这个答案中提供的推理和结论。

  • 对于打印和显示,永远不要嵌套 打印一个扁平化的视图,每个选项 2 从 SELECT 返回的行。这就是 31 年来我们一直在做的事情,当打印或显示关系数据时。 更容易阅读、调试、搜索、查找、折叠、装订、切割。 你不能嵌套数组任何事情,除非看看它,然后说很有趣

代码

警告

我更愿意拿你的代码修改它,但实际上,看看你的代码,它写得不好,结构也不好,不能合理修改。 其次,如果我使用它,那将是一个糟糕的教学工具。 所以我必须给你新鲜、干净的代码,否则你将学不到正确的方法。

此代码示例遵循我的建议,因此我不打算重复。 这远远超出了最初的问题。

  • 查询打印

    您的请求,使用您的选项 2。一个 SELECT 执行一次。 接下来是一个循环。 如果你愿意,你可以“漂亮”。

通常,最佳做法是在尽可能少的访问数据库中获取所需的数据,然后将数据映射到适当的对象中。 (选项 2)

但是,要回答您的问题,我会问自己您的数据的用例是什么。 如果您确定需要您的人和您的电话号码数据,那么我会说第二种方法是您的最佳选择。

但是,当加入的数据是可选的时,选项一也可以有它的用例。一个例子可能是在 UI 上你有一个所有人员的表格,如果用户想要查看特定人员的电话号码,他们必须点击那个人。 然后可以“延迟加载”所有电话号码。

这是一个常见的问题,特别是如果您正在创建 WebAPI,将这些表集转换为嵌套数组是一件大事。

我总是为你选择第二个选项(尽管方法略有不同),因为第一个是最糟糕的方法......我从我的经验中学到的一件事是永远不要在循环内查询,这是对数据库调用的浪费,你知道我想说什么。

虽然我不接受 PerformanceDBA 所说的所有内容,但有两个主要的事情我需要地址, 1. 不要有重复的数据 2. 只获取你想要的数据

我在加入表格中看到的唯一问题是,我们最终复制了很多数据,以您的数据为例,加入个人和电话号码表,我们最终复制了每个人的每个电话号码,对于两个表格,几百行很好,想象一下我们需要将 5 个表与数千行合并在一起......所以这是我的解决方案:
询问:

SELECT id, fullName From Person;
SELECT personId, phoneNumber FROM phoneNumbers 
WHERE personId IN (SELECT id From Person);

所以我得到了结果集中的表格,现在我将 Table[0] 分配给我的 Person 列表,并使用 2 个循环将正确的 phoneNumbers 放入正确的人......

代码:

personList = ConvertToEntity<List<Person>>(dataset.Table[0]);    
pnoList = ConvertToEntity<List<PhoneNumber>>(dataset.Table[1]);

    foreach (person in personList) {
        foreach (pno in pnoList) {
            if(pno.PersonId = person.Id)
                person.PhoneNumer.Add(pno)
        }
    }

我认为上述方法减少了很多重复,只能得到我想要的东西,如果上述方法有任何缺点,请告诉我......感谢你提出这些问题......

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM