Automatyczne zwiększanie kolumn w Apache Doris

Kolumna auto-increment jest podstawową funkcją jednowęzłowych transakcyjnych baz danych. Przypisuje ona unikalny identyfikator dla każdego wiersza w sposób, który wymaga najmniejszego ręcznego wysiłku ze strony użytkowników. Dzięki kolumnie automatycznego zwiększania w tabeli, za każdym razem, gdy do tabeli zostanie wstawiony nowy wiersz, zostanie mu przypisana następna dostępna wartość z sekwencji automatycznego zwiększania. Jest to zautomatyzowany mechanizm, który sprawia, że utrzymanie bazy danych jest łatwe i niezawodne.

Kolumna auto-increment jest podstawą wielu funkcji w bazach danych:

  • Kodowanie słownika: Identyfikatory użytkowników i zamówień są często przechowywane jako ciągi znaków. Jednak ciągi nie są przyjazne dla precyzyjnego wykonywania zapytań deduplikacji. Aby uzyskać optymalną wydajność, powszechną praktyką jest kodowanie słownikowe ciągów, a następnie tworzenie mapy bitowej dla operacji agregacji. Rola kolumny autoinkrementacji w tym procesie polega na tym, że przyspiesza kodowanie słownikowe, a tym samym przyspiesza deduplikację ciągów znaków.
  • Generowanie klucza głównego: Kolumna z autouzupełnianiem jest idealnym kandydatem na klucz podstawowy tabeli. Klucze podstawowe muszą być unikalne i niepuste, podczas gdy kolumny autoinkrementacji gwarantują unikalny identyfikator dla każdego wiersza.
  • Szczegółowe aktualizacje danych: Aktualizacja tabel szczegółowych jest trudna, ale może być łatwa, jeśli doda się do niej tabelę z automatycznym zwiększaniem. Daje to każdemu rekordowi danych w bazie danych unikalny identyfikator, który może działać jako klucz podstawowy, a następnie aktualizacje danych mogą być wykonywane na podstawie klucza podstawowego.
  • Wydajna paginacja: Paginacja jest często wymagana przy wyświetlaniu danych. Jest ona zazwyczaj implementowana przez limit lub offset + order by w zapytaniach SQL. Jednak taka implementacja wymaga pełnego odczytu danych i sortowania, co nie ma większego sensu w zapytaniach z głęboką paginacją (tych z dużymi przesunięciami). W tym momencie z pomocą przychodzą kolumny auto-increment. Jak już wspomniałem, nadaje ona unikalny identyfikator każdemu wierszowi, więc maksymalny identyfikator ostatniej strony może być użyty jako warunek filtrowania dla następnej strony. W ten sposób można uniknąć wielu niepotrzebnych operacji skanowania danych i zwiększyć wydajność paginacji.

Pomysł automatycznego zwiększania kolumn jest intuicyjny, ale jeśli chodzi o rozproszone bazy danych, staje się inną grą, ponieważ musi uwzględniać transakcje globalne. Jako rozproszony DBMS, Apache Doris zapewnia innowacyjne i wydajne rozwiązanie automatycznego zwiększania, które nie szkodzi wydajności zapisu danych.

Składnia i użycie

Aby włączyć automatyczne zwiększanie kolumny w Doris, proszę dodać AUTO_INCREMENT do kolumny w instrukcji tworzenia tabeli (CREAT TABLE). Mogą Państwo określić wartość początkową dla kolumny autoinkrementacji poprzez AUTO_INCREMENT(start_value); jeśli nie, domyślną wartością początkową jest 1.

Na przykład, można utworzyć tabelę w folderze Duplicate Key model, w którym jedna z kolumn klucza jest kolumną z automatycznym zwiększaniem.

CREATE TABLE `demo`.`tbl` (
     `id` BIGINT NOT NULL AUTO_INCREMENT,
     `value` BIGINT NOT NULL
) ENGINE=OLAP
DUPLICATE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);

Oprócz kolumny klucza można również określić kolumnę wartości jako kolumnę automatycznego zwiększania wartości (przykład poniżej):

CREATE TABLE `demo`.`tbl` (
     `uid` BIGINT NOT NULL,
     `name` BIGINT NOT NULL,
     `id` BIGINT NOT NULL AUTO_INCREMENT,
     `value` BIGINT NOT NULL
) ENGINE=OLAP
DUPLICATE KEY(`uid`, `name`)
DISTRIBUTED BY HASH(`uid`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);

AUTO_INCREMENT jest obsługiwana zarówno w modelu Duplicate Key, jak i w modelu Unique Key model. Użycie w tym ostatnim jest podobne.

Przeprowadzę Państwa przez resztę drogi na przykładzie poniższej tabeli:

CREATE TABLE `demo`.`tbl` (
   `id` BIGINT NOT NULL AUTO_INCREMENT,
   `name` varchar(65533) NOT NULL,
   `value` int(11) NOT NULL
) ENGINE=OLAP
UNIQUE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);

Po wprowadzeniu danych do tej tabeli przy użyciu pliku insert into oświadczenia, jeśli id nie ma określonej wartości w oryginalnym pliku danych, zostanie ona automatycznie wypełniona wartościami autouzupełniania.

mysql> insert into tbl(name, value) values("Bob", 10), ("Alice", 20), ("Jack", 30);
Query OK, 3 rows affected (0.09 sec)
{'label':'label_183babcb84ad4023_a2d6266ab73fb5aa', 'status':'VISIBLE', 'txnId':'7'}

mysql> select * from tbl order by id;
+------+-------+-------+
| id   | name  | value |
+------+-------+-------+
|    1 | Bob   |    10 |
|    2 | Alice |    20 |
|    3 | Jack  |    30 |
+------+-------+-------+
3 rows in set (0.05 sec)

Podobnie, podczas pozyskiwania pliku danych test.csv przez Stream Load, plik id kolumna zostanie również automatycznie wypełniona automatycznie zwiększanymi wartościami.

test.csv:
Tom,40
John,50
curl --location-trusted -u user:passwd -H "columns:name,value" -H "column_separator:," -T ./test.csv http://{host}:{port}/api/{db}/tbl/_stream_load
select * from tbl order by id;
+------+-------+-------+
| id   | name  | value |
+------+-------+-------+
|    1 | Bob   |    10 |
|    2 | Alice |    20 |
|    3 | Jack  |    30 |
|    4 | Tom   |    40 |
|    5 | John  |    50 |
+------+-------+-------+
5 rows in set (0.04 sec)

Obowiązujące scenariusze

1. Kodowanie słownika

W Apache Doris typ danych bitmapy i agregacje związane z bitmapą są implementowane za pomocą RoaringBitmap, co może zapewnić wysoką wydajność, zwłaszcza gdy kodowanie słownika generuje gęste wartości.

Jak wspomniano, kolumny z automatycznym zwiększaniem umożliwiają szybkie kodowanie słownika. Umieszczę Państwa w kontekście profilowania użytkownika, aby pokazać, jak to działa.

W celu analizy odsłon offline (PV) i unikalnych odwiedzających (UV), proszę przechowywać szczegóły w tabeli zachowań użytkowników:

CREATE TABLE `demo`.`dwd_dup_tbl` (
   `user_id` varchar(50) NOT NULL,
   `dim1` varchar(50) NOT NULL,
   `dim2` varchar(50) NOT NULL,
   `dim3` varchar(50) NOT NULL,
   `dim4` varchar(50) NOT NULL,
   `dim5` varchar(50) NOT NULL,
   `visit_time` DATE NOT NULL
) ENGINE=OLAP
DUPLICATE KEY(`user_id`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 32
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);

Proszę utworzyć tabelę słownikową w następujący sposób AUTO_INCREMENT:

CREATE TABLE `demo`.`dictionary_tbl` (
   `user_id` varchar(50) NOT NULL,
   `aid` BIGINT NOT NULL AUTO_INCREMENT
) ENGINE=OLAP
UNIQUE KEY(`user_id`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 32
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);

Proszę załadować istniejące user_id do tabeli słownika i utworzyć mapowania z user_id na wartości całkowite.

insert into dictionary_tbl(user_id)
select user_id from dwd_dup_tbl group by user_id;

Jeśli potrzebują Państwo załadować tylko przyrostowe user_id do tabeli słownika, można użyć następującego polecenia. W praktyce można również użyć polecenia Flink Doris Connector do zapisu danych.

insert into dictionary_tbl(user_id)
select dwd_dup_tbl.user_id from dwd_dup_tbl left join dictionary_tbl
on dwd_dup_tbl.user_id = dictionary_tbl.user_id where dwd_dup_tbl.visit_time  '2023-12-10' and dictionary_tbl.user_id is NULL;

Załóżmy, że mają Państwo wymiary analityczne jako dim1, dim3, dim5proszę utworzyć tabelę w Aggregate Key model w celu uwzględnienia wyników agregacji danych:

CREATE TABLE `demo`.`dws_agg_tbl` (
   `dim1` varchar(50) NOT NULL,
   `dim3` varchar(50) NOT NULL,
   `dim5` varchar(50) NOT NULL,
   `user_id_bitmap` BITMAP BITMAP_UNION NOT NULL,
   `pv` BIGINT SUM NOT NULL 
) ENGINE=OLAP
AGGREGATE KEY(`dim1`,`dim3`,`dim5`)
DISTRIBUTED BY HASH(`dim1`) BUCKETS 32
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);

Proszę załadować zagregowane wyniki do tabeli:

insert into dws_agg_tbl
select dwd_dup_tbl.dim1, dwd_dup_tbl.dim3, dwd_dup_tbl.dim5, BITMAP_UNION(TO_BITMAP(dictionary_tbl.aid)), COUNT(1)
from dwd_dup_tbl INNER JOIN dictionary_tbl on dwd_dup_tbl.user_id = dictionary_tbl.user_id
group by dwd_dup_tbl.dim1, dwd_dup_tbl.dim3, dwd_dup_tbl.dim5;

Następnie proszę wysłać zapytanie PV/UV za pomocą następującej instrukcji:

select dim1, dim3, dim5, bitmap_count(user_id_bitmap) as uv, pv from dws_agg_tbl;

2. Szczegółowe aktualizacje danych

W Doris model Unique Key ma zastosowanie do przypadków użycia z częstymi aktualizacjami danych, podczas gdy model Duplicate Key jest przeznaczony do szczegółowego przechowywania danych bez wymagań dotyczących aktualizacji danych.

Jednak w rzeczywistości użytkownicy mogą czasami potrzebować zaktualizować swoje szczegółowe dane, co może być trudne do wdrożenia, ponieważ tabele danych nie mają unikalnych kolumn kluczy.

W takim przypadku mogą Państwo użyć kolumny z automatycznym zwiększaniem jako klucza głównego dla danych szczegółowych.

Na przykład instytucja finansowa prowadzi rejestr pożyczek klientów i zapisuje go w tabeli Duplicate Key, w której jeden użytkownik może mieć wiele rekordów pożyczek.

CREATE TABLE loan_records (
  `user_id` VARCHAR(20) DEFAULT NULL COMMENT 'Customer ID',
  `loan_amount` DECIMAL(10, 2) DEFAULT NULL COMMENT 'Amount of loan',
  `interest_rate` DECIMAL(10, 2) DEFAULT NULL COMMENT 'Interest rate',
  `loan_start_date` DATE DEFAULT NULL COMMENT 'Start date of the loan',
  `loan_end_date` DATE DEFAULT NULL COMMENT 'End date of the loan',
  `total_debt` DECIMAL(10, 2) DEFAULT NULL COMMENT 'Amount of debt'
) DUPLICATE KEY(`user_id`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 10
PROPERTIES (
   "replication_allocation" = "tag.location.default: 3"
);

Załóżmy, że w ramach kampanii promocyjnej instytucja oferuje 10% zniżki na oprocentowanie dla swoich obecnych klientów. W związku z tym istnieje potrzeba aktualizacji tabeli interest_rate oraz total_debt w tabeli.

W tym celu można utworzyć tabelę unikalnego klucza dla tych samych danych, ale dodając do niej atrybut auto_id i ustawić je jako klucz podstawowy.

CREATE TABLE loan_records (
  `auto_id` BIGINT NOT NULL AUTO_INCREMENT,
  `user_id` VARCHAR(20) DEFAULT NULL COMMENT 'Customer ID',
  `loan_amount` DECIMAL(10, 2) DEFAULT NULL COMMENT 'Amount of loan',
  `interest_rate` DECIMAL(10, 2) DEFAULT NULL COMMENT 'Interest rate',
  `loan_start_date` DATE DEFAULT NULL COMMENT 'Start date of the loan',
  `loan_end_date` DATE DEFAULT NULL COMMENT 'End date of the loan',
  `total_debt` DECIMAL(10, 2) DEFAULT NULL COMMENT 'Amount of debt'
) UNIQUE KEY(`auto_id`)
DISTRIBUTED BY HASH(`auto_id`) BUCKETS 10
PROPERTIES (
   "replication_allocation" = "tag.location.default: 3"
);

Teraz proszę wpisać kilka nowych rekordów do tabeli i zobaczyć, co się stanie. (Proszę zauważyć, że nie trzeba zapisywać w polu auto_id ).

INSERT INTO loan_records (user_id, loan_amount, interest_rate, loan_start_date, loan_end_date, total_debt) VALUES
('10001', 5000.00, 5.00, '2024-03-01', '2024-03-31', 5020.55),
('10002', 10000.00, 5.00, '2024-03-01', '2024-05-01', 10082.56),
('10003', 2000.00, 5.00, '2024-03-01', '2024-03-15', 2003.84),
('10004', 7500.00, 5.00, '2024-03-01', '2024-04-15', 7546.23),
('10005', 3000.00, 5.00, '2024-03-01', '2024-03-21', 3008.22),
('10002', 8000.00, 5.00, '2024-03-01', '2024-06-01', 8100.82),
('10007', 6000.00, 5.00, '2024-03-01', '2024-04-10', 6032.88),
('10008', 4000.00, 5.00, '2024-03-01', '2024-03-26', 4013.70),
('10001', 5500.00, 5.00, '2024-03-01', '2024-04-05', 5526.37),
('10010', 9000.00, 5.00, '2024-03-01', '2024-05-10', 9086.30);

Proszę sprawdzić na stronie select * from loan_records i można zobaczyć, że unikalny identyfikator jest już wprowadzony dla każdego nowo nadanego rekordu:

mysql> select * from loan_records;
+---------+---------+-------------+---------------+-----------------+---------------+------------+
| auto_id | user_id | loan_amount | interest_rate | loan_start_date | loan_end_date | total_debt |
+---------+---------+-------------+---------------+-----------------+---------------+------------+
|       1 | 10001   |     5000.00 |          5.00 | 2024-03-01      | 2024-03-31    |    5020.55 |
|       4 | 10004   |     7500.00 |          5.00 | 2024-03-01      | 2024-04-15    |    7546.23 |
|       2 | 10002   |    10000.00 |          5.00 | 2024-03-01      | 2024-05-01    |   10082.56 |
|       3 | 10003   |     2000.00 |          5.00 | 2024-03-01      | 2024-03-15    |    2003.84 |
|       6 | 10002   |     8000.00 |          5.00 | 2024-03-01      | 2024-06-01    |    8100.82 |
|       8 | 10008   |     4000.00 |          5.00 | 2024-03-01      | 2024-03-26    |    4013.70 |
|       7 | 10007   |     6000.00 |          5.00 | 2024-03-01      | 2024-04-10    |    6032.88 |
|       9 | 10001   |     5500.00 |          5.00 | 2024-03-01      | 2024-04-05    |    5526.37 |
|       5 | 10005   |     3000.00 |          5.00 | 2024-03-01      | 2024-03-21    |    3008.22 |
|      10 | 10010   |     9000.00 |          5.00 | 2024-03-01      | 2024-05-10    |    9086.30 |
+---------+---------+-------------+---------------+-----------------+---------------+------------+
10 rows in set (0.01 sec)

Proszę wykonać te dwie instrukcje SQL, aby zaktualizować interest_rate i total_debt, odpowiednio:

update loan_records set interest_rate = interest_rate * 0.9 where user_id <= 10005;
update loan_records set total_debt = loan_amount + (loan_amount * (interest_rate / 100) * DATEDIFF(loan_end_date, loan_start_date) / 365);

Proszę ponownie sprawdzić, czy stare rekordy zostały zastąpione nowymi:

mysql> select * from loan_records order by auto_id;
+---------+---------+-------------+---------------+-----------------+---------------+------------+
| auto_id | user_id | loan_amount | interest_rate | loan_start_date | loan_end_date | total_debt |
+---------+---------+-------------+---------------+-----------------+---------------+------------+
|       1 | 10001   |     5000.00 |          4.50 | 2024-03-01      | 2024-03-31    |    5018.49 |
|       2 | 10002   |    10000.00 |          4.50 | 2024-03-01      | 2024-05-01    |   10075.21 |
|       3 | 10003   |     2000.00 |          4.50 | 2024-03-01      | 2024-03-15    |    2003.45 |
|       4 | 10004   |     7500.00 |          4.50 | 2024-03-01      | 2024-04-15    |    7541.61 |
|       5 | 10005   |     3000.00 |          4.50 | 2024-03-01      | 2024-03-21    |    3007.40 |
|       6 | 10002   |     8000.00 |          4.50 | 2024-03-01      | 2024-06-01    |    8090.74 |
|       7 | 10007   |     6000.00 |          5.00 | 2024-03-01      | 2024-04-10    |    6032.88 |
|       8 | 10008   |     4000.00 |          5.00 | 2024-03-01      | 2024-03-26    |    4013.70 |
|       9 | 10001   |     5500.00 |          4.50 | 2024-03-01      | 2024-04-05    |    5523.73 |
|      10 | 10010   |     9000.00 |          5.00 | 2024-03-01      | 2024-05-10    |    9086.30 |
+---------+---------+-------------+---------------+-----------------+---------------+------------+
10 rows in set (0.01 sec)

3. Wydajna paginacja

Proszę sobie wyobrazić, że trzeba posortować dane w określonej kolejności, a następnie pobrać rekord nr 90 001 do rekordu nr 90 010. Oznacza to, że mają Państwo duże przesunięcie wynoszące 90 000. Nazywamy to zapytaniem z głęboką paginacją. Nawet jeśli wymaga Pan tylko zestawu wyników składającego się z 10 wierszy, system bazy danych nadal musi odczytać cały zestaw danych do pamięci i wykonać pełne sortowanie.

Aby uzyskać wyższą wydajność wykonywania zapytań z głęboką paginacją, można wykorzystać moc automatycznego zwiększania kolumn. Główną ideą jest zapisanie max_value z unique_value na poprzedniej stronie i proszę przesunąć predykaty w dół o where unique_value > max_value limit rows_per_page.

Na przykład podczas tworzenia tabeli włącza Pan kolumnę automatycznego zwiększania: unique_value, która nadaje każdemu wierszowi identyfikator.

CREATE TABLE `demo`.`records_tbl` (
   `user_id` int(11) NOT NULL COMMENT "",
   `name` varchar(26) NOT NULL COMMENT "",
   `address` varchar(41) NOT NULL COMMENT "",
   `city` varchar(11) NOT NULL COMMENT "",
   `nation` varchar(16) NOT NULL COMMENT "",
   `region` varchar(13) NOT NULL COMMENT "",
   `phone` varchar(16) NOT NULL COMMENT "",
   `mktsegment` varchar(11) NOT NULL COMMENT "",
   `unique_value` BIGINT NOT NULL AUTO_INCREMENT
) DUPLICATE KEY (`user_id`, `name`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 10
PROPERTIES (
   "replication_allocation" = "tag.location.default: 3"
);

W zapytaniach z paginacją, załóżmy, że każda strona wyświetla 100 wyników, w ten sposób można pobrać pierwszą stronę zestawu wyników.

select * from records_tbl order by unique_value limit 100;

Proszę używać programów do rejestrowania maksymalnej unique_value w zwróconym wyniku. Załóżmy, że maksimum wynosi 99, można zapytać o dane z drugiej strony za pomocą następującej instrukcji:

select * from records_tbl where unique_value > 99 order by unique_value limit 100;

Jeśli trzeba zapytać o dane z głębszej strony, na przykład strony 101, co oznacza, że trudno jest uzyskać maksimum unique_value bezpośrednio z poprzedniej strony, można użyć następującej instrukcji:

select user_id, name, address, city, nation, region, phone, mktsegment
from records_tbl, (select unique_value as max_value from records_tbl order by unique_value limit 1 offset 9999) as previous_data
where records_tbl.unique_value > previous_data.max_value
order by unique_value limit 100;

Implementacja

Typowe bazy danych OLTP wykonują przyrostowe dopasowywanie identyfikatorów za pomocą mechanizmów transakcyjnych. Jednak w rozproszonym systemie baz danych opartym na MPP, takim jak Apache Doris, takie podejście może łatwo zadusić wydajność zapisu danych.

Dlatego Apache Doris 2.1 wprowadza innowacyjną implementację automatycznego zwiększania identyfikatorów. W zadaniu pozyskiwania danych, jeden z węzłów backendu (BE) będzie działał jako koordynator, który jest odpowiedzialny za alokację identyfikatorów auto-increment. Koordynator BE żąda zbiorczo zakresu identyfikatorów od front-endu (FE). FE upewnia się, że zakresy ID przydzielone każdemu BE nie pokrywają się, gwarantując w ten sposób unikalność ID.

Proces ten ilustruje poniższy rysunek. StreamLoad1 ma BE1 jako koordynatora. BE1 żąda partii identyfikatorów (zakres: 1-1000) od FE i buforuje je lokalnie. Po przydzieleniu wszystkich 1000 identyfikatorów BE1 zażąda nowej partii od FE. W tym samym czasie StreamLoad 2 wybiera BE3 jako koordynatora, a BE3 również żąda identyfikatorów z FE. Ponieważ identyfikatory 1-1000 zostały już przydzielone do BE1, FE przypisuje identyfikatory 1001-2000 do BE3.

streamload

Załóżmy, że StreamLoad1 i StreamLoad2 zapisują po 50 nowych rekordów danych, a przypisane im identyfikatory automatycznego zwiększania będą miały wartości 1-50 i 1001-1050.

Załóżmy, że StreamLoad3 pojawi się później i wybierze BE1 jako koordynatora, BE1 przypisze identyfikatory zaczynające się od 51 do danych zapisanych przez StreamLoad3. Z punktu widzenia użytkownika, wiersze zapisane przez StreamLoad3 otrzymają mniejsze numery ID niż te zapisane przez StreamLoad2, mimo że StreamLoad2 poprzedza StreamLoad3 w czasie.

Proszę zauważyć

Proszę zwrócić uwagę na:

  • Zakres gwarancji unikalności: Doris zapewnia, że wartości generowane w kolumnie autouzupełniania są unikalne w obrębie tabeli, ale dotyczy to tylko wartości automatycznie wypełnianych przez Doris. Jeśli użytkownik jawnie wstawi wartości do kolumny autouzupełniania, Doris nie może zagwarantować unikalności tych wartości.
  • Gęstość i ciągłość wartości: Doris zapewnia, że wartości generowane przez kolumnę automatycznego zwiększania są gęste. Jednak ze względu na wydajność nie może zagwarantować, że automatycznie wypełniane wartości są ciągłe. Oznacza to, że w kolumnie autouzupełniania mogą wystąpić skoki wartości. Dodatkowo, ponieważ wartości autouzupełniania są wstępnie przydzielane i buforowane w BE, wielkość wartości autouzupełniania nie może odzwierciedlać kolejności importu danych.

Wnioski

AUTO_INCREMENT zapewnia większą stabilność i niezawodność Doris w przetwarzaniu danych na dużą skalę. Jeśli brzmi to jak coś, czego Państwo potrzebują, proszę pobrać Apache Doris i proszę ją wypróbować. W przypadku problemów, które napotkają Państwo po drodze, proszę dołączyć do nas w sekcji Społeczność programistów i użytkowników Apache Doris i chętnie Państwu pomożemy.