市场模拟(第 23 部分):SQL 入门(六)
概述
在上一篇文章 市场模拟(第 22 部分):SQL 入门(五) 中,我们探讨了如何使用主键,或者更准确地说,如何将主键与外键相关联。如果这还不清楚,我建议先阅读之前的文章。为了充分理解我们在此要做的工作,了解这一背景是必要的。
理解查询结果
在上一篇文章中,我们创建了一个小型数据库,以说明主键和外键的使用。然而,当我们查询此配置时,我们很可能会得到一个与我们预期大相径庭的结果。为了更清楚地说明这一点,我们将使用以下代码来查询数据库。
SELECT * FROM tb_Quotes;
代码 01
结果如下所示。

图 01
此时,你可能会问自己:“我做错了什么?”答案是:没有。许多人避免在数据库中使用外键,正是因为他们不了解这种键背后的概念。因此,他们最终选择了创建非关系型数据库,这会让数据库的维护以及整体结构都变得更加复杂。这也增加了 SQL 需要处理的数据量和整体负担。
SQL 的一大优势在于,它允许我们创建看似独立的表,同时又能将它们关联起来 — 或者更准确地说,是将存储在表中的值关联起来。这使我们能够避免 SQL 出现之前最严重的问题之一:数据重复。你可能还没有意识到重复对数据库的危害有多大,但它实际上会抵消掉在为应用程序构建数据库时使用 SQL 的主要优势之一。
数据库图可视化
有多种工具可用于数据库图表的可视化,它们各自具有不同的功能集。在我们这里用来解释某些要点的 DB Browser 中,至少在撰写本文时,无法生成此类图表。由于 DB Browser 是开源的,并且通过 GitHub 接收更新,因此当你阅读本文时,此功能可能已经可用。另一种方法是使用 DBeaver 。该程序有免费版本,可以轻松下载和使用。DBeaver 安装完成后,请按照以下步骤查看图表:

图 02
这将打开一个新窗口,如下图所示。

图 03
这里我们需要选择要显示的数据库类型。请记住,我们使用的是 SQLite。找到并选择它,如下图所示。

图 04
很好,现在我们将看到以下窗口。

图 05
在此处,您需要输入要打开的数据库的路径和名称,如下所示。

图 06
之后,点击“完成”,最终屏幕将会出现。

图 07
如果一切操作都正确,结果将与图 07 所示类似,此时我们的数据库已准备就绪,可供使用。我们现在可以通过点击下方高亮显示的项目来打开图表:

图 08
将会打开如下所示的窗口:

图 09
现在,为新图表命名,如图所示。你可以选择任何你想要的名称。

图 10
最后,点击“完成”按钮后,我们得到以下结果:

图 11
解读图表
图 11 清晰地显示了 tb_Quotes 表和 tb_Symbols 表之间的关系。起初,你可能会认为这是一种一对一的关系,即 tb_Symbols 中的每一条记录在 tb_Quotes 中都有一条对应的记录。在某些情况下,这种情况可能会发生。然而,请注意图表中的一个细节。它很微妙,但很重要,下面的图片恰好突出了你应该关注的内容。

图 12
注意,一侧有一个小圆圈,而另一侧则没有这样的标记。请密切关注这一点。这是什么意思?它表示一种一对多的关系。换句话说,带圆圈一侧的表中,可能对应另一侧表中每一条记录的多条记录。了解如何正确解读这一点有助于我们编写更优质的查询。它还使数据库的更改变得更加容易,且不易出错。
这很重要,因为当一个表中的记录可以在一个或多个其他表中多次被引用时,我们能够更好地理解和设计用于添加新数据、删除记录或更新数据库信息的触发器。但这是另一个讨论的话题。在此阶段,我们需要了解这种关系如何帮助我们正确可视化数据库内容。
在 SQL 中处理数据
到目前为止,我们只研究了如何创建数据库图表。现在我们将返回 DB Browser。请记住,虽然查询数据库所用的代码略有不同,但 MetaEditor 也可以用来获得此处所示的相同结果。相关代码如下所示:
01. PRAGMA FOREIGN_KEYS = ON; 02. 03. DROP TABLE IF EXISTS tb_Quotes; 04. DROP TABLE IF EXISTS tb_Symbols; 05. 06. CREATE TABLE IF NOT EXISTS tb_Symbols 07. ( 08. id INTEGER PRIMARY KEY, 09. symbol TEXT NOT NULL UNIQUE 10. ); 11. 12. CREATE TABLE IF NOT EXISTS tb_Quotes 13. ( 14. of_day TEXT NOT NULL, 15. price NUMERIC NOT NULL, 16. fk_id INTEGER NOT NULL, 17. FOREIGN KEY (fk_id) REFERENCES tb_Symbols(id) 18. ); 19. 20. INSERT INTO tb_Symbols (id, symbol) VALUES(1, 'BOVA11'); 21. INSERT INTO tb_Symbols (id, symbol) VALUES(3, 'PETR4'); 22. INSERT INTO tb_Symbols (id, symbol) VALUES(2, 'WDOQ23'); 23. INSERT INTO tb_Symbols (id, symbol) VALUES(4, 'VALE3'); 24. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('17-07-2023', 12.90, 4); 25. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('14-07-2023', 118.12, 2); 26. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('13-07-2023', 119.53, 1); 27. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('12-07-2023', 117.45, 2); 28. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('11-07-2023', 119.30, 3); 29. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('10-07-2023', 120.59, 1); 30. 31. SELECT * FROM tb_Quotes, tb_Symbols;
代码 02
执行此脚本时,SQL 返回 24 条记录。为什么呢?我们来看看发生了什么。请看下面图片中代码产生的输出结果:

图 13
您可以看到,第 17 行到第 20 行中,相同的报价数值和日期被重复显示。换句话说,SQL 将 SELECT 命令解释为合并 tb_Quotes 和 tb_Symbols 的内容的请求。因此,tb_Quotes 中的值会重复出现,而 tb_Symbols 中的值则会发生变化。由于 tb_Quotes 包含 6 条记录,而 tb_Symbols 包含 4 条记录,因此将这两个表合并会产生 24 条记录。现在明白为什么 SQL 会返回 24 条记录了吗?
现在仔细观察图 13。在某些情况下,fk_id 值与 id 值匹配。很有趣,不是吗?为什么会发生这种情况?fk_id 中存储的值指示我们应该查找哪个值。回到图 12 中的图表,我们可以看到 id 属于 tb_Symbols,而 fk_id 属于 tb_Quotes。
现在我们需要使用所谓的别名。换言之,我们为表分配别名,以便将一个表与另一个表相关联。这告诉 SQL 如何正确地组装结果集。使用别名时,请避免使用保留字。虽然某些实现可能会处理这些保留字,但为了避免给其他 SQL 程序员造成混淆,通常在 SQL 代码中不应将保留字用作别名。新代码如下所示:
01. PRAGMA FOREIGN_KEYS = ON; 02. 03. DROP TABLE IF EXISTS tb_Quotes; 04. DROP TABLE IF EXISTS tb_Symbols; 05. 06. CREATE TABLE IF NOT EXISTS tb_Symbols 07. ( 08. id INTEGER PRIMARY KEY, 09. symbol TEXT NOT NULL UNIQUE 10. ); 11. 12. CREATE TABLE IF NOT EXISTS tb_Quotes 13. ( 14. of_day TEXT NOT NULL, 15. price NUMERIC NOT NULL, 16. fk_id INTEGER NOT NULL, 17. FOREIGN KEY (fk_id) REFERENCES tb_Symbols(id) 18. ); 19. 20. INSERT INTO tb_Symbols (id, symbol) VALUES(1, 'BOVA11'); 21. INSERT INTO tb_Symbols (id, symbol) VALUES(3, 'PETR4'); 22. INSERT INTO tb_Symbols (id, symbol) VALUES(2, 'WDOQ23'); 23. INSERT INTO tb_Symbols (id, symbol) VALUES(4, 'VALE3'); 24. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('17-07-2023', 12.90, 4); 25. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('14-07-2023', 118.12, 2); 26. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('13-07-2023', 119.53, 1); 27. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('12-07-2023', 117.45, 2); 28. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('11-07-2023', 119.30, 3); 29. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('10-07-2023', 120.59, 1); 30. 31. SELECT * 32. FROM tb_Quotes AS tq, tb_Symbols AS ts 33. WHERE tq.fk_id = ts.id;
代码 03
执行这段代码后,我们将得到如下图所示的结果:

图 14
就像变魔术一样,我们现在得到了完全符合预期的结果:六条报价记录,显示了交易品种、日期和价格。现在,让我们来看看为什么会发生这种情况。在脚本的第 32 行,我们在每个表名后添加了一个别名。
第 33 行告诉 SQL 如何关联这两个表并返回正确的结果。这里,WHERE 子句定义了用于筛选结果集的条件。在这种情况下,只要 tb_Quotes 表中的 fk_id 值与 tb_Symbols 表中的 id 值相匹配,该行就应包含在输出中。因此,SELECT 命令可以考虑前面显示的各种组合,但 WHERE 筛选器使 SQL 只返回我们实际请求的行。
对上述解释的一个小澄清:严格来说,SQL 并不一定需要物理遍历所有可能的行组合。引擎为查询选择更高效的执行计划。如果轻率地这么做,很快就会降低整个系统的性能。在实践中,SQL 会从一个表开始,然后在另一个表中查找所需的字段以执行比较和匹配。这样可以大大提高查询效率。
我们可以进一步优化 SQL 查询的输出结果。列名通常看起来有点奇怪,有时,正如您在图 14 中看到的那样,SQL 会返回我们不需要的列。如何删除这些额外信息?很简单:告诉 SQL 我们想要哪些列以及它们应该有什么名称。接下来,我们来看下面显示的脚本:
01. PRAGMA FOREIGN_KEYS = ON; 02. 03. DROP TABLE IF EXISTS tb_Quotes; 04. DROP TABLE IF EXISTS tb_Symbols; 05. 06. CREATE TABLE IF NOT EXISTS tb_Symbols 07. ( 08. id INTEGER PRIMARY KEY, 09. symbol TEXT NOT NULL UNIQUE 10. ); 11. 12. CREATE TABLE IF NOT EXISTS tb_Quotes 13. ( 14. of_day TEXT NOT NULL, 15. price NUMERIC NOT NULL, 16. fk_id INTEGER NOT NULL, 17. FOREIGN KEY (fk_id) REFERENCES tb_Symbols(id) 18. ); 19. 20. INSERT INTO tb_Symbols (id, symbol) VALUES(1, 'BOVA11'); 21. INSERT INTO tb_Symbols (id, symbol) VALUES(3, 'PETR4'); 22. INSERT INTO tb_Symbols (id, symbol) VALUES(2, 'WDOQ23'); 23. INSERT INTO tb_Symbols (id, symbol) VALUES(4, 'VALE3'); 24. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('17-07-2023', 12.90, 4); 25. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('14-07-2023', 118.12, 2); 26. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('13-07-2023', 119.53, 1); 27. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('12-07-2023', 117.45, 2); 28. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('11-07-2023', 119.30, 3); 29. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('10-07-2023', 120.59, 1); 30. 31. SELECT tq.of_day AS 'Quote Date', 32. tq.price AS 'Current Price', 33. ts.symbol AS 'Asset Name' 34. FROM tb_Quotes AS tq, tb_Symbols AS ts 35. WHERE tq.fk_id = ts.id;
代码 04
结果显示如下:

图 15
“哇!所以 SQL 和我想象中的不太一样。我一直以为它只是一系列毫无意义的指令,但现在我发现它其实可以很有趣。"而这仅仅是热身而已。许多人认为需要外部程序来进一步处理返回的数据,但其实这可以直接在 SQL 中完成。例如,假设我们希望数据按照价格从高到低排序,价格最高的排在最前面,价格最低的排在最后。为此,我们可以使用如下代码:
01. PRAGMA FOREIGN_KEYS = ON; 02. 03. DROP TABLE IF EXISTS tb_Quotes; 04. DROP TABLE IF EXISTS tb_Symbols; 05. 06. CREATE TABLE IF NOT EXISTS tb_Symbols 07. ( 08. id INTEGER PRIMARY KEY, 09. symbol TEXT NOT NULL UNIQUE 10. ); 11. 12. CREATE TABLE IF NOT EXISTS tb_Quotes 13. ( 14. of_day TEXT NOT NULL, 15. price NUMERIC NOT NULL, 16. fk_id INTEGER NOT NULL, 17. FOREIGN KEY (fk_id) REFERENCES tb_Symbols(id) 18. ); 19. 20. INSERT INTO tb_Symbols (id, symbol) VALUES(1, 'BOVA11'); 21. INSERT INTO tb_Symbols (id, symbol) VALUES(3, 'PETR4'); 22. INSERT INTO tb_Symbols (id, symbol) VALUES(2, 'WDOQ23'); 23. INSERT INTO tb_Symbols (id, symbol) VALUES(4, 'VALE3'); 24. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('17-07-2023', 12.90, 4); 25. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('14-07-2023', 118.12, 2); 26. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('13-07-2023', 119.53, 1); 27. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('12-07-2023', 117.45, 2); 28. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('11-07-2023', 119.30, 3); 29. INSERT INTO tb_Quotes (of_day, price, fk_id) VALUES ('10-07-2023', 120.59, 1); 30. 31. SELECT tq.of_day AS 'Quote Date', 32. tq.price AS 'Current Price', 33. ts.symbol AS 'Asset Name' 34. FROM tb_Quotes AS tq, tb_Symbols AS ts 35. WHERE tq.fk_id = ts.id 36. ORDER BY price DESC;
代码 05
执行 SQL 代码 05 的结果如下所示:

图 16
处理记录时需谨慎
在文章“市场模拟(第 19 部分):SQL入门(二)”中,我们解释了如何使用 UPDATE 和 DELETE 命令更改或删除记录。虽然这种方法可行,但并不是处理关系数据库的最佳方式,因为在多个表相互关联的情况下,这种方法容易出错。为了理解这个问题,让我们回到代码 05,做一些非常简单的事情。到第 29 行,我们创建数据库并向其中插入数据。数据是否正确在这里并不重要;我们只是想创建数据库。当我们在第 31 行执行 SELECT 查询时,会得到如下所示的结果:

图 17
此时,我们注意到一条记录包含错误信息。我们决定使用 UPDATE 命令来更正值,而不是重新创建整个数据库或删除记录。如下所示操作:
1. UPDATE tb_Quotes SET price = 29.58 WHERE fk_id = 3; 2. 3. SELECT tq.of_day AS 'Quote Date', 4. tq.price AS 'Current Price', 5. ts.symbol AS 'Asset Name' 6. FROM tb_Quotes AS tq, tb_Symbols AS ts 7. WHERE tq.fk_id = ts.id 8. ORDER BY price DESC;
代码 06
我们得到如下所示的结果:

图 18
好,记录已更新,您可能会感到高兴,认为自己已经知道如何使用 SQL。然而,如果你查看代码 05,也就是我们向数据库插入数据的地方,你会发现 PETR4 只有一条记录。在这种情况下,信息很容易被更改甚至删除。现在,让我们稍微思考一下。如果我们不是要更改 PETR4 的记录,而是需要更改 WDOQ23 的记录,那该怎么办?我们该怎么做呢?有人可能会立刻说:“很简单;我会使用以下命令:”
01. UPDATE tb_Quotes 02. SET price = 119 03. WHERE fk_id = 2 04. AND of_day = '14-07-2023'; 05. 06. SELECT tq.of_day AS 'Quote Date', 07. tq.price AS 'Current Price', 08. ts.symbol AS 'Asset Name' 09. FROM tb_Quotes AS tq, tb_Symbols AS ts 10. WHERE tq.fk_id = ts.id 11. ORDER BY price DESC;
代码 07
您又说对了,结果如下所示。这个方法之所以有效,是因为我们同时根据 fk_id 和 of_day 对行进行了过滤。

图 19
更新关系数据库中的记录
修改关系数据库的一种较好方式(尽管不是唯一方式)是准确理解每条 SQL 命令的作用。换句话说,如果你不了解 SQL 命令,你可能会认为 SQL 的功能非常有限。但一旦你理解了它,你就能做很多事情。我说的“很多”,是指真的很多。我见过有人编写外部代码来解决本可以直接用 SQL 处理的任务。他们之所以这样做,通常是因为他们并不完全了解 SQL 的工作原理。考虑到这一点,让我们来看看如何像上一节那样进行相同的更改,但这次不需要手动输入 fk_id 值。
此时,你可能会想:“那么,我们要使用非关系型表结构吗?”不,亲爱的读者。我们将保留关系型模型。为了达到与上一节相同的目标,我们将对代码 07 进行如下更新。
01. UPDATE tb_Quotes 02. SET price = 4985.5 03. WHERE fk_id = (SELECT ts.id FROM tb_Symbols AS ts WHERE ts.symbol = 'WDOQ23') 04. AND of_day = '14-07-2023'; 05. 06. SELECT tq.of_day AS 'Quote Date', 07. tq.price AS 'Current Price', 08. ts.symbol AS 'Asset Name' 09. FROM tb_Quotes AS tq, tb_Symbols AS ts 10. WHERE tq.fk_id = ts.id 11. ORDER BY price DESC;
代码 08
“天哪,这到底是什么疯狂的行为?你确定它会奏效吗?我从未见过有人做出如此疯狂的事。"亲爱的读者,您的怀疑是可以理解的。但请看下面的结果:

图 20
请注意,我们使用了略微不同的值,特意以此证明它是有效的。那么问题来了:上述代码是如何工作的呢?我们从未指定过要使用的 fk_id 或标识符。然而,正确的记录却被更改了。我们提供交易品种名称和相应的日期。这一点很重要:尽管每条记录都有自己的日期,但我们还是应该更现实地思考。在同一天,数据库中可能包含价格不同的不同交易品种。因此,同时按日期和交易品种进行筛选可以大大提高查询准确率,并返回所请求的记录。
您可能没有注意到,但我们刚才已经解释了上述代码的工作原理。如果你没有理解或完全领会这个概念,不要气馁。这些事情一开始很难理解。让我们仔细研究一下代码 08 中发生了什么。请注意,UPDATE 命令被分成了多行。我们故意这样做,以便能够解释正在发生的情况。
在第 01 行,我们声明要更新 tb_Quotes 表中的一条记录。如果我们使用 DELETE 而不是 UPDATE,则该记录将从表中删除。因此,该解释同样适用于删除记录的情况。在第 02 行,我们指定了要更新的列以及更新后该列应包含的值。我认为到目前为止,推理已经很清楚了。在第 03 行,我们使用保留字 WHERE 定义了筛选条件,以便 SQL 知道如何找到所需的记录。请特别注意这一点,因为很多初学者都会在这里产生混淆。
从某种意义上说,这些命令中的每一个都像是一个函数:它们都会返回值。返回值可能是一个单一的值,也可能是包含多个值或字段的复合结果。因此,SELECT 命令可以只返回一个值,就像这里一样。它还可以以类似表格的结构返回多个值,这与执行第 06 行的 SELECT 命令时的情况完全相同。
当筛选条件足够具体且应用于正确的表时,SELECT 命令会返回一个唯一值,这意味着结果几乎可以像变量一样处理。“但是等一下,SQLite 不允许我们使用变量,对吗?”嗯,差不多吧。如果我们知道如何使用 SQL,而不仅仅是 SQLite,那么在实践中,我们就可以像使用变量一样在 SQL 代码中使用值。这个问题稍后会变得更加清晰,因为我们在使用相关表时需要采取额外的步骤来确保数据库的一致性。
回到解释部分,请注意,在第 03 行,我们指定了符号名称,在本例中为 WDOQ23。这使得 SQL 在 SELECT 命令中指定的表 tb_Symbols 中进行查找。它在寻找什么?ts.id 的值。换句话说,一旦执行查询,SELECT 将从交易品种名称为 WDOQ23 的行中返回 ID 值。SQL 然后使用一个表中找到的值来确定应该更新另一个表中的哪条记录。就这些吗?是的,亲爱的读者,这正是我们在此所做的。而且出错的可能性要低得多。
然而,并非一切都是完美的。我们可能会提供一个交易品种的名称,但无法找到其标识符。尽管这看起来可能很不寻常,但实际上这种情况相当普遍。因此,代码 08 无法妥善处理这种情况。更明确地说:我们可以尝试执行某些操作,而 SQL 并不一定会告诉我们出现了错误,或者命令的解释与我们预期的不同。我们可能只是简单地认为一切正常,因为我们唯一能看到的输出就是第 06 行 SELECT 命令的结果。
在这个阶段,可能还不太清楚这为什么会是个问题。如果所使用的实现没有报告任何错误,我们可能会误以为一切都很顺利。之后,当我们试图查找或使用我们认为已经存在的数据时,可能会得到一个令人不快的惊喜:信息并不处于预期状态。通常,诊断和解决问题所需的信息将不再可用。
检查记录是否已更新
SQLite 提供了一种相对简单的方法来检查我们想要更新的记录值是否确实被更新了。为此,我们需要一小段代码。但在继续讨论代码之前,我们应当考虑一些事情。我们经常听说,在数据库中更新甚至添加记录后,应运行 SELECT 语句来检查是否一切顺利。这种方法存在一个小问题。如果我们正在处理一个小型数据库,或者一个只有少量更改的数据库,那么使用 SELECT 来验证更新确实是合理的。然而,尤其是在共享数据库中,使用 SELECT 语句检查多条已更新的记录并不总是那么简单。
幸运的是,共享数据库通常运行在服务器上,因此 SQLite 通常不属于这种情况。即便如此,SQLite 数据库仍然可能需要更新数百甚至数千条记录。想象一下,用 SELECT 语句逐一检查每条记录以确认数据库是否已正确更新,这需要多么繁重的工作。这就是为什么我们需要一种稍微不同的方法。只有在数据库只进行小幅更改时,逐一检查每条更新记录才是完全可接受的。
然而,随着工作量和操作数量的增加,我们需要采用不同的方法。除了刚才展示的方法外,还有另一种几乎等效的方法,但我们稍后再讨论。现在,让我们先关注第一个任务:如何使用 SQLite 来确认所请求的记录更新是否已执行?
有一种相当简单的办法。然而,请谨慎使用,以避免因使用不当而引发问题。下面展示的是示例代码:
1. UPDATE tb_Quotes 2. SET price = 4987.5 3. WHERE fk_id = (SELECT ts.id FROM tb_Symbols AS ts WHERE ts.symbol = 'WDOQ23') 4. AND of_day = '14-07-2023'; 5. 6. SELECT changes() 'Updated Records';
代码 09
执行这段代码后,我们将看到以下结果:

图 21
请注意,结果值为 1。为什么呢?因为 UPDATE 语句在数据库中执行了一次更新或一次更改。如果我们执行了数百甚至数千次更新,那么显示的数值将对应于 UPDATE 语句更改的行数。然而,请考虑以下情况:如果按照下图所示对相同代码进行更改,结果将会不同,正如你在同一张图中看到的那样:

图 22
在这张图片中,与之前版本的不同之处以红色突出显示。请注意,只有交易品种名称发生了变化。由于不存在这样的交易品种,SELECT 命令无法确定应该使用哪个标识符。因此,UPDATE 命令不会更改任何行。当我们使用 SELECT 来检查更改了多少行时,它返回的值与我们预期的不同。在这种情况下,值为零:我们期望执行一次 UPDATE 操作,但由于我们指定的符号名称在表中不存在,因此虽然执行了 UPDATE 语句,但未影响任何行。
上图还有一个细节,用绿色高亮标出。SQL 返回的列标题正是 SELECT 命令中指定的信息。如果我们不了解这一点,我们可能会倾向于去做一些更复杂的事情。但是,如你所见,有些事情是很容易控制的。
然而,如前所述,我们使用的命令仅用于检查最近的 UPDATE 语句更改了多少行。由于 SQLite 允许我们创建本地、用户管理的数据库,我们也可以采用一种完全不同的方式来实现这一点。
对于那些不了解的人,SQLite 可以直接从其 GitHub 存储库下载,并且可以修改其源代码,使其行为与标准版本略有不同。由于本系列文章的目的并非展示如何做到这一点,因此我们仅提及这一事实。根据所使用的 SQLite 实现,更新或插入过程中的错误处理方式可能与本文所示的方法大不相同。换句话说,为了选择最佳方法,请仔细阅读您打算使用的实现方案的文档。
在继续下一个话题之前,让我们更详细地研究一下我们用来检查数据库更新的方法。在这种情况下,SELECT changes() 只显示最近一次 UPDATE 命令的结果。假设我们使用下面所示的代码片段:
1. UPDATE tb_Quotes SET price = 119 WHERE fk_id = 2 AND of_day = '14-07-2023'; 2. 3. UPDATE tb_Quotes SET price = 4987.5 WHERE fk_id = (SELECT ts.id FROM tb_Symbols AS ts WHERE ts.symbol = 'WDOQ24') AND of_day = '14-07-2023'; 4. 5. SELECT changes() 'Updated Records';
代码 10
请注意,我们现在运行两个 UPDATE 命令。当然,我们可能会期待得到一个累积的结果,或者至少是具有代表性的结果,因为这两个命令都在同一个脚本中。但事实并非如此。如果我们仔细查看第 03 行,可以发现它与图 22 中显示的代码相匹配。代码10 中唯一的新部分是第 01 行。因此,尽管第 03 行的 UPDATE 语句会执行,但肯定不会影响任何行,但我们在执行第 05 行时可能会预期得到一个非零值。但是,当我们运行代码时,SQL 返回零。出了什么问题?第 01 行和第 03 行都失败了吗?不是的,第 01 行更改了一行数据,而第 03 行的 UPDATE 语句虽然被执行,但并未影响任何行。
为什么呢?因为在检查第 01 行的结果之前,我们在第 03 行向 SQL 发送了另一个请求。因此,当我们询问 SQL 发生了什么时,它只报告最近一条 UPDATE 语句的结果,即第 03 行被执行,但未影响任何行。有些人会花费大量时间试图理解为什么脚本会或不会按预期工作。归根结底,问题通常在于我们做出了假设。在编程中,假设很少能带来好的结果:它们意味着我们期待某事发生,而不是去验证它是否真的发生了。所以,永远不要依赖假设。在责怪周围的一切之前,先确保一切正常运作。
最后的想法
无论如何,在使用相关数据库或表时,我们还需要解决另一个问题 — 例如在我们的系统中,一个表存储交易品种名称,另一个表存储报价数据。这样的模式可能会复杂得多,涉及多个紧密相关的表之间的多种关系。所有这些都是通过主键和外键来处理的。
为了理解问题的规模或未来任务的复杂性,请考虑以下场景:在未来的某个时刻,我们决定停止使用某个特定符号,因为它不再值得存储或监控。这样的决定迟早会做出。主要问题,也是难点所在,在于:我们如何从数据库中删除该交易品种的记录?很多人会说,解决方案很简单:创建一个循环或运行一个 SQL 查询来删除我们的记录。是的,倘若我们处理的数据库仅由一个庞大的记录表构成,那么情况大致便是如此,但依我之见,此举实为荒谬至极。但这没关系;每个人都知道,或者应该知道,自己在做什么。
编写 SQL 查询来删除特定交易品种的所有记录非常简单。你只需要一个 DELETE 命令,该命令本身与 UPDATE 命令非常相似。这是一个小细节,所以我们在此不作讨论。但是,如果数据库不是使用一个表,而是使用多个相关联的表呢?如何删除与特定交易品种相关的所有记录?现在事情变得有点复杂了,不是吗?
现在,我们来思考一下我们正在使用的模式。如果从 tb_Symbols 中删除交易品种名称,则必然会影响使用以下命令查询数据库时返回的结果。
SELECT tq.of_day AS 'Quote Date', tq.price AS 'Current Price', ts.symbol AS 'Asset Name' FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;
代码 11
| 文件 | 描述 |
|---|---|
| Experts\Expert Advisor.mq5 | 演示 Chart Trade 与 EA 之间的交互。交互操作需要 Mouse Study。 |
| Indicators\Chart Trade.mq5 | 创建一个窗口,用于配置要发送的订单。交互操作需要 Mouse Study。 |
| Indicators\Market Replay.mq5 | 创建用于与回放/模拟服务交互的控件。交互操作需要 Mouse Study。 |
| Indicators\Mouse Study.mq5 | 提供图形控件与用户之间的交互。它既是回放/模拟系统所必需的,也是实时市场操作所必需的。 |
| Services\Market Replay.mq5 | 创建并维护市场回放/模拟服务,这是整个系统的主要文件。 |
| Code VS C++\Servidor.cpp | 为 Mini Chat 版本创建并维护一个用 C++ 编写的服务器套接字。 |
| Code in Python\Server.py | 创建并维护用于 MetaTrader 5 和 Excel 之间通信的 Python 套接字。 |
| Indicators\Mini Chat.mq5 | 允许通过指标实现迷你聊天。运行需要服务器。 |
| Experts\Mini Chat.mq5 | 允许使用 EA 实现迷你聊天功能。运行需要服务器。 |
| Scripts\SQLite.mq5 | 演示如何在 MQL5 中使用 SQL 脚本。 |
| Files\Script 01.sql | 演示如何创建带有外键的简单表。 |
| Files\Script 02.sql | 演示如何向表格中添加值。 |
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/12987
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
在MQL5中自定义交易品种:创建三维K线自定义品种
极值优化(EO)