MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

SQLite分组与去重操作实例

2023-03-086.3k 阅读

SQLite 分组操作

分组操作概述

在 SQLite 中,分组操作是将数据按照一个或多个列的值进行划分,形成不同的组。分组操作通常与聚合函数(如 SUM、AVG、COUNT 等)结合使用,以对每个组的数据进行汇总计算。通过分组,我们可以从大量数据中提取有价值的信息,例如计算每个部门的平均工资、统计每个城市的用户数量等。

GROUP BY 子句基础用法

GROUP BY 子句用于对查询结果进行分组。其基本语法如下:

SELECT column1, aggregate_function(column2)
FROM table_name
GROUP BY column1;

在这个语法中,column1 是用于分组的列,aggregate_function(column2) 是对 column2 应用的聚合函数,例如 SUM(column2)AVG(column2) 等。

假设有一个 employees 表,结构如下:

CREATE TABLE employees (
    id INTEGER PRIMARY KEY,
    name TEXT,
    department TEXT,
    salary REAL
);

并且插入了以下数据:

INSERT INTO employees (name, department, salary) VALUES
('Alice', 'HR', 5000),
('Bob', 'IT', 6000),
('Charlie', 'HR', 5500),
('David', 'IT', 6500);

要计算每个部门的平均工资,可以使用以下查询:

SELECT department, AVG(salary) AS average_salary
FROM employees
GROUP BY department;

这个查询会按照 department 列对数据进行分组,然后计算每个组中 salary 列的平均值,并将结果以 average_salary 作为别名显示。

按多列分组

除了按单列分组,SQLite 还支持按多列分组。按多列分组时,只有当所有分组列的值都相同时,才会被分到同一组。语法如下:

SELECT column1, column2, aggregate_function(column3)
FROM table_name
GROUP BY column1, column2;

假设 employees 表新增了 job_title 列:

ALTER TABLE employees ADD COLUMN job_title TEXT;
UPDATE employees SET job_title = 'Manager' WHERE name = 'Alice';
UPDATE employees SET job_title = 'Developer' WHERE name = 'Bob';
UPDATE employees SET job_title = 'Assistant' WHERE name = 'Charlie';
UPDATE employees SET job_title = 'Developer' WHERE name = 'David';

现在要计算每个部门、每个职位的平均工资,可以这样查询:

SELECT department, job_title, AVG(salary) AS average_salary
FROM employees
GROUP BY department, job_title;

这个查询会先按 department 分组,在每个部门组内再按 job_title 分组,然后计算每个分组的平均工资。

分组中的过滤 - HAVING 子句

HAVING 子句用于对分组后的结果进行过滤。与 WHERE 子句不同,WHERE 子句用于在分组之前对行进行过滤,而 HAVING 子句用于在分组之后对组进行过滤。语法如下:

SELECT column1, aggregate_function(column2)
FROM table_name
GROUP BY column1
HAVING condition;

例如,要找出平均工资大于 6000 的部门,可以这样查询:

SELECT department, AVG(salary) AS average_salary
FROM employees
GROUP BY department
HAVING AVG(salary) > 6000;

在这个查询中,先按 department 分组并计算平均工资,然后 HAVING 子句过滤出平均工资大于 6000 的组。

分组与排序结合

通常在分组操作后,我们还需要对结果进行排序,以便更好地查看数据。可以在查询中使用 ORDER BY 子句来实现。语法如下:

SELECT column1, aggregate_function(column2)
FROM table_name
GROUP BY column1
HAVING condition
ORDER BY column1 [ASC|DESC];

例如,要计算每个部门的平均工资,并按平均工资从高到低排序:

SELECT department, AVG(salary) AS average_salary
FROM employees
GROUP BY department
ORDER BY average_salary DESC;

这个查询先按 department 分组计算平均工资,然后按 average_salary 降序排序。

SQLite 去重操作

去重操作概述

在 SQLite 中,去重操作用于从查询结果中去除重复的行。重复行是指所有列的值都完全相同的行。去重操作在处理数据时非常有用,例如当我们从多个数据源合并数据时,可能会出现重复记录,通过去重可以得到唯一的数据集。

DISTINCT 关键字基础用法

DISTINCT 关键字用于从查询结果中去除重复的行。其基本语法如下:

SELECT DISTINCT column1, column2
FROM table_name;

假设我们有一个 products 表,结构如下:

CREATE TABLE products (
    id INTEGER PRIMARY KEY,
    name TEXT,
    category TEXT
);

并且插入了以下数据,其中存在一些重复数据:

INSERT INTO products (name, category) VALUES
('Product A', 'Electronics'),
('Product B', 'Clothing'),
('Product A', 'Electronics'),
('Product C', 'Food');

要获取不重复的产品名称和类别,可以使用以下查询:

SELECT DISTINCT name, category
FROM products;

这个查询会返回 namecategory 组合不重复的行,去除了重复的 ('Product A', 'Electronics') 行。

对单个列去重

当只对单个列进行去重时,DISTINCT 关键字的使用更加简单。例如,要获取不重复的产品类别:

SELECT DISTINCT category
FROM products;

这个查询会返回 products 表中不重复的 category 值,即 ElectronicsClothingFood

DISTINCT 与聚合函数结合

DISTINCT 关键字也可以与聚合函数结合使用。例如,要计算不同产品类别的数量,可以这样查询:

SELECT COUNT(DISTINCT category) AS distinct_category_count
FROM products;

在这个查询中,COUNT(DISTINCT category) 会先对 category 列进行去重,然后计算去重后的类别数量,并以 distinct_category_count 作为别名显示。

去重的性能考虑

在大数据量的情况下,使用 DISTINCT 去重可能会对性能产生影响。因为 SQLite 需要对整个数据集进行扫描,以识别并去除重复行。如果可能,尽量在数据插入阶段就避免重复数据的插入,例如使用 INSERT OR IGNORE 语句。另外,如果只是需要判断是否存在重复数据,而不是获取去重后的结果,可以使用 EXISTS 子查询结合聚合函数来优化查询性能。

例如,要判断 products 表中是否存在重复的产品名称,可以这样查询:

SELECT EXISTS (
    SELECT name
    FROM products
    GROUP BY name
    HAVING COUNT(*) > 1
);

这个查询通过分组和 HAVING 子句来判断是否有产品名称出现次数大于 1,如果存在则返回 1,否则返回 0。

复杂去重场景

有时候,我们需要在更复杂的场景下去重。例如,在多表连接的查询结果中去重。假设有一个 orders 表,记录了订单信息,结构如下:

CREATE TABLE orders (
    order_id INTEGER PRIMARY KEY,
    product_id INTEGER,
    quantity INTEGER,
    FOREIGN KEY (product_id) REFERENCES products(id)
);

并且插入了一些订单数据:

INSERT INTO orders (product_id, quantity) VALUES
(1, 2),
(2, 1),
(1, 3);

现在要获取所有订单涉及的不重复的产品名称,可以通过连接 orders 表和 products 表,并使用 DISTINCT 关键字:

SELECT DISTINCT p.name
FROM products p
JOIN orders o ON p.id = o.product_id;

这个查询通过 JOIN 操作将 products 表和 orders 表连接起来,然后对连接结果中的 p.name 进行去重,得到所有订单涉及的不重复产品名称。

分组与去重操作的综合应用

综合应用场景介绍

在实际的数据库应用中,分组和去重操作常常结合使用。例如,在电商数据分析中,我们可能需要统计每个用户购买的不同产品类别数量,这就需要先按用户分组,然后在每个用户组内对产品类别进行去重并计数。

示例实现

假设有一个 orders 表记录订单信息,一个 products 表记录产品信息,结构如下:

CREATE TABLE orders (
    order_id INTEGER PRIMARY KEY,
    user_id INTEGER,
    product_id INTEGER,
    FOREIGN KEY (product_id) REFERENCES products(id)
);

CREATE TABLE products (
    id INTEGER PRIMARY KEY,
    name TEXT,
    category TEXT
);

插入一些示例数据:

INSERT INTO products (name, category) VALUES
('Product A', 'Electronics'),
('Product B', 'Clothing'),
('Product C', 'Electronics'),
('Product D', 'Food');

INSERT INTO orders (user_id, product_id) VALUES
(1, 1),
(1, 2),
(2, 1),
(2, 3),
(1, 4);

要统计每个用户购买的不同产品类别数量,可以使用以下查询:

SELECT o.user_id, COUNT(DISTINCT p.category) AS distinct_category_count
FROM orders o
JOIN products p ON o.product_id = p.id
GROUP BY o.user_id;

这个查询首先通过 JOINorders 表和 products 表连接起来,然后按 o.user_id 分组,在每个用户组内对 p.category 进行去重并计数,得到每个用户购买的不同产品类别数量。

优化综合操作

在进行分组与去重的综合操作时,性能优化至关重要。可以通过合理创建索引来提高查询效率。例如,在上述查询中,可以在 orders 表的 user_idproduct_id 列,以及 products 表的 idcategory 列上创建索引:

CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_product_id ON orders(product_id);
CREATE INDEX idx_products_id ON products(id);
CREATE INDEX idx_products_category ON products(category);

这些索引可以加速连接操作、分组操作以及去重过程中的数据查找,从而提高整个查询的性能。

复杂综合场景

在更复杂的场景下,可能还需要结合其他 SQL 特性。例如,假设 orders 表新增了 order_date 列记录订单日期,现在要统计每个月每个用户购买的不同产品类别数量,并且只统计购买类别数量大于 1 的用户。可以使用以下查询:

SELECT
    strftime('%Y-%m', o.order_date) AS month,
    o.user_id,
    COUNT(DISTINCT p.category) AS distinct_category_count
FROM orders o
JOIN products p ON o.product_id = p.id
GROUP BY strftime('%Y-%m', o.order_date), o.user_id
HAVING COUNT(DISTINCT p.category) > 1;

在这个查询中,使用 strftime 函数提取订单日期的年月部分,然后按年月和用户 ID 分组,对每个分组内的产品类别去重计数,并通过 HAVING 子句过滤出购买类别数量大于 1 的分组。

实际案例分析

案例一:网站访问统计

假设有一个 website_visits 表,记录了网站的访问信息,结构如下:

CREATE TABLE website_visits (
    visit_id INTEGER PRIMARY KEY,
    user_id INTEGER,
    page_url TEXT,
    visit_time DATETIME
);

每天有成千上万条访问记录。现在要分析每个用户访问的不同页面数量,并且只关注访问页面数量大于 10 的用户。可以使用以下查询:

SELECT user_id, COUNT(DISTINCT page_url) AS distinct_page_count
FROM website_visits
GROUP BY user_id
HAVING COUNT(DISTINCT page_url) > 10;

这个查询先按 user_id 分组,然后在每个用户组内对 page_url 进行去重计数,最后通过 HAVING 子句过滤出访问页面数量大于 10 的用户。

案例二:销售数据分析

在一个销售系统中,有 sales 表记录销售信息,products 表记录产品信息,结构如下:

CREATE TABLE sales (
    sale_id INTEGER PRIMARY KEY,
    product_id INTEGER,
    customer_id INTEGER,
    sale_date DATE,
    quantity INTEGER,
    price REAL,
    FOREIGN KEY (product_id) REFERENCES products(id)
);

CREATE TABLE products (
    id INTEGER PRIMARY KEY,
    name TEXT,
    category TEXT
);

要分析每个月每个客户购买的不同产品类别总销售额,并且按销售额从高到低排序。可以使用以下查询:

SELECT
    strftime('%Y-%m', s.sale_date) AS month,
    s.customer_id,
    p.category,
    SUM(s.quantity * s.price) AS total_sales
FROM sales s
JOIN products p ON s.product_id = p.id
GROUP BY strftime('%Y-%m', s.sale_date), s.customer_id, p.category
ORDER BY total_sales DESC;

这个查询通过连接 sales 表和 products 表,按月份、客户 ID 和产品类别分组,计算每个分组的总销售额,并按总销售额降序排序。

案例三:学生成绩分析

在学校的成绩管理系统中,有 students 表记录学生信息,courses 表记录课程信息,scores 表记录学生的成绩信息,结构如下:

CREATE TABLE students (
    student_id INTEGER PRIMARY KEY,
    name TEXT
);

CREATE TABLE courses (
    course_id INTEGER PRIMARY KEY,
    name TEXT
);

CREATE TABLE scores (
    score_id INTEGER PRIMARY KEY,
    student_id INTEGER,
    course_id INTEGER,
    score REAL,
    FOREIGN KEY (student_id) REFERENCES students(student_id),
    FOREIGN KEY (course_id) REFERENCES courses(course_id)
);

要分析每个学生通过的不同课程数量(假设 60 分及以上为通过),并且按通过课程数量从多到少排序。可以使用以下查询:

SELECT s.student_id, COUNT(DISTINCT c.course_id) AS passed_course_count
FROM students s
JOIN scores sc ON s.student_id = sc.student_id
JOIN courses c ON sc.course_id = c.course_id
WHERE sc.score >= 60
GROUP BY s.student_id
ORDER BY passed_course_count DESC;

这个查询通过多表连接,筛选出成绩大于等于 60 分的记录,然后按学生 ID 分组,对每个学生通过的课程进行去重计数,并按通过课程数量降序排序。

常见问题及解决方法

分组结果与预期不符

  1. 问题描述:在使用 GROUP BY 子句时,得到的分组结果与预期不一致,例如分组错误或者聚合函数计算结果不正确。
  2. 原因分析:可能是分组列选择错误,或者在聚合函数中使用了不应该参与分组计算的列。另外,数据类型不匹配也可能导致问题。
  3. 解决方法:仔细检查分组列,确保其能够准确划分数据。同时,确认聚合函数中使用的列与分组逻辑一致。如果是数据类型问题,检查数据是否正确录入,必要时进行数据类型转换。

DISTINCT 无法正确去重

  1. 问题描述:使用 DISTINCT 关键字后,查询结果仍然包含重复行。
  2. 原因分析:可能是因为存在隐藏的不可见字符,或者在比较时忽略了某些列。另外,数据库版本问题也可能导致 DISTINCT 行为异常。
  3. 解决方法:检查数据中是否存在隐藏字符,可以使用函数去除特殊字符后再进行去重。确认在 SELECT 子句中列出了所有需要考虑的列,以确保完全相同的行被正确识别。如果怀疑是数据库版本问题,可以升级或切换到稳定版本进行测试。

分组与去重结合查询性能问题

  1. 问题描述:在进行分组与去重结合的复杂查询时,查询执行时间过长,性能低下。
  2. 原因分析:大数据量下,分组和去重操作本身就较为消耗资源。同时,缺少合适的索引、不合理的表连接方式等都可能导致性能问题。
  3. 解决方法:对涉及的列创建合适的索引,优化表连接方式,例如选择合适的连接类型(INNER JOIN、LEFT JOIN 等)。如果可能,在数据插入阶段就对数据进行预处理,避免重复数据的插入,减少后续去重的工作量。另外,可以考虑对大数据集进行分区处理,以提高查询效率。

通过以上对 SQLite 分组与去重操作的详细介绍、代码示例、综合应用及常见问题解决,希望能帮助读者全面掌握这两种重要的数据库操作,在实际开发和数据分析中能够更加高效地处理数据。