MySQL: My table size using a query does not match the IBD's file size on disk
Of course, it never does.
In your case, you have about 66.67% fragmentation
I have wickedly good script you can run to calculate the fragmentation based on the difference between the .ibd filesize and the (data_length+index_length) of the any table:
MYSQL_AUTH=&quot;-h... -u... -p...&quot;

DATADIR=`mysql -ANe&quot;SELECT @@global.datadir&quot;`

TAG_LIST_FILE=/tmp/list_of_tables.txt
TAG_LIST_ERRS=/tmp/list_of_tables.err
DAT_LIST_FILE=/tmp/list_of_databases.txt
FULL_RPT_FILE=Full_FragReport.txt

MB=${1}

if [ &quot;${MB}&quot; == &quot;&quot; ] ; then exit ; fi

NUM_OK=0
if [ &quot;${MB}&quot; ==  &quot;128&quot; ] ; then NUM_OK=1 ; fi
if [ &quot;${MB}&quot; ==  &quot;256&quot; ] ; then NUM_OK=1 ; fi
if [ &quot;${MB}&quot; ==  &quot;512&quot; ] ; then NUM_OK=1 ; fi
if [ &quot;${MB}&quot; == &quot;1024&quot; ] ; then NUM_OK=1 ; fi
if [ &quot;${MB}&quot; == &quot;2048&quot; ] ; then NUM_OK=1 ; fi
if [ ${NUM_OK} -eq 0 ] ; then exit ; fi

MINIMUM_SIZE=&quot;(1024 * 1024 * ${MB})&quot;
(( MIN_SIZE = MB * 1048576 ))

SQL=&quot;SELECT CONCAT(table_schema,':',table_name,':',data_length+index_length,':',data_free) FROM information_schema.tables&quot;
SQL=&quot;${SQL} WHERE table_schema NOT IN ('information_schema','performance_schema','mysql') AND engine IN ('InnoDB','MyISAM')&quot;
SQL=&quot;${SQL} AND data_length+index_length &gt; ${MIN_SIZE}&quot;
mysql ${MYSQL_AUTH} -ANe&quot;${SQL}&quot; &gt;${TAG_LIST_FILE} 2&gt;${TAG_LIST_ERRS}
TAG_FILESIZE=`ls -l ${TAG_LIST_FILE} | awk '{print $5}'`
ERR_FILESIZE=`ls -l ${TAG_LIST_ERRS} | awk '{print $5}'`
if [ ${ERR_FILESIZE} -gt 0 ]
then
        echo &quot;Errors&quot;
        echo &quot;======&quot;
        cat ${ERR_FILESIZE}
        echo
        exit
fi
if [ ${TAG_FILESIZE} -eq 0 ]
then
        echo &quot;No Databases with Tables &gt; ${MB}M (${MIN_SIZE})&quot;
        exit
fi

SPC=&quot;&quot;
TAGS=&quot;&quot;
for TAG in `cat ${TAG_LIST_FILE}`
do
        NEWTAG=${TAG}
        DB=`echo ${TAG} | sed 's/:/ /g' | awk '{print $1}'`
        TB=`echo ${TAG} | sed 's/:/ /g' | awk '{print $2}'`
        IS=`echo ${TAG} | sed 's/:/ /g' | awk '{print $3}'`
        FR=`echo ${TAG} | sed 's/:/ /g' | awk '{print $4}'`
        IBD=${DATADIR}/${DB}/${TB}.ibd
        MYD=${DATADIR}/${DB}/${TB}.MYD
        MYI=${DATADIR}/${DB}/${TB}.MYI
        if [ -f ${IBD} ]
        then
                (( N1 = IS + FR ))
                N2=`ls -l ${IBD} | awk '{print $5}'`
                if [ ${N1} -lt ${N2} ]
                then
                        (( DF = N2 - N1 ))
                        NEWTAG=&quot;${DB}:${TB}:${IS}:${DF}&quot;
                fi
        else
                ND=`ls -l ${MYD} | awk '{print $5}'`
                NI=`ls -l ${MYI} | awk '{print $5}'`
                (( N1 = IS + FR ))
                (( N2 = ND + NI ))
                if [ ${N1} -lt ${N2} ]
                then
                        (( DF = N2 - N1 ))
                        NEWTAG=&quot;${DB}:${TB}:${IS}:${DF}&quot;
                fi
        fi
        TAGS=&quot;${TAGS}${SPC}${NEWTAG}&quot;
        SPC=&quot; &quot;
done

# Collect List of DBs
DBLIST=&quot;&quot;
SPC=&quot;&quot;
for DB in `cat ${TAG_LIST_FILE} | sed 's/:/ /' | awk '{print $1}' | sort -u`
do
        DBLIST=&quot;${DBLIST}${SPC}${DB}&quot;
        SPC=&quot; &quot;
done

for D2 in ${DBLIST}
do
        SUM=0
        SUM_DF=0
        SQL=&quot;&quot;
        SELECT=&quot;SELECT&quot;
        for TAG in ${TAGS}
        do
                DB=`echo ${TAG} | sed 's/:/ /g' | awk '{print $1}'`
                if [ &quot;${DB}&quot; == &quot;${D2}&quot; ]
                then
                        TB=`echo ${TAG} | sed 's/:/ /g' | awk '{print $2}'`
                        IS=`echo ${TAG} | sed 's/:/ /g' | awk '{print $3}'`
                        FR=`echo ${TAG} | sed 's/:/ /g' | awk '{print $4}'`
                        (( OS = IS + FR ))
                        SQL=&quot;${SQL} ${SELECT} '${TB}' TableName,LPAD(FORMAT(${OS},0),18,' ') FileSize,LPAD(FORMAT(${OS}/POWER(1024,3),2),10,' ') FileSizeGB,LPAD(FORMAT(${IS},0),18,' ') TableSize,LPAD(FORMAT(${IS}/POWER(1024,3),2),11,' ') TableSizeGB,LPAD(FORMAT(${FR},0),18,' ') Difference,LPAD(FORMAT(${FR}/POWER(1024,3),2),12,' ') DifferenceGB,LPAD(FORMAT(${FR}*100.0/${OS},2),7,' ') FragPct&quot;
                        SELECT=&quot;UNION SELECT&quot;
                        (( SUM    += OS ))
                        (( SUM_DF += FR ))
                fi
        done
        FRAG_RPT=FragReport_${D2}.txt
        DT=`date +&quot;%Y-%m-%d %H:%M:%S&quot;`
        echo &quot;Fragmentation for Database ${D2} (${DT})&quot; &gt; ${FRAG_RPT}
        echo &quot;Minimum Table Size (${MB} MB = ${MIN_SIZE})&quot; &gt;&gt; ${FRAG_RPT}
        echo &gt;&gt; ${FRAG_RPT}
        BIGSQL=&quot;SELECT * FROM (${SQL}) A ORDER BY Difference DESC;&quot;
        BIGSQL=&quot;${BIGSQL} SELECT FORMAT(${SUM}/POWER(1024,3),2) DiskSpaceUsed,FORMAT(${SUM_DF}/POWER(1024,3),2) SumDiff;&quot;
        mysql ${MYSQL_AUTH} --table -Ae&quot;${BIGSQL}&quot; 2&gt;&amp;1 &gt;&gt; ${FRAG_RPT}
done

rm -f ${FULL_RPT_FILE}
for TXT in `ls FragReport*.txt`
do
        if [ -f ${FULL_RPT_FILE} ]
        then
                echo -e &quot;\n================================================================================\n&quot; &gt;&gt; ${FULL_RPT_FILE}
        fi
        cat ${TXT} &gt;&gt; ${FULL_RPT_FILE}
done
less ${FULL_RPT_FILE}

You will have too run this script as the OS root user or another OS that has rights to read the datadir
GIVE IT A TRY !!!
I wrote How to monitor mysql percentage disk utilization? like 6 years ago based on the same principles