独自のデータベースを作成する: リレーショナル代数の導出とクエリ ツリーの実行効率

前のセクションでは、SQL インタープリターの実装を完了しました。SQL ステートメントを解析することで、SQL ステートメントが何をしたいのかを知ることができ、その後、SQL ステートメントの意図を実行する必要があります。つまり、指定されたテーブルから必要なデータを抽出する必要があります。SQL ステートメントを実行するには、いわゆる「関係代数」を理解する必要があります。いわゆる代数は、基本的に演算子と操作オブジェクトの定義です。関係代数には、select、project、product の 3 種類の演算子があります。 、操作オブジェクトはデータベース テーブルです。

select に相当する操作は、与えられたデータテーブルから、各行のフィールドを変更せずに、条件を満たす行を抽出することです。たとえば、ステートメント「select * from customer where id=1234」。このステートメントが実行されると、customer テーブル内のすべてのレコードの id フィールドが 1234 に等しい行が抽出されて、新しいテーブルが形成されます。

プロジェクトに対応する操作は、指定されたデータ テーブルから複数のフィールドを選択して新しいテーブルを形成することです。新しいテーブルの列は変更されますが、行数は元のテーブルと同じです。たとえば、ステートメント「select name」 、顧客からの年齢」、このステートメントは、元のテーブルから 2 つのフィールド名と年齢を抽出して、新しいテーブルを形成します。新しいテーブルの列は元のテーブルより少なくなりますが、行数は標準化されていません。

Product はデカルト積に相当し、2 つのテーブルを操作します。左側のテーブルから順番に 1 行を抽出し、右側のテーブルのすべての行と結合します。したがって、左側のテーブルの行数と列数がLr、Lc、右の表 表の行数と列数はRr、Rc、演算結果の新しい表では行数はLr * Rr、列数はLc+Rcとなります。

上記の関係代数と組み合わせると、指定された SQL ステートメントを解析した後、対応する操作を実行するために、クエリ ツリーと呼ばれる特定のデータ構造を構築する必要があります。クエリ ツリーの特徴は、そのリーフ ノードがデータベースに対応していることです。テーブルとその親ノード 上で説明した関係代数演算に対応して、具体的な例を見てみましょう: 「給与が 2000 を超える顧客から名前、年齢を選択する」

このステートメントは、select と project という 2 つのリレーショナル代数操作に対応します。2 つのタイプのクエリ ツリーに対応できます。1 つ目は次のとおりです。
画像の説明を追加してください
このクエリ ツリーは、プロジェクト操作が最初にデータ テーブルの顧客に対して実行されることを意味します。テーブルから名前と年齢の 2 つの列を選択し、行数を変更しないでください。次に、この結果で各行をフィルタリングし、フィールドの給与が 2000 より大きい行を選択します。別の種類のクエリ ツリーを使用することもできることは想像に難くありません。つまり、最初に選択操作を実行します。つまり、最初に給与 > 2000 を満たすテーブル内のすべての行を選択し、次にこれに基づいて次の値を追加します。名前と年齢の 2 つの名前。抽出された列と対応するクエリ ツリーは次のとおりです。
画像の説明を追加してください
異なるクエリ ツリーは本質的に同じであると感じるかもしれません。実際、異なるクエリ ツリーはデータ操作の効率に大きな影響を与えます。あるクエリ ツリーに対応する操作の効率は、別のクエリ ツリーの実行効率よりも 10 倍、さらには 100 倍優れている場合があるため、考えられるすべてのクエリ ツリーを構築した後、さまざまなクエリ ツリーの実行効率も計算する必要があります。このステップは計画と呼ばれます。

前の章では、Scan インターフェイスを実装しました。このインターフェイスは、前述のいくつかの演算子に適用できるため、前の章では、TableScan、SelectScan、ProjectScan、ProductScan をそれぞれ実装しました。後の 3 つの Scan オブジェクトは次のとおりです。初期化中に、Scan インターフェイスを実装するオブジェクトを入力する必要があります。これは、上記のクエリ ツリー構造に対応できます。一番下のリーフ ノードは、TableScan に対応します。初期化中に上位レベルで SelectScan によって入力された Scan オブジェクトは、TableScan です。最上位のノードは、 ProjectScan に対応して、初期化中に入力される Scan オブジェクトは SelectScan です。

これらのセクションを組み合わせてコードに実装してみましょう。まず知覚的な知識を得ることができます。その後、上記の計画をさらに分析します。前の解析プロセスでは、select ステートメントを解析し、最終的に QueryData オブジェクトを構築しました。このオブジェクトには 3 つの部分が含まれています。最初の部分は、プロジェクト操作の実装に使用できるフィールドです。2 番目の部分は、テーブル名です。 TableScan オブジェクトを構築するには、最終的な pred オブジェクトを TableScan オブジェクトと組み合わせて SelectScan オブジェクトを構築します。

以前、record_manager を勉強したときに TableScan を実装しましたが、table_scan_test.go というテスト ファイルの実装も提供しました。TestTableScanInsertAndDelete という関数があり、データ テーブルのデータ ストレージを構築し、TableScan オブジェクトを使用してテーブルを走査します。ここでは、当時の慣行を真似して最初にstudentテーブルを作成し、このテーブルにstring型のname、int型のage、そして最後にidの3つのフィールドのみを持つように設定します。数値型を指定し、このテーブルに数行のデータを追加し、main.go に次のコードを追加します。


func main() {
    
    
	//构造 student 表
	file_manager, _ := fm.NewFileManager("recordtest", 400)
	log_manager, _ := lm.NewLogManager(file_manager, "logfile.log")
	buffer_manager := bmg.NewBufferManager(file_manager, log_manager, 3)

	tx := tx.NewTransation(file_manager, log_manager, buffer_manager)
	sch := NewSchema()

	//name 字段有 16 个字符长
	sch.AddStringField("name", 16)
	//age,id 字段为 int 类型
	sch.AddIntField("age")
	sch.AddIntField("id")
	layout := NewLayoutWithSchema(sch)
	//构造student 表
	ts := query.NewTableScan(tx, "student", layout)
	//插入 3 条数据
	ts.BeforeFirst()
	//第一条记录("jim", 16, 123)
	ts.Insert()
	ts.SetString("name", "jim")
	ts.SetInt("age", 16)
	ts.SetInt("id", 123)
	//第二条记录 ("tom", 18, 567)
	ts.Insert()
	ts.SetString("name", "tom")
	ts.SetInt("age", 18)
	ts.SetInt("id", 567)
	//第三条数据 hanmeimei, 19, 890
	ts.Insert()
	ts.SetString("name", "HanMeiMei")
	ts.SetInt("age", 19)
	ts.SetInt("id", 890)

	//构造查询 student 表的 sql 语句,这条语句包含了 select, project 两种操作
	sql := "select name, age from student where id=890"
	sqlParser := parser.NewSQLParser(sql)
	queryData := sqlParser.Query()

	//根据 queryData 分别构造 TableScan, SelectScan, ProjectScan 并执行 sql 语句
	//创建查询树最底部的数据表节点
	tableScan := query.NewTableScan(tx, "student", layout)
	//构造上面的 SelectScan 节点
	selectScan := query.NewSelectionScan(tableScan, queryData.Pred())
	//构造顶部 ProjectScan 节点
	projectScan := query.NewProductionScan(selectScan, queryData.Fields())
	//为遍历记录做初始化
	projectScan.BeforeFirst()
	for true {
    
    
		//查找满足条件的记录
		if projectScan.Next() == true {
    
    
			fmt.Println("found record!")
			for _, field := range queryData.Fields() {
    
    
				fmt.Printf("field name: %s, its value is : %v:\n", projectScan.GetVal(field))
			}
		} else {
    
    
		    break
		}
		
	}

	fmt.Println("complete sql execute")
	tx.Close()
}

上記のコードでいくつかの分析を行う必要があります。SelectScan と ProjectScan が初期化されるとき、Scan オブジェクトを渡す必要があります。最初のクエリ ツリーに従う場合、ProjectScan は構築用のパラメーターとして SelectScan に渡されます。 2番目のクエリツリー、SelectScanをたどります。これは構築用のパラメータとしてProjectScanに渡されますが、いずれにしてもTableScanはクエリツリーのリーフノードに相当するので、構築用のパラメータとしてSelectScanまたはProjectScanに渡されます。そのため、ProjectScan.Next はコード SelectScan.Next で呼び出され、最後に TableScan.Next を呼び出します。TableScan.Next は、下部のファイル ストレージから各レコードをフェッチし、SelectScan に返す役割を担います。後者は、フェッチされたレコードが次の条件を満たすかどうかを判断します。その場合、True が返されるため、ProjectScan.Next は True を取得するため、コードは where 条件を満たすレコードを検索し、ProjectScan はレコード内の指定されたフィールドを取り出して、 SQL ステートメント。詳細については、ステーション b を参照してください。「coding Disney」を検索してデバッグ デモ ビデオを表示し、対応するコードをダウンロードします。

リンク: https://pan.baidu.com/s/1hIACldEXaABVkbZiJAevIg 抽出コード: vb46

クエリ ツリーの効率を計算する方法を見てみましょう。データベース システムの動作において、最もリソースと時間を消費する操作はハードディスクの読み取りですが、メモリの読み取りに比べて、ハードディスクの読み取り速度は 2 ~ 3 桁遅くなります。ハードディスクはメモリの読み取りに比べて 1 桁、100 倍以上遅いため、クエリ ツリーの実行効率を判断するときは、指定されたデータを返すためにハードディスクに何回アクセスする必要があるかを判断する必要があります。アクセス数が少ないほど効率が高くなります。

Scan インターフェイスを実装するオブジェクトを表すために s を使用するため、 s は TableScan、SelectScan、ProjectScan、ProductScan およびその他のオブジェクトのインスタンスを表すことができます。条件を満たすレコードを返すために特定のインスタンス オブジェクトがアクセスする必要があるブロックの数を表すには B(s) を使用し、特定のインスタンス オブジェクトが必要なレコードを返す前にクエリする必要があるレコードの数を表すには R(s) を使用します。レコード、および表す V(s, F) データベース テーブルの走査後に Scan インスタンス オブジェクト s によって返されるレコードでは、F フィールドにはさまざまな値の数が含まれます。V(s, F) は比較的抽象的です。見てみましょう。具体的な例として、Student テーブルに name、age. 、id という 3 つのフィールドが含まれているとします。ステートメント「select * from Student where age <= 20」を実行すると、このステートメントの実行後に返される 3 つのレコードは次のとおりです。 {(
名前: ジョン),(年齢: 18),(id=1)}, {(名前: ジム),(年齢: 19),(id=2)},{(名前:リリー),(年齢: 20),(id=3)},{(name:mike),(age: 20), (id=4)} つまり、
ステートメントの実行後に 4 つのレコードが返されます。F がフィールド名に対応する場合、 4 つのレコードの内容がフィールド名に対して異なるため、V(s, F)=4 となります。したがって、V(s,F)=4 となります。F がフィールドの年齢に対応する場合、V(s,F) )=3、4 つのレコードの最後の 2 つの対応するフィールドの値は両方とも 20 であるため、4 つのレコードのフィールド age の値は 、異なる値を持つレコードの数は 3 です。

B(s)、R(s)、V(s,F) は、クエリ ブックの効率を計算する導出プロセスにおいて非常に重要な役割を果たします。また、知っておく必要があるのは、SelectScan、ProductScan、および ProjectScan を作成するときに、初期化関数が Scan インターフェイスを満たすオブジェクト (署名コード:= query.NewProductionScan(selectScan, queryData.Fields) の projectScan など) を渡すことです。 ()) , したがって、3 つの式 B(s), R(s), V(s,F) の場合、 s は変数 projectScan に対応します。コンストラクターは selectScan インスタンスを入力することに注意してください。同時に、コードを入力すると、 projectScan が Next() インターフェイスを調査するときに、実際には selectScan の Next インターフェイスの呼び出しに切り替わるので、B (projectScan) の計算は実際には B (selectScan) の計算に依存する必要があります。

したがって、s インスタンスを構築するために入力されたパラメーターを表すために s1 を使用します。次に、B(s) を計算するには、B(s1) を計算する必要があります。次に、B(s)、R(s)、V( s,F) の導出。
最初のケースでは、s は TableScan のインスタンスです。TableScan の構築時に他の Scan インターフェイス インスタンスを入力しなかったため、対応する B(s) の値は Next の実行中に TableScan がアクセスする必要があるブロックの数であり、V(s) は TableScan インスタンスが Next( ).走査されたレコード数 V(s,F) は、Next() 調査の実行後に走査されたレコードのうち、フィールド F の値が異なるレコードの数です。

s が selectScan のインスタンスである場合、インスタンスのコンストラクターにも Predicate オブジェクトがあることに注意してください。この Predicate の対応する形式が A=c であると仮定します。ここで、A はレコード内のフィールドを表し、c は定数、s1 です。 selectScan の構造を表すために使用されます。オブジェクトは、渡された Scan オブジェクトです。SelectScan コードから、実行時に Next() インターフェイスが入力 Scan オブジェクトの Next() インターフェイスを呼び出すことがわかります。 ) の SelectScan が返され、受信スキャンでオブジェクトが訪問したブロック数が返されます。その後、SelectScan オブジェクトは対応するブロックに訪問しているため、B(s) = B(s1) となります。ここで、s は SelectScan オブジェクトに対応し、s1 は対応します。コンストラクターによって渡された Scan オブジェクトに渡します。

R(s) の値を見てみましょう。SelectScan の Scan 実装を見てみましょう。最初に受信 Scan オブジェクトの Next インターフェイスを呼び出してレコードを取得し、次に Predicate の IsSatisfied インターフェイスを呼び出して、取得したレコードが正しいかどうかを判断します。フィルタリング条件を満たしている場合は、すぐに戻ります。満たされていない場合は、Next of Scan が False を返すまで、Scan オブジェクトの Next インターフェイスを呼び出して次のレコードを取得し続けます。

たとえば、Student テーブルに 100 個のレコードがあり、各レコードに年齢フィールドが含まれているとします。これらの 100 個のレコードには年齢の値が 4 つあるとします (特定の値はここでは関係ありません)。 、値を知る必要があるだけです。値の状況は存在するだけあります)、したがって、V(s1, age) == 4 となります。もちろん、各状況に対応するレコードの数を知ることはできないので、次の数であると仮定します。各値に対応するレコードの数は等しいため、各値に対応するレコードの数は R(s1) / V(s1, age) = 25 となります。クエリ定数 c の値が 4 つの条件のいずれかを満たす場合、 SelectScan の Next によって返されるレコードの数は R(s1) /V(s1,age) です。このことから、R(s) はフィルター条件「A=c」を満たすと結論付けられます。ここで、A はフィールドに対応し、c は定数です。この場合、R(s) の値は R( s1)/V(s1, A) となります。

フィルタ条件が A=B (A と B が両方ともテーブルのフィールドである場合) の場合、この問題は複雑になります。この状況には、確率論の知識が必要です。まず、フィールド B の値シチュエーションの数がフィールド A よりも大きいと仮定します。テーブル内のフィールド B の値カテゴリの数を表すのに F_B を使用し、フィールド A の値カテゴリの数を表すのに F_A を使用します。テーブルの中に。分析の便宜上、さらに仮定を立てます。テーブルには 100 個のレコードがあり、その中にフィールド B の値カテゴリが 10 個、フィールド A の値カテゴリが 4 個あると仮定します。テーブルからレコードとフィールドの値をランダムに取得します。 B は 10 個のカテゴリの中の特定のカテゴリの確率は 1/10 で、フィールド A がフィールド B と同じ値を持つ確率も 1/10 です。なぜなら、フィールド A の値を B と等しくしたい場合、また、範囲内の B の値カテゴリに含まれている必要があり、現在のフィールド B と同じ値を持つ確率も 1/10 です。したがって、A を満たすためにこのレコードがランダムに選択される確率は、 =B は 1/10 * 1/10 = 1/100 です。テーブル内のフィールド B には 10 個の可能な値カテゴリがあるため、レコードがランダムに選択された場合、A=B を満たす確率は 10 です。 * 1 / 100 = 1 / 10。したがって、テーブル内のすべてのレコードを走査すると、A = B の予想されるレコード数は 100 * (1 / 10) = 10 レコードを満たすことができます。

上記の分析では、フィールド B の値が A より大きいと仮定します。A の値が B より大きい場合、分析プロセスは同じであり、上記の分析プロセスの A と B を入れ替えます。したがって、フィルター条件が A = B (A と B が両方ともテーブル内のフィールドである場合) の場合、R(s) によって返されることが期待されるレコードの数は R(s1) / max{V(s1,A), V(s1,B) } ですが、確率論の知識が絡むので少しわかりにくいです。

s に対応するインスタンスが ProjectScan の場合、その Next インターフェイスの実装から、入力 Scan オブジェクトの Next インターフェイスのみを呼び出すことがわかります。したがって、後者がアクセスするブロックと返されるレコードの数も同様です。ブロックとレコードの数にアクセスするため、s が ProjectScan オブジェクトに対応する場合、B(s)=B(s1)、R(s)=R(s1)、V(s,F)=V(s1,F) となります。

最も複雑なものは ProductScan です。最初にその実装コードを見てみましょう。そのコンストラクターは 2 つの Scan オブジェクトを渡します。これらをそれぞれ表すために s1 と s2 を使用します。その BforeFirst() 関数では、最初に s1 の Next 関数を呼び出します。 Next 関数で、s2 の Next が true を返した場合、インターフェイスの実行は完了します。false を返した場合は、s2 の beforeFirst() 関数を呼び出して、s2 のレコード ポインタを最初の項目にリダイレクトしてから、s1.Next を実行します。 () また、これは s1 のレコードが次のレコードをポイントしていることを意味します。つまり、s1 のすべてのレコードは s2 のすべてのレコードと結合する必要があります。

s1.Next() が false を返すと、ProductScan の Next() は false を返すため、ProjectScan の Next() 関数が戻ると、s1 が走査するブロックの数に関係なく、同じブロックも走査します。同時に、s2 の Next が false を返すたびに、この時点で s2 によって返されるブロックの数は R(s2) です。その後、s2 は BeforeFirst() インターフェイスを呼び出してレコード ポインタを最初のレコードにリダイレクトし、その後 s1 を呼び出しますNext インターフェイスは、レコードが次のレコードを指すようにします。その後、s2 はすべてのレコードを再度走査します。このプロセスでアクセスするブロックの数は R(s2) であるため、ProductScan の Next() インターフェイスの呼び出し中に、次のインターフェイスにアクセスします。 R(s1) * B(s2) ブロックであるため、アクセスするブロックの数は B(s1) + R(s1) * B(s2) となります。

R(s) の場合、その Next() インターフェイスで、s2 がすべてのレコードを走査するたびに、s1 が Next() ポインターを介して次のレコードを指し、その後 s2 がそのすべてのレコードを再び走査することがわかります。 ) = R(s1) * R(s2)。

V(s,F) の場合、F が s1 が存在するテーブルのフィールドに対応する場合、V(s,F) = V(s1, F) となります。それが s2 が存在するテーブルのフィールドに対応する場合、 、この場合、V(s, F)= V(s2, F)となります。

ここで重要な点の 1 つは、ProductScan オブジェクト インスタンスを構築するときに 2 つの入力 Scan オブジェクトの位置を交換すると、B(s) の値が異なることです。スキャン オブジェクトのパラメータは、実行効率にさまざまな影響を与えます。

RPB(s) = R(s) / B(s) を導入します。この変数は、各ブロックに対応するレコードの平均数を表します。したがって、B(s) = B(s1) + R(s1) * B(s2) は B(s) = B(s1) + (RPB(s1) * B(s1) * B(s2)) に変換できます。 ProductScan を構築するときに s1、s2 の順序を置き換えると、B(s2 ) * となるため、B(s) = B(s2) + (RPB(s2) * B(s2) * B(s1)) になります。 B(s1) は B(s1) または B(s2) よりもはるかに大きいため、B(s) の増加を判断するときは、方程式の右側の B(s1) または B(s2) を無視できます。式の右側のプラス記号の右側を調べると、RPB(s1) < RPB(s2) の場合、ProductScan を構築するときに s1 が s2 の前に配置され、構築されたNext() 関数の実行時にインスタンスにアクセスすると、それに応じてブロックの数も少なくなり、その逆も同様です。

具体的な例を見てみましょう。Student とDepartment という 2 つのテーブルがあるとします。Student テーブルにはフィールド SId、SName、Grad Year、MajorId があり、それぞれ学生の ID、名前、卒業時期、専攻 ID を表します。部門テーブルにはフィールド DId と DName が含まれており、それぞれ専門職 ID と専門職名を表します。

このうち、Student テーブルに対応する B(s) は 4500、つまりすべてのデータのストレージが 4500 ブロックを占有し、その R(s) は 45000、つまりテーブルには 45000 レコードがあり、V(s) 、SId)= 45000、つまり 45000 個の SId 値の状況が存在します。つまり、各レコードに対応する SId フィールド値が異なります。V (s, SName) = 44960、つまり、すべての生徒の名前フィールド値はエントリは 44960 件あります。つまり、45000 人の学生のうち (45000 - 44960)=40 人の学生がいます。彼らの名前は他の学生と同じ名前です、V(s, MajorId)=40、つまり、これらの学生は以下に属します40の異なる専攻。

部門テーブルの場合、対応する B(s) は 2、つまりすべてのデータが 2 ブロックを占有し、R(s)=40、つまり合計 40 レコードがあります、V(s, DId) = V (s , DName) = 40、つまり、各レコードのフィールド DId と DName の値は異なります。

数学部の学生全員の名前を検索したいとします。したがって、最初に、Department テーブルの DName フィールドが「Mathematics」に等しいレコードを抽出し、次に Student テーブルで得られた結果に対して Product 操作を実行し、最後にその操作でフィールド MajorId = DId を持つレコードを見つける必要があります。これに基づいて、プロジェクト操作を実行して SName フィールドを抽出します。対応する SQL ステートメントは、Student.MajorId=DepartMent.DId および DepartMent.DName=“math” の select Student.SName です。この SQL ステートメントは、ツリー:
画像の説明を追加してください
前のコードに従って、上記の検索ツリーに対応するコードを次のように実行します。

file_manager, _ := fm.NewFileManager("studentdb", 1024)
log_manager, _ := lm.NewLogManager(file_manager, "logfile.log")
buffer_manager := bmg.NewBufferManager(file_manager, log_manager, 3)
tx := tx.NewTransation(file_manager, log_manager, buffer_manager)
matedata_manager := mgm.NewMetaDataManager(true, tx)

slayout := metadata_manager.GetLayout("student", tx)
dlayout := metadata_manager.GetLayout("department",tx)

s1 := query.NewTableScan(tx, "student", slayout)
s2 := query.NewTableScan(tx, "department", dlayout)
pred1 := queryData.Pred() //对应dname = 'math'

s3 := query.NewSelectScan(s2, pred1)
s4 := query.NewProductScan(s1, s3)

pred2 := queryData.Pred() //对应 majorid=did
s5 := query.NewSelectScan(s4, pred2)
fields := queryData.Fields() //获得"sname"
s6 := query.NewProjectScan(s5, fields)

上記のコードで注意する必要があるのは、ProductScan に対応する s4 です。最初に入力された Scan オブジェクトは、学生テーブルの TableScan オブジェクトに対応する s1 です。その Next インターフェイスは、すべてのレコード (45,000 レコード) を走査します。最初のスキャン オブジェクトは s3 で、s2 を pred1 でフィルタリングした結果です。Department テーブルの各レコードの dname フィールドの内容が異なるため、s3 には 1 レコードのみが含まれます。

ProudctScan コードの実装によれば、Next インターフェイスを呼び出すとき、最初の Scan オブジェクトの Next インターフェイスを 1 回呼び出します。ここでは、このオブジェクトは s1 に対応し、次に 2 番目の Scan オブジェクトの Next インターフェイスを呼び出して、そのすべてのレコードを走査します。ここでは、入力 s3 に対応します。s3 は SelectScan インスタンスであるため、指定されたレコードを返すときにアクセスするブロックの数は、構築時に渡された Scan オブジェクトと等しくなります。コード内の s3 で構築された Scan オブジェクトは次のとおりです。テーブルには 2 つのブロックがあるため、特定のレコードを返すには、s3 はすべてのブロックにアクセスするために受信 TableScan オブジェクトを必要とするため、s3 は Next インターフェイスの実行後に最大 2 ブロックにアクセスする必要があるため、s4 Next インターフェイスを 1 回実行します。構築中に入力された s1 オブジェクトが呼び出されます。これは Student テーブルに対応する TableScan の Next インターフェイスです。テーブルには 45,000 レコードがあるため、s1 の Next は 45,000 回実行できます。Next が実行されるたびに、 , Next of s2 は 1 回呼び出されるため、Department テーブルの 2 つのブロックが走査されるため、s4 の Next 実行では少なくとも 2 * 45000 ブロックにアクセスする必要があります。

しかし、s4 を構築するコードを s4 = query.NewProductScan(s3, s1) に変更すると、s4 は Next インターフェイスを 1 回実行し、s3 の Next() が実行されます。s3 には 1 つのレコードしかないため、その Nextコードは 1 回だけ実行され、その後 s1 の Next インターフェイスが 45,000 回実行されるため、s1 に対応するデータベース テーブルのすべてのブロックが 1 回走査されます。Student テーブルには 4500 のブロックがあるため、s4 の Next インターフェイスのみ4500 ブロックにアクセスします。前の分析と比較して、2 つの入力パラメータの位置を交換するだけで、アクセスされるブロックが大幅に減少し、速度が大幅に向上します。

次のセクションでは、このセクションの理論をコードに実装する方法を見ていきます。詳しくはステーションbのコーディングディズニーで検索してください。

おすすめ

転載: blog.csdn.net/tyler_download/article/details/132698594