发布于 2026-01-06 1 阅读
0

我本周为工作编写的奇葩 SQL 查询

我本周为工作编写的奇葩 SQL 查询

喜欢SQL。在我看来,它是最强大、最优美的语言之一。它已经存在很长时间了,但我仍然在不断学习新知识。讽刺的是,我在大学里上的 SQL 课是我上过的唯一一门有用的计算机科学课程,而且我在那里学到的知识至今仍然有用。

简而言之,到底出了什么问题?呃……我不能透露具体细节,所以我编一个比较接近事实的故事吧。

前提

假设你正在编写一个任务管理器,并且刚刚往你的某个项目中添加了 1000 个任务。1000 个任务数量庞大且难以管理,所以你决定将它们分成 100 个一组。编写一个连接数据库的 Node 脚本似乎很简单……但是等等,这意味着你需要以某种方式连接到数据库。你不想创建一个新项目,将代码提交到现有代码库感觉不太好,而且你的 UI 也无法同时处理 1000 个任务。

但是……但是 PgAdmin 就在那里,已经连接到数据库了。我可以用 SQL 做这件事吗?你问自己,因为现在是下午 4 点,你想回家,而且 SQL 是你最喜欢的语言,所以你决定用 SQL 来做。

注意:在现实世界中,有 100,000 个“任务”,所以,是的,我无法想象任何非分页 UI 能够像轻而易举一样处理 100,000 个任务。

还有!这是我和另一位工程师合作完成的,所以当我说“不得不写”的时候,实际上是我和我的同事一起完成的 :) 谢谢兄弟!

最终查询

DO $FN$
DECLARE
  project_id integer
BEGIN
  FOR counter IN 1..10 LOOP
    RAISE NOTICE 'Counter: %', counter;

    EXECUTE $$ INSERT INTO projects(name) VALUES ('Group ' || $1) RETURNING id $$ 
      INTO project_id 
      USING counter;

    EXECUTE $$ UPDATE tasks 
      SET project_id = $1
      WHERE tasks.id IN (
          SELECT tasks.id 
          FROM tasks
          INNER JOIN projects ON projects.id = tasks.project_id
          WHERE projects.name = $2
          ORDER BY tasks.id ASC
          LIMIT 100) $$ 
     USING project_id, 'Client project';

     RAISE NOTICE 'Finished Counter: %', counter;
  END LOOP;
END;$FN$
Enter fullscreen mode Exit fullscreen mode

解释

我不想逐行讲解查询语句,但我很想解释代码中的各个“块”,以便更好地理解,并让您了解 SQL 的工作原理。

请注意,其中很多内容是postgres 特有的,这意味着 MySQL、MSSQL 或其他 SQL 数据库可能无法访问此语法的某些部分。

街区DO $FN$

DO $FN$
 -- code goes here
$FN$
Enter fullscreen mode Exit fullscreen mode

本质上,这和其他“do”代码块一样。“do”代码块的文档将其比作一个返回void且不接受任何参数的自执行匿名函数。

$FN$就是所谓的美元符号字符串常量。在 Postgres 中,有几种类型的引号:

  1. 单引号 ( 'string goes here') 表示内容为字符串
  2. 双引号 ( "column_name") 允许我们引用有问题的列名或表名,从而安全地使用它。
  3. 美元符号字符串($$ SELECT * FROM projects WHERE name = 'test'; $$)与单引号字符串类似,区别在于你可以在其中使用其他类型的引号。

如果想在多个引用中嵌套多个引用呢?你可以使用转义符\,但 Postgres 非常强大,它允许你在引用符内指定自定义标签,并根据需要嵌套任意层级:

$outer_quote$
  $inner_quote$
    $$ SELECT * FROM projects where name = 'test' $$
  $inner_quote$
$outer_quote$
Enter fullscreen mode Exit fullscreen mode

你无需在任何地方声明这些自定义标签,它们就能正常工作。只需在美元符号之间插入一个单词即可。

声明变量

下一个代码块(位于我们以美元报价的自定义标签字符串中)是声明代码块:

-- dollar quote start
DECLARE
  project_id integer;
-- rest of the code
Enter fullscreen mode Exit fullscreen mode

这样我们就可以声明要在函数其余部分使用的变量。是的,你必须预先声明它们,并且要声明类型。我找不到关于这条语句的具体文档(DECLARE在代码块之外DO使用语句与游标有关,但这与我们这里讨论的内容无关)。

功能体

DECLARE
  -- variables
BEGIN
  -- more code
END;
Enter fullscreen mode Exit fullscreen mode

本质上,到目前为止的代码都是在为运行一些代码做准备。我们通过代码块告诉 Postgres 我们想要执行某些操作DO,然后通过声明变量来声明我们想要使用的变量DECLARE

所有中间环节BEGIN以及END运行常规 SQL 语句的操作。

for 循环

FOR counter IN 1..10 LOOP
  -- more code
END LOOP;
Enter fullscreen mode Exit fullscreen mode

是的,SQL 中也可以使用 for 循环!:)相关的循环和控制文档中有详细的说明,其中就列出了这个例子!

counter是一个会被更新的变量,我们可以在循环内部访问它。它1..10创建了一个范围(循环 1 到 10)。

注意:经验丰富的 Postgres 和 SQL 老手会告诉我,进行 1 到 10 的计算是不必要的,因为……而且你猜怎么着……你可以1..10用 SQL 查询来代替,创建一个“for each”类型的循环。例如,FOR task IN SELECT * FROM tasks LOOP它会遍历数据库中的每个任务。

发出通知!

RAISE NOTICE 'Counter: %', counter;
Enter fullscreen mode Exit fullscreen mode

关于这一点,简单说明一下,这是一个日志。就是这样。这样做RAISE NOTICE不会抛出任何错误。它的工作方式类似于格式化字符串,%字符串后面的内容会被替换成变量。就像其他语言一样,你可以传入多个%符号,然后在字符串末尾传入多个变量进行替换。

没必要,我只是喜欢看日志而已 :)

执行...到...使用(创建一个新项目)

EXECUTE $$ INSERT INTO projects(name) VALUES ('Group ' || $1) RETURNING id $$ 
      INTO project_id 
      USING counter;
Enter fullscreen mode Exit fullscreen mode

该语句将使用该counter变量创建一个全新的项目,项目名称Group后跟计数器编号,并将生成的项目 ID 存储在该project_id变量中。

为了避免让人感到不知所措,我会把这句话解释得更详细一些。起初我也觉得这句话难以理解,事实上,最初的查询语句写得非常粗糙,就是为了避免EXECUTE完全理解这句话的意思。

EXECUTE语句会执行给定的任何字符串。我们已经知道它$$可以代替引号使用。因此,INSERT INTO .... RETURNING id它是一个我们想要运行的 SQL 字符串。

这里有两点需要说明:

  1. $1是用于替换的(稍后我会详细介绍USING)。在我们的示例中,我们使用了Postgres 中的字符串连接运算符||
  2. RETURNING id是 Postgres 的一个特性。你可以插入一条记录(或批量插入多条记录)并id立即获取结果。你甚至可以使用 `get_request_id()`RETURNING *来获取整个新记录。

这条INTO语句会将查询结果存储在我们之前声明的变量中。我之前不知道这一点,一直尝试project_id := EXECUTE $$ statement $$这样做,但都失败了 :(。在我们的例子中,我们将返回的 id 存储到project_id.

USING语句用于变量替换。您可以在此处传入一个变量列表。第$1一个变量将被替换为您在该USING语句中提供的第一个变量/值。第二个变量可以通过 `{{1}}` 访问,$2依此类推。

呼……好。

EXECUTE...USING 的文档此操作执行的是动态语句。令人困惑的是……这与EXECUTE执行预处理语句的命令不同。

将最多 100 项任务转移到新项目

EXECUTE $$ UPDATE tasks 
      SET project_id = $1
      WHERE tasks.id IN (
          SELECT tasks.id 
          FROM documents 
          INNER JOIN projects ON projects.id = tasks.project_id
          WHERE projects.name = $2
          ORDER BY tasks.id ASC
          LIMIT 100) $$ 
     USING project_id, 'Client project';
Enter fullscreen mode Exit fullscreen mode

EXECUTE...INTO...USING我很庆幸之前的问题已经解释清楚了,因为这个问题要复杂得多

首先,我们来看一下简化后的查询:

UPDATE tasks SET project_id = $1 WHERE tasks.id IN 
  (-- subquery)
Enter fullscreen mode Exit fullscreen mode

我们将新项目 ID 设置为project_id从子查询结果中获取的先前 EXECUTE 语句中获取的任务 ID。

接下来是子查询:

SELECT tasks.id 
  FROM documents 
  INNER JOIN projects ON projects.id = tasks.project_id
  WHERE projects.name = $2
    ORDER BY tasks.id ASC
    LIMIT 100
Enter fullscreen mode Exit fullscreen mode

在子查询中,我们查找属于需要拆分的那个大型项目的 100 个任务。我们获取该项目中的下 100 个任务。我们使用 `<project_name>`$2替换为原始大型项目的项目名称。我们按 `<sort_name>` 排序,tasks.id ASC以确保任务在拆分过程中不会顺序错乱。

最后,是包含所有步骤的 execute 语句:

EXECUTE $$ 
 -- query
$$
USING project_id, 'Client project';
Enter fullscreen mode Exit fullscreen mode

与之前一样,我们执行一条语句,但这次我们不存储查询结果。我们替换为两个不同的变量,这两个变量可以通过查询$1访问。$2

另一份提高通知

RAISE NOTICE 'Finished Counter: %', counter;
Enter fullscreen mode Exit fullscreen mode

和之前一样,这会让我们知道循环已经完成。我只是喜欢记录日志。

我需要知道这个吗?

我的天啊,当然不是。除非你每天都泡在数据库里,否则没人需要对这些功能了如指掌,但了解一下总是好的我在学习或使用任何技术时都会秉持这种理念。我知道我可以用 SQL 做所有这些事情,因为我之前有过使用 SQL 的经验,我只需要花些时间阅读 Postgres 的文档,就能弄清楚如何完成这项任务。我认为这种知识非常宝贵。

文章来源:https://dev.to/antjanus/the-wild-sql-loop-i-had-to-write-for-work-this-week-24n2