Skip to main content

audits_questions

One row per question copied from the audit's template into a specific audit instance. Questions sit inside categories (audits_category); answers to these questions live in audits_answer.

Source Mapping

CouchDB document type: an "audit" document — rows are produced by asyncconvert.py's audits_questions(obj) handler iterating obj.questions[*].questions[*] (the per-category question lists). The handler skips when obj.deleted != 'null'.

ColumnCouchDB sourceNotes
categoryidcomputed (hash)hash(question.categoryName) — same as audits_category.categoryid for the same category name.
questionidcomputed (hash)hash(categoryid + ":" + question_text). Same category + same question text → same questionid across audits. See Pitfalls.
questionobj.questions[i].questions[j].questionThe question text shown to the auditor. Free text.
descriptionobj.questions[i].questions[j].descriptionOptional helper text below the question. Free text.
answertypeobj.questions[i].questions[j].settings.answertypeThe expected answer type (e.g. 'multiplechoice', 'text', 'number'). Free text — no closed enum is enforced at the storage layer.
auditobj.idThe audits.id this question belongs to.
projectidobj.databaseThe project this audit belongs to.
couchdbidobj.couchDbIdLegacy CouchDB document identifier (the audit's).

Column Reference

ColumnTypeNullableDescription
categoryidVARCHAR(255)yesComputed hash(categoryName). Joins to audits_category.categoryid — but always also filter on audit (see Pitfalls).
questionidVARCHAR(255)yesComputed hash(categoryid + ":" + question_text). Same text → same id across audits.
questionTEXTyesThe question text shown to the auditor.
descriptionTEXTyesOptional helper text below the question.
answertypeVARCHAR(255)yesExpected answer type — free text from source (e.g. 'multiplechoice', 'text', 'number').
auditVARCHAR(255)yesForeign-key reference to audits.id.
projectidVARCHAR(255)yesThe project this audit belongs to.
couchdbidVARCHAR(255)yesLegacy CouchDB identifier of the audit document.

Keys

  • No primary key, no unique constraint. Identity within an audit is the composite (audit, categoryid, questionid); enforced procedurally by the converter (delete-then-reinsert per audit document).

Indexes

  • idx_categoryid on categoryid
  • idx_project on projectid
  • idx_parent on audit

There is no index on questionid at this table — but the parallel table audit_answers_tickets has one on questionID. Cross-table joins on questionid will scan audits_questions if not pre-filtered by audit or projectid; both indexes exist for that purpose.

Relationships

DirectionOther tableJoinCardinality
outgoingauditsaudits_questions.audit = audits.idmany questions → one audit
outgoingaudits_categoryaudits_questions.audit = audits_category.audit AND audits_questions.categoryid = audits_category.categoryidmany questions → one category
outgoingprojectsaudits_questions.projectid = projects.idmany rows → one project
incomingaudits_answeraudits_answer.audit = audits_questions.audit AND audits_answer.questionid = audits_questions.questionidone question → zero or more answers
incomingaudit_answers_ticketsaudit_answers_tickets.audit = audits_questions.audit AND audit_answers_tickets.questionID = audits_questions.questionidone question → zero or more follow-up tickets

None of these are foreign-keyed in MariaDB; joins are by string match.

Refresh Semantics

audits_questions is rebuilt per audit document. On each observation of an audit, the converter:

  1. Issues DELETE FROM audits_questions WHERE audit = <audit_id> (when delete tracking is enabled).
  2. Iterates obj.questions[*].questions[*] and inserts one row per question.

Reading during an in-flight reindex of an audit may briefly return zero rows for that audit.

A reindex with type=audit (or a full reindex) rebuilds this table as part of the audit reindex.

Common Queries

Questions in an audit, grouped by category

SELECT
ac.categoryname,
aq.question,
aq.answertype
FROM audits_questions aq
JOIN audits_category ac
ON ac.audit = aq.audit
AND ac.categoryid = aq.categoryid
WHERE aq.audit = 'your_audit_id'
ORDER BY ac.categoryname, aq.question;

Question count per audit

SELECT
audit,
COUNT(*) AS question_count
FROM audits_questions
WHERE projectid = 'your_project_id'
GROUP BY audit
ORDER BY question_count DESC;

Most common questions across a project

SELECT
question,
COUNT(DISTINCT audit) AS audits_using_question
FROM audits_questions
WHERE projectid = 'your_project_id'
GROUP BY question
ORDER BY audits_using_question DESC
LIMIT 50;

This intentionally groups by free-text question rather than by questionid — equivalent here, since questionid is a hash of the text plus category, so duplicates collapse identically.

Cross-table queries (completion %, tickets created from audit findings) live on the Audits domain landing.

Pitfalls

  • questionID collision across audits. questionid is hash(categoryid + ":" + question_text). Two audits that share an identical category-name and question-text pair will have the same questionid. Joining audits_questions ↔ audits_answer (or ↔ audit_answers_tickets) on questionid alone collapses rows across unrelated audits. Always also filter or join on audit or projectid. See the domain landing's deep dive.
  • No primary key. Identity within an audit is the composite (audit, categoryid, questionid). If a partial reindex fails between the DELETE and the INSERTs, an audit may briefly have zero questions. Reindex again to recover.
  • answertype is free text. Common values include 'multiplechoice', 'text', 'number', but the storage layer does not enforce a closed enum. Coerce defensively when branching on this column.
  • questionid is not indexed on this table. Joining audits_answer to audits_questions on questionid alone will scan; pre-filter by audit or projectid so the optimizer can use idx_parent or idx_project.