645
0
0
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
Назад

Семь лайфхаков SQL

Время чтения 38 минут
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
645
0
0
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники

Приветствую специалистов по данным!

Я Павел Беляев, тимлид группы обработки данных в дата-офисе сервиса eLama. Основная задача моей команды — разрабатывать и поддерживать витрины данных. Желательно побыстрей и чтоб качественно. В этой статье я покажу несколько приемов и приемчиков, которые мы используем в реальной практике.

Семь лайфхаков SQL

Проверка на дубли

Для разогрева начнем с простого. Посчитаем количество повторяющихся сочетаний имени и фамилии в таблице с данными о людях (например, актерах).

SELECT CONCAT(first_name, ' ', last_name) AS fio, 
    COUNT(*) AS c
FROM actors
GROUP BY 1
HAVING с > 1

Последний элемент в исторических данных

Если имеем дело с таблицей, которая обновляется не перезаписью с нуля, а добавлением новых строк для объектов, которые изменились, вот как можно взять последнюю строку:

SELECT *
FROM
(
    SELECT *
        MAX(updated_at) OVER (PARTITION BY payment_id) AS last_update
    FROM payment
)
WHERE updated_at = last_update

 

Здесь payment — таблица, содержащая транзакции пользователя. Одна строка отражает состояние транзакции на момент добавления строки, т. е. на дату updated_at. Каждая транзакция имеет уникальный идентификатор payment_id, но он не уникален в рамках всей таблицы.

Сводные таблицы

Предположим, у нас есть таблица с фильмами, где для каждого из них имеется столбец release_year с годом выпуска и length с длительностью в минутах. Мы хотим подсчитать количество фильмов до 90 минут, до 120 минут и более 120 минут.

Когда нужно подсчитать суммы или количество величин из разных категорий, содержащихся в одном столбце, можно делать так:

PostgreSQL

WITH films AS
(
    SELECT 2006 AS release_year, 86 AS "length"
    UNION ALL
    SELECT 2006 AS release_year, 180 AS "length"
    UNION ALL
    SELECT 2006 AS release_year, 95 AS "length"
    UNION ALL
    SELECT 2006 AS release_year, 135 AS "length"
    UNION ALL
    SELECT 2006 AS release_year, 120 AS "length"
    UNION ALL
    SELECT 2007 AS release_year, 85 AS "length"
    UNION ALL
    SELECT 2007 AS release_year, 130 AS "length"
    UNION ALL
    SELECT 2007 AS release_year, 120 AS "length"
    UNION ALL
    SELECT 2007 AS release_year, 97 AS "length"
)
SELECT release_year,
    SUM(CASE WHEN "length"<90 THEN 1 ELSE 0 END) AS less_90 ,
    SUM(CASE WHEN "length">=90 AND "length"<120 THEN 1 ELSE 0 END) AS less_120,
    SUM(CASE WHEN "length">=120 THEN 1 ELSE 0 END) AS over_120
FROM films
GROUP BY 1 
-- GROUP BY ROLLUP (1) -- добавляет в конец строку с итогом
ORDER BY 1

 

ClickHouse

В Кликхаусе есть более изящная функция для подсчета по условию — countIf():

SELECT release_year,
    countIf("length"<90) AS less_90 ,
    countIf("length">=90 AND "length"<120) AS less_120,
    countIf("length">=120) AS over_120
FROM films
GROUP BY 1 
ORDER BY 1

 

Постфикс -If можно добавлять и к другим агрегирующим функциям.

BigQuery

Если категории уже определены и содержатся в строках, то в BigQuery есть специальный оператор для создания сводных таблиц. Например, у нас есть данные о количестве продаж фильмов и значение квартала продажи (здесь может быть и просто дата) в отдельном столбце. Чтобы вынести каждый квартал в свой столбец, используем код:

with films AS (
    SELECT 'Film 1' as film, 51 as sales, 'Q1' as quarter UNION ALL
    SELECT 'Film 1', 91, 'Q2' UNION ALL
    SELECT 'Film 1', 45, 'Q3' UNION ALL
    SELECT 'Film 1', 3, 'Q4' UNION ALL
    SELECT 'Film 2', 77, 'Q1' UNION ALL
    SELECT 'Film 2', 0, 'Q2' UNION ALL
    SELECT 'Film 2', 25, 'Q3' UNION ALL
    SELECT 'Film 2', 2, 'Q4'
)
SELECT * FROM
(
    SELECT film, sales, quarter FROM films
)
PIVOT(SUM(sales) FOR quarter IN ('Q1', 'Q2', 'Q3', 'Q4'))

 

Сравнение таблиц

Я уже писал о том, как проверять свои витрины, сравнивая новую версию с предыдущей. Простой, но очень удобный прием — использовать EXCEPT DISTINCT:

CREATE TABLE t1 (a Integer, b varchar(255));
INSERT INTO t1 (a, b) VALUES (1, 'a'), (2, 'b'), (3, 'c'), (6, 'f');
CREATE TABLE t2 (a Integer, b varchar(255));
INSERT INTO t2 (a, b) VALUES (1, 'a'), (2, 'b'), (4, 'c'), (5, 'd');
(
    SELECT 'old vs new' AS _type, *
    FROM t1
    EXCEPT DISTINCT
    SELECT 'old vs new' AS _type, *
    FROM t2
)
UNION ALL
(
    SELECT 'new vs old' AS _type, *
    FROM t2
    EXCEPT DISTINCT
    SELECT 'new vs old' AS _type, *
    FROM t1
)
ORDER BY b, _type;

 

Заполняем пропущенные даты

Для многих задач обработки временных рядов (например, прогнозирования) важно, чтобы не было пропусков дат. Но входные данные зачастую этим грешат. Это можно чинить с помощью генератора дат:

PostgreSQL

SELECT  date_paid, IFNULL(amount, 0) AS amount
FROM 
(
    SELECT gs::date AS date_paid
    FROM generate_series('2005-05-24', '2005-08-23', interval '1 day') as gs
) AS dates
LEFT JOIN
( -- данные о платежах с пропущенными датами
    SELECT DISTINCT DATE(payment_date) d,
        SUM(amount) AS amount
    FROM payment
    GROUP BY 1
) AS p ON p.d = dates.date_paid
ORDER BY 1

 

BigQuery

Генерация ряда с датами с 2024-01-01 по вчера:

SELECT ds
FROM UNNEST(GENERATE_DATE_ARRAY('2024-01-01', CURRENT_DATE() - 1, INTERVAL 1 DAY)) AS ds

 

ClickHouse

В Кликхаусе всё несколько хитрее:

SELECT arrayJoin(
    arrayMap(
        x -> toDate(x),
        range(
            toUInt32(toStartOfDay(toDate('2024-01-01'))),
            toUInt32(now() + toIntervalDay(1)),
            24 * 3600
        )
    )
) AS ds

 

«Уплощение» таблиц

Предположим, в таблице films есть поле special_features, содержащее массив дополнительных материалов к фильму:

Мы хотим «уплощить» этот массив, т. е. сделать так, чтобы на каждую строку приходилось только одно его значение. Используем код:

PostgreSQL

SELECT title, 
    UNNEST(special_features) AS feature
FROM films

 

ClickHouse

SELECT title, 
  arrayJoin(special_features) AS special_features
FROM films

 

BigQuery

SELECT title, sf
FROM films, UNNEST(special_features) AS sf

 

WHERE 1=1 для отладки

Напоследок маленький совет, как сделать отладку запроса чуть удобней. Когда тестируем витрину и смотрим результат при разных условиях, можно добавить WHERE 1=1, а со следующей строки ставить AND и любые другие условия. Тогда всё, что после 1=1, можно спокойно убирать/добавлять, не заботясь о том, что забыли закомментировать какой-нибудь AND:

SELECT *
FROM datamart
-- Отладочный фильтр:
WHERE 1=1
  --AND a='x'
  --AND b=3
  AND d>='2024-01-01'

Резюме

Мы рассмотрели советы о том, как облегчить работу с SQL в некоторых случаях:

  • Проверка на дубли
  • Вывод последней строки в исторических данных
  • Создание сводных таблиц в PostgreSQL, ClickHouse, BigQuery
  • Сравнение таблиц и вывод различающихся строк
  • Заполнение нулями пропущенных строк во временном ряду
  • «Уплощение» таблиц, содержащих массивы в полях, для PostgreSQL, ClickHouse, BigQuery
  • WHERE 1=1 для удобства отладки витрин
Комментарии0
Тоже интересно
Комментировать
Поделиться
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники