Допустим, у меня есть следующая таблица:
WITH tbl AS (
SELECT 1 AS id, "Phone" AS product, 105 AS cost UNION ALL
SELECT 2 AS id, "Camera" AS product, 82 AS cost UNION ALL
SELECT 3 AS id, "Cup" AS product, 103 AS cost
) SELECT * FROM tbl
Как мне получить N различных значений для каждого столбца? Например, аналогично показанному здесь «Распределению значений» (внизу справа) из Power BI:
Меня не волнует количество каждого значения, а просто получить репрезентативную выборку значений (скажем, до десяти значений).
Например, отобразить выборку значений без необходимости запуска запроса для каждого столбца? То есть мне хотелось бы охватить их все за один раз. Пока у меня есть что-то вроде:
WITH tbl AS (
SELECT 1 AS id, 'Phone' AS product, 105 AS cost UNION ALL
SELECT 2 AS id, 'Camera' AS product, 82 AS cost UNION ALL
SELECT 3 AS id, 'Cup' AS product, 103 AS cost
)
SELECT
ARRAY_AGG(DISTINCT id LIMIT 2),
ARRAY_AGG(DISTINCT product LIMIT 2),
ARRAY_AGG(DISTINCT cost LIMIT 2)
FROM tbl
Это работает, но кажется очень неэффективным (я считаю, что это то же самое, что выполнять запрос для каждого столбца). Как лучше это сделать?
Или, обобщая то, что я считаю плохим подходом, но применимым за пределами BQ:
WITH tbl AS (
SELECT 1 AS id, 'Phone' AS product, 105 AS cost UNION ALL
SELECT 2 AS id, 'Camera' AS product, 82 AS cost UNION ALL
SELECT 3 AS id, 'Cup' AS product, 103 AS cost
)
select 'id' as field, array(select distinct cast(id as string) from tbl limit 2) as values union all
select 'product', array(select distinct cast(product as string) from tbl limit 2) union all
select 'cost', array(select distinct cast(cost as string) from tbl limit 2);
И дальнейшее улучшение на основе ответа Михаила:
WITH tbl AS (
SELECT 1 AS id, "Phone" AS product, 105 AS cost, true as is_big, date '2014-01-01' as d UNION ALL
SELECT 2 AS id, "Camera" AS product, 82 AS cost, false as is_big, date '2017-01-01' as d UNION ALL
SELECT 3 AS id, "Cup" AS product, 103 AS cost, false as is_big, date '2015-01-01' as d union all
SELECT 7 AS id, "Several" AS product, 103 AS cost, true as is_big, date '2016-01-01' as d
)
SELECT
name,
IF(
array_length(quantiles) is not null,
ARRAY(SELECT CAST(tmp AS STRING) FROM UNNEST(quantiles) tmp),
ARRAY(SELECT value FROM t.top_values)
) values
FROM ML.DESCRIBE_DATA(
(SELECT * FROM tbl), STRUCT(3 AS num_quantiles, 4 AS top_k)
) t;
@Омкар звучит хорошо. Тогда хотите показать это в ответе — порядок не имеет значения, поэтому вы можете использовать row_number
.
Должны ли быть разные 100 значений? Тогда первый подход достаточно хорош.
Первый вариант самый лучший, если можно удалить DISTINCT
@Charlieface, что, если все первые 100 значений равны нулю? Я хочу получить выборку значений, например (любые) 100 из них.
Ваш вопрос оставляет место для интерпретации. Вы упоминаете «первый», но не даете ему определения. В вашем запросе есть DISTINCT
без упоминания об этом ранее. В вашем образце нет ни нулевых значений, ни дубликатов, неясно, как с ними бороться.
Используя подзапрос в Postgres, выполняется одно очень дешевое последовательное сканирование и останавливается на маленьком LIMIT
:
SELECT array_agg(id) AS ids
, array_agg(product) AS products
, array_agg(cost) AS costs
FROM (
SELECT id, product, cost
FROM tbl
-- no ORDER BY, take arbitrary rows cheaply
LIMIT 2
) sub;
Более репрезентативную выборку можно получить с помощью «агрегирующей функции упорядоченного набора» процентиля_диска(). Нравиться:
SELECT percentile_disc('{0,.5,1}'::float[]) WITHIN GROUP (ORDER BY id) AS pctl_id
, percentile_disc('{0,.5,1}'::float[]) WITHIN GROUP (ORDER BY product) AS pctl_product
, percentile_disc('{0,.5,1}'::float[]) WITHIN GROUP (ORDER BY cost) AS pctl_cost
FROM tbl;
Этот вариант percentile_disc()
принимает float8[]
— массив процентов — и выбирает значение столбца для каждого заданного процента в пределах определенного порядка сортировки. Скорее утонченный. Вы можете выбрать любое количество значений столбца из любой позиции в порядке сортировки.
Таким образом, вы можете выбрать, где в порядке сортировки и сколько значений выбирать из каждого столбца, при этом просматривая таблицу один раз. Или, для больших таблиц, на основе небольшой полуслучайной выборки, чтобы сделать ее дешевле (хотя и менее репрезентативной):
SELECT percentile_disc('{0,.5,1}'::float[]) WITHIN GROUP (ORDER BY id) AS pctl_id
, ...
FROM tbl TABLESAMPLE SYSTEM (10);
percentile_disc()
игнорирует нулевые значения, array_agg()
включает их.
Дубликаты не подвергаются специальной обработке (пока) ни в одном из них. Любую комбинацию этих функций (и многих других) можно оптимизировать с точки зрения производительности, случайности, уникальности, достоверности и т. д.
Вам просто нужно точно определить, что вам нужно.
Видеть:
Похоже, что BigQuery предоставляет только вариант процентиля_диска() для получения одного значения.
спасибо за подробный ответ, Эрвин, я обновил вопрос, добавив более подробную информацию и скриншот того, что я хочу сделать / вдохновение, почему.
Мой ответ в основном для Postgres (как отмечено). Вы, кажется, все-таки используете BQ...
Я использую оба для тестирования (на самом деле мне нужно подключиться к обоим — к Postgres локально и к BQ удаленно).
Рассмотрим ниже вариант (BigQuery) — быстрый и дешевый, не зависящий от количества столбцов!
WITH tbl AS (
SELECT 1 AS id, "Phone" AS product, 105 AS cost UNION ALL
SELECT 2 AS id, "Camera" AS product, 82 AS cost UNION ALL
SELECT 3 AS id, "Cup" AS product, 103 AS cost
)
SELECT name, quantiles AS num_values, ARRAY(SELECT value FROM t.top_values) AS string_values
FROM ML.DESCRIBE_DATA(
(SELECT * FROM tbl),
STRUCT(1 AS num_quantiles, 2 AS top_k)
) t;
довольно круто. Хочу объяснить, как это работает, особенно перекрестное соединение в конце. Кроме того, я считаю, что num_quantiles
должно быть N-1
и top_k
должно быть N
. Это верно? очень круто!
Точно! Вы можете увидеть полную спецификацию здесь — cloud.google.com/bigquery/docs/reference/standard-sql/…
Кстати, я добавил (к своему вопросу) улучшение, которое помещает значения в один столбец. Однако есть одна проблема... массивы не допускают значений null
, что обычно является огромной проблемой для неизвестных данных (которые часто содержат много нулевых значений!) Жаль, что BQ имеет такое ограничение на типы массивов! Клянусь, это моя самая большая претензия к BQ (и проблема, с которой я сталкиваюсь больше всего).
Пропущенное сканирование с помощью рекурсивного cte будет делать именно то, что вы хотите, именно так, как вы этого хотите: запустить только один раз и итеративно получать значения, которые еще не были собраны, пока не соберется желаемое количество отдельных выборок для каждого столбца. . В PostgreSQL это есть, В BigQuery тоже есть.
На 300 тысячах случайных строк с 3 столбцами, содержащими 1 тысячу, 190 и 10 тысяч уникальных значений соответственно, требуется 0.12ms
, чтобы получить 3 образца, 1.2ms
, чтобы получить 30, 40.0ms
, чтобы получить 120 каждого, без индекса: демо в db<>fiddle
prepare recursive_cte_3way_skip_scan(int) as
with recursive cte as (
( select 1 as i
, array[id] ids
, array[product] products
, array[cost] costs
from tbl limit 1) --get 1 random, most convenient value
union all
select cte.i+1 as i
, (select array[id] ids
from tbl
where array_position(cte.ids,tbl.id) is null
limit 1) ||cte.ids
, (select array[product] products
from tbl
where array_position(cte.products,tbl.product) is null
limit 1) ||cte.products
, (select array[cost] costs
from tbl
where array_position(cte.costs,tbl.cost) is null
limit 1) ||cte.costs
from cte where $1>i
)
select ids, products, costs
from cte order by i desc limit 1;
Я использую array_position() вместо <>all()
, чтобы иметь возможность перехватывать значения null
. Последний использует обычное равенство =
, поэтому никогда не будет их искать или сопоставлять. Вместо этого первый использует конструкцию is not distinct from
, поэтому он прекрасно с ними справляется.
Вы можете придумать сценарий, в котором ваше предположение неверно:
Это работает, но кажется очень неэффективным (я считаю, что это то же самое, что выполнять запрос для каждого столбца).
Наличие «запроса для каждого столбца» на самом деле может быть желательным, если у вас есть индексы по ним. PostgreSQL разделит его на 3 независимых InitPlans по трем индексам, извлекая из каждого из них желаемое количество примеров.
В том же тесте требуется 0.9ms
, и поскольку это всего лишь N шагов вверх по BTree, вы можете ожидать, что это будет линейно масштабироваться относительно целевого размера выборки N, независимо от того, насколько велик ваш набор данных:
create index idx1 on tbl (id);
create index idx2 on tbl (product);
create index idx3 on tbl (cost);
vacuum analyze tbl;
prepare subqueries_three as
select (select array_agg(id) from (select distinct id from tbl limit 3)_)
,(select array_agg(product) from (select distinct product from tbl limit 3)_)
,(select array_agg(cost) from (select distinct cost from tbl limit 3)_);
explain analyze verbose execute subqueries_three ;
execute subqueries_three;
Тем не менее, если у вас настроены индексы, вы можете преобразовать ранее пропущенное сканирование в сканирование с пропуском индекса . Это третий из демо, самый быстрый из всех, собрав 120 образцов в 5ms
.
Проверка набора данных без индексов — это именно то, для чего был создан tablesample , уже упомянутый @Erwin Brandstetter. Тот факт, что distinct
нужно пройти лишь небольшую часть таблицы, может сэкономить большую часть работы.
Время выполнения зависит от размера выборки — если вы уменьшите его слишком сильно, вы увеличите риск того, что полученный вами срез не будет содержать желаемого количества примеров:
SELECT
(ARRAY_AGG(DISTINCT id))[:3],
(ARRAY_AGG(DISTINCT product))[:3],
(ARRAY_AGG(DISTINCT cost))[:3]
FROM tbl tablesample system(.1) ;
С индексами сканирование с пропуском индекса по-прежнему может выполняться сопоставимо без риска попадания в нерепрезентативный срез или необходимости настройки параметра.
Без них, если ваши значения распределены равномерно или вам повезло, это самый простой способ ускорить процесс.
Approx_top_count дает до 200 уников на предмет?
Если целевая таблица не совсем свежая (PostgreSQL собрал по ней некоторую статистику или вы запустили анализ ), то вы можете получить most_common_vals
из pg_stats системного представления: db<>fiddle demo
select attname as column_name
,(most_common_vals::text::text[])[:5] as sample
from pg_stats
where tablename='tbl';
Вы получаете его за постоянное время, поскольку он обычно предварительно извлекается из базы данных самостоятельно - вы просто повторно используете то, что уже есть.
Это любой массив , который нельзя подписать или привести напрямую, поэтому я сначала просматриваю его текстовое представление. Затем [:5] получает кусочек первых 5 элементов. Представление также предлагает most_common_freqs
, который показывает, какому % строк соответствуют эти значения, поэтому вы можете бесплатно обновить выборку значений любого N до N верхних значений.
Если вам нужно больше образцов, чем база данных собирает сама, вы можете настроить это на уровне столбца или глобального уровня.
Если порядок не имеет значения, вы можете группировать по столбцам и использовать
ROW_NUMBER
для выбора номеров строк < N. Другое решение — ВЫБРАТЬ РАЗЛИЧНЫЕ для одного столбца. Это все равно будет более эффективно, чем ваше текущее решение SELECT .. ORDER BY .. UNION ALL.