Эффективное хранение частично столбчатых данных в базе данных DuckDB в чисто столбчатой ​​форме

У меня есть некоторые частично столбчатые данные, подобные этим:

"hello", "2024 JAN", "2024 FEB"
"a", 0, 1

Если бы он был чисто столбчатым, он выглядел бы так:

"hello", "year", "month", "value"
"a", 2024, "JAN", 0
"a", 2024, "FEB", 1

Предположим, что данные представлены в виде массива numpy, например:

import numpy as np

data = np.array([["hello", "2024 JAN", "2024 FEB"], ["a", "0", "1"]], dtype = "<U")
data
array([['hello', '2024 JAN', '2024 FEB'],
       ['a', '0', '1']], dtype='<U8')

Представьте также, что я создал таблицу:

import duckdb as ddb

conn = ddb.connect("hello.db")
conn.execute("CREATE TABLE columnar (hello VARCHAR, year UINTEGER, month VARCHAR, value VARCHAR);")

Как я могу эффективно вставить data в таблицу DuckDB columnar?

Наивным/простым способом было бы грубое преобразование данных в столбчатый формат в памяти в Python перед вставкой их в таблицу DuckDB.

Здесь я имею в виду конкретно:

import re

data_header = data[0]
data_proper = data[1:]

date_pattern = re.compile(r"(?P<year>[\d]+) (?P<month>JAN|FEB)")
common_labels: list[str] = []
known_years: set[int] = set()
known_months: set[str] = set()
header_to_date: Dict[str, tuple[int, str]] = dict()
for header in data_header:
    if matches := date_pattern.match(header):
        year, month = int(matches["year"]), str(matches["month"])
        known_years.add(year)
        known_months.add(month)
        header_to_date[header] = (year, month)
    else:
        common_labels.append(header)

# hello, year, month, value
new_rows_per_old_row = len(known_years) * len(known_months)
new_headers = ["year", "month", "value"]
purely_columnar = np.empty(
    (
        1 + data_proper.shape[0] * new_rows_per_old_row,
        len(common_labels) + len(new_headers),
    ),
    dtype=np.object_,
)
purely_columnar[0] = common_labels + ["year", "month", "value"]
for rx, row in enumerate(data_proper):
    common_data = []
    ym_data = []
    for header, element in zip(data_header, row):
        if header in common_labels:
            common_data.append(element)
        else:
            year, month = header_to_date[header]
            ym_data.append([year, month, element])

    for yx, year_month_value in enumerate(ym_data):
        purely_columnar[
            1 + rx * new_rows_per_old_row + yx, : len(common_labels)
        ] = common_data
        purely_columnar[
            1 + rx * new_rows_per_old_row + yx, len(common_labels) :
        ] = year_month_value

print(f"{purely_columnar=}")
purely_columnar=
array([[np.str_('hello'), 'year', 'month', 'value'],
       [np.str_('a'), 2024, 'JAN', np.str_('0')],
       [np.str_('a'), 2024, 'FEB', np.str_('1')]], dtype=object)

Теперь хранить эти данные в DuckDB достаточно легко:

purely_columnar_data = np.transpose(purely_columnar[1:])
conn.execute(
    """INSERT INTO columnar
    SELECT * FROM purely_columnar_data
  """
)
conn.sql("SELECT * FROM columnar")
┌─────────┬────────┬─────────┬─────────┐
│  hello  │  year  │  month  │  value  │
│ varchar │ uint32 │ varchar │ varchar │
├─────────┼────────┼─────────┼─────────┤
│ a       │   2024 │ JAN     │ 0       │
│ a       │   2024 │ FEB     │ 1       │
└─────────┴────────┴─────────┴─────────┘

Но есть ли какой-либо другой способ, с помощью которого я могу вставить данные в DuckDB в чисто столбчатой ​​форме, кроме предварительного перебора данных в чисто столбчатую форму?

Примечание. Я отметил этот вопрос postgresql, потому что диалект SQL DuckDB очень похож на диалект PostgreSQL.

Какой простой способ вы имеете в виду? Вы имеете в виду что-то вроде first = data[: :1].T; rest = data[:, :1]; duckdb.sql("from first positional join rest") - (на мгновение игнорируя разделение даты)

jqurious 29.06.2024 11:04

Возможно, будет полезно опубликовать код Python/pandas, который выполняет нужное вам преобразование. Это может помочь идентифицировать эквивалент SQL.

Ian Gow 29.06.2024 19:39

@jqurious Я новичок в SQL, поэтому не совсем понимаю ваш «простой способ». Я обновил вопрос, указав, по моему мнению, «простой способ» (грубое преобразование данных в Python в «чисто столбчатую форму»).

bzm3r 30.06.2024 06:05

@IanGow Я обновил вопрос этим! Я не использую pandas; просто Python и numpy.

bzm3r 30.06.2024 06:05

Если вы действительно начинаете с необработанного текста и используете регулярное выражение для извлечения определенных частей - это может быть «проблемой XY». Вы можете просто использовать read_text() для загрузки данных в DuckDB и использовать его собственные функции регулярных выражений вместо того, чтобы делать это в Python.

jqurious 30.06.2024 12:52

@bzm3r Круто. Кажется, вы получили ответ.

Ian Gow 01.07.2024 16:09
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
6
107
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Примечание. Я добавил еще одну строку ("b",1,0), чтобы сделать данные более информативными и было легче увидеть, что происходит.

По сути, у вас есть «повернутый» набор данных:

D SELECT * FROM 'pivoted-data.csv';
┌─────────┬──────────┬──────────┐
│  hello  │ 2024 JAN │ 2024 FEB │
│ varchar │  int64   │  int64   │
├─────────┼──────────┼──────────┤
│ a       │        0 │        1 │
│ b       │        1 │        0 │
└─────────┴──────────┴──────────┘

Итак UNPIVOT это:

D SELECT hello, string_split(month, ' ')[1]::INTEGER AS year, string_split(month, ' ')[2] AS month, value
  FROM (UNPIVOT 'pivoted-data.csv' ON '2024 JAN', '2024 FEB' INTO NAME month VALUE value);
┌─────────┬───────┬─────────┬───────┐
│  hello  │ year  │  month  │ value │
│ varchar │ int32 │ varchar │ int64 │
├─────────┼───────┼─────────┼───────┤
│ a       │  2024 │ JAN     │     0 │
│ a       │  2024 │ FEB     │     1 │
│ b       │  2024 │ JAN     │     1 │
│ b       │  2024 │ FEB     │     0 │
└─────────┴───────┴─────────┴───────┘

После этого вы можете использовать одну из строковых функций для дальнейшего анализа month.

Обновлено: у меня была проблема с разделением строки, это не самый эффективный способ, но я решил, что оптимизатор запросов, вероятно, видит дублирование кода и оптимизирует его.

EDIT2: Хорошо, EXPLAIN подтверждает, что дублирование оптимизировано:

┌─────────────────────────────┐
│┌───────────────────────────┐│
││       Physical Plan       ││
│└───────────────────────────┘│
└─────────────────────────────┘
┌───────────────────────────┐
│         PROJECTION        │
│   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │
│           hello           │
│            year           │
│           month           │
│           value           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│         PROJECTION        │
│   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │
│           hello           │
│  string_split(month, ' ') │
│           value           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│         PROJECTION        │
│   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │
│             #0            │
│             #3            │
│             #4            │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│           FILTER          │
│   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │
│    (value IS NOT NULL)    │
│   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │
│           EC: 1           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│           UNNEST          │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│       READ_CSV_AUTO       │
│   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │
│           hello           │
│          2024 JAN         │
│          2024 FEB         │
│   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │
│           EC: 3           │
└───────────────────────────┘

Как EXPLAIN подтверждает, что дублирование кода исчезло?

bzm3r 30.06.2024 19:33

@bzm3r Посмотрите на вторую проекцию сверху, только одна string_split, а в верхней осуществляется доступ только к элементам (по заданным именам).

suvayu 30.06.2024 22:29

Небольшой вариант ответа @suvayu:

SELECT hello, CAST(strptime(month, '%Y %b') AS DATE) AS month, value
FROM (
  UNPIVOT 'pivoted-data.csv'
  ON COLUMNS(* EXCLUDE (hello)) 
  INTO NAME month VALUE value);

Два различия заключаются в следующем:

  1. Использование strptime() для анализа даты.
  2. Использование выражения COLUMNS, чтобы сделать запрос более динамичным (я предполагаю, что в реальном наборе данных больше дат).

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