多租户应用跨库统计:GROUP_CONCAT长度超限的通用解决方法
问题:多租户独立数据库的全租户统计通用实现
我们的多租户应用具备以下特性:
- 每个租户拥有独立数据库,库内包含
users表 - 存在
landlord.tenants表,存储所有租户对应的数据库名称(db_name字段)
为避免PHP循环遍历租户的低效方式,尝试在MySQL端直接实现全租户用户总数统计,编写了如下SQL:
# landlord.tenants table has a db_name column # each tenant DB has a users table # We want to know how many users in total we have on the app SELECT GROUP_CONCAT( CONCAT( '(SELECT count(id) FROM `', db_name, '`.`users`)' ) SEPARATOR ' + ' ) as tmp FROM landlord.tenants INTO @sql; SET @sql := CONCAT('SELECT ', @sql); SELECT @sql; PREPARE stmt FROM @sql; EXECUTE stmt;
但该方案在租户数量较多时,因GROUP_CONCAT存在默认长度限制,会导致拼接的SQL被截断,统计结果不准确。需要一个通用化的统计实现方案。
可行解决方案
方案1:临时调整GROUP_CONCAT长度限制(应急快速方案)
如果只是临时需要统计,可在执行统计SQL前,临时调高当前会话的group_concat_max_len值:
# 设置会话级的GROUP_CONCAT最大长度,根据实际需要调整数值 SET SESSION group_concat_max_len = 1000000; # 执行原统计SQL SELECT GROUP_CONCAT( CONCAT( '(SELECT count(id) FROM `', db_name, '`.`users`)' ) SEPARATOR ' + ' ) as tmp FROM landlord.tenants INTO @sql; SET @sql := CONCAT('SELECT ', @sql); PREPARE stmt FROM @sql; EXECUTE stmt;
注意:该设置仅在当前会话生效,适合临时统计场景,不建议作为长期通用方案。
方案2:使用存储过程循环累加(通用稳定方案)
通过创建存储过程,循环遍历每个租户数据库并逐步累加用户数,彻底避开GROUP_CONCAT的长度限制:
DELIMITER // CREATE PROCEDURE CalculateTotalUsers() BEGIN DECLARE done INT DEFAULT FALSE; DECLARE tenant_db VARCHAR(255); DECLARE total BIGINT DEFAULT 0; DECLARE tenant_cursor CURSOR FOR SELECT db_name FROM landlord.tenants; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; OPEN tenant_cursor; read_loop: LOOP FETCH tenant_cursor INTO tenant_db; IF done THEN LEAVE read_loop; END IF; # 动态执行单租户统计并累加 SET @count_sql = CONCAT('SELECT COUNT(id) INTO @tenant_count FROM `', tenant_db, '`.`users`'); PREPARE stmt FROM @count_sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET total = total + @tenant_count; END LOOP; CLOSE tenant_cursor; # 返回总用户数 SELECT total AS total_users; END // DELIMITER ; # 调用存储过程获取结果 CALL CalculateTotalUsers();
该方案不受租户数量限制,逻辑清晰,适合作为长期通用的统计方案。
方案3:利用UNION ALL拼接子查询(替代GROUP_CONCAT)
如果不想使用存储过程,可通过拼接UNION ALL生成子查询后统一求和:
SELECT GROUP_CONCAT( CONCAT( 'SELECT COUNT(id) AS user_count FROM `', db_name, '`.`users`' ) SEPARATOR ' UNION ALL ' ) as tmp FROM landlord.tenants INTO @sql; SET @sql := CONCAT('SELECT SUM(user_count) AS total_users FROM (', @sql, ') AS temp'); PREPARE stmt FROM @sql; EXECUTE stmt;
若租户数量仍导致GROUP_CONCAT截断,可临时调高group_concat_max_len,或结合存储过程的循环方式拼接语句。
内容的提问来源于stack exchange,提问作者Vincent Mimoun-Prat
相关产品推荐
相关产品推荐

