У меня есть таблица в postgres, и я пытаюсь дублировать строки на основе разделителя @ в столбце описания. Вот моя таблица:
Используя приведенный ниже код postgresql, строки дублируются на основе набора из четырех значений:
select txn_id
,max(case when nr%4=1 then elem end) cncy_cd
,max(case when nr%4=2 then elem end) txn_cncy_amt
,max(case when nr%4=3 then elem end) txn_exch_rate
,max(case when nr%4=0 then elem end) txn_nzd_amt
from test t
left join lateral (select elem,row_number()over() nr
from unnest(string_to_array(t.description, '@')) AS a(elem)) ON true
group by txn_id,(nr-1)/4 ;
и результат будет:
Ниже приведена демо-версия кода postgres: https://dbfiddle.uk/BGOeoIlM
Я пытаюсь добиться того же результата с помощью DuckDB и преобразовать его в фрейм данных Polars, и ниже приведен мой код:
a = duckdb.sql("create table test (txn_id int, description varchar(200));insert into test values (3332654,'[email protected]@0.9397@$10.64@[email protected]@23.8235@$6.30@KRW@36,[email protected]@$41.84@[email protected]@1.5711@$12.73@[email protected]@0.6013@$8.32@[email protected]@10.6013@$18.32'),(3332655,'[email protected]@0.8197@$11.64@[email protected]@21.8135@$61.30@KRW@36,[email protected]@$411.84@[email protected]@11.5711@$11.73');select txn_id ,max(case when nr%4=1 then elem end) cncy_cd ,max(case when nr%4=2 then elem end) txn_cncy_amt, max(case when nr%4=3 then elem end) txn_exch_rate ,max(case when nr%4=0 then elem end) txn_nzd_amt from test t left join lateral (select elem,row_number()over() nr from unnest(string_to_array(t.description, '@')) AS a(elem)) ON true group by txn_id,((nr-1)//4);")
Ниже приведены выходные данные DuckDB, которые не совпадают с выводами Postgres.
Не уверен, чего мне здесь не хватает, поскольку я пытался потратить на это много времени и не мог понять, почему это будет вести себя по-другому. Отлично, если кто-нибудь может мне помочь с этим?





Что ж, при использовании SQL вам нужно помнить, что в целом набор записей SQL представляет собой неупорядоченный набор строк. По сути, это означает, что не существует гарантированного порядка строк при их выборе без предложения order by. Это одно из ключевых отличий от современных коллекций строк DataFrame и запросов «связанных выражений».
В вашем случае это означает, что запуск row_number() над невложенным массивом может быть совершенно случайным - после того, как массив не вложен, у вас больше нет порядка элементов массива. В принципе, это здорово, что версия Postgres работает стабильно, но неудивительно, что DuckDB не работает.
Чтобы ваш код был более готов к работе, я бы предложил использовать предложение с порядковым номером в Postgres, которое будет нумеровать строки набора результатов функции:
select
txn_id,
max(case when nr%4=1 then elem end) as cncy_cd,
max(case when nr%4=2 then elem end) as txn_cncy_amt,
max(case when nr%4=3 then elem end) as txn_exch_rate,
max(case when nr%4=0 then elem end) as txn_nzd_amt
from test as t
left join lateral unnest(string_to_array(t.description, '@')) with ordinality as a(elem, nr) on true
group by
txn_id,
(nr-1)/4 ;
А в DuckDB вы можете использовать функцию generate_subscripts, которая будет генерировать индексы по N-му (в вашем случае - первому и единственному) измерению массива:
duckdb.sql("""
drop table if exists test;
create table test (txn_id int, description varchar(200));
insert into test
values
(3332654,'[email protected]@0.9397@$10.64@[email protected]@23.8235@$6.30@KRW@36,[email protected]@$41.84@[email protected]@1.5711@$12.73@[email protected]@0.6013@$8.32@[email protected]@10.6013@$18.32'),
(3332655,'[email protected]@0.8197@$11.64@[email protected]@21.8135@$61.30@KRW@36,[email protected]@$411.84@[email protected]@11.5711@$11.73');
select
txn_id,
max(case when nr%4=1 then elem end) cncy_cd,
max(case when nr%4=2 then elem end) txn_cncy_amt,
max(case when nr%4=3 then elem end) txn_exch_rate,
max(case when nr%4=0 then elem end) txn_nzd_amt
from test t
left join lateral (select string_to_array(t.description, '@')) as a(data) on true
left join lateral (select unnest(a.data), generate_subscripts(a.data, 1)) as b(elem, nr) on true
group by
txn_id,
(nr-1)//4;
""")
┌─────────┬─────────┬──────────────┬───────────────┬─────────────┐
│ txn_id │ cncy_cd │ txn_cncy_amt │ txn_exch_rate │ txn_nzd_amt │
│ int32 │ varchar │ varchar │ varchar │ varchar │
├─────────┼─────────┼──────────────┼───────────────┼─────────────┤
│ 3332655 │ CAD │ 11.00 │ 0.8197 │ $11.64 │
│ 3332654 │ THB │ 150.00 │ 23.8235 │ $6.30 │
│ 3332655 │ FJD │ 21.00 │ 11.5711 │ $11.73 │
│ 3332654 │ FJD │ 20.00 │ 1.5711 │ $12.73 │
│ 3332655 │ KRW │ 36,001.00 │ 861.3722 │ $411.84 │
│ 3332654 │ EUR │ 5.00 │ 0.6013 │ $8.32 │
│ 3332654 │ SGP │ 15.00 │ 10.6013 │ $18.32 │
│ 3332655 │ THB │ 110.00 │ 21.8135 │ $61.30 │
│ 3332654 │ CAD │ 10.00 │ 0.9397 │ $10.64 │
│ 3332654 │ KRW │ 36,000.00 │ 860.3722 │ $41.84 │
├─────────┴─────────┴──────────────┴───────────────┴─────────────┤
│ 10 rows 5 columns │
└────────────────────────────────────────────────────────────────┘
Возможно, стоит взглянуть на list_slice(), функцию DuckDB (см. здесь).
В документации DuckDB для unnest() говорится: «Когда несколько списков не вложены в одно и то же предложение SELECT, списки не вложены рядом».
Я предполагаю, что это означает, что заказы остаются прежними, и это так, но неясно, гарантировано ли это.
Я использовал dplyr (пакет R) для генерации следующего SQL:
SELECT
txn_id,
unnest(list_slice(description, 1, length, 4)) AS cncy_cd,
unnest(list_slice(description, 2, length, 4)) AS txn_cncy_amt,
unnest(list_slice(description, 3, length, 4)) AS txn_exch_rate,
unnest(list_slice(description, 4, length, 4)) AS txn_nzd_amt
FROM (
SELECT q01.*, len(description) AS length
FROM (
SELECT txn_id, string_to_array(description, '@') AS description
FROM df
) q01
) q01
Р-код:
library(tidyverse)
library(DBI)
db <- dbConnect(duckdb::duckdb())
df <- tribble(
~txn_id, ~description,
3332654, "[email protected]@0.9397@$10.64@[email protected]@23.8235@$6.30@KRW@36,[email protected]@$41.84@[email protected]@1.5711@$12.73@[email protected]@0.6013@$8.32@[email protected]@10.6013@$18.32",
3332655, "[email protected]@0.8197@$11.64@[email protected]@21.8135@$61.30@KRW@36,[email protected]@$411.84@[email protected]@11.5711@$11.73"
) |> copy_to(db, df = _, name = "df", overwrite = TRUE)
res <-
df |>
mutate(description = string_to_array(description, '@'),
length = len(description)) |>
mutate(cncy_cd = unnest(list_slice(description, 1L, length, 4L)),
txn_cncy_amt = unnest(list_slice(description, 2L, length, 4L)),
txn_exch_rate = unnest(list_slice(description, 3L, length, 4L)),
txn_nzd_amt = unnest(list_slice(description, 4L, length, 4L))) |>
select(-description, -length) |>
collapse()
res |> show_query()
#> <SQL>
#> SELECT
#> txn_id,
#> unnest(list_slice(description, 1, length, 4)) AS cncy_cd,
#> unnest(list_slice(description, 2, length, 4)) AS txn_cncy_amt,
#> unnest(list_slice(description, 3, length, 4)) AS txn_exch_rate,
#> unnest(list_slice(description, 4, length, 4)) AS txn_nzd_amt
#> FROM (
#> SELECT q01.*, len(description) AS length
#> FROM (
#> SELECT txn_id, string_to_array(description, '@') AS description
#> FROM df
#> ) q01
#> ) q01
res |> collect() |> knitr::kable("pipe")
Created on 2024-06-29 with reprex v2.1.0