ITPUB

InnoDB 让 MySQL 流行,DuckDB 使其伟大

图片

作者介绍

司马辽太杰,10 余年互联网、金融、运营商等行业数据库管理经验,擅长常见关系型、NoSQL、MPP 、云原生等类型数据库的架构设计和运维管理。工作之余,热爱历史、足球,也喜欢读点闲书。欢迎关注我的个人公众号“程序猿读历史”。

图片

MySQL 现状:事务很稳,分析很痛

事务很稳

这两年 MySQL 在全球热度持续下降,但把时间拉到过去二十余年,无疑是全球最成功、影响力最广泛的数据库产品之一,得益于其接入层、执行引擎层与存储引擎层的清晰分层设计,以及线程执行模型,MySQL 在互联网应用中展现出了极高的稳定性和性能。

互联网的高并发、小事务的业务模型,使 MySQL 成为其事实标准。尤其是在 InnoDB 引擎逐步成熟之后,事务一致性、行级锁以及并发控制的完善支持,进一步巩固了在互联网在线事务处理领域的统治地位。

分析很痛

MySQL的AP能力一直为行业诟病。从存储结构上看,InnoDB 行存 、BTree 适合高选择性访问,不擅长大范围顺序扫描;从执行模型上看,MySQL 的执行引擎以 标量执行、瀑布式处理为主,难以充分利用现代 CPU 的 SIMD 能力; 从优化器策略上看,其核心假设仍然围绕 OLTP 工作负载展开,在复杂 Join 顺序、统计信息失真以及大表关联时,容易产生非最优执行计划。

这些都决定了MySQL不具备AP能力的数据库,实际上更严重的是,分析 SQL 带来的问题并不仅限于自身执行时间:

  • 大范围扫描会污染 Buffer Pool,挤出 OLTP 热数据

  • 长查询持有资源时间过长,放大锁与调度竞争

  • CPU 与 IO 被分析任务占用,事务延迟抖动明显

在生产环境中,这种分析拖垮交易的风险。而另一方面,今天的企业用户对数据库是既要、又要、还要。

用户需求:既要、又要、还要

随着数据库技术与开源生态日趋成熟,用户的数据库选型不再局限于少数产品。据 DB-Engines 统计,全球数据库产品已超 400 种,国内墨天轮统计亦达 200 种以上。选择愈发丰富,企业对数据库的诉求也升级为既要、又要、还要。

既要混合负载

在实际业务中,企业通常既要支撑实时交易 OLTP,又要支撑分析报表OLAP。比如笔者曾经维护的电商系统:

  • 商品数据需要在下单等交易环节使用,又要用于分析。

  • 订单数据不仅用于支付、履约,还需要生成经营报表统计。

  • 用户行为数据,要实时支撑业务决策,也要做离线或准实时分析。

MySQL 缓冲池同时缓存数据和索引,OLAP 扫描会占用大量内存,降低事务命中率;优化器对大表 join、聚合等分析任务优化有限,hash join 至今也无法解决内存溢出时退化 nested loop join 或者 sort merge join ;日志和持久化机制也会与分析查询争用磁盘带宽等等问题。

目前常见的做法是在 MySQL 实现交易数据,再通过 ETL 将数据搬到分析系统。这种方式存在明显问题:数据延迟高、链路复杂、同步过程中容易出现不一致,运维成本也大。因此,无论从业务要求,以及系统性能、稳定性、总拥有成本等多个角度看,在一个数据库中解决这两类需求是最佳实践。

又要资源隔离

与此同时,用户并不希望分析查询与在线交易操作争抢同一份资源。在生产系统中,为了在线交易系统的稳定性,往往会要求做好两则的资源隔离。理想状态下:

  • OLTP 工作负载应保持稳定、可预测

  • OLAP 查询即便执行缓慢,也不应反向影响交易

这意味着,单纯依使用一个引擎同时做好两件事,在工程挑战极大。而用户需要的是在数据一致的前提下, 将分析负载和交易负载分离开来的能力。因此,两种类型的资源隔离成了用户选择的基础。

还要兼容 MySQL

站在企业视角兼容MySQL 是一个自然而然的能力。一方面,存量系统中已经沉淀了大量基于 MySQL 语法和行为特性的 SQL。无论是复杂的业务查询、历史兼容写法,还是隐含依赖执行计划的灰色 SQL,都很难在短时间内系统性改写。

另一方面,应用侧并不只是连着一个数据库。ORM 框架、数据库中间件、连接池、SQL 解析与路由组件,普遍假设后端是一个 MySQL 协议与语义完整实现的系统。哪怕 SQL 层面差异不大,协议、返回结果、事务行为上的细微不一致,也可能引发连锁问题。

同时,组织层面的技术能力也是基于MySQL 而构建。现有的监控指标、告警策略、故障处理流程,以及 DBA 和研发团队长期积累的经验,几乎都围绕 MySQL 构建。彻底替换数据库,意味着不仅要迁移数据和应用,还要重建一整套稳定性保障体系。

因此,在大多数企业里是否替换 MySQL并不是一个可以自由讨论的问题。通常只能是在不改变 MySQL 使用方式的前提下,引入新的能力。

DuckDB:小身材、大算力

DuckDB 是一个 In-Process 的 OLAP 嵌入式数据库,常被称为 OLAP 的 SQLite,采用 MIT 开源协议¹。开源地址:

https://github.com/duckdb/duckdb

2019年,由荷兰  Centrum Wiskunde & Informatica(CWI) 数据库组发布(CWI数据库组非常牛逼,曾推出过 MonetDB、Vectorwise 等顶级数据库产品)。同年,DuckDB 的核心设计与实现以论文 《DuckDB: An Embedded Analytical Database》 被 SIGMOD 2019 正式收录,系统性地介绍了其整体架构与关键技术原理 ²。

小身材

DuckDB 是以轻巧、敏捷著称,安装部署不需独立服务器,可直接集成到应用程序中,启动迅速、资源占用极低,单个二进制文件仅几十MB。笔者本地部署的duckdb 二进制文件仅为55MB,相比于 MySQL 8.0 的二进制文件则有1.3GB,相差25倍。

[root@centos8-03 duckdb]# du -sh duckdb
55M      duckdb
[root@centos8-03 duckdb]# 
[root@centos8-03 duckdb]# 
[root@centos8-03 mysql]#cd /usr/local/mysql
[root@centos8-03 mysql]# du -sh bin
1.3G      bin
[root@centos8-03 mysql]# 

DuckDB 没有复杂的集群管理、没有独立的服务进程,甚至可以直接以内嵌库的形式运行在应用进程中。这使 DuckDB 在部署、运维与资源使用上,具备极低的门槛,适合作为 分析侧的计算引擎而存在。但这种小身材的背后,并不是性能的缺失。

大算力

与小身材形成鲜明对比的是Duckdb大算力。根据DuckDB的官方说明,随机生成了1亿数据,不确定的多列筛选条件,DuckDB 在使用 Morton 和 Hilbert 算法排序,查询在毫秒级 ³。DuckDB 采用:

  • 列式存储

  • 向量化执行引擎

  • 批量算子处理

  • 现代 CPU 友好的内存布局

在扫描、过滤、聚合、Join 等典型分析算子上,DuckDB 能够充分利用 CPU 缓存与 SIMD 指令集,将每一次内存访问的价值最大化。更重要的是,DuckDB 的设计假设与 MySQL 截然不同:它并不试图兼顾高并发写入与复杂事务,而是将重心放在 读密集、大计算量、复杂 SQL 的执行效率上。这使得两则成为一个非常理想的互补角色。

AliSQL的就要、又要、还要

最近阿里云发布了 AliSQL  DuckDB 项目,通过DuckDB的极致性能、灵活以及兼容性,以及AliSQL的极致单机性能、稳定性,满足用户既要、又要、还要的需求。

AliSQL 即 阿里云 RDS MySQL 的开源品牌,最初源于淘宝数据库团队。2018年后,阿里云主推云原生数据库 PolarDB ,AliSQL 沉寂了数年,甚至开源版本多年以来一直停留在 MySQL 5.6.32 。最近,AliSQL以王者之势归来,开源版本直接跃升至 8.0.44,并融入了 DuckDB 和向量检索能力。⁴

Image

AliSQL Duckdb 架构图

AliSQL 在阿里旗下淘宝、支付宝的双十一超高并发事务、稳定性上表现卓越,却难以承载复杂分析负载,一旦运行报表或大查询,极易拖垮核心交易系统。今天,AliSQL 通过内置 DuckDB,实现了 一套数据、两个引擎、物理隔离,100% 兼容 MySQL 协议与语法,应用无需修改一行代码,真正实现既要、又要、还要的企业级混合负载需求。

AliSQL DuckDB 的实际性能究竟如何?我们基于 TPCH 基准,对 RDS MySQL 集成的 DuckDB 引擎展开实测验证。

AliSQL DuckDB 测试结果

使用TPCH 测试阿里云RDS MySQL DuckDB OLAP的性能,并对比 innodb 和duckdb 在分析场景下的性能差距。其中 RDS 规格是 8C16G300GB 高性能磁盘,DuckDB 作为只读分析实例。

测试结论

TPCH的22个SQL,RDS MySQL InnoDB  22条SQL 接近4000秒。而 duckdb 22 条SQL 一共耗时48.218 秒,相差约2个数量级。实际上不具备可比性。

Image

测试方法

  • TPCH

TPC-H 是国际事务处理性能委员会(TPC)发布的面向决策支持系统的标准基准测试,主要用于评估数据库在复杂分析型查询场景下的性能。本次测试采用 TPC-H v3.0.1 版本,数据规模为 100GB,共包含 22 条分析型查询,不涉及数据更新或事务操作。⁵

  • 测试模型

  • region:区域表 

  • nation:国家表 

  • supplier:供应商表 

  • customer:客户表 

  • part:商品表 

  • partsupp:供应商物件表 

  • orders:订单表,

  • lineitem:订单明细表 

  • 构建数据集

本次在客户端机器上安装TPCH服务,安装过程略。TPC-H测试的数据量大小为100GB,最大的 lineitem 表数据量约6亿条。构建命令如下:

./dbgen -s 100 -f -v 

详细的测试过程,见文末附录。

总结:鱼和熊掌 MySQL 都要

过去很长时间,在MySQL 生态开源数据库世界中,OLTP和OLAP如同鱼与熊掌难以兼得。传统的ETL方案,不仅提高了使用门槛,也带来了数据延迟、链路复杂与运维负担。

AliSQL DuckDB 项目出现,提供了一条新颖解决方案。对应用而言,一切访问习惯、协议接口乃至运维体系都保持不变;对数据而言,无需经历漫长的抽取、转换与加载,即可享受专用引擎带来的算力飞跃。真正实现了企业级用户对 MySQL 既要、又要、还要的需求。 

有需求和兴趣的同学,建议下载试用以及POC,大概率会爱上它。

引用

1、https://github.com/duckdb/duckdb

2、https://duckdb.org/pdf/SIGMOD2019-demo-duckdb.pdf

3、https://duckdb.org/2025/06/06/advanced-sorting-for-fast-selective-queries

4、https://github.com/alibaba/AliSQL/wiki

5、http://www.tpc.org/tpch

附录:测试过程
  • 创建表

CREATE TABLE region (
  r_regionkey INT NOT NULL,
  r_name      CHAR(25),
  r_comment   VARCHAR(152),
  PRIMARY KEY (r_regionkey)
) ENGINE=InnoDB;

CREATE TABLE nation (
  n_nationkey INT NOT NULL,
  n_name      CHAR(25),
  n_regionkey INT,
  n_comment   VARCHAR(152),
  PRIMARY KEY (n_nationkey)
) ENGINE=InnoDB;

CREATE TABLE supplier (
  s_suppkey   BIGINT NOT NULL,
  s_name      CHAR(25),
  s_address   VARCHAR(40),
  s_nationkey INT,
  s_phone     CHAR(15),
  s_acctbal   DECIMAL(15,2),
  s_comment   VARCHAR(101),
  PRIMARY KEY (s_suppkey)
) ENGINE=InnoDB;

CREATE TABLE part (
  p_partkey     BIGINT NOT NULL,
  p_name        VARCHAR(55),
  p_mfgr        CHAR(25),
  p_brand       CHAR(10),
  p_type        VARCHAR(25),
  p_size        INT,
  p_container   CHAR(10),
  p_retailprice DECIMAL(15,2),
  p_comment     VARCHAR(23),
  PRIMARY KEY (p_partkey)
) ENGINE=InnoDB;

CREATE TABLE partsupp (
  ps_partkey    BIGINT NOT NULL,
  ps_suppkey    BIGINT NOT NULL,
  ps_availqty   INT,
  ps_supplycost DECIMAL(15,2),
  ps_comment    VARCHAR(199),
  PRIMARY KEY (ps_partkey, ps_suppkey)
) ENGINE=InnoDB;

CREATE TABLE customer (
  c_custkey    BIGINT NOT NULL,
  c_name       VARCHAR(25),
  c_address    VARCHAR(40),
  c_nationkey  INT,
  c_phone      CHAR(15),
  c_acctbal    DECIMAL(15,2),
  c_mktsegment CHAR(10),
  c_comment    VARCHAR(117),
  PRIMARY KEY (c_custkey)
) ENGINE=InnoDB;

CREATE TABLE orders (
  o_orderkey      BIGINT NOT NULL,
  o_custkey       BIGINT,
  o_orderstatus   CHAR(1),
  o_totalprice    DECIMAL(15,2),
  o_orderdate     DATE,
  o_orderpriority CHAR(15),
  o_clerk         CHAR(15),
  o_shippriority  INT,
  o_comment       VARCHAR(79),
  PRIMARY KEY (o_orderkey)
) ENGINE=InnoDB;

CREATE TABLE lineitem (
  l_orderkey      BIGINT NOT NULL,
  l_partkey       BIGINT,
  l_suppkey       BIGINT,
  l_linenumber    INT,
  l_quantity      DECIMAL(15,2),
  l_extendedprice DECIMAL(15,2),
  l_discount      DECIMAL(15,2),
  l_tax           DECIMAL(15,2),
  l_returnflag    CHAR(1),
  l_linestatus    CHAR(1),
  l_shipdate      DATE,
  l_commitdate    DATE,
  l_receiptdate   DATE,
  l_shipinstruct  CHAR(25),
  l_shipmode      CHAR(10),
  l_comment       VARCHAR(44),
  PRIMARY KEY (l_orderkey, l_linenumber)
) ENGINE=InnoDB;
  • 导入数据

通过RDS 集群地址导入,innodb和duckdb引擎会自动活得数据。

load data local infile '/data/tpch-v3/dbgen/customer.tbl' into table customer fields terminated by '|';
load data local infile '/data/tpch-v3/dbgen/lineitem.tbl' into table lineitem fields terminated by '|';
load data local infile '/data/tpch-v3/dbgen/nation.tbl' into table nation fields terminated by '|';
load data local infile '/data/tpch-v3/dbgen/orders.tbl' into table orders fields terminated by '|';
load data local infile '/data/tpch-v3/dbgen/partsupp.tbl' into table partsupp fields terminated by '|';
load data local infile '/data/tpch-v3/dbgen/part.tbl' into table part fields terminated by '|'; 
load data local infile '/data/tpch-v3/dbgen/region.tbl' into table region fields terminated by '|'; 
load data local infile '/data/tpch-v3/dbgen/supplier.tbl' into table supplier fields terminated by '|'; 
  • 测试SQL

-- Q1
SELECT
    l_returnflag,
    l_linestatus,
    sum(l_quantity) AS sum_qty,
    sum(l_extendedprice) AS sum_base_price,
    sum(l_extendedprice * (1 - l_discount)) AS sum_disc_price,
    sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)) AS sum_charge,
    avg(l_quantity) AS avg_qty,
    avg(l_extendedprice) AS avg_price,
    avg(l_discount) AS avg_disc,
    count(*) AS count_order
FROM
    lineitem
WHERE
    l_shipdate <= CAST('1998-09-02' AS date)
GROUP BY
    l_returnflag,
    l_linestatus
ORDER BY
    l_returnflag,
    l_linestatus;


-- Q2
SELECT
    s_acctbal,
    s_name,
    n_name,
    p_partkey,
    p_mfgr,
    s_address,
    s_phone,
    s_comment
FROM
    part,
    supplier,
    partsupp,
    nation,
    region
WHERE
    p_partkey = ps_partkey
    AND s_suppkey = ps_suppkey
    AND p_size = 15
    AND p_type LIKE '%BRASS'
    AND s_nationkey = n_nationkey
    AND n_regionkey = r_regionkey
    AND r_name = 'EUROPE'
    AND ps_supplycost = (
        SELECT
            min(ps_supplycost)
        FROM
            partsupp,
            supplier,
            nation,
            region
        WHERE
            p_partkey = ps_partkey
            AND s_suppkey = ps_suppkey
            AND s_nationkey = n_nationkey
            AND n_regionkey = r_regionkey
            AND r_name = 'EUROPE')
ORDER BY
    s_acctbal DESC,
    n_name,
    s_name,
    p_partkey
LIMIT 100;


-- Q3
SELECT
    l_orderkey,
    sum(l_extendedprice * (1 - l_discount)) AS revenue,
    o_orderdate,
    o_shippriority
FROM
    customer,
    orders,
    lineitem
WHERE
    c_mktsegment = 'BUILDING'
    AND c_custkey = o_custkey
    AND l_orderkey = o_orderkey
    AND o_orderdate < CAST('1995-03-15' AS date)
    AND l_shipdate > CAST('1995-03-15' AS date)
GROUP BY
    l_orderkey,
    o_orderdate,
    o_shippriority
ORDER BY
    revenue DESC,
    o_orderdate
LIMIT 10;


-- Q4
SELECT
    o_orderpriority,
    count(*) AS order_count
FROM
    orders
WHERE
    o_orderdate >= CAST('1993-07-01' AS date)
    AND o_orderdate < CAST('1993-10-01' AS date)
    AND EXISTS (
        SELECT
            *
        FROM
            lineitem
        WHERE
            l_orderkey = o_orderkey
            AND l_commitdate < l_receiptdate)
GROUP BY
    o_orderpriority
ORDER BY
    o_orderpriority;


-- Q5
SELECT
    n_name,
    sum(l_extendedprice * (1 - l_discount)) AS revenue
FROM
    customer,
    orders,
    lineitem,
    supplier,
    nation,
    region
WHERE
    c_custkey = o_custkey
    AND l_orderkey = o_orderkey
    AND l_suppkey = s_suppkey
    AND c_nationkey = s_nationkey
    AND s_nationkey = n_nationkey
    AND n_regionkey = r_regionkey
    AND r_name = 'ASIA'
    AND o_orderdate >= CAST('1994-01-01' AS date)
    AND o_orderdate < CAST('1995-01-01' AS date)
GROUP BY
    n_name
ORDER BY
    revenue DESC;


-- Q6
SELECT
    sum(l_extendedprice * l_discount) AS revenue
FROM
    lineitem
WHERE
    l_shipdate >= CAST('1994-01-01' AS date)
    AND l_shipdate < CAST('1995-01-01' AS date)
    AND l_discount BETWEEN 0.05 AND 0.07
    AND l_quantity < 24;


-- Q7
SELECT
    supp_nation,
    cust_nation,
    l_year,
    sum(volume) AS revenue
FROM (
    SELECT
        n1.n_name AS supp_nation,
        n2.n_name AS cust_nation,
        extract(year FROM l_shipdate) AS l_year,
        l_extendedprice * (1 - l_discount) AS volume
    FROM
        supplier,
        lineitem,
        orders,
        customer,
        nation n1,
        nation n2
    WHERE
        s_suppkey = l_suppkey
        AND o_orderkey = l_orderkey
        AND c_custkey = o_custkey
        AND s_nationkey = n1.n_nationkey
        AND c_nationkey = n2.n_nationkey
        AND ((n1.n_name = 'FRANCE'
                AND n2.n_name = 'GERMANY')
            OR (n1.n_name = 'GERMANY'
                AND n2.n_name = 'FRANCE'))
        AND l_shipdate BETWEEN CAST('1995-01-01' AS date)
        AND CAST('1996-12-31' AS date)) AS shipping
GROUP BY
    supp_nation,
    cust_nation,
    l_year
ORDER BY
    supp_nation,
    cust_nation,
    l_year;


-- Q8
SELECT
    o_year,
    sum(
        CASE WHEN nation = 'BRAZIL' THEN
            volume
        ELSE
            0
        END) / sum(volume) AS mkt_share
FROM (
    SELECT
        extract(year FROM o_orderdate) AS o_year,
        l_extendedprice * (1 - l_discount) AS volume,
        n2.n_name AS nation
    FROM
        part,
        supplier,
        lineitem,
        orders,
        customer,
        nation n1,
        nation n2,
        region
    WHERE
        p_partkey = l_partkey
        AND s_suppkey = l_suppkey
        AND l_orderkey = o_orderkey
        AND o_custkey = c_custkey
        AND c_nationkey = n1.n_nationkey
        AND n1.n_regionkey = r_regionkey
        AND r_name = 'AMERICA'
        AND s_nationkey = n2.n_nationkey
        AND o_orderdate BETWEEN CAST('1995-01-01' AS date)
        AND CAST('1996-12-31' AS date)
        AND p_type = 'ECONOMY ANODIZED STEEL') AS all_nations
GROUP BY
    o_year
ORDER BY
    o_year;


-- Q9
SELECT
    nation,
    o_year,
    sum(amount) AS sum_profit
FROM (
    SELECT
        n_name AS nation,
        extract(year FROM o_orderdate) AS o_year,
        l_extendedprice * (1 - l_discount) - ps_supplycost * l_quantity AS amount
    FROM
        part,
        supplier,
        lineitem,
        partsupp,
        orders,
        nation
    WHERE
        s_suppkey = l_suppkey
        AND ps_suppkey = l_suppkey
        AND ps_partkey = l_partkey
        AND p_partkey = l_partkey
        AND o_orderkey = l_orderkey
        AND s_nationkey = n_nationkey
        AND p_name LIKE '%green%') AS profit
GROUP BY
    nation,
    o_year
ORDER BY
    nation,
    o_year DESC;


-- Q10
SELECT
    c_custkey,
    c_name,
    sum(l_extendedprice * (1 - l_discount)) AS revenue,
    c_acctbal,
    n_name,
    c_address,
    c_phone,
    c_comment
FROM
    customer,
    orders,
    lineitem,
    nation
WHERE
    c_custkey = o_custkey
    AND l_orderkey = o_orderkey
    AND o_orderdate >= CAST('1993-10-01' AS date)
    AND o_orderdate < CAST('1994-01-01' AS date)
    AND l_returnflag = 'R'
    AND c_nationkey = n_nationkey
GROUP BY
    c_custkey,
    c_name,
    c_acctbal,
    c_phone,
    n_name,
    c_address,
    c_comment
ORDER BY
    revenue DESC
LIMIT 20;


-- Q11
SELECT
    ps_partkey,
    sum(ps_supplycost * ps_availqty) AS value
FROM
    partsupp,
    supplier,
    nation
WHERE
    ps_suppkey = s_suppkey
    AND s_nationkey = n_nationkey
    AND n_name = 'GERMANY'
GROUP BY
    ps_partkey
HAVING
    sum(ps_supplycost * ps_availqty) > (
        SELECT
            sum(ps_supplycost * ps_availqty) * 0.0001000000
        FROM
            partsupp,
            supplier,
            nation
        WHERE
            ps_suppkey = s_suppkey
            AND s_nationkey = n_nationkey
            AND n_name = 'GERMANY')
ORDER BY
    value DESC;


-- Q12
SELECT
    l_shipmode,
    sum(
        CASE WHEN o_orderpriority = '1-URGENT'
            OR o_orderpriority = '2-HIGH' THEN
            1
        ELSE
            0
        END) AS high_line_count,
    sum(
        CASE WHEN o_orderpriority <> '1-URGENT'
            AND o_orderpriority <> '2-HIGH' THEN
            1
        ELSE
            0
        END) AS low_line_count
FROM
    orders,
    lineitem
WHERE
    o_orderkey = l_orderkey
    AND l_shipmode IN ('MAIL', 'SHIP')
    AND l_commitdate < l_receiptdate
    AND l_shipdate < l_commitdate
    AND l_receiptdate >= CAST('1994-01-01' AS date)
    AND l_receiptdate < CAST('1995-01-01' AS date)
GROUP BY
    l_shipmode
ORDER BY
    l_shipmode;


-- Q13
SELECT
    c_count,
    count(*) AS custdist
FROM (
    SELECT
        c_custkey,
        count(o_orderkey)
    FROM
        customer
    LEFT OUTER JOIN orders ON c_custkey = o_custkey
    AND o_comment NOT LIKE '%special%requests%'
GROUP BY
    c_custkey) AS c_orders (c_custkey,
        c_count)
GROUP BY
    c_count
ORDER BY
    custdist DESC,
    c_count DESC;


-- Q14
SELECT
    100.00 * sum(
        CASE WHEN p_type LIKE 'PROMO%' THEN
            l_extendedprice * (1 - l_discount)
        ELSE
            0
        END) / sum(l_extendedprice * (1 - l_discount)) AS promo_revenue
FROM
    lineitem,
    part
WHERE
    l_partkey = p_partkey
    AND l_shipdate >= date '1995-09-01'
    AND l_shipdate < CAST('1995-10-01' AS date);


-- Q15
SELECT
    s_suppkey,
    s_name,
    s_address,
    s_phone,
    total_revenue
FROM
    supplier,
    (
        SELECT
            l_suppkey AS supplier_no,
            sum(l_extendedprice * (1 - l_discount)) AS total_revenue
        FROM
            lineitem
        WHERE
            l_shipdate >= CAST('1996-01-01' AS date)
            AND l_shipdate < CAST('1996-04-01' AS date)
        GROUP BY
            supplier_no) revenue0
WHERE
    s_suppkey = supplier_no
    AND total_revenue = (
        SELECT
            max(total_revenue)
        FROM (
            SELECT
                l_suppkey AS supplier_no,
                sum(l_extendedprice * (1 - l_discount)) AS total_revenue
            FROM
                lineitem
            WHERE
                l_shipdate >= CAST('1996-01-01' AS date)
                AND l_shipdate < CAST('1996-04-01' AS date)
            GROUP BY
                supplier_no) revenue1)
ORDER BY
    s_suppkey;


-- Q16
SELECT
    p_brand,
    p_type,
    p_size,
    count(DISTINCT ps_suppkey) AS supplier_cnt
FROM
    partsupp,
    part
WHERE
    p_partkey = ps_partkey
    AND p_brand <> 'Brand#45'
    AND p_type NOT LIKE 'MEDIUM POLISHED%'
    AND p_size IN (49, 14, 23, 45, 19, 3, 36, 9)
    AND ps_suppkey NOT IN (
        SELECT
            s_suppkey
        FROM
            supplier
        WHERE
            s_comment LIKE '%Customer%Complaints%')
GROUP BY
    p_brand,
    p_type,
    p_size
ORDER BY
    supplier_cnt DESC,
    p_brand,
    p_type,
    p_size;


-- Q17
SELECT
    sum(l_extendedprice) / 7.0 AS avg_yearly
FROM
    lineitem,
    part
WHERE
    p_partkey = l_partkey
    AND p_brand = 'Brand#23'
    AND p_container = 'MED BOX'
    AND l_quantity < (
        SELECT
            0.2 * avg(l_quantity)
        FROM
            lineitem
        WHERE
            l_partkey = p_partkey);


-- Q18
SELECT
    c_name,
    c_custkey,
    o_orderkey,
    o_orderdate,
    o_totalprice,
    sum(l_quantity)
FROM
    customer,
    orders,
    lineitem
WHERE
    o_orderkey IN (
        SELECT
            l_orderkey
        FROM
            lineitem
        GROUP BY
            l_orderkey
        HAVING
            sum(l_quantity) > 300)
    AND c_custkey = o_custkey
    AND o_orderkey = l_orderkey
GROUP BY
    c_name,
    c_custkey,
    o_orderkey,
    o_orderdate,
    o_totalprice
ORDER BY
    o_totalprice DESC,
    o_orderdate
LIMIT 100;


-- Q19
SELECT
    sum(l_extendedprice * (1 - l_discount)) AS revenue
FROM
    lineitem,
    part
WHERE (p_partkey = l_partkey
    AND p_brand = 'Brand#12'
    AND p_container IN ('SM CASE', 'SM BOX', 'SM PACK', 'SM PKG')
    AND l_quantity >= 1
    AND l_quantity <= 1 + 10
    AND p_size BETWEEN 1 AND 5
    AND l_shipmode IN ('AIR', 'AIR REG')
    AND l_shipinstruct = 'DELIVER IN PERSON')
    OR (p_partkey = l_partkey
        AND p_brand = 'Brand#23'
        AND p_container IN ('MED BAG', 'MED BOX', 'MED PKG', 'MED PACK')
        AND l_quantity >= 10
        AND l_quantity <= 10 + 10
        AND p_size BETWEEN 1 AND 10
        AND l_shipmode IN ('AIR', 'AIR REG')
        AND l_shipinstruct = 'DELIVER IN PERSON')
    OR (p_partkey = l_partkey
        AND p_brand = 'Brand#34'
        AND p_container IN ('LG CASE', 'LG BOX', 'LG PACK', 'LG PKG')
        AND l_quantity >= 20
        AND l_quantity <= 20 + 10
        AND p_size BETWEEN 1 AND 15
        AND l_shipmode IN ('AIR', 'AIR REG')
        AND l_shipinstruct = 'DELIVER IN PERSON');


-- Q20
SELECT
    s_name,
    s_address
FROM
    supplier,
    nation
WHERE
    s_suppkey IN (
        SELECT
            ps_suppkey
        FROM
            partsupp
        WHERE
            ps_partkey IN (
                SELECT
                    p_partkey
                FROM
                    part
                WHERE
                    p_name LIKE 'forest%')
                AND ps_availqty > (
                    SELECT
                        0.5 * sum(l_quantity)
                    FROM
                        lineitem
                    WHERE
                        l_partkey = ps_partkey
                        AND l_suppkey = ps_suppkey
                        AND l_shipdate >= CAST('1994-01-01' AS date)
                        AND l_shipdate < CAST('1995-01-01' AS date)))
            AND s_nationkey = n_nationkey
            AND n_name = 'CANADA'
        ORDER BY
            s_name;


-- Q21
SELECT
    s_name,
    count(*) AS numwait
FROM
    supplier,
    lineitem l1,
    orders,
    nation
WHERE
    s_suppkey = l1.l_suppkey
    AND o_orderkey = l1.l_orderkey
    AND o_orderstatus = 'F'
    AND l1.l_receiptdate > l1.l_commitdate
    AND EXISTS (
        SELECT
            *
        FROM
            lineitem l2
        WHERE
            l2.l_orderkey = l1.l_orderkey
            AND l2.l_suppkey <> l1.l_suppkey)
    AND NOT EXISTS (
        SELECT
            *
        FROM
            lineitem l3
        WHERE
            l3.l_orderkey = l1.l_orderkey
            AND l3.l_suppkey <> l1.l_suppkey
            AND l3.l_receiptdate > l3.l_commitdate)
    AND s_nationkey = n_nationkey
    AND n_name = 'SAUDI ARABIA'
GROUP BY
    s_name
ORDER BY
    numwait DESC,
    s_name
LIMIT 100;


-- Q22
SELECT
    cntrycode,
    count(*) AS numcust,
    sum(c_acctbal) AS totacctbal
FROM (
    SELECT
        substring(c_phone FROM 1 FOR 2) AS cntrycode,
        c_acctbal
    FROM
        customer
    WHERE
        substring(c_phone FROM 1 FOR 2) IN ('13', '31', '23', '29', '30', '18', '17')
        AND c_acctbal > (
            SELECT
                avg(c_acctbal)
            FROM
                customer
            WHERE
                c_acctbal > 0.00
                AND substring(c_phone FROM 1 FOR 2) IN ('13', '31', '23', '29', '30', '18', '17'))
            AND NOT EXISTS (
                SELECT
                    *
                FROM
                    orders
                WHERE
                    o_custkey = c_custkey)) AS custsale
GROUP BY
    cntrycode
ORDER BY
    cntrycode;

Image