php.net manual's session_set_save_handler example is file based. I tried to implement it for mysql.
I work with uniserver in windows. My setting for error displaying is development environment.
my issue is I couldn't make garbage collection function work automatically. I got no error/warning. all my related codes are below.
What I mean by automatically is: if I use $handler->gc($maxlifetime); in index.php after my session testing, it works and deletes expired rows.
If I don't use $handler->gc($maxlifetime); in index.php, expired rows remain.
my questions
q1 - should i use $handler->gc($maxlifetime); in index.php?
q2 - if answer to q1 is YES, why callback function has a gc function. 
q3 - if answer to q1 is NO, why my expired rows are not deleted (in case that I don't have $handler->gc($maxlifetime); in index.php) after I close browser (ie11), refresh the index.php and check my mysql sessions table? Please notice that $maxlifetime is 1 (second).
thanks, best regards
my index.php
// populate inclusion paths
...
// mysql PDO connect
require_once("config.php");
$dbh = new PDO("mysql:host=$my_host;dbname=$my_db;charset=utf8", $my_username, $my_password);
// start session
require_once("class_Session.php");
$maxlifetime = 1; // seconds
$handler = new MySessionHandler($dbh, $maxlifetime);
session_set_save_handler(
array($handler, 'open'),
array($handler, 'close'),
array($handler, 'read'),
array($handler, 'write'),
array($handler, 'destroy'),
array($handler, 'gc')
);
// the following prevents unexpected effects when using objects as save handlers
register_shutdown_function('session_write_close');
session_start();
$_SESSION['user'] = 'joe';
if (isset($_SESSION['user'])) { echo $_SESSION['user']; }
// note if I try code below, expired rows are deleted
// $handler->gc($maxlifetime);
// some other irrelevant-to-this case codes
...
// close mysql connection
$dbh = NULL;
MySessionHandler class
class MySessionHandler {
    private $_db;
    private $maxlifetime;
    function __construct(PDO $dbh, $maxlifetime) 
    {
        $this->_db = $dbh;
        $this->maxlifetime = $maxlifetime;
        return (TRUE);
    }
    function open()
    {       
    }
    function close()
    {
        $this->_db = NULL;
        return (TRUE);
    }
    function read($id)
    {
        $query = "SELECT `data` FROM `sessions` WHERE id = ? LIMIT 1";
        $this->read_stmt = $this->_db->prepare($query);
        $this->read_stmt->bindParam(1, $id, PDO::PARAM_STR);
        $this->read_stmt->execute();
        $data = $this->read_stmt->fetchColumn();
        return $data;
    }
    function write($id, $data)
    {
        $query = "REPLACE `sessions` (`id`, `data`) VALUES(?,?)";
        $this->w_stmt = $this->_db->prepare($query);
        $this->w_stmt->bindParam(1, $id, PDO::PARAM_STR);
        $this->w_stmt->bindParam(2, $data, PDO::PARAM_STR);
        $this->w_stmt->execute();
        return (TRUE);
    }
    function destroy($id)
    {
        $query = "DELETE FROM `sessions` WHERE id = ?";
        $this->delete_stmt = $this->_db->prepare($query);
        $this->delete_stmt->bindParam(1, $id, PDO::PARAM_STR);
        $this->delete_stmt->execute();
        return (TRUE);
    }
    function gc($maxlifetime)
    {
        // instead of CURRENT_TIMESTAMP(), I also tried NOW() command
        $query = "DELETE FROM `sessions` WHERE `update_time` < CURRENT_TIMESTAMP() - INTERVAL ? SECOND";
        $this->gc_stmt = $this->_db->prepare($query);
        $this->gc_stmt->bindParam(1, $this->maxlifetime, PDO::PARAM_STR);
        $this->gc_stmt->execute();
        return (TRUE);
    }
}
related SQL for sessions table
CREATE TABLE `sessions`
(
    `id` CHAR(32) NOT NULL,
    `data` BLOB,
    `update_time` TIMESTAMP NOT NULL,
    PRIMARY KEY (id),
    INDEX (update_time)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;
