#MySQL#GREATEST#LEAST

規則


  • 如果有任一參數是 NULL,結果為 NULL。
  • 如果所有參數都是整數,則以整數方式比較。
  • 如果至少有一個參數是 Double,則以 Double 比較;否則,如果至少有一個是 DECIMAL,則以 DECIMAL 比較。
  • 如果參數是數字與字串混合,則以字串方式比較。
  • 如果有任何參數是非二進位(nonbinary)字串,則以非二進位字串方式比較。
  • 其他情況,則以二進位字串方式比較。

Example


GREATEST(value1,value2,…)

mysql> SELECT GREATEST(2,0);
        -> 2
mysql> SELECT GREATEST(34.0,3.0,5.0,767.0);
        -> 767.0
mysql> SELECT GREATEST('B','A','C');
        -> 'C'

LEAST(value1,value2,…)

mysql> SELECT LEAST(2,0);
        -> 0
mysql> SELECT LEAST(34.0,3.0,5.0,767.0);
        -> 3.0
mysql> SELECT LEAST('B','A','C');
        -> 'A'

Laravel DB Listen


#PHP#Laravel#MySQL

Laravel ORM 很好用讓我們不用寫 SQL,但常常會需要看自己的 SQL 有無效能問題跟符合預期,會在 Builder 使用 toSql()dd(),Laravel 10 後的版本還有新增直接幫你把 bindings 帶入的 toRawSql() 和 ddRawSql() 讓開發者更方便觀看 ORM 的 SQL。

toSql()


User::query()->where('email', 'MTsung103035@gmail.com')->toSql();
// select * from `users` where `email` = ?

dd()


User::query()->where('email', 'MTsung103035@gmail.com')->dd();
// select * from `users` where `email` = ?
// ['MTsung103035@gmail.com']

toRawSql()


User::query()->where('email', 'MTsung103035@gmail.com')->toRawSql();
// select * from `users` where `email` = 'MTsung103035@gmail.com'

ddRawSql()


User::query()->where('email', 'MTsung103035@gmail.com')->ddRawSql();
// select * from `users` where `email` = 'MTsung103035@gmail.com'

但每次都要一個一個 toRawSql() 實在有點不方便,所以 Laravel 有提供 DB::listen 用來聆聽所有 DB 操作的 function

use Illuminate\Support\Facades\DB;

DB::listen(function ($query) {
    // $query->sql;
    // $query->bindings;
    // $query->time;
    // $query->toRawSql();
});

有了這個方法後就能加到 AppServiceProvider 讓每個 request 的 SQL 都紀錄起來,記得設定 env QUERY_LOG 只在開發環境中紀錄。

<?php
 
namespace App\Providers;
 
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;
 
class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        // ...
    }
 
    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        if (env('QUERY_LOG', false)) {
            DB::listen(function ($query) {
                Log::channel('sql-query')->debug('', [
                    $query->toRawSql(),
                    $query->time . 'ms',
                ]);
            });
        }
    }
}

Log


[2025-03-28 08:07:07] develop.DEBUG:  ["select count(*) as aggregate from `member`","384.33ms"]
[2025-03-28 08:07:07] develop.DEBUG:  ["select * from `member` order by `id` desc limit 5 offset 0","81.03ms"]
[2025-03-28 08:07:07] develop.DEBUG:  ["select * from `member_creditcard` where `member_creditcard`.`member_id` in (1, 2, 3, 4, 5) and `member_creditcard`.`deleted_at` is null order by `is_default` desc, `id` desc","81.01ms"]
[2025-03-28 08:07:07] develop.DEBUG:  ["select `laravel_table`.*, @laravel_row := if(@laravel_group = `member_id`, @laravel_row + 1, 1) as `laravel_row`, @laravel_group := `member_id` from (select @laravel_row := 0, @laravel_group := 0) as `laravel_vars`, (select * from `task` where `task`.`member_id` in (1, 2, 3, 4, 5) order by `task`.`member_id` asc, `id` desc) as `laravel_table` having `laravel_row` <= 1 order by `laravel_row`","83.59ms"]


#PHP#Laravel#MySQL#Deferred Join

在 MySQL 中當資料表筆數過多時,使用 limit N offset M 查詢頁面越後面越久,因為 offset 會把 M 筆資料全都抓出來後再丟棄,造成 I/O 的資源浪費。

SELECT id, name FROM users ORDER BY id LIMIT 10 OFFSET 0 -- 很快
SELECT id, name FROM users ORDER BY id LIMIT 10 OFFSET 10000000 -- 超慢,會撈取 10000010 筆資料再丟棄前面的 10000000

解決方法有兩種,取決於需不需要跳頁(頁碼),如果服務是無限滾動的分頁方式,可以參考 Laravel 的 Cursor Pagination,他會使用主鍵(Primary Key)透過 Index 搜尋加速查詢。

SELECT id, name FROM users id > 10000000 ORDER BY id ASC LIMIT 10

若需要頁碼的話也是需要索引(Index)的幫助,先撈出需要的 id 再透過 WHERE IN 方式撈取。

SELECT id FROM users ORDER BY id LIMIT 10 OFFSET 10000000 -- 很快,因為只有用到 PK,不需回表查詢(Lookup)
SELECT id, name FROM users id IN (
	SELECT id FROM users ORDER BY id LIMIT 10 OFFSET 10000000
)

但這麼做會有可能因為 MySQL 設定出現

#1235 - This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'

解決方法有兩種

1. 拆成兩個 Query,先將 ids 取出後再 WHERE IN 

SELECT id FROM users ORDER BY id LIMIT 10 OFFSET 10000000;

SELECT id, name FROM users WHERE id IN (10000001, 10000002, 10000003, 10000004, 10000005, 10000006, 10000007, 10000008, 10000009, 10000010);
<?php 

// Simple Paginate
$ids = collect(Users::query()->select('id')->orderBy('id')->simplePaginate(10)->items())->pluck('id');

// Custom Offset and Limit
$ids = Users::query()->select('id')->orderBy('id')->offset(10000000)->limit(10)->get();

$data = Users::query()->select(['id', 'name'])->whereIn('id', $ids)->get();

2. Deferred Join - 自己 JOIN 自己

SELECT users.id, users.name FROM (
	SELECT id FROM users ORDER BY id LIMIT 10 OFFSET 10000000
) AS index_users
JOIN users ON users.id = index_users.id
<?php 

$subQuery = User::query()->select('id')->orderBy('id')->offset(10000000)->limit(10);

$data = User::query()
	->select('users.id', 'users.name')
	->rightJoinSub($subQuery, 'index_users', function ($join) {
        $join->on('users.id', '=', 'index_users.id');
    })
    ->get();

當然這方式還是有極限,真的還是很慢可能就要考慮分片(Sharding)了。



#SQL Injection#資安#PHP#MySQL

注碼攻擊 (SQL Injection) 是 PHP 初學者程式碼中常出現的漏洞,就算有使用框架的 ORM 還是很可能會在 Table Column Name 的地方用 Request Parameter 組字串造成 PDO prepare 做白工。

這篇文章著重在攻擊者會如何玩你的系統,在有限的條件下要想辦法取得資料,一步一步把你資料庫中的資料慢慢搬走。

產生範例


請 ChatGPT 寫一個範例,我直覺他會寫出有漏洞的程式,果不其然有一個地方可以利用,以下是我詠唱的指令

使用 PHP 寫登入會員,登入後有最新消息列表跟內容頁面可以看
裡面有三個預設會員,密碼你設定後跟我說;裡面預設有五篇文章
最後會產生一個 PHP 檔案與一個 SQL 檔案
使用繁體中文回答

MySQL 匯入匯出資料庫指令


#MySQL#備份資料庫

雖然一些 MySQL GUI 管理工具很好用,但通常只適用微量的資料,資料一多就很可能發生匯入匯出到一半失敗的窘境,還是使用 Command Line 執行匯入匯出動作比較好。

file.sql 匯入名為 database_name 的資料庫

mysql -u username -p database_name < file.sql

將名為 database_name 的資料庫匯出到 file.sql

mysqldump -u username -p database_name > file.sql

若不是本機可加上 -h 設定 hostname