Как получить первые N значений нескольких столбцов?

Допустим, у меня есть следующая таблица:

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 для выбора номеров строк < N. Другое решение — ВЫБРАТЬ РАЗЛИЧНЫЕ для одного столбца. Это все равно будет более эффективно, чем ваше текущее решение SELECT .. ORDER BY .. UNION ALL.

Omkar 11.04.2024 03:41

@Омкар звучит хорошо. Тогда хотите показать это в ответе — порядок не имеет значения, поэтому вы можете использовать row_number.

David542 11.04.2024 06:39

Должны ли быть разные 100 значений? Тогда первый подход достаточно хорош.

Salman Arshad 11.04.2024 07:08

Первый вариант самый лучший, если можно удалить DISTINCT

Charlieface 11.04.2024 13:28

@Charlieface, что, если все первые 100 значений равны нулю? Я хочу получить выборку значений, например (любые) 100 из них.

David542 11.04.2024 22:19
ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
4
5
324
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Ваш вопрос оставляет место для интерпретации. Вы упоминаете «первый», но не даете ему определения. В вашем запросе есть 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 предоставляет только вариант процентиля_диска() для получения одного значения.

спасибо за подробный ответ, Эрвин, я обновил вопрос, добавив более подробную информацию и скриншот того, что я хочу сделать / вдохновение, почему.

David542 11.04.2024 22:01

Мой ответ в основном для Postgres (как отмечено). Вы, кажется, все-таки используете BQ...

Erwin Brandstetter 12.04.2024 00:06

Я использую оба для тестирования (на самом деле мне нужно подключиться к обоим — к Postgres локально и к BQ удаленно).

David542 12.04.2024 00:31

Рассмотрим ниже вариант (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. Это верно? очень круто!

David542 11.04.2024 22:10

Точно! Вы можете увидеть полную спецификацию здесь — cloud.google.com/bigquery/docs/reference/standard-sql/…

Mikhail Berlyant 11.04.2024 22:15

Кстати, я добавил (к своему вопросу) улучшение, которое помещает значения в один столбец. Однако есть одна проблема... массивы не допускают значений null, что обычно является огромной проблемой для неизвестных данных (которые часто содержат много нулевых значений!) Жаль, что BQ имеет такое ограничение на типы массивов! Клянусь, это моя самая большая претензия к BQ (и проблема, с которой я сталкиваюсь больше всего).

David542 11.04.2024 22:18
Ответ принят как подходящий

Пропущенное сканирование с помощью рекурсивного 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, поэтому он прекрасно с ними справляется.

идентификаторы продукты расходы {94 937 743} {"Коврик для мыши",Пол,Вилка} {213,9,64,3,362,5}

Вы можете придумать сценарий, в котором ваше предположение неверно:

Это работает, но кажется очень неэффективным (я считаю, что это то же самое, что выполнять запрос для каждого столбца).

Наличие «запроса для каждого столбца» на самом деле может быть желательным, если у вас есть индексы по ним. 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;
ПЛАН ЗАПРОСА Результат (стоимость = 126,35..126,36 строк = 1, ширина = 96) (фактическое время = 0,829..0,831 строк = 1 цикл = 1)   Выход: $0, $1, $2.   InitPlan 1 (возвращает 0 долларов США)                       -> Сканировать только индекс с использованием idx1 в public.tbl (стоимость = 0,42..5724,42 строк = 300000, ширина = 4) (фактическое время = 0,041..0,081 строк = 430 циклов = 1)                             Вывод: табл.id   InitPlan 2 (возвращает 1 доллар)                       -> Сканировать только индекс с использованием idx2 в public.tbl tbl_1 (стоимость = 0,42..5772,42 строк = 300000, ширина = 8) (фактическое время = 0,034..0,365 строк = 3155 циклов = 1)                             Вывод: tbl_1.product   InitPlan 3 (возвращает 2 доллара США)                       -> Сканировать только индекс с использованием idx3 в public.tbl tbl_2 (стоимость = 0,42..8000,42 строк = 300000, ширина = 6) (фактическое время = 0,037..0,043 строк = 54 цикла = 1)                             Выход: tbl_2.cost Время планирования: 1,046 мс Время выполнения: 0,900 мс
array_agg array_agg array_agg {0,1,2} {"Освежитель воздуха",Яблоко,Сумка} {0.0,0.1,0.2}

Тем не менее, если у вас настроены индексы, вы можете преобразовать ранее пропущенное сканирование в сканирование с пропуском индекса . Это третий из демо, самый быстрый из всех, собрав 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) ;
ПЛАН ЗАПРОСА Агрегат (стоимость = 26,35..26,36 строк = 1, ширина = 96) (фактическое время = 0,467..0,468 строк = 1 цикл = 1)   Вывод: (array_agg(DISTINCT id))[:3], (array_agg(DISTINCT product))[:3], (array_agg(DISTINCT стоимость))[:3]   -> Сортировка (стоимость = 23,34..24,09 строк = 300, ширина = 18) (фактическое время = 0,119..0,138 строк = 321 цикл = 1)         Вывод: идентификатор, продукт, стоимость.         Ключ сортировки: tbl.id         Метод сортировки: быстрая сортировка. Память: 39 КБ.         -> Пример сканирования на public.tbl (стоимость = 0,00..11,00 строк = 300, ширина = 18) (фактическое время = 0,013..0,061 строк = 321 цикл = 1)               Вывод: идентификатор, продукт, стоимость.               Выборка: система ('0.1'::real) Время планирования: 0,092 мс Время выполнения: 0,494 мс
array_agg array_agg array_agg {10,15,33} {"Освежитель воздуха",Яблоко,Сумка} {0.03,1.27,1.42}

С индексами сканирование с пропуском индекса по-прежнему может выполняться сопоставимо без риска попадания в нерепрезентативный срез или необходимости настройки параметра.
Без них, если ваши значения распределены равномерно или вам повезло, это самый простой способ ускорить процесс.

Если целевая таблица не совсем свежая (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';
имя_столбца образец идентификатор {866,77,760,812,419} продукт {"Резинка",Хлеб,Твистер,"Автомобильная модель",Нож} расходы {430,0960,5474,8575,3705,8}

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

Это любой массив , который нельзя подписать или привести напрямую, поэтому я сначала просматриваю его текстовое представление. Затем [:5] получает кусочек первых 5 элементов. Представление также предлагает most_common_freqs, который показывает, какому % строк соответствуют эти значения, поэтому вы можете бесплатно обновить выборку значений любого N до N верхних значений.

Если вам нужно больше образцов, чем база данных собирает сама, вы можете настроить это на уровне столбца или глобального уровня.

Zegarek 19.04.2024 12:49

Другие вопросы по теме