約transl。:この記事では、英国の会社であるTicketsolveのチームリーダーが、彼の非常に具体的な問題の解決策を共有し、最新のMySQL8.0機能を使用していわゆる累積関数を作成するための一般的なアプローチを示します。そのリストは視覚的であり、詳細な説明が提供されているため、それほど深く掘り下げていない人でも、問題の本質を理解するのに役立ちます。
MySQLの累積関数を使用して更新を実行するための一般的な戦略は、カスタム変数とパターンを使用すること
UPDATE [...] SET mycol = (@myvar := EXPRESSION(@myvar, mycol))
です。
このパターンはオプティマイザーではうまく機能しないため(非決定的な動作につながる)、彼らはそれを放棄することにしました。(比較的)複雑なロジックを実装するのが(少なくとも同じ単純さで)より困難になるため、結果は一種の空虚になります。
この記事では、それを実装する2つの方法について説明します。ウィンドウ関数の使用(標準的なアプローチ)と再帰的CTEの使用(一般的なテーブル式)です。
要件と背景
CTEはかなり直感的ですが、CTEにあまり詳しくない人は、このトピックに関する以前の投稿を参照することをお勧めします。
ウィンドウ関数についても同じことが言えます。クエリ/概念について詳しくコメントしますが、一般的な考え方は問題ありません。ウィンドウ機能に特化した本や出版物はたくさんあります(それが私がまだそれらについて書いていない理由です)。ただし、ほとんどの例では、計算は財務結果または人口統計指標のいずれかで実行されます。ただし、この記事では実際のケースを使用します。
ソフトウェアの場合、MySQL 8.0.19の使用をお勧めします(必須ではありません)。再利用するには、すべての式を同じコンソールで実行する必要があります
@venue_id
。
ソフトウェアの世界では、有名なアーキテクチャ上のジレンマがあります。ロジックはアプリケーションレベルで実装する必要がありますか、それともデータベースレベルで実装する必要がありますか。これは完全に有効な質問ですが、私たちの場合、ロジックは基本レベルのままである必要があると想定しています。この理由は、たとえば、速度要件である可能性があります(私たちの場合のように)。
仕事
このタスクでは、特定のホール(劇場)に座席を割り当てます。
ビジネス上の目的で、各場所には、いわゆる「グループ化」(それを表す追加の番号)を割り当てる必要があります。
グループ化の値を決定するためのアルゴリズムは次のとおりです。
- 0から開始し、左上。
- 現在の行と前の行の間に空のスペースがある場合、またはこれが新しい行である場合は、前の値に2を追加します(これが絶対的な最初の場所でない場合)。それ以外の場合は、値を1増やします。
- グループを場所に割り当てます。
- 同じ行の新しい場所または次の行(前の行が終わっている場合)に移動し、ポイント2から繰り返します。場所がなくなるまですべてを続けます。
疑似コードのアルゴリズム:
current_grouping = 0
for each row:
for each number:
if (is_there_a_space_after_last_seat or is_a_new_row) and is_not_the_first_seat:
current_grouping += 2
else
current_grouping += 1
seat.grouping = current_grouping
実際には、左側の構成で右側に示す値を指定する必要があります:
x→ 0 1 2 0 1 2
y ╭───┬───┬───╮ ╭───┬───┬───╮
↓ 0 │ x │ x │ │ │ 1 │ 2 │ │
├───┼───┼───┤ ├───┼───┼───┤
1 │ x │ │ x │ │ 4 │ │ 6 │
├───┼───┼───┤ ├───┼───┼───┤
2 │ x │ │ │ │ 8 │ │ │
╰───┴───┴───╯ ╰───┴───┴───╯
トレーニング
ベーステーブルを次の最小限の構造にします。
CREATE TABLE seats (
id INT AUTO_INCREMENT PRIMARY KEY,
venue_id INT,
y INT,
x INT,
`row` VARCHAR(16),
number INT,
`grouping` INT,
UNIQUE venue_id_y_x (venue_id, y, x)
);
私たちは本当に必要はありません
row
し、列をnumber
。一方、レコードが完全にインデックスに含まれているテーブルは使用したくありません(実際の問題に近づけるためだけです)。
上の図に基づくと、各場所の座標は(y、x)です。
- (0、0)、(0、1)
- (1、0)、(1、2)
- (20)
行の追跡を容易にするため、最初の座標としてyを 使用していることに注意してください。
オプティマイザが予期しない短いパスを見つけないように、十分な数のレコードをロードする必要があります。もちろん、再帰CTEを使用します。
INSERT INTO seats(venue_id, y, x, `row`, number)
WITH RECURSIVE venue_ids (id) AS
(
SELECT 0
UNION ALL
SELECT id + 1 FROM venue_ids WHERE id + 1 < 100000
)
SELECT /*+ SET_VAR(cte_max_recursion_depth = 1M) */
v.id,
c.y, c.x,
CHAR(ORD('A') + FLOOR(RAND() * 3) USING ASCII) `row`,
FLOOR(RAND() * 3) `number`
FROM venue_ids v
JOIN (
VALUES
ROW(0, 0),
ROW(0, 1),
ROW(1, 0),
ROW(1, 2),
ROW(2, 0)
) c (y, x)
;
ANALYZE TABLE seats;
いくつかの注意:
- ここでは、CTEが興味深い(うまくいけば!)方法で使用されています。各ループはvenue_idを表しますが、会場ごとに複数の場所を生成する必要があるため、場所を含むテーブルとの相互結合を行います。
- v8.0.19(
VALUES ROW()...
)行コンストラクターは、(結合可能な)テーブルを実際に作成せずに表すために使用されます。 - 行と番号のランダムな値をプレースホルダーとして生成します。
- 簡単にするために、最適化は行いませんでした(たとえば、データタイプが必要以上に広い、レコードを挿入する前にインデックスが追加されるなど)。
古いアプローチ
古き良きアプローチは非常に単純明快です。
SET @venue_id = 5000; -- venue id; () id
SET @grouping = -1;
SET @y = -1;
SET @x = -1;
WITH seat_groupings (id, y, x, `grouping`, tmp_y, tmp_x) AS
(
SELECT
id, y, x,
@grouping := @grouping + 1 + (seats.x > @x + 1 OR seats.y != @y),
@y := seats.y,
@x := seats.x
FROM seats
WHERE venue_id = @venue_id
ORDER BY y, x
)
UPDATE
seats s
JOIN seat_groupings sg USING (id)
SET s.grouping = sg.grouping
;
-- Query OK, 5 rows affected, 3 warnings (0,00 sec)
それは簡単でした(ただし、警告を忘れないでください)。
小さな逸脱:この場合、私はブール演算のプロパティを使用します。次の式は同等です。
SELECT seats.x > @x + 1 OR seats.y != @y `increment`;
SELECT IF (
seats.x > @x + 1 OR seats.y != @y,
1,
0
) `increment`;
これを直感的に感じる人もいれば、そうでない人もいます。それは好みの問題です。これからはもっとコンパクトな表現を使います。
結果を見てみましょう:
SELECT id, y, x, `grouping` FROM seats WHERE venue_id = @venue_id ORDER BY y, x;
-- +-------+------+------+----------+
-- | id | y | x | grouping |
-- +-------+------+------+----------+
-- | 24887 | 0 | 0 | 1 |
-- | 27186 | 0 | 1 | 2 |
-- | 29485 | 1 | 0 | 4 |
-- | 31784 | 1 | 2 | 6 |
-- | 34083 | 2 | 0 | 8 |
-- +-------+------+------+----------+
素晴らしいアプローチ!
残念ながら、これには「マイナーな」欠点があります。機能しない場合を除いて、うまく機能します...
ポイントは、クエリオプティマイザが必ずしも左から右に計算を実行するとは限らないため、割り当て(:=)が間違った順序で実行される可能性があることです。間違った結果につながります。MySQLを更新した後、人々はしばしばこの問題に直面します。
MySQL 8.0では、この機能は実際に非推奨になっています。
-- UPDATE.
--
SHOW WARNINGS\G
-- *************************** 1. row ***************************
-- Level: Warning
-- Code: 1287
-- Message: Setting user variables within expressions is deprecated and will be removed in a future release. Consider alternatives: 'SET variable=expression, ...', or 'SELECT expression(s) INTO variables(s)'.
-- [...]
さて、状況を直そう!
現代のアプローチ#1:ウィンドウ機能
ウィンドウ関数の導入は、MySQLの世界で非常に期待されているイベントです。
一般的に、ウィンドウ関数の「スライド」の性質は、累積関数でうまく機能します。ただし、一部の複雑な累積関数では、最後の式の結果が必要です。ウィンドウ関数は列を操作するため、これらの関数はサポートしていません。
これは、問題を解決できないことを意味するのではなく、再考する必要があるだけです。
私たちの場合、タスクは2つの部分に分けることができます。各場所のグループ化は、次の2つの値の合計と見なすことができます。
- 各場所のシリアル番号、
- これより前のすべての場所の増分の累積値。
ウィンドウ機能に精通している人は、ここで典型的なパターンを認識します。
各シートのシーケンス番号は、組み込みの機能です。
ROW_NUMBER() OVER <window>
しかし、累積値を使用すると、すべてがはるかに興味深いものになります...それを計算するには、次の2つのアクションを実行します。
- 各場所の増分をカウントし、それをテーブル(またはCTE)に書き留めます。
- 次に、場所ごとに、ウィンドウ関数を使用してその場所の増分を合計します。
SQLを見てみましょう。
WITH
increments (id, increment) AS
(
SELECT
id,
x > LAG(x, 1, x - 1) OVER tzw + 1 OR y != LAG(y, 1, y) OVER tzw
FROM seats
WHERE venue_id = @venue_id
WINDOW tzw AS (ORDER BY y, x)
)
SELECT
s.id, y, x,
ROW_NUMBER() OVER tzw + SUM(increment) OVER tzw `grouping`
FROM seats s
JOIN increments i USING (id)
WINDOW tzw AS (ORDER BY y, x)
;
-- +-------+---+---+----------+
-- | id | y | x | grouping |
-- +-------+---+---+----------+
-- | 24887 | 0 | 0 | 1 |
-- | 27186 | 0 | 1 | 2 |
-- | 29485 | 1 | 0 | 4 |
-- | 31784 | 1 | 2 | 6 |
-- | 34083 | 2 | 1 | 8 |
-- +-------+---+---+----------+
すごい!
(簡単にするために、今後はUPDATEを省略していることに注意してください。)
リクエストを分析してみましょう。
高レベルのロジック
次のCTE (編集済み):
SELECT
id,
x > LAG(x, 1, x - 1) OVER tzw + 1 OR y != LAG(y, 1, y) OVER tzw `increment`
FROM seats
WHERE venue_id = @venue_id
WINDOW tzw AS (ORDER BY y, x)
;
-- +-------+-----------+
-- | id | increment |
-- +-------+-----------+
-- | 24887 | 0 |
-- | 27186 | 0 |
-- | 29485 | 1 |
-- | 31784 | 1 |
-- | 34083 | 1 |
-- +-------+-----------+
…前の場所からの各場所の増分を計算します(詳細は
LAG()
後で説明します)。これは、すべてのレコードとその前のレコードで機能し、累積的ではありません。
ここで、累積増分を計算するために、ウィンドウ関数を使用して、各場所までの合計を計算します。
-- (CTE here...)
SELECT
s.id, y, x,
ROW_NUMBER() OVER tzw `pos.`,
SUM(increment) OVER tzw `cum.incr.`
FROM seats s
JOIN increments i USING (id)
WINDOW tzw AS (ORDER BY y, x);
-- +-------+---+---+------+-----------+
-- | id | y | x | pos. | cum.incr. | (grouping)
-- +-------+---+---+------+-----------+
-- | 24887 | 0 | 0 | 1 | 0 | = 1 + 0 (curr.)
-- | 27186 | 0 | 1 | 2 | 0 | = 2 + 0 (#24887) + 0 (curr.)
-- | 29485 | 1 | 0 | 3 | 1 | = 3 + 0 (#24887) + 0 (#27186) + 1 (curr.)
-- | 31784 | 1 | 2 | 4 | 2 | = 4 + 0 (#24887) + 0 (#27186) + 1 (#29485) + 1 (curr.)
-- | 34083 | 2 | 1 | 5 | 3 | = 5 + 0 (#24887) + 0 (#27186) + 1 (#29485) + 1 (#31784)↵
-- +-------+---+---+------+-----------+ + 1 (curr.)
LAG()ウィンドウ関数
LAG関数は、最も単純な形式(
LAG(x)
)で、指定された列の前の値を返します。このような機能の典型的な不便は、ウィンドウの最初のエントリを処理することです。以前のレコードがないため、NULLを返します。LAGの場合、3番目のパラメーターとして目的の値を指定できます。
LAG(x, 1, x - 1) -- `x -1`
LAG(y, 1, y) -- `y`
デフォルト値を指定することにより、ウィンドウの境界の最初の場所が、行(y)を変更せずに、他の場所(x-1)の次の場所と同じロジックを持つようにします。
別の解決策はを使用すること
IFNULL
ですが、式は非常に面倒です。
-- , !
--
IFNULL(x > LAG(x) OVER tzw + 1 OR y != LAG(y) OVER tzw, 0)
IFNULL(x > LAG(x) OVER tzw + 1, FALSE) OR IFNULL(y != LAG(y) OVER tzw, FALSE)
2番目のパラメーター
LAG()
は、ウィンドウ内で戻る位置の数です。1は前の値です(これはデフォルトでもあります)。
技術的側面
名前付きウィンドウ
私たちのクエリは同じウィンドウを何度も使用します。次の2つのクエリは形式的に同等です。
SELECT
id,
x > LAG(x, 1, x - 1) OVER tzw + 1
OR y != LAG(y, 1, y) OVER tzw
FROM seats
WHERE venue_id = @venue_id
WINDOW tzw AS (ORDER BY y, x);
SELECT
id,
x > LAG(x, 1, x - 1) OVER (ORDER BY y, x) + 1
OR y != LAG(y, 1, y) OVER (ORDER BY y, x)
FROM seats
WHERE venue_id = @venue_id;
ただし、2つ目は、最適でない動作につながる可能性があります(少なくとも過去に遭遇しました)。オプティマイザーは、ウィンドウを独立していると見なし、それぞれを個別に計算できます。このため、常に名前付きウィンドウを使用することをお勧めします(少なくとも繰り返される場合)。
PARTITIONBYステートメント
通常、ウィンドウ機能はパーティションで実行されます。私たちの場合、次のようになります。
SELECT
id,
x > LAG(x, 1, x - 1) OVER tzw + 1
OR y != LAG(y, 1, y) OVER tzw
FROM seats
WHERE venue_id = @venue_id
WINDOW tzw AS (PARTITION BY venue_id ORDER BY y, x); -- !
ウィンドウはレコードの完全なセット(条件によってフィルタリングされる
WHERE
)と一致するため、ウィンドウ(パーティション)を指定する必要はありません。
ただし、このクエリをテーブル全体で実行
seats
する必要がある場合は、すべてのユーザーのウィンドウがリセットされるようにする必要がありますvenue_id
。
並べ替え
リクエスト
ORDER BY
はウィンドウレベルで設定されます。
SELECT
id,
x > LAG(x, 1, x - 1) OVER tzw + 1
OR y != LAG(y, 1, y) OVER tzw
FROM seats
WHERE venue_id = @venue_id
WINDOW tzw AS (ORDER BY y, x)
この場合、ウィンドウの並べ替えはSELECTとは別のものです。それは非常に重要です!このリクエストの動作:
SELECT
id,
x > LAG(x, 1, x - 1) OVER tzw + 1
OR y != LAG(y, 1, y) OVER tzw
FROM seats
WHERE venue_id = @venue_id
WINDOW tzw AS ()
ORDER BY y, x
…未定義。マニュアルに目を向けましょう:
クエリ結果の文字列は、WHERE、GROUP BY、およびHAVING句が実行された後のFROM句から決定され、ウィンドウ内での実行はORDER BY、LIMIT、およびSELECTDISTINCTの前に行われます。
いくつかの考慮事項
一般的に、このタイプの問題では、各レコードを前のレコードの関数として表すのではなく、各レコードの状態変化を計算してから合計するのが理にかなっています。
このソリューションは、置き換える機能よりも複雑ですが、同時に信頼性があります。残念ながら、このアプローチは常に可能または簡単に実装できるとは限りません。ここで、再帰CTEが役立ちます。
最新のアプローチ#2:再帰的CTE
MySQLのCTEの機能は限られているため、このアプローチには少し注意が必要です。一方、これは万能の直接的なソリューションであるため、グローバルなアプローチを再考する必要はありません。
最終リクエストの簡略版から始めましょう。
-- `p_` `Previous`
--
WITH RECURSIVE groupings (p_id, p_venue_id, p_y, p_x, p_grouping) AS
(
(
SELECT id, venue_id, y, x, 1
FROM seats
WHERE venue_id = @venue_id
ORDER BY y, x
LIMIT 1
)
UNION ALL
SELECT
s.id, s.venue_id, s.y, s.x,
p_grouping + 1 + (s.x > p_x + 1 OR s.y != p_y)
FROM groupings, seats s
WHERE s.venue_id = p_venue_id AND (s.y, s.x) > (p_y, p_x)
ORDER BY s.venue_id, s.y, s.x
LIMIT 1
)
SELECT * FROM groupings;
ビンゴ!このクエリは(比較的)単純ですが、さらに重要なことに、累積グループ化関数を可能な限り単純な方法で表現します。
p_grouping + 1 + (s.x > p_x + 1 OR s.y != p_y)
-- :
@grouping := @grouping + 1 + (seats.x > @x + 1 OR seats.y != @y),
@y := seats.y,
@x := seats.x
CTEにあまり詳しくない人でも、論理は明確です。最初の列は、次の順序でホールの最初の座席です。
SELECT id, venue_id, y, x, 1
FROM seats
WHERE venue_id = @venue_id
ORDER BY y, x
LIMIT 1
再帰的な部分では、次のことを繰り返します。
SELECT
s.id, s.venue_id, s.y, s.x,
p_grouping + 1 + (s.x > p_x + 1 OR s.y != p_y)
FROM groupings, seats s
WHERE s.venue_id = p_venue_id AND (s.y, s.x) > (p_y, p_x)
ORDER BY s.venue_id, s.y, s.x
LIMIT 1
条件
WHERE
一緒にオペレータとORDER BY
し、LIMIT
単に次に、同じと場所を見つけるvenue_id
が、使用のための配列(venue_id、X、Y)にlshimi座標(x、y)。ソート式
の部分
s.venue_id
は非常に重要です!これにより、インデックスを使用できます。
オペレーター
SELECT
:
- 蓄積を実行(計算
(p_)grouping
)、 - 現在の位置(の値を提供し
s.id
、s.venue_id
、s.y
、s.x
次のサイクルで)。
FROM groupings
CTEの再帰性の要件を満たすこと
を選択します。
ここで興味深いのは、再帰CTEを反復子として使用
groupings
し、再帰サブクエリでテーブルからフェッチし、それを結合してseats
、さらに処理するためのデータを見つけることです。
JOIN
は正式にはクロスですが、演算子のためにLIMIT
1つのレコードのみが返されます。
作業バージョン
残念ながら、上記のクエリは
ORDER BY
現在再帰サブクエリでサポートされていないため、機能しません。さらに、LIMIT
ここで使用されるセマンティクスは、外部クエリに適用される一般的なセマンティクスとは異なります。
LIMITがサポートされるようになりました[..]結果のデータセットへの影響は、外部SELECTでLIMITを使用する場合と同じです。
しかし、これはそれほど深刻な問題ではありません。動作中のバージョンを見てみましょう:
WITH RECURSIVE groupings (p_id, p_venue_id, p_y, p_x, p_grouping) AS
(
(
SELECT id, venue_id, y, x, 1
FROM seats
WHERE venue_id = @venue_id
ORDER BY y, x
LIMIT 1
)
UNION ALL
SELECT
s.id, s.venue_id, s.y, s.x,
p_grouping + 1 + (s.x > p_x + 1 OR s.y != p_y)
FROM groupings, seats s WHERE s.id = (
SELECT si.id
FROM seats si
WHERE si.venue_id = p_venue_id AND (si.y, si.x) > (p_y, p_x)
ORDER BY si.venue_id, si.y, si.x
LIMIT 1
)
)
SELECT * FROM groupings;
-- +-------+------+------+------------+
-- | p_id | p_y | p_x | p_grouping |
-- +-------+------+------+------------+
-- | 24887 | 0 | 0 | 1 |
-- | 27186 | 0 | 1 | 2 |
-- | 29485 | 1 | 0 | 4 |
-- | 31784 | 1 | 2 | 6 |
-- | 34083 | 2 | 0 | 8 |
-- +-------+------+------+------------+
サブクエリを使用するのは少し不快ですが、このアプローチは機能し、とにかくいくつかの式が必要なため、ここでは定型文が最小限に抑えられます。
ここでは、ユニオン
groupings
とに関連付けられた順序付けと制限を行う代わりにseats
、サブクエリ内でそれを行い、それを外部クエリに渡します。外部クエリは、ターゲットレコードのみを選択します。
パフォーマンスに関する考察
EXPLAINANALYZEを使用してクエリ実行計画を調べてみましょう。
mysql> EXPLAIN ANALYZE WITH RECURSIVE groupings [...]
-> Table scan on groupings (actual time=0.000..0.001 rows=5 loops=1)
-> Materialize recursive CTE groupings (actual time=0.140..0.141 rows=5 loops=1)
-> Limit: 1 row(s) (actual time=0.019..0.019 rows=1 loops=1)
-> Index lookup on seats using venue_id_y_x (venue_id=(@venue_id)) (cost=0.75 rows=5) (actual time=0.018..0.018 rows=1 loops=1)
-> Repeat until convergence
-> Nested loop inner join (cost=3.43 rows=2) (actual time=0.017..0.053 rows=2 loops=2)
-> Scan new records on groupings (cost=2.73 rows=2) (actual time=0.001..0.001 rows=2 loops=2)
-> Filter: (s.id = (select #5)) (cost=0.30 rows=1) (actual time=0.020..0.020 rows=1 loops=5)
-> Single-row index lookup on s using PRIMARY (id=(select #5)) (cost=0.30 rows=1) (actual time=0.014..0.014 rows=1 loops=5)
-> Select #5 (subquery in condition; dependent)
-> Limit: 1 row(s) (actual time=0.007..0.008 rows=1 loops=9)
-> Filter: ((si.y,si.x) > (groupings.p_y,groupings.p_x)) (cost=0.75 rows=5) (actual time=0.007..0.007 rows=1 loops=9)
-> Index lookup on si using venue_id_y_x (venue_id=groupings.p_venue_id) (cost=0.75 rows=5) (actual time=0.006..0.006 rows=4 loops=9)
計画は期待に沿ったものです。この場合、最適な計画の基礎はインデックス検索にあります。
-> Nested loop inner join (cost=3.43 rows=2) (actual time=0.017..0.053 rows=2 loops=2)
-> Single-row index lookup on s using PRIMARY (id=(select #5)) (cost=0.30 rows=1) (actual time=0.014..0.014 rows=1 loops=5)
-> Index lookup on si using venue_id_y_x (venue_id=groupings.p_venue_id) (cost=0.75 rows=5) (actual time=0.006..0.006 rows=4 loops=9)
...最も重要なこと。インデックススキャンを実行すると、パフォーマンスが大幅に低下します(つまり、必要なレコードを一度に探すのではなく、インデックスレコードを線形にスキャンします)。
このように、仕事へのこの戦略のために、関連する索引は場所になければなりませんし、オプティマイザによって、可能な限り効率的に使用すること。
将来的に制限が解除された場合、サブクエリを使用する必要がなくなり、オプティマイザのタスクが大幅に簡素化されます。
次善の計画の代替案
最適な計画を決定できない場合は、一時テーブルを使用してください。
CREATE TEMPORARY TABLE selected_seats (
id INT NOT NULL PRIMARY KEY,
y INT,
x INT,
UNIQUE (y, x)
)
SELECT id, y, x
FROM seats WHERE venue_id = @venue_id;
WITH RECURSIVE
groupings (p_id, p_y, p_x, p_grouping) AS
(
(
SELECT id, y, x, 1
FROM seats
WHERE venue_id = @venue_id
ORDER BY y, x
LIMIT 1
)
UNION ALL
SELECT
s.id, s.y, s.x,
p_grouping + 1 + (s.x > p_x + 1 OR s.y != p_y)
FROM groupings, seats s WHERE s.id = (
SELECT ss.id
FROM selected_seats ss
WHERE (ss.y, ss.x) > (p_y, p_x)
ORDER BY ss.y, ss.x
LIMIT 1
)
)
SELECT * FROM groupings;
このクエリでインデックススキャンが渡されたとしても、テーブルが
selected_seats
非常に小さいため、多くの費用がかかります。
結論
効率的であるが欠陥のあるワークフローが、MySQL8.0で導入された非常に単純な機能に置き換えられることを非常に嬉しく思います。
それまでの間、8.0の新機能の開発は継続されており、すでに成功しているリリースがさらに改善されています。
成功した再帰!
翻訳者からのPS
私たちのブログも読んでください: