You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

替代CTE简化SQL查询:按小时统计收银员交易数的优化方案

问题

我有一张交易表,结构如下:

timestamp           | cashier   | amount |
-----------------------------------------
2023-05-01 07:15:25 | cashier_1 | 25     |
2023-05-01 08:16:25 | cashier_1 | 18     |
2023-05-01 09:35:00 | cashier_2 | 76     |
2023-05-01 10:26:00 | cashier_3 | 45     |
2023-05-01 10:42:13 | cashier_3 | 12     |
2023-05-01 11:04:12 | cashier_4 | 80     |

需要生成一份报表,展示各收银员每小时的交易数量,报表格式如下:

hour  | cashier_1 | cashier_2 | cashier_3 |
-------------------------------------------
7:00  |   43      |   14      |    98     | 
8:00  |   12      |   67      |    76     |
9:00  |   15      |   32      |    54     |
10:00 |   54      |   45      |    34     |
11:00 |   12      |   98      |    54     |
12:00 |   18      |   76      |    65     |
13:00 |   56      |   12      |    76     |

当前我使用多个CTE编写查询,代码如下:

WITH cashier_1 AS (
    SELECT HOUR(timestamp) AS HOUR,
        COUNT(*) as cashier
    FROM my_table
    WHERE cashier = 'cashier_1'
    GROUP BY HOUR(timestamp)
    ORDER BY HOUR(timestamp)
),
cashier_2 AS (
    SELECT HOUR(timestamp) AS HOUR,
        COUNT(*) as cashier
    FROM my_table
    WHERE cashier = 'cashier_2'
    GROUP BY HOUR(timestamp)
    ORDER BY HOUR(timestamp)
),
cashier_3 AS (
    SELECT HOUR(timestamp) AS HOUR,
        COUNT(*) as cashier
    FROM my_table
    WHERE cashier = 'cashier_3'
    GROUP BY HOUR(timestamp)
    ORDER BY HOUR(timestamp)
)
SELECT CONCAT(CAST(c1.HOUR AS varchar), ':00') as HOUR,
    c1.cashier AS cashier_1,
    c2.cashier AS cashier_2,
    c3.cashier AS cashier_3
FROM cashier_1 c1
    LEFT JOIN cashier_2 c2 ON c1.HOUR = c2.HOUR
    LEFT JOIN cashier_3 c3 ON c1.HOUR = c3.HOUR
ORDER BY c1.HOUR

但由于收银员数量众多,这种写法会导致查询代码长达数百行。请问是否有SQL方案可以简化查询的冗长性,从而提升代码的可维护性和扩展性?

解决方案

1. 静态透视(适用于收银员列表固定的场景)

如果收银员名单固定,用条件聚合替代多CTE,仅需一次分组即可完成统计,代码大幅精简:

SELECT 
    CONCAT(CAST(HOUR(timestamp) AS VARCHAR), ':00') AS hour,
    COUNT(CASE WHEN cashier = 'cashier_1' THEN 1 END) AS cashier_1,
    COUNT(CASE WHEN cashier = 'cashier_2' THEN 1 END) AS cashier_2,
    COUNT(CASE WHEN cashier = 'cashier_3' THEN 1 END) AS cashier_3,
    -- 新增收银员时,仅需追加对应行即可
    COUNT(CASE WHEN cashier = 'cashier_4' THEN 1 END) AS cashier_4
FROM my_table
GROUP BY HOUR(timestamp)
ORDER BY HOUR(timestamp);

若需要将无交易的小时显示为0而非NULL,可将COUNT替换为SUM并指定ELSE值:

SUM(CASE WHEN cashier = 'cashier_1' THEN 1 ELSE 0 END) AS cashier_1

2. 动态透视(适用于收银员列表不固定的场景)

若收银员数量频繁变化,静态写法仍需手动修改代码,此时可采用动态SQL自动生成透视列,不同数据库语法略有差异:

MySQL/MariaDB 版本

SET @sql = NULL;
SELECT
  GROUP_CONCAT(DISTINCT
    CONCAT(
      'COUNT(CASE WHEN cashier = ''',
      cashier,
      ''' THEN 1 END) AS ',
      cashier
    )
  ) INTO @sql
FROM my_table;

SET @sql = CONCAT('SELECT CONCAT(CAST(HOUR(timestamp) AS CHAR), '':00'') AS hour, ', @sql, ' 
                  FROM my_table 
                  GROUP BY HOUR(timestamp) 
                  ORDER BY HOUR(timestamp);');

PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SQL Server 版本

DECLARE @cols AS NVARCHAR(MAX),
        @query  AS NVARCHAR(MAX);

SET @cols = STUFF((SELECT DISTINCT ',' + QUOTENAME(cashier) 
                   FROM my_table
                   FOR XML PATH(''), TYPE
                  ).value('.', 'NVARCHAR(MAX)'),1,1,'');

SET @query = 'SELECT CONCAT(CAST(DATEPART(HOUR, timestamp) AS VARCHAR), '':00'') AS hour, ' + @cols + ' 
              FROM (
                    SELECT cashier, DATEPART(HOUR, timestamp) AS hour
                    FROM my_table
                   ) AS src
              PIVOT (
                    COUNT(cashier)
                    FOR cashier IN (' + @cols + ')
                   ) AS pvt
              ORDER BY hour;';

EXECUTE(@query);

PostgreSQL 版本

需先启用tablefunc扩展,再使用crosstab函数实现透视:

-- 启用扩展(仅需执行一次)
CREATE EXTENSION IF NOT EXISTS tablefunc;

SELECT * FROM crosstab(
  'SELECT CONCAT(CAST(HOUR(timestamp) AS VARCHAR), '':00'') AS hour,
          cashier,
          COUNT(*) AS count
   FROM my_table
   GROUP BY HOUR(timestamp), cashier
   ORDER BY 1, 2',
  'SELECT DISTINCT cashier FROM my_table ORDER BY 1'
) AS ct(
  hour VARCHAR,
  cashier_1 INT,
  cashier_2 INT,
  cashier_3 INT,
  cashier_4 INT -- 根据实际收银员数量补充列定义
);

若需完全动态生成列,可结合PL/pgSQL编写自定义函数实现。

方案对比

  • 静态透视:代码简洁、性能稳定,适合收银员名单固定的场景。
  • 动态透视:无需手动维护列、扩展性强,适合收银员频繁新增/删除的场景,但调试复杂度较高,部分数据库需权限支持动态SQL。

内容的提问来源于stack exchange,提问作者David López

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 15:12:05