← 返回蜂巢洞察

如何在SQL中使用子查询

每当你在SQL中看到一个查询嵌套在另一个查询内部时,这就是子查询。子查询也被称为内查询,而包含它的那个查询则被称为主查询或外查询。 子查询的作用是为主查询提供额外的数据,这些数据可以以派生列或派生表的形式出现,或者它们也可以用来过滤主查询返回的行。 对于刚开始学习SQL的初学者来说,子查询可能相当难以理解。本文将帮助大家简化这一概念,使其更易于理解。读完这篇文章后,你应该能够更加熟练地使用子查询来解决问题了。 目录 先决条件 子查询的工作原理 执行顺序 子查询的类型 非相关子查询 相关子查询 结论 先决条件: 子查询属于高级SQL概念,因此,必须牢固掌握SQL的基础知识,包括SELECT、FR

每当你在SQL中看到一个查询嵌套在另一个查询内部时,这就是子查询。子查询也被称为内查询,而包含它的那个查询则被称为主查询或外查询。

子查询的作用是为主查询提供额外的数据,这些数据可以以派生列或派生表的形式出现,或者它们也可以用来过滤主查询返回的行。

对于刚开始学习SQL的初学者来说,子查询可能相当难以理解。本文将帮助大家简化这一概念,使其更易于理解。读完这篇文章后,你应该能够更加熟练地使用子查询来解决问题了。

目录

先决条件:

子查询属于高级SQL概念,因此,必须牢固掌握SQL的基础知识,包括SELECT、FROM、WHERE、JOINS语句、CASE语句以及查询执行的正确顺序。

子查询的工作原理

让我们先来看一个包含子查询的查询示例。

SELECT *
FROM registration
WHERE student_id 
                IN (SELECT 
                        id 
                    FROM student
                    WHERE location = 'Lagos')

上述查询由两部分组成:主查询和子查询。

这是主查询:

SELECT *
FROM registration
WHERE student_id 
                IN (...)

请注意,主查询的括号中目前还没有任何内容。

被放在括号里面的那部分代码就是子查询。它的作用是用来过滤主查询返回的行。现在让我们来看看这个子查询的具体代码:

SELECT 
    id 
FROM student 
WHERE location = 'Lagos'

执行顺序

当你像这样在管理系统中运行整个查询时:

SELECT *
FROM registration
WHERE student_id 
                IN (SELECT 
                        id 
                    FROM student
                    WHERE location = 'Lagos')

你将会得到如下结果:

结果显示了所有来自拉各斯的学生的注册信息

查询结果显示了所有来自拉各斯的学生们的注册信息。但为了得到这样的结果,SQL会按照一定的执行顺序来运行这些语句,我们下面将会讨论这个执行顺序。

当你执行整个查询时,在后台,子查询会首先被执行:

SELECT 
    id 
FROM student 
WHERE location = 'Lagos'

这个子查询会从student表中检索出所有来自拉各斯的学生们的ID。执行完成后,你会得到这样的结果:

当子查询在主查询之外被执行时,它将返回这些值

在后台,子查询返回的这些ID会被传递给主查询,从而使主查询变成这样的形式:

SELECT *
FROM registration
WHERE student_id 
            IN('STU1', 'STU13','STU2','STU4','STU23','STU27')

主查询会将其student_id列中的每个值与子查询返回的ID进行比较。如果找到匹配项,就会返回该学生的注册信息;否则,这条记录就会被忽略。

你可能会想:“为什么不直接从student表中获取ID,然后直接传递给主查询呢?为什么要使用子查询呢?”

其实,这样做属于硬编码。虽然目前这样的查询可以正常运行,但后来如果有新的学生加入,当你重新执行这个查询时,你只会得到那些你之前手动将ID传递给主查询的学生的信息,而新加入的学生的信息则会被忽略。

使用子查询的方法是动态的:它会不断从student表中获取数据,从而确保查询结果始终是最新的。

子查询的类型

根据依赖关系,子查询可以分为两种类型:非相关子查询(独立子查询)和相关子查询(依赖子查询)。

非相关子查询

这类子查询与主查询是相互独立的。当它们在主查询之外被执行时,依然可以正常运行。上面提到的查询就是一个很好的例子。

SELECT *
FROM registration
WHERE student_id 
                IN (SELECT 
                        id 
                    FROM student
                    WHERE location = 'Lagos')

需要特别注意的是,无论是相关子查询还是非相关子查询,它们都可以出现在SQL查询的不同部分。子查询出现在查询的哪个位置,决定了它在主查询中扮演的角色——它可以作为派生列、派生表或过滤条件来使用。

让我们来讨论一下主查询中哪些地方可以使用子查询,以及这些子查询是如何辅助主查询完成工作的。

1. 子查询作为派生列

当你在SELECT语句中看到子查询时,它实际上是一个派生列。派生列是指通过使用表中其他列的值来生成的列。这类列并不会被存储在数据库中,而只会在执行该查询的整个过程中存在。

例如,要想计算每门课程在总注册人数中所占的比例,你需要在同一行中包含以下三项信息:

  • 课程名称

  • 每门课程的注册人数(分子)

  • 所有课程的总注册人数(分母)

首先,你可以编写一个主查询,用来获取所有课程的名称以及每门课程的注册人数:

SELECT
    course_name,
    COUNT(reg_id) AS registrations
FROM course AS l
LEFT JOIN registration AS r
ON l.id = r.course_id
GROUP BY course_name

查询结果如下:

该图片显示了每门课程的名称及其注册人数

主查询列出了每门课程及其对应的注册人数。不过,要想计算出每门课程在总注册人数中所占的比例,还必须在结果表的每一行中包含所有课程的总注册人数。

为此,你可以嵌套一个子查询,让这个子查询将所有课程的总注册人数作为一列返回到主查询中,具体操作方法如下:

SELECT COUNT(reg_id) FROM registration

当这个子查询在主查询之外被执行时,它会返回如下结果:

该图片显示了子查询的计算结果,即所有课程的总注册人数

从上图中可以看出,所有课程的总注册人数为30。现在,要让这个子查询的结果成为主查询中的一列,首先需要用括号将其括起来,如下所示:

(SELECT COUNT(reg_id) FROM registration)

接下来,将这个子查询插入到主查询的列列表中,具体代码如下:

SELECT
    course_name,
    COUNT(reg_id) AS regs,
    (SELECT COUNT(reg_id) FROM registration) AS total -- 子查询
FROM course AS l
LEFT JOIN registration AS r
ON l.id = r.course_id
GROUP BY course_name

当这个查询被执行时,它会返回如下结果:

该图片显示了所有课程的名称、每门课程的注册人数以及所有课程的总注册人数

在上面的结果表中,主查询返回了各课程的名称及其注册人数,这些数据被赋予了regs这个别名;而子查询则添加了一列total,其中包含了所有课程的总注册人数,即30。这个数值会出现在结果表的每一行中。

通过每一行中列出的课程名称、注册人数以及所有课程的总注册人数,我们现在可以计算出每门课程在总注册人数中所占的比例。

计算公式:

比例 = 注册人数 / 总注册人数 * 100

首先,注册人数总注册人数这两列中的数据都是整数。如果直接进行除法运算,结果将会是0。为避免这种情况,必须使用CAST()函数将分子转换为FLOAT类型,具体操作如下:

-- 将分子转换为FLOAT类型
CAST(
    COUNT(reg_id) 
     AS FLOAT)

在将分子转换为FLOT类型之后,就可以进行除法运算了,计算公式如下:


CAST(COUNT(reg_id) AS FLOAT) -- 分子

/ -- 除号

(SELECT COUNT(reg_id) FROM registration) -- 分母

* 100 -- 转换为百分比

将以上步骤合并起来,最终的SQL查询语句如下:

SELECT
    course_name,
    CAST(COUNT(reg_id) AS FLOAT)/
    (SELECT 
        COUNT(reg_id) 
     FROM registration) * 100 AS percent_of_total
FROM course AS l
LEFT JOIN registration AS r
ON l.id = r.course_id
GROUP BY course_name

执行上述查询后,将会得到如下结果:

该图像显示了课程名称及其在总注册人数中所占的比例。

结果中列出了所有课程及其在总注册人数中所占的比例。如果想要控制percent_of_total列中数值的小数位数,可以使用SQL的ROUND()函数将它们四舍五入到小数点后一位。更新后的查询语句如下:

SELECT
    course_name,
    ROUND(CAST(COUNT(reg_id) AS FLOAT)/
    (SELECT 
        COUNT(reg_id) 
     FROM registration) * 100,1) AS percent_of_total
FROM course AS l
LEFT JOIN registration AS r
ON l.id = r.course_id
GROUP BY course_name

最终得到的结果如下:

该图像显示了课程名称及其在总注册人数中所占的比例。

这样,我们就得到了每门课程对总注册人数所做出的贡献比例。

2. 将子查询作为派生表使用

当你在FROM子句中看到一个子查询时,它实际上是一个派生表。派生表是从某个查询的结果中创建出来的临时表,它在数据库中不会被永久保存,而只会在该查询运行的过程中存在。

举个例子,假设你想编写一个查询来统计每个地区的学生人数。

但是,当使用以下查询语句来查询学生表时:

SELECT * FROM student

你会发现该表中并没有region这一列,而是有一列名为state的字段:

这张图片展示了学生表中的所有列

为了解决这个问题,你可以利用state列中的数据,通过SQL的CASE语句来创建一个派生的region列,具体方法如下:

SELECT
    *, -- 学生表中的所有列
    CASE
        WHEN location 
           IN('Abeokuta','Ibadan','Mokola','Iyana Ipaja','Lagos') 
        THEN 'West'

        WHEN location 
           IN('Anambra','Owerri','Enugu','Port Harcourt') 
        THEN 'East'

        WHEN location 
           IN('Abuja','Ilorin','Kaduna','Kano','Jos') 
        THEN 'North'
    END AS region -- 派生的region列
FROM student

当执行上述查询语句时,它会返回如下结果:

结果展示了学生表中的所有列,以及通过CASE语句创建的region列。

现在,结果中已经包含了通过CASE语句创建的region列,以及学生表中的其他所有列。

既然我们已经得到了这个region列,那么接下来就可以将整个查询语句转换成一个子查询了。

首先,需要用括号将整个查询语句括起来,并给它起一个别名:

(SELECT
    *, 
    CASE
        WHEN location 
           IN('Abeokuta','Ibadan','Mokola','Iyana Ipaja','Lagos') 
        THEN 'West'

        WHEN location 
           IN('Anambra','Owerri','Enugu','Port Harcourt') 
        THEN 'East'

        WHEN location 
           IN('Abuja','Ilorin','Kaduna','Kano','Jos') 
        THEN 'North'
    END AS region 
FROM student) AS data_prep -- 给这个子查询起了别名“dataprep”

要使用data Prep,只需将整个查询语句(包括括号和别名)放在FROM子句中,就像操作普通表格一样:

FROM (SELECT
    *, 
    CASE
        WHEN location 
           IN('Abeokuta','Ibadan','Mokola','Iyana Ipaja','Lagos') 
        THEN 'West'

        WHEN location 
           IN('Anambra','Owerri','Enugu','Port Harcourt') 
        THEN 'East'

        WHEN location 
           IN('Abuja','Ilorin','Kaduna','Kano','Jos') 
        THEN 'North'
    END AS region 
FROM student) AS data_prep

从这一刻起,你就可以从dataprep中选取任何数据了。它就像一个普通的表格一样可以使用。

现在,让我们利用dataprep中的数据来计算每个地区的学生人数吧:

SELECT  
    region,  
    COUNT(id) AS students  
FROM (SELECT  
        *,  
        CASE  
          WHEN location IN('Abeokuta','Ibadan','Mokola','Iyana Ipaja','Lagos')  
          THEN 'West'  
          WHEN location IN('Anambra','Owerri','Enugu','Port Harcourt')  
          THEN 'East'  
          WHEN location IN('Abuja','Ilorin','Kaduna','Kano','Jos')  
          THEN 'North'  
        END AS region  
        FROM student) AS data_prep  
GROUP BY region  

当执行上述查询时,它会返回以下结果:

结果显示了各地区的学生人数。

结果展示了各地区的学生人数。

3. 作为过滤条件的子查询

当你在WHERE子句中看到子查询时,它实际上是在对主查询进行过滤。你可以使用多种不同的操作符来对主查询进行筛选。

逻辑运算符

这些运算符包括INANYALL。当子查询返回多个值时,你可以使用这些运算符,将数据集中的列值与子查询返回的值进行比较。

前面你已经看到了一个使用IN的例子,现在我们来看另外两种运算符。

示例:获取所有年龄大于任何一位女学生的男生的记录。下面是使用ANY来实现这一目标的代码:

SELECT  
	*  
FROM student  
WHERE gender = 'Male'  
  AND age > ANY (SELECT  
                       DISTINCT age  
                     FROM student  
                     WHERE gender = 'Female')  

上述查询由两部分组成:主查询和子查询。

这是主查询的部分:

SELECT  
	*  
FROM student  
WHERE gender = 'Male'  
  AND age > ANY (...)  

而子查询的部分则是:

SELECT  
    DISTINCT age  
FROM student  
WHERE gender = 'Female'  

当你在管理系统中执行整个查询时,它的执行过程如下:

SELECT  
	*  
FROM student  
WHERE gender = 'Male'  
  AND age > ANY (SELECT  
                       DISTINCT age  
                     FROM student  
                     WHERE gender = 'Female')  

在实际执行过程中,系统会先计算子查询的结果:

SELECT  
   DISTINCT age  
FROM student  
WHERE gender = 'Female'  

子查询会从学生表中提取所有女学生的年龄值,并返回这些不同的年龄值:

该图像显示了子查询的结果,即所有女学生的不同年龄。子查询的结果会被传递给主查询,因此整个查询在运行过程中的实际逻辑如下所示:

SELECT 
	*
FROM student 
WHERE gender = 'Male' 
      AND age > ANY (23, 25, 26, 27, 28, 29, 30)

现在,主查询会将每名男学生的年龄与子查询返回的女学生的年龄进行比较。只有那些年龄大于至少一名女学生年龄的男学生才会被主查询返回:

查询结果展示了那些年龄大于至少一名女学生的男学生的详细信息。

查询结果展示了那些年龄大于至少一名女学生的男学生的详细信息。

在上述查询中,ANY操作符的作用是确保只有那些年龄大于至少一名女学生的男学生的记录才会被主查询返回。

而如果使用ALL操作符,情况就会有所不同。此时,这名男学生的年龄必须大于所有女学生的年龄,他的记录才会被主查询返回。请看下面使用ALL的操作示例:

SELECT 
	*
FROM student 
WHERE gender = 'Male' 
      AND age > ALL (SELECT 
                       DISTINCT age 
                     FROM student 
                     WHERE gender = 'Female')

结果:

查询结果返回了唯一一名年龄大于所有女学生的男学生。

查询结果返回了唯一一名年龄大于所有女学生的男学生。

比较运算符

这些运算符用于将列值与子查询返回的单一标量值进行比较。它们包括:

  1. 等于 (==)

  2. 大于 (>)

  3. 大于或等于 (>=)

  4. 小于 (<)

  5. 小于或等于 (=<)

  6. 不等于 (<>> 或 !=)

例如,如果你想检索那些年龄大于所有学生平均年龄的学生的详细信息,可以参考下面的查询:

SELECT *
FROM student
WHERE age > (SELECT AVG(age) FROM student)

执行这条查询后,会得到那些年龄大于所有学生平均年龄的学生的详细信息:

该图片展示了那些年龄大于所有学生平均年龄的学生的记录。

让我们来讨论一下这个查询的执行顺序。

首先,当你执行整个查询时,在后台,子查询会先被执行:

SELECT AVG(age) FROM student

这个子查询会返回一个值,即25——所有学生的平均年龄:

这张图片显示了所有学生的平均年龄

一旦子查询返回了25这个值,在后台,整个查询就会变成如下形式:

SELECT * FROM student WHERE age > 25

主查询会将学生表中每个学生的年龄与子查询返回的25进行比较。如果某个学生的年龄大于25,那么他们的记录就会被包含在结果中;否则,这些记录就会被忽略。

请看下面主查询的结果。可以看到,age列中的所有数值都超过了25。

这张图片显示了那些年龄大于平均年龄的学生的记录

根据你的需求,你也可以更换比较运算符。下面是一些其他比较运算符的用法示例。

-- 获取年龄大于或等于所有学生平均年龄的学生记录

SELECT *
FROM student
WHERE age >= (SELECT AVG(age) FROM student)

-- 获取年龄小于所有学生平均年龄的学生记录

SELECT *
FROM student
WHERE age < (SELECT AVG(age) FROM student)

-- 获取年龄小于或等于所有学生平均年龄的学生记录

SELECT *
FROM student
WHERE age <= (SELECT AVG(age) FROM student)

-- 获取年龄等于所有学生平均年龄的学生记录

SELECT *
FROM student
WHERE age = (SELECT AVG(age) FROM student)

-- 获取年龄不等于所有学生平均年龄的学生记录

SELECT *
FROM student
WHERE age != (SELECT AVG(age) FROM student)

-- 或者,你也可以使用!=,它表示“不等于”

SELECT *
FROM student
WHERE age != (SELECT AVG(age) FROM student)

相关子查询

这类子查询的执行依赖于主查询中的某个值,如果脱离主查询单独执行,它们是无法正常工作的。

举个例子,假设你想使用相关子查询来统计每门课程的注册人数。

首先,让我们来看一个包含非相关子查询的示例,这个子查询用于计算所有课程的总注册人数。

SELECT 
	course_name,
	(SELECT COUNT(reg_id) FROM registration) AS regs -- 子查询
FROM course 

当主查询被执行时,它会从course表中检索每门课程的名称;而子查询则会从registration表中计算出所有课程的总注册人数。子查询的结果会以派生列的形式显示出来,与course_name一同呈现。请看下面的结果:

该图片展示了各门课程的名称以及它们的总注册人数。

但是,上述结果并不符合我们的需求。我们实际上想要看到的是每门课程的注册人数,而不是所有课程的总注册人数。

为了解决这个问题,我们需要将最初的非相关子查询修改为基于主查询中的某个值进行过滤,从而将其转换成相关子查询。

请看下面的非相关子查询示例:

(SELECT COUNT(reg_id) FROM registration) AS regs

要将上述子查询修改为相关子查询,请按照以下步骤操作。

首先,复制之前使用的整个查询语句,并为主查询和子查询中的表格分别指定别名:

-- 为course表指定别名l,为registration表指定别名r
SELECT 
	course_name,
	(SELECT COUNT(reg_id) FROM registration AS r) AS regs 
FROM course AS l

接下来,使用course表中的id列与registration表中的course_id列,将主查询的表格与子查询的表格连接起来,具体操作如下:

SELECT 
    COUNT(reg_id) 
FROM registration AS r 
WHERE r.course_id = l.id -- 这就是连接条件

现在,上述子查询已经变成了相关子查询。如果将其单独执行,就会得到下面的结果。

当相关子查询在主查询之外被执行时,会返回错误信息。

这个错误说明,子查询中使用的l.id字段无法被识别,因为它实际上来自主查询。

为了让这个子查询能够正常工作,必须将其作为列嵌入到主查询中。请看下面完整的查询语句:

SELECT 
    course_name,
    (SELECT 
        COUNT(reg_id) 
     FROM registration AS r
     WHERE r.course_id = l.id) AS regs -- 这就是相关子查询的部分
FROM course AS l

当这个查询被执行时,它会返回以下结果:

该图片展示了各门课程的名称以及它们的注册人数。

上述结果展示了各课程的名称以及对应的注册人数。

现在,让我们来了解这条查询的执行顺序。

当整个查询被执行时,在后台,首先会评估主查询:

SELECT 
    course_name 
FROM course AS l

主查询并不会一次性运行并返回所有课程名称,而是会逐个返回课程名称。对于主查询返回的每一个课程名称,都会针对该特定课程执行子查询。

例如,在整个查询的第一轮迭代中,主查询首先返回的课程名称是“data analytics”,其ID为CS01。随后,主查询会将CS01传递给子查询,此时子查询在后台会变成如下形式:

SELECT 
    COUNT(reg_id) 
FROM registration AS r
WHERE r.course_id = CS01 -- 即“data analytics”的ID

上述子查询只会统计那些课程ID与主查询提供的值相匹配的注册记录,最终返回该课程的注册人数以及课程名称。

在下一轮迭代中,

  • 主查询会获取一个课程名称,并将其ID传递给子查询。

  • 子查询会统计该特定课程的注册人数。

  • 最终会将注册人数与课程名称一起显示出来。

这个过程会持续进行,直到主查询表中的所有课程都被处理完毕。实际上,表中课程的数量决定了查询需要执行的迭代次数。

请查看以下最终结果:

图片展示了各课程的名称以及对应的注册人数

最终结果会显示所有课程的名称及其注册人数。那些尚未有任何注册记录的课程,其注册人数会被显示为0。

EXISTS操作符

EXISTS用于检查相关表中是否存在匹配的记录,如果找到了匹配记录,则返回TRUE,如果没有找到,则返回FALSE

例如,如果你想找出至少有一名注册学生的课程,可以这样写:

SELECT 
    course_name
FROM course AS l
WHERE EXISTS (SELECT 1
              FROM registration AS r
              WHERE r.course_id = l.id)

上述查询由主查询和相关子查询组成。

以下是主查询的部分:

SELECT 
    course_name
FROM course AS l
WHERE EXISTS (...)

请注意,在上述主查询中,EXISTS操作符前面并没有列名。这是因为EXISTS并不是在比较主查询中的某列值与子查询返回的值,而是检查子查询是否至少返回了一行记录。如果找到了记录,那么EXISTS会返回TRUE;如果没有找到任何记录,就会返回FALSE

SELECT 1
FROM registration AS r
WHERE r.course_id = l.id

子查询的作用是检查注册表中,主查询提供的ID是否对应任何注册记录。在子查询的SELECT语句中,你会看到没有列出任何列,而只是使用了占位符“1”。这样做的原因是,子查询实际返回的值并不重要,关键在于是否存在相应的记录。

现在我们已经了解了这个查询的结构,接下来让我们深入探讨它的执行过程。

SELECT course_name FROM course AS l WHERE EXISTS (SELECT 1 FROM registration AS r WHERE r.course_id = l.id)

当整个查询被执行时,其评估过程会从FROM子句开始:

FROM course AS l

然后,系统会检查课程表中的第一行记录,并通过EXISTS来判断子查询中是否存在与之匹配的记录:

WHERE EXISTS (SELECT 1 FROM registration AS r WHERE r.course_id = l.id)

假设课程表中的第一行对应的数据分析课程,其IDCS01。这个ID会被传递给子查询,使得子查询变为如下形式:

SELECT 1 FROM registration AS r WHERE r.course_id = CS01 --- 数据分析课程的ID

系统会将CS01与注册表中course_id列的值进行比较。如果找到匹配的记录,EXISTS的结果将为TRUE,这意味着数据分析课程会被包含在主查询返回的结果中。

可以看到,EXISTS就是通过这种方式来判断条件的:

EXISTS → TRUE → 该课程被包含在结果中

但如果子查询中提供的ID在注册表中找不到匹配的记录,EXISTS的结果将为FALSE,那么这个课程就不会被包含在结果中。

EXISTS → FALSE → 该课程不会被包含在结果中

主查询中的每一行都会经过这样的判断过程,直到所有行都被处理完毕。下面是整个查询的最终结果:

此图像显示了至少有一项注册记录的所有课程

结果中显示了所有至少有一项注册记录的课程。

SELECT course_name FROM course AS l WHERE NOT EXISTS (SELECT 1 FROM registration AS r WHERE r.course_id = l.id)

那么,如果我们想要显示那些还没有任何注册记录的课程该怎么办呢?

EXISTS之前加上NOT,就可以反转这个条件。查询的结果将不再是那些至少有一人注册的课程,而是那些没有任何人注册的课程:

这张图片展示了目前还没有任何人注册的两门课程

查询结果显示的就是这两门目前还没有任何人注册的课程。

结论

初看时,子查询可能会让人感到有些复杂,尤其是当它们被嵌套在更复杂的查询中时。但一旦你明白了它们的作用仅仅是为主查询提供支持,就会发现使用它们其实非常简单。

重要的是不要试图记住所有可能的子查询结构,而应该学会理解主查询需要什么,以及子查询如何满足这些需求。有了这样的思维方式,像派生列、派生表、INEXISTSNOT EXISTS这类概念,就会变成实用的工具,而不再是复杂的SQL语法。

通过足够的练习,你会逐渐发现:子查询并不是SQL中某种复杂的功能,而是解决更复杂问题的一种自然方式。

相关文章

技术实践

如果你已经不再需要使用原有的定时任务调度工具了,应该该怎么办呢?

大多数开发者在开始自动化工作的过程中都会采取类似的方法。他们会编写一些脚本来完成某些有用的任务,比如从API中获取数据、调整一批图片的大小,或者发送报告邮件,然后将这些脚本安排在每天早上自动运行。 这样做之后,他们会在自己的crontab文件中添加相应的指令,这样就能感受到自己对自动化流程的控制力了。现在,在他们睡觉的时候,电脑会自动执行这些脚本。 有一段时间里,这样的安排确实足够用了。但后来就不再适用了。 也许某个备份脚本在凌晨3点悄悄地失败了,而你直到当天晚些时候需要使用该脚本时才发现了这个问题;又或者你编写了一个在终端环境中运行得非常顺利的脚本,但却发现在用crontab安排它执行时却会

阅读全文
技术实践

Firestore数据建模指南:内嵌文档与引用方式的选择(结合一个博客案例进行分析)

当开发人员从关系型数据库世界(如MySQL、PostgreSQL)转向Firebase这种NoSQL文档数据库时,他们往往会沿用原有的习惯,试图在新的系统中复制表结构、外键以及关联操作。 其结果是什么呢?复杂的查询语句、飞涨的读取成本,以及那种在添加少量功能后就变得难以维护的数据库结构。 要理解Firestore的工作原理,我们首先需要对比它所基于的关系型模型。只有弄清楚SQL是如何处理数据的,才能清楚地看到Firestore与它的区别所在,从而了解如何正确地构建NoSQL数据结构。 在本指南中,我们将介绍NoSQL的设计原则、数据嵌入与引用机制,以及各种类型的关系建模方法(1-1、1-N、N

阅读全文
技术实践

如何利用WCAG 2.2标准打造更加易于访问的网站

一个网站可能看起来很专业,使用鼠标操作时也能运行得非常顺畅,但对某些用户来说,使用它仍然会遇到困难。 有时,某个表单会仅通过颜色来提示用户出现了问题;固定的页眉可能会完全覆盖当前处于键盘焦点位置的元素;登录表单可能会阻止用户从密码管理工具中复制密码并粘贴到表单中;而某个自定义按钮,用鼠标点击时可以正常使用,但当人们使用键盘操作时却毫无反应。 这些其实都是开发过程中做出的决策,并非只有在进行无障碍性审核时才会出现的问题。 《Web内容无障碍性指南》为识别和消除这些障碍提供了统一的标准。WCAG 2.2是最新的WCAG 2建议标准,世界万维网联盟也建议开发人员和各类组织在可能的情况下使用WCAG

阅读全文
技术实践

在React中处理高频实时数据:从环形缓冲区到离屏canvas技术

React在很多方面都表现得非常出色。但如果你曾经尝试过每秒向它传输数千个数据点,你就会很快意识到:React并不像一根能输送大量水流的消防水管,而更像是一根普通的花园浇水软管。 如果强迫它处理过多的数据,要么会导致“草坪被淹没”(即DOM结构变得混乱),要么会使得“管道爆裂”(也就是应用程序运行出现严重问题)。 还有另一个与上述观点相关的观察结果:你的笔记本电脑通常拥有8到16个CPU核心,而你的React应用程序几乎总是只使用其中的一个核心。主线程负责处理JavaScript代码、DOM操作、布局计算以及绘制工作;而其他核心则处于闲置状态,因为主线程实在难以维持每秒60帧的渲染速度。 这两

阅读全文