Для выполнения операций COUNT DISTINCT или SELECT DISTINCT обычно требуется сканирование всей таблицы или индекса и извлечение из него только уникальных значений. Существует две основные стратегии выполнения этой операции. Конечно, некоторые СУБД также поддерживают алгоритм HyperLogLog для приближенного расчета, но в этой статье мы сосредоточимся на более традиционном подходе. Этот метод обычно используется, когда запросу требуется cгруппировать уникальные значения в неиндексированной колонке. Во время этого процесса база данных сканирует строки, вычисляет хеш-значение для каждого значения колонки и сохраняет фактические значения в хэш-бакетах. Несмотря на то, что этот метод может быть тяжелым с точки зрения использования памяти для больших наборов данных, он часто является самым быстрым подходом, когда на сервере достаточно памяти для хранения хэш-таблицы. Потоковая агрегация используется, когда данные, которые нужно сгруппировать, уже отсортированы. В этом случае, движку базы данных не надо хранить все данные в памяти, а достаточно лишь сравнивать текущее значение с предыдущим в процессе сканирования. Если текущая строка равна предыдущей, система баз данных переходит к следующей строке. Когда значение отличается, предыдущее передается дальше по плану выполнения, текущее запоминается и так дальше. Потоковая агрегация использует гораздо меньше памяти по сравнению с хэш-таблицей (необходимо хранить только одно значение), но зато требует сортировки данных, которая может включать дополнительную операцию сортировки, если данные уже не отсортированы индексом. Но обе эти стратегии все равно требуют полного сканирования всех значений в колонке, и есть более эффективная стратегия в MySQL для операции SELECT DISTINCT при работе с колонками с низкой кардинальностью, которой не обладают PostgreSQL и MS SQL Server. Loose Scan - это техника оптимизации в MySQL, которая хорошо применима для операций GROUP BY, особенно в сценариях, где количество уникальных значений в столбце относительно небольшое по сравнению с общим числом строк в таблице. Эта техника значительно улучшает производительность запросов, уменьшая количество данных, которое требуется прочитать из базы данных. В сущности, техника Loose Scan работает по простому принципу: вместо сканирования всего индекса (так называемого "tight scan"), она выбирает только первое значение для каждой группы. После этого она немедленно переходит к следующей группе. Для этого обязательно нужен индекс, чтобы уметь быстро переходить к следующему значению. Этот метод обеспечивает снижение количества строк, которые нужно оценивать, тем самым сокращая общее время, необходимое для выполнения запроса. Помимо MySQL, подобная техника также реализована в других движках баз данных. Она называется "Skip Scan" в Oracle, SQLite и CockroachDB, "Jump Scan" в DB2, и "Hybrid Scan" в YugabyteDB. Думаю, теории было достаточно; перейдем к практической части и проведем сравнительный анализ техники Loose Scan в MySQL по сравнению с перебором всех значений в PostgreSQL и Microsoft SQL Server. Я буду использовать последние версии Docker контейнеров MySQL 8.0.33, PostgreSQL 15.3 и MS SQL 2022-CU4 на своем ноутбуке. Будет создана таблица с 1 миллионом строк и тремя столбцами целочисленного типа с разной кардинальностью. В первом столбце 100 тысяч уникальных значений, во втором 1 тысяча, а в третьем всего 10 уникальных значений. Также я добавлю три отдельных некластеризованных индекса и выполню запросы COUNT DISTINCT для каждого столбца. Каждый запрос перед замером времени будет выполнен 5 раз, чтобы немного "прогреть" базу данных, а затем еще 20 раз с замером времени выполнения, и после этого мы сравним среднее время выполнения. Таким образом, я постарался поставить все движки баз данных в примерно равные условия. Вот скрипт, который я использовал для инициализации таблиц во всех базах данных: Запросы, которые будут использоваться для сравнения работы разных СУБД: Напомню еще раз, что абсолютно не важно, сколько времени работают СУБД на одном запросе в сравнении друг с другом. Это время будет сильно отличаться у разных людей. Важно, как работают разные запросы в рамках одной СУБД. Все базы данных работают примерно одинаково на колонке A, где кардинальность данных высока (100 тысяч уникальных значений из 1 миллиона строк). Общее время выполнения PostgreSQL составило 4,72 секунды, MS SQL Server - 1,42 секунды, а MySQL - 3,11 секунды. Но уже на колонке B, где кардинальность составляет всего 1000 уникальных значений, общее время выполнения MySQL падает до 58,6 миллисекунд, в то время как PostgreSQL выполняет это за 4,2 секунды, а MS SQL Server - за 1,08 секунды. Таким образом, MySQL выполняет второй запрос в 70 раз быстрее, чем PostgreSQL, и в 18 раз быстрее, чем MS SQL Server. Ситуация еще интереснее на колонке C, где всего 10 уникальных значений: MySQL - 12,5 мс, PostgreSQL - 3,33 секунды, а MS SQL Server - 1,02 секунды. В последнем примере MySQL работает с поразительной скоростью и превосходит PostgreSQL более чем в 250 раз и MS SQL Server - более чем в 80 раз. Ниже представлена визуализация на логарифмической шкале. Она показывает общее время выполнения после 20 выполнений. Хотя в настоящее время в PostgreSQL и MS SQL Server отсутствует такая оптимизация, есть трюк, который вы можете использовать для улучшения производительности ваших запросов SELECT/COUNT DISTINCT в этих СУБД. Идея заключается в том, чтобы выполнить несколько поисков в индексе, вместо того чтобы полагаться на полное сканирование индекса по умолчанию. Например, в PostgreSQL вы можете выполнить рекурсивный запрос, чтобы найти все уникальные значения. Первая итерация выбирает минимальное значение из столбца, а каждая последующая итерация выбирает следующее значение, большее предыдущего. Мы можем использовать тот же трюк и в MS SQL Server. Но, к сожалению, рекурсивные запросы MS SQL Server не поддерживают операторы TOP или агрегации, поэтому я буду использовать временную таблицу для хранения результата и итерации с использованием LOOP. Это, конечно, требует больше накладных расходов, но, похоже, нет другого общего способа выполнить такую оптимизацию в SQL Server. Теперь давайте сравним, как эти модифицированные запросы работают по сравнению с оригиналом и MySQL. Я буду обозначать модифицированные запросы как A1, B1 и C1 к соответствующим столбцам. Вот таблица с полными результатами.  A A1 B B1 C C1 MySQL 3.11s  58.6ms  12.5ms  PostgreSQL 4.72s 6.18s 4.2s 70.93ms 3.33s 15.69ms MS SQL Server 1.42s 67.67s 1.08s 771.99ms 1.02s 74.66ms Результаты довольно очевидны: Loose Scan - отличная оптимизация, которая значительно уменьшает количество строк, обрабатываемых при запросах SELECT DISTINCT или COUNT DISTINCT при использовании индекса. Несмотря на то, что PostgreSQL позволяет писать сложные рекурсивные запросы для обработки столбцов с низкой кардинальностью также эффективно, как это делает MySQL, он сталкивается со значительным ухудшением производительности на столбце A, где кардинальность высока. MS SQL Server явно отстает в этом примере, хотя какое-то обходное решение все равно будет лучше, чем исходный запрос. Будем надеяться, что PostgreSQL и MS SQL Server также реализуют оптимизацию Loose Scan в какой-то момент в следующих версиях.