SQL Profiles-Part I--老熊
Oracle 11g從釋出到現在,也有幾個年頭了。而在國內來說,Oracle 10g仍然是主流,甚至一些電信運營商的核心繫統仍然在使用9i。作為Oracle 10g的一項新特性,SQL Profiles被使用得並不太多。不管是在論壇、個人的BLOG還是其他一些地方,SQL Profiles的介紹也相對較少。對我個人來說,已經在多個最佳化場合中使用SQL Profiles,在這裡向大家介紹SQL Profiles,就是希望能夠了解Oracle資料庫的這一功能。
SQL Profiles可以說是Outlines的進化。Outlines能夠實現的功能SQL Profiles也完全能夠實現,而SQL Profiles具有Outlines不具備的最佳化,個人認為最重要的有2點:
- SQL Profiles更容易生成、更改和控制。
- SQL Profiles在對SQL語句的支援上做得更好,也就是適用範圍更廣。
關於這2方面的優點,我後面會詳細地闡述。
現在我在使用Outlines的場合,均使用SQL Profiles來替代。有一次準備對1條SQL語句使用Outline進行執行計劃的穩定,結果使用Outline之後,系統出現大量的library cache latch的爭用,不得不關閉Outline的使用,但是改用SQL Profiles不再有這個問題。這或許是個BUG,不過既然能用SQL Profiles代替,也就沒再深入去研究這個問題。
使用SQL Profiles無非是兩個目的:
- 鎖定或者說是穩定執行計劃。
- 在不能修改應用中的SQL的情況下使SQL語句按指定的執行計劃執行。
那麼SQL Profile到底是什麼?在我看來,SQL Profile就是為某一SQL語句提供除了系統統計資訊、物件(表和索引等)統計資訊之外的其他資訊,比如執行環境、額外的更準確的統計資訊,以幫助最佳化器為SQL語句選擇更適合的執行計劃。這些說法顯得比較枯燥,還是來看看下面的測試。
首先建2個測試表:
- SQL> create table t1 as select object_id,object_name from dba_objects where rownum<=50000;
- 表已建立。
- SQL> create table t2 as select * from dba_objects;
- 表已建立。
- SQL> create index t2_idx on t2(object_id);
- 索引已建立。
- SQL> exec dbms_stats.gather_table_stats(user,'t1',cascade=>true,method_opt=>'for all columns size 1');
- PL/SQL 過程已成功完成。
- SQL> exec dbms_stats.gather_table_stats(user,'t2',cascade=>true,method_opt=>'for all columns size 1');
- PL/SQL 過程已成功完成。
然後看看下面這一條SQL:
- SQL> select t1.*,t2.owner from t1,t2 where t1.object_name like '%T1%' and t1.object_id=t2.object_id;
- 已選擇29行。
- 執行計劃
- ----------------------------------------------------------
- Plan hash value: 1838229974
- ---------------------------------------------------------------------------
- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
- ---------------------------------------------------------------------------
- | 0 | SELECT STATEMENT | | 2498 | 99920 | 219 (4)| 00:00:03 |
- |* 1 | HASH JOIN | | 2498 | 99920 | 219 (4)| 00:00:03 |
- |* 2 | TABLE ACCESS FULL| T1 | 2498 | 72442 | 59 (6)| 00:00:01 |
- | 3 | TABLE ACCESS FULL| T2 | 49954 | 536K| 159 (2)| 00:00:02 |
- ---------------------------------------------------------------------------
- Predicate Information (identified by operation id):
- ---------------------------------------------------
- 1 - access("T1"."OBJECT_ID"="T2"."OBJECT_ID")
- 2 - filter("T1"."OBJECT_NAME" LIKE '%T1%')
- 統計資訊
- ----------------------------------------------------------
- 0 recursive calls
- 0 db block gets
- 932 consistent gets
- 0 physical reads
- 0 redo size
- 1352 bytes sent via SQL*Net to client
- 385 bytes received via SQL*Net from client
- 2 SQL*Net roundtrips to/from client
- 0 sorts (memory)
- 0 sorts (disk)
- 29 rows processed
這裡省略了SELECT出來的具體資料,但是我們關心的是返回的結果行數、執行計劃以及邏輯讀這些資訊。
首先從執行計劃可以看到,這條SQL語句在2個表上都是全表掃描。在第1個表T1上,有 like ‘%T1%’這樣的條件,導致只能全表掃描,這沒有問題。但是第2個表,也是全表掃描,這裡有沒有問題呢?或者說是有沒有最佳化的餘地,答案顯然是肯定的。
這裡的問題在於執行計劃ID=1的那一行,Oracle最佳化器評估T1 like ‘%T1%’返回的結果行數為2498行,即T1表總行數的5%,如果2個表採用index range scan+nested loop連線,oracle評估的成本會高於full table scan+hash join。下面可以看到Oracle最佳化器評估的index range_scan+nested loop的成本:
- SQL> explain plan for select /*+ use_nl(t1 t2) index(t2) */ t1.*,t2.owner
- from t1,t2
- where t1.object_name like '%T1%'
- and t1.object_id=t2.object_id;
- 已解釋。
- SQL> @showplan
- PLAN_TABLE_OUTPUT
- --------------------------------------------------------------------------------------
- Plan hash value: 3787413387
- --------------------------------------------------------------------------------------
- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
- --------------------------------------------------------------------------------------
- | 0 | SELECT STATEMENT | | 2498 | 99920 | 5061 (1)| 00:01:01 |
- | 1 | TABLE ACCESS BY INDEX ROWID| T2 | 1 | 11 | 2 (0)| 00:00:01 |
- | 2 | NESTED LOOPS | | 2498 | 99920 | 5061 (1)| 00:01:01 |
- |* 3 | TABLE ACCESS FULL | T1 | 2498 | 72442 | 59 (6)| 00:00:01 |
- |* 4 | INDEX RANGE SCAN | T2_IDX | 1 | | 1 (0)| 00:00:01 |
- --------------------------------------------------------------------------------------
- Predicate Information (identified by operation id):
- ---------------------------------------------------
- 3 - filter("T1"."OBJECT_NAME" LIKE '%T1%')
- 4 - access("T1"."OBJECT_ID"="T2"."OBJECT_ID")
從執行計劃可以看到Oracle最佳化器評估的成本為5061,遠遠高於原來的219。
但是實際的邏輯讀是多少呢?
- 統計資訊
- ----------------------------------------------------------
- 0 recursive calls
- 0 db block gets
- 290 consistent gets
- 0 physical reads
- 0 redo size
- 1352 bytes sent via SQL*Net to client
- 385 bytes received via SQL*Net from client
- 2 SQL*Net roundtrips to/from client
- 0 sorts (memory)
- 0 sorts (disk)
- 29 rows processed
加了HINT之後實際的邏輯讀只有290,低於原始SQL的932。所以這裡可以看出來,由於Oracle最佳化器過高地估計了T1表經過like操作過濾返回的行數,也就過高地估計了nest loop的成本,最終也就選擇了不是最優的執行計劃。
下面我們用Oracle的SQL Tuning Advisor來嘗試這條SQL:
- SQL> var tuning_task varchar2(100);
- SQL> DECLARE
- 2 l_sql_id v$session.prev_sql_id%TYPE;
- 3 l_tuning_task VARCHAR2(30);
- 4 BEGIN
- 5 l_sql_id:='4zbqykx89yc8v';
- 6 l_tuning_task := dbms_sqltune.create_tuning_task(sql_id => l_sql_id);
- 7 :tuning_task:=l_tuning_task;
- 8 dbms_sqltune.execute_tuning_task(l_tuning_task);
- 9 dbms_output.put_line(l_tuning_task);
- 10 END;
- 11 /
- 任務_74
- PL/SQL 過程已成功完成。
- SQL> print tuning_task;
- TUNING_TASK
- ---------------------------------------------------------------------------------------------------------
- 任務_74
- SQL> SELECT dbms_sqltune.report_tuning_task(:tuning_task) FROM dual;
- DBMS_SQLTUNE.REPORT_TUNING_TASK(:TUNING_TASK)
- --------------------------------------------------------------------------------
- GENERAL INFORMATION SECTION
- -------------------------------------------------------------------------------
- Tuning Task Name : 任務_74
- Tuning Task Owner : TEST1
- Scope : COMPREHENSIVE
- Time Limit(seconds) : 1800
- Completion Status : COMPLETED
- Started at : 12/15/2010 09:56:02
- Completed at : 12/15/2010 09:56:03
- Number of SQL Profile Findings : 1
- -------------------------------------------------------------------------------
- Schema Name: TEST1
- SQL ID : 4zbqykx89yc8v
- SQL Text : select t1.*,t2.owner from t1,t2 where t1.object_name like '%T1%'
- and t1.object_id=t2.object_id
- -------------------------------------------------------------------------------
- FINDINGS SECTION (1 finding)
- -------------------------------------------------------------------------------
- 1- SQL Profile Finding (see explain plans section below)
- --------------------------------------------------------
- 為此語句找到了效能
- Recommendation (estimated benefit: 46.62%)
- ------------------------------------------
- -考慮接受推薦的 SQL
- executedbms_sqltune.accept_sql_profile(task_name => '任務_74', replace =
- TRUE);
- -------------------------------------------------------------------------------
- EXPLAIN PLANS SECTION
- -------------------------------------------------------------------------------
- 1- Original With Adjusted Cost
- ------------------------------
- Plan hash value: 1838229974
- ---------------------------------------------------------------------------
- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
- ---------------------------------------------------------------------------
- | 0 | SELECT STATEMENT | | 29 | 1160 | 219 (4)| 00:00:03 |
- |* 1 | HASH JOIN | | 29 | 1160 | 219 (4)| 00:00:03 |
- |* 2 | TABLE ACCESS FULL| T1 | 29 | 841 | 59 (6)| 00:00:01 |
- | 3 | TABLE ACCESS FULL| T2 | 49954 | 536K| 159 (2)| 00:00:02 |
- ---------------------------------------------------------------------------
- Predicate Information (identified by operation id):
- ---------------------------------------------------
- 1 - access("T1"."OBJECT_ID"="T2"."OBJECT_ID")
- 2 - filter("T1"."OBJECT_NAME" LIKE '%T1%')
- 2- Using SQL Profile
- --------------------
- Plan hash value: 3787413387
- --------------------------------------------------------------------------------------
- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
- --------------------------------------------------------------------------------------
- | 0 | SELECT STATEMENT | | 29 | 1160 | 117 (3)| 00:00:02 |
- | 1 | TABLE ACCESS BY INDEX ROWID| T2 | 1 | 11 | 2 (0)| 00:00:01 |
- | 2 | NESTED LOOPS | | 29 | 1160 | 117 (3)| 00:00:02
- |
- |* 3 | TABLE ACCESS FULL | T1 | 29 | 841 | 59 (6)| 00:00:01 |
- |* 4 | INDEX RANGE SCAN | T2_IDX | 1 | | 1 (0)| 00:00:01 |
- --------------------------------------------------------------------------------------
- Predicate Information (identified by operation id):
- ---------------------------------------------------
- 3 - filter("T1"."OBJECT_NAME" LIKE '%T1%')
- 4 - access("T1"."OBJECT_ID"="T2"."OBJECT_ID")
- -------------------------------------------------------------------------------
上面程式碼中的sql_id是從v$sql來,對應的是沒有加hint的SQL。
結果看起來非常棒,SQL Tuning Advisor為我們找到了理想的執行計劃,T1表上經過謂詞過濾後返回的行數評估為29,相當地精確。我們要做的就是Accept SQL Profile,接受這個SQL Profile。
- SQL> execute dbms_sqltune.accept_sql_profile(task_name => :tuning_task,replace => TRUE,force_match=>true);
- PL/SQL 過程已成功完成。
那麼我們再執行其他的類似SQL看看:
- SQL> select t1.*,t2.owner from t1,t2 where t1.object_name like '%T2%' and t1.object_id=t2.object_id;
- 已選擇77行。
- 執行計劃
- ----------------------------------------------------------
- Plan hash value: 3787413387
- --------------------------------------------------------------------------------------
- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
- --------------------------------------------------------------------------------------
- | 0 | SELECT STATEMENT | | 29 | 1160 | 117 (3)| 00:00:02 |
- | 1 | TABLE ACCESS BY INDEX ROWID| T2 | 1 | 11 | 2 (0)| 00:00:01 |
- | 2 | NESTED LOOPS | | 29 | 1160 | 117 (3)| 00:00:02 |
- |* 3 | TABLE ACCESS FULL | T1 | 29 | 841 | 59 (6)| 00:00:01 |
- |* 4 | INDEX RANGE SCAN | T2_IDX | 1 | | 1 (0)| 00:00:01 |
- --------------------------------------------------------------------------------------
- Predicate Information (identified by operation id):
- ---------------------------------------------------
- 3 - filter("T1"."OBJECT_NAME" LIKE '%T2%')
- 4 - access("T1"."OBJECT_ID"="T2"."OBJECT_ID")
- Note
- -----
- - SQL profile "SYS_SQLPROF_014b39f084c88000" used for this statement
- 統計資訊
- ----------------------------------------------------------
- 1 recursive calls
- 0 db block gets
- 343 consistent gets
- 0 physical reads
- 0 redo size
- 2840 bytes sent via SQL*Net to client
- 385 bytes received via SQL*Net from client
- 2 SQL*Net roundtrips to/from client
- 0 sorts (memory)
- 0 sorts (disk)
- 77 rows processed
這一次,儘管我們更改了LIKE 後面的值,但是執行計劃與SQL Tuning Advisor產生的執行計劃完全一樣。從執行計劃的”Note“一節也可以看到,SQL Profile起作用了。SQL Profile的名字為”SYS_SQLPROF_014b39f084c88000″。
- SQL> select name,category,signature,type,status,force_matching from dba_sql_profiles;
- NAME CATEGORY SIGNATURE TYPE STATUS FOR
- ------------------------------ ------------------------------ --------------------- --------- ---------- ---
- SYS_SQLPROF_014b39f084c88000 DEFAULT 3960696072677096522 MANUAL ENABLED YES
一些複雜的SQL,我經常會先透過SQL Tuning Advisor來分析一下,看能不能讓Oracle自已找出一個更好的執行計劃。
我們來看看,SQL Profiles實際上是些什麼:
- SQLselect * from sys.sqlprof$attr;
- SIGNATURE CATEGORY ATTR# ATTR_VAL
- --------------------- ------------------------------ ---------- ----------------------------------------
- 3960696072677096522 DEFAULT 1 OPT_ESTIMATE(@"SEL$1", TABLE, "T1"@"SEL$
- 1", SCALE_ROWS=0.01161091426)
從sys.sqlprof$attr這個數字字典裡面,我們可以看到兩樣東西:signature和attr。
signature是什麼?可以理解為與sql_id、sql_hash_value類似的值,用來標識SQL。在10g以上的版本中,檢視v$sql的定義就可以發現2列:exact_matching_signature、force_matching_signature。透過下面的資料可以看出區別:
- SQL> select rownum,a.* from
- (select exact_matching_signature,force_matching_signature,plan_hash_value,sql_text
- from v$sql where sql_text like '%/*%xjs%' and sql_text not like '%v$sql%' order by 1) a;
- ROWNUM EXACT_MATCHING_SIGNATURE FORCE_MATCHING_SIGNATURE PLAN_HASH_VALUE SQL_TEXT
- ---------- ------------------------ ------------------------ --------------- --------------------------------------------------
- 1 3939730931515200254 17443893418101517951 3617692013 select /* xjs */ object_name from T1 where obje
- ct_name='t1'
- 2 10964210455693560558 11097449316038436385 3836375644 select /* xjs */ object_name from T1 where rown
- um<=3
- 3 10964210455693560558 11097449316038436385 3836375644 select /* xjs */ object_name from T1 where ro
- wnum<=3
- 4 11217690300719901571 354482119692997204 3836375644 select /* xjs */ 2 from t1 where rownum<=1
- 5 11974975582747367847 354482119692997204 3836375644 select /* xjs */ 1 from t1 where rownum<=1
- 6 12941882703651921406 17443893418101517951 3617692013 select /* xjs */ object_name from T1 where obje
- ct_name='T1'
- 7 17986178357953662359 11097449316038436385 3836375644 select /* xjs */ object_name from T1 where rown
- um<=1
- 8 17986178357953662359 11097449316038436385 3836375644 select /* xjs */ OBJECT_NAME from T1 where rownum
- =1
- 9 17986178357953662359 11097449316038436385 3836375644 SELECT /* xjs */ object_name from T1 where rown
- um<=1
- 10 17986178357953662359 11097449316038436385 3836375644 select /* xjs */ object_name from t1 where rownum
- =1
從上面的資料可以看出:
- 第2、3條SQL的exact_matching_signature相同,第7、8、9、10條SQL的exact_matching_signature相同。
- 第2、3條SQL的force_matching_signature相同,第4、5條SQL的force_matching_signature相同,第7、8、9、10條的SQL的force_matching_signature相同。第1、6條SQL的force_matching_signature相同
有如下的結論:對SQL語句,去掉重複的空格(不包括字元常量),將大小寫轉換成相同,比如均為大寫(不包括字元常量)後,如果SQL相同,那麼SQL語句的exact_matching_signature就是相同的。對SQL語句,去掉重複的空格(不包括字元常量),將大小寫轉換成相同,比如均為大寫(不包括字元常量),然後去掉SQL中的常量,如果SQL相同,那麼SQL語句的force_matching_signature就是相同的。但是例外的情況是:如果SQL中有繫結變數,force_matching_signature就會與exact_matching_signature一樣的生成標準。
- SQL> select rownum,a.* from
- (select exact_matching_signature,force_matching_signature,plan_hash_value,sql_text
- from v$sql where sql_text like '%/*%xjs2%' and sql_text not like '%v$sql%' order by 1) a;
- ROWNUM EXACT_MATCHING_SIGNATURE FORCE_MATCHING_SIGNATURE PLAN_HASH_VALUE SQL_TEXT
- ---------- ------------------------ ------------------------ --------------- --------------------------------------------------
- 1 5363536431331905229 5363536431331905229 3836375644 select /* xjs2 */ object_name from T1 where obj
- ect_name='T1' and rownum<=:rn
- 2 5363536431331905229 5363536431331905229 3836375644 select /* xjs2 */ object_name from t1 where obj
- ect_name='T1' and rownum<=:rn
- 3 12992689086515482106 12992689086515482106 3836375644 select /* xjs2 */ object_name from t1 where obj
- ect_name='T2' and rownum<=:rn
可以看到,現在exact_matching_signature與force_matching_signature完全一樣了。
從force_matching_signature的特性,我們可以想到一個用途,用於查詢沒有使用繫結變數的SQL語句,類似於使用plan_hash_value來查詢。
回到前面,accept_sql_profile這個過程,force_match引數設為TRUE,那麼dba_sql_profiles中的signature則是由SQL的force_matching_signature而來,否則便是exact_matching_signature。對於Outlines來說,則只能是exact_matching_signature。從這個角度上講,Sql Profiles比Outlines的使用範圍更廣,因為Sql profiles對沒有使用繫結變數的SQL也支援得很好。值得注意的是,Sql profiles的force_match屬性是不能更改的,只能在建立時指定,如果要更改,則只能重新建立改Sql Profile。
下面來看看sys.sqlprof$attr資料字典。這裡面沒有SQL Profile的名字,而是用的sql的signature。大家從attr_val的結果發現了什麼?
- OPT_ESTIMATE(@"SEL$1", TABLE, "T1"@"SEL$1", SCALE_ROWS=0.01161091426)
可以看到,SQL Profiles的attr_val實際上就是一些Hints,這跟Outlines沒有本質上的區別。只是SQL Profiles中的Hint,沒有指定SQL使用哪個索引,也沒有指定表的連線方法和連線順序。這裡只指定了T1表評估返回的行數,與原始的評估返回的行數的放大縮小的倍數。2498*0.01161091426正好為29。這裡就是告訴Oracle最佳化器,T1表經過謂語過濾後返回行數應該為評估的0.01161091426倍。從這裡可以看出,SQL Profiles並不會鎖定SQL的執行計劃,只是提供了更多、更準確的統計資訊給最佳化器。看下面的測試:
- SQL> exec dbms_stats.set_table_stats('TEST1','T1',numrows=>5000000);
- PL/SQL 過程已成功完成。
- SQL> explain plan for select t1.*,t2.owner
- 2 from t1,t2
- 3 where t1.object_name like '%T1%'
- 4 and t1.object_id=t2.object_id;
- 已解釋。
- SQL> @showplan
- PLAN_TABLE_OUTPUT
- ----------------------------------------------------------------------------------
- Plan hash value: 1838229974
- ---------------------------------------------------------------------------
- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
- ---------------------------------------------------------------------------
- | 0 | SELECT STATEMENT | | 2903 | 113K| 448 (53)| 00:00:06 |
- |* 1 | HASH JOIN | | 2903 | 113K| 448 (53)| 00:00:06 |
- |* 2 | TABLE ACCESS FULL| T1 | 2903 | 84187 | 288 (81)| 00:00:04 |
- | 3 | TABLE ACCESS FULL| T2 | 49954 | 536K| 159 (2)| 00:00:02 |
- ---------------------------------------------------------------------------
- Predicate Information (identified by operation id):
- ---------------------------------------------------
- 1 - access("T1"."OBJECT_ID"="T2"."OBJECT_ID")
- 2 - filter("T1"."OBJECT_NAME" LIKE '%T1%')
- Note
- -----
- - SQL profile "SYS_SQLPROF_014b39f084c88000" used for this statement
將T1表的統計資訊中的錶行數改為500萬,Oracle就會評估為返回5000000*5%*0.01161091426=2903行。這裡執行計劃又變回為full scan+hash join。可以看到,雖然SQL Profile起作用了,但是並沒有鎖定執行計劃。
小結:本文簡單介紹了什麼是SQL Profiles及其作用,如何使用SQL Tuning Advisor來生成SQL Profile,以及生成的SQL Profile產生的Hint。同時也介紹了SQL的signature。
下一篇將會介紹如何手工來為建立、生成SQL Profile,以及如何讓SQL Profile也能像Outlines一樣鎖定SQL的執行計劃,以保持SQL執行計劃的穩定性。
-->>轉載於:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/29119536/viewspace-1242415/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 瓦解邪惡“熊心豹膽”,360安全大腦獨家披露“老豹”放毒之路
- PostgreSQL 從熊燦燦一個獲取固定字元的SQL 分析巧妙之處SQL字元
- 熊貓直播倒了!
- 現在的熊孩子
- 前後端不分離 "老" 專案,SQL 注入漏洞處理實踐後端SQL
- 某熊的技術之路指北 ☯
- 萌熊6月j講題
- Browns lazy shuffle (布朗熊爬動屏保)
- 年末重磅!【位元熊充電棧】Launch
- 老樹常青!SQL為何成為程式設計界的不敗傳奇?SQL程式設計
- CSS3大熊貓卡通效果CSSS3
- 【瞎寫】熊是什麼顏色的?
- polarbear北極熊高畫質動態桌布
- 位元熊故事匯獨家 | .NET 感恩專場
- 【位元熊故事匯】12月MVP英雄故事 ——2021年末特別企劃,位元熊與你溫暖相約MVP
- css3繪製百度度熊CSSS3
- 熊孩子們走了,《我的世界》又變酷了
- 遊戲防沉迷系統如何智鬥“熊孩子”遊戲
- 夢熊 NOIP 十三連測模擬賽記錄
- 2023~某熊的成長之路:擁抱更大的世界
- APT Group系列:來自北極熊的威脅——TurlaAPT
- 熊逸:我一以貫之的讀書方法論
- 2024夢熊BeiJing集訓題目題解目錄
- 位元熊故事匯2.0 | 迎風起,去看新風景
- 【SQL】17 SQL 檢視(Views)、SQL Date 函式、SQL NULL 值、SQLView函式Null
- 通達信建倉牛熊爭霸副圖原始碼原始碼
- 熊孩子狂氪10多萬!父母該如何追回“損失”?
- 2018-某熊的技術之路: 做些有趣的產品
- 【位元熊充電棧】Azure OpenAI 特輯,滿電待充OpenAI
- 熊孩子為了玩遊戲有多拼,只有客服知道遊戲
- 知足的人不易老
- 【SQL】19 SQL函式SQL函式
- SQL注射/SQL Injection漏洞SQL
- SQL------SQL效能分析SQL
- 智慧養老導航:開啟便捷養老新篇章
- 為什麼一提到不婚族養老就會是養老院,居家養老不行嗎?
- 【SQL】13 SQL 別名、SQL 連線(JOIN)、SQL INNER JOIN 關鍵字、SQL LEFT JOIN 關鍵字、SQL RIGHT JOIN 關鍵字、SQL FULL OUTER JSQL
- 多利熊基於分散式架構實踐穩定性建設分散式架構
- 【MX-S3】夢熊周賽 · 提高組 3 & FeOI Round 1S3