Superset умеет строить сводные таблицы,  но при выводе подытогов и итогов (subtotals/total)  в сводных таблицах с иерархически структурированной метрикой сталкиваешься с ограничением: из коробки можно строить только простые метрики. А если нужна процентная метрика с корректными подытогами и итогами — приходится искать обходные решения.

 В этой статье разберём, как обойти это ограничение на уровне обращения к БД (ClickHouse).

Я  нашла два подхода: UNION и массивы. И начну с первого, детально разобрав решение именно через UNION.

Разберем на примере расчета показателя Потери, %

Потери, % = Потери, шт/Продажи, шт

Если мы построим итоги и подытоги стандартным функционалом SuperSet,

стандартый функционал в SuperSet
стандартый функционал в SuperSet

то в результате получим не совсем то, что нужно.

В подытогах/итогах сложатся уже рассчитанные значения по форматам, что не является корректным.

вид таблицы при выводе итогов и подытогов с помощью функционала SupserSet
вид таблицы при выводе итогов и подытогов с помощью функционала SupserSet

Как обойти это ограничение я покажу на примере ClickHouse — у PostgreSQL тот же приём может не сработать

Считаем показатель для каждого уровня:

  1. Дата-Регион-Формат

  2. Дата-Регион /*подытог*/

  3. Дата /*Итог*/

И далее "схлопываем" через UNION

Вид запроса:

/*рассчитываем до Дата-Регион- Формат*/
SELECT 
	DAY_ID  AS PERIOD,
	REGION,
	FORMAT AS FRMT,
	Sum(LOST) AS LOST,
  	Sum(SALE) AS SALE
FROM temp.my_table
WHERE DAY_ID BETWEEN CURDATE()-7 AND CURDATE() - 1
GROUP BY 1,2,3

/*рассчитываем подытог Дата-Регион, вместо формата прописываем  слово «Итог» */
UNION ALL

SELECT 
	DAY_ID ASPERIOD,
	REGION,
	'Итог' AS FRMT, 
	Sum(LOST) AS LOST,
  	Sum(SALE) AS SALE
FROM temp.my_table
WHERE DAY_ID BETWEEN CURDATE()-7 AND CURDATE() - 1
GROUP BY 1,2,3

/*рассчитываем итог по компании, вместо формата ставим одинарные кавычки (внутри пусто), вместо региона прописываем «Итог по компании» */
UNION ALL

SELECT 
	DAY_ID AS PERIOD,
	'Итог по компании' AS REGION, 
	'' AS FRMT, 
	Sum(LOST) AS LOST,
  	Sum(SALE) AS SALE
FROM temp.my_table
WHERE DAY_ID BETWEEN CURDATE()-7 AND CURDATE() - 1
GROUP BY 1,2,3

Нюансы. Когда мы прописываем «Итог» в Select-е,  то поле имеет формат String, и если поле, которое вы объединяете имеет формат Int, например, то его надо преобразовать toString(MyColumn)

Строим сводную таблицу:

Сводная таблица при построении запроса через UNION
Сводная таблица при построении запроса через UNION

Итоги и подытоги рассчитались корректно, но таблица имеет не совсем правильный вид: "Итог" между форматами, а "Итог по компании" расположился под одним из округов.

Поправим это, использовав «Невидимый символ». Это не пробел, а именно символ.  Можно в поисковик ввести Invisible symbol, перейти на предложенный сайт и там скопировать этот символ.

Вид запроса с добавлением «невидимого символа» в  '‎ Итог по компании' и '‎ Итог'

/*рассчитываем до Дата-Регион- Формат*/
SELECT 
	DAY_ID AS PERIOD,
	REGION,
	FORMAT AS FRMT,
	Sum(LOST) AS LOST,
  	Sum(SALE) AS SALE
FROM temp.my_table
WHERE DAY_ID BETWEEN CURDATE()-7 AND CURDATE() - 1
GROUP BY 1,2,3

/*рассчитываем подытог Дата-Регион, вместо формата прописываем  слово «Итог» впереди пустой символ */
UNION ALL

SELECT 
	DAY_ID ASPERIOD,
	REGION,
	'ㅤИтог' AS FRMT, 
	Sum(LOST) AS LOST,
  	Sum(SALE) AS SALE
FROM temp.my_table
WHERE DAY_ID BETWEEN CURDATE()-7 AND CURDATE() - 1
GROUP BY 1,2,3

/*рассчитываем итог по компании, вместо формата ставим одинарные кавычки (внутри пусто), вместо региона прописываем «Итог по компании» впереди пустой символ*/
UNION ALL

SELECT 
	DAY_ID AS PERIOD,
	'ㅤИтог по компании' AS REGION, 
	'' AS FRMT, 
	Sum(LOST) AS LOST,
  	Sum(SALE) AS SALE
FROM temp.my_table
WHERE DAY_ID BETWEEN CURDATE()-7 AND CURDATE() - 1
GROUP BY 1,2,3

Новый вид сводной таблицы после применения "невидимого символа"

Итог по компании и Итог теперь расположены правильно: "Итог" снизу под форматами, а "Итог по компании" в самом конце таблицы под всеми округами.

Но пользователям больше нравится, когда итог и подытог расположены сверху, чтобы  сразу первой строчкой видеть «Итог по компании», а не пролистывать вниз.

Также такое расположение проще форматировать с помощью CSS.

Для того, чтобы итоги и подытоги расположились первой строкой, в '‎ ‎Итог по компании' и '‎ Итог' вначале добавим один «невидимый символ», а к названию округа и формата присоединим два невидимых символа. Соответственно сортировка пройдет по количеству символов.

Запрос будет иметь вид

/*рассчитываем до Дата-Регион- Формат
к региону и формату через CONCAT Добавляем для пустых символа */
SELECT 
	DAY_ID AS PERIOD,
	CONCAT('ㅤㅤ',REGION) AS REGION,
	CONCAT('ㅤㅤ',FORMAT) AS FRMT,
	Sum(LOST) AS LOST,
  	Sum(SALE) AS SALE
FROM temp.my_table
WHERE DAY_ID BETWEEN CURDATE()-7 AND CURDATE() - 1
GROUP BY 1,2,3

/*рассчитываем подытог Дата-Регион, 
вместо формата прописываем слово «Итог» впереди пустой символ, 
к региону через CONCAT Добавляем для пустых символа */
UNION ALL

SELECT 
	DAY_ID ASPERIOD,
	CONCAT('ㅤㅤ',REGION) AS REGION,
	'ㅤИтог' AS FRMT,
	Sum(LOST) AS LOST,
  	Sum(SALE) AS SALE
FROM temp.my_table
WHERE DAY_ID BETWEEN CURDATE()-7 AND CURDATE() - 1
GROUP BY 1,2,3

/*рассчитываем итог по компании, 
вместо формата ставим одинарные кавычки (внутри пусто), 
вместо региона прописываем «Итог по компании» впереди пустой символ*/
UNION ALL

SELECT 
	DAY_ID AS PERIOD,
	'ㅤИтог по компании' AS REGION, 
	'' AS FRMT, 
	Sum(LOST) AS LOST,
  	Sum(SALE) AS SALE
FROM temp.my_table
WHERE DAY_ID BETWEEN CURDATE()-7 AND CURDATE() - 1
GROUP BY 1,2,3

Вид итоговой сводной таблицы:

Итоговый вид сводной таблицы
Итоговый вид сводной таблицы

Знаю, что многим интересен еще и код CSS, поэтому размещаю его ниже:

.pivot_table_v_2 table.pvtTable {
 width:55% !important;
 font-size: 12px !important;
 background-color:  white;
} /*задаем динамическую ширину сводной таблицы*/

.pvtTable {
  border: 2px solid lightgrey; 
  border-radius: 5px;
} /*граница вокруг всей сводной*/ 

.pivot_table_v_2 table.pvtTable tr th.pvtAxisLabel{ 
  font-size: 0px;
  border-color: white;
  width: 0%;
  padding: 0px !important;
} /*скрываем названия измерений слово metric и уменьшаем ширину*/

.pivot_table_v_2 table.pvtTable tr:nth-of-type(3) th.pvtAxisLabel { 
  font-size: 13px;
  font-weight: 700;
  text-align: center;
  padding: 1px 8px !important;
} /*название столбцов возвращаем*/

.pivot_table_v_2 table.pvtTable thead tr th.pvtTotalLabel {
  border: 0px solid white;
  padding: 0px !important;
} /*убираем границы у левой ячейки в заголовках*/

.pivot_table_v_2 table.pvtTable thead tr:nth-of-type(1) th.pvtColLabel {
  color: black;
  text-align: center;
  font-size: 13px;
  font-weight: 600;
  padding: 1px 8px !important;
  border-left: 1px solid lightgrey; 
} /*подкрашиваем 1 строку заголовка в сводной*/

.pivot_table_v_2 table.pvtTable thead tr:nth-of-type(2) th.pvtColLabel {
  color: black;
  background-color: #dbdbdb; /*светло-серая заливка*/
  text-align: center;
  font-size: 13px;
  font-weight: 600;
  padding: 1px 8px !important;
  text-wrap: nowrap!important; 
} /*подкрашиваем 2 строку заголовка в сводной*/

.pivot_table_v_2 table.pvtTable tr td.pvtVal {
  text-wrap: nowrap!important; 
  text-align: center;
  padding: 1px 8px;
  vertical-align: middle;
  color: black; 
} /*параметры для значений сводных таблиц*/

.pivot_table_v_2 table.pvtTable tr th.pvtRowLabel {
  text-wrap: nowrap!important; 
  padding: 1px 8px;
  font-size: 13px;
  vertical-align: middle !important;
} /*в заголовках строк убираем перенос текста*/

/**** красные разделители*******/

.pivot_table_v_2 table.pvtTable tr:nth-of-type(1) .pvtVal,
.pivot_table_v_2 table.pvtTable tr:nth-of-type(1) th.pvtRowLabel {
  border-top: 2px solid #a73333;
} /*полоса под шапкой*/

.pivot_table_v_2 table.pvtTable thead tr:nth-of-type(n) th:nth-of-type(2).pvtColLabel,
.pivot_table_v_2 table.pvtTable thead tr:nth-of-type(1) th:nth-of-type(3).pvtColLabel,
.pivot_table_v_2 table.pvtTable td:nth-of-type(1).pvtVal,
.pivot_table_v_2 table.pvtTable tbody tr.pvtRowTotals td:nth-of-type(1){
  border-left: 2px solid #a73333;
} /*полоса отделяем названия строк от значений*/

.pivot_table_v_2 table.pvtTable th.pvtRowLabel[rowspan]:not([rowspan="1"]) { 
    border-top: 2px solid #a73333 !important;
    font-weight: 600;
} /*граница  первого столбца*/

.pivot_table_v_2 table.pvtTable tr:nth-of-type(-n+1) th:nth-of-type(-n+2).pvtRowLabel,
.pivot_table_v_2 table.pvtTable tr:nth-of-type(-n+1) td.pvtVal
{ 
  background: #FBEEEC; /*розовая заливка*/
  font-weight: 600;
} /*подкрашиваем и выделяем линией строку с итогом компании*/

.pivot_table_v_2 table.pvtTable tr:nth-of-type(n+2) th[rowspan="1"]:nth-of-type(2).pvtRowLabel,
.pivot_table_v_2 table.pvtTable tr:nth-of-type(n+2):has(th[rowspan="1"]:nth-of-type(2).pvtRowLabel) .pvtVal
{ 
  background: #f5f5f5; /*светло-серая заливка*/
  font-weight: 600;
  border-top: 2px solid #a73333 !important;
  border-bottom: 2px solid #a73333 !important;
  vertical-align: middle;
} /*подкрашиваем и выделяем линией подытог начиная со второй строки*/

Вывод: продолжаем экспериментировать, SuperSet не так уж прост, и в нем можно реализовать даже самые смелые идеи.

Комментарии (0)